Skip to main content
This page covers how to integrate the Bungee Deposit flow using the quote and status endpoints. The flow is:
  1. Fetch a quote with deposit mode enabled (enableDepositAddress=true).
  2. Read result.deposit from the quote response.
  3. Use either:
  • deposit.txData for direct programmatic execution, or
  • deposit.depositData to display transfer details for user-driven submission.
  1. Poll /status using deposit.requestHash until terminal status.
Currently, the deposit flow is supported on all EVM chains, Solana, Tron and Stellar.
  • Tron supports direct deposits from USDT0 OFT chains to USDT on Tron. No other tokens or cross-chain swaps are supported at this time.
  • Stellar supports Base USDC deposits to and from USDC on Stellar. No other tokens or cross-chain swaps are supported at this time.

Integration Steps

Step 1: Get a Quote in Deposit Mode

Use the quote endpoint with enableDepositAddress=true and refundAddress=USER_ADDRESS. The userAddress is optional for the deposit flow. The depositDestinationMemo may be applicable for Stellar transactions.
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";

const quoteParams = {
  originChainId: "8453",
  destinationChainId: "42161",
  inputAmount: "2000000",
  inputToken: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  outputToken: "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
  userAddress: USER_ADDRESS,  // Optional for the deposit flow
  receiverAddress: RECEIVER_ADDRESS,
  refundAddress: USER_ADDRESS, // Required when `enableDepositAddress` is `true`
  enableDepositAddress: "true",
  disableAuto: "true",
};

async function getDepositQuote(params) {
  const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote?${new URLSearchParams(params)}`;
  const response = await fetch(url);

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

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

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

  const deposit = data?.result?.deposit;
  if (!deposit?.txData || !deposit?.requestHash) {
    throw new Error(`Missing deposit txData/requestHash. server-req-id: ${serverReqId}`);
  }

  return {
    requestHash: deposit.requestHash,
    txData: deposit.txData,
    userOp: deposit.userOp,
  };
}
result.deposit.requestHash is the identifier you should use with the status endpoint.

Step 2: Submit deposit.txData

Use deposit.txData.type to select the transaction submission method for the origin chain. Example below shows EVM submission with viem. In addition to executing with deposit.txData, you can also use deposit.depositData from the quote response to display the direct transfer details (recipient address, token, amount, chainId, memo if present) so users can submit the transfer manually via a QR code UI.
For Stellar deposits, when transferring USDC for the first time from a wallet, ensure the wallet has a USDC trustline before submitting the transaction.
async function submitEvmTransaction(txData, originChainId, privateKey) {
  if (!txData?.to) {
    throw new Error("Transaction 'to' is required");
  }

  const account = privateKeyToAccount(privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`);
  const walletClient = createWalletClient({
    chain,
    account,
    transport: http(),
  });
  const publicClient = createPublicClient({
    chain,
    transport: http(),
  });

  const hash = await walletClient.sendTransaction({
    to: txData.to,
    data: txData.data,
    value: BigInt(txData.value ?? "0"),
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (!receipt || receipt.status !== "success") {
    throw new Error(`Transaction failed: ${hash}`);
  }

  return { hash, receipt };
}

Step 3: Poll Status with requestHash

After transaction submission, poll the status endpoint using the request hash returned in step 1.
async function checkStatus(requestHash) {
  const response = await fetch(
    `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${requestHash}`
  );

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

  const data = await response.json();
  if (!data.success) {
    throw new Error(
      `Status error: ${data.error?.message || data.message || "Unknown error"}`
    );
  }
  if (!Array.isArray(data.result) || data.result.length === 0) {
    throw new Error("No status result found");
  }

  return data.result[0];
}

async function pollDepositStatus(requestHash, intervalMs = 10000, maxAttempts = 60) {
  let attempts = 0;

  while (attempts < maxAttempts) {
    attempts += 1;
    await new Promise((resolve) => setTimeout(resolve, intervalMs));

    const status = await checkStatus(requestHash);
    const code = status?.bungeeStatusCode;

    if (code === 3 || code === 4) return status;
    if (code === 5 || code === 6 || code === 7) {
      throw new Error(`Request failed with status code ${code}`);
    }
  }

  throw new Error("Status polling timed out");
}
For status code details, see Request status codes.

Examples

Queries

https://public-backend.bungee.exchange/api/v1/bungee/quote?originChainId=8453&destinationChainId=1110002&inputAmount=20000000&inputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&outputToken=USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN&refundAddress=0x664b591aB924C6bb2CacA533ed702386934A11d6&receiverAddress=GAEQYFK3GOLJO7OR24DMBEAGVQ7TRPQXXMSRGLI4BQVZNH6SJT7PTBRC&enableDepositAddress=true&depositDestinationMemo=3063557953
https://public-backend.bungee.exchange/api/v1/bungee/quote?originChainId=1110002&destinationChainId=8453&inputAmount=20000000&inputToken=USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN&receiverAddress=0x7B346f63e6F8f663CBEC6d526bA762B42A1Fdd7A&outputToken=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&enableDepositAddress=true&refundAddress=0x7B346f63e6F8f663CBEC6d526bA762B42A1Fdd7A&depositDestinationMemo=3063557953&disableAuto=true

Scripts

import dotenv from "dotenv";
import { TronWeb } from "tronweb";
import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base, arbitrum, optimism } from "viem/chains";
import * as StellarSdk from "@stellar/stellar-sdk";
import StellarHDWallet from "stellar-hd-wallet";
import {
  Keypair,
  Connection,
  Transaction as SolanaTransaction,
  VersionedTransaction,
} from "@solana/web3.js";
import bs58 from "bs58";

