WEB3DEV

Cover image for Como implantar seu primeiro contrato inteligente na Ethereum com Solidity e Hardhat
Isabela Curado Nehme
Isabela Curado Nehme

Posted on

Como implantar seu primeiro contrato inteligente na Ethereum com Solidity e Hardhat

17 de setembro, 2021
Leitura de 12min

Image description

Hardhat

Estava planejando lançar um tutorial sobre como implantar seu primeiro contrato inteligente NFT, mas enquanto o escrevia, compreendi que mais da metade do conteúdo era sobre como configurar Hardhat para ter o ambiente de desenvolvimento perfeito pronto para seu próximo projeto.

Então, vamos começar por aqui e quando eu lançar o tutorial sobre NFT você pode aproveitar o trabalho que fizemos hoje para estar pronto instantaneamente para começar a desenvolver seu próximo contrato inteligente. Vamos prosseguir?

O que você vai aprender hoje?

  • Configurar o Hardhat para compilar, implantar, testar e depurar seu código Solidity
  • Escrever um contrato inteligente
  • Testar seu contrato inteligente
  • Implantar seu contrato inteligente na Rinkeby
  • Ter seu contrato inteligente verificado pela Etherscan

Hardhat

Hardhat é a ferramenta principal que usaremos hoje e indispensável se você precisar desenvolver e depurar seu contrato inteligente localmente antes de implantá-lo na rede de teste e depois na rede principal.

O que é Hardhat?

Hardhat é um ambiente de desenvolvimento para compilar, implantar, testar e depurar seu software da Ethereum. Ajuda desenvolvedores a gerenciar e automatizar as tarefas recorrentes inerentes ao processo de criação de contratos inteligentes e dApps, além de introduzir facilmente mais funcionalidades nesse fluxo de trabalho. Isso significa compilar, executar e testar contratos inteligentes em seu núcleo.

Por que usar hardhat?

  • Você pode executar a Solidity localmente para implantar, executar, testar e depurar seus contratos sem lidar com ambientes ativos.
  • Você obtém rastreamentos de pilha da Solidity, console.log e mensagens de erro explícitas quando as transações falharem. Sim, você ouviu direito, console.log no seu contrato Solidity!
  • Toneladas de plugins para adicionar funcionalidades legais e permitir que você integre suas ferramentas existentes. Usaremos alguns deles para sentir essa magia!
  • Suporte nativo completo para TypeScript.

Links úteis para saber mais

Crie seu primeiro projeto Hardhat

É hora de abrir seu Terminal, seu IDE de código do Visual Studio, e começar a construir!

Que projeto vamos construir?

Neste tutorial, vamos construir um contrato inteligente “fácil”. Será bem estúpido, mas o nosso principal objetivo aqui é configurar o hardhat e implantar o contrato, não o efeito do contrato.

O que o contrato vai fazer?

  • Acompanhar o “propósito” do contrato em uma string que será visível publicamente
  • Você pode alterar o propósito somente se pagar mais que o antigo proprietário
  • Você não pode alterar o propósito se já for o proprietário
  • Você pode retirar seu ETH somente se não for o proprietário do propósito atual
  • Emitir um evento de Mudança de Propósito (PurposeChange) quando o propósito for alterado

Crie o projeto, inicie-o e configure o Hardhat

Abra seu terminal e vamos lá. Vamos chamar nosso projeto de propósito mundial!

mkdir world-purpose
cd world-purpose
npm init -y
yarn add --dev hardhat
npx hardhat
Enter fullscreen mode Exit fullscreen mode

Com npx hardhat o assistente hardhat dará o pontapé inicial para ajudá-lo a configurar seu projeto.

  • Escolha Create a basic sample project
  • Escolha o padrão Hardhat project root porque neste tutorial estamos configurando apenas o hardhat.
  • Confirme para adicionar o .gitignore
  • Confirme para instalar o sample project's dependecies with yarn, isso vai instalar algumas dependências necessárias para o seu projeto que usaremos ao longo do caminho

