Skip to main content

Overview

The Inbox Contract integration is required for native tokens and can also be used for ERC20 tokens when:
  • Permit2 is not an option
  • The integration is directly onchain
  • You prefer a fully onchain approach
This method involves:
  1. Getting a quote from the Bungee API
  2. Creating and submitting a request via the inbox contract
  3. Monitoring the status via the API

Integration Steps

Step 1: Get a Quote

For native tokens, request a quote using the address 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE.
API Reference: Quote Endpoint
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";

const quoteParamsNative = {
  userAddress: "0x...",
  originChainId: 10, // Optimism
  destinationChainId: 42161, // Arbitrum
  inputToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH on Optimism
  inputAmount: "100000000000000", // 0.0001 ETH in wei
  receiverAddress: "0x...",
  outputToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH on Arbitrum
};
When integrating Bungee Auto, you can specify the useInbox parameter to directly create a request onchain and bypass permit2 gasless signatures:
  • useInbox: Enables autoRoute.txData for ERC20 tokens and disables autoRoute.signTypedData

Example

/**
 * Get a quote with ERC20 onchain request information
 */
async function getQuoteWithUseInbox() {
  // Set up the parameters for the quote request
  const quoteParams = {
    userAddress: "0xYourUsersAddress",
    originChainId: "1", // Ethereum
    destinationChainId: "10", // Optimism
    inputToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
    outputToken: "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", // USDC on Optimism
    inputAmount: "1000000", // 1 USDC (6 decimals)
    receiverAddress: "0xYourUsersAddress",
    useInbox: true, // Enables autoRoute.txData for ERC20 tokens and disables autoRoute.signTypedData
  };

  // Build the URL with query parameters
  const url = `${BUNGEE_API_BASE_URL}/api/v1/bungee/quote`;
  const queryParams = new URLSearchParams(quoteParams);
  const fullUrl = `${url}?${queryParams}`;

  // Make the request
  const response = await fetch(fullUrl, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });

  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}. server-req-id: ${serverReqId}`
    );
  }

  return data.result;
}
Before submitting a user request to BungeeInbox, check if the request needs approval.You can verify this by checking if the field autoRoute.approvalData contains any data.
  • If approvalData is present and populated, it means approval is required.
  • If approvalData is empty, no approval is needed, and you can submit the request directly.
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);
    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}. server-req-id: ${serverReqId}`
      );
    }

    if (!data.result.autoRoute) {
      throw new Error(`No autoRoute available. server-req-id: ${serverReqId}`);
    }

    // Extract transaction data for native token
    const txData = data.result.autoRoute.txData;
    const requestHash = data.result.autoRoute.requestHash;
    const quoteId = data.result.autoRoute.quoteId;
    const requestType = data.result.autoRoute.requestType;

    console.log("- Quote ID:", quoteId);
    console.log("- Request Type:", requestType);

    // Log request hash
    if (data.result.autoRoute.requestHash) {
      console.log("- Request Hash:", data.result.autoRoute.requestHash);
    }

    return {
      quoteId,
      requestType,
      txData,
      requestHash,
      fullResponse: data,
    };
  } catch (error) {
    console.error("Failed to get quote:", error);
    throw error;
  }
}

Step 2: Submit the Transaction to the Inbox Contract

For native tokens, submit the transaction directly to the inbox contract using the transaction data from the quote.
async function submitNativeTransaction(txData) {
  const hash = await walletClient.sendTransaction({
    to: txData.to,
    value: BigInt(txData.value),
    data: txData.data,
  });

  const receipt = await publicClient.waitForTransactionReceipt({ hash });

  return { hash, receipt };
}

Step 3: Check Request Status

After submitting the request, check its status to track progress. You can implement a polling mechanism to continuously check until completion:
API Reference: Status Endpoint
async function checkStatus(requestHash) {
  const response = await fetch(
    `${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${requestHash}`
  );

  const data = await response.json();

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

  return data.result[0];
}

