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 from EVM to Solana
  • 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

Most major EVM chains on Bungee are supported to bridge to Solana.

Integration Steps

The “Bungee Auto” 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 Quote: Call the /quote endpoint which returns an autoRoute object containing txData, requestHash, and optionally approvalData. The transaction data is ready to use directly from the quote response.
  3. Handle Token Approval (EVM only): For EVM transactions, check if approvalData exists in the quote response. If present, handle token approvals before submitting the transaction. Solana transactions do not require approvals.
  4. Submit Transaction: Execute the transaction using txData from the quote response. The transaction type (evm or solana) determines the submission method and required parameters.
  5. Track Transaction Status: Monitor the transaction’s progress by polling the /status endpoint using the source txHash 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 dotenv from "dotenv";
dotenv.config();

import { privateKeyToAccount } from "viem/accounts";
import { createPublicClient, http, createWalletClient } from "viem";
import { base } from "viem/chains";
import { Keypair, Connection, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction } from "@solana/web3.js";
import bs58 from "bs58";

// Helper function to fetch address lookup table accounts for Solana transactions
async function getAddressLookupTableAccounts(lookupTableAddresses, connection) {
  const lookupTables = await Promise.all(
    lookupTableAddresses.map(async (address) => {
      const result = await connection.getAddressLookupTable(new PublicKey(address));
      return result.value;
    })
  );
  return lookupTables.filter((table) => table !== null);
}

// Check if PRIVATE_KEY is set
if (!process.env.PRIVATE_KEY) {
  console.error("Error: PRIVATE_KEY environment variable is not set");
  console.error(
    "Example: PRIVATE_KEY=<YOUR_64_HEX_PRIVATE_KEY>"
  );
  process.exit(1);
}

// Check if SOLANA_PRIVATE_KEY is set
if (!process.env.SOLANA_PRIVATE_KEY) {
  console.error("Error: SOLANA_PRIVATE_KEY environment variable is not set");
  console.error(
    "Example: SOLANA_PRIVATE_KEY=your_base58_private_key_or_array"
  );
  process.exit(1);
}

// Create EVM account from private key
// Trim whitespace and ensure proper format
const evmPrivateKey = process.env.PRIVATE_KEY.trim();
const account = privateKeyToAccount(evmPrivateKey);

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

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

// Parse Solana private key and create keypair
let solanaKeypair;
try {
  const solanaPrivateKey = process.env.SOLANA_PRIVATE_KEY;

  // Try parsing as base58 string first
  let privateKeyBytes;
  try {
    privateKeyBytes = bs58.decode(solanaPrivateKey);
  } catch {
    // If not base58, try parsing as JSON array
    try {
      privateKeyBytes = Uint8Array.from(JSON.parse(solanaPrivateKey));
    } catch {
      // If that fails, try as comma-separated string
      privateKeyBytes = Uint8Array.from(solanaPrivateKey.split(",").map(Number));
    }
  }

  solanaKeypair = Keypair.fromSecretKey(privateKeyBytes);
  console.log("Solana address:", solanaKeypair.publicKey.toBase58());
} catch (error) {
  console.error("Error parsing Solana private key:", error);
  console.error("Please provide a valid base58 string or array format");
  process.exit(1);
}

const solanaAddress = solanaKeypair.publicKey.toBase58();

// API and token parameters
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";
const SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com";

// EVM to Solana
// const originChainId = 8453;
// const destinationChainId = 89999;
// const inputToken = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
// const outputToken = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
// const inputAmount = "5000000"; // 5 USDC (6 decimals)

// Solana to EVM
const originChainId = 89999;
const destinationChainId = 8453;
const inputToken = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; // Native SOL
const outputToken = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base
const inputAmount = "170000000"; // ~$15 of SOL (9 decimals)

// Conditionally set addresses based on origin chain
const quoteParams = {
  userAddress: originChainId === 89999 ? solanaAddress : account.address,
  receiverAddress: originChainId === 89999 ? account.address : solanaAddress,
  originChainId,
  destinationChainId,
  inputToken,
  outputToken,
  inputAmount,
};

