feat: a runtime's own page
`/quantus/runtime/152` — every pallet, call, event, error, storage entry and constant the runtime declares, with the constants' values decoded against their own declared types. Nothing transcribed from a source tree: a source tree says what the chain should be running, metadata says what it ran. The set of runtimes is found rather than waited for. Decoding caches one the moment it needs it, so left alone the cache is "whatever the backfill has walked past" — days on a chain a million blocks deep. `spec_version` only increases, so the boundaries are a sorted sequence and bisection finds each in log₂(height) probes. On Heisenberg that turned up six runtimes and the block each took over at: v126 from 1, v128 from 132, v131 from 342,813, v136 from 669,129, v144 from 812,055, v148 from 977,079 — two more than the four upgrade boundaries this approach was originally verified against. Discovery is its own task, not a poll step. `state_getRuntimeVersion` at an old block makes the node instantiate the runtime WASM from that block's state: ~4 s against Heisenberg's endpoint versus ~0.25 s at the tip, and a hundred of those inside a four-second loop stalls difficulty and the summary broadcast for minutes on every start. Reading Heisenberg's two ends side by side is the case for the page. Between v126 and v148 the chain dropped Referenda, ConvictionVoting, Recovery, Assets and AssetsHolder, added Vesting and Origins, gained `WeightReclaim` and moved `ChargeTransactionPayment` after the two Quantus-specific extensions. Every one breaks a decoder written against the other version and none is visible from a block. `U256`/`U512` now render as one decimal rather than four or eight little-endian limbs, identified by registry path exactly as `AccountId32` already was. Difficulty as `[1189189, 0, 0, 0, 0, 0, 0, 0]` describes the bytes correctly and tells a reader nothing. `ChainSummary` gained `spec_version`/`spec_name` so the footer can say what the site is decoding against, and link to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* One runtime, described by itself.
|
||||
*
|
||||
* Everything on this page came out of metadata the chain produced —
|
||||
* `state_getMetadata` executed against the runtime WASM in a block's own state
|
||||
* — and none of it is transcribed from a source tree. That is the distinction
|
||||
* worth having: a source tree says what the chain *should* be running, and this
|
||||
* says what it *was*, at a height, for a runtime that may have been upgraded
|
||||
* past months ago.
|
||||
*
|
||||
* Nothing here is written per pallet. A runtime that grew a pallet last week
|
||||
* renders the same way as `System`, because both describe themselves in the
|
||||
* same registry.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import type { RuntimeDetail } from '../api/generated/RuntimeDetail'
|
||||
import type { RuntimePallet } from '../api/generated/RuntimePallet'
|
||||
import type { RuntimeSummary } from '../api/generated/RuntimeSummary'
|
||||
import type { RuntimeVariant } from '../api/generated/RuntimeVariant'
|
||||
import { RequestFailed, fetchRuntime, fetchRuntimes } from '../api/rest'
|
||||
import { height as fmtHeight, when } from '../lib/format'
|
||||
import { href } from '../lib/routes'
|
||||
|
||||
/** `3 calls`, `1 call`, or nothing at all for none. */
|
||||
function count(n: number, noun: string): string | null {
|
||||
if (n === 0) return null
|
||||
return `${n} ${noun}${n === 1 ? '' : 's'}`
|
||||
}
|
||||
|
||||
/** A variant's signature, the way the runtime's own source would write it. */
|
||||
function signature(v: RuntimeVariant): string {
|
||||
if (v.fields.length === 0) return v.name
|
||||
const args = v.fields.map((f) => (f.name ? `${f.name}: ${f.type_name}` : f.type_name))
|
||||
return `${v.name}(${args.join(', ')})`
|
||||
}
|
||||
|
||||
/** The first line of a doc comment, which is the summary line by convention. */
|
||||
function summary(docs: string[]): string | null {
|
||||
const first = docs.find((d) => d.trim().length > 0)
|
||||
return first ? first.trim() : null
|
||||
}
|
||||
|
||||
function Variants({ title, variants }: { title: string; variants: RuntimeVariant[] }) {
|
||||
if (variants.length === 0) return null
|
||||
return (
|
||||
<div className="runtime-group">
|
||||
<div className="eyebrow">
|
||||
{title} ({variants.length})
|
||||
</div>
|
||||
<ul className="runtime-list">
|
||||
{variants.map((v) => (
|
||||
<li key={`${title}-${v.index}`}>
|
||||
{/* The index is the byte on the wire, and the reason a call encoded
|
||||
against one runtime does not decode against another. */}
|
||||
<span className="runtime-index">{v.index}</span>
|
||||
<code>{signature(v)}</code>
|
||||
{summary(v.docs) && <span className="runtime-doc">{summary(v.docs)}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Pallet({ pallet }: { pallet: RuntimePallet }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const counts = [
|
||||
count(pallet.calls.length, 'call'),
|
||||
count(pallet.events.length, 'event'),
|
||||
count(pallet.errors.length, 'error'),
|
||||
count(pallet.constants.length, 'constant'),
|
||||
// "storage" is already the plural of itself here — one storage entry, five
|
||||
// storage entries — so it takes the noun rather than an s.
|
||||
pallet.storage.length ? `${pallet.storage.length} storage` : null,
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<section className="runtime-pallet">
|
||||
<button className="runtime-pallet-head" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
<span className="runtime-index">{pallet.index}</span>
|
||||
<span className="runtime-pallet-name">{pallet.name}</span>
|
||||
<span className="runtime-counts">{counts.join(' · ') || 'nothing declared'}</span>
|
||||
<span className="runtime-chevron" aria-hidden="true">
|
||||
{open ? '−' : '+'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="runtime-pallet-body">
|
||||
{pallet.constants.length > 0 && (
|
||||
<div className="runtime-group">
|
||||
{/* The half most worth reading: these are numbers somebody chose,
|
||||
compiled into the runtime, and reading them out of the chain
|
||||
beats reading them out of a source tree that may not be the
|
||||
one this runtime was built from. */}
|
||||
<div className="eyebrow">Constants ({pallet.constants.length})</div>
|
||||
<ul className="runtime-list">
|
||||
{pallet.constants.map((c) => (
|
||||
<li key={c.name}>
|
||||
<code>{c.name}</code>
|
||||
<span className="runtime-value">
|
||||
{c.value === null || c.value === undefined
|
||||
? '—'
|
||||
: typeof c.value === 'object'
|
||||
? JSON.stringify(c.value)
|
||||
: String(c.value)}
|
||||
</span>
|
||||
<span className="runtime-type">{c.type_name}</span>
|
||||
{summary(c.docs) && <span className="runtime-doc">{summary(c.docs)}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Variants title="Calls" variants={pallet.calls} />
|
||||
<Variants title="Events" variants={pallet.events} />
|
||||
|
||||
{pallet.storage.length > 0 && (
|
||||
<div className="runtime-group">
|
||||
<div className="eyebrow">Storage ({pallet.storage.length})</div>
|
||||
<ul className="runtime-list">
|
||||
{pallet.storage.map((st) => (
|
||||
<li key={st.name}>
|
||||
<code>{st.name}</code>
|
||||
<span className="runtime-type">{st.shape}</span>
|
||||
<span className="runtime-modifier">{st.modifier}</span>
|
||||
{summary(st.docs) && <span className="runtime-doc">{summary(st.docs)}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Variants title="Errors" variants={pallet.errors} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function RuntimePanel({ chain, spec }: { chain: string; spec: number }) {
|
||||
const [detail, setDetail] = useState<RuntimeDetail | null>(null)
|
||||
const [all, setAll] = useState<RuntimeSummary[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
setDetail(null)
|
||||
setError(null)
|
||||
fetchRuntime(chain, spec, controller.signal)
|
||||
.then(setDetail)
|
||||
.catch((e: unknown) => {
|
||||
if (controller.signal.aborted) return
|
||||
setError(e instanceof RequestFailed ? e.message : 'Could not reach the observer.')
|
||||
})
|
||||
return () => controller.abort()
|
||||
}, [chain, spec])
|
||||
|
||||
// The list is a separate request because it does not change when the
|
||||
// selected runtime does — the strip stays put while the body swaps.
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
fetchRuntimes(chain, controller.signal)
|
||||
.then(setAll)
|
||||
.catch(() => {
|
||||
// The strip is navigation, not content. Its absence costs nothing the
|
||||
// page below does not still say.
|
||||
})
|
||||
return () => controller.abort()
|
||||
}, [chain])
|
||||
|
||||
return (
|
||||
<section className="panel" style={{ marginBottom: 26 }}>
|
||||
<div className="panel-head">
|
||||
<div>
|
||||
<div className="eyebrow">Runtime</div>
|
||||
<h2 className="panel-title" style={{ marginTop: 4 }}>
|
||||
{detail ? `${detail.summary.spec_name} v${detail.summary.spec_version}` : `v${spec}`}
|
||||
</h2>
|
||||
{detail && (
|
||||
<dl className="identifiers">
|
||||
<dt>In force from</dt>
|
||||
<dd className="numeral">
|
||||
<Link to={href({ chain, block: String(detail.summary.first_seen_height) })}>
|
||||
#{fmtHeight(detail.summary.first_seen_height)}
|
||||
</Link>
|
||||
</dd>
|
||||
<dt>Metadata</dt>
|
||||
<dd className="numeral">
|
||||
v{detail.metadata_version} · {detail.types} types ·{' '}
|
||||
{Math.round(detail.summary.bytes / 1024)} KiB · read{' '}
|
||||
{when(detail.summary.fetched_at)}
|
||||
</dd>
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{all.length > 1 && (
|
||||
<nav className="runtime-strip" aria-label="Runtime version">
|
||||
{all.map((r) => (
|
||||
<Link
|
||||
key={r.spec_version}
|
||||
to={href({ chain, runtime: r.spec_version })}
|
||||
className={r.spec_version === spec ? 'chain-chip is-active' : 'chain-chip'}
|
||||
aria-current={r.spec_version === spec ? 'page' : undefined}
|
||||
title={`In force from block ${fmtHeight(r.first_seen_height)}`}
|
||||
>
|
||||
<span className="chain-name">v{r.spec_version}</span>
|
||||
<span className="chain-nodes">#{fmtHeight(r.first_seen_height)}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{error && <p className="empty">{error}</p>}
|
||||
{!error && !detail && <p className="empty">Reading the runtime…</p>}
|
||||
|
||||
{detail && (
|
||||
<>
|
||||
<div className="runtime-group runtime-envelope">
|
||||
{/* The extrinsic envelope, which is where this chain differs from
|
||||
vanilla Substrate and the reason a hand-written transaction
|
||||
decoder does not survive an upgrade. Two of these exist nowhere
|
||||
in Substrate: ReversibleTransactionExtension and
|
||||
WormholeProofRecorderExtension. */}
|
||||
<div className="eyebrow">
|
||||
Extrinsic envelope (v{detail.extrinsic_version}) — {detail.signed_extensions.length}{' '}
|
||||
signed extensions, in the order applied
|
||||
</div>
|
||||
<ol className="runtime-list runtime-extensions">
|
||||
{detail.signed_extensions.map((e, i) => (
|
||||
<li key={`${e.identifier}-${i}`}>
|
||||
<span className="runtime-index">{i}</span>
|
||||
<code>{e.identifier}</code>
|
||||
{e.additional !== '()' && (
|
||||
<span className="runtime-type">signs over {e.additional}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="runtime-pallets">
|
||||
{detail.pallets.map((p) => (
|
||||
<Pallet key={p.index} pallet={p} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="panel-note">
|
||||
Read from the runtime's own metadata — the type registry the node produces by executing
|
||||
this runtime's WASM against a block in its own state. Nothing here is transcribed from a
|
||||
source tree, so it describes what the chain ran rather than what a repository says it
|
||||
should have. Pallet and variant indexes are the bytes on the wire: a pallet inserted
|
||||
rather than appended renumbers the ones after it, which is what makes a decoder written
|
||||
against one runtime wrong about another.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user