/**
* Script: Call CctpExtension.batchDepositForBurnWithAuth
* - Generates EIP-3009 receiveWithAuthorization signature
* - Executes a CCTP burn via the extension
* - Supports Forwarder hook data to auto-forward to HyperCore
*/
import {
createWalletClient,
createPublicClient,
http,
parseUnits,
formatUnits,
type Address,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrumSepolia } from "viem/chains";
// -------- Contract ABIs --------
const CCTP_EXTENSION_ABI = [
{
name: "batchDepositForBurnWithAuth",
type: "function",
stateMutability: "nonpayable",
inputs: [
{
name: "_receiveWithAuthorizationData",
type: "tuple",
components: [
{ name: "amount", type: "uint256" },
{ name: "authValidAfter", type: "uint256" },
{ name: "authValidBefore", type: "uint256" },
{ name: "authNonce", type: "bytes32" },
{ name: "v", type: "uint8" },
{ name: "r", type: "bytes32" },
{ name: "s", type: "bytes32" },
],
},
{
name: "_depositForBurnData",
type: "tuple",
components: [
{ name: "amount", type: "uint256" },
{ name: "destinationDomain", type: "uint32" },
{ name: "mintRecipient", type: "bytes32" },
{ name: "destinationCaller", type: "bytes32" },
{ name: "maxFee", type: "uint256" },
{ name: "minFinalityThreshold", type: "uint32" },
{ name: "hookData", type: "bytes" },
],
},
],
outputs: [],
},
] as const;
// -------- Configuration --------
const config = {
privateKey: (process.env.PRIVATE_KEY || "0x") as Hex,
// Contract addresses (Arbitrum Sepolia Testnet)
cctpExtension: "0x8E4e3d0E95C1bEC4F3eC7F69aa48473E0Ab6eB8D" as Address,
usdcToken: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d" as Address,
// Transfer parameters
amount: "2", // USDC amount to transfer
maxFee: "0.2", // Max fee in USDC
// CCTP parameters
destinationDomain: 19, // HyperEVM domain
cctpForwarder: "0x02e39ECb8368b41bF68FF99ff351aC9864e5E2a2" as Address, // HyperEVM testnet
// HyperCore recipient
forwardRecipient: process.env.FORWARD_RECIPIENT as Address,
destinationDex: 0, // 0 = perps, 4294967295 = spot
// EIP-3009 validity window (seconds)
validAfter: Math.floor(Date.now() / 1000) - 3600, // 1 hour ago
validBefore: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
};
// -------- Generate Hook Data --------
function encodeForwardHookData(
hyperCoreMintRecipient?: `0x${string}`,
hyperCoreDestinationDex: number = 0,
): `0x${string}` {
if (hyperCoreMintRecipient && !hyperCoreMintRecipient.startsWith("0x")) {
throw new Error("Address must start with 0x");
}
const magic = "cctp-forward";
const magicHex = Buffer.from(magic, "utf-8").toString("hex").padEnd(48, "0");
const version = "00000000";
if (!hyperCoreMintRecipient) {
const dataLength = "00000000";
return `0x${magicHex}${version}${dataLength}`;
}
const dataLength = "00000018";
const address = hyperCoreMintRecipient.slice(2).toLowerCase();
const dex = (hyperCoreDestinationDex >>> 0).toString(16).padStart(8, "0");
return `0x${magicHex}${version}${dataLength}${address}${dex}`;
}
// -------- Generate Random Nonce --------
function generateNonce(): Hex {
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
return `0x${Array.from(randomBytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")}`;
}
// -------- Main Function --------
async function main() {
// Validate private key and recipient
if (!config.privateKey || config.privateKey === "0x") {
throw new Error("Set PRIVATE_KEY");
}
if (!config.forwardRecipient) {
throw new Error("Set FORWARD_RECIPIENT");
}
// Setup account and clients
const account = privateKeyToAccount(config.privateKey);
const publicClient = createPublicClient({
chain: arbitrumSepolia,
transport: http(),
});
const walletClient = createWalletClient({
chain: arbitrumSepolia,
transport: http(),
account,
});
const amount = parseUnits(config.amount, 6);
const maxFee = parseUnits(config.maxFee, 6);
console.log("User:", account.address);
console.log("Extension:", config.cctpExtension);
console.log("USDC:", config.usdcToken);
console.log("Total (USDC):", config.amount);
console.log(
"Dest Domain:",
config.destinationDomain,
"\nMint Recipient:",
config.cctpForwarder,
);
console.log("Max Fee (USDC):", config.maxFee, "\nMin Finality:", 1000);
// Check USDC balance
const balance = await publicClient.readContract({
address: config.usdcToken,
abi: [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ name: "", type: "uint256" }],
},
],
functionName: "balanceOf",
args: [account.address],
});
if (balance < amount) {
throw new Error(
`Insufficient USDC: have ${formatUnits(balance, 6)}, need ${config.amount}`,
);
}
// Generate hook data
const hookData = encodeForwardHookData(
config.forwardRecipient,
config.destinationDex,
);
console.log(
"Forwarder hook enabled -> Final recipient:",
config.forwardRecipient,
);
console.log("Hook Data:", hookData);
// Convert addresses to bytes32
const mintRecipientBytes32 =
`0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex;
const destinationCallerBytes32 =
`0x${config.cctpForwarder.slice(2).padStart(64, "0")}` as Hex;
console.log("Destination Caller (bytes32):", destinationCallerBytes32);
// Generate nonce for EIP-3009
const nonce = generateNonce();
// Sign EIP-3009 ReceiveWithAuthorization
const signature = await walletClient.signTypedData({
domain: {
name: "USD Coin",
version: "2",
chainId: arbitrumSepolia.id,
verifyingContract: config.usdcToken,
},
types: {
ReceiveWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "ReceiveWithAuthorization",
message: {
from: account.address,
to: config.cctpExtension,
value: amount,
validAfter: BigInt(config.validAfter),
validBefore: BigInt(config.validBefore),
nonce,
},
});
// Parse signature into v, r, s
const r = signature.slice(0, 66) as Hex;
const s = `0x${signature.slice(66, 130)}` as Hex;
const v = parseInt(signature.slice(130, 132), 16);
// Estimate gas
const gasEstimate = await publicClient.estimateContractGas({
address: config.cctpExtension,
abi: CCTP_EXTENSION_ABI,
functionName: "batchDepositForBurnWithAuth",
args: [
{
amount,
authValidAfter: BigInt(config.validAfter),
authValidBefore: BigInt(config.validBefore),
authNonce: nonce,
v,
r,
s,
},
{
amount,
destinationDomain: config.destinationDomain,
mintRecipient: mintRecipientBytes32,
destinationCaller: destinationCallerBytes32,
maxFee,
minFinalityThreshold: 1000,
hookData,
},
],
account,
});
console.log("Estimated gas:", gasEstimate.toString());
// Execute batchDepositForBurnWithAuth
const hash = await walletClient.writeContract({
address: config.cctpExtension,
abi: CCTP_EXTENSION_ABI,
functionName: "batchDepositForBurnWithAuth",
args: [
{
amount,
authValidAfter: BigInt(config.validAfter),
authValidBefore: BigInt(config.validBefore),
authNonce: nonce,
v,
r,
s,
},
{
amount,
destinationDomain: config.destinationDomain,
mintRecipient: mintRecipientBytes32,
destinationCaller: destinationCallerBytes32,
maxFee,
minFinalityThreshold: 1000,
hookData,
},
],
gas: (gasEstimate * 120n) / 100n, // +20%
});
console.log("Tx hash:", hash);
// Wait for transaction receipt
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("Status:", receipt.status === "success" ? "SUCCESS" : "FAILED");
console.log(
"Block:",
receipt.blockNumber,
"\nGas Used:",
receipt.gasUsed.toString(),
);
}
// Run
main().catch((error) => {
console.error("Error:", error.message);
process.exit(1);
});