Skip to content

Event‐driven Testing: Solidity Events

Ananthan edited this page May 25, 2024 · 7 revisions

In traditional applications, we often use logs to catch and depict what’s happening at a particular time. These logs are regularly used to debug applications, detect particular events, or notify the watcher of the logs that something occurred. These logs are very useful when writing or interacting with smart contracts. So, here we discuss how Ethereum works.

An event is an inheritable part of a smart contract. It stores the arguments passed in transaction logs during event emission. These transaction logs are stored on the blockchain and are accessible with the contract's address until the contract is present on the blockchain. Generated events are not accessible from within contracts.

Let us see how we can use Ethers to listen to our contract events.

Querying Events

Here, we will use the previous DApp to generate certificates on the blockchain. Let us go through the steps to get the overall idea for this exercise:

  1. Add an Event to the Contract.
  2. Querying Past Events.

Add an Event to the Contract

We will use the Certificate contract, from our Certificate DApp.

Now, we will add an event to the contract. Open 'Cert.sol' and add a new event, which will be triggered whenever a new certificate is added to the system.

Just as shown below.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Cert {
    address admin;

    // Event declaration
    event Issued(string course, uint256 id, string grade);

    constructor() {
        admin = msg.sender;
    }

    modifier onlyAdmin() {
        require(msg.sender == admin, "Access Denied");
        _;
    }

    struct Certificate {
        string name;
        string course;
        string grade;
        string date;
    }

    mapping(uint256 => Certificate) public Certificates;

    function issue(
        uint256 _id,
        string memory _name,
        string memory _course,
        string memory _grade,
        string memory _date
    ) public onlyAdmin {
        Certificates[_id] = Certificate(_name, _course, _grade, _date);

        // Event emission
        emit Issued(_course, _id, _grade);
    }
}

Now, compile and deploy the updated contract. For that, we will run the Hardhat node using the following command:

npx hardhat node

Next, we need to compile our contract using the Hardhat framework. For that, we will run the following command on another terminal.

npx hardhat compile

Deploy the compiled contract to the simulated Ethereum network using the following command.

npx hardhat ignition deploy ./ignition/modules/Cert.js --network local

Querying Past Events

Now that our contract is set up and deployed, we can start listening to events related to the smart contract. To do so, let's create a new file named 'events.js' in the 'routes' folder.

To retrieve a smart contract's past events, we can use a method named getLogs from the ethers. We can call this method directly from the provider object. Copy and paste the code below into 'events.js'.

import { Router } from 'express'
import { id, Interface, JsonRpcProvider } from 'ethers'
import details from '../lib/deployed_addresses.json' assert { type: 'json' }
import Cert from '../lib/Cert.json' assert { type: 'json' }
const router = Router()

const provider = new JsonRpcProvider('http://127.0.0.1:8545')
const eventTopic = id('Issued(string,uint256,string)')
const iface = new Interface(Cert.abi)

router.get('/', async (req, res) => {
  let eventlogs = []

  BigInt.prototype.toJSON = function () {
    return this.toString()
  }

  await provider
    .getLogs({
      fromBlock: 'earliest',
      toBlock: 'latest',
      address: details['CertModule#Cert'],
      topics: [eventTopic],
    })
    .then((logs) => {
      logs.forEach((log) => {
        eventlogs.push(iface.parseLog(log))
      })
    })

  res.json(eventlogs)
})

export default router

Edit the 'app.js' file to configure the new handler.

import eventsRouter from './routes/events.js'
app.use('/events', eventsRouter)

Let's explain the code.

First, we create a new provider object using JsonRpcProvider class.

const provider = new JsonRpcProvider('http://127.0.0.1:8545')

To query an event, we need to convert the event definition to a hex value and use it as a 'topic' for the query. For the conversion, we can use a function id from Ethers, which is used to compute a 32-byte identifier.

const eventTopic = id('Issued(string,uint256,string)')

Now, we need the abi to create an interface for parsing the logs to a readable format.

const iface = new Interface(Cert.abi)

Afterwards, we define our route handler. Let's take a look at the different parameters.

  • fromBlock & toBlock - These are used to specify the query range of the event, the starting block, and the ending blocks, respectively. The input can be the block number or one of the following:
  • earliest - The very first block
  • latest - The very recent block
  • address - Contract address.
  • topics - Think of topics as searchable tags for your event logs, allowing you to pinpoint relevant events without sifting through all event data.

Event logs have variables of type BigInt. At the moment, we cannot JSON serialize these variables. So, we add this tweak to convert all BigInt values to strings and send them through JSON.

BigInt.prototype.toJSON = function () {
  return this.toString()
}

For the getLogs function, we must pass in the range of the blocks we're querying. Here, we are simply querying from the first block to the latest. However, we can limit the range as we want. Then, we pass the contract address and topic. As you see, the property topics accepts an array. We will further utilize this later.

Finally, we parse the returned logs in a then block using the interface and push it to an array we defined above. Then, we send the array as the response.

await provider
  .getLogs({
    fromBlock: 0,
    toBlock: 'latest',
    address: details['CertModule#Cert'],
    topics: [eventTopic],
  })
  .then((logs) => {
    logs.forEach((log) => {
      eventlogs.push(iface.parseLog(log))
    })
  })

res.json(eventlogs)

Start/restart the server. Use cURL/Postman to send a 'GET' request to '/events' route.

curl http://127.0.0.1:8080/events

This will return an array of Issued events occured from fromBlock to toBlock.

Querying Specific Events

Let's customize our event querying a little bit more. First, add indexed modifier to the course in the event definition. This will store the parameter as a topic.

event Issued(string indexed course, uint256 id, string grade);

We need to update our logic from the previous chapter to incorporate the new change. First, we must convert the desired course to hex format to use it as a topic.

const courseTopic = id('Ethereum Developer Program')

Then add courseTopic to topics array inside getLogs as argument along the event topic.

await provider
  .getLogs({
    fromBlock: 'earliest',
    toBlock: 'latest',
    address: details['CertModule#Cert'],
    topics: [eventTopic, courseTopic],
  })
  .then((logs) => {
    logs.forEach((log) => {
      eventlogs.push(iface.parseLog(log))
    })
  })

Now, instead of querying all Issued events from the contract, we can narrow it down to specific courses.

Listening Events Concurrently

We must establish a WebSocket connection with the blockchain to query the events in real-time.

So instead of importing JsonRpcProvider, we need WebSocketProvider from ethers. Create a new file, 'event-listener.js', and let's update it further.

const wsprovider = new WebSocketProvider('ws://127.0.0.1:8545')

Create a wallet from the provider and a contract instance.

const wallet = new Wallet('<your-private-key>', wsprovider)
const instance = new Contract(contractAddress, abi, wallet)

Finally, we write a self-executing anonymous function that listens to the contract instance for our event Issued and logs the events in the console whenever it occurs.

;(() => {
  console.log('Listening for Issue Events...')
  instance.on('Issued', (course, id, grade, event) => {
    console.log('**** EVENT OCCURRED ****')
    console.log('course:', course)
    console.log('id:', id)
    console.log('grade:', grade)
    console.log('event:', event)
    console.log('************************')
  })
})()

We can listen to events concurrently using the on function on the contract instance. The function accepts the event's name as its first argument, followed by the arguments of the event and the entire event itself. We can use console.log to view all these values.

Execute the file using Node.js.

node ./event-listener.js

The terminal will constantly listen for Issued events from the smart contract. Whenever a event occurs, it will be logged on to the console.



Clone this wiki locally