Skip to main content
Learn how to swap SOL to USDC on Arbitrum using OneBalance APIs.

Complete Code Repository

Get the full working example with setup instructions. 100% free and ready to run.

Setup

Install packages and create environment file:
pnpm install @solana/web3.js bs58 dotenv viem
.env
SOLANA_PRIVATE_KEY=your_base58_private_key_here
ONEBALANCE_API_KEY=your_api_key_here
ARBITRUM_RECIPIENT=0x895Cf62399bF1F8b88195E741b64278b41EB7F09

Example

swap.ts
import { Keypair, MessageV0, VersionedTransaction, PublicKey } from '@solana/web3.js';
import { formatUnits, parseUnits } from 'viem';
import bs58 from 'bs58';
import dotenv from 'dotenv';

// Load environment variables
dotenv.config();

// Configuration from environment
const SOLANA_PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY!;
const ARBITRUM_RECIPIENT = process.env.ARBITRUM_RECIPIENT!;
const API_KEY = process.env.ONEBALANCE_API_KEY || "42bb629272001ee1163ca0dbbbc07bcbb0ef57a57baf16c4b1d4672db4562c11";

// Create Solana keypair from private key
const keypair = Keypair.fromSecretKey(bs58.decode(SOLANA_PRIVATE_KEY));
const SOLANA_ACCOUNT = keypair.publicKey.toString();

// Solana asset IDs
const SOL_ASSET_ID = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501";
const USDC_ARB_ASSET_ID = "eip155:42161/erc20:0xaf88d065e77c8cC2239327C5EDb3A432268e5831";

console.log(`Using Solana account: ${SOLANA_ACCOUNT}`);
console.log(`Arbitrum recipient: ${ARBITRUM_RECIPIENT}`);

/**
 * Check SOL balance before swapping
 */
async function checkSOLBalance() {
  try {
    console.log('Checking SOL balance...');
    
    const response = await fetch(
      `https://be.onebalance.io/api/v3/balances/aggregated-balance?account=solana:${SOLANA_ACCOUNT}&aggregatedAssetId=ob:sol`,
      { headers: { 'x-api-key': API_KEY } }
    );
    
    if (!response.ok) {
      throw new Error('Failed to fetch balance');
    }
    
    const balanceData = await response.json();
    const solBalance = balanceData.balanceByAggregatedAsset?.find(
      (asset: any) => asset.aggregatedAssetId === 'ob:sol'
    );
    
    if (!solBalance) {
      throw new Error('No SOL balance found');
    }
    
    const balanceInSOL = parseFloat(formatUnits(BigInt(solBalance.balance), 9));
    console.log(`Available SOL: ${balanceInSOL.toFixed(4)} SOL`);
    
    return balanceInSOL;
    
  } catch (error) {
    console.error('Balance check failed:', error);
    throw error;
  }
}

/**
 * Swap SOL to USDC on Arbitrum (cross-chain)
 */
