Introdução
Em 2024, a Solana ganhou destaque, tornando-se a quarta blockchain pública em classificação por Total Value Locked (TVL), capturando o interesse de investidores e desenvolvedores.
A BlockSec criou a série "Solana Simplificado", que inclui artigos sobre os conceitos básicos da Solana, tutoriais sobre como escrever contratos inteligentes na Solana e guias para analisar transações na Solana. O objetivo é ajudar os leitores a entender o ecossistema da Solana e dominar habilidades essenciais para desenvolver projetos e realizar transações na Solana.
No primeiro artigo desta série, aprofundamos os conceitos-chave da rede Solana, incluindo seu mecanismo de operação, modelo de contas e transações, estabelecendo uma base sólida para escrever contratos Solana corretos e de alto desempenho.
Neste artigo, vamos guiá-lo na escrita de um programa Solana (ou seja, um contrato inteligente Solana) para publicar e exibir artigos. Isso ajudará a consolidar os conceitos do primeiro artigo e apresentará algumas funcionalidades da Solana que ainda não discutimos. O programa relevante e o código de teste foram publicados no GitHub.
Configurar o Ambiente
Nota: Os comandos a seguir cobrem apenas o sistema Ubuntu. Alguns comandos podem não funcionar nos sistemas Windows e macOS.
👉 Você pode usar comandos alternativos ou consultar Configurar o ambiente de desenvolvimento local e instalar a CLI da Solana para resolver isso.
Vamos compilar e implantar programas Solana no ambiente local. Antes de mergulhar no desenvolvimento de programas Solana, precisamos instalar alguns comandos e dependências.
Rust
Os programas Solana são predominantemente escritos na linguagem de programação Rust, portanto, precisamos executar o seguinte comando para instalar o Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
node.js & TypeScript
O script de teste é escrito em TypeScript, portanto, precisamos instalar o Node.js e a cadeia de ferramentas TypeScript:
curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash -
sudo apt-get install -y nodejs npm
npm install -g typescript
npm install -g ts-node
CLI da Solana
Em seguida, precisamos instalar a ferramenta CLI da Solana, que fornece comandos para tarefas como criar carteiras e implantar contratos:
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"
Execute o seguinte comando para aplicar as alterações ao terminal atual:
source ~/.profile
Você pode executar solana --version para verificar se a CLI da Solana foi instalada com sucesso:
$ solana --version
solana-cli 1.17.25 (src:d0ed878d; feat:3580551090, client:SolanaLabs)
Carteira Local
Em seguida, execute o seguinte comando para gerar uma carteira no sistema de arquivos local:
solana-keygen new
Por padrão, este comando cria um arquivo de chave privada em ~/.config/solana/id.json, que usaremos no script de teste mais tarde. Você pode usar o comando solana address para ver o endereço gerado por ele:
$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy
Devnet
O programa será implantado na devnet, portanto, precisamos executar o seguinte comando para alternar a CLI da Solana para a devnet:
solana config set --url https://api.devnet.solana.com
Tokens SOL
Como a implantação de contratos e o envio de transações de teste não são gratuitos, precisamos solicitar alguns tokens SOL. Você pode executar solana airdrop 2 ou solicitar tokens diretamente pelo faucet público na web.
Escrever o Programa
O programa que vamos apresentar permite que os usuários publiquem artigos e listem todos os artigos publicados atualmente. Ele lida com três tipos de instruções:
- Instrução init: Inicializa o programa criando uma conta de dados para armazenar o índice máximo atual de artigos.
- Instrução post: Armazena um artigo publicado em uma nova conta de dados.
- Instrução list: Imprime todos os artigos publicados no log.
Abaixo está a estrutura do programa:
$ tree program/
program/
├── Cargo.lock
├── Cargo.toml
└── src
├── instructions
│ ├── init.rs
│ ├── list.rs
│ ├── mod.rs
│ └── post.rs
├── lib.rs
└── processor.rs
O Cargo.toml especifica as bibliotecas externas para o projeto:
[dependencies]
borsh = "0.10.0"
borsh-derive = "0.10.0"
solana-program = "=1.17.25"
Nota: A versão do solana-program deve corresponder à versão da CLI da Solana instalada (você pode verificar isso usando
solana --version). Caso contrário, você pode encontrar erros durante a compilação.
A função de entrada do contrato é definida em processor.rs. O arquivo começa importando as dependências necessárias (pulamos partes semelhantes em outros arquivos Rust):

