Em 2009 surgia o Bitcoin, a primeira grande aplicação de sucesso da tecnologia blockchain. No entanto, seu algoritmo interno extremamente focado em transações financeiras P2P não era apropriado para o desenvolvimento de scripts mais complexos ou até mesmo programas de computador inteiros, o que motivou anos mais tarde, em 2014, a criação da blockchain Ethereum, a primeira de segunda geração e que permitia o desenvolvimento de aplicações muito mais robustas que seu antecessor, chamadas de smart contracts. Mais alguns anos se passaram e ficou evidente problemas crônicos na rede Ethereum, frutos da sua própria popularidade e de escolhas à época como manter o Proof of Work do Bitcoin como modelo de consenso: baixa performance e custos altos de transação. Para tentar endereçar esses problemas, a Ethereum seguiu evoluindo, mas as blockchains de terceira geração surgiram para aproveitar esta oportunidade, sendo que em 2017 Anatoly Yakovenko junto de seus dois amigos, Greg Fitzgerald e Eric Williams, viriam a lançar a rede Solana.
Com tempo de bloco inferior a 1 segundo e taxas inferiores a 1 centavo de dólar por transação, a Solana já em 2021 era vista como a principal concorrente à Ethereum no campo da web3 e da programação para blockchain. Ainda longe da líder de mercado mas crescendo rapidamente e se mostrando muito promissora, é natural que muitos devs interessem-se por aprender a criar aplicações para ela.
A minha ideia com este tutorial é lhe dar uma introdução no desenvolvimento de programas para Solana, lhe despertando então interesse (e capacidade) por conhecer mais sobre este universo que não pára de crescer. Acredito estar desta forma colaborando com o desenvolvimento de novos profissionais neste meio, principalmente aqueles sem grandes conhecimentos do idioma Inglês, onde você encontra muito material acerca de programação blockchain.
Então vamos lá!
#1 – Setup do Projeto
Programas para a rede Solana são escritos na linguagem Rust, uma linguagem fortemente tipada de propósito geral, em contraponto ao Solidity da rede Ethereum, que foi criado especificamente para os programas da tal rede. Para escrita, testes, deploy, etc dos programas, usaremos ainda o Anchor, o framework de desenvolvimento mais popular para rede Solana (o equivalente ao HardHat, do mundo EVM).
Para poder utilizar estas tecnologias tem uma série de preparativos que devem ser feitos na sua máquina, que são detalhados neste artigo, que é obrigatório antes de seguir neste tutorial. Você verá logo no início que tem instruções para Windows, Mac e Linux, então escolha e siga as instruções para o seu sistema operacional.
Após terminar o setup geral, você precisa ter um keypair padrão na sua conta de usuário do sistema operacional. Para fazer isso, abra o terminal onde instalou as dependências da Solana e rode o seguinte comando.
1 2 3 |
solana-keygen new --no-bip39-passphrase |
O resultado desse comando será a criação de um par de chaves e no console será exibida a sua chave pública e a sua frase de recuperação (seedphrase). Enquanto que a primeira é pública, a segunda informação deve ser guardada em segredo pois quem tiver ela pode recuperar seu keypair em outra máquina, o que pode ser um grande problema.
Agora que você já tem o ambiente preparado, é hora de criarmos o nosso primeiro projeto no Anchor. Segundo o site oficial, o Anchor é o framework de desenvolvimento líder para construir programas para Solana (o que chamamos de smart contracts no mundo Ethereum), servindo para simplificar o processo de escrita, testes, deploy e interação com outros programas Solana. Além disso, o framework Anchor ajuda os desenvolvedores a programarem de forma mais profissional, produtiva e segura, já que dispõe de recursos nativos de segurança.
Para criar um projeto Anchor, abra um terminal onde você tenha todas as dependências instaladas conforme ensinado anteriormente e rode o comando abaixo na pasta onde deseja salvar seu projeto.
1 2 3 |
anchor init hello-world-solana |
Este comando vai baixar e instalar todos os arquivos necessários para seu projeto “hello-world-solana”, o que pode demorar um bocado, então não se assuste. Ao término da instalação você terá a estrutura de pastas e arquivos abaixo, sendo que estou com o projeto aberto no VS Code.
Para verificar se está tudo funcionamento corretamente, primeiro vamos fazer uma compilação do projeto padrão, com o comando abaixo.
1 2 3 |
anchor build |
Talvez você tenha alguns warnings ao final do build, mas não se preocupe com eles, a menos que sejam erros ao invés disso. O resultado da compilação fica na pasta target e dentre os recursos interessantes de você prestar atenção aqui está a pasta idl, onde terá um arquivo JSON dentro com a interface de comunicação com nosso programa Solana. IDL é uma sigla para Interface Definition Language e é o equivalente ao ABI do mundo EVM, sendo útil para que outros programas (e UIs) possam interagir com a sua criação.
Outra etapa igualmente importante neste primeiro momento é rodar os testes padrões deixados na pasta tests. Para isso, apenas rode o comando abaixo.
1 2 3 |
anchor test |
Se você tiver o erro “Error: Unable to read keypair file” quer dizer que você não criou o keypair corretamente conforme instruções anteriores. Volte ao início desta seção, crie e rode os testes novamente. Se tudo rodar com sucesso, você terá mensagens parecidas com abaixo, indicando que está tudo 100% certo com seu ambiente e com seu projeto de exemplo.
Agora podemos partir para a personalização do mesmo!
#2 – Criando o Hello World
A parte mais importante pra gente neste primeiro momento fica dentro da pasta programs na raiz do projeto. Lá você vai achar uma pasta com o mesmo nome do projeto (“hello-world-solana”), uma subpasta src e dentro dela um arquivo lib.rs com um programa de exemplo escrito em Rust, a linguagem oficial para programação na rede Solana.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use anchor_lang::prelude::*; declare_id!("7D5MDwidRQmcs7UaLmkgTbhJQUsyZA9m5N8kBhmcn2ua"); #[program] pub mod hello_world_solana { use super::*; pub fn initialize(ctx: Context<Initialize>) -> Result<()> { msg!("Greetings from: {:?}", ctx.program_id); Ok(()) } } #[derive(Accounts)] pub struct Initialize {} |
Esse programa apenas inicializa a si próprio (exibindo uma mensagem) e se encerra logo na sequência (função Ok), sendo que o teste padrão que vem programado no projeto de exemplo apenas testa isso: se o programa consegue subir corretamente.
Como vamos criar o nosso programa do zero e ele vai ser bem simples, recomendo excluir a função initialize e a struct Initialize, ficando como abaixo.
1 2 3 4 5 6 7 8 9 10 |
use anchor_lang::prelude::*; declare_id!("7D5MDwidRQmcs7UaLmkgTbhJQUsyZA9m5N8kBhmcn2ua"); #[program] pub mod hello_world_solana { use super::*; } |
Agora vamos criar uma nova função em Rust que vai servir para inicializar nosso programa. Ela vai ser uma função (fn) pública (pub, ou seja, possível de ser chamado por qualquer um) e se chamar initialize, coloque ela dentro do escopo do módulo hello_world_solana, logo abaixo do “use super”.
1 2 3 4 5 6 |
pub fn initialize(ctx: Context<Initialize>, message: String) -> Result<()> { ctx.accounts.hello_world_account.message = message; Ok(()) } |
Pra você entender esse código eu preciso primeiro lhe passar um pouco de teoria de como funcionam as accounts na rede Solana. Accounts são espaços de armazenamento na blockchain que podem conter um programa executável ou dados de um programa. O nome é account pois cada um desses espaços possui um par de chaves e saldo próprio, sendo literalmente contas mesmo, mas que podem guardar muito mais do que isso.
Diferente da programação de smart contracts na EVM, onde temos lógica e armazenamento juntos, no mesmo local, em programas Solana os “smart contracts” são stateless, ou seja, não guardam informação alguma, apenas processam lógicas. Qualquer dado que você queira que seja armazenado deve ser feito em outra account não-executável, apenas de dados. Accounts de dados (ou seja, que não executam código) funcionam como o storage de contratos Solidity, com a diferença de que ficam fora dos programas. Assim, um programa que necessite ler ou escrever dados, deve receber como referência uma ou mais accounts onde os dados estão/devem ficar.
Voltando ao nosso código, o primeiro parâmetro da função initialize, ctx, é do tipo Context e diz respeito às accounts que essa função precisa para funcionar. Falaremos mais delas em breve e você entenderá o generics/subtipo “Initialize” que passamos pro Context. Além do contexto, as funções podem esperar outros parâmetros, ligados aos inputs necessários para a mesma, onde resolvi colocar uma mensagem inicial, que o usuário irá definir no formato de String.
Como retorno da nossa função (símbolo ->) foi definido um objeto do tipo Result, que é bem genérico para essa finalidade. Agora vamo falar do conteúdo dessa função.
Na primeira linha nós pegamos o contexto, dentro dele acessamos as accounts, mais especificamente a account de nome hello_world_account (Rust segue o padrão snake case de maiúsculas e minúsculas, vá se acostumando…), que ainda não existe mas que vamos especificar em breve. Essa account servirá para guardar os dados do nosso programa e vou definir nela uma propriedade chamada message, que armazenará a mensagem recebida do usuário na inicialização do programa.
Por fim finalizamos com um Ok, que indica que o programa rodou com sucesso.
No entanto, considerando que um programa pode ter várias accounts e cada uma delas pode ter várias informações guardadas, é na definição do contexto que colocamos ordem nessa bagunça. Com base no que você definir no contexto é como o programa irá lidar com os dados nas accounts, de outra maneira seria impossível a serialização e desserialização correta dos bytes armazenados na blockchain da Solana. Vamos fazer isso ao final do nosso arquivo, logo depois do fechamento do módulo principal. Abaixo o código completo do nosso contexto, que usamos em nossa função initialize.
1 2 3 4 5 6 7 8 9 10 |
#[derive(Accounts)] pub struct Initialize<'info>{ #[account(init, payer=user, space=8+32)] pub hello_world_account: Account<'info, HelloWorldAccount>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } |
No framework Anchor usamos muitas macros que servem para ajudar o compilador a entender como validar os blocos de código, garantindo mais segurança e eficiência no código de máquina resultante da compilação. A primeira instrução é uma macro que diz que esta estrutura de contexto deriva de Accounts, ou seja, ela terá a estrutura geral necessária para um contexto de função Solana. Na sequência, declaramos o contexto Initialize como pub (público) e struct (estrutura). Já o ‘info (não pode faltar o apóstrofo) é padrão, diz respeito a informações da account, ou seja, que esta estrutura é de dados de contas e que os dados devem ser persistidos enquanto a estrutura existir (ou algo próximo disso, ainda não dominei os lifetimes do Rust).
Dentro da estrutura do contexto precisamos definir as informações necessárias para a função poder ser executada: a account de dados necessária, as credenciais da account (user) que vai pagar pela transação de criação de account (a criação é uma transação pois escreve dados na blockchain) e o programa de sistema que vai executar a criação a mesmaa (existem diferentes programas nativos na Solana, precisamos que algum deles crie a account pra gente).
O primeiro elemento “pub hello_world_account” é a account que vamos criar para guardar os dados do nosso programa. Eu defini ela como sendo do tipo Account e na macro logo acima dele eu estou dizendo que esta account será criada automaticamente na primeira chamada de função que usar ela (init), que o responsável por pagar as taxas da transação de criação (payer) será a account de nome user (definida logo mais) e que o espaço necessário para os dados é 8+32 bytes (8 é fixo para metadados/descritor da account e o resto para os dados de hello_world_account, sendo que 32 bytes é um tamanho comum para strings). Repare que Account possui dois subtipos: ‘info (padrão) e HelloWorldAccount, que falarei mais à frente.
Já os dois parâmetros seguintes da struct dizem respeito às credenciais do usuário que pagará as taxas desta transação (signer), sendo que usei a macro account(mut) para dizer que essa informação pode ser alterada (é mutável). Por fim, a última propriedade diz que o programa de sistema chamado System será o responsável por criar a account quando a função for chamada. Com esse contexto definido, quer dizer que quem quiser chamar a função initialize do nosso programa precisará passar três informações na chamada: hello_world_account, user e system_program.
Mas voltando ao “pub hello_world_account”, ou seja, aos dados da nossa função, o subtipo HelloWorldAccount (em Pascal Case) precisa ser definido, ou seja, qual a estrutura de dados desta account específica. Eu faço isso abaixo, logo depois do fechamento da pub struct do contexto.
1 2 3 4 5 6 |
#[account] pub struct HelloWorldAccount { pub message: String } |
A primeira macro diz que a estrutura a seguir é de uma account. Definimos esta estrutura (struct) como pública (pub) e definimos o seu nome como HelloWorldAccount (use Pascal Case, é um novo tipo de dado). Dentro dessa struct você define tudo que deseja que seja lido/escrito nessa account, o que para nós é apenas uma mensagem String, conforme já tínhamos escrito lá atrás na função initialize do programa, lembra? Lembre-se: todos os dados que você quiser usar no seu programa devem ser especificados em accounts como essa.
Alguns tipos válidos para variáveis, dentre muitos:
- i64: inteiro de 64 bits, mas pode variar de 18 a i128;
- u64: número inteiro sem sinal de 64 bits, mas pode variar de u8 a u128;
- f32 e f64: número com ponto flutuante de 32 e 64 bits (desencorajado);
- bool: booleano (true/false);
- char: apenas um caracter;
- String: textos (vários caracteres);
- Vec<i64>: vetor dinâmico de inteiros de 64 bits (Rust tem arrays, mas eles são fixos);
Agora que temos nossa função initialize e todas suas dependências codificadas, é hora de testarmos ela. Antes disso, rode o comando abaixo para fazer a compilação, o que não apenas vai nos dizer se nosso código está correto como gerar os types dele para os testes.
1 2 3 |
anchor build |
Talvez apareçam alguns warnings, mas pode ignorar, apenas não avance se tiver erros. Agora sim, hora dos testes!
#3 – Testando o Hello World
Testes são uma parte crucial do desenvolvimento de qualquer software mas programação para blockchain coloca isso em outro patamar. Isso porque como seu programa ficará visível na blockchain e na maioria das vezes ele envolverá dinheiro, é crucial que ele esteja em pleno funcionamento e que seja seguro. Não apenas isso, as transações na blockchain são imutáveis, então não temos margem para corrigir pequenas coisinhas depois, como é comum no mundo web.
Dito isso, vá até a sua pasta de tests e verá um arquivo hello-world-solana.ts, ou seja, um arquivo TypeScript para unit testing usando o framework Mocha.
Dentro deste arquivo vamos escrever os testes unitários do nosso programa Rust, para isso o Anchor vai nos fornecer uma série de ferramentas úteis para essa finalidade. Caso esteja familiarizado com suítes de teste como Jest, Tape, Chai ou Jasmine, se sentirá em casa.
Vamos começar excluindo tudo que veio por padrão no arquivo e iniciando a codificação dos testes pelas importações das dependências.
1 2 3 4 5 6 |
import assert from "assert"; import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; import { HelloWorldSolana } from "../target/types/hello_world_solana"; |
O import assert serve para as asserções (condicionais de validação do teste), enquanto que anchor é o objeto de acesso de ferramentas do framework para testar os programas. Program é a classe que representa um programa e HelloWorldSolana é o type do nosso programa, gerado a partir da compilação.
Agora para criar a estrutura padrão dos testes chame a função describe, defina um nome para a suíte de testes e na função de callback que é o segundo parâmetro, iremos colocar nossos futuros testes. Também vou definir algumas configurações iniciais que explicarei na sequência.
1 2 3 4 5 6 7 8 9 10 11 12 |
describe("HelloWorldSolana Tests", () => { // conexão com a blockchain anchor.setProvider(anchor.AnchorProvider.env()); // dependências para os testes const program = anchor.workspace.HelloWorldSolana as Program<HelloWorldSolana>; const helloWorldAccount = anchor.web3.Keypair.generate(); //testes vão aqui }); |
Antes do primeiro teste você deve dizer ao objeto anchor qual o provider de acesso à blockchain (conexão) que ele deve utilizar. Aqui eu usei a função setProvider passando a informação genérica do ambiente de desenvolvimento, ou seja, blockchain local, o que para unit tests é o padrão.
Na sequência, eu inicializo duas variáveis que vamos precisar nos testes. A primeira, program, diz respeito ao nosso programa, obtemos ela a partir do anchor.workspace e fazemos um casting (tipagem) para Program de HelloWorldSolana. Já a segunda variável é uma account que usaremos nos testes e repare que ela nada mais é do que um novo keypair (par de chaves), ou seja, o nome account não é ao acaso: elas são contas de usuário na blockchain ao mesmo tempo que podem armazenar programas ou dados, como mencionado antes.
Agora vamos escrever nosso primeiro e único teste unitário, que vai chamar a função initialize passando uma mensagem e verá se a mesma ficou salva nos dados do programa.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
it("Hello World", async () => { const testMessage = "Hello World"; let tx = await program.methods.initialize(testMessage) .accounts({ helloWorldAccount: helloWorldAccount.publicKey, user: anchor.getProvider().publicKey }) .signers([helloWorldAccount]) .rpc(); const account = await program.account.helloWorldAccount.fetch(helloWorldAccount.publicKey); assert.ok(account.message === testMessage); }); |
Aqui a gente começa definindo a mensagem do teste, depois vem a chamada do método initialize do nosso programa, com program.methods.initialize. Nos parâmetros desta função, que só vai ser reconhecida pelo VS Code se você compilou seu projeto com sucesso primeiro, devemos passar a mensagem que queremos escrever, referente ao segundo parâmetro lá do nosso programa Rust, lembra? Mas e o primeiro, parâmetro, o contexto (ctx)?
As informações do contexto nós definimos chamando algumas funções encadeadas, como accounts, onde definimos o endereço da account que será criada (usamos a chave pública da carteira que criamos para os testes) e o usuário que vai pagar pela transação (usamos um usuário de teste genérico do anchor). A outra função encadeada é a signers, que somente é necessária em chamadas que criam accounts e serve para para passar a carteira que vai assinar a transação e ser a dona da account, tendo de ser a mesma helloWorldAccount ou dará erro de assinatura na hora que executar o teste.
Por fim, enviamos a transação para a blockchain usando a função rpc (sigla para Remote Procedure Call ou Chamada Remota de Procedimento). O resultado desta chamada é o hash da transação, mas ele não tem utilidade no momento, apenas não dando erro já está ok.
Para validar se a mensagem foi salva na blockchain com sucesso, nós vamos acessar novamente nosso programa, mais especificamente a account de nome helloWorldAccount e fazer um fetch nela, que serve para pegar seus dados. O fetch espera por parâmetro o endereço da account que queremos ler, informação essa que é a publicKey da conta helloWorldAccount. O resultado é um objeto com todos os dados da account, incluindo a message, que podemos comparar no assert se dá match com a mensagem original. Tudo estando certo, rode o comando abaixo para os testes novamente.
1 2 3 |
anchor test |
E confira no terminal se passou com sucesso.
E com isso finalizamos o nosso primeiro tutorial de programação Solana com Anchor aqui do blog!
Até a próxima!

Como integrar ReactJS com carteira Solana

Como configurar ambiente para desenvolvimento Solana

Como criar sua própria carteira de criptomoedas com JS (Solana) - Parte 2

Como configurar a Brave Wallet para desenvolvimento blockchain
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.