async function swapSOLtoUSDC() {
  try {
    console.log('Starting SOL → USDC (Arbitrum) swap...\n');
    
    // Validate Solana address format
    try {
      new PublicKey(SOLANA_ACCOUNT);
    } catch (error) {
      throw new Error('Invalid Solana address format');
    }
    
    // Check balance
    const balance = await checkSOLBalance();
    const swapAmount = 0.01; // 0.01 SOL
    
    if (balance < swapAmount) {
      throw new Error(`Insufficient balance. Need ${swapAmount} SOL, have ${balance.toFixed(4)} SOL`);
    }
    
    console.log(`Swapping ${swapAmount} SOL to USDC on Arbitrum...`);
    
    // Step 1: Get quote
    console.log('Getting quote...');
    
    const quoteRequest = {
      from: {
        accounts: [{
          type: "solana",
          accountAddress: SOLANA_ACCOUNT
        }],
        asset: {
          assetId: SOL_ASSET_ID
        },
        amount: parseUnits(swapAmount.toString(), 9).toString() // SOL has 9 decimals
      },
      to: {
        asset: {
          assetId: USDC_ARB_ASSET_ID // USDC on Arbitrum
        },
        account: `eip155:42161:${ARBITRUM_RECIPIENT}`
      }
    };

    const quoteResponse = await fetch('https://be.onebalance.io/api/v3/quote', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY
      },
      body: JSON.stringify(quoteRequest)
    });

    if (!quoteResponse.ok) {
      const error = await quoteResponse.json();
      throw new Error(`Quote failed: ${error.message}`);
    }

    const quote = await quoteResponse.json();
    console.log('Quote received:', {
      id: quote.id,
      willReceive: `${formatUnits(BigInt(quote.destinationToken.amount), 6)} USDC`,
      fiatValue: `$${quote.destinationToken.fiatValue}`,
      fees: `$${quote.fees.cumulativeUSD}`
    });

    // Step 2: Sign the Solana operation
    console.log('Signing Solana transaction...');
    
    const solanaOperation = quote.originChainsOperations.find((op: any) => op.type === 'solana');
    if (!solanaOperation) {
      throw new Error('No Solana operation found in quote');
    }
    
    const signedOperation = signSolanaOperation(solanaOperation);
    console.log('Transaction signed successfully');
    
    const signedQuote = {
      ...quote,
      originChainsOperations: quote.originChainsOperations.map((op: any) => 
        op.type === 'solana' ? signedOperation : op
      )
    };

    // Step 3: Execute the swap
    console.log('Executing cross-chain swap...');
    
    const executeResponse = await fetch('https://be.onebalance.io/api/v3/quote/execute', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': API_KEY
      },
      body: JSON.stringify(signedQuote)
    });

    if (!executeResponse.ok) {
      const error = await executeResponse.json();
      throw new Error(`Execution failed: ${error.message}`);
    }

    const result = await executeResponse.json();
    console.log('Swap submitted successfully!');
    
    // Step 4: Monitor transaction completion
    console.log('Monitoring transaction...');
    await monitorCompletion(quote.id);
    
    console.log('Cross-chain swap completed successfully!');
    console.log(`USDC delivered to Arbitrum address: ${ARBITRUM_RECIPIENT}`);
    
    return result;

  } catch (error) {
    console.error('Swap failed:', (error as Error).message);
    throw error;
  }
}

// Real Solana signing implementation
function signSolanaOperation(chainOp: any): any {
  try {
    const msgBuffer = Buffer.from(chainOp.dataToSign, 'base64');
    const message = MessageV0.deserialize(msgBuffer);
    const transaction = new VersionedTransaction(message);
    
    // Sign with keypair
    transaction.sign([keypair]);
    
    // Extract signature
    const signature = bs58.encode(Buffer.from(transaction.signatures[transaction.signatures.length - 1]));
    
    return {
      ...chainOp,
      signature,
    };
    
  } catch (error) {
    console.error('Signing failed:', error);
    throw new Error(`Failed to sign Solana transaction: ${(error as Error).message}`);
  }
}

// Monitor transaction completion
async function monitorCompletion(quoteId: string): Promise<void> {
  const timeout = 60_000; // 60 seconds
  const startTime = Date.now();
  
  while (Date.now() - startTime < timeout) {
    try {
      const statusResponse = await fetch(
        `https://be.onebalance.io/api/v3/status/get-execution-status?quoteId=${quoteId}`,
        { headers: { 'x-api-key': API_KEY } }
      );
      
      if (statusResponse.ok) {
        const status = await statusResponse.json();
        console.log(`Status: ${status.status.status}`);

        if (status.status.status === 'COMPLETED') {
          console.log('Transaction completed successfully!');
          return;
        } else if (status.status.status === 'FAILED') {
          throw new Error('Transaction failed');
        }
      }
    } catch (error) {
      console.log('Status check error (retrying):', (error as Error).message);
    }

    // Wait 3 seconds before next check
    await new Promise(resolve => setTimeout(resolve, 3000));
  }
  
  throw new Error('Transaction monitoring timeout');
}

// Run the swap
async function main() {
  try {
    await swapSOLtoUSDC();
  } catch (error) {
    console.error('Failed:', error);
    process.exit(1);
  }
}

main();

How It Works

The script performs a complete cross-chain swap with validation and monitoring:
  1. Balance Check: Verify you have sufficient SOL for the swap
  2. Request Quote: Get pricing for 0.01 SOL → USDC on Arbitrum
  3. Sign Transaction: Sign the Solana operation with your private key
  4. Execute Swap: Submit the signed transaction to OneBalance
  5. Monitor Progress: Track the transaction until completion
Expected Result: ~0.015 USDC delivered to your Arbitrum address with full monitoring

Run the Example

Set your environment variables and run:
npx tsx swap.ts
Make sure your Solana account has at least 0.01 SOL for the swap.

Next Steps

Got stuck? Check the Solana Troubleshooting Guide for common issues and solutions.
Last modified on October 24, 2025