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
| Element | Opis | Źródło |
|---|---|---|
Batch | Instrukcja Batch (dyskryminator 255). | Źródło |
process_batch | Logika 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:
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 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:
[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?