Skip to content

Solana Swap Examples

This guide demonstrates how to execute Silent Swaps with Solana assets using the Core SDK in a Node.js backend environment. Solana swaps require special handling for bridge operations and address formats.

Prerequisites

  • Solana Web3.js library
  • EVM wallet (for facilitator operations and deposit calldata)
  • Solana keypair or wallet adapter
  • Understanding of CAIP-19 asset identifiers

Setup

import {
  createSilentSwapClient,
  createViemSigner,
  parseTransactionRequestForViem,
  createSignInMessage,
  createEip712DocForWalletGeneration,
  createEip712DocForOrder,
  createHdFacilitatorGroupFromEntropy,
  queryDepositCount,
  hexToBytes,
  quoteResponseToEip712Document,
  solveOptimalUsdcAmount,
  caip19FungibleEvmToken,
  caip19SplToken,
  DeliveryMethod,
  FacilitatorKeyType,
  PublicKeyArgGroups,
  ENVIRONMENT,
  N_RELAY_CHAIN_ID_SOLANA,
  SB58_ADDR_SOL_PROGRAM_SYSTEM,
  isSolanaNativeToken,
  parseSolanaCaip19,
  fetchRelayQuote,
  createPhonyDepositCalldata,
  X_MAX_IMPACT_PERCENT,
  getRelayStatus,
  type SilentSwapClient,
  type EvmSigner,
  type SolveUsdcResult,
} from '@silentswap/sdk';
import { createWalletClient, http, publicActions, erc20Abi, encodeFunctionData, erc20Abi as erc20AbiForEncode } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { avalanche, mainnet } from 'viem/chains';
import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js';
import { getAssociatedTokenAddress, createTransferInstruction, getAccount } from '@solana/spl-token';
import BigNumber from 'bignumber.js';
 
// Create EVM wallet client (required for facilitator operations)
const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
const evmClient = createWalletClient({
  account: evmAccount,
  chain: avalanche,
  transport: http(),
}).extend(publicActions);
 
// Create EVM signer
const evmSigner = createViemSigner(evmAccount, evmClient);
 
// Create SilentSwap client
const silentswap = createSilentSwapClient({
  environment: ENVIRONMENT.MAINNET,
  baseUrl: 'https://api.silentswap.com',
});
 
// Create Solana connection
const solanaConnection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
 
// Load Solana keypair (in production, use secure key management)
const solanaKeypair = Keypair.fromSecretKey(
  Buffer.from(JSON.parse(process.env.SOLANA_SECRET_KEY || '[]'))
);
const solanaAddress = solanaKeypair.publicKey.toString();

Example 1: Solana Native SOL → EVM Token Swap

This example swaps native SOL on Solana to USDC on Ethereum.

