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
| Elemento | Descrizione | Sorgente |
|---|---|---|
Batch | L'istruzione Batch (discriminatore 255). | Sorgente |
process_batch | Logica 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:
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 programpub 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:
[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?