Em seguida, vem a definição da função de entrada do contrato:

Na linha 10 do programa, a macro entrypoint! da biblioteca Solana especifica que a função de entrada é process_instruction. Esta função aceita três parâmetros:
- program_id: O endereço implantado do programa atual.
- accounts: Todas as contas envolvidas na instrução.
- instruction_data: O array de bytes usado para processar as instruções.
process_instruction extrai o primeiro byte de instruction_data para identificar o tipo de instrução e chama a função correspondente para tratá-la. Em seguida, veremos como essas três funções são implementadas.
init
Cada uma das três funções é definida em um arquivo com o mesmo nome no diretório program/src/instructions. Vamos começar com init.rs.

Como a instrução init não requer um array de bytes adicional, ela aceita apenas dois parâmetros: program_id e accounts.
Entre as linhas 12 e 15, o programa extrai sequencialmente as informações de conta necessárias, depois chama a função find_program_address para calcular o endereço da conta de dados usada para armazenar o índice máximo atual de artigos, index_pda_key. Em seguida, o programa verifica que index_pda_key é igual ao endereço de index_pda.
Aqui, o endereço de index_pda é um endereço especial chamado PDA (Program Derived Addresses). Ao contrário dos endereços de conta "carteira" gerados a partir de chaves públicas, os PDAs são derivados de um array de bytes opcional, um byte chamado "bump" e o endereço de um programa. O array de bytes e o endereço do programa são fornecidos explicitamente pelo chamador, enquanto o bump é gerado automaticamente e retornado pela função find_program_address. O bump garante que os endereços criados dessa forma não possam colidir com endereços gerados por chave pública.

Após confirmar que o endereço de index_pda é o esperado, usamos a instrução create_account fornecida pelo SystemProgram para criar o index_pda. Na linha 22, o programa cria uma variável do tipo IndexAccountData, que registra o índice máximo atual de artigos (inicializado em 0). Como mostrado na figura abaixo, esse tipo implementa as traits BorshSerialize e BorshDeserialize, permitindo que seja serializado e desserializado no formato Borsh:

As linhas 23 a 24 calculam o espaço necessário para armazenar os dados da conta e o aluguel mínimo para criar a conta.
As linhas 27 a 37 criam uma instrução create_account do SystemProgram e a invocam usando o método invoke_signed. A ação de invocar uma instrução para outro programa dentro de um programa é chamada de CPI (Cross-Program Invocation). Na Solana, você pode usar duas funções para realizar CPI: invoke e invoke_signed.
A função invoke processa diretamente a instrução fornecida, e sua assinatura de função é a seguinte:
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>]
) -> Result<(), ProgramError>
A única diferença entre invoke_signed e invoke é que invoke_signed fornece uma maneira de assinar contas PDA. As contas PDA não possuem par de chaves pública-privada e, portanto, não podem fornecer assinaturas diretamente. invoke_signed resolve esse problema. Esta função aceita um parâmetro adicional chamado signers_seeds. Sua assinatura de função é a seguinte.
De forma semelhante ao funcionamento da função find_program_address, invoke_signed calcula um conjunto de endereços PDA com base nos signers_seeds fornecidos e no program_id do chamador. Se account_infos contiver esses endereços PDA, eles serão marcados como signatários, como se tivessem fornecido assinaturas.
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>
Aqui, como a instrução create_account requer que a conta sendo criada seja uma signatária, precisamos usar invoke_signed para fornecer uma assinatura para index_pda.

No final do programa, o contrato chama o método serialize em index_data para serializar os dados e gravá-los no campo data de index_pda.
post_article

Agora vamos ver a implementação da instrução post. Ao contrário da instrução init, a instrução post aceita um artigo publicado, então ela tem um parâmetro adicional chamado instruction_data_inner, que armazena os dados serializados do artigo.
Como de costume, o programa começa extraindo as informações de conta necessárias para a instrução. Como geramos uma conta PDA separada para cada artigo, o programa aceita uma conta adicional, new_article_pda, na linha 23.
Semelhante à instrução init, as linhas 27 a 30 usam find_program_address para verificar que o endereço do index_pda passado está correto. Em seguida, desserializamos os dados no campo data para ler o índice máximo alocado atualmente.
As linhas 33 a 40 também verificam o endereço de new_article_pda. O programa usa a string "ARTICLE_PDA" e o índice atual para gerar um novo endereço. O índice é incrementado a cada upload de artigo, o que garante que o endereço gerado seja único.

