Skip to main content

How to 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 recipient required for quotes
  • Fee collection only works for EVM → Solana direction

Token Support

  • Any supported token can be sent TO Solana
  • Only specific tokens (ETH, USDC, USDT) can be received when bridging TO EVM
Special Token Address Handling
  • Ethereum Native Token (0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee):

    When the fromTokenAddress is set to this address in a quote request, it indicates the user intends to use native ETH (on EVM chains) or native SOL (on Solana). For Solana-specific flows (e.g., Solana → EVM), the Mayan bridge assumes the user has native SOL available. If a swap is required (e.g., SOL to USDC), the bridge instructions will handle this automatically.

  • Wrapped SOL (wSOL) (So11111111111111111111111111111111111111112):

    When the fromTokenAddress is set to this address, it assumes the user holds wSOL (the SPL token version of SOL) and has sufficient SOL to unwrap it if needed. No additional SOL-to-wSOL swap is assumed or performed by the bridge in this case.

Supported chains

Currently, these chains are supported to bridge between EVM and Solana:

  • Ethereum
  • Optimism
  • Arbitrum
  • Polygon
  • Base
  • Avalanche
  • Binance Smart Chain
info

This chain list is for reference only.

Single Transaction Bridging

Bungee enables cross-chain swaps in a single transaction between Solana and EVM chains. But the direction of swaps and support determine which assets are supported:

  • EVM → Solana: Swap any supported token to Solana
  • Solana → EVM: Swap any supported token to specific EVM tokens only:
    • ETH on Ethereum, Optimism, Arbitrum and Base
    • USDC on all supported chains
    • USDT on Binance Smart Chain (BSC)

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.
info

For Solana quotes, please ensure both userAddress and recipient are defined.

Also, remember Solana does not require token approvals.

Examples

Queries

Quote from Base USDC to Solana TRUMP
Find the MELANIA token on Solana with user balance
Check trending tokens on Solana

Scripts

Quote and swap from Base USDC to Solana SOL (Viem)
import { createPublicClient, createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
import dotenv from 'dotenv';
dotenv.config();

// Environment variables
const SOLANA_WALLET_ADDRESS = process.env.SOLANA_WALLET_ADDRESS;
const EVM_WALLET_ADDRESS = process.env.EVM_WALLET_ADDRESS;
const EVM_PRIVATE_KEY = process.env.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)
});

const account = privateKeyToAccount(`0x${EVM_PRIVATE_KEY}`);

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);
return;
}
}

// 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();
})();
Quote and swap from Solana SOL to Polygon USDC (Solana/web3.js)
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();

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 = process.env.SOLANA_WALLET_ADDRESS;
const EVM_WALLET_ADDRESS = process.env.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();
})();