Circle USYC 백서

Circle USYC

원문

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

3:17:02.453Z" } ``` The response includes: * `allowance`: The remaining Fast Transfer allowance in USDC units * `lastUpdated`: The UTC timestamp when the allowance was last updated <Note> **Fast Transfer allowance details:** * The allowance represents the total value of USDC that can be minted through Fast Transfer before related burns on source chains reach hard finality. * When you initiate a Fast Transfer, the burn amount temporarily debits the allowance. * Once the burn reaches finality on the source chain, the corresponding amount is credited back to the allowance. * If the allowance is insufficient for your transfer, you should either wait for the allowance to replenish or use Standard Transfer instead. </Note> # Get the fee for your transfer Source: https://developers.circle.com/cctp/howtos/get-transfer-fee Retrieve CCTP transfer fees using the API This guide shows you how to retrieve the fee for a USDC transfer using the CCTP API. Fees vary based on the source and destination blockchains, and whether you use Fast Transfer or Standard Transfer. ## Prerequisites Before you begin, ensure you have: * Installed cURL on your development machine ## Get the transfer fee Call the [`GET /v2/burn/USDC/fees`](/api-reference/cctp/all/get-burn-usdc-fees) endpoint to retrieve the fees for transferring USDC between two blockchains. **Request parameters** * `sourceDomainId`: The [domain ID](/cctp/concepts/supported-chains-and-domains#domain-identifiers) of the source blockchain * `destDomainId`: The domain ID of the destination blockchain **Example request** ```shell Shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/0/26' \ --header 'Accept: application/json' ``` This example retrieves the fees for transferring USDC from Ethereum Sepolia (domain 0) to Arc Testnet (domain 26). **Response** ```json theme={null} [ { "finalityThreshold": 1000, "minimumFee": 1 }, { "finalityThreshold": 2000, "minimumFee": 0 } ] ``` <Note> **Fee details:** * Fees are specified in basis points (bps), for example, 1 = 0.01%. * Fast Transfer fees vary by route. * You specify the maximum fee you're willing to pay when calling `depositForBurn`. The actual fee charged will not exceed this amount. </Note> # Resolve attestation issues Source: https://developers.circle.com/cctp/howtos/resolve-stuck-attestation Troubleshoot and resolve common problems with CCTP attestations This guide helps you resolve issues when a CCTP attestation takes longer than expected to become available or when the attestation API returns unexpected responses. ## Understanding attestation timing After a successful burn transaction, Circle's Attestation Service (Iris) must: 1. Observe the burn event on the source blockchain 2. Wait for sufficient block confirmations 3. Sign the message and make the attestation available through the [CCTP API](/api-reference/cctp/all/get-messages-v2) This process takes different amounts of time depending on the transfer type: | Transfer type | Finality threshold | Typical wait time | | - | - | - | | Fast Transfer | ≤ 1000 | Seconds to a few minutes | | Standard Transfer | ≥ 2000 | Varies by blockchain (see [Finality and block confirmations](/cctp/concepts/finality-and-block-confirmations)) | ## Why 404 responses are expected The attestation API returns a 404 response until the attestation service has observed and processed your burn transaction. This is expected and does not indicate an error. The API returns 404 when: * The burn transaction hasn't reached the required block confirmations * The attestation service hasn't yet indexed the transaction * The transaction hash or domain ID is incorrect Don't treat 404 as a failure. Instead, implement polling with appropriate intervals. ## Check attestation status Query the [`GET /v2/messages`](/api-reference/cctp/all/get-messages-v2) endpoint to check the current status of your attestation. The following table explains the possible responses and what to do next: | Response | Meaning | Action | | - | - | - | | 404 | Attestation not yet observed | Continue polling until the attestation is available | | `{ "messages": [] }` | Transaction found but not processed | Continue polling until the transaction is processed | | `{ "status": "pending" }` | Awaiting block confirmations | Continue polling until the block confirmations are reached | | `{ "status": "complete" }` | Attestation ready | Proceed to mint | ## Implement effective polling Poll the attestation API at regular intervals without exceeding rate limits: ```ts TypeScript theme={null} async function waitForAttestation( sourceDomain: number, transactionHash: string, ) { const pollInterval = 5000; // Poll every 5 seconds const maxWaitTime = 1200000; // 20 minutes maximum const startTime = Date.now(); while (Date.now() - startTime < maxWaitTime) { try { const response = await fetch( `https://iris-api-sandbox.circle.com/v2/messages/${sourceDomain}?transactionHash=${transactionHash}`, ); // 404 is expected while waiting - continue polling if (response.status === 404) { console.log("Attestation not yet available (404), waiting..."); await new Promise((resolve) => setTimeout(resolve, pollInterval)); continue; } // Rate limited - wait before retrying if (response.status === 429) { console.log("Rate limited, waiting 5 minutes..."); await new Promise((resolve) => setTimeout(resolve, 300000)); continue; } if (!response.ok) { throw new Error(`Unexpected HTTP error: ${response.status}`); } const data = await response.json(); // Empty messages array - transaction found but not processed if (!data.messages || data.messages.length === 0) { console.log("Transaction found, awaiting processing..."); await new Promise((resolve) => setTimeout(resolve, pollInterval)); continue; } const message = data.messages[0]; // Attestation complete if (message.status === "complete" && message.attestation) { console.log("Attestation retrieved successfully!"); return { message: message.message, attestation: message.attestation, decodedMessage: message.decodedMessage, }; } // Still pending console.log(`Attestation status: ${message.status}`); await new Promise((resolve) => setTimeout(resolve, pollInterval)); } catch (error) { console.error("Error fetching attestation:", (error as Error).message); await new Promise((resolve) => setTimeout(resolve, pollInterval)); } } throw new Error("Attestation not received within maximum wait time"); } ``` ### Avoid rate limiting The attestation service limits requests to 35 per second. If you exceed this limit, the service blocks all API requests for 5 minutes and returns HTTP 429. Best practices: * Use a poll interval of at least 5 seconds * Implement exponential back-off for repeated 429 responses * Don't poll from multiple clients for the same transaction ## Troubleshooting checklist If your attestation isn't available after the expected wait time: <Steps> <Step title="Verify the burn transaction"> Check the source blockchain's block explorer to confirm: * The transaction succeeded (not reverted) * The transaction is included in a mined block * Sufficient blocks have been confirmed since the transaction </Step> <Step title="Confirm request parameters"> Verify you're using the correct: * Source domain ID (see [Supported Chains and Domains](/cctp/concepts/supported-chains-and-domains)) * Transaction hash (full hash, including `0x` prefix for EVM chains) * API environment (sandbox vs. production) </Step> <Step title="Check finality requirements"> Standard Transfers require full finality. For some blockchains, this can take significantly longer than Fast Transfers. Check [Finality and block confirmations](/cctp/concepts/finality-and-block-confirmations) for expected times. </Step> <Step title="Verify API connectivity"> Test that you can reach the API: ```shell Shell theme={null} curl --request GET \ --url 'https://iris-api-sandbox.circle.com/v2/publicKeys' \ --header 'Accept: application/json' ``` If this fails, check your network connectivity and firewall settings. </Step> </Steps> # Retry a failed mint Source: https://developers.circle.com/cctp/howtos/retry-failed-mint Complete a CCTP transfer when the mint transaction fails This guide helps you complete a CCTP transfer when you have a valid attestation but the mint transaction on the destination blockchain fails or was never submitted. ## Minting is safe to retry CCTP minting is idempotent. Each attestation contains a unique nonce that can only be used once. If you submit the same attestation multiple times, only the first successful transaction mints USDC. Subsequent attempts revert with a "nonce already used" error but don't result in duplicate minting. This means you can safely retry a failed mint without risking double-spending. ## Common mint failure reasons | Failure reason | Symptoms | Solution | | - | - | - | | Insufficient gas | Transaction reverts or times out | Increase gas limit and retry | | Nonce already used | Transaction reverts with nonce error | The mint already succeeded; check recipient balance | | Wrong contract address | Transaction may succeed with no USDC minted | Verify you're using the correct `MessageTransmitterV2` address for the destination blockchain | | Destination caller restriction | Transaction reverts | Check if the burn specified a `destinationCaller`; only that address can mint | | Token account doesn't exist (Solana) | Transaction fails | Create the recipient's USDC token account first | | Attestation expired | Transaction reverts | Use re-attestation API to get a fresh attestation | ## Verify the current state Before retrying, check whether the mint already succeeded: <Steps> <Step title="Check recipient balance"> Query the recipient's USDC balance on the destination blockchain. If the expected amount is present, the mint already completed. </Step> <Step title="Check for existing mint transaction"> Search the destination blockchain's block explorer for `receiveMessage` transactions from your wallet to the `MessageTransmitterV2` contract. </Step> <Step title="Check attestation status"> Query the [attestation API](/api-reference/cctp/all/get-messages-v2) to confirm you have a `complete` status: </Step> </Steps> ## Retry the mint transaction If the mint hasn't completed, submit a new `receiveMessage` transaction using your attestation. <Tabs> <Tab title="EVM chains"> Call `receiveMessage` on the `MessageTransmitterV2` contract: ```ts TypeScript theme={null} import { createWalletClient, createPublicClient, http, encodeFunctionData, } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { arcTestnet } from "viem/chains"; interface AttestationData { message: string; attestation: string; } const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY!; const account = privateKeyToAccount(PRIVATE_KEY as `0x${string}`); const walletClient = createWalletClient({ chain: arcTestnet, transport: http(), account, }); const publicClient = createPublicClient({ chain: arcTestnet, transport: http(), }); // MessageTransmitterV2 contract address - verify for your destination chain // See: https://developers.circle.com/cctp/references/contract-addresses const MESSAGE_TRANSMITTER_V2 = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; async function retryMint(data: AttestationData) { console.log("Retrying mint transaction..."); try { const txHash = await walletClient.sendTransaction({ to: MESSAGE_TRANSMITTER_V2, data: encodeFunctionData({ abi: [ { type: "function", name: "receiveMessage", stateMutability: "nonpayable", inputs: [ { name: "message", type: "bytes" }, { name: "attestation", type: "bytes" }, ], outputs: [], }, ], functionName: "receiveMessage", args: [ data.message as `0x${string}`, data.attestation as `0x${string}`, ], }), }); console.log(`Mint transaction submitted: ${txHash}`); // Wait for confirmation const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, }); if (receipt.status === "success") { console.log("Mint successful!"); return { success: true, txHash }; } else { console.log("Mint transaction reverted"); return { success: false, txHash }; } } catch (error) { // Check if the error indicates nonce already used const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("nonce") || errorMessage.includes("already")) { console.log( "Nonce already used - mint may have already completed. Check recipient balance.", ); } throw error; } } // Use attestation data from the API const attestationData: AttestationData = { message: "0x00000001000000000000001a...", // Full message hex from API attestation: "0xde09db65dea64090570d8143...", // Full attestation hex from API }; await retryMint(attestationData); ``` </Tab> <Tab title="Solana"> Call `receiveMessage` on the `MessageTransmitterV2` program. For Solana, ensure the recipient's USDC token account exists before calling `receiveMessage`. ```ts TypeScript theme={null} import crypto from "crypto"; import { address, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, getAddressEncoder, getProgramDerivedAddress, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, signTransactionMessageWithSigners, } from "@solana/kit"; import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system"; import { TOKEN_PROGRAM_ADDRESS } from "@solana-program/token"; interface AttestationData { message: string; attestation: string; } // 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 solanaPrivateKey = JSON.parse(process.env.SOLANA_PRIVATE_KEY!); const solanaKeypair = await createKeyPairSignerFromBytes( Uint8Array.from(solanaPrivateKey), ); // Solana CCTP Program Addresses (Devnet) const MESSAGE_TRANSMITTER_PROGRAM = address( "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC", ); const TOKEN_MESSENGER_MINTER_PROGRAM = address( "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe", ); const USDC_MINT = address("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); const ASSOCIATED_TOKEN_PROGRAM = address( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", ); async function retryMintOnSolana( attestationData: AttestationData, sourceDomain: number, ) { console.log("Retrying mint on Solana..."); const addressEncoder = getAddressEncoder(); // Derive receiver's USDC token account const [receiverUsdcAccount] = await getProgramDerivedAddress({ programAddress: ASSOCIATED_TOKEN_PROGRAM, seeds: [ addressEncoder.encode(solanaKeypair.address), addressEncoder.encode(TOKEN_PROGRAM_ADDRESS), addressEncoder.encode(USDC_MINT), ], }); // Derive required PDAs const [messageTransmitter] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter")], }); const [authorityPda] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("message_transmitter_authority")], }); // Calculate used nonces PDA const messageBytes = Buffer.from(attestationData.message.slice(2), "hex"); const nonce = messageBytes.readBigUInt64BE(12); const firstNonce = (nonce / 6400n) * 6400n; const firstNonceBuffer = Buffer.alloc(8); firstNonceBuffer.writeBigUInt64BE(firstNonce); const sourceDomainBuffer = Buffer.alloc(4); sourceDomainBuffer.writeUInt32BE(sourceDomain); const [usedNonces] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [ new TextEncoder().encode("used_nonces"), sourceDomainBuffer, firstNonceBuffer, ], }); // Derive TokenMessengerMinterV2 PDAs const [tokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_messenger")], }); const [remoteTokenMessenger] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("remote_token_messenger"), new TextEncoder().encode(sourceDomain.toString()), ], }); const [tokenMinter] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("token_minter")], }); const [localToken] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("local_token"), addressEncoder.encode(USDC_MINT), ], }); const sourceTokenBytes = messageBytes.slice(133, 165); const [tokenPair] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("token_pair"), sourceDomainBuffer, sourceTokenBytes, ], }); const [custody] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [ new TextEncoder().encode("custody"), addressEncoder.encode(USDC_MINT), ], }); const [eventAuthority] = await getProgramDerivedAddress({ programAddress: MESSAGE_TRANSMITTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); const [tokenProgramEventAuthority] = await getProgramDerivedAddress({ programAddress: TOKEN_MESSENGER_MINTER_PROGRAM, seeds: [new TextEncoder().encode("__event_authority")], }); // Build instruction const discriminator = crypto .createHash("sha256") .update("global:receive_message") .digest() .slice(0, 8); const messageBuffer = Buffer.from(attestationData.message.slice(2), "hex"); const attestationBuffer = Buffer.from( attestationData.attestation.slice(2), "hex", ); const messageLenBuffer = Buffer.alloc(4); messageLenBuffer.writeUInt32LE(messageBuffer.length); const attestationLenBuffer = Buffer.alloc(4); attestationLenBuffer.writeUInt32LE(attestationBuffer.length); const instructionData = new Uint8Array( Buffer.concat([ discriminator, messageLenBuffer, messageBuffer, attestationLenBuffer, attestationBuffer, ]), ); const receiveMessageIx = { programAddress: MESSAGE_TRANSMITTER_PROGRAM, accounts: [ { address: solanaKeypair.address, role: 3, signer: solanaKeypair }, { address: solanaKeypair.address, role: 0 }, { address: authorityPda, role: 0 }, { address: messageTransmitter, role: 0 }, { address: usedNonces, role: 1 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, { address: SYSTEM_PROGRAM_ADDRESS, role: 0 }, { address: eventAuthority, role: 0 }, { address: MESSAGE_TRANSMITTER_PROGRAM, role: 0 }, { address: tokenMessenger, role: 0 }, { address: remoteTokenMessenger, role: 0 }, { address: tokenMinter, role: 1 }, { address: localToken, role: 1 }, { address: tokenPair, role: 0 }, { address: receiverUsdcAccount, role: 1 }, { address: custody, role: 1 }, { address: TOKEN_PROGRAM_ADDRESS, role: 0 }, { address: tokenProgramEventAuthority, role: 0 }, { address: TOKEN_MESSENGER_MINTER_PROGRAM, role: 0 }, ], data: instructionData, }; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(solanaKeypair, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstruction(receiveMessageIx, tx), ); const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions, }); try { await sendAndConfirmTransactio
프로젝트로 돌아가기