Chamando o Token Program via CPI

Programas on-chain movem tokens emitindo uma Cross Program Invocation (CPI) no Token Program. Seu programa constrói uma instrução do Token Program, fornece as contas necessárias e chama invoke (ou invoke_signed quando um Program Derived Address assina). O Token Program então executa essa instrução com os privilégios que seu programa lhe concede.

Esta página aborda o lado específico dos tokens nesse fluxo. Para o mecanismo de CPI em si — como invoke e invoke_signed funcionam, como os privilégios de assinante e de escrita se propagam, e o modelo de custo por CPI — consulte Cross Program Invocation.

Padrões comuns

A maioria das CPIs de token segue a mesma estrutura: construa a instrução, passe as token accounts e uma autoridade, e invoque. O exemplo abaixo transfere tokens com TransferChecked, que verifica o mint e os decimais como parte da transferência.

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

Emitir, queimar e fechar contas seguem o mesmo padrão com uma instrução diferente — MintTo, Burn e CloseAccount no módulo anchor_spl::token do Anchor, ou os construtores equivalentes em pinocchio-token. Para programas completos e executáveis que fazem CPI no Token Program tanto em Anchor quanto em Rust nativo, consulte os exemplos de token program — em particular o exemplo "Transfer Tokens".

Assinando com uma autoridade PDA

Quando a autoridade em uma token account é um Program Derived Address pertencente ao seu programa, o programa assina a CPI com invoke_signed, passando as seeds do PDA como seeds de assinante. No Anchor, use CpiContext::new_with_signer com as seeds em vez de CpiContext::new. A instrução é, em tudo o mais, idêntica aos exemplos acima. Consulte CPIs com PDA Signers para a mecânica completa.

Agrupamento em Lote

A instrução Batch executa várias instruções do Token Program dentro de uma única invocação de programa. Como cada CPI possui um custo de computação fixo, executar várias operações de token em um único lote de CPI consome menos unidades de computação do que emitir um CPI separado por operação — consulte o modelo de custo de CPI para entender como os custos por CPI se acumulam.

Um menor uso de computação reduz as taxas de prioridade que uma transação paga por unidade de computação e aumenta suas chances de ser processada — o que é mais relevante para programas que realizam muitas operações de token em uma única instrução. Usos comuns incluem distribuir uma transferência para vários destinatários ou executar um fluxo de token com múltiplas etapas (por exemplo, sincronizar, transferir e encerrar) em um único CPI.

Batch aceita apenas instruções do Token Program como filhas, e um lote não pode conter outro lote. Para as operações que movimentam tokens (como transferências, emissões e queimas), o programa verifica se as contas afetadas pertencem ao Token Program antes de executá-las.

Referência de código-fonte

ItemDescriçãoFonte
BatchA instrução Batch (discriminador 255).Fonte
process_batchLógica do processador de lotes.Fonte

Chamando Batch via CPI

O crate pinocchio-token expõe um construtor Batch em pinocchio_token::instructions::Batch. Você prepara cada instrução filha nos buffers do lote e, em seguida, emite um único CPI com invoke.

Um Batch é suportado por três buffers fornecidos pelo chamador: um para os instruction data serializados, um para os metadados de conta de cada instrução filha, e um para as visualizações de conta passadas ao CPI. Construa-os como fatias MaybeUninit, passe-os para Batch::new, adicione as instruções filhas e, em seguida, invoque:

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 escreve o discriminador de lote no primeiro byte do buffer de dados e retorna um Batch que rastreia quanto de cada buffer foi utilizado. Cada construtor de instrução do Token Program (como Transfer, TransferChecked, MintTo e Burn) implementa o trait IntoBatch, portanto into_batch acrescenta os dados dessa instrução, os metadados de conta e as visualizações de conta ao lote. Chamar invoke emite o lote montado como um único CPI para o Token Program.

Os buffers limitam a capacidade do lote. Dimensione o buffer de dados para 1 + Σ(2 + child_data_len) bytes (o discriminador, mais um cabeçalho de 2 bytes e os dados serializados de cada filho) e cada buffer de contas para o número total de contas de todos os filhos. Se um filho não couber, into_batch retorna ProgramError::InvalidArgument. Para referência, Batch::MAX_DATA_LEN equivale a 10 KiB e Batch::MAX_ACCOUNTS_LEN corresponde ao número máximo de contas CPI permitido pelo runtime.

Para lotes cujo tamanho só é conhecido em tempo de execução, habilite o recurso alloc do crate e use BatchState::new(accounts_len, data_len) para alocar os buffers no heap, depois chame as_batch para obter um Batch.

Quando a autoridade de uma instrução filha é um PDA pertencente ao seu programa, assine o CPI com invoke_signed em vez de invoke, passando as seeds do PDA como entradas Signer.

Layout do instruction data

Um lote codifica seus filhos um após o outro após o discriminador 255. Todas as contas são passadas de forma plana, em ordem, e fatiadas por num_accounts para cada filho. O construtor produz exatamente este formato de transmissão:

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)

A capacidade de um lote é limitada pelos buffers passados para Batch::new: o buffer de dados deve conter o discriminador mais o cabeçalho e os dados de cada filho, e os buffers de contas devem conter as contas de cada filho. Dimensione-os para o maior lote que você construir, ou use BatchState (com o recurso alloc) para dimensioná-los em tempo de execução. O CPI combinado ainda está sujeito aos limites de contas e tamanho da transação; portanto, para fan-outs muito grandes, use Address Lookup Tables para incluir mais contas.

Is this page helpful?

Índice

Editar Página
© 2026 Fundação Solana. Todos os direitos reservados.