Como criar NFTs usando Solidity e HardHat (JS) - Parte 2

Cripto

Como criar NFTs usando Solidity e HardHat (JS) - Parte 2

Luiz Duarte
Escrito por Luiz Duarte em 02/12/2023
Junte-se a mais de 34 mil devs

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

Recentemente escrevi um tutorial ensinando como implementar um contrato de NFTs usando HardHat, Solidity e TypeScript, além da biblioteca de contratos OpenZeppelin que segue a especificação ERC-721. Se você não fez este primeiro tutorial, corre lá que é obrigatório pois nessa parte 2 vamos escrever apenas os testes automatizados para nossos contratos, ok? Link aqui.

Outra recomendação é você entender primeiro do padrão de NFT em si, algo que abordei em outro tutorial de NFT ERC721 onde escrevemos todo o contrato do zero, pois é ali que construímos o conhecimento de como tudo funciona, permitindo que a gente consiga saber como testar um NFT também.

Então mãos à obra pois temos muitos testes a escrever!

Curso Node.js e MongoDB

Principais testes do nosso contrato

Comece abrindo o arquivo MyNFT.test.ts na pasta test e vamos escrever nosso primeiro teste, que irá verificar se o nome e o symbol da nossa coleção estão corretos.

Começamos os testes sempre chamando a função deployFixture que criamos anteriormente para pegar uma instância zerada do contrato a fim de manter a independência entre os testes. Na sequência chamamos a função em questão que iremos testar: name() no primeiro teste e symbol() no segundo. No final das contas a asserção é simples, com uso de .to.equal.

Eu não vou ficar citando, mas a cada teste codificado, o ideal é rodar a bateria toda com o comando ‘npx hardhat test’ a ver se estão passando, ok? Outra possibilidade é rodar com ‘npx hardhat coverage’ para ter relatórios de cobertura de código.

O teste seguinte é mais complexo, para verificar se estamos conseguindo mintar novos tokens.

Aqui eu resolvi fazer um teste de integração bem completo ao invés de unitário, para ganharmos tempo. Começamos carregando a instância do contrato e o owner que fez o deploy dele. Depois, chamamos a função de mint() em si, enquanto que o resto todo do código são as variáveis de verificação se o minting funcionou. Para isso eu optei por verificar se o saldo do owner é 1, se o token na posição 0 global é igual ao token na posição 0 do owner, se o dono do token é o owner e se o total supply de tokens na coleção é 1.

Opcionalmente você pode quebrar este grande teste em vários testes menores ou então até mesmo criar variações, testando de forma diferente.

Agora vamos escrever o teste para a funcionalidade de burn.

Aqui nós temos de primeiro mintar um token, já que precisamos que exista algum para queimá-lo. Então logo após o mint chamamos a função de burn e o restante do código são para coleta de indícios que comprovem que o burn foi efetuado com sucesso como verificar se o total supply está zerado e se o saldo do owner também está zerado.

Outra variação de teste da funcionalidade de burn diz respeito a burn delegado, isto é, o owner do token pode ceder o controle dele a outra pessoa, que pode decidir por queimar o token. Isso envolve uma série de mecanismos internos e é interessante de ser testado.

Esta variação é bem mais complexa que o teste de burn tradicional pois envolve aprovar um operador/controlador diferente para o token após mintá-lo. Então usamos a função connect para gerar uma conexão com o contrato se fazendo passar por outra carteira e realizamos o burn a partir dela. O restante do código é a coleta de evidências do burn.

Existe ainda mais uma variação do burn que é quando a delegação do controle é total sobre a coleção de um owner.

O teste é praticamente idêntico ao anterior, então dispensa maiores explicações. Mas e os cenários de fracasso? Para o mint não creio que tenhamos algum que valha a pena testar, mas para o burn nós temos ao menos dois.

No primeiro cenário nós tentamos queimar um token que não foi mintado, o que deve gerar um custom error que especifiquei. Este custom error eu peguei diretamente do ERC721Burnable.sol na biblioteca OpenZeppelin. No segundo teste, uma segunda conta tenta queimar um token que não é dela e que não possui o controle, obviamente obtendo fracasso.

Agora vamos implementar os testes ligados aos metadados dos nossos NFTs. Vou criar um teste que retorna com sucesso a URL do JSON de metadados e outra que fracassa pois o token em questão não foi mintado ainda.

Repare que você terá de ajustar o primeiro teste à sua realidade de URL, e lembro também que esta minha URL é fictícia, apenas para fins didáticos, não vai funcionar se tentar acessá-la no navegador, mas em um cenário real, deveria.

Testes de Transferência

Agora todos os demais testes são questionáveis de serem feitos. Isso porque dos códigos que escrevemos, os testes acima cobrem completamente e qualquer outra coisa que testarmos estará testando na verdade os contratos da OpenZeppelin, códigos que não são nossos. Claro que é interessante escrever estes testes principalmente para fins didáticos, para termos um melhor entendimento do funcionamento dos contratos e de como usar o nosso contrato em uma aplicação real, por isso vou fazê-lo, mas do ponto de vista de cobertura de código/testes, eles não agregam nem 1%.

