How to listen for smart contract events and get logs using Ethers JS

In this tutorial, we are going to learn how to get all the logs of a smart contract and how to filter them and how to listen for smart contract events using Ethers JS and JavaScript.

In this tutorial, we are going to learn how to get all the logs of a smart contract and how to filter them and how to listen for smart contract events using Ethers JS and JavaScript.

As an example I use the smart contract of the Uniswap token (UNI) that follows the ERC-20 standard and has Transfer events.

Here is the code to listen for events on the smart contract and get past logs:

const ethers = require("ethers")

const provider = new ethers.providers.Web3Provider(YOUR_PROVIDER_HERE)


// Example using the smart contract of the Uniswap token (UNI)

const uniAddress = "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984"
const ABI = [
    // ... rest of the ABI
    "event Transfer(address indexed from, address indexed to, uint amount)"
]

// Create the Contract instance
const uni = new ethers.Contract(uniAddress, ABI, provider)

const myAddress = "0x5b8f1310A956ee1521A7bB56160451C786289aa9"
const anotherAddress = "0x5F70Ddd9908B04f952b9cB2A6F8E4D451725ceDC"

// Create filters for the Transfer event
const filterFromAddress1ToAddress2 = uni.filters.Transfer(myAddress, anotherAddress)

// Get the logs using the filters
const logs = await uni.queryFilter(filterFromAddress1ToAddress2)

// Listen for Transfer events
contract.on("Transfer", (from, to, amount) => {
    console.log("New Transfer event with the arguments:")
    console.log(from, to, amount)
})

Let's break down this code and explain everything.

Explanations about events and logs

An Event is something a smart contract emits which generates a log on the blockchain. It is a cheap form of storage as it only costs 8 gas per byte to generate it. It is used to store data that anyone can access and trigger changes on front-end applications.

Events have arguments that provide information about the event and if the arguments are marked as "indexed" you can use them to filter the logs when you want to get them.

Creating the Contract instance

Before doing anything else, first you need to create a Contract instance using the contract's address and an ABI that contains the declaration of the Event you want to interact with

The simplest way to create the ABI array is to go to the Etherscan page of the smart contract and go to the "Contract" tab and in the "Code" section you'll find the full ABI. You can just copy the part where the Event is defined and put that in your ABI in your code.

How to listen for smart contract events

To run a callback function when a smart contract emits a specific event, you can call the .on method of your Contract instance and pass the name of the event to listen to and the callback function.

The callback function takes in parameters the values of arguments passed by the smart contract to that Event when creating the log.

Here is an example:

contract.on("Transfer", (from, to, amount) => {
    console.log("New Transfer event with the arguments:")
    console.log(from, to, amount)
})

The Transfer event has 3 parameters, the address that sent the tokens, the address that received the tokens and the amount of tokens transferred.

That means the callback function will have the same parameters, in the same order.

You can also subscribe to the event using the once function which will run the callback function for a singe event and then unsubscribe.

More information about the function related to events here.

How to get logs from a smart contract and filter them

To get logs from a smart contract, you can call the queryFilter function which takes in parameter an EventFilter object. You can create that object yourself or use a function like I've done above.

It's easier to use the functions but if you want to have a look at it, check out this part of the documentation.

Otherwise, to create a filter, you can use the filters property of the contract you create and call the event as a method. In the parameters of that method, you can pass the values of the indexed parameters to filter with:

const filter = uni.filters.Transfer(from, to)

// (uni is the Contract instance created in the first example)

In an ERC-20 smart contract, the only indexed parameters are from and to so that's the only filters you can use and that's the only values you can pass to the Transfer method.

Here are a few examples to show you how it works when you only want to pass 1 parameter, none or both:

const from = "0x5b8f1310A956ee1521A7bB56160451C786289aa9"
const to = "0x5F70Ddd9908B04f952b9cB2A6F8E4D451725ceDC"

const filterNone = uni.filters.Transfer()
const filterFrom = uni.filters.Transfer(from)
const filterTo = uni.filters.Transfer(null, to)
const filterBoth = uni.filter.Transfer(from, to)

If you pass an array for one of the filters, it will be like saying "this value OR that value".

Here is an example:

const address1 = ""
const address2 = ""

const filterFromAddress1orAddress2 = uni.filters.Transfer([address1, address2])

To learn more about the logic inside the filters, check out this part of the documentation:

Events
Documentation for ethers, a complete, tiny and simple Ethereum library.

Once you have your filter, you can call the queryFilter method on your smart contract. It returns a Promise that returns an array of logs when it resolves:

const address1 = "0x5F70Ddd9908B04f952b9cB2A6F8E4D451725ceDC"
const address2 = "0x5b8f1310A956ee1521A7bB56160451C786289aa9"

const filterFromAddress1orAddress2 = uni.filters.Transfer([address1, address2])

const logs = await uni.queryFilter(filterFromAddress1orAddress2)

The queryFilter function takes in 3 parameters, the last 2 are optional and the first one is the filter.

The second one is the block to start searching logs from and the second one is the last block to check for logs in. By default, it will look for logs from the earliest block on the blockchain to the latest.

In these parameters, you can pass the following values:

  • "latest" for the latest block, "earliest" for the earliest block and "pending" for the currently pending block
  • a number (the block number or height) or the block hash (as a string)

Then, a log object returned by queryFilter looks like this:

  • blockNumber: the number of the block that contains the transaction that created this log
  • blockHash: : the hash of the block that contains the transaction that created this log
  • removed : if the transaction was cancelled, this will be true, otherwise it's false
  • transactionLogIndex : the index of this log in the transaction
  • address: the address of the smart contract that created this log
  • data: an hexadecimal string all the data that this log contains (the arguments passed when the log was created)
  • topics: the list of indexed values for this log. It's an array containing the values in as an hexadecimal string.
  • transactionHash: the hash of the transaction that created this log.
  • transactionIndex: the index of the transaction in the block that mined this transaction.
  • logIndex: the index of the log across all logs of the block where this log is in.

As you can see, you might run into an issue because the data and topics properties return hexadecimal values so you can't access the arguments passed to the log directly.

To decide that, you need to create an interface out of your ABI and then call parseLog:

const logs = await uni.queryFilter(filterFromAddress1orAddress2)

const interface = new ethers.Interface(ABI)

const parsedLogs = logs.map(log => {
    return interface.parseLog({ data: log.data, topics: log.topics });
})

The parseLog function will return an object that contains an args property which is an object that contains the values of arguments passed by the smart contract to the Event when creating the log.

And that's it 🎉

Thank you for reading this article