Skip to main content
This guide demonstrates how to integrate Lidian’s cross-chain swap functionality using pure TypeScript and viem. We’ll walk through the complete flow: requesting quotes, accepting a quote, approving tokens, signing the order, and monitoring execution.

Prerequisites

Before you begin, ensure you have:
  • A Web3-enabled wallet (e.g., MetaMask installed in browser)
  • Access to testnet tokens on Base Sepolia and Polygon Amoy
  • Basic understanding of EIP-712 signatures
  • Node.js 18+ and your preferred package manager

Installation

npm
npm install viem
yarn
yarn add viem
pnpm
pnpm add viem

Configuration

Set up your chain and token configurations:
import { 
  createPublicClient, 
  createWalletClient, 
  custom, 
  http,
  parseUnits,
  formatUnits,
  type Address,
  type Hash
} from 'viem';
import { baseSepolia, polygonAmoy } from 'viem/chains';

// Chain configurations
export const CHAINS = {
  'base-sepolia': {
    name: 'Base Sepolia',
    chainId: 84532,
    chain: baseSepolia,
    rpcUrl: 'https://sepolia.base.org',
    explorerUrl: 'https://sepolia.basescan.org'
  },
  'polygon-amoy': {
    name: 'Polygon Amoy',
    chainId: 80002,
    chain: polygonAmoy,
    rpcUrl: 'https://rpc-amoy.polygon.technology',
    explorerUrl: 'https://amoy.polygonscan.com'
  }
} as const;

export type ChainKey = keyof typeof CHAINS;

// Token addresses by chain
export const TOKENS = {
  'base-sepolia': {
    USDC: '0x036CbD53842c5426634e7929541eC2318f3dCF7e' as Address,
    USDT: '0x66F5018cdb5d6Eb0a3d1AC57F8b77243eb0010fF' as Address,
    DAI: '0xD68450706D96997899223dEd69B61a16068735f7' as Address
  },
  'polygon-amoy': {
    USDC: '0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582' as Address,
    USDT: '0x86f6D9931BE92Ad61572e80c0F5FAe45950D4cA1' as Address,
    DAI: '0xD0a00D630D8A01c3E53A8a210Ae3141BbF1A799b' as Address
  }
} as const;

export type TokenSymbol = 'USDC' | 'USDT' | 'DAI';

// Lidian contract addresses by chain
export const LIDIAN_CONTRACTS: Record<ChainKey, Address> = {
  'base-sepolia': '0xc2c00E85ab6bcf865986DE84053a6169bCBfa70F',
  'polygon-amoy': '0x1234567890123456789012345678901234567890'
};

// Token decimals
export const TOKEN_DECIMALS: Record<TokenSymbol, number> = {
  USDC: 6,
  USDT: 6,
  DAI: 18
};

// API configuration
export const API_BASE_URL = 'https://api.lidian.finance';

// ERC20 ABI for allowance and approve
export const ERC20_ABI = [
  {
    type: 'function',
    name: 'allowance',
    stateMutability: 'view',
    inputs: [
      { name: 'owner', type: 'address' },
      { name: 'spender', type: 'address' }
    ],
    outputs: [{ name: '', type: 'uint256' }]
  },
  {
    type: 'function',
    name: 'approve',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' }
    ],
    outputs: [{ name: '', type: 'bool' }]
  }
] as const;

Setup Clients

Create viem clients for interacting with the blockchain:
/**
 * Create a public client for reading blockchain state
 */
function createPublicClientForChain(chainKey: ChainKey) {
  return createPublicClient({
    chain: CHAINS[chainKey].chain,
    transport: http(CHAINS[chainKey].rpcUrl)
  });
}

/**
 * Create a wallet client for signing transactions
 * Assumes browser environment with window.ethereum
 */
function createWalletClientForChain(chainKey: ChainKey) {
  if (typeof window === 'undefined' || !window.ethereum) {
    throw new Error('No ethereum provider found');
  }

  return createWalletClient({
    chain: CHAINS[chainKey].chain,
    transport: custom(window.ethereum)
  });
}

