The event interest list was an allowlist of two entries, which meant every capability the chain grew was invisible until somebody remembered to add a line — the exact failure the metadata-oracle approach exists to avoid. It is a denylist now. Nothing is excluded for being unused. Whether a capability is exercised is the question an analyst is asking: a chain that ships vesting and governance nobody touches is telling you something, and it can only tell you that if the silence is recorded rather than filtered on the way in. The rule for exclusion is narrower than volume and narrower than usefulness — an event carrying no account can never answer "everything involving this account", which is what the index is for. Three kinds both name no account and fire every block, so they are skipped: ZkTree::LeafInserted, QPoW::DifficultyAdjusted, System::ExtrinsicSuccess. They render as "not indexed" rather than as a zero, because a zero reads as disuse. Balances::Minted looked like a fourth. It fired exactly as often as MinerRewarded across a 120-block sample and for the same reason, but it names an account, minting is not only for miners, and showing a reward twice on one page is a presentation problem to solve on that page rather than a reason to lose the record. Kept. `/:chain/event` is the call index's other half — 19 of 107 kinds fired — and `/:chain/event/:pallet/:variant` the feed behind a row. Measured on live mainnet the widening took the index from ~1.2 to ~3.4 rows per block and bought 739 accounts' worth of wormhole transfers that were previously invisible. Signatures also lose their associated-type ceremony: the runtime writes `<<T as frame_system::Config>::Lookup as StaticLookup>::Source`, which is correct and forty characters of scaffolding around one word, and three of those in a row pushed the counts off the side of the table. Generics are untouched — `BalanceOf<T>` is information. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
/**
|
|
* Every firing of one kind of event.
|
|
*
|
|
* The feed behind a row of the event index: which block, which accounts, what
|
|
* fields. Rendered through the same `Payload` as every other decoded value, so
|
|
* a pallet added next year has a working page the first time it emits.
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
|
|
import type { AccountEvent } from '../api/generated/AccountEvent'
|
|
import { RequestFailed, fetchEventFeed } from '../api/rest'
|
|
import { height as fmtHeight, when } from '../lib/format'
|
|
import { href } from '../lib/routes'
|
|
import { Payload } from './Payload'
|
|
|
|
export function EventPanel({
|
|
chain,
|
|
pallet,
|
|
variant,
|
|
decimals,
|
|
symbol,
|
|
}: {
|
|
chain: string
|
|
pallet: string
|
|
variant: string
|
|
decimals: number
|
|
symbol: string
|
|
}) {
|
|
const [rows, setRows] = useState<AccountEvent[] | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [before, setBefore] = useState<number | null>(null)
|
|
|
|
useEffect(() => {
|
|
const controller = new AbortController()
|
|
setRows(null)
|
|
setError(null)
|
|
fetchEventFeed(chain, pallet, variant, before ?? undefined, controller.signal)
|
|
.then(setRows)
|
|
.catch((e: unknown) => {
|
|
if (controller.signal.aborted) return
|
|
setError(e instanceof RequestFailed ? e.message : 'Could not reach the observer.')
|
|
})
|
|
return () => controller.abort()
|
|
}, [chain, pallet, variant, before])
|
|
|
|
const oldest = rows && rows.length > 0 ? rows[rows.length - 1]!.height : null
|
|
|
|
return (
|
|
<section className="panel" style={{ marginBottom: 26 }}>
|
|
<div className="panel-head">
|
|
<div>
|
|
<div className="eyebrow">Event</div>
|
|
<h2 className="panel-title" style={{ marginTop: 4, textTransform: 'none' }}>
|
|
<code>
|
|
{pallet}::{variant}
|
|
</code>
|
|
</h2>
|
|
</div>
|
|
<Link className="segmented panel-button" to={href({ chain, index: 'event' })}>
|
|
All events
|
|
</Link>
|
|
</div>
|
|
|
|
{error && <p className="empty">{error}</p>}
|
|
{!error && rows === null && <p className="empty">Reading the record…</p>}
|
|
{rows !== null && rows.length === 0 && (
|
|
<p className="empty">
|
|
This event has not fired in the indexed range. The runtime declares it and the chain has
|
|
not reached for it.
|
|
</p>
|
|
)}
|
|
|
|
{rows !== null && rows.length > 0 && (
|
|
<div className="scroll-x">
|
|
<table className="board">
|
|
<caption className="visually-hidden">
|
|
Every firing of {pallet}::{variant}, newest first
|
|
</caption>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Block</th>
|
|
<th scope="col" className="left">
|
|
Fields
|
|
</th>
|
|
<th scope="col" className="event-when">
|
|
When
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((e) => (
|
|
<tr key={`${e.height}-${e.index}`}>
|
|
<td className="numeral">
|
|
<Link to={href({ chain, block: String(e.height) })}>
|
|
#{fmtHeight(e.height)}
|
|
</Link>
|
|
</td>
|
|
<td className="left">
|
|
<Payload
|
|
value={e.fields}
|
|
ctx={{ chain, addresses: e.addresses, decimals, symbol }}
|
|
/>
|
|
</td>
|
|
<td className="numeral event-when" style={{ color: 'var(--text-muted)' }}>
|
|
{when(e.at)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
<div className="pager">
|
|
<button
|
|
className="segmented panel-button"
|
|
disabled={before === null}
|
|
onClick={() => setBefore(null)}
|
|
>
|
|
← Newest
|
|
</button>
|
|
<button
|
|
className="segmented panel-button"
|
|
disabled={oldest === null || oldest <= 1}
|
|
onClick={() => setBefore(oldest)}
|
|
>
|
|
Older →
|
|
</button>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|