From 3e156a6eb8e9830337b277d12f2b29e7c07ccf66 Mon Sep 17 00:00:00 2001 From: Niran Babalola Date: Thu, 9 Apr 2026 10:58:00 -0500 Subject: [PATCH] Pass through raw meterBundleResponse in block API Store the raw meter_bundle_response from S3 on each BlockTransaction instead of extracting individual fields (executionTimeUs, stateRootTimeUs, gasUsed). The API serializes it as metering: { transaction, bundle } where transaction is results[0] and bundle is everything else. This means new fields added to MeterBundleResponse or TransactionResult in the base node (e.g. stateRootAccountNodeCount, stateRootStorageNodeCount from v0.7) flow through to API consumers automatically without TIPS code changes. --- src/app/api/block/[hash]/route.ts | 119 ++++++++++++------------------ src/app/block/[hash]/page.tsx | 64 ++++++++++------ src/lib/s3.ts | 14 +--- 3 files changed, 92 insertions(+), 105 deletions(-) diff --git a/src/app/api/block/[hash]/route.ts b/src/app/api/block/[hash]/route.ts index dcfc94c..37d0144 100644 --- a/src/app/api/block/[hash]/route.ts +++ b/src/app/api/block/[hash]/route.ts @@ -8,7 +8,6 @@ import { getBlockFromCache, getBundleHistory, getTransactionMetadataByHash, - type MeterBundleResult, } from "@/lib/s3"; function serializeBlockData(block: BlockData) { @@ -18,11 +17,23 @@ function serializeBlockData(block: BlockData) { timestamp: block.timestamp.toString(), gasUsed: block.gasUsed.toString(), gasLimit: block.gasLimit.toString(), - transactions: block.transactions.map((tx) => ({ - ...tx, - gasLimit: tx.gasLimit.toString(), - gasUsed: tx.gasUsed?.toString() ?? null, - })), + transactions: block.transactions.map((tx) => { + let metering: { transaction: unknown; bundle: unknown } | null = null; + if (tx.meterBundleResponse) { + const { results, ...bundle } = tx.meterBundleResponse; + const txResults = results as unknown[] | undefined; + metering = { transaction: txResults?.[0] ?? null, bundle }; + } + return { + hash: tx.hash, + from: tx.from, + to: tx.to, + gasLimit: tx.gasLimit.toString(), + bundleId: tx.bundleId, + index: tx.index, + metering, + }; + }), }; } @@ -67,6 +78,35 @@ async function fetchBlockFromRpcByNumber( } } +async function enrichTransactionWithBundleData(txHash: string): Promise<{ + bundleId: string | null; + meterBundleResponse: Record | null; +}> { + const metadata = await getTransactionMetadataByHash(txHash); + if (!metadata || metadata.bundle_ids.length === 0) { + return { bundleId: null, meterBundleResponse: null }; + } + + const bundleId = metadata.bundle_ids[0]; + const bundleHistory = await getBundleHistory(bundleId); + if (!bundleHistory) { + return { bundleId, meterBundleResponse: null }; + } + + const receivedEvent = bundleHistory.history.find( + (e) => e.event === "Received", + ); + if (!receivedEvent?.data?.bundle?.meter_bundle_response) { + return { bundleId, meterBundleResponse: null }; + } + + return { + bundleId, + meterBundleResponse: receivedEvent.data.bundle + .meter_bundle_response as unknown as Record, + }; +} + async function buildAndCacheBlockData( rpcBlock: Block, hash: Hash, @@ -80,11 +120,9 @@ async function buildAndCacheBlockData( from: tx.from, to: tx.to, gasLimit: tx.gas, - gasUsed: enriched.gasUsed != null ? BigInt(enriched.gasUsed) : null, - executionTimeUs: enriched.executionTimeUs, - stateRootTimeUs: enriched.stateRootTimeUs, bundleId: enriched.bundleId, index, + meterBundleResponse: enriched.meterBundleResponse, }; }), ); @@ -111,63 +149,6 @@ function isSystemTransaction(tx: BlockTransaction): boolean { return tx.index === 0; } -async function enrichTransactionWithBundleData(txHash: string): Promise<{ - bundleId: string | null; - executionTimeUs: number | null; - stateRootTimeUs: number | null; - gasUsed: number | null; -}> { - const metadata = await getTransactionMetadataByHash(txHash); - if (!metadata || metadata.bundle_ids.length === 0) { - return { - bundleId: null, - executionTimeUs: null, - stateRootTimeUs: null, - gasUsed: null, - }; - } - - const bundleId = metadata.bundle_ids[0]; - const bundleHistory = await getBundleHistory(bundleId); - if (!bundleHistory) { - return { - bundleId, - executionTimeUs: null, - stateRootTimeUs: null, - gasUsed: null, - }; - } - - const receivedEvent = bundleHistory.history.find( - (e) => e.event === "Received", - ); - if (!receivedEvent?.data?.bundle?.meter_bundle_response?.results) { - return { - bundleId, - executionTimeUs: null, - stateRootTimeUs: null, - gasUsed: null, - }; - } - - const meterResponse = receivedEvent.data.bundle.meter_bundle_response; - - // TODO: Switch to meterResponse.totalExecutionTimeUs once 0.7 is deployed. - // On 0.6, totalExecutionTimeUs is the wall-clock total_time_us which includes - // setup, teardown, and state root (double-counting stateRootTimeUs). PR #1111 - // fixes this on main to be the sum of per-tx execution times. - const txResult = meterResponse.results.find( - (r: MeterBundleResult) => r.txHash.toLowerCase() === txHash.toLowerCase(), - ); - - return { - bundleId, - executionTimeUs: txResult?.executionTimeUs ?? null, - stateRootTimeUs: meterResponse.stateRootTimeUs ?? null, - gasUsed: txResult?.gasUsed ?? null, - }; -} - async function refetchMissingTransactionSimulations( block: BlockData, ): Promise<{ updatedBlock: BlockData; hasUpdates: boolean }> { @@ -194,11 +175,7 @@ async function refetchMissingTransactionSimulations( return { ...tx, bundleId: refetchResult.bundleId, - executionTimeUs: refetchResult.executionTimeUs, - stateRootTimeUs: refetchResult.stateRootTimeUs, - ...(refetchResult.gasUsed != null && { - gasUsed: BigInt(refetchResult.gasUsed), - }), + meterBundleResponse: refetchResult.meterBundleResponse, }; } return tx; diff --git a/src/app/block/[hash]/page.tsx b/src/app/block/[hash]/page.tsx index b159cdb..c5d0a4a 100644 --- a/src/app/block/[hash]/page.tsx +++ b/src/app/block/[hash]/page.tsx @@ -6,6 +6,25 @@ import type { BlockData, BlockTransaction } from "@/lib/s3"; const BLOCK_EXPLORER_URL = process.env.NEXT_PUBLIC_BLOCK_EXPLORER_URL; +// The API serializes metering as { transaction, bundle } from the raw +// meterBundleResponse. The page receives this shape via JSON. +// biome-ignore lint/suspicious/noExplicitAny: opaque metering JSON +function metering(tx: BlockTransaction): any { + return (tx as any).metering; +} + +function executionTimeUs(tx: BlockTransaction): number | null { + return metering(tx)?.transaction?.executionTimeUs ?? null; +} + +function stateRootTimeUs(tx: BlockTransaction): number | null { + return metering(tx)?.bundle?.stateRootTimeUs ?? null; +} + +function gasUsed(tx: BlockTransaction): number | null { + return metering(tx)?.transaction?.gasUsed ?? null; +} + interface PageProps { params: Promise<{ hash: string }>; } @@ -78,11 +97,11 @@ function Card({ } function getHeatmapStyle( - executionTimeUs: number, + timeUs: number, maxTime: number, ): { bg: string; text: string } { if (maxTime === 0) return { bg: "bg-amber-50", text: "text-amber-700" }; - const ratio = Math.min(executionTimeUs / maxTime, 1); + const ratio = Math.min(timeUs / maxTime, 1); if (ratio < 0.2) return { bg: "bg-amber-100", text: "text-amber-800" }; if (ratio < 0.4) return { bg: "bg-amber-200", text: "text-amber-900" }; if (ratio < 0.6) return { bg: "bg-orange-200", text: "text-orange-900" }; @@ -98,13 +117,14 @@ function TransactionRow({ maxTotalTime: number; }) { const hasBundle = tx.bundleId !== null; - const hasExecutionTime = tx.executionTimeUs !== null; - const executionTime = tx.executionTimeUs ?? 0; - const stateRootTime = tx.stateRootTimeUs ?? 0; - const totalTime = executionTime + stateRootTime; - const heatmapStyle = hasExecutionTime + const execTime = executionTimeUs(tx); + const srTime = stateRootTimeUs(tx); + const hasMetering = execTime !== null; + const totalTime = (execTime ?? 0) + (srTime ?? 0); + const heatmapStyle = hasMetering ? getHeatmapStyle(totalTime, maxTotalTime) : null; + const txGasUsed = gasUsed(tx); const content = (
- {hasExecutionTime && heatmapStyle ? ( + {hasMetering && heatmapStyle ? ( @@ -152,8 +172,8 @@ function TransactionRow({
)}
- {tx.gasUsed != null - ? `${tx.gasUsed.toLocaleString()} / ${tx.gasLimit.toLocaleString()} gas` + {txGasUsed != null + ? `${txGasUsed.toLocaleString()} / ${tx.gasLimit.toLocaleString()} gas` : `${tx.gasLimit.toLocaleString()} gas limit`}
@@ -168,15 +188,15 @@ function TransactionRow({ } function BlockStats({ block }: { block: BlockData }) { - const txsWithTime = block.transactions.filter( - (tx) => tx.executionTimeUs !== null, + const meteredTxs = block.transactions.filter( + (tx) => executionTimeUs(tx) !== null, ); - const totalExecutionTime = txsWithTime.reduce( - (sum, tx) => sum + (tx.executionTimeUs ?? 0), + const totalExecTime = meteredTxs.reduce( + (sum, tx) => sum + (executionTimeUs(tx) ?? 0), 0, ); - const totalStateRootTime = txsWithTime.reduce( - (sum, tx) => sum + (tx.stateRootTimeUs ?? 0), + const totalSrTime = meteredTxs.reduce( + (sum, tx) => sum + (stateRootTimeUs(tx) ?? 0), 0, ); const bundleCount = block.transactions.filter( @@ -208,17 +228,13 @@ function BlockStats({ block }: { block: BlockData }) {
Total Exec Time
- {totalExecutionTime > 0 - ? `${totalExecutionTime.toLocaleString()}μs` - : "—"} + {totalExecTime > 0 ? `${totalExecTime.toLocaleString()}μs` : "—"}
Total State Root
- {totalStateRootTime > 0 - ? `${totalStateRootTime.toLocaleString()}μs` - : "—"} + {totalSrTime > 0 ? `${totalSrTime.toLocaleString()}μs` : "—"}
@@ -303,8 +319,8 @@ export default function BlockPage({ params }: PageProps) { const maxTotalTime = data ? Math.max( ...data.transactions - .filter((tx) => tx.executionTimeUs !== null) - .map((tx) => (tx.executionTimeUs ?? 0) + (tx.stateRootTimeUs ?? 0)), + .filter((tx) => executionTimeUs(tx) !== null) + .map((tx) => (executionTimeUs(tx) ?? 0) + (stateRootTimeUs(tx) ?? 0)), 0, ) : 0; diff --git a/src/lib/s3.ts b/src/lib/s3.ts index 23630a0..191fa18 100644 --- a/src/lib/s3.ts +++ b/src/lib/s3.ts @@ -187,11 +187,9 @@ export interface BlockTransaction { from: string; to: string | null; gasLimit: bigint; - gasUsed: bigint | null; - executionTimeUs: number | null; - stateRootTimeUs: number | null; bundleId: string | null; index: number; + meterBundleResponse: Record | null; } export interface BlockData { @@ -223,14 +221,10 @@ export async function getBlockFromCache( gasUsed: BigInt(parsed.gasUsed), gasLimit: BigInt(parsed.gasLimit), transactions: parsed.transactions.map( - (tx: { - gasLimit?: string; - gasUsed?: string | null; - [key: string]: unknown; - }) => ({ + (tx: { gasLimit?: string; [key: string]: unknown }) => ({ ...tx, - gasLimit: BigInt(tx.gasLimit ?? tx.gasUsed ?? "0"), - gasUsed: tx.gasUsed != null ? BigInt(tx.gasUsed) : null, + gasLimit: BigInt(tx.gasLimit ?? "0"), + meterBundleResponse: tx.meterBundleResponse ?? null, }), ), } as BlockData;