dotenv.config();

const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";
const TRON_FULL_HOST = "https://api.trongrid.io";
const SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com";
const STELLAR_HORIZON_URL = "https://horizon.stellar.org";

const SOLANA_CHAIN_ID = 89999;

// Tron only accepts direct deposit from USDT0 OFT chains to USDT on Tron
// No other tokens or source chain swaps are supported at the moment
const TRON_CHAIN_ID = 728126428;
const TRON_USDT_CONTRACT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";

// Stellar only supports Base USDC from/to USDC on Stellar at the moment
// No other tokens or source chain swaps are supported at the moment
const STELLAR_CHAIN_ID = 1110002;
const STELLAR_USDC_ISSUER = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN";
const STELLAR_USDC_ASSET = `USDC-${STELLAR_USDC_ISSUER}`;

let cachedEvmAccount = undefined;
let cachedTronContext = undefined;
let cachedSolanaContext = undefined;
let cachedStellarContextPromise = null;

// Parameters to edit for each deposit
const originChainId = <ADD_CHAIN_ID_HERE>;
const destinationChainId = <ADD_CHAIN_ID_HERE>;
const inputAmount = <ADD_AMOUNT_HERE>;
const inputToken = <ADD_TOKEN_ADDRESS_HERE>;
const outputToken = <ADD_TOKEN_ADDRESS_HERE>;

function sanitizePrivateKey(value) {
  if (!value) return null;
  return value.startsWith("0x") ? value : `0x${value}`;
}

function parseSolanaKeypair(value) {
  if (!value) return null;

  let privateKeyBytes;
  try {
    privateKeyBytes = bs58.decode(value);
  } catch {
    try {
      privateKeyBytes = Uint8Array.from(JSON.parse(value));
    } catch {
      privateKeyBytes = Uint8Array.from(value.split(",").map(Number));
    }
  }

  return Keypair.fromSecretKey(privateKeyBytes);
}

function getEvmAccount() {
  if (cachedEvmAccount !== undefined) {
    return cachedEvmAccount;
  }

  const evmPrivateKey = sanitizePrivateKey(process.env.PRIVATE_KEY?.trim());
  cachedEvmAccount = evmPrivateKey ? privateKeyToAccount(evmPrivateKey) : null;
  return cachedEvmAccount;
}

function getTronContext() {
  if (cachedTronContext !== undefined) {
    return cachedTronContext;
  }

  const tronPrivateKey = process.env.TRON_PRIVATE_KEY?.replace(/^0x/, "");
  if (!tronPrivateKey) {
    cachedTronContext = null;
    return cachedTronContext;
  }

  const tronWeb = new TronWeb({
    fullHost: TRON_FULL_HOST,
    privateKey: tronPrivateKey,
  });
  const tronAddress = tronWeb.address.fromPrivateKey(tronPrivateKey);

  cachedTronContext = {
    tronWeb,
    tronAddress,
  };

  return cachedTronContext;
}

