Circle USYC 백서

Circle USYC

원문

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

Step 4: Full script Create an `index.ts` file in your project directory and paste the full script following. Keep `receive_message.mv` in the `precompiled-move-scripts` directory next to it. ```ts index.ts expandable theme={null} import { Account, Aptos, AptosConfig, Ed25519PrivateKey, MoveVector, Network, } from "@aptos-labs/ts-sdk"; import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { createPublicClient, createWalletClient, http, parseAbi, type Address, type Hex, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; type Attestation = { message: string; attestation: string; }; const evmPrivateKey = process.env.EVM_PRIVATE_KEY; const aptosPrivateKey = process.env.APTOS_PRIVATE_KEY; if (!evmPrivateKey || !aptosPrivateKey) { throw new Error("Set EVM_PRIVATE_KEY and APTOS_PRIVATE_KEY in .env"); } const APTOS_DOMAIN = 9; const ARC_TESTNET_DOMAIN = 26; const ARC_USDC_ADDRESS = "0x3600000000000000000000000000000000000000" as Address; const TOKEN_MESSENGER_V2_ADDRESS = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA" as Address; const BYTECODE_PATH = fileURLToPath( new URL("./precompiled-move-scripts/receive_message.mv", import.meta.url), ); const AMOUNT = 1_000_000n; // 1 USDC, 6 decimals const FAST_TRANSFER = process.env.FAST_TRANSFER === "true"; const MIN_FINALITY_THRESHOLD = FAST_TRANSFER ? 1_000 : 2_000; const MAX_FEE = FAST_TRANSFER ? BigInt(process.env.FAST_MAX_FEE ?? "100") : 0n; const IRIS_API_URL = "https://iris-api-sandbox.circle.com"; const account = privateKeyToAccount(evmPrivateKey as Hex); const walletClient = createWalletClient({ account, chain: arcTestnet, transport: http(), }); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); const aptosClient = new Aptos(new AptosConfig({ network: Network.TESTNET })); const aptosAccount = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(aptosPrivateKey), }); const usdcAbi = parseAbi([ "function approve(address spender, uint256 amount) returns (bool)", ]); const tokenMessengerV2Abi = parseAbi([ "function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold)", ]); async function approveUSDC() { console.log("Approving Arc USDC transfer..."); const approvalHash = await walletClient.writeContract({ account, chain: arcTestnet, address: ARC_USDC_ADDRESS, abi: usdcAbi, functionName: "approve", args: [TOKEN_MESSENGER_V2_ADDRESS, AMOUNT], }); const approvalReceipt = await publicClient.waitForTransactionReceipt({ hash: approvalHash, }); if (approvalReceipt.status !== "success") { throw new Error(`USDC approval reverted: ${approvalHash}`); } console.log(`Approval confirmed: ${approvalHash}`); } async function burnUSDC() { const transferMode = FAST_TRANSFER ? "Fast" : "Standard"; console.log( `Burning ${Number(AMOUNT) / 1_000_000} USDC on Arc (${transferMode} Transfer)...`, ); const mintRecipient = aptosAccount.accountAddress.toString() as Hex; const destinationCaller = `0x${"0".repeat(64)}` as Hex; const burnHash = await walletClient.writeContract({ account, chain: arcTestnet, address: TOKEN_MESSENGER_V2_ADDRESS, abi: tokenMessengerV2Abi, functionName: "depositForBurn", args: [ AMOUNT, APTOS_DOMAIN, mintRecipient, ARC_USDC_ADDRESS, destinationCaller, MAX_FEE, MIN_FINALITY_THRESHOLD, ], }); const burnReceipt = await publicClient.waitForTransactionReceipt({ hash: burnHash, }); if (burnReceipt.status !== "success") { throw new Error(`Deposit-for-burn reverted: ${burnHash}`); } console.log(`Deposit-for-burn confirmed: ${burnHash}`); return burnHash; } async function retrieveAttestation( transactionHash: string, ): Promise<Attestation> { const url = `${IRIS_API_URL}/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`; console.log(`Polling Iris V2 for attestation: ${url}`); while (true) { const response = await fetch(url); if (!response.ok && response.status !== 404) { throw new Error(`Iris V2 returned HTTP ${response.status}`); } if (response.ok) { const data = (await response.json()) as { messages?: Array<{ message?: string; attestation?: string }>; }; const result = data.messages?.[0]; if ( result?.message && result.attestation && result.attestation !== "PENDING" ) { return { message: result.message, attestation: result.attestation, }; } } await new Promise((resolve) => setTimeout(resolve, 2_000)); } } async function receiveMessage({ message, attestation }: Attestation) { console.log("Receiving the message on Aptos..."); const bytecode = new Uint8Array(await readFile(BYTECODE_PATH)); const transaction = await aptosClient.transaction.build.simple({ sender: aptosAccount.accountAddress, data: { bytecode, functionArguments: [ MoveVector.U8(Buffer.from(message.replace("0x", ""), "hex")), MoveVector.U8(Buffer.from(attestation.replace("0x", ""), "hex")), ], }, }); const pendingTransaction = await aptosClient.signAndSubmitTransaction({ signer: aptosAccount, transaction, }); const receiveTransaction = await aptosClient.waitForTransaction({ transactionHash: pendingTransaction.hash, }); console.log( `Receive-message confirmed: https://explorer.aptoslabs.com/txn/${receiveTransaction.hash}`, ); return receiveTransaction.hash; } export default async function main() { await approveUSDC(); const burnTransactionHash = await burnUSDC(); const attestation = await retrieveAttestation(burnTransactionHash); console.log("Attestation received from Iris V2."); const receiveTransactionHash = await receiveMessage(attestation); console.log("USDC transfer completed."); return { evmTransactionHash: burnTransactionHash, aptosTransactionHash: receiveTransactionHash, }; } if ( process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href ) { console.log("Starting CCTP flow..."); main().catch((error: unknown) => { console.error(error instanceof Error ? error.message : error); process.exitCode = 1; }); } ``` ## Step 5: Test the script Run the following command to execute the script: ```shell Shell theme={null} npm run start ``` When the transfer finishes, the console logs a completion message and the relevant transaction hashes. Successful output looks similar to the following: ```bash Shell theme={null} Starting CCTP flow... Approving Arc USDC transfer... Approval confirmed: 0x... Burning 1 USDC on Arc (Fast Transfer)... Deposit-for-burn confirmed: 0x... Polling Iris V2 for attestation: https://iris-api-sandbox.circle.com/v2/messages/26?transactionHash=<arc-transaction-hash> Attestation received from Iris V2. Receiving the message on Aptos... Receive-message confirmed: https://explorer.aptoslabs.com/txn/<aptos-transaction-hash> USDC transfer completed. ``` Attestation polling can take several minutes depending on network conditions and the finality threshold you chose. The script retries every 2 seconds with no timeout, so allow the process to continue while Iris prepares the attestation. <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> </Tab> </Tabs> # Transfer USDC from Ethereum to Arc Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-ethereum-to-arc Build a script to transfer USDC between EVM blockchains using CCTP This guide demonstrates how to transfer USDC from Ethereum Sepolia to Arc testnet using CCTP. You use the [viem](https://viem.sh/) framework to interact with [CCTP contracts](/cctp/references/contract-addresses) and the [CCTP API](/api-reference/cctp/all/get-messages-v2) to retrieve attestations. <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 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 wallet with the following testnet tokens: * Sepolia ETH (native token) from a [public faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) * Sepolia USDC from the [Circle Faucet](https://faucet.circle.com) * 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-evm-transfer cd cctp-evm-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 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} PRIVATE_KEY=YOUR_ETHEREUM_SEPOLIA_PRIVATE_KEY ``` * `PRIVATE_KEY` is the private key for the Ethereum Sepolia EOA that signs the source-chain approval and burn transactions. The direct-mint path also uses the same 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 This section covers the necessary setup for the transfer script, including defining keys and addresses, and configuring the wallet client for interacting with the source and destination chains. ### 2.1. Define configuration constants The script predefines the contract addresses, transfer amount, and maximum fee. Update the `DESTINATION_ADDRESS` with your wallet address. For simplicity, this quickstart uses the same EOA as the Ethereum Sepolia source signer and the Arc recipient. In production, these can be different addresses. ```ts TypeScript theme={null} // 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 ``` ### 2.2. Set up wallet clients The wallet client configures the appropriate network settings using `viem`. The direct-mint path below uses clients for both Ethereum Sepolia and Arc testnet. The [Forwarding Service](/cctp/concepts/forwarding-service) path only needs the source-chain client on Ethereum Sepolia. ```ts TypeScript theme={null} // Set up the wallet clients const sepoliaClient = createWalletClient({ chain: sepolia, transport: http(), account, }); const arcClient = createWalletClient({ chain: arcTestnet, transport: http(), account, }); ``` ## Step 3: Implement the transfer logic The following sections outline the core transfer logic. The path diverges at the source-chain burn transaction: * **Direct mint** uses `depositForBurn`, then retrieves an attestation and calls `receiveMessage` on Arc. * **Forwarding Service** uses `depositForBurnWithHook`, then lets Circle handle the destination-side mint on Arc. <Tabs> <Tab title="Forwarding Service"> ### 3.1. Get forwarding fees and calculate the burn amount Before you burn USDC with the Forwarding Service, query the CCTP fee endpoint with `forward=true`. The forwarding fee is dynamic, so fetch it immediately before the transfer. The returned `maxFee` must cover both the CCTP protocol fee and the forwarding fee. ```ts TypeScript theme={null} const FORWARDING_SERVICE_HOOK_DATA = "0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`; async function getForwardingFees() { 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()}`); } return response.json(); } async function calculateForwardingAmounts() { const fees = await getForwardingFees(); const feeData = fees.find( (fee: { finalityThreshold: number }) => fee.finalityThreshold === 1000, ); if (!feeData) { throw new Error("Fast-transfer forwarding fees not available"); } 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; return { maxFee, totalAmount }; } ``` ### 3.2. Approve the total burn amount Approve the total amount you will burn on the source chain. For the forwarding path, that is the transfer amount plus the forwarding and protocol fees. ```ts TypeScript theme={null} 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}`); } ``` ### 3.3. Burn USDC with the Forwarding Service hook Use `depositForBurnWithHook` on the source chain. The forwarding hook data tells Circle to handle the destination-side `receiveMessage` call on Arc. ```ts TypeScript theme={null} 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; } ``` ### 3.4. Verify the forwarded mint After the burn is confirmed, poll the Iris API until it returns a `forwardTxHash`. That hash is the Arc destination mint transaction submitted by Circle. In the forwarding path, `forwardTxHash` is the completion signal for the destination-side mint. You do not need to retrieve an attestation and call `receiveMessage` yourself. ```ts TypeScript theme={null} 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)); } } ``` </Tab> <Tab title="Direct mint"> ### 3.1. Approve USDC Grant approval for the [`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on Ethereum Sepolia to withdraw USDC from your wallet. This allows the contract to burn USDC when you initiate the transfer. ```ts TypeScript theme={null} 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}`); } ``` ### 3.2. Burn USDC Call the `depositForBurn` function from the [`TokenMessengerV2` contract](/cctp/references/contract-interfaces#depositforburn) deployed on Ethereum Sepolia to burn USDC on that source chain. You specify the following parameters: * **Burn amount**: The amount of USDC to burn * **Destination domain**: The target blockchain for minting USDC (see [supported chains and domains](/cctp/concepts/supported-chains-and-domains)) * **Mint recipient**: The wallet address that will receive the minted USDC * **Burn token**: The contract address of the USDC token being burned on the source chain * **Destination caller**: The address on the target chain to call `receiveMessage` * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer * **Finality t
프로젝트로 돌아가기