Circle USYC 백서

Circle USYC

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 37쪽

st using source domain 6 (Base Sepolia) and destination domain 1 (Avalanche Fuji): ```typescript theme={null} const response = await fetch( "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/1?forward=true", { method: "GET", headers: { "Content-Type": "application/json" }, }, ); const fees = await response.json(); console.log(fees); ``` **Example response:** ```json theme={null} [ { "finalityThreshold": 1000, // Fast transfer "minimumFee": 1.3, // Basis points (0.013% fee rate) "forwardFee": { // Gas-based, fluctuates based on destination chain gas prices "low": 56035, // 0.056035 USDC "med": 57543, // 0.057543 USDC "high": 59052 // 0.059052 USDC } }, { "finalityThreshold": 2000, // Standard transfer "minimumFee": 0, // No fee "forwardFee": { "low": 56035, // 0.056035 USDC "med": 57543, // 0.057543 USDC "high": 59052 // 0.059052 USDC } } ] ``` The `forwardFee` is the fee charged by the Forwarding Service. The `minimumFee` is the CCTP protocol fee rate in basis points, applied as a percentage of the transfer amount. <Tip> Circle recommends selecting the `med` fee level or higher from the `forwardFee` object in the API response. Note that `forwardFee` values fluctuate based on destination chain gas prices. Make the query immediately before initiating your transfer. </Tip> <Accordion title="Forwarding to Solana"> When the destination blockchain is Solana, the `forwardFee` values include both gas and rent costs. If the recipient does not have an existing USDC [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account), add `includeRecipientSetup=true` to the fee query so the returned fee covers ATA creation: ```typescript theme={null} // Source domain 6 (Base Sepolia), destination domain 5 (Solana) const response = await fetch( "https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/5?forward=true&includeRecipientSetup=true", { method: "GET", headers: { "Content-Type": "application/json" }, }, ); ``` Unlike EVM destinations, the `mintRecipient` for Solana must be the recipient's **USDC token account address** (ATA), not the wallet address. Derive the ATA from the wallet address and the USDC mint: ```typescript theme={null} import { getAssociatedTokenAddressSync } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; const recipientWallet = new PublicKey("RecipientSolanaWalletAddress"); const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet); const mintRecipientBytes32 = `0x${Buffer.from(recipientAta.toBytes()).toString("hex")}` as `0x${string}`; ``` If the recipient does not have an existing ATA and you passed `includeRecipientSetup=true` in the fee query, you must also encode ATA creation fields in the hook data. See [Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation) for the extended hook data format. </Accordion> ### Step 2. Calculate the USDC amounts and fees Calculate the total fee by combining the protocol fee and the Forwarding Service fee. The `maxFee` parameter must cover both fees for the transfer to succeed. ```typescript theme={null} // Amount to transfer (10 USDC in subunits) const transferAmount = 10_000_000n; // Parse fees from API response const feeData = fees[0]; // Use fast transfer fees (finalityThreshold: 1000) const forwardFee = BigInt(feeData.forwardFee.med); // Calculate protocol fee (minimumFee is in basis points) const minimumFeeBps = feeData.minimumFee; const protocolFee = (transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n; // Total max fee should cover both fees const maxFee = forwardFee + protocolFee; const totalAmount = transferAmount + maxFee; // Total to burn console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC"); console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC"); console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC"); console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC"); console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC"); ``` In this example, for a 10 USDC transfer with forwarding, the total fee is 0.058843 USDC (0.057543 USDC Forwarding Service fee + 0.0013 USDC CCTP protocol fee). For the recipient to receive 10 USDC, you must burn 10.058843 USDC in total. <Warning> If the `maxFee` parameter is insufficient to cover the both Fast Transfer protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding execution over Fast Transfer. This means that the transfer will execute as a Standard Transfer with the Forwarding Service. </Warning> ### Step 3. Approve the USDC transfer Grant approval for the [`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on Base to transfer USDC from your wallet. Approve at least `totalAmount` (including fees) calculated in Step 2. ```typescript theme={null} import { createWalletClient, http, encodeFunctionData } from "viem"; import { baseSepolia } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // Configuration const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; const BASE_SEPOLIA_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; // Set up wallet client const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const client = createWalletClient({ chain: baseSepolia, transport: http(), account, }); async function approveUSDC(amount: bigint) { console.log("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, amount], }), }); console.log("USDC Approval Tx:", approveTx); return approveTx; } // Approve the total amount (from Step 2) await approveUSDC(totalAmount); ``` ### Step 4. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract Create and send a `depositForBurnWithHook` transaction with the Forwarding Service hook data. The hook data tells the CCTP Forwarding Service to automatically forward the mint transaction on the destination chain. The Forwarding Service hook data is a static 32-byte value containing the magic bytes `cctp-forward`, version `0`, and length `0`. For details on the hook format, see [Forwarding Service hook format](/cctp/concepts/forwarding-service#hook-format). ```typescript theme={null} // Forwarding Service hook data: magic bytes ("cctp-forward") + version (0) + additional data length (0) const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000"; ``` <Accordion title="Hook data for Solana destinations"> When forwarding to Solana, use the same static hook data if the recipient already has a USDC [Associated Token Account (ATA)](https://spl.solana.com/associated-token-account). If the recipient does not have an ATA and you included `includeRecipientSetup=true` in the fee query (see Step 1), construct extended hook data that requests ATA creation: ```typescript theme={null} import { PublicKey } from "@solana/web3.js"; // Magic bytes "cctp-forward" padded to 24 bytes const magicBytes = Buffer.alloc(24); magicBytes.write("cctp-forward", "utf-8"); // Version (uint32, big-endian) = 0 const version = Buffer.alloc(4); // Length of additional Circle hook data (uint32, big-endian) = 33 const length = Buffer.alloc(4); length.writeUInt32BE(33); // ATA creation flag = 1 const ataFlag = Buffer.from([1]); // Recipient wallet address (32 bytes) const recipientWallet = new PublicKey("RecipientSolanaWalletAddress"); const walletBytes = Buffer.from(recipientWallet.toBytes()); const FORWARDING_SERVICE_HOOK_DATA = ("0x" + Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString( "hex", )) as `0x${string}`; ``` For the full hook data format, see [Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation). </Accordion> Then, send the `depositForBurnWithHook` transaction: <Note> Use `totalAmount` (transfer amount + fees) for the `amount` parameter. The recipient receives only the transfer amount after fees are deducted. </Note> ```typescript theme={null} import { pad, encodeFunctionData } from "viem"; // Configuration const AVALANCHE_FUJI_DOMAIN = 1; const DESTINATION_ADDRESS = "0xYOUR_DESTINATION_ADDRESS" as `0x${string}`; // Convert address to bytes32 format const mintRecipientBytes32 = pad(DESTINATION_ADDRESS, { size: 32 }); async function depositForBurnWithHook() { console.log("Burning USDC on Base with Forwarding Service hook..."); 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: [ totalAmount, // Total to burn (recipient receives transferAmount after fees) AVALANCHE_FUJI_DOMAIN, mintRecipientBytes32, BASE_SEPOLIA_USDC, pad("0x", { size: 32 }), // destinationCaller (empty = any caller) maxFee, 1000, FORWARDING_SERVICE_HOOK_DATA, ], }), }); console.log("Burn Tx:", burnTx); return burnTx; } ``` Once the burn transaction is confirmed on Base, the Circle Forwarding Service automatically handles the attestation and mint transaction on Avalanche. The USDC is minted directly to the `mintRecipient` address on the destination chain. The recipient receives `transferAmount` USDC (fees are automatically deducted from the `totalAmount` on the destination chain). ### Step 5. Verify the mint transaction After the burn transaction is confirmed, query the Circle Iris API to retrieve the forwarding details. The API returns the `forwardTxHash`, which is the mint transaction hash on the destination chain. The attestation may take time to become available, depending on the destination chain. Poll the API until the message is ready: ```typescript theme={null} // Configuration const BASE_SEPOLIA_DOMAIN = 6; process.stdout.write("Waiting for attestation..."); let mintTx; while (!mintTx) { const messageResponse = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`, ); const data = await messageResponse.json(); if (data.messages?.[0]?.forwardTxHash) { mintTx = data.messages[0].forwardTxHash; console.log(); // New line after dots } else { process.stdout.write("."); await new Promise((resolve) => setTimeout(resolve, 2000)); } } console.log("Mint Tx:", mintTx); ``` ## Full example code The following is a complete example of how to transfer USDC from Base Sepolia to Avalanche Fuji using the Forwarding Service. Remember to set the `PRIVATE_KEY` and `DESTINATION_ADDRESS` environment variables. ```typescript script.ts expandable theme={null} import { createWalletClient, http, encodeFunctionData, pad } from "viem"; import { baseSepolia } from "viem/chains"; import { privateKeyToAccount } from "viem/accounts"; // Validate environment variables if (!process.env.PRIVATE_KEY || !process.env.DESTINATION_ADDRESS) { throw new Error( "PRIVATE_KEY and DESTINATION_ADDRESS environment variables are required", ); } // Configuration const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e"; const BASE_SEPOLIA_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA"; const BASE_SEPOLIA_DOMAIN = 6; const AVALANCHE_FUJI_DOMAIN = 1; const DESTINATION_ADDRESS = process.env.DESTINATION_ADDRESS as `0x${string}`; // Forwarding Service hook data const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`; // Set up wallet client const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`); const client = createWalletClient({ chain: baseSepolia, transport: http(), account, }); async function main() { console.log("Wallet address:", account.address); console.log("Destination address:", DESTINATION_ADDRESS); // 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_SEPOLIA_DOMAIN}/${AVALANCHE_FUJI_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 = 10_000_000n; // 10 USDC const feeData = fees[0]; // Fast transfer const forwardFee = BigInt(feeData.forwardFee.med); const minimumFeeBps = feeData.minimumFee; const protocolFee = (transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n; const maxFee = forwardFee + protocolFee; const totalAmount = transferAmount + maxFee; // Total to burn console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC"); console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC"); console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC"); console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC"); console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC"); // 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, totalAmount], }), }); console.log("Approval Tx:", approveTx); // Step 4: Burn USDC with Forwarding Service hook console.log("\nStep 4: Burning USDC with Forwarding Service hook..."); 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: [ totalAmount, AVALANCHE_FUJI_DOMAIN, pad(DESTINATION_ADDRESS as `0x${string}`, { size: 32 }), BASE_SEPOLIA_USDC, pad("0x", { size: 32 }), 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.", ); // Step 5: Verify the mint transaction console.log("\nStep 5: Verifying mint transaction..."); process.stdout.write("Waiting for attestation..."); let mintTx; while (!mintTx) { const messageResponse = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`, ); const data = await messageResponse.json(); if (data.messages?.[0]?.forwardTxHash) { mintTx = data.messages[0].forwardTxHash; console.log("\n"); // New line after dots } else { process.stdout.write("."); await new Promise((resolve) => setTimeout(resolve, 2000)); } } console.log("Mint Tx:", mintTx); } main().catch(console.error); ``` Run the script: ```bash theme={null} node --env-file=.env script.ts ``` # How-to: Transfer USDC with upfront fees Source: https://developers.circle.com/cctp/howtos/transfer-usdc-with-upfront-fees Pay CCTP fees upfront on the source blockchain so the full USDC amount mints on the destination blockchain. This example transfers 10 USDC from Base Sepolia to Arc Testnet, paying both the Fast Transfer and Forwarding Service fees [upfront](/cctp/concepts/upfront-fees) so the recipient receives the full transfer amount on the destination blockchain. You can use the same steps for any EVM source blockchain where both upfront fees and Fast Transfer are available, and any destination blockchain that the Forwarding Service supports. ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22+](https://nodejs.org/) * Created a TypeScript project and installed the `viem` package * Created a wallet with the private key accessible from your local development environment * Funded the wallet on Base Sepolia with: * Base Sepolia ETH for gas from a [public faucet](https://www.alchemy.com/faucets/base-sepolia) * Base Sepolia USDC from the [Circle Faucet](https://faucet.circle.com) * Created a `.env` file with your private key <Warning> Keep your private key secret. Store it in a `.env` file that's listed in `.gitignore`, and never commit it or expose it in logs or shell history. </Warning> ## Steps ### Step 1: Request a signed quote from the Quote API Request a quote from the [Quote API](/api-reference/cctp/all/create-usdc-burn-quote) for the fees you want to pay upfront. This example requests both `FORWARD` (Forwarding Service) and `PRE_FINALITY` (Fast Transfer) fees, and sets `feeToken` to the source blockchain's USDC address to pay the fee in USDC. The following request uses source domain 6 (Base Sepolia) and destination domain 26 (Arc Testnet). The USDC address and decimals come from the token definition in [`viem/tokens`](https://viem.sh/tokens), so you don't hardcode either one: ```typescript theme={null} import { parseUnits } from "viem"; import { baseSepolia } from "viem/chains"; import { usdc } from "viem/tokens"; // USDC on the source blockchain, including its address and decimals const sourceUsdc = usdc(baseSepolia.id); // The quote binds to this amount, so reuse it for the onchain burn const transferAmount = parseUnits("10", sourceUsdc.decimals); const response = await fetch( "https://iris-api-sandbox.circle.com/v2/quote/burn/usdc/6/26", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: transferAmount.toString(), // minor units feeToken: sourceUsdc.address, // omit to pay the fee in the native gas token requests: [ { type: "FORWARD" }, // Omit if the source blockchain doesn't support Fast Transfer { type: "PRE_FINALITY" }, ], }), }, ); const quote = await response.json(); console.log("Quote:", JSON.stringify(quote, null, 2)); ``` **Example response:** ```json theme={null} { "signedQuote": "0x0100000000000000000000000000000000000000000000000000000000000000...c9afdd35731b00", "issuedAt": 1783407750, "expiry": { "mode": "BLOCK_NUMBER", "expiresAtBlock": 43819792, "blockEstimatedAt": 1783407872 }, "feeTotalAmount": "24453", "feeToken": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "items": [ { "type": "FORWARD", "amount": "23153", "args": ["..."], "argsHash": "0x6cea1d25ab9b4b9edfc5c788082c28c6669c6fc2f35fa8e047c7774e60c66d4c" }, { "type": "PRE_FINALITY", "amount": "1300", "args": ["..."], "argsHash": "0x5743df2ad8e9696c8a5e70bfced256f31
프로젝트로 돌아가기