Skip to main content
Version: 1.0.0

Multi-Chain

Supported Chains

Chain ClassFamilyNetworks
EVMChainEVMEthereum, Arbitrum, Optimism, Avalanche, Base, Polygon, BSC
SolanaChainSolanaSolana Mainnet, Devnet
AptosChainAptosAptos Mainnet, Testnet
SuiChainSuiSui Mainnet, Testnet
TONChainTONTON Mainnet, Testnet

Connecting

TypeScript
import { EVMChain } from '@chainlink/ccip-sdk'

// From RPC URL
const ethereum = await EVMChain.fromUrl('https://rpc.sepolia.org')
const arbitrum = await EVMChain.fromUrl('https://sepolia-rollup.arbitrum.io/rpc')

// With custom logger
const chain = await EVMChain.fromUrl(
'https://rpc.sepolia.org',
{ logger: console }
)

console.log('Connected to:', chain.network.name)
console.log('Chain selector:', chain.network.chainSelector)

Unified Interface

All chain classes implement the Chain interface for chain-agnostic code:

TypeScript
import { type Chain, EVMChain, SolanaChain } from '@chainlink/ccip-sdk'

async function trackMessage(chain: Chain, txHash: string) {
const requests = await chain.getMessagesInTx(txHash)
return requests
}

// Use with EVM
const evmChain = await EVMChain.fromUrl('https://rpc.sepolia.org')
const evmRequests = await trackMessage(evmChain, '0x1234...')

// Use with Solana
const solanaChain = await SolanaChain.fromUrl('https://api.devnet.solana.com')
const solanaRequests = await trackMessage(solanaChain, 'base58signature...')

Cross-Chain Token Transfers

For token-only transfers (no data), the SDK abstracts away all chain-specific complexity. You only need receiver and tokenAmounts — the SDK auto-populates extraArgs with sensible defaults for every destination chain:

TypeScript
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl('https://rpc.sepolia.org')
const router = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59'
const destSelector = networkInfo('avalanche-testnet-fuji').chainSelector

const message = {
receiver: '0xRecipientAddress...',
tokenAmounts: [{ token: LINK_TOKEN, amount: 1000000000000000000n }],
}

const fee = await source.getFee({ router, destChainSelector: destSelector, message })
await source.sendMessage({
router, destChainSelector: destSelector,
message: { ...message, fee },
wallet,
})

The SDK automatically handles computeUnits (Solana), gasLimit (Aptos), tokenReceiver, allowOutOfOrderExecution, and other chain-specific fields.

Cross-Family Messaging

For messages with data (arbitrary messaging or programmable token transfers), you need to provide chain-specific extraArgs. The sections below show the SDK usage. For full details on building the message structure for each chain, see the dedicated tutorials:

EVM to Solana

TypeScript
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl('https://rpc.sepolia.org')

const message = {
receiver: '...', // Solana program address
data: '0x1234...',
tokenAmounts: [],
feeToken: '0x' + '0'.repeat(40),
extraArgs: {
computeUnits: 200000n,
accountIsWritableBitmap: 0n,
allowOutOfOrderExecution: true,
tokenReceiver: '',
accounts: [],
},
}

const destSelector = networkInfo('solana-devnet').chainSelector
const router = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59'
const fee = await source.getFee({
router,
destChainSelector: destSelector,
message,
})

When sending data + tokens to Solana, tokenReceiver is required — it must be the Solana wallet or PDA that will receive the tokens. See Build CCIP messages to Solana for full details on accounts, accountIsWritableBitmap, and computeUnits.

Solana to EVM

TypeScript
import { SolanaChain, networkInfo } from '@chainlink/ccip-sdk'

const source = await SolanaChain.fromUrl('https://api.devnet.solana.com')

const message = {
receiver: '0xEVMReceiverAddress...',
data: Buffer.from('hello'),
tokenAmounts: [],
extraArgs: {
gasLimit: 200000n,
allowOutOfOrderExecution: false,
},
}

const destSelector = networkInfo('ethereum-testnet-sepolia').chainSelector

See Build CCIP messages from Solana for full details.

EVM to Aptos

TypeScript
import { EVMChain, networkInfo } from '@chainlink/ccip-sdk'

const source = await EVMChain.fromUrl('https://rpc.sepolia.org')

const message = {
receiver: '0xAptosModuleAddress...', // Aptos account with ccip_receive
data: '0x1234...',
tokenAmounts: [],
feeToken: '0x' + '0'.repeat(40),
extraArgs: {
gasLimit: 200000n,
allowOutOfOrderExecution: true,
},
}

const destSelector = networkInfo('aptos-testnet').chainSelector
const router = '0x0BF3dE8c5D3e8A2B34D2BEeB17ABfCeBaf363A59'
const fee = await source.getFee({
router,
destChainSelector: destSelector,
message,
})

See Build CCIP messages to Aptos for details on receiver addressing, resource accounts, and programmable token transfers.

Aptos to EVM

TypeScript
import { AptosChain, networkInfo } from '@chainlink/ccip-sdk'

const source = await AptosChain.fromUrl('https://fullnode.testnet.aptoslabs.com/v1')

const message = {
receiver: '0xEVMReceiverAddress...',
data: new Uint8Array([0x12, 0x34]),
tokenAmounts: [],
extraArgs: {
gasLimit: 200000n,
allowOutOfOrderExecution: false,
},
}

const destSelector = networkInfo('ethereum-testnet-sepolia').chainSelector

See Build CCIP messages from Aptos for full details.

Chain-Specific Notes

