Smart Contract CRUD em Solidity com Truffle e JS

Cripto

Smart Contract CRUD em Solidity com Truffle e JS

Luiz Duarte
Escrito por Luiz Duarte em 23/09/2022
Junte-se a mais de 34 mil devs

Entre para minha lista e receba conteúdos exclusivos e com prioridade

Todas as vezes que comecei a aprender uma nova linguagem de programação, logo depois de escrever o primeiro Olá Mundo, eu inicio um projeto de CRUD. CRUD é uma abreviação para Create, Retrieve, Update e Delete, ou seja, as 4 operações elementares em um sistema de cadastro. E com Solidity não foi diferente, ajudando a descobrir diversas coisas fundamentais desta tecnologia. Neste tutorial de hoje eu vou lhe ensinar como criar um smart contract de CRUD de clientes com Solidity, Truffle e testes escritos em JavaScript.

Este não deve ser o seu primeiro contato com Solidity ou com Truffle, para isso recomendo que tenha feito este tutorial antes.

Caso prefira, o vídeo abaixo possui PRATICAMENTE o mesmo conteúdo, salvo algumas adições que fiz aqui no blog. Então mesmo que assista ao vídeo, dê uma olhada no projeto pronto ao final do post para aprender algumas coisas adicionais.

#1 – Criando o Projeto

Para criar esse projeto você vai precisar ter Node.js e Truffle instalados na sua máquina. Crie uma pasta crud-solidity onde for mais prático para você, abra o terminal e navegue até ela, executando o comando para criação do projeto Truffle de exemplo.

Agora limpe as pastas contracts e test, além de deixar somente a primeira migration na respectiva pasta.

Na pasta contracts, crie um novo arquivo chamado StoreCustomers.sol, que será o nosso contrato de CRUD de clientes de uma loja fictícia (store = loja). Dentro dele, defina a licença, a versão e deixe a estrutura inicial do contrato pronta, como abaixo.

Repare que estou usando a versão 0.8.17 ou superior, você deve usar a versão que possuir instalada na sua máquina, a partir da que informei acima. Para fixar essa versão na compilação, abra seu arquivo truffle-config.js e ajuste o campo version do compilador (solc) e aproveite para remover as redes (networks) também, ficando com um arquivo como abaixo.

Na pasta migrations, ajuste o arquivo de migration que restou para que ele aponte para nosso contrato, evitando erros de compilação, mesmo que a gente não vá fazer deploy deste contrato agora.

E por fim, vamos deixar a estrutura de nossos testes preparada, criando na pasta test um arquivo StoreCustomers.test.js e deixando a estrutura abaixo nele.

Aqui estamos carregando o contrato Solidity (linha 1), definindo um bloco de testes de nome ‘StoreCustomers’ e dizendo que antes de cada teste (beforeEach) o contrato deve ser inicializado como novo, o que vai garantir a independência entre cada teste e a limpeza dos dados do contrato.

Com isso nosso projeto está pronto para implementarmos o CRUD.

#2 – Cadastrando Clientes

A primeira operação do CRUD que vamos fazer é o cadastro. Como em Solidity tudo é persistido no disco por padrão, basta que a gente crie alguma estrutura de dados e guarde um objeto dentro dela e teremos o mesmo salvo na blockchain. Para que não vire uma bagunça e tenhamos uma estrutura para estes dados, eu proponho que a gente comece criando uma struct com os parâmetros que todo cliente deve ter.

Uma struct, caso não conheça, é uma estrutura de dados que define os campos que um objeto deve ter, como se fosse uma classe anêmica, ou seja, sem funções. Em Solidity, podemos definir structs assim (dentro do contrato).

Isso quer dizer que toda vez que quisermos salvar um cliente, precisamos definir o nome e idade dele (adicione outras propriedades conforme seu interesse). Repare aqui que usei uint8 para idade, pois não teremos idades negativas e em 8-bit a gente consegue representar até o número 255, mais do que suficiente para idade, lembrando que na blockchain nós vamos pagar mais caro por dados maiores, então é bom economizar nas variáveis.

Agora que temos nossa struct, podemos criar uma estrutura que armazene vários clientes em nosso contrato. Optei por usar um mapping, que é como se fosse um array associativo ou dicionário de objetos, mas o termo mais correto seria que um mapping é um hashmap, ou seja: dado um índice definido por você (um id único) ele aponta (map = mapeia) para onde está armazenada uma informação. Esse tipo de estrutura é muito eficiente para busca e inserção.

Para definir nosso mapping eu vou determinar que cada cliente vai ser referenciado pelo seu id único positivo de 32-bit (uint32).

