Bungee Auto (ERC20)
This guide covers how to integrate Bungee Auto for same-chain and cross-chain 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:
- Eliminating the need for separate approval transactions
- Enabling gasless approvals, saving users gas fees
- Only transferring tokens if the auction is completed and a transmitter has picked up the request
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.
API Reference: Quote Endpoint
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,
};
}
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.");
}
Step 3: Sign and Submit the Request
After ensuring approval, sign the typed data and submit the request. Use the actual function and parameter names from the script.
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:
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) {
console.log("Transaction complete:", status.destinationData?.txHash);
return status;
}
attempts++;
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error("Polling timed out. Transaction may not have completed.");
}
Complete Integration Example
Complete Integration Using Viem (Full Script)
import { privateKeyToAccount } from "viem/accounts";
import { createPublicClient, http, createWalletClient } from "viem";
import { optimism } from "viem/chains";
import { logToJson } from "../utils/logger.js";
import path from "node:path";
// Define script-specific output path
const OUTPUT_DIR = path.resolve(
process.cwd(),
"scripts/examples/output/auto-erc20"
);
// Check if PRIVATE_KEY is set, set in console, i.e.: export PRIVATE_KEY=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
if (!process.env.PRIVATE_KEY) {
console.error("Error: PRIVATE_KEY environment variable is not set");
console.error(
"Example: PRIVATE_KEY=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
);
process.exit(1);
}
// Create account from private key
const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`);
// 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)
};
// 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}`;
// Log the request parameters
logToJson(params, "quote_request_params", OUTPUT_DIR);
const response = await fetch(fullUrl);
const data = await response.json();
const serverReqId = response.headers.get("server-req-id");
// Log the response
logToJson(data, "quote_response", OUTPUT_DIR);
// Check for errors
if (!data.success) {
throw new Error(
`Quote error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
);
}
// Check if autoRoute exists
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;
console.log("- Quote ID:", quoteId);
console.log("- Request Type:", requestType);
// Extract data based on the response structure
let witness = null;
let signTypedData = null;
if (data.result.autoRoute.signTypedData) {
signTypedData = data.result.autoRoute.signTypedData;
// The witness is located in signTypedData.values.witness
if (signTypedData?.values?.witness) {
witness = signTypedData.values.witness;
}
}
// Extract approval data if present
const approvalData = data.result.autoRoute.approvalData;
// Log request hash
if (data.result.autoRoute.requestHash) {
console.log("- Request Hash:", data.result.autoRoute.requestHash);
}
// Witness object is available but not logged to keep output clean
// Log the typed data structure for debugging
if (signTypedData) {
logToJson(signTypedData, "typed_data_structure", OUTPUT_DIR);
}
return {
quoteId,
requestType,
witness,
signTypedData,
approvalData,
fullResponse: data,
};
} catch (error) {
console.error("Failed to get quote:", error);
throw error;
}
}
// Function to sign typed data using viem
async function viemSignTypedData(signTypedData) {
try {
const signature = await account.signTypedData({
types: signTypedData.types,
primaryType: "PermitWitnessTransferFrom",
message: signTypedData.values,
domain: signTypedData.domain,
});
console.log("- Signature:", signature);
return signature;
} catch (error) {
console.error("Failed to sign typed data:", error);
throw error;
}
}
// Function to submit the signed request
async function submitSignedRequest(
requestType,
request,
userSignature,
quoteId
) {
try {
// Prepare request body
const requestBody = {
requestType,
request,
userSignature,
quoteId,
};
// Log the request body
logToJson(requestBody, "submit_request", OUTPUT_DIR);
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();
// Log the response data
logToJson(data, "submit_response", OUTPUT_DIR);
// Response data is saved to file but not logged to keep output clean
if (!data.success) {
throw new Error(
`Submit error: ${data.error?.message || "Unknown error"}`
);
}
console.log("- Request Hash:", data.result.requestHash);
return data.result;
} catch (error) {
console.error("Failed to submit signed request:", error);
throw error;
}
}
// Function to check the status of a request
async function checkStatus(requestHash) {
try {
// Log the request parameters
logToJson({ requestHash }, "status_request_params", OUTPUT_DIR);
const response = await fetch(
`${BUNGEE_API_BASE_URL}/api/v1/bungee/status?requestHash=${requestHash}`
);
const data = await response.json();
// Log the response data
logToJson(data, "status_response", OUTPUT_DIR);
if (!data.success) {
throw new Error(
`Status error: ${data.error?.message || "Unknown error"}`
);
}
return data.result[0];
} catch (error) {
console.error("Failed to check status:", 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;
}
console.log("\nChecking token approval...");
// 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 {
const currentAllowance = await publicClient.readContract({
address: approvalData.tokenAddress,
abi: erc20Abi,
functionName: "allowance",
args: [
approvalData.userAddress,
// TODO:Fall back to known Permit2 address if spenderAddress is "0"
approvalData.spenderAddress === "0"
? "0x000000000022D473030F116dDEE9F6B43aC78BA3"
: approvalData.spenderAddress,
],
});
console.log(`Current allowance: ${currentAllowance}`);
console.log(`Required approval: ${approvalData.amount}`);
// Check if approval is needed
if (BigInt(currentAllowance) >= BigInt(approvalData.amount)) {
console.log("Sufficient allowance already exists.");
return;
}
console.log("Insufficient allowance. Approving tokens...");
// Send approval transaction
const hash = await walletClient.writeContract({
address: approvalData.tokenAddress,
abi: erc20Abi,
functionName: "approve",
args: [
// Fall back to known Permit2 address if spenderAddress is "0"
approvalData.spenderAddress === "0"
? "0x000000000022D473030F116dDEE9F6B43aC78BA3"
: approvalData.spenderAddress,
approvalData.amount,
],
});
console.log(`Approval transaction sent: ${hash}`);
// Wait for transaction to be mined
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Approval confirmed in block ${receipt.blockNumber}`);
return receipt;
} catch (error) {
console.error("Error in approval process:", error);
throw error;
}
}
// Main function to handle the flow
async function main() {
try {
console.log("Starting Bungee ERC20 Token Test (Auto Route)...");
// Get the quote and extract data
console.log("\n1. Getting quote...");
const quoteResponse = await getQuote(quoteParamsERC20);
const { quoteId, requestType, witness, signTypedData } = quoteResponse;
// Check if approval is needed and handle it
if (quoteResponse.approvalData) {
await checkAndApproveToken(quoteResponse.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(
"\n4. Submission complete:",
"\n- Hash:",
submitResult.requestHash,
"\n- Type:",
submitResult.requestType
);
// Check the status
// Wait for 5 seconds
console.log("\n4. Waiting for 5 seconds...");
let status;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("\n5. Checking status...");
try {
status = await checkStatus(submitResult.requestHash);
console.log("- Status details:", status.bungeeStatusCode);
} catch (error) {
console.error(
"Failed to check status:",
error?.message || "Unknown error"
);
}
} while (status?.bungeeStatusCode !== 3);
console.log(
"\n6. Transaction complete:",
"\n- Hash:",
status.destinationData?.txHash || "Transaction hash not available"
);
} else {
console.log("No signature data available in the quote response");
}
} catch (error) {
console.error("Error in processing:", error?.shortMessage || error.message);
throw error;
}
}
// Execute the main function
main();