1. Explorer homepage
Let's explore how we built the sim Explorer homepage with sim
Goal
Our old homepage for sim Explorer / evm.storage was a search bar with a static list of three contracts that you could click into. We wanted to make a new one with sim that was more engaging and dynamic, and decided on adding modules for recent blocks, active contracts, and latest transactions. Here's the new design:
Canvas
APIs
Recent blocks
The recent blocks API uses the @sim.blocks core table to pull the most recent ten blocks. The query includes CTEs to apply the latest ENS resolution for each coinbase address. We also use whether the address has been the sender on any transactions as a simple heuristic to classify it as an EOA as opposed to a contract.
Active contracts
The active contracts API sums calls and transactions in blocks across the last hour and the hour before that for every contract, then it sorts by call counts descending and returns the ten most active contracts. This table uses the @org.contract_call_counts_block
table built in this canvas and described below.
Latest transactions
The latest transactions API uses the @sim.ethereum_transactions core table to pull the most recent transactions. Like we did for coinbase in the recent blocks API, we add in ENS names and add identifiers for whether the from or to addresses are EOAs.
Data pipeline
To build the @org.contract_call_counts_block
table, we built a simple data pipeline that runs at the tip for Ethereum and Base. The Lambda hooks on each txn/call. Ignoring precompiles, if the call recipient is seen for the first time, it's added to the activeContracts
array. We increment the call_counts
value for that address by one. And, if it's an external transaction, i.e., call_depth == 0
, we also increment the txn_counts
variable.
function postTransaction() public {
CallContext storage ctx = getCallContext();
// ignore precompiles
if (uint160(ctx.txn.to()) <= 9) {
return;
}
if (!isActive[ctx.txn.to()]) {
activeContracts.push(ctx.txn.to());
isActive[ctx.txn.to()] = true;
}
call_counts[ctx.txn.to()]++;
if (ctx.call_depth == 0) {
txn_counts[ctx.txn.to()]++;
}
}
Because standard EVM Lambda state is reset between blocks, activeContracts
, call_counts
, and txn_counts
are stateful within the block and adjust on each transaction. At the end of the block, we simply iterate over the activeContracts
array and emit the counts and some other data for each contract:
function postBlock() public {
for (uint256 i = activeContracts.length; i > 0; i--) {
simEmitToSchema_call_counts(
SchemaCall_countsColumns({
chain_id: uint64(block.chainid),
block_timestamp: uint64(block.timestamp),
block_number: uint64(block.number),
contract_address: activeContracts[i-1],
name: getName(activeContracts[i-1]),
symbol: getSymbol(activeContracts[i-1]),
call_count: call_counts[activeContracts[i-1]],
txn_count: txn_counts[activeContracts[i-1]]
})
);
}
}
You'll notice we also define getName
and getSymbol
functions. These just allow us to grab the name and symbol on contracts that expose them without causing reversions when trying to fetch them from contracts that don't.
Updated about 1 month ago