No dia 21 de setembro de 2023 a ConsenSys, empresa responsável por produtos e serviços web3 como MetaMask, Infura e Truffle anunciou que estará encerrando o desenvolvimento e suporte deste último até o final deste ano (leia aqui). Já fazia algum tempo que a suíte Truffle vinha perdendo adesão na comunidade em virtude do crescimento e vantagens significativas do seu maior concorrente, HardHat. E é justamente essa a recomendação da ConsenSys para todos que ainda possuem projetos Truffle, que sejam migrados para HardHat até o final do período do suporte.
Então se esse é o seu caso, esse tutorial vai te ajudar nessa migração.
Usarei como exemplo fontes de projetos Truffle que eu ensinei a desenvolver aqui mesmo no blog e que você encontra abaixo.
Você não precisa ter feito nenhum desses tutoriais para conseguir acompanhar o que vou ensinar, mas deve conhecer o básico de Truffle e de HardHat, sendo que este último eu ensino neste tutorial.
Se preferir, pode assistir ao vídeo abaixo ao invés de ler o artigo, com exceção a parte de deploy, que neste artigo está mais atualizado (o HardHat mudou recentemente).
Vamos lá!
Truffle x HardHat
A primeira coisa que você precisa entender são as principais diferenças entre Truffle x HardHat. Entendendo essas diferenças você vai perceber os gaps que terá de estudar e a necessidade de alterações nos códigos mais gritantes.
Em termos do smart contract em si, é importantíssimo se tranquilizar pois nada muda, já que ambas trabalha com Solidity. Um problema a menos aqui nas suas migrações. O mesmo vale para ambiente de desenvolvimento, já que ambas são toolkits para Node.js. A única dica é que se você usa as extensões Truffle ou Solidity (Juan Blanco) no VS Code, eu recomendo que troque para a extensão da própria HardHat (NomicLabs).
A primeira grande diferença que você tem de entender é que Truffle era um toolkit focado em JS, enquanto que HardHat foca-se em TypeScript. Isso significa que se você ainda não aprendeu TypeScript que deve fazê-lo o quanto antes, não apenas pela exigência do HardHat em si mas pelo mercado como um todo, que cada vez mais usa esse superset. Você pode aprender sobre TS nesta série de tutoriais aqui do blog.
A segunda grande diferença é que Truffle usa como lib web3 a Web3.js, enquanto que HardHat usa a EthersJS. A Web3.js já foi líder incontestável de mercado, mas isso foi há algum tempo. A EthersJS a cada versão tem se mostrado uma lib mais madura, elegante e também muito mais focada em TypeScript, o que nos leva de volta ao ponto citado anteriormente. Essa diferença talvez seja a mais significativa de todas, já que isso impacta todo o resto. Você pode aprender sobre EthersJS neste e em outros tutoriais aqui do blog.
Uma terceira diferença, não tão significativa mas que impacta especialmente nos testes é que o Truffle/Web3.js (nas últimas versões) usam a lib BN.js para números gigantes, que aliás são muito comuns em contratos blockchain, enquanto que o HardHat/Ethers nas versões mais recentes (Ethers 6+) usam o tipo BigInt nativo do ECMAScript 2020 suportado pelo TypeScript, o que garante economia no tamanho da lib, maior compatibilidade com outras libs TS e menor curva de aprendizado em seu uso.
Depois desses dois pontos divergentes, todo o resto das diferenças é mais cosmético e os exemplos de código a seguir, embora não cubram 100% dos casos, vão te dar fôlego para migrar a maior parte dos projetos.
Migrando os Testes do Truffle para HardHat
Ambos toolkits utilizam o Jest como framework de testes, mas com variações no plugin de asserções, o que nos leva a lógicas de completamente iguais na forma de dividir e codificar os testes, mas “expects” diferentes. Além disso, as diferenças entre Web3.js e EthersJS também se fazem notar por aqui. Para não ficar apenas na teoria, vamos começar apresentando o contrato que vou usar como case de estudo, neste caso um CRUD de livros abaixo, sendo que omiti o conteúdo das funções pois são irrelevantes do ponto de vista deste estudo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
pragma solidity ^0.8.9; contract BookDatabase { struct Book { string title; uint16 year; } mapping(uint32 => Book) public books; uint32 private nextId = 0; uint32 public count; address private immutable owner; constructor() { owner = msg.sender; } function addBook(Book memory book) public { //... } function getBook(uint32 id) public view returns (Book memory) { //... } function editBook(uint32 id, Book memory newBook) public { //... } function removeBook(uint32 id) public { //... } } |
Em Truffle, os testes para este contrato seriam:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const HelloWorld = artifacts.require("HelloWorld"); contract('HelloWorld', function (accounts) { beforeEach(async () => { contract = await HelloWorld.new(); }) it('Should Hello the World', async () => { const result = await contract.helloWorld(); assert(result === "Hello World!"); }) }); |
Vejamos como ficavam os testes com Truffle, inicialmente apenas a preparação dos testes, para comparamos por partes.
1 2 3 4 5 6 7 8 |
const BookDatabase = artifacts.require("BookDatabase"); contract('BookDatabase', function (accounts) { beforeEach(async () => { contract = await BookDatabase.new(); }) |
Repare como usamos artifacts.require para carregar o contrato e antes de cada teste (beforeEach) nós usamos a função new para criar uma cópia “nova” dele. Agora veja a lógica semelhante usando HardHat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import { ethers } from "hardhat"; describe("BookDatabase", () => { async function deployFixture() { const [owner] = await ethers.getSigners(); const BookDatabase = await ethers.getContractFactory("BookDatabase"); const bookDatabase = await BookDatabase.deploy(); return { bookDatabase, owner }; } |
À primeira vista, pode parecer que o HardHat é mais trabalhoso ou mais complexo, visto que a quantidade de código é maior, certo?
O que acontece aqui é que no Truffle temos muitas coisas implícitas como a variável contract, o deploy do contrato, várias libs e outros. Enquanto no HardHat nós temos um controle maior de todo o processo através do conceito de fixtures, que são funções para deploy que você configura para os testes, como se fossem presets. Essa função de fixture roda apenas uma vez e depois seu resultado é carregado “limpo” no início de cada teste (loadFixture, veremos mais adiante), servindo não apenas para inicializar o contrato rapidamente como para obter carteiras de teste e outras questões.
Já o teste em si, não é tão diferente assim, apenas a asserção mesmo, onde o assert do Truffle:
1 2 3 4 5 6 7 8 9 10 11 |
it('Should add book', async () => { await contract.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); const count = await contract.count(); assert(count.toNumber() === 1, "Couldn't add the book."); }) |
Vira expect no HardHat (repare que sem o beforeEach, temos de chamar o loadFixture no início de cada teste):
1 2 3 4 5 6 7 8 9 10 11 12 |
it("Should add book", async () => { const { bookDatabase } = await loadFixture(deployFixture); await bookDatabase.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); expect(await bookDatabase.count()).to.equal(1); }); |
Agora mais um teste, desta vez de obtenção de um livro já salvo no smart contract. Primeiro em Truffle:
1 2 3 4 5 6 7 8 9 10 11 |
it('Should Get Book', async () => { await contract.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); const book = await contract.getBook(1); assert(book.title === "Criando apps para empresas com Android", "Couldn't add the book."); }) |
E depois em HardHat:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it('Should Get Book', async () => { const { bookDatabase } = await loadFixture(deployFixture); await bookDatabase.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); const book = await bookDatabase.getBook(1); expect(book.title).to.equal("Criando apps para empresas com Android"); }) |
Repare como é praticamente a mesma coisa, o que muda é que na versão Truffle o carregamento do contrato ficou lá no beforeEach.
Mais um, agora o teste de edição em Truffle
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it('Should Edit Book', async () => { await contract.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); await contract.editBook(1, { title: "Criando apps com Android" }); const book = await contract.getBook(1); assert(customer.title === "Criando apps com Android" && customer.year === 2015, "Couldn't edit the book."); }) |
E em HardHat. Aqui existe uma leve variação mas por decisão do programador em separar melhor as duas asserções, pois poderia ter sido testada apenas em uma linha também.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
it('Should Edit Book', async () => { const { bookDatabase } = await loadFixture(deployFixture); await bookDatabase.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); await bookDatabase.editBook(1, { title: "Criando apps com Android", year: 0 }); const book = await bookDatabase.getBook(1); expect(book.title).to.equal("Criando apps com Android"); expect(book.year).to.equal(2015); }) |
E por fim, o último teste, de exclusão, no Truffle.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it('Should Remove Book', async () => { await contract.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); await contract.removeBook(1, { from: accounts[0] }); const count = await contract.count(); assert(count.toNumber() === 0, "Couldn't delete the book."); }) }); |
E no HardHat. Aqui notamos mais um ponto diferente em virtude das abordagens de cada lib para impersonalização de chamadas à blockchain. Enquanto que na Web3.js nós temos um array implícito de accounts com contas de teste, no HardHat essas contas são provenientes da fixture também, recebendo variáveis para cada uma. E aqui também podemos notar a diferença no tratamento de números gigantes, onde a BN.js do Truffle não é tratada nativamente no JS, mas os números gigantes do HardHat são nativos do TS.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
it('Should Remove Book', async () => { const { bookDatabase, owner } = await loadFixture(deployFixture); await bookDatabase.addBook({ title: "Criando apps para empresas com Android", year: 2015 }); await bookDatabase.removeBook(1, { from: owner.address }); expect(await bookDatabase.count()).to.equal(0); }) |
Existem mais diferenças do que essas, mas já dá pra ter uma ideia. Um bom ponto de partida é você explorar a página de testes do HardHat onde eles descrevem algumas funcionalidades muito bacanas como auditoria de cobertura de código, auditoria de consumo de gás, avanço da blockchain no tempo (para testes time-based) e muito mais.
Migrando o Deploy do Truffle para HardHat
Agora vamos falar de deploy, que é um elemento onipresente em todos projetos de smart contracts.
Na pasta migrations do Truffle encontramos algo como abaixo, para o mesmo contrato de CRUD de livros que mostrei acima.
1 2 3 4 5 6 7 |
const BookDatabase = artifacts.require("BookDatabase"); module.exports = function(deployer) { deployer.deploy(BookDatabase); }; |
Enquanto que em HardHat nós temos a pasta ignition/modules:
1 2 3 4 5 6 7 8 9 10 11 12 |
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; const BookDatabaseModule = buildModule("BookDatabaseModule", (m) => { const contract = m.contract("BookDatabase"); return { contract }; }); export default BookDatabaseModule; |
À primeira vista, a função de deploy do Truffle é mais simples e de fato ela é. No caso do HardHat ele usa uma feature chamada Ignition, que é uma forma declarativa de realizar o deploy, baseada na instanciação e manipulação de contratos.
Lembrando que, para que um deploy em Truffle funcione, é necessário configurar no truffle-config.js o compilador, a rede (que depende de plugin de terceiros) e a verificação do contrato (que também depende de outro plugin). Exemplo abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
require("dotenv").config(); const HDWalletProvider = require("@truffle/hdwallet-provider"); module.exports = { plugins: ["truffle-plugin-verify"], api_keys: { bscscan: process.env.API_KEY }, networks: { bsctest: { provider: new HDWalletProvider({ mnemonic: { phrase: process.env.SECRET }, providerOrUrl: "https://data-seed-prebsc-1-s1.binance.org:8545/", }), network_id: "97" } }, compilers: { solc: { version: "^0.8.17" } } }; |
Olhe como fica a mesma configuração no arquivo hardhat.config.ts do HardHat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; import dotenv from 'dotenv'; dotenv.config(); import "@nomiclabs/hardhat-etherscan"; const config: HardhatUserConfig = { solidity: "0.8.17", networks: { bsctest: { url: "https://data-seed-prebsc-1-s1.binance.org:8545/", chainId: 97, accounts: { mnemonic: process.env.SECRET } } }, etherscan: { apiKey: process.env.API_KEY } }; export default config; |
Nesse ponto o HardHat é mais enxuto uma vez que não depende de plugins adicionais a serem instalados para tarefas como deploy e verificação de contrato.
Fugindo um pouco do código e indo para ambiente, se você estava acostumado a usar o Ganache como blockchain de testes/desenvolvimento, recomendo já ir se acostumando mais com a HardHat Network, que abordo neste post.
E estes foram os principais pontos que você precisa saber para migrar um projeto de smart contract Truffle para HardHat. Salvo funcionalidades bem específicas, ambos toolkits são bem parecidos, ficando as maiores diferenças por conta de JS x TS e Web3.js x EthersJS mesmo.
Um ponto importante, digno de ser citado, é que independente da “morte” do Truffle em alguns meses, a lib Web3.js segue forte e sendo interessante de ser estudada, principalmente para projetos de dapps (frontend). Isso se deve principalmente ao fato de que ela não é desenvolvida pela ConsenSys, mas pela ChainSafe
Um abraço e até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.