-
Notifications
You must be signed in to change notification settings - Fork 44
Event‐driven Testing: Solidity Events
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.
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:
- Add an Event to the Contract.
- Querying Past Events.
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
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
.
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.
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.
- Introduction
- Rise of Ethereum
- Ethereum Fundamentals
- DApps & Smart Contracts
- MetaMask Wallet & Ether
- Solidity: Basics
- Solidity: Advanced
- Solidity Use cases and Examples
- DApp Development: Introduction
- DApp Development: Contract
- DApp Development: Hardhat
- DApp Development: Server‐side Communication
- DApp Development: Client-side Communication
- Advanced DApp Concepts: Infura
- Advanced DApp Concepts: WalletConnect
- Event‐driven Testing
- Interacting with the Ethereum Network
- Tokens: Introduction
- Solidity: Best Practises
- Smart Contract Audit
- Ethereum: Advanced Concepts
- Evolution of Ethereum
- Conclusion