|
| 1 | +--- |
| 2 | +title: Calling the Token Program via CPI |
| 3 | +description: |
| 4 | + Invoke the Token Program from an on-chain program with a Cross Program |
| 5 | + Invocation (CPI) — transfer, mint, and burn tokens, sign with a PDA authority, |
| 6 | + and batch several token instructions into a single CPI. |
| 7 | +url: /docs/tokens/advanced/cpi |
| 8 | +type: tutorial |
| 9 | +related: |
| 10 | + - /docs/core/cpi |
| 11 | + - /docs/tokens/basics/transfer-tokens |
| 12 | + - /docs/tokens/advanced/withdraw-excess-lamports |
| 13 | + - /docs/tokens/advanced/unwrap-lamports |
| 14 | +--- |
| 15 | + |
| 16 | +On-chain programs move tokens by issuing a |
| 17 | +[Cross Program Invocation (CPI)](/docs/core/cpi) into the Token Program. Your |
| 18 | +program builds a Token Program instruction, supplies the accounts it needs, and |
| 19 | +calls _rs`invoke`_ (or _rs`invoke_signed`_ when a Program Derived Address |
| 20 | +signs). The Token Program then runs that instruction with the privileges your |
| 21 | +program extends to it. |
| 22 | + |
| 23 | +This page covers the token-specific side of that flow. For the CPI mechanism |
| 24 | +itself — how _rs`invoke`_ and _rs`invoke_signed`_ work, how signer and writable |
| 25 | +privileges propagate, and the |
| 26 | +[per-CPI cost model](/docs/core/cpi/cpi-cost-model) — see |
| 27 | +[Cross Program Invocation](/docs/core/cpi). |
| 28 | + |
| 29 | +## Common patterns |
| 30 | + |
| 31 | +Most token CPIs follow the same shape: build the instruction, pass the token |
| 32 | +accounts and an authority, and invoke. The example below transfers tokens with |
| 33 | +_rs`TransferChecked`_, which verifies the mint and decimals as part of the |
| 34 | +transfer. |
| 35 | + |
| 36 | +<CodeTabs> |
| 37 | + |
| 38 | +```rust !! title="Anchor" |
| 39 | +use anchor_lang::prelude::*; |
| 40 | +use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked}; |
| 41 | + |
| 42 | +declare_id!("11111111111111111111111111111111"); |
| 43 | + |
| 44 | +#[program] |
| 45 | +pub mod token_cpi { |
| 46 | + use super::*; |
| 47 | + |
| 48 | + pub fn transfer(ctx: Context<TokenTransfer>, amount: u64) -> Result<()> { |
| 49 | + let cpi_accounts = TransferChecked { |
| 50 | + from: ctx.accounts.source.to_account_info(), |
| 51 | + mint: ctx.accounts.mint.to_account_info(), |
| 52 | + to: ctx.accounts.destination.to_account_info(), |
| 53 | + authority: ctx.accounts.authority.to_account_info(), |
| 54 | + }; |
| 55 | + let cpi_ctx = CpiContext::new( |
| 56 | + ctx.accounts.token_program.to_account_info(), |
| 57 | + cpi_accounts, |
| 58 | + ); |
| 59 | + |
| 60 | + transfer_checked(cpi_ctx, amount, ctx.accounts.mint.decimals)?; |
| 61 | + Ok(()) |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +#[derive(Accounts)] |
| 66 | +pub struct TokenTransfer<'info> { |
| 67 | + #[account(mut)] |
| 68 | + pub source: InterfaceAccount<'info, TokenAccount>, |
| 69 | + pub mint: InterfaceAccount<'info, Mint>, |
| 70 | + #[account(mut)] |
| 71 | + pub destination: InterfaceAccount<'info, TokenAccount>, |
| 72 | + pub authority: Signer<'info>, |
| 73 | + pub token_program: Interface<'info, TokenInterface>, |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +```rust !! title="Native (Pinocchio)" |
| 78 | +use pinocchio::{account::AccountView, error::ProgramError, ProgramResult}; |
| 79 | +use pinocchio_token::instructions::TransferChecked; |
| 80 | + |
| 81 | +/// Account layout: |
| 82 | +/// [0] source (writable) — source token account |
| 83 | +/// [1] mint — token mint |
| 84 | +/// [2] destination (writable) — destination token account |
| 85 | +/// [3] authority (signer) — owner or delegate of the source account |
| 86 | +/// [4] token_program — SPL Token program |
| 87 | +pub fn process(accounts: &[AccountView], data: &[u8]) -> ProgramResult { |
| 88 | + if accounts.len() < 5 { |
| 89 | + return Err(ProgramError::NotEnoughAccountKeys); |
| 90 | + } |
| 91 | + if data.len() < 9 { |
| 92 | + return Err(ProgramError::InvalidInstructionData); |
| 93 | + } |
| 94 | + |
| 95 | + let amount = u64::from_le_bytes(data[0..8].try_into().unwrap()); |
| 96 | + let decimals = data[8]; |
| 97 | + |
| 98 | + let source = &accounts[0]; |
| 99 | + let mint = &accounts[1]; |
| 100 | + let destination = &accounts[2]; |
| 101 | + let authority = &accounts[3]; |
| 102 | + |
| 103 | + TransferChecked::new(source, mint, destination, authority, amount, decimals).invoke()?; |
| 104 | + |
| 105 | + Ok(()) |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +</CodeTabs> |
| 110 | + |
| 111 | +Minting, burning, and closing accounts follow the same pattern with a different |
| 112 | +instruction — _rs`MintTo`_, _rs`Burn`_, and _rs`CloseAccount`_ in Anchor's |
| 113 | +[`anchor_spl::token`](https://docs.rs/anchor-spl/latest/anchor_spl/token/index.html) |
| 114 | +module, or the matching builders in |
| 115 | +[`pinocchio-token`](https://crates.io/crates/pinocchio-token). For complete, |
| 116 | +runnable programs that CPI into the Token Program in both Anchor and native |
| 117 | +Rust, see the [token program examples](/docs/programs/examples#tokens) — in |
| 118 | +particular the "Transfer Tokens" example. |
| 119 | + |
| 120 | +## Signing with a PDA authority |
| 121 | + |
| 122 | +When the authority on a token account is a |
| 123 | +[Program Derived Address](/docs/core/pda) owned by your program, the program |
| 124 | +signs the CPI itself with _rs`invoke_signed`_, passing the PDA seeds as signer |
| 125 | +seeds. In Anchor, use _rs`CpiContext::new_with_signer`_ with the seeds instead |
| 126 | +of _rs`CpiContext::new`_. The instruction is otherwise identical to the examples |
| 127 | +above. See [CPIs with PDA Signers](/docs/core/cpi/cpi-with-pda) for the full |
| 128 | +mechanics. |
| 129 | + |
| 130 | +## Batching |
| 131 | + |
| 132 | +The _rs`Batch`_ instruction executes several Token Program instructions inside a |
| 133 | +single program invocation. Because each CPI carries a fixed compute cost, |
| 134 | +running several token operations in one batch CPI uses fewer |
| 135 | +[compute units](/docs/core/fees/compute-budget) than issuing a separate CPI per |
| 136 | +operation — see the [CPI cost model](/docs/core/cpi/cpi-cost-model) for how |
| 137 | +per-CPI costs add up. |
| 138 | + |
| 139 | +Lower compute usage reduces the [priority fees](/docs/core/fees) a transaction |
| 140 | +pays per compute unit and improves its chance of landing — which matters most |
| 141 | +for programs that perform many token operations in a single instruction. Common |
| 142 | +uses include fanning out a transfer to many recipients, or running a multi-step |
| 143 | +token flow (for example sync, transfer, and close) in a single CPI. |
| 144 | + |
| 145 | +<Callout type="info"> |
| 146 | + |
| 147 | +_rs`Batch`_ only accepts Token Program instructions as children, and a batch |
| 148 | +cannot contain another batch. For the operations that move tokens (such as |
| 149 | +transfers, mints, and burns), the program verifies that the affected accounts |
| 150 | +are owned by the Token Program before executing them. |
| 151 | + |
| 152 | +</Callout> |
| 153 | + |
| 154 | +### Source reference |
| 155 | + |
| 156 | +| Item | Description | Source | |
| 157 | +| ------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------- | |
| 158 | +| _rs`Batch`_ | The Batch instruction (discriminator 255). | [Source](https://github.com/solana-program/token/blob/main/pinocchio/interface/src/instruction.rs) | |
| 159 | +| _rs`process_batch`_ | Batch processor logic. | [Source](https://github.com/solana-program/token/blob/main/pinocchio/program/src/processor/batch.rs) | |
| 160 | + |
| 161 | +### Calling Batch via CPI |
| 162 | + |
| 163 | +The [`pinocchio-token`](https://crates.io/crates/pinocchio-token) crate exposes |
| 164 | +a _rs`Batch`_ builder under _rs`pinocchio_token::instructions::Batch`_. You |
| 165 | +stage each child instruction into the batch's buffers and then issue a single |
| 166 | +CPI with _rs`invoke`_. |
| 167 | + |
| 168 | +A _rs`Batch`_ is backed by three caller-provided buffers: one for the serialized |
| 169 | +instruction data, one for the per-child instruction account metas, and one for |
| 170 | +the account views passed to the CPI. Construct them as _rs`MaybeUninit`_ slices, |
| 171 | +hand them to _rs`Batch::new`_, append children, then invoke: |
| 172 | + |
| 173 | +```rust title="Batch two transfers in one CPI" |
| 174 | +use core::mem::MaybeUninit; |
| 175 | +use pinocchio::{account::AccountView, error::ProgramError, ProgramResult}; |
| 176 | +use pinocchio_token::instructions::{Batch, IntoBatch, Transfer}; |
| 177 | + |
| 178 | +/// Process SwapBatch. |
| 179 | +/// |
| 180 | +/// Performs two transfers in a single batch CPI instead of two separate |
| 181 | +/// `invoke()` calls. |
| 182 | +/// |
| 183 | +/// Data layout: |
| 184 | +/// [0..8] amount_a_to_b (u64 LE) |
| 185 | +/// [8..16] amount_b_to_a (u64 LE) |
| 186 | +/// |
| 187 | +/// Account layout: |
| 188 | +/// [0] source_a (writable) — token account A |
| 189 | +/// [1] source_b (writable) — token account B |
| 190 | +/// [2] authority_a (signer) — authority for account A |
| 191 | +/// [3] authority_b (signer) — authority for account B |
| 192 | +/// [4] token_program — SPL Token program |
| 193 | +pub fn process(accounts: &[AccountView], data: &[u8]) -> ProgramResult { |
| 194 | + if data.len() < 16 { |
| 195 | + return Err(ProgramError::InvalidInstructionData); |
| 196 | + } |
| 197 | + if accounts.len() < 5 { |
| 198 | + return Err(ProgramError::NotEnoughAccountKeys); |
| 199 | + } |
| 200 | + |
| 201 | + let amount_a_to_b = u64::from_le_bytes(data[0..8].try_into().unwrap()); |
| 202 | + let amount_b_to_a = u64::from_le_bytes(data[8..16].try_into().unwrap()); |
| 203 | + |
| 204 | + let source_a = &accounts[0]; |
| 205 | + let source_b = &accounts[1]; |
| 206 | + let authority_a = &accounts[2]; |
| 207 | + let authority_b = &accounts[3]; |
| 208 | + |
| 209 | + // Two child transfers, each with 3 accounts and 9 bytes of data |
| 210 | + // (1-byte discriminator + 8-byte amount). |
| 211 | + const NUM_CHILDREN: usize = 2; |
| 212 | + const CHILD_ACCOUNTS: usize = 3; |
| 213 | + const CHILD_DATA_LEN: usize = 9; |
| 214 | + |
| 215 | + // Data buffer: 1-byte batch discriminator + per child (2-byte header + data). |
| 216 | + const DATA_LEN: usize = 1 + NUM_CHILDREN * (2 + CHILD_DATA_LEN); |
| 217 | + // Account buffers: total accounts across all children. |
| 218 | + const ACCOUNTS_LEN: usize = NUM_CHILDREN * CHILD_ACCOUNTS; |
| 219 | + |
| 220 | + let mut data_buf = [MaybeUninit::uninit(); DATA_LEN]; |
| 221 | + let mut ix_accounts_buf = [MaybeUninit::uninit(); ACCOUNTS_LEN]; |
| 222 | + let mut accounts_buf = [MaybeUninit::uninit(); ACCOUNTS_LEN]; |
| 223 | + |
| 224 | + let mut batch = Batch::new(&mut data_buf, &mut ix_accounts_buf, &mut accounts_buf)?; |
| 225 | + |
| 226 | + Transfer::new(source_a, source_b, authority_a, amount_a_to_b).into_batch(&mut batch)?; |
| 227 | + Transfer::new(source_b, source_a, authority_b, amount_b_to_a).into_batch(&mut batch)?; |
| 228 | + |
| 229 | + batch.invoke()?; |
| 230 | + |
| 231 | + Ok(()) |
| 232 | +} |
| 233 | +``` |
| 234 | + |
| 235 | +_rs`Batch::new`_ writes the batch discriminator into the first byte of the data |
| 236 | +buffer and returns a _rs`Batch`_ that tracks how much of each buffer has been |
| 237 | +used. Every Token Program instruction builder (such as _rs`Transfer`_, |
| 238 | +_rs`TransferChecked`_, _rs`MintTo`_, and _rs`Burn`_) implements the |
| 239 | +_rs`IntoBatch`_ trait, so _rs`into_batch`_ appends that instruction's data, |
| 240 | +account metas, and account views to the batch. Calling _rs`invoke`_ issues the |
| 241 | +assembled batch as one CPI into the Token Program. |
| 242 | + |
| 243 | +The buffers bound the batch's capacity. Size the data buffer to |
| 244 | +`1 + Σ(2 + child_data_len)` bytes (the discriminator, plus a 2-byte header and |
| 245 | +the serialized data for each child) and each account buffer to the total number |
| 246 | +of accounts across all children. If a child does not fit, _rs`into_batch`_ |
| 247 | +returns _rs`ProgramError::InvalidArgument`_. For reference, |
| 248 | +_rs`Batch::MAX_DATA_LEN`_ is 10 KiB and _rs`Batch::MAX_ACCOUNTS_LEN`_ equals the |
| 249 | +runtime's maximum CPI account count. |
| 250 | + |
| 251 | +For batches whose size is only known at runtime, enable the crate's `alloc` |
| 252 | +feature and use _rs`BatchState::new(accounts_len, data_len)`_ to allocate the |
| 253 | +buffers on the heap, then call _rs`as_batch`_ to obtain a _rs`Batch`_. |
| 254 | + |
| 255 | +When a child instruction's authority is a PDA owned by your program, sign the |
| 256 | +CPI with _rs`invoke_signed`_ instead of _rs`invoke`_, passing the PDA seeds as |
| 257 | +_rs`Signer`_ entries. |
| 258 | + |
| 259 | +### Instruction data layout |
| 260 | + |
| 261 | +A batch encodes its children one after another behind the `255` discriminator. |
| 262 | +All accounts are passed flat, in order, and sliced by `num_accounts` for each |
| 263 | +child. The builder produces exactly this wire format: |
| 264 | + |
| 265 | +```text title="Batch Wire Format" |
| 266 | +[255] // Batch discriminator |
| 267 | +// For each child instruction, in order: |
| 268 | +// [num_accounts: u8] // accounts this instruction consumes |
| 269 | +// [data_len: u8] // length of this instruction's data |
| 270 | +// [data: u8; data_len]// the instruction data (begins with its own discriminator) |
| 271 | +``` |
| 272 | + |
| 273 | +<Callout type="warn"> |
| 274 | + |
| 275 | +A batch's capacity is bounded by the buffers passed to _rs`Batch::new`_: the |
| 276 | +data buffer must hold the discriminator plus every child's header and data, and |
| 277 | +the account buffers must hold every child's accounts. Size them for the largest |
| 278 | +batch you build, or use _rs`BatchState`_ (with the `alloc` feature) to size them |
| 279 | +at runtime. The combined CPI is still subject to the transaction's account and |
| 280 | +size limits, so for very large fan-outs use Address Lookup Tables to fit more |
| 281 | +accounts. |
| 282 | + |
| 283 | +</Callout> |
0 commit comments