Skip to content

kauemurakami/api-ethereum-wallet

Repository files navigation

Criando uma API com node.js e hardhat de uma carteira ethereum

Pré requisitos

Intro

Este projeto é a reconstrução deste projeto feito com flutter, Ganache Truffle Switch, Remix, Infura mas que resolvi refazer ao descobrir que o Ganache Truffle Switch não tem mais suporte desde o começo do ano de 2024.
Ganache Truffle Switch é uma ferramenta que fornece um blockchain Ethereum local, permitindo o desenvolvimento, teste e depuração de contratos inteligentes em um ambiente controlado. Ele simula a rede Ethereum, proporcionando uma plataforma onde você pode experimentar sem a necessidade de gastar Ether real ou esperar por confirmações de transações, leia mais sobre na documentação.

Mas e dessa vez, o que usar pra emular um ambiente de testes para a blocchain Ethereum?
Pra isso usaremos o Hardhat, que também nos possibilita as mesmas funcionalidades do Ganache Truffle Switch e mais algumas como:

  • Compilação de Contratos Inteligentes: Compila contratos Solidity usando o compilador Solidity (solc), sem isso usavamos o Remix para compilar nossos contratos e gerar os arquivos necessários.
  • Deploy de Contratos: Facilita o processo de implantação de contratos inteligentes em várias redes Ethereum, incluindo redes de teste e a rede principal.
  • Testes: Integração com frameworks de teste como Mocha e Chai para escrever e executar testes automatizados para seus contratos inteligentes.
  • Scripts de Execução: Permite escrever scripts personalizados para realizar tarefas específicas, como deploy, migrações e interações com contratos.
  • Debugging: Ferramentas avançadas de debugging que ajudam a identificar e corrigir problemas em seus contratos inteligentes.
  • Hardhat Network: Um nó local que pode ser usado para desenvolvimento, permitindo mineração instantânea, contas locais, e uma blockchain que pode ser resetada facilmente.
    Dentre outras que podem ser exploradas e veremos aqui neste tutorial.
    -> Projeto antigo: https://github.com/kauemurakami/etherum-wallet

Iniciando projeto

Antes de iniciar o projeto irei passar minha referência, e serei o mais fiel possível à ela, a própria documentação do Hardhat doc e Hardhat tutorial

Então vamos começar!!!

Primeiro crie um diretório, via VSCode, terminal ou pelo explorador de arquivos pro nosso projeto, no meu caso nomeei de api-ethereum-wallet, mas pode colocar o nome que desejar.

Abra esse diretório no VSCode, abra um terminal no diretório via Terminal do VSCode, ou apenas cliquei com o botão direito sobre a pasta e selecionar a opção Open in integrated Terminal.

Você verá no terminal algo como PS C:\projetos\api-ethereum-wallet>, e então tudo estara ok.

No terminal vamos começar instalando o Hardhat com o seguinte comando:
npm install --save-dev hardhat

Agora vamos rodar um comando do Hardhat para que gerar a estrutura básica pro nosso projeto, no terminal digite:
npx hardhat init
Você irá se deparar com essa tela:

$ npx hardhat init
888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

👷 Welcome to Hardhat v2.22.6 👷‍

? What do you want to do? …
❯ Create a JavaScript project
  Create a TypeScript project
  Create a TypeScript project (with Viem)
  Create an empty hardhat.config.js
  Quit

Selecione a primeira opção > Create a JavaScript project, usaremos JavaScript neste tutorial, mas nada impede que faça com TypeScript também.

Você verá essas mensagens no terminal:

√ What do you want to do? · Create a JavaScript project  
? Hardhat project root: » C:\projetos\api-ethereum-wallet

Confirme

Agora de um enter e irá se deparar com uma mensagem como:
Create .gitignore que por padrão vem y, tecle n, pois o mesmo conteúdo desse .gitignore que poderia ser gerado, já é adicionado ao .gitignore existente ao instalarmos o Hardhat com npm.

Agora você deve ter uma estrutura parecida com:

- api-ethereum-wallet (ou seu diretório raiz)
  - contracts/
  - ignition/
  - node_modules/
  - test/
  - hardhat.config.js 
  ... arquivos padrão do nodejs

