fix: show a block's events, joined to the extrinsics that caused them
deploy / build (push) Successful in 9m18s
deploy / deploy-web (push) Successful in 4s
deploy / deploy-api (push) Successful in 14s

The block page listed extrinsics and nothing else. Block 1 holds twenty-eight
indexed events and the route returned none of them — visible from the outside,
because an account page linked to a `Wormhole::NativeTransferred` at block 1 and
block 1 showed no sign of it.

`phase` and `extrinsic_index` were stored for exactly this join and had never
been used. Each extrinsic now carries its own events beneath it, and the block's
own events sit in their phases.

Block 1 is the argument for doing it that way. As extrinsics alone it is a
single `Timestamp::set`. Its events are the entire opening distribution:
twenty-one `Wormhole::NativeTransferred` in `Initialization`, owned by no
extrinsic at all and sent from the minting account — not block zero, and not
anything a transaction did. `Vesting::LaunchMomentSet` fires there too, so
vesting has been exercised despite the call index showing zero `Vesting::*`
dispatched: a pallet the runtime uses looks unused from the call side alone.

Closes #3

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
2026-09-10 09:14:56 +03:00
co-authored by Claude Opus 5
parent dd8086df0d
commit 88086552cb
10 changed files with 337 additions and 6 deletions
+72 -5
View File
@@ -20,6 +20,7 @@ import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import type { BlockDetail } from '../api/generated/BlockDetail'
import type { BlockEvent } from '../api/generated/BlockEvent'
import { RequestFailed, fetchBlock } from '../api/rest'
import {
ago,
@@ -60,6 +61,36 @@ function Unknown({ why }: { why: string }) {
return <span className="fact-unknown">not recorded — {why}</span>
}
/** A run of events, each as `Pallet::Variant` and its decoded fields. */
function EventList({
events,
chain,
decimals,
symbol,
wide,
}: {
events: BlockEvent[]
chain: string
decimals: number
symbol: string
wide?: boolean
}) {
if (events.length === 0) return null
return (
<ul className={wide ? 'runtime-list' : 'runtime-list block-event-list'}>
{events.map((e) => (
<li key={e.index}>
<span className="runtime-index">{e.index}</span>
<code>
{e.pallet}::{e.variant}
</code>
<Payload value={e.fields} ctx={{ chain, addresses: e.addresses, decimals, symbol }} />
</li>
))}
</ul>
)
}
export function BlockPanel({
chain,
block,
@@ -232,11 +263,17 @@ export function BlockPanel({
</Fact>
</div>
{/* The body, decoded. The count in the facts above comes from the
node and goes when the node forgets the block; these come from
this observer's index and stay. An empty list is therefore not
"no extrinsics" — a block always has at least a timestamp — it is
"the index has not read this block yet", and it says so. */}
{/* The body, decoded, with each extrinsic's own events beneath it.
An event's `phase` says whether it belongs to an extrinsic or to
the block, and `extrinsic_index` says which — that join is what
turns "what was asked" and "what happened" into one readable
block, and it is why those two columns are stored.
The count in the facts above comes from the node and goes when the
node forgets the block; these come from this observer's index and
stay. An empty list is therefore not "no extrinsics" — a block
always has at least a timestamp — it is "the index has not read
this block yet", and it says so. */}
<div className="runtime-group" style={{ padding: '14px 16px 4px' }}>
<div className="eyebrow">
Extrinsics{detail.body.length > 0 && ` (${detail.body.length})`}
@@ -306,6 +343,18 @@ export function BlockPanel({
value={x.args}
ctx={{ chain, addresses: x.addresses, decimals, symbol }}
/>
{/* What this dispatch actually caused. Under the
call rather than in a table of its own, because
an event's meaning is mostly which extrinsic
produced it. */}
<EventList
events={detail.events.filter(
(e) => e.phase === 'ApplyExtrinsic' && e.extrinsic_index === x.index,
)}
chain={chain}
decimals={decimals}
symbol={symbol}
/>
</td>
<td
className="numeral"
@@ -334,6 +383,24 @@ export function BlockPanel({
<GenesisPanel chain={chain} decimals={decimals} symbol={symbol} />
)}
{/* Events owned by the block rather than by any extrinsic. Easy to
assume these do not exist — block 1 on this chain sends its entire
opening distribution as twenty-one wormhole transfers in
`Initialization`, owned by no extrinsic at all, and a page showing
only extrinsics shows none of it. */}
{(['Initialization', 'Finalization'] as const).map((phase) => {
const events = detail.events.filter((e) => e.phase === phase)
if (events.length === 0) return null
return (
<div key={phase} className="runtime-group" style={{ padding: '14px 16px 4px' }}>
<div className="eyebrow">
{phase} ({events.length})
</div>
<EventList events={events} chain={chain} decimals={decimals} symbol={symbol} wide />
</div>
)
})}
<div className="block-links">
{detail.parent_hash && (
<div>