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
Name | Type | Description |
---|---|---|
dest_chain_selector | u64 | The unique CCIP blockchain identifier of the destination blockchain. |
message | SVM2AnyMessage | Read Messages for more details. |
token_indexes | Vec<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.
Field | Type | Writable? | Description |
---|---|---|---|
config | Account<Config> | No | Router config PDA. Derivation: ["config"] under the ccip_router program. |
dest_chain_state | Account<DestChain> | Yes | Per-destination blockchain PDA for sequence_number , chain config, etc. Derivation: ["dest_chain_state", dest_chain_selector] under the ccip_router program. |
nonce | Account<Nonce> | Yes | Current nonce PDA for (authority, dest_chain_selector) . Derivation: ["nonce", dest_chain_selector, authority_pubkey] under the ccip_router program. |
authority | Signer<'info> | Yes | The user/wallet paying for the ccip_send transaction. Also, it must match the seeds in nonce . |
system_program | Program<'info, System> | No | Standard System Program. |
fee_token_program | Interface<'info, TokenInterface> | No | The token program used for fee payment (e.g., SPL Token). If paying with native SOL, this is just the SystemProgramID . |
fee_token_mint | InterfaceAccount<'info, Mint> | No | Fee 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_account | UncheckedAccount<'info> | Yes | If 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_receiver | InterfaceAccount<'info, TokenAccount> | Yes | The ATA where all the fees are collected. Derivation: from [fee_billing_signer,fee_token_program.key(),fee_token_mint.key()] . |
fee_billing_signer | UncheckedAccount<'info> | No | PDA 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_quoter | UncheckedAccount<'info> | No | The Fee Quoter program ID. |
fee_quoter_config | UncheckedAccount<'info> | No | The global Fee Quoter config PDA. Derivation: ["config"] under the fee_quoter program. |
fee_quoter_dest_chain | UncheckedAccount<'info> | No | Per-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_config | UncheckedAccount<'info> | No | A 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_config | UncheckedAccount<'info> | No | PDA 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_remote | UncheckedAccount<'info> | No | The RMN program ID used to verify if a given chain is cursed. |
rmn_remote_curses | UncheckedAccount<'info> | No | PDA containing list of curses chain selectors and global curses. Derivation: ["curses"] under the rmn_remote program. |
rmn_remote_config | UncheckedAccount<'info> | No | RMN config PDA, containing configuration that control how curse verification works. Derivation: ["config"] under the rmn_remote program. |
token_pools_signer | UncheckedAccount<'info> | Yes | PDA 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] | Yes | You 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:
- A list of
token_amounts
you want to transfer cross-chain. - A slice of
remaining_accounts
containing the per-token PDAs (e.g., user token ATA, pool config, token admin registry PDA, etc.). - A
token_indexes
array tells the Router where inremaining_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 i
th token's accounts begin inside remaining_accounts
.
The Router:
- Slices out
[start..end)
for thei
th 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:
- The user's token account (ATA).
- The token's chain PDAs.
- 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:
Index | Account | Description |
---|---|---|
0 | user_token_account | ATA for (authority, mint, token_program) . |
1 | token_billing_config | Per-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. |
2 | pool_chain_config | PDA ["ccip_tokenpool_chainconfig", dest_chain_selector, mint] under fee_quoter program. |
3 | lookup_table | Address Lookup Table that the token's admin registry claims. Must match the Token Admin Registry's lookup_table field. |
4 | token_admin_registry | PDA ["token_admin_registry", mint] under the ccip_router program. |
5 | pool_program | The Token Pool program ID (for CPI calls). |
6 | pool_config | PDA [ "ccip_tokenpool_config", mint ] under the pool_program. |
7 | pool_token_account | ATA for (pool_signer , mint , token_program ). |
8 | pool_signer | PDA [ "ccip_tokenpool_signer", mint ] under the pool_program. |
9 | token_program | Token program ID (e.g. spl_token or 2022). Also, it must match the mint's owner . |
10 | mint | The SPL Mint (public key) for this token. |
11 | fee_token_config | A 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. |
12 | … | Additional accounts are passed if required by the token pool. |
Examples
One Token Transfer
Suppose you want to send one token (myMint
) cross-chain:
token_amounts
: length = 1, e.g.[{ token: myMint_pubkey, amount: 1000000 }]
.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]
.
- The 0th token's remaining_accounts sub-slice will be
- 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.
Two Token Transfers
Suppose you want to send two tokens (mintA
and mintB
) cross-chain:
token_amounts
: length = 2, e.g.[{ token: mintA_pubkey, amount: 1000000 },{ token: mintB_pubkey, amount: 2000000 } ]
.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:
- For the first token: The sub slice is [2..14).
- For the second token: The sub slice is [14…endOfArray).
- After we skip the user ATAs at indices [0..2), we want the next 12 accounts for the first token to lie in
- Your
remaining_accounts
must have:- 2 user ATAs (one for
mintA
, one formintB
). - 12 pool accounts for
mintA
. - 12 pool accounts for
mintB
.
- 2 user ATAs (one for
Thus 2 + 12 + 12 = 26
accounts in remaining_accounts
.