// Function to get a quote
async function getQuote(params) {
  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);
    console.log(fullUrl);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    const serverReqId = response.headers.get("server-req-id");

    // Check for errors
    if (!data.success) {
      throw new Error(
        `Quote error: ${data.statusCode}: ${data.message || data.error?.message || "Unknown error"}. server-req-id: ${serverReqId}`
      );
    }

    // Check if autoRoute exists
    if (!data.result || !data.result.autoRoute) {
      throw new Error(`No autoRoute available. server-req-id: ${serverReqId}`);
    }

    const quoteId = data.result.autoRoute.quoteId;
    const requestHash = data.result.autoRoute.requestHash;
    const txData = data.result.autoRoute.txData;

    if (!quoteId) {
      throw new Error("Quote ID not found in response");
    }
    if (!requestHash) {
      throw new Error("Request hash not found in response");
    }
    if (!txData) {
      throw new Error("Transaction data not found in response");
    }

    const approvalData = data.result.autoRoute.approvalData;

    console.log("- Quote ID:", quoteId);
    console.log("- Request Hash:", requestHash);
    console.log("- Transaction Type:", txData.type);
    if (approvalData) {
      console.log("- Approval Required: Yes");
    }

    return {
      quoteId,
      requestHash,
      txData,
      approvalData,
      fullResponse: data,
    };
  } catch (error) {
    console.error("Failed to get quote:", error.message || error);
    throw error;
  }
}

// Function to submit EVM transaction
async function submitEVMTransaction(txData) {
  try {
    if (!txData || txData.type !== "evm") {
      throw new Error("Invalid EVM transaction data");
    }
    if (!txData.to) {
      throw new Error("Transaction 'to' address is required");
    }
    if (!txData.data) {
      throw new Error("Transaction data is required");
    }

    console.log("  To:", txData.to);
    console.log("  Value:", txData.value || "0");
    console.log("  Data:", txData.data.substring(0, 66) + "...");

    // Send the transaction
    const hash = await walletClient.sendTransaction({
      to: txData.to,
      value: BigInt(txData.value || "0"),
      data: txData.data,
    });

    console.log("- Transaction sent:", hash);

    // Wait for transaction to be mined
    const receipt = await publicClient.waitForTransactionReceipt({ hash });

    if (receipt.status === "reverted") {
      throw new Error(`Transaction reverted in block ${receipt.blockNumber}`);
    }

    console.log("- Transaction mined in block:", receipt.blockNumber);

    return {
      hash,
      receipt,
    };
  } catch (error) {
    console.error("Failed to submit EVM transaction:", error.message || error);
    throw error;
  }
}

// Function to check and handle token approvals
async function checkAndApproveToken(approvalData) {
  if (!approvalData || !approvalData.tokenAddress) {
    console.log("No approval data found or required");
    return;
  }

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

  try {
    // Validate required fields
    if (!approvalData.userAddress) {
      throw new Error("userAddress is required in approvalData");
    }
    if (!approvalData.spenderAddress) {
      throw new Error("spenderAddress is required in approvalData");
    }
    if (!approvalData.amount) {
      throw new Error("amount is required in approvalData");
    }

    // Check current allowance
    const currentAllowance = await publicClient.readContract({
      address: approvalData.tokenAddress,
      abi: erc20Abi,
      functionName: "allowance",
      args: [
        approvalData.userAddress,
        approvalData.spenderAddress,
      ],
    });

    // Check if approval is needed
    if (BigInt(currentAllowance) >= BigInt(approvalData.amount)) {
      return;
    }

    console.log("Insufficient allowance. Approving tokens...");
    const hash = await walletClient.writeContract({
      address: approvalData.tokenAddress,
      abi: erc20Abi,
      functionName: "approve",
      args: [
        approvalData.spenderAddress,
        approvalData.amount,
      ],
    });

    console.log(`Approval transaction sent: ${hash}`);
    const receipt = await publicClient.waitForTransactionReceipt({ hash });
    if (receipt.status === "reverted") {
      throw new Error(`Approval transaction reverted in block ${receipt.blockNumber}`);
    }

    console.log(`Approval confirmed in block ${receipt.blockNumber}`);

    // Wait 1 block after approval before proceeding as submission sometimes is too fast
    const approvalBlock = receipt.blockNumber;
    while (true) {
      const currentBlock = await publicClient.getBlockNumber();
      if (currentBlock > approvalBlock) {
        break;
      }
      await new Promise((resolve) => setTimeout(resolve, 2000));
    }

    return receipt;
  } catch (error) {
    console.error("Error in approval process:", error.message || error);
    throw error;
  }
}

