NOVO: Helius adquire a Light Protocol
Introdução ao Anchor
Blog/Desenvolvimento

Introdução ao Anchor: guia para iniciantes sobre como criar programas Solana

Developer Experience Engineer0xIchigo no X0xIchigo no LinkedIn0xIchigo no GitHub
46 min de leitura

Um grande agradecimento a Noah, Mike, Jonas, Ryan, Prames e bl0ckpain pela revisão deste artigo.

Sobre o que é este artigo?

Rust costuma ser descrito como a língua franca do desenvolvimento de programas Solana. No entanto, seria mais preciso descrever o Anchor dessa forma, já que a maior parte do desenvolvimento em Rust usa esse framework. O Anchor é um framework opinativo e poderoso, projetado para criar programas Solana seguros com rapidez. Ele simplifica o processo de desenvolvimento ao reduzir o código repetitivo em áreas como a (des)serialização de contas e os dados de instruções, realizar verificações essenciais de segurança, gerar bibliotecas cliente automaticamente e oferecer um ambiente de testes abrangente.

Este artigo mostra como desenvolver programas com Anchor. Ele aborda a instalação do Anchor, o uso do Solana Playground e a criação, compilação e implantação de um programa simples Hello, World!. Em seguida, vamos analisar em mais detalhes como o Anchor simplifica o processo de desenvolvimento, examinando IDLs, macros, a estrutura dos programas Anchor, tipos e restrições de contas e tratamento de erros. Também abordaremos brevemente Cross-Program Invocations e Program Derived Addresses. Este artigo oferecerá tudo o que você precisa saber para começar a usar o Anchor hoje mesmo.

Conhecimentos prévios

Este artigo pressupõe que você conheça o modelo de programação da Solana. Se você ainda não desenvolve na Solana, recomendo a leitura da minha publicação anterior, O modelo de programação da Solana: uma introdução ao desenvolvimento na Solana. 

Não se preocupe se você ainda não conhece Rust — não é necessário ter conhecimentos avançados para começar a desenvolver com Anchor. A documentação do Anchor afirma que os desenvolvedores só precisam conhecer os fundamentos de Rust (ou seja, os nove primeiros capítulos do Livro de Rust). Recomendo assistir ao Guia de sobrevivência em Rust para conferir uma boa explicação dos conceitos essenciais da programação em Rust. Também é fundamental entender as regras de memória, propriedade e empréstimo do Rust.

Para facilitar o aprendizado, recomendo que desenvolvedores sem experiência em linguagens de programação de baixo nível estudem diferentes conceitos específicos da programação de sistemas que os recursos sobre Rust costumam deixar de lado. Por exemplo, vale a pena explorar temas como tamanhos de variáveis, ponteiros e vazamentos de memória. Também recomendo o Rust por exemplo e meu repositório sobre diversas estruturas de dados e algoritmos escritos em Rust para ver exemplos práticos de Rust.

Prefere usar TypeScript? Aprenda como escrever programas Solana em TypeScript usando o framework Poseidon para transpilar TypeScript para Rust e gerar programas Anchor válidos.

Este artigo trata do desenvolvimento com Anchor e somente disso. Não abordaremos como desenvolver programas em Rust nativo, nem pressuporemos qualquer conhecimento sobre o assunto. Além disso, este artigo não abordará o desenvolvimento do lado do cliente com Anchor — em um artigo futuro, mostraremos como testar e interagir com programas Anchor usando TypeScript.

Dito isso, vamos começar a usar o Anchor!

Instalando o Anchor

A configuração do Anchor envolve algumas etapas simples para instalar as ferramentas e os pacotes necessários. Esta seção aborda a instalação dessas ferramentas e desses pacotes (ou seja, Rust, o conjunto de ferramentas da Solana, Yarn e o Anchor Version Manager).

Instalando o Rust

O Rust pode ser instalado pelo site oficial do Rust ou pela linha de comando:

Código
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Instalando o conjunto de ferramentas da Solana

O Anchor também exige o conjunto de ferramentas da Solana. A versão mais recente (1.17.16 — no momento em que este artigo foi escrito) pode ser instalada com o seguinte comando no macOS e no Linux:

Código
sh -c "$(curl -sSfL https://release.solana.com/v1.17.16/install)"

Usuários do Windows podem instalar o conjunto de ferramentas da Solana usando o seguinte comando:

Código
cmd /c "curl https://release.solana.com/v1.17.16/solana-install-init-x86_64-pc-windows-msvc.exe --output C:\solana-install-tmp\solana-install-init.exe --create-dirs"

No entanto, é altamente recomendável usar o Subsistema do Windows para Linux (WSL). Isso permite executar um ambiente Linux em sua máquina Windows sem precisar de inicialização dupla nem de uma máquina virtual separada. Se você optar por esse caminho, consulte novamente as instruções de instalação para Linux (ou seja, o comando curl).

Os desenvolvedores também podem substituir v1.17.16 pela tag de versão que desejam baixar. Outra opção é usar os nomes de canal stable, beta ou edge. Após a instalação, execute solana –-version para confirmar que a versão desejada do solana está instalada.

Instalando o Yarn

O Anchor também exige o Yarn. Ele pode ser usado com o Corepack, incluído em todas as versões oficiais do Node.js a partir do Node.js 14.9 / 16.9. No entanto, durante a fase experimental atual, ele precisa ser habilitado. Portanto, precisamos executar corepack enable antes de ativá-lo. Algumas distribuições de terceiros podem não incluir o Corepack por padrão. Nesse caso, talvez você precise executar npm install -g corepack antes de corepack enable.

Instalando o Anchor com o AVM

