> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bungee.exchange/llms.txt
> Use this file to discover all available pages before exploring further.

# Swap across EVM & Solana

> Bridge and swap assets between EVM chains and Solana using Bungee

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

<Accordion title="Special Token Address Handling">
  - **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
</Accordion>

### Supported chains

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

## Integration Steps

The ["Bungee Auto" guide from Bungee Docs](/integrate/integration-guides/auto-onchain-requests) 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.

<Info>
  For Solana quotes, please ensure both `userAddress` and `receiverAddress` are defined.

  Also, remember Solana does not require token approvals.
</Info>

## Examples

### Queries

<Accordion title="Quote from Base USDC to Solana TRUMP">
  ```
  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
  ```
</Accordion>

<Accordion title="Find the MELANIA token on Solana with user balance">
  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
  ```
</Accordion>

<Accordion title="Check trending tokens on Solana">
  ```
  https://public-backend.bungee.exchange/api/v1/tokens/list?chainIds=89999&list=trending
  ```
</Accordion>

### Scripts

<Accordion title="Quote and swap from Solana SOL to Base USDC (Viem & Solana/web3.js)">
  ```tsx theme={null}
  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: destinationChainId === 89999 ? solanaAddress : account.address,
    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...");
      let direction;
      if (originChainId === 89999 && destinationChainId === 89999) {
        direction = "Solana → Solana";
      } else if (originChainId === 89999) {
        direction = "Solana → EVM";
      } else if (destinationChainId === 89999) {
        direction = "EVM → Solana";
      } else {
        direction = "Unknown";
      }
      console.log(`Direction: ${direction}`);
      console.log(`EVM Address: ${account.address}`);
      console.log(`Solana Address: ${solanaAddress}`);

      const SOLANA_CHAIN_ID = 89999;
      const isSameChainSwap = originChainId === destinationChainId;
      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;
      let submissionReceipt;

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

        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;
        submissionReceipt = receipt;

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

      if (isSameChainSwap) {
        const completeStepNumber = isSolanaTx ? "4" : "5";
        console.log(
          `\n${completeStepNumber}. Transaction complete (confirmed on-chain):`,
          "\n- Hash:",
          submissionTxHash,
          "\n- Receipt Status:",
          isSolanaTx ? "confirmed" : submissionReceipt?.status || "unknown"
        );
        return;
      }

      // 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();
  ```
</Accordion>
