Les programmes on-chain déplacent des tokens en émettant une
Cross Program Invocation (CPI) vers le Token Program. Votre
programme construit une instruction du Token Program, fournit les comptes
nécessaires, et appelle invoke (ou invoke_signed lorsqu'un Program
Derived Address signe). Le Token Program exécute ensuite cette instruction avec
les privilèges que votre programme lui accorde.
Cette page couvre le côté spécifique aux tokens de ce flux. Pour le mécanisme
CPI lui-même — comment invoke et invoke_signed fonctionnent, comment
les privilèges de signataire et d'écriture se propagent, ainsi que le
modèle de coût par CPI — consultez
Cross Program Invocation.
Modèles courants
La plupart des CPI de tokens suivent la même structure : construire
l'instruction, passer les token accounts et une autorité, puis invoquer.
L'exemple ci-dessous transfère des tokens avec TransferChecked, qui
vérifie le mint et les décimales dans le cadre du transfert.
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>,}
L'émission, la destruction et la fermeture de comptes suivent le même modèle
avec une instruction différente — MintTo, Burn, et
CloseAccount dans le module
anchor_spl::token
d'Anchor, ou les constructeurs correspondants dans
pinocchio-token. Pour des
programmes complets et exécutables effectuant des CPI vers le Token Program en
Anchor et en Rust natif, consultez les
exemples de token program — notamment
l'exemple « Transfer Tokens ».
Signer avec une autorité Program Derived Address
Lorsque l'autorité sur un token account est un
Program Derived Address détenu par votre programme, le
programme signe lui-même la CPI avec invoke_signed, en passant les seeds
du Program Derived Address comme seeds de signataire. Dans Anchor, utilisez
CpiContext::new_with_signer avec les seeds à la place de
CpiContext::new. L'instruction est par ailleurs identique aux exemples
ci-dessus. Consultez CPIs with PDA Signers pour
les mécanismes complets.
Traitement par lots
L'instruction Batch exécute plusieurs instructions Token Program au sein
d'une seule invocation de programme. Comme chaque CPI entraîne un coût de calcul
fixe, regrouper plusieurs opérations de token dans un seul lot CPI consomme
moins d' unités de calcul que d'émettre un CPI
distinct par opération — consultez le
modèle de coût CPI pour comprendre comment les
coûts par CPI s'accumulent.
Une consommation de calcul réduite diminue les frais de priorité qu'une transaction paye par unité de calcul et améliore ses chances d'être incluse — ce qui importe surtout pour les programmes effectuant de nombreuses opérations de token dans une seule instruction. Les usages courants incluent la distribution d'un transfert vers de nombreux destinataires, ou l'exécution d'un flux multi-étapes (par exemple synchronisation, transfert et fermeture) dans un seul CPI.
Batch accepte uniquement des instructions Token Program en tant
qu'enfants, et un lot ne peut pas contenir un autre lot. Pour les opérations qui
déplacent des tokens (telles que les transferts, les frappes et les
destructions), le programme vérifie que les comptes concernés sont bien détenus
par le Token Program avant de les exécuter.
Référence source
| Élément | Description | Source |
|---|---|---|
Batch | L'instruction Batch (discriminant 255). | Source |
process_batch | Logique du processeur de lot. | Source |
Appel de Batch via CPI
La crate pinocchio-token expose un
constructeur Batch sous pinocchio_token::instructions::Batch. Vous
préparez chaque instruction enfant dans les tampons du lot, puis émettez un seul
CPI avec invoke.
Un Batch s'appuie sur trois tampons fournis par l'appelant : un pour les
instruction data sérialisées, un pour les métadonnées de compte des instructions
enfants, et un pour les vues de compte transmises au CPI. Construisez-les en
tant que tranches MaybeUninit, passez-les à Batch::new, ajoutez les
enfants, puis invoquez :
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 écrit le discriminateur de lot dans le premier octet du tampon
de données et retourne un Batch qui suit la quantité utilisée de chaque
tampon. Chaque constructeur d'instruction Token Program (tel que Transfer,
TransferChecked, MintTo et Burn) implémente le trait
IntoBatch, de sorte que into_batch ajoute les données de cette
instruction, les métadonnées de compte et les vues de compte au lot. L'appel de
invoke émet le lot assemblé sous forme d'un seul CPI vers le Token
Program.
Les tampons délimitent la capacité du lot. Dimensionnez le tampon de données à
1 + Σ(2 + child_data_len) octets (le discriminateur, plus un en-tête de 2
octets et les données sérialisées de chaque enfant) et chaque tampon de compte
au nombre total de comptes pour tous les enfants. Si un enfant ne tient pas,
into_batch retourne ProgramError::InvalidArgument. À titre de
référence, Batch::MAX_DATA_LEN correspond à 10 Kio et
Batch::MAX_ACCOUNTS_LEN est égal au nombre maximum de comptes CPI du
runtime.
Pour les lots dont la taille n'est connue qu'à l'exécution, activez la
fonctionnalité alloc du crate et utilisez
BatchState::new(accounts_len, data_len) pour allouer les tampons sur le
tas, puis appelez as_batch pour obtenir un Batch.
Lorsque l'autorité d'une instruction enfant est un PDA appartenant à votre
programme, signez le CPI avec invoke_signed plutôt qu'avec invoke,
en passant les seeds du PDA sous forme d'entrées Signer.
Disposition des instruction data
Un lot encode ses enfants les uns après les autres derrière le discriminateur
255. Tous les comptes sont transmis à plat, dans l'ordre, et découpés par
num_accounts pour chaque enfant. Le constructeur produit exactement ce format
réseau :
[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é d'un lot est délimitée par les tampons passés à Batch::new :
le tampon de données doit contenir le discriminateur ainsi que l'en-tête et les
données de chaque enfant, et les tampons de compte doivent contenir tous les
comptes de chaque enfant. Dimensionnez-les pour le lot le plus grand que vous
construisez, ou utilisez BatchState (avec la fonctionnalité alloc) pour
les dimensionner à l'exécution. Le CPI combiné reste soumis aux limites de
compte et de taille de la transaction ; pour les très grands fan-outs, utilisez
des Address Lookup Tables afin de faire tenir davantage de comptes.
Is this page helpful?