Repare como disse que o id aponta para objeto Customer, que é o tipo da struct que definimos antes. Além disso coloquei que nosso mapping customers é público, ou seja, pode ser lido por qualquer um na blockchain.

Para podermos adicionar novos clientes neste mapping de clientes precisamos ter alguma forma de gerar ids únicos, a fim de que cada cliente tenha seu próprio id. Para isso, vou criar uma propriedade de controle e uma função que sempre me retorna o próximo id único a ser utilizado, como abaixo. Lembrando que Solidity guarda os dados na blockchain e nela tudo salva no disco por padrão, então estes ids estão sempre salvos corretamente.

Comecei definindo nosso id como zero e deixando-o como privado. Depois disso defini uma função que incrementa e retorna o id a ser usado no cadastro de um novo cliente. Repare que também deixei a função como privada pois é para uso interno do nosso contrato apenas.

Agora podemos criar a função pública que adiciona um novo cliente com um novo id único, como abaixo.

Como o nosso struct Customer não é um tipo primitivo, devemos usar o modificador memory na declaração do parâmetro dele. Isso quer dizer que por parâmetro devemos receber um objeto Customer, gerar um id para ele e salvá-lo em nosso mapping com o id sendo a chave e o customer sendo o valor.

Para testar esta nossa função Solidity nós devemos criar um teste JS em nosso arquivo de testes. No entanto, as funções que salvam dados na blockchain sempre retornam recibos de transação e não algum valor que possa ser facilmente verificado em uma asserção (assert). Para mudarmos isso, vamos adicionar uma variável de contagem em nosso contrato, para sabermos quantos clientes já cadastramos nele, como abaixo.

Além de declarar a variável de contagen, adicionei mais uma linha na função de cadastro para incrementar o count a cada adição. Com isso podemos escrever o nosso teste que cadastra um novo cliente e verifica se a contagem aumentou. Para quem já escreveu testes em Jest ou Tape antes, não tem nenhuma novidade aqui.

Repare aqui o uso da função toNumber() pois a função count e muitas outras outras que retornam numéricos da blockchain usam o tipo BigNumber ao invés do Number normal do JS, para permitir a manipulação de números gigantes. Não é exatamente uma boa prática usar o toNumber, mas por ora vai funcionar.

Para rodar nosso teste, execute o comando ‘truffle test’ no terminal e tudo deve passar como esperado.

#3 – Retornando um Cliente

Agora vamos fazer o R do CRUD, criando uma nova função em nosso smart contract para retornar um cliente já salvo.

Essa função Solidity é bem simples pois ela espera o id por parâmetro e usa ele como chave no mapping para retornar o Customer. Apenas atenção ao fato dela ser view (somente leitura) e que retorna um Customer memory, já que não preciso salvar o retorno em disco, ele deve ser em memória.

Para escrever o teste desta função em nosso arquivo de testes é bem simples também, como abaixo.

Como o beforeEach garante que meu contrato seja inicializado como novo a cada teste, eu terei de fazer um cadastro inicial e depois posso chamar o getCustomer para buscar o cliente de id 1 e ver se retornou o cliente de nome certo.

Em muitos testes de smart contracts você vai ter de ou deixar uma carga de dados pronta antes de todos testes (beforeAll) ou preparar o teste logo no início dos mesmos com os dados necessários, um a um.

Rode os testes com truffle test e deve estar tudo passando.

Curso Node.js e MongoDB

#4 – Atualizando um Cliente

Agora vamos para o U de Update em nosso CRUD. Aqui temos uma complicação que a função de edit vai sempre receber um objeto Customer mas nem sempre iremos querer alterar todos os dados de um cliente. Sendo assim, devemos criar uma lógica para comparar campo a campo e alterar somente aqueles que mudaram de fato.

Até aí tudo bem, exceto pelo fato de que o campo nome é uma string. String são um problema em linguagens como C e Solidity pois elas não são tipos primitivos e são armazenadas de um jeito que dificulta a sua comparação. Abaixo proponho uma função simples para comparação de strings que atende bem para casos simples.

Dada duas strings recebidas em memóry (a e b), nós pegamos os bytes deles e geramos um hash com eles, usando a função keccak256 (este é um algoritmo de hashing nativo do Solidity). Se os hashes de ambas forem iguais, quer dizer que eram a mesma palavra. Achou complicado? Outra opção é você fazer um for e comparar byte a byte da string, já que a comparação direta com sinais de igualdade não funciona com a string inteira.

Agora que temos a nossa função de comparação de string retornando um booleano quando duas strings forem iguais, podemos criar a nossa função de edição que compara campo a campo para alterar somente aqueles que foram informados e que mudaram em relação aos valores já armazenados.

