Swap across EVM & Solana
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
andreceiverAddress
required for quotes - Fee collection only works for EVM → Solana direction
Special Token Address Handling
- Native Tokens across all Chains (
0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
): - Wrapped SOL (wSOL) (
So11111111111111111111111111111111111111112
):
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.
- Select Chains: Users choose the source and destination chains, which determine the tokens available for bridging.
- Fetch Routes: After selecting the tokens, retrieve possible bridging routes using the
/quote
endpoint. - Token Approval: For ERC-20 tokens, users must grant permission for the protocol to spend their tokens.
- Build Transaction: Obtain the transaction data from the
/build-tx
endpoint. Use this data to execute the bridging transaction on the source chain. - 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
Find the MELANIA token on Solana with user balance
Querying the Solana RPC directly is recommended, notheless the current API supports:
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();
})();