import { createPublicClient, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { optimism } from "viem/chains";
// Check if PRIVATE_KEY is set, set in console, i.e.: export PRIVATE_KEY=<YOUR_PRIVATE_KEY>
if (!process.env.PRIVATE_KEY) {
console.error("Error: PRIVATE_KEY environment variable is not set");
console.error(
"Example: export PRIVATE_KEY=<YOUR_PRIVATE_KEY>"
);
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);
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 transfer (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: "10000000", // 10 USDC (with decimals)
enableManual: true, // Important: enables manual routes
};
async function getQuote(params) {
try {
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();
if (!data.success) {
throw new Error(`Quote error: ${data.error?.message || "Unknown error"}`);
}
if (!data.result.manualRoutes || data.result.manualRoutes.length === 0) {
throw new Error("No manual routes available");
}
return {
manualRoutes: data.result.manualRoutes,
fullResponse: data,
};
} catch (error) {
console.error("Failed to get quote:", error);
throw error;
}
}
// Function to build transaction from quote ID
async function buildTransaction(quoteId) {
try {
const response = await fetch(
`${BUNGEE_API_BASE_URL}/api/v1/bungee/build-tx?quoteId=${quoteId}`
);
const data = await response.json();
if (!data.success) {
throw new Error(
`Build TX error: ${data.error?.message || "Unknown error"}`
);
}
return data.result;
} catch (error) {
console.error("Failed to build transaction:", error);
throw error;
}
}
// Function to submit the ERC20 transaction
async function submitERC20Transaction(txData) {
try {
console.log("- Submitting transaction to inbox contract...");
console.log(" To:", txData.to);
console.log(" Value:", txData.value);
console.log(" Data:", txData.data);
// Send the transaction
const hash = await walletClient.sendTransaction({
to: txData.to,
value: BigInt(txData.value),
data: txData.data,
});
console.log("- Transaction sent:", hash);
// Wait for transaction to be mined
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("- Transaction mined in block:", receipt.blockNumber);
return {
hash,
receipt,
};
} catch (error) {
console.error("Failed to submit transaction:", error);
throw error;
}
}
// Function to check the status of a request
async function checkStatus(requestHash) {
try {
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];
} catch (error) {
console.error("Failed to check status:", error);
throw error;
}
}
async function main() {
try {
console.log("Starting Bungee ERC20 Token Test (Manual Route)...");
// Step 1: Get the quote with manual routes
console.log("\n1. Getting quote with manual routes...");
const { manualRoutes } = await getQuote(quoteParamsERC20);
if (!manualRoutes || manualRoutes.length === 0) {
throw new Error("No manual routes available");
}
// Step 2: Select a route (using the first route as an example)
console.log("\n2. Selecting route...");
const selectedRoute = manualRoutes[0];
const quoteId = selectedRoute.quoteId;
console.log("- Selected Quote ID:", quoteId);
console.log("- Available routes:", manualRoutes.length);
// Step 3: Build the transaction from the selected route
console.log("\n3. Building transaction...");
const buildResult = await buildTransaction(quoteId);
const txData = buildResult.txData;
const approvalData = buildResult.approvalData;
const requestHash = buildResult.requestHash;
// Step 4: Handle token approvals if needed
if (approvalData) {
console.log("\n4. Handling token approvals...");
// For ERC20 tokens, approvals are required before transfer
// Check current allowance and create approval transaction if needed
console.log("- Approval required:", approvalData);
// TODO: Implement approval transaction if needed
} else {
console.log("\n4. No approvals required");
}
// Step 5: Submit the transaction
console.log("\n5. Submitting transaction...");
const { hash, receipt } = await submitERC20Transaction(txData);
console.log(
"\n6. Transaction submitted:",
"\n- Hash:",
hash,
"\n- Status:",
receipt.status
);
console.log("- Request Hash:", requestHash);
// Step 6: Poll for completion
// Terminal states: 3 = FULFILLED, 4 = SETTLED (success), 5 = EXPIRED, 6 = CANCELLED, 7 = REFUNDED (failure)
console.log("\n7. Polling for completion...");
const TERMINAL_SUCCESS = [3, 4];
const TERMINAL_FAILURE = [5, 6, 7];
const TERMINAL_STATES = [...TERMINAL_SUCCESS, ...TERMINAL_FAILURE];
let status;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("Checking status...");
try {
status = await checkStatus(requestHash);
console.log("- Status code:", status.bungeeStatusCode);
} catch (error) {
console.error(
"Failed to check status:",
error?.message || "Unknown error"
);
}
} while (!TERMINAL_STATES.includes(status?.bungeeStatusCode));
// Handle terminal states
if (TERMINAL_SUCCESS.includes(status?.bungeeStatusCode)) {
console.log(
"\n8. Transaction complete:",
"\n- Hash:",
status.destinationData?.txHash || "Transaction hash not available"
);
} else {
const statusNames = { 5: "EXPIRED", 6: "CANCELLED", 7: "REFUNDED" };
console.error(
"\n8. Transaction failed:",
`\n- Status: ${statusNames[status?.bungeeStatusCode]} (${status?.bungeeStatusCode})`,
"\n- Details:", JSON.stringify(status, null, 2)
);
throw new Error(`Request ${statusNames[status?.bungeeStatusCode]}`);
}
} catch (error) {
console.error("Error in processing:", error?.shortMessage || error.message);
throw error;
}
}
// Execute the main function
main();