Getting Started with Solidity Events


22 Nov 2022  Sergio Martin Rubio  7 mins read.

Ethereum also supports events and these can very important for notifying external applications. Events are part of the transaction receipts and they are executed by clients. Events are stored as logs in the blockchain so you can retrieve them.

The event logs are not accessible from within smart contracts, so you need an external client to listen to these events.

Solidity has a built-in type event that we can use for defining our events.

Event Definition

You can define a event as part of a smart contract as follows:

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

contract MyContract {
    event MessageSent(address indexed sender, string message);

    // functions
}

As you can see we used the keyword indexed for the sender address and this means you will be able to search for the attribute using a filter. e.g. MessageSent({sender: <ADDRESS_TO_SEARCH>}). Non-indexed parameters are harder to search because they are ABI encoded and therefore you need the ABI.

Take into consideration that indexing parameters has a cost and will take more gas fees for the transaction to be mined.

Solidity also provides another keyword for sending events named emit, and it’s similar to calling a function.

contracts/MyContract.sol:

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

contract MyContract {
    event MessageSent(address indexed sender, string message);

    function sendMessage(string memory _message) public {
        emit MessageSent(msg.sender, _message);
    }
}

Events use gas.

Listening to Events

As we mentioned before events cannot be consumed by Smart Contract, therefore we have to build a external application.

We are going to use HardHat and run local Ethereum node to showcase how Ethereum events work.

Prerequisites:

  • Node.js
  • A project with HardHat initialized. yarn init and yarn hardhat init.
  1. Run a node:
yarn hardhat node
  1. Deploy the contract:

scripts/deploy.ts

import { ethers } from "hardhat"

async function main() {
    const myContractFactory = await ethers.getContractFactory("MyContract")
    console.log("Deploying contract...")
    const myContract = await myContractFactory.deploy()
    await myContract.deployed()
    console.log(`Deployed contract to: ${myContract.address}`)
}

main().catch((error) => {
    console.error(error)
    process.exitCode = 1
})

We want to deploy the contract to the local node so update your HardHat config file accordingly:

hardhat.config.ts:

import { HardhatUserConfig } from "hardhat/config"
import "@nomicfoundation/hardhat-toolbox"
import "@typechain/hardhat"
import "dotenv/config"

const config: HardhatUserConfig = {
    defaultNetwork: "hardhat",
    networks: {
        localhost: { url: "http://127.0.0.1:8545/", chainId: 31337 },
    },
    solidity: "0.8.17",
}

export default config

We are also adding the import "dotenv/config" since we will use it for injecting some environment variables from .env.

Deploy the contract:

yarn hardhat run scripts/event-listener.ts --network localhost

Add the contract address to your .env. e.g. CONTRACT_ADDRESS=0x5FbDB2315678afecb367f032d93F642f64180aa3

  1. Start listening to events:

scripts/listen.ts:

import { ethers } from "hardhat"

const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS || "0xaddress"

export default async function listen() {
    const myContractFactory = await ethers.getContractFactory("MyContract")
    const myContract = await myContractFactory.attach(CONTRACT_ADDRESS)

    console.log(`Listening to events from contract: ${CONTRACT_ADDRESS}`)
    myContract.on("MessageSent", (sender, message) => {
        console.log(`Received message from ${sender} with message ${message}`)
    })
}

listen()

myContract.on() subscribes to the event MessageSent and logs when the event occurs.

Run the listener:

yarn hardhat run scripts/event-listener.ts --network localhost
  1. Run the script that invokes the send message function:

scripts/send.ts:

import { ethers } from "hardhat"

const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS || "0xaddress"

async function main() {
    const myContractFactory = await ethers.getContractFactory("MyContract")
    const myContract = await myContractFactory.attach(CONTRACT_ADDRESS)

    console.log(`Calling perform operation on contract: ${CONTRACT_ADDRESS}`)
    await myContract.sendMessage("Hello Event!")
}

main().catch((error) => {
    console.error(error)
    process.exitCode = 1
})

We just simply invoke the Smart Contract function sendMessage() that will emit the event.

yarn hardhat run scripts/send.ts --network localhost

You should see on the listener process something like: “Received message from 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 with message Hello Event!” 🚀

Testing Events

Testing Smart Contracts is a extremely important task of development process. Mocha provides an API for testing events.

test/test-deploy.ts:

import { ethers } from "hardhat"
import { expect } from "chai"
import { MyContract, MyContract__factory } from "../typechain-types"

describe("MyContract", function () {
    let myContractFactory: MyContract__factory
    let myContract: MyContract

    beforeEach(async function () {
        myContractFactory = (await ethers.getContractFactory(
            "MyContract"
        )) as MyContract__factory
        myContract = await myContractFactory.deploy()
    })

    it("Should emit event", async function () {
        let senders = await ethers.getSigners()
        await expect(myContract.sendMessage("foo"))
            .to.emit(myContract, "MessageSent")
            .withArgs(await senders[0].getAddress(), "foo")
    })
})

Now you can run the tests:

yarn hardhat test

Source Code