
Um guia para testar programas Solana
Índice
- Introdução
- O que são testes?
- Testes unitários
- Testes de integração
- Testes de ponta a ponta (E2E)
- Combinando essas metodologias de teste
- Como escrever bons testes
- Desenvolvimento Orientado a Testes (TDD)
- O padrão Arrange-Act-Assert (AAA)
- Melhores práticas do setor
- Como escrever testes unitários básicos
- Testes unitários em Rust
- Testes unitários em TypeScript com Mocha e Chai
- Frameworks populares de testes para Solana
- Bankrun
- solana-program-test
- solana-test-framework
- Exemplo de cenário de teste
- O programa
- Como escrever testes
- Conclusão
- Recursos adicionais
Introdução
Os testes em um ambiente blockchain vão além do paradigma tradicional de testes de software, trazendo desafios únicos e consequências mais sérias. No ambiente de alta capacidade e baixa latência da Solana, a margem para erros é pequena. Os testes automatizados deixam de ser apenas uma prática recomendada e se tornam uma necessidade fundamental para garantir a confiabilidade e a segurança dos programas que operam no ambiente dinâmico e implacável da Solana.
Este artigo explora os principais tipos de testes automatizados: testes unitários, testes de integração e testes de ponta a ponta (E2E). Também aborda a criação de testes unitários básicos em JavaScript/TypeScript e Rust antes de analisar frameworks populares para testar programas Solana. Por fim, o artigo apresenta um exemplo prático que testa um programa de jogo “King of the Hill”.
Este artigo pressupõe que você conheça o modelo de programação e o desenvolvimento de programas da Solana. Não abordaremos o processo de criação de um programa nem conceitos específicos da Solana. Nosso foco é aprender a testar programas Solana.
Se você ainda não conhece a Solana, recomendo ler primeiro estas publicações anteriores do blog:
- O modelo de programação da Solana: uma introdução ao desenvolvimento na Solana
- Uma introdução ao Anchor: guia para iniciantes sobre como criar programas Solana
Este artigo também complementa nosso artigo anterior sobre segurança de programas Solana. Recomendo ler os dois em conjunto.
O que são testes?
Os testes são realizados para verificar se um trecho de código ou uma aplicação inteira funciona como esperado. Há dois tipos gerais de testes:
- Testes manuais: processo centrado em pessoas, no qual os casos de teste são executados por desenvolvedores, analistas de garantia da qualidade, pentesters ou qualquer outra pessoa responsável
- Testes automatizados: processo centrado em código, no qual scripts são escritos para executar casos de teste predefinidos de forma programática
Os testes manuais são um processo altamente flexível e independente do tipo de aplicação testada. Eles são adequados para testar novos recursos, usabilidade e acessibilidade. Os testes manuais dependem da intuição de quem testa sobre como uma aplicação deve se comportar. No entanto, são inerentemente lentos, sujeitos a erros, demorados e muitas vezes incompletos — ou seja, nem todos os cenários são cobertos — devido à falta de ferramentas no processo de teste.
Os testes automatizados buscam solucionar as desvantagens dos testes manuais. Por exemplo, costumam ser mais rápidos, especialmente quando executados em paralelo. Também estão menos sujeitos a erros humanos, pois seguem um script predefinido. Eles ampliam a cobertura por conseguirem processar com eficiência um grande número de casos de teste, o que os torna uma solução altamente escalável. Porém, devido à sua objetividade rígida, os testes automatizados são menos precisos em cenários que dependem de interação humana, julgamento ou raciocínio crítico.
Neste artigo, nosso foco são os testes automatizados para programas Solana, pois testá-los manualmente na mainnet seria bastante caro, enquanto testá-los manualmente na devnet exigiria muito tempo. No entanto, tanto os testes manuais quanto os automatizados devem fazer parte do processo antes de enviar código para produção. Um processo de testes robusto pode minimizar a quantidade de bugs introduzidos em produção ao identificá-los mais cedo durante o desenvolvimento.
Há vários tipos de testes automatizados, entre eles:
- Testes unitários
- Testes de integração
- Testes de ponta a ponta (E2E)
Testes unitários
Testes unitários são um processo no qual as menores unidades funcionais do código são testadas para garantir que funcionem corretamente. Idealmente, as unidades são os menores componentes possíveis de um programa — como funções ou módulos individuais — que, quando combinados, formam o produto final. A ideia central é que o programa como um todo deve funcionar conforme esperado se testarmos minuciosamente seus componentes básicos.
Os testes unitários são fundamentais para o desenvolvimento na Solana, pois garantem que cada parte de um programa se comporte conforme esperado. A reutilização desses testes garante que novos recursos ou atualizações atendam às especificações do projeto e às expectativas dos usuários definidas no caso de teste. Por isso, os testes unitários incentivam naturalmente a otimização e a refatoração do código, para que novas melhorias não afetem negativamente a funcionalidade do programa. Além de garantir que cada segmento de código funcione corretamente sob diversas condições de teste, eles também asseguram interações eficientes e seguras com a blockchain. Detectar bugs antecipadamente por meio de testes unitários é crucial para evitar que possíveis vulnerabilidades cheguem à produção.
Vários frameworks de teste ajudam a simplificar os testes unitários ao facilitar a simulação das condições da rede e o gerenciamento do estado do programa, como veremos mais adiante. Com testes unitários, os desenvolvedores Solana podem alcançar um alto grau de confiabilidade e desempenho do código.
Testes de integração
Os testes de integração vão além dos testes unitários para examinar como diferentes unidades de um programa funcionam em conjunto. Verificar se as funções e os módulos de um programa trabalham juntos é crucial para o desenvolvimento na Solana, onde as interações entre programas costumam ser complexas e podem ter consequências financeiras. O objetivo dos testes de integração é identificar e resolver problemas que não ficam imediatamente evidentes quando as unidades são testadas isoladamente, mas surgem quando os componentes interagem. Esses problemas podem incluir incompatibilidades de formatos de dados, inconsistências de tipos, dependências entre programas ou problemas com APIs de terceiros.
No contexto da Solana, em que programas interagem inerentemente com outros programas, carteiras e oráculos, os testes de integração verificam se essas interações ocorrem conforme esperado. Mesmo que cada unidade funcione perfeitamente, a combinação delas pode introduzir comportamentos inesperados ou ineficiências que aparecem sob essas condições simuladas. Os desenvolvedores podem usar diferentes frameworks de teste para simular diversos fluxos de transações e interações entre programas, reproduzindo de perto cenários reais. Por exemplo, o Bankrun é um framework de teste robusto e leve que permite aos desenvolvedores avançar e retroceder no tempo e definir dinamicamente os dados das contas. Esses recursos não são possíveis ao usar o solana-test-validator. Os testes de integração são essenciais para garantir que um programa seja robusto, confiável e esteja pronto para as exigências das condições da rede Solana.
Testes de ponta a ponta (E2E)
Os testes de ponta a ponta (E2E) são a etapa culminante do processo de testes. Eles se concentram em avaliar o fluxo operacional completo de um programa como ocorreria em cenários reais. Essa metodologia difere dos testes unitários e de integração porque examina o programa pela perspectiva do usuário: todos os fluxos e recursos possíveis que um usuário final encontraria devem funcionar conforme esperado.
Os testes E2E são essenciais para verificar se um programa atende aos requisitos funcionais e oferece uma experiência fluida ao usuário. Essa fase ajuda a revelar problemas que talvez não tenham sido identificados durante os testes unitários ou de integração, como atrasos no processamento de transações, problemas de persistência de estado, otimizações de unidades de computação ou condições de rede inesperadas. Embora os testes E2E geralmente sejam aplicados a uma dApp completa, testar os fluxos operacionais de um programa e verificar como a transação de um usuário interage com suas várias funções e módulos é crucial para criar um programa seguro e bem-sucedido.
Combinando essas metodologias de teste
É crucial usar uma estratégia de testes em camadas e combinar testes unitários, de integração e E2E em seu processo de desenvolvimento. Cada metodologia desempenha um papel distinto no ciclo de vida do desenvolvimento e aborda diferentes aspectos da funcionalidade e do desempenho de um programa.
Os testes unitários são a base de uma abordagem em camadas e permitem que os desenvolvedores identifiquem e resolvam problemas rapidamente no nível mais granular do código. Embora sejam excelentes para garantir a correção objetiva de funções ou módulos individuais, eles não consideram como essas unidades trabalham juntas nem como se integram à experiência do usuário.
Os testes de integração preenchem essa lacuna ao avaliar como diferentes unidades interagem entre si e revelar problemas que surgem durante a integração desses componentes. No entanto, sozinhos, talvez não representem por completo a experiência do usuário final nem o comportamento do programa em condições reais.
Os testes E2E complementam os testes unitários e de integração ao simular cenários reais de usuários e testar as aplicações como um todo. Essa abordagem é valiosa para avaliar a experiência geral do usuário, mas não oferece os insights granulares necessários para identificar e resolver rapidamente problemas específicos.
Ao integrar essas metodologias, os desenvolvedores podem criar um framework de teste robusto que cubra todo o espectro de possíveis problemas. Uma abordagem abrangente não apenas melhora a qualidade e a segurança do programa, como também simplifica o processo de desenvolvimento. Os desenvolvedores podem tomar decisões e fazer revisões rapidamente, com a confiança de que suas alterações serão verificadas em vários níveis. Combinar essas metodologias é crucial para garantir, antes da implantação, que um programa Solana seja tecnicamente sólido e esteja alinhado às expectativas dos usuários em condições reais.
Como escrever bons testes
Escrever testes eficazes é fundamental para desenvolver programas Solana confiáveis e seguros. A essência de um bom teste está em se concentrar no comportamento testado, e não no framework usado nem nos detalhes de implementação do código. Ao integrar os princípios do Desenvolvimento Orientado a Testes (TDD), o padrão Arrange-Act-Assert (AAA) e as melhores práticas do setor, os desenvolvedores podem criar uma estratégia eficiente de testes que melhore a qualidade do código.
Desenvolvimento Orientado a Testes (TDD)
O TDD é uma abordagem robusta de desenvolvimento de software orientada pela criação de testes. Ou seja, a ideia principal é escrever os testes antes do código propriamente dito. O ciclo de TDD geralmente envolve três etapas:
- Escrever um teste que falha: o desenvolvimento deve começar pela criação de um teste para a próxima funcionalidade que o desenvolvedor deseja adicionar. O teste inevitavelmente falhará, pois a funcionalidade testada ainda não existe
- Implementar o código: os desenvolvedores devem escrever a quantidade mínima de código necessária para que o teste passe. O objetivo aqui é velocidade e simplicidade
- Refatorar: depois que o teste passa, os desenvolvedores devem refatorar o código para melhorar sua estrutura e clareza sem alterar o comportamento. O teste aprovado funciona como uma rede de segurança contra a introdução de alterações incompatíveis. Essa etapa pode incluir a remoção de código duplicado, a divisão de métodos em unidades menores, a reorganização de hierarquias de herança ou a adoção de nomes autoexplicativos. No contexto da Solana, essa etapa pode envolver a otimização da quantidade de CUs solicitadas para uma determinada transação, a redução do número de CPIs ou a simplificação dos fluxos de transações.
Embora o TDD não seja necessário para criar bons programas Solana, os desenvolvedores devem considerar sua filosofia: ele promove uma abordagem minuciosa à criação de programas, sua natureza iterativa favorece um processo de desenvolvimento flexível e adaptável e se alinha perfeitamente à necessidade de precisão, segurança e eficiência. O TDD incentiva os desenvolvedores a escrever um código mais limpo, focado e otimizado para os requisitos únicos de rede e desempenho da Solana, como a otimização de CUs.
O padrão Arrange-Act-Assert (AAA)
O padrão AAA oferece uma estrutura simples, mas poderosa, para escrever testes claros, concisos e eficazes. Em essência, ele incentiva uma abordagem disciplinada à criação de testes, dividida em três fases distintas:
- Arrange: comece configurando o ambiente de teste e preparando todas as entradas relevantes. Isso pode envolver gerar contas, simular saldos de contas ou preparar instruções. O objetivo é criar um cenário controlado que reproduza as condições sob as quais o comportamento será testado
- Act: execute o comportamento que está sendo testado. O foco aqui é a ação que aciona o comportamento a ser testado. Por exemplo, o que acontece quando chamo a função x e passo a conta y?
- Assert: avalie o resultado da ação em relação ao resultado esperado. Essa etapa é crucial para verificar se o teste passa ou falha. As asserções podem variar desde uma simples verificação de valor até validações complexas que envolvem várias alterações de estado, por exemplo. A implementação dessas asserções depende, em última análise, do framework ou protocolo utilizado. O Lighthouse é um programa que fornece instruções de asserção que podem ser adicionadas às transações para identificar estados indesejáveis, resultados de simulação falsificados ou gastos excessivos, por exemplo. Exploraremos os benefícios e as particularidades do Lighthouse em mais detalhes em outro artigo.
A força do padrão AAA está em sua adaptabilidade, o que o torna útil para testes unitários, de integração e E2E. Por exemplo:
- Testes unitários: um teste para determinada função pode usar o Arrange para configurar o estado do programa, o Act para invocar a função e o Assert para verificar o valor retornado ou as alterações de estado resultantes
- Testes de integração: testar interações entre vários programas pode envolver o Arrange para implantar os programas e definir seus estados iniciais, o Act para executar as transações relevantes e o Assert para verificar o estado final de cada programa envolvido
- Testes E2E: um teste E2E de um programa pode usar o Arrange para configurar o estado do programa, o Act para percorrer todo um fluxo esperado do usuário — por exemplo, criar uma conta, criar uma proposta, votar nela, encerrar sua fase de votação etc. — e o Assert para comparar os resultados do fluxo com os resultados esperados
O padrão AAA é crucial para o desenvolvimento de programas. Ele impõe uma abordagem centrada no comportamento, necessária para verificar se um programa agirá conforme esperado. Testes estruturados em torno do AAA são mais fáceis de entender e manter, pois cada etapa é claramente dividida entre configuração, ação e verificação. Além disso, o AAA promove a criação de testes independentes e desacoplados, focados em comportamentos ou interações específicos.
Melhores práticas do setor
Escrever bons testes não é algo exclusivo do desenvolvimento na Solana. Podemos aproveitar aprendizados gerais do desenvolvimento de software e aplicá-los aos testes de programas Solana, mantendo o foco nos comportamentos esperados em vez de nos detalhes da implementação.
Por exemplo, testes unitários geralmente devem se concentrar na interface pública de um método, fornecendo argumentos específicos e verificando se os resultados correspondem ao esperado. Essa abordagem garante que os testes unitários continuem válidos mesmo que a implementação interna do método mude, desde que o comportamento permaneça consistente. No contexto do desenvolvimento na Solana, isso significa que alterações na lógica de um programa que não afetem seu comportamento externo não devem exigir nenhuma refatoração dos testes.
Além disso, um erro comum ao escrever testes unitários é torná-los dependentes demais do funcionamento interno do método testado. Isso inclui esperar que certos métodos privados sejam chamados um número específico de vezes ou sejam implementados de determinada maneira. Esses testes são muito frágeis e propensos a falhar após qualquer refatoração do código, mesmo quando o comportamento real do método testado permanece inalterado. Em vez disso, o foco deve estar nos resultados e efeitos colaterais do método observados externamente. É nesse ponto que ferramentas de cobertura de código podem ser usadas para garantir que os testes sejam abrangentes sem depender excessivamente de mecanismos internos.
Adotar essas práticas recomendadas no desenvolvimento na Solana aumenta a robustez e a adaptabilidade dos programas. Priorizar o teste de comportamentos em vez de detalhes de implementação resulta em um código mais resiliente e fácil de manter, algo essencial ao implantar código em um ambiente de rede dinâmico como a Solana. Essa abordagem garante que alterações na lógica do programa não exijam novos testes extensivos e que o programa esteja preparado para implantação.
Agora, vamos começar a escrever alguns testes.
Como escrever testes unitários básicos
Testes unitários em Rust
Rust adota uma abordagem única para testes unitários e incentiva os desenvolvedores a colocar os testes nos mesmos arquivos que o código. Isso é feito por meio do tests módulo e é condicionado pelo atributo #[cfg(test)]. O atributo de teste garante que esses testes só sejam compilados e executados ao testar explicitamente o software com o comando cargo test, ou seja, eles não são executados com o comando cargo build. Os desenvolvedores também podem optar por excluir testes da execução regular com o atributo #[ignore]. Isso é útil para testes particularmente lentos e ainda permite executá-los quando são chamados explicitamente pelo comando cargo test -- --ignored.
Como exemplo, considere a seguinte função em Rust:
pub fn bubble_sort<T: Ord>(array: &mut [T]) {
if array.is_empty() {
return;
}
for i in 0..array.len() {
for j in 0..array.len() - 1 - i {
if array[j] > array[j + 1] {
array.swap(j, j + 1);
}
}
}
}A ordenação por bolha é um tipo de algoritmo de ordenação que percorre repetidamente os elementos de uma lista, compara o elemento atual com o seguinte e troca seus valores quando necessário. Se quiséssemos testar essa função para garantir que ela funciona conforme esperado, poderíamos escrever os seguintes testes:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bubble_sort() {
let mut test1 = vec![12, 39, 4, 36, 777];
assert_eq!(bubble_sort(&mut test1), vec![4, 12, 36, 39, 777]);
let mut test2 = vec![21, 55, 14, -123, 32, 0];
assert_eq!(bubble_sort(&mut test2), vec![-123, 0, 14, 21, 32, 55]);
let mut test3 = vec!["Orange", "Pear", "Apple", "Grape", "Banana"];
assert_eq!(bubble_sort(&mut test3), vec!["Apple", "Banana", "Grape", "Orange", "Pear"]);
}
}Este exemplo mostra como nosso módulo tests é anotado com o atributo #[cfg(test)]. Dentro do módulo, importamos todos os itens públicos do módulo pai para o escopo do módulo de teste atual com use super::*;. Em seguida, temos vários casos de teste nos quais declaramos como os vetores ordenados devem ficar. Rust fornece várias macros para asserções, como assert! para verificações gerais de valores verdadeiros, assert_eq! para igualdade e assert_ne! para desigualdade. Essas asserções são a base da estratégia de testes do Rust: elas são tudo de que você realmente precisa para começar a escrever testes.
Usando um exemplo bastante rudimentar, imagine que você tenha uma função que determina se uma conta possui saldo suficiente para pagar por uma determinada transação:
pub fn has_sufficient_balance(account_balance: u64, transaction_fee: u64) -> bool {
account_balance >= transaction_fee
}Essa função recebe dois argumentos: o saldo atual de uma determinada conta e a taxa esperada da transação. Ela retorna true se o saldo da conta for suficiente para cobrir a taxa da transação ou false caso contrário. Isso pode ser testado facilmente com o seguinte teste unitário:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sufficient_funds_for_transaction() {
let account_balance = 1_000_000;
let transaction_fee = 5_000;
assert!(has_sufficient_balance(account_balance, transaction_fee));
}
#[test]
fn insufficient_funds_for_transaction() {
let account_balance = 1_000;
let transaction_fee = 5_000;
assert!(!has_sufficient_balance(account_balance, transaction_fee));
}
}No primeiro teste, verificamos que has_sufficient_balance retorna true quando o saldo da conta é significativamente maior que a taxa da transação, indicando que há fundos suficientes para cobri-la. No segundo teste, verificamos que has_sufficient_funds retorna false quando o saldo da conta é menor que a taxa da transação, indicando que não há fundos suficientes para cobri-la.
Outras ressalvas importantes sobre testes em Rust
Rust possui o atributo #[should_panic] para marcar testes que devem entrar em pânico sob determinadas condições. Isso é útil para testar fluxos de tratamento de erros e especificar as mensagens de pânico esperadas:
#[test]
#[should_panic(expected = "Divide-by-zero error")]
fn test_divide_by_zero() {
divide_non_zero_result(0, 0);
}Ao contrário de muitas outras linguagens, Rust permite testar funções privadas diretamente. Isso possibilita testes unitários mais detalhados, pois todos os aspectos da funcionalidade do código podem ser cobertos.
Rust também oferece suporte a técnicas mais avançadas de organização de testes:
- Módulos aninhados: em projetos complexos, os testes podem ser organizados em módulos aninhados, permitindo uma estrutura hierárquica clara que reflete a organização do projeto
- Testes baseados em resultados: Rust permite que os testes retornem um tipo
Result<(), E>. Isso permite que os desenvolvedores usem o operador?nos testes, proporcionando um tratamento de erros mais expressivo
Testes unitários em TypeScript com Mocha e Chai
TypeScript se tornou uma escolha popular para testar programas devido ao domínio completo do Anchor como a língua franca do desenvolvimento em Rust na Solana. Com o comando anchor init, o framework de teste Mocha e a biblioteca de asserções Chai são inicializados por padrão em novos projetos Anchor.
Mocha é um framework de testes JavaScript rico em recursos que é executado no Node.js. Isso torna os testes assíncronos muito simples. O principal uso do Mocha no desenvolvimento na Solana é testar a lógica de cliente das dApps e outras interações com a blockchain.
Chai é uma biblioteca de asserções que pode ser combinada com qualquer framework de testes JavaScript, como o Mocha. Ela oferece aos desenvolvedores diversas funções para expressar asserções de maneira legível. As interfaces expect, should e assert do Chai permitem que os desenvolvedores escrevam testes abrangentes e intuitivos de ler e criar. Com as interfaces expect e should, ele usa cadeias de linguagem — ou seja, getters encadeáveis — para melhorar a legibilidade das asserções. Graças ao Chai, escrever expect({a: 1, b: 2}).to.not.have.any.keys(“c”, “d”); resulta em uma asserção válida e muito legível.
Por exemplo, se criarmos um projeto hello_world usando o comando anchor init hello_world, o seguinte arquivo de teste hello_world.ts será criado no diretório hello_world/tests:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloWorld } from "../target/types/hello_world";
describe("hello_world", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.HelloWorld as Program<HelloWorld>;
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods.initialize().rpc();
console.log("Your transaction signature", tx);
});
});Vamos detalhar o que tudo isso significa.
Mocha usa blocos describe para agrupar testes e funções it para definir casos de teste. Este exemplo segue o padrão AAA para oferecer uma abordagem estruturada aos testes:
- Arrange: aqui,
anchor.setProvider(anchor.AnchorProvider.env());configura o cliente Anchor para usar o provedor padrão do ambiente, que geralmente aponta para um validador de teste local da Solana. Em seguida, a declaração constprograminicializa uma instância do programa a ser testado, permitindo chamar seus métodos dentro do teste - Act: em nosso caso de teste
“Is initialized!”, chamamos o métodoinitializedo programa e enviamos a transação - Assert: neste caso de teste, registramos a assinatura da transação sem fornecer uma asserção. Normalmente, é nessa etapa que incorporaríamos o Chai para fazer asserções. Como exemplo bastante rudimentar, poderíamos modificar o código de teste padrão com uma asserção como
expect(tx).to.be.a(“string”);. Um teste mais detalhado poderia recuperar e inspecionar o estado do programa após a inicialização e verificar se ele corresponde aos valores esperados
A combinação do Mocha e do Chai com o padrão AAA, configurado por padrão em projetos Anchor, oferece um framework robusto para testar programas. Os desenvolvedores Solana podem garantir que os programas funcionem de modo previsível e confiável ao organizar claramente o ambiente de teste, executar os métodos do programa e verificar os resultados.
Por exemplo, imagine que você esteja desenvolvendo um programa que permite aos usuários depositar e sacar SOL de um cofre no contexto da Solana. Um teste escrito em TypeScript com Mocha e Chai para garantir que a funcionalidade de depósito funcione conforme esperado poderia ser assim:
import { expect } from "chai";
import { PublicKey } from "@solana/web3.js";
import { depositSOL } from "../src/vault";
describe("Vault Program", function() {
describe("Deposit functionality", function() {
it("should correctly deposit SOL into the vault", async function() {
const vaultPublicKey = new PublicKey(/* vault public key */);
const userPublicKey = new PublicKey(/* user public key */);
const depositAmount = 1; // 1 SOL
const initialVaultBalance = await getVaultBalance(vaultPublicKey);
await depositSOL(vaultPublicKey, userPublicKey, depositAmount);
const finalVaultBalance = await getVaultBalance(vaultPublicKey);
expect(finalVaultBalance).to.equal(initialVaultBalance + depositAmount);
});
});
});Este exemplo testa uma função hipotética depositSOL, que processa o depósito de SOL em um cofre. Ele verifica se o saldo do cofre aumenta no valor correto após o depósito. Usamos a função getVaultBalance, uma função utilitária presumida que busca o saldo atual do cofre.
Outras ressalvas importantes sobre testes em TypeScript com Mocha e Chai
O sistema de tipos estáticos do TypeScript às vezes pode dificultar um pouco a criação de testes, especialmente ao lidar com tipos complexos ou mal definidos. Use asserções de tipo para evitar problemas relacionados a tipos em seus testes. No entanto, garanta que essas asserções não ocultem possíveis erros de runtime decorrentes de tipos incorretos.
Ao criar mocks de objetos ou funções no TypeScript, garanta que as entidades simuladas respeitem os tipos corretos. Bibliotecas como ts-sinon ou ts-mockito podem ajudar a criar mocks com segurança de tipos para que os testes permaneçam precisos e reflitam o comportamento real do programa.
Mocha oferece os métodos only e skip para executar exclusivamente ou ignorar testes específicos. Embora isso seja útil durante o desenvolvimento, é fácil enviar esses métodos por acidente para a produção, resultando em execuções incompletas dos testes. Sempre verifique se há only ou skip antes de enviar os testes para produção. Além disso, tenha cuidado ao usar os hooks do Mocha — isto é, beforeEach, afterEach, before e after — com código assíncrono. Garanta que as promises sejam tratadas corretamente com async/await ou chame o método de callback done para evitar promises não resolvidas ou callbacks não invocados.
Ao usar o expect().to.deep.equal() do Chai, esteja ciente de seu comportamento com objetos que contêm propriedades geradas dinamicamente, como datas ou valores aleatórios. Essas propriedades podem causar falhas inesperadas em testes que esperam igualdade profunda. Quando aplicável, considere usar o expect().to.include() do Chai para asserções mais específicas.
Frameworks populares de testes para Solana
Bankrun
Um banco monitora as contas dos clientes, gerencia a execução dos programas e mantém a integridade e a progressão do ledger da Solana. Essencialmente, ele é um snapshot do ledger em um determinado momento, encapsulando o estado resultante das transações de um bloco específico.
Bankrun é um framework de testes leve e flexível, escrito em Node.js para programas da Solana. Ele prioriza a facilidade de uso e a velocidade, permitindo que os desenvolvedores escrevam e executem testes nos programas rapidamente. O verdadeiro valor do Bankrun é que ele é um framework de testes que permite aos desenvolvedores simular e interagir com bancos da Solana em um ambiente controlado e eficiente. O Bankrun simplifica o processo de testes ao reproduzir a dinâmica dos bancos da Solana sem a sobrecarga normalmente associada à configuração desse tipo de ambiente.
O design do Bankrun se baseia em um BanksServer leve que imita o comportamento de um nó RPC, mas com desempenho e flexibilidade significativamente maiores. Os desenvolvedores podem interagir com esse servidor por meio do BanksClient. Esse cliente oferece um conjunto abrangente de ferramentas, com métodos para consultar saldos de contas e status de transações, além de simular transações. Vale destacar que o método tryProcessTransaction permite processar transações que devem falhar sem gerar erros de JavaScript. Isso permite que os desenvolvedores verifiquem diretamente modos de falha ou mensagens de log específicos.
O repositório do Futarchy da Meta-DAO no GitHub é um ótimo exemplo de uso do Bankrun para testar código pronto para produção.
Integração com o Anchor
Integrar o Bankrun ao Anchor é muito simples. Com startAnchor, os desenvolvedores podem implantar automaticamente no ambiente de testes todos os programas de um workspace do Anchor. Isso garante que os testes reproduzam com precisão o comportamento do programa em um ambiente Solana completo. A documentação do Bankrun apresenta o seguinte exemplo de código:
import { startAnchor } from "solana-bankrun";
import { PublicKey } from "@solana/web3.js";
test("anchor", async () => {
const context = await startAnchor("tests/anchor-example", [], []);
const programId = new PublicKey(
"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
);
const executableAccount = await context.banksClient.getAccount(programId);
expect(executableAccount).not.toBeNull();
expect(executableAccount?.executable).toBe(true);
});O pacote anchor-bankrun é uma extensão poderosa que viabiliza o uso conjunto do Anchor e do Bankrun ao exportar uma classe BankrunProvider, que pode ser usada como substituta direta de AnchorProvider durante os testes.
Gravação de contas arbitrárias com o Bankrun
Um recurso de destaque do Bankrun é sua capacidade de gravar dados arbitrários em contas. Essa funcionalidade permite que os desenvolvedores contornem as limitações dos estados das contas, oferecendo um nível de flexibilidade sem precedentes. Por exemplo, um desenvolvedor pode simular uma conta com uma quantidade significativa de USDC sem possuir o par de chaves de emissão do USDC. Isso é extremamente valioso para testes, pois elimina a necessidade de manipular tokens reais e simplifica a configuração de cenários complexos.
A documentação do Bankrun apresenta um exemplo de código para emissão infinita de USDC, demonstrando esse recurso por meio da função start. A função start prepara o ambiente de testes implantando programas e definindo os dados das contas conforme especificado.
Viagem no tempo
Outro recurso independente é a capacidade do Bankrun de viajar no tempo, ou seja, manipular o conceito de tempo para fins de teste. A possibilidade de manipular o tempo permite que os desenvolvedores avancem ou retrocedam instantaneamente o relógio do cluster da Solana, isto é, a sysvar Clock, para simular condições temporais específicas. Esse recurso é essencial para testar programas baseados em lógica temporal, incluindo cronogramas de vesting, bloqueios de tokens ou qualquer funcionalidade acionada quando um determinado momento é alcançado.
Viajar no tempo é simples graças ao método setClock. Esse método permite que os desenvolvedores definam o horário atual do cluster como um timestamp Unix predefinido, deslocando todo o ambiente de testes para esse momento no passado ou no futuro. As operações e transações do teste continuam como se o horário especificado fosse o atual, permitindo avaliar com precisão o comportamento do programa nessas condições.
Veja um exemplo bastante básico de como viajar no tempo com o Bankrun:
import { start } from "solana-bankrun";
import { PublicKey, Transaction, SystemProgram } from "@solana/web3.js";
async function simulateTimeTravel(context, secondsForward) {
const newTimestamp = context.clock.unixTimestamp + secondsForward;
context.adjustClock(newTimestamp);
}
test("One Year Later...", async () => {
const context = await start([], []);
const { banksClient, payer } = context;
// Simulate setting the cluster clock forward by one year (in seconds)
const oneYearInSeconds = 365 * 24 * 60 * 60;
await simulateTimeTravel(context, oneYearInSeconds);
// Proceed with tests assuming the future time
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: PublicKey.unique(),
lamports: 100,
}),
);
transaction.recentBlockhash = context.lastBlockhash;
transaction.sign(payer);
await banksClient.processTransaction(transaction);
// Add assertions here to test expected future behavior
});Bankrun vs. solana-test-validator
A escolha entre o Bankrun e o solana-test-validator depende, em grande parte, dos requisitos específicos dos cenários de teste. A velocidade, a flexibilidade e os recursos especializados do Bankrun fazem dele a opção preferencial para a maioria dos cenários de desenvolvimento, especialmente os que exigem iterações rápidas ou simulações detalhadas. No entanto, o solana-test-validator continua relevante para testes que dependem do comportamento real de um validador e do uso de métodos RPC não compatíveis com o BanksServer.
solana-program-test
O crate solana-program-test oferece um framework de testes baseado em Rust e desenvolvido especificamente para programas da Solana. Esse framework é centrado no BanksClient. Ele simula as operações de um banco da Solana, permitindo que os desenvolvedores implantem, utilizem e avaliem o comportamento dos programas em condições de teste semelhantes às da mainnet, assim como o Bankrun. Complementando o BanksClient, há a struct ProgramTest, um utilitário para inicializar o ambiente de testes. Ou seja, ela facilita o desenvolvimento dos programas especificados e a configuração das contas necessárias. Outras structs, como BanksTransactionResultWithMetadata, InvokeContext e ProgramTestContext, oferecem informações e contexto detalhados sobre as transações processadas durante os testes, aprimorando o processo geral de depuração e verificação.
Para simplificar o desenvolvimento e os testes locais, o solana-program-test pré-carrega automaticamente vários programas:
- SPL Token (e sua versão de 2022)
- SPL Memo (versões 1.0 e 3.0)
- SPL Associated Token Account
Esses programas pré-carregados oferecem uma configuração de testes mais rápida e direcionada, pois não é necessário configurar manualmente esses programas comuns.
O Marginfi tem em seu repositório do GitHub vários ótimos exemplos de implementação do solana-program-test em código pronto para produção. O guia de desenvolvimento da Bonfida também apresenta um ótimo passo a passo para escrever testes de integração usando o framework solana-program-test.
solana-test-framework
O solana-test-framework é uma extensão do solana-program-test desenvolvida pela Halborn. Ele foi criado para enriquecer o ambiente de testes ao estender BanksClient, RpcClient, ProgramTest e ProgramTestContext com vários métodos utilitários. Assim como no Bankrun, as extensões do ProgramTestContext, por exemplo, permitem cenários avançados de teste nos quais os desenvolvedores podem avançar para timestamps específicos e atualizar os preços dos oráculos.
Essas extensões oferecem as seguintes melhorias:
- Gerenciamento de transações: simplifica a montagem, a assinatura e o pagamento de transações por meio de
transaction_from_instructions - Desserialização de contas: facilita, respectivamente, a consulta e a desserialização de contas Anchor e Borsh com
get_account_with_anchor and get_account_with_borsh - Criação de contas e implantação de programas: permite que os desenvolvedores configurem o ambiente de testes com eficiência usando funções como
create_account,create_token_mint,create_token_accountedeploy_program.
O solana-test-framework é compatível tanto com clusters externos quanto com runtimes simulados. Ele funciona com várias versões da Solana e do Anchor, incluindo as versões 1.9 a 1.14 da Solana e as versões correspondentes 1.9, 1.10 e 1.14 do Anchor.
Exemplo de cenário de teste
O programa
Considere o seguinte programa como exemplo:
use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_instruction;
use solana_program::program::invoke;
declare_id!("3vMZa7r3CpHGejvXYbUpPXmm54FxCDPF1QAYnnzL88J9");
#[program]
pub mod king_of_the_hill {
use super::*;
pub fn initialize(ctx: Context<Initialize>, initial_prize: u64) -> Result<()> {
// In case the person who went first didn't send any SOL as the initial prize
require!(initial_prize > 0, ErrorCode::NeedAnInitialPrize);
let game_state = &mut ctx.accounts.game_state;
game_state.king = ctx.accounts.initial_king.key();
game_state.prize = initial_prize;
let transfer_instruction = system_instruction::transfer(
&ctx.accounts.initial_king.key(),
&ctx.accounts.prize_pool.key(),
initial_prize,
);
invoke(
&transfer_instruction,
&[
ctx.accounts.initial_king.to_account_info(),
ctx.accounts.prize_pool.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
Ok(())
}
pub fn become_king(ctx: Context<BecomeKing>, new_prize: u64) -> Result<()> {
require!(
new_prize > ctx.accounts.game_state.prize,
ErrorCode::BidTooLow
);
let transfer_to_pool_instruction = system_instruction::transfer(
&ctx.accounts.payer.key(),
&ctx.accounts.prize_pool.key(),
new_prize,
);
// Send the new king's funds to the pool
invoke(
&transfer_to_pool_instruction,
&[
ctx.accounts.payer.to_account_info(),
ctx.accounts.prize_pool.to_account_info(),
ctx.accounts.system_program.to_account_info(),
],
)?;
// Send the old king's funds back
ctx.accounts.prize_pool.sub_lamports(ctx.accounts.game_state.prize);
ctx.accounts.king.add_lamports(ctx.accounts.game_state.prize);
ctx.accounts.game_state.king = ctx.accounts.payer.key();
ctx.accounts.game_state.prize = new_prize;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(
init,
payer = initial_king,
space = 8 + 32 + 8 + 1,
seeds = [b"game_state"],
bump,
)]
pub game_state: Account<'info, GameState>,
#[account(mut)]
pub initial_king: Signer<'info>,
#[account(
init,
payer = initial_king,
space = 8 + 8,
seeds = [b"prize_pool"],
bump,
)]
/// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
pub prize_pool: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct BecomeKing<'info> {
#[account(
mut,
has_one = king,
)]
pub game_state: Account<'info, GameState>,
#[account(mut)]
/// CHECK: This is okay - it's only receiving SOL and we don't need any other access
pub king: UncheckedAccount<'info>,
#[account(mut)]
pub payer: Signer<'info>,
#[account(
mut,
seeds = [b"prize_pool"],
bump,
)]
/// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
pub prize_pool: UncheckedAccount<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct GameState {
pub king: Pubkey,
pub prize: u64,
pub prize_pool_bump: u8,
}
#[error_code]
pub enum ErrorCode {
#[msg("The initial prize must be greater than zero")]
NeedAnInitialPrize,
#[msg("The bid must be higher than the current prize")]
BidTooLow,
#[msg("Invalid prize pool account")]
InvalidPrizePoolAccount,
}Esse programa implementa um jogo simples de “Rei da Colina” na Solana. O jogo permite que os usuários se tornem o “rei” enviando mais SOL ao pool de prêmios do que o rei atual enviou. Quando um novo rei assume o lugar, o SOL enviado pelo rei anterior é devolvido a ele.
O programa funciona da seguinte forma:
- Inicializar: essa função configura o jogo com um rei inicial, isto é, o primeiro jogador a inicializar o jogo, e um valor inicial de prêmio. O prêmio inicial deve ser maior que zero. Em seguida, a função transfere o prêmio inicial do rei inicial para um pool de prêmios
- Tornar-se rei: essa função permite que um novo jogador se torne rei ao oferecer mais SOL do que o prêmio atual. Ela transfere o prêmio atual para o rei que está deixando o posto e atualiza o pool de prêmios com a oferta do novo rei, tornando-o o novo rei. Essa oferta deve ser maior que o prêmio atual
Como escrever testes
Podemos testar o jogo Rei da Colina com sucesso usando o seguinte código:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";
import { assert } from "chai";
const web3 = require("@solana/web3.js");
describe("King of the Hill Tests", () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.KingOfTheHill
as Program<KingOfTheHill>;
let initialKing, newKing;
let gameStatePDA, prizePoolPDA;
// Utility function for airdrops
async function fundWallet(account, amount) {
const publicKey = account.publicKey ? account.publicKey : account;
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(publicKey, amount),
"confirmed"
);
}
before(async () => {
initialKing = web3.Keypair.generate();
newKing = web3.Keypair.generate();
await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);
[gameStatePDA] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("game_state")],
program.programId
);
[prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("prize_pool")],
program.programId
);
});
it("Initializes the game correctly", async () => {
// Arrange
await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);
let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);
// Act
const tx = await program.methods
.initialize(initialPrize)
.accounts({
gameState: gameStatePDA,
initialKing: initialKing.publicKey,
prizePool: prizePoolPDA,
systemProgram: web3.SystemProgram.programId,
})
.signers([initialKing])
.rpc();
// Assert
let gameState: any = await program.account.gameState.fetch(gameStatePDA);
assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
assert.equal(
gameState.prize.toString(),
new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
);
});
it("Changes the king correctly", async () => {
// Arrange
const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);
// Act
const becomeKingTx = await program.methods.becomeKing(newPrize)
.accounts({
gameState: gameStatePDA,
king: initialKing.publicKey, // Correct usage of current king
payer: newKing.publicKey, // New king who pays and becomes the king
prizePool: prizePoolPDA,
systemProgram: web3.SystemProgram.programId,
})
.signers([newKing]) // Signing by newKing
.rpc();
// Assert
const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);
const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");
// Fetch the updated game state.
const updatedGameState = await program.account.gameState.fetch(gameStatePDA);
// Assertions to confirm the state has updated as expected.
assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
})
});Vamos analisar tudo em detalhes.
Primeiro, começamos com as importações e a configuração do ambiente de testes no Anchor. Neste exemplo, estou testando em TypeScript com Mocha e Chai no localhost:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";
import { assert } from "chai";
const web3 = require("@solana/web3.js");Usamos describe para agrupar nossos casos de teste. Também configuramos o cliente para usar o cluster local, definimos o programa corretamente, inicializamos as variáveis para o rei inicial, o novo rei, a PDA do estado do jogo e a PDA do pool de prêmios, respectivamente, e criamos uma função utilitária para facilitar os airdrops:
describe("King of the Hill Tests", () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.KingOfTheHill as Program<KingOfTheHill>;
let initialKing, newKing;
let gameStatePDA, prizePoolPDA;
// Utility function for airdrops
async function fundWallet(account, amount) {
const publicKey = account.publicKey ? account.publicKey : account;
await provider.connection.confirmTransaction(
await provider.connection.requestAirdrop(publicKey, amount),
"confirmed"
);
}
// Other code
});Em seguida, usamos o hook before para configurar e financiar os pares de chaves do rei inicial e do novo rei, além de derivar as PDAs do estado do jogo e do pool de prêmios. Esse bloco será executado uma vez antes dos casos de teste, permitindo simplificar cada caso de forma mais clara no padrão AAA:
before(async () => {
initialKing = web3.Keypair.generate();
newKing = web3.Keypair.generate();
await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);
[gameStatePDA] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("game_state")],
program.programId
);
[prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("prize_pool")],
program.programId
);
});O primeiro caso de teste é bastante simples: verificamos se o jogo é inicializado corretamente. Na preparação, financiamos as PDAs do estado do jogo e do pool de prêmios para podermos interagir com elas posteriormente e definimos o prêmio inicial como 1 SOL. Depois, executamos a ação chamando o método initialize com o initialPrize. Para as contas, passamos a PDA do estado do jogo, o rei inicial, a PDA do pool de prêmios e o programa do sistema. O rei inicial é o signatário dessa ação. Por fim, verificamos se o estado do jogo atualizou corretamente o rei e o prêmio:
it("Initializes the game correctly", async () => {
// Arrange
await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);
let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);
// Act
const tx = await program.methods
.initialize(initialPrize)
.accounts({
gameState: gameStatePDA,
initialKing: initialKing.publicKey,
prizePool: prizePoolPDA,
systemProgram: web3.SystemProgram.programId,
})
.signers([initialKing])
.rpc();
// Assert
let gameState: any = await program.account.gameState.fetch(gameStatePDA);
assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
assert.equal(
gameState.prize.toString(),
new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
);
});O próximo caso de teste garante que outra pessoa possa se tornar rei. Na preparação, consultamos o saldo inicial do rei e definimos o novo prêmio como 2 SOL. Em seguida, chamamos a função becomeKing e passamos o valor mais recente do prêmio. Para as contas, passamos a PDA do estado do jogo, a chave pública do rei atual, o novo rei como pagador, a PDA do pool de prêmios e o programa do sistema. O novo rei é definido como signatário. Por fim, verificamos se o rei recebe o SOL inicial que contribuiu para se tornar rei e se o estado do jogo foi atualizado corretamente:
it("Changes the king correctly", async () => {
// Arrange
const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);
// Act
const becomeKingTx = await program.methods.becomeKing(newPrize)
.accounts({
gameState: gameStatePDA,
king: initialKing.publicKey, // Correct usage of current king
payer: newKing.publicKey, // New king who pays and becomes the king
prizePool: prizePoolPDA,
systemProgram: web3.SystemProgram.programId,
})
.signers([newKing]) // Signing by newKing
.rpc();
// Assert
const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);
const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");
// Fetch the updated game state.
const updatedGameState = await program.account.gameState.fetch(gameStatePDA);
// Assertions to confirm the state has updated as expected.
assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
})Neste cenário de teste, examinamos as principais funcionalidades do nosso programa Rei da Colina. Com testes unitários, validamos a integridade da lógica do programa em um nível granular. Ao testar se o rei muda corretamente, também confirmamos que o programa funciona conforme o esperado em condições simuladas do mundo real. Esses testes ressaltam a importância de uma estratégia abrangente para garantir a qualidade e a funcionalidade do nosso programa Rei da Colina.
Conclusão
Os testes são a base do desenvolvimento de programas da Solana seguros, confiáveis e eficientes. Neste artigo, exploramos a importância de combinar testes unitários, de integração e E2E para abranger todas as etapas do ciclo de vida do desenvolvimento de programas. Ao integrar essas metodologias e usar frameworks de testes poderosos, como Bankrun, solana-program-test e solana-test-framework, os desenvolvedores podem melhorar significativamente a qualidade dos seus programas da Solana. Conforme você avança em sua jornada como desenvolvedor da Solana, use os princípios, as práticas e os exemplos apresentados aqui como orientação para criar programas robustos, eficientes e seguros.
Se você leu até aqui, agradecemos, anon! Insira seu endereço de e-mail abaixo para nunca perder nenhuma novidade sobre a Solana. Quer se aprofundar? Confira os artigos mais recentes no blog da Helius e continue hoje mesmo sua jornada na Solana.
Recursos adicionais
Artigos relacionados
Assine a Helius
Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos


