Quick Start
Solana Chain ID on Bungee API
The chain ID for Solana queries is89999.
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
- Both
userAddressandreceiverAddressrequired for quotes - Fee collection only works from EVM to Solana
Special Token Address Handling
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
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.- Select Chains: Users choose the source and destination chains, which determine the tokens available for bridging.
- Fetch Quote: Call the
/quoteendpoint which returns anautoRouteobject containingtxData,requestHash, and optionallyapprovalData. The transaction data is ready to use directly from the quote response. - Handle Token Approval (EVM only): For EVM transactions, check if
approvalDataexists in the quote response. If present, handle token approvals before submitting the transaction. Solana transactions do not require approvals. - Submit Transaction: Execute the transaction using
txDatafrom the quote response. The transaction type (evmorsolana) determines the submission method and required parameters. - Track Transaction Status: Monitor the transaction’s progress by polling the
/statusendpoint using the sourcetxHashuntil 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
Quote from Base USDC to Solana TRUMP
Quote from Base USDC to Solana TRUMP
Copy
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
Find the MELANIA token on Solana with user balance
Find the MELANIA token on Solana with user balance
Querying the Solana RPC directly is recommended, nonetheless the current API supports:
Copy
https://public-backend.bungee.exchange/api/v1/tokens/search?q=FUAfBo2jgks6gB4Z4LfZkqSZgzNucisEHqnNebaRxM1P&userAddress=7BchahMyqpBZYmQS3QnbY3kRweDLgPCRpWdo1rxWmJ3g
Check trending tokens on Solana
Check trending tokens on Solana
Copy
https://public-backend.bungee.exchange/api/v1/tokens/list?chainIds=89999&list=trending
Scripts
Quote and swap from Solana SOL to Base USDC (Viem & Solana/web3.js)
Quote and swap from Solana SOL to Base USDC (Viem & Solana/web3.js)
Copy
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();