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:
@@ -80,6 +80,16 @@ finalized_height: number | null,
|
||||
* genuinely interesting statistic a chain with no RPC still yields.
|
||||
*/
|
||||
client_versions: Array<ClientVersion>,
|
||||
/**
|
||||
* The `spec_version` of the runtime currently in force, when this observer
|
||||
* has read one. Everything on the site is decoded against it, so it is the
|
||||
* answer to "decoded how?" and the way in to the runtime's own page.
|
||||
*/
|
||||
spec_version: number,
|
||||
/**
|
||||
* Its `spec_name`, e.g. `quantus-runtime`.
|
||||
*/
|
||||
spec_name: string | null,
|
||||
/**
|
||||
* When these numbers were computed.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* A constant and the value the runtime carries for it.
|
||||
*/
|
||||
export type RuntimeConstant = {
|
||||
/**
|
||||
* Constant name.
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* Its type.
|
||||
*/
|
||||
type_name: string,
|
||||
/**
|
||||
* The decoded value. `null` if it did not decode against its own declared
|
||||
* type, which would be a runtime describing itself wrongly and is worth
|
||||
* showing as a gap rather than hiding.
|
||||
*/
|
||||
value: unknown,
|
||||
/**
|
||||
* Documentation carried in the metadata.
|
||||
*/
|
||||
docs: Array<string>, };
|
||||
@@ -0,0 +1,45 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ChainId } from "./ChainId";
|
||||
import type { RuntimePallet } from "./RuntimePallet";
|
||||
import type { RuntimeSignedExtension } from "./RuntimeSignedExtension";
|
||||
import type { RuntimeSummary } from "./RuntimeSummary";
|
||||
|
||||
/**
|
||||
* Everything discoverable about one runtime.
|
||||
*/
|
||||
export type RuntimeDetail = {
|
||||
/**
|
||||
* Which chain it was cached from.
|
||||
*/
|
||||
chain: ChainId,
|
||||
/**
|
||||
* The row this expands.
|
||||
*/
|
||||
summary: RuntimeSummary,
|
||||
/**
|
||||
* Metadata format version — 14 on every Quantus runtime seen so far.
|
||||
*/
|
||||
metadata_version: number,
|
||||
/**
|
||||
* Extrinsic format version — 4, the standard envelope.
|
||||
*/
|
||||
extrinsic_version: number,
|
||||
/**
|
||||
* How many distinct types the registry describes. The one number that
|
||||
* moves visibly between upgrades, and a rough measure of how much runtime
|
||||
* there is.
|
||||
*/
|
||||
types: number,
|
||||
/**
|
||||
* Every pallet, in declaration order.
|
||||
*/
|
||||
pallets: Array<RuntimePallet>,
|
||||
/**
|
||||
* The extrinsic envelope's signed extensions, in the order applied.
|
||||
*
|
||||
* The reason a hand-written transaction decoder does not survive an
|
||||
* upgrade, and where this chain's deviations from vanilla Substrate are
|
||||
* visible: `ReversibleTransactionExtension` and
|
||||
* `WormholeProofRecorderExtension` sit in this list and nowhere else.
|
||||
*/
|
||||
signed_extensions: Array<RuntimeSignedExtension>, };
|
||||
@@ -0,0 +1,15 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One field of a variant.
|
||||
*/
|
||||
export type RuntimeField = {
|
||||
/**
|
||||
* `null` for a tuple variant's positional fields.
|
||||
*/
|
||||
name: string | null,
|
||||
/**
|
||||
* The type, as the runtime wrote it where it says so, else rendered from
|
||||
* the registry.
|
||||
*/
|
||||
type_name: string, };
|
||||
@@ -0,0 +1,39 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RuntimeConstant } from "./RuntimeConstant";
|
||||
import type { RuntimeStorage } from "./RuntimeStorage";
|
||||
import type { RuntimeVariant } from "./RuntimeVariant";
|
||||
|
||||
/**
|
||||
* One pallet and everything it declares.
|
||||
*/
|
||||
export type RuntimePallet = {
|
||||
/**
|
||||
* The pallet's index, which is the first byte of every call and event it
|
||||
* encodes — so a pallet inserted rather than appended renumbers the ones
|
||||
* after it and breaks anything holding the old numbers.
|
||||
*/
|
||||
index: number,
|
||||
/**
|
||||
* Its name, as the runtime declares it.
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* Dispatchable calls.
|
||||
*/
|
||||
calls: Array<RuntimeVariant>,
|
||||
/**
|
||||
* Events it can emit.
|
||||
*/
|
||||
events: Array<RuntimeVariant>,
|
||||
/**
|
||||
* Errors its calls can return.
|
||||
*/
|
||||
errors: Array<RuntimeVariant>,
|
||||
/**
|
||||
* Constants, with the values this runtime was compiled with.
|
||||
*/
|
||||
constants: Array<RuntimeConstant>,
|
||||
/**
|
||||
* Storage entries it owns.
|
||||
*/
|
||||
storage: Array<RuntimeStorage>, };
|
||||
@@ -0,0 +1,19 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One signed extension in the extrinsic envelope.
|
||||
*/
|
||||
export type RuntimeSignedExtension = {
|
||||
/**
|
||||
* The extension's identifier, e.g. `CheckMortality`.
|
||||
*/
|
||||
identifier: string,
|
||||
/**
|
||||
* What it contributes to the extrinsic itself.
|
||||
*/
|
||||
type_name: string,
|
||||
/**
|
||||
* What it contributes to the signed payload but not to the extrinsic —
|
||||
* `()` for most, a genesis hash or a spec version for the checks.
|
||||
*/
|
||||
additional: string, };
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One storage entry.
|
||||
*/
|
||||
export type RuntimeStorage = {
|
||||
/**
|
||||
* Entry name.
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* The value's type for a plain entry, `Key → Value` for a map.
|
||||
*/
|
||||
shape: string,
|
||||
/**
|
||||
* `optional` or `default`.
|
||||
*/
|
||||
modifier: string,
|
||||
/**
|
||||
* Documentation carried in the metadata.
|
||||
*/
|
||||
docs: Array<string>, };
|
||||
@@ -0,0 +1,33 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One runtime version, as a row in a list.
|
||||
*/
|
||||
export type RuntimeSummary = {
|
||||
/**
|
||||
* The runtime's `spec_version` — the number that has to change for a
|
||||
* runtime upgrade to take effect, and the key everything here is filed
|
||||
* under.
|
||||
*/
|
||||
spec_version: number,
|
||||
/**
|
||||
* Its `spec_name`, e.g. `quantus-runtime`.
|
||||
*/
|
||||
spec_name: string,
|
||||
/**
|
||||
* The first block this runtime is known to have been in force at.
|
||||
*
|
||||
* The upgrade height when discovery bisected the boundary, and otherwise a
|
||||
* *lower bound* — the earliest block the observer happened to decode
|
||||
* against it. The distinction is worth keeping: one is a fact about the
|
||||
* chain and the other is a fact about this observer.
|
||||
*/
|
||||
first_seen_height: number,
|
||||
/**
|
||||
* When the metadata was fetched and cached.
|
||||
*/
|
||||
fetched_at: string,
|
||||
/**
|
||||
* Size of the metadata blob.
|
||||
*/
|
||||
bytes: number, };
|
||||
@@ -0,0 +1,23 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RuntimeField } from "./RuntimeField";
|
||||
|
||||
/**
|
||||
* One call, event or error variant.
|
||||
*/
|
||||
export type RuntimeVariant = {
|
||||
/**
|
||||
* Index within its enum, which is the byte on the wire.
|
||||
*/
|
||||
index: number,
|
||||
/**
|
||||
* Variant name.
|
||||
*/
|
||||
name: string,
|
||||
/**
|
||||
* Its fields, named where the runtime names them.
|
||||
*/
|
||||
fields: Array<RuntimeField>,
|
||||
/**
|
||||
* Documentation carried in the metadata from the runtime's source.
|
||||
*/
|
||||
docs: Array<string>, };
|
||||
@@ -11,6 +11,8 @@ import type { ApiError } from './generated/ApiError'
|
||||
import type { BlockDetail } from './generated/BlockDetail'
|
||||
import type { ChainSeries } from './generated/ChainSeries'
|
||||
import type { MinerDetail } from './generated/MinerDetail'
|
||||
import type { RuntimeDetail } from './generated/RuntimeDetail'
|
||||
import type { RuntimeSummary } from './generated/RuntimeSummary'
|
||||
import type { Window as WindowName } from './generated/Window'
|
||||
|
||||
const BASE = import.meta.env.VITE_API_BASE_URL ?? '/v1'
|
||||
@@ -100,6 +102,26 @@ export function fetchAccount(
|
||||
)
|
||||
}
|
||||
|
||||
/** Every runtime this chain has been seen running, oldest first. */
|
||||
export function fetchRuntimes(chain: string, signal?: AbortSignal): Promise<RuntimeSummary[]> {
|
||||
return get<RuntimeSummary[]>(`/chains/${encodeURIComponent(chain)}/runtimes`, signal)
|
||||
}
|
||||
|
||||
/**
|
||||
* One runtime, described by itself.
|
||||
*
|
||||
* Served from the cached metadata rather than the node — which is the whole
|
||||
* reason it was cached: a runtime's description outlives the state the node
|
||||
* would need to regenerate it.
|
||||
*/
|
||||
export function fetchRuntime(
|
||||
chain: string,
|
||||
spec: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RuntimeDetail> {
|
||||
return get<RuntimeDetail>(`/chains/${encodeURIComponent(chain)}/runtimes/${spec}`, signal)
|
||||
}
|
||||
|
||||
/** One miner's standing and history. */
|
||||
export function fetchMiner(
|
||||
chain: string,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1039,3 +1039,155 @@ tr.mine .share-bar > i {
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ---- runtime ------------------------------------------------------------- */
|
||||
|
||||
/* The version strip. Reuses the chain chip: it is the same gesture — a row of
|
||||
short labels where one is current — and a second visual language for it
|
||||
would say the two are different sorts of choice. */
|
||||
.runtime-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: var(--rule);
|
||||
}
|
||||
|
||||
.runtime-pallet {
|
||||
border-bottom: var(--rule);
|
||||
}
|
||||
|
||||
.runtime-pallets > .runtime-pallet:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* A whole-row target. A pallet's name is not the only part of the row worth
|
||||
clicking — the counts beside it are what tells you whether opening it is
|
||||
worth doing. */
|
||||
.runtime-pallet-head {
|
||||
display: grid;
|
||||
grid-template-columns: 34px 1fr auto 20px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
padding: 11px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.runtime-pallet-head:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
|
||||
.runtime-pallet-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* The pallet index, and the variant indexes inside. Monospace and tabular
|
||||
because they are the bytes on the wire, not decoration: reading down the
|
||||
column is how you see that a pallet was inserted rather than appended. */
|
||||
.runtime-index {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
background: var(--surface-3);
|
||||
padding: 1px 6px;
|
||||
text-align: right;
|
||||
min-width: 26px;
|
||||
}
|
||||
|
||||
.runtime-counts {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.runtime-chevron {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.runtime-pallet-body {
|
||||
padding: 4px 16px 18px;
|
||||
background: var(--surface-1);
|
||||
}
|
||||
|
||||
.runtime-group {
|
||||
padding: 12px 0 4px;
|
||||
}
|
||||
|
||||
.runtime-envelope {
|
||||
padding: 14px 16px;
|
||||
border-bottom: var(--rule);
|
||||
}
|
||||
|
||||
.runtime-list {
|
||||
list-style: none;
|
||||
margin: 8px 0 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Flow rather than a grid: the parts of a line are a name, sometimes a value,
|
||||
sometimes a type and sometimes a sentence, and a column per possible part
|
||||
would be four columns mostly empty. */
|
||||
.runtime-list li {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 4px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.runtime-list code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* The compiled-in value, in the data hue: it is the one thing on the line that
|
||||
is a fact about this runtime rather than about its shape. */
|
||||
.runtime-value {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--data-bright);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.runtime-type {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* `optional` or `default`, which is a property of the entry and belongs on its
|
||||
line rather than under it. */
|
||||
.runtime-modifier {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Documentation the runtime carries. Given the whole remaining width so it
|
||||
wraps as prose, because that is what it is. */
|
||||
.runtime-doc {
|
||||
flex-basis: 100%;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.runtime-extensions {
|
||||
counter-reset: none;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface Route {
|
||||
miner: string | null
|
||||
/** An SS58 address. */
|
||||
account: string | null
|
||||
/** A `spec_version`. */
|
||||
runtime: number | null
|
||||
}
|
||||
|
||||
export const EMPTY: Route = {
|
||||
@@ -46,12 +48,25 @@ export const EMPTY: Route = {
|
||||
block: null,
|
||||
miner: null,
|
||||
account: null,
|
||||
runtime: null,
|
||||
}
|
||||
|
||||
function asWindow(segment: string | undefined): WindowName | null {
|
||||
return WINDOWS.find((w) => w.id === segment)?.id ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* A `spec_version`, which is a bare number.
|
||||
*
|
||||
* `null` rather than `NaN` for anything else, so a mistyped runtime falls
|
||||
* through to the front page like every other unrecognised path rather than
|
||||
* rendering a panel that asks the API about `NaN`.
|
||||
*/
|
||||
function spec(segment: string): number | null {
|
||||
const n = Number(segment.replace(/^v/, ''))
|
||||
return Number.isInteger(n) && n >= 0 ? n : null
|
||||
}
|
||||
|
||||
/** A block number, or a 32-byte hash. */
|
||||
export function isBlockRef(segment: string): boolean {
|
||||
return /^\d+$/.test(segment) || /^0x[0-9a-f]{64}$/i.test(segment)
|
||||
@@ -74,6 +89,11 @@ export function parse(pathname: string): Route {
|
||||
if (parts[0] === 'miner' && parts[1]) {
|
||||
return { ...EMPTY, miner: parts[1].toLowerCase() }
|
||||
}
|
||||
// Likewise a bare runtime: a spec_version is only meaningful against a chain,
|
||||
// and the default chain is the one a link without one means.
|
||||
if (parts[0] === 'runtime' && parts[1]) {
|
||||
return { ...EMPTY, runtime: spec(parts[1]) }
|
||||
}
|
||||
|
||||
const chain = parts[0] ?? null
|
||||
if (!chain) return EMPTY
|
||||
@@ -84,6 +104,7 @@ export function parse(pathname: string): Route {
|
||||
if (kind === 'block') return { ...EMPTY, chain, block: value.toLowerCase() }
|
||||
if (kind === 'miner') return { ...EMPTY, chain, miner: value.toLowerCase() }
|
||||
if (kind === 'account') return { ...EMPTY, chain, account: value }
|
||||
if (kind === 'runtime') return { ...EMPTY, chain, runtime: spec(value) }
|
||||
}
|
||||
// A bare height or hash as the second segment: the shape the site used
|
||||
// before this module existed. Recognised so links already sent stay good.
|
||||
@@ -98,6 +119,9 @@ export function href(route: Partial<Route>): string {
|
||||
if (route.block) return `/${chain}/block/${route.block}`
|
||||
if (route.miner) return chain ? `/${chain}/miner/${route.miner}` : `/miner/${route.miner}`
|
||||
if (route.account) return `/${chain}/account/${route.account}`
|
||||
if (route.runtime !== null && route.runtime !== undefined) {
|
||||
return chain ? `/${chain}/runtime/${route.runtime}` : `/runtime/${route.runtime}`
|
||||
}
|
||||
return `/${chain}/${route.window ?? DEFAULT_WINDOW}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user