function getSolanaContext() {
  if (cachedSolanaContext !== undefined) {
    return cachedSolanaContext;
  }

  const solanaPrivateKey = process.env.SOLANA_PRIVATE_KEY;
  if (!solanaPrivateKey) {
    cachedSolanaContext = null;
    return cachedSolanaContext;
  }

  try {
    const solanaKeypair = parseSolanaKeypair(solanaPrivateKey);
    cachedSolanaContext = {
      solanaKeypair,
      solanaAddress: solanaKeypair.publicKey.toBase58(),
    };
  } catch (error) {
    throw new Error(
      `Failed to parse SOLANA_PRIVATE_KEY. Use base58 or array/comma format. ${error.message}`
    );
  }

  return cachedSolanaContext;
}

async function addStellarUsdcTrustline(account, signer, server, networkPassphrase) {
  console.log("\nNo USDC trustline found, adding trustline...");

  const transaction = new StellarSdk.TransactionBuilder(account, {
    fee: StellarSdk.BASE_FEE,
    networkPassphrase,
  })
    .addOperation(
      StellarSdk.Operation.changeTrust({
        asset: new StellarSdk.Asset("USDC", STELLAR_USDC_ISSUER),
      })
    )
    .setTimeout(30)
    .build();

  transaction.sign(signer);
  const result = await server.submitTransaction(transaction);
  console.log("Trustline added! Hash:", result.hash);
}

async function loadStellarWalletContext() {
  if (!process.env.STELLAR_SEED_PHRASE) {
    throw new Error(
      "STELLAR_SEED_PHRASE is required when Stellar chain is used"
    );
  }

  let wallet;
  try {
    wallet = StellarHDWallet.fromMnemonic(process.env.STELLAR_SEED_PHRASE);
  } catch (error) {
    throw new Error(`Failed to parse STELLAR_SEED_PHRASE: ${error.message}`);
  }

  const secretKey = wallet.getSecret(0);
  const walletAddress = wallet.getPublicKey(0);
  const signer = StellarSdk.Keypair.fromSecret(secretKey);
  const server = new StellarSdk.Horizon.Server(STELLAR_HORIZON_URL);
  const networkPassphrase = StellarSdk.Networks.PUBLIC;

  let account;
  try {
    account = await server.loadAccount(walletAddress);
  } catch (error) {
    throw new Error(`Failed to load Stellar account ${walletAddress}: ${error.message}`);
  }

  const xlm = account.balances.find((b) => b.asset_type === "native");
  const xlmBalance = Number(xlm?.balance ?? "0");

  console.log("Stellar Address:    ", walletAddress);
  console.log("Stellar XLM Balance:", xlm ? xlm.balance : "0");

  if (!Number.isFinite(xlmBalance) || xlmBalance <= 0) {
    throw new Error("No XLM balance in Stellar wallet");
  }

  let hasUsdcTrustline = account.balances.find(
    (b) => b.asset_code === "USDC" && b.asset_issuer === STELLAR_USDC_ISSUER
  );

  console.log("Stellar USDC Trustline:", hasUsdcTrustline ? "YES" : "NO");

  if (!hasUsdcTrustline) {
    await addStellarUsdcTrustline(account, signer, server, networkPassphrase);
    const updatedAccount = await server.loadAccount(walletAddress);

    hasUsdcTrustline = updatedAccount.balances.find(
      (b) => b.asset_code === "USDC" && b.asset_issuer === STELLAR_USDC_ISSUER
    );

    if (!hasUsdcTrustline) {
      throw new Error("Failed to create USDC trustline on Stellar wallet");
    }
  }

  return {
    walletAddress,
    signer,
    server,
    networkPassphrase,
  };
}

async function getStellarWalletContext() {
  if (!cachedStellarContextPromise) {
    cachedStellarContextPromise = loadStellarWalletContext();
  }

  return cachedStellarContextPromise;
}

// REMOVE THIS OR EXPAND IF YOU ADD MORE CHAIN SUPPORT
// MERELY SETTING THIS FOR EXAMPLE PURPOSES
function getEvmChain(chainId) {
  const numericChainId = Number(chainId);

  if (numericChainId === base.id) return base;
  if (numericChainId === arbitrum.id) return arbitrum;
  if (numericChainId === optimism.id) return optimism;

  throw new Error(
    `Unsupported EVM chain ${numericChainId}. Add it to getEvmChain before submitting EVM txs.`
  );
}

