Skip to content

Commit 12cfb37

Browse files
authored
docs: add advanced Token Program instructions (batch, withdraw/unwrap lamports) (#1579)
* docs: add advanced Token Program instructions Document the batch, withdraw excess lamports, and unwrap lamports instructions under Tokens > Advanced, with Kit and Rust examples. * docs: expand Batch page into Token Program CPI guide Repurpose tokens/advanced/batch.mdx into a token-specific CPI page (tokens/advanced/cpi) covering how to invoke the Token Program from an on-chain program. Adds Anchor and native (Pinocchio) transfer examples, a PDA-authority signing note, and folds the existing Batch content in as a Batching section. Defers CPI mechanics to /docs/core/cpi. Updates nav (meta.json), the advanced index card/callout, and related links in the sibling lamport-recovery pages. * docs: address review on cpi.mdx (interface accounts, batch buffer constants)
1 parent 1c1fb8e commit 12cfb37

6 files changed

Lines changed: 498 additions & 1 deletion

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
title: Advanced Token Instructions
3+
description:
4+
Batch multiple token operations into a single instruction, and recover SOL
5+
from token accounts, mints, and multisigs with the Token Program.
6+
url: /docs/tokens/advanced
7+
---
8+
9+
Beyond the common create, mint, and transfer operations, the Token Program is
10+
also called from on-chain programs via CPI and includes instructions for
11+
recovering lamports:
12+
13+
<Cards>
14+
<Card
15+
title="Calling the Token Program via CPI"
16+
href="/docs/tokens/advanced/cpi"
17+
>
18+
Invoke the Token Program from an on-chain program — transfer, mint, and burn
19+
tokens, sign with a PDA authority, and batch instructions into a single CPI.
20+
</Card>
21+
22+
<Card
23+
title="Withdraw Excess Lamports"
24+
href="/docs/tokens/advanced/withdraw-excess-lamports"
25+
>
26+
Recover SOL above the rent-exempt minimum from token accounts, mints, and
27+
multisig accounts without changing token balances.
28+
</Card>
29+
30+
<Card title="Unwrap Lamports" href="/docs/tokens/advanced/unwrap-lamports">
31+
Convert wrapped SOL back into lamports, in full or in part, without closing
32+
the token account.
33+
</Card>
34+
</Cards>
35+
36+
<Callout type="info">
37+
38+
_rs`WithdrawExcessLamports`_ and _rs`UnwrapLamports`_ are available through the
39+
[`@solana-program/token`](https://www.npmjs.com/package/@solana-program/token)
40+
client. Calling the Token Program via CPI applies to on-chain programs.
41+
42+
</Callout>
43+
44+
## Lamport recovery
45+
46+
_rs`WithdrawExcessLamports`_ recovers SOL above the rent-exempt minimum from
47+
token accounts, mints, and multisig accounts. _rs`UnwrapLamports`_ converts
48+
wrapped SOL in a native token account back into lamports.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"title": "Advanced",
3+
"pages": ["cpi", "withdraw-excess-lamports", "unwrap-lamports"]
4+
}

0 commit comments

Comments
 (0)