A documentação do Anchor recomenda instalar o Anchor por meio do Anchor Version Manager (AVM). O AVM simplifica o gerenciamento e a seleção de várias instalações do binário anchor-cli. Isso pode ser necessário para produzir compilações verificáveis ou trabalhar com versões diferentes em vários programas. Ele pode ser instalado usando o Cargo com o comando: cargo install --git [https://github.com/coral-xyz/anchor](https://github.com/coral-xyz/anchor) avm --locked --force. Em seguida, instale e use a versão mais recente:

Código
avm install latest
avm use latest

# Verify the installation
avm --version

Para ver uma lista das versões disponíveis do anchor-cli, use o comando avm list. Os desenvolvedores podem usar avm use <version> para selecionar uma versão específica. Essa versão permanecerá em uso até ser alterada. Para desinstalar uma versão específica, use o comando avm uninstall <version>.

Instalando o Anchor com binários e compilando a partir do código-fonte

No Linux, os binários do Anchor estão disponíveis por meio do pacote npm @coral-xyz/anchor-cli. Atualmente, somente o Linux x86_64 é compatível. Portanto, os desenvolvedores precisam compilar a partir do código-fonte em outros sistemas operacionais. É possível usar o Cargo para instalar a CLI diretamente. Por exemplo:

Código
cargo install --git https://github.com/coral-xyz/anchor --tag v0.29.0 anchor-cli --locked

Altere o argumento --tag para instalar outra versão desejada do Anchor. Talvez seja necessário instalar dependências adicionais se a instalação com o Cargo falhar. Por exemplo, no Ubuntu:

Código
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev

Em seguida, os desenvolvedores podem verificar a instalação do Anchor com o comando anchor --version.

Solana Playground

Como alternativa, os desenvolvedores podem começar a usar o Anchor com o Solana Playground (Solpg). O Solana Playground é uma IDE executada no navegador que facilita o rápido desenvolvimento, teste e implantação de programas Solana. 

Ao usar o Solana Playground pela primeira vez, os desenvolvedores precisam criar uma Playground Wallet. Clique no indicador de status vermelho chamado Não conectado, no canto inferior esquerdo da tela. A seguinte janela será exibida:

Recomenda-se salvar o arquivo do par de chaves da carteira como backup antes de clicar em Continuar. Isso ocorre porque a Playground Wallet é salva no armazenamento local do navegador. Limpar o cache do navegador removerá a carteira. 

Clique em Continuar para criar uma carteira da devnet pronta para uso na IDE.

Para adicionar fundos à carteira, os desenvolvedores podem executar o seguinte comando solana airdrop <amount> no terminal do Playground, substituindo <amount> pela quantidade desejada de SOL da devnet. Como alternativa, acesse este faucet para obter SOL da devnet. Recomendo consultar o seguinte guia sobre como obter SOL da devnet.

Você pode encontrar o seguinte erro:

Código
Error: unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer funds

Isso geralmente ocorre porque o faucet da devnet está sem fundos e/ou porque foi solicitada uma quantidade muito grande de SOL. O limite atual é de 5 SOL, mais do que suficiente para implantar este programa. Portanto, recomenda-se solicitar 5 SOL ao faucet ou executar o comando solana airdrop 5. Solicitar quantidades menores de forma incremental pode resultar em limitação de taxa.

Hello, World!

Programas Hello, World! são considerados uma excelente introdução a novos frameworks ou linguagens de programação. Isso se deve à simplicidade deles, já que desenvolvedores de todos os níveis conseguem entendê-los. Esses programas também esclarecem a estrutura e a sintaxe básicas do novo modelo de programação sem introduzir lógica ou funções complexas. Eles rapidamente se tornaram um padrão para iniciantes em programação, então é natural escrevermos um para o Anchor. Esta seção mostra como compilar e implantar um programa Hello, World! com uma configuração local do Anchor e também com o Solana Playground.

Criando um novo projeto com uma configuração local do Anchor

Criar um novo projeto com o Anchor instalado é muito simples:

Código
anchor init hello-world
cd hello-world

Esses comandos inicializarão um novo projeto Anchor chamado hello-world e acessarão seu diretório. Nesse diretório, navegue até hello-world/programs/hello-world/src/lib.rs. Esse arquivo contém o seguinte código inicial:

Código
use anchor_lang::prelude::*;

declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

#[program]
pub mod hello-world {
    use super::*;

    pub fn initialize(ctx: Context) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

O Anchor preparou vários arquivos e diretórios para nós. Especificamente:

  • Um app vazio para o cliente do programa
  • Uma pasta programs que armazenará todos os nossos programas Solana
  • Uma pasta tests para testes em JavaScript. Ela inclui um arquivo de teste gerado automaticamente para o código inicial
  • Um arquivo de configuração Anchor.toml. Se você ainda não conhece Rust, um arquivo TOML é um formato minimalista de arquivo de configuração, fácil de ler devido à sua semântica. O arquivo Anchor.toml é usado para configurar como o Anchor interagirá com o programa. Por exemplo, em qual cluster o programa deve ser implantado.

Criando um novo projeto com o Solana Playground

Criar um novo projeto no Solana Playground é muito simples. Vá até o canto superior esquerdo e clique em Criar um novo projeto:

A seguinte janela será exibida:

Dê um nome ao programa, selecione Anchor(Rust) e clique em Criar. Isso criará um novo projeto Anchor diretamente no navegador. Na seção Programa, à esquerda, você verá um diretório src. Ele contém o arquivo lib.rs, que traz o seguinte código inicial:

Código
use anchor_lang::prelude::*;

// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("11111111111111111111111111111111");

#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data); // Message will show up in the tx logs
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    // We must specify the space in order to initialize an account.
    // First 8 bytes are default account discriminator,
    // next 8 bytes come from NewAccount.data being type u64.
    // (u64 = 64 bits unsigned integer = 8 bytes)
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct NewAccount {
    data: u64
}

Observe que o Solana Playground gera apenas os arquivos client.ts e anchor.test.ts. Recomendo ler a seção sobre como criar um programa com o Anchor localmente para entender o que normalmente é gerado em um novo projeto Anchor.

Escrevendo Hello, World!

Seja usando o Anchor localmente ou por meio do Solana Playground, para criar um programa Hello, World! muito simples, substitua o código inicial pelo seguinte:

Código
use anchor_lang::prelude::*;

declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

#[program]
mod hello_world {
use super::*;

pub fn hello(_ctx: Context<Hello>) -> Result<()> {
	msg!("Hello, World!");
	Ok(())
}

#[derive(Accounts)]
pub struct Hello {}
}

Analisaremos os detalhes de cada parte nas próximas seções. Por enquanto, é importante observar o uso de macros e traits para simplificar o processo de desenvolvimento. A macro declare_id! define a chave pública do programa. No desenvolvimento local, o comando anchor init usado para configurar o programa gerará um par de chaves no diretório target/deploy e preencherá essa macro. O Solana Playground também fará isso automaticamente.

Em nosso módulo principal hello_world, criamos uma função que registra Hello, World!. Ela também retorna Ok(()) para indicar que o programa foi executado com sucesso. Observe que adicionamos um sublinhado como prefixo de ctx para evitar avisos de variável não utilizada no console. Hello é uma struct de conta que não exige o fornecimento de nenhuma conta, pois o programa apenas registra uma nova mensagem.

É só isso! Não precisamos receber nenhuma conta nem implementar uma lógica complexa. O código apresentado acima cria um programa que registra Hello, World!

Compilando e implantando localmente

Esta seção se concentrará na implantação no Localhost. Embora o Solana Playground use a devnet por padrão, um ambiente de desenvolvimento local oferece uma experiência significativamente melhor. Além de ser mais rápido, ele evita vários problemas comuns nos testes realizados na devnet. Por exemplo, SOL insuficiente para transações, implantações lentas e a impossibilidade de realizar testes quando a devnet está indisponível. Em contrapartida, o desenvolvimento local pode garantir um estado limpo a cada teste. Isso proporciona um ambiente de desenvolvimento mais controlado e eficiente.

Configurando nossas ferramentas

Primeiro, queremos garantir que o conjunto de ferramentas da Solana esteja configurado corretamente para o desenvolvimento no Localhost. Execute o comando solana config set --url localhost para garantir que todas as configurações apontem para URLs do Localhost. 

Também garanta que você tenha um par de chaves local para interagir com a Solana localmente. Para implantar um programa com a Solana CLI, você precisa de uma carteira Solana com saldo em SOL. Execute o comando solana address para verificar se você já tem um par de chaves local. Se encontrar um erro, execute o comando solana-keygen new. Por padrão, uma nova carteira no sistema de arquivos será criada no caminho ~/.config/solana/id.json. Também será fornecida uma frase de recuperação que pode ser usada para recuperar as chaves pública e privada. Recomenda-se salvar esse par de chaves, mesmo que ele seja usado localmente. Observe também que, se você já tiver uma carteira no sistema de arquivos salva no local padrão, o comando solana-keygen new não a substituirá, a menos que isso seja especificado com o comando --force.

Configurando o Anchor.toml

Em seguida, queremos garantir que nosso arquivo Anchor.toml aponte corretamente para o Localhost. Verifique se ele contém o seguinte código:

Código
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Localnet"
wallet = '~config/solana/id.json'

Aqui, [programs.localnet] se refere ao ID do programa na localnet (ou seja, no Localhost). O ID do programa é sempre especificado em relação ao cluster. Isso ocorre porque o mesmo programa pode ser implantado em outro endereço em um cluster diferente. Do ponto de vista da experiência do desenvolvedor, declarar novos IDs para programas implantados em clusters diferentes pode ser incômodo. 

O ID do programa é público. No entanto, seu par de chaves fica armazenado na pasta target/deploy. Ele segue uma convenção específica de nomenclatura baseada no nome do programa. Por exemplo, se o programa se chamar hello_world, o Anchor procurará um par de chaves em target/deploy/hello-world-keypair.json. Se não encontrar esse arquivo durante a implantação, o Anchor gerará um novo par de chaves. Isso resultará em um novo ID de programa. Portanto, é fundamental atualizar o ID do programa após a primeira implantação. O arquivo hello-world-keypair.json serve como comprovante de propriedade do programa. Se o par de chaves vazar, agentes mal-intencionados poderão fazer alterações não autorizadas no programa. 

Com [provider], estamos instruindo o Anchor a usar o Localhost e a carteira especificada para pagar pelo armazenamento e pelas transações.

Compilando, implantando e executando um ledger local

Use o comando anchor build para compilar o programa. Para compilar um programa específico pelo nome, use o comando anchor build -p <program name>, substituindo <program name> pelo nome do programa. Como estamos desenvolvendo na localnet, podemos usar os comandos de localnet da Anchor CLI para simplificar o processo de desenvolvimento. Por exemplo, anchor localnet --skip-build é particularmente útil para ignorar a compilação de um programa no workspace. Isso pode economizar tempo ao executar testes quando o código do programa não foi alterado.

Se tentarmos executar o comando anchor deploy agora, receberemos um erro. Isso ocorre porque não há um cluster da Solana em execução em nossa própria máquina para realizarmos testes. Podemos executar um ledger local para simular um cluster na máquina. A Solana CLI já inclui um validador de teste. Executar o comando solana-test-validator iniciará um cluster completo de nó único em sua estação de trabalho. Isso oferece várias vantagens, como ausência de limites de taxa de RPC e de airdrops, implantação direta de programas on-chain, carregamento de contas a partir de arquivos e clonagem de contas de um cluster público. O validador de teste deve ser executado em uma janela separada e aberta do terminal e permanecer ativo para que o cluster do localhost continue online e disponível para interação. 

Agora podemos executar anchor deploy com sucesso para implantar o programa em nosso ledger local. Todos os dados transmitidos ao ledger local serão salvos em uma pasta test-ledger gerada no diretório de trabalho atual. Recomenda-se adicionar essa pasta ao arquivo .gitignore para evitar incluí-la em commits no repositório. Além disso, encerrar o ledger local (ou seja, pressionar Ctrl + C no terminal) não removerá nenhum dado enviado ao cluster. Para removê-los, exclua a pasta test-ledger ou execute solana-test-validator --reset.

Parabéns! Você acabou de implantar seu primeiro programa Solana no Localhost!

Solana Explorer

Os desenvolvedores também podem configurar o Solana Explorer para usar o ledger local. Acesse o Solana Explorer. Na barra de navegação, clique no botão verde que mostra o cluster atual:

Isso abrirá uma barra lateral na qual você poderá escolher um cluster. Clique em URL de RPC personalizada. O campo deve ser preenchido automaticamente com http://localhost:8899. Caso isso não aconteça, preencha-o para que o explorer aponte para a porta 8899 da sua máquina:

Isso é extremamente valioso por vários motivos:

  • Permite que os desenvolvedores inspecionem transações em seu ledger local em tempo real, reproduzindo os recursos que normalmente teriam com um explorador de blocos analisando a devnet ou a mainnet
  • Facilita a visualização do estado de contas, tokens e programas como se estivessem operando em um cluster ativo
  • Fornece informações detalhadas sobre erros e falhas em transações
  • Oferece uma experiência de desenvolvimento consistente entre clusters por usar uma interface familiar

Implantando na devnet

Embora recomendemos o desenvolvimento no Localhost, os desenvolvedores também podem implantar na devnet se quiserem testar especificamente nesse cluster. O processo é praticamente o mesmo, exceto pelo fato de não ser necessário executar um ledger local, pois já temos um cluster completo da Solana com o qual podemos interagir.

Execute o comando solana config set --url devnet para alterar o cluster selecionado para a devnet. A partir de agora, qualquer comando solana executado no terminal será processado na devnet. Em seguida, no arquivo Anchor.toml, duplique a seção [programs.localnet] e renomeie-a como [programs.devnet]. Altere também [provider] para que aponte para a devnet:

Código
...
[programs.localnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"

[programs.devnet]
hello-world = "EJTW6qsbfya86xeLRQpKLM8qhn11cJXmU35QbJwE11R8"
...
[provider]
cluster = "Devnet"
wallet = '~config/solana/id.json'

Os desenvolvedores precisam garantir que tenham SOL da devnet para implantar o programa. Use o comando solana airdrop <amount> para enviar um airdrop ao local padrão do par de chaves em ~/.config/solana/id.json. Também é possível especificar o endereço de uma carteira usando solana aidrop <amount> <wallet address>. Como alternativa, acesse este faucet para obter SOL da devnet. Recomendo consultar o seguinte guia sobre como obter SOL da devnet.

Isso geralmente ocorre porque o faucet da devnet está sem fundos e/ou porque foi solicitada uma quantidade muito grande de SOL de uma só vez. O limite atual é de 5 SOL, mais do que suficiente para implantar este programa. Portanto, recomenda-se solicitar 5 SOL ao faucet ou executar o comando solana airdrop 5. Solicitar quantidades menores de forma incremental pode resultar em limitação de taxa.

Agora, compile e implante o programa usando os seguintes comandos:

Código
anchor build
anchor deploy

Parabéns! Você acabou de implantar localmente seu primeiro programa Solana na devnet!

Compilando e implantando no Solana Playground

No Solana Playground, acesse o ícone Ferramentas na barra lateral esquerda. Clique em Compilar. Você deverá ver o seguinte no console:

Código
Building...
Build successful. Completed in 2.20s..

Observe que o ID na macro declare_id! foi substituído. Esse novo endereço é onde implantaremos o programa. Agora, clique em Implantar. Você deverá ver algo semelhante a isto no console:

Código

Deploying... This could take a while depending on the program size and network conditions.
Warning: 41 transactions not confirmed, retrying...
Deployment successful. Completed in 17s

Parabéns! Você acabou de implantar seu primeiro programa Solana na devnet por meio do Solana Playground!

Abstração eficaz: IDLs e macros

O Anchor simplifica o desenvolvimento de programas por meio de uma abstração eficaz. Ou seja, ele simplifica conceitos complexos de programação em blockchain, tornando-os mais acessíveis e fáceis de usar. Por exemplo, o Anchor utiliza uma Interface Definition Language (IDL) para definir a interface do programa. Ao compilar um programa, o Anchor gera um arquivo JSON que representa a IDL do programa. Essencialmente, essa estrutura pode ser usada no lado do cliente para definir como interagir com as funções e estruturas de dados do programa. O Anchor também oferece abstrações de alto nível para lidar com o gerenciamento de estado. Ele permite que os desenvolvedores definam o estado do programa usando structs do Rust, o que pode ser mais intuitivo do que trabalhar com arrays de bytes brutos ou serializações manuais. Assim, os desenvolvedores podem definir o estado como fariam normalmente com qualquer estrutura de dados típica do Rust, e o Anchor cuida da serialização subjacente e do armazenamento em contas.

Também é muito simples publicar uma IDL on-chain. Os desenvolvedores podem publicar uma IDL com o seguinte comando:

Código
anchor idl init --filepath   --provider.cluster  --provider.wallet

Verifique se a carteira fornecida é a autoridade do programa e tem SOL suficiente para a transação. Agora, os desenvolvedores podem visualizar sua IDL em um explorador de blocos, como o Orb.

Por exemplo, veja a IDL do agregador v4 da DFlow no Orb.

As macros do Anchor são uma das abstrações mais importantes, talvez a mais importante. No Rust, uma macro é um trecho de código que gera outro trecho de código. Essa é uma forma de metaprogramação. As macros declarativas são a forma de macro mais usada no Rust. Elas permitem que os desenvolvedores escrevam algo semelhante a uma expressão match por meio da construção macro_rules!. As macros procedurais funcionam mais como uma função: recebem um código como entrada, operam sobre ele e produzem uma saída. No Anchor, por exemplo, a macro #[account] define e aplica restrições às contas da Solana. Isso ajuda a reduzir a complexidade e os possíveis erros relacionados ao gerenciamento de contas. Abordar as macros do Anchor inevitavelmente exige uma discussão sobre a estrutura dos programas Anchor.

Estrutura de um programa Anchor

A estrutura dos programas Anchor foi projetada para aproveitar uma combinação de macros e traits a fim de gerar código boilerplate e aplicar a lógica do programa. Essa filosofia de design desempenha um papel importante na simplificação do processo de desenvolvimento e na garantia de consistência e confiabilidade no comportamento do programa.

As declarações use ficam no início do arquivo. Observe que elas fazem parte da semântica geral da linguagem Rust e não são específicas do Anchor. Essas declarações criam um ou mais vínculos de nomes locais equivalentes a outro caminho — as declarações use encurtam o caminho necessário para fazer referência a um item de módulo. Elas podem aparecer em módulos ou blocos. Além disso, a palavra-chave self pode vincular uma lista de caminhos com um prefixo comum e seu módulo pai compartilhado. Por exemplo, todas estas são declarações use válidas: 

Código
use anchor_lang::prelude::*;
use std::collections::hash_map::{self, HashMap};

use a::b::{c, d, e::f, g::h::i};
use a::b::{self, c, d::e};

A primeira macro do Anchor que um desenvolvedor encontrará é declare_id!. Ela é usada para declarar o endereço do programa (o ID do programa), garantindo que todas as interações sejam encaminhadas corretamente ao programa. O Anchor gera um novo par de chaves quando um desenvolvedor compila um programa Anchor pela primeira vez. Esse é o par de chaves usado para implantar o programa, salvo indicação em contrário. A chave pública do par de chaves deve ser fornecida como o ID do programa para a macro declare_id!:

Código
declare_id!("HZfVb1ohL1TejhZNkgFSKqGsyTznYtrwLV6GpA8BwV5Q");

A macro de atributo #[program] indica o módulo de lógica de instruções do programa. Ela funciona como ponto de entrada, definindo como o programa interpreta e executa as instruções recebidas. Essa macro simplifica o encaminhamento dessas instruções para a função apropriada no programa, tornando o código mais organizado e gerenciável. Cada função dentro desse módulo é tratada como uma instrução separada. Cada função recebe como primeiro argumento um parâmetro de contexto (ctx) do tipo Context. Os desenvolvedores podem acessar as contas, o ID do programa em execução e as contas restantes.

O tipo Context é definido como:

Código
pub struct Context<'a, 'b, 'c, 'info, T: Bumps> {
    pub program_id: &'a Pubkey,
    pub accounts: &'b mut T,
    pub remaining_accounts: &'c [AccountInfo<'info>],
    pub bumps: T::Bumps,
}

Isso ajuda a fornecer entradas que não são argumentos para um determinado programa. O campo program_id é do tipo Pubkey e representa o ID do programa atualmente em execução. accounts refere-se às contas serializadas, enquanto remaining_accounts refere-se às contas restantes que foram fornecidas, mas não desserializadas nem validadas — tenha muito cuidado ao usá-lo diretamente. O campo bumps é do tipo Bumps gerado por #[derive(Accounts)]. Ele representa as bump seeds encontradas durante a validação das restrições. Abordaremos as restrições de conta em uma seção posterior. Por enquanto, é importante saber que isso é fornecido por conveniência, para que os handlers não precisem recalcular as bump seeds nem passá-las como argumentos.

Observe que Context é um tipo genérico. No Rust, os genéricos permitem que os desenvolvedores escrevam código flexível e reutilizável que funciona com qualquer tipo de dado. Eles permitem definir tipos para structs, enums, funções e métodos sem especificar o tipo exato com o qual trabalharão. Em vez disso, usa-se um placeholder para esses tipos, geralmente indicado como T. Os genéricos ajudam a reduzir código repetitivo e aumentam a clareza. Por exemplo, é possível definir um enum para conter tipos de dados genéricos:

Código
enum Option<T> {
  Some(T),
  None,
}

O trecho de código acima apresenta o enum Option<T>. Trata-se de um enum padrão do Rust que pode encapsular um valor de qualquer tipo (ou seja, Some(T)) ou nenhum tipo (None).

Para nossos fins, Context é um tipo genérico em que T especifica as contas necessárias para uma instrução (ou seja, qualquer tipo que um desenvolvedor queira criar para armazenar dados). Ao usar Context, os desenvolvedores podem definir T como uma struct que implementa o trait Accounts. Por exemplo, Context<SetData>. Os desenvolvedores podem acessar os campos do tipo Context usando a notação de ponto. Por exemplo, ctx.accounts acessa o campo accounts da struct Context.

Como mencionado, a macro #[account] define tipos de conta personalizados. Nas próximas seções, investigaremos tipos e restrições de conta usando #[account(...)]. Por enquanto, é importante observar que a struct Accounts é onde um desenvolvedor define quais contas uma instrução deve esperar e quais restrições essas contas devem seguir.

Tipos de conta

O tipo Account é usado quando uma instrução precisa acessar os dados desserializados de uma conta. A struct Account é genérica sobre T e é definida como: 

Código
pub struct Account<'info, T: AccountSerialize + AccountDeserialize + Clone> { /* private fields */ }

Ele é um wrapper para AccountInfo que verifica a propriedade do programa e desserializa os dados subjacentes em um tipo Rust. Ele verifica a propriedade do programa de modo que Account.info.owner == T::owner(). Ou seja, verifica se o proprietário dos dados é igual ao ID (criado anteriormente com declare_id!) do crate em que #[account] é usado. Isso significa que o tipo de dado encapsulado por Account (=T) deve implementar o trait Owner. O atributo #[account] implementa o trait para uma struct usando o crate::ID declarado por declare_id! no mesmo programa. Na maioria das vezes, os desenvolvedores podem simplesmente usar o atributo #[account] para adicionar os traits e as implementações necessários aos dados. O atributo #[account] gera implementações para os seguintes traits:

Os 8 bytes iniciais são alocados para um discriminador de conta exclusivo ao implementar traits para serialização de contas. Esse discriminador é determinado pelos primeiros 8 bytes do hash SHA-256 do identificador Rust da conta. Toda chamada ao try_deserialize de AccountDeserialize verificará esse discriminador e encerrará a desserialização da conta com um erro caso uma conta inválida tenha sido fornecida.

Haverá situações em que os desenvolvedores precisarão interagir com programas que não usam Anchor. Nesses casos, eles podem obter todos os benefícios de Account criando seu próprio tipo de wrapper personalizado em vez de usar #[account]. Considere o seguinte trecho de código como exemplo:

Código
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;

// Rest of the program

#[derive(Accounts)]
pub struct SetData<'info> {
    #[account(mut)]
    pub my_account: Account<'info, MyAccount>,
    #[account(
        constraint = my_account.mint == token_account.mint,
        has_one = owner
    )]
    pub token_account: Account<'info, TokenAccount>,
    pub owner: Signer<'info>
}

