Circle USYC 백서

Circle USYC

원문

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

ts for the next five minutes and returns an HTTP 429 (Too Many Requests) response. </Note> </Tab> </Tabs> # Transfer USDC between Stellar and Arc Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-stellar-arc Build scripts to transfer USDC between Arc Testnet and Stellar Testnet using CCTP Use CCTP to transfer USDC with Stellar Testnet as the source or destination. <Note> On Stellar, USDC precision and address encoding differ from other CCTP-supported blockchains. Before you integrate beyond these examples, read [CCTP on Stellar](/cctp/references/stellar). </Note> Pick the tab that matches the direction of your transfer. <Tabs> <Tab title="Stellar to Arc"> This quickstart demonstrates how to transfer USDC from Stellar Testnet to Arc Testnet using CCTP. You use the [@stellar/stellar-sdk](https://github.com/stellar/js-stellar-sdk) library to interact with Stellar Soroban contracts, and [`viem`](https://viem.sh/) to mint USDC on Arc Testnet. When you finish, you will have executed a full burn-attest-mint flow. You should be comfortable using a terminal and Node.js. Familiarity with Stellar Soroban transactions and basic EVM usage helps you follow and adapt the script. Examples use Arc Testnet as the destination, but you can use any [supported blockchain](/cctp/concepts/supported-chains-and-domains). ## Prerequisites Before you begin this tutorial, ensure you have: * Installed [Node.js v22.18+](https://nodejs.org/) * Prepared an EVM wallet with the private key available for Arc Testnet * Added the Arc Testnet network to your wallet ([network details](https://docs.arc.io/arc/references/connect-to-arc#wallet-setup)) * Funded your wallet with Arc Testnet USDC (for gas fees) from the [Circle Faucet](https://faucet.circle.com) * Prepared a Stellar Testnet wallet with the secret key (`S...`) available * Funded your Stellar wallet with testnet XLM from the [Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees on Stellar Testnet) * Established a [USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) on your Stellar account so you can hold the testnet USDC you burn * Funded your Stellar wallet with Stellar Testnet USDC from the [Circle Faucet](https://faucet.circle.com) <Tip> You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund accounts and establish USDC trustlines. </Tip> ## Step 1: Set up the project This step shows you how to prepare your project and environment. ### 1.1. Set up your development environment Create a new directory and install the required dependencies: ```bash Shell theme={null} # Set up your directory and initialize a Node.js project mkdir cctp-stellar-to-arc cd cctp-stellar-to-arc 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 @stellar/stellar-sdk viem # Install dev dependencies npm install --save-dev typescript @types/node ``` ### 1.2. Initialize and configure the project <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. Configure environment variables Open .env in your editor and add: ```text theme={null} STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY ``` * `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban transactions on Stellar Testnet. * `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc Testnet. <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 contract addresses, amounts, and clients for Stellar Testnet and Arc Testnet. ### 2.1. Define configuration constants The script predefines the contract addresses, transfer amount, and maximum fee. ```ts TypeScript theme={null} import { Address, Contract, Keypair, nativeToScVal, rpc, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; import { createPublicClient, createWalletClient, encodeFunctionData, http, pad, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // Contract Addresses const STELLAR_TOKEN_MESSENGER_MINTER = "CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP"; const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; // Transfer Parameters const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals) const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals) // Chain-specific Parameters const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet // Stellar Soroban Configuration const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"; // Authentication const stellarKeypair = Keypair.fromSecret( process.env.STELLAR_SECRET_KEY as string, ); ``` ### 2.2. Set up wallet clients The wallet client configures the appropriate network settings using `viem`. In this example, the script connects to Arc Testnet. ```ts TypeScript theme={null} const evmAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}`, ); const arcWalletClient = createWalletClient({ chain: arcTestnet, transport: http(), account: evmAccount, }); const arcPublicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); ``` ### 2.3. Add helper function The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban contract transaction. ```ts TypeScript theme={null} async function submitSorobanTx( server: rpc.Server, contractId: string, method: string, args: xdr.ScVal[], ) { const account = await server.getAccount(stellarKeypair.publicKey()); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { fee: "10000000", networkPassphrase: STELLAR_NETWORK_PASSPHRASE, }) .addOperation(contract.call(method, ...args)) .setTimeout(120) .build(); const simulated = await server.simulateTransaction(tx); if (rpc.Api.isSimulationError(simulated)) { throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`); } const prepared = rpc.assembleTransaction(tx, simulated).build(); prepared.sign(stellarKeypair); const sendResult = await server.sendTransaction(prepared); if (sendResult.status === "ERROR") { throw new Error(`Send failed: ${JSON.stringify(sendResult)}`); } let getResult = await server.getTransaction(sendResult.hash); while (getResult.status === "NOT_FOUND") { await new Promise((resolve) => setTimeout(resolve, 2000)); getResult = await server.getTransaction(sendResult.hash); } if (getResult.status !== "SUCCESS") { throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`); } return sendResult.hash; } ``` ## Step 3: Implement the transfer logic This step implements the core transfer logic: approve and burn on Stellar, poll for an attestation, then mint on Arc. A successful run prints transaction hashes and a completion message in the console. ### 3.1. Approve USDC on Stellar Approve the `TokenMessengerMinterV2` contract to spend your USDC. The `submitSorobanTx` helper is used to submit the approve call to the Stellar USDC contract. ```ts TypeScript theme={null} async function approveUSDC() { console.log("Approving USDC spend on Stellar..."); const server = new rpc.Server(STELLAR_RPC_URL); const latestLedger = await server.getLatestLedger(); const expirationLedger = latestLedger.sequence + 100_000; const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [ new Address(stellarKeypair.publicKey()).toScVal(), new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(expirationLedger, { type: "u32" }), ]); console.log(`Approve Tx: ${approveHash}`); } ``` ### 3.2. Burn USDC on Stellar Call `deposit_for_burn` to burn USDC on Stellar. The `submitSorobanTx` helper is used to submit the burn call with the transfer parameters listed below: * **Burn amount**: The amount of USDC to burn (in Stellar subunits, 7 decimals) * **Destination domain**: The target blockchain for minting USDC (see [supported blockchains and domains](/cctp/concepts/supported-chains-and-domains)) * **Mint recipient**: The wallet address that receives the minted USDC on Arc * **Burn token**: The contract address of the USDC token on Stellar * **Destination caller**: The address on the target blockchain that may call `receiveMessage` * **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer (in Stellar subunits, 7 decimals) * **Finality threshold**: 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 Stellar..."); // Bytes32 Formatted Parameters const evmAddressBytes32 = pad(evmAccount.address); const mintRecipient = xdr.ScVal.scvBytes( Buffer.from(evmAddressBytes32.slice(2), "hex"), ); const server = new rpc.Server(STELLAR_RPC_URL); const txHash = await submitSorobanTx( server, STELLAR_TOKEN_MESSENGER_MINTER, "deposit_for_burn", [ new Address(stellarKeypair.publicKey()).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }), mintRecipient, new Address(STELLAR_USDC).toScVal(), xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller nativeToScVal(MAX_FEE, { type: "i128" }), nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold ], ); console.log(`Burn Tx: ${txHash}`); return txHash; } ``` ### 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 `STELLAR_DOMAIN` for the `sourceDomain` path parameter, using the [CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers) for Stellar Testnet (27). * Pass `transactionHash` from the value returned by `submitSorobanTx` 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/${STELLAR_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 on Arc Call the [`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage) from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses) deployed on Arc Testnet to mint USDC on the destination blockchain. * Pass the signed attestation and the message bytes as parameters. * The contract verifies the attestation and mints USDC to the recipient encoded in the CCTP message. ```ts TypeScript theme={null} async function mintUSDCOnArc(attestation: AttestationMessage) { console.log("Minting USDC on Arc Testnet..."); const hash = await arcWalletClient.sendTransaction({ to: ARC_MESSAGE_TRANSMITTER as `0x${string}`, 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}`, ], }), }); await arcPublicClient.waitForTransactionReceipt({ hash }); console.log(`Mint Tx: ${hash}`); } ``` ## Step 4: Full script Create an `index.ts` file in your project directory and paste the full script below so you can run the flow from one file. ```ts index.ts expandable theme={null} import { Address, Contract, Keypair, nativeToScVal, rpc, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; import { createPublicClient, createWalletClient, encodeFunctionData, http, pad, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationMessage { message: string; attestation: string; status: string; } interface AttestationResponse { messages: AttestationMessage[]; } // ============ Configuration Constants ============ // Contract Addresses const STELLAR_TOKEN_MESSENGER_MINTER = "CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP"; const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; // Transfer Parameters const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals) const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals) // Chain-specific Parameters const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet // Stellar Soroban Configuration const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org"; const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"; // Authentication const stellarKeypair = Keypair.fromSecret( process.env.STELLAR_SECRET_KEY as string, ); // Set up wallet clients const evmAccount = privateKeyToAccount( process.env.EVM_PRIVATE_KEY as `0x${string}`, ); const arcWalletClient = createWalletClient({ chain: arcTestnet, transport: http(), account: evmAccount, }); const arcPublicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); async function submitSorobanTx( server: rpc.Server, contractId: string, method: string, args: xdr.ScVal[], ) { const account = await server.getAccount(stellarKeypair.publicKey()); const contract = new Contract(contractId); const tx = new TransactionBuilder(account, { fee: "10000000", networkPassphrase: STELLAR_NETWORK_PASSPHRASE, }) .addOperation(contract.call(method, ...args)) .setTimeout(120) .build(); const simulated = await server.simulateTransaction(tx); if (rpc.Api.isSimulationError(simulated)) { throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`); } const prepared = rpc.assembleTransaction(tx, simulated).build(); prepared.sign(stellarKeypair); const sendResult = await server.sendTransaction(prepared); if (sendResult.status === "ERROR") { throw new Error(`Send failed: ${JSON.stringify(sendResult)}`); } let getResult = await server.getTransaction(sendResult.hash); while (getResult.status === "NOT_FOUND") { await new Promise((resolve) => setTimeout(resolve, 2000)); getResult = await server.getTransaction(sendResult.hash); } if (getResult.status !== "SUCCESS") { throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`); } return sendResult.hash; } // ============ CCTP Flow Functions ============ async function approveUSDC() { console.log("Approving USDC spend on Stellar..."); const server = new rpc.Server(STELLAR_RPC_URL); const latestLedger = await server.getLatestLedger(); const expirationLedger = latestLedger.sequence + 100_000; const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [ new Address(stellarKeypair.publicKey()).toScVal(), new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(expirationLedger, { type: "u32" }), ]); console.log(`Approve Tx: ${approveHash}`); } async function burnUSDC() { console.log("Burning USDC on Stellar..."); // Bytes32 Formatted Parameters const evmAddressBytes32 = pad(evmAccount.address); const mintRecipient = xdr.ScVal.scvBytes( Buffer.from(evmAddressBytes32.slice(2), "hex"), ); const server = new rpc.Server(STELLAR_RPC_URL); const txHash = await submitSorobanTx( server, STELLAR_TOKEN_MESSENGER_MINTER, "deposit_for_burn", [ new Address(stellarKeypair.publicKey()).toScVal(), nativeToScVal(AMOUNT, { type: "i128" }), nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }), mintRecipient, new Address(STELLAR_USDC).toScVal(), xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller nativeToScVal(MAX_FEE, { type: "i128" }), nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold ], ); console.log(`Burn Tx: ${txHash}`); return txHash; } async function retrieveAttestation(transactionHash: string) { console.log("Retrieving attestation..."); const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_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 mintUSDCOnArc(attestation: AttestationMessage) { console.log("Minting USDC on Arc Testnet..."); const hash = await arcWalletClient.sendTransaction({ to: ARC_MESSAGE_TRANSMITTER as `0x${string}`, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stat
프로젝트로 돌아가기