import { createWalletClient, http, encodeFunctionData, pad } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
// -------- Configuration --------
const CONFIG = {
PRIVATE_KEY: process.env.PRIVATE_KEY,
// Contracts (Base Sepolia testnet)
BASE_SEPOLIA_USDC: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
BASE_SEPOLIA_TOKEN_MESSENGER: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA",
// Transfer parameters
TRANSFER_AMOUNT_USDC: "10", // 10 USDC
DESTINATION_ADDRESS: process.env.DESTINATION_ADDRESS,
// Domain IDs
BASE_DOMAIN: 6,
AVALANCHE_DOMAIN: 1,
};
// Forwarding Service hook data: magic bytes ("cctp-forward") + version (0) + additional data length (0)
const FORWARDING_SERVICE_HOOK_DATA =
"0x636374702d666f72776172640000000000000000000000000000000000000000";
// -------- Helper functions --------
function toUSDC(amount) {
return BigInt(Math.floor(parseFloat(amount) * 1_000_000));
}
// -------- Main function --------
async function main() {
const {
PRIVATE_KEY,
BASE_SEPOLIA_USDC,
BASE_SEPOLIA_TOKEN_MESSENGER,
TRANSFER_AMOUNT_USDC,
DESTINATION_ADDRESS,
BASE_DOMAIN,
AVALANCHE_DOMAIN,
} = CONFIG;
// Set up wallet client
const account = privateKeyToAccount(PRIVATE_KEY);
const client = createWalletClient({
chain: baseSepolia,
transport: http(),
account,
});
console.log("Wallet address:", account.address);
console.log("Destination address:", DESTINATION_ADDRESS);
console.log("Transfer amount:", TRANSFER_AMOUNT_USDC, "USDC");
// Step 1: Get fees from API
console.log("\nStep 1: Getting CCTP fees...");
const feeResponse = await fetch(
`https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${BASE_DOMAIN}/${AVALANCHE_DOMAIN}?forward=true`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
const fees = await feeResponse.json();
console.log("Fees:", JSON.stringify(fees, null, 2));
// Step 2: Calculate amounts
console.log("\nStep 2: Calculating amounts...");
const transferAmount = toUSDC(TRANSFER_AMOUNT_USDC);
const feeData = fees[0]; // Fast transfer
const forwardFee = BigInt(feeData.forwardFee.med);
const protocolFeeBps = BigInt(feeData.minimumFee);
const protocolFee = (transferAmount * protocolFeeBps) / 10000n;
const maxFee = forwardFee + protocolFee;
console.log("Transfer amount:", transferAmount.toString(), "minor units");
console.log("Max fee:", maxFee.toString(), "minor units");
// Step 3: Approve USDC
console.log("\nStep 3: Approving USDC transfer...");
const approveTx = await client.sendTransaction({
to: BASE_SEPOLIA_USDC,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "approve",
stateMutability: "nonpayable",
inputs: [
{ name: "spender", type: "address" },
{ name: "amount", type: "uint256" },
],
outputs: [{ name: "", type: "bool" }],
},
],
functionName: "approve",
args: [BASE_SEPOLIA_TOKEN_MESSENGER, transferAmount],
}),
});
console.log("Approval Tx:", approveTx);
// Step 4: Burn USDC with Forwarding Service hook
console.log("\nStep 4: Burning USDC with Forwarding Service hook...");
const mintRecipientBytes32 = pad(DESTINATION_ADDRESS, { size: 32 });
const zeroBytes32 = pad("0x0000000000000000000000000000000000000000", {
size: 32,
});
const burnTx = await client.sendTransaction({
to: BASE_SEPOLIA_TOKEN_MESSENGER,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "depositForBurnWithHook",
stateMutability: "nonpayable",
inputs: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "burnToken", type: "address" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
outputs: [],
},
],
functionName: "depositForBurnWithHook",
args: [
transferAmount,
AVALANCHE_DOMAIN,
mintRecipientBytes32,
BASE_SEPOLIA_USDC,
zeroBytes32,
maxFee,
1000, // Fast Transfer
FORWARDING_SERVICE_HOOK_DATA,
],
}),
});
console.log("Burn Tx:", burnTx);
console.log(
"\nTransfer initiated! The Forwarding Service will automatically mint USDC on Avalanche.",
);
}
main().catch(console.error);