Wywoływanie Token Program przez CPI

Programy on-chain przesuwają tokeny, wydając Cross Program Invocation (CPI) do Token Program. Twój program buduje instrukcję Token Program, dostarcza wymagane konta i wykonuje invoke (lub invoke_signed, gdy podpisuje Program Derived Address). Token Program uruchamia następnie tę instrukcję z uprawnieniami, które mu przekazuje Twój program.

Ta strona omawia specyfikę tokenową tego przepływu. Jeśli chodzi o sam mechanizm CPI — jak działają invoke i invoke_signed, jak propagują się uprawnienia podpisującego i uprawnienia do zapisu oraz model kosztów CPI — zapoznaj się z Cross Program Invocation.

Typowe wzorce

Większość tokenowych CPI ma tę samą strukturę: zbuduj instrukcję, przekaż token accounts i autoryteta, a następnie wywołaj. Poniższy przykład transferuje tokeny za pomocą TransferChecked, który weryfikuje mint i liczbę miejsc dziesiętnych jako część transferu.

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

Mintowanie, spalanie i zamykanie kont mają ten sam wzorzec z inną instrukcją — MintTo, Burn i CloseAccount w module anchor_spl::token biblioteki Anchor, lub odpowiednikach w pinocchio-token. Kompletne, uruchamialne programy wykonujące CPI do Token Program zarówno w Anchor, jak i natywnym Ruście, znajdziesz w przykładach programów tokenowych — w szczególności w przykładzie "Transfer Tokens".

Podpisywanie z użyciem autorytetu PDA

Gdy autorytent token account jest Program Derived Address należącym do Twojego programu, program podpisuje CPI samodzielnie za pomocą invoke_signed, przekazując ziarna PDA jako ziarna podpisującego. W Anchor użyj CpiContext::new_with_signer z ziarenami zamiast CpiContext::new. Sama instrukcja jest identyczna jak w powyższych przykładach. Pełną mechanikę znajdziesz w CPI z podpisującymi PDA.

Grupowanie wsadowe

Instrukcja Batch wykonuje kilka instrukcji Token Program w ramach jednego wywołania programu. Ponieważ każde CPI wiąże się ze stałym kosztem obliczeniowym, uruchamianie kilku operacji na tokenach w jednym wsadowym CPI zużywa mniej jednostek obliczeniowych niż wydawanie oddzielnego CPI na operację — więcej informacji o tym, jak koszty poszczególnych CPI się sumują, znajdziesz w modelu kosztów CPI.

Mniejsze zużycie jednostek obliczeniowych obniża opłaty priorytetowe płacone przez transakcję za jednostkę obliczeniową i zwiększa szansę na jej realizację — co ma największe znaczenie dla programów wykonujących wiele operacji na tokenach w ramach jednej instrukcji. Typowe zastosowania obejmują rozesłanie transferu do wielu odbiorców lub przeprowadzenie wieloetapowego przepływu tokenów (na przykład synchronizację, transfer i zamknięcie) w ramach jednego CPI.

Batch akceptuje wyłącznie instrukcje Token Program jako elementy podrzędne, a partia nie może zawierać innej partii. W przypadku operacji przenoszących tokeny (takich jak transferów, mintowania i spalania) program weryfikuje, czy odpowiednie konta są własnością Token Program przed ich wykonaniem.

Odniesienie do źródła

ElementOpisŹródło
BatchInstrukcja Batch (dyskryminator 255).Źródło
process_batchLogika procesora wsadowego.Źródło

Wywoływanie Batch przez CPI

Crate pinocchio-token udostępnia builder Batch pod pinocchio_token::instructions::Batch. Każdą instrukcję podrzędną stawiasz w buforach partii, a następnie wydajesz pojedyncze CPI za pomocą invoke.

Batch jest obsługiwany przez trzy bufory dostarczone przez wywołującego: jeden dla serializowanych instruction data, jeden dla metadanych kont instrukcji podrzędnych i jeden dla widoków kont przekazywanych do CPI. Utwórz je jako wycinki MaybeUninit, przekaż je do Batch::new, dodaj elementy podrzędne, a następnie wywołaj:

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 zapisuje dyskryminator wsadowy do pierwszego bajtu bufora danych i zwraca Batch, który śledzi, ile każdego bufora zostało wykorzystane. Każdy konstruktor instrukcji Token Program (taki jak Transfer, TransferChecked, MintTo i Burn) implementuje trait IntoBatch, więc into_batch dołącza dane tej instrukcji, metadane kont i widoki kont do wsadu. Wywołanie invoke wysyła złożony wsad jako jedno CPI do Token Program.

Bufory ograniczają pojemność wsadu. Rozmiar bufora danych powinien wynosić 1 + Σ(2 + child_data_len) bajtów (dyskryminator plus 2-bajtowy nagłówek i zserializowane dane każdego elementu podrzędnego), a każdy bufor kont powinien odpowiadać łącznej liczbie kont wszystkich elementów podrzędnych. Jeśli element podrzędny nie mieści się w buforze, into_batch zwraca ProgramError::InvalidArgument. Dla odniesienia, Batch::MAX_DATA_LEN wynosi 10 KiB, a Batch::MAX_ACCOUNTS_LEN odpowiada maksymalnej liczbie kont CPI środowiska uruchomieniowego.

W przypadku wsadów, których rozmiar jest znany dopiero w czasie wykonywania, włącz funkcję alloc dla skrzynki i użyj BatchState::new(accounts_len, data_len), aby przydzielić bufory na stercie, a następnie wywołaj as_batch, aby uzyskać Batch.

Gdy uprawnienie instrukcji podrzędnej należy do PDA będącego własnością Twojego programu, podpisz CPI za pomocą invoke_signed zamiast invoke, przekazując ziarna PDA jako wpisy Signer.

Układ instruction data

Wsad koduje swoje elementy podrzędne jeden po drugim za dyskryminatorem 255. Wszystkie konta są przekazywane płasko, w kolejności, i dzielone przez num_accounts dla każdego elementu podrzędnego. Konstruktor generuje dokładnie ten format przesyłu:

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)

Pojemność wsadu jest ograniczona przez bufory przekazane do Batch::new: bufor danych musi pomieścić dyskryminator oraz nagłówek i dane każdego elementu podrzędnego, a bufory kont muszą pomieścić konta każdego elementu podrzędnego. Dopasuj ich rozmiar do największego wsadu, który tworzysz, lub użyj BatchState (z funkcją alloc), aby określić ich rozmiar w czasie wykonywania. Łączone CPI nadal podlega limitom kont i rozmiaru transakcji, więc w przypadku bardzo dużych fan-outów użyj Address Lookup Tables, aby zmieścić więcej kont.

Is this page helpful?

Spis treści

Edytuj stronę