/**
 * Get the connected wallet address
 */
async function getWalletAddress(): Promise<Address> {
  if (typeof window === 'undefined' || !window.ethereum) {
    throw new Error('No ethereum provider found');
  }

  const accounts = await window.ethereum.request({ 
    method: 'eth_requestAccounts' 
  }) as Address[];
  
  if (!accounts || accounts.length === 0) {
    throw new Error('No accounts found');
  }

  return accounts[0];
}

Step 1: Request a Quote

Create a quote request to get competitive rates from liquidity providers:
interface QuoteRequestParams {
  fromChain: ChainKey;
  toChain: ChainKey;
  fromToken: TokenSymbol;
  toToken: TokenSymbol;
  amount: string; // Human-readable amount (e.g., "100.5")
  userAddress: Address;
}

/**
 * Request a quote from liquidity providers
 * @returns Quote request ID for polling
 */
async function requestQuote(params: QuoteRequestParams): Promise<string> {
  const { fromChain, toChain, fromToken, toToken, amount, userAddress } = params;
  
  console.log('Creating quote request...');
  console.log(`  From: ${amount} ${fromToken} on ${CHAINS[fromChain].name}`);
  console.log(`  To: ${toToken} on ${CHAINS[toChain].name}`);
  
  // Convert amount to proper decimals
  const decimals = TOKEN_DECIMALS[fromToken];
  const inputAmount = parseUnits(amount, decimals).toString();
  
  // Get token addresses
  const inputToken = TOKENS[fromChain][fromToken];
  const outputToken = TOKENS[toChain][toToken];
  
  // Create quote request
  const response = await fetch(`${API_BASE_URL}/api/v1/quote-requests`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    credentials: 'include', // Use JWT from SIWE authentication
    body: JSON.stringify({
      offerer: userAddress,
      recipient: userAddress,
      inputToken,
      outputToken,
      inputAmount,
      inputChain: fromChain,
      outputChain: toChain
    })
  });
  
  if (!response.ok) {
    const error = await response.json().catch(() => ({ message: 'Request failed' }));
    throw new Error(error.message || 'Failed to create quote request');
  }
  
  const data = await response.json();
  const quoteRequestId = data?.data?.quoteId;
  
  if (!quoteRequestId) {
    throw new Error('No quote request ID received');
  }
  
  console.log(`✓ Quote request created: ${quoteRequestId}`);
  return quoteRequestId;
}

Step 2: Poll for Quotes

After requesting a quote, poll the API to retrieve competitive quotes from liquidity providers:
interface Quote {
  id: string;
  orderId: string;
  inputAmount: string;
  outputAmount: string;
  inputToken: string;
  outputToken: string;
  inputChain: string;
  outputChain: string;
  liquidityProviderId: string;
  typedData: {
    domain: {
      name: string;
      version: string;
      chainId: number;
      verifyingContract: Address;
    };
    types: {
      Order: Array<{ name: string; type: string }>;
    };
    primaryType: string;
    message: Record<string, any>;
  };
  createdAt: string;
}

/**
 * Poll for quotes from liquidity providers
 * @param quoteRequestId - The quote request ID to poll for
 * @param maxAttempts - Maximum number of polling attempts (default: 15)
 * @param intervalMs - Interval between polls in milliseconds (default: 2000)
 * @returns Array of quotes sorted by best output amount
 */
