Skip to main content

Best Practices

Guidelines for building robust applications with the SatsTerminal Borrow SDK.

Initialization

Always Call Setup First

// 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

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

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

// Bad
await sdk.getLoan(options);

// Good
try {
  await sdk.getLoan(options);
} catch (error) {
  handleError(error);
}

Use Typed Error Handling

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

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

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

function handleDisconnect() {
  sdk.clearSession();
  // Navigate to connect page
}

Workflow Tracking

Always Provide All Callbacks

await sdk.getLoan({
  ...options,
  onStatusUpdate: (status) => {
    updateUI(status);
  },
  onDepositReady: (info) => {
    showDepositModal(info);
  },
  onComplete: () => {
    showSuccessMessage();
  },
  onError: (error) => {
    showErrorMessage(error);
  }
});

Persist Workflow IDs

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

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

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

const sdk = new BorrowSDK({
  ...config,
  quoteSelector: (quotes) => {
    // Custom logic
    const preferred = quotes.find(q => q.protocol === 'aave');
    return preferred || quotes[0];
  }
});

Transactions

Validate Inputs

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

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

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

// Instead of multiple sequential calls
const [history, positions, portfolio] = await Promise.all([
  sdk.getLoanHistory(),
  sdk.getWalletPositions(),
  sdk.getWalletPortfolio()
]);

Use Appropriate Poll Interval

const sdk = new BorrowSDK({
  ...config,
  // Adjust based on needs
  workflowPollInterval: 3000 // 3 seconds for less frequent updates
});

Security

Never Log Sensitive Data

// 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

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

// 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

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

it('handles API errors', async () => {
  mockSDK.getQuotes.mockRejectedValue(
    new ApiError('Server error', 500)
  );

  await expect(getQuotesWrapper()).rejects.toThrow('Server error');
});

Logging

Structured Logging

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

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

  1. Initialize properly - Always call setup() before operations
  2. Handle all errors - Use typed error handling
  3. Manage sessions - Check validity, refresh proactively
  4. Track workflows - Persist IDs, handle all callbacks
  5. Validate inputs - Check parameters before API calls
  6. Cache when possible - Reduce unnecessary API calls
  7. Secure sensitive data - Don’t log secrets, validate addresses
  8. Test thoroughly - Mock SDK, test error paths
  9. Log structured data - Track important events