Back-End

16 nov, 2023

Blockchain: Integração com Smart Contracts com Node.js e Web3.js

Publicidade

A web3 está aí e cada vez mais as empresas estão querendo criar funcionalidades e até sistemas inteiros baseados na blockchain. Desta forma, para acompanhar esta tendência é importante que os programadores aprendam como implementar isto e felizmente a esta altura já existem muitas formas de fazer este tipo de integração, em especial com as blockchain baseadas em Ethereum ou EVM-compatible.

No tutorial de hoje eu vou ensinar como escrever aplicações Node.js que se conectam à blockchain e interagem com smart contratcs, fazendo consultas e transações neles. Não é imprescindível o conhecimento de smart contracts para implementar os exemplos de código, mas ajuda bastante, então deixo abaixo um vídeo sobre o assunto.

E se você já fez outro tutorial sobre integração com blockchain aqui no blog recentemente, pode aproveitar o mesmo projeto, mas não pule a parte de setup, dá uma revisada completa nela porque veremos algumas coisas novas aqui. Se não fez o outro tutorial e não sabe do que estou falando, não se preocupe, apenas segue o passo a passo abaixo que não tem erro.

Então vamos lá!

Blockchain: Integração com Smart Contracts com Node.js e Web3.js

#1 – Setup

Neste tutorial vamos usar Node.js e ele não é indicado para quem nunca programou Node.js na vida. Inclusive você precisa ter esta ferramenta já instalada na sua máquina antes de começar. No vídeo abaixo ensino como instalar Node.js e VS Code, a ferramenta que uso para programar.

Depois de ter o Node.js instalado na sua máquina, o próximo passo é ter uma carteira de criptomoedas, já que para se comunicar com a blockchain é obrigatório ter uma. Como vamos programar para blockchains Ethereum, tem de ser uma carteira compatível com a mesma e eu recomendo a MetaMask. O vídeo abaixo ensina o que é e como criar a sua, gratuitamente.

LEIA MAIS: 7Masters Blockchain – Projetos em Blockchain e Criptomoedas

A MetaMask vem por padrão configurada para a rede Mainnet da Ethereum, o que não é muito indicado para desenvolvimento. Indo no select de redes no topo dela você pode usar a opção “mostrar redes de teste” e habilitar o aparecimento da rede Goerli, usada para testes e que vamos usar aqui.

Mesmo sendo uma rede de testes você vai precisar de saldo em ETH para poder fazer transações para ela, então recomendo que use este Faucet PoW para ganhar algumas moedas. Basta conectar a sua carteira, deixar minerando e em minutos terá saldo para usar na rede Goerli. Caso tenha problemas com a Goerli, outra opção é usar a BSC Testnet da Binance, que no vídeo que passei acima eu ensino a configurar na MetaMask ou qualquer outra rede compatível com EVM (Avalanche, Polygon, etc).

Para que nosso script possa assinar transações usando a sua carteira ele vai precisar da chave privada dela. Para obtê-la vá na MetaMask, nas reticências e em Detalhes da Conta, como mostra a imagem abaixo, indo na opção “Exportar Chave privada” que vai exigir a sua senha da MetaMask. Guarde essa chave privada em um lugar seguro, vamos precisar dela logo mais.

Agora que você tem as ferramentas de desenvolvimento e uma carteira cripto com saldo, o próximo passo é obter um full node RPC da rede Ethereum para podermos nos conectar. Você pode obter um gratuitamente com a Infura, um dos maiores provedores de Blockchain as a Service do mundo. Crie uma conta gratuita no site deles e depois crie um node da Sepolia para você assim que conseguir entrar no painel. Guarde a URL que vai receber, vamos precisar dela mais tarde.

Agora sim, temos todo o necessário para começar a programar, então vamos criar nosso projeto. Crie na sua máquina uma pasta metamask-web3-node e dentro dela rode o comando para inicialização do projeto.

npm init -y

Com o projeto inicializado, vamos instalar as dependências que usaremos.

npm i dotenv web3

O dotenv é para guardarmos as configurações de ambiente, enquanto que o web3.js é o módulo que usaremos para comunicação com a blockchain. Ele nada mais faz do que abstrai todas as chamadas ao node RPC da blockchain que vamos nos conectar, facilitando toda a comunicação da nossa aplicação com o mesmo, já que usaremos boas e velhas funções JS.

Para configurar nossas variáveis de ambiente, crie um arquivo .env na raiz da aplicação e nele coloque as seguintes variáveis.

