CCIP Solana SDK
June 15, 2026 ยท View on GitHub
A TypeScript SDK for interacting with the Chainlink CCIP protocol on Solana.
Note: Token pool setup, admin registry, and ALT management for cross-chain tokens (CCT) were removed from this repo. Use ccip-solana-bs58-generator for those workflows. This SDK still supports CCIP messaging, fee calculation, token transfers, and receiver operations.
Installation
This SDK is part of the CCIP library and is not published as a standalone npm package. To use it:
- Clone this repository
- Import the modules directly from the codebase
// Example import from local codebase
import { CCIPClient } from "../path/to/ccip-lib/svm";
Key Features
- Fee calculation for CCIP messages with support for various token types
- Message sending with transaction monitoring and compute budget management
- Message ID extraction from transaction logs with event parsing
- ExtraArgs generation for cross-chain messages with customizable gas limits
- Comprehensive PDA utilities for all CCIP account types
- Anchor-generated bindings for all CCIP accounts and instructions
- Flexible configuration management with dependency injection
- Structured logging throughout the SDK
- Router client for cross-chain messaging and token transfers
Architecture
The SDK follows a modular architecture with clear separation of concerns:
Core Components
- CCIPClient: Main entry point for SDK functionality (fee calculation and message sending)
- CCIPAccountReader: Handles fetching and decoding on-chain CCIP accounts
- Models: Type definitions shared across all components
Bindings
- Accounts: Auto-generated Anchor bindings for all on-chain account structs
- Instructions: Auto-generated Anchor bindings for all on-chain instructions
- Types: Type definitions for core CCIP protocol data structures
Utilities
- SolanaAccountManager: Utilities for managing Solana account write permissions and bitmap calculations
- PDAs: Comprehensive utilities for deriving all program-derived addresses
- Logger: Structured logging with configurable log levels
- Conversion: Helper functions for data type conversions
- Error Handling: Standardized error types and handling
Usage
Basic Usage
import {
CCIPClient,
CCIPSendRequest,
ExtraArgsOptions,
CCIPContext,
CCIPProvider,
CCIPCoreConfig,
LogLevel,
createLogger,
} from "../path/to/ccip-lib/svm";
import { PublicKey, Connection, Keypair } from "@solana/web3.js";
import { BN } from "@coral-xyz/anchor";
// Create a provider (application responsibility)
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const keypair = Keypair.fromSecretKey(/* your secret key */);
const provider: CCIPProvider = {
connection,
wallet: keypair,
getAddress(): PublicKey {
return keypair.publicKey;
},
async signTransaction(tx) {
if (tx instanceof VersionedTransaction) {
tx.sign([keypair]);
} else {
tx.partialSign(keypair);
}
return tx;
},
};
// Create configuration (application responsibility)
const config: CCIPCoreConfig = {
ccipRouterProgramId: new PublicKey(
"Ccip8ZTcM2qHjVt8FYHtuCAqjc637yLKnsJ5q5r2e6eL"
),
feeQuoterProgramId: new PublicKey(
"FeeQhewH1cd6ZyHqhfMiKAQntgzPT6bWwK26cJ5qSFo6"
),
rmnRemoteProgramId: new PublicKey(
"RmnAZiCJdaYtwR1f634Ba7yNJXuK3pS6kHuX4FgNgX8"
),
linkTokenMint: new PublicKey("D3HCrigxfvScYyokPC1YGpNgqyheVMVwbgP7XPywvEdc"),
tokenMint: new PublicKey("7AC59PVvR64EoMnLX45FHnJAYzPsxdViyYBsaGEQPFvh"),
nativeSol: PublicKey.default,
systemProgramId: new PublicKey("11111111111111111111111111111111"),
ethereumSepoliaSelector: BigInt("16015286601757825753"),
programId: new PublicKey("52XvWQKuZHRjnR7qHsEGE532jqgQ3MBiBMgVkBowP1LD"),
};
// Create context with provider and config
const context: CCIPContext = {
provider,
config,
logger: createLogger("my-app", { level: LogLevel.INFO }),
};
// Create CCIPClient with the context
const client = new CCIPClient(context);
// Create extraArgs configuration
const extraArgsConfig: ExtraArgsOptions = {
gasLimit: 200000,
allowOutOfOrderExecution: true,
};
// Generate the extraArgs buffer
const extraArgs = client.createExtraArgs(extraArgsConfig);
// Create a CCIP send request
const sendRequest: CCIPSendRequest = {
destChainSelector: new BN("16015286601757825753"),
receiver: Buffer.from(
"0x9d087fC03ae39b088326b67fA3C788236645b717".slice(2),
"hex"
),
data: Buffer.alloc(0), // Empty data for token transfer only
tokenAmounts: [
{
token: new PublicKey("7AC59PVvR64EoMnLX45FHnJAYzPsxdViyYBsaGEQPFvh"),
amount: new BN(10000000), // 0.01 tokens with 9 decimals
},
],
feeToken: PublicKey.default, // Use native SOL
extraArgs: extraArgs,
};
// Calculate fee first (optional)
const feeRequest = {
destChainSelector: sendRequest.destChainSelector,
message: {
receiver: sendRequest.receiver,
data: sendRequest.data,
tokenAmounts: sendRequest.tokenAmounts,
feeToken: sendRequest.feeToken,
extraArgs: extraArgs,
},
};
const feeResult = await client.getFee(feeRequest);
console.log(`Estimated fee: ${feeResult.amount.toString()}`);
// Send the message and get message ID
const result = await client.sendWithMessageId(sendRequest);
console.log(`Message sent! Transaction: ${result.txSignature}`);
console.log(`Message ID: ${result.messageId}`);
console.log(`Destination Chain Selector: ${result.destinationChainSelector}`);
console.log(`Sequence Number: ${result.sequenceNumber}`);
Using with Wallet Adapters
You can easily integrate with Solana wallet adapters:
import {
CCIPClient,
CCIPContext,
CCIPProvider,
CCIPCoreConfig,
} from "../path/to/ccip-lib/svm";
import {
Connection,
PublicKey,
Transaction,
VersionedTransaction,
} from "@solana/web3.js";
import { useWallet } from "@solana/wallet-adapter-react";
// In your React component
function MyComponent() {
const { publicKey, signTransaction } = useWallet();
const connection = new Connection("https://api.devnet.solana.com");
// Create provider from wallet adapter
const provider: CCIPProvider = {
connection,
wallet: null, // Not used directly with wallet adapter
getAddress(): PublicKey {
return publicKey;
},
async signTransaction(tx: Transaction | VersionedTransaction) {
return await signTransaction(tx);
},
};
// Get your config from application
const config: CCIPCoreConfig = {
// ...your config properties
};
// Create context
const context: CCIPContext = {
provider,
config,
};
// Create client
const client = new CCIPClient(context);
// Use client...
}
ExtraArgs Configuration
The SDK supports creating properly formatted extraArgs for CCIP messages:
// Configure extraArgs with options
const extraArgs = client.createExtraArgs({
gasLimit: 200000, // Gas limit for execution on destination chain
allowOutOfOrderExecution: true, // Whether to allow out-of-order execution
});
// Or use default values
const defaultExtraArgs = client.createExtraArgs();
Using the Account Reader
The CCIPAccountReader provides convenient methods to fetch and decode on-chain accounts:
// Get the account reader from the client
const accountReader = client.getAccountReader();
// Fetch token admin registry for a token
const tokenRegistry = await accountReader.getTokenAdminRegistry(tokenMint);
// Fetch CCIP configuration
const config = await accountReader.getConfig();
// Fetch destination chain state
const destChainState = await accountReader.getDestChainState(destChainSelector);
PDA Utilities
The SDK provides comprehensive utility functions for calculating Program Derived Addresses (PDAs) used throughout the CCIP protocol.
Token Pools Signer PDAs
When delegating token authority for CCIP cross-chain transfers, it's crucial to understand that there are two different types of PDA signers:
feeBillingSignerPDA- Used for fee payments (derived with thefee_billing_signerseed)tokenPoolsSignerPDA- Used for token transfers (derived with theexternal_token_pools_signerseed)
Important Note About Token Pool Signers
The token pool signer PDA is more complex than it initially appears. In the CCIP protocol, the actual PDA used for token transfers is derived using both:
- The
external_token_pools_signerseed - The pool program ID (which is specific to each token)
The pool program ID is stored in a lookup table that is part of the token admin registry for each token. This means a simple findExternalTokenPoolsSignerPDA function that uses only the seed is not sufficient for accurate delegation.
PDA Functions
To address this complexity, the SDK provides several functions:
// Basic function - not sufficient for token transfers
findExternalTokenPoolsSignerPDA(programId: PublicKey): [PublicKey, number]
// Recommended - dynamically finds the correct token pool signer PDA
findDynamicTokenPoolsSignerPDA(
mint: PublicKey,
routerProgramId: PublicKey,
connection: Connection
): Promise<[PublicKey, number]>
// Alternative using CCIPAccountReader
findTokenPoolsSignerWithAccountReader(
mint: PublicKey,
routerProgramId: PublicKey,
accountReader: CCIPAccountReader,
connection: Connection
): Promise<[PublicKey, number]>
Other Important PDAs
The SDK provides utilities for all PDAs used in the CCIP protocol:
// Router configuration PDA
findConfigPDA(programId: PublicKey): [PublicKey, number]
// Fee billing signer PDA (for fee payments)
findFeeBillingSignerPDA(programId: PublicKey): [PublicKey, number]
// Token admin registry PDA
findTokenAdminRegistryPDA(mint: PublicKey, programId: PublicKey): [PublicKey, number]
// Destination chain state PDA
findDestChainStatePDA(chainSelector: bigint, programId: PublicKey): [PublicKey, number]
// Nonce PDA
findNoncePDA(chainSelector: bigint, authority: PublicKey, programId: PublicKey): [PublicKey, number]
// Token pool chain config PDA
findTokenPoolChainConfigPDA(
chainSelector: bigint,
tokenMint: PublicKey,
programId: PublicKey
): [PublicKey, number]
Usage for Token Delegation
When delegating token authority to the CCIP protocol, you should:
- Use
findFeeBillingSignerPDAfor fee token delegation (e.g., SOL) - Use
findDynamicTokenPoolsSignerPDAfor token transfer delegation (e.g., BnM, LINK)
// For fee token delegation (e.g., SOL)
const [feeBillingSigner] = findFeeBillingSignerPDA(routerProgramId);
// For token transfer delegation (e.g., BnM, LINK)
const [tokenPoolsSigner] = await findDynamicTokenPoolsSignerPDA(
tokenMint,
routerProgramId,
connection
);
Error Handling
The SDK uses standardized error handling with custom error types:
import {
CCIPError,
CCIPTransactionError,
CCIPAccountError,
} from "../path/to/ccip-lib/svm";
try {
await client.sendWithMessageId(sendRequest);
} catch (error) {
if (error instanceof CCIPTransactionError) {
console.error(`Transaction failed: ${error.message}`, error.txSignature);
} else if (error instanceof CCIPAccountError) {
console.error(`Account error: ${error.message}`, error.account);
} else if (error instanceof CCIPError) {
console.error(`CCIP error: ${error.message}`);
} else {
console.error(`Unknown error: ${error}`);
}
}
Logging
The SDK supports structured logging with configurable log levels:
import { createLogger, LogLevel } from "../path/to/ccip-lib/svm";
// Create a logger with custom configuration
const logger = createLogger("my-component", {
level: LogLevel.DEBUG,
pretty: true, // For human-readable logs
skipTimestamp: false, // Include timestamps
});
// Use the logger
logger.info("Processing request", { destination: "ethereum" });
logger.debug("Technical details", { request: sendRequest });
logger.error("Operation failed", { error });
// Change log level at runtime
logger.setLevel(LogLevel.WARN);
Using the Solana Account Manager
The SolanaAccountManager provides utilities for managing Solana account write permissions and bitmap calculations used in CCIP messages:
import {
SolanaAccountManager,
AccountSpec
} from "../path/to/ccip-lib/svm";
// Define account specifications for a CCIP message
const accounts: AccountSpec[] = [
{
publicKey: "11111111111111111111111111111111", // State PDA
isWritable: false // Read-only
},
{
publicKey: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", // Messages storage PDA
isWritable: true // Writable
},
{
publicKey: "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", // Token mint
isWritable: true // Writable
}
];
// Calculate the writable bitmap from account specifications
const bitmap = SolanaAccountManager.calculateWritableBitmap(accounts);
console.log(`Writable bitmap: ${bitmap} (binary: ${bitmap.toString(2)})`);
// Output: Writable bitmap: 6 (binary: 110)
// Create bitmap from boolean flags (alternative approach)
const writableFlags = [false, true, true]; // [readonly, writable, writable]
const bitmapFromFlags = SolanaAccountManager.createBitmapFromFlags(writableFlags);
console.log(`Bitmap from flags: ${bitmapFromFlags}`); // Output: 6
// Get human-readable explanation of a bitmap
const explanation = SolanaAccountManager.explainBitmap(BigInt(46), 7);
console.log("Bitmap explanation:", explanation);
/* Output:
{
binary: "0101110",
decimal: 46,
accounts: [
{ index: 0, writable: false },
{ index: 1, writable: true },
{ index: 2, writable: true },
{ index: 3, writable: true },
{ index: 4, writable: false },
{ index: 5, writable: true },
{ index: 6, writable: false }
]
}
*/
// Validate that a bitmap is appropriate for the number of accounts
try {
SolanaAccountManager.validateBitmap(BigInt(46), 7); // Valid
console.log("Bitmap is valid for 7 accounts");
} catch (error) {
console.error(`Invalid bitmap: ${error.message}`);
}
// Example: Invalid bitmap for account count
try {
SolanaAccountManager.validateBitmap(BigInt(255), 3); // Invalid - bitmap too large
} catch (error) {
console.error(`Invalid bitmap: ${error.message}`);
// Output: Invalid bitmap: Invalid bitmap 255 for 3 accounts. Maximum valid bitmap is 7 (binary: 111)
}
Common CCIP Bitmap Patterns
The SDK makes it easy to work with common CCIP bitmap patterns:
// For arbitrary messaging (2 accounts: state, message_storage)
const messagingAccounts: AccountSpec[] = [
{ publicKey: "state_pda_address", isWritable: false }, // bit 0 = 0
{ publicKey: "message_storage_pda", isWritable: true } // bit 1 = 1
];
const messagingBitmap = SolanaAccountManager.calculateWritableBitmap(messagingAccounts);
// Result: 2 (binary: 10)
// For data and tokens (7 accounts with specific write permissions)
const dataTokensAccounts: AccountSpec[] = [
{ publicKey: "state_pda", isWritable: false }, // bit 0 = 0
{ publicKey: "message_storage_pda", isWritable: true }, // bit 1 = 1
{ publicKey: "token_mint", isWritable: true }, // bit 2 = 1
{ publicKey: "source_token_account", isWritable: true }, // bit 3 = 1
{ publicKey: "token_admin_pda", isWritable: false }, // bit 4 = 0
{ publicKey: "recipient_token_account", isWritable: true }, // bit 5 = 1
{ publicKey: "token_program", isWritable: false } // bit 6 = 0
];
const dataTokensBitmap = SolanaAccountManager.calculateWritableBitmap(dataTokensAccounts);
// Result: 46 (binary: 0101110)
// Verify the calculation
const verification = SolanaAccountManager.explainBitmap(dataTokensBitmap, 7);
console.log("Data and tokens bitmap breakdown:", verification.accounts);