Building Block Explorers (Part 1) - Multi-chain Block Transaction Explorer
Blocks lie at the heart of blockchains. And what are blocks? They’re simply a series of bundled transactions, nothing more, nothing less. It is therefore not surprising that a key on-chain data primitive is that of a transaction, grouped together into numbered blocks. In this series, we’ll dive straight into understanding the data that is produced by blocks, and try our hands at building a couple of views that we typically see in a block explorer: transactions by block, latest transactions, transactions of an address, and individual transactions.
Where Does GoldRush Come In?
GoldRush is a read-only API that has indexed every single block since genesis for 100+ blockchains, including Ethereum, Polygon, Avalanche, Arbitrum, Optimism, etc. This means that you can easily query for all the transactions in any block of your choosing. The transactions data response we return is super data rich - each item in the response can contain up to 48 unique fields, and we typically return as many items as there are transactions in 1 API call (very often going up to the 100s).
A Fun Exercise
Before we begin, let’s do a fun exercise. Let’s say I am an internet archeologist, and I want to know what the data from the first-ever block on Ethereum look like. How can we go about getting that?
For this, you can use GoldRush’s Get all Transactions in a block endpoint.
Let’s proceed to our API reference and input 1
as the blockHeight
and click Run.
We see the following response returned on the right. And from this, we can see that the data from the first-ever block is…nothing!
Wait - what is going on here? Is there something wrong with the GoldRush API?
Let’s try to input the next few numbers and see what we get.
A blank wall of nothingness, all the way up to 100, and even 1000, and 10000.
What is going on?
Well, a quick googling will tell us that the first ever transaction that happened on the Ethereum chain was at block 46147. 😆
Fun fun fun facts.
Now, querying for block 46147 with the API reference…
Our persistence is finally rewarded. We see 1 transaction item indeed - the golden, first-ever transaction on the Ethereum chain. What data does it contain? Expanding it to a new window by clicking the top right arrow…
Lo and behold, the data from the sacred transaction. It has a transaction hash of 0x5c504…
(tattoo idea, anyone?), made from the ‘0xa1e’ address, and has a value of 31337 wei, which is…0.000000000000031337 ETH. The transaction fee, on the other hand, was 1.05 ETH.
Talking about a ridiculously bad transaction.
In any case, the legend has been made, and we shall proceed with the real agenda of today: building a multi-chain block transactions explorer.
Notice that I said ‘block transactions’ explorer: this means that it’ll fetch all the transactions of blocks and display them. Notice I also mentioned ‘multi-chain’: luckily for you, GoldRush has done the hard work of indexing 100+ chains so that you don’t have to worry about any differences in chain architecture. What you build for Ethereum will work perfectly for Avalanche, Polygon, BSC, Oasis, Arbitrum, and so on. This is exactly what you’ll see in a bit.
By the end of this guide, you would have built the following component:
By clicking the chain selector dropdown on the left, you would be able to select and see the latest 5 blocks from up to 100 chains as well as the transactions contained within. Our table also boasts an ‘events’ column, which are the events that are produced from that transaction, similar to what you’ll see when you go to the logs page on Etherscan.
Knowing the events produced gives you great insight into what goes on beneath the hood of a transaction - it’s a measure of transaction complexity, something we can glean from our table much easier than with Etherscan.
Ready? Let’s begin.
(Estimated time to follow along: 20mins)
Prerequisites
Basic familiarity with React.
Some familiarity with the Antd UI library, which we’ll be using extensively.
Some HTML/CSS knowledge.
Fetching data using APIs.
Tutorial: Building a Block Explorer Component
1
Initialize the Project
1.1
.env
file, like so:1.2
1.3
classNames
.part1-block-transactions-end
branch, run it, and examine the code yourself (be sure to supply your API key in the .env file). If not, follow along for a guided walkthrough.)2
Fetching the Data for the Latest Blocks
blocks
state variable by calling setBlocks
, and populating it with the 5 block responses. This will allow me to render the 5 blocks seen above. The response for each call to Get a block item looks like this:signed_at
variable.2.1
function App() {
const [selectedChainName, setSelectedChainName] = useState('eth-mainnet')
const [loadingBlocks, setLoadingBlocks] = useState(false)
const [blocks, setBlocks] = useState([...Array(5).keys()])
const [selectedBlock, setSelectedBlock] = useState(null)
useEffect(() => {
setLoadingBlocks(true)
let latestBlock
const blocksEndpoint = (blockNumber) => `https://api.covalenthq.com/v1/${selectedChainName}/block_v2/${blockNumber}/`
const config = {
auth: {
username: process.env.REACT_APP_COVALENT_APIKEY
}
}
axios.get(blocksEndpoint('latest'), config)
.then(res => latestBlock = res.data.data.items[0])
.then(() => {
const block2 = axios.get(blocksEndpoint(Number(latestBlock.height) - 1), config)
const block3 = axios.get(blocksEndpoint(Number(latestBlock.height) - 2), config)
const block4 = axios.get(blocksEndpoint(Number(latestBlock.height) - 3), config)
const block5 = axios.get(blocksEndpoint(Number(latestBlock.height) - 4), config)
axios.all([block2, block3, block4, block5])
.then(axios.spread((...res) => {
setBlocks([latestBlock,
res[0].data.data.items[0],
res[1].data.data.items[0],
res[2].data.data.items[0],
res[3].data.data.items[0],
])
setLoadingBlocks(false)
}))
})
.catch(err => console.log(err))
}, [selectedChainName])
return (
...
)
}
export default App;
2.2
#1999308
, 1 sec ago
, and block #1990322
) with the following:<div className='blocks'>
{blocks.map(block => (
<div className='block' key={block} onClick={() => setSelectedBlock(block.height)}>
<div><img src='<https://res.cloudinary.com/dl4murstw/image/upload/v1686185288/virtual_awy3m3.png>' alt='block' height='126' width='126'/></div>
<div className='blockNumber'>{loadingBlocks ? <Spin />: '#' + block.height }</div>
<div className='time'>{loadingBlocks ? <Spin /> : ((Date.now() - new Date(block.signed_at))/1000).toFixed(0) + ' secs ago' }</div>
</div>
))}
</div>
...
<div className='title'>{!selectedBlock ? null : 'block #' + selectedBlock}</div>
<div className='transactionsComponent'>
2.3
3
Displaying Transactions of Each Block.
<Transactions />
component inside the components/Transactions.js
file (we’ll keep it empty for now). Then, let’s import it into App.js and render it in the JSX like so:3.1
div
with className of ‘block’ to set the selectedBlock variable on click:selectedBlock
is passed into Transactions component when we click the block on the page above. Nice!3.2
<Transactions />
component like so:// Transactions.js
import React, { useState, useEffect } from 'react'
import { transform } from '../utils/transform'
import { Table, Tag, Popover, Descriptions, Spin } from 'antd'
import { blockExplorerURLs } from '../utils/blockExplorerURLs'
import axios from 'axios'
const Transactions = ({ chainName, selectedBlock }) => {
const [txns, setTxns] = useState([])
const [isLoading, setIsLoading] = useState(false)
const blockExplorer = blockExplorerURLs.filter(item => item.chainId[1] === chainName)
const blockexplorerURL = (blockExplorer?.length) ? blockExplorer[0].url : '<https://blockscan.com/>'
useEffect(() => {
setIsLoading(true)
const blockTxnsEndpoint = `https://api.covalenthq.com/v1/${chainName}/block/${selectedBlock}/transactions_v3/`
const config = {
auth: {
username: process.env.REACT_APP_COVALENT_APIKEY
}
}
axios.get(blockTxnsEndpoint, config)
.then(res => {
const txns = res.data.data.items
const transformedTxns = transform(txns.filter((txn) => txn.log_events?.length < 30))
setTxns(transformedTxns)
setIsLoading(false)
})
.catch(err => console.log(err))
}, [chainName, selectedBlock])
if (isLoading) {
return <Spin />
}
if (txns.length) {
return (
<>
<Table dataSource={txns} className='table' columns={columns(blockexplorerURL)} rowKey="txnHash" />
</>
)
}
}
export default Transactions
blockTxnsEndpoint
, supplying the chainName
and selectedBlock
as arguments, and txns
state variable.3.3
npm start
now, this won’t work. We’ve yet to define a few important dependencies for our Ant Design table: columns
, and a truncateEthAddress
function.//Transactions.js
...
export default Transactions
const columns = (blockexplorerURL) => ([
{
title: 'Txn Hash',
dataIndex: 'tx_hash',
key: 'tx_hash',
width: '15%',
render: (text) => (<><a href={blockexplorerURL + 'tx/' + text} target="_blank" rel="noreferrer">{text.slice(0,15)}</a>...</>)
},
{
title: 'Events',
dataIndex: 'events',
key: 'event',
width: '25%',
render: (_, record) => {
if (record.log_events.length > 0) {
return (
<>
{record.log_events.map((log_events, index) => {
if (log_events) {
let color = log_events.name.length > 5 ? 'geekblue' : 'green'
if (log_events.name === 'Transfer') {
color = 'volcano'
}
const content = (
<>
<Descriptions size={'small'} column={1} title={log_events.name + ' Event'} bordered>
{Object.entries(log_events).map((item) => {
return (
<Descriptions.Item key={item[0]} label={item[0]}>
{item[1]}
</Descriptions.Item>
)
})}
</Descriptions>
</>
)
return (
<Popover key={index} content={content} placement="rightBottom" trigger="click">
<Tag color={color} key={index} style={{ cursor: 'pointer' }}>
{log_events.name.toUpperCase()}
</Tag>
</Popover>
)
}
return null
})}
</>
)
}
}
},
{
title: 'Block',
dataIndex: 'block_height',
width: '5%',
key: 'tx_hash'
},
{
title: 'Date',
dataIndex: 'block_signed_at',
width: '15%',
key: 'tx_hash',
render: (text, record) => {
const newDate = new Date(record.block_signed_at)
const timeString12Hr = newDate.toLocaleTimeString('en-US', {
timeZone: 'UTC',
hour12: true,
hour: 'numeric',
minute: 'numeric',
second: 'numeric'
})
const day = newDate.toLocaleString('en-US', { day: 'numeric', month: 'short' })
return (
<>
<div>
{day}, {timeString12Hr}
</div>
</>
)
}
},
{
title: 'From',
dataIndex: 'from_address',
width: '10%',
key: 'from_address',
render: (text) => (<>
<a href={blockexplorerURL + 'address/' + text} target="_blank" rel="noreferrer">{truncateEthAddress(text)} </a>
<img src="<https://res.cloudinary.com/dl4murstw/image/upload/v1685502645/copy_prfttd.png>"
alt="copy"
height="16px"
onClick={() => navigator.clipboard.writeText(text)} className='copyText'
/>
</>
)
},
{
title: 'To',
dataIndex: 'to_address',
width: '10%',
key: 'to_address',
render: (text) => (<>
<a href={blockexplorerURL + 'address/' + text} target="_blank" rel="noreferrer">{truncateEthAddress(text)} </a>
<img src="<https://res.cloudinary.com/dl4murstw/image/upload/v1685502645/copy_prfttd.png>"
alt="copy"
height="16px"
onClick={() => navigator.clipboard.writeText(text)} className='copyText'
/>
</>
)
},
{
title: 'Value',
dataIndex: 'value',
width: '15%',
key: 'value',
render: (text) => Number(text) !== 0 ? (Number(text)/10**18).toFixed(8) + ' ETH': 0 + ' ETH'
},
{
title: 'Fees',
dataIndex: 'fees_paid',
width: '15%',
key: 'fees_paid',
render: (text) => Number(text) !== 0 ? (Number(text)/10**18).toFixed(8) + ' ETH': 0 + ' ETH'
},
])
const truncateRegex = /^(0x[a-zA-Z0-9]{4})[a-zA-Z0-9]+([a-zA-Z0-9]{4})$/
const truncateEthAddress = (address) => {
const match = address.match(truncateRegex);
if (!match) return address;
return `${match[1]}…${match[2]}`;
};
Events
, I’ve relied on a few Antd components, such as Popover, Tags, and Descriptions, to display the data in the way I want.blockExplorerURL
variable. This is a helper array defined in utils that matches a chain name or ID to their various block explorers so that the links we used are correctly defined. This too is in the utils folder.3.4
//App.js
...
const handleChange = (value) => {
setSelectedChainName(value)
setSelectedBlock(null)
setLoadingBlocks(true)
};
const chainOptions = blockExplorerURLs.map(item => ({
value: item.chainId[1],
label: item.chainId[1]
}))
return (
...
<Select
defaultValue="eth-mainnet"
style={{ width: 156 }}
onChange={handleChange}
options={chainOptions}
/>
)
3.5
npm start
...3.6
There you have it: a super powerful multi-chain block explorer in around 20 minutes.
Clicking on each of the event tags even opens up a popover that shows us the details of the data contained within the event.
All in a Day’s Work
That was quite a lot! Hopefully, you’ll be able to see how easy it is to spin up your own multi-chain block explorer for your project in just around 20 minutes, with the help of the GoldRush API.
As always, GoldRush’s API is free to try, if you haven’t already, get your FREE API key and give it a spin today!
Happy buidling.