Skip to main content
Bungee also supports swap assets from any EVM chain to Solana and from Solana to any EVM chain. This guide walks you through swapping assets between EVM chains and Solana using Bungee. It covers supported chains, transaction options, and current limitations.

Quick Start

Solana Chain ID on Bungee API

The chain ID for Solana queries is 89999.

Key differences

Here’s what’s different when integrating Solana Bungee API compared to standard EVM chains: Transaction Flow
  • No token approvals are needed on Solana
API Behavior
  • Both userAddress and receiverAddress required for quotes
  • Fee collection only works for EVM to Solana direction
  • Native Tokens across all Chains (0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee): Use this address to represent native tokens on any chain (ETH, SOL, etc.)
  • Wrapped SOL (wSOL) (So11111111111111111111111111111111111111112): The SPL token address for wrapped SOL on Solana

Supported chains

Currently, these chains are supported to bridge between EVM and Solana:
  • Ethereum
  • Optimism
  • Arbitrum
  • Polygon
  • Base
  • Avalanche
  • Binance Smart Chain
This chain list is for reference only.

Integration Steps

The “Bungee Manual” guide from Bungee Docs outlines how to integrate a bridging process that allows users to swap and bridge tokens across chains in a single transaction on the source chain, eliminating the need for any transactions on the destination chain. This same process should be implemented when bridging to Solana.
  1. Select Chains: Users choose the source and destination chains, which determine the tokens available for bridging.
  2. Fetch Routes: After selecting the tokens, retrieve possible bridging routes using the /quote endpoint.
  3. Token Approval: For ERC-20 tokens, users must grant permission for the protocol to spend their tokens.
  4. Build Transaction: Obtain the transaction data from the /build-tx endpoint. Use this data to execute the bridging transaction on the source chain.
  5. Track Transaction Status: Monitor the transaction’s progress by polling the /status endpoint until the bridging process is complete.
For Solana quotes, please ensure both userAddress and receiverAddress are defined.Also, remember Solana does not require token approvals.

Examples

Queries

https://public-backend.bungee.exchange/api/v1/bungee/quote?userAddress=0x3e8cB4bd04d81498aB4b94a392c334F5328b237b&originChainId=8453&destinationChainId=89999&inputAmount=100000000&inputToken=0x833589fcd6edb6e08f4c7c32d4f71b54bda02913&enableManual=true&receiverAddress=7BchahMyqpBZYmQS3QnbY3kRweDLgPCRpWdo1rxWmJ3g&refuel=false&outputToken=6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
Querying the Solana RPC directly is recommended, nonetheless the current API supports:
https://public-backend.bungee.exchange/api/v1/tokens/search?q=FUAfBo2jgks6gB4Z4LfZkqSZgzNucisEHqnNebaRxM1P&userAddress=7BchahMyqpBZYmQS3QnbY3kRweDLgPCRpWdo1rxWmJ3g

Scripts

import { createPublicClient, createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import dotenv from 'dotenv';
dotenv.config();

// Environment variable validation helper
function getRequiredEnv(name) {
  const value = process.env[name];
  if (!value || value.trim() === '') {
    throw new Error(`Error: ${name} environment variable is not set or is empty`);
  }
  return value;
}

// Environment variables
const SOLANA_WALLET_ADDRESS = getRequiredEnv('SOLANA_WALLET_ADDRESS');
const EVM_WALLET_ADDRESS = getRequiredEnv('EVM_WALLET_ADDRESS');
const EVM_PRIVATE_KEY = getRequiredEnv('EVM_PRIVATE_KEY');

// API information
const BUNGEE_API_BASE_URL = 'https://public-backend.bungee.exchange';
const BASE_RPC_URL = 'https://base.drpc.org';

// Contract Addresses
const SOLANA_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; // native
const BASE_USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913';

// Transaction details
const TX_GAS_LIMIT = 420000n;
const SWAP_AMOUNT = 100000000n;

// ERC-20 Approval ABI
const erc20Abi = [
  {
    name: 'approve',
    type: 'function',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' }
    ],
    outputs: [{ type: 'bool' }]
  }
];