Repare que eu começo testando se o nome do cliente antigo tem comprimento (length). Isso porque caso o id passado não seja de nenhum cliente, o Solidity vai me retornar um cliente com campos vazios ao invés de um erro ou null como seria comum em outras linguagens. Neste caso, eu não posso editar um cliente que não existe, certo?

Depois disso, a gente testa, campo a campo, se o campo foi informado na edição e se ele é diferente do valor antigo naquele campo do cliente. Se os valores divergirem fazemos a troca e no final de tudo, a adição por cima do registro antigo usando o mesmo id no mapping.

Agora, para escrevermos o teste desta função nós precisamos fazer o cadastro antes da edição e depois a consulta para ver se somente o campo que passamos, mudou.

Repare como informei idade 0 na edição, para que ela seja ignorada e se mantenha a idade original do cadastro. Assim no teste consigo testar os dois campos, para ver se ficaram como espero, apenas com o nome alterado.

Se rodar a bateria de testes com truffle test, todos devem passar.

#5 – Excluindo um Cliente

Agora vamos para a quarta e última operação do CRUD: o D de Delete. Aqui vamos inventar um complicador para aprendermos um pouco mais de Solidity. Vou definir uma regra que somente o criador do contrato pode excluir clientes. Não me pergunte se faz sentido, é apenas uma brincadeira para aprendermos algo novo.

Como criador do contrato você tem duas opções: um endereço de carteira arbitrário ou então pegar o endereço da carteira que fez o deploy do contrato na blockchain. Eu vou usar a segunda opção por ela envolver mais conceitos novos, mas é fácil de você mudar para a primeira se assim desejar.

Quando você faz o deploy do contrato o seu construtor é chamado. Nesta invocação, podemos pegar o endereço da carteira de quem fez o send (envio de transação) e armazená-lo de forma imutável em nosso contrato como sendo o dono (owner) dele. Abaixo um exemplo de como fazer isso.

O tipo address é um tipo primitivo do Solidity para armazenamento de endereços (carteiras, contratos, etc), como a variável owner é um endereço de carteira, nos atende muito bem. Além disso deixarei ela como privada e como immutable, o que faz com que ela seja uma constante que eu posso definir uma única vez, via constructor do contrato.

No constructor eu faço essa definição, pegando o msg.sender que nada mais é do que o endereço da carteira que está enviando a transação atual (msg, que é um objeto global). Como o constructor é chamado somente no deploy do contrato, a transação atual será a de deploy, feita pelo dono do mesmo. Ao invés disso, caso deseje deixar um owner fixo no código, troque immutable para constant e defina o valor de owner com o endereço no formato hexadecimal (0x…).

Tudo isso para que, na nossa função de remoção de clientes a gente possa exigir que somente o dono possa enviar transações de exclusão. Essa exigência é feita através da chamada require logo no início da função, que determina a condição para que a transação possa avançar, comparando o sender da msg com o owner já armazenado no contrato.

Se a exigência for atendida, a execução avança pegando o cliente já salvo no mapping, para ver se ele existe, excluindo ele se existir e decrementando o contador de clientes para que a contagem se mantenha fiel.

Repare que eu somente faço o teste de existência do cliente para evitar decrementar o count sem necessidade, a fim de que ele realmente esteja preciso na sua contagem.

Para escrever o teste desta função é bem simples, devemos apenas cadastrar um cliente primeiro, depois removê-lo e por fim ver a contagem.

Aqui eu incluí um objeto de configurações da transação na chamada ao removeCustomer. Quando você passar accounts[0] para o from, você está dizendo que a transação deve ser invocada pela primeira conta de teste disponível, que naturalmente é a mesma que faz o deploy e o teste deve passar normalmente.

No entanto, experimente usar accounts[1] para você ver que deve dar erro no teste, pois assim que estará chamando a função, e consequentemente tentando fazer a transação, não será a mesma conta que fez o deploy e portanto não atende à exigência do require no contrato.

Com isso, ao rodar o comando truffle test, todos os seus testes devem estar passando, como abaixo. E se quiser fazer o deploy na blockchain, dá uma olhada neste tutorial e se quiser aprender a fazer frontends para seus smart contracts, leia este tutorial. E se quiser aprender a reduzir o consumo de gás nos seus smart contracts, este é o artigo certo.

Agora se estiver buscando algo mais complexo, o caminho é esse tutorial de criação de novos tokens ERC-20.

Até a próxima!

 

Olá, tudo bem?

O que você achou deste conteúdo? Conte nos comentários.

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *