Circle USYC 백서

Circle USYC

원문

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

hreshold**: Determines whether it's a [Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times) (1000 or less) or a [Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times) (2000 or more) ```ts TypeScript theme={null} async function burnUSDC() { console.log("Burning USDC on Ethereum Sepolia..."); const burnTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurn", 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" }, ], outputs: [], }, ], functionName: "depositForBurn", args: [ AMOUNT, ARC_TESTNET_DOMAIN, DESTINATION_ADDRESS_BYTES32, ETHEREUM_SEPOLIA_USDC, DESTINATION_CALLER_BYTES32, maxFee, 1000, // minFinalityThreshold (1000 or less for Fast Transfer) ], }), }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } ``` ### 3.3. Retrieve attestation Retrieve the attestation required to complete the CCTP transfer by calling Circle's attestation API. * Call Circle's [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) API endpoint to retrieve the attestation. * Pass the `srcDomain` argument from the [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) for your source chain. * Pass `transactionHash` from the value returned by `sendTransaction` in the `burnUSDC` function preceding. ```ts TypeScript theme={null} async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${ text ? ` - ${text}` : "" }`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } ``` ### 3.4. Mint USDC Call the [`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage) from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses) deployed on the Arc testnet to mint USDC on that destination chain. * Pass the signed attestation and the message data as parameters. * The function processes the attestation and mints USDC to the specified Arc testnet wallet address. ```ts TypeScript theme={null} async function mintUSDC(attestation: AttestationMessage) { console.log("Minting USDC on Arc testnet..."); const mintTx = await arcClient.sendTransaction({ to: ARC_TESTNET_MESSAGE_TRANSMITTER, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); console.log(`Mint Tx: ${mintTx}`); } ``` </Tab> </Tabs> ## Step 4: Complete script Create a `index.ts` file in your project directory and populate it with the complete code below for the path you want to test. <Tabs> <Tab title="Forwarding Service"> ```ts index.ts expandable theme={null} import { createPublicClient, createWalletClient, http, encodeFunctionData, pad, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { sepolia } from "viem/chains"; type FeeQuote = { finalityThreshold: number; minimumFee: number; forwardFee: { med: number }; }; const PRIVATE_KEY = process.env.PRIVATE_KEY; const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; const ETHEREUM_SEPOLIA_TOKEN_MESSENGER = "0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa"; const DESTINATION_ADDRESS = account.address; const AMOUNT = 1_000_000n; const ETHEREUM_SEPOLIA_DOMAIN = 0; const ARC_TESTNET_DOMAIN = 26; const DESTINATION_ADDRESS_BYTES32 = pad(DESTINATION_ADDRESS, { size: 32 }); const DESTINATION_CALLER_BYTES32 = pad("0x", { size: 32 }); const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`; const sepoliaClient = createWalletClient({ chain: sepolia, transport: http(), account, }); const sepoliaPublicClient = createPublicClient({ chain: sepolia, transport: http(), }); async function approveUSDC(amount: bigint) { console.log("Approving USDC transfer..."); const approveTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_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: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, amount], }), }); console.log(`USDC Approval Tx: ${approveTx}`); await sepoliaPublicClient.waitForTransactionReceipt({ hash: approveTx }); } async function getForwardingFeeQuote() { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${ETHEREUM_SEPOLIA_DOMAIN}/${ARC_TESTNET_DOMAIN}?forward=true`, { method: "GET", headers: { "Content-Type": "application/json" }, }, ); if (!response.ok) { throw new Error(`Failed to fetch fees: ${await response.text()}`); } const fees = (await response.json()) as FeeQuote[]; const feeData = fees.find((fee) => fee.finalityThreshold === 1000); if (!feeData) { throw new Error("Fast-transfer forwarding fees not available"); } return feeData; } async function calculateForwardingAmounts() { const feeData = await getForwardingFeeQuote(); const forwardFee = BigInt(feeData.forwardFee.med); const protocolFee = (AMOUNT * BigInt(Math.round(feeData.minimumFee * 100))) / 1_000_000n; const maxFee = forwardFee + protocolFee; const totalAmount = AMOUNT + maxFee; 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"); return { maxFee, totalAmount }; } async function burnUSDCWithForwarding(totalAmount: bigint, maxFee: bigint) { console.log("Burning USDC on Ethereum Sepolia with Forwarding Service..."); const burnTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_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, ARC_TESTNET_DOMAIN, DESTINATION_ADDRESS_BYTES32, ETHEREUM_SEPOLIA_USDC, DESTINATION_CALLER_BYTES32, maxFee, 1000, FORWARDING_SERVICE_HOOK_DATA, ], }), }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } async function waitForForwardedMint(transactionHash: string) { console.log("Waiting for Forwarding Service to mint on Arc..."); while (true) { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`, { method: "GET" }, ); if (!response.ok) { await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = await response.json(); const forwardTxHash = data?.messages?.[0]?.forwardTxHash; if (forwardTxHash) { console.log(`Forwarded Mint Tx: ${forwardTxHash}`); return forwardTxHash; } await new Promise((resolve) => setTimeout(resolve, 5000)); } } async function main() { console.log("Wallet address:", account.address); // [1] Quote forwarding fees and derive the total source-chain burn amount. const { maxFee, totalAmount } = await calculateForwardingAmounts(); // [2] Approve the total burn amount, including forwarding and protocol fees. await approveUSDC(totalAmount); // [3] Burn on the source chain with the forwarding hook enabled. const burnTx = await burnUSDCWithForwarding(totalAmount, maxFee); // [4] Poll until Iris returns the destination mint transaction hash. await waitForForwardedMint(burnTx); console.log("USDC transfer completed with Forwarding Service."); } main().catch(console.error); ``` </Tab> <Tab title="Direct mint"> ```ts index.ts expandable theme={null} import { createPublicClient, createWalletClient, http, encodeFunctionData, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet, sepolia } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // ============ Configuration Constants ============ // Authentication const PRIVATE_KEY = process.env.PRIVATE_KEY; const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); // Contract Addresses const ETHEREUM_SEPOLIA_USDC = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"; const ETHEREUM_SEPOLIA_TOKEN_MESSENGER = "0x8fe6b999dc680ccfdd5bf7eb0974218be2542daa"; const ARC_TESTNET_MESSAGE_TRANSMITTER = "0xe737e5cebeeba77efe34d4aa090756590b1ce275"; // Transfer Parameters const DESTINATION_ADDRESS = account.address; // Address to receive minted tokens on destination chain const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits) const maxFee = 500n; // 0.0005 USDC (500 subunits) // Bytes32 Formatted Parameters const DESTINATION_ADDRESS_BYTES32 = `0x000000000000000000000000${DESTINATION_ADDRESS.slice( 2, )}`; // Destination address in bytes32 format const DESTINATION_CALLER_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; // Empty bytes32 allows any address to call MessageTransmitterV2.receiveMessage() // Chain-specific Parameters const ETHEREUM_SEPOLIA_DOMAIN = 0; // Source domain ID for Ethereum Sepolia const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc testnet // Set up wallet clients const sepoliaClient = createWalletClient({ chain: sepolia, transport: http(), account, }); const sepoliaPublicClient = createPublicClient({ chain: sepolia, transport: http(), }); const arcClient = createWalletClient({ chain: arcTestnet, transport: http(), account, }); // ============ CCTP Flow Functions ============ async function approveUSDC() { console.log("Approving USDC transfer..."); const approveTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_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: [ETHEREUM_SEPOLIA_TOKEN_MESSENGER, 10_000_000n], // 10 USDC allowance }), }); console.log(`USDC Approval Tx: ${approveTx}`); await sepoliaPublicClient.waitForTransactionReceipt({ hash: approveTx }); } async function burnUSDC() { console.log("Burning USDC on Ethereum Sepolia..."); const burnTx = await sepoliaClient.sendTransaction({ to: ETHEREUM_SEPOLIA_TOKEN_MESSENGER, data: encodeFunctionData({ abi: [ { type: "function", name: "depositForBurn", 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" }, ], outputs: [], }, ], functionName: "depositForBurn", args: [ AMOUNT, ARC_TESTNET_DOMAIN, DESTINATION_ADDRESS_BYTES32 as `0x${string}`, ETHEREUM_SEPOLIA_USDC, DESTINATION_CALLER_BYTES32, maxFee, 1000, // minFinalityThreshold (1000 or less for Fast Transfer) ], }), }); console.log(`Burn Tx: ${burnTx}`); return burnTx; } async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${ETHEREUM_SEPOLIA_DOMAIN}?transactionHash=${transactionHash}`; while (true) { try { const response = await fetch(url, { method: "GET" }); if (!response.ok) { if (response.status !== 404) { const text = await response.text().catch(() => ""); console.error( "Error fetching attestation:", `${response.status} ${response.statusText}${ text ? ` - ${text}` : "" }`, ); } await new Promise((resolve) => setTimeout(resolve, 5000)); continue; } const data = (await response.json()) as AttestationResponse; if (data?.messages?.[0]?.status === "complete") { console.log("Attestation retrieved successfully!"); return data.messages[0]; } console.log("Waiting for attestation..."); await new Promise((resolve) => setTimeout(resolve, 5000)); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error("Error fetching attestation:", message); await new Promise((resolve) => setTimeout(resolve, 5000)); } } } async function mintUSDC(attestation: AttestationMessage) { console.log("Minting USDC on Arc testnet..."); const mintTx = await arcClient.sendTransaction({ to: ARC_TESTNET_MESSAGE_TRANSMITTER, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ attestation.message as `0x${string}`, attestation.attestation as `0x${string}`, ], }), }); console.log(`Mint Tx: ${mintTx}`); } // ============ Main Execution ============ async function main() { await approveUSDC(); const burnTx = await burnUSDC(); const attestation = await retrieveAttestation(burnTx); await mintUSDC(attestation); console.log("USDC transfer completed."); } main().catch(console.error); ``` </Tab> </Tabs> ## Step 5: Test the script Run the following command to execute the script: ```shell Shell theme={null} npm run start ``` Once the script runs and the transfer is finalized, a confirmation receipt is logged in the console. <Note> **Rate limit:** The attestation service rate limit is 40 requests per second. If you exceed this limit, the service blocks all API requests for the next five minutes and returns an HTTP 429 (Too Many Requests) response. </Note> # Transfer USDC from Solana to Arc Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-solana-to-arc Transfer USDC from Solana to an EVM blockchain using CCTP This guide demonstrates how to transfer USDC from Solana Devnet to Arc Testnet using CCTP. You use the [Solana Kit](https://github.com/anza-xyz/kit) library to interact with [Solana CCTP programs](/cctp/references/solana-programs), and viem to mint USDC on Arc Testnet. <Tip> **Use [Bridge Kit](https://www.npmjs.com/package/@circle-fin/bridge-kit) to simplify crosschain transfers with CCTP.** This quickstart shows how to transfer USDC from to &#x20;using a manual CCTP integration. The example is for learning or for developers who need a manual integration. To streamline this, use Bridge Kit to transfer USDC in just a few lines of code. </Tip> ## Prerequisites Before you begin, ensure that you've: * Installed [Node.js v22.18+](https://nodejs.org/) * Prepared a Solana wallet and have the private key array available * Funded your Solana wallet with the following testnet tokens: * Solana Devnet SOL (native token) from a [public faucet](https://faucet.solana.com/) * Solana Devnet USDC from the [Circle Faucet](https://faucet.circle.com) * Prepared an EVM testnet wallet with the private key available * Added Arc testnet network to your wallet ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)) * Funded your EVM wallet with Arc Testnet USDC from the [Circle Faucet](https://faucet.circle.com) if you choose the direct mint path below, because the destination wallet must pay gas to call `receiveMessage` ## Step 1. Set up the project ### 1.1. Create the project and install dependencies ```shell theme={null} # Set up your directory and initialize a Node.js project mkdir cctp-solana-transfer cd cctp-solana-transfer npm init -y # Set up module type and start command npm pkg set type=module npm pkg set scripts.start="node --env-file=.env index.ts" # Install runtime dependencies npm install @solana/kit @solana-program/system @solana-program/token viem # Install dev dependencies npm install --save-dev @types/node typescript ``` ### 1.2. Configure TypeScript (optional) <Tip> This step is optional. It helps prevent missing types in your IDE or editor. </Tip> Create a `tsconfig.json` file: ```shell theme={null} npx tsc --init ``` Then, update the `tsconfig.json` file: ```shell theme={null} cat <<'EOF' > tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "types": ["node"] } } EOF ``` ### 1.3. Set environment variables Open `.env` in your editor and add: ```text theme={null} SOLANA_PRIVATE_KEY=YOUR_SOLANA_PRIVATE_KEY_ARRAY EVM_PRIVATE_KEY=YOUR_ARC_PRIVATE_KEY ``` * `SOLANA_PRIVATE_KEY` is the private key array for the Solana Devnet wallet that signs the source-chain burn transaction. * `EVM_PRIVATE_KEY` is used to derive the Arc recipient address. The direct-mint path also uses this key to submit the destination mint on Arc. <Tip> Open `.env` in your editor rather than writing values with shell commands, and add `.env` to your `.gitignore`. This prevents credentials from leaking into your shell history or version control. </Tip> The `npm run start` command loads variables from `.env` using Node.js native env-file support. <Warning> This example uses one or more private keys for local testing. In production, use a secure key management solution and never expose or share private keys. </Warning> ## Step 2: Configure the script Define the configuration constants for interacting with Solana and Arc Testnet. ### 2.1. Setup chains and wallets The script predefines the program addresses, transfer amount, and other parameters: ```ts TypeScript expandable theme={null} // Solana Configuration const SOLANA_RPC = "https://api.devnet.solana.com"; const SOLANA_WS = "wss://api.devnet.solana.com"; const rpc = createSolanaRpc(SOLANA_RPC); const rpcSubscriptions = createSolanaRpcSubscriptions(SOLANA_WS); const solanaPrivateK
프로젝트로 돌아가기