A maior parte da validação de contas é feita por meio de restrições de conta, que abordaremos na próxima seção. Por enquanto, veja como o tipo TokenAccount é usado para garantir que a conta recebida pertença ao programa de tokens. TokenAccount encapsula a struct Account do programa de tokens e adiciona as funções necessárias. Isso garante que o Anchor possa desserializar a conta e que os desenvolvedores possam usar seus campos nas restrições de conta e na função de instrução.

Observe também, no trecho de código acima, que a macro derive encapsula toda a struct. Isso implementa um desserializador Accounts em SetData e é usado para validar as contas recebidas.

Vários tipos Account podem ser usados na struct de validação de contas, incluindo:

Restrições de conta

As restrições de conta são essenciais para desenvolver programas Anchor seguros. Em artigos futuros, abordaremos com mais profundidade a segurança dos programas Solana e a exploração de programas Anchor. No entanto, é importante abordar as restrições aqui. Elas permitem que os desenvolvedores verifiquem se determinadas contas ou os dados que elas contêm atendem a requisitos predefinidos. Vários tipos de restrição podem ser aplicados usando o atributo #[account(...)], que também pode fazer referência a outras estruturas de dados. O formato é o seguinte:

Código
#[account(constraint goes here)]
pub account: AccountType

Também é importante observar que, dentro da macro Accounts, os desenvolvedores podem acessar os argumentos da instrução usando o atributo #[instruction(...)]. Os argumentos da instrução precisam ser listados na mesma ordem em que aparecem na instrução, mas é possível omitir todos os argumentos posteriores ao último necessário. Por exemplo, na documentação do Anchor: 

