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 blockchains 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á!
#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.
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 Sepolia, 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 e em segundos terá saldo para usar na rede Sepolia. Caso tenha problemas com a Sepolia, 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 API Key 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-ethers-node e dentro dela rode o comando para inicialização do projeto.
1 2 3 |
npm init -y |
Com o projeto inicializado, vamos instalar as dependências que usaremos.
1 2 3 |
npm i dotenv ethers |
O dotenv é para guardarmos as configurações de ambiente, enquanto que o Ethers é 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.
1 2 3 4 5 6 7 |
WALLET=<o endereço público da sua MetaMask> PRIVATE_KEY=<sua chave privada da MetaMask> NETWORK=<o nome da sua rede, em minúsculo> INFURA_API_KEY=<sua API Key da Infura> CONTRACT_ADDRESS=<explicarei a seguir> |
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 API Key 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.
#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ê.
1 2 3 |
[{"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 Sepolia.
1 2 3 |
CONTRACT_ADDRESS=0x0F8A765CACA46d9338f663652894e19fC430F5b5 |
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.
1 2 3 4 5 6 7 8 9 10 |
require('dotenv').config(); const ABI = require("./abi.json"); const { ethers } = require("ethers"); const provider = new ethers.InfuraProvider( process.env.NETWORK, process.env.INFURA_API_KEY ) |
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.
Depois, importei a classe InfuraProvider da biblioteca EthersJS. Com ela eu inicializei um objeto provider conectando ele em nosso node da Infura que você deve ter configurado a API Key e Network no seu .env. É com esse objeto provider 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 RSCoin, ele possui as operações padrões para tokens o que inclui chamadas para ver o saldo em frações de RSCoins. Primeiro, vamos criar rapidamente a função que pega e imprime o saldo de uma carteira que possua este token no console, como abaixo.
1 2 3 4 5 6 7 8 |
async function getRscBalance(from) { const contract = new ethers.Contract(process.env.CONTRACT_ADDRESS, ABI, provider); const balance = await contract.balanceOf(from); console.log(ethers.formatEther(balance)); } getRscBalance("0xE4ffEEd88111e1DFCc3a852d9334C65e38BF2880"); |
No exemplo acima nós usamos ethers.Contract para criar uma conexão com o contrato passando o seu ABI, endereço e provider (Infura) por parâmetro. Uma vez com o objeto de contrato configurado, podemos usar contract 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 formatEther da ethers para converter da menor fração para a maior (RSCoin 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 RSC. O resultado deve ser a impressão do saldo dela em RSC.
Agora vamos fazer algo mais elaborado, uma transação!
#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 RSC (a moeda que inventei). Uma delas é a função approve que serve para você fornecer autorização para outra conta transferir RSCs 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 (RSC).
Vamos criar a nossa função approvePrcTransfer no index.js, como abaixo. Esta função é diferente da anterior pois parte da complexidade é a autenticação, que é a primeira coisa que resolvemos aqui criando o objeto signer a partir de ethers.Wallet e a chave privada.
1 2 3 4 5 6 7 8 9 10 11 12 |
async function approveRscTransfer(spender, value) { const signer = new ethers.Wallet(process.env.PRIVATE_KEY, provider); const contract = new ethers.Contract(process.env.CONTRACT_ADDRESS, ABI, signer); const tx = await contract.approve(spender, value, {from: process.env.WALLET}); const receipt = await tx.wait(); console.log(tx.hash); return tx.hash; } approveRscTransfer("0x0D1195969395B8a23dA37Dce78b823BE8cD5a0a4", "1000"); |
Depois a gente inicializa o contrato, como já fizemos no passado, usando o ABI, o endereço do contrato e o signer que configuramos antes.
Na linha seguinte, chamamos a função approve presente em nosso ABI e passamos a ela os dois parâmetros recebidos na função approveRscTransfer: 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 dizendo que quem vai enviar esta transação é a nossa carteira.
O resultado é um recibo de transação que você pode pegar o hash e usar para consultar depois o EtherScan. Um exemplo de transação realizada com o código acima pode ser vista aqui (é na Goerli, mas serve como exemplo).
E com isso você aprendeu como fazer calls e sends em smart contracts existentes na blockchain usando Node.js e a biblioteca EthersJS, com uma ajudinha da MetaMask e Infura.
Até o próximo tutorial!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.