How to Track Blockchain Transactions with web3.js

The number of dApps and tokens on the market is growing fast: every year more than 1000 new distributed applications appear, along with 500 new tokens and online services built around both. Blockchain technology brings a lot of fascinating features and advantages. Still, the underlying idea is always the same: to transfer crypto funds or execute any other smart contract function, you have to publish a transaction on the blockchain and get it mined.
For some applications and online services, it's essential to know the current state of a user's wallet or of the transactions generated by their actions. There are many web services that let developers get wallet and transaction details through a JSON API, like BlockCypher. Even so, there were few places, if any, where you could see all the coins and tokens you needed in one place.
In this article I'll explain how to solve that in practice.
Different Projects, Same Tasks
Each blockchain uses different consensus protocols, but when it comes to watching wallets and transactions, they all have a lot in common. Detecting the moment funds are transferred and verifying that the transaction has reached the number of confirmations required for security reasons is a typical task in crypto projects.
That's why crypto asset tracking can be split into two parts. First, watch the wallet to detect incoming transfers and keep the one that matches our filtering criteria. Second, use that transfer's hash to track its confirmations. Tracking ETH Transactions
Let's start with the currency of the Ethereum blockchain: Ether. Thanks to the work of the Ethereum development team and the open source community, we have a fairly impressive number of Web3 implementations:
- Python Web3.py
- Haskell hs-web3
- Java web3j
- Scala web3j-scala
- Purescript purescript-web3
- PHP web3.php
- PHP ethereum-php
- Swift web3swift
That said, the original and most popular implementation is the JavaScript one, called web3.js. So let's use this powerful tool to work directly with Ethereum blockchain nodes and see how it works:
npm install web3
Subscribing to ETH Transactions
The Web3 API doesn't let you subscribe directly to a wallet's incoming transactions, so to solve this specific problem we need a different strategy. First we subscribe to all pending transactions, and then we filter them by the sender wallet's address and other criteria.
Web3.js lets you communicate directly with an Ethereum node through several protocols, including HTTP and WebSockets. This example uses the Rinkeby Testnet and the nodes provided by Infura. But you can use any other provider, including local nodes.
In the function below, we create a new web3 instance with the WebSocket provider and use it to set up a subscription to newly created transactions on the blockchain. This instance is called pending.
A subscription object can be managed with two methods: subscribe() and unsubscribe(). Both accept a callback function to handle the subscription's errors and results. With pending transaction subscriptions we can handle two events: data and error. Obviously, data is the key event we need to find the transfer.
The data event handler only receives one input parameter, representing a transaction hash, and checks whether each transaction matches our search criteria whenever we need more detail than just the hash. That's where another web3.js method comes in, getTransaction(), used to read the transaction's details. Since it's an asynchronous call, we just wrap it in a try-catch and wait for the response with await. You can check the response object's format in the official web3 documentation here.
Once we get the response and the transaction matches the filter conditions, we need to start the transaction confirmation process and cancel the subscription by calling its unsubscribe() method. If no transaction matches the filter, we simply exit the function and stay subscribed.
Here's the function:
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); } });}
Counting ETH Transaction Confirmations
Once we know which transaction to track, we can start monitoring its confirmations until they reach the required amount.
There's a very simple way to calculate the number of confirmations from the number of the block the transaction was originally mined in: it's the difference between the current block number and the transaction's block number. To get it, I suggest using the helper function below.
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); }}
Calling this function gives us an integer representing the number of confirmations. Some projects require 100 confirmations and others fewer than 10, which is why this function needs to be called recursively.
In the following example, getConfirmations() is called through the setTimeout() function, but you can implement it in other ways too.
Here we've set 10 confirmations as the default and a 30-second interval, a bit more than Ethereum's block mining time (10–19 seconds).
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);}
Tracking ERC20 Tokens
Knowing that ERC20 tokens are built on top of Ethereum as smart contracts, at first glance it might seem that tracking them is as easy as tracking Ether. But finding incoming transactions to a wallet with a specific amount works differently, because the transaction's recipient address will always be the smart contract's address. That said, every token transfer calls a special function on the token's contract. When it runs successfully, this function emits Transfer events on the blockchain, and the blockchain logs all events, which stay accessible at any time.
Our goal is to capture the transaction that meets the requirements the moment it's published on the blockchain, so we'll use the same WebSocketProvider. But instead of subscription objects, we'll instantiate token contract objects and listen for transfer events.
We need the token's contract to subscribe and parse the event data, since the raw data is in hexadecimal format and isn't human-readable. To instantiate the contract object we need the token's JSON ABI (see example) and the Ethereum address where the contract is deployed.
The web3.js API lets you filter events by their indexed parameters. For the transfer event, those parameters are _from**,_to*, and*_value*. When setting up the event subscription's options object, we can also specify which block to start searching from.
By filtering transfer events, we'll get the transaction we're looking for as soon as it appears on the blockchain and can move on to the confirmation phase.
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; });}
The transaction object for an Ether transfer follows the same rules as a Transfer transaction, so we can apply the exact same confirmEtherTransaction() function we used with the Ether transaction in the previous example.
Final Thoughts
These examples specifically show how to track ERC20 token transfers, but the same technique can be applied to any other smart contract function depending on your business needs. The examples above can be put together into a simple node.js service that serves as a starting point for exploring the blockchain.
With them, you can build a repository with complete, working examples of every state described in this article and use it as a foundation for your next project.