EVM

  • Full log filtering by topic and address
  • Supports block range pagination
  • Transaction receipts include gas used
TypeScript
import { EVMChain } from '@chainlink/ccip-sdk'

const chain = await EVMChain.fromUrl('https://rpc.sepolia.org')

for await (const log of chain.getLogs({
topics: ['CCIPMessageSent'],
startBlock: 1000000,
endBlock: 1001000,
})) {
console.log('Log at block:', log.blockNumber)
}

Solana

  • Requires program address for log filtering
  • Uses signatures instead of transaction hashes
  • Account-based model affects message structure
TypeScript
import { SolanaChain } from '@chainlink/ccip-sdk'

const chain = await SolanaChain.fromUrl('https://api.devnet.solana.com')

for await (const log of chain.getLogs({
address: 'CCIPProgramAddress...',
topics: ['CCIPMessageSent'],
})) {
console.log('Found message')
}

Solana Transaction Flow

For Solana, use getTransaction() to fetch transaction details before extracting messages:

TypeScript
import { SolanaChain } from '@chainlink/ccip-sdk'

const chain = await SolanaChain.fromUrl('https://api.devnet.solana.com')

// Fetch transaction details from signature
const tx = await chain.getTransaction(signature)

// Extract messages from the transaction
const messages = await chain.getMessagesInTx(tx)
const messageId = messages[0]?.message.messageId

Solana Wallet Integration

Modern wallets like Phantom automatically add compute budget and priority fee instructions:

TypeScript
import { SolanaChain } from '@chainlink/ccip-sdk'
import { TransactionMessage, VersionedTransaction } from '@solana/web3.js'

const chain = await SolanaChain.fromUrl('https://api.devnet.solana.com')

// Generate unsigned transaction
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)

// Wallet automatically adds compute budget and priority fees
const signature = await sendTransaction(transaction, connection)

Aptos

  • Move-based smart contracts
  • Different event structure
  • Sequence numbers for ordering
TypeScript
import { AptosChain } from '@chainlink/ccip-sdk'

const chain = await AptosChain.fromUrl('https://fullnode.testnet.aptoslabs.com/v1')

const requests = await chain.getMessagesInTx('0xAptosTransactionHash...')

Network Information

Use networkInfo to get chain details:

TypeScript
import { networkInfo } from '@chainlink/ccip-sdk'

// Get by network name
const sepolia = networkInfo('ethereum-testnet-sepolia')
console.log('Chain selector:', sepolia.chainSelector)
console.log('Family:', sepolia.family) // 'EVM'

// Get by chain selector
const fuji = networkInfo(14767482510784806043n)
console.log('Network name:', fuji.name) // 'avalanche-testnet-fuji'

// Check chain family
const solana = networkInfo('solana-devnet')
console.log('Is EVM:', solana.family === 'EVM') // false
console.log('Is Solana:', solana.family === 'SVM') // true

Chain-Agnostic Code

Use the base Chain type for maximum flexibility:

TypeScript
import {
type Chain,
EVMChain,
SolanaChain,
networkInfo,
ChainFamily
} from '@chainlink/ccip-sdk'

async function getChain(networkName: string): Promise<Chain> {
const network = networkInfo(networkName)

switch (network.family) {
case ChainFamily.EVM:
return EVMChain.fromUrl(getRpcUrl(networkName))
case ChainFamily.Solana:
return SolanaChain.fromUrl(getRpcUrl(networkName))
default:
throw new Error(`Unsupported chain family: ${network.family}`)
}
}

async function trackAnyMessage(networkName: string, txHash: string) {
const chain = await getChain(networkName)
const requests = await chain.getMessagesInTx(txHash)

console.log('Found', requests.length, 'messages on', networkName)
return requests
}

Complete Example

Track a cross-chain message from any source to any destination:

TypeScript
import {
EVMChain,
SolanaChain,
discoverOffRamp,
networkInfo,
ChainFamily,
type Chain,
} from '@chainlink/ccip-sdk'

async function createChain(networkName: string, rpcUrl: string): Promise<Chain> {
const { family } = networkInfo(networkName)
switch (family) {
case ChainFamily.EVM:
return EVMChain.fromUrl(rpcUrl)
case ChainFamily.Solana:
return SolanaChain.fromUrl(rpcUrl)
default:
throw new Error(`Unsupported: ${family}`)
}
}

async function trackCrossChainMessage(
sourceNetwork: string,
sourceRpc: string,
destNetwork: string,
destRpc: string,
sourceTxHash: string
) {
const source = await createChain(sourceNetwork, sourceRpc)
const dest = await createChain(destNetwork, destRpc)

console.log(`Tracking ${sourceNetwork} -> ${destNetwork}`)

const requests = await source.getMessagesInTx(sourceTxHash)
const request = requests[0]
console.log('Message ID:', request.message.messageId)

const offRamp = await discoverOffRamp(source, dest, request.lane.onRamp)
console.log('OffRamp:', offRamp)

for await (const execution of dest.getExecutionReceipts({
offRamp,
messageId: request.message.messageId,
})) {
console.log('Execution state:', execution.receipt.state)
return execution
}

console.log('Not yet executed')
return null
}

// Track EVM to EVM
await trackCrossChainMessage(
'ethereum-testnet-sepolia',
'https://rpc.sepolia.org',
'avalanche-testnet-fuji',
'https://rpc.fuji.avax.network',
'0x1234...'
)

// Track EVM to Solana
await trackCrossChainMessage(
'ethereum-testnet-sepolia',
'https://rpc.sepolia.org',
'solana-devnet',
'https://api.devnet.solana.com',
'0x5678...'
)