Seu Primeiro Smart Contract Solana do Zero
Em 2024, a Solana ganhou destaque, tornando-se a quarta blockchain pública em valor total bloqueado (TVL), capturando o interesse de investidores e desenvolvedores.
No primeiro artigo desta série, exploramos os conceitos-chave da rede Solana, incluindo seu mecanismo de operação, modelo de conta e transações, estabelecendo uma base sólida para escrever contratos Solana corretos e de alto desempenho.
Neste artigo, vou te guiar na escrita de um programa Solana (ou seja, um Smart Contract Solana) para postar e exibir artigos. Isso ajudará a consolidar os conceitos do primeiro artigo e introduzir algumas funcionalidades da Solana que ainda não discutimos.
O programa e o código de teste foram publicados no GitHub.
Configurando o Ambiente
Nota: Os seguintes comandos cobrem apenas o OS Ubuntu. Alguns comandos podem não funcionar nos sistemas Windows e macOS.
👉 Você pode usar comandos alternativos ou consultar a configuração de desenvolvimento local e instalar o 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, então 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, então precisamos instalar o Node.js e a ferramenta 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
Solana CLI
Em seguida, precisamos instalar a ferramenta CLI da Solana, que fornece comandos para tarefas como criação de carteiras e implantação de 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 o CLI da Solana foi instalado com sucesso:
solana --version
solana-cli 1.17.25 (src; feat:3580551090, client)
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 posteriormente. Você pode usar o comando solana address
para ver o endereço gerado por ele:
solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy
Devnet
O programa será implantado no devnet, então precisamos executar o seguinte comando para alternar o CLI da Solana para o 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 do faucet público.
Escrevendo o Programa
O programa que vamos introduzir permite que os usuários publiquem artigos e listem todos os artigos atualmente postados. 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 dos artigos.
- instrução post: Armazena um artigo postado em uma nova conta de dados.
- instrução list: Imprime todos os artigos postados 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 arquivo 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 do 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 no processor.rs
. O arquivo começa importando as dependências necessárias (vamos pular partes semelhantes em outros arquivos Rust):
Em seguida, segue 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 recebe 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 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 lidar com ela. A seguir, 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 as informações necessárias da conta sequencialmente e, em seguida, chama a função find_program_address
para calcular o endereço da conta de dados usada para armazenar o índice máximo atual dos artigos, index_pda_key
. Em seguida, o programa afirma que index_pda_key
é igual ao endereço de index_pda
.
Aqui, o endereço de index_pda
é um endereço especial chamado PDAs (Program Derived Addresses – Endereços Derivados de Programas). Diferente dos endereços de contas “wallet” gerados a partir de chaves públicas, os PDAs são derivados de um array de bytes opcional, de um byte chamado “bump”, e do endereço de um programa. O array de bytes e o endereço do programa são explicitamente fornecidos 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 index_pda
. Na linha 22, o programa cria uma variável do tipo IndexAccountData
, que registra o índice máximo atual dos artigos (inicializado em 0). Como mostrado na figura abaixo, esse tipo implementa os traits BorshSerialize
e BorshDeserialize
, permitindo que ele seja serializado e desserializado no formato Borsh:
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.
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 é referida como CPI (Cross-Program Invocation – Invocação Cruzada de Programas). Na Solana, você pode usar duas funções para realizar CPI: invoke
e invoke_signed
.
A função invoke
processa diretamente a instrução dada, 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 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:
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>
De forma semelhante a como a função find_program_address
funciona, 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.
Aqui, como a instrução create_account
exige que a conta sendo criada seja uma signatária, precisamos usar invoke_signed
para fornecer uma assinatura para index_pda
.
post_article
Em seguida, vamos analisar a implementação da instrução post
. Diferente da instrução init
, a instrução post
aceita um artigo postado, portanto, ela possui 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 da 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 se o endereço do index_pda
passado está correto. Em seguida, desserializamos os dados no campo de dados para ler o índice máximo atual alocado.
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 possui 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 dentro de uma única conta.
Os passos seguintes também são semelhantes à instrução init
: o programa primeiro calcula o espaço necessário e o aluguel para a conta, depois invoca a instrução create_account
do SystemProgram
para criar a conta PDA para armazenar o artigo. Finalmente, o artigo postado é serializado no campo de dados de new_article_pda
, e o índice máximo atual é incrementado em um e serializado no campo de dados de index_pda
.
list_articles
Finalmente, 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 se o tamanho do vetor é o mesmo que o índice máximo atual e verifica a correção do endereço de cada conta.
Teste de Transações
Para testar nosso contrato, utilizamos um script em 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 seção anterior, e POST_ARTICLE_SCHEMA
é o objeto usado para a serialização dos 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
. Esse método utiliza o mesmo algoritmo do contrato em Rust, garantindo que ele compute o mesmo endereço com os mesmos parâmetros. O objeto de conexão especifica que usaremos o devnet
para testes.
Em seguida, enviamos a primeira transação de inicialização. Criamos uma transação que contém uma única instrução. O campo de dados dessa instrução contém apenas um byte “0”, indicando que esta é uma invocação da 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 então 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 (caso não seja o caminho padrão). Em seguida, compile e implante o smart contract em Rust executando os seguintes comandos:
cd program
cargo build-bpf
solana program deploy ./target/deploy/hello_solana.so
Depois, 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, em seguida, execute npm start
para executar o script.
Depois disso, podemos usar o Solscan para visualizar os detalhes da execução das transações de teste.
Vamos olhar diretamente para a segunda transação de 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 da instrução CreateAccount
do System Program
.
De forma semelhante, podemos inspecionar a terceira transação para listar todos os artigos. Na seção de Program Logs
, podemos verificar que os índices, títulos e conteúdos dos dois artigos são impressos.
Neste artigo, você aprendeu como configurar o ambiente de programação e execução do Solana localmente. Em seguida, detalhamos a lógica de implementação de um contrato Solana.
Finalmente, testamos as funcionalidades do contrato usando um script em TypeScript.
Com este tutorial passo a passo, acredito que você aprendeu como escrever um smart contract simples na Solana 🥳.
Recentemente a BlockSec criou a série “Solana Simplified”, que inclui artigos sobre os conceitos básicos da Solana, tutoriais sobre como escrever smart contracts e guias para analisar transações.
O objetivo é ajudar os leitores a entender o ecossistema Solana e dominar as habilidades essenciais para desenvolver projetos e realizar transações na Solana.
O próximo artigo, vai ser um guia detalhado sobre como visualizar e analisar transações da Solana usando o Phalcon Explorer (o Phalcon Explorer agora suporta Solana).
Ficou com alguma dúvida?
Participe do nosso grupo no WhatsApp e tire suas dúvidas diretamente conosco! Clique aqui para entrar.