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
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.Copy
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.Copy
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 thewitness object from the quote response at result.autoRoute.signTypedData.values.witness and pass it as the request parameter when submitting the signed request.
Copy
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:Copy
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
Complete Integration Using Viem (Full Script)
Complete Integration Using Viem (Full Script)
Copy
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();