import { randomBytes } from "node:crypto";
import { pad, zeroAddress, formatUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet, baseSepolia } from "viem/chains";
/* Constants */
const GATEWAY_API_BASE = "https://gateway-api-testnet.circle.com";
const GATEWAY_WALLET_ADDRESS = "0x0077777d7EBA4688BDeF3E311b846F25870A19B9";
const GATEWAY_MINTER_ADDRESS = "0x0022222ABE238Cc2C7Bb1f21003F0a260052475B";
const TRANSFER_VALUE = 10_000000n; // 10 USDC (6 decimals)
const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 300_000; // 5 minutes
const sourceChain = {
name: "arcTestnet",
chain: arcTestnet,
usdcAddress: "0x3600000000000000000000000000000000000000",
domainId: 26,
};
const destinationChain = {
name: "baseSepolia",
chain: baseSepolia,
usdcAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
domainId: 6,
};
const domain = { name: "GatewayWallet", version: "1" };
const EIP712Domain = [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
] as const;
const TransferSpec = [
{ name: "version", type: "uint32" },
{ name: "sourceDomain", type: "uint32" },
{ name: "destinationDomain", type: "uint32" },
{ name: "sourceContract", type: "bytes32" },
{ name: "destinationContract", type: "bytes32" },
{ name: "sourceToken", type: "bytes32" },
{ name: "destinationToken", type: "bytes32" },
{ name: "sourceDepositor", type: "bytes32" },
{ name: "destinationRecipient", type: "bytes32" },
{ name: "sourceSigner", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "value", type: "uint256" },
{ name: "salt", type: "bytes32" },
{ name: "hookData", type: "bytes" },
] as const;
const BurnIntent = [
{ name: "maxBlockHeight", type: "uint256" },
{ name: "maxFee", type: "uint256" },
{ name: "spec", type: "TransferSpec" },
] as const;
if (!process.env.EVM_PRIVATE_KEY) throw new Error("EVM_PRIVATE_KEY not set");
const account = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
console.log(`Using account: ${account.address}`);
console.log(`Transfer: ${sourceChain.name} → ${destinationChain.name}`);
console.log(`Amount: ${formatUnits(TRANSFER_VALUE, 6)} USDC`);
console.log(`Forwarding: enabled\n`);
// Create the transfer spec
const spec = {
version: 1,
sourceDomain: sourceChain.domainId,
destinationDomain: destinationChain.domainId,
sourceContract: GATEWAY_WALLET_ADDRESS,
destinationContract: GATEWAY_MINTER_ADDRESS,
sourceToken: sourceChain.usdcAddress,
destinationToken: destinationChain.usdcAddress,
sourceDepositor: account.address,
destinationRecipient: account.address,
sourceSigner: account.address,
destinationCaller: zeroAddress,
value: TRANSFER_VALUE,
salt: "0x" + randomBytes(32).toString("hex"),
hookData: "0x",
};
const specBytes32 = {
...spec,
sourceContract: pad(spec.sourceContract.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationContract: pad(
spec.destinationContract.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceToken: pad(spec.sourceToken.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationToken: pad(spec.destinationToken.toLowerCase() as `0x${string}`, {
size: 32,
}),
sourceDepositor: pad(spec.sourceDepositor.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationRecipient: pad(
spec.destinationRecipient.toLowerCase() as `0x${string}`,
{ size: 32 },
),
sourceSigner: pad(spec.sourceSigner.toLowerCase() as `0x${string}`, {
size: 32,
}),
destinationCaller: pad(
spec.destinationCaller.toLowerCase() as `0x${string}`,
{ size: 32 },
),
};
// Estimate fees
console.log("Estimating fees...");
const estimateResponse = await fetch(
`${GATEWAY_API_BASE}/v1/estimate?enableForwarder=true`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([{ spec: specBytes32 }], (_key, value) =>
typeof value === "bigint" ? value.toString() : value,
),
},
);
if (!estimateResponse.ok) {
const text = await estimateResponse.text();
throw new Error(`Estimate API error: ${estimateResponse.status} ${text}`);
}
const estimateResult = await estimateResponse.json();
const estimated = estimateResult.body[0].burnIntent;
const maxFee = BigInt(estimated.maxFee);
const maxBlockHeight = BigInt(estimated.maxBlockHeight);
const { fees } = estimateResult;
if (fees.forwardingFee) {
console.log(` Forwarding fee: ${fees.forwardingFee} ${fees.token}`);
}
console.log(` Estimated maxFee: ${formatUnits(maxFee, 6)} ${fees.token}`);