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;