Para ter uma ideia rápida do que está disponível e do que está acontecendo execute npx hardhat, o único problema até agora é que devemos rodar esse comando do módulo do hardhat em node_modules/hardhat, você pode entrar nesse caminho e testar, mas já trago a solução pro nosso comando não ficar tão extensos e termos que trabalhar de dentro de uma pasta node_modules diretamente, pra isso faça:

  • Vá até o arquivo package.json, lá, caso não tenha adicionado outras configurações, o arquivo estará assim:
{
 "devDependencies": {
   "@nomicfoundation/hardhat-toolbox": "^5.0.0",
   "hardhat": "^2.22.6"
 },
}
  • Vamos fazer um alteração, adicionando a seção scripts para criar uma script que rode o npx hardhat direto de node_modules/hardhat:
{
 "devDependencies": {
   "@nomicfoundation/hardhat-toolbox": "^5.0.0",
   "hardhat": "^2.22.6"
 },
  "scripts": {
   "hardhat": "cd node_modules/hardhat && npx hardhat"
 }
}

Agora basta rodar npm run hardhat, e o comando npx hardhat será executado de dentro de node_modules/hardhat, esse comando mostra tudo que está disponível via npx hardhat, caso tudo ocorra vem significar que nossa instalação foi um sucesso. Com isso vamos continuar.

Compilando nossos contratos inteligentes

Criando contrato e eventos

Repare que na pasta /contracts/ já existe um contrato de exemplo Lock.sol mas vamos criar nosso próprio contract para compila-lo.
Em /contratcs/ crie um arquivo chamado SimpleWallet.sol com o seguinte conteúdo:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

// Uncomment this line to use console.log
// import "hardhat/console.sol";

contract SimpleWallet {
    mapping(address => uint256) private balances;
    mapping(string => address) private qrCodeToAddress;

    // Event to log deposits
    event Deposit(address indexed account, uint256 amount);

    // Event to log withdrawals
    event Withdrawal(address indexed account, uint256 amount);

    // Event to log charges
    event Charge(address indexed account, uint256 amount);

    // Event to log sent funds
    event Sent(address indexed from, address indexed to, uint256 amount);

    // Event emitted when a QR code is linked to an address
    event QRCodeLinked(string indexed qrCodeHash, address indexed account);

    // Event emitted when a QR code address is retrieved
    event QRCodeAddressRetrieved(
        string indexed qrCodeHash,
        address indexed account
    );

    // Function to deposit funds
    function deposit(uint256 _amount) public {
        require(_amount > 0, "Deposit amount must be greater than zero");
        balances[msg.sender] += _amount  ;
        //console.log("Deposited %s to %s", _amount, msg.sender);
        emit Deposit(msg.sender, _amount);
    }

    // Function to withdraw funds
    function withdraw(uint256 _amount) public {
        require(_amount > 0, "Withdrawal amount must be greater than zero");
        require(balances[msg.sender] >= _amount, "Insufficient balance");
        balances[msg.sender] -= _amount;
        // console.log("Withdrew %s from %s", _amount, msg.sender);
        emit Withdrawal(msg.sender, _amount);
    }

    // Function to charge funds (add a balance for specific service)
    function charge(uint256 _amount) public {
        require(_amount > 0, "Charge amount must be greater than zero");
        balances[msg.sender] += _amount;
        // console.log("Charged %s to %s", _amount, msg.sender);
        emit Charge(msg.sender, _amount);
    }

    function send(
        uint256 _amount,
        address payable _to,
        string memory _qrCodeHash
    ) public {
        require(_amount > 0, "Send amount must be greater than zero");
        require(balances[msg.sender] >= _amount, "Insufficient balance");

        address payable recipient = _to;

        if (bytes(_qrCodeHash).length > 0) {
            recipient = payable(qrCodeToAddress[_qrCodeHash]);
            require(recipient != address(0), "Invalid QR code");
            balances[msg.sender] -= _amount;
            (bool success, ) = recipient.call{value: _amount}("");//_amount, gas: gasleft()
            require(success, "Transfer failed.");
            emit QRCodeAddressRetrieved(_qrCodeHash, recipient);
        } else {
            require(
                recipient != address(0),
                "Recipient address must be provided"
            );
            balances[msg.sender] -= _amount;
            (bool success, ) = recipient.call{value: _amount}("");
            // require(success, "Transfer failed."); // Esta linha pode ser removida
            emit Sent(msg.sender, recipient, _amount);
        }
    }

    // Send function to send funds to another account or retrieve funds linked to a QR code
    // function send(uint256 _amount, address payable _to, string memory _qrCodeHash) public {
    //     require(_amount > 0, "Send amount must be greater than zero");
    //     require(balances[msg.sender] >= _amount, "Insufficient balance");

    //     address payable recipient = _to;

    //     if (bytes(_qrCodeHash).length > 0) {
    //         recipient = payable(qrCodeToAddress[_qrCodeHash]);
    //         require(recipient != address(0), "Invalid QR code");
    //         balances[msg.sender] -= _amount;
    //         (bool success, ) = recipient.call{value: _amount}("");
    //         require(success, "Transfer failed.");
    //         emit QRCodeAddressRetrieved(_qrCodeHash, recipient);
    //     } else {
    //         require(recipient != address(0), "Recipient address must be provided");
    //         balances[msg.sender] -= _amount;
    //         (bool success, ) = recipient.call{value: _amount}("");
    //         require(success, "Transfer failed.");
    //         emit Sent(msg.sender, recipient, _amount);
    //     }
    // }



    // Function to associate a QR code hash with an address
    function linkQRCodeToAddress(
        string memory _qrCodeHash,
        address _address
    ) public {
        require(_address != address(0), "Invalid address");
        qrCodeToAddress[_qrCodeHash] = _address;
        emit QRCodeLinked(_qrCodeHash, _address);
    }

    // Function to retrieve the address linked to a QR code hash
    function getQRCodeAddress(
        string memory _qrCodeHash
    ) public view returns (address) {
        return qrCodeToAddress[_qrCodeHash];
    }

    // Function to check balance
    function getBalance() public view returns (uint256) {
        uint256 balance = balances[msg.sender];
        // console.log("Balance of %s is %s", msg.sender, balance);
        return balance;
    }
}

