Вызов Token Program через CPI

On-chain программы перемещают токены, выполняя Cross Program Invocation (CPI) в Token Program. Ваша программа формирует инструкцию Token Program, передаёт необходимые аккаунты и вызывает invoke (или invoke_signed, когда подписывает Program Derived Address). Token Program затем выполняет эту инструкцию с привилегиями, которые ваша программа ей предоставляет.

На этой странице рассматривается токен-специфичная часть данного процесса. Информацию о самом механизме CPI — о том, как работают invoke и invoke_signed, как распространяются привилегии подписанта и записи, а также о модели стоимости CPI — см. Cross Program Invocation.

Распространённые паттерны

Большинство токен-CPI следуют одной схеме: сформировать инструкцию, передать token account и авторизацию, затем выполнить вызов. Пример ниже переводит токены с помощью TransferChecked, который проверяет минт и количество десятичных знаков в рамках перевода.

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

Выпуск, сжигание и закрытие аккаунтов следуют той же схеме с другой инструкцией — MintTo, Burn и CloseAccount в модуле anchor_spl::token Anchor, или соответствующих строителях из pinocchio-token. Полные, готовые к запуску программы с CPI в Token Program на Anchor и нативном Rust см. в примерах токен-программ — в частности, в примере «Transfer Tokens».

Подпись с помощью PDA-авторизации

Когда авторизация token account является Program Derived Address, принадлежащим вашей программе, программа подписывает CPI самостоятельно с помощью invoke_signed, передавая сиды PDA в качестве сидов подписанта. В Anchor используйте CpiContext::new_with_signer с сидами вместо CpiContext::new. В остальном инструкция идентична приведённым выше примерам. Полное описание механики см. в CPI с PDA-подписантами.

Пакетная обработка

Инструкция Batch выполняет несколько инструкций Token Program в рамках одного вызова программы. Поскольку каждый CPI несёт фиксированные вычислительные затраты, выполнение нескольких токен-операций в одном пакетном CPI потребляет меньше вычислительных единиц, чем отдельный CPI на каждую операцию — см. модель стоимости CPI для понимания того, как накапливаются затраты на каждый CPI.

Меньшее потребление вычислительных ресурсов снижает приоритетные комиссии, уплачиваемые транзакцией за вычислительную единицу, и повышает вероятность её обработки — что особенно важно для программ, выполняющих множество токен-операций в одной инструкции. Типичные сценарии использования включают распределение перевода между множеством получателей или выполнение многоэтапного токен-потока (например, синхронизация, перевод и закрытие) в рамках одного CPI.

Batch принимает только инструкции Token Program в качестве дочерних, и пакет не может содержать другой пакет. Для операций, перемещающих токены (таких как переводы, минтинг и сжигание), программа проверяет, что затронутые аккаунты принадлежат Token Program, прежде чем их выполнять.

Исходный справочник

ЭлементОписаниеИсточник
BatchИнструкция Batch (дискриминатор 255).Источник
process_batchЛогика обработчика Batch.Источник

Вызов Batch через CPI

Крейт pinocchio-token предоставляет конструктор Batch в модуле pinocchio_token::instructions::Batch. Вы поэтапно добавляете каждую дочернюю инструкцию в буферы пакета, а затем выполняете один CPI с помощью invoke.

Batch опирается на три буфера, предоставленных вызывающей стороной: один для сериализованных instruction data, один для метаданных аккаунтов дочерних инструкций и один для представлений аккаунтов, передаваемых в CPI. Создайте их как срезы MaybeUninit, передайте их в Batch::new, добавьте дочерние элементы и выполните вызов:

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 записывает пакетный дискриминатор в первый байт буфера данных и возвращает Batch, отслеживающий, какая часть каждого буфера была использована. Каждый конструктор инструкций Token Program (например, Transfer, TransferChecked, MintTo и Burn) реализует трейт IntoBatch, поэтому into_batch добавляет данные, метаданные аккаунтов и представления аккаунтов этой инструкции в пакет. Вызов invoke выполняет собранный пакет как один CPI в Token Program.

Буферы ограничивают ёмкость пакета. Задайте размер буфера данных равным 1 + Σ(2 + child_data_len) байт (дискриминатор, плюс 2-байтовый заголовок и сериализованные данные каждого дочернего элемента), а каждый буфер аккаунтов — суммарному количеству аккаунтов по всем дочерним элементам. Если дочерний элемент не помещается, into_batch возвращает ProgramError::InvalidArgument. Для справки: Batch::MAX_DATA_LEN равен 10 КиБ, а Batch::MAX_ACCOUNTS_LEN соответствует максимальному количеству аккаунтов CPI в среде выполнения.

Для пакетов, размер которых известен только во время выполнения, включите фичу alloc крейта и используйте BatchState::new(accounts_len, data_len) для выделения буферов в куче, затем вызовите as_batch, чтобы получить Batch.

Когда авторитет дочерней инструкции является PDA, принадлежащим вашей программе, подпишите CPI с помощью invoke_signed вместо invoke, передав сиды PDA в виде записей Signer.

Структура instruction data

Пакет кодирует дочерние элементы последовательно, после дискриминатора 255. Все аккаунты передаются в плоском виде, по порядку, и разбиваются по num_accounts для каждого дочернего элемента. Конструктор формирует именно этот формат передачи данных:

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)

Ёмкость пакета ограничена буферами, переданными в Batch::new: буфер данных должен вмещать дискриминатор, а также заголовок и данные каждого дочернего элемента; буферы аккаунтов должны вмещать аккаунты каждого дочернего элемента. Задавайте их размер исходя из наибольшего пакета, который вы формируете, или используйте BatchState (с фичей alloc) для определения размера во время выполнения. Суммарный CPI по-прежнему подчиняется ограничениям транзакции по аккаунтам и размеру, поэтому для очень больших разветвлений используйте Address Lookup Tables, чтобы вместить больше аккаунтов.

Is this page helpful?

Содержание

Редактировать страницу