bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 37쪽
st using
source domain 6 (Base Sepolia) and destination domain 1 (Avalanche Fuji):
```typescript theme={null}
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/1?forward=true",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
const fees = await response.json();
console.log(fees);
```
**Example response:**
```json theme={null}
[
{
"finalityThreshold": 1000, // Fast transfer
"minimumFee": 1.3, // Basis points (0.013% fee rate)
"forwardFee": {
// Gas-based, fluctuates based on destination chain gas prices
"low": 56035, // 0.056035 USDC
"med": 57543, // 0.057543 USDC
"high": 59052 // 0.059052 USDC
}
},
{
"finalityThreshold": 2000, // Standard transfer
"minimumFee": 0, // No fee
"forwardFee": {
"low": 56035, // 0.056035 USDC
"med": 57543, // 0.057543 USDC
"high": 59052 // 0.059052 USDC
}
}
]
```
The `forwardFee` is the fee charged by the Forwarding Service. The `minimumFee`
is the CCTP protocol fee rate in basis points, applied as a percentage of the
transfer amount.
<Tip>
Circle recommends selecting the `med` fee level or higher from the `forwardFee`
object in the API response. Note that `forwardFee` values fluctuate based on
destination chain gas prices. Make the query immediately before initiating your
transfer.
</Tip>
<Accordion title="Forwarding to Solana">
When the destination blockchain is Solana, the `forwardFee` values include both
gas and rent costs. If the recipient does not have an existing USDC
[Associated Token Account (ATA)](https://spl.solana.com/associated-token-account),
add `includeRecipientSetup=true` to the fee query so the returned fee covers ATA
creation:
```typescript theme={null}
// Source domain 6 (Base Sepolia), destination domain 5 (Solana)
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/6/5?forward=true&includeRecipientSetup=true",
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
```
Unlike EVM destinations, the `mintRecipient` for Solana must be the recipient's
**USDC token account address** (ATA), not the wallet address. Derive the ATA
from the wallet address and the USDC mint:
```typescript theme={null}
import { getAssociatedTokenAddressSync } from "@solana/spl-token";
import { PublicKey } from "@solana/web3.js";
const recipientWallet = new PublicKey("RecipientSolanaWalletAddress");
const USDC_MINT = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // Solana devnet
const recipientAta = getAssociatedTokenAddressSync(USDC_MINT, recipientWallet);
const mintRecipientBytes32 =
`0x${Buffer.from(recipientAta.toBytes()).toString("hex")}` as `0x${string}`;
```
If the recipient does not have an existing ATA and you passed
`includeRecipientSetup=true` in the fee query, you must also encode ATA creation
fields in the hook data. See
[Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation)
for the extended hook data format.
</Accordion>
### Step 2. Calculate the USDC amounts and fees
Calculate the total fee by combining the protocol fee and the Forwarding Service
fee. The `maxFee` parameter must cover both fees for the transfer to succeed.
```typescript theme={null}
// Amount to transfer (10 USDC in subunits)
const transferAmount = 10_000_000n;
// Parse fees from API response
const feeData = fees[0]; // Use fast transfer fees (finalityThreshold: 1000)
const forwardFee = BigInt(feeData.forwardFee.med);
// Calculate protocol fee (minimumFee is in basis points)
const minimumFeeBps = feeData.minimumFee;
const protocolFee =
(transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n;
// Total max fee should cover both fees
const maxFee = forwardFee + protocolFee;
const totalAmount = transferAmount + maxFee; // Total to burn
console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC");
console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");
```
In this example, for a 10 USDC transfer with forwarding, the total fee is
0.058843 USDC (0.057543 USDC Forwarding Service fee + 0.0013 USDC CCTP protocol
fee). For the recipient to receive 10 USDC, you must burn 10.058843 USDC in
total.
<Warning>
If the `maxFee` parameter is insufficient to cover the both Fast Transfer
protocol fee and the Forwarding Service fee, CCTP will prioritize forwarding
execution over Fast Transfer. This means that the transfer will execute as a
Standard Transfer with the Forwarding Service.
</Warning>
### Step 3. Approve the USDC transfer
Grant approval for the
[`TokenMessengerV2` contract](/cctp/references/contract-addresses) deployed on
Base to transfer USDC from your wallet. Approve at least `totalAmount`
(including fees) calculated in Step 2.
```typescript theme={null}
import { createWalletClient, http, encodeFunctionData } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
// Configuration
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_TOKEN_MESSENGER =
"0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
// Set up wallet client
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
chain: baseSepolia,
transport: http(),
account,
});
async function approveUSDC(amount: bigint) {
console.log("Approving USDC transfer...");
const approveTx = await client.sendTransaction({
to: BASE_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: [BASE_SEPOLIA_TOKEN_MESSENGER, amount],
}),
});
console.log("USDC Approval Tx:", approveTx);
return approveTx;
}
// Approve the total amount (from Step 2)
await approveUSDC(totalAmount);
```
### Step 4. Sign and broadcast a `depositForBurnWithHook` transaction on the `TokenMessengerV2` contract
Create and send a `depositForBurnWithHook` transaction with the Forwarding
Service hook data. The hook data tells the CCTP Forwarding Service to
automatically forward the mint transaction on the destination chain.
The Forwarding Service hook data is a static 32-byte value containing the magic
bytes `cctp-forward`, version `0`, and length `0`. For details on the hook
format, see
[Forwarding Service hook format](/cctp/concepts/forwarding-service#hook-format).
```typescript theme={null}
// Forwarding Service hook data: magic bytes ("cctp-forward") + version (0) + additional data length (0)
const FORWARDING_SERVICE_HOOK_DATA =
"0x636374702d666f72776172640000000000000000000000000000000000000000";
```
<Accordion title="Hook data for Solana destinations">
When forwarding to Solana, use the same static hook data if the recipient
already has a USDC
[Associated Token Account (ATA)](https://spl.solana.com/associated-token-account).
If the recipient does not have an ATA and you included
`includeRecipientSetup=true` in the fee query (see Step 1), construct extended
hook data that requests ATA creation:
```typescript theme={null}
import { PublicKey } from "@solana/web3.js";
// Magic bytes "cctp-forward" padded to 24 bytes
const magicBytes = Buffer.alloc(24);
magicBytes.write("cctp-forward", "utf-8");
// Version (uint32, big-endian) = 0
const version = Buffer.alloc(4);
// Length of additional Circle hook data (uint32, big-endian) = 33
const length = Buffer.alloc(4);
length.writeUInt32BE(33);
// ATA creation flag = 1
const ataFlag = Buffer.from([1]);
// Recipient wallet address (32 bytes)
const recipientWallet = new PublicKey("RecipientSolanaWalletAddress");
const walletBytes = Buffer.from(recipientWallet.toBytes());
const FORWARDING_SERVICE_HOOK_DATA =
("0x" +
Buffer.concat([magicBytes, version, length, ataFlag, walletBytes]).toString(
"hex",
)) as `0x${string}`;
```
For the full hook data format, see
[Solana hook data for ATA creation](/cctp/concepts/forwarding-service#solana-hook-data-for-ata-creation).
</Accordion>
Then, send the `depositForBurnWithHook` transaction:
<Note>
Use `totalAmount` (transfer amount + fees) for the `amount` parameter. The
recipient receives only the transfer amount after fees are deducted.
</Note>
```typescript theme={null}
import { pad, encodeFunctionData } from "viem";
// Configuration
const AVALANCHE_FUJI_DOMAIN = 1;
const DESTINATION_ADDRESS = "0xYOUR_DESTINATION_ADDRESS" as `0x${string}`;
// Convert address to bytes32 format
const mintRecipientBytes32 = pad(DESTINATION_ADDRESS, { size: 32 });
async function depositForBurnWithHook() {
console.log("Burning USDC on Base with Forwarding Service hook...");
const burnTx = await client.sendTransaction({
to: BASE_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, // Total to burn (recipient receives transferAmount after fees)
AVALANCHE_FUJI_DOMAIN,
mintRecipientBytes32,
BASE_SEPOLIA_USDC,
pad("0x", { size: 32 }), // destinationCaller (empty = any caller)
maxFee,
1000,
FORWARDING_SERVICE_HOOK_DATA,
],
}),
});
console.log("Burn Tx:", burnTx);
return burnTx;
}
```
Once the burn transaction is confirmed on Base, the Circle Forwarding Service
automatically handles the attestation and mint transaction on Avalanche. The
USDC is minted directly to the `mintRecipient` address on the destination chain.
The recipient receives `transferAmount` USDC (fees are automatically deducted
from the `totalAmount` on the destination chain).
### Step 5. Verify the mint transaction
After the burn transaction is confirmed, query the Circle Iris API to retrieve
the forwarding details. The API returns the `forwardTxHash`, which is the mint
transaction hash on the destination chain.
The attestation may take time to become available, depending on the destination
chain. Poll the API until the message is ready:
```typescript theme={null}
// Configuration
const BASE_SEPOLIA_DOMAIN = 6;
process.stdout.write("Waiting for attestation...");
let mintTx;
while (!mintTx) {
const messageResponse = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`,
);
const data = await messageResponse.json();
if (data.messages?.[0]?.forwardTxHash) {
mintTx = data.messages[0].forwardTxHash;
console.log(); // New line after dots
} else {
process.stdout.write(".");
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
console.log("Mint Tx:", mintTx);
```
## Full example code
The following is a complete example of how to transfer USDC from Base Sepolia to
Avalanche Fuji using the Forwarding Service. Remember to set the `PRIVATE_KEY`
and `DESTINATION_ADDRESS` environment variables.
```typescript script.ts expandable theme={null}
import { createWalletClient, http, encodeFunctionData, pad } from "viem";
import { baseSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
// Validate environment variables
if (!process.env.PRIVATE_KEY || !process.env.DESTINATION_ADDRESS) {
throw new Error(
"PRIVATE_KEY and DESTINATION_ADDRESS environment variables are required",
);
}
// Configuration
const BASE_SEPOLIA_USDC = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
const BASE_SEPOLIA_TOKEN_MESSENGER =
"0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const BASE_SEPOLIA_DOMAIN = 6;
const AVALANCHE_FUJI_DOMAIN = 1;
const DESTINATION_ADDRESS = process.env.DESTINATION_ADDRESS as `0x${string}`;
// Forwarding Service hook data
const FORWARDING_SERVICE_HOOK_DATA =
"0x636374702d666f72776172640000000000000000000000000000000000000000" as `0x${string}`;
// Set up wallet client
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
chain: baseSepolia,
transport: http(),
account,
});
async function main() {
console.log("Wallet address:", account.address);
console.log("Destination address:", DESTINATION_ADDRESS);
// Step 1: Get fees from API
console.log("\nStep 1: Getting CCTP fees...");
const feeResponse = await fetch(
`https://iris-api-sandbox.circle.com/v2/burn/USDC/fees/${BASE_SEPOLIA_DOMAIN}/${AVALANCHE_FUJI_DOMAIN}?forward=true`,
{
method: "GET",
headers: { "Content-Type": "application/json" },
},
);
const fees = await feeResponse.json();
console.log("Fees:", JSON.stringify(fees, null, 2));
// Step 2: Calculate amounts
console.log("\nStep 2: Calculating amounts...");
const transferAmount = 10_000_000n; // 10 USDC
const feeData = fees[0]; // Fast transfer
const forwardFee = BigInt(feeData.forwardFee.med);
const minimumFeeBps = feeData.minimumFee;
const protocolFee =
(transferAmount * BigInt(Math.round(minimumFeeBps * 100))) / 1_000_000n;
const maxFee = forwardFee + protocolFee;
const totalAmount = transferAmount + maxFee; // Total to burn
console.log("Transfer amount:", Number(transferAmount) / 1_000_000, "USDC");
console.log("Forward fee:", Number(forwardFee) / 1_000_000, "USDC");
console.log("Protocol fee:", Number(protocolFee) / 1_000_000, "USDC");
console.log("Max fee:", Number(maxFee) / 1_000_000, "USDC");
console.log("Total to burn:", Number(totalAmount) / 1_000_000, "USDC");
// Step 3: Approve USDC
console.log("\nStep 3: Approving USDC transfer...");
const approveTx = await client.sendTransaction({
to: BASE_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: [BASE_SEPOLIA_TOKEN_MESSENGER, totalAmount],
}),
});
console.log("Approval Tx:", approveTx);
// Step 4: Burn USDC with Forwarding Service hook
console.log("\nStep 4: Burning USDC with Forwarding Service hook...");
const burnTx = await client.sendTransaction({
to: BASE_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,
AVALANCHE_FUJI_DOMAIN,
pad(DESTINATION_ADDRESS as `0x${string}`, { size: 32 }),
BASE_SEPOLIA_USDC,
pad("0x", { size: 32 }),
maxFee,
1000, // Fast Transfer
FORWARDING_SERVICE_HOOK_DATA,
],
}),
});
console.log("Burn Tx:", burnTx);
console.log(
"\nTransfer initiated. The Forwarding Service will automatically mint USDC on Avalanche.",
);
// Step 5: Verify the mint transaction
console.log("\nStep 5: Verifying mint transaction...");
process.stdout.write("Waiting for attestation...");
let mintTx;
while (!mintTx) {
const messageResponse = await fetch(
`https://iris-api-sandbox.circle.com/v2/messages/${BASE_SEPOLIA_DOMAIN}?transactionHash=${burnTx}`,
);
const data = await messageResponse.json();
if (data.messages?.[0]?.forwardTxHash) {
mintTx = data.messages[0].forwardTxHash;
console.log("\n"); // New line after dots
} else {
process.stdout.write(".");
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}
console.log("Mint Tx:", mintTx);
}
main().catch(console.error);
```
Run the script:
```bash theme={null}
node --env-file=.env script.ts
```
# How-to: Transfer USDC with upfront fees
Source: https://developers.circle.com/cctp/howtos/transfer-usdc-with-upfront-fees
Pay CCTP fees upfront on the source blockchain so the full USDC amount mints on the destination blockchain.
This example transfers 10 USDC from Base Sepolia to Arc Testnet, paying both the
Fast Transfer and Forwarding Service fees [upfront](/cctp/concepts/upfront-fees)
so the recipient receives the full transfer amount on the destination
blockchain. You can use the same steps for any EVM source blockchain where both
upfront fees and Fast Transfer are available, and any destination blockchain
that the Forwarding Service supports.
## Prerequisites
Before you begin, ensure that you've:
* Installed [Node.js v22+](https://nodejs.org/)
* Created a TypeScript project and installed the `viem` package
* Created a wallet with the private key accessible from your local development
environment
* Funded the wallet on Base Sepolia with:
* Base Sepolia ETH for gas from a
[public faucet](https://www.alchemy.com/faucets/base-sepolia)
* Base Sepolia USDC from the [Circle Faucet](https://faucet.circle.com)
* Created a `.env` file with your private key
<Warning>
Keep your private key secret. Store it in a `.env` file that's listed in
`.gitignore`, and never commit it or expose it in logs or shell history.
</Warning>
## Steps
### Step 1: Request a signed quote from the Quote API
Request a quote from the
[Quote API](/api-reference/cctp/all/create-usdc-burn-quote) for the fees you
want to pay upfront. This example requests both `FORWARD` (Forwarding Service)
and `PRE_FINALITY` (Fast Transfer) fees, and sets `feeToken` to the source
blockchain's USDC address to pay the fee in USDC. The following request uses
source domain 6 (Base Sepolia) and destination domain 26 (Arc Testnet).
The USDC address and decimals come from the token definition in
[`viem/tokens`](https://viem.sh/tokens), so you don't hardcode either one:
```typescript theme={null}
import { parseUnits } from "viem";
import { baseSepolia } from "viem/chains";
import { usdc } from "viem/tokens";
// USDC on the source blockchain, including its address and decimals
const sourceUsdc = usdc(baseSepolia.id);
// The quote binds to this amount, so reuse it for the onchain burn
const transferAmount = parseUnits("10", sourceUsdc.decimals);
const response = await fetch(
"https://iris-api-sandbox.circle.com/v2/quote/burn/usdc/6/26",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
amount: transferAmount.toString(), // minor units
feeToken: sourceUsdc.address, // omit to pay the fee in the native gas token
requests: [
{ type: "FORWARD" },
// Omit if the source blockchain doesn't support Fast Transfer
{ type: "PRE_FINALITY" },
],
}),
},
);
const quote = await response.json();
console.log("Quote:", JSON.stringify(quote, null, 2));
```
**Example response:**
```json theme={null}
{
"signedQuote": "0x0100000000000000000000000000000000000000000000000000000000000000...c9afdd35731b00",
"issuedAt": 1783407750,
"expiry": {
"mode": "BLOCK_NUMBER",
"expiresAtBlock": 43819792,
"blockEstimatedAt": 1783407872
},
"feeTotalAmount": "24453",
"feeToken": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"items": [
{
"type": "FORWARD",
"amount": "23153",
"args": ["..."],
"argsHash": "0x6cea1d25ab9b4b9edfc5c788082c28c6669c6fc2f35fa8e047c7774e60c66d4c"
},
{
"type": "PRE_FINALITY",
"amount": "1300",
"args": ["..."],
"argsHash": "0x5743df2ad8e9696c8a5e70bfced256f31프로젝트로 돌아가기