Skip to main content
This guide covers how to integrate Bungee Auto for samechain and crosschain swaps using ERC20 tokens as input tokens with Permit2 for gasless approvals.

Overview

The Permit2 integration flow allows users to swap ERC20 tokens across chains without requiring separate approval transactions. This provides a better user experience by:
  1. Eliminating the need for separate approval transactions
  2. Enabling gasless approvals, saving users gas fees
  3. Only transferring tokens if the auction is completed and a transmitter has picked up the request
Smart Contract WalletsIf the wallet supports EIP-1271, you can use the Permit2 signature flow for approvals.Otherwise, use the fallback mechanism by sending the request via the BungeeInbox contract.

Integration Steps

Step 1: Get a Quote

Request a quote for your ERC20 transfer using the actual parameters from the script. This ensures you have all the required data for the next steps.
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";

const quoteParamsERC20 = {
  userAddress: account.address,
  receiverAddress: account.address,
  originChainId: 10, // Optimism
  destinationChainId: 42161, // Arbitrum
  inputToken: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", // USDC on Optimism
  outputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
  inputAmount: "1000000", // 1 USDC (6 decimals)
};

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();
  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}`);
  }

  const quoteId = data.result.autoRoute.quoteId;
  const requestType = data.result.autoRoute.requestType;
  let witness = null;
  let signTypedData = null;
  if (data.result.autoRoute.signTypedData) {
    signTypedData = data.result.autoRoute.signTypedData;
    if (signTypedData.values && signTypedData.values.witness) {
      witness = signTypedData.values.witness;
    }
  }
  const approvalData = data.result.autoRoute.approvalData;
  return {
    quoteId,
    requestType,
    witness,
    signTypedData,
    approvalData,
    fullResponse: data,
  };
}
Quote Expiry HandlingBungee quotes are typically valid for 60 seconds.If the user’s token approval transaction takes significant time to confirm, the quoteId may expire. For the best user experience, consider refreshing the quote after the approval confirmation and before requesting the Permit2 signature.

Step 2: Check and Approve Token (ERC20 Approval Step)

Before signing and submitting the request, you must ensure the permit2 contract has sufficient allowance to spend your tokens. The script checks for allowance and sends an approval transaction if needed. Read more about permit2 here.
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",
    },
  ];

  const currentAllowance = await publicClient.readContract({
    address: approvalData.tokenAddress,
    abi: erc20Abi,
    functionName: "allowance",
    args: [
      approvalData.userAddress,
      approvalData.spenderAddress === "0"
        ? "0x000000000022D473030F116dDEE9F6B43aC78BA3" // Permit2 default
        : approvalData.spenderAddress,
    ],
  });

  if (BigInt(currentAllowance) >= BigInt(approvalData.amount)) {
    console.log("Sufficient allowance already exists.");
    return;
  }

  // Send approval transaction
  const hash = await walletClient.writeContract({
    address: approvalData.tokenAddress,
    abi: erc20Abi,
    functionName: "approve",
    args: [
      approvalData.spenderAddress === "0"
        ? "0x000000000022D473030F116dDEE9F6B43aC78BA3"
        : approvalData.spenderAddress,
      approvalData.amount,
    ],
  });

  console.log(`Approval transaction sent: ${hash}`);
  await publicClient.waitForTransactionReceipt({ hash });
  console.log("Approval confirmed.");
}
Important: approvalData is NOT a Transaction ObjectThe approvalData returned by the API contains raw parameters (tokenAddress, spenderAddress, amount, userAddress).It is not a pre-built transaction object that you can blindly send to a wallet.You must use these parameters to construct an approve() transaction yourself, as shown in the example below.Additionally, the API may return approvalData even if the user already has sufficient allowance. Always check the onchain allowance before prompting the user to approve.Optimization Tip: When approving for Permit2, we recommend approving the maximum amount (MaxUint256). This allows the user to perform gasless signatures for all future swaps without needing another onchain transaction. For other flows (e.g. Manual), use the exact amount specified in approvalData.

Step 3: Sign and Submit the Request

After ensuring approval, sign the typed data and submit the request. Extract the witness object from the quote response at result.autoRoute.signTypedData.values.witness and pass it as the request parameter when submitting the signed request.
async function viemSignTypedData(signTypedData) {
  const signature = await account.signTypedData({
    types: signTypedData.types,
    primaryType: "PermitWitnessTransferFrom",
    message: signTypedData.values,
    domain: signTypedData.domain,
  });
  return signature;
}

async function submitSignedRequest(
  requestType,
  request,
  userSignature,
  quoteId
) {
  const requestBody = {
    requestType,
    request,
    userSignature,
    quoteId,
  };
  const response = await fetch(`${BUNGEE_API_BASE_URL}/api/v1/bungee/submit`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(requestBody),
  });
  const data = await response.json();
  if (!data.success) {
    throw new Error(`Submit error: ${data.error?.message || "Unknown error"}`);
  }
  return data.result;
}

Step 4: Check Request Status

After submitting the request, check its status to track progress. You can implement a polling mechanism to continuously check until completion:
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 { privateKeyToAccount } from "viem/accounts";
import { createPublicClient, http, createWalletClient } from "viem";
import { optimism } from "viem/chains";

// Check if PRIVATE_KEY is set
if (!process.env.PRIVATE_KEY) {
  console.error("Error: PRIVATE_KEY environment variable is not set");
  process.exit(1);
}

// Create account from private key
const normalizedPrivateKey = process.env.PRIVATE_KEY.startsWith('0x') 
  ? process.env.PRIVATE_KEY 
  : `0x${process.env.PRIVATE_KEY}`;
const account = privateKeyToAccount(normalizedPrivateKey);

// Create Viem clients
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 ERC20 token test (USDC from Optimism to Arbitrum)
const quoteParamsERC20 = {
  userAddress: account.address,
  receiverAddress: account.address,
  originChainId: 10, // Optimism
  destinationChainId: 42161, // Arbitrum
  inputToken: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", // USDC on Optimism
  outputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on Arbitrum
  inputAmount: "1000000", // 1 USDC (6 decimals)
};

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();
  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}`);
  }

  const quoteId = data.result.autoRoute.quoteId;
  const requestType = data.result.autoRoute.requestType;
  let witness = null;
  let signTypedData = null;

  if (data.result.autoRoute.signTypedData) {
    signTypedData = data.result.autoRoute.signTypedData;
    if (signTypedData?.values?.witness) {
      witness = signTypedData.values.witness;
    }
  }

  return {
    quoteId,
    requestType,
    witness,
    signTypedData,
    approvalData: data.result.autoRoute.approvalData,
    fullResponse: data,
  };
}

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",
    },
  ];

  const currentAllowance = await publicClient.readContract({
    address: approvalData.tokenAddress,
    abi: erc20Abi,
    functionName: "allowance",
    args: [
      approvalData.userAddress,
      approvalData.spenderAddress === "0"
        ? "0x000000000022D473030F116dDEE9F6B43aC78BA3" // Permit2 default
        : approvalData.spenderAddress,
    ],
  });

  if (BigInt(currentAllowance) >= BigInt(approvalData.amount)) {
    console.log("Sufficient allowance already exists.");
    return;
  }

  // Send approval transaction
  const hash = await walletClient.writeContract({
    address: approvalData.tokenAddress,
    abi: erc20Abi,
    functionName: "approve",
    args: [
      approvalData.spenderAddress === "0"
        ? "0x000000000022D473030F116dDEE9F6B43aC78BA3"
        : approvalData.spenderAddress,
      approvalData.amount,
    ],
  });

  console.log(`Approval transaction sent: ${hash}`);
  await publicClient.waitForTransactionReceipt({ hash });
  console.log("Approval confirmed.");
}