async function pollForQuotes(
  quoteRequestId: string,
  maxAttempts: number = 15,
  intervalMs: number = 2000
): Promise<Quote[]> {
  console.log('Polling for quotes...');
  let attempts = 0;
  
  while (attempts < maxAttempts) {
    try {
      const response = await fetch(
        `${API_BASE_URL}/api/v1/quotes?quoteRequestId=${quoteRequestId}`,
        { credentials: 'include' }
      );
      
      if (response.ok) {
        const data = await response.json();
        const quotes: Quote[] = data?.data?.quotes || [];
        
        if (quotes.length > 0) {
          // Sort by best output amount (highest first)
          const sortedQuotes = quotes.sort((a, b) => 
            Number(BigInt(b.outputAmount) - BigInt(a.outputAmount))
          );
          
          console.log(`✓ Received ${quotes.length} quote(s)`);
          return sortedQuotes;
        }
      }
    } catch (error) {
      console.error('Quote polling error:', error);
    }
    
    attempts++;
    if (attempts < maxAttempts) {
      console.log(`  Waiting for quotes (${attempts}/${maxAttempts})...`);
      await new Promise(resolve => setTimeout(resolve, intervalMs));
    }
  }
  
  throw new Error('No quotes received within timeout period');
}

Step 3: Select the Best Quote

/**
 * Select the best quote from available quotes
 * @param quotes - Array of quotes (should be pre-sorted by best output)
 * @param outputToken - Output token symbol for display
 * @returns Best quote
 */
function selectBestQuote(quotes: Quote[], outputToken: TokenSymbol): Quote {
  if (quotes.length === 0) {
    throw new Error('No quotes available');
  }
  
  // Quotes are already sorted by best output, so take the first
  const bestQuote = quotes[0];
  
  // Display quote information
  const decimals = TOKEN_DECIMALS[outputToken];
  const outputAmount = formatUnits(BigInt(bestQuote.outputAmount), decimals);
  
  console.log('Best Quote:');
  console.log(`  Quote ID: ${bestQuote.id}`);
  console.log(`  Order ID: ${bestQuote.orderId}`);
  console.log(`  Output Amount: ${outputAmount} ${outputToken}`);
  console.log(`  LP: ${bestQuote.liquidityProviderId}`);
  
  return bestQuote;
}

Step 4: Check and Approve Token Allowance

Before executing the swap, ensure the Lidian contract has sufficient allowance to spend your tokens:
/**
 * Check token allowance and approve if necessary
 * @param quote - The selected quote
 * @param fromChain - Source chain key
 * @param fromToken - Source token symbol
 * @param userAddress - User's wallet address
 */
async function checkAndApproveToken(
  quote: Quote,
  fromChain: ChainKey,
  fromToken: TokenSymbol,
  userAddress: Address
): Promise<void> {
  const tokenAddress = TOKENS[fromChain][fromToken];
  const spender = LIDIAN_CONTRACTS[fromChain];
  const inputAmount = BigInt(quote.inputAmount);
  
  // Create clients
  const publicClient = createPublicClientForChain(fromChain);
  const walletClient = createWalletClientForChain(fromChain);
  
  console.log('Checking token allowance...');
  
  // Check current allowance
  const allowance = await publicClient.readContract({
    address: tokenAddress,
    abi: ERC20_ABI,
    functionName: 'allowance',
    args: [userAddress, spender]
  }) as bigint;
  
  console.log(`  Current allowance: ${allowance.toString()}`);
  console.log(`  Required amount: ${inputAmount.toString()}`);
  
  // Approve if insufficient allowance
  if (allowance < inputAmount) {
    console.log('Approving token spend...');
    
    const hash = await walletClient.writeContract({
      address: tokenAddress,
      abi: ERC20_ABI,
      functionName: 'approve',
      args: [spender, inputAmount],
      account: userAddress
    });
    
    console.log(`  Approval tx: ${hash}`);
    console.log('  Waiting for confirmation...');
    
    // Wait for transaction receipt
    const receipt = await publicClient.waitForTransactionReceipt({ hash });
    
    if (receipt.status === 'success') {
      console.log('✓ Token approval confirmed');
    } else {
      throw new Error('Token approval failed');
    }
  } else {
    console.log('✓ Sufficient allowance already exists');
  }
}

Step 5: Sign the Order (EIP-712)

Sign the order using EIP-712 typed data:
/**
 * Sign the order using EIP-712 typed data
 * @param quote - The quote containing typed data to sign
 * @param userAddress - User's wallet address
 * @returns Signature string
 */
