Appeler le Token Program via CPI

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émentDescriptionSource
BatchL'instruction Batch (discriminant 255).Source
process_batchLogique 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 :

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 é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 :

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é 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?

Table des matières

Modifier la page
© 2026 Fondation Solana. Tous droits réservés.