Em seguida, o programa desserializa instruction_data_inner em dados de artigo, que são do tipo PostArticleData:

Essa estrutura tem apenas dois campos: title e content. Nas linhas 45 a 49, o programa realiza uma verificação para garantir que o artigo não seja grande demais para ser contido em uma única conta.

Os passos seguintes também são semelhantes à instrução init: O programa primeiro calcula o espaço e o aluguel necessários para a conta, depois invoca a instrução create_account do SystemProgram para criar a conta PDA que armazenará o artigo. Por fim, o artigo publicado é serializado no campo data de new_article_pda, e o índice máximo atual é incrementado em um e serializado no campo data de index_pda.
list_articles

Por fim, vamos ver a implementação da instrução list. Como essa instrução precisa listar todos os artigos, um vetor é usado na linha 15 para representar todas as contas PDA que armazenam os artigos, e o endereço da conta index_pda também é validado.

Depois disso, o programa verifica que o tamanho do vetor é igual ao índice máximo atual e verifica a correção do endereço de cada conta.

No final do programa, ele itera sobre cada conta e usa a macro msg! do solana-program para exibir o conteúdo desserializado de cada artigo um por um.
Teste de Transações
Para testar nosso contrato, usamos um script TypeScript localizado em client/main.ts no repositório.

No topo do script, importamos todas as bibliotecas necessárias e definimos três constantes globais. KEYPAIR_PATH indica o caminho para o arquivo de chave privada gerado usando solana-keygen new, PROGRAM_ID é o endereço do programa implantado que escrevemos na última seção, e POST_ARTICLE_SCHEMA é o objeto usado para serialização de artigos.


No corpo principal do script, ele primeiro chama a função loadKeyFromFile para analisar o arquivo de chave privada e obter um objeto Keypair como pagador das taxas de transação. Em seguida, usa o método findProgramAddressSync fornecido por @solana/web3.js para calcular o endereço de indexPda. Este método usa o mesmo algoritmo que o contrato Rust, garantindo que calcule o mesmo endereço com os mesmos parâmetros. O objeto connection especifica que usaremos a devnet para os testes.

Em seguida, enviamos a primeira transação de inicialização. Criamos uma transação que contém uma única instrução. O campo data desta instrução contém apenas um byte "0", indicando que esta é uma invocação à instrução init. A transação é então enviada e confirmada usando sendAndConfirmTransaction.

A segunda transação invoca a instrução post para publicar alguns artigos. Das linhas 56 a 63, o script serializa dois artigos e calcula os endereços PDA correspondentes. Em seguida, duas instruções post são adicionadas a uma única transação para criar esses artigos.

A transação final invoca a instrução list, e em seguida os logs são impressos após a confirmação da transação.
Para testar o programa com o script, substitua o KEYPAIR_PATH pelo caminho para sua chave privada (se não for o caminho padrão). Em seguida, compile e implante o contrato inteligente Rust executando os seguintes comandos:
cd program
cargo build-bpf
solana program deploy ./target/deploy/hello_solana.so
Em seguida, copie o endereço implantado e cole-o no campo PROGRAM_ID. No diretório raiz do projeto, execute npm install para instalar as dependências, e depois execute npm start para executar o script.
Depois disso, podemos usar o Solscan para visualizar os detalhes de execução das transações de teste.

Vamos ver diretamente a segunda transação para publicação de artigos. Ela contém duas instruções, cada uma com o primeiro byte do Instruction Data Raw sendo 0x01. Além disso, cada instrução inclui uma instrução interna, que é uma invocação à instrução CreateAccount do System Program.

Da mesma forma, podemos inspecionar a terceira transação para listar todos os artigos. Na seção Program Logs, podemos verificar que os índices, títulos e conteúdos dos dois artigos são impressos.
Conclusão
Neste artigo, primeiro explicamos como configurar o ambiente de programação e execução da Solana localmente. Em seguida, detalhamos a lógica de implementação de um contrato Solana. Por fim, testamos as funcionalidades do contrato usando um script TypeScript. Com este tutorial passo a passo, acreditamos que você aprendeu como escrever um contrato inteligente Solana simples 🥳.
No próximo artigo, forneceremos um guia detalhado sobre como visualizar e analisar transações Solana usando o Phalcon Explorer (o Phalcon Explorer agora suporta Solana). Fique ligado!



