Het Token Program aanroepen via CPI

On-chain programma's verplaatsen tokens door een Cross Program Invocation (CPI) naar het Token Program te sturen. Jouw programma bouwt een Token Program-instructie op, levert de benodigde accounts aan en roept invoke aan (of invoke_signed wanneer een Program Derived Address ondertekent). Het Token Program voert die instructie vervolgens uit met de bevoegdheden die jouw programma eraan verleent.

Deze pagina behandelt de token-specifieke kant van die stroom. Voor het CPI-mechanisme zelf — hoe invoke en invoke_signed werken, hoe ondertekenaar- en schrijfrechten worden doorgegeven, en het CPI-kostenmodel — zie Cross Program Invocation.

Veelvoorkomende patronen

De meeste token-CPI's volgen dezelfde structuur: bouw de instructie op, geef de token accounts en een autoriteit mee, en roep aan. Het onderstaande voorbeeld draagt tokens over met TransferChecked, dat de mint en decimalen verifieert als onderdeel van de overdracht.

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>,
}

Minten, verbranden en accounts sluiten volgen hetzelfde patroon met een andere instructie — MintTo, Burn, en CloseAccount in Anchor's anchor_spl::token module, of de bijbehorende builders in pinocchio-token. Voor volledige, uitvoerbare programma's die via CPI het Token Program aanroepen in zowel Anchor als native Rust, zie de token program-voorbeelden — met name het "Transfer Tokens"-voorbeeld.

Ondertekenen met een PDA-autoriteit

Wanneer de autoriteit op een token account een Program Derived Address is dat eigendom is van jouw programma, ondertekent het programma de CPI zelf met invoke_signed, waarbij de PDA-seeds worden meegegeven als signer seeds. Gebruik in Anchor CpiContext::new_with_signer met de seeds in plaats van CpiContext::new. De instructie is verder identiek aan de bovenstaande voorbeelden. Zie CPI's met PDA-ondertekenaars voor de volledige werking.

Batching

De instructie Batch voert meerdere Token Program-instructies uit binnen één enkele programma-aanroep. Omdat elke CPI een vaste berekeningskost met zich meebrengt, verbruikt het uitvoeren van meerdere tokenbewerkingen in één batch-CPI minder compute units dan het uitsturen van een aparte CPI per bewerking — zie het CPI-kostenmodel voor hoe kosten per CPI oplopen.

Lager rekenverbruik verlaagt de prioriteitskosten die een transactie per compute unit betaalt en vergroot de kans dat deze wordt verwerkt — wat het meest van belang is voor programma's die veel tokenbewerkingen uitvoeren in één enkele instructie. Veelvoorkomende toepassingen zijn het uitwaaieren van een overdracht naar meerdere ontvangers, of het uitvoeren van een meerstaps-tokenflow (bijvoorbeeld synchroniseren, overdragen en sluiten) in één enkele CPI.

Batch accepteert alleen Token Program-instructies als onderliggende instructies, en een batch kan geen andere batch bevatten. Voor bewerkingen waarbij tokens worden verplaatst (zoals overdrachten, mints en burns) verifieert het programma of de betrokken accounts eigendom zijn van het Token Program voordat deze worden uitgevoerd.

Bronverwijzing

ItemBeschrijvingBron
BatchDe Batch-instructie (discriminator 255).Bron
process_batchLogica voor batchverwerking.Bron

Batch aanroepen via CPI

De crate pinocchio-token biedt een Batch-builder aan onder pinocchio_token::instructions::Batch. U plaatst elke onderliggende instructie in de buffers van de batch en verstuurt vervolgens één enkele CPI met invoke.

Een Batch maakt gebruik van drie door de aanroeper aangeleverde buffers: één voor de geserialiseerde instruction data, één voor de accountmetadata per onderliggende instructie, en één voor de accountweergaven die aan de CPI worden doorgegeven. Maak ze aan als MaybeUninit-slices, geef ze door aan Batch::new, voeg onderliggende instructies toe en roep vervolgens aan:

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 schrijft de batch-discriminator naar het eerste byte van de databuffer en geeft een Batch terug die bijhoudt hoeveel van elke buffer is gebruikt. Elke Token Program instructiebouwer (zoals Transfer, TransferChecked, MintTo en Burn) implementeert de IntoBatch trait, zodat into_batch de instruction data, accountmeta's en accountweergaven van die instructie aan de batch toevoegt. Het aanroepen van invoke verstuurt de samengestelde batch als één CPI naar het Token Program.

De buffers begrenzen de capaciteit van de batch. Stel de databuffer in op 1 + Σ(2 + child_data_len) bytes (de discriminator, plus een 2-byte-header en de geserialiseerde data voor elk kind) en elke accountbuffer op het totale aantal accounts over alle kinderen. Als een kind niet past, geeft into_batch ProgramError::InvalidArgument terug. Ter referentie: Batch::MAX_DATA_LEN is 10 KiB en Batch::MAX_ACCOUNTS_LEN is gelijk aan het maximale CPI-accountaantal van de runtime.

Voor batches waarvan de omvang alleen tijdens runtime bekend is, schakel je de alloc-feature van de crate in en gebruik je BatchState::new(accounts_len, data_len) om de buffers op de heap te alloceren, en roep je vervolgens as_batch aan om een Batch te verkrijgen.

Wanneer de autoriteit van een kindinstructie een PDA is die eigendom is van jouw programma, onderteken je de CPI met invoke_signed in plaats van invoke, waarbij je de PDA-seeds doorgeeft als Signer-vermeldingen.

Indeling van instruction data

Een batch codeert zijn kinderen één voor één achter de 255-discriminator. Alle accounts worden vlak doorgegeven, op volgorde, en per kind gesegmenteerd door num_accounts. De bouwer produceert precies dit wire-formaat:

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)

De capaciteit van een batch wordt begrensd door de buffers die aan Batch::new worden doorgegeven: de databuffer moet de discriminator plus de header en data van elk kind bevatten, en de accountbuffers moeten de accounts van elk kind bevatten. Stel ze in op de grootste batch die je bouwt, of gebruik BatchState (met de alloc-feature) om ze tijdens runtime in te stellen. De gecombineerde CPI is nog steeds onderworpen aan de account- en groottelimieten van de transactie, dus gebruik voor zeer grote fan-outs Address Lookup Tables om meer accounts te kunnen verwerken.

Is this page helpful?

Inhoudsopgave

Pagina Bewerken
© 2026 Solana Foundation. Alle rechten voorbehouden.