- Base Sepolia to Arc Testnet: CCTP burns the source USDC. On Arc Testnet, the wrapper receives the mint and calls xReserve through DepositForHandler in the same transaction.
- Arc Testnet to Movement Bardock: xReserve attests the deposit, and the remote blockchain’s settlement process mints USDCx to the recipient.
Prerequisites
Before you begin, ensure that you’ve:- Installed Node.js v22.18+ and
curl. - Created a Base Sepolia wallet and funded it with test USDC from the Circle Faucet and Base Sepolia ETH for gas.
- Created a Movement Bardock recipient account. See the Movement deposit quickstart.
Step 1: Set up your project
Create a project and installviem:
.env from version control. Open .env in your
editor and replace the wallet key and recipient placeholders:
.env
BASE_SEPOLIA_RPC_URL. If you omit it, viem uses the default Base
Sepolia RPC.
Keep your private key secret. Don’t commit .env or print the key in logs or
shell history. You don’t need an Arc wallet for this flow.
Step 2: Configure the testnet route
Keep all parts of the transfer on testnets. Use these values for this example:
Create
config.ts:
config.ts
bytes32, with empty remote hook data.
Step 3: Build the deposit hook data
Build the data for the action that the wrapper runs on Arc. It has four layers:
DepositForHandler fills in the amount minted on Arc. Offset
4 is the start of
the amount argument after the four-byte function selector. The two hook frames
are siblings; don’t nest one in the other. See the
CCTP hook format.
Create hookData.ts:
hookData.ts
request.ts. Both the quote and the burn use the same hook data from
this file:
request.ts
refundAddress. The recovery address doesn’t change the remote
recipient.
Step 4: Request a signed fee quote
Get a price for the transfer before you approve or burn USDC. The quote covers Fast Transfer (PRE_FINALITY) and forwarding (FORWARD), with fees paid in
source USDC. The fee is charged in addition to the deposit amount, so the full 1
USDC arrives at xReserve on Arc. See
How upfront fees work.
Create prepare.ts:
prepare.ts
signedQuote, feeTotalAmount, feeToken, items, and
expiry. The items array breaks down the fee by service. The script uses
feeTotalAmount for the total; you don’t need to sum items yourself. The
signed quote binds to the amount, destination domain, destination caller, and
hook data. If you change the route, amount, or recipient, regenerate
request.json and request another quote before submission.
Quotes expire soon after issue. The Base Sepolia quote can expire while an
approval is pending. The next step checks expiry again after approval; if it
expires, repeat the quote request and submission step. Don’t treat the advisory
blockEstimatedAt as the authoritative expiry for a block-number quote. See
Quote expiry.
Step 5: Approve USDC and submit the burn
Approve the deposit amount plus the quoted fee to TokenMessengerWithFees, then submit the source transaction. BothmintRecipient and destinationCaller must
be the wrapper on Arc, encoded as bytes32.
Create submit.ts. It checks the source network and balance, waits for a
successful approval, checks quote expiry, and simulates the burn before sending
it:
submit.ts
feeToken, approve only the deposit amount, and attach the
quoted native fee as value. Don’t mix a native-token quote with the USDC fee
handling in this script. See
Transfer USDC with upfront fees.
Step 6: Track both settlement legs
Show progress for each stage: source burn, Arc forward, xReserve attestation, and remote mint. Only the last stage confirms that the recipient has USDCx.6.1. Find the Arc forwarding transaction
Use your Base Sepolia burn hash with the CCTP message API:YOUR_SOURCE_BURN_TX_HASH with the source burn hash printed by
submit.ts in Step 5.
The forwarding hash is inside each entry in messages. For example, an
abbreviated response after forwarding looks like this (hashes are placeholders):
forwardTxHash. If it’s pending, keep
tracking the same transfer. Check the Arc receipt for success and the xReserve
deposit event before moving to the next leg. If a source transaction contains
multiple CCTP messages, match the message to your amount, destination caller,
and hook data rather than selecting the first entry.
6.2. Fetch the xReserve attestation
Use the successful Arc transaction hash, rather than the Base Sepolia hash, with the xReserve attestation API:YOUR_ARC_FORWARD_TX_HASH with the forwardTxHash from the CCTP
message API response. Check that the Arc receipt succeeded before using that
hash here.
An abbreviated response for this example looks like this (hex values are
placeholders):
attestations array has the payload, message hash, signature,
and remote domain. Match it to your deposit and remote domain. This proves that
xReserve attested the deposit. It doesn’t prove that the remote blockchain
minted USDCx.
6.3. Confirm the remote mint
Query the recipient’s USDCx balance on Movement Bardock using its public RPC:YOUR_MOVEMENT_RECIPIENT with the address you set in .env. The
response is an array containing a balance string in base units. For example,
["1000000"] means 1 USDCx. For this deposit, expect the balance to increase by
1,000,000 base units from the value recorded before the burn, assuming no other
activity on the recipient account.
Use the
Movement testnet explorer
to confirm a successful mint transaction for the intended recipient and amount.
Match its deposit nonce to the xReserve attestation so that you don’t mistake an
unrelated credit for this deposit. See the
Movement deposit quickstart
for more on the remote integration.
Keep the source hash, Arc hash, xReserve message hash, and remote mint reference
together for support and reconciliation.
Step 7: Handle failures and delayed deposits
Check your route before burning. The quote service checks that the forwarding workflow is enabled. It doesn’t check every xReserve argument in the hook data. A quote doesn’t prove that a remote domain, recipient, or remote hook data is valid. Test each route and recipient format before users can use it.
A paused or reverting destination action can require coordinated manual replay
of the unconsumed CCTP message after the cause is resolved. This retries the
original deposit; it is separate from the planned recovery feature. Don’t assume
the service automatically retries every failed deposit. A source burn can’t be
undone by requesting another quote.
Upfront fees aren’t refunded.
The fee claim’s
refundAddress is reserved for potential future use; setting it
doesn’t promise a refund if a later leg fails. The wrapper’s recoveryAddress
is a separate field reserved for compatibility with a future recovery feature.
The wrapper does not implement a recovery function. Supplying a
recoveryAddress does not enable fund recovery.