async function executeSolanaToEvmSwap(
  solAmount: string, // Human-readable amount (e.g., "1.5")
  recipientEvmAddress: `0x${string}`,
  destinationTokenAddress: `0x${string}`,
  destinationChainId: number = 1 // Ethereum mainnet
) {
  try {
    // Step 1: Authenticate and derive entropy (using EVM wallet)
    console.log('Step 1: Authenticating with SilentSwap...');
    const entropy = await authenticateAndDeriveEntropy(silentswap, evmSigner);
    console.log('✓ Authentication successful');
 
    // Step 2: Create facilitator group
    console.log('\nStep 2: Creating facilitator group...');
    const depositCount = await queryDepositCount(evmAccount.address);
    const group = await createHdFacilitatorGroupFromEntropy(
      hexToBytes(entropy),
      depositCount
    );
    console.log(`✓ Facilitator group created (deposit count: ${depositCount})`);
 
    // Step 3: Calculate optimal USDC amount from Solana bridge
    console.log('\nStep 3: Calculating bridge USDC amount...');
    
    // Parse Solana CAIP-19 for native SOL
    const solanaCaip19 = `solana:5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1/slip44:501`;
    const solanaParsed = parseSolanaCaip19(solanaCaip19);
    if (!solanaParsed) {
      throw new Error('Invalid Solana CAIP-19 format');
    }
 
    // Convert SOL amount to lamports (9 decimals)
    const solAmountBN = BigNumber(solAmount);
    const solAmountInLamports = solAmountBN.shiftedBy(9).toFixed(0);
 
    // Origin currency for relay.link: system program for native SOL
    const originCurrency = SB58_ADDR_SOL_PROGRAM_SYSTEM;
 
    // Create phony deposit calldata for solving (uses EVM signer address)
    const depositorAddress = silentswap.s0xDepositorAddress;
    const phonyDepositCalldata = createPhonyDepositCalldata(evmAccount.address);
 
    // Solve for optimal USDC amount
    // CRITICAL: Pass Solana address for 'user' parameter, EVM address for recipient and deposit calldata
    const bridgeResult: SolveUsdcResult = await solveOptimalUsdcAmount(
      N_RELAY_CHAIN_ID_SOLANA,
      originCurrency, // System program address for native SOL
      solAmountInLamports,
      solanaAddress, // Solana user address (base58)
      phonyDepositCalldata,
      X_MAX_IMPACT_PERCENT,
      depositorAddress,
      evmAccount.address, // EVM address for recipient and deposit calldata
    );
 
    console.log(`✓ Bridge will provide ${BigNumber(bridgeResult.usdcAmountOut.toString()).shiftedBy(-6).toFixed()} USDC (provider: ${bridgeResult.provider})`);
 
    // Step 4: Get quote using bridged USDC amount
    console.log('\nStep 4: Requesting quote with bridged USDC...');
    const viewer = await group.viewer();
    const { publicKeyBytes: pk65_viewer } = viewer.exportPublicKey(
      '*',
      FacilitatorKeyType.SECP256K1
    );
 
    const groupPublicKeys = await group.exportPublicKeys(1, [
      ...PublicKeyArgGroups.GENERIC,
    ]);
 
    // Request quote with USDC amount from bridge
    const [quoteError, quoteResponse] = await silentswap.quote({
      signer: evmAccount.address, // EVM signer address (matches deposit calldata)
      viewer: pk65_viewer,
      outputs: [
        {
          method: DeliveryMethod.SNIP,
          recipient: recipientEvmAddress,
          asset: caip19FungibleEvmToken(destinationChainId, destinationTokenAddress),
          value: bridgeResult.usdcAmountOut.toString() as `${bigint}`, // USDC amount in microUSDC
          facilitatorPublicKeys: groupPublicKeys[0],
        },
      ],
    });
 
    if (quoteError || !quoteResponse) {
      throw new Error(`Failed to get quote: ${quoteError?.type}: ${quoteError?.error}`);
    }
    console.log(`✓ Quote received (Order ID: ${quoteResponse.quoteId})`);
 
    // Step 5: Sign authorizations and create order
    console.log('\nStep 5: Signing authorizations and creating order...');
    const orderResponse = await createOrder(
      silentswap,
      evmSigner,
      group,
      quoteResponse,
      {
        sourceAsset: {
          caip19: solanaCaip19,
          amount: solAmountInLamports,
        },
        sourceSender: {
          contactId: `caip10:solana:*:${solanaAddress}`,
        },
      }
    );
    console.log(`✓ Order created (Order ID: ${orderResponse.response.orderId})`);
 
    // Step 6: Execute Solana bridge transaction
    console.log('\nStep 6: Executing Solana bridge transaction...');
    const depositTxHash = await executeSolanaBridge(
      solanaCaip19,
      solAmountInLamports,
      bridgeResult.usdcAmountOut.toString(),
      solanaAddress,
      evmAccount.address,
      orderResponse,
      bridgeResult.provider
    );
    console.log(`✓ Bridge transaction completed: ${depositTxHash}`);
 
    // Step 7: Watch for completion
    console.log('\nStep 7: Watching for order completion...');
    await watchForCompletion(
      evmClient,
      destinationTokenAddress,
      recipientEvmAddress,
      group,
      6 // USDC decimals
    );
 
    return {
      orderId: orderResponse.response.orderId,
      depositHash: depositTxHash,
      quote: quoteResponse,
      bridgeProvider: bridgeResult.provider,
      usdcAmountReceived: bridgeResult.usdcAmountOut.toString(),
    };
  } catch (err) {
    console.error('Solana to EVM swap error:', err);
    throw err;
  }
}
 
