API Version: v1.6.0

CCIP v1.6.0 SVM Router API Reference

Router

Below is a complete API reference for the ccip_send instruction from the CCIP Router program.

ccip_send

This instruction is the entry point for sending a cross-chain message from an SVM-based blockchain to a specified destination blockchain.

fn ccip_send(
    ctx: Context<CcipSend>,
    dest_chain_selector: u64,
    message: SVM2AnyMessage,
    token_indexes: Vec<u8>
) -> Result<[u8; 32]>;

Parameters

NameTypeDescription
dest_chain_selectoru64The unique CCIP blockchain identifier of the destination blockchain.
messageSVM2AnyMessageRead Messages for more details.
token_indexesVec<u8>Index offsets slicing the remaining accounts so each token's subset can be grouped (see Context).

Context (Accounts)

These are the required accounts passed alongside the instructions. For relevant PDAs, the instructions on how to derive seeds are given below.

FieldTypeWritable?Description
configAccount<Config>NoRouter config PDA.
Derivation: ["config"] under the ccip_router program.
dest_chain_stateAccount<DestChain>YesPer-destination blockchain PDA for sequence_number, chain config, etc.
Derivation: ["dest_chain_state", dest_chain_selector] under the ccip_router program.
nonceAccount<Nonce>YesCurrent nonce PDA for (authority, dest_chain_selector).
Derivation: ["nonce", dest_chain_selector, authority_pubkey] under the ccip_router program.
authoritySigner<'info>YesThe user/wallet paying for the ccip_send transaction. Also, it must match the seeds in nonce.
system_programProgram<'info, System>NoStandard System Program.
fee_token_programInterface<'info, TokenInterface>NoThe token program used for fee payment (e.g., SPL Token). If paying with native SOL, this is just the SystemProgramID.
fee_token_mintInterfaceAccount<'info, Mint>NoFee token mint. If paying in SPL, pass your chosen token mint. If paying in native SOL, a special "zero" mint (Pubkey::default()) or "native mint" (native_mint::ID) is used.
fee_token_user_associated_accountUncheckedAccount<'info>YesIf fees are paid in SPL, this is the user's ATA.
Derivation: It is derived via the Associated Token Program seeds: [authority_pubkey, fee_token_program.key(), fee_token_mint.key() ] under the relevant Token Program (Make sure you use the correct token program ID—Token-2022 vs.SPL Token). If paying with native SOL, pass the zero address (Pubkey::default()) and do not mark it writable.
fee_token_receiverInterfaceAccount<'info, TokenAccount>YesThe ATA where all the fees are collected.
Derivation: from [fee_billing_signer,fee_token_program.key(),fee_token_mint.key()].
fee_billing_signerUncheckedAccount<'info>NoPDA is the router's billing authority for transferring fees (native SOL or SPL tokens).
from fee_token_user_associated_account to fee_token_receiver.
Derivation: ["fee_billing_signer"] under the ccip_router program.
fee_quoterUncheckedAccount<'info>NoThe Fee Quoter program ID.
fee_quoter_configUncheckedAccount<'info>NoThe global Fee Quoter config PDA.
Derivation: ["config"] under the fee_quoter program.
fee_quoter_dest_chainUncheckedAccount<'info>NoPer-destination blockchain PDA in the Fee Quoter program. It stores chain-specific configuration (gas price data, limits, etc.) for SVM2Any messages.
Derivation: ["dest_chain",dest_chain_selector] under the fee_quoter program.
fee_quoter_billing_token_configUncheckedAccount<'info>NoA per-fee-token PDA in the Fee Quoter program stores token-specific parameters (price data, billing premiums, etc.) used to calculate fees.
Derivation: If the message pays fees in native SOL, the seed uses the native_mint::ID; otherwise, it uses the SPL token's mint public key. ["fee_billing_token_config", seed] under the fee_quoter program.
fee_quoter_link_token_configUncheckedAccount<'info>NoPDA containing the Fee Quoter's LINK token billing configuration (LINK price data, premium multipliers, etc.). The fee token amount is converted into "juels" using LINK's valuation from this account during fee calculation.
Derivation: ["fee_billing_token_config", link_token_mint] under the fee_quoter program.
rmn_remoteUncheckedAccount<'info>NoThe RMN program ID used to verify if a given chain is cursed.
rmn_remote_cursesUncheckedAccount<'info>NoPDA containing list of curses chain selectors and global curses.
Derivation: ["curses"] under the rmn_remote program.
rmn_remote_configUncheckedAccount<'info>NoRMN config PDA, containing configuration that control how curse verification works.
Derivation: ["config"] under the rmn_remote program.
token_pools_signerUncheckedAccount<'info>YesPDA with the authority to CPI into token pool logic (mint/burn, lock/release).
Derivation: ["external_token_pools_signer"] under the ccip_router program.
remaining_accounts&[AccountInfo] (slice)YesYou pass extra accounts for each token you wish to transfer (does not include fee tokens). Typically includes the sender ATA, the token pool config, token admin registry PDAs… etc.

How remaining_accounts and token_indexes Work

When you call the Router's ccip_send instruction, you pass:

  1. A list of token_amounts you want to transfer cross-chain.
  2. A slice of remaining_accounts containing the per-token PDAs (e.g., user token ATA, pool config, token admin registry PDA, etc.).
  3. A token_indexes array tells the Router where in remaining_accounts each token's sub-slice begins.
Reason for remaining_accounts

On Solana, each Anchor instruction has a fixed set of named accounts. However, CCIP must handle any number of tokens, each requiring many accounts. Rather than define a massive static context, the Router uses Anchor's dynamic ctx.remaining_accounts: All token-specific accounts are packed into one slice.

Reason for token_indexes

The Router must figure out which segment of that slice corresponds to token #0, token #1, etc. So you provide an integer offset in token_indexes[i] indicating where the ith token's accounts begin inside remaining_accounts.

The Router:

  • Slices out [start..end) for the ith token's accounts. The subslice is from start up to but not including end. This is how you indicate that the token i's accounts occupy positions start, start+1, …, end-1.
  • Validates each account.
  • Calls the appropriate token pool to the lock-or-burn operation on them.
Structure of Each Token's Sub-slice

Inside each token's sub-slice, the Router expects:

  1. The user's token account (ATA).
  2. The token's chain PDAs.
  3. Lookup table PDAs, token admin registry, pool program, pool config, pool signer, token program, the mint, etc.

In total, it is typically 12 or more accounts per token. Repeat that "per-token chunk" of ~12 accounts for each token if you have multiple tokens. These accounts are extracted in this order:

IndexAccountDescription
0user_token_accountATA for (authority, mint, token_program).
1token_billing_configPer-destination blockchain-specific fee overrides for a given token.
Note: In most cases, tokens do not have a custom billing fee structure. In these cases, CCIP uses the fallback default fee configuration.
PDA ["per_chain_per_token_config", dest_chain_selector, mint] under the fee_quoter program.
2pool_chain_configPDA ["ccip_tokenpool_chainconfig", dest_chain_selector, mint] under fee_quoter program.
3lookup_tableAddress Lookup Table that the token's admin registry claims. Must match the Token Admin Registry's lookup_table field.
4token_admin_registryPDA ["token_admin_registry", mint] under the ccip_router program.
5pool_programThe Token Pool program ID (for CPI calls).
6pool_configPDA [ "ccip_tokenpool_config", mint ] under the pool_program.
7pool_token_accountATA for (pool_signer, mint, token_program).
8pool_signerPDA [ "ccip_tokenpool_signer", mint ] under the pool_program.
9token_programToken program ID (e.g. spl_token or 2022). Also, it must match the mint's owner.
10mintThe SPL Mint (public key) for this token.
11fee_token_configA token billing configuration account under the Fee Quoter program. It contains settings such as whether there is a specific pricing for the token, its pricing in USD, and any premium multipliers.
Note: In most cases, tokens do not have a custom billing fee structure. In these cases, CCIP uses the fallback default fee configuration.
PDA ["fee_billing_token_config", mint] under the fee_quoter program.
12Additional accounts are passed if required by the token pool.
Examples
One Token Transfer

Suppose you want to send one token (myMint) cross-chain:

  1. token_amounts: length = 1, e.g. [{ token: myMint_pubkey, amount: 1000000 }].
  2. token_indexes: [1]. Meaning:
    • The 0th token's remaining_accounts sub-slice will be [token_indexes[0] .. endOfArray), i.e. [1..].
    • The user's Associated Token Account (ATA) for that token is found at remaining_accounts[0].
  3. Your remaining_accounts must have:
    • 1 user's ATA (the sender ATA for the single token).
    • 12 pool-related accounts (pool config, chain config, token program, etc.). That is 13 total.
Example of remaining accounts for one token
Two Token Transfers

Suppose you want to send two tokens (mintA and mintB) cross-chain:

  1. token_amounts: length = 2, e.g. [{ token: mintA_pubkey, amount: 1000000 },{ token: mintB_pubkey, amount: 2000000 } ].
  2. token_indexes must be length=2 since there are two tokens, and token_indexes = [2, 14]. Explanation:
    • After we skip the user ATAs at indices [0..2), we want the next 12 accounts for the first token to lie in [2..14), and then the next 12 for the second token to lie in [14..end).
    • The Router program will use token_indexes:
      1. For the first token: The sub slice is [2..14).
      2. For the second token: The sub slice is [14…endOfArray).
  3. Your remaining_accounts must have:
    • 2 user ATAs (one for mintA, one for mintB).
    • 12 pool accounts for mintA.
    • 12 pool accounts for mintB.

Thus 2 + 12 + 12 = 26 accounts in remaining_accounts.

Example of remaining accounts for two tokens

Get the latest Chainlink content straight to your inbox.