Agora sua estrutura de projeto deve ter esses arquivos/pasta

  • contracts onde todos os seus contratos serão armazenados
  • scripts onde todos seus scripts/tarefas serão armazenados
  • test onde seus testes de contrato inteligente serão armazenados
  • hardhat.config.js onde você configurará seu projeto de hardhat

Comandos e conceitos importantes de hardhat que você deve dominar

  • Arquivo de Configuração Hardhat
  • Tarefas Hardhat
  • npx hardhat node - Execute a tarefa do nó para iniciar um servidor JSON-RPC no topo da Rede Hardhat (sua blockchain ethereum local)
  • npx hardhat test - Para executar testes armazenados na pasta de teste
  • npx hardhat compile - Para compilar o projeto completo, construindo seu contrato inteligente
  • npx hardhat clean - Para limpar o cache e excluir contratos inteligentes compilados
  • npx hardhat run —-network <network> script/path - Para executar um script específico em uma network específica

Adicionar Suporte TypeScript

Como dissemos, o Hardhat suporta nativamente o typescript e nós vamos utilizá-lo. Usar javascript ou typescript não muda muito, mas essa é minha preferência pessoal porque penso que te permite cometer menos erros e entender melhor como usar bibliotecas externas.

Vamos instalar algumas dependências necessárias

yarn add --dev ts-node typescript
yarn add --dev chai @types/node @types/mocha @types/chai
Enter fullscreen mode Exit fullscreen mode

Altere a extensão do arquivo de configuração do hardhat para .ts e atualize o conteúdo com isso

import { task } from 'hardhat/config';
import '@nomiclabs/hardhat-waffle';

// Essa é uma tarefa de amostra do Hardhat. Para aprender como criar sua a sua própria acesse
// https://hardhat.org/guides/create-task.html
task('accounts', 'Prints the list of accounts', async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// Você precisa exportar um objeto para estabelecer sua configuração
// Acesse https://hardhat.org/config/ para saber mais
export default {
  solidity: '0.8.4',
};
Enter fullscreen mode Exit fullscreen mode

Agora crie um default tsconfig.json na raiz do seu projeto com esse conteúdo

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist"
  },
  "include": ["./scripts", "./test"],
  "files": ["./hardhat.config.ts"]
}
Enter fullscreen mode Exit fullscreen mode

Agora atualize também o arquivo de teste e o arquivo de script para o tipo de arquivo typescript .tse altere seu código.

import { expect } from 'chai';
import { ethers } from 'hardhat';

describe('Greeter', function () {
  it("Should return the new greeting once it's changed", async function () {
    const Greeter = await ethers.getContractFactory('Greeter');
    const greeter = await Greeter.deploy('Hello, world!');
    await greeter.deployed();

    expect(await greeter.greet()).to.equal('Hello, world!');

    const setGreetingTx = await greeter.setGreeting('Hola, mundo!');

    // espera até que a transação seja minada
    await setGreetingTx.wait();

    expect(await greeter.greet()).to.equal('Hola, mundo!');
  });
});
Enter fullscreen mode Exit fullscreen mode
// Exigimos o Ambiente de Tempo de Execução Hardhat Runtime Environment explicitamente aqui. Isso é opcional
// porém útil para executar o script de forma independente através do `node <script>`.
//
// Ao executar o script com `npx hardhat run <script>` você encontrará o Hardhat
// Membros do Ambiente de Tempo de Execução disponíveis no escopo global
import { ethers } from 'hardhat';

async function main() {
  // O Hardhat sempre executa a tarefa de compilação ao executar os scripts com sua interface
  // linha de comando
  //
  // Se esse script for executado diretamente usando `node` você pode querer chamar de compilar
  // manualmente para garantir que tudo está compilado 
  // await hre.run('compile');

  // Recebemos o contrato para implantar 
  const Greeter = await ethers.getContractFactory('Greeter');
  const greeter = await Greeter.deploy('Hello, Hardhat!');

  await greeter.deployed();

  console.log('Greeter deployed to:', greeter.address);
}

// Recomendamos este padrão para poder usar async/await em todos os lugares
// e lidar adequadamente com os erros.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
Enter fullscreen mode Exit fullscreen mode

