CCTP

Transfer USDC on testnet between Sui and Ethereum

Explore this tutorial for transferring USDC between Sui testnet and Ethereum Sepolia Testnet via CCTP V1

To get started with CCTP on Sui testnet, follow the example scripts provided here. The Readme contains instructions for running the scripts. The examples use the Sui SDK, to transfer USDC to and from an address on Sui testnet and an address on an external blockchain.

Summary of calling deposit_for_burn() (full runnable script can be found in the sui-cctp repository):

JavaScript
// Create DepositForBurn tx
const depositForBurnTx = new Transaction();

// Split USDC to send in depositForBurn call
const ownedCoins = await client.getAllCoins({owner: signer.toSuiAddress()})
const usdcStruct = ownedCoins.data.find(c => c.coinType.includes(usdcId));
if (!usdcStruct || Number(usdcStruct.balance) < USDC_AMOUNT) {
  throw new Error("Insufficient tokens in wallet to initiate transfer.");
}

const [coin] = depositForBurnTx.splitCoins(
  usdcStruct.coinObjectId,
  [USDC_AMOUNT]
);

// Create the deposit_for_burn move call
depositForBurnTx.moveCall({
  target: `${tokenMessengerMinterId}::deposit_for_burn::deposit_for_burn`,
  arguments: [
    depositForBurnTx.object(coin), // Coin<USDC>
    depositForBurnTx.pure.u32(DESTINATION_DOMAIN), // destination_domain
    depositForBurnTx.pure.address(evmUserAddress), // mint_recipient
    depositForBurnTx.object(tokenMessengerMinterStateId), // token_messenger_minter state
    depositForBurnTx.object(messageTransmitterStateId), // message_transmitter state
    depositForBurnTx.object("0x403"), // deny_list id, fixed address
    depositForBurnTx.object(treasuryId) // treasury object Treasury<USDC>
  ],
  typeArguments: [`${usdcId}::usdc::USDC`],
});

// Broadcast the transaction
console.log("Broadcasting sui deposit_for_burn tx...");
const depositForBurnOutput = await executeTransactionHelper({
  client: client,
  signer: signer,
  transaction: depositForBurnTx,
});
assert(!depositForBurnOutput.errors);
console.log(`deposit_for_burn transaction successful: 0x${depositForBurnOutput.digest} \n`);

// Get USDC balance changes (optional)
const suiUsdcBalanceChange = depositForBurnOutput.balanceChanges?.find(b => b.coinType.includes(usdcId))
const balances = await client.getAllBalances({ owner: signer.toSuiAddress() });
const usdcBalance = balances.find(b => b.coinType.includes(usdcId))?.totalBalance;

// Get the message emitted from the tx
const messageRaw: Uint8Array = (depositForBurnOutput.events?.find((event) =>
  event.type.includes("send_message::MessageSent")
)?.parsedJson as any).message;
const messageBuffer = Buffer.from(messageRaw);
const messageHex = `0x${messageBuffer.toString("hex")}`;
const messageHash = web3.utils.keccak256(messageHex);
console.log(`Message hash: ${messageHash}`);

Summary of calling receive_message() (full runnable script can be found in the sui-cctp repository):

JavaScript
// Create receiveMessage transaction
const receiveMessageTx = new Transaction()

// Add receive_message move call to MessageTransmitter
const [receipt] = receiveMessageTx.moveCall({
  target: `${messageTransmitterId}::receive_message::receive_message`,
  arguments: [
    receiveMessageTx.pure.vector(
      'u8',
      Buffer.from(evmBurnTx.message.replace('0x', ''), 'hex'),
    ), // message as byte array
    receiveMessageTx.pure.vector(
      'u8',
      Buffer.from(attestation.replace('0x', ''), 'hex'),
    ), // attestation as byte array
    receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state
  ],
})

// Add handle_receive_message call to TokenMessengerMinter with Receipt from receive_message call
const [stampReceiptTicketWithBurnMessage] = receiveMessageTx.moveCall({
  target: `${tokenMessengerMinterId}::handle_receive_message::handle_receive_message`,
  arguments: [
    receipt, // Receipt object returned from receive_message call
    receiveMessageTx.object(tokenMessengerMinterStateId), // token_messenger_minter state
    receiveMessageTx.object('0x403'), // deny list, fixed address
    receiveMessageTx.object(treasuryId), // usdc treasury object Treasury<T>
  ],
  typeArguments: [`${usdcId}::usdc::USDC`],
})

// Add deconstruct_stamp_receipt_ticket_with_burn_message call
const [stampReceiptTicket] = receiveMessageTx.moveCall({
  target: `${tokenMessengerMinterId}::handle_receive_message::deconstruct_stamp_receipt_ticket_with_burn_message`,
  arguments: [stampReceiptTicketWithBurnMessage],
})

// Add stamp_receipt call
const [stampedReceipt] = receiveMessageTx.moveCall({
  target: `${messageTransmitterId}::receive_message::stamp_receipt`,
  arguments: [
    stampReceiptTicket, // Receipt ticket returned from deconstruct_stamp_receipt_ticket_with_burn_message call
    receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state
  ],
  typeArguments: [
    `${tokenMessengerMinterId}::message_transmitter_authenticator::MessageTransmitterAuthenticator`,
  ],
})

// Add complete_receive_message call to MessageTransmitter with StampedReceipt from stamp_receipt call.
// Receipt and StampedReceipt are Hot Potatoes so they must be destroyed for the
// transaction to succeed.
receiveMessageTx.moveCall({
  target: `${messageTransmitterId}::receive_message::complete_receive_message`,
  arguments: [
    stampedReceipt, // Stamped receipt object returned from handle_receive_message call
    receiveMessageTx.object(messageTransmitterStateId), // message_transmitter state
  ],
})

// Broadcast the transaction
console.log('Broadcasting Sui receive_message tx...')
const receiveMessageOutput = await executeTransactionHelper({
  client: client,
  signer: signer,
  transaction: receiveMessageTx,
})
Did this page help you?
© 2023-2025 Circle Technology Services, LLC. All rights reserved.