// Function to submit Solana transaction
async function submitSolanaTransaction(txData) {
  try {
    if (!txData || txData.type !== "solana") {
      throw new Error("Invalid Solana transaction data");
    }
    if (!txData.data || !txData.data.instructions) {
      throw new Error("Transaction instructions are required");
    }
    if (!txData.data.lookupTables) {
      throw new Error("Lookup tables are required");
    }

    // Convert instructions to TransactionInstruction[]
    const connection = new Connection(SOLANA_RPC_URL);
    const clientInstructions = txData.data.instructions.map(
      (instruction, index) => {
        try {
          return new TransactionInstruction({
            programId: new PublicKey(instruction.programId),
            keys: instruction.keys.map((key) => ({
              pubkey: new PublicKey(key.pubkey),
              isSigner: key.isSigner,
              isWritable: key.isWritable,
            })),
            data: Buffer.from(instruction.data, "base64"),
          });
        } catch (error) {
          throw new Error(`Failed to parse instruction ${index}: ${error.message}`);
        }
      }
    );

    // Get lookup tables using Solana Connection API
    let clientLookupTables;
    try {
      clientLookupTables = await getAddressLookupTableAccounts(
        txData.data.lookupTables,
        connection
      );
    } catch (error) {
      throw new Error(`Failed to fetch lookup tables: ${error.message}`);
    }

    // Get latest blockhash
    let blockhash, lastValidBlockHeight;
    try {
      const blockhashResult = await connection.getLatestBlockhash("finalized");
      blockhash = blockhashResult.blockhash;
      lastValidBlockHeight = blockhashResult.lastValidBlockHeight;
    } catch (error) {
      throw new Error(`Failed to get latest blockhash: ${error.message}`);
    }

    // Create MessageV0 with lookup tables
    let messageV0;
    try {
      messageV0 = new TransactionMessage({
        payerKey: solanaKeypair.publicKey,
        recentBlockhash: blockhash,
        instructions: clientInstructions,
      }).compileToV0Message(clientLookupTables);
    } catch (error) {
      throw new Error(`Failed to compile transaction message: ${error.message}`);
    }

    // Create versioned transaction
    const transaction = new VersionedTransaction(messageV0);

    // Handle signers if present
    const signers = [solanaKeypair];
    if (txData.data.signers && txData.data.signers.length > 0) {
      try {
        const additionalSigners = txData.data.signers.map((signer) =>
          Keypair.fromSecretKey(Uint8Array.from(signer))
        );
        signers.push(...additionalSigners);
      } catch (error) {
        throw new Error(`Failed to parse signers: ${error.message}`);
      }
    }

    transaction.sign(signers);

    // Send transaction
    let signature;
    try {
      signature = await connection.sendRawTransaction(
        transaction.serialize(),
        {
          skipPreflight: false,
          preflightCommitment: "confirmed",
        }
      );
    } catch (error) {
      throw new Error(`Failed to send transaction: ${error.message}`);
    }

    console.log("- Transaction sent:", signature);

    // Wait for confirmation
    let confirmation;
    try {
      confirmation = await connection.confirmTransaction({
        signature,
        blockhash,
        lastValidBlockHeight,
      });
    } catch (error) {
      throw new Error(`Failed to confirm transaction: ${error.message}`);
    }

    if (confirmation.value.err) {
      throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`);
    }

    console.log("- Transaction confirmed");

    return {
      hash: signature,
      receipt: confirmation,
    };
  } catch (error) {
    console.error("Failed to submit Solana transaction:", error.message || error);
    throw error;
  }
}

// Function to check the status of a request (uses submission transaction hash)
async function checkStatus(submissionTxHash) {
  try {
    if (!submissionTxHash) {
      throw new Error("Submission transaction hash is required");
    }

    const response = await fetch(
      `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${submissionTxHash}`
    );

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();

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

    if (!data.result || !Array.isArray(data.result) || data.result.length === 0) {
      throw new Error("No status result found");
    }

    return data.result[0];
  } catch (error) {
    console.error("Failed to check status:", error.message || error);
    throw error;
  }
}

// Main function to handle the flow
async function main() {
  try {
    console.log("Starting Bungee Solana Auto Swap...");
    console.log(`Direction: ${originChainId === 89999 ? "Solana → EVM" : "EVM → Solana"}`);
    console.log(`EVM Address: ${account.address}`);
    console.log(`Solana Address: ${solanaAddress}`);

    const SOLANA_CHAIN_ID = 89999;
    if (originChainId !== SOLANA_CHAIN_ID && destinationChainId !== SOLANA_CHAIN_ID) {
      throw new Error("One of originChainId or destinationChainId must be 89999 (Solana)");
    }

    console.log("\n1. Getting quote...");
    const quoteResponse = await getQuote(quoteParams);

    if (!quoteResponse.txData) {
      throw new Error("No transaction data available in the quote response");
    }

    const txData = quoteResponse.txData;
    const isSolanaTx = txData.type === "solana";
    let submissionTxHash;

    if (isSolanaTx) {
      // Solana → EVM (no approvals needed)
      console.log("\n2. Submitting Solana transaction...");
      const { hash } = await submitSolanaTransaction(txData);
      submissionTxHash = hash;

      console.log(
        "\n3. Transaction submitted:",
        "\n- Hash:",
        hash
      );
    } else if (txData.type === "evm") {
      // EVM → Solana
      // Check and handle approvals first
      if (quoteResponse.approvalData) {
        console.log("\n2. Checking token approval...");
        await checkAndApproveToken(quoteResponse.approvalData);
      }

      console.log("\n3. Submitting EVM transaction...");
      const { hash, receipt } = await submitEVMTransaction(txData);
      submissionTxHash = hash;

      console.log(
        "\n4. Transaction submitted:",
        "\n- Hash:",
        hash,
        "\n- Status:",
        receipt.status
      );
    } else {
      throw new Error(`Unknown transaction type: ${txData.type}`);
    }

    // Poll status until complete (using submission transaction hash)
    const statusStepNumber = isSolanaTx ? "4" : "5";
    console.log(`\n${statusStepNumber}. Waiting before checking status...`);
    await new Promise((resolve) => setTimeout(resolve, 5000));

    let status;
    let attempts = 0;
    const maxAttempts = 60; // Maximum 10 minutes (60 * 10 seconds)

    do {
      attempts++;
      if (attempts > maxAttempts) {
        throw new Error(`Status check timeout after ${maxAttempts} attempts`);
      }

      await new Promise((resolve) => setTimeout(resolve, 10000));
      const checkStepNumber = isSolanaTx ? "5" : "6";
      console.log(`\n${checkStepNumber}. Checking status (attempt ${attempts})...`);
      try {
        status = await checkStatus(submissionTxHash);
        console.log("- Status code:", status.bungeeStatusCode);
        if (status.bungeeStatusCode === 3) {
          break;
        }
      } catch (error) {
        console.error(
          "Failed to check status:",
          error?.message || "Unknown error"
        );
        // Continue polling even if one check fails
      }
    } while (!status || status.bungeeStatusCode !== 3);

    if (!status) {
      throw new Error("Failed to get final status");
    }

    const completeStepNumber = isSolanaTx ? "6" : "7";
    console.log(
      `\n${completeStepNumber}. Transaction complete:`,
      "\n- Status Code:",
      status.bungeeStatusCode,
      "\n- Destination Hash:",
      status.destinationData?.txHash || "Transaction hash not available"
    );
  } catch (error) {
    console.error("\n❌ Error in processing:", error?.shortMessage || error?.message || error);
    process.exit(1);
  }
}

main();