Habilitaremos o console.log do hardhat depois, por enquanto deixe os console.sol e .log comentados

Pronto temos um contrato em mãos, agora basta compilarmos ele.

Compilando seu contrato

Antes de rodarmos o comando no terminal, vamos criar mais dois scripts no package.json para encurtar nosso trabalho e não precisar ir para node_modules/hardhat e rodar de dentro, criando dois novos scripts:

{
  "devDependencies": {
    "@nomicfoundation/hardhat-toolbox": "^5.0.0",
    "hardhat": "^2.22.6"
  },
   "scripts": {
    "hardhat": "cd node_modules/hardhat && npx hardhat",
    "hardhat-compile": "cd node_modules/hardhat && npx hardhat compile",
    "hardhat-compile-v": "cd node_modules/hardhat && npx hardhat compile --show-stack-traces --verbose"
  }
}

Criamos dois comandos npm run hardhat-compile para compilar o contrato e npm run hardhat-compile-v que é um modo verboso do compile com toda stack trace gerada.
Aqui podemos apagar nosso contrato Lock.sol, pra não gerarmos arquivos desnecessários.
Agora vamos rodar nosso comando no terminal da raiz do nosso projeto npm run hardhat-compile, se tudo ocorrer bem você verá dois novos diretórios sendo eles cache/ e artifacts/, explore esses arquivos para saber mais.

Criando nossos testes

Primeiro vamos garantir que alguns modulos depreciados não estejam no n osso projeto, rode:
npm uninstall @nomiclabs/hardhat-waffle ethereum-waffle @nomiclabs/hardhat-ethers @nomiclabs/hardhat-etherscan chai@4 ethers hardhat-gas-reporter solidity-coverage @typechain/hardhat typechain @typechain/ethers-v5 @ethersproject/abi @ethersproject/providers

Agora vamos garantir que hardhat-toolbox esteja instalado, rode:
npm install --save-dev @nomicfoundation/hardhat-toolbox
Agora na pasta test/ exclua o Lock.js e crie o SimpleWallet.js arquivo com o seguinte conteúdo:

const { expect } = require("chai");
const { ethers } = require("hardhat");
require("@nomicfoundation/hardhat-toolbox");
// const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
describe("SimpleWallet", function () {
  let SimpleWallet;
  let owner;
  let addr1;
  let addr2;

  beforeEach(async function () {
    // get signers
    [owner, addr1, addr2] = await ethers.getSigners();

    // Load the contract SimpleWallet
    const SimpleWalletFactory = await ethers.getContractFactory("SimpleWallet");
    SimpleWallet = await SimpleWalletFactory.deploy();
    await SimpleWallet.waitForDeployment();

  });


  describe("deposit", function () {
    it("Should deposit funds", async function () {
      // Doing a deposit
      await SimpleWallet.deposit(100);

      // Check if balance of the owner address after the deposit
      const ownerBalance = await SimpleWallet.getBalance();
      expect(ownerBalance).to.equal(100, "Owner's balance should be 100 after deposit");
    });
  });

  describe("charge", function () {
    it("Should charge funds", async function () {
      const chargeAmount = ethers.parseEther("1");

      await SimpleWallet.connect(addr1).charge(chargeAmount);

      const balance = ethers.parseEther("1");
      expect(balance).to.equal(chargeAmount, "Contract balance should be equal to charge amount");
    });

    it("Should emit a Charge event", async function () {
      const chargeAmount = ethers.parseEther("1");

      await expect(SimpleWallet.connect(addr1).charge(chargeAmount))
        .to.emit(SimpleWallet, "Charge")
        .withArgs(addr1.address, chargeAmount);
    });

    it("Should revert on zero amount charge", async function () {
      await expect(SimpleWallet.connect(addr1).charge(0)).to.be.revertedWith("Charge amount must be greater than zero");
    });
  });

  describe("send", function () {
    it("Should send funds to another account", async function () {
      const initialBalance = await ethers.provider.getBalance(owner.address);
      const sendAmount = ethers.parseEther("1");

      // Deposit an initial specified balance
      await SimpleWallet.connect(owner).deposit(initialBalance);

      // Execute the send function to transfer funds
      await expect(SimpleWallet.connect(owner).send(sendAmount, addr2.address, ""))
        .to.emit(SimpleWallet, "Sent")
        .withArgs(owner.address, addr2.address, sendAmount);
       

      // Check the balance after the transaction
      const contractBalance = await SimpleWallet.connect(owner).getBalance();
      expect(contractBalance).to.equal(initialBalance - sendAmount);
    });

    it("Should emit a Sent event", async function () {
      const sendAmount = ethers.parseEther("1");

      await SimpleWallet.connect(addr1).deposit(sendAmount);

      await expect(SimpleWallet.connect(addr1).send(sendAmount, addr2.address, ""))
        .to.emit(SimpleWallet, "Sent")
        .withArgs(addr1.address, addr2.address, sendAmount);
    });

    it("Should revert on insufficient balance", async function () {
      const sendAmount = ethers.parseEther("1");

      await expect(SimpleWallet.connect(addr1).send(sendAmount, addr2.address, "")).to.be.revertedWith("Insufficient balance");
    });

    it("Should revert on zero amount send", async function () {
      await expect(SimpleWallet.connect(addr1).send(0, addr2.address, "")).to.be.revertedWith("Send amount must be greater than zero");
    });

    it("Should revert on invalid QR code", async function () {
      const sendAmount = ethers.parseEther("1");

      await SimpleWallet.connect(addr1).deposit(sendAmount);

      await expect(SimpleWallet.connect(addr1).send(sendAmount, addr2.address, "invalidQRCode")).to.be.revertedWith("Invalid QR code");
    });

    it("Should revert on missing recipient address", async function () {
      const sendAmount = ethers.parseEther("1");

      await SimpleWallet.connect(addr1).deposit(sendAmount);

      await expect(SimpleWallet.connect(addr1).send(sendAmount, ethers.ZeroAddress, "")).to.be.revertedWith("Recipient address must be provided");
    });


  });

  describe("linkQRCodeToAddress", function () {
    it("Should revert with 'Invalid address' when linking to AddressZero", async function () {
      const qrCode = "exampleQRCode";

      await expect(
        SimpleWallet.connect(owner).linkQRCodeToAddress(qrCode, ethers.ZeroAddress)
      ).to.be.revertedWith("Invalid address");
    });

    it("Should link QR code to a valid address", async function () {
      const qrCode = "exampleQRCode";
      const validAddress = addr1.address;

      // Link QR code to a valid address
      await SimpleWallet.connect(owner).linkQRCodeToAddress(qrCode, validAddress);

      // Check that the QR code is linked to the correct address
      const linkedAddress = await SimpleWallet.getQRCodeAddress(qrCode);
      expect(linkedAddress).to.equal(validAddress, "QR code should be linked to the correct address");
    });
  });

  describe("getBalance", function () {
    it("Should return the correct balance", async function () {
      // Check opening balance
      const initialBalance = await SimpleWallet.getBalance();
      expect(initialBalance).to.equal(0, "Initial balance should be 0");

      // Make a deposit
      const depositAmount = ethers.parseEther("0.3"); // 0.3 ETH in wei
      await SimpleWallet.deposit(depositAmount);

      // Check balance after deposit
      const balanceAfterDeposit = await SimpleWallet.getBalance();
      expect(balanceAfterDeposit).to.equal(depositAmount, "Balance should be 0.3 ETH after deposit");
    });
  });

  describe("withdraw", function () {
    it("Should withdraw funds", async function () {
      const depositAmount = ethers.parseEther("1");

      await SimpleWallet.connect(addr1).deposit(depositAmount);
      await SimpleWallet.connect(addr1).withdraw(depositAmount);

      expect(await SimpleWallet.getBalance()).to.equal(0);
    });

    it("Should emit a Withdrawal event", async function () {
      const depositAmount = ethers.parseEther("1");

      await SimpleWallet.connect(addr1).deposit(depositAmount);

      await expect(SimpleWallet.connect(addr1).withdraw(depositAmount))
        .to.emit(SimpleWallet, "Withdrawal")
        .withArgs(addr1.address, depositAmount);
    });

    it("Should revert on insufficient balance", async function () {
      const depositAmount = ethers.parseEther("1");

      await expect(SimpleWallet.connect(addr1).withdraw(depositAmount)).to.be.revertedWith("Insufficient balance");
    });

    it("Should revert on zero amount withdrawal", async function () {
      await expect(SimpleWallet.connect(addr1).withdraw(0)).to.be.revertedWith("Withdrawal amount must be greater than zero");
    });
  });
});