function applyStellarAssetConstraint({
  originChainId,
  destinationChainId,
  inputToken,
  outputToken,
}) {
  let resolvedInputToken = inputToken;
  let resolvedOutputToken = outputToken;

  if (Number(originChainId) === STELLAR_CHAIN_ID && inputToken !== STELLAR_USDC_ASSET) {
    console.log(
      `Overriding inputToken to ${STELLAR_USDC_ASSET} because originChainId is ${STELLAR_CHAIN_ID}`
    );
    resolvedInputToken = STELLAR_USDC_ASSET;
  }

  if (
    Number(destinationChainId) === STELLAR_CHAIN_ID &&
    outputToken !== STELLAR_USDC_ASSET
  ) {
    console.log(
      `Overriding outputToken to ${STELLAR_USDC_ASSET} because destinationChainId is ${STELLAR_CHAIN_ID}`
    );
    resolvedOutputToken = STELLAR_USDC_ASSET;
  }

  return {
    inputToken: resolvedInputToken,
    outputToken: resolvedOutputToken,
  };
}

async function getWalletAddressForChain(chainId) {
  if (Number(chainId) === SOLANA_CHAIN_ID) {
    const solanaContext = getSolanaContext();
    if (!solanaContext?.solanaAddress) {
      throw new Error("SOLANA_PRIVATE_KEY is required when Solana chain is used");
    }
    return solanaContext.solanaAddress;
  }

  if (Number(chainId) === TRON_CHAIN_ID) {
    const tronContext = getTronContext();
    if (!tronContext?.tronAddress) {
      throw new Error("TRON_PRIVATE_KEY is required when Tron chain is used");
    }
    return tronContext.tronAddress;
  }

  if (Number(chainId) === STELLAR_CHAIN_ID) {
    const stellarContext = await getStellarWalletContext();
    return stellarContext.walletAddress;
  }

  const evmAccount = getEvmAccount();
  if (!evmAccount) {
    throw new Error("PRIVATE_KEY is required when an EVM chain is used");
  }
  return evmAccount.address;
}

async function getQuote(params) {
  const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
  const fullUrl = `${url}?${new URLSearchParams(params)}`;

  const response = await fetch(fullUrl);
  console.log(fullUrl);

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

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

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

  const deposit = data?.result?.deposit;
  if (!deposit) {
    throw new Error(`No deposit available in quote response. server-req-id: ${serverReqId}`);
  }

  if (!deposit.txData) {
    throw new Error(`deposit.txData is missing. server-req-id: ${serverReqId}`);
  }

  if (!deposit.requestHash) {
    throw new Error(`deposit.requestHash is missing. server-req-id: ${serverReqId}`);
  }

  console.log("- Request Hash:", deposit.requestHash);
  console.log("- User Op:", deposit.userOp || "N/A");
  console.log("- Transaction Type:", deposit.txData.type || "unknown");

  return {
    requestHash: deposit.requestHash,
    txData: deposit.txData,
    fullResponse: data,
  };
}

