Skip to main content
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 reasonSymptomsSolution
Insufficient gasTransaction reverts or times outIncrease gas limit and retry
Nonce already usedTransaction reverts with nonce errorThe mint already succeeded; check recipient balance
Wrong contract addressTransaction may succeed with no USDC mintedVerify you’re using the correct MessageTransmitterV2 address for the destination blockchain
Destination caller restrictionTransaction revertsCheck if the burn specified a destinationCaller; only that address can mint
Token account doesn’t exist (Solana)Transaction failsCreate the recipient’s USDC token account first
Attestation expiredTransaction revertsUse re-attestation API to get a fresh attestation

Verify the current state

Before retrying, check whether the mint already succeeded:
1

Check recipient balance

Query the recipient’s USDC balance on the destination blockchain. If the expected amount is present, the mint already completed.
2

Check for existing mint transaction

Search the destination blockchain’s block explorer for receiveMessage transactions from your wallet to the MessageTransmitterV2 contract.
3

Check attestation status

Query the attestation API to confirm you have a complete status:

Retry the mint transaction

If the mint hasn’t completed, submit a new receiveMessage transaction using your attestation.
Call receiveMessage on the MessageTransmitterV2 contract:
TypeScript
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);

Handle destination caller restrictions

If the burn specified a destinationCaller address, only that address can call receiveMessage. If you’re seeing authorization errors:
  1. Check the destinationCaller field in the attestation’s decodedMessage
  2. If it’s not 0x0000...0000, ensure you’re calling from the specified address