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프로젝트로 돌아가기