Advanced Anchor
Às vezes, a abstração feita com o Anchor torna impossível construir a lógica que nosso program precisa. Por essa razão, nesta seção vamos falar sobre como usar alguns conceitos avançados para trabalhar com nosso program.
Feature Flags
Engenheiros de software rotineiramente precisam de ambientes distintos para desenvolvimento local, testes e produção. Feature flags fornecem uma solução elegante ao habilitar compilação condicional e configurações específicas por ambiente sem manter bases de código separadas.
Cargo Features
As Cargo features oferecem um mecanismo poderoso para compilação condicional e dependências opcionais. Você define features nomeadas na tabela [features] do seu Cargo.toml e depois as habilita ou desabilita conforme necessário:
Habilite features via linha de comando: --features feature_name
Habilite features para dependências diretamente no Cargo.toml
Isso lhe dá controle granular sobre o que é incluído no seu binário final.
Feature Flags no Anchor
Programas Anchor comumente usam feature flags para criar comportamentos, restrições ou configurações diferentes com base no ambiente alvo. Use o atributo cfg para compilar código condicionalmente:
#[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 são excelentes para lidar com diferenças entre ambientes. Como nem todos os tokens são implantados na Mainnet e Devnet, você frequentemente precisa de endereços diferentes para redes diferentes:
#[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";Essa abordagem elimina erros de configuração de implantação e simplifica seu fluxo de trabalho de desenvolvimento ao trocar de ambiente em tempo de compilação em vez de tempo de execução.
É assim que você configuraria no seu arquivo Cargo.toml:
[features]
default = ["localnet"]
localnet = []Depois disso, você pode especificar a flag com a qual deseja compilar seu program assim:
# Uses default (localnet)
anchor build
# Build for mainnet
anchor build --no-default-featuresWorking with Raw Accounts
O Anchor simplifica o manuseio de accounts ao desserializar automaticamente as accounts através do contexto de account:
#[derive(Accounts)]
pub struct Instruction<'info> {
pub account: Account<'info, MyAccount>,
}
#[account]
pub struct MyAccount {
pub data: u8,
}No entanto, essa desserialização automática torna-se problemática quando você precisa de processamento condicional de accounts, como desserializar e modificar uma account apenas quando critérios específicos são atendidos.
Para cenários condicionais, use UncheckedAccount para adiar a validação e desserialização até o tempo de execução. Isso previne erros fatais quando accounts podem não existir ou quando você precisa validá-las programaticamente:
#[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
O runtime da Solana impõe limites rígidos de memória: 4KB para memória de stack e 32KB para memória de heap. Além disso, a stack cresce 10KB para cada account carregada. Essas restrições tornam a desserialização tradicional impossível para accounts grandes, exigindo técnicas de zero-copy para gerenciamento eficiente de memória.
Quando as accounts excedem esses limites, você encontrará erros de stack overflow como: Stack offset of -30728 exceeded max offset of -4096 by 26632 bytes
Para accounts de tamanho médio, você pode usar Box para mover dados da stack para o heap (como mencionado na introdução), mas accounts maiores exigem a implementação de zero_copy.
Zero-copy ignora completamente a desserialização automática usando acesso direto à memória bruta. Para definir um tipo de account que usa zero-copy, anote a struct com #[account(zero_copy)]:
#[account(zero_copy)]
pub struct Data {
// 10240 bytes - 8 bytes account discriminator
pub data: [u8; 10_232],
}O atributo #[account(zero_copy)] implementa automaticamente vários traits necessários para desserialização zero-copy:
#[derive(Copy, Clone)],#[derive(bytemuck::Zeroable)],#[derive(bytemuck::Pod)], e#[repr(C)]
Para desserializar uma account zero-copy no contexto da sua instruction, use AccountLoader<'info, T>, onde T é o seu tipo de account zero-copy:
#[derive(Accounts)]
pub struct Instruction<'info> {
pub data_account: AccountLoader<'info, Data>,
}Inicializando uma Account Zero Copy
Existem duas abordagens diferentes para inicialização, dependendo do tamanho da sua account:
Para accounts abaixo de 10.240 bytes, use a restrição init diretamente:
#[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>,
}Para accounts que precisam de mais de 10.240 bytes, você deve primeiro criar a account separadamente chamando o System Program múltiplas vezes, adicionando 10.240 bytes por transação. Isso permite criar accounts até o tamanho máximo da Solana de 10MB (10.485.760 bytes), contornando a limitação de CPI.
Após criar a account externamente, use a restrição zero em vez de init. A restrição zero verifica se a account não foi inicializada checando se seu discriminator não está definido:
#[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>,
}Para ambos os métodos de inicialização, chame load_init() para obter uma referência mutável aos dados da account e definir o discriminator da account:
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(())
}Carregando uma Account Zero Copy
Uma vez inicializada, use load() para ler os dados da account:
#[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(())
}Working with Raw CPIs
O Anchor abstrai a complexidade de cross-program invocation (CPI), mas entender a mecânica subjacente é crucial para o desenvolvimento avançado na Solana.
Toda instruction consiste em três componentes principais: um program_id, um array de accounts e bytes de instruction_data que o Solana Runtime processa via syscall sol_invoke.
No nível do sistema, a Solana executa CPIs através desta syscall:
/// 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;O Runtime recebe ponteiros para seus dados de instruction e informações de account, e então executa o program alvo com essas entradas.
Veja como você invocaria um programa Anchor usando primitivas brutas da 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 para outro Anchor Program
A macro declare_program!() do Anchor habilita invocações cross-program com segurança de tipos sem adicionar o program alvo como dependência. A macro gera módulos Rust a partir do IDL do program, fornecendo helpers de CPI e tipos de account para interações perfeitas entre programas.
Coloque o arquivo IDL do program alvo em um diretório /idls em qualquer lugar da estrutura do seu projeto:
project/
├── idls/
│ └── target_program.json
├── programs/
│ └── your_program/
└── Cargo.tomlEntão use a macro para gerar os módulos necessários:
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>,
}