bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 50쪽
eMutability: "nonpayable",
inputs: [
{ name: "message", type: "bytes" },
{ name: "attestation", type: "bytes" },
],
outputs: [],
},
],
functionName: "receiveMessage",
args: [
attestation.message as `0x${string}`,
attestation.attestation as `0x${string}`,
],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash });
console.log(`Mint Tx: ${hash}`);
}
// ============ Main Execution ============
async function main() {
await approveUSDC();
const burnTx = await burnUSDC();
const attestation = await retrieveAttestation(burnTx);
await mintUSDCOnArc(attestation);
console.log("USDC transfer from Stellar to Arc completed!");
}
main().catch(console.error);
```
## 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}
Approving USDC spend on Stellar...
Approve Tx: <stellar-transaction-hash>
Burning USDC on Stellar...
Burn Tx: <stellar-transaction-hash>
Retrieving attestation...
Waiting for attestation...
Waiting for attestation...
Attestation retrieved successfully!
Minting USDC on Arc Testnet...
Mint Tx: 0x...
USDC transfer from Stellar to Arc completed!
```
Attestation polling can take several minutes depending on network conditions and
the finality threshold you chose. The script retries every 5 seconds with no
timeout, so if it appears to stop responding at `Waiting for attestation...`,
allow at least five minutes before investigating.
<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 Stellar">
This quickstart demonstrates how to transfer USDC from Arc Testnet to Stellar
Testnet using CCTP. You use the [viem](https://viem.sh/) library to burn USDC on
Arc, and [`@stellar/stellar-sdk`](https://github.com/stellar/js-stellar-sdk) to
mint and forward tokens on the Stellar CCTP Forwarder. 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 Stellar Soroban transactions helps you follow and adapt the
script. Examples use Arc Testnet as the source, but you can use any
[supported blockchain](/cctp/concepts/supported-chains-and-domains).
<Warning>
Always
[use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and
`destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/stellar-contracts).
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is set to a user account or muxed address, USDC is not sent
to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
</Warning>
## 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 gas fees and the transfer
amount) from the [Circle Faucet](https://faucet.circle.com)
* Prepared a Stellar Testnet wallet with the secret key (`S...`) available
* Funded your Stellar wallet with testnet XLM from the
[Stellar Friendbot](https://lab.stellar.org/account/fund) (for Soroban fees
on Stellar Testnet)
* If needed, identified the forward recipient Stellar `strkey` (`G...`, `C...`,
or `M...`). For `G...` or `M...` recipients, established a
[USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) before
receiving minted USDC
<Tip>
You can use [Stellar Lab](https://lab.stellar.org/) on Stellar Testnet to fund
accounts and establish USDC trustlines.
</Tip>
## 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 and install the required dependencies:
```bash Shell theme={null}
# Set up your directory and initialize a Node.js project
mkdir cctp-arc-to-stellar
cd cctp-arc-to-stellar
npm init -y
# Set up module type and start command
npm pkg set type=module
npm pkg set scripts.start="node --env-file=.env index.ts"
# Install runtime dependencies
npm install @stellar/stellar-sdk viem
# Install dev dependencies
npm install --save-dev 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
STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
# FORWARD_RECIPIENT=G_OR_C_OR_M_STELLAR_STRKEY
```
* `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc
Testnet.
* `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban
transactions on Stellar Testnet.
* `FORWARD_RECIPIENT` is required when the final recipient is a `G...` or `M...`
address. When omitted, the script defaults to the public key derived from
`STELLAR_SECRET_KEY`.
<EnvFileTip />
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
This section covers the necessary setup for the transfer script, including
defining keys and addresses, and configuring the wallet clients for interacting
with Arc and Stellar.
### 2.1. Define configuration constants
The script predefines the contract addresses, transfer amount, and other
parameters.
```ts TypeScript theme={null}
import {
Contract,
Keypair,
StrKey,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
// ============ Configuration Constants ============
// Contract Addresses
const ARC_USDC = "0x3600000000000000000000000000000000000000";
const ARC_TOKEN_MESSENGER = "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA";
const STELLAR_CCTP_FORWARDER =
"CA66Q2WFBND6V4UEB7RD4SAXSVIWMD6RA4X3U32ELVFGXV5PJK4T4VSZ";
// Transfer Parameters
const AMOUNT = 1_000_000n; // 1 USDC (1 USDC = 1,000,000 subunits)
const MAX_FEE = 500n; // 0.0005 USDC (500 subunits)
// Chain-specific Parameters
const ARC_TESTNET_DOMAIN = 26; // Source domain ID for Arc Testnet
const STELLAR_DOMAIN = 27; // Destination domain ID for Stellar
// Stellar Soroban Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
```
### 2.2. Set up wallet clients
The wallet clients configure the appropriate network settings using `viem`. In
this example, the script connects to Arc Testnet.
```ts TypeScript theme={null}
const evmAccount = privateKeyToAccount(
process.env.EVM_PRIVATE_KEY as `0x${string}`,
);
const arcWalletClient = createWalletClient({
chain: arcTestnet,
transport: http(),
account: evmAccount,
});
const arcPublicClient = createPublicClient({
chain: arcTestnet,
transport: http(),
});
```
### 2.3. Encode the CCTP Forwarder hook data
When transferring to Stellar, the Arc burn encodes the forward recipient in the
`hookData` field. The Stellar CCTP Forwarder contract is set as both the
`mintRecipient` and `destinationCaller`.
The hook data encodes the forward recipient in a specific binary format:
```text Byte layout theme={null}
[32-byte header + forward recipient bytes]
├─ Bytes 0-23: Zero padding (0x000...000)
├─ Bytes 24-27: Hook version (uint32, currently 0)
├─ Bytes 28-31: Forward recipient strkey length (uint32, byte length)
└─ Bytes 32+: Forward recipient strkey as UTF-8 bytes
```
Example: For forward recipient `GABC...XYZ` (56 chars):
* Zero padding: 0x000000000000000000000000000000000000000000000000
* Hook version: 0x00000000
* Length: 0x00000038 (56 in hex)
* Forward recipient: `GABC...XYZ` encoded as UTF-8
This encoding tells the CCTP Forwarder where to send tokens after minting.
<Note>
`G` and `M` strkey forward recipients need an established
[USDC trustline](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/accounts#trustlines)
before receiving funds. Transfers without a trustline fail.
</Note>
<Accordion title="How the CCTP Forwarder flow works">
This path follows the
[CCTP Forwarder](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
pattern:
1. **Burn with hook**: `depositForBurnWithHook` on the Arc
[`TokenMessengerV2`](/cctp/references/contract-interfaces#depositforburnwithhook)
contract. `mintRecipient` is the Stellar
[CCTP Forwarder](/cctp/references/stellar-contracts#cctpforwarder), and
`hookData` carries the forward recipient strkey.
2. **Attest**: Circle's attestation service signs the burn event.
3. **Mint and forward**: `mint_and_forward` on the Stellar CCTP Forwarder calls
`receive_message` on `MessageTransmitter`, mints tokens, and forwards them
per the hook data.
<Note>
The CCTP Forwarder flow is non-custodial. In one atomic Soroban transaction,
`mint_and_forward` mints to `CctpForwarder` and pays `forwardRecipient` onchain.
Circle does not take custody of the minted balance in between.
</Note>
</Accordion>
```ts TypeScript theme={null}
// Convert a Stellar contract address (C...) to 0x-prefixed bytes32
function contractStrkeyToBytes32(strkey: string): `0x${string}` {
if (!StrKey.isValidContract(strkey)) {
throw new Error(`Invalid contract strkey: ${strkey}`);
}
return `0x${Buffer.from(StrKey.decodeContract(strkey)).toString("hex")}`;
}
// Build hook data encoding the forward recipient
function buildCctpForwarderHookData(
forwardRecipientStrkey: string,
): `0x${string}` {
const isValid =
StrKey.isValidEd25519PublicKey(forwardRecipientStrkey) ||
StrKey.isValidContract(forwardRecipientStrkey) ||
StrKey.isValidMed25519PublicKey(forwardRecipientStrkey);
if (!isValid) {
throw new Error(
`Invalid forward recipient: ${forwardRecipientStrkey} (expected G..., C..., or M... address)`,
);
}
const recipientBytes = Buffer.from(forwardRecipientStrkey, "utf8");
const hookData = Buffer.alloc(32 + recipientBytes.length);
hookData.writeUInt32BE(0, 24); // hook version = 0
hookData.writeUInt32BE(recipientBytes.length, 28); // recipient byte length
recipientBytes.copy(hookData, 32); // recipient strkey as UTF-8
return `0x${hookData.toString("hex")}`;
}
const stellarKeypair = Keypair.fromSecret(
process.env.STELLAR_SECRET_KEY as string,
);
// Falls back to the Stellar public key derived from STELLAR_SECRET_KEY
// when FORWARD_RECIPIENT is unset or empty.
const forwardRecipient =
process.env.FORWARD_RECIPIENT || stellarKeypair.publicKey();
const hookData = buildCctpForwarderHookData(forwardRecipient);
```
## Step 3: Implement the transfer logic
This step implements the core transfer logic: approve and burn on Arc, poll for
an attestation, then mint and forward on Stellar. A successful run prints
transaction hashes and a completion message in the console.
### 3.1. Approve USDC on Arc
Grant approval for the
[`TokenMessengerV2` contract](/cctp/references/contract-addresses) to withdraw
USDC from your wallet. This allows the contract to burn USDC when you initiate
the transfer.
```ts TypeScript theme={null}
async function approveUSDC() {
console.log("Approving USDC spend on Arc...");
const approveTx = await arcWalletClient.sendTransaction({
to: ARC_USDC as `0x${string}`,
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: [ARC_TOKEN_MESSENGER as `0x${string}`, AMOUNT],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: approveTx });
console.log(`Approve Tx: ${approveTx}`);
}
```
### 3.2. Burn USDC on Arc
Call `depositForBurnWithHook` to burn USDC with the CCTP Forwarder hook data.
You specify the following parameters:
* **Burn amount**: The amount of USDC to burn (in Arc subunits, 6 decimals)
* **Destination domain**: The target blockchain for minting USDC (see
[supported blockchains and domains](/cctp/concepts/supported-chains-and-domains))
* **Mint recipient**: The CCTP Forwarder contract address on Stellar (encoded as
`bytes32`)
* **Burn token**: The contract address of the USDC token being burned on Arc
* **Destination caller**: The CCTP Forwarder contract address on Stellar
(restricts who can call `receive_message`)
* **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
* **Finality threshold**: Determines whether it's a
[Fast Transfer](/cctp/concepts/finality-and-block-confirmations#fast-transfer-attestation-times)
(1000 or less) or a
[Standard Transfer](/cctp/concepts/finality-and-block-confirmations#standard-transfer-attestation-times)
(2000 or more)
* **Hook data**: Encodes the final Stellar recipient address for the CCTP
Forwarder
<Warning>
Always
[use `CctpForwarder`](/cctp/references/stellar#use-cctpforwarder-for-stellar-recipients)
when routing CCTP USDC to a Stellar address. Set both `mintRecipient` and
`destinationCaller` to the `CctpForwarder`
[contract address](/cctp/references/stellar-contracts).
* If `destinationCaller` is wrong, the forwarder cannot complete the transfer.
* If `mintRecipient` is set to a user account or muxed address, USDC is not sent
to the forwarder.
In either case, funds become permanently stuck and **cannot be recovered**.
</Warning>
`mintRecipient` and `destinationCaller` must be the Stellar CCTP Forwarder
contract address. `TokenMessengerMinter` assumes `mintRecipient` is a contract
address, so this example validates `STELLAR_CCTP_FORWARDER` as a contract
`strkey` and uses the final recipient only in `hookData`.
```ts TypeScript theme={null}
async function burnUSDC() {
console.log("Burning USDC on Arc (with hook)...");
const cctpForwarderBytes32 = contractStrkeyToBytes32(STELLAR_CCTP_FORWARDER);
const burnTx = await arcWalletClient.sendTransaction({
to: ARC_TOKEN_MESSENGER as `0x${string}`,
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: [
AMOUNT,
STELLAR_DOMAIN,
cctpForwarderBytes32, // mintRecipient = Stellar CCTP Forwarder
ARC_USDC as `0x${string}`,
cctpForwarderBytes32, // destinationCaller = Stellar CCTP Forwarder
MAX_FEE,
2000, // Standard Transfer finality threshold
hookData,
],
}),
});
await arcPublicClient.waitForTransactionReceipt({ hash: burnTx });
console.log(`Burn Tx: ${burnTx}`);
return burnTx;
}
```
### 3.3. 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 `ARC_TESTNET_DOMAIN` for the `sourceDomain` path parameter, using the
[CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
for Arc Testnet (26).
* Pass `transactionHash` from the value returned by `burnUSDC` preceding.
```ts TypeScript theme={null}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${ARC_TESTNET_DOMAIN}?transactionHash=${transactionHash}`;
while (true) {
try {
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
if (response.status !== 404) {
const text = await response.text().catch(() => "");
console.error(
"Error fetching attestation:",
`${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}
await new Promise((resolve) => setTimeout(resolve, 5000));
continue;
}
const data = (await response.json()) as AttestationResponse;
if (data?.messages?.[0]?.status === "complete") {
console.log("Attestation retrieved successfully!");
return data.messages[0];
}
console.log("Waiting for attestation...");
await new Promise((resolve) => setTimeout(resolve, 5000));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("Error fetching attestation:", message);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
}
```
### 3.4. Mint and forward USDC on Stellar
The `submitSorobanTx` helper builds, signs, submits, and confirms a Soroban
contract transaction.
```ts TypeScript theme={null}
async function submitSorobanTx(
server: rpc.Server,
contractId: string,
method: string,
args: xdr.ScVal[],
) {
const account = await server.getAccount(stellarKeypair.publicKey());
const contract = new Contract(contractId);
const tx = new TransactionBuilder(account, {
fee: "10000000",
networkPassphrase: STELLAR_NETWORK_PASSPHRASE,
})
.addOperation(contract.call(method, ...args))
.setTimeout(120)
.build();
const simulated = await server.simulateTransaction(tx);
if (rpc.Api.isSimulationError(simulated)) {
throw new Error(`Simulation failed: ${JSON.stringify(simulated)}`);
}
const prepared = rpc.assembleTransaction(tx, simulated).build();
prepared.sign(stellarKeypair);
const sendResult = await server.sendTransaction(prepared);
if (sendResult.status === "ERROR") {
throw new Error(`Send failed: ${JSON.stringify(sendResult)}`);
}
let getResult = await server.getTransaction(sendResult.hash);
while (getResult.status === "NOT_FOUND") {
await new Promise((resolve) => setTimeout(resolve, 2000));
getResult = await server.getTransaction(sendResult.hash);
}
if (getResult.status !== "SUCCESS") {
throw new Error(`Transaction failed: ${JSON.stringify(getResult)}`);
}
return sendResult.hash;
}
```
Use the `submitSorobanTx` helper to call `mint_and_forward` on the Stellar CCTP
Forwarder. This verifies the CCTP message and attestation, mints USDC through
the `TokenMessengerMinter`, and forwards it to the recipient encoded in the hook
data:
```ts TypeScript theme={null}
async function mintAndForwardOnStellar(attestation: AttestationMessage) {
console.log("Minting and forwarding USDC o프로젝트로 돌아가기