5 min read

[EthersJs - 6] Gathering on-chain data

[EthersJs - 6] Gathering on-chain data
Photo by Chris Yang / Unsplash

Part of having on-chain superpowers is knowing things retail doesn't know. In the end, crypto market is a zero sum game. Not everyone can win. Chances of you losing is low if you have better actionable data. This chapter goes over the basics of gathering the said data.

The EVM has special opcodes (5 in total - Log 0, Log1, Log 2, Log 3, Log 4)  for emitting data as event logs for smart-contracts to use. We will go over the details of these opcodes in a different article.  For now, it is enough knowing that all the smart-contract high level languages such as Solidity and Vyper, has wrappers around these opcodes to make broadcasting  events off-chain easier, and most of the public facing contracts do so as a standard practice.

All we have to do is continuously listen to these events to have situational awareness.

"I listen to events and I know things" – Node Lannister

The two main reasons of a smart contract to adhere to these standards are

  1. To send data off-chain
  2. Using these logs as a cheap form of storage (more on this in other chapters in our journey)

There are a lot we can do with this data, a few examples are -

  • We can listen to these events to look for favorable situations for us to trade or liquidate (more on these later).  
  • We can even keep an eye on time-locked contracts to look for changes that might affect us.

In this chapter though, we will sniff basic events, and gradually we will do other complicated things.

The ERC20 contract broadcasts transfer events every time a transfer happens, lets learn how to work with events by listening to this particular event :

First, we will be needing access to the blockchain with a provider. This has been throughly discussed in previous tutorials. We will then, have to connect to a contract ( ERC20 contract in this case, where the events are defined). The Event definition needs to be part of the ERC20 ABI.

const ERC20_ABI = [
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function totalSupply() view returns (uint256)",
    "function balanceOf(address) view returns (uint)",

    "event Transfer(address indexed from, address indexed to, uint amount)"
];

We will be connecting to ethereum mainnet and listen to all the LUSD transfers, for this we connect to the mainnet using infura and instantiate the LUSD contract (address: 0x5f98805A4E8be255a32880FDeC7F6728C6568bA0) using the aforementioned ERC20 ABI with transfer event definition:

const ethers = require("ethers");
const provider = new ethers.providers.InfuraProvider(
    "mainnnet",        
    "you-infura-api-key-here"
)

const ERC20_ABI = [
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function totalSupply() view returns (uint256)",
    "function balanceOf(address) view returns (uint)",

    "event Transfer(address indexed from, address indexed to, uint amount)"
];

const address = '0x5f98805A4E8be255a32880FDeC7F6728C6568bA0' // LUSD
const contract = new ethers.Contract(address, ERC20_ABI, provider)

Now that we have the contract instance, we can get all the transfer events between two blocks using:

contract.queryFilter('Transfer', from block, to block)

As this step is asynchronous, we have to use await and wrap it in a async function. Putting this all together, we have:

const ethers = require("ethers");
const provider = new ethers.providers.InfuraProvider(
    "mainnnet",        
    "you-infura-api-key-here"
)

const ERC20_ABI = [
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function totalSupply() view returns (uint256)",
    "function balanceOf(address) view returns (uint)",

    "event Transfer(address indexed from, address indexed to, uint amount)"
];

const address = '0x5f98805A4E8be255a32880FDeC7F6728C6568bA0' // LUSD
const contract = new ethers.Contract(address, ERC20_ABI, provider)

const main = async () => {
    const block = await provider.getBlockNumber()

    const transferEvents = await contract.queryFilter('Transfer', block - 10, block)
    console.log(transferEvents)
}

main()

In above code, we get the current block using const block = await provider.getBlockNumber() and then look for all the transfer events in the last blocks with const transferEvents = await contract.queryFilter('Transfer', block - 10, block)

Putting this script in play.js and running node play, You will get a response like this:  

What we get is an array of Events. If you get and empty array  - [], try increasing the number of blocks we are looking the transfer for in the line:  const transferEvents = await contract.queryFilter('Transfer', block - 10, block)

If you see the event array, congrats! you have mastered yet another skill! You may notice the red arrow pointing at the amount. The amount is a bignumber object, and therefore not displayed properly in the console. We will later learn how to parse and filter that amount. That will form the basis of tracking whales in later chapters.

Getting  real-time contract data, listening to contract events


Getting history of events is cool, but getting real time streaming access to events is  cooler! And that is what we intend to do now. We will listen to contract events.

To do this,  we will need to change a few things

First, we will need a Websocket provider. We do so by using ethers.providers.WebSocketProvider and passing in infura websocket gateway , which you can get from infura by clicking the "websocket" tab in API keys section:

the endpoint will start with wss:// as it is a not a http gateway.

Second, Instead of using .queryfilter() function to get historical data, we need to listen to events using .on()function and pass in the event we are listening for - in this case Transfer

The code therefore will look like :

contract.on("Transfer", 
     ...
  });

We can then further read the parameters of Transfer events.

The Transfer event looks like -

event Transfer(address indexed from, address indexed to, uint amount)

It has  from, to and the value of the transfer as the parameters ..we can extract that in our code like so:

contract.on("Transfer", (from, to, value) => {
     ...
  });

Putting this all together, we have:

const ethers = require("ethers");
var provider = new ethers.providers.WebSocketProvider(
  "wss://mainnet.infura.io/ws/v3/f579b1382aa5446cadfccfd3643caf37"
);

const ERC20_ABI = [
    "function name() view returns (string)",
    "function symbol() view returns (string)",
    "function totalSupply() view returns (uint256)",
    "function balanceOf(address) view returns (uint)",
    "event Transfer(address indexed from, address indexed to, uint amount)"
];

const address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // usdc
const contract = new ethers.Contract(address, ERC20_ABI, provider)

async function main() { 
  contract.on("Transfer", (from, to, value) => {
      let formattedValue = ethers.utils.formatUnits(value, 6);
      console.log(`${from} -> ${to}, value: ${formattedValue}`);
  });
}
main();
Note:  instead of LUSD, we are now listening to USDC transfers, as that is more frequent. So we had to do is change the contract address to 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 Also, the decimals for USDC is 6, so when we parse the value to human readable format, we pass in the decimal 6 t0 formatUnits() utility function.

And running this script, we get:

You will see the events data streaming in as the transfers are completed. You can end the streaming anytime with "Ctrl + C"

If you have come this far, congrats you have unlocked yet another level

So, in summary, we learned:

  1. How to read historical events on a contract between two blocks
  2. How to use websocket to stream events data of a contract
  3. How to parse an Event