async function pollForCompletion(
  requestHash,
  interval = 5000,
  maxAttempts = 60
) {
  let attempts = 0;

  while (attempts < maxAttempts) {
    const status = await checkStatus(requestHash);
    const code = status?.bungeeStatusCode;

    if (code === 3 || code === 4) {
      // FULFILLED or SETTLED
      console.log("Transaction complete:", status.destinationData?.txHash);
      return status;
    }
    if (code === 5) {
      throw new Error(`Request expired. Status: ${JSON.stringify(status)}`);
    }
    if (code === 6) {
      throw new Error(`Request cancelled. Status: ${JSON.stringify(status)}`);
    }
    if (code === 7) {
      throw new Error(`Request refunded. Status: ${JSON.stringify(status)}`);
    }

    attempts++;
    await new Promise((resolve) => setTimeout(resolve, interval));
  }

  throw new Error("Polling timed out. Transaction may not have completed.");
}

Complete Integration Example

import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { optimism } from "viem/chains";

// Check if PRIVATE_KEY is set, set in console, i.e.: export PRIVATE_KEY=<YOUR_PRIVATE_KEY>
if (!process.env.PRIVATE_KEY) {
  console.error("Error: PRIVATE_KEY environment variable is not set");
  console.error("Example: export PRIVATE_KEY=<YOUR_PRIVATE_KEY>");
  process.exit(1);
}

// Create account from private key
// Normalize private key to ensure it has exactly one "0x" prefix (required by viem)
const normalizedPrivateKey = `0x${process.env.PRIVATE_KEY.replace(/^0x/i, '')}`;
const account = privateKeyToAccount(normalizedPrivateKey);

const publicClient = createPublicClient({
  chain: optimism,
  transport: http(),
});

const walletClient = createWalletClient({
  account,
  chain: optimism,
  transport: http(),
});

// API and token parameters
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";

// Quote parameters for native token test (ETH from Optimism to Arbitrum)
const quoteParamsNative = {
  userAddress: account.address,
  receiverAddress: account.address,
  originChainId: 10, // Optimism
  destinationChainId: 42161, // Arbitrum
  inputToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH on Optimism
  outputToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // ETH on Arbitrum
  inputAmount: "100000000000000", // 0.0001 ETH in wei
};

async function getQuote(params) {
  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"}`);
  }

  if (!data.result.autoRoute) {
    throw new Error("No autoRoute available for native token transfer");
  }

  return {
    txData: data.result.autoRoute.txData,
    requestHash: data.result.autoRoute.requestHash,
  };
}

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

  const receipt = await publicClient.waitForTransactionReceipt({ hash });
  return { hash, receipt };
}

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

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

  return data.result[0];
}

async function main() {
  console.log("Starting Bungee Native Token Test (Auto Route)...");

  // Get the quote
  console.log("\n1. Getting quote...");
  const { txData, requestHash } = await getQuote(quoteParamsNative);

  if (txData) {
    // Submit the transaction
    console.log("\n2. Submitting transaction...");
    const { hash, receipt } = await submitNativeTransaction(txData);
    console.log("Transaction sent:", hash);

    // Poll for completion
    console.log("\n3. Checking status...");
    let status;
    let attempts = 0;
    const maxAttempts = 60;
    const interval = 5000;

    while (attempts < maxAttempts) {
      await new Promise((resolve) => setTimeout(resolve, interval));
      status = await checkStatus(requestHash);
      const code = status?.bungeeStatusCode;
      console.log("Status:", code);

      if (code === 3 || code === 4) {
        // FULFILLED or SETTLED
        console.log("Transaction complete:", status.destinationData?.txHash);
        break;
      }

      if (code === 5) {
        throw new Error(`Request expired. Status: ${JSON.stringify(status)}`);
      }
      if (code === 6) {
        throw new Error(`Request cancelled. Status: ${JSON.stringify(status)}`);
      }
      if (code === 7) {
        throw new Error(`Request refunded. Status: ${JSON.stringify(status)}`);
      }

      attempts++;
    }

    if (attempts >= maxAttempts) {
      throw new Error(
        `Polling timed out after ${maxAttempts} attempts. Last status: ${JSON.stringify(status)}`
      );
    }
  }
}

main();