Розширений Anchor
Іноді абстракція, створена за допомогою Anchor, унеможливлює побудову логіки, яка потрібна нашій програмі. З цієї причини в цьому розділі ми поговоримо про те, як використовувати деякі розширені концепції для роботи з нашою програмою.
Feature Flags
Інженерам-програмістам регулярно потрібні різні середовища для локальної розробки, тестування та виробництва. Прапорці функцій (feature flags) надають елегантне рішення, дозволяючи умовну компіляцію та конфігурації для конкретних середовищ без необхідності підтримувати окремі кодові бази.
Функції Cargo
Функції Cargo пропонують потужний механізм для умовної компіляції та опціональних залежностей. Ви визначаєте іменовані функції у таблиці [features]
вашого Cargo.toml
, а потім вмикаєте або вимикаєте їх за потреби:
- Увімкнення функцій через командний рядок: --features feature_name
- Увімкнення функцій для залежностей безпосередньо в Cargo.toml
Це дає вам детальний контроль над тим, що включається у ваш кінцевий бінарний файл.
Прапорці функцій в Anchor
Програми Anchor зазвичай використовують прапорці функцій для створення різної поведінки, обмежень або конфігурацій залежно від цільового середовища. Використовуйте атрибут cfg
для умовної компіляції коду:
#[cfg(feature = "testing")]
fn function_for_testing() {
// Compiled only when "testing" feature is enabled
}
#[cfg(not(feature = "testing"))]
fn function_for_production() {
// Compiled only when "testing" feature is disabled
}
Прапорці функцій відмінно справляються з відмінностями середовищ. Оскільки не всі токени розгортаються в Mainnet і Devnet, вам часто потрібні різні адреси для різних мереж:
#[cfg(feature = "localnet")]
pub const TOKEN_ADDRESS: &str = "Local_Token_Address_Here";
#[cfg(not(feature = "localnet"))]
pub const TOKEN_ADDRESS: &str = "Mainnet_Token_Address_Here";
Цей підхід усуває помилки конфігурації розгортання та спрощує ваш робочий процес розробки, перемикаючи середовища під час компіляції, а не під час виконання.
Ось як ви можете налаштувати це у вашому файлі Cargo.toml
:
[features]
default = ["localnet"]
localnet = []
Після цього ви можете вказати прапорець, з яким хочете побудувати свою програму, ось так:
# Uses default (localnet)
anchor build
# Build for mainnet
anchor build --no-default-features
Working with Raw Accounts
Anchor спрощує обробку облікових записів, автоматично десеріалізуючи їх через контекст облікового запису:
#[derive(Accounts)]
pub struct Instruction<'info> {
pub account: Account<'info, MyAccount>,
}
#[account]
pub struct MyAccount {
pub data: u8,
}
Однак ця автоматична десеріалізація стає проблематичною, коли вам потрібна умовна обробка облікових записів, наприклад, десеріалізація та модифікація облікового запису лише за певних критеріїв.
Для умовних сценаріїв використовуйте UncheckedAccount
, щоб відкласти валідацію та десеріалізацію до часу виконання. Це запобігає виникненню критичних помилок, коли облікові записи можуть не існувати або коли вам потрібно перевірити їх програмно:
#[derive(Accounts)]
pub struct Instruction<'info> {
/// CHECK: Validated conditionally in instruction logic
pub account: UncheckedAccount<'info>,
}
#[account]
pub struct MyAccount {
pub data: u8,
}
pub fn instruction(ctx: Context<Instruction>, should_process: bool) -> Result<()> {
if should_process {
// Deserialize the account data
let mut account = MyAccount::try_deserialize(&mut &ctx.accounts.account.to_account_info().data.borrow_mut()[..])
.map_err(|_| error!(InstructionError::AccountNotFound))?;
// Modify the account data
account.data += 1;
// Serialize back the data to the account
account.try_serialize(&mut &mut ctx.accounts.account.to_account_info().data.borrow_mut()[..])?;
}
Ok(())
}
Zero Copy Accounts
Середовище виконання Solana встановлює суворі обмеження пам'яті: 4 КБ для стекової пам'яті та 32 КБ для динамічної пам'яті. Крім того, стек збільшується на 10 КБ для кожного завантаженого облікового запису. Ці обмеження роблять традиційну десеріалізацію неможливою для великих облікових записів, що вимагає використання техніки zero-copy для ефективного управління пам'яттю.
Коли облікові записи перевищують ці обмеження, ви зіткнетеся з помилками переповнення стеку, наприклад: Stack offset of -30728 exceeded max offset of -4096 by 26632 bytes
Для облікових записів середнього розміру ви можете використовувати Box
, щоб перемістити дані зі стеку в динамічну пам'ять (як згадувалося у вступі), але більші облікові записи вимагають реалізації zero_copy
.
Zero-copy повністю обходить автоматичну десеріалізацію, використовуючи прямий доступ до пам'яті. Щоб визначити тип облікового запису, який використовує zero-copy, анотуйте структуру за допомогою #[account(zero_copy)]
:
#[account(zero_copy)]
pub struct Data {
// 10240 bytes - 8 bytes account discriminator
pub data: [u8; 10_232],
}
Атрибут #[account(zero_copy)]
автоматично реалізує кілька трейтів, необхідних для десеріалізації zero-copy:
#[derive(Copy, Clone)]
,#[derive(bytemuck::Zeroable)]
,#[derive(bytemuck::Pod)]
, та#[repr(C)]
Щоб десеріалізувати zero-copy акаунт у контексті вашої інструкції, використовуйте AccountLoader<'info, T>
, де T
це ваш тип zero-copy акаунта:
#[derive(Accounts)]
pub struct Instruction<'info> {
pub data_account: AccountLoader<'info, Data>,
}
Ініціалізація Zero Copy акаунта
Існує два різні підходи до ініціалізації, залежно від розміру вашого акаунта:
Для акаунтів розміром менше 10 240 байтів використовуйте обмеження init
безпосередньо:
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
// 10240 bytes is max space to allocate with init constraint
space = 8 + 10_232,
payer = payer,
)]
pub data_account: AccountLoader<'info, Data>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
Для акаунтів, що потребують більше ніж 10 240 байтів, спочатку потрібно створити акаунт окремо, викликаючи System Program кілька разів, додаючи по 10 240 байтів за транзакцію. Це дозволяє створювати акаунти розміром до максимального розміру Solana у 10 МБ (10 485 760 байтів), обходячи обмеження CPI.
Після створення акаунта зовнішньо, використовуйте обмеження zero
замість init
. Обмеження zero
перевіряє, що акаунт не був ініціалізований, перевіряючи, що його дискримінатор не встановлено:
#[account(zero_copy)]
pub struct Data {
// 10,485,780 bytes - 8 bytes account discriminator
pub data: [u8; 10_485_752],
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(zero)]
pub data_account: AccountLoader<'info, Data>,
}
Для обох методів ініціалізації викличте load_init()
щоб отримати змінне посилання на дані акаунта та встановити дискримінатор акаунта:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let account = &mut ctx.accounts.data_account.load_init()?;
// Set your account data here
// account.data = something;
Ok(())
}
Завантаження Zero Copy акаунта
Після ініціалізації використовуйте load()
для читання даних акаунта:
#[derive(Accounts)]
pub struct ReadOnly<'info> {
pub data_account: AccountLoader<'info, Data>,
}
pub fn read_only(ctx: Context<ReadOnly>) -> Result<()> {
let account = &ctx.accounts.data_account.load()?;
// Read your data here
// let value = account.data;
Ok(())
}
Робота з необробленими CPI
Anchor абстрагує складність міжпрограмного виклику (CPI), але розуміння базових механізмів є критично важливим для просунутої розробки на Solana.
Кожна інструкція складається з трьох основних компонентів: program_id
, масиву accounts
та байтів instruction_data
, які Runtime Solana обробляє через системний виклик sol_invoke
.
На системному рівні Solana виконує CPI через цей системний виклик:
/// Solana BPF syscall for invoking a signed instruction.
fn sol_invoke_signed_c(
instruction_addr: *const u8,
account_infos_addr: *const u8,
account_infos_len: u64,
signers_seeds_addr: *const u8,
signers_seeds_len: u64,
) -> u64;
Runtime отримує вказівники на дані вашої інструкції та інформацію про акаунти, а потім виконує цільову програму з цими вхідними даними.
Ось як викликати програму Anchor, використовуючи базові примітиви Solana:
pub fn manual_cpi(ctx: Context<MyCpiContext>) -> Result<()> {
// Construct instruction discriminator (8-byte SHA256 hash prefix)
let discriminator = sha256("global:instruction_name")[0..8]
// Build complete instruction data
let mut instruction_data = discriminator.to_vec();
instruction_data.extend_from_slice(&[additional_instruction_data]); // Your instruction parameters
// Define account metadata for the target program
let accounts = vec![
AccountMeta::new(ctx.accounts.account_1.key(), true), // Signer + writable
AccountMeta::new_readonly(ctx.accounts.account_2.key(), false), // Read-only
AccountMeta::new(ctx.accounts.account_3.key(), false), // Writable
];
// Collect account infos for the syscall
let account_infos = vec![
ctx.accounts.account_1.to_account_info(),
ctx.accounts.account_2.to_account_info(),
ctx.accounts.account_3.to_account_info(),
];
// Create the instruction
let instruction = solana_program::instruction::Instruction {
program_id: target_program::ID,
accounts,
data: instruction_data,
};
// Execute the CPI
solana_program::program::invoke(&instruction, &account_infos)?;
Ok(())
}
// For PDA-signed CPIs, use invoke_signed instead:
pub fn pda_signed_cpi(ctx: Context<PdaCpiContext>) -> Result<()> {
// ... instruction construction same as above ...
let signer_seeds = &[
b"seed",
&[bump],
];
solana_program::program::invoke_signed(
&instruction,
&account_infos,
&[signer_seeds],
)?;
Ok(())
}
CPI до іншої програми Anchor
Макрос declare_program!()
в Anchor дозволяє виконувати типобезпечні міжпрограмні виклики без додавання цільової програми як залежності. Макрос генерує модулі Rust з IDL програми, надаючи допоміжні функції CPI та типи облікових записів для безперешкодної взаємодії програм.
Розмістіть файл IDL цільової програми в директорії /idls
будь-де у структурі вашого проєкту:
project/
├── idls/
│ └── target_program.json
├── programs/
│ └── your_program/
└── Cargo.toml
Потім використовуйте макрос для генерації необхідних модулів:
use anchor_lang::prelude::*;
declare_id!("YourProgramID");
// Generate modules from IDL
declare_program!(target_program);
// Import the generated types
use target_program::{
accounts::Counter, // Account types
cpi::{self, accounts::*}, // CPI functions and account structs
program::TargetProgram, // Program type for validation
};
#[program]
pub mod your_program {
use super::*;
pub fn call_other_program(ctx: Context<CallOtherProgram>) -> Result<()> {
// Create CPI context
let cpi_ctx = CpiContext::new(
ctx.accounts.target_program.to_account_info(),
Initialize {
payer: ctx.accounts.payer.to_account_info(),
counter: ctx.accounts.counter.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
},
);
// Execute the CPI using generated helper
target_program::cpi::initialize(cpi_ctx)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct CallOtherProgram<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut)]
pub counter: Account<'info, Counter>, // Uses generated account type
pub target_program: Program<'info, TargetProgram>,
pub system_program: Program<'info, System>,
}