Muitas vezes em aplicações web nós enviamos uma requisição para um servidor e ela levará um tempo considerável para ser processada. Quando não sabemos quanto é esse tempo e não podemos manter a conexão ativa até receber uma resposta sob risco de timeout temos de pensar em alternativas. A alternativa mais simples e ineficiente é fazer “pulling”, uma técnica que consiste em criar um timer e ficar fazendo requisições ao servidor pedindo por atualizações. A melhor alternativa é buscar ser avisado pelo servidor de algum forma quando o processamento acabar, geralmente envolvendo websockets ou webhooks.
Na programação para blockchain temos este mesmo problema elevado à enésima potência uma vez que o processamento em nós da rede é caro e geralmente lento, ao menos nas redes mais famosas como a Ethereum. Mesmo que esteja usando uma rede mais barata como a BNB Smart Chain (BSC) ou Polygon, ainda enfrentará esse dilema de como saber que um processamento terminou sem ficar consultando várias vezes a blockchain?
Neste universo em específico a solução é monitorar/escutar por eventos específicos nos smart contracts aguardando por novidades e é isso que vamos explorar no tutorial de hoje, usando a biblioteca Web3 e a tecnologia Node.js.
#1 – Eventos em Solidity
O primeiro ponto é entender como os eventos em Solidity funcionam, assumindo aqui que você estará trabalhando com blockchains baseadas em Ethereum/EVM. Quando estamos programando nossos smart contracts nós podemos usar um keyword event para declarar um evento, como abaixo retirado de um contrato de token ERC-20.
1 2 3 |
event Transfer(address indexed from, address indexed to, uint256 value); |
Este evento Transfer é declarado no contrato e indica que ele pode ser monitorado/escutado por alguém conectado à blockchain. Agora para que o evento aconteça de fato, seja disparado, o programador do smart contract deve usar a keyword emit em alguma função do contrato, como abaixo, retirado do mesmo contrato ERC-20.
1 2 3 |
emit Transfer(msg.sender, to, value); |
Assim, quando chamamos emit Transfer, nós enviamos um registro de evento para a blockchain que será incluído junto aos logs do bloco minerado e qualquer sistema que esteja monitorando este evento em específico será avisado, como uma carteira de criptomoedas por exemplo.
Um último ponto ainda sobre eventos no Solidity é que você pode querer escutar os eventos de uma carteira específica, ao invés de ficar escutando qualquer transferência que acontecer. Isso é possível através do uso da keyword indexed como mostrado acima, onde tanto o from quanto o to podem ser usados como filtro no monitoramento para receber apenas eventos de um from ou de um to específicos. Importante frisar que o uso da keyword indexed aumenta o custo de gás das transações que emitirem este evento, além do que existe um limite de até 3 variáveis indexed por evento apenas, então use com parcimônia.
Apesar do exemplo acima ser bem comum, envolvendo tokens e transferências, você pode criar o evento que quiser para qualquer finalidade. Por exemplo, se tiver um smart contract de uma dex (exchange descentralizada), você pode querer ter um evento para quando um trade é realizado entre dois traders, como abaixo.
1 2 3 |
event NewTrade(uint timestamp, address indexed from, address indexed to, uint amount); |
Repare que este evento não tem nada a ver com o anterior, seus campos são completamente diferentes. E aí, na função que faz o trade acontecer de fato, você emite este evento para avisar quem quer que esteja monitorando este evento neste contrato.
1 2 3 |
emit NewTrade(block.timestamp, msg.sender, to, amount); |
Dito isso, agora que você entendeu como os eventos são declarados e emitidos nos contratos é hora de entender como monitoramos eles em nossas aplicações web.
#2 – Monitorando eventos no backend
Existem duas bibliotecas para criar aplicações Web3 no mercado que realmente valem a pena ser citadas: a Web3.js e a EthersJS. Neste tutorial usarei a primeira por ser a mais antiga e mais largamente utilizada.
Para nosso exemplo de aplicação web frontend eu usarei Node.js, que é uma tecnologia muito usada em backend para Web3. Mesmo sem conhecimento algum de Node.js você pode criar um projeto simples usando o comando abaixo na sua máquina, dentro de uma pasta criada para guardar o projeto (a minha chamei de nodejs-evento-web3).
1 2 3 |
npm init -y |
Depois, entre na pasta do projeto e vamos instalar as bibliotecas web3.js e dotenv que vamos usar.
1 2 3 |
npm i web3 dotenv |
Agora com tudo preparado, vamos criar uma funcionalidade no backend (crie um arquivo index.js) que espera pelo evento de transferência de um token. Para que você consiga simular esse teste é importante que você já tenha publicado um contrato seu na blockchain, a fim de poder interagir com ele de forma a causar o disparo do evento que estará monitorando. Eu estarei usando este contrato de token ERC-20 que publiquei na BSC Testnet.
Para fazer a conexão com seu contrato você vai precisar ter o ABI dele (Application Binary Interface) que você consegue após compilar ele ou após verificá-lo no BSCScan/EtherScan. O meu ABI do contrato ERC-20 está abaixo e ele define toda a especificação pública do contrato.
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":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","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":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","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":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] |
Com seu ABI em mãos, crie um abi.json na pasta do seu projeto de backend e importe ele no topo do arquivo index.js junto do pacote Web3 e do carregamento do dotenv (que vamos configurar logo mais).
1 2 3 4 5 |
const ABI = require("./abi.json"); const Web3 = require("web3"); require("dotenv").config(); |
Para receber eventos da blockchain é importante que você tenha uma conexão de websockets com um full node. Felizmente hoje é dia é muito fácil conseguir acesso a um usando provedores de blockchain as a service como Infura, Moralis, Ankr, Quicknode e outros que fornecem um acesso limitado gratuito aos mesmos. No meu caso que tenho o contrato publicado na BSC Testnet vou usar a Quicknode neste exemplo que é um dos provedores que fornecem nós para BSC. Se vai publicar na Ethereum ou Sepolia, pode usar a Infura por exemplo. Apenas crie uma conta no provedor da sua preferência e use o nó gratuito para obter a URL do servidor de websockets que será necessário mais abaixo.
Essa URL, bem como o endereço do seu contrato na blockchain devem ser colocados por segurança em um arquivo .env na raiz do seu projeto, que não deve ser versionado, como abaixo.
1 2 3 4 |
WEBSOCKET_URL=<sua url> CONTRACT_ADDRESS=<seu contrato> |
Com as importações realizadas e variáveis de ambiente posicionadas, vamos escrever a função dentro do index.js que fará o monitoramento e tratamento do evento depois de recebido.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
async function doListen() { const web3 = new Web3(process.env.WEBSOCKET_URL); const contract = new web3.eth.Contract(ABI, process.env.CONTRACT_ADDRESS); contract.events.Transfer({ filter: { from: "0xE4ffEEd88111e1DFCc3a852d9334C65e38BF2880" }, fromBlock: "latest" }) .on('data', event => console.log("event: " + JSON.stringify(event))) .on('changed', changed => console.log("changed: " + changed)) .on('error', err => console.error(err)) .on('connected', str => console.log("connected: " + str)); } doListen(); |
Começamos a função carregando um objeto Web3 com a URL do servidor de websockets. Depois inicializamos com este objeto um contrato configurado com o ABI e endereço do contrato que temos publicado na blockchain. Com este contrato podemos configurar a assinatura (eth_subscribe) de eventos da blockchain onde o nome do evento é Transfer em meu exemplo, conforme consta no contrato em Solidity. Para a função Transfer podemos passar ainda um objeto de opções com as propriedades filter, fromBlock e toBlock. O filter (que eu usei a minha carteira como exemplo) permite usar qualquer propriedade indexed do evento para usar como filtro, enquanto que fromBlock e toBlock definem os números de blocos válidos para receber os eventos.
Uma vez configurada essa conexão ao evento Transfer nós definimos alguns listeners de eventos como o data, que é disparado cada vez que um novo evento é enviado ou o connected que é disparado quando a conexão é estabelecida. Neste exemplo eu configurei todos os eventos possíveis apenas imprimindo no console, para você conseguir testar.
Agora rode a sua aplicação com ‘node index.js’ e conseguirá testar facilmente desde que consiga simular a situação que emitirá o evento, como na imagem abaixo. Com isso finalizamos nosso exemplo de backend.
E com isso finalizamos o nosso tutorial de como monitorar eventos da blockchain com Web3.js, espero que tenha lhe ajudado!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.