bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 52쪽
: [USDC_TYPE],
arguments: [tx.withdrawal({ amount: AMOUNT, type: USDC_TYPE })],
});
const [burnReceipt, returnedCoin] = tx.moveCall({
target: `${CCTP_V2_CONFIG.tmmV2PackageId}::deposit_for_burn::deposit_for_burn`,
arguments: [
coin,
tx.pure.u32(ARC_DOMAIN),
tx.pure.address(padAddressToBytes32(account.address)),
tx.pure.address("0x0"),
tx.pure.u256(MAX_FEE),
tx.pure.u32(MIN_FINALITY_THRESHOLD),
tx.pure.vector("u8", []),
tx.object(CCTP_V2_CONFIG.tmmV2StateId),
],
typeArguments: [USDC_TYPE],
});
const [completeBurnTicket] = tx.moveCall({
target: `${CCTP_V2_CONFIG.handlerPackageId}::handler::burn`,
arguments: [
tx.object(CCTP_V2_CONFIG.handlerStateId),
burnReceipt,
returnedCoin,
tx.object("0x403"), // Sui deny list
tx.object(CCTP_V2_CONFIG.treasuryId),
],
});
tx.moveCall({
target: `${CCTP_V2_CONFIG.tmmV2PackageId}::deposit_for_burn::complete_burn`,
arguments: [
completeBurnTicket,
tx.object(CCTP_V2_CONFIG.tmmV2StateId),
tx.object(CCTP_V2_CONFIG.mtV2StateId),
],
typeArguments: [
USDC_TYPE,
`${CCTP_V2_CONFIG.handlerPackageId}::handler::Auth`,
],
});
const digest = await executeTransaction(tx);
console.log(
`Deposit-for-burn confirmed: https://suiscan.xyz/testnet/tx/${digest}`,
);
return digest;
}
```
### 3.2. 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 `SUI_DOMAIN` for the `sourceDomain` path parameter, using the
[CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
for Sui (8).
* Pass the Sui transaction digest returned by `burnUSDC` as `transactionHash`.
Sui digests are base58 and must not include a `0x` prefix.
```ts TypeScript theme={null}
async function retrieveAttestation(
transactionHash: string,
): Promise<Attestation> {
const url = `${IRIS_API_URL}/v2/messages/${SUI_DOMAIN}?transactionHash=${transactionHash}`;
console.log(`Polling Iris V2 for attestation: ${url}`);
while (true) {
const response = await fetch(url);
if (!response.ok && response.status !== 404) {
throw new Error(`Iris V2 returned HTTP ${response.status}`);
}
if (response.ok) {
const data = (await response.json()) as {
messages?: Array<{ message?: string; attestation?: string }>;
};
const result = data.messages?.[0];
if (
result?.message &&
result.attestation &&
result.attestation !== "PENDING" &&
result.message !== "0x"
) {
return {
message: result.message,
attestation: result.attestation,
};
}
}
await new Promise((resolve) => setTimeout(resolve, 2_000));
}
}
```
### 3.3. 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.
* Pass the signed attestation and 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 mintUSDC({ message, attestation }: Attestation) {
console.log("Receiving the message on Arc...");
const receiveTransactionHash = await walletClient.writeContract({
account,
chain: arcTestnet,
address: ARC_MESSAGE_TRANSMITTER,
abi: messageTransmitterAbi,
functionName: "receiveMessage",
args: [message as Hex, attestation as Hex],
});
const receiveReceipt = await publicClient.waitForTransactionReceipt({
hash: receiveTransactionHash,
});
if (receiveReceipt.status !== "success") {
throw new Error(
`EVM receive transaction reverted: ${receiveTransactionHash}`,
);
}
console.log(`Receive confirmed: ${receiveTransactionHash}`);
return receiveTransactionHash;
}
```
## Step 4: Full script
Create an `index.ts` file in your project directory and paste the full script
following.
```ts index.ts expandable theme={null}
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction } from "@mysten/sui/transactions";
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
type Address,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
type Attestation = {
message: string;
attestation: string;
};
const SUI_DOMAIN = 8;
const ARC_DOMAIN = 26;
const ARC_MESSAGE_TRANSMITTER =
"0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275" as Address;
const IRIS_API_URL = "https://iris-api-sandbox.circle.com";
const CCTP_V2_CONFIG = {
mtV2PackageId:
"0xe9678cd42a81886e18e21361088071f275105636a4d3751e1d7f255211f73bbe",
mtV2StateId:
"0xfee3a2b47f9ef2de2405fc63d79194307945f8ea768815cfc46083bc20fbe6ed",
tmmV2PackageId:
"0x267d3c0cb776eace2840f27e4d33da9c6d952f9749403f1fe6579f4962ed3c64",
tmmV2StateId:
"0x72cb55cd14d01e6361386d6ea93eecda9fa1efc3c202b8dd69c1e4683e4c0ca0",
handlerPackageId:
"0xbe8479044396a45e07de2c7f14789c35b8338406eebc53b2527da56839a91561",
handlerStateId:
"0xfbd9c0517c4f0e1817445ee2be598806e3c392a465d7e3d773d1055f3f0eed32",
usdcPackageId:
"0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29",
treasuryId:
"0x7170137d4a6431bf83351ac025baf462909bffe2877d87716374fb42b9629ebe",
};
const USDC_TYPE = `${CCTP_V2_CONFIG.usdcPackageId}::usdc::USDC`;
const AMOUNT = 1_000_000n; // 1 USDC, 6 decimals
const MAX_FEE = 0n;
const MIN_FINALITY_THRESHOLD = 2_000; // Standard Transfer
const evmPrivateKey = process.env.EVM_PRIVATE_KEY;
const suiPrivateKey = process.env.SUI_PRIVATE_KEY;
if (!evmPrivateKey || !suiPrivateKey) {
throw new Error("Set EVM_PRIVATE_KEY and SUI_PRIVATE_KEY in .env");
}
const account = privateKeyToAccount(evmPrivateKey as Hex);
const walletClient = createWalletClient({
account,
chain: arcTestnet,
transport: http(),
});
const publicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
const suiClient = new SuiGrpcClient({
network: "testnet",
baseUrl: "https://fullnode.testnet.sui.io:443",
});
const suiSigner = Ed25519Keypair.fromSecretKey(
decodeSuiPrivateKey(suiPrivateKey).secretKey,
);
const messageTransmitterAbi = parseAbi([
"function receiveMessage(bytes message, bytes attestation) returns (bool success)",
]);
function padAddressToBytes32(address: string): Hex {
return `0x${address.replace(/^0x/, "").padStart(64, "0").toLowerCase()}` as Hex;
}
async function executeTransaction(transaction: Transaction) {
const res = await suiClient.signAndExecuteTransaction({
signer: suiSigner,
transaction,
include: { effects: true, events: true },
});
const body =
(
res as {
Transaction?: {
digest?: string;
status?: { success?: boolean; error?: string | null };
};
}
).Transaction ??
(
res as {
FailedTransaction?: {
digest?: string;
status?: { success?: boolean; error?: string | null };
};
}
).FailedTransaction ??
{};
const digest = body.digest ?? "";
await suiClient.waitForTransaction({ digest });
if (!body.status?.success) {
console.dir(res, { depth: null, colors: true });
throw new Error(`Transaction failed! ${body.status?.error ?? ""}`);
}
return digest;
}
async function burnUSDC() {
console.log(`Burning ${Number(AMOUNT) / 1_000_000} USDC on Sui...`);
const tx = new Transaction();
const [coin] = tx.moveCall({
target: "0x2::coin::redeem_funds",
typeArguments: [USDC_TYPE],
arguments: [tx.withdrawal({ amount: AMOUNT, type: USDC_TYPE })],
});
const [burnReceipt, returnedCoin] = tx.moveCall({
target: `${CCTP_V2_CONFIG.tmmV2PackageId}::deposit_for_burn::deposit_for_burn`,
arguments: [
coin,
tx.pure.u32(ARC_DOMAIN),
tx.pure.address(padAddressToBytes32(account.address)),
tx.pure.address("0x0"),
tx.pure.u256(MAX_FEE),
tx.pure.u32(MIN_FINALITY_THRESHOLD),
tx.pure.vector("u8", []),
tx.object(CCTP_V2_CONFIG.tmmV2StateId),
],
typeArguments: [USDC_TYPE],
});
const [completeBurnTicket] = tx.moveCall({
target: `${CCTP_V2_CONFIG.handlerPackageId}::handler::burn`,
arguments: [
tx.object(CCTP_V2_CONFIG.handlerStateId),
burnReceipt,
returnedCoin,
tx.object("0x403"),
tx.object(CCTP_V2_CONFIG.treasuryId),
],
});
tx.moveCall({
target: `${CCTP_V2_CONFIG.tmmV2PackageId}::deposit_for_burn::complete_burn`,
arguments: [
completeBurnTicket,
tx.object(CCTP_V2_CONFIG.tmmV2StateId),
tx.object(CCTP_V2_CONFIG.mtV2StateId),
],
typeArguments: [
USDC_TYPE,
`${CCTP_V2_CONFIG.handlerPackageId}::handler::Auth`,
],
});
const digest = await executeTransaction(tx);
console.log(
`Deposit-for-burn confirmed: https://suiscan.xyz/testnet/tx/${digest}`,
);
return digest;
}
async function retrieveAttestation(
transactionHash: string,
): Promise<Attestation> {
const url = `${IRIS_API_URL}/v2/messages/${SUI_DOMAIN}?transactionHash=${transactionHash}`;
console.log(`Polling Iris V2 for attestation: ${url}`);
while (true) {
const response = await fetch(url);
if (!response.ok && response.status !== 404) {
throw new Error(`Iris V2 returned HTTP ${response.status}`);
}
if (response.ok) {
const data = (await response.json()) as {
messages?: Array<{ message?: string; attestation?: string }>;
};
const result = data.messages?.[0];
if (
result?.message &&
result.attestation &&
result.attestation !== "PENDING" &&
result.message !== "0x"
) {
return {
message: result.message,
attestation: result.attestation,
};
}
}
await new Promise((resolve) => setTimeout(resolve, 2_000));
}
}
async function mintUSDC({ message, attestation }: Attestation) {
console.log("Receiving the message on Arc...");
const receiveTransactionHash = await walletClient.writeContract({
account,
chain: arcTestnet,
address: ARC_MESSAGE_TRANSMITTER,
abi: messageTransmitterAbi,
functionName: "receiveMessage",
args: [message as Hex, attestation as Hex],
});
const receiveReceipt = await publicClient.waitForTransactionReceipt({
hash: receiveTransactionHash,
});
if (receiveReceipt.status !== "success") {
throw new Error(
`EVM receive transaction reverted: ${receiveTransactionHash}`,
);
}
console.log(`Receive confirmed: ${receiveTransactionHash}`);
return receiveTransactionHash;
}
export default async function main() {
const burnDigest = await burnUSDC();
const attestation = await retrieveAttestation(burnDigest);
console.log("Attestation received from Iris V2.");
const receiveTransactionHash = await mintUSDC(attestation);
console.log("USDC transfer completed.");
return {
suiTransactionDigest: burnDigest,
evmTransactionHash: receiveTransactionHash,
};
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
) {
console.log("Starting CCTP flow...");
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
}
```
## Step 5: Test the script
Run the following command to execute the script:
```shell Shell theme={null}
npm run start
```
When the transfer finishes, the console logs a completion message and the
relevant transaction hashes. Successful output looks similar to the following:
```bash Shell theme={null}
Starting CCTP flow...
Burning 1 USDC on Sui...
Deposit-for-burn confirmed: https://suiscan.xyz/testnet/tx/<sui-transaction-digest>
Polling Iris V2 for attestation: https://iris-api-sandbox.circle.com/v2/messages/8?transactionHash=<sui-transaction-digest>
Attestation received from Iris V2.
Receiving the message on Arc...
Receive confirmed: 0x...
USDC transfer completed.
```
Attestation polling can take several minutes depending on network conditions and
the finality threshold you chose. The script retries every 2 seconds with no
timeout, so allow the process to continue while Iris prepares the attestation.
<Note>
**Rate limit:** The attestation service rate limit is 40 requests per second. If
you exceed this limit, the service blocks all API requests for the next five
minutes and returns an HTTP 429 (Too Many Requests) response.
</Note>
</Tab>
<Tab title="Arc to Sui">
This quickstart demonstrates how to transfer USDC from Arc Testnet to Sui
Testnet using CCTP. You use [`viem`](https://viem.sh/) to approve and burn USDC
on Arc, and the [`@mysten/sui`](https://www.npmjs.com/package/@mysten/sui) SDK
to receive and mint USDC on Sui. 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 basic
EVM usage and Sui programmable transactions helps you follow and adapt the
script. Examples use Arc Testnet as the source, but you can use any
[supported EVM blockchain](/cctp/concepts/supported-chains-and-domains).
<Note>
This script uses `SuiGrpcClient`, which requires a Sui **gRPC** endpoint (not
JSON-RPC). The default fullnode URL below serves gRPC on port `443`.
</Note>
## 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 the transfer amount from the
[Circle Faucet](https://faucet.circle.com)
* Funded your wallet with Arc Testnet ETH for gas fees
* Prepared a Sui Testnet wallet with the private key available (`suiprivkey1…`)
* Funded your account with testnet SUI for the receive transaction fee
## 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, initialize and set up a new Node.js project, and install
the required dependencies:
```bash Shell theme={null}
mkdir cctp-arc-to-sui
cd cctp-arc-to-sui
npm init -y
npm pkg set type=module
npm pkg set scripts.start="node --env-file=.env --import=tsx index.ts"
npm install @mysten/sui viem
npm install --save-dev tsx 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}
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
SUI_PRIVATE_KEY=YOUR_SUI_PRIVATE_KEY
```
* `EVM_PRIVATE_KEY` is the private key for the EVM wallet used to burn USDC on
Arc Testnet.
* `SUI_PRIVATE_KEY` is the Sui Testnet private key (`suiprivkey1…`) for the
account that receives the minted USDC and signs the receive transaction.
<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 the contract addresses, transfer amount, finality settings, and wallet
clients for Arc Testnet and Sui Testnet.
### 2.1. Define configuration constants
The script predefines the Arc USDC and TokenMessengerV2 addresses, the Sui CCTP
V2 package and State object IDs, and the CCTP domain IDs.
```ts TypeScript theme={null}
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { Transaction } from "@mysten/sui/transactions";
import { normalizeSuiAddress } from "@mysten/sui/utils";
import {
createPublicClient,
createWalletClient,
http,
parseAbi,
type Address,
type Hex,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
type Attestation = {
message: string;
attestation: string;
};
const SUI_DOMAIN = 8; // Destination domain ID for Sui Testnet
const ARC_DOMAIN = 26; // Source domain ID for Arc Testnet
const ARC_USDC = "0x3600000000000000000000000000000000000000" as Address;
const ARC_TOKEN_MESSENGER =
"0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA" as Address;
const IRIS_API_URL = "https://iris-api-sandbox.circle.com";
/** Sandbox Sui CCTP V2 package and State object IDs. */
const CCTP_V2_CONFIG = {
mtV2PackageId:
"0xe9678cd42a81886e18e21361088071f275105636a4d3751e1d7f255211f73bbe",
mtV2StateId:
"0xfee3a2b47f9ef2de2405fc63d79194307945f8ea768815cfc46083bc20fbe6ed",
tmmV2PackageId:
"0x267d3c0cb776eace2840f27e4d33da9c6d952f9749403f1fe6579f4962ed3c64",
tmmV2StateId:
"0x72cb55cd14d01e6361386d6ea93eecda9fa1efc3c202b8dd69c1e4683e4c0ca0",
handlerPackageId:
"0xbe8479044396a45e07de2c7f14789c35b8338406eebc53b2527da56839a91561",
handlerStateId:
"0xfbd9c0517c4f0e1817445ee2be598806e3c392a465d7e3d773d1055f3f0eed32",
usdcPackageId:
"0xa1ec7fc00a6f40db9693ad1415d0c193ad3906494428cf252621037bd7117e29",
treasuryId:
"0x7170137d4a6431bf83351ac025baf462909bffe2877d87716374fb42b9629ebe",
};
const USDC_TYPE = `${CCTP_V2_CONFIG.usdcPackageId}::usdc::USDC`;
const AMOUNT = 1_000_000n; // 1 USDC, 6 decimals
const MAX_FEE = 0n;
const MIN_FINALITY_THRESHOLD = 2_000; // Standard Transfer
```
`ARC_DOMAIN` is the source domain used when requesting the attestation. The
destination domain for Sui Testnet is 8. This example uses a
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
(`minFinalityThreshold` of `2000`) with a maximum fee of zero.
### 2.2. Set up wallet clients
The wallet clients use `viem` for Arc Testnet and `@mysten/sui` for Sui Testnet.
The Sui keypair signs the programmable transaction that receives and mints USDC.
```ts TypeScript theme={null}
const evmPrivateKey = process.env.EVM_PRIVATE_KEY;
const suiPrivateKey = process.env.SUI_PRIVATE_KEY;
if (!evmPrivateKey || !suiPrivateKey) {
throw new Error("Set EVM_PRIVATE_KEY and SUI_PRIVATE_KEY in .env");
}
const account = privateKeyToAccount(evmPrivateKey as Hex);
const walletClient = createWalletClient({
account,
chain: arcTestnet,
transport: http(),
});
const publicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
const suiClient = new SuiGrpcClient({
network: "testnet",
baseUrl: "https://fullnode.testnet.sui.io:443",
});
const suiSigner = Ed25519Keypair.fromSecretKey(
decodeSuiPrivateKey(suiPrivateKey).secretKey,
);
const usdcAbi = parseAbi([
"function approve(address spender, uint256 amount) returns (bool)",
]);
const tokenMessengerV2Abi = parseAbi([
"function depositForBurn(uint256 amount, uint32 destinationDomain, bytes32 mintRecipient, address burnToken, bytes32 destinationCaller, uint256 maxFee, uint32 minFinalityThreshold)",
]);
```
### 2.3. Format the Sui recipient for Arc
`depositForBurn` accepts the destination recipient as `bytes32`. A Sui address
is already a 32-byte value, so the script passes it as a hexadecimal value to
`viem`.
```ts TypeScript theme={null}
function padAddressToBytes32(address: string): Hex {
return `0x${address.replace(/^0x/, "").padStart(64, "0").toLowerCase()}` as Hex;
}
```
## Step 3: Implement the transfer logic
This step implements the core transfer logic: approve and burn on A프로젝트로 돌아가기