async function signOrder(quote: Quote, userAddress: Address): Promise<Hash> {
  console.log('Requesting signature...');
  
  const { domain, types, message } = quote.typedData;
  
  if (!domain || !types || !message) {
    throw new Error('Quote missing typed data for signing');
  }
  
  // Create wallet client
  const walletClient = createWalletClientForChain(quote.inputChain as ChainKey);
  
  // Sign the typed data
  const signature = await walletClient.signTypedData({
    account: userAddress,
    domain,
    types,
    primaryType: 'Order',
    message
  });
  
  console.log('✓ Order signed successfully');
  return signature;
}

Step 6: Submit the Swap

Submit the signed order to execute the swap:
/**
 * Submit the swap to the Lidian API
 * @param quote - The accepted quote
 * @param signature - The EIP-712 signature
 * @returns Order ID for monitoring
 */
async function submitSwap(quote: Quote, signature: Hash): Promise<string> {
  console.log('Submitting swap...');
  
  const response = await fetch(`${API_BASE_URL}/api/v1/swaps`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    credentials: 'include',
    body: JSON.stringify({
      orderId: quote.orderId,
      quoteId: quote.id,
      signature
    })
  });
  
  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Swap submission failed: ${errorText}`);
  }
  
  const data = await response.json();
  const orderId = data?.data?.orderId || quote.orderId;
  
  console.log('✓ Swap submitted successfully');
  console.log(`  Order ID: ${orderId}`);
  
  return orderId;
}

Step 7: Monitor Order Status

Poll the order status to track execution:
type OrderStatus = 
  | 'created'      // Order created, awaiting deposit
  | 'received'     // Source chain deposit confirmed
  | 'fulfilled'    // Destination tokens sent
  | 'completed'    // Settlement complete
  | 'failed'       // Order failed
  | 'cancelled';   // Order cancelled

interface OrderDetails {
  orderId: string;
  status: OrderStatus;
  quoteId: string;
  signature: string;
  moneyMoverAccountId: string;
  liquidityProviderAccountId: string;
  details: {
    srcChainTxHash?: string;
    dstChainTxHash?: string;
    settlementTxHash?: string;
    orderHash?: string;
    srcChain?: string;
    dstChain?: string;
  };
  createdAt: string;
  updatedAt: string;
  completedAt?: string;
  failureReason?: string;
}

/**
 * Monitor order status until completion or failure
 * @param orderId - The order ID to monitor
 * @param maxAttempts - Maximum polling attempts (default: 150 = 5 minutes)
 * @param intervalMs - Polling interval in ms (default: 2000)
 * @param onStatusChange - Optional callback for status updates
 * @returns Final order details
 */
async function monitorOrderStatus(
  orderId: string,
  maxAttempts: number = 150,
  intervalMs: number = 2000,
  onStatusChange?: (status: OrderStatus, details: OrderDetails) => void
): Promise<OrderDetails> {
  console.log('Monitoring order status...');
  let attempts = 0;
  let lastStatus: OrderStatus | null = null;
  
  while (attempts < maxAttempts) {
    try {
      const response = await fetch(
        `${API_BASE_URL}/api/v1/orders/${orderId}`,
        { credentials: 'include' }
      );
      
      if (response.ok) {
        const data = await response.json();
        const order: OrderDetails = data?.data;
        
        if (order?.status) {
          // Notify on status change
          if (order.status !== lastStatus) {
            console.log(`  Status: ${order.status}`);
            if (order.details?.srcChainTxHash) {
              console.log(`  Source tx: ${order.details.srcChainTxHash}`);
            }
            if (order.details?.dstChainTxHash) {
              console.log(`  Destination tx: ${order.details.dstChainTxHash}`);
            }
            onStatusChange?.(order.status, order);
            lastStatus = order.status;
          }
          
          // Check for terminal states
          if (['completed', 'settled', 'fulfilled'].includes(order.status)) {
            console.log('✓ Swap completed successfully!');
            if (order.details?.settlementTxHash) {
              console.log(`  Settlement tx: ${order.details.settlementTxHash}`);
            }
            return order;
          }
          
          if (['failed', 'cancelled'].includes(order.status)) {
            const reason = order.failureReason || 'Unknown error';
            throw new Error(`Order ${order.status}: ${reason}`);
          }
        }
      } else if (response.status === 404) {
        // Order not yet visible, continue polling
        if (attempts % 5 === 0) {
          console.log('  Order not yet visible, continuing to poll...');
        }
      }
    } catch (error) {
      if (error instanceof Error && error.message.includes('Order')) {
        throw error; // Rethrow order-specific errors
      }
      console.error('  Order status check error:', error);
    }
    
    attempts++;
    await new Promise(resolve => setTimeout(resolve, intervalMs));
  }
  
  throw new Error('Order monitoring timed out');
}

Complete End-to-End Script

Here’s how to put it all together in a single executable script:
/**
 * Execute a complete cross-chain swap
 */
async function executeSwap(
  fromChain: ChainKey,
  toChain: ChainKey,
  fromToken: TokenSymbol,
  toToken: TokenSymbol,
  amount: string
): Promise<OrderDetails> {
  console.log('=== Starting Lidian Cross-Chain Swap ===\n');
  
  try {
    // Get user's wallet address
    const userAddress = await getWalletAddress();
    console.log(`Wallet: ${userAddress}\n`);
    
    // Step 1: Request quote
    console.log('Step 1: Requesting Quote');
    const quoteRequestId = await requestQuote({
      fromChain,
      toChain,
      fromToken,
      toToken,
      amount,
      userAddress
    });
    console.log();
    
    // Step 2: Poll for quotes
    console.log('Step 2: Waiting for Quotes');
    const quotes = await pollForQuotes(quoteRequestId);
    console.log();
    
    // Step 3: Select best quote
    console.log('Step 3: Selecting Best Quote');
    const bestQuote = selectBestQuote(quotes, toToken);
    console.log();
    
    // Step 4: Check and approve token
    console.log('Step 4: Checking Token Allowance');
    await checkAndApproveToken(bestQuote, fromChain, fromToken, userAddress);
    console.log();
    
    // Step 5: Sign the order
    console.log('Step 5: Signing Order');
    const signature = await signOrder(bestQuote, userAddress);
    console.log();
    
    // Step 6: Submit swap
    console.log('Step 6: Submitting Swap');
    const orderId = await submitSwap(bestQuote, signature);
    console.log();
    
    // Step 7: Monitor status
    console.log('Step 7: Monitoring Order Status');
    const finalOrder = await monitorOrderStatus(orderId, 150, 2000, (status, details) => {
      // Optional: Add custom status change handlers
      if (status === 'received') {
        console.log('  → Source chain deposit confirmed');
      } else if (status === 'fulfilled') {
        console.log('  → Destination tokens delivered');
      }
    });
    
    console.log('\n=== Swap Complete ===');
    return finalOrder;
    
  } catch (error) {
    console.error('\n❌ Swap Failed:');
    console.error(error instanceof Error ? error.message : 'Unknown error');
    throw error;
  }
}

// Example usage
async function main() {
  try {
    const result = await executeSwap(
      'base-sepolia',      // From chain
      'polygon-amoy',      // To chain
      'USDC',              // From token
      'USDT',              // To token
      '100'                // Amount
    );
    
    console.log('\nFinal Order:', result);
  } catch (error) {
    console.error('Error:', error);
    process.exit(1);
  }
}

// Run if this is the main module
if (import.meta.url === `file://${process.argv[1]}`) {
  main();
}