async function submitEvmTransaction(txData) {
  const evmAccount = getEvmAccount();
  if (!evmAccount) {
    throw new Error("PRIVATE_KEY is required for evm txData.type");
  }
  if (!txData.to) {
    throw new Error("EVM transaction 'to' is required");
  }
  const chain = getEvmChain(originChainId);

  const walletClient = createWalletClient({
    chain,
    account: evmAccount,
    transport: http(),
  });
  const publicClient = createPublicClient({
    chain,
    transport: http(),
  });

  console.log("  To:", txData.to);
  console.log("  Value:", txData.value);
  console.log("  Data:", txData.data?.slice(0, 66) ? `${txData.data.slice(0, 66)}...` : "N/A");

  const hash = await walletClient.sendTransaction({
    to: txData.to,
    data: txData.data,
    value: BigInt(txData.value),
  });

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

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  if (!receipt || receipt.status !== "success") {
    throw new Error(`EVM transaction failed: ${hash}`);
  }

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

async function submitTronTransaction(txData) {
  const tronContext = getTronContext();
  if (!tronContext?.tronWeb || !tronContext?.tronAddress) {
    throw new Error("TRON_PRIVATE_KEY is required for tron txData.type");
  }
  const { tronWeb, tronAddress } = tronContext;
  if (!txData.to) {
    throw new Error("Tron transaction 'to' is required");
  }
  if (!txData.data) {
    throw new Error("Tron transaction 'data' is required");
  }

  console.log("  To:", txData.to);
  console.log("  Value:", txData.value);
  console.log("  Data:", txData.data);

  const tronTo =
    typeof txData.to === "string" && txData.to.startsWith("T")
      ? txData.to
      : tronWeb.address.fromHex(txData.to);

  const transaction = await tronWeb.transactionBuilder.triggerSmartContract(
    tronTo,
    "",
    {
      input: txData.data.replace(/^0x/, ""),
      callValue: Number(txData.value),
      feeLimit: Number(100000000),
    },
    [],
    tronAddress
  );

  const signed = await tronWeb.trx.sign(transaction.transaction);
  const result = await tronWeb.trx.sendRawTransaction(signed);

  if (!result.result) {
    throw new Error(`Tron transaction failed: ${JSON.stringify(result)}`);
  }

  const hash = result.txid;
  console.log("- Transaction sent:", hash);

  let receipt;
  while (!receipt) {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    try {
      const txInfo = await tronWeb.trx.getTransactionInfo(hash);
      if (txInfo?.blockNumber) {
        receipt = {
          blockNumber: txInfo.blockNumber,
          status: txInfo.receipt ? "success" : "failed",
          transactionHash: hash,
        };
      }
    } catch {
      // Keep polling until the receipt is available.
    }
  }

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

async function submitSolanaTransaction(txData) {
  const solanaContext = getSolanaContext();
  const solanaKeypair = solanaContext?.solanaKeypair;
  if (!solanaKeypair) {
    throw new Error("SOLANA_PRIVATE_KEY is required for solana txData.type");
  }

  const connection = new Connection(SOLANA_RPC_URL);

  const serializedPayload =
    typeof txData?.data === "string"
      ? txData.data
      : typeof txData?.data?.serializedTx === "string"
        ? txData.data.serializedTx
        : typeof txData?.data?.transaction === "string"
          ? txData.data.transaction
          : typeof txData?.data?.tx === "string"
            ? txData.data.tx
            : null;
  if (!serializedPayload) {
    throw new Error("Solana txData.data must be a serialized transaction payload");
  }

  const normalizedPayload = serializedPayload
    .trim()
    .replace(/\s+/g, "")
    .replace(/-/g, "+")
    .replace(/_/g, "/");
  const base64Padded =
    normalizedPayload +
    "=".repeat((4 - (normalizedPayload.length % 4 || 4)) % 4);
  const serializedTxBytes = Buffer.from(base64Padded, "base64");

  if (!serializedTxBytes.length) {
    throw new Error("Failed to decode serialized Solana txData.data payload");
  }

  console.log("  Serialized Solana payload detected");
  console.log(
    "  Data:",
    serializedPayload.length > 66 ? `${serializedPayload.slice(0, 66)}...` : serializedPayload
  );

  let wireTxBuffer;
  try {
    const versionedTx = VersionedTransaction.deserialize(serializedTxBytes);
    versionedTx.sign([solanaKeypair]);
    wireTxBuffer = versionedTx.serialize();
  } catch (versionedError) {
    try {
      const legacyTx = SolanaTransaction.from(serializedTxBytes);
      legacyTx.sign(solanaKeypair);
      wireTxBuffer = legacyTx.serialize();
    } catch (legacyError) {
      throw new Error(
        `Failed to decode serialized Solana tx payload as versioned or legacy transaction. ` +
        `versionedError=${versionedError.message}; legacyError=${legacyError.message}`
      );
    }
  }

  const signature = await connection.sendRawTransaction(wireTxBuffer, {
    skipPreflight: false,
    preflightCommitment: "confirmed",
  });

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

  const confirmation = await connection.confirmTransaction(signature, "confirmed");
  if (confirmation.value.err) {
    throw new Error(`Solana transaction failed: ${JSON.stringify(confirmation.value.err)}`);
  }

  console.log("- Transaction confirmed");
  return { hash: signature, receipt: confirmation };
}

function getStellarNetworkPassphrase(txData) {
  return (
    txData?.networkPassphrase ||
    txData?.data?.networkPassphrase ||
    txData?.meta?.networkPassphrase ||
    StellarSdk.Networks.PUBLIC
  );
}

function parseStellarMemo(memoValue) {
  if (memoValue === undefined || memoValue === null || memoValue === "") {
    return null;
  }

  const memoString = String(memoValue).trim();
  if (!memoString) {
    return null;
  }

  if (/^\d+$/.test(memoString)) {
    return StellarSdk.Memo.id(memoString);
  }

  return StellarSdk.Memo.text(memoString);
}

function parseStellarAsset(assetPayload) {
  if (!assetPayload) {
    throw new Error("Missing Stellar operation.asset payload");
  }

  const assetCode = assetPayload.code || assetPayload.assetCode;
  const assetIssuer = assetPayload.issuer || assetPayload.assetIssuer;

  if (!assetCode) {
    throw new Error("Missing Stellar asset code");
  }

  if (assetCode.toUpperCase() === "XLM") {
    return StellarSdk.Asset.native();
  }

  if (!assetIssuer) {
    throw new Error(`Missing issuer for Stellar asset ${assetCode}`);
  }

  return new StellarSdk.Asset(assetCode, assetIssuer);
}

async function buildStellarTransactionFromOperation({
  txData,
  signer,
  server,
  networkPassphrase,
}) {
  const operation = txData?.operation;
  if (!operation) {
    throw new Error("Missing txData.operation for stellar transaction");
  }

  if (!operation.destination) {
    throw new Error("Missing txData.operation.destination for stellar transaction");
  }

  if (!operation.amount) {
    throw new Error("Missing txData.operation.amount for stellar transaction");
  }

  const sourceAccount = await server.loadAccount(signer.publicKey());
  const asset = parseStellarAsset(operation.asset);
  const memo = parseStellarMemo(txData?.memo);

  const builder = new StellarSdk.TransactionBuilder(sourceAccount, {
    fee: StellarSdk.BASE_FEE,
    networkPassphrase,
  }).addOperation(
    StellarSdk.Operation.payment({
      destination: operation.destination,
      asset,
      amount: String(operation.amount),
    })
  );

  if (memo) {
    builder.addMemo(memo);
  }

  return builder.setTimeout(30).build();
}

function extractStellarXdr(txData) {
  const candidates = [
    txData?.xdr,
    txData?.transactionXdr,
    txData?.envelopeXdr,
    txData?.unsignedXdr,
    txData?.data,
    txData?.data?.xdr,
    txData?.data?.transactionXdr,
    txData?.data?.envelopeXdr,
    txData?.data?.unsignedXdr,
    txData?.data?.transaction?.xdr,
    txData?.rawTransaction,
  ];

  for (const candidate of candidates) {
    if (typeof candidate === "string" && candidate.trim().length > 0) {
      return candidate.trim();
    }
  }

  const queue = [txData];
  while (queue.length > 0) {
    const current = queue.shift();
    if (!current || typeof current !== "object") {
      continue;
    }

    for (const [key, value] of Object.entries(current)) {
      if (
        typeof value === "string" &&
        value.trim().length > 0 &&
        /xdr|envelope/i.test(key)
      ) {
        return value.trim();
      }
      if (value && typeof value === "object") {
        queue.push(value);
      }
    }
  }

  throw new Error(
    "No Stellar XDR found in txData. Expected xdr/transactionXdr/envelopeXdr field."
  );
}

async function submitStellarTransaction(txData) {
  const { signer, server } = await getStellarWalletContext();
  const networkPassphrase = getStellarNetworkPassphrase(txData);

  let transaction;
  try {
    const xdr = extractStellarXdr(txData);
    transaction = StellarSdk.TransactionBuilder.fromXDR(xdr, networkPassphrase);
  } catch {
    transaction = await buildStellarTransactionFromOperation({
      txData,
      signer,
      server,
      networkPassphrase,
    });
  }

  transaction.sign(signer);

  const result = await server.submitTransaction(transaction);
  console.log("- Transaction sent:", result.hash);

  return {
    hash: result.hash,
    receipt: result,
  };
}

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

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

  const data = await response.json();

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

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

  return data.result[0];
}

async function main() {
  try {
    const constrainedAssets = applyStellarAssetConstraint({
      originChainId,
      destinationChainId,
      inputToken,
      outputToken,
    });
    const originWalletAddress = await getWalletAddressForChain(originChainId);
    const destinationWalletAddress = await getWalletAddressForChain(destinationChainId);
    const quoteParams = {
      originChainId: String(originChainId),
      destinationChainId: String(destinationChainId),
      inputAmount,
      inputToken: constrainedAssets.inputToken,
      outputToken: constrainedAssets.outputToken,
      userAddress: originWalletAddress,
      receiverAddress: destinationWalletAddress,
      refundAddress: originWalletAddress,
      enableDepositAddress: "true",
      disableAuto: "true",
    };

    console.log("Starting Bungee Deposit flow...");
    console.log(`Origin Chain: ${originChainId}`);
    console.log(`Destination Chain: ${destinationChainId}`);
    console.log(`User Address: ${quoteParams.userAddress}`);
    console.log(`Receiver Address: ${quoteParams.receiverAddress}`);

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

    const txData = quoteResponse.txData;
    if (!txData || !txData.type) {
      throw new Error("No txData.type available in deposit response");
    }

    let submission;

    if (txData.type === "evm") {
      console.log("\n2. Submitting EVM transaction...");
      submission = await submitEvmTransaction(txData);
    } else if (txData.type === "tron") {
      console.log("\n2. Submitting Tron transaction...");
      submission = await submitTronTransaction(txData);
    } else if (txData.type === "solana") {
      console.log("\n2. Submitting Solana transaction...");
      submission = await submitSolanaTransaction(txData);
    } else if (txData.type === "stellar") {
      console.log("\n2. Submitting Stellar transaction...");
      submission = await submitStellarTransaction(txData);
    } else {
      throw new Error(`Unknown transaction type: ${txData.type}`);
    }

    console.log("\n3. Transaction submitted:");
    console.log("- Hash:", submission.hash);

    const statusRequestHash = quoteResponse.requestHash;
    const waitTime = 10000; // 10 seconds
    console.log(`\n4. Waiting ${waitTime / 2}ms before status polling...`);
    await new Promise((resolve) => setTimeout(resolve, waitTime / 2));

    let status;
    let attempts = 0;
    const maxAttempts = 100;

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

      await new Promise((resolve) => setTimeout(resolve, waitTime));
      console.log(`\n5. Checking status (attempt ${attempts})...`);

      try {
        status = await checkStatus(statusRequestHash);
        console.log("- Status code:", status.bungeeStatusCode);
      } catch (error) {
        console.error("- Status check failed:", error?.message || error);
      }
    } while (!status || status.bungeeStatusCode !== 3);

    console.log("\n6. Transaction complete:");
    console.log("- Status Code:", status.bungeeStatusCode);
    console.log(
      "- Destination Hash:",
      status.destinationData?.txHash || "Transaction hash not available"
    );
  } catch (error) {
    console.error("\nError in processing:", error?.shortMessage || error?.message || error);
    process.exit(1);
  }
}

