Programmable Token Transfers: Canton as Source
This tutorial demonstrates how to send a programmable token transfer (PTT) โ tokens plus arbitrary data โ from Canton testnet to Ethereum Sepolia using the ccip-starter-kit-canton.
Introduction
A programmable token transfer combines a token transfer with a data payload delivered to the destination receiver contract. On Sepolia, the receiver's ccipReceive function receives both the tokens and the calldata.
This tutorial uses the canton2any:data-and-token script to send LINK plus a UTF-8 string in a single CCIP message.
What You Will Build
In this tutorial, you will:
- Run
canton2any:data-and-tokenwith--dataStringand--amount. - Pay CCIP fees in Amulet or LINK.
- Track delivery in the CCIP Explorer.
- Verify token delivery and optional manual execution on Sepolia.
Understanding Programmable Token Transfers (canton2any)
| Aspect | Token-only | Programmable token transfer |
|---|---|---|
| Data | Empty | UTF-8 string (or 0x: prefixed ABI-encoded string) |
| Tokens | LINK only | LINK + data in one message |
| Destination | Tokens to receiver | Tokens + ccipReceive calldata on Sepolia |
The send path on Canton combines token pool lock/burn (TokenPoolTicket) with CommitteeVerifier tickets in a single PerPartyRouter_CCIPSend, as described in the Canton as Source flow.
How the Script Works
scripts/canton2any/ccipSendDataAndToken.ts builds a message with both data (from encodeMessageData) and tokenAmounts (Canton LINK instrument), then submits via CantonChain.sendMessage with the same fee and execution options as the other canton2any scripts.
Running the Programmable Token Transfer
Prerequisites Check
-
Complete prerequisites.
-
Confirm LINK and fee-token balances:
Terminalnpm run check-balance
Execute the Script
Send 0.001 LINK with a data payload:
npm run canton2any:data-and-token -- --dataString "Hello" --amount 0.001
Pay fee in LINK and skip auto-execution:
npm run canton2any:data-and-token -- --dataString "Hello" --amount 0.001 --feeToken link --no-exec
Custom Sepolia receiver and a higher gas limit (default is 200000):
npm run canton2any:data-and-token -- \
--dataString "Hello" \
--amount 0.001 \
--evmReceiver 0xYourReceiverAddress \
--gasLimit 300000
Understanding the Output
๐ง๐ช Sending data + 0.001 LINK from Canton โ Sepolia: "Hello"
๐ CCIP Message ID: 0xโฆ
๐ CCIP Explorer: https://ccip.chain.link/#/side-drawer/msg/0xโฆ
Verification and Monitoring
CCIP Explorer
Monitor message status and destination execution state in the CCIP Explorer.
Manual execution on Sepolia
After --no-exec sends, run manual execution once Committee Verifier proofs are on the indexer:
npm run canton2any:manual-exec -- <cantonUpdateId | messageId>
Pass either the Canton update ID (๐ Canton transaction / Lighthouse link) or the CCIP Message ID (๐ CCIP Message ID, 0xโฆ) from the send output โ canton2any:manual-exec accepts both.
Verify on Sepolia
Check TEST token balance and inspect the receiver contract's events on Sepolia Etherscan to confirm both token delivery and data processing.