Permit2 Integration (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​
For ERC20 tokens, request a quote using the token address.
API Reference: Quote Endpoint
const quoteParamsERC20 = {
userAddress: "0x...",
originChainId: 42161, // Arbitrum chain ID
destinationChainId: 10, // Optimism chain ID
inputToken: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // Arbitrum USDC
inputAmount: "10000000", // Amount 10 USDC
receiverAddress: "0x...",
outputToken: "0x0b2c639c533813f4aa9d7837caf62653d097ff85", // Optimism USDC
};
async function getQuote(params) {
const url = `https://public-backend.bungee.exchange/bungee/quote`;
const queryParams = new URLSearchParams(params);
const queryUrl = `${url}?${queryParams}`;
const response = await fetch(queryUrl);
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;
witness = signTypedData.values.witness;
}
return {
quoteId,
requestType,
witness,
signTypedData,
completeResponse: data,
serverReqId,
};
}
Step 2: Sign and Submit the Request​
For ERC20 tokens, sign the typed data and submit the request. The request
field in submitSignedRequest
is the witness
from getQuote()
. It contains the data needed for Permit2 and serves as proof for the backend to verify and process the request.
API Reference: Submit Endpoint
async function signTypedData(signTypedData) {
const signature = await account.signTypedData({
types: signTypedData.types,
primaryType: "PermitWitnessTransferFrom",
message: signTypedData.values,
domain: signTypedData.domain,
});
return signature;
}
async function submitSignedRequest(
quoteId,
request,
requestType,
userSignature
) {
const requestBody = {
quoteId,
request,
requestType,
userSignature,
};
const response = await fetch(
`https://public-backend.bungee.exchange/bungee/submit`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestBody),
}
);
const data = await response.json();
if (!data.success) {
const serverReqId = response.headers.get("server-req-id");
throw new Error(
`Submit error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
);
}
return data.result;
}
Step 3: Check Request Status​
After submitting the request, you'll need to check its status to track progress.
API Reference: Status Endpoint
async function checkStatus(requestHash) {
const response = await fetch(
`https://public-backend.bungee.exchange/bungee/status?id=${requestHash}`
);
const data = await response.json();
if (!data.success) {
const serverReqId = response.headers.get("server-req-id");
throw new Error(
`Status error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
);
}
return data.result;
}
You can implement a polling mechanism to continuously check the status until completion:
async function pollForCompletion(
requestHash,
interval = 5000,
maxAttempts = 60
) {
let attempts = 0;
console.log("Polling for transaction status...");
while (attempts < maxAttempts) {
const status = await checkStatus(requestHash);
const code = status[0]?.bungeeStatusCode;
console.log(`Attempt ${attempts + 1}: Status code = ${code}`);
if (code === 3) {
console.log("Transaction complete:", status[0].destinationData.txHash);
return status;
}
attempts++;
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error("Polling timed out. Transaction may not have completed.");
}
For a detailed explanation of all status codes, see the Request Status Codes guide.
Complete Integration Example​
Complete Integration Using Viem
import { privateKeyToAccount } from "viem/accounts";
// 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 account = privateKeyToAccount(process.env.PRIVATE_KEY);
console.log("Account address:", account.address);
// Constant parameters
const BUNGEE_API_BASE_URL = "https://public-backend.bungee.exchange";
const USER_ADDRESS = account.address;
const RECEIVER_ADDRESS = account.address;
const DESTINATION_CHAIN_ID = 10; // OP Mainnet
const ORIGIN_CHAIN_ID = 42161; // Arbitrum
const OUTPUT_TOKEN = "0x0b2c639c533813f4aa9d7837caf62653d097ff85"; // USDC on OP Mainnet
const INPUT_TOKEN = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; // USDC on Arbitrum
const INPUT_AMOUNT = "10000000"; // Amount 10 USDC
const quoteParamsERC20 = {
userAddress: USER_ADDRESS,
originChainId: ORIGIN_CHAIN_ID,
destinationChainId: DESTINATION_CHAIN_ID,
inputToken: INPUT_TOKEN,
inputAmount: INPUT_AMOUNT,
receiverAddress: RECEIVER_ADDRESS,
outputToken: OUTPUT_TOKEN,
};
// Function to get a quote
async function getQuote(params) {
try {
const url = `${BUNGEE_API_BASE_URL}/bungee/quote`;
const queryParams = new URLSearchParams(params);
const queryUrl = `${url}?${queryParams}`;
const response = await fetch(queryUrl);
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;
witness = signTypedData.values.witness;
}
return {
quoteId,
requestType,
witness,
signTypedData,
completeResponse: data,
serverReqId,
};
} catch (error) {
console.error("Failed to get quote:", error);
throw error;
}
}
// Function to sign typed data using viem
async function signTypedData(signTypedData) {
try {
const signature = await account.signTypedData({
types: signTypedData.types,
primaryType: "PermitWitnessTransferFrom",
message: signTypedData.values,
domain: signTypedData.domain,
});
return signature;
} catch (error) {
console.error("Failed to sign typed data:", error);
throw error;
}
}
// Function to submit the signed request
async function submitSignedRequest(
quoteId,
request,
requestType,
userSignature
) {
try {
// Prepare request body
const requestBody = {
quoteId,
request,
requestType,
userSignature,
};
const response = await fetch(`${BUNGEE_API_BASE_URL}/bungee/submit`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestBody),
});
const data = await response.json();
const serverReqId = response.headers.get("server-req-id");
console.log("- Submit Response JSON:");
console.log(data);
console.log(`- server-req-id: ${serverReqId}`);
if (!data.success) {
throw new Error(
`Submit error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
);
}
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 {
const response = await fetch(
`${BUNGEE_API_BASE_URL}/bungee/status?id=${requestHash}`
);
const data = await response.json();
const serverReqId = response.headers.get("server-req-id");
if (!data.success) {
throw new Error(
`Status error: ${data.statusCode}: ${data.message}. server-req-id: ${serverReqId}`
);
}
console.log("- Status:", data.result);
return data.result;
} catch (error) {
console.error("Failed to check status:", error);
throw error;
}
}
// Function to poll for completion
async function pollForCompletion(
requestHash,
interval = 5000,
maxAttempts = 60
) {
let attempts = 0;
console.log("Polling for transaction status...");
while (attempts < maxAttempts) {
const status = await checkStatus(requestHash);
const code = status[0]?.bungeeStatusCode;
console.log(`Attempt ${attempts + 1}: Status code = ${code}`);
if (code === 3) {
console.log("Transaction complete:", status[0].destinationData.txHash);
return status;
}
attempts++;
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error("Polling timed out. Transaction may not have completed.");
}
// Main function to handle the flow
async function main() {
try {
console.log("Starting Bungee Auto ERC20 test...");
console.log("\n1. Getting quote...");
const quote = await getQuote(quoteParamsERC20);
console.log(
"- Raw quote Response:",
JSON.stringify(quote.completeResponse, null, 2)
);
console.log("- Quote server-req-id:", quote.serverReqId);
console.log("- Quote ID:", quote.quoteId);
console.log("- Request Type:", quote.requestType);
if (quote.signTypedData && quote.witness) {
console.log("- Witness:", quote.witness);
console.log("\n2. Signing typed data...");
const signature = await signTypedData(quote.signTypedData);
console.log("- Signature:", signature);
console.log("\n3. Submitting signed request...");
const submitResult = await submitSignedRequest(
quote.quoteId,
quote.witness,
quote.requestType,
signature
);
console.log("- Submission Hash:", submitResult.requestHash);
console.log("\n4. Polling for status...");
const finalStatus = await pollForCompletion(submitResult.requestHash);
console.log(
"\n5. Transaction complete with destination hash:",
finalStatus[0].destinationData.txHash
);
} else {
console.error("No signature data available in the quote response");
}
} catch (error) {
console.error("Error in processing:", error?.message || String(error));
throw error;
}
}
main();