Anchor
Anchor для початківців

Anchor для початківців

Розширений Anchor

Іноді абстракція, створена за допомогою Anchor, унеможливлює побудову логіки, яка потрібна нашій програмі. З цієї причини в цьому розділі ми поговоримо про те, як використовувати деякі розширені концепції для роботи з нашою програмою.

Feature Flags

Інженерам-програмістам регулярно потрібні різні середовища для локальної розробки, тестування та виробництва. Прапорці функцій (feature flags) надають елегантне рішення, дозволяючи умовну компіляцію та конфігурації для конкретних середовищ без необхідності підтримувати окремі кодові бази.

Функції Cargo

Функції Cargo пропонують потужний механізм для умовної компіляції та опціональних залежностей. Ви визначаєте іменовані функції у таблиці [features] вашого Cargo.toml, а потім вмикаєте або вимикаєте їх за потреби:

  • Увімкнення функцій через командний рядок: --features feature_name
  • Увімкнення функцій для залежностей безпосередньо в Cargo.toml

Це дає вам детальний контроль над тим, що включається у ваш кінцевий бінарний файл.

Прапорці функцій в Anchor

Програми Anchor зазвичай використовують прапорці функцій для створення різної поведінки, обмежень або конфігурацій залежно від цільового середовища. Використовуйте атрибут cfg для умовної компіляції коду:

rust
#[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, вам часто потрібні різні адреси для різних мереж:

rust
#[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:

toml
[features]
default = ["localnet"]
localnet = []

Після цього ви можете вказати прапорець, з яким хочете побудувати свою програму, ось так:

 
# Uses default (localnet)
anchor build

# Build for mainnet
anchor build --no-default-features

Після збірки програми, бінарний файл матиме лише той умовний прапорець, який ви увімкнули, що означає, що під час тестування та розгортання він дотримуватиметься цієї умови.

Working with Raw Accounts

Anchor спрощує обробку облікових записів, автоматично десеріалізуючи їх через контекст облікового запису:

rust
#[derive(Accounts)]
pub struct Instruction<'info> {
    pub account: Account<'info, MyAccount>,
}
 
#[account]
pub struct MyAccount {
    pub data: u8,
}

Однак ця автоматична десеріалізація стає проблематичною, коли вам потрібна умовна обробка облікових записів, наприклад, десеріалізація та модифікація облікового запису лише за певних критеріїв.

Для умовних сценаріїв використовуйте UncheckedAccount, щоб відкласти валідацію та десеріалізацію до часу виконання. Це запобігає виникненню критичних помилок, коли облікові записи можуть не існувати або коли вам потрібно перевірити їх програмно:

rust
#[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)]:

rust
#[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 додайте крейт bytemuck до ваших залежностей з функцією min_const_generics, щоб працювати з масивами будь-якого розміру у ваших типах zero-copy.

Щоб десеріалізувати zero-copy акаунт у контексті вашої інструкції, використовуйте AccountLoader<'info, T>, де T це ваш тип zero-copy акаунта:

rust
#[derive(Accounts)]
pub struct Instruction<'info> {
    pub data_account: AccountLoader<'info, Data>,
}

Ініціалізація Zero Copy акаунта

Існує два різні підходи до ініціалізації, залежно від розміру вашого акаунта:

Для акаунтів розміром менше 10 240 байтів використовуйте обмеження init безпосередньо:

rust
#[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>,
}

Обмеження init має ліміт у 10 240 байтів через обмеження CPI. Під капотом init робить CPI виклик до System Program для створення акаунта.

Для акаунтів, що потребують більше ніж 10 240 байтів, спочатку потрібно створити акаунт окремо, викликаючи System Program кілька разів, додаючи по 10 240 байтів за транзакцію. Це дозволяє створювати акаунти розміром до максимального розміру Solana у 10 МБ (10 485 760 байтів), обходячи обмеження CPI.

Після створення акаунта зовнішньо, використовуйте обмеження zero замість init. Обмеження zero перевіряє, що акаунт не був ініціалізований, перевіряючи, що його дискримінатор не встановлено:

rust
#[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() щоб отримати змінне посилання на дані акаунта та встановити дискримінатор акаунта:

rust
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() для читання даних акаунта:

rust
#[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 через цей системний виклик:

rust
/// 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:

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

Ви можете виконати CPI до іншої програми Anchor, додавши у ваш Cargo.toml під [dependencies]: callee = { path = "../callee", features = ["cpi"] } після збірки вашої програми, виконавши anchor build -- --features cpi та використовуючи callee::cpi::<instruction>(). Це не рекомендується, оскільки може призвести до помилки циклічної залежності. `

Blueshift © 2025Commit: 6d01265
Blueshift | Anchor для початківців | Просунутий Anchor