Інструкції та CPI
Інструкції є будівельними блоками програм Solana, які визначають дії, які можна виконувати. В Anchor інструкції реалізовані як функції з певними атрибутами та обмеженнями. Давайте розглянемо, як ефективно працювати з ними.
Структура інструкції
В Anchor інструкції визначаються за допомогою модуля #[program]
та окремих функцій інструкцій. Ось базова структура:
use anchor_lang::prelude::*;
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
// Instruction logic here
Ok(())
}
}
Контекст інструкції
Кожна функція інструкції отримує структуру Context
як свій перший параметр. Цей контекст містить:
accounts
: Акаунти, передані в інструкціюprogram_id
: Публічний ключ програмиremaining_accounts
: Будь-які додаткові акаунти, які не визначені явно в структурі контекстуbumps
: Полеbumps
особливо корисне при роботі з PDA, оскільки воно надає bump-значення, які використовувались для отримання адрес PDA (тільки якщо ви виводите їх у структурі акаунту)
До них можна отримати доступ таким чином:
// Accessing accounts
ctx.accounts.account_1
ctx.accounts.account_2
// Accessing program ID
ctx.program_id
// Accessing remaining accounts
for remaining_account in ctx.remaining_accounts {
// Process remaining account
}
// Accessing bumps for PDAs
let bump = ctx.bumps.pda_account;
Дискримінатор інструкції
Як і акаунти, інструкції в Anchor використовують дискримінатори для ідентифікації різних типів інструкцій. Дискримінатор за замовчуванням — це 8-байтовий префікс, згенерований за допомогою sha256("global:<instruction_name>")[0..8]
. Назва інструкції повинна бути в форматі snake_case.
Користувацький дискримінатор інструкції
Ви також можете вказати власний дискримінатор для ваших інструкцій:
#[instruction(discriminator = 1)]
pub fn custom_discriminator(ctx: Context<Custom>) -> Result<()> {
// Instruction logic
Ok(())
}
Шаблон інструкції
Ви можете писати свої інструкції різними способами. У цьому розділі ми розглянемо деякі стилі та способи, якими ви можете їх налаштувати
Логіка інструкцій
Логіку інструкцій можна організувати різними способами, залежно від складності вашої програми та вашого улюбленого стилю кодування. Ось основні підходи:
- Вбудована логіка інструкцій
Для простих інструкцій ви можете написати логіку безпосередньо у функції інструкції:
pub fn initialize(ctx: Context<Transfer>, amount: u64) -> Result<()> {
// Transfer tokens logic
// Close token account logic
Ok(())
}
- Реалізація в окремому модулі
Для дуже складних програм ви можете організувати логіку в окремих модулях:
// In a separate file: transfer.rs
pub fn execute(ctx: Context<Transfer>, amount: u64) -> Result<()> {
// Transfer tokens logic
// Close token account logic
Ok(())
}
// In your lib.rs
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
transfer::execute(ctx, amount)
}
- Окрема реалізація контексту
Для складніших інструкцій ви можете перенести логіку до реалізації структури контексту:
// In a separate file: transfer.rs
pub fn execute(ctx: Context<Transfer>, amount: u64) -> Result<()> {
ctx.accounts.transfer_tokens(amount)?;
ctx.accounts.close_token_account()?;
Ok(())
}
impl<'info> Transfer<'info> {
/// Transfers tokens from source to destination account
pub fn transfer_tokens(&mut self, amount: u64) -> Result<()> {
// Transfer tokens logic
Ok(())
}
/// Closes the source token account after transfer
pub fn close_token_account(&mut self) -> Result<()> {
// Close token account logic
}
}
// In your lib.rs
pub fn transfer(ctx: Context<Transfer>, amount: u64) -> Result<()> {
transfer::execute(ctx, amount)
}
Параметри інструкцій
Інструкції можуть приймати параметри, окрім контексту. Ці параметри автоматично серіалізуються та десеріалізуються Anchor. Ось ключові моменти щодо параметрів інструкцій:
- Базові типи
Anchor підтримує всі примітивні типи Rust та поширені типи Solana:
pub fn complex_instruction(
ctx: Context<Complex>,
amount: u64,
pubkey: Pubkey,
vec_data: Vec<u8>,
) -> Result<()> {
// Instruction logic
Ok(())
}
- Користувацькі типи
Ви можете використовувати користувацькі типи як параметри, але вони повинні реалізовувати AnchorSerialize
та AnchorDeserialize
:
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct InstructionData {
pub field1: u64,
pub field2: String,
}
pub fn custom_type_instruction(
ctx: Context<Custom>,
data: InstructionData,
) -> Result<()> {
// Instruction logic
Ok(())
}
Найкращі практики
-
Зосереджуйте інструкції: Кожна інструкція повинна добре виконувати одну задачу. Якщо інструкція робить занадто багато, розгляньте можливість розділити її на кілька інструкцій.
-
Використовуйте реалізацію контексту: Для складних інструкцій використовуйте підхід реалізації контексту, щоб:
- Підтримувати код організованим
- Полегшити тестування
- Покращити можливість повторного використання
- Додати належну документацію
-
Обробка помилок: Завжди використовуйте належну обробку помилок і повертайте змістовні повідомлення про помилки:
#[error_code]
pub enum TransferError {
#[msg("Insufficient balance")]
InsufficientBalance,
#[msg("Invalid amount")]
InvalidAmount,
}
impl<'info> Transfer<'info> {
pub fn transfer_tokens(&mut self, amount: u64) -> Result<()> {
require!(amount > 0, TransferError::InvalidAmount);
require!(
self.source.amount >= amount,
TransferError::InsufficientBalance
);
// Transfer logic
Ok(())
}
}
- Документація: Завжди документуйте логіку ваших інструкцій, особливо при використанні реалізації контексту:
impl<'info> Transfer<'info> {
/// # Transfers tokens
///
/// Transfers tokens from source to destination account
pub fn transfer_tokens(&mut self, amount: u64) -> Result<()> {
// Implementation
Ok(())
}
}
Міжпрограмні виклики (CPIs)
Міжпрограмні виклики (CPI) відносяться до процесу, коли одна програма викликає інструкції іншої програми, що забезпечує композиційність програм Solana. Anchor надає зручний спосіб здійснення CPI через CpiContext
та програмно-специфічні білдери.
Примітка: Ви можете знайти всі CPI системної програми, використовуючи основний крейт anchor і виконавши: use anchor_lang::system_program::*
; а для тих, що стосуються програми SPL токенів, нам потрібно імпортувати крейт anchor_spl і виконати: use anchor_spl::token::*
Базова структура CPI
Ось як зробити базовий CPI:
use anchor_lang::solana_program::program::invoke_signed;
use anchor_lang::system_program::{transfer, Transfer};
pub fn transfer_lamport(ctx: Context<TransferLamport>, amount: u64) -> Result<()> {
let cpi_accounts = Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
};
let cpi_program = ctx.accounts.system_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
transfer(cpi_ctx, amount)?;
Ok(())
}
CPI з PDA-підписувачами
При здійсненні CPI, які вимагають підписів PDA, використовуйте CpiContext::new_with_signer
:
use anchor_lang::solana_program::program::invoke_signed;
use anchor_lang::system_program::{transfer, Transfer};
pub fn transfer_lamport_with_pda(ctx: Context<TransferLamportWithPda>, amount: u64) -> Result<()> {
let seeds = &[
b"vault".as_ref(),
&[ctx.bumps.vault],
];
let signer = &[&seeds[..]];
let cpi_accounts = Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
};
let cpi_program = ctx.accounts.system_program.to_account_info();
let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
transfer(cpi_ctx, amount)?;
Ok(())
}
Обробка помилок
Anchor надає надійну систему обробки помилок для інструкцій. Ось як реалізувати власні помилки та обробляти їх у ваших інструкціях:
#[error_code]
pub enum MyError {
#[msg("Custom error message")]
CustomError,
#[msg("Another error with value: {0}")]
ValueError(u64),
}
pub fn handle_errors(ctx: Context<HandleErrors>, value: u64) -> Result<()> {
require!(value > 0, MyError::CustomError);
require!(value < 100, MyError::ValueError(value));
Ok(())
}