fix: show a block's events, joined to the extrinsics that caused them
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user