Cómo rastrear transacciones de blockchain con web3.js

El número de dApps y tokens en el mercado crece rápidamente: cada año aparecen más de 1000 nuevas aplicaciones distribuidas, 500 tokens nuevos y servicios online construidos alrededor de ambos. La tecnología blockchain aporta muchas funcionalidades y ventajas fascinantes. Sin embargo, la idea de fondo siempre es la misma: para transferir fondos cripto o ejecutar cualquier otra función de un smart contract, hay que publicar una transacción en la blockchain y que se mine.
Para algunas aplicaciones y servicios online es fundamental conocer el estado actual de la wallet de un usuario o de las transacciones que generan sus acciones. Hay muchos servicios web que permiten a los desarrolladores obtener detalles de wallets y transacciones a través de una API JSON, como BlockCypher. Aun así, había pocos sitios, si es que había alguno, donde ver todas las monedas y tokens necesarios en un mismo lugar.
En este artículo te explico cómo resolverlo en la práctica.
Proyectos distintos, mismas tareas
Cada blockchain usa protocolos de consenso diferentes, pero en lo que respecta a vigilar wallets y transacciones, todas tienen mucho en común. Detectar el momento de la transferencia de fondos y verificar que la transacción ha alcanzado el número de confirmaciones necesario por motivos de seguridad es una tarea típica de los proyectos cripto.
Por eso, el seguimiento de criptoactivos se puede dividir en dos partes. Primero, vigilar la wallet para detectar transferencias entrantes y quedarse con la que cumpla nuestros criterios de filtrado. Segundo, usar el hash de esa transferencia para seguir sus confirmaciones. Seguimiento de transacciones de ETH
Empecemos por la moneda de la blockchain de Ethereum: Ether. Gracias al trabajo del equipo de desarrollo de Ethereum y de la comunidad open source, contamos con un número bastante impresionante de implementaciones de Web3:
- Python Web3.py
- Haskell hs-web3
- Java web3j
- Scala web3j-scala
- Purescript purescript-web3
- PHP web3.php
- PHP ethereum-php
- Swift web3swift
No obstante, la implementación original y más popular es la de JavaScript, llamada web3.js. Así que vamos a usar esta potente herramienta para trabajar directamente con los nodos de la blockchain de Ethereum y ver cómo funciona:
npm install web3
Suscripciones a transacciones de ETH
La API de Web3 no permite suscribirse directamente a las transacciones entrantes de una wallet, así que para resolver este problema concreto hay que usar otra estrategia. Primero nos suscribimos a todas las transacciones pendientes y después las filtramos por la dirección de la wallet del remitente y otros criterios.
Web3.js permite comunicarse directamente con un nodo de Ethereum mediante varios protocolos, entre ellos HTTP y WebSockets. En este ejemplo se usa la Testnet Rinkeby y los nodos que ofrece Infura. Pero puedes usar cualquier otro proveedor, incluidos nodos locales.
En la función que verás más abajo se crea una nueva instancia de web3 con el proveedor de WebSockets y se usa para establecer una suscripción a las transacciones recién creadas en la blockchain. Esta instancia se llama pending.
Un objeto de suscripción se puede gestionar con dos métodos: subscribe() y unsubscribe(). Ambos aceptan una función callback para gestionar los errores y los resultados de la suscripción. Con las suscripciones a transacciones pendientes podemos gestionar dos eventos: data y error. Evidentemente, data es el evento clave que necesitamos para encontrar la transferencia.
El handler del evento data solo recibe un parámetro de entrada, que representa el hash de una transacción, y comprueba si cada transacción cumple nuestros criterios de búsqueda cuando necesitamos más detalles que el simple hash. Ahí es donde entra otro método de web3.js, getTransaction(), que sirve para leer los detalles de la transacción. Como es una llamada asíncrona, basta con envolverla en un try-catch y esperar la respuesta con await. Puedes consultar el formato del objeto de respuesta en la documentación oficial de web3 aquí.
Cuando recibimos la respuesta y la transacción cumple las condiciones del filtro, hay que iniciar el proceso de confirmación de la transacción y cancelar la suscripción llamando a su método unsubscribe(). Si no hay ninguna transacción que cumpla el filtro, simplemente salimos de la función para seguir suscritos.
Esta es la función:
function watchEtherTransfers() { // Instantiate web3 with WebSocket provider const web3 = new Web3(new Web3.providers.WebsocketProvider('wss://rinkeby.infura.io/ws')); // Instantiate subscription object const subscription = web3.eth.subscribe('pendingTransactions'); // Subscribe to pending transactions subscription .subscribe((error, result) => { if (error) console.log(error); }) .on('data', async (txHash) => { try { // Instantiate web3 with HttpProvider const web3Http = new Web3('https://rinkeby.infura.io/'); // Get transaction details const trx = await web3Http.eth.getTransaction(txHash); const valid = validateTransaction(trx); // If transaction is not valid, simply return if (!valid) return; console.log( 'Found incoming Ether transaction from ' + process.env.WALLET_FROM + ' to ' + process.env.WALLET_TO ); console.log('Transaction value is: ' + process.env.AMOUNT); console.log('Transaction hash is: ' + txHash + '\n'); // Initiate transaction confirmation confirmEtherTransaction(txHash); // Unsubscribe from pending transactions. subscription.unsubscribe(); } catch (error) { console.log(error); } });}
Recuento de confirmaciones de transacciones de ETH
Una vez que sabemos qué transacción hay que seguir, podemos empezar a monitorizar sus confirmaciones hasta que alcancen la cantidad necesaria.
Hay una forma muy sencilla de calcular el número de confirmaciones a partir del número del bloque en el que se minó inicialmente la transacción: es la diferencia entre el número del bloque actual y el del bloque de la transacción. Para obtenerlo, te propongo usar la función auxiliar que tienes a continuación.
async function getConfirmations(txHash) { try { // Instantiate web3 with HttpProvider const web3 = new Web3('https://rinkeby.infura.io/'); // Get transaction details const trx = await web3.eth.getTransaction(txHash); // Get current block number const currentBlock = await web3.eth.getBlockNumber(); // When transaction is unconfirmed, its block number is null. // In this case we return 0 as number of confirmations return trx.blockNumber === null ? 0 : currentBlock - trx.blockNumber; } catch (error) { console.log(error); }}
Al llamar a esta función obtenemos un número entero que representa el número de confirmaciones. Algunos proyectos exigen 100 confirmaciones y otros menos de 10, por eso hay que usar esta función de forma recursiva.
En el siguiente ejemplo se llama a getConfirmations() mediante la función setTimeout(), pero también puedes implementarlo de otras formas.
Aquí se han fijado 10 confirmaciones por defecto y un intervalo de 30 segundos, algo más que el tiempo de minado de un bloque en Ethereum (10–19 segundos).
function confirmEtherTransaction(txHash, confirmations = 10) { setTimeout(async () => { // Get current number of confirmations and compare it with sought-for value const trxConfirmations = await getConfirmations(txHash); console.log( 'Transaction with hash ' + txHash + ' has ' + trxConfirmations + ' confirmation(s)' ); if (trxConfirmations >= confirmations) { // Handle confirmation event according to your business logic console.log('Transaction with hash ' + txHash + ' has been successfully confirmed'); return; } // Recursive call return confirmEtherTransaction(txHash, confirmations); }, 30 * 1000);}
Seguimiento de tokens ERC20
Sabiendo que los tokens ERC20 se construyen sobre Ethereum como smart contracts, a primera vista puede parecer que seguirlos es tan fácil como seguir Ether. Pero encontrar las transacciones entrantes a una wallet con una cantidad concreta funciona de otra manera, porque la dirección del destinatario de la transacción siempre será la del smart contract. Eso sí, en cada transferencia de tokens se llama a una función especial del contrato del token. Cuando se ejecuta correctamente, esta función emite eventos Transfer en la blockchain, y la blockchain registra todos los eventos, que quedan accesibles en cualquier momento.
Nuestro objetivo es capturar la transacción que cumple los requisitos en el momento en que se publica en la blockchain, así que usaremos el mismo WebSocketProvider. Pero, en lugar de objetos de suscripción, instanciaremos objetos del contrato del token y escucharemos los eventos de transferencia.
Necesitamos el contrato del token para suscribirnos y parsear los datos del evento, ya que los datos en bruto están en formato hexadecimal y no son legibles para una persona. Para instanciar el objeto del contrato hace falta el JSON ABI del token (ver ejemplo) y la dirección de Ethereum en la que está desplegado el contrato.
La API de web3.js permite filtrar los eventos por sus parámetros indexados. En el caso del evento de transferencia, esos parámetros son _from**,_to*, y*_value*. Al configurar el objeto de opciones de la suscripción a eventos, también podemos indicar a partir de qué bloque empezar a buscar.
Al filtrar los eventos de transferencia, obtendremos la transacción que buscamos en cuanto aparezca en la blockchain y podremos pasar a la fase de confirmación.
function watchTokenTransfers() { // Instantiate web3 with WebSocketProvider const web3 = new Web3(new Web3.providers.WebsocketProvider('wss://rinkeby.infura.io/ws')); // Instantiate token contract object with JSON ABI and address const tokenContract = new web3.eth.Contract( TOKEN_ABI, process.env.TOKEN_CONTRACT_ADDRESS, (error, result) => { if (error) console.log(error); } ); // Generate filter options const options = { filter: { _from: process.env.WALLET_FROM, _to: process.env.WALLET_TO, _value: process.env.AMOUNT, }, fromBlock: 'latest', }; // Subscribe to Transfer events matching filter criteria tokenContract.events.Transfer(options, async (error, event) => { if (error) { console.log(error); return; } console.log( 'Found incoming Pluton transaction from ' + process.env.WALLET_FROM + ' to ' + process.env.WALLET_TO + '\n' ); console.log('Transaction value is: ' + process.env.AMOUNT); console.log('Transaction hash is: ' + txHash + '\n'); // Initiate transaction confirmation confirmEtherTransaction(event.transactionHash); return; });}
El objeto de transacción de una transferencia de Ether sigue las mismas reglas que una transacción Transfer, así que podemos aplicar exactamente la misma función confirmEtherTransaction() que usamos con la transacción de Ether en el ejemplo anterior.
Reflexiones finales
Estos ejemplos muestran cómo seguir en concreto las transferencias de tokens ERC20, pero la misma técnica se puede aplicar a cualquier otra función de un smart contract según las necesidades de tu negocio. Los ejemplos anteriores se pueden reunir en un servicio sencillo de node.js que sirva como punto de partida para explorar la blockchain.
Con ellos puedes montar un repositorio con ejemplos completos y funcionales de todos los estados descritos en este artículo y usarlo como base para tu próximo proyecto.