async function getQuote() {
  const params = {
    userAddress: EVM_WALLET_ADDRESS,
    originChainId: 8453, // Base
    destinationChainId: 89999, // Bungee Solana chain ID
    inputAmount: SWAP_AMOUNT.toString(),
    inputToken: BASE_USDC,
    enableManual: true, // Enable manual routes
    receiverAddress: SOLANA_WALLET_ADDRESS,
    refuel: false,
    outputToken: SOLANA_TOKEN_ADDRESS,
  };

  try {
    const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
    const queryParams = new URLSearchParams(params);
    const fullUrl = `${url}?${queryParams}`;

    const response = await fetch(fullUrl);
    const data = await response.json();

    if (!data.success) {
      throw new Error(`Quote error: ${data.error?.message || "Unknown error"}`);
    }

    // Check if manual routes are available
    if (!data.result.manualRoutes || data.result.manualRoutes.length === 0) {
      throw new Error("No manual routes available for this bridge");
    }

    // Select the first manual route
    const selectedRoute = data.result.manualRoutes[0];
    console.log("Selected manual route:", selectedRoute.quoteId);
    console.log("Expected Output Amount:", selectedRoute.output.amount);

    await buildAndSendTx(selectedRoute.quoteId, selectedRoute.approvalData);
  } catch (error) {
    console.error('Failed to get quote:', error);
    throw error;
  }
}