Usage Examples

Basic Swap

// Simple cross-chain swap
await executeSwap(
  'base-sepolia',
  'polygon-amoy',
  'USDC',
  'USDT',
  '100'
);

Same-Chain Swap

// Swap between tokens on the same chain
await executeSwap(
  'base-sepolia',
  'base-sepolia',
  'USDC',
  'USDT',
  '50.5'
);

With Custom Status Handling

const orderId = await submitSwap(bestQuote, signature);

await monitorOrderStatus(orderId, 150, 2000, (status, details) => {
  // Custom handling for each status
  switch (status) {
    case 'received':
      console.log('Deposit confirmed on source chain');
      notifyUser('Your tokens have been deposited');
      break;
    case 'fulfilled':
      console.log('Tokens delivered on destination chain');
      notifyUser('Your swap is complete!');
      break;
  }
});

Order Lifecycle

Understanding the order status flow:
created → received → fulfilled → completed
           ↓           ↓           ↓
         failed     failed     failed/cancelled
Status Descriptions:
  1. created - Order created in system, awaiting source chain deposit
  2. received - Source chain deposit transaction confirmed
  3. fulfilled - Destination chain tokens delivered to recipient
  4. completed - Settlement finalized on-chain
Terminal Failure States:
  • failed - Order execution failed at any stage
  • cancelled - Order was cancelled by user or system

