Los programas on-chain mueven tokens emitiendo una
Cross Program Invocation (CPI) hacia el Token Program. Tu
programa construye una instrucción del Token Program, proporciona las cuentas
necesarias y llama a invoke (o invoke_signed cuando firma un Program
Derived Address). El Token Program ejecuta entonces esa instrucción con los
privilegios que tu programa le otorga.
Esta página cubre la parte específica de tokens en ese flujo. Para conocer el
mecanismo CPI en sí — cómo funcionan invoke y invoke_signed, cómo se
propagan los privilegios de firmante y escritura, y el
modelo de coste por CPI — consulta
Cross Program Invocation.
Patrones comunes
La mayoría de los CPIs de tokens siguen la misma estructura: construir la
instrucción, pasar las token accounts y una autoridad, e invocar. El ejemplo a
continuación transfiere tokens con TransferChecked, que verifica el mint y
los decimales como parte de la transferencia.
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>,}
Acuñar, quemar y cerrar cuentas siguen el mismo patrón con una instrucción
diferente — MintTo, Burn y CloseAccount en el módulo
anchor_spl::token
de Anchor, o los constructores equivalentes en
pinocchio-token. Para programas
completos y ejecutables que realizan CPI hacia el Token Program tanto en Anchor
como en Rust nativo, consulta los
ejemplos de token program — en particular el
ejemplo "Transfer Tokens".
Firmando con una autoridad PDA
Cuando la autoridad de una token account es un
Program Derived Address propiedad de tu programa, el programa
firma el CPI por sí mismo con invoke_signed, pasando las seeds del PDA
como signer seeds. En Anchor, usa CpiContext::new_with_signer con las
seeds en lugar de CpiContext::new. La instrucción es, por lo demás,
idéntica a los ejemplos anteriores. Consulta
CPIs with PDA Signers para conocer la mecánica
completa.
Procesamiento por lotes
La instrucción Batch ejecuta varias instrucciones de Token Program dentro
de una única invocación de programa. Dado que cada CPI conlleva un costo fijo de
cómputo, ejecutar varias operaciones de token en un único CPI por lotes consume
menos unidades de cómputo que emitir un CPI
separado por operación. Consulta el
modelo de costos de CPI para ver cómo se
acumulan los costos por CPI.
Un menor uso de cómputo reduce las tarifas de prioridad que paga una transacción por unidad de cómputo y mejora sus posibilidades de ser procesada, lo que resulta especialmente relevante para programas que realizan muchas operaciones de token en una sola instrucción. Los usos más comunes incluyen distribuir una transferencia a múltiples destinatarios, o ejecutar un flujo de token de varios pasos (por ejemplo, sincronizar, transferir y cerrar) en un único CPI.
Batch solo acepta instrucciones de Token Program como elementos
secundarios, y un lote no puede contener otro lote. Para las operaciones que
mueven tokens (como transferencias, acuñaciones y quemas), el programa verifica
que las cuentas afectadas sean propiedad del Token Program antes de ejecutarlas.
Referencia del código fuente
| Elemento | Descripción | Fuente |
|---|---|---|
Batch | La instrucción Batch (discriminador 255). | Fuente |
process_batch | Lógica del procesador de lotes. | Fuente |
Llamada a Batch mediante CPI
El crate pinocchio-token expone un
constructor Batch bajo pinocchio_token::instructions::Batch. Puedes
preparar cada instrucción secundaria en los búferes del lote y luego emitir un
único CPI con invoke.
Un Batch se apoya en tres búferes proporcionados por el invocador: uno
para los instruction data serializados, uno para los metadatos de cuenta de cada
instrucción secundaria, y uno para las vistas de cuenta pasadas al CPI.
Construyelos como slices MaybeUninit, pásalos a Batch::new, añade
los elementos secundarios y luego 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 escribe el discriminador de lote en el primer byte del búfer de
datos y devuelve un Batch que registra cuánto se ha utilizado de cada
búfer. Cada constructor de instrucciones de Token Program (como Transfer,
TransferChecked, MintTo y Burn) implementa el trait
IntoBatch, por lo que into_batch agrega los datos de instrucción,
las metainformaciones de cuenta y las vistas de cuenta de esa instrucción al
lote. Llamar a invoke emite el lote ensamblado como una única CPI hacia el
Token Program.
Los búferes limitan la capacidad del lote. Dimensiona el búfer de datos con
1 + Σ(2 + child_data_len) bytes (el discriminador, más una cabecera de 2 bytes
y los datos serializados de cada hijo) y cada búfer de cuentas con el número
total de cuentas de todos los hijos. Si un hijo no cabe, into_batch
devuelve ProgramError::InvalidArgument. Como referencia,
Batch::MAX_DATA_LEN equivale a 10 KiB y Batch::MAX_ACCOUNTS_LEN es
igual al número máximo de cuentas CPI del entorno de ejecución.
Para lotes cuyo tamaño solo se conoce en tiempo de ejecución, habilita la
función alloc del crate y usa BatchState::new(accounts_len, data_len)
para asignar los búferes en el heap; luego llama a as_batch para obtener
un Batch.
Cuando la autoridad de una instrucción hija es un PDA propiedad de tu programa,
firma la CPI con invoke_signed en lugar de invoke, pasando las
semillas del PDA como entradas Signer.
Diseño de instruction data
Un lote codifica sus hijos uno tras otro detrás del discriminador 255. Todas
las cuentas se pasan de forma plana, en orden, y son segmentadas por
num_accounts para cada hijo. El constructor produce exactamente este formato
de 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 capacidad de un lote está limitada por los búferes pasados a
Batch::new: el búfer de datos debe contener el discriminador más la
cabecera y los datos de cada hijo, y los búferes de cuentas deben contener las
cuentas de cada hijo. Dimensiónalos para el lote más grande que construyas, o
usa BatchState (con la función alloc) para dimensionarlos en tiempo de
ejecución. La CPI combinada sigue estando sujeta a los límites de cuentas y
tamaño de la transacción, por lo que para fan-outs muy grandes usa Address
Lookup Tables para incluir más cuentas.
Is this page helpful?