Código
...
pub fn initialize(ctx: Context, bump: u8, authority: Pubkey, data: u64) -> anchor_lang::Result<()> {
    ...
    Ok(())
}
...
#[derive(Accounts)]
#[instruction(bump: u8)]
pub struct Initialize<'info> {
    ...
}

As restrições de conta podem ser divididas em restrições normais e restrições SPL. Abordaremos restrições específicas ao longo do restante deste artigo. Nesses exemplos, <expr> representa uma expressão arbitrária que pode ser fornecida, desde que seja avaliada como um valor do tipo esperado. Por exemplo, owner = token_program.key().

Análise das restrições de um programa

Recomendo consultar a documentação do Anchor sobre contas para ver uma lista mais abrangente das possíveis restrições. Seria trabalhoso demais percorrer cada restrição e fornecer uma definição formal em algum tipo de tabela. Para nossos fins, é mais útil analisar o programa a seguir para entender as restrições de conta em ação:

Código
use anchor_lang::prelude::*;
#[cfg(not(feature = "no-entrypoint"))]
use {default_env::default_env, solana_security_txt::security_txt};

declare_id!("fanqeMu3fw8R4LwKNbahPtYXJsyLL6NXyfe2BqzhfB6");

pub mod errors;
pub mod instructions;
pub mod state;