Helper Utilities

Display Quote Comparison

function displayQuotes(quotes: Quote[], outputToken: TokenSymbol): void {
  const decimals = TOKEN_DECIMALS[outputToken];
  
  console.log('Available Quotes:');
  quotes.forEach((quote, index) => {
    const output = formatUnits(BigInt(quote.outputAmount), decimals);
    const rank = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '  ';
    console.log(`${rank} ${index + 1}. ${output} ${outputToken} - LP ${quote.liquidityProviderId}`);
  });
}

Calculate Exchange Rate

function calculateRate(
  quote: Quote,
  inputAmount: string,
  fromToken: TokenSymbol,
  toToken: TokenSymbol
): string {
  const inputDecimals = TOKEN_DECIMALS[fromToken];
  const outputDecimals = TOKEN_DECIMALS[toToken];
  
  const inputNum = parseFloat(formatUnits(BigInt(quote.inputAmount), inputDecimals));
  const outputNum = parseFloat(formatUnits(BigInt(quote.outputAmount), outputDecimals));
  
  const rate = outputNum / inputNum;
  
  return `1 ${fromToken} = ${rate.toFixed(6)} ${toToken}`;
}

Estimate Fees

function estimateFees(inputAmount: string, fromToken: TokenSymbol): string {
  const decimals = TOKEN_DECIMALS[fromToken];
  const amount = parseUnits(inputAmount, decimals);
  
  // Protocol fee: 0.75% (75 basis points)
  const fee = (amount * 75n) / 10000n;
  
  return formatUnits(fee, decimals);
}

Error Handling

Implement comprehensive error handling:
async function executeSwapWithRetry(
  params: Parameters<typeof executeSwap>,
  maxRetries: number = 3
): Promise<OrderDetails> {
  let lastError: Error | null = null;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await executeSwap(...params);
    } catch (error) {
      lastError = error instanceof Error ? error : new Error('Unknown error');
      
      // Don't retry on certain errors
      if (
        lastError.message.includes('insufficient') ||
        lastError.message.includes('rejected') ||
        lastError.message.includes('denied')
      ) {
        throw lastError;
      }
      
      if (attempt < maxRetries) {
        console.log(`Attempt ${attempt} failed, retrying...`);
        await new Promise(resolve => setTimeout(resolve, 2000 * attempt));
      }
    }
  }
  
  throw lastError || new Error('Max retries exceeded');
}

Best Practices

1. Always Validate Inputs

