Tracking some Bored Apes: The Ethereum event logs tutorial
Introduction
Imagine that you are holding a meeting to discuss the progress of a project. During the meeting, you touch upon various topics, make decisions, assign tasks, and, maybe even partake in some friendly banter. Once the session is adjourned, a minute of the meeting will be prepared and it will serve as a record of all the important things that were discussed and decided during the meeting. This will include information such as the date and time of the meeting, the attendees, the agenda, and the minutes of each discussion point (no, you don't include the banter).
In programming, logs have a similar purpose. They are used to record important events and activities that occur within the program. For example, when a customer makes a purchase from an online store, the program can record the event by writing a log entry to a file or database. This log entry would contain information such as the date and time of the purchase, the customer's information, the item purchased, and the price. These entries will act as valuable sources of information for debugging, troubleshooting, and understanding how the program works. And just like meeting minutes, logs in programming can also be used to look back at the history of events and activities that have occurred within the program.
In Web3, people who have dabbled in the art of writing Solidity programs know that Solidity doesn't support any explicit form of logging. You can’t print
or display
the data from within a Solidity program. So how do you keep track of the happenings within a Solidity smart contract? Well, the workaround for this comes in the form of events.
You see, Ethereum (or the Ethereum Virtual Machine (EVM), to be exact) supports the storage of specifically indexed data structures and this feature is conveniently called logs. The EVM logs allow for the recording and storage of events and activities on the Ethereum network. These logs can be accessed and analyzed by developers, allowing them to gain a deeper understanding of the activities taking place on the Ethereum network. This logging feature is leveraged by Solidity in order to implement events.
Just like the print
statements in other languages, events
in Solidity are a way for smart contracts to communicate with the outside world and to record important information on the Ethereum network. Events are useful for a variety of purposes, such as tracking the execution of transactions, monitoring the state of a contract, and providing notifications to other contracts or external applications. Understanding how to fetch and utilize these events will enable developers to better audit and monitor the behavior of smart contracts on the network and this article is essentially a guide that will help you do just that. So, LFG!!!
Understanding event logs
In Solidity, an event is a way to log information or data that can be read by external clients. Events are declared using the event
keyword, and they allow smart contracts to send asynchronous notifications to clients. Events can be used to notify users of changes in the contract state, and they can be filtered, searched, and archived for future reference.
An event can have parameters, which are typed variables that provide additional information about the event. To emit an event in a Solidity function, use the emit
keyword followed by the event name and its parameters. Event logs are stored in the blockchain, and they can be accessed through APIs or block explorers. The following code demonstrates the declaration and emission of events in Solidity:
pragma solidity ^0.8.0;
contract Example {
event LogSomethingHappened(string message);
function doSomething() public {
emit LogSomethingHappened("Something happened!");
}
}
Event logs are a type of log entry in the Ethereum blockchain that contains information about events that have been triggered by smart contracts. Event logs are created when a contract emits an event in Solidity.
The following are the major components of an event log:
- Event signature — a unique identifier for the event, which is generated by hashing the event name and its parameter types using the Keccak-256 hash function.
- Contract address — address of the smart contract that emitted the event.
- Topics — an array of indexed parameters that provide additional information about the event. Each event can have up to 4 topics in the array, which can be used to filter and search for specific events. The first topic is always the event signature.
- Data — the field where every non-indexed parameter in an event is added to. It can contain any type of information, including strings, numbers, or arrays. The data is encoded as hexadecimal values.
- Block number — the number of blocks in the blockchain where the event was emitted.
- Transaction hash — the hash of the transaction that triggered the event.
To understand these components better, we can refer to an actual contract event. Now, for this, we might as well go for one of the most eventful contracts out there, The Bored Ape Yacht Club (BAYC) contract.
Prerequisites
Before you start writing the code, make sure you have the following components installed on your system:
- Node (Version ≥ 16) and the corresponding npm
- curl (https://curl.se/download.html)
- A code editor (VS Code, preferably)
Once you have everything, the next step is to set up a node project and install the web3.js library.
-
Create a new directory.
-
Open a terminal in the directory.
-
Run the following command to initialize a Node project:
npm init
-
Provide the details for the project, as per the prompt.
-
Once the project is initialized, use the following command to install the web3.js package:
npm install web3
Decoding the logs using Bored Apes
The Bored Ape Yacht Club (BAYC) is a non-fungible token (NFT) collectible based on the Ethereum blockchain. The BAYC contract is the smart contract that manages the creation, transfer, and ownership of the Bored Ape Yacht Club NFTs.
Each Bored Ape Yacht Club NFT is unique and can be bought, sold, and traded on various NFT marketplaces. The BAYC contract enforces the rules of ownership and transfer of the NFTs, ensuring that each NFT can only be owned by one person at a time.
The BAYC contract also includes events that are emitted whenever a transfer of an NFT occurs. These events can be monitored by external clients to track the state of the BAYC contract and keep track of transfers that have taken place. To understand the event logs better, let’s take a look at the BAYC contract’s Transfer
event :
/**
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
*/
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
The event consists of three parameters—from
, to
, and tokenId
—and all of them are preceded by the indexed
keyword.
Upon emitting this event, an event log will be created and within that log, the event signature will be generated by hashing (using Keccak-256) the name (Transfer
) and parameter types (address
, address
,uint256
) of the event. Below is the web3.js code for generating the event signature for the Transfer
event:
const Web3 = require('web3');
keccakHash = Web3.utils.keccak256("Transfer(address,address,uint256)")
//output : 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
A much simpler way to get the event signature is to use a block explorer, for example, Etherscan. When you view the contract details using the Etherscan explorer, there is a specific section dedicated to events and this section contains the latest event logs. You can use these logs to get the event signature—check topic[0]
of every log.
The topics
array represents all the event parameters that are marked using the indexed
keyword: from
, to
, and tokenId
, in our case. These topics help query and identify particular event logs. The topics
array can include a maximum of 4 elements (topics) and the first element in the array will always be the event signature.
Each topic (parameter data) in the array can be represented using a maximum of 32 bytes of data and while representing the data, if the data falls short of 32 bytes, you can add filler data to cover the size specification.
Any topic carrying data exceeding the prescribed size limit will be hashed before storing. Due to the size limit, arrays or string type data are not given as indexed parameters in order to avoid them from being part of the topics
array.
To explain the topics
array better, let’s look at an example. Suppose a Bored Ape NFT with token ID: 8009
was transferred from address 0xdafce4acc2703a24f29d1321adaadf5768f54642
to the address 0xdbfd76af2157dc15ee4e57f3f942bb45ba84af24
. The topics
array of the generated Transfer
event log then will look like this:
topics: [
//event signature
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
// from address
'0x000000000000000000000000dafce4acc2703a24f29d1321adaadf5768f54642',
// to address
'0x000000000000000000000000dbfd76af2157dc15ee4e57f3f942bb45ba84af24',
// hex representation of the token id
'0x0000000000000000000000000000000000000000000000000000000000001f49'
]
To get the token ID of a particular Bored Ape NFT, you can use NFT marketplaces like OpenSea.
Here, you can see that some of the topics have a lot of zeros in them. These zeros are the fillers used to fill up the 32-byte size specification of the topic fields. I.e. an address is represented using 20 bytes of data, so while we add any address type data to the topics array, zeros amounting to 12 bytes (20 + 12 = 32 bytes) will be added in between to cover the rest of the size specification. The number of zeros in between will defer based on the type of data that is being added.
Why you see more than 32 characters
If you are confused as to why the elements in the
topics
array have 64 characters (omit0x
) instead of 32, know that they are in hex format and 2 hex digits make up one byte.
All the non-indexed parameters (ones without the indexed
keyword) are added to the data
section of the log. Unlike topics
, the data
section cannot be queried. In the case of our Transfer
event, since all the parameters are indexed, the data
section will be empty and it will simply contain 0x
as a placeholder.
All the other fields, like the contract address, block number, and transaction hash contain the respective information. As a whole, the log for the Transfer
event will look something like this:
{
address: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D', //contract address
topics: [ //topics array
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x000000000000000000000000dafce4acc2703a24f29d1321adaadf5768f54642',
'0x000000000000000000000000dbfd76af2157dc15ee4e57f3f942bb45ba84af24',
'0x0000000000000000000000000000000000000000000000000000000000001f49'
],
data: '0x', //data
blockNumber: 16547382,
transactionHash: '0x4bb12bc228be6a9cbe40a7f45d803a15be2cc8b48cc7dd07848e40fed9406950',
transactionIndex: 116,
blockHash: '0x1a5d5d08da3ade3e35bff2bebaf0216cb096f4179e34f984e92383d63ab87f1a',
logIndex: 214,
removed: false, //true, if the log was removed
id: 'log_cd90ebee' //log id
}
Alright, now all this looks nice, but how do we actually capture these logs? Well, let’s find out.
Ways to capture the logs
Get an Ethereum node
One of the most crucial components that we need in order to capture the event logs is access to an Ethereum node. To set up your own node:
- Head over to Chainstack and set up an account.
- Once you have your account, deploy an Ethereum mainnet node in Chainstack.
As with many other functionalities of Ethereum, the method for capturing the event logs includes a curl way and a code way.
Using curl to capture event logs
To get the event logs using curl, we need to invoke the eth_getLogs
RPC method. This
method retrieves the logs that match certain filter criteria defined by the user. The filter criteria can include things like the address of the contract emitting the logs, the topics of the logs, and the range of blocks to search. Here is the sample curl request for fetching the Transfer
event logs from the BAYC contract:
curl <HTTP://chainstack-node-url> -X POST \
-H "Content-Type: application/json" \
--data '{"method":"eth_getLogs","params":[{"fromBlock": "0xFC7FA4","toBlock":"latest","address": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"]}],"id":1,"jsonrpc":"2.0"}'
As you can see, within the params
of the request, we have provided the fromBlock
and toBlock
fields. They are used to represent the range of blocks to search. While providing the range, keep in mind that the larger the range, the higher the number of logs and thus the higher the response size.
Recommended block range cap
Even though there are no limitations set in stone, in order to avoid node resource wastage and bulky response payloads, it is recommended to cap the block range at 8,000 for Chainstack Ethereum nodes.
While providing block numbers for the range, you can either give the hexadecimal representation or use words like latest
to refer to the latest block. You can also use pending
and earliest
for referring to transactions that are not yet mined.
The address
field in the request parameter helps filter the events based on the contract (represented using the address) from which it was emitted. Since a contract can emit multiple events, we can use the topics
field to provide additional filtering. Here, within the topics
array, we have provided the signature of the Transfer
event—topics[0]
. This will get us the logs of only the Transfer
event from the BAYC contract.
Using JavaScript to fetch the logs
The web3.js library provides a far more convenient way to fetch the event logs. To do this, we make use of the subscribe()
function. The function helps us subscribe to incoming logs, which we can filter and process using different options. The following script help subscribe to the Transfer
event logs from the BAYC contract:
const Web3 = require('web3');
//node url
const web3 = new Web3("wss://chainstack-node-endpoint");
//contract address
const BAYC_CONTRACT_ADDRESS = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'
//event signature
const BAYC_TRANSFER_SIGN = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
var subscription = web3.eth.subscribe('logs', {
fromBlock: '<START_BLOCK>',
toBlock: 'latest',
address: BAYC_CONTRACT_ADDRESS,
topics: [BAYC_TRANSFER_SIGN]
}, function(error, result){
if (!error)
console.log(result);
})
.on("connected", function(subscriptionId){
console.log(subscriptionId);
})
.on("data", function(log){
console.log(log);
})
.on("changed", function(log){
});
// unsubscribes the subscription
subscription.unsubscribe(function(error, success){
if(success)
console.log('Successfully unsubscribed!');
});
Using the above script, we are setting up a subscription channel that will capture all the logs that were generated within the given block range (fromBlock
,toBlock
). In the script, while providing the block number, you can directly use the decimal representation of the number.
Here, since we are expecting a stream of data, we are using the WSS endpoint of our Chainstack node. Just like our curl request, we have provided the contract address and the event signature within the topics
field as a means to filter the events.
To provide more filtering, you can specify more topics in the topics
array. To demonstrate this, here is a script that subscribes to the Transfer
event log of a particular BAYC NFT based on its token ID:
const Web3 = require('web3');
//node URL
const web3 = new Web3("wss://chainstack-node-endpoint");
//contract address
const BAYC_CONTRACT_ADDRESS = '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D'
//event signature
const BAYC_TRANSFER_SIGN = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
//token id
var token_id = <token_id>
//get the hex representation of the token id
var hex = web3.utils.toHex(token_id)
// add zeros to cover the 32 byte size limit
var hex_32 = web3.utils.padLeft(hex,64)
var subscription = web3.eth.subscribe('logs', {
fromBlock: '<START_BLOCK>',
address: BAYC_CONTRACT_ADDRESS,
topics: [BAYC_TRANSFER_SIGN,null,null,hex_32]
}, function(error, result){
if (!error)
console.log(result);
})
.on("connected", function(subscriptionId){
console.log(subscriptionId);
})
.on("data", function(log){
console.log(log);
})
.on("changed", function(log){
});
// unsubscribes the subscription
subscription.unsubscribe(function(error, success){
if(success)
console.log('Successfully unsubscribed!');
});
In the above-given script, we have provided the hex value of the token ID in our topics
array. This script will only fetch the Transfer
event logs that were generated while transferring that particular NFT. When providing topics to the topics
array, we can use the null
value to represent all the topics whose values we didn't specify (the from
and to
address in this case). Also, before adding the token ID to the array, we used the padLeft()
function to add zeros to the hex value, in order to cover the 32-byte size specification.
And with that, we have covered all the major ways of fetching Ethereum event logs.
Conclusion
Ethereum event logs play a crucial role in providing transparency and accountability in DApps. They serve as a record of all activities taking place on the blockchain, enabling developers and users to retrieve and analyze important information. The process of fetching these logs is relatively straightforward and can be done using various tools and libraries, such as curl or web3.js. By understanding the importance of event logs and learning how to access them, we can gain deeper insights into the inner workings of Ethereum-based DApps and make more informed decisions.
See also
About the author
Updated about 1 year ago