WALLET=<o endereço público da sua MetaMask>
PRIVATE_KEY=<sua chave privada da MetaMask>
INFURA_URL=<sua URL da Infura>
CONTRACT_ADDRESS=<endereço/hash do seu contrato na blockchain>

Atenção com a private key da sua carteira MetaMask: jamais informe ela, para ninguém. Também não versione esse arquivo .env, certifique-se de colocá-lo no seu .gitignore se estiver usando Git. Já a URL da Infura é legal manter em segredo também, já que ela possui limites de uso, mas nada muito crítico.

Agora terminamos o setup e podemos começar a programar.

LEIA MAIS: 7Masters Blockchain – Smart Contracts Multi-Signed

#2 – Calls em Smart Contracts

Existem dois tipos de interações que você pode fazer com smart contracts: as calls e os sends.

As calls são chamadas somente de leitura, para pedir informações a um contrato como por exemplo o saldo de um token ou uma pesquisa. Já as sends são transações, ou seja, operações que escrevem novos dados na blockchain e portanto possuem taxas associadas para pagar o trabalho dos mineradores. Sim, enquanto calls são instantâneos, como se fossem chamadas GET de APIs, sends exigem mineração para registrar os novos dados.

A primeira coisa que você deve aprender é como fazer calls em contratos e depois como fazer sends/transacionar em contratos. Caso você possua algum smart contract publicado, pode usar ele, caso contrário use este meu aqui que é um exemplo de token ERC-20 (tutorial aqui) que você confere tudo que ele faz na aba contract do EtherScan. Para chamar este meu contrato ou qualquer outro, você precisa ter acesso ao ABI do contrato, a Application Binary Interface: um array JSON com objetos explicando todas as chamadas públicas que podem ser feitas, com seus parâmetros e retornos. Este ABI é necessário para que as bibliotecas consigam fazer as requests corretamente ao servidor RPC e se foi você que fez o deploy, tem acesso à API na pasta de build do seu contrato. Se não foi você, muitas vezes ele está disponível no EtherScan, que é o meu caso e que reproduzo abaixo o ABI para você.

[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_owner","type":"address"},{"indexed":true,"internalType":"address","name":"_spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_from","type":"address"},{"indexed":true,"internalType":"address","name":"_to","type":"address"},{"indexed":false,"internalType":"uint256","name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"},{"internalType":"address","name":"_spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"remaining","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_spender","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"address","name":"_to","type":"address"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"success","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]

 

Se for usar o meu contrato, pegue este ABI acima e coloque em um arquivo abi.json na raiz do seu projeto. Além do ABI, a segunda configuração que será necessária é o endereço do seu contrato na variável CONTRACT_ADDRESS do .env, certifique-se de ter preenchido. Caso esteja usando o meu contrato, segue o endereço dele na Goerli.

CONTRACT_ADDRESS=0x6e9fea1f661c599009844a97fa63c96bfc6ad83c

Para começarmos a programar, crie um arquivo index.js na raiz da sua aplicação e vamos começar importando as bibliotecas que precisamos e inicializando alguns objetos.

require('dotenv').config();
const ABI = require("./abi.json");
const Web3 = require('web3');
const web3 = new Web3(process.env.INFURA_URL);

Aqui eu importei e configurei o dotenv, que vai carregar todas nossas variáveis de ambiente contidas no arquivo .env para a memória. Depois importei o ABI a partir do arquivo JSON, usaremos ele mais tarde nas chamadas.

LEIA MAIS: 7Masters Blockchain – Blockchain em 7 minutos

Depois, importei a classe Web3 da biblioteca de mesmo nome. Com ela eu inicializei um objeto web3 conectando ele em nosso node da Infura que você deve ter configurado a URL no seu .env. É com esse objeto web3 que faremos todas as chamadas à blockchain.

Essa configuração inicial é suficiente para fazer chamadas/calls, ou seja, operações apenas de leitura. Mais à frente vou falar da diferença para sends/transações.

Meu contrato é de um novo token ERC-20 chamado ProtoCoin, ele possui as operações padrões para tokens o que inclui chamadas para ver o saldo em frações de ProtoCoins. Primeiro, vamos criar rapidamente a função que pega e imprime o saldo de uma carteira que possua este token no console, como abaixo.

async function getPrcBalance(from){
    const contract = new web3.eth.Contract(ABI, process.env.CONTRACT_ADDRESS);
    const balance = await contract.methods.balanceOf(from).call();
    console.log(web3.utils.fromWei(balance));
}
getPrcBalance("0xE4ffEEd88111e1DFCc3a852d9334C65e38BF2880");

 

