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:
#[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:
#[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:
[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
Bekerja dengan Akun Raw
Anchor menyederhanakan penanganan akun dengan melakukan deserialisasi akun secara otomatis melalui konteks akun:
#[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:
#[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)]
:
#[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)]
Untuk mendeserialkan akun zero-copy dalam konteks instruksi Anda, gunakan AccountLoader<'info, T>
, di mana T
adalah tipe akun zero-copy Anda:
#[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:
#[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>,
}
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:
#[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:
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:
#[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:
/// 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:
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>,
}