Memanggil Token Program melalui CPI

Program on-chain memindahkan token dengan mengeluarkan Cross Program Invocation (CPI) ke Token Program. Program Anda membangun instruksi Token Program, menyediakan akun-akun yang diperlukan, dan memanggil invoke (atau invoke_signed ketika Program Derived Address menandatangani). Token Program kemudian menjalankan instruksi tersebut dengan hak akses yang diberikan program Anda kepadanya.

Halaman ini membahas sisi khusus token dari alur tersebut. Untuk mekanisme CPI itu sendiri — cara kerja invoke dan invoke_signed, cara hak penanda tangan dan writable disebarkan, serta model biaya per-CPI — lihat Cross Program Invocation.

Pola umum

Sebagian besar CPI token mengikuti bentuk yang sama: buat instruksi, teruskan token account dan otoritas, lalu panggil. Contoh di bawah ini mentransfer token dengan TransferChecked, yang memverifikasi mint dan desimal sebagai bagian dari transfer.

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

Minting, burning, dan penutupan akun mengikuti pola yang sama dengan instruksi berbeda — MintTo, Burn, dan CloseAccount dalam modul anchor_spl::token Ankhor, atau pembangun yang sesuai di pinocchio-token. Untuk program lengkap yang dapat dijalankan dan melakukan CPI ke Token Program baik dengan Anchor maupun native Rust, lihat contoh token program — khususnya contoh "Transfer Tokens".

Menandatangani dengan otoritas Program Derived Address

Ketika otoritas pada sebuah token account adalah Program Derived Address yang dimiliki oleh program Anda, program tersebut menandatangani CPI itu sendiri dengan invoke_signed, dengan meneruskan seed Program Derived Address sebagai seed penanda tangan. Di Anchor, gunakan CpiContext::new_with_signer dengan seed tersebut sebagai pengganti CpiContext::new. Instruksinya identik dengan contoh-contoh di atas. Lihat CPI dengan PDA Signer untuk mekanisme lengkapnya.

Pengelompokan (Batching)

Instruksi Batch mengeksekusi beberapa instruksi Token Program dalam satu panggilan program tunggal. Karena setiap CPI memiliki biaya komputasi tetap, menjalankan beberapa operasi token dalam satu batch CPI menggunakan lebih sedikit unit komputasi dibandingkan mengeluarkan CPI terpisah per operasi — lihat model biaya CPI untuk mengetahui bagaimana biaya per-CPI terakumulasi.

Penggunaan komputasi yang lebih rendah mengurangi biaya prioritas yang dibayar transaksi per unit komputasi dan meningkatkan peluang keberhasilannya — hal ini paling penting bagi program yang melakukan banyak operasi token dalam satu instruksi. Penggunaan umum meliputi penyebaran transfer ke banyak penerima, atau menjalankan alur token bertahap (misalnya sinkronisasi, transfer, dan penutupan) dalam satu CPI.

Batch hanya menerima instruksi Token Program sebagai turunan, dan sebuah batch tidak dapat berisi batch lain. Untuk operasi yang memindahkan token (seperti transfer, mint, dan burn), program memverifikasi bahwa akun yang terpengaruh dimiliki oleh Token Program sebelum mengeksekusinya.

Referensi sumber

ItemDeskripsiSumber
BatchInstruksi Batch (diskriminator 255).Sumber
process_batchLogika pemroses Batch.Sumber

Memanggil Batch melalui CPI

Crate pinocchio-token mengekspos sebuah builder Batch di bawah pinocchio_token::instructions::Batch. Anda memasukkan setiap instruksi turunan ke dalam buffer batch, lalu mengeluarkan satu CPI dengan invoke.

Sebuah Batch didukung oleh tiga buffer yang disediakan pemanggil: satu untuk instruction data yang diserialisasi, satu untuk meta akun instruksi per-turunan, dan satu untuk tampilan akun yang diteruskan ke CPI. Buat sebagai slice MaybeUninit, berikan ke Batch::new, tambahkan turunan, lalu panggil:

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 menulis diskriminator batch ke byte pertama dari buffer data dan mengembalikan Batch yang melacak seberapa banyak setiap buffer yang telah digunakan. Setiap pembangun instruksi Token Program (seperti Transfer, TransferChecked, MintTo, dan Burn) mengimplementasikan trait IntoBatch, sehingga into_batch menambahkan data instruksi tersebut, meta akun, dan tampilan akun ke dalam batch. Memanggil invoke akan mengirimkan batch yang telah dirakit sebagai satu CPI ke Token Program.

Buffer membatasi kapasitas batch. Ukuran buffer data ke 1 + Σ(2 + child_data_len) byte (diskriminator, ditambah header 2-byte dan data terserialisasi untuk setiap instruksi anak) dan setiap buffer akun ke jumlah total akun di semua instruksi anak. Jika instruksi anak tidak muat, into_batch mengembalikan ProgramError::InvalidArgument. Sebagai referensi, Batch::MAX_DATA_LEN adalah 10 KiB dan Batch::MAX_ACCOUNTS_LEN sama dengan jumlah akun CPI maksimum dari runtime.

Untuk batch yang ukurannya hanya diketahui saat runtime, aktifkan fitur alloc pada crate dan gunakan BatchState::new(accounts_len, data_len) untuk mengalokasikan buffer di heap, lalu panggil as_batch untuk mendapatkan Batch.

Ketika otoritas instruksi anak adalah PDA yang dimiliki oleh program Anda, tandatangani CPI dengan invoke_signed sebagai pengganti invoke, dengan meneruskan seed PDA sebagai entri Signer.

Tata letak instruction data

Sebuah batch mengenkode instruksi-instruksi anaknya satu per satu di belakang diskriminator 255. Semua akun diteruskan secara datar, berurutan, dan dipotong oleh num_accounts untuk setiap instruksi anak. Builder menghasilkan tepat format wire ini:

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)

Kapasitas batch dibatasi oleh buffer yang diteruskan ke Batch::new: buffer data harus menampung diskriminator ditambah header dan data setiap instruksi anak, serta buffer akun harus menampung akun-akun setiap instruksi anak. Ukurkan buffer untuk batch terbesar yang Anda bangun, atau gunakan BatchState (dengan fitur alloc) untuk menentukan ukurannya saat runtime. CPI gabungan tetap tunduk pada batas akun dan ukuran transaksi, sehingga untuk fan-out yang sangat besar, gunakan Address Lookup Tables agar lebih banyak akun dapat termuat.

Is this page helpful?

Daftar Isi

Edit Halaman
© 2026 Yayasan Solana. Semua hak dilindungi.