Multi-Chain
The SDK provides a unified Chain interface across all supported blockchain families. This guide covers connecting to each chain, cross-family messaging, and chain-specific details.
For EVM-only usage, see Sending Messages and Tracking Messages — they cover the full workflow without multi-chain considerations.
Supported Chains
| Chain Class | Family | Wallet Type | Send | Execute | Token Pools |
|---|---|---|---|---|---|
EVMChain | EVM | ethers Signer or viemWallet(client) | Yes | Yes | Yes |
SolanaChain | Solana | Anchor-compatible Wallet | Yes | Yes | Yes |
AptosChain | Aptos | AptosAsyncAccount | Yes | Yes | Yes |
SuiChain | Sui | Keypair | No | Yes | No |
TONChain | TON | TONWallet | Yes | Yes | No |
CantonChain | Canton | CantonWallet | Yes | Yes | No |
Sui is execution-only — it can execute incoming messages but cannot send cross-chain messages. Token pool queries are not implemented on Sui, TON, or Canton.
Canton is partial — it supports send and manual execution but requires a CantonConfig object (party IDs, JWT, EDS URLs). Fee estimation, balance queries, message tracking via getMessagesInTx, and registry/pool queries are not yet implemented.
Connecting
- EVM
- Solana
- Aptos
- TON
- Canton
import { EVMChain } from '@chainlink/ccip-sdk'
const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')
console.log('Connected to:', chain.network.name)
console.log('Chain selector:', chain.network.chainSelector)
import { SolanaChain } from '@chainlink/ccip-sdk'
const chain = await SolanaChain.fromUrl('https://api.devnet.solana.com')
console.log('Connected to:', chain.network.name)
import { AptosChain } from '@chainlink/ccip-sdk'
const chain = await AptosChain.fromUrl('https://fullnode.testnet.aptoslabs.com/v1')
console.log('Connected to:', chain.network.name)
import { TONChain } from '@chainlink/ccip-sdk'
const chain = await TONChain.fromUrl('https://toncenter.com/api/v2/jsonRPC')
console.log('Connected to:', chain.network.name)
import { CantonChain } from '@chainlink/ccip-sdk'
const chain = await CantonChain.fromUrl('https://ledger.example.com/api/json', {
cantonConfig: {
party: 'sender::1220...',
ccipParty: 'ccip::1220...',
jwt: 'eyJ...',
edsUrl: 'https://eds.example.com',
transferInstructionUrl: 'https://transfer-instruction.example.com',
indexerUrl: 'https://indexer.example.com',
senderInstanceId: 'ccipsender',
chainId: 'canton:TestNet',
},
})
console.log('Connected to:', chain.network.name)
Canton requires cantonConfig in the second argument to fromUrl. See Canton Configuration for the full config schema.
All chains accept the same ChainContext options:
const chain = await EVMChain.fromUrl('https://rpc.sepolia.org', {
logger: console, // Custom logger
apiClient: null, // Disable CCIP API (RPC-only mode)
})
Unified Interface
All chain classes implement the Chain interface, so you can write chain-agnostic code:
import {
type Chain,
EVMChain,
SolanaChain,
AptosChain,
CantonChain,
networkInfo,
ChainFamily,
} from '@chainlink/ccip-sdk'
function createChain(
rpcUrl: string,
family: ChainFamily,
cantonConfig?: Parameters<typeof CantonChain.fromUrl>[1],
): Promise<Chain> {
switch (family) {
case ChainFamily.EVM:
return EVMChain.fromUrl(rpcUrl)
case ChainFamily.Solana:
return SolanaChain.fromUrl(rpcUrl)
case ChainFamily.Aptos:
return AptosChain.fromUrl(rpcUrl)
case ChainFamily.Canton:
return CantonChain.fromUrl(rpcUrl, cantonConfig)
default:
throw new Error(`Unsupported family: ${family}`)
}
}
// Works with any chain
async function getMessages(chain: Chain, txHash: string) {
const requests = await chain.getMessagesInTx(txHash)
for (const req of requests) {
console.log('Message ID:', req.message.messageId)
}
return requests
}
Cross-Chain Token Transfers
For token-only transfers (no data), the SDK auto-populates extraArgs with sensible defaults for the destination chain. You only need receiver and tokenAmounts:
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'
import { parseUnits } from 'viem'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const router = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59'
// Same code works regardless of destination chain family
const destSelector = networkInfo('solana-devnet').chainSelector // or 'avalanche-testnet-fuji', 'aptos-testnet'
const tokenInfo = await source.getTokenInfo(tokenAddress)
const amount = parseUnits('10', tokenInfo.decimals)
const message = {
receiver: 'RecipientAddressOnDestChain...',
tokenAmounts: [{ token: tokenAddress, amount }],
}
const fee = await source.getFee({ router, destChainSelector: destSelector, message })
await source.sendMessage({
router,
destChainSelector: destSelector,
message,
wallet,
})
The SDK handles computeUnits (Solana), gasLimit (EVM/Aptos), tokenReceiver, allowOutOfOrderExecution, and other chain-specific fields automatically.
Cross-Family Messaging
For messages with data (arbitrary messaging or programmable token transfers), you must provide chain-specific extraArgs. The SDK encodes them automatically — just pass the right fields.
For full details on receiver contract setup and account configuration, see the dedicated tutorials:
- Build CCIP messages to Solana
- Build CCIP messages from Solana
- Build CCIP messages to Aptos
- Build CCIP messages from Aptos
- EVM → Solana
- Solana → EVM
- EVM → Aptos
- Aptos → EVM
- Canton → EVM
- EVM → Canton
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const destSelector = networkInfo('solana-devnet').chainSelector
const message = {
receiver: 'SolanaProgramAddress...',
data: '0x1234...',
tokenAmounts: [],
extraArgs: {
computeUnits: 200000n,
accountIsWritableBitmap: 0n,
allowOutOfOrderExecution: true,
tokenReceiver: '', // Required when sending data + tokens
accounts: [], // Additional accounts the program needs
},
}
const fee = await source.getFee({ router, destChainSelector: destSelector, message })
When sending data + tokens, tokenReceiver must be set to the Solana wallet or PDA that will receive the tokens.
import { SolanaChain, networkInfo } from '@chainlink/ccip-sdk'
const source = await SolanaChain.fromUrl('https://api.devnet.solana.com')
const destSelector = networkInfo('ethereum-testnet-sepolia').chainSelector
const message = {
receiver: '0xEVMReceiverAddress...',
data: Buffer.from('hello'),
tokenAmounts: [],
extraArgs: {
gasLimit: 200000n,
allowOutOfOrderExecution: false,
},
}
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const destSelector = networkInfo('aptos-testnet').chainSelector
const message = {
receiver: '0xAptosModuleAddress...',
data: '0x1234...',
tokenAmounts: [],
extraArgs: {
gasLimit: 200000n,
allowOutOfOrderExecution: true,
},
}
const fee = await source.getFee({ router, destChainSelector: destSelector, message })
import { AptosChain, networkInfo } from '@chainlink/ccip-sdk'
const source = await AptosChain.fromUrl('https://fullnode.testnet.aptoslabs.com/v1')
const destSelector = networkInfo('ethereum-testnet-sepolia').chainSelector
const message = {
receiver: '0xEVMReceiverAddress...',
data: new Uint8Array([0x12, 0x34]),
tokenAmounts: [],
extraArgs: {
gasLimit: 200000n,
allowOutOfOrderExecution: false,
},
}
import { CantonChain, networkInfo } from '@chainlink/ccip-sdk'
const source = await CantonChain.fromUrl('https://ledger.example.com/api/json', {
cantonConfig: { /* party, ccipParty, jwt, edsUrl, transferInstructionUrl, ... */ },
})
const destSelector = networkInfo('ethereum-testnet-sepolia').chainSelector
const message = {
receiver: '0xEVMReceiverAddress...',
data: '0x1234...',
tokenAmounts: [{ token: 'admin::1220...::link-token', amount: 1_000_000_000n }],
extraArgs: {
gasLimit: 200_000n,
feeTokenHoldingCids: ['#contract-id-1'], // Amulet or LINK fee token holdings
executorMode: 'default',
},
}
// router = CCIPSender instance id on Canton (not an EVM address)
const request = await source.sendMessage({
router: 'ccipsender',
destChainSelector: destSelector,
message: { ...message, fee: 0n }, // Canton fees are computed on-ledger
wallet,
})
On Canton source, router is the CCIPSender instance id (e.g. ccipsender), not a contract address. getFee() returns 0n — fees are computed on-ledger during send.
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'
const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const destSelector = networkInfo('canton-testnet').chainSelector
const message = {
receiver: '0x...', // keccak256(partyId) as 32-byte hex
data: '0x1234...',
tokenAmounts: [],
extraArgs: {
gasLimit: 200_000n,
finality: 'finalized', // GenericExtraArgsV3 for Canton destinations
},
}
const fee = await source.getFee({ router, destChainSelector: destSelector, message })
Canton receivers use keccak256(partyId) as the 32-byte CCIP address. Manual execution on Canton requires a CantonConfig with indexerUrl for CCV verifications.
The extraArgs fields depend on the destination chain family:
| Destination | Extra Args Type | Key Fields |
|---|---|---|
| EVM | EVMExtraArgsV1/V2/V3 | gasLimit, allowOutOfOrderExecution |
| Solana | SVMExtraArgsV1 | computeUnits, accounts, accountIsWritableBitmap, tokenReceiver, allowOutOfOrderExecution |
| Aptos | EVMExtraArgsV2 | gasLimit, allowOutOfOrderExecution |
| Sui | SuiExtraArgsV1 | gasLimit, allowOutOfOrderExecution, tokenReceiver, receiverObjectIds |
| TON | EVMExtraArgsV2 | gasLimit, allowOutOfOrderExecution |
| Canton (source) | CantonExtraArgsV1 | feeTokenHoldingCids, ccvRawAddresses, gasLimit, executorMode |
| Canton (destination) | GenericExtraArgsV3 | finality (defaults to 'finalized') |
For token-only transfers (no data), you can omit extraArgs entirely — the SDK fills in defaults.
Solana Notes
Transaction Flow
Solana uses transaction signatures (not hashes). Use getTransaction() to fetch details:
const chain = await SolanaChain.fromUrl('https://api.devnet.solana.com')
const tx = await chain.getTransaction(signature)
const messages = await chain.getMessagesInTx(tx)
Unsigned Transactions
For browser wallets (Phantom, Solflare), generate unsigned transactions and let the wallet handle signing, compute budget, and priority fees:
import { SolanaChain } from '@chainlink/ccip-sdk'
import { TransactionMessage, VersionedTransaction } from '@solana/web3.js'
const chain = await SolanaChain.fromUrl('https://api.devnet.solana.com')
const unsignedTx = await chain.generateUnsignedSendMessage({
sender: walletPublicKey.toBase58(),
router: routerAddress,
destChainSelector,
message,
})
// Build versioned transaction
const { blockhash } = await connection.getLatestBlockhash()
const messageV0 = new TransactionMessage({
payerKey: walletPublicKey,
recentBlockhash: blockhash,
instructions: unsignedTx.instructions,
}).compileToV0Message(unsignedTx.lookupTables)
const transaction = new VersionedTransaction(messageV0)
const signature = await sendTransaction(transaction, connection)
Canton Notes
Configuration
Canton requires a CantonConfig object passed via ChainContext.cantonConfig when calling CantonChain.fromUrl. Required fields: party, ccipParty, jwt, edsUrl, transferInstructionUrl. See Canton Configuration for the full schema.
Identity and Addresses
- Party IDs (
hint::1220...) identify Canton participants on the ledger - CCIP wire addresses for Canton receivers are
keccak256(partyId)as 32-byte hex - Transaction IDs are Canton ledger update IDs (
1220+ 32-byte SHA-256 digest), not EVM tx hashes
Send Extras
Canton source sends use CantonExtraArgsV1 (not on-wire encoded bytes):
| Field | Description |
|---|---|
feeTokenHoldingCids | Contract IDs for fee token holdings (Amulet or LINK). Optional on sendMessage (auto-discovered from the party's holdings via fillCantonSendDefaults); required when calling generateUnsignedSendMessage directly. |
ccvRawAddresses | CCV instance addresses in instanceId@party format |
gasLimit | Gas limit for destination ccipReceive callback |
executorMode | 'default' or 'none' (skip executor for no-execution lanes) |
Fee Model
getFee() returns 0n on Canton — fees are computed on-ledger during the send transaction. Fee tokens are instrument IDs (native Amulet or CCIP LINK).
Unsigned Transactions
Canton unsigned sends return JsCommands (Daml exercise commands with disclosed contracts):
const unsignedTx = await source.generateUnsignedSendMessage({
sender: cantonConfig.party,
router: 'ccipsender',
destChainSelector,
message,
})
// unsignedTx.commands — submit via interactive submission (prepare → sign → execute)
Related
- Sending Messages — EVM send workflow, message types, extra args
- Tracking Messages — Track message lifecycle
- Querying Data — Network info, balances, fees
- Token Pools — Pool configuration and rate limits