pub use instructions::*;
pub use state::*;

#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
  name: "Fanout",
  project_url: "http://helium.com",
  contacts: "email:hello@helium.foundation",
  policy: "https://github.com/helium/helium-program-library/tree/master/SECURITY.md",

  // Optional Fields
  preferred_languages: "en",
  source_code: "https://github.com/helium/helium-program-library/tree/master/programs/fanout",
  source_revision: default_env!("GITHUB_SHA", ""),
  source_release: default_env!("GITHUB_REF_NAME", ""),
  auditors: "Sec3"
}

#[program]
pub mod fanout {
  use super::*;

  pub fn initialize_fanout_v0(
    ctx: Context<InitializeFanoutV0>,
    args: InitializeFanoutArgsV0,
  ) -> Result<()> {
    instructions::initialize_fanout_v0::handler(ctx, args)
  }

  pub fn stake_v0(ctx: Context<StakeV0>, args: StakeArgsV0) -> Result<()> {
    instructions::stake_v0::handler(ctx, args)
  }

  pub fn unstake_v0(ctx: Context<UnstakeV0>) -> Result<()> {
    instructions::unstake_v0::handler(ctx)
  }

  pub fn distribute_v0(ctx: Context<DistributeV0>) -> Result<()> {
    instructions::distribute_v0::handler(ctx)
  }
}

Este é o programa Fanout da Helium. Trata-se de um programa razoavelmente complexo para distribuir tokens proporcionalmente aos titulares de acordo com suas participações. Neste momento, o projeto não parece tão útil para nós, pois não há restrições. No entanto, se analisarmos a struct StakeV0 da instrução stake_v0, encontraremos várias restrições para explorar.

mut

A primeira restrição nessa instrução é a restrição de conta mut. mut é definida como #[account(mut)] ou #[account(mut @ <custom_error>)], com suporte a erros personalizados por meio da notação @. Essa restrição verifica se uma conta é mutável e faz com que o Anchor persista todas as alterações de estado. No programa da Helium, a restrição garante que a conta payer seja mutável:

Código
...
pub struct StakeV0<'info> {
  #[account(mut)]
  pub payer: Signer<'info>,
  pub staker: Signer<'info>,
  /// CHECK: Just needed to receive nft
  pub recipient: AccountInfo<'info>,
...

has_one

A restrição has_one é definida como #[account(has_one = <target_account)] ou #[account(has_one = <target_account> @ <custom_error>)]. Ela verifica o campo target_account para determinar se a conta corresponde à chave do campo target_account na struct Accounts. Há suporte a erros personalizados por meio da anotação @.

No contexto da struct StakeV0, a restrição has_one é usada para verificar se a conta possui membership_mint, token_account e membership_collection:

Código
...
#[account(
  mut,
  has_one = membership_mint,
  has_one = token_account,
  has_one = membership_collection
)]
pub fanout: Box<Account<'info, FanoutV0>>,
pub membership_mint: Box<Account<'info, Mint>>,
pub token_account: Box<Account<'info, TokenAccount>>,
pub membership_collection: Box<Account<'info, Mint>>,
...

Observe que há várias restrições has_one e que a restrição mut também está sendo usada. Com as restrições de conta, várias restrições podem ser aplicadas simultaneamente a uma conta.

seeds, bump

As restrições seeds e bump são usadas para verificar se uma determinada conta é uma PDA derivada do programa atualmente em execução, das seeds e, se fornecido, do bump:

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

Se o bump não for fornecido, o Anchor usará o bump canônico. Seeds::program = <expr> pode ser usado para derivar a PDA de um programa diferente daquele que está sendo executado.

No programa Fanout da Helium, a restrição seeds verifica se o texto “metadata”, a chave token_metadata_program, a chave membership_collection e o texto “edition” são seeds usadas para derivar essa PDA. A restrição seeds::program garante que token_metadata_program seja usado para derivar a PDA, em vez do programa atual:

Código
...
#[account(
  mut,
  seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), membership_collection.key().as_ref()],
  seeds::program = token_metadata_program.key(),
  bump,
)]
pub collection_metadata: UncheckedAccount<'info>,
...

token::mint, token::authority

As restrições token::mint e token::authority são definidas da seguinte forma:

  • #[account(token::mint = <target account>, token::authority = <target account>)]
  • #[account(token::mint = <target account>, token::authority = <target account>, token::token_program = <target account>)]

As restrições de token mint e authority são usadas para verificar o endereço de mint e a autoridade de um TokenAccount. Essas restrições podem ser usadas como verificação ou com a restrição init para criar uma conta de token com o endereço de mint e a autoridade fornecidos. Quando usadas como verificação, é possível especificar apenas um subconjunto das restrições. 

No contexto do programa da Helium, essas restrições são usadas para verificar se o mint de associated_token é igual a membership_mint e se a autoridade do token está definida como staker:

Código
...
#[account(
  mut,
  associated_token::mint = membership_mint,
  associated_token::authority = staker,
)]
pub from_account: Box<Account<'info, TokenAccount>>,
...

init, payer, space

Neste ponto, faz sentido avançar um pouco no código para analisar as restrições init, payer e space. A restrição init é definida como [#account(init, payer = <target_account>, space = <num_bytes>)]. Essa restrição cria a conta por meio de uma CPI para o System Program e a inicializa definindo seu discriminador de conta. Isso marcará a conta como mutável e é mutuamente exclusivo com mut. Para contas maiores que 10 kibibytes, use #[account(zero)].

A restrição init deve ser usada com algumas restrições adicionais. Ela exige a restrição payer, que especifica a conta que pagará pela criação da conta. Também exige que o System Program esteja presente na struct e seja chamado system_program. A restrição space também deve ser definida. Na seção Espaço da conta, analisaremos essa restrição e os requisitos de espaço em mais detalhes.

No programa Fanout da Helium, o comando init cria uma nova conta. payer é definido como payer, estabelecido anteriormente na struct como pub payer: Signer<'info>. O espaço da conta é definido como o tamanho de FanoutVoucherV0, mais 8 bytes para o discriminador e 61 bytes adicionais de espaço:

Código
...
#[account(
  init,
  payer = payer,
  space = 60 + 8 + std::mem::size_of::<FanoutVoucherV0>() + 1,
  seeds = ["fanout_voucher".as_bytes(), mint.key().as_ref()],
  bump,
)]
pub voucher: Box<Account<'info, FanoutVoucherV0>>,
...

init_if_needed

A restrição init_if_needed é definida como #[account(init_if_nedded, payer = <target_Account>)] ou #[account(init)if_needed, payer = <target_account>, space = <num_bytes>)]. Essa restrição tem exatamente a mesma funcionalidade que init. No entanto, ela só é executada se a conta ainda não existir. Se a conta existir, init_if_needed ainda verificará se todas as restrições de inicialização foram atendidas, como a alocação da quantidade correta de espaço para a conta ou as seeds corretas no caso de uma PDA.

init_if_needed deve ser usado com cautela, pois fica protegido por uma feature flag devido aos possíveis riscos. Para habilitá-lo, importe anchor-lang com a feature init-if-needed do cargo. Ao usar init_if_needed, é essencial se proteger contra ataques de reinicialização. Os desenvolvedores devem garantir que o código inclua verificações para impedir que a conta seja redefinida ao estado inicial após sua inicialização, a menos que esse comportamento seja intencional. Manter os caminhos de execução das instruções simples é considerado uma prática recomendada para mitigar esses ataques. Considere dividir as instruções em uma para inicialização e outras para as operações posteriores.

O programa Fanout da Helium usa a restrição init_if_needed para inicializar recipient_account, caso a conta ainda não exista:

Código
...
#[account(
  init_if_needed,
  payer = payer,
  associated_token::mint = mint,
  associated_token::authority = recipient,
)]
pub receipt_account: Box<Account<'info, TokenAccount>>,
...

constraint

A restrição constraint é definida como #[account(constraint = <expr>)] ou #[account(constraint = <expr> @ <custom_error>)]. Ela verifica se a expressão fornecida é avaliada como verdadeira. Isso é útil quando nenhuma outra restrição atende ao caso de uso desejado. Ela também oferece suporte a erros personalizados por meio da anotação @.

O programa Fanout usa constraint para verificar se o supply do mint está definido como zero:

