Anchor
Anchor for Dummies

Anchor for Dummies

Anchor Tingkat Lanjut

Terkadang, abstraksi yang dilakukan dengan Anchor membuat tidak mungkin untuk membangun logika yang dibutuhkan oleh program kita. Karena alasan ini, di bagian ini kita akan membahas tentang bagaimana menggunakan beberapa konsep lanjutan untuk bekerja dengan program kita.

Feature Flags

Insinyur perangkat lunak secara rutin membutuhkan lingkungan yang berbeda untuk pengembangan lokal, pengujian, dan produksi. Feature flags memberikan solusi elegan dengan memungkinkan kompilasi bersyarat dan konfigurasi khusus lingkungan tanpa perlu memelihara basis kode terpisah.

Cargo Features

Cargo features menawarkan mekanisme yang kuat untuk kompilasi bersyarat dan dependensi opsional. Anda mendefinisikan fitur bernama di Cargo.toml Anda pada tabel [features], kemudian mengaktifkan atau menonaktifkannya sesuai kebutuhan:

  • Aktifkan fitur melalui command line: --features feature_name
  • Aktifkan fitur untuk dependensi langsung di Cargo.toml

Ini memberi Anda kontrol yang detail atas apa yang disertakan dalam binary final Anda.

Feature Flags di Anchor

Program Anchor umumnya menggunakan feature flags untuk membuat perilaku, batasan, atau konfigurasi yang berbeda berdasarkan lingkungan target. Gunakan atribut cfg untuk mengompilasi kode secara bersyarat:

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
}

Feature flags sangat baik untuk menangani perbedaan lingkungan. Karena tidak semua token di-deploy di Mainnet dan Devnet, Anda sering membutuhkan alamat yang berbeda untuk jaringan yang berbeda:

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";

Pendekatan ini menghilangkan kesalahan konfigurasi deployment dan menyederhanakan alur kerja pengembangan Anda dengan beralih lingkungan pada waktu kompilasi daripada waktu runtime.

Beginilah cara Anda mengaturnya di file Cargo.toml Anda:

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

Setelah itu, Anda dapat menentukan flag yang ingin Anda gunakan untuk membangun program Anda seperti ini:

 
# Uses default (localnet)
anchor build

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

Setelah Anda membangun program, binary hanya akan memiliki flag kondisional yang Anda aktifkan, artinya setelah Anda menguji dan men-deploy, program akan menghormati kondisi tersebut.

Bekerja dengan Akun Raw

Anchor menyederhanakan penanganan akun dengan melakukan deserialisasi akun secara otomatis melalui konteks akun:

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

Namun, deserialisasi otomatis ini menjadi bermasalah ketika Anda memerlukan pemrosesan akun bersyarat, seperti mendeserialkan dan memodifikasi akun hanya ketika kriteria tertentu terpenuhi.

Untuk skenario bersyarat, gunakan UncheckedAccount untuk menunda validasi dan deserialisasi hingga runtime. Ini mencegah error keras ketika akun mungkin tidak ada atau ketika Anda perlu memvalidasinya secara programatis:

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(())
}

Akun Zero Copy

Runtime Solana menerapkan batasan memori yang ketat: 4KB untuk memori stack dan 32KB untuk memori heap. Selain itu, stack bertambah sebesar 10KB untuk setiap akun yang dimuat. Batasan ini membuat deserialisasi tradisional tidak mungkin dilakukan untuk akun besar, sehingga memerlukan teknik zero-copy untuk pengelolaan memori yang efisien.

Ketika akun melebihi batasan ini, Anda akan mengalami error stack overflow seperti: Stack offset of -30728 exceeded max offset of -4096 by 26632 bytes

Untuk akun berukuran sedang, Anda dapat menggunakan Box untuk memindahkan data dari stack ke heap (seperti disebutkan dalam pengantar), tetapi akun yang lebih besar memerlukan implementasi zero_copy.