Pronto, os tests são auto explicativos em seus nomes, agora vamos criar dois novos scripts em package.json para rodar esses tests, em package.json adicione mais esses scripts:

"hardhat-node": "cd node_modules/hardhat && npx hardhat node",
"hardhat-test": "cd node_modules/hardhat && npx hardhat test"

Com npm run hardhat-node vamos criar rodar a rede local do hardhat em um terminal, em outro terminal rode npm run hardhat-test, aqui todos os 17 testes devem passar. Você não precisa necessariamente rodar o node para rodar o test, mas lá você pode ter noção das contas que estamos usamos e conferir se está tudo ok para rodarmos nossa aplicação na rede local do hardhat.

Fazendo deploy do contrato para rede local via ignition

Agora para fazermos o deploy, primeiramente, na rede local, depois faremos em uma rede teste online via Infura para se conectar à rede testnet da Sepolia, recomendada pela própria Ethereum.org aqui, mas antes vamos fazer o seguinte.
Primeiro crie um arquivo js em /ignition/modules/ chamado SimpleWallet.js com o seguinte conteúdo:

const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

module.exports = buildModule("SimpleWalletModule",  (m) => {
  const simpleWallet =  m.contract("SimpleWallet", [])
  return { simpleWallet }
});

Agora criaremos outro script para rodar npx hardhat ignition deploy ./ignition/modules/SimpleWallet.js --network localhost no nosso package.json:

"hardhat-ignition-deploy": "cd node_modules/hardhat && npx hardhat ignition deploy ../../ignition/modules/SimpleWallet.js --network %npm_config_name%"

A única diferença nesse script é que ele vai receber o nome da network dinâmicamente com --name=localhost por exemplo, pra que no futuro possamos usar outra rede caso necessário, você poderia fazer o mesmo definindo outra variável caso possua mais de uma ignition e também devemos nos lembrar que o npx roda de dentro de node_modules/hardhat, portanto estamos voltando alguns repositórios para encontrar nossa /ignition.

Pra esse caso será necessário que a rede esteja rodando em um terminal e você execute o ignition deploy em outro.
Em um terminal rode : npm run hardhat-node
Em outro terminal rode : npm run hardhat-ignition-deploy --name=localhost

Se tude ocorrer bem você deve receber uma mensagem no terminal do node como:

eth_call
  Contract deployment: SimpleWallet
  Contract address:    0x5fbdb2315678afecb367f032d93f642f64180aa3    
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266    

eth_sendTransaction
  Contract deployment: SimpleWallet
  Contract address:    0x5fbdb2315678afecb367f032d93f642f64180aa3    
  Transaction:         0x106888b066befe7989423edd6d6779dce44cb1f2d0f1598acc977b10dc6460d7
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266    
  Value:               0 ETH
  Gas used:            1196395 of 1196395
  Block #1:            0x147493863adf7f7c17a1628a33dcdd99a41f7d58699d8f2e521ad907ff71b94f

E um aviso em vermelho hardhat_setLedgerOutputEnabled - Method not supported pode ser ignorado com segurança, mas não devemos alertá-lo sobre fazer algo que é totalmente esperado (uma implantação do Ignition) (comentário), segundo essa própria issue do NomicFoundation responsável pelo própio hardhat.

E no terminal do ignition deploy :
SimpleWalletModule#SimpleWallet - 0x5FbDB2315678afecb367f032d93F642f64180aa3

Criando nossos endpoints