bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 75쪽 중 49쪽
ts for the next five
minutes and returns an HTTP 429 (Too Many Requests) response.
</Note>
</Tab>
</Tabs>
# Transfer USDC between Stellar and Arc
Source: https://developers.circle.com/cctp/quickstarts/transfer-usdc-stellar-arc
Build scripts to transfer USDC between Arc Testnet and Stellar Testnet using CCTP
Use CCTP to transfer USDC with Stellar Testnet as the source or destination.
<Note>
On Stellar, USDC precision and address encoding differ from other CCTP-supported
blockchains. Before you integrate beyond these examples, read
[CCTP on Stellar](/cctp/references/stellar).
</Note>
Pick the tab that matches the direction of your transfer.
<Tabs>
<Tab title="Stellar to Arc">
This quickstart demonstrates how to transfer USDC from Stellar Testnet to Arc
Testnet using CCTP. You use the
[@stellar/stellar-sdk](https://github.com/stellar/js-stellar-sdk) library to
interact with Stellar Soroban contracts, and [`viem`](https://viem.sh/) to mint
USDC on Arc Testnet. 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 Stellar
Soroban transactions and basic EVM usage helps you follow and adapt the script.
Examples use Arc Testnet as the destination, but you can use any
[supported blockchain](/cctp/concepts/supported-chains-and-domains).
## 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) 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)
* Established a
[USDC trustline](/stablecoins/quickstart-setup-usdc-trustline-stellar) on
your Stellar account so you can hold the testnet USDC you burn
* Funded your Stellar wallet with Stellar Testnet USDC from the
[Circle Faucet](https://faucet.circle.com)
<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-stellar-to-arc
cd cctp-stellar-to-arc
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}
STELLAR_SECRET_KEY=YOUR_STELLAR_SECRET_KEY
EVM_PRIVATE_KEY=YOUR_EVM_PRIVATE_KEY
```
* `STELLAR_SECRET_KEY` is the Stellar secret key `(S...)` used to sign Soroban
transactions on Stellar Testnet.
* `EVM_PRIVATE_KEY` is the private key for the EVM wallet you use on Arc
Testnet.
<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 contract addresses, amounts, and clients for Stellar Testnet and Arc
Testnet.
### 2.1. Define configuration constants
The script predefines the contract addresses, transfer amount, and maximum fee.
```ts TypeScript theme={null}
import {
Address,
Contract,
Keypair,
nativeToScVal,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
pad,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { arcTestnet } from "viem/chains";
interface AttestationMessage {
message: string;
attestation: string;
status: string;
}
interface AttestationResponse {
messages: AttestationMessage[];
}
// Contract Addresses
const STELLAR_TOKEN_MESSENGER_MINTER =
"CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
// Transfer Parameters
const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)
// Chain-specific Parameters
const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet
// Stellar Soroban Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
// Authentication
const stellarKeypair = Keypair.fromSecret(
process.env.STELLAR_SECRET_KEY as string,
);
```
### 2.2. Set up wallet clients
The wallet client configures 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. Add helper function
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;
}
```
## Step 3: Implement the transfer logic
This step implements the core transfer logic: approve and burn on Stellar, poll
for an attestation, then mint on Arc. A successful run prints transaction hashes
and a completion message in the console.
### 3.1. Approve USDC on Stellar
Approve the `TokenMessengerMinterV2` contract to spend your USDC. The
`submitSorobanTx` helper is used to submit the approve call to the Stellar USDC
contract.
```ts TypeScript theme={null}
async function approveUSDC() {
console.log("Approving USDC spend on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const latestLedger = await server.getLatestLedger();
const expirationLedger = latestLedger.sequence + 100_000;
const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
new Address(stellarKeypair.publicKey()).toScVal(),
new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(expirationLedger, { type: "u32" }),
]);
console.log(`Approve Tx: ${approveHash}`);
}
```
### 3.2. Burn USDC on Stellar
Call `deposit_for_burn` to burn USDC on Stellar. The `submitSorobanTx` helper is
used to submit the burn call with the transfer parameters listed below:
* **Burn amount**: The amount of USDC to burn (in Stellar subunits, 7 decimals)
* **Destination domain**: The target blockchain for minting USDC (see
[supported blockchains and domains](/cctp/concepts/supported-chains-and-domains))
* **Mint recipient**: The wallet address that receives the minted USDC on Arc
* **Burn token**: The contract address of the USDC token on Stellar
* **Destination caller**: The address on the target blockchain that may call
`receiveMessage`
* **Max fee**: The maximum [fee](/cctp/concepts/fees) allowed for the transfer
(in Stellar subunits, 7 decimals)
* **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)
```ts TypeScript theme={null}
async function burnUSDC() {
console.log("Burning USDC on Stellar...");
// Bytes32 Formatted Parameters
const evmAddressBytes32 = pad(evmAccount.address);
const mintRecipient = xdr.ScVal.scvBytes(
Buffer.from(evmAddressBytes32.slice(2), "hex"),
);
const server = new rpc.Server(STELLAR_RPC_URL);
const txHash = await submitSorobanTx(
server,
STELLAR_TOKEN_MESSENGER_MINTER,
"deposit_for_burn",
[
new Address(stellarKeypair.publicKey()).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }),
mintRecipient,
new Address(STELLAR_USDC).toScVal(),
xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller
nativeToScVal(MAX_FEE, { type: "i128" }),
nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold
],
);
console.log(`Burn Tx: ${txHash}`);
return txHash;
}
```
### 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 `STELLAR_DOMAIN` for the `sourceDomain` path parameter, using the
[CCTP domain](/cctp/concepts/supported-chains-and-domains#domain-identifiers)
for Stellar Testnet (27).
* Pass `transactionHash` from the value returned by `submitSorobanTx` in the
`burnUSDC` function preceding.
```ts TypeScript theme={null}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_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 USDC on Arc
Call the
[`receiveMessage` function](/cctp/references/contract-interfaces#receivemessage)
from the [`MessageTransmitterV2` contract](/cctp/references/contract-addresses)
deployed on Arc Testnet to mint USDC on the destination blockchain.
* Pass the signed attestation and the 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 mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc Testnet...");
const hash = await arcWalletClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stateMutability: "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}`);
}
```
## Step 4: Full script
Create an `index.ts` file in your project directory and paste the full script
below so you can run the flow from one file.
```ts index.ts expandable theme={null}
import {
Address,
Contract,
Keypair,
nativeToScVal,
rpc,
TransactionBuilder,
xdr,
} from "@stellar/stellar-sdk";
import {
createPublicClient,
createWalletClient,
encodeFunctionData,
http,
pad,
} 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 STELLAR_TOKEN_MESSENGER_MINTER =
"CDNG7HXAPBWICI2E3AUBP3YZWZELJLYSB6F5CC7WLDTLTHVM74SLRTHP";
const STELLAR_USDC = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA";
const ARC_MESSAGE_TRANSMITTER = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275";
// Transfer Parameters
const AMOUNT = 10_000_000n; // 1 USDC (Stellar has 7 decimals)
const MAX_FEE = 100_000n; // 0.01 USDC in Stellar subunits (7 decimals)
// Chain-specific Parameters
const STELLAR_DOMAIN = 27; // Source domain ID for Stellar Testnet
const ARC_TESTNET_DOMAIN = 26; // Destination domain ID for Arc Testnet
// Stellar Soroban Configuration
const STELLAR_RPC_URL = "https://soroban-testnet.stellar.org";
const STELLAR_NETWORK_PASSPHRASE = "Test SDF Network ; September 2015";
// Authentication
const stellarKeypair = Keypair.fromSecret(
process.env.STELLAR_SECRET_KEY as string,
);
// Set up wallet clients
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(),
});
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;
}
// ============ CCTP Flow Functions ============
async function approveUSDC() {
console.log("Approving USDC spend on Stellar...");
const server = new rpc.Server(STELLAR_RPC_URL);
const latestLedger = await server.getLatestLedger();
const expirationLedger = latestLedger.sequence + 100_000;
const approveHash = await submitSorobanTx(server, STELLAR_USDC, "approve", [
new Address(stellarKeypair.publicKey()).toScVal(),
new Address(STELLAR_TOKEN_MESSENGER_MINTER).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(expirationLedger, { type: "u32" }),
]);
console.log(`Approve Tx: ${approveHash}`);
}
async function burnUSDC() {
console.log("Burning USDC on Stellar...");
// Bytes32 Formatted Parameters
const evmAddressBytes32 = pad(evmAccount.address);
const mintRecipient = xdr.ScVal.scvBytes(
Buffer.from(evmAddressBytes32.slice(2), "hex"),
);
const server = new rpc.Server(STELLAR_RPC_URL);
const txHash = await submitSorobanTx(
server,
STELLAR_TOKEN_MESSENGER_MINTER,
"deposit_for_burn",
[
new Address(stellarKeypair.publicKey()).toScVal(),
nativeToScVal(AMOUNT, { type: "i128" }),
nativeToScVal(ARC_TESTNET_DOMAIN, { type: "u32" }),
mintRecipient,
new Address(STELLAR_USDC).toScVal(),
xdr.ScVal.scvBytes(Buffer.alloc(32)), // destination_caller
nativeToScVal(MAX_FEE, { type: "i128" }),
nativeToScVal(1000, { type: "u32" }), // Fast Transfer finality threshold
],
);
console.log(`Burn Tx: ${txHash}`);
return txHash;
}
async function retrieveAttestation(transactionHash: string) {
console.log("Retrieving attestation...");
const url = `https://iris-api-sandbox.circle.com/v2/messages/${STELLAR_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));
}
}
}
async function mintUSDCOnArc(attestation: AttestationMessage) {
console.log("Minting USDC on Arc Testnet...");
const hash = await arcWalletClient.sendTransaction({
to: ARC_MESSAGE_TRANSMITTER as `0x${string}`,
data: encodeFunctionData({
abi: [
{
type: "function",
name: "receiveMessage",
stat프로젝트로 돌아가기