// Helper: Execute Solana bridge transaction
async function executeSolanaBridge(
  sourceAsset: string,
  sourceAmount: string,
  usdcAmount: string,
  solanaSenderAddress: string,
  evmSignerAddress: `0x${string}`,
  orderResponse: any,
  provider: 'relay' | 'debridge'
): Promise<string> {
  if (provider !== 'relay') {
    throw new Error('Only relay.link is supported for Solana swaps');
  }
 
  // Get deposit parameters from order
  const depositParams = orderResponse.transaction.metadata?.params;
  if (!depositParams) {
    throw new Error('Missing deposit parameters in order response');
  }
 
  // Encode deposit calldata (matches React SDK implementation)
  const depositorAddress = silentswap.s0xDepositorAddress;
  const DEPOSITOR_ABI = [
    {
      inputs: [
        {
          components: [
            { internalType: 'address', name: 'signer', type: 'address' },
            { internalType: 'bytes32', name: 'orderId', type: 'bytes32' },
            { internalType: 'address', name: 'notary', type: 'address' },
            { internalType: 'address', name: 'approver', type: 'address' },
            { internalType: 'bytes', name: 'orderApproval', type: 'bytes' },
            { internalType: 'uint256', name: 'approvalExpiration', type: 'uint256' },
            { internalType: 'uint256', name: 'duration', type: 'uint256' },
            { internalType: 'bytes32', name: 'domainSepHash', type: 'bytes32' },
            { internalType: 'bytes32', name: 'payloadHash', type: 'bytes32' },
            { internalType: 'bytes', name: 'typedDataSignature', type: 'bytes' },
            { internalType: 'bytes', name: 'receiveAuthorization', type: 'bytes' },
          ],
          internalType: 'struct SilentSwapV2Gateway.DepositParams',
          name: 'params',
          type: 'tuple',
        },
      ],
      name: 'depositProxy2',
      outputs: [],
      stateMutability: 'nonpayable',
      type: 'function',
    },
  ] as const;
 
  const depositCalldata = encodeFunctionData({
    abi: DEPOSITOR_ABI,
    functionName: 'depositProxy2',
    args: [
      {
        ...depositParams,
        signer: evmSignerAddress, // EVM signer address (matches quote request)
        approvalExpiration: BigInt(String(depositParams.approvalExpiration)),
        duration: BigInt(String(depositParams.duration)),
      },
    ],
  });
 
  // Encode USDC approval calldata
  const XG_UINT256_MAX = (1n << 256n) - 1n;
  const S0X_ADDR_USDC_AVALANCHE = '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E';
  const approveUsdcCalldata = encodeFunctionData({
    abi: erc20AbiForEncode,
    functionName: 'approve',
    args: [depositorAddress, XG_UINT256_MAX],
  });
 
  // Fetch Relay.link quote for execution (EXACT_OUTPUT with txs)
  const relayQuote = await fetchRelayQuote({
    user: solanaSenderAddress, // Solana address
    referrer: 'silentswap',
    originChainId: N_RELAY_CHAIN_ID_SOLANA,
    destinationChainId: 43114, // Avalanche
    originCurrency: SB58_ADDR_SOL_PROGRAM_SYSTEM, // Native SOL
    destinationCurrency: S0X_ADDR_USDC_AVALANCHE,
    amount: usdcAmount, // Target USDC amount
    tradeType: 'EXACT_OUTPUT', // CRITICAL: use EXACT_OUTPUT for execution
    recipient: evmSignerAddress, // EVM address for recipient
    txsGasLimit: 600_000,
    txs: [
      {
        to: S0X_ADDR_USDC_AVALANCHE,
        value: '0',
        data: approveUsdcCalldata,
      },
      {
        to: depositorAddress,
        value: '0',
        data: depositCalldata,
      },
    ],
  });
 
  // Execute Solana transactions from relay quote
  const transaction = new Transaction();
  
  for (const step of relayQuote.steps || []) {
    if (step.kind !== 'transaction') continue;
    
    for (const item of step.items) {
      const itemData = item.data as any;
      if ('instructions' in itemData) {
        // Add Solana instructions to transaction
        for (const instruction of itemData.instructions) {
          transaction.add({
            keys: instruction.keys.map((k: any) => ({
              pubkey: new PublicKey(k.pubkey),
              isSigner: k.isSigner,
              isWritable: k.isWritable,
            })),
            programId: new PublicKey(instruction.programId),
            data: Buffer.from(instruction.data, 'base64'),
          });
        }
      }
    }
  }
 
  // Sign and send Solana transaction
  transaction.recentBlockhash = (await solanaConnection.getLatestBlockhash()).blockhash;
  transaction.feePayer = solanaKeypair.publicKey;
  transaction.sign(solanaKeypair);
 
  const signature = await solanaConnection.sendRawTransaction(transaction.serialize());
  await solanaConnection.confirmTransaction(signature, 'confirmed');
 
  // Monitor bridge status
  const requestId = relayQuote.steps?.find((s) => s.requestId)?.requestId;
  if (!requestId) {
    throw new Error('Missing relay.link request ID');
  }
 
  const depositTxHash = await monitorRelayBridgeStatus(requestId);
  return depositTxHash;
}
 