async function viemSignTypedData(signTypedData) {
  const signature = await account.signTypedData({
    types: signTypedData.types,
    primaryType: "PermitWitnessTransferFrom",
    message: signTypedData.values,
    domain: signTypedData.domain,
  });
  return signature;
}

async function submitSignedRequest(requestType, request, userSignature, quoteId) {
  const requestBody = { requestType, request, userSignature, quoteId };
  const response = await fetch(`${BUNGEE_API_BASE_URL}/api/v1/bungee/submit`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(requestBody),
  });
  const data = await response.json();

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

  return data.result;
}

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)}`);
    }
    console.log("Status:", code);
    attempts++;
    await new Promise((resolve) => setTimeout(resolve, interval));
  }
  throw new Error("Polling timed out. Transaction may not have completed.");
}

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

  // Get the quote
  console.log("\n1. Getting quote...");
  const { quoteId, requestType, witness, signTypedData, approvalData } =
    await getQuote(quoteParamsERC20);

  // Handle approval if needed
  if (approvalData) {
    await checkAndApproveToken(approvalData);
  }

  if (signTypedData && witness) {
    // Sign the typed data
    console.log("\n2. Signing typed data...");
    const signature = await viemSignTypedData(signTypedData);

    // Submit the signed request
    console.log("\n3. Submitting signed request...");
    const submitResult = await submitSignedRequest(
      requestType,
      witness,
      signature,
      quoteId
    );
    console.log("Request Hash:", submitResult.requestHash);

    // Poll for completion
    console.log("\n4. Checking status...");
    const status = await pollForCompletion(submitResult.requestHash);
  } else {
    console.error("Missing required data for signing and submission:");
    console.error("- signTypedData:", signTypedData ? "present" : "missing");
    console.error("- witness:", witness ? "present" : "missing");
    console.error("- quoteId:", quoteId);
    throw new Error("Cannot proceed: signTypedData and witness are required for submitting signed requests. Check your quote response.");
  }
}

main();