function validateSwapParams(
  fromChain: ChainKey,
  toChain: ChainKey,
  fromToken: TokenSymbol,
  toToken: TokenSymbol,
  amount: string
): void {
  // Validate chains
  if (!CHAINS[fromChain] || !CHAINS[toChain]) {
    throw new Error('Invalid chain');
  }
  
  // Validate tokens exist on chains
  if (!TOKENS[fromChain][fromToken]) {
    throw new Error(`${fromToken} not available on ${fromChain}`);
  }
  if (!TOKENS[toChain][toToken]) {
    throw new Error(`${toToken} not available on ${toChain}`);
  }
  
  // Validate amount
  const numAmount = parseFloat(amount);
  if (isNaN(numAmount) || numAmount <= 0) {
    throw new Error('Invalid amount');
  }
}

2. Check User Balance

async function checkBalance(
  chain: ChainKey,
  token: TokenSymbol,
  userAddress: Address,
  requiredAmount: string
): Promise<boolean> {
  const publicClient = createPublicClientForChain(chain);
  const tokenAddress = TOKENS[chain][token];
  const decimals = TOKEN_DECIMALS[token];
  
  const balance = await publicClient.readContract({
    address: tokenAddress,
    abi: [{
      type: 'function',
      name: 'balanceOf',
      stateMutability: 'view',
      inputs: [{ name: 'account', type: 'address' }],
      outputs: [{ name: '', type: 'uint256' }]
    }],
    functionName: 'balanceOf',
    args: [userAddress]
  }) as bigint;
  
  const required = parseUnits(requiredAmount, decimals);
  return balance >= required;
}

3. Handle Network Switching

async function ensureCorrectNetwork(chainKey: ChainKey): Promise<void> {
  if (typeof window === 'undefined' || !window.ethereum) {
    throw new Error('No ethereum provider found');
  }

  const targetChainId = CHAINS[chainKey].chainId;
  const currentChainId = await window.ethereum.request({ 
    method: 'eth_chainId' 
  }) as string;
  
  if (parseInt(currentChainId, 16) !== targetChainId) {
    console.log(`Switching to ${CHAINS[chainKey].name}...`);
    
    try {
      await window.ethereum.request({
        method: 'wallet_switchEthereumChain',
        params: [{ chainId: `0x${targetChainId.toString(16)}` }],
      });
      console.log('✓ Network switched');
    } catch (error: any) {
      // Chain not added to wallet
      if (error.code === 4902) {
        throw new Error(`Please add ${CHAINS[chainKey].name} to your wallet`);
      }
      throw error;
    }
  }
}

Troubleshooting

No Quotes Received

// Add more detailed logging
async function pollForQuotesVerbose(quoteRequestId: string): Promise<Quote[]> {
  console.log('Polling with verbose logging...');
  
  for (let i = 0; i < 15; i++) {
    const response = await fetch(
      `${API_BASE_URL}/api/v1/quotes?quoteRequestId=${quoteRequestId}`,
      { credentials: 'include' }
    );
    
    console.log(`Attempt ${i + 1}:`);
    console.log(`  Status: ${response.status}`);
    
    if (response.ok) {
      const data = await response.json();
      console.log(`  Quotes: ${data?.data?.quotes?.length || 0}`);
      
      if (data?.data?.quotes?.length > 0) {
        return data.data.quotes;
      }
    }
    
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
  
  throw new Error('No quotes received');
}

Transaction Failures

// Add gas estimation
async function approveWithGasEstimate(
  tokenAddress: Address,
  spender: Address,
  amount: bigint,
  userAddress: Address,
  chainKey: ChainKey
): Promise<Hash> {
  const publicClient = createPublicClientForChain(chainKey);
  const walletClient = createWalletClientForChain(chainKey);
  
  // Estimate gas
  const gasEstimate = await publicClient.estimateContractGas({
    address: tokenAddress,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [spender, amount],
    account: userAddress
  });
  
  console.log(`Estimated gas: ${gasEstimate}`);
  
  // Add 20% buffer
  const gasLimit = (gasEstimate * 120n) / 100n;
  
  return walletClient.writeContract({
    address: tokenAddress,
    abi: ERC20_ABI,
    functionName: 'approve',
    args: [spender, amount],
    account: userAddress,
    gas: gasLimit
  });
}

Next Steps

Support