Skip to main content

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:

  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

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