import { privateKeyToAccount } from "viem/accounts";
import { createPublicClient, http, createWalletClient } from "viem";
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 account = privateKeyToAccount(
process.env.PRIVATE_KEY.startsWith('0x')
? process.env.PRIVATE_KEY
: `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
const quoteParamsERC20 = {
userAddress: account.address,
receiverAddress: account.address,
originChainId: 10, // Optimism
destinationChainId: 1337, // HyperCore
inputToken: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // USDC on Optimism
outputToken: "0x2000000000000000000000000000000000000000", // USDC (SPOT) on HyperCore
//outputToken: "0x6d1e7cde53ba9467b783cb7c530ce054", // USDC (Perps) on HyperCore
inputAmount: "2000000", // 2 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}`;
const response = await fetch(fullUrl);
console.log(fullUrl);
const data = await response.json();
const serverReqId = response.headers.get("server-req-id");
// 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);
}
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,
};
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();
const serverReqId = response.headers.get("server-req-id");
console.log("Server request id", serverReqId);
// 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 {
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;
}
}
// 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,
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: [
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;
console.log(JSON.stringify(quoteResponse.fullResponse, null, 2));
// 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("\n5. Waiting for 5 seconds...");
let status;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
console.log("\n6. 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 && status?.bungeeStatusCode !== 4);
console.log(
"\n7. 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();