Código
...
#[account(
  mut,
  constraint = mint.supply == 0,
  mint::decimals = 0,
  mint::authority = voucher,
  mint::freeze_authority = voucher,
)]
pub mint: Box<Account<'info, Mint>>,
...

mint::authority, mint::decimals, mint::freeze_authority 

No trecho de código acima, as restrições mint::decimals, mint::authority e mint::freeze_authority são usadas para verificar se as casas decimais do mint estão definidas como zero e se voucher possui autoridade e autoridade de congelamento.

Para contextualizar, as restrições mint::authority, mint::decimals e mint::freeze_authority são definidas como:

  • #[account(mint::authority = <target account>, mint::decimals = <expr>)]
  • #[account(mint::authority = <target account>, mint::decimals = <expr>, mint::freeze_authority = <target account>)]

Essas restrições são autoexplicativas — ou seja, verificam, respectivamente, a autoridade, as casas decimais e a autoridade de congelamento do token. Elas podem ser usadas como verificação ou com init para criar uma conta de mint com as casas decimais e a autoridade de mint fornecidas. A autoridade de congelamento é totalmente opcional quando usada com init. Quando usadas como verificação, é possível especificar apenas um subconjunto dessas restrições.

Espaço da conta 

Cada conta usada por um programa na Solana deve ter seu espaço de armazenamento alocado explicitamente. Essa alocação é crucial para o gerenciamento eficiente de recursos, garantindo que apenas os dados necessários sejam armazenados on-chain. Isso também contribui para custos de transação previsíveis e aumenta a eficiência da execução das transações, que podem ser processadas sem a necessidade de alocar ou redimensionar dinamicamente o armazenamento da conta. Além disso, a pré-alocação de dados garante que a conta tenha espaço suficiente para armazenar todos os dados necessários, reduzindo o risco de falhas nas transações ou possíveis vulnerabilidades de segurança.

Dimensionamento de variáveis

Diferentes tipos de dados têm diferentes requisitos de espaço. Veja um guia simplificado para ajudar a estimar esses requisitos:

  • Tipos básicos: tipos de dados simples como bool, u8, i8, u16, i16, u32, i32, u64, i64, u128 e i128 têm tamanhos fixos. Eles variam de 1 byte para um bool (embora use apenas 1 bit) a 16 bytes para u128 / i128
  • Arrays: para um array [T;amount], o espaço é calculado como o tamanho de T multiplicado pelo número de elementos (ou seja, amount). Por exemplo, um array de 16 u16 exigiria 32 bytes
  • Pubkey: uma chave pública sempre ocupa 32 bytes na Solana
  • Tipos dinâmicos: String e Vec<T> exigem atenção especial. Ambos precisam de 4 bytes para armazenar seu comprimento, além do espaço para o conteúdo em si. É essencial alocar espaço suficiente para o tamanho máximo esperado. Para uma String, isso corresponde a 4 bytes mais o comprimento da String em bytes. Para um Vec<T>, corresponde a 4 bytes mais o espaço do tipo fornecido multiplicado pelo número esperado de elementos (ou seja, 4 + space(T) * amount)
  • Options e Enums: um tipo Option<T> exige 1 byte mais o espaço para o tipo T. Enums exigem 1 byte para o discriminador do enum mais o espaço necessário para a maior variante
  • Pontos flutuantes: tipos como f32 e f64 ocupam 4 e 8 bytes, respectivamente. Tenha cuidado com valores NaN, pois eles podem causar falhas na serialização

O guia a seguir se aplica apenas a contas que não usam a serialização zero-copy. A serialização zero-copy é indicada pelo atributo #[zero_copy]. Ela utiliza o atributo repr(c) para o layout de memória, permitindo conversões diretas de ponteiros para acessar os dados. Essa é uma maneira eficiente de trabalhar com dados on-chain sem o overhead da desserialização tradicional. #[zero_copy] é uma forma abreviada de aplicar #[derive(Copy, Clone)], #[derive(bytemuck::Zeroable)], #[derive(bytemuck::Pod)] e #[repr(C)]. Esses atributos garantem que a conta possa ser tratada com segurança como uma sequência de bytes e seja compatível com a desserialização zero-copy. A desserialização zero-copy é crucial para contas que exigem tamanhos significativamente grandes — contas que não podem ser serializadas com eficiência usando Borsh ou os mecanismos de serialização padrão do Anchor sem atingir os limites de heap ou stack.

Discriminador interno do Anchor

Os desenvolvedores devem adicionar 8 à restrição space para o discriminador interno do Anchor. Por exemplo, se uma conta exigir 32 bytes, ela precisará de 40. É considerada uma boa prática definir a restrição de espaço como space = 8 + <account size> para deixar claro que o discriminador interno está sendo considerado no cálculo do espaço.  

Como observação, um discriminador é um identificador exclusivo usado para distinguir diferentes tipos de dados. Isso é útil para diferenciar diversos tipos de estruturas de dados de contas durante a execução. Ele também é usado como prefixo de instruções, ajudando a encaminhá-las para seus métodos correspondentes em um programa Anchor. O discriminador é um array de 8 bytes que representa o identificador exclusivo do tipo de dado.

Cálculo do espaço inicial

Calcular o requisito inicial de espaço para uma conta pode ser desafiador. A macro InitSpace adiciona uma constante INIT_SPACE que pode ser usada na estrutura da conta. Não é necessário que a estrutura contenha a macro #[account] para gerar a constante. A documentação do Anchor apresenta o seguinte exemplo:

Código
#[account]
#[derive(InitSpace)]
pub struct ExampleAccount {
  pub data: u64,
  // max_len represents the length of the structure
  #[max_len(50)]
  pub string_one: String,
  #[max_len(10, 5)]
  pub nested: Vec<Vec<u8>>,
}

#[derive(Accounts)]
pub struct Initialize<'info> {
  #[account(mut)]
  pub payer: Signer<'info>,
  pub system_program: Program<'info, System>,
  #[account(init, payer = payer, space = 8 + ExampleAccount::INIT_SPACE)]
  pub data: Account<'info, ExampleAccount>,
}

Neste exemplo, ExampleAccount::INIT_SPACE calcula automaticamente o espaço necessário para ExampleAccount. Ele também considera o discriminador interno do Anchor no cálculo do espaço.

Redimensionamento do espaço do programa

A restrição realloc é usada para ajustar o espaço de uma conta de programa no início de uma instrução. Ela exige que a conta seja mutável (ou seja, mut) e se aplica aos tipos Account ou AccountLoader. Ela é definida como #[account(realloc = <space>, realloc::payer = <target>, realloc::zero = <bool>)]. Ao aumentar o comprimento dos dados da conta, lamports são transferidos de realloc::payer para a conta do programa a fim de manter a isenção de aluguel. Se o comprimento dos dados diminuir, os lamports são transferidos de volta da conta do programa para realloc::payer. A restrição realloc::zero determina se a memória recém-alocada deve ser inicializada com zeros. Essa inicialização garante que a nova memória esteja limpa e livre de dados residuais ou indesejados.

Não é recomendável usar AccountInfo::realloc manualmente em vez da restrição realloc. Isso se deve à ausência de verificações durante a execução que garantam que a realocação não ultrapasse o limite MAX_PERMITTED_DATA_INCREASE, o que poderia sobrescrever dados de outras contas. A restrição também verifica e impede realocações repetidas dentro de uma única instrução.

Por exemplo:

Código
#[derive(Accounts)]
pub struct Data {
#[account(mut)]
pub payer: Signer<'info>,
  #[account(
    mut,
    seeds = [b"data"],
    bump,
    realloc = 8 + std::mem::size_of::<()>() + 48,
    realloc::payer = payer,
    realloc::zero = false
  )]
  pub update_account: Account<'info, NewData>,
  system_program: Program<'info, System>,
}

Erros

O tratamento de erros é um aspecto essencial do desenvolvimento de programas. É um mecanismo para identificar e gerenciar erros que podem interromper a execução de um programa. O tratamento de erros deve ser deliberado e planejado para garantir a qualidade, a manutenção e a funcionalidade do código. O Anchor simplifica isso com mecanismos robustos de tratamento de erros. Os erros em programas Anchor podem ser divididos em AnchorErrors e erros que não são do Anchor. Esta seção se concentrará em AnchorErrors, pois os erros que não são do Anchor abrangem uma grande variedade de erros do Rust. Para erros que não são do Anchor, recomendo consultar o capítulo do Rust Book sobre tratamento de erros e a seção do Rust By Example sobre tratamento de erros.

