Chiamare il Token Program tramite CPI

I programmi on-chain spostano i token emettendo una Cross Program Invocation (CPI) nel Token Program. Il tuo programma costruisce un'istruzione del Token Program, fornisce gli account necessari e chiama invoke (oppure invoke_signed quando firma un Program Derived Address). Il Token Program esegue quindi quell'istruzione con i privilegi che il tuo programma gli estende.

Questa pagina tratta il lato specifico dei token in questo flusso. Per il meccanismo CPI in sé — come funzionano invoke e invoke_signed, come si propagano i privilegi di firma e scrittura, e il modello di costo per CPI — consulta Cross Program Invocation.

Pattern comuni

La maggior parte delle CPI token seguono la stessa struttura: costruire l'istruzione, passare i token account e un'autorità, quindi invocare. L'esempio seguente trasferisce token con TransferChecked, che verifica il mint e i decimali come parte del trasferimento.

use anchor_lang::prelude::*;
use anchor_spl::token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked};
declare_id!("11111111111111111111111111111111");
#[program]
pub mod token_cpi {
use super::*;
pub fn transfer(ctx: Context<TokenTransfer>, amount: u64) -> Result<()> {
let cpi_accounts = TransferChecked {
from: ctx.accounts.source.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.destination.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
transfer_checked(cpi_ctx, amount, ctx.accounts.mint.decimals)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct TokenTransfer<'info> {
#[account(mut)]
pub source: InterfaceAccount<'info, TokenAccount>,
pub mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
pub destination: InterfaceAccount<'info, TokenAccount>,
pub authority: Signer<'info>,
pub token_program: Interface<'info, TokenInterface>,
}

Il conio, la combustione e la chiusura di account seguono lo stesso pattern con un'istruzione diversa — MintTo, Burn e CloseAccount nel modulo anchor_spl::token di Anchor, oppure i builder corrispondenti in pinocchio-token. Per programmi completi ed eseguibili che eseguono CPI nel Token Program sia con Anchor che con Rust nativo, consulta gli esempi di token program — in particolare l'esempio "Transfer Tokens".

Firma con un'autorità PDA

Quando l'autorità su un token account è un Program Derived Address di proprietà del tuo programma, il programma firma la CPI direttamente con invoke_signed, passando i seed del PDA come signer seed. In Anchor, usa CpiContext::new_with_signer con i seed al posto di CpiContext::new. L'istruzione è altrimenti identica agli esempi precedenti. Consulta CPI con PDA Signer per la meccanica completa.

Batching

L'istruzione Batch esegue diverse istruzioni Token Program all'interno di una singola invocazione del programma. Poiché ogni CPI comporta un costo di calcolo fisso, eseguire più operazioni su token in un unico batch CPI consuma meno unità di calcolo rispetto all'emissione di una CPI separata per ogni operazione — consulta il modello di costo CPI per capire come i costi per CPI si accumulano.

Un utilizzo inferiore delle unità di calcolo riduce le commissioni di priorità che una transazione paga per unità di calcolo e migliora le probabilità che vada a buon fine — il che è particolarmente rilevante per i programmi che eseguono molte operazioni su token in una singola istruzione. Gli utilizzi più comuni includono la distribuzione di un trasferimento a più destinatari, oppure l'esecuzione di un flusso token multi-fase (ad esempio sync, transfer e close) in un'unica CPI.

Batch accetta solo istruzioni Token Program come elementi figlio, e un batch non può contenere un altro batch. Per le operazioni che movimentano token (come trasferimenti, mint e burn), il programma verifica che gli account interessati siano di proprietà del Token Program prima di eseguirle.

Riferimento al codice sorgente

ElementoDescrizioneSorgente
BatchL'istruzione Batch (discriminatore 255).Sorgente
process_batchLogica del processore Batch.Sorgente

Chiamare Batch tramite CPI

Il crate pinocchio-token espone un builder Batch sotto pinocchio_token::instructions::Batch. Puoi aggiungere ogni istruzione figlio nei buffer del batch e poi emettere una singola CPI con invoke.

Un Batch è supportato da tre buffer forniti dal chiamante: uno per le instruction data serializzate, uno per i metadati degli account delle istruzioni figlio, e uno per le view degli account passate alla CPI. Costruiscili come slice MaybeUninit, passali a Batch::new, aggiungi i figli, poi invoca:

Batch two transfers in one CPI
use core::mem::MaybeUninit;
use pinocchio::{account::AccountView, error::ProgramError, ProgramResult};
use pinocchio_token::instructions::{Batch, IntoBatch, Transfer};
/// Process SwapBatch.
///
/// Performs two transfers in a single batch CPI instead of two separate
/// `invoke()` calls.
///
/// Data layout:
/// [0..8] amount_a_to_b (u64 LE)
/// [8..16] amount_b_to_a (u64 LE)
///
/// Account layout:
/// [0] source_a (writable) — token account A
/// [1] source_b (writable) — token account B
/// [2] authority_a (signer) — authority for account A
/// [3] authority_b (signer) — authority for account B
/// [4] token_program — SPL Token program
pub fn process(accounts: &[AccountView], data: &[u8]) -> ProgramResult {
if data.len() < 16 {
return Err(ProgramError::InvalidInstructionData);
}
if accounts.len() < 5 {
return Err(ProgramError::NotEnoughAccountKeys);
}
let amount_a_to_b = u64::from_le_bytes(data[0..8].try_into().unwrap());
let amount_b_to_a = u64::from_le_bytes(data[8..16].try_into().unwrap());
let source_a = &accounts[0];
let source_b = &accounts[1];
let authority_a = &accounts[2];
let authority_b = &accounts[3];
// Two child transfers, each with 3 accounts and 9 bytes of data
// (1-byte discriminator + 8-byte amount).
const NUM_CHILDREN: usize = 2;
const CHILD_ACCOUNTS: usize = 3;
const CHILD_DATA_LEN: usize = 9;
// Data buffer: 1-byte batch discriminator + per child (2-byte header + data).
const DATA_LEN: usize = 1 + NUM_CHILDREN * (2 + CHILD_DATA_LEN);
// Account buffers: total accounts across all children.
const ACCOUNTS_LEN: usize = NUM_CHILDREN * CHILD_ACCOUNTS;
let mut data_buf = [MaybeUninit::uninit(); DATA_LEN];
let mut ix_accounts_buf = [MaybeUninit::uninit(); ACCOUNTS_LEN];
let mut accounts_buf = [MaybeUninit::uninit(); ACCOUNTS_LEN];
let mut batch = Batch::new(&mut data_buf, &mut ix_accounts_buf, &mut accounts_buf)?;
Transfer::new(source_a, source_b, authority_a, amount_a_to_b).into_batch(&mut batch)?;
Transfer::new(source_b, source_a, authority_b, amount_b_to_a).into_batch(&mut batch)?;
batch.invoke()?;
Ok(())
}

Batch::new scrive il discriminatore batch nel primo byte del buffer dati e restituisce un Batch che tiene traccia di quanto di ciascun buffer è stato utilizzato. Ogni costruttore di istruzioni Token Program (come Transfer, TransferChecked, MintTo e Burn) implementa il trait IntoBatch, quindi into_batch aggiunge i dati di quell'istruzione, i meta degli account e le viste degli account al batch. La chiamata a invoke invia il batch assemblato come un unico CPI nel Token Program.

I buffer delimitano la capacità del batch. Dimensiona il buffer dati a 1 + Σ(2 + child_data_len) byte (il discriminatore, più un'intestazione di 2 byte e i dati serializzati per ogni figlio) e ciascun buffer degli account al numero totale di account tra tutti i figli. Se un figlio non entra, into_batch restituisce ProgramError::InvalidArgument. A titolo di riferimento, Batch::MAX_DATA_LEN è 10 KiB e Batch::MAX_ACCOUNTS_LEN corrisponde al numero massimo di account CPI consentito dal runtime.

Per i batch la cui dimensione è nota solo a runtime, abilita la feature alloc del crate e usa BatchState::new(accounts_len, data_len) per allocare i buffer nell'heap, quindi chiama as_batch per ottenere un Batch.

Quando l'autorità di un'istruzione figlia è un PDA di proprietà del tuo programma, firma il CPI con invoke_signed invece di invoke, passando i seed del PDA come voci Signer.

Layout di instruction data

Un batch codifica i propri figli uno dopo l'altro dopo il discriminatore 255. Tutti gli account vengono passati in modo piatto, in ordine, e suddivisi da num_accounts per ciascun figlio. Il costruttore produce esattamente questo formato wire:

Batch Wire Format
[255] // Batch discriminator
// For each child instruction, in order:
// [num_accounts: u8] // accounts this instruction consumes
// [data_len: u8] // length of this instruction's data
// [data: u8; data_len]// the instruction data (begins with its own discriminator)

La capacità di un batch è limitata dai buffer passati a Batch::new: il buffer dati deve contenere il discriminatore più l'intestazione e i dati di ogni figlio, e i buffer degli account devono contenere gli account di ogni figlio. Dimensionali per il batch più grande che intendi costruire, oppure usa BatchState (con la feature alloc) per dimensionarli a runtime. Il CPI combinato è comunque soggetto ai limiti di account e dimensione della transazione, quindi per fan-out molto grandi utilizza le Address Lookup Tables per includere più account.

Is this page helpful?

Indice dei contenuti

Modifica pagina
© 2026 Solana Foundation. Tutti i diritti riservati.