P18: Block Explorer
P18: Block Explorer
Project Overview
| Attribute | Value |
|---|---|
| Main Language | TypeScript |
| Alternative Languages | Python, Go |
| Difficulty | Intermediate |
| Coolness Level | Level 3: Genuinely Clever |
| Business Potential | The âService & Supportâ Model |
| Knowledge Area | Infrastructure / Data |
| Software or Tool | Etherscan Clone |
| Main Book | âMastering Ethereumâ by Andreas M. Antonopoulos & Gavin Wood |
Learning Objectives
By completing this project, you will:
- Master Ethereum JSON-RPC API understanding how to query blocks, transactions, receipts, and state from an Ethereum node
- Implement ABI encoding/decoding learning how Solidity function calls and events are serialized to and from raw bytes
- Build a blockchain indexer creating a data pipeline that syncs on-chain data to a queryable database in real-time
- Parse and interpret event logs extracting meaningful information from raw log topics and data fields
- Design efficient database schemas for blockchain data that supports fast queries by address, block, transaction hash, and token
- Implement real-time updates using WebSockets to push new blocks and transactions to connected clients
- Build a production-quality API with proper pagination, caching, and error handling for blockchain data
The Core Question Youâre Answering
âHow do you read and interpret the raw blockchain data into human-understandable information?â
The blockchain is essentially a giant, append-only database. But unlike traditional databases, the data is encoded in compact binary formats designed for efficiency, not human readability. A transactionâs input field is just hex bytes. A logâs topics are 32-byte hashes. An addressâs âtoken balanceâ isnât stored anywhere directlyâit must be computed by replaying all transfer events.
Block explorers are the translation layer between the raw blockchain and human users. They answer questions like:
- What did this transaction actually do?
- How much ETH does this address hold?
- What tokens does this address own?
- When was this block mined and by whom?
- What happened in this smart contract call?
Building a block explorer means understanding every layer of the Ethereum data modelâfrom raw RLP-encoded blocks to high-level decoded function calls.
Deep Theoretical Foundation
How Ethereum Stores Data
Ethereum is organized into three main data structures:
World State
|
+---------------+---------------+
| | |
Accounts Accounts Accounts
(EOA/Contract) (EOA/Contract) (EOA/Contract)
|
+----+----+
| |
Storage Code
Trie (bytecode)
State Trie: Maps addresses to account objects containing:
nonce: Number of transactions sent (EOA) or contracts created (contract)balance: ETH balance in weistorageRoot: Root hash of contractâs storage triecodeHash: Hash of contract bytecode
Blocks contain:
- Header (parent hash, state root, transaction root, receipts root, etc.)
- Transactions list
- Uncle/Ommer blocks (for PoW)
Transactions contain:
from: Sender address (derived from signature)to: Recipient address (null for contract creation)value: ETH value in weiinput: Encoded function call or contract bytecodegas,gasPrice/maxFeePerGas/maxPriorityFeePerGas- Signature (v, r, s)
Receipts contain:
status: Success (1) or failure (0)gasUsed: Actual gas consumedlogs: Array of event logs emitted
Understanding ABI Encoding
The Application Binary Interface (ABI) defines how to encode function calls and decode return values.
Function Call Encoding:
Input data = function_selector + encoded_arguments
function_selector = first 4 bytes of keccak256(function_signature)
Example:
transfer(address,uint256)
keccak256("transfer(address,uint256)") = 0xa9059cbb...
selector = 0xa9059cbb
Encoded arguments:
address: left-padded to 32 bytes
uint256: left-padded to 32 bytes
Full input for transfer(0x1234..., 1000):
0xa9059cbb
0000000000000000000000001234567890123456789012345678901234567890
00000000000000000000000000000000000000000000000000000000000003e8
Why This Matters for Block Explorers:
When you see a raw transaction like:
{
"to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"input": "0xa9059cbb0000000000000000000000005a52e96bacdabb82fd05763e25335261b270efcb00000000000000000000000000000000000000000000000000000000003d0900"
}
You want to display:
Transfer 4,000,000 USDT to 0x5a52...Efcb
This requires:
- Knowing the contract (USDT)
- Having the ABI (or at least the function signature)
- Decoding the selector to find the function name
- Decoding the arguments according to their types
Event Logs and Topics
Smart contracts emit events for off-chain observability:
event Transfer(address indexed from, address indexed to, uint256 value);
emit Transfer(0xAlice, 0xBob, 1000);
This creates a log entry:
{
"address": "0xTokenContract",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", // keccak256("Transfer(address,address,uint256)")
"0x000000000000000000000000Alice_address_padded_to_32_bytes",
"0x000000000000000000000000Bob_address_padded_to_32_bytes"
],
"data": "0x00000000000000000000000000000000000000000000000000000000000003e8" // 1000 encoded
}
Topic Structure:
topics[0]: Event signature hash (identifies the event type)topics[1-3]: Indexed parameters (max 3)data: Non-indexed parameters (ABI-encoded)
Why Indexed vs Non-Indexed?
- Indexed parameters can be filtered/searched efficiently
- Non-indexed parameters are cheaper to emit (no separate topic slot)
- Anonymous events have no signature in topics[0]
Token Standards and Balance Tracking
ERC-20 and ERC-721 donât store balances in a way you can directly query for âall tokens owned by address X.â Instead, you must:
- Index all Transfer events for every token contract
- Compute running balances by replaying:
balance[to] += value; balance[from] -= value; - Store the computed state in your database
This is why block explorers are fundamentally indexersâthey transform event streams into queryable state.
Raw Blockchain Events:
Transfer(0x0, Alice, 1000) // Mint
Transfer(Alice, Bob, 300)
Transfer(Alice, Carol, 200)
Computed Token Balances:
Alice: 500
Bob: 300
Carol: 200
The JSON-RPC Interface
Ethereum nodes expose a JSON-RPC API for querying blockchain data:
+-------------+ JSON-RPC +------------------+
| Your | ----------------> | Ethereum Node |
| Indexer | | (Geth/Erigon) |
+-------------+ <---------------- +------------------+
Block/Tx Data
Key Methods:
| Method | Purpose |
|---|---|
eth_blockNumber |
Get latest block number |
eth_getBlockByNumber |
Get block with transactions |
eth_getBlockByHash |
Get block by hash |
eth_getTransactionByHash |
Get single transaction |
eth_getTransactionReceipt |
Get receipt with logs |
eth_getLogs |
Query logs with filters |
eth_getBalance |
Get ETH balance |
eth_getCode |
Get contract bytecode |
eth_call |
Execute read-only call |
Rate Limits and Providers:
- Infura: 100k requests/day (free tier)
- Alchemy: 300M compute units/month (free tier)
- Running your own node: No limits, ~1TB storage
Concepts You Must Understand First
Before building, ensure you understand these concepts deeply:
1. Ethereum Data Structures
Chapter Reference: âMastering Ethereumâ Chapter 4 (Transactions) & Chapter 7 (Smart Contracts)
- How blocks, transactions, and receipts are structured
- The difference between
transactionandreceipt - How gas accounting works
2. ABI Encoding Specification
Reference: Solidity ABI Specification
- Static vs dynamic types
- How arrays and strings are encoded
- Function selectors (first 4 bytes of keccak256)
- Event topic encoding
3. ERC Token Standards
References:
- ERC-20 Specification
- ERC-721 Specification
- Required functions and events for each standard
- How to detect which standard a contract implements
- The Transfer event signature for each
4. JSON-RPC Protocol
Reference: Ethereum JSON-RPC
- Request/response format
- Batch requests for efficiency
- Error codes and handling
5. Database Indexing Strategies
Reference: âDesigning Data-Intensive Applicationsâ Chapters 3 & 5
- B-tree vs LSM-tree indexes
- Composite indexes for multi-column queries
- Trade-offs between write and read performance
6. Event-Driven Architecture
- Handling chain reorganizations (reorgs)
- Eventual consistency considerations
- Idempotent event processing
7. WebSocket Real-Time Communication
- Subscription-based updates
- Connection management and reconnection
- Backpressure handling
Questions to Guide Your Design
Indexer Architecture
- How will you handle chain reorganizations (reorgs)? What happens if block 1000 is replaced?
- Should you index from genesis or start from a recent block? What are the trade-offs?
- How will you parallelize block fetching without missing or duplicating data?
- How do you handle the indexer crashing mid-block? (Atomicity)
- Should you index all contracts or only âknownâ ones with verified ABIs?
Database Schema Design
- What are the primary query patterns? (By address, by block, by hash)
- How will you store decoded function calls? (Denormalized or separate table?)
- Should you store raw transaction data or just parsed fields?
- How will you handle token balances? (Materialized view vs computed on-demand)
- What indexes are essential for performance? Whatâs the write penalty?
ABI Handling
- Where will you get ABIs? (Etherscan API, 4byte.directory, user upload)
- How do you handle contracts without verified source code?
- Should you decode using known function signatures (4byte database) when ABI is unavailable?
- How do you handle proxy contracts? (delegatecall pattern)
Real-Time Updates
- How will you detect new blocks? (Polling vs WebSocket subscription)
- How do you push updates to connected web clients?
- Whatâs your strategy for handling burst traffic during high-activity periods?
- How do you handle the delay between block mining and database indexing?
Search Optimization
- How will you implement address search? (Exact match vs prefix search)
- Should transaction hashes be indexed? (Large index, rarely used)
- How do you handle searching by block range efficiently?
- Do you need full-text search for contract names/labels?
API Design
- What pagination strategy? (Offset vs cursor-based)
- How do you handle rate limiting?
- Should you cache responses? Whatâs the invalidation strategy?
- How do you version your API for breaking changes?
Thinking Exercise: Trace Through the System
Before writing code, trace through these scenarios on paper:
Exercise 1: Decode a Raw Transaction
Given this raw transaction input:
0xa9059cbb0000000000000000000000005a52e96bacdabb82fd05763e25335261b270efcb00000000000000000000000000000000000000000000000000000000003d0900
- Extract the function selector (first 4 bytes)
- Look up
0xa9059cbbin a function signature database - Parse the remaining bytes according to
transfer(address,uint256) - Convert the address and amount to human-readable format
Exercise 2: Parse Transfer Events from Logs
Given this log from a transaction receipt:
{
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x0000000000000000000000005a52e96bacdabb82fd05763e25335261b270efcb",
"0x000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43"
],
"data": "0x00000000000000000000000000000000000000000000000000000000003d0900"
}
- Verify this is a Transfer event by checking topics[0]
- Extract the
fromaddress from topics[1] - Extract the
toaddress from topics[2] - Decode the
valuefrom data - Look up the tokenâs decimals (USDT = 6 decimals)
- Calculate the human-readable amount
Exercise 3: Query Address Transaction History
Design the database queries needed to show:
- The 20 most recent transactions for an address (as sender OR receiver)
- All ERC-20 token transfers involving this address
- Current token balances for this address
Consider:
- How do you ORDER BY block number and transaction index efficiently?
- How do you handle the UNION of sent and received transactions?
- For token balances, do you query a pre-computed table or aggregate on-the-fly?
The Interview Questions Theyâll Ask
JSON-RPC and Node Interaction
- Whatâs the difference between
eth_getBlockByNumberwith and without full transactions?- Without: Returns only transaction hashes (faster, less data)
- With: Returns full transaction objects (one request vs N+1)
- How do you get all logs for a specific event across many contracts?
- Use
eth_getLogswith topic filter but null address - Be aware of block range limits (typically 2000-10000 blocks)
- Use
- Whatâs the difference between a transaction and a transaction receipt?
- Transaction: What was submitted (intent)
- Receipt: What actually happened (result, logs, gas used, status)
- How does
eth_calldiffer frometh_sendTransaction?eth_call: Read-only simulation, no state change, no gas costeth_sendTransaction: Actual execution, costs gas, changes state
- What happens when you query a block number that doesnât exist yet?
- Returns null or error depending on method
- For
latest, always returns the current head
ABI Encoding/Decoding
- What is a function selector and how is it computed?
- First 4 bytes of keccak256(function_signature)
- Signature uses canonical types (uint256, not uint)
- How are dynamic types (strings, arrays) encoded differently from static types?
- Static: Inline at fixed offset
- Dynamic: Pointer + data section at end
- How do you decode a function call when you donât have the ABI?
- Use 4byte.directory to look up selector
- Attempt common signatures
- Fall back to raw hex display
- Whatâs the difference between topics[0] and the other topics in an event log?
- topics[0]: Event signature hash (unless anonymous)
- topics[1-3]: Indexed parameter values (padded to 32 bytes)
- How do you handle events from proxy contracts?
- Events are emitted with the proxyâs address
- Need to resolve the implementation contract for ABI
- Watch for delegate calls
Database and Indexing
- Why canât you just query the node for âall transactions for address Xâ?
- Nodes donât index by arbitrary address
- Would require scanning every block
- This is exactly what indexers solve
- How do you handle chain reorganizations in your indexer?
- Store block hashes, not just numbers
- On reorg, delete blocks above the fork point
- Re-index from the new canonical chain
- Whatâs the difference between computing token balances on-the-fly vs pre-computing?
- On-the-fly: Always accurate, slow for large histories
- Pre-computed: Fast queries, complex to maintain with reorgs
- How would you design indexes for âlast 20 transactions for addressâ?
- Composite index on (address, block_number DESC, tx_index DESC)
- Separate tables for sent vs received, or UNION query
- How do you handle the âfinalityâ problem for displayed data?
- Mark recent blocks as âpending confirmationâ
- Use block confirmations count
- Different strategies for PoW vs PoS
Real-Time Updates
- How do you detect new blocks in real-time?
- WebSocket subscription to
newHeads - Polling
eth_blockNumber(fallback) - Node-specific APIs (Alchemy, Infura)
- WebSocket subscription to
- Whatâs the difference between WebSocket and Server-Sent Events for pushing updates?
- WebSocket: Bidirectional, more complex
- SSE: Server-to-client only, simpler, HTTP-based
- How do you handle the delay between block creation and indexing completion?
- Show âpendingâ state for recent blocks
- Optimistic display before full indexing
- Background job queue with status
Architecture and Scale
- How does Etherscan handle millions of requests per day?
- Heavy caching (CDN, Redis)
- Read replicas for database
- Separate write (indexer) and read (API) paths
- Whatâs the approximate storage requirement for indexing all Ethereum mainnet data?
- Raw block/tx data: ~1TB
- With decoded events and indexes: 2-5TB
- Archive node access for historical state
Hints in Layers
Layer 1: Project Setup and Node Connection
Start with a basic TypeScript project that can connect to an Ethereum node:
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider(
process.env.ETH_RPC_URL || "https://eth.llamarpc.com"
);
async function getLatestBlock() {
const blockNumber = await provider.getBlockNumber();
const block = await provider.getBlock(blockNumber, true); // true = include transactions
console.log(`Block ${blockNumber}: ${block?.transactions.length} transactions`);
return block;
}
Key insight: The true parameter includes full transaction objects. Without it, you only get hashes and need N additional RPC calls.
Layer 2: Block and Transaction Fetching
Build a basic indexer loop that processes blocks sequentially:
+----------------+
| Start Block |
+----------------+
|
v
+-------------------+ +----------------+ +------------------+
| Fetch Block |->| Process Txs |->| Store in DB |
| eth_getBlock... | | For each tx | | Insert block, |
+-------------------+ +----------------+ | txs, logs |
^ | +------------------+
| v |
| +----------------+ |
| | Fetch Receipts | |
| | eth_getTx... | |
| +----------------+ |
| | |
+--------------------+---------------------+
|
v
+----------------+
| Next Block |
+----------------+
Important: You need both the transaction AND the receipt. The receipt contains logs (events) and actual gas used.
Layer 3: ABI Decoding Infrastructure
Build or integrate an ABI decoder. ethers.js provides this:
const erc20Interface = new ethers.Interface([
"function transfer(address to, uint256 amount)",
"function approve(address spender, uint256 amount)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
]);
// Decode function call
const decoded = erc20Interface.parseTransaction({ data: tx.data });
console.log(`Function: ${decoded?.name}, Args: ${decoded?.args}`);
// Decode event log
const parsedLog = erc20Interface.parseLog({ topics: log.topics, data: log.data });
console.log(`Event: ${parsedLog?.name}, Args: ${parsedLog?.args}`);
Strategy for unknown contracts:
- Check if address is in your verified contracts table (with full ABI)
- Try common ABIs (ERC-20, ERC-721, etc.)
- Look up function selector in 4byte.directory
- Fall back to raw hex display
Layer 4: Event Parsing and Token Tracking
The Transfer event is the same across ERC-20 and ERC-721 (different semantics though):
Transfer(address indexed from, address indexed to, uint256 value)
Topic0: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
To track token balances:
For each Transfer event:
if (from != 0x0) {
token_balances[token][from] -= value
}
if (to != 0x0) {
token_balances[token][to] += value
}
Special cases:
from == 0x0: Mint (new tokens created)to == 0x0: Burn (tokens destroyed)
Layer 5: Database Schema Design
Design tables for efficient querying:
+------------------+ +-------------------+ +------------------+
| blocks | | transactions | | logs |
+------------------+ +-------------------+ +------------------+
| number (PK) |<----| block_number (FK) | | tx_hash (FK) |
| hash | | tx_hash (PK) |<----| log_index (PK) |
| parent_hash | | tx_index | | address |
| timestamp | | from_address | | topic0 |
| miner | | to_address | | topic1 |
| gas_used | | value | | topic2 |
| gas_limit | | gas_used | | topic3 |
| base_fee | | gas_price | | data |
| tx_count | | input | | decoded_name |
+------------------+ | status | | decoded_args |
| decoded_func | +------------------+
| decoded_args |
+-------------------+
+------------------+ +-------------------+
| token_transfers | | token_balances |
+------------------+ +-------------------+
| tx_hash (FK) | | address |
| log_index | | token_address |
| token_address | | balance |
| from_address | | last_updated_block|
| to_address | +-------------------+
| value |
| token_type | (ERC20, ERC721, ERC1155)
+------------------+
Critical Indexes:
CREATE INDEX idx_tx_from ON transactions(from_address, block_number DESC);
CREATE INDEX idx_tx_to ON transactions(to_address, block_number DESC);
CREATE INDEX idx_logs_address ON logs(address, block_number DESC);
CREATE INDEX idx_transfers_from ON token_transfers(from_address, block_number DESC);
CREATE INDEX idx_transfers_to ON token_transfers(to_address, block_number DESC);
CREATE INDEX idx_balances_address ON token_balances(address);
Layer 6: API Layer Implementation
Build a REST API with proper pagination:
+-------------+
| Frontend |
+-------------+
|
v
+---------------------|---------------------+
| API |
| +---------------+ +------------------+ |
| | /blocks | | /address/:addr | |
| | /block/:num | | /addr/:a/txs | |
| | /tx/:hash | | /addr/:a/tokens | |
| +---------------+ +------------------+ |
+------------------------------------------+
|
v
+---------------+
| PostgreSQL |
+---------------+
Pagination Strategy (cursor-based is better for blockchain):
GET /address/0x123/transactions?cursor=18000000_99&limit=20
Response:
{
"data": [...],
"next_cursor": "17999980_45",
"has_more": true
}
Cursor format: {block_number}_{tx_index} ensures stable pagination even as new blocks arrive.
Layer 7: Real-Time WebSocket Updates
Implement subscriptions for live data:
// Server-side
wss.on("connection", (ws) => {
ws.on("message", (msg) => {
const { subscribe, channel } = JSON.parse(msg);
if (channel === "newBlocks") {
// Add to subscribers list
blockSubscribers.add(ws);
}
});
});
// When new block is indexed
function onBlockIndexed(block) {
for (const ws of blockSubscribers) {
ws.send(JSON.stringify({ type: "newBlock", data: block }));
}
}
Subscription Types:
newBlocks: All new blocksaddress:0x123: Transactions involving specific addresstoken:0x456: Transfers for specific token
Layer 8: Contract Verification and Source Code
To display contract source code (like Etherscanâs âContractâ tab):
- Accept source code upload with compiler settings
- Recompile and verify bytecode matches on-chain
- Store source code, ABI, and compiler version
- Enable decoded views for this contract
User uploads source.sol + compiler version
|
v
+------------------+ +-------------------+
| Compile with | | Compare bytecode |
| same settings |---->| with on-chain |
+------------------+ +-------------------+
|
Match? | Mismatch?
+--------+--------+
| |
v v
+-------------+ +--------------+
| Store as | | Reject |
| verified | | verification |
+-------------+ +--------------+
Real World Outcome
Web Interface: Homepage
+==============================================================================+
| [ Logo ] Mini Explorer [ Search: Address/Tx/Block ] |
+==============================================================================+
Latest Blocks Latest Transactions
+-------------------------------------+ +----------------------------------+
| Block | Age | Txs | Gas Used| | Tx Hash | From | To |
|---------|--------|-------|---------| |--------------|--------|----------|
| 18543210| 12s ago| 147 | 58.3% | | 0xabc1... | 0x12...| 0x45... |
| 18543209| 24s ago| 189 | 72.1% | | 0xdef2... | 0x78...| Contract |
| 18543208| 36s ago| 156 | 61.8% | | 0x890a... | 0x90...| 0x12... |
| 18543207| 48s ago| 201 | 78.4% | | 0xbcd3... | 0xab...| Contract |
| 18543206| 60s ago| 133 | 52.7% | | 0xef01... | 0xcd...| 0xef... |
+-------------------------------------+ +----------------------------------+
[View All Blocks] [View All Transactions]
+------------------------------------------------------------------------------+
| Network Stats |
| ETH Price: $2,345.67 | Gas: 25 gwei | Total Tx: 2.1B | Blocks: 18.5M |
+------------------------------------------------------------------------------+
Web Interface: Block Details Page
+==============================================================================+
| Block #18543210 [Copy] |
+==============================================================================+
+------------------------------------------------------------------------------+
| Overview |
+------------------------------------------------------------------------------+
| Block Height | 18,543,210 |
| Timestamp | Dec 27, 2024, 10:23:45 AM UTC (12 seconds ago) |
| Transactions | 147 transactions in this block |
| Withdrawals | 16 withdrawals (32.5 ETH) |
+------------------------------------------------------------------------------+
| Block Hash | 0x8a7b3c4d5e6f...9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3 |
| Parent Hash | 0x7f6e5d4c3b2a...1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7 |
| State Root | 0x1234567890ab...cdef1234567890abcdef1234567890abcd |
+------------------------------------------------------------------------------+
| Fee Recipient | 0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5 (Lido) |
| Total Difficulty | 58,750,003,716,598,352,816,469 |
| Size | 98,432 bytes |
+------------------------------------------------------------------------------+
| Gas Used | 14,234,567 (47.45% of limit) |
| | [=================== ] 30M limit |
| Gas Limit | 30,000,000 |
| Base Fee Per Gas | 25.234 Gwei |
| Burnt Fees | 0.359 ETH |
+------------------------------------------------------------------------------+
Transactions (147)
+------------------------------------------------------------------------------+
| Hash | Method | From | To | Value | Fee |
|----------------|------------|-----------|------------|----------|----------|
| 0xabc123... | Transfer | 0x12a... | 0x45b... | 2.5 ETH | 0.003 ETH|
| 0xdef456... | Swap | 0x78c... | Uniswap V3 | 0 ETH | 0.008 ETH|
| 0x789abc... | Approve | 0x90d... | USDT | 0 ETH | 0.001 ETH|
| ... | ... | ... | ... | ... | ... |
+------------------------------------------------------------------------------+
[Show More]
Web Interface: Transaction Details Page
+==============================================================================+
| Transaction Details |
+==============================================================================+
+------------------------------------------------------------------------------+
| Transaction Hash | 0xabc123def456789... [Copy] |
| Status | [Success] (Confirmed in 24 blocks) |
| Block | 18543210 (Confirmed) |
| Timestamp | Dec 27, 2024, 10:23:45 AM UTC (12 minutes ago) |
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
| From | 0x5a52e96bacdabb82fd05763e25335261b270efcb |
| To (Contract) | 0xdAC17F958D2ee523a2206206994597C13D831ec7 (USDT) |
| Value | 0 ETH |
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
| Input Data (Decoded) |
+------------------------------------------------------------------------------+
| Function: transfer(address _to, uint256 _value) |
| |
| Parameters: |
| _to : 0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43 |
| _value : 4000000 (4.0 USDT) |
| |
| [View Raw Input Data] |
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
| Transaction Fee |
+------------------------------------------------------------------------------+
| Gas Used | 46,109 (of 60,000 limit) |
| Gas Price | 25.234 Gwei |
| Transaction Fee | 0.001163 ETH ($2.73) |
| Burnt | 0.001048 ETH |
| Savings | 0.000115 ETH (to validator) |
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
| Event Logs (1) |
+------------------------------------------------------------------------------+
| #0 Transfer (address from, address to, uint256 value) |
| |
| from : 0x5a52e96bacdabb82fd05763e25335261b270efcb |
| to : 0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43 |
| value : 4000000 (4.0 USDT) |
+------------------------------------------------------------------------------+
Web Interface: Address Page
+==============================================================================+
| Address 0x5a52e96bacdabb82fd05763e25335261b270efcb |
+==============================================================================+
+--------------------------------+ +------------------------------------------+
| Overview | | More Info |
+--------------------------------+ +------------------------------------------+
| ETH Balance | 12.456 ETH | | Token Tracker | 15 ERC-20 tokens |
| ETH Value | $29,213.54 | | Transactions | 1,234 total |
+--------------------------------+ +------------------------------------------+
+------------------------------------------------------------------------------+
| Token Holdings |
+------------------------------------------------------------------------------+
| Token | Balance | Value | Contract |
|----------------|-------------------|---------------|------------------------|
| USDT | 10,500.00 | $10,500.00 | 0xdAC17F958D2ee... |
| USDC | 5,234.56 | $5,234.56 | 0xA0b86991c62... |
| WETH | 2.345 | $5,499.76 | 0xC02aaA39b22... |
| UNI | 125.50 | $876.55 | 0x1f9840a85d5... |
| LINK | 89.25 | $1,249.50 | 0x514910771AF... |
+------------------------------------------------------------------------------+
[View All Token Holdings]
+------------------------------------------------------------------------------+
| Transactions |
+------------------------------------------------------------------------------+
[ All ] [ Sent ] [ Received ] [ Token Transfers ] [ NFT Transfers ]
| Tx Hash | Method | Block | Age | From | To | Value |
|--------------|----------|-----------|----------|---------|----------|--------|
| 0xabc123... | Transfer | 18543210 | 12m ago | (self) | 0x45b... | 2.5 ETH|
| 0xdef456... | Swap | 18543102 | 2h ago | (self) | Uniswap | 1.0 ETH|
| 0x789abc... | IN | 18542890 | 6h ago | 0x12a...| (self) | 5.0 ETH|
| 0xcde890... | Approve | 18542567 | 12h ago | (self) | USDT | 0 ETH |
+------------------------------------------------------------------------------+
[Load More]
Web Interface: Contract Page
+==============================================================================+
| Contract 0xdAC17F958D2ee523a2206206994597C13D831ec7 |
| [ Verified ] Tether USD (USDT) |
+==============================================================================+
[ Contract ] [ Read Contract ] [ Write Contract ] [ Transactions ] [ Events ]
+------------------------------------------------------------------------------+
| Contract Overview |
+------------------------------------------------------------------------------+
| Balance | 0 ETH |
| Transactions | 234,567,890 |
| Token Tracker | Tether USD (USDT) |
| Creator | 0xc6cde7c39eb2f0f0095f41570af89efc2c1ea828 |
| Created At | Block 4634748 (Nov 28, 2017) |
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
| Contract Source Code |
| Compiler: v0.4.18+commit.9cf6e910 | Optimization: Yes (200 runs) |
+------------------------------------------------------------------------------+
| File 1 of 1: TetherToken.sol |
| |
| pragma solidity ^0.4.17; |
| |
| library SafeMath { |
| function mul(uint256 a, uint256 b) internal pure returns (uint256) { |
| if (a == 0) { |
| return 0; |
| } |
| uint256 c = a * b; |
| assert(c / a == b); |
| return c; |
| } |
| // ... more source code |
| } |
+------------------------------------------------------------------------------+
+------------------------------------------------------------------------------+
| Contract ABI |
+------------------------------------------------------------------------------+
| [ |
| { |
| "constant": true, |
| "inputs": [], |
| "name": "name", |
| "outputs": [{"name": "", "type": "string"}], |
| "payable": false, |
| "stateMutability": "view", |
| "type": "function" |
| }, |
| // ... more ABI entries |
| ] |
| [Copy ABI] [Download JSON] |
+------------------------------------------------------------------------------+
API Response Examples
GET /api/block/18543210
{
"number": 18543210,
"hash": "0x8a7b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
"parentHash": "0x7f6e5d4c3b2a1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b",
"timestamp": 1703671425,
"miner": "0xdafea492d9c6733ae3d56b7ed1adb60692c98bc5",
"gasUsed": "14234567",
"gasLimit": "30000000",
"baseFeePerGas": "25234000000",
"transactionCount": 147,
"transactions": [
{
"hash": "0xabc123...",
"from": "0x5a52e96bacdabb82fd05763e25335261b270efcb",
"to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"value": "0",
"gasUsed": "46109",
"status": 1,
"decodedInput": {
"function": "transfer",
"args": {
"_to": "0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43",
"_value": "4000000"
}
}
}
]
}
GET /api/tx/0xabc123âŚ
{
"hash": "0xabc123def456789...",
"status": 1,
"blockNumber": 18543210,
"blockHash": "0x8a7b3c4d...",
"timestamp": 1703671425,
"from": "0x5a52e96bacdabb82fd05763e25335261b270efcb",
"to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"value": "0",
"gasUsed": "46109",
"gasPrice": "25234000000",
"input": "0xa9059cbb0000000000000000000000005a52e96bacdabb82fd05763e25335261b270efcb00000000000000000000000000000000000000000000000000000000003d0900",
"decodedInput": {
"function": "transfer",
"signature": "transfer(address,uint256)",
"args": {
"_to": "0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43",
"_value": "4000000"
}
},
"logs": [
{
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"topics": ["0xddf252ad...", "0x000...5a52e96...", "0x000...a9d1e08..."],
"data": "0x00000000000000000000000000000000000000000000000000000000003d0900",
"decoded": {
"event": "Transfer",
"args": {
"from": "0x5a52e96bacdabb82fd05763e25335261b270efcb",
"to": "0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43",
"value": "4000000"
}
}
}
],
"tokenTransfers": [
{
"token": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"tokenName": "Tether USD",
"tokenSymbol": "USDT",
"tokenDecimals": 6,
"from": "0x5a52e96bacdabb82fd05763e25335261b270efcb",
"to": "0xa9d1e08c7793af67e9d92fe308d5697fb81d3e43",
"value": "4000000",
"valueFormatted": "4.0"
}
]
}
GET /api/address/0x5a52âŚ/transactions?limit=20&cursor=18543210_99
{
"address": "0x5a52e96bacdabb82fd05763e25335261b270efcb",
"balance": "12456000000000000000",
"transactionCount": 1234,
"data": [
{
"hash": "0xabc123...",
"blockNumber": 18543210,
"txIndex": 45,
"timestamp": 1703671425,
"from": "0x5a52e96bacdabb82fd05763e25335261b270efcb",
"to": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"value": "0",
"direction": "out",
"method": "Transfer"
}
],
"nextCursor": "18543102_12",
"hasMore": true
}
GET /api/address/0x5a52âŚ/tokens
{
"address": "0x5a52e96bacdabb82fd05763e25335261b270efcb",
"tokens": [
{
"contract": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"name": "Tether USD",
"symbol": "USDT",
"decimals": 6,
"balance": "10500000000",
"balanceFormatted": "10500.0"
},
{
"contract": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"name": "USD Coin",
"symbol": "USDC",
"decimals": 6,
"balance": "5234560000",
"balanceFormatted": "5234.56"
}
]
}
Complete Architecture Diagram
+==============================================================================+
| BLOCK EXPLORER ARCHITECTURE |
+==============================================================================+
USERS
|
v
+------------------------------------------------------------------------------+
| FRONTEND (React/Vue) |
| +----------------+ +----------------+ +----------------+ +-----------+ |
| | Block View | | Transaction | | Address View | | Search | |
| | | | View | | | | | |
| +----------------+ +----------------+ +----------------+ +-----------+ |
| | |
| WebSocket | HTTP/REST |
+------------------------------------------------------------------------------+
|
v
+------------------------------------------------------------------------------+
| API LAYER |
| +------------------+ +------------------+ +-------------------+ |
| | REST Endpoints | | WebSocket | | Rate Limiter | |
| | /block/:num | | Server | | & Cache | |
| | /tx/:hash | | newBlocks | | (Redis) | |
| | /address/:addr | | addressActivity | | | |
| +------------------+ +------------------+ +-------------------+ |
+------------------------------------------------------------------------------+
|
v
+------------------------------------------------------------------------------+
| DATABASE (PostgreSQL) |
| |
| +----------+ +-------------+ +--------+ +-----------------+ |
| | blocks |<-->| transactions|<-->| logs |<-->| token_transfers | |
| +----------+ +-------------+ +--------+ +-----------------+ |
| | |
| +---------------+ +------------------+ v |
| | contracts | | verified_sources | +------------------+ |
| | (metadata) | | (source, ABI) | | token_balances | |
| +---------------+ +------------------+ +------------------+ |
+------------------------------------------------------------------------------+
^
|
+------------------------------------------------------------------------------+
| INDEXER SERVICE |
| |
| +-------------------+ |
| | Block Processor | For each new block: |
| | | 1. Fetch block + transactions |
| | +-----------+ | 2. Fetch all receipts |
| | | Fetch | | 3. Decode function calls (if ABI known) |
| | | Block | | 4. Parse event logs |
| | +-----------+ | 5. Extract token transfers |
| | | | 6. Update token balances |
| | v | 7. Store in database |
| | +-----------+ | 8. Notify WebSocket subscribers |
| | | Fetch | | |
| | | Receipts | | |
| | +-----------+ | |
| | | | |
| | v | |
| | +-----------+ | |
| | | Decode | | |
| | | & Parse | | |
| | +-----------+ | |
| | | | |
| | v | |
| | +-----------+ | |
| | | Store | | |
| | +-----------+ | |
| +-------------------+ |
| ^ |
| | |
+-------------|----------------------------------------------------------------+
|
v
+------------------------------------------------------------------------------+
| ETHEREUM NODE |
| +--------------------+ +--------------------+ +--------------------+ |
| | Infura/Alchemy | | Local Geth/Erigon | | Archive Node | |
| | (managed) | | (self-hosted) | | (historical state) | |
| +--------------------+ +--------------------+ +--------------------+ |
| |
| JSON-RPC API: |
| eth_getBlockByNumber, eth_getTransactionReceipt, eth_getLogs, etc. |
+------------------------------------------------------------------------------+
Data Flow: Indexing a New Block
+------------------+
| 1. Detect New | eth_blockNumber or WebSocket subscription
| Block | returns block 18543210
+------------------+
|
v
+------------------+
| 2. Fetch Block | eth_getBlockByNumber(18543210, true)
| + Transactions| Returns block header + 147 transaction objects
+------------------+
|
v
+------------------+
| 3. Fetch All | eth_getTransactionReceipt(tx_hash) x 147
| Receipts | Returns status, gasUsed, logs for each
+------------------+ (Use batch RPC for efficiency)
|
v
+------------------+
| 4. Decode | For each transaction:
| Function Calls| - Extract selector (first 4 bytes of input)
+------------------+ - Lookup ABI or use 4byte.directory
| - Decode arguments
v
+------------------+
| 5. Parse Event | For each log:
| Logs | - Match topic[0] to known event signatures
+------------------+ - Decode indexed params from topics[1-3]
| - Decode data field
v
+------------------+
| 6. Extract Token | For Transfer events:
| Transfers | - Identify token contract
+------------------+ - Extract from, to, value
| - Determine ERC-20 vs ERC-721 vs ERC-1155
v
+------------------+
| 7. Update Token | For each transfer:
| Balances | - Decrement sender balance
+------------------+ - Increment receiver balance
| - Handle mint (from=0x0) and burn (to=0x0)
v
+------------------+
| 8. Database | BEGIN TRANSACTION
| Transaction | INSERT INTO blocks ...
+------------------+ INSERT INTO transactions ... (bulk)
| INSERT INTO logs ... (bulk)
v INSERT INTO token_transfers ... (bulk)
+------------------+ UPSERT token_balances ...
| 9. Notify | COMMIT
| WebSocket |
| Subscribers | Push new block to subscribed clients
+------------------+
Books That Will Help
| Book | Author(s) | Why It Helps |
|---|---|---|
| Mastering Ethereum | Andreas M. Antonopoulos & Gavin Wood | Essential reference for understanding transactions, receipts, events, and the EVM. Chapter 7 on events is particularly relevant. |
| Designing Data-Intensive Applications | Martin Kleppmann | Crucial for understanding database design, indexing strategies, and handling streaming data. Chapter 3 on storage engines and Chapter 11 on stream processing directly apply. |
| Building Microservices | Sam Newman | Helpful for designing the API layer and understanding how to split the indexer from the API service. |
| Web Scalability for Startup Engineers | Artur Ejsmont | Practical guidance on caching strategies, database scaling, and handling high traffic. |
| High Performance Browser Networking | Ilya Grigorik | For understanding WebSocket optimization and real-time communication patterns. |
Additional Resources
Ethereum JSON-RPC
ABI and Encoding
- Solidity ABI Specification
- 4byte Directory - Function signature database
- ethers.js ABI Documentation
Libraries
- ethers.js - Ethereum library for TypeScript/JavaScript
- viem - Modern TypeScript Ethereum library
- web3.py - Python Ethereum library
Open Source Block Explorers
- Blockscout - Full-featured open source explorer (Elixir)
- Otterscan - Fast block explorer for Erigon nodes
- Expedition - Simple React-based explorer
Database and Indexing
- The Graph - Decentralized indexing protocol
- Subsquid - Blockchain indexing framework
- Ponder - Framework for building blockchain indexers
Token Standards
- ERC-20 - Fungible token standard
- ERC-721 - Non-fungible token standard
- ERC-1155 - Multi-token standard
Learning Milestones
Track your progress with these milestones:
Milestone 1: Basic Block/Transaction Display
- Connect to Ethereum node via JSON-RPC
- Fetch and display latest blocks
- Fetch and display transactions within a block
- Display basic block details (hash, timestamp, gas used)
You understand: How to query the blockchain
Milestone 2: Transaction Decoding
- Decode function calls using known ABIs
- Display human-readable function names and arguments
- Handle unknown contracts gracefully (show raw hex)
- Implement 4byte.directory lookup for unknown selectors
You understand: ABI encoding
Milestone 3: Event Log Parsing
- Parse Transfer events from receipts
- Identify ERC-20 vs ERC-721 transfers
- Display decoded event arguments
- Handle multiple events per transaction
You understand: Ethereum events and indexing
Milestone 4: Address Pages with Token Balances
- Track token transfers in database
- Compute and display token balances
- Show transaction history for addresses
- Handle chain reorganizations
You understand: State reconstruction from events
Milestone 5: Real-Time Updates
- Subscribe to new blocks via WebSocket
- Push updates to connected frontend clients
- Handle disconnections and reconnections
- Display âpendingâ confirmations
You understand: Real-time blockchain data
Milestone 6: Search and Scale
- Implement address/hash search
- Add proper pagination (cursor-based)
- Optimize database queries with indexes
- Add caching for frequently accessed data
You understand: Blockchain data infrastructure
Time Estimate
| Phase | Duration | Description |
|---|---|---|
| Setup & Node Connection | 2-3 days | Project structure, RPC connection, basic fetching |
| Database Schema | 2-3 days | Design schema, set up PostgreSQL, create migrations |
| Indexer Core | 3-5 days | Block processing, receipt fetching, event parsing |
| ABI Decoding | 2-3 days | Function/event decoding, signature lookup |
| Token Tracking | 2-3 days | Balance computation, transfer indexing |
| API Layer | 3-4 days | REST endpoints, pagination, caching |
| Frontend | 4-5 days | React app, block/tx/address pages |
| WebSocket Updates | 2-3 days | Real-time subscriptions, push notifications |
| Polish & Testing | 2-3 days | Error handling, edge cases, documentation |
| Total | 2-3 weeks | Â |
Prerequisites
Before starting this project, you should have completed:
- Project 9: ERC-20 Token from Scratch - Understanding token standards and events
- Basic web development - React/Vue, REST APIs, PostgreSQL
- Familiarity with async programming - Promises, async/await in TypeScript
Recommended but not required:
- Experience with WebSockets
- Basic understanding of database indexing
Building a block explorer teaches you that the blockchain is just dataâcomplex, encoded, distributed data. But once you can read it, parse it, and present it, you become a translator between the raw cryptographic world and human understanding. And thatâs a skill that applies far beyond just block explorers.