async function buildAndSendTx(quoteId, approvalData) {
  try {
    console.log("Building transaction for quote ID:", quoteId);

    // Build transaction using quoteId parameter
    const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/build-tx?quoteId=${quoteId}`;
    const response = await fetch(url);
    const data = await response.json();

    if (!data.success) {
      throw new Error(`Build transaction error: ${data.error?.message || "Unknown error"}`);
    }

    const txData = data.result.txData;

    // Create Viem clients
    const publicClient = createPublicClient({
      chain: base,
      transport: http(BASE_RPC_URL)
    });

    // Normalize private key to ensure it has 0x prefix
    const normalizedPrivateKey = EVM_PRIVATE_KEY.startsWith('0x') 
      ? EVM_PRIVATE_KEY 
      : `0x${EVM_PRIVATE_KEY}`;
    const account = privateKeyToAccount(normalizedPrivateKey);

    const walletClient = createWalletClient({
      account,
      chain: base,
      transport: http(BASE_RPC_URL)
    });

    // Get gas price
    const gasPrice = await publicClient.getGasPrice();

    // Handle token approval if needed
    if (approvalData) {
      console.log("Token approval required");

      try {
        const approveTx = await walletClient.writeContract({
          address: approvalData.tokenAddress,
          abi: erc20Abi,
          functionName: 'approve',
          args: [
            approvalData.spenderAddress,
            BigInt(approvalData.amount)
          ],
          gasPrice,
          gas: TX_GAS_LIMIT
        });

        console.log('Approval Transaction sent! Hash:', approveTx);

        // Wait for transaction confirmation
        const approvalReceipt = await publicClient.waitForTransactionReceipt({
          hash: approveTx
        });
        console.log('Approval successful!', approvalReceipt);
      } catch (error) {
        console.error('Error during approval:', error);
        throw error;
      }
    }

    // Send bridge transaction
    try {
      const bridgeTx = await walletClient.sendTransaction({
        to: txData.to,
        data: txData.data,
        value: BigInt(txData.value || "0"),
        gasPrice,
        gas: TX_GAS_LIMIT
      });

      console.log('Bridge Transaction sent. Hash:', bridgeTx);

      // Wait for transaction confirmation
      const receipt = await publicClient.waitForTransactionReceipt({
        hash: bridgeTx
      });
      console.log('Bridge Transaction confirmed:', receipt);
    } catch (error) {
      console.error('Error during bridge transaction:', error);
    }
  } catch (error) {
    console.error('Failed to build and send transaction:', error);
    throw error;
  }
}

(async () => {
  await getQuote();
})();
import * as solanaWeb3 from '@solana/web3.js';
import { getAddressLookupTableAccounts } from '@mayanfinance/swap-sdk';
import fs from 'fs';
import path from 'path';
import os from 'os';
import dotenv from 'dotenv';
dotenv.config();

// Environment variable validation helper
function getRequiredEnv(name) {
  const value = process.env[name];
  if (!value || value.trim() === '') {
    console.error(`Error: ${name} environment variable is not set or is empty`);
    process.exit(1);
  }
  return value;
}

const keyPath = path.join(os.homedir(), '.config', 'solana', 'id.json');
const KEY = JSON.parse(fs.readFileSync(keyPath, 'utf-8'));

// Environment variables
const SOLANA_WALLET_ADDRESS = getRequiredEnv('SOLANA_WALLET_ADDRESS');
const EVM_WALLET_ADDRESS = getRequiredEnv('EVM_WALLET_ADDRESS');

// API Information - Updated to Bungee API
const BUNGEE_API_BASE_URL = 'https://public-backend.bungee.exchange';

// Token Addresses
const SOLANA_TOKEN_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; // native
const POLYGON_USDC = '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359';

// Transaction Parameters
const SWAP_AMOUNT = '50000000';
const FROM_CHAIN_ID = '89999'; // Solana chain ID
const TO_CHAIN_ID = '137'; // Polygon chain ID

async function getQuote() {
  const params = {
    userAddress: SOLANA_WALLET_ADDRESS,
    receiverAddress: EVM_WALLET_ADDRESS,
    originChainId: FROM_CHAIN_ID,
    destinationChainId: TO_CHAIN_ID,
    inputToken: SOLANA_TOKEN_ADDRESS,
    outputToken: POLYGON_USDC,
    inputAmount: SWAP_AMOUNT,
    enableManual: true, // Enable manual routes
  };

  try {
    const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
    const queryParams = new URLSearchParams(params);
    const fullUrl = `${url}?${queryParams}`;
    console.log(fullUrl);

    const response = await fetch(fullUrl);
    const data = await response.json();

    if (!data.success) {
      throw new Error(`Quote error: ${data.error?.message || "Unknown error"}`);
    }

    // Check if manual routes are available
    if (!data.result.manualRoutes || data.result.manualRoutes.length === 0) {
      throw new Error("No manual routes available for this bridge");
    }

    // Select the first manual route
    const selectedRoute = data.result.manualRoutes[0];
    console.log("Selected manual route:", selectedRoute.quoteId);
    console.log("Expected Output Amount:", selectedRoute.output.amount);

    await getbuildtx(selectedRoute.quoteId);
  } catch (error) {
    console.error('Failed to get quote:', error);
    throw error;
  }
}

async function getbuildtx(quoteId) {
  try {
    console.log("Building transaction for quote ID:", quoteId);

    // Build transaction using quoteId parameter
    const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/build-tx?quoteId=${quoteId}`;
    const response = await fetch(url);
    const data = await response.json();

    if (!data.success) {
      throw new Error(`Build transaction error: ${data.error?.message || "Unknown error"}`);
    }

    const txData = data.result.txData;

    const clientInstructions = txData.instructions.map(
      (instruction) =>
        new solanaWeb3.TransactionInstruction({
          programId: new solanaWeb3.PublicKey(instruction.programId),
          keys: instruction.keys.map((key) => ({
            pubkey: new solanaWeb3.PublicKey(key.pubkey),
            isSigner: key.isSigner,
            isWritable: key.isWritable,
          })),
          data: Buffer.from(instruction.data),
        }),
    );

    const solanaConnection = new solanaWeb3.Connection(
      solanaWeb3.clusterApiUrl('mainnet-beta'),
    );

    const clientLookupTables = await getAddressLookupTableAccounts(
      txData.lookupTables,
      solanaConnection,
    );

    const _signers = txData.signers.map((signer) =>
      solanaWeb3.Keypair.fromSecretKey(Uint8Array.from(signer)),
    );

    const feePayer = new solanaWeb3.PublicKey(SOLANA_WALLET_ADDRESS);
    const { blockhash, lastValidBlockHeight } =
      await solanaConnection.getLatestBlockhash('finalized');

    const message = solanaWeb3.MessageV0.compile({
      instructions: clientInstructions,
      payerKey: feePayer,
      recentBlockhash: blockhash,
      addressLookupTableAccounts: clientLookupTables,
    });
    const transaction = new solanaWeb3.VersionedTransaction(message);
    transaction.sign(_signers);

    const privateKey = Uint8Array.from(KEY);
    const keypair = solanaWeb3.Keypair.fromSecretKey(privateKey);
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = keypair.publicKey;
    transaction.sign([keypair]);

    const fee = await transaction.getEstimatedFee(solanaConnection);

    // Convert lamports to SOL
    const feeInSol = fee / solanaWeb3.LAMPORTS_PER_SOL;

    const signature = await solanaConnection.sendRawTransaction(
      transaction.serialize(),
      {
        skipPreflight: false,
        preflightCommitment: 'confirmed',
      },
    );

    // Optional: Wait for confirmation
    const confirmation = await solanaConnection.confirmTransaction({
      signature,
      blockhash: blockhash,
      lastValidBlockHeight: lastValidBlockHeight,
    });

    if (confirmation.value.err) {
      throw new Error(`Transaction failed: ${confirmation.value.err}`);
    }
    console.log({ signature });

    return signature;
  } catch (error) {
    console.error('Failed to build and send transaction:', error);
    throw error;
  }
}

(async () => {
  await getQuote();
})();