Skip to main content
Version: 1.15.0

CCT on Canton

CantonTokenManager configures registry-pool CCT deployments on Canton. It supports pool deployment, remote-lane updates, and reads for pool, registry, and rate-limiter state.

TypeScript
import { CantonChain } from '@chainlink/ccip-sdk'
import { CantonTokenManager } from '@chainlink/ccip-sdk/cct/canton'

const chain = await CantonChain.fromUrl(process.env.CANTON_LEDGER_URL!, {
cantonConfig: {
party: process.env.CANTON_PARTY!,
ccipParty: process.env.CCIP_PARTY!,
jwt: process.env.CANTON_JWT!,
edsUrl: process.env.EDS_URL!,
transferInstructionUrl: process.env.TRANSFER_INSTRUCTION_URL!,
chainId: 'canton:TestNet',
},
})
const cct = CantonTokenManager.fromChain(chain)

The signed operations below take a wallet. See Multi-Chain for Canton connection and wallet configuration.

Required configuration​

ValuePurpose
Ledger URL and JWTConnect and authenticate the Canton JSON Ledger API client.
Party and CCIP partySet the transaction and protocol identities in cantonConfig.
Instrument ID and Canton decimalsIdentify the bridged instrument and its on-ledger scale.
Observer partiesRequired for EDS auto-detection. The ledger rejects an empty list.
Remote addresses and rate limitersRequired for each lane added during deployment or later updates.

On canton:TestNet and canton:MainNet, the SDK supplies the shared CCIP contracts (TokenAdminRegistry, FeeQuoter, RMNRemote). On other networks, pass them through deps as raw instance addresses ("instanceId@party").

Set up the Canton pool​

On Canton, a single atomic operation stands up the whole token pool, so most of the setup happens in one call.

Deploy and initialize the pool​

Canton deployment is atomic: deployTokenPool creates the burn/mint or lock/release token pool, initializes it, registers the token, and creates the specified lane rate limiters in one operation. Both the token administrator and the pool owner must authorize it.

TypeScript
const deployment = await cct.deployTokenPool({
wallet,
poolType: 'burnMint',
instanceId: 'example-token-pool-001',
instrumentId: { admin: process.env.CANTON_PARTY!, id: 'EXAMPLE' },
decimals: 10,
observers: [process.env.CANTON_PARTY!],
lanes: [],
})

console.log('Pool:', deployment.poolInstanceAddress)
console.log('Token config CID:', deployment.tokenConfigCid)

The pool owner is instrumentId.admin, and the token administrator admin defaults to it. The CCIP owner is the owner of the pool's deps.tokenAdminRegistry.

deployment.poolInstanceAddress is optional on the result, so narrow it before reusing it in applyChainUpdates or getTokenPoolState, both of which require a string:

TypeScript
if (!deployment.poolInstanceAddress) {
throw new Error('deployTokenPool did not return a pool instance address')
}
const poolInstanceAddress = deployment.poolInstanceAddress

decimals is the Canton instrument's decimals, not the remote token's decimals. An incorrect value mis-scales transfers. Add initial lanes through lanes, or configure them later with applyChainUpdates.

Third-party token administrators​

If a third party was already proposed as the instrument's administrator, pass that party as admin. deployTokenPool finds the instrument's TokenConfig awaiting an administrator and hands it to Initialize. For a new self-administered instrument, deployTokenPool registers the token atomically.

Configure remote chains​

applyChainUpdates consumes the pool contract and returns a new one. The returned contract ID changes on every call, so read state again after each update instead of retaining the prior CID.

TypeScript
const update = await cct.applyChainUpdates({
wallet,
poolInstanceAddress,
chainsToAdd: [
{
remoteChainSelector: BigInt(process.env.DEST_CHAIN_SELECTOR!),
remotePools: [process.env.REMOTE_POOL!],
remoteTokenAddress: process.env.REMOTE_TOKEN!,
inboundRateLimiter: process.env.INBOUND_RATE_LIMITER!,
outboundRateLimiter: process.env.OUTBOUND_RATE_LIMITER!,
},
],
})

console.log('Updated pool CID:', update.poolCid)

Write remotePools and remoteTokenAddress in the remote chain's own format, such as 0x… for EVM or base58 for Solana. The remote chain family must be registered with the SDK, for example with import '@chainlink/ccip-sdk/all'. normalizeRemoteAddress(address, remoteChainSelector) converts an address to the stored form that getTokenPoolState returns.

Inbound and outbound rate limiters are required and must be distinct. For a finality configuration faster than finality (WaitForSafe or BlockDepth), also provide the inbound custom-block-confirmations limiter.

When deploying initial lanes, every lanes entry requires all three limiter specs: inbound, outbound, and inbound custom finality. This is true even when the lane uses standard finality, because the pool initializes all three limiter contracts together.

note

applyChainUpdates references inboundRateLimiter and outboundRateLimiter by raw instance address ("instanceId@party"). It does not create them and does not accept capacity or rate. Limiter contracts are created only by deployTokenPool, whose atomic Initialize builds each lane's limiters from a full spec (instance ID, enabled, capacity, rate) and returns their contract IDs on the result as rateLimiterCids. The manager exposes no standalone rate-limiter creation operation, so the limiter contracts a post-deploy lane points at must already exist on-ledger.

Verify the configuration​

Read the pool, registry, and rate-limiter state back before you transfer through a lane, so you know the deployment landed as intended.

Read the pool and registry​

TypeScript
const pool = await cct.getTokenPoolState({ poolInstanceAddress })
console.log('Pool type:', pool.poolType)

const registry = await cct.getTokenAdminRegistry({
instrumentId: { admin: process.env.CANTON_PARTY!, id: 'EXAMPLE' },
adminParty: process.env.CANTON_PARTY!,
})

poolOwner is optional on the pool and rate-limiter reads. It defaults to the owner in a raw "instanceId@owner" address, else to the chain's cantonConfig.party.

Check rate limiters​

Use getRateLimiterState to verify each lane limiter is enabled and has the expected capacity and rate.

Confirm getTokenAdminRegistry returns the expected pool, getTokenPoolState contains the intended remote lane, and every referenced limiter is enabled with sufficient capacity before transferring through the configured lane.

Sends from a Canton source follow Sending Messages. A Canton-source transfer also needs source-side extraArgs (feeTokenHoldingCids, ccvRawAddresses), so it is not a single copy-paste CLI line the way an EVM send is.