Expanding your blockchain horizons: The eth_getBlockReceipts emulator
Introduction
The eth_getBlockReceipts
method is a powerful tool in the Ethereum ecosystem that offers a window into the inner workings of the network. Retrieving the receipts of all transactions within a block provides insight into the status and outcome of each transaction. Whether you're developing a decentralized exchange, a contract auditing tool, or just curious about the Ethereum network, the eth_getBlockReceipts
method is an essential resource that can help you uncover the details and outcomes of transactions on the blockchain. It's like having an x-ray of the Ethereum network, revealing what's happening beneath the surface and providing a clear picture of the network's activity.
The caveat is that this method is only available by querying nodes running the Erigon client. This guide will show you how you can emulate this method in essentially any EVM-compatible network, even if the node is not running on Erigon.
Read the Erigon vs. Geth guide to get a better understanding of these two popular Ethereum clients.
This article is code-centered, and you should read the Uncovering the power of eth_getBlockReceipts guide to have more theory insights.
Update on eth_getBlockReceipts
As of September 2023, the
eth_getBlockReceipts
method is also available on the Geth client from V 1.13.0.
Prerequisites
To start with a JavaScript development project, you'll need to install node.js
, a powerful JavaScript runtime environment that enables developers to run JavaScript code outside a web browser. For this project, it's recommended to use at least version 16. You can download it from their website.
With node.js
installed, you're ready to start using JavaScript. However, you'll need access to a blockchain node to query the data. Here's where Chainstack comes in to save the day. Simply follow these steps to sign up and deploy your own blockchain node with Chainstack for free:
Disclaimer
This tool was developed and tested using an Avalanche endpoint, but you can choose any EVM-compatible network.
The project
Now that you have all the tools required, you are ready to create your eth_getBlockReceipt
emulator.
Initialize an npm project
An npm (Node Package Manager) project is a software project managed using the npm
platform. npm is the default package manager for the JavaScript runtime environment node.js and is needed to manage the dependencies and packages used in a project.
At the heart of an npm
project is the package.json
file, a blueprint that outlines the packages and dependencies your project requires. This file acts as a roadmap, guiding npm in managing your project's dependencies.
npm
makes it easy for developers to share and reuse code, and its vast library of packages can be easily installed and used in any project. This allows developers to focus on writing their own code and let npm
manage external dependencies.
To initialize an npm
project, open your terminal in the root directory of your project and run the following command:
npm init
This command will prompt a few questions, answer them, and it will create a package.json
file. Note that these answers do not affect the functionality of your project, so don’t stress too much about what you input.
You can also run the command with the -y
flag, which will skip all questions.
npm init -y
You have now created an npm
project and are ready to start development.
Install the dependencies
As previously mentioned, creating this tool requires some npm packages. Specifically the dotenv
and web3
packages.
During development, we used the following versions:
- dotenv ^16.0.3
- web3 ^1.8.1
Install those packages by running the following:
npm i web3 dotenv
web3.js is a tool for interacting with an EVM-compatible blockchain, while dotenv is a package for managing configuration settings in a secure and convenient manner. Together, these two packages provide a powerful and flexible toolkit for developing DApps.
The code
This eth_getBlockReceipt
emulator will be developed on a single JavaScript file, so go ahead and create a file named index.js
in the root directory of your project.
The tool will do the following:
- Create a provider instance and import packages.
- Generate an eth_getBlockReceipt-like object with a function.
- Loop into the object and extract specific fields with a function.
- Run the program with the main function.
Import packages and create a provider instance
The first step of any tool using the web3.js library is to import the necessary packages and create a provider instance. So add the following code at the top of your index.js
file.
const Web3 = require("web3");
require('dotenv').config();
// Create provider instance
const NODE_URL = process.env.CHAINSTACK_NODE_URL;
const web3 = new Web3(NODE_URL);
This creates a web3
instance that can access a high-level API for interacting with a blockchain; essentially, it’s your connection to the blockchain node.
This tool uses an environment variable to import the node URL to keep sensitive information safe, so it is more difficult to inadvertently share it.
This is done using the dotenv
package. Create a file named .env
in the root directory and create a variable holding your Chainstack node URL.
CHAINSTACK_NODE_URL="YOUR_ENDPOINT_URL_HERE"
It is important that the environment variable has the same name as the variable imported in the
index.js
file.
Generate an eth_getBlockReceipt-like object
Now, let’s start working on the bulk of the logic for this project. Create a new async
function named getBlockReceipts
which takes two parameters: the provider
and the target block
for retrieving receipts.
Then paste the following code into the index.js
file:
async function getBlockReceipts(provider, block) {
const extractTransactions = await provider.eth.getBlock(block, false)
let receipts = [];
for (const transaction of extractTransactions.transactions) {
// Get the transaction receipt
const txReceipt = await web3.eth.getTransactionReceipt(transaction);
for (let log of txReceipt.logs) {
let logData = {
address: log.address,
topics: log.topics,
data: log.data,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockHash: log.blockHash,
logIndex: log.logIndex,
removed: log.removed,
id: log.id
}
txReceipt.logs = logData;
}
// replace the original logs array with the logData
receipts.push(txReceipt);
}
return receipts;
}
Let’s go over the logic here.
The first step is to retrieve all of the transactions hashed from the desired block. This can be achieved by utilizing the eth_getBlockByNumber
method from the Ethereum JSON-RPC API. When calling this method, set the full transactions flag to false
to ensure that only the transaction hashes are returned in an array format.
The transaction hashes retrieved in this step will be stored in the extractTransactions
constant for further processing.
const extractTransactions = await provider.eth.getBlock(block, false)
The next step will loop through the array of hashes and call the eth_getTransactionReceipt
method on each hash, do some parsing, and store the result in an array called receipts
.
let receipts = [];
for (const transaction of extractTransactions.transactions) {
// Get the transaction receipt
const txReceipt = await web3.eth.getTransactionReceipt(transaction);
for (let log of txReceipt.logs) {
let logData = {
address: log.address,
topics: log.topics,
data: log.data,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockHash: log.blockHash,
logIndex: log.logIndex,
removed: log.removed,
id: log.id
}
txReceipt.logs = logData;
}
// replace the original logs array with the logData
receipts.push(txReceipt);
}
The second loop for (let log of txReceipt.logs)
will extract the logs data and replace the logs object
from the original receipt. This step aims to provide a more user-friendly representation of the logs data. Without this processing, the logs
data in the transaction receipt would only be returned as an array of objects, [object, object]
, which may require additional steps from the end-user to extract the relevant information.
By replacing the original logs object with the logData
object, the final response includes a more intuitive representation of the log data, eliminating the need for additional processing.
Once all the logs in the transaction receipt have been processed, the code proceeds to the next iteration of the loop and repeats the same process for the next transaction.
The final step pushes the unpacked transaction receipt into the receipts
array, and after all of the transactions have been processed, the function returns the receipts
array.
Support function to isolate fields
This script also includes a support function called getElement
which takes the receipts array returned by getBlockReceipts
and the name of a field as the input parameters.
Paste the following function into the index.js
file.
// Extract a specific field and return an array
async function getElement(receipts, field) {
return receipts.map(receipt => receipt[field]);
}
The function returns an array that contains the values of the specified field from each object in the receipts
array. This is achieved through the map
method, which iterates over each element in the receipts
array and returns the specified field's value for that element.
In essence, the getElement
function provides a convenient way to extract a specific field from a collection of objects and returns the extracted values as an array.
For example:
const logData = await getElement(receipts, 'gasUsed');
// Response = [ 396022, 1442653, 664107 ]
Main function and full code
The final step of the script is the main()
function, which runs the full code and demonstrates the logic.
Paste the following at the bottom of the file.
async function main() {
const blockNumber = 25792736;
// Retrieve the transactions receipts
const receipts = await getBlockReceipts(web3, blockNumber);
console.log(receipts);
// Use the getElement function to extract a specific field from each receipt.
const logData = await getElement(receipts, 'to');
console.log(logData);
}
main();
This function can be seen as the execution of the program where:
- A block number is specified as input to the function.
- The
getBlockReceipts
function is invoked within the function. - The results of the
getBlockReceipts
function are output to the console. - The second part of the function calls the
getElement
function. - The
to
field is extracted from each receipt using thegetElement
function.
Full code
The previous sections show the script broken down into all its components to make it easier to understand.
Here, you can find the full code:
const Web3 = require("web3");
require('dotenv').config();
// Create provider instance
const NODE_URL = process.env.CHAINSTACK_NODE_URL;
const web3 = new Web3(NODE_URL);
// Create receipts object
async function getBlockReceipts(provider, block) {
// Extract the transactions from the block
const extractTransactions = await provider.eth.getBlock(block, false)
let receipts = [];
for (const transaction of extractTransactions.transactions) {
// Get the transaction receipt
const txReceipt = await web3.eth.getTransactionReceipt(transaction);
// Parse the logs information
for (let log of txReceipt.logs) {
let logData = {
address: log.address,
topics: log.topics,
data: log.data,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockHash: log.blockHash,
logIndex: log.logIndex,
removed: log.removed,
id: log.id
}
txReceipt.logs = logData;
}
// replace the original logs array with the logData
receipts.push(txReceipt);
}
return receipts;
}
// Extract a specific field and return an array
async function getElement(receipts, field) {
return receipts.map(receipt => receipt[field]);
}
async function main() {
const blockNumber = 25792736;
const receipts = await getBlockReceipts(web3, blockNumber);
console.log(receipts);
// Use the getElement function to extract a specific field from each receipt.
const logData = await getElement(receipts, 'to');
console.log(logData);
}
main();
Run the program
Save the file and run the program by executing the following command:
node index
If this program is run on block 25792736
on the Avalanche mainnet, the response will be the following:
[
{
blockHash: '0x451155957eee73e4ea17edd5a26e4aeaff30cc828b2a4a81f2197d7d980cd00e',
blockNumber: 25792736,
contractAddress: null,
cumulativeGasUsed: 396022,
effectiveGasPrice: 27500000000,
from: '0x6e752dcb0acb921c1fa446992c590a28661f27ca',
gasUsed: 396022,
logs: {
address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7',
topics: [Array],
data: '0x0000000000000000000000000000000000000000000000027eaff286463ffb12',
blockNumber: 25792736,
transactionHash: '0x3708f39015b7816a156d0fc24ab7f658bcac130ebdd63e62ae93b9c8093ad41e',
transactionIndex: 0,
blockHash: '0x451155957eee73e4ea17edd5a26e4aeaff30cc828b2a4a81f2197d7d980cd00e',
logIndex: 10,
removed: false,
id: 'log_e7f37c48'
},
logsBloom: '0x00000000000000000020000000000001000000040000000020000000000000000000000000400040004000000000000100000000000020020000420000200000040080000000080800000008000000008000000000400000000000000408020000000011000008400000000000004000000000000000040000000010000800010008000002000000080000000000080000000010000000000000000000000000020400000000000000000000000004000000000000004100000000000000000080000002021004000000000001000000000000000000008000001802000000000010000000000004000400000008000000000000000200020000000000000100',
status: true,
to: '0x1111111254eeb25477b68fb85ed929f73a960582',
transactionHash: '0x3708f39015b7816a156d0fc24ab7f658bcac130ebdd63e62ae93b9c8093ad41e',
transactionIndex: 0,
type: '0x0'
},
{
blockHash: '0x451155957eee73e4ea17edd5a26e4aeaff30cc828b2a4a81f2197d7d980cd00e',
blockNumber: 25792736,
contractAddress: null,
cumulativeGasUsed: 1838675,
effectiveGasPrice: 26500000000,
from: '0x0d6c6017b639c3ee31c79f8a300acd5cbd1ab866',
gasUsed: 1442653,
logs: {
address: '0x83a283641C6B4DF383BCDDf807193284C84c5342',
topics: [Array],
data: '0x00000000000000000000000000000000000000000000003291a743cf2f536ff5',
blockNumber: 25792736,
transactionHash: '0x182eab9950cf7d6711222a437331995e9484b15126abd575c529bda13cc26017',
transactionIndex: 1,
blockHash: '0x451155957eee73e4ea17edd5a26e4aeaff30cc828b2a4a81f2197d7d980cd00e',
logIndex: 15,
removed: false,
id: 'log_98091197'
},
logsBloom: '0x00000000000000000000000000040000001000000000000000000000000000000080001008020000000000000040800000000000000000000000000000000000000040000000000000000008020000000000000000000000000000000000008000000000000008000000008010020001000000000000000000000110000000000000000000020000100000000000080000000090000000000000000000002800000000008000000000000040000000000000000000000010000000020400000000040002000000000000000000000000200000010000100000000080000000000000040000040000008000800000000000000000002000000000000000000000',
status: true,
to: '0x0efc8ef83d7318121449e9c5dbdf7135bcc1fa90',
transactionHash: '0x182eab9950cf7d6711222a437331995e9484b15126abd575c529bda13cc26017',
transactionIndex: 1,
type: '0x2'
},
{
blockHash: '0x451155957eee73e4ea17edd5a26e4aeaff30cc828b2a4a81f2197d7d980cd00e',
blockNumber: 25792736,
contractAddress: null,
cumulativeGasUsed: 2502782,
effectiveGasPrice: 26500000000,
from: '0xab1d3dd66e0f0799d09ca530c30e8f0b90d87f85',
gasUsed: 664107,
logs: {
address: '0xc8cEeA18c2E168C6e767422c8d144c55545D23e9',
topics: [Array],
data: '0x0000000000000000000000000000000000000000000000034f3202e0e51db900',
blockNumber: 25792736,
transactionHash: '0x9cdcd3970e0666dabbbdbc386425f1760830087215be4b495c7a4d4a27a596a7',
transactionIndex: 2,
blockHash: '0x451155957eee73e4ea17edd5a26e4aeaff30cc828b2a4a81f2197d7d980cd00e',
logIndex: 40,
removed: false,
id: 'log_a3adc356'
},
logsBloom: '0x000080002000000000000000000000000080001000004000000000000001010200000000000080000000000002000000000000000000000000040000002400000001000000000000000000080000000000000000008400100000000080008000000004000200000000000000000008000000000000080000000000100000000100400010800000000000000000000000000000010000000000000800000000800200000000000000000000000000000000000200000000000002000040000000000400020000000000000000000200010000000000000000000000000000600001100c0000000000000000000004000000200000040000400000000000000000',
status: true,
to: '0xc8ceea18c2e168c6e767422c8d144c55545d23e9',
transactionHash: '0x9cdcd3970e0666dabbbdbc386425f1760830087215be4b495c7a4d4a27a596a7',
transactionIndex: 2,
type: '0x2'
}
]
[
'0x1111111254eeb25477b68fb85ed929f73a960582',
'0x0efc8ef83d7318121449e9c5dbdf7135bcc1fa90',
'0xc8ceea18c2e168c6e767422c8d144c55545d23e9'
]
Conclusion
In conclusion, this script effectively leverages the power of the Web3 library and its methods.
With this function, you can now use eth_getBlockReceipts
even if your node is not running the Erigon client. At the same time, you just learned that, if a specific method is not available, you can always build it yourself.
See also
About the author
Updated 11 months ago