Llamando al Token Program mediante CPI

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

ElementoDescripciónFuente
BatchLa instrucción Batch (discriminador 255).Fuente
process_batchLó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:

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

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

Tabla de Contenidos

Editar Página