Best Practices
Guidelines for building robust applications with the SatsTerminal Borrow SDK.Initialization
Always Call Setup First
Copy
// Correct
const sdk = new BorrowSDK(config);
await sdk.setup();
await sdk.getLoan({ ... });
// Incorrect - will fail
const sdk = new BorrowSDK(config);
await sdk.getLoan({ ... }); // Error: not initialized
Validate Configuration
Copy
function validateConfig(config: Partial<BorrowSDKConfig>): void {
if (!config.apiKey) throw new Error('API key required');
if (!config.baseUrl) throw new Error('Base URL required');
if (!config.wallet?.address) throw new Error('Wallet address required');
if (typeof config.wallet?.signMessage !== 'function') {
throw new Error('signMessage must be a function');
}
}
const config = {
apiKey: process.env.API_KEY,
baseUrl: process.env.BASE_URL,
// ...
};
validateConfig(config);
const sdk = new BorrowSDK(config as BorrowSDKConfig);
Use Environment-Specific Configuration
Copy
const config: BorrowSDKConfig = {
apiKey: process.env.SATSTERMINAL_API_KEY!,
baseUrl: process.env.NODE_ENV === 'production'
? 'https://api.satsterminal.com'
: 'https://api-staging.satsterminal.com',
chain: ChainType.ARBITRUM,
wallet: walletProvider,
// Verbose logging in development only
logger: process.env.NODE_ENV === 'development'
? console
: { debug: () => {}, info: () => {}, warn: console.warn, error: console.error }
};
Error Handling
Always Handle Errors
Copy
// Bad
await sdk.getLoan(options);
// Good
try {
await sdk.getLoan(options);
} catch (error) {
handleError(error);
}
Use Typed Error Handling
Copy
import {
BorrowSDKError,
WalletNotConnectedError,
SmartAccountError,
ApiError,
QuoteError,
WorkflowError
} from '@satsterminal-sdk/borrow';
try {
await sdk.getLoan(options);
} catch (error) {
if (error instanceof WalletNotConnectedError) {
showConnectWallet();
} else if (error instanceof ApiError && error.statusCode === 429) {
showRateLimitMessage();
} else if (error instanceof QuoteError) {
showAdjustParameters();
} else {
showGenericError(error);
}
}
Handle Workflow Errors Separately
Copy
await sdk.getLoan({
...options,
onError: (error) => {
// Handle workflow-specific errors
handleWorkflowError(error);
}
}).catch((error) => {
// Handle setup/initialization errors
handleSetupError(error);
});
Session Management
Check Session Before Operations
Copy
async function ensureValidSession(sdk: BorrowSDK): Promise<void> {
const { userStatus } = sdk;
if (!userStatus.hasActiveSession) {
await sdk.setup();
return;
}
const now = Date.now() / 1000;
const expiry = userStatus.sessionExpiry || 0;
const buffer = 300; // 5 minute buffer
if (now + buffer > expiry) {
await sdk.setup(); // Refresh before expiry
}
}
// Use before operations
await ensureValidSession(sdk);
await sdk.getLoanHistory();
Clear Session on Disconnect
Copy
function handleDisconnect() {
sdk.clearSession();
// Navigate to connect page
}
Workflow Tracking
Always Provide All Callbacks
Copy
await sdk.getLoan({
...options,
onStatusUpdate: (status) => {
updateUI(status);
},
onDepositReady: (info) => {
showDepositModal(info);
},
onComplete: () => {
showSuccessMessage();
},
onError: (error) => {
showErrorMessage(error);
}
});
Persist Workflow IDs
Copy
const result = await sdk.getLoan(options);
// Store for recovery
localStorage.setItem('activeWorkflow', JSON.stringify({
id: result.workflowId,
startedAt: Date.now()
}));
// Clean up on completion
onComplete: () => {
localStorage.removeItem('activeWorkflow');
}
Resume Pending Workflows
Copy
async function checkPendingWorkflows(sdk: BorrowSDK) {
const stored = localStorage.getItem('activeWorkflow');
if (!stored) return;
const { id } = JSON.parse(stored);
const status = await sdk.getStatus(id);
if (!status.isComplete && !status.isFailed) {
await sdk.resumeLoan(id, callbacks);
} else {
localStorage.removeItem('activeWorkflow');
}
}
Quotes
Validate Quote Selection
Copy
function selectBestQuote(quotes: Quote[]): Quote {
if (quotes.length === 0) {
throw new Error('No quotes available');
}
// Sort by lowest APY
const sorted = [...quotes].sort((a, b) =>
parseFloat(a.borrowApy.variable) - parseFloat(b.borrowApy.variable)
);
return sorted[0];
}
Configure Quote Selector
Copy
const sdk = new BorrowSDK({
...config,
quoteSelector: (quotes) => {
// Custom logic
const preferred = quotes.find(q => q.protocol === 'aave');
return preferred || quotes[0];
}
});
Transactions
Validate Inputs
Copy
async function repayLoan(
sdk: BorrowSDK,
loanId: string,
amount: string,
withdrawAddress?: string
) {
// Validate loan ID
if (!loanId || typeof loanId !== 'string') {
throw new Error('Invalid loan ID');
}
// Validate amount
const numAmount = parseFloat(amount);
if (isNaN(numAmount) || numAmount <= 0) {
throw new Error('Invalid amount');
}
// Validate BTC address if provided
if (withdrawAddress && !isValidBtcAddress(withdrawAddress)) {
throw new Error('Invalid BTC address');
}
return sdk.repay(loanId, amount, {
userBtcWithdrawAddress: withdrawAddress
});
}
Check Collateral Before Withdrawal
Copy
async function safeWithdraw(
sdk: BorrowSDK,
loanId: string,
amount: string,
address: string
) {
const info = await sdk.getLoanCollateralInfo(loanId);
if (!info) {
throw new Error('Could not get collateral info');
}
if (parseFloat(amount) > parseFloat(info.maxWithdrawable)) {
throw new Error(
`Amount exceeds max withdrawable (${info.maxWithdrawable} BTC)`
);
}
return sdk.withdrawCollateral(loanId, amount, address);
}
Performance
Cache Positions
Copy
class PositionsCache {
private cache: WalletPosition[] = [];
private lastFetch = 0;
private ttl = 30000; // 30 seconds
async getPositions(sdk: BorrowSDK): Promise<WalletPosition[]> {
const now = Date.now();
if (this.cache.length > 0 && now - this.lastFetch < this.ttl) {
return this.cache;
}
const response = await sdk.getWalletPositions();
this.cache = response.data;
this.lastFetch = now;
return this.cache;
}
invalidate() {
this.cache = [];
this.lastFetch = 0;
}
}
Batch Operations
Copy
// Instead of multiple sequential calls
const [history, positions, portfolio] = await Promise.all([
sdk.getLoanHistory(),
sdk.getWalletPositions(),
sdk.getWalletPortfolio()
]);
Use Appropriate Poll Interval
Copy
const sdk = new BorrowSDK({
...config,
// Adjust based on needs
workflowPollInterval: 3000 // 3 seconds for less frequent updates
});
Security
Never Log Sensitive Data
Copy
// Bad
console.log('Config:', config); // May log API key
// Good
console.log('Config:', {
baseUrl: config.baseUrl,
chain: config.chain,
walletAddress: config.wallet.address
});
Validate BTC Addresses
Copy
function isValidBtcAddress(address: string): boolean {
// Bech32 (native segwit)
if (address.startsWith('bc1')) {
return /^bc1[a-zA-HJ-NP-Z0-9]{39,59}$/.test(address);
}
// Legacy P2PKH
if (address.startsWith('1')) {
return /^1[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address);
}
// Legacy P2SH
if (address.startsWith('3')) {
return /^3[a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(address);
}
return false;
}
Use Secure Storage
Copy
// For sensitive data, use encrypted storage
const sdk = new BorrowSDK({
...config,
storage: {
getItem: (key) => decrypt(secureStorage.get(key)),
setItem: (key, value) => secureStorage.set(key, encrypt(value)),
removeItem: (key) => secureStorage.delete(key),
clear: () => secureStorage.clear()
}
});
Testing
Mock the SDK
Copy
const mockSDK = {
setup: jest.fn().mockResolvedValue({
baseWallet: { address: '0x...' },
userStatus: { isConnected: true }
}),
getQuotes: jest.fn().mockResolvedValue([mockQuote]),
getLoan: jest.fn().mockResolvedValue({ workflowId: 'test' })
};
// Use in tests
await mockSDK.setup();
expect(mockSDK.setup).toHaveBeenCalled();
Test Error Scenarios
Copy
it('handles API errors', async () => {
mockSDK.getQuotes.mockRejectedValue(
new ApiError('Server error', 500)
);
await expect(getQuotesWrapper()).rejects.toThrow('Server error');
});
Logging
Structured Logging
Copy
const sdk = new BorrowSDK({
...config,
logger: {
debug: (msg) => logger.debug({ sdk: true, level: 'debug' }, msg),
info: (msg) => logger.info({ sdk: true, level: 'info' }, msg),
warn: (msg) => logger.warn({ sdk: true, level: 'warn' }, msg),
error: (msg) => logger.error({ sdk: true, level: 'error' }, msg)
}
});
Log Important Events
Copy
await sdk.getLoan({
...options,
onStatusUpdate: (status) => {
logger.info({
event: 'workflow_status',
workflowId: result.workflowId,
stage: status.stage,
step: status.step
});
},
onComplete: () => {
logger.info({
event: 'loan_complete',
workflowId: result.workflowId
});
},
onError: (error) => {
logger.error({
event: 'loan_error',
workflowId: result.workflowId,
error
});
}
});
Summary
- Initialize properly - Always call
setup()before operations - Handle all errors - Use typed error handling
- Manage sessions - Check validity, refresh proactively
- Track workflows - Persist IDs, handle all callbacks
- Validate inputs - Check parameters before API calls
- Cache when possible - Reduce unnecessary API calls
- Secure sensitive data - Don’t log secrets, validate addresses
- Test thoroughly - Mock SDK, test error paths
- Log structured data - Track important events