Zero-copy sepenuhnya melewati deserialisasi otomatis dengan menggunakan akses memori raw. Untuk mendefinisikan tipe akun yang menggunakan zero-copy, anotasi struct dengan #[account(zero_copy)]:

rust
#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10_232],
}

Atribut #[account(zero_copy)] secara otomatis mengimplementasikan beberapa trait yang diperlukan untuk deserialisasi zero-copy:

  • #[derive(Copy, Clone)],
  • #[derive(bytemuck::Zeroable)],
  • #[derive(bytemuck::Pod)], dan
  • #[repr(C)]

Catatan: Untuk menggunakan zero-copy, tambahkan crate bytemuck ke dependensi Anda dengan fitur min_const_generics untuk bekerja dengan array berukuran apa pun dalam tipe zero-copy Anda.

Untuk mendeserialkan akun zero-copy dalam konteks instruksi Anda, gunakan AccountLoader<'info, T>, di mana T adalah tipe akun zero-copy Anda:

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

Menginisialisasi Akun Zero Copy

Ada dua pendekatan berbeda untuk inisialisasi, tergantung pada ukuran akun Anda:

Untuk akun di bawah 10.240 byte, gunakan batasan init secara langsung:

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

Batasan init dibatasi hingga 10.240 byte karena keterbatasan CPI. Di balik layar, init membuat panggilan CPI ke Program Sistem untuk membuat akun.

Untuk akun yang membutuhkan lebih dari 10.240 byte, Anda harus terlebih dahulu membuat akun secara terpisah dengan memanggil Program Sistem beberapa kali, menambahkan 10.240 byte per transaksi. Ini memungkinkan Anda membuat akun hingga ukuran maksimum Solana sebesar 10MB (10.485.760 byte), melewati batasan CPI.

Setelah membuat akun secara eksternal, gunakan batasan zero alih-alih init. Batasan zero memverifikasi bahwa akun belum diinisialisasi dengan memeriksa bahwa diskriminatornya belum diatur:

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

Untuk kedua metode inisialisasi, panggil load_init() untuk mendapatkan referensi yang dapat diubah ke data akun dan mengatur diskriminator akun:

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(())
}

Memuat Akun Zero Copy

Setelah diinisialisasi, gunakan load() untuk membaca data akun:

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(())
}

Bekerja dengan Raw CPIs

Anchor mengabstraksi kompleksitas cross-program invocation (CPI), tetapi memahami mekanisme yang mendasarinya sangat penting untuk pengembangan Solana tingkat lanjut.

Setiap instruksi terdiri dari tiga komponen inti: sebuah program_id, array accounts, dan byte instruction_data yang diproses Runtime Solana melalui syscall sol_invoke.

Pada level sistem, Solana mengeksekusi CPI melalui syscall ini:

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 menerima pointer ke data instruksi dan informasi akun Anda, kemudian mengeksekusi program target dengan input tersebut.

Berikut cara memanggil program Anchor menggunakan primitif Solana dasar:

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 ke Program Anchor Lain

Makro declare_program!() dari Anchor memungkinkan cross-program invocation yang type-safe tanpa perlu menambahkan program target sebagai dependensi. Makro ini menghasilkan modul Rust dari IDL program, menyediakan helper CPI dan tipe akun untuk interaksi program yang mulus.

Letakkan file IDL program target di direktori /idls di mana saja dalam struktur proyek Anda:

 
project/
├── idls/
│   └── target_program.json
├── programs/
│   └── your_program/
└── Cargo.toml

Kemudian gunakan makro untuk menghasilkan modul yang diperlukan:

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

Anda dapat melakukan CPI ke program Anchor lain dengan menambahkannya di Cargo.toml Anda di bawah [dependencies]: callee = { path = "../callee", features = ["cpi"] } setelah membangun program Anda dengan anchor build -- --features cpi dan menggunakan callee::cpi::<instruction>(). Cara ini tidak direkomendasikan karena dapat menyebabkan error dependensi sirkular. `

Daftar Isi
Lihat Sumber
Blueshift © 2025Commit: 96f50c6