O seguinte struct define AnchorError:

Código
pub struct AnchorError {
  pub error_name: String,
  pub error_code_number: u32,
  pub error_msg: String,
  pub error_origin: Option<ErrorOrigin>,
  pub compared_values: Option<ComparedValues>,
}

Esses campos são relativamente simples. error_name é uma string que representa o nome do erro. error_code_number é um identificador exclusivo (ou seja, um inteiro sem sinal exclusivo que ocupa 32 bits de espaço) para o erro. error_msg é uma mensagem descritiva que explica o erro. error_origin é um campo opcional que fornece informações sobre a origem do erro, como o arquivo-fonte ou a conta envolvida. compared_values é um campo opcional que detalha os valores comparados quando o erro ocorreu. Isso é extremamente útil para depuração.

AnchorError implementa um método de log. Ele inclui informações sobre a origem do erro e os valores envolvidos, o que é útil para depuração e resolução de erros. Esse método usa error_origin e compared_values para fornecer essas informações.

Os AnchorErrors podem ser subdivididos em erros internos do Anchor e erros personalizados. O Anchor tem uma longa lista de códigos de erro internos que podem ser retornados. Esses erros internos não devem ser usados pelos usuários. No entanto, é útil conhecer as correspondências entre os códigos e suas causas. Eles geralmente são lançados quando uma restrição é violada. Os códigos de erro internos seguem este esquema:

  • >= 100 são códigos de erro de instrução
  • >= 1000 são códigos de erro de IDL
  • >= 2000 são códigos de erro de restrição
  • >= 3000 são códigos de erro de conta
  • >= 4100 são códigos de erro diversos
  • = 5000 são códigos de erro obsoletos.

Os erros personalizados começam em ERROR_CODE_OFFSET (ou seja, 6000).

Os desenvolvedores podem implementar seus próprios erros personalizados usando o atributo error_code. Esse atributo é usado em um enum, e as variantes do enum podem ser usadas como erros em todo o programa. Uma mensagem pode ser adicionada a cada variante. O cliente pode exibir essa mensagem se o erro ocorrer. Por exemplo:

Código
#[error_code]
pub enum HeliusError {
  #[msg(“This RPC provider is too good”)]
  RPCTooGood
}

As macros err! e error! podem ser usadas para lançar esses erros. Por exemplo:

Código
require!(rpc.speed > 9000, HeliusError::RPCTooGood);

É fundamental observar que há várias macros require disponíveis. A grande maioria dessas macros trata de valores que não são chaves públicas. Por exemplo, a macro require_gte verifica se o primeiro valor que não é uma chave pública é maior ou igual ao segundo valor que não é uma chave pública:

Código
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
    require_gte!(ctx.accounts.data.data, 1);
    ctx.accounts.data.data = data;
    Ok(());
}

Também há algumas ressalvas ao comparar chaves públicas. Por exemplo, os desenvolvedores devem usar require_keys_eq em vez de require_eq, pois o último é mais caro.

Todos os programas retornarão um ProgramError. Esse tipo de erro inclui um campo específico para um número de erro personalizado, que o Anchor usa para armazenar seus códigos de erro internos e personalizados. No entanto, trata-se apenas de um número, então ele não é tão útil. O registro mencionado anteriormente feito pelo Anchor com AnchorErrors é muito mais útil. Os clientes Anchor são projetados para analisar esses logs. Porém, há cenários em que isso pode ser difícil. Por exemplo, recuperar logs de transações processadas com as verificações de preflight desativadas não é tão simples. Da mesma forma, o Anchor também emprega um mecanismo de fallback para programas que não são do Anchor ou programas legados que não registram AnchorErrors da maneira padrão. Nesse caso, o Anchor verifica se o número de erro retornado pela transação corresponde a um código de erro interno do Anchor ou a um número de erro definido na IDL do programa. Quando encontra uma correspondência, o Anchor enriquece as informações do erro para fornecer mais contexto. Sempre que possível, o Anchor também tenta analisar a pilha de erros do programa para rastrear a causa original. ProgramError funciona como um tipo de erro fundamental, cuja utilidade é ampliada pelos mecanismos de registro e análise do Anchor para fornecer informações detalhadas sobre erros.

Invocações entre programas (CPIs)

As invocações entre programas (CPIs) foram mencionadas ao longo deste artigo, portanto é justo dedicar uma seção a elas. As CPIs são fundamentais para a composibilidade da Solana, pois permitem que programas chamem outros programas diretamente. Para os desenvolvedores, isso transforma o ecossistema da Solana em uma API ampla e interconectada. Para ser breve, recomendo ler a documentação do Anchor sobre CPIs, que apresenta um exemplo útil de CPIs em ação com um programa de marionete e mestre de marionetes.

Ainda assim, uma CPI pode ser definida como uma chamada de um programa para outro, direcionada a uma instrução específica no programa chamado. O programa que faz a invocação fica suspenso até que o programa invocado termine de processar a instrução. 

Elevação de privilégios

As CPIs permitem que um programa chamador estenda seus privilégios de signatário ao programa chamado. A extensão de privilégios é conveniente, mas pode ser muito perigosa. Se uma CPI acidentalmente apontar para um programa malicioso, esse programa obterá os mesmos privilégios do chamador. O Anchor reduz esse risco com duas proteções:

  • O tipo Program<’info, T> garante que a conta especificada corresponda ao programa esperado (T)
  • Mesmo que o tipo Program não seja usado, a função de CPI gerada automaticamente verificará se o argumento cpi_program corresponde ao programa esperado

Execução de uma CPI

Um programa pode executar uma CPI usando invoke ou invoke_signed do crate solana_program. O Anchor também fornece a struct CpiContext para especificar entradas que não são argumentos para CPIs.

invoke

A função invoke é usada quando um PDA não precisa funcionar como assinatura. Nesse caso, o runtime estende a assinatura original do programa chamador ao programa chamado. A função é definida como:

Código
pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
) -> ProgramResult

Invocar outro programa envolve criar uma Instruction que inclua o ID do programa, os dados da instrução para o programa chamado e uma lista das contas que ele acessará. Um programa só recebe valores AccountInfo do runtime em seu ponto de entrada. Toda conta necessária ao programa chamado para sua invocação deve ser incluída e fornecida pelo programa que o chama. Por exemplo, se o programa chamado precisar modificar uma conta específica, o programa chamador deverá incluir essa conta na lista de valores AccountInfo. Isso também se aplica ao ID do programa chamado (ou seja, o chamador deve especificar explicitamente qual programa está chamando ao incluir o ID do programa chamado).

A Instruction normalmente é construída dentro do programa chamador, embora possa ser desserializada a partir de uma saída externa.

A transação inteira falhará imediatamente se o programa chamado encontrar um erro ou for abortado. Isso ocorre porque a função invoke só retorna em caso de sucesso. Use as funções set_return_data ou get_return_data para retornar dados como resultado de uma CPI. Observe que o tipo retornado deve implementar os traits AnchorSerialize e AnchorDeserialize. Como alternativa, faça o programa chamado gravar os dados em uma conta dedicada

Embora um programa possa chamar a si mesmo recursivamente, chamadas recursivas indiretas (ou seja, reentrância) por outro programa farão a transação falhar imediatamente.

Por exemplo, se tivéssemos um programa que transferisse tokens por CPI, usaríamos invoke assim:

Código
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
    require_gte!(ctx.accounts.data.data, 1);
    ctx.accounts.data.data = data;
    Ok(());
}

invoke_signed

invoke_signed é usado para CPIs que exigem um PDA como signatário. Ele permite que um programa chamador atue em nome de um PDA fornecendo as seeds necessárias para derivá-lo:

Código
pub fn invoke_signed(
	instruction: &Instruction,
	account_infos: &[AccountInfo<'_>],
  signers_seeds: &[&[&[u8]]]
) -> ProgramResult

PDAs também podem atuar como signatários em uma CPI. O runtime usará as seeds fornecidas e o program_id do programa chamador para gerar o PDA internamente por meio de create_program_address. Em seguida, o PDA é validado em relação aos endereços passados com a instrução (ou seja, account_infos) para confirmar que é um signatário válido.

Com essa função, uma invocação pode assinar em nome de um ou mais PDAs controlados pelo programa chamador. Isso permite que o programa chamado interaja com as contas fornecidas como se elas tivessem sido assinadas criptograficamente. signer_seeds consiste em slices de seeds usados para derivar o PDA. Durante a invocação, o runtime considera como “assinada” qualquer conta correspondente em account_info. Por exemplo, se tivéssemos um programa que criasse uma conta para um PDA, chamaríamos invoke_signed assim:

Código
invoke_signed(
	&system_instruction::create_account(
  	&payer.key,
  	&vault_pda.key,
  	lamports,
  	vault_size,
  	&program_id,
  ),
  &[
  	payer.clone(),
  	vault_pda.clone(),
  ],
  &[
  	&[
  		b"vault",
  		payer.key.as_ref(),
  		&[vault_bump_seed],
  	],
  ]
)?;

CpiContext

O Anchor fornece CpiContext como uma maneira mais simples de fazer CPIs, em vez de usar invoke ou invoke_signed. Essa struct especifica as entradas necessárias que não são argumentos para CPIs, refletindo de perto a funcionalidade de Context. Ela fornece informações sobre as contas necessárias para a instrução, quaisquer contas adicionais envolvidas, o ID do programa invocado e as seeds para derivar PDAs, se necessário. Use CpiContext::new para CPIs sem PDAs e CpiContext::new_with_signer para CPIs que exigem PDAs signatários.

CpiContext é definido da seguinte forma, sendo T um tipo genérico que abrange qualquer objeto que implemente os traits ToAccountMetas e ToAccountInfos<’info>:

Código
pub struct CpiContext<'a, 'b, 'c, 'info, T>where
    T: ToAccountMetas + ToAccountInfos<'info>,{
    pub accounts: T,
    pub remaining_accounts: Vec>,
    pub program: AccountInfo<'info>,
    pub signer_seeds: &'a [&'b [&'c [u8]]],
}

Accounts é um tipo genérico, permitindo qualquer objeto que implemente os traits ToAccountMetas e ToAccountInfos<’info>. Isso é possibilitado pela macro de atributo #[derive(Accounts)] para facilitar a organização do código e aumentar a segurança de tipos. 

CpiContext simplifica a invocação de programas Anchor e que não são do Anchor. Para programas Anchor, basta declarar uma dependência no arquivo Cargo.toml do projeto e usar o módulo cpi gerado pelo Anchor:

Código
[dependencies]
callee = { path = "../callee", features = ["cpi"]}

Definir features = [“cpi”] concede ao programa acesso ao módulo callee::cpi. O Anchor gera esse módulo automaticamente e expõe as instruções do programa como uma função Rust. Essa função recebe um CpiContext e quaisquer dados adicionais da instrução, seguindo o formato das funções de instrução regulares em programas Anchor, mas substituindo Context por CpiContext. O módulo cpi também fornece as structs de conta necessárias para chamar as instruções.

Por exemplo, se o programa chamado tiver uma instrução chamada hello_there que exija contas específicas definidas na struct GeneralKenobi, invoque-a da seguinte forma:

Código
// We assume "jedi" is an Anchor program with a published crate
use jedi::cpi::accounts::GeneralKenobi;
use jedi::cpi::hello_there;
use anchor_lang::prelude::*;

#[program]
pub mod fight_on_utapau {
use super::*;

pub fn call_hello_there(ctx: Context<CallGeneralKenobi>, data: GreetingParams) -> Result<()> {
	let cpi_accounts = GeneralKenobi {
		jedi: ctx.accounts.jedi.to_account_info(),
		// Other account infos needed for the GeneralKenobi struct go here
	};

	let cpi_program = ctx.accounts.jedi_program.to_account_info();
	let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

	hello_there(cpi_ctx, data);
}

#[derive(Accounts)]
pub struct CallGeneralKenobi<'info> {
	pub jedi: UncheckedAccount<'info>,
	pub jedi_program: Program<'info, Jedi>,
	// Other required accounts
}

pub struct GreetingParams {
	// Params required for the hello_there function
}

No módulo fight_on_utapau, uma CPI é executada usando CpiContext. A função call_hello_there foi projetada para interagir com o programa jedi. Ela cria um CpiContext com as informações de conta necessárias para a struct de conta GeneralKenobi do programa jedi e as informações da conta do programa jedi. Esse contexto invoca hello_there, passando quaisquer parâmetros adicionais necessários especificados pela struct GreetingParams. A struct CallGeneralKenobi define as contas necessárias para essa função, simplificando o processo.

Por fim, ao invocar instruções de programas que não são do Anchor, verifique se os mantenedores do programa publicaram seu próprio crate com funções auxiliares para chamar o programa. Se não houver funções auxiliares para o programa cujas instruções precisam ser invocadas, recorra a invoke e invoke_signer para organizar e preparar as CPIs.

Endereços derivados de programa (PDAs)

Lembre-se: os PDAs estão fora da curva e não têm uma chave privada associada. Eles permitem que programas assinem instruções e que desenvolvedores criem estruturas semelhantes a hashmaps on-chain. Um PDA é derivado usando uma lista de seeds opcionais, uma bump seed e um ID de programa. 

Reiterando, as seguintes restrições são usadas para verificar se uma determinada conta é um PDA derivado do programa em execução no momento, das seeds e, se fornecido, do bump:

  • #[account(seeds = <seeds>, bump)]
  • #[account(seeds = <seeds>, bump, seeds::program = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>)]
  • #[account(seeds = <seeds>, bump = <expr>, seeds::program = <expr>)]

Se o bump não for fornecido, o Anchor usará o bump canônico. Seeds::program = <expr> pode ser usado para derivar o PDA de um programa diferente daquele que está em execução no momento.

O uso das restrições seeds e bump simplifica o processo de derivação:

Código
#[derive(Accounts)]
struct ExamplePDA<'info> {
	#[account(seeds = [b"example"], bump)]
	pub example_pda: Account<'info, AccountType>,
}

Aqui, a restrição seeds é usada para derivar o PDA. O Anchor verifica automaticamente se a conta passada para a instrução corresponde ao PDA derivado das seeds. O Anchor usa o bump canônico por padrão quando a restrição de bump é usada sem um valor específico.

O Anchor também permite seeds dinâmicas baseadas em outros campos da conta ou nos dados da instrução. Isso é feito referenciando outros campos dentro da struct ou usando a macro de atributo #[instruction(...)] para incluir dados desserializados da instrução. Por exemplo, na struct a seguir, example_pda é restrito a usar uma combinação de uma seed estática, dados da instrução e a chave pública do signatário:

Código
#[derive(Accounts)]
#[instruction(instruction_data: String)]
pub struct ExamplePDA<'info> {
	#[account(seeds = [b"example", signor.key().as_ref(), instruction_data.as_bytes()], bump)]
 	pub example_pda: Account<'info, AccountType>,
	#[account(mut)]
	pub signoooorrr: Signer<'info>
}

Conclusão

Dizer que o Anchor é um framework poderoso seria pouco. Sua capacidade de simplificar o processo de desenvolvimento fica evidente ao explorarmos as diversas macros e traits que o Anchor utiliza para reduzir o código. Ele conta com uma documentação bem mantida e um ecossistema robusto de tutoriais e crates relacionados. O Anchor é utilizado e adorado pela grande maioria dos desenvolvedores da Solana.

Este artigo é um guia muito, muito abrangente sobre o desenvolvimento de programas com o Anchor. Abordamos a instalação do Anchor, o uso do Solana Playground e a criação, compilação e implantação de um programa Hello, World!. Em seguida, exploramos os métodos de abstração eficiente do Anchor, a estrutura de um programa Anchor típico e os diversos tipos de conta e restrições disponíveis. Também abordamos a importância de alocar espaço para contas e tratar erros. Por fim, exploramos CPIs e PDAs. Este é o artigo sobre Anchor — ele tem tudo o que você precisa para começar a desenvolver programas na Solana hoje mesmo.

Se você leu até aqui, valeu, anônimo! Insira seu endereço de e-mail abaixo para nunca perder uma atualização sobre as novidades da Solana. Quer se aprofundar? Entre no nosso Discord para começar a desenvolver programas com o Anchor.

Recursos adicionais

Assine a Helius

Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos

Imagem ampliada