No exemplo acima nós usamos web3.eth para criar uma conexão com o contrato passando o seu ABI e endereço por parâmetro. Uma vez com o objeto de contrato configurado, podemos usar contract.methods para chamar qualquer função pública do mesmo que esteja definida no seu ABI, como no caso da balanceOf que espera uma carteira por parâmetro e retorna o saldo dela na menor fração da moeda. Repare que usei a função fromWei da web3.utils para converter da menor fração para a maior (ProtoCoin no meu caso). Apesar de eu não estar trabalhando com ETH mas uma moeda própria, as funções utilitárias funcionam normalmente.

A última linha é apenas para chamar a função quando rodarmos a aplicação e optei por passar a minha carteira que possui PRC. O resultado deve ser a impressão do saldo dela em PRC.

Agora vamos fazer algo mais elaborado, uma transação!

LEIA MAIS: O futuro dos brasileiros é o blockchain

#3 – Sends/Transactions em Smart Contracts

Além de calls, outra tarefa muito recorrente é a necessidade de fazer sends em contratos. Isso porque todas as funções de um contrato que exijam a escrita de dados na blockchain exige que você envie e pague por uma transação. Como estamos em uma rede ETH, as taxas de transação são sempre em ETH e para que uma transferência ocorra é necessário enviar uma transação para a rede, assinada pela carteira que vai invocar a função de escrita.

Se estiver usando o meu contrato de exemplo, tem algumas funções que podemos usar mesmo que você não tenha saldo em PRC (a moeda que inventei). Uma delas é a função approve que serve para você fornecer autorização para outra conta transferir PRCs da sua carteira, o que chamamos de transferência delegada. Se você não entende nada de tokens ERC-20, não se preocupe, é uma função inofensiva pois estamos falando de um token fictício (PRC).

Para que você possa assinar transações vamos precisar mudar a forma como estamos nos conectando à blockchain, então mude a configuração do nosso objeto web3 como abaixo.

require('dotenv').config();
const ABI = require("./abi.json");
const Web3 = require('web3');
const web3 = new Web3(process.env.INFURA_URL);
const account = web3.eth.accounts.privateKeyToAccount('0x' + process.env.PRIVATE_KEY);
web3.eth.accounts.wallet.add(account);

Além de fazermos a conexão ao nó Infura com a classe Web3, nós carregamos uma account a partir da chave privada dela (configurada no .env). Depois, injetamos esta conta como sendo a primeira carteira a ser usada pelo objeto web3, logo, a que será usada nas transações por padrão (a menos que especifique um from).

Atenção ao fato de que esta alteração é completamente opcional se você quer apenas ler (calls) a blockchain. Agora se você quer escrever (sends) precisa estar devidamente autenticado, o que é claro não afeta as leituras, pode testar sua call novamente que vai continuar funcionando.

Agora vamos criar a nossa função approvePrcTransfer no index.js, como abaixo. Esta função não é muito diferente da anterior pois parte da complexidade (a autenticação) já foi resolvida na instanciação do objeto web3. Então vamos lá!

async function approvePrcTransfer(spender, value){
    const contract = new web3.eth.Contract(ABI, process.env.CONTRACT_ADDRESS, { from: process.env.WALLET });
    const tx = await contract.methods.approve(spender, value).send();
    console.log(tx.transactionHash);
    return tx;
}
approvePrcTransfer("0x0D1195969395B8a23dA37Dce78b823BE8cD5a0a4", "1000");

LEIA MAIS: Blockchain – A arquitetura disruptiva

A primeira inicializa o contrato, como já fizemos no passado, usando o ABI, o endereço do contrato e dizendo que quem vai assinar esta requisição é a carteira configurada no .env. Importante frisar que esta carteira deve ser a mesma usada para inicializar o objeto web3, a diferença é que lá tivemos de usar a chave privada e aqui estamos usando apenas o endereço público.

Na linha seguinte, chamamos a função approve presente em nosso ABI e passamos a ela os dois parâmetros recebidos na função approvePrcTransfer: o spender, que é um endereço de carteira que vamos autorizar gastar nossos tokens, e o value, uma string com o valor (em wei) que o spender pode gastar. Finalizamos chamando a função sendo que envia a transação para a blockchain, sendo que esta transação será assinada com a privateKey que você informou na inicialização do objeto de carteira, no início do código, lembra?

O resultado é um recibo de transação que você pode pegar o hash e usar para consultar depois o EtheScan. Um exemplo de transação realizada com o código acima pode ser vista aqui.

E com isso você aprendeu como fazer calls e sends em smart contracts existentes na blockchain usando Node.js e a biblioteca web3.js, com uma ajudinha da MetaMask e Infura.

Até o próximo tutorial!