// Helper: Monitor relay bridge status
async function monitorRelayBridgeStatus(requestId: string): Promise<string> {
  const { getRelayStatus } = await import('@silentswap/sdk');
  
  while (true) {
    const status = await getRelayStatus(requestId);
    
    if (status.status === 'success') {
      return status.txHashes?.[0] || '0x';
    }
    
    if (status.status === 'failed' || status.status === 'refund') {
      throw new Error(`Bridge failed: ${status.details || 'Unknown error'}`);
    }
    
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}

Example 2: EVM Token → Solana SPL Token Swap

This example swaps USDC on Avalanche to USDC SPL token on Solana.

async function executeEvmToSolanaSwap(
  usdcAmount: string, // Human-readable amount (e.g., "100")
  recipientSolanaAddress: string, // Base58 Solana address
  destinationTokenMint: string // Base58 SPL token mint address
) {
  try {
    // Step 1: Authenticate and derive entropy
    console.log('Step 1: Authenticating with SilentSwap...');
    const entropy = await authenticateAndDeriveEntropy(silentswap, evmSigner);
    console.log('✓ Authentication successful');
 
    // Step 2: Create facilitator group
    console.log('\nStep 2: Creating facilitator group...');
    const depositCount = await queryDepositCount(evmAccount.address);
    const group = await createHdFacilitatorGroupFromEntropy(
      hexToBytes(entropy),
      depositCount
    );
    console.log(`✓ Facilitator group created (deposit count: ${depositCount})`);
 
    // Step 3: Get quote (direct USDC deposit on Avalanche)
    console.log('\nStep 3: Requesting quote...');
    const viewer = await group.viewer();
    const { publicKeyBytes: pk65_viewer } = viewer.exportPublicKey(
      '*',
      FacilitatorKeyType.SECP256K1
    );
 
    const groupPublicKeys = await group.exportPublicKeys(1, [
      ...PublicKeyArgGroups.GENERIC,
    ]);
 
    // Convert USDC amount to microUSDC (6 decimals)
    const usdcAmountBN = BigNumber(usdcAmount);
    const usdcAmountMicro = usdcAmountBN.shiftedBy(6).toFixed(0);
 
    // Create Solana destination CAIP-19
    const solanaDestinationCaip19 = caip19SplToken(
      '5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1', // Solana mainnet chain ID
      destinationTokenMint
    );
 
    // Request quote
    const [quoteError, quoteResponse] = await silentswap.quote({
      signer: evmAccount.address,
      viewer: pk65_viewer,
      outputs: [
        {
          method: DeliveryMethod.SNIP,
          recipient: recipientSolanaAddress, // Base58 Solana address (NOT CAIP-10)
          asset: solanaDestinationCaip19,
          value: usdcAmountMicro as `${bigint}`,
          facilitatorPublicKeys: groupPublicKeys[0],
        },
      ],
    });
 
    if (quoteError || !quoteResponse) {
      throw new Error(`Failed to get quote: ${quoteError?.type}: ${quoteError?.error}`);
    }
    console.log(`✓ Quote received (Order ID: ${quoteResponse.quoteId})`);
 
    // Step 4: Sign authorizations and create order
    console.log('\nStep 4: Signing authorizations and creating order...');
    const orderResponse = await createOrder(
      silentswap,
      evmSigner,
      group,
      quoteResponse,
      {
        sourceAsset: {
          caip19: 'eip155:43114/erc20:0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', // USDC on Avalanche
          amount: usdcAmountMicro,
        },
        sourceSender: {
          contactId: `caip10:eip155:43114:${evmAccount.address}`,
        },
      }
    );
    console.log(`✓ Order created (Order ID: ${orderResponse.response.orderId})`);
 
    // Step 5: Execute deposit transaction (direct USDC deposit)
    console.log('\nStep 5: Executing deposit transaction...');
    const depositHash = await executeDeposit(evmClient, orderResponse);
    console.log(`✓ Deposit transaction sent: ${depositHash}`);
 
    // Step 6: Watch for completion on Solana
    console.log('\nStep 6: Watching for order completion on Solana...');
    await watchForSolanaCompletion(
      solanaConnection,
      destinationTokenMint,
      recipientSolanaAddress,
      group
    );
 
    return {
      orderId: orderResponse.response.orderId,
      depositHash,
      quote: quoteResponse,
    };
  } catch (err) {
    console.error('EVM to Solana swap error:', err);
    throw err;
  }
}
 
// Helper: Watch for completion on Solana
async function watchForSolanaCompletion(
  connection: Connection,
  tokenMint: string,
  recipientAddress: string,
  group: Awaited<ReturnType<typeof createHdFacilitatorGroupFromEntropy>>
) {
  // Get facilitator account for Solana (coin type 501)
  const facilitator0Sol = await group.account('501', 0);
  const facilitator0SolEvm = await facilitator0Sol.evmSigner();
  
  // In a real implementation, you would watch for SPL token transfers
  // This is a simplified example - you'd use Solana webhooks or polling
  const recipientPubkey = new PublicKey(recipientAddress);
  const mintPubkey = new PublicKey(tokenMint);
  const ata = await getAssociatedTokenAddress(mintPubkey, recipientPubkey);
 
  console.log(`Watching for tokens at: ${ata.toString()}`);
  
  // Poll for token account balance
  return new Promise<void>((resolve) => {
    const interval = setInterval(async () => {
      try {
        const account = await getAccount(connection, ata);
        if (account.amount > 0n) {
          console.log(`✓ Recipient received ${account.amount.toString()} tokens`);
          clearInterval(interval);
          resolve();
        }
      } catch (err) {
        // Account doesn't exist yet, continue polling
      }
    }, 2000);
  });
}

Usage Examples

Example 1: Swap 1 SOL → USDC on Ethereum

const result = await executeSolanaToEvmSwap(
  '1', // 1 SOL
  '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb5', // Recipient EVM address
  '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on Ethereum
  1 // Ethereum mainnet
);
 
console.log('Swap completed:', result);

Example 2: Swap 100 USDC → USDC SPL on Solana

const result = await executeEvmToSolanaSwap(
  '100', // 100 USDC
  '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM', // Recipient Solana address (base58)
  'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC SPL token mint
);
 
console.log('Swap completed:', result);

Key Points

  1. Dual Address Requirement:

    • Solana address: For Solana transactions (source swaps)
    • EVM address: For facilitator operations and deposit calldata
  2. Bridge Provider: Solana swaps use relay.link automatically

  3. Address Formats:

    • Solana addresses: Base58 format (e.g., 9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM)
    • EVM addresses: Hex format with 0x prefix
  4. CAIP-19 Identifiers:

    • Native SOL: solana:<chainId>/slip44:501
    • SPL Tokens: solana:<chainId>/erc20:<tokenMint>
  5. Trade Type: Execution quotes must use EXACT_OUTPUT with txs parameter for Solana swaps

Helper Functions

These helper functions are used in the examples above. See the Complete Example for full implementations of common helpers.

// Helper: Authenticate and derive entropy
async function authenticateAndDeriveEntropy(
  silentswap: SilentSwapClient,
  signer: EvmSigner
): Promise<`0x${string}`> {
  // Get nonce
  const [nonceError, nonceResponse] = await silentswap.nonce(signer.address);
  if (!nonceResponse || nonceError) {
    throw new Error(`Failed to get nonce: ${nonceError?.type}: ${nonceError?.error}`);
  }
 
  // Create sign-in message
  const signInMessage = createSignInMessage(
    signer.address,
    nonceResponse.nonce,
    'silentswap.com'
  );
 
  // Sign message
  const siweSignature = await signer.signEip191Message(signInMessage.message);
 
  // Authenticate
  const [authError, authResponse] = await silentswap.authenticate({
    siwe: {
      message: signInMessage.message,
      signature: siweSignature,
    },
  });
 
  if (!authResponse || authError) {
    throw new Error(`Failed to authenticate: ${authError?.type}: ${authError?.error}`);
  }
 
  // Derive entropy from auth token
  const eip712Doc = createEip712DocForWalletGeneration(authResponse.secretToken);
  const entropy = await signer.signEip712TypedData(eip712Doc);
 
  return entropy;
}
 
// Helper: Create order
async function createOrder(
  silentswap: SilentSwapClient,
  signer: EvmSigner,
  group: Awaited<ReturnType<typeof createHdFacilitatorGroupFromEntropy>>,
  quoteResponse: any,
  metadata?: {
    sourceAsset?: { caip19: string; amount: string };
    sourceSender?: { contactId: string };
  }
) {
  // Sign authorizations (empty for Solana swaps)
  const signedAuths = await Promise.all(
    quoteResponse.authorizations.map(async (g_auth: any) => ({
      ...g_auth,
      signature: '0x' as `0x${string}`, // No EIP-3009 for Solana
    }))
  );
 
  // Sign the order's EIP-712
  const orderDoc = quoteResponseToEip712Document(quoteResponse);
  const signedQuote = await signer.signEip712TypedData(orderDoc);
 
  // Approve proxy authorizations
  const facilitatorReplies = await group.approveProxyAuthorizations(
    quoteResponse.facilitators,
    {
      proxyPublicKey: silentswap.proxyPublicKey,
    }
  );
 
  // Place the order
  const [orderError, orderResponse] = await silentswap.order({
    quote: quoteResponse.quote,
    quoteId: quoteResponse.quoteId,
    authorizations: signedAuths,
    eip712Domain: orderDoc.domain,
    signature: signedQuote,
    facilitators: facilitatorReplies,
    metadata,
  });
 
  if (orderError || !orderResponse) {
    throw new Error(`Failed to place order: ${orderError?.type}: ${orderError?.error}`);
  }
 
  return orderResponse;
}
 
// Helper: Execute deposit (for EVM direct deposits)
async function executeDeposit(
  client: ReturnType<typeof createWalletClient>,
  orderResponse: any
) {
  // Parse transaction request
  const txRequestParams = parseTransactionRequestForViem(orderResponse.transaction);
 
  // Send transaction
  const hash = await client.sendTransaction(txRequestParams);
 
  // Wait for confirmation
  const txReceipt = await client.waitForTransactionReceipt({ hash });
  console.log(
    `Deposit confirmed: ${BigNumber(orderResponse.response.order.deposit)
      .shiftedBy(-6)
      .toFixed()} USDC at ${txReceipt.transactionHash}`
  );
 
  return hash;
}
 
// Helper: Monitor relay bridge status
async function monitorRelayBridgeStatus(requestId: string): Promise<string> {
  while (true) {
    const status = await getRelayStatus(requestId);
    
    if (status.status === 'success') {
      return status.txHashes?.[0] || '0x';
    }
    
    if (status.status === 'failed' || status.status === 'refund') {
      throw new Error(`Bridge failed: ${status.details || 'Unknown error'}`);
    }
    
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}
 
// Helper: Watch for completion on EVM chain
async function watchForCompletion(
  client: ReturnType<typeof createWalletClient>,
  tokenAddress: `0x${string}`,
  recipientAddress: `0x${string}`,
  group: Awaited<ReturnType<typeof createHdFacilitatorGroupFromEntropy>>,
  tokenDecimals: number
) {
  // Get facilitator account for coin type 60 (ETH) at output index 0
  const facilitator0Eth = await group.account('60', 0);
  const facilitator0EthEvm = await facilitator0Eth.evmSigner();
 
  // Create client for destination chain (Mainnet)
  const destinationClient = createWalletClient({
    chain: mainnet,
    transport: http(),
  }).extend(publicActions);
 
  // Watch for ERC-20 transfer event
  return new Promise<void>((resolve) => {
    destinationClient.watchContractEvent({
      address: tokenAddress,
      abi: erc20Abi,
      eventName: 'Transfer',
      args: {
        to: recipientAddress,
        from: facilitator0EthEvm.address,
      },
      onLogs: (logs) => {
        for (const log of logs) {
          const { to, value } = log.args;
          console.log(
            `✓ Recipient ${to} received ${BigNumber(value!)
              .shiftedBy(-tokenDecimals)
              .toFixed()} tokens`
          );
        }
        resolve();
      },
    });
  });
}

Next Steps