main();

Edge Cases and Best Practices

  • Always persist requestHash immediately after quote generation.
  • Provide refundAddress explicitly so refunds can be directed deterministically.
  • Validate chain, token, destination receiver, and memo before showing the deposit instructions.
  • Treat the source transfer tx hash and requestHash as separate objects; use requestHash for Bungee status checks.
  • For Stellar as destination, ensure the receiver wallet has a USDC trustline before first USDC transfer.
  • Surface terminal failure states in UX (EXPIRED, CANCELLED, REFUNDED) and show retry guidance.

Debugging Checklist

  1. Store requestHash and server-req-id from quote/status calls.
  2. Confirm the submitted transfer matches depositData (address, token, amount, and memo if present).
  3. Poll /status with requestHash until terminal state.
  4. If unresolved, share requestHash and server-req-id with support.

Failure Cases API Behavior

Status codes: 5 = EXPIRED, 6 = CANCELLED, 7 = REFUNDED
Failure ModeCategoryBungee Status CodeAPI Behavior
Wrong token depositedUser error5 (EXPIRED)Quote expires, deposit not recognized
Deposited on wrong chainUser error5 (EXPIRED)Quote expires, funds on wrong chain are not detected
Less amount depositedUser error5 (EXPIRED)Deposit is detected, but no execution happens
More amount depositedUser errorSUCCESSRe-quote with actual amount and suggested slippage is applied
Balance monitoring expired, no depositTiming5 (EXPIRED)Address released
Slippage exceeds toleranceMarketSTAYS PENDINGRefund is needed