Vamos testar se tudo está funcionando corretamente

npx hardhat test
npx hardhat node
npx hardhat run --network localhost scripts/sample-script.ts
Enter fullscreen mode Exit fullscreen mode

Vamos adicionar alguns plugins do Hardhat e alguns utilitários de código

adicionar solhint, solhint-plugin-prettier e hardhat-solhint plugin

Agora queremos adicionar suporte para solhint, um utilitário de linting para o código Solidity que irá nos ajudar a seguir regras rígidas enquanto desenvolvemos nosso contrato inteligente. Essas regras são úteis tanto para seguir a melhor prática padrão de estilo de código quanto para aderir às melhores abordagens de segurança.

solhint+solhint prettier

yarn add --dev solhint solhint-plugin-prettier prettier prettier-plugin-solidity

Adicione um arquivo de configuração .solhint.json em sua pasta raiz

{
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": "error"
  }
}
Enter fullscreen mode Exit fullscreen mode

Adicione um arquivo de configuração .prettierrc e adicione os estilos que você preferir. Esta é minha escolha pessoal

{
  "arrowParens": "always",
  "singleQuote": true,
  "bracketSpacing": false,
  "trailingComma": "all",
  "printWidth": 120,
  "overrides": [
    {
      "files": "*.sol",
      "options": {
        "printWidth": 120,
        "tabWidth": 4,
        "useTabs": false,
        "singleQuote": false,
        "bracketSpacing": false,
        "explicitTypes": "always"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Se você quiser saber mais sobre solhint e seu plugin prettier, confira seu site de documentação

hardhat-solhint

Se você quer saber mais, siga a documentação plugin solhint do Hardhat.

yarn add --dev @nomiclabs/hardhat-solhint

e atualize a configuração hardhat adicionando essa linha à importação

import “@nomiclabs/hardhat-solhint”;

typechain para suporte de contrato digitado

Esta parte é totalmente opcional. Como eu disse, amo typescript e gosto de usá-la em todos os lugares sempre que possível. Adicionar suporte para contratos digitados me permite saber perfeitamente quais funções estão disponíveis, quais parâmetros são necessários de qual tipo e o que estão retornando.

Você poderia seguir totalmente sem ele, mas eu sugiro fortemente que você siga este passo.

Por que typechain?

  • Uso de Configuração Zero - Execute a tarefa de compilação normalmente, e os artefatos do Typechain serão automaticamente gerados em um diretório raiz chamado typechain.
  • Geração incremental - somente arquivos recompilados terão seus types regenerados
  • Sem atrito - o type de retorno de ethers.getContractFactory será digitado corretamente - não há necessidade de lançamentos

Se você quiser se aprofundar neste projeto e saber todas as opções de configurações possíveis, você pode seguir estes links:

Vamos fazer isso

yarn add --dev typechain @typechain/hardhat @typechain/ethers-v5

Adicione essas importações ao seu arquivo de configuração Hardhat

import '@typechain/hardhat'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
Enter fullscreen mode Exit fullscreen mode

Adicione "resolveJsonModule": true ao seu tsconfig.json

Agora, quando você compilar o typechain do seu contrato, gerará o type correspondente dentro da pasta typechain e você poderá usá-la nos seus testes e aplicativos da web!

Atualize os arquivos de testes para usar os types gerados pelo Typechain

import {ethers, waffle} from 'hardhat';
import chai from 'chai';

import GreeterArtifact from '../artifacts/contracts/Greeter.sol/Greeter.json';
import {Greeter} from '../typechain/Greeter';

const {deployContract} = waffle;
const {expect} = chai;

describe('Greeter', function () {
  let greeter: Greeter;

  it("Should return the new greeting once it's changed", async function () {
    const signers = await ethers.getSigners();
    greeter = (await deployContract(signers[0], GreeterArtifact, ['Hello, world!'])) as Greeter;

    expect(await greeter.greet()).to.equal('Hello, world!');

    const setGreetingTx = await greeter.setGreeting('Hola, mundo!');

    // aguarda até que a transação seja minada
    await setGreetingTx.wait();

    expect(await greeter.greet()).to.equal('Hola, mundo!');
  });
});
Enter fullscreen mode Exit fullscreen mode

Atualizar scripts em package.json

Esta parte é totalmente opcional, mas lhe permite executar comandos mais rapidamente e pré-configurá-los. Abra seu package.json e substitua a seção scripts com esses comandos:

"clean": "npx hardhat clean",
"chain": "npx hardhat node",
"deploy": "npx hardhat run --network localhost scripts/deploy.ts",
"deploy:rinkeby": "npx hardhat run --network rinkeby scripts/deploy.ts",
"test": "npx hardhat test"
Enter fullscreen mode Exit fullscreen mode

Agora, se você quiser simplesmente executar o Hardhat, você pode escrever no seu console yarn chain e bum! Estamos prontos para ir!

Se você tiver pulado a parte TypeScript e quiser usar apenas Javascript, basta mudar aqueles .ts de volta para .js e tudo funcionará conforme esperado.

Desenvolvendo o contrato inteligente

Certo, agora estamos realmente prontos para ir. Renomeie Greeter.sol na sua pasta contracts para WorldPurpose.sol e vamos começar a construir daqui.

Nosso contrato precisa fazer estas coisas:

  • Acompanhar o “propósito” do contrato em uma estrutura
  • Você pode alterar o propósito apenas se pagar mais que o proprietário anterior
  • Você não pode alterar o propósito se você já é o proprietário
  • Você pode retirar seu ETH somente se você não é o proprietário do propósito atual
  • Emitir um evento de Mudança de Propósito quando o propósito for alterado

Conceitos que você deve dominar

Código de Contrato Inteligente

Vamos começar criando uma Estrutura para armazenar as informações do Propósito

/// @notice estrutura do Propósito
struct Purpose {
   address owner;
   string purpose;
   uint256 investment;
}
Enter fullscreen mode Exit fullscreen mode

Vamos agora definir algumas variáveis de estado para acompanhar tanto o Propósito atual quanto o investimento do usuário

/// @notice Acompanhar investimento do usuário
mapping(address => uint256) private balances;

/// @notice Acompanhar o propósito mundial atual
Purpose private purpose;

Enter fullscreen mode Exit fullscreen mode

Por último, mas não menos importante, defina um Evento que vai ser emitido quando um novo Propósito Mundial for definido

/// @notice Evento para acompanhar o novo Propósito
event PurposeChange(address indexed owner, string purpose, uint256 investment);
Enter fullscreen mode Exit fullscreen mode

Vamos criar algumas funções utilitárias para obter o purpose atual e o saldo do usuário que ainda está no contrato. Precisamos fazer isto porque as variáveis balances e purpose são private para que não possam ser acessadas diretamente dos aplicativos externos de contrato/web3. Precisamos expô-los através dessas funções

/**
 @notice Recolhimento para o propósito atual
 @return currentPurpose O propósito mundial atual ativo
*/
function getCurrentPurpose() public view returns (Purpose memory currentPurpose) {
   return purpose;
}

/**
 @notice Obtenha o valor total do investimento que você fez. Retorna ambos os investimentos bloqueados e desbloqueados.
 @return balance O saldo que você ainda tem no contrato
*/
function getBalance() public view returns (uint256 balance) {
   return balances[msg.sender];
}
Enter fullscreen mode Exit fullscreen mode

Agora, vamos criar a função setPurpose. Ela recebe um parâmetro como entrada: _purpose. A função deve ser payable porque queremos aceitar alguns Ethers para definir o propósito (aqueles ethers serão retornáveis pelo proprietário que tiver seu propósito substituído por outra pessoa).

A transação vai revert se algumas condições não forem atendidas:

  • o parâmetro de entrada _purpose está vazio
  • o msg.value (quantidade de ether enviado com a transação) está vazio (0 ethers)
  • o msg.value é menor que a do propósito atual
  • o proprietário do propósito mundial atual tentar substituir seu propósito (msg.sender deve ser diferente do proprietário atual)
/**
 @notice Modifier  para verificar se o proprietário anterior do propósito não é o mesmo que o novo proponente
*/
modifier onlyNewUser() {
    // Verifica se o novo dono não é o anterior
    require(purpose.owner != msg.sender, "You cannot override your own purpose");
    _;
}

/**
 @notice Define o novo propósito mundial
 @param _purpose O conteúdo do novo propósito
 @return newPurpose O novo propósito mundial ativo
*/
function setPurpose(string memory _purpose) public payable onlyNewUser returns (Purpose memory newPurpose) {
    // Verifica se o novo proprietário nos enviou fundos suficientes para substituir o propósito anterior
    require(msg.value > purpose.investment, "You need to invest more than the previous purpose owner");

    // Verifica se o novo propósito está vazio
    bytes memory purposeBytes = bytes(_purpose);
    require(purposeBytes.length > 0, "You need to set a purpose message");

    // Atualiza o propósito com o novo
    purpose = Purpose(msg.sender, _purpose, msg.value);

    // Atualiza o valor do remetente
    balances[msg.sender] += msg.value;

    // Emite o evento de MudançaDePropósito
    emit PurposeChange(msg.sender, _purpose, msg.value);

    //Retorna o novo propósito
    return purpose;
}

Enter fullscreen mode Exit fullscreen mode

Se tudo funcionar, definimos o novo propósito, atualizamos o saldo e emitimos o evento.

Agora queremos permitir que os usuários retirem seus investimentos de propósitos anteriores. Note, por favor, que apenas o investimento do propósito atual está “bloqueado”. Ele será desbloqueado somente quando uma nova pessoa definir o novo propósito.

/**
 @notice Retira os fundos do propósito antigo. Se você tem um propósito ativo, esses fundos são “bloqueados”
*/
function withdraw() public {
    // Obtém o saldo do usuário
    uint256 withdrawable = balances[msg.sender];

    // Agora precisamos verificar quanto o usuário pode retirar
    address currentOwner = purpose.owner;
    if (currentOwner == msg.sender) {
        withdrawable -= purpose.investment;
    }

    // Verifica se o usuário tem saldo suficiente para retirar 
    require(withdrawable > 0, "You don't have enough withdrawable balance");

    // Atualiza o saldo
    balances[msg.sender] -= withdrawable;

    // Transfere o saldo
    (bool sent, ) = msg.sender.call{value: withdrawable}("");
    require(sent, "Failed to send user balance back to the user");
}
Enter fullscreen mode Exit fullscreen mode

Implante-o localmente apenas para testar que tudo funciona conforme esperado. Você deve ver algo assim:

Image description

Implementação do contrato na blockchain local bem-sucedida

Adicionar Teste local

Agora eu não vou entrar em detalhes sobre o código dentro do arquivo de teste, mas vou explicar o conceito por trás dele. Você deve sempre criar casos de teste para seu contrato. É um jeito rápido de entender se a lógica do contrato está funcionando conforme o esperado e permite evitar a implantação de algo que não está funcionando.

Meu fluxo de trabalho atual é assim: pego todas as funções e testo que elas estão fazendo o que eu espero que façam, tanto em casos de sucesso quanto em casos de reversão. Nada mais, nada menos.

Ao escrever testes para contratos Solidity, você usará a Waffle. Vá até o site deles para ter uma visão geral da biblioteca, é muito bem feita e oferece muitos utilitários.

Agora, exclua o arquivo que você tem da sua pasta test, crie um novo chamado worldpurpose.ts e substitua o conteúdo com isso

import {ethers, waffle} from 'hardhat';
import chai from 'chai';

import WorldPurposeArtifact from '../artifacts/contracts/WorldPurpose.sol/WorldPurpose.json';
import {WorldPurpose} from '../typechain/WorldPurpose';
import {SignerWithAddress} from '@nomiclabs/hardhat-ethers/signers';

const {deployContract} = waffle;
const {expect} = chai;

describe('WorldPurpose Contract', () => {
  let owner: SignerWithAddress;
  let addr1: SignerWithAddress;
  let addr2: SignerWithAddress;
  let addrs: SignerWithAddress[];

  let worldPurpose: WorldPurpose;

  beforeEach(async () => {
    // eslint-disable-next-line no-unused-vars
    [owner, addr1, addr2, ...addrs] = await ethers.getSigners();

    worldPurpose = (await deployContract(owner, WorldPurposeArtifact)) as WorldPurpose;
  });

  describe('Test setPurpose', () => {
    it("set purpose success when there's no purpose", async () => {
      const purposeTitle = 'Reduce the ETH fee cost in the next 3 months';
      const purposeInvestment = ethers.utils.parseEther('0.1');
      await worldPurpose.connect(addr1).setPurpose(purposeTitle, {
        value: purposeInvestment,
      });

      // Verifica que o propósito foi definido
      const currentPurpose = await worldPurpose.getCurrentPurpose();
      expect(currentPurpose.purpose).to.equal(purposeTitle);
      expect(currentPurpose.owner).to.equal(addr1.address);
      expect(currentPurpose.investment).to.equal(purposeInvestment);

      // Verifica que o saldo foi atualizado
      const balance = await worldPurpose.connect(addr1).getBalance();
      expect(balance).to.equal(purposeInvestment);
    });

    it('override the prev purpose', async () => {
      await worldPurpose.connect(addr2).setPurpose("I'm the old world purpose", {
        value: ethers.utils.parseEther('0.1'),
      });

      const purposeTitle = "I'm the new world purpose!";
      const purposeInvestment = ethers.utils.parseEther('0.11');
      await worldPurpose.connect(addr1).setPurpose(purposeTitle, {
        value: purposeInvestment,
      });

      // Verifica se o propósito foi definido
      const currentPurpose = await worldPurpose.getCurrentPurpose();
      expect(currentPurpose.purpose).to.equal(purposeTitle);
      expect(currentPurpose.owner).to.equal(addr1.address);
      expect(currentPurpose.investment).to.equal(purposeInvestment);

      // Verifica se o saldo foi atualizado
      const balance = await worldPurpose.connect(addr1).getBalance();
      expect(balance).to.equal(purposeInvestment);
    });

    it('Check PurposeChange event is emitted ', async () => {
      const purposeTitle = "I'm the new world purpose!";
      const purposeInvestment = ethers.utils.parseEther('0.11');
      const tx = await worldPurpose.connect(addr1).setPurpose(purposeTitle, {
        value: purposeInvestment,
      });

      await expect(tx).to.emit(worldPurpose, 'PurposeChange').withArgs(addr1.address, purposeTitle, purposeInvestment);
    });

    it("You can't override your own purpose", async () => {
      await worldPurpose.connect(addr1).setPurpose("I'm the new world purpose!", {
        value: ethers.utils.parseEther('0.10'),
      });

      const tx = worldPurpose.connect(addr1).setPurpose('I want to override the my own purpose!', {
        value: ethers.utils.parseEther('0.11'),
      });

      await expect(tx).to.be.revertedWith('You cannot override your own purpose');
    });

    it('Investment needs to be greater than 0', async () => {
      const tx = worldPurpose.connect(addr1).setPurpose('I would like to pay nothing to set a purpose, can I?', {
        value: ethers.utils.parseEther('0'),
      });

      await expect(tx).to.be.revertedWith('You need to invest more than the previous purpose owner');
    });

    it('Purpose message must be not empty', async () => {
      const tx = worldPurpose.connect(addr1).setPurpose('', {
        value: ethers.utils.parseEther('0.1'),
      });

      await expect(tx).to.be.revertedWith('You need to set a purpose message');
    });

    it('New purpose investment needs to be greater than the previous one', async () => {
      await worldPurpose.connect(addr1).setPurpose("I'm the old purpose!", {
        value: ethers.utils.parseEther('0.10'),
      });

      const tx = worldPurpose
        .connect(addr2)
        .setPurpose('I want to pay less than the previous owner of the purpose, can I?', {
          value: ethers.utils.parseEther('0.01'),
        });

      await expect(tx).to.be.revertedWith('You need to invest more than the previous purpose owner');
    });
  });

  describe('Test withdraw', () => {
    it('Withdraw your previous investment', async () => {
      const firstInvestment = ethers.utils.parseEther('0.10');
      await worldPurpose.connect(addr1).setPurpose('First purpose', {
        value: ethers.utils.parseEther('0.10'),
      });

      await worldPurpose.connect(addr2).setPurpose('Second purpose', {
        value: ethers.utils.parseEther('0.11'),
      });

      const tx = await worldPurpose.connect(addr1).withdraw();

      // Verifica que o meu saldo atual no contrato é 0
      const balance = await worldPurpose.connect(addr1).getBalance();
      expect(balance).to.equal(0);

      // Verifica que eu coloquei de volta na minha carteira toda a importação
      await expect(tx).to.changeEtherBalance(addr1, firstInvestment);
    });

    it('Withdraw only the unlocked investment', async () => {
      const firstInvestment = ethers.utils.parseEther('0.10');
      await worldPurpose.connect(addr1).setPurpose('First purpose', {
        value: ethers.utils.parseEther('0.10'),
      });

      await worldPurpose.connect(addr2).setPurpose('Second purpose', {
        value: ethers.utils.parseEther('0.11'),
      });

      const secondInvestment = ethers.utils.parseEther('0.2');
      await worldPurpose.connect(addr1).setPurpose('Third purpose from the first addr1', {
        value: secondInvestment,
      });

      const tx = await worldPurpose.connect(addr1).withdraw();

      // Neste caso, o usuário pode retirar apenas o primeiro investimento
      // O segundo ainda está “bloqueado” porque ele é o dono do propósito atual

      // Verifica que meu saldo atual no contrato é 0
      const balance = await worldPurpose.connect(addr1).getBalance();
      expect(balance).to.equal(secondInvestment);

      // Verifica que eu coloquei de volta na minha carteira toda a importação
      await expect(tx).to.changeEtherBalance(addr1, firstInvestment);
    });

    it('You cant withdraw when your balance is empty', async () => {
      const tx = worldPurpose.connect(addr1).withdraw();

      await expect(tx).to.be.revertedWith("You don't have enough withdrawable balance");
    });

    it('Withdraw only the unlocked investment', async () => {
      await worldPurpose.connect(addr1).setPurpose('First purpose', {
        value: ethers.utils.parseEther('0.10'),
      });

      const tx = worldPurpose.connect(addr1).withdraw();

      // Seus fundos ainda estão “bloqueados” porque você é o proprietário do propósito atual
      await expect(tx).to.be.revertedWith("You don't have enough withdrawable balance");
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Execute seus testes com yarn test e veja se tudo passou como esperado

Image description

Execução dos testes

Implante seu contrato na rede de teste

===== INÍCIO DA ISENÇÃO DE RESPONSABILIDADE =====
NÃO USE SUA CARTEIRA PRINCIPAL PRIVADA PARA FINS DE TESTES
USE UMA CARTEIRA DE QUEIMA OU CRIE UMA CARTEIRA NOVA
===== FIM DA ISENÇÃO DE RESPONSABILIDADE =====

Nós já vimos como implantar o contrato em nossa chain de hardhat local, mas agora queremos fazê-lo para a rede de teste Rinkeby (o procedimento é o mesmo para a rede principal, mas não é o alcance do tutorial).

Para implantar na chain local, só precisamos digitar no console este comando

npx hardhat run --network localhost scripts/deploy.ts

Para implantar seus contratos em outra rede, precisamos apenas alterar o valor do parâmetro de --network , porém, antes de fazê-lo, precisamos fazer algumas etapas preparatórias.

Obtenha um pouco de ETH de uma rede de teste torneira (Faucet)

Implantar um contrato custa gas, então precisamos de um pouco de uma torneira (Faucet). Escolha uma das redes de teste e vá até a torneira para obter fundos.

No nosso caso, decidimos usar a Rinkeby, então vá e obtenha alguns fundos por lá.

Escolha um Provedor de Ethereum

Para implantar nosso contrato, precisamos de um jeito de interagir diretamente com a rede da Ethereum. Hoje em dia, temos duas opções possíveis:

No nosso caso, vamos usar a Alchemy, então vá lá, crie uma conta, crie um novo aplicativo e pegue a Chave API da Rinkeby.

Atualize o arquivo de configuração Hardhat

Instale as dependências dotenv, essa biblioteca de NodeJs nos permite criar um arquivo de ambiente .env para armazenar nossas variáveis sem expô-las ao código fonte.

yarn add --dev dotenv

Agora crie, na raiz do seu projeto, um arquivo .env e cole toda a informação que você coletou

RENKEBY_URL=https://eth-rinkeby.alchemyapi.io/v2/<YOUR_ALCHEMY_APP_ID>
PRIVATE_KEY=<YOUR_BURNER_WALLET_PRIVATE_KEY>

Nunca é suficiente enfatizar. Não use o caso da sua carteira privada principal para fins de teste. Crie uma carteira separada ou use uma carteira de queima para fazer esse tipo de teste!

A última etapa é atualizar o arquivo hardhat.config.ts para adicionar dotenv e atualizar as informações da rinkeby.

No topo do arquivo adicione require("dotenv").config();; e agora atualize a configuração assim

const config: HardhatUserConfig = {
 solidity: '0.8.4',
 networks: {
   rinkeby: {
     url: process.env.RENKEBY_URL || '',
     accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
   },
 },
};
Enter fullscreen mode Exit fullscreen mode

Estamos preparados? Vamos implantá-lo!

npx hardhat run --network rinkeby scripts/deploy.ts

Se tudo correr bem, você deverá ver algo assim no seu console

Image description

Se você copiar aquele endereço de contrato e colar no Etherscan da Rinkeby, poderá vê-lo ao vivo! O meu está localizado aqui:
0xAf688A3405531e0FC274F6453cD8d3f3dB07D706

Tenha seu contrato verificado pela Etherscan

A verificação do código-fonte fornece transparência para os usuários interagirem com contratos inteligentes. Ao fazer o upload do código-fonte, a Etherscan combinará o código compilado com o da blockchain. Assim como os contratos, um “contrato inteligente” deve fornecer aos usuários finais mais informações sobre o que eles estão “assinando digitalmente” e dar aos usuários uma oportunidade de auditar o código para verificar independentemente que ele realmente faz o que deveria fazer.

Então, vamos fazê-lo. É realmente um passo fácil porque o Hardhat fornece um Plugin específico para fazer isso: hardhat-etherscan.

Vamos instalar yarn add --dev @nomiclabs/hardhat-etherscan

e incluí-lo no seu hardhat.config.ts adicionando import "@nomiclabs/hardhat-etherscan"; ao final das suas importações.

O próximo passo é registrar uma conta na Etherscan e gerar uma Chave API.

Uma vez que você a tiver, precisamos atualizar nosso arquivo .env adicionando essa linha de código

ETHERSCAN_API_KEY=<PAST_YOU_API_KEY>

e atualizar o hardhat.config.ts adicionando a configuração da Etherscan necessária pelo plugin.

A última coisa a fazer é executar a tarefa verify que foi adicionada pelo plugin:

npx hardhat verify --network rinkeby <YOUR_CONTRACT_ADDRESS>

O plugin tem muitas configurações e opções diferentes, então confira a documentação se você quiser saber mais.

Se tudo sair bem, você deverá ver algo assim no seu console

Image description

Contrato verificado pela Etherscan

Se você passar pela página de contrato da Etherscan novamente, você deverá ver uma marca de seleção verde no topo da seção de Contrato. Muito bom!

Conclusão e os próximos passos

Se você quiser usar este solidity-template para dar o pontapé no seu próximo projeto de contrato inteligente, basta acessar o repositório GitHub https://github.com/StErMi/solidity-template e ir no botão verde “Use este modelo” e bum, você está pronto para ir!

Esse é apenas o começo, estou planejando adicionar mais recursos ao longo do tempo como por exemplo:

Você tem algum feedback ou quer contribuir com o modelo da Solidity? Basta criar uma solicitação Pull Request na página do GitHub e vamos discutir isso!

Gostou desse conteúdo? Me siga para mais!

Esse artigo foi escrito por StErMi e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Latest comments (0)