Vamos começar então testando a transferência de NFT entre contas.

Aqui começamos mintando o token para em seguida transferi-lo, sendo o restante apenas as verificações do teste, para saber se o owner original não tem mais a propriedade do mesmo e se o novo owner tem.

Uma outra coisa que é interessante verificarmos, pois a especificação ERC721 assim exige, é se os eventos estão sendo corretamente emitidos. Então vamos testar a emissão do evento de transferência, na mesma situação acima.

Aqui o segredo é o expect que usa o .to.emit para ler os logs da blockchain e saber se o evento foi devidamente registrado. Porque na prática os eventos da blockchain nada mais são do que logs registrados e associados aos blocos.

Agora vamos testar outro tipo de transferência, a delegada, quando o owner do token permite que outra carteira possa transferir o mesmo.

Aqui a única diferença em relação aos testes de transferência anteriores é a chamada ao approve para permitir que a gente se conecte à segunda carteira na sequência e faça a transferência normalmente. Como já verificamos o processo normal de transferência, deixo os expects aqui cobrirem apenas o que toca a ideia de aprovação. E como a aprovação emite também um evento específico, vamos testá-lo em outro teste.

Um outro ponto importante ainda levando em conta a aprovação é que quando uma transferência ocorre, todas as aprovações ao token devem ser eliminadas, o que será testado a seguir.

E a seguir a mesma ideia de testes, mas considerando uma aprovação completa na coleção do owner, ao invés de aprovar somente um token.

Na sequência, temos que escrever os cenários de fracasso, certo? Porque existem várias coisas que podem impedir uma transferência de ser realizada com sucesso e vou cobrir aqui a falta de permissão, a não existência do token, a aprovação para outra carteira que não a sua e a aprovação completa para outra carteira que não a sua.

Note que a maioria dos testes de fracasso falham com a mesma mensagem pois caem na mesma situação de ausência de permissão. Escrevi todos eles mais para testar os mecanismos de aprovação mesmo.

E por fim, vamos escrever mais um teste para a função supportsInterface e com isso garantir 100% de cobertura.

Este código que passei para a função consta na documentação do ERC-721 e representa a interface, facilitando a verificação se ela está suportada em nosso contrato.

Agora experimente rodar a bateria de testes com ‘npxhardhat test’ e espero que você tenha o mesmo resultado que eu: tudo passando.

E com isso finalizamos mais este tutorial. Se quiser aprender como fazer deploy desta moeda em blockchain local, recomendo aprender a usar o HardHat Network neste tutorial. Agora se quiser colocar em uma blockchain pública, recomendo fazer este tutorial aqui. Uma outra dica é hospedar os metadados e as mídias dos seus NFT no IPFS, falo disso no vídeo abaixo e neste tutorial.

E se quiser aprender uma forma mais avançada do padrão 721, que economiza muito nas taxas de gás, confira este tutorial aqui de ERC-721a (Azuki) ou estude diminuição de gás em linhas gerais nesse artigo.

E outro padrão muito usado para NFTs é o ERC-1155 que ensino neste tutorial.

Um abraço e sucesso!

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 *

2 Replies to “Como criar NFTs usando Solidity e HardHat (JS) – Parte 2”

Kelvi Maicom Lessa

Bom dia Luiz!

Me chamo Kelvi, sou de Chapecó SC e antes de mais nada quero te meus parabéns pelo excelente trabalho, produzindo conteúdos tão ricos de forma tão bem elaborada.

Virei seu fã.

Acabei de ler os artigos Como criar NFTs usando Solidity e HardHat (JS) – Parte 1 e 2, e achei excelente, pois aprendi muito com eles.

Quero apenas fazer duas observações para os códigos propostos, pois precisei fazer 2 pequenos ajustes, então resolvi compartilhar com você com o simples objetivo de colaborar.

Primeiro, a function supportsInterface foi necessário adicionar ERC721URIStorage no override(), ficando assim a assinatura da função:

function supportsInterface(bytes4 interfaceId) public view override(ERC721, ERC721Enumerable, ERC721URIStorage) returns (bool){

Segundo, no arquivo de testes a função deployFixture precisa retornar uma terceira conta, chamada oneMoreAccount, para que alguns dos testes funcionem, deixando a função assim:

async function deployFixture() {
const [owner, otherAccount, oneMoreAccount] = await ethers.getSigners();
const MyNFT = await ethers.getContractFactory(“MyNFT”);
const myNFT = await MyNFT.deploy();
return { myNFT, owner, otherAccount, oneMoreAccount };
}

Espero ter ajudado.

Luiz Duarte

Grato pelas contribuições!