feat: mark the accounts a chain names, and make genesis unmissable
Some accounts are special and nothing about the address said so. The treasury looked like any other empty account, the minting sentinel looked like a wallet, and the twenty-one accounts endowed at genesis looked like they earned it. No curated list. The chain names them itself, in three places that are three different claims and are not flattened into one badge: a runtime constant (compiled in, immutable for that spec_version), a storage value (assigned, and changeable), and a balance in block zero (history, and not a role — an account can be endowed and have no job). Labels derive from the chain's own name, so a pallet added next year is labelled without an edit. The chip is the claim; the citation beneath it is the evidence. Two rules hold it together, both learned the hard way and both now in CLAUDE.md. An account is told from a hash by registry path, never by shape — after `normalise` both are `0x` and sixty-four hex characters, so `decode_typed` now returns the account set the decoder collected while it still knew. And a storage entry names an account only if its value *is* one: `System::Events` is full of accounts and names none, and the first cut labelled half the chain's active addresses `Events`. Computing all of this walks every plain storage entry and enumerates block zero — dozens of round trips, fine once and pathological on every account page view — so it is cached for five minutes. Constants change with the runtime, state assignments almost never, genesis never. Block zero gets the endowments, because they have no extrinsic and no event and are invisible to anyone who has not read the chainspec, and a Genesis link sits in the nav to give somebody who would never think to look an unmissable way to. Mainnet started with 5,670,000 QTC across 21 accounts — one holds 5,669,940 and the other twenty got 3 each — shown beside what each holds today, so what a founding account did with its stake is one row. When genesis state cannot be read the page says so rather than showing an empty table: missing data and a chain that endowed nobody are different claims. Closes #2 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jp6a8EDar9ueEhAxzep4V5
This commit is contained in:
@@ -23,6 +23,7 @@ import { RequestFailed, fetchAccount } from '../api/rest'
|
||||
import { exactTokens, height as fmtHeight, shortAddress, tokens, when } from '../lib/format'
|
||||
import { href } from '../lib/routes'
|
||||
import { Payload } from './Payload'
|
||||
import { RoleChips } from './RoleChips'
|
||||
|
||||
/** Human labels for what the site sees most. Anything else shows its own
|
||||
* `Pallet::name`, which is already the runtime's answer and not a guess. */
|
||||
@@ -91,6 +92,11 @@ export function AccountPanel({
|
||||
style={{ marginTop: 4, textTransform: detail?.display ? undefined : 'none' }}
|
||||
>
|
||||
{detail?.display ?? shortAddress(detail?.address ?? address)}
|
||||
{detail && detail.roles.length > 0 && (
|
||||
<span className="title-chips">
|
||||
<RoleChips roles={detail.roles} />
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<dl className="identifiers">
|
||||
<dt>Address</dt>
|
||||
@@ -114,6 +120,39 @@ export function AccountPanel({
|
||||
both already do what a close button would. */}
|
||||
</div>
|
||||
|
||||
{/* The citation. The chip beside the title is the claim; this is the
|
||||
evidence, and it is where the *kind* of specialness gets stated —
|
||||
compiled into the runtime, assigned in state, or simply held a
|
||||
balance in block zero. */}
|
||||
{detail && detail.roles.length > 0 && (
|
||||
<div className="banner banner-role">
|
||||
{detail.roles.map((r) => (
|
||||
<div key={`${r.source}-${r.cited}`}>
|
||||
{r.source === 'constant' && (
|
||||
<>
|
||||
Named by the runtime constant <code>{r.cited}</code> — compiled in, and fixed for
|
||||
as long as this runtime is in force.
|
||||
</>
|
||||
)}
|
||||
{r.source === 'state' && (
|
||||
<>
|
||||
Named by <code>{r.cited}</code> in chain state — assigned, and changeable by
|
||||
whatever call the pallet provides for it.
|
||||
</>
|
||||
)}
|
||||
{r.source === 'genesis' && (
|
||||
<>
|
||||
Endowed at genesis
|
||||
{r.endowment && ` with ${tokens(r.endowment, decimals, 4)} ${symbol}`}. History,
|
||||
not a job — an account can be endowed and have no role at all.
|
||||
</>
|
||||
)}
|
||||
{r.docs && <span className="banner-doc">{r.docs}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <p className="empty">{error}</p>}
|
||||
{!error && !detail && <p className="empty">Reading the record…</p>}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { AccountRow } from '../api/generated/AccountRow'
|
||||
import { RequestFailed, fetchAccounts } from '../api/rest'
|
||||
import { exactTokens, height as fmtHeight, shortAddress, tokens } from '../lib/format'
|
||||
import { href } from '../lib/routes'
|
||||
import { RoleChips } from './RoleChips'
|
||||
|
||||
export function AccountsIndex({
|
||||
chain,
|
||||
@@ -116,6 +117,7 @@ export function AccountsIndex({
|
||||
node?
|
||||
</span>
|
||||
)}
|
||||
<RoleChips roles={row.roles} />
|
||||
{row.attribution === 'carried' && (
|
||||
<span
|
||||
className="chip chip-named chip-guess"
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
shortMiner,
|
||||
} from '../lib/format'
|
||||
import { href } from '../lib/routes'
|
||||
import { GenesisPanel } from './GenesisPanel'
|
||||
import { Payload } from './Payload'
|
||||
|
||||
/** A label and its value, with the caveat that makes the value readable. */
|
||||
@@ -324,6 +325,15 @@ export function BlockPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Block zero is not an ordinary block. Whoever opened it is exactly
|
||||
the person who should see who the chain started with, and the
|
||||
endowments exist nowhere else. Keyed on the height rather than on
|
||||
the URL so it appears whether the block was reached by number or
|
||||
by its genesis hash. */}
|
||||
{detail.height === 0 && (
|
||||
<GenesisPanel chain={chain} decimals={decimals} symbol={symbol} />
|
||||
)}
|
||||
|
||||
<div className="block-links">
|
||||
{detail.parent_hash && (
|
||||
<div>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Block zero, and what it handed out.
|
||||
*
|
||||
* Endowments happen here and are invisible to anyone who has not read the
|
||||
* chainspec — there is no extrinsic for them, no event, nothing in the ordinary
|
||||
* record. They exist only as balances in block zero's state.
|
||||
*
|
||||
* Which is why this is attached to the block page rather than filed somewhere a
|
||||
* reader would have to think to look: whoever opens block zero is exactly the
|
||||
* person who should see who the chain started with.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import type { GenesisDetail } from '../api/generated/GenesisDetail'
|
||||
import { RequestFailed, fetchGenesis } from '../api/rest'
|
||||
import { exactTokens, shortAddress, tokens, when } from '../lib/format'
|
||||
import { href } from '../lib/routes'
|
||||
|
||||
export function GenesisPanel({
|
||||
chain,
|
||||
decimals,
|
||||
symbol,
|
||||
}: {
|
||||
chain: string
|
||||
decimals: number
|
||||
symbol: string
|
||||
}) {
|
||||
const [detail, setDetail] = useState<GenesisDetail | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
setDetail(null)
|
||||
setError(null)
|
||||
fetchGenesis(chain, 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])
|
||||
|
||||
return (
|
||||
<section className="panel" style={{ marginBottom: 26 }}>
|
||||
<div className="panel-head">
|
||||
<div>
|
||||
<div className="eyebrow">Genesis</div>
|
||||
<h2 className="panel-title" style={{ marginTop: 4 }}>
|
||||
Who this chain started with
|
||||
</h2>
|
||||
{detail?.hash && (
|
||||
<dl className="identifiers">
|
||||
<dt>Genesis hash</dt>
|
||||
<dd className="numeral">{detail.hash}</dd>
|
||||
{detail.started_at && (
|
||||
<>
|
||||
<dt>Started</dt>
|
||||
<dd className="numeral">{when(detail.started_at)}</dd>
|
||||
</>
|
||||
)}
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <p className="empty">{error}</p>}
|
||||
{!error && detail === null && <p className="empty">Reading block zero…</p>}
|
||||
|
||||
{detail !== null && !detail.readable && (
|
||||
/* Missing data, not an absence of endowment. Genesis state lives only
|
||||
on an archive node, and a page that showed an empty table here would
|
||||
be asserting something it cannot know. */
|
||||
<p className="empty">
|
||||
Block zero's state is not available from this node. Endowments live only there, so they
|
||||
cannot be shown — this is missing data rather than a chain that endowed nobody.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{detail !== null && detail.readable && (
|
||||
<>
|
||||
<div className="stats" style={{ margin: 0, border: 0, borderBottom: 'var(--rule)' }}>
|
||||
<div className="stat">
|
||||
<div className="stat-label">Endowed accounts</div>
|
||||
<div className="stat-value">{detail.endowed.length}</div>
|
||||
<div className="stat-note">held a balance in block zero</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-label">Handed out</div>
|
||||
<div
|
||||
className="stat-value"
|
||||
title={`${exactTokens(detail.total_endowed, decimals)} ${symbol}`}
|
||||
>
|
||||
{tokens(detail.total_endowed, decimals, 2)}
|
||||
</div>
|
||||
<div className="stat-note">{symbol} at genesis</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detail.endowed.length === 0 ? (
|
||||
<p className="empty">No account held a balance in block zero.</p>
|
||||
) : (
|
||||
<div className="scroll-x">
|
||||
<table className="board">
|
||||
<caption className="visually-hidden">
|
||||
Accounts endowed at genesis, largest first
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="left">
|
||||
Account
|
||||
</th>
|
||||
<th scope="col">Endowed</th>
|
||||
<th scope="col">Holds now</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detail.endowed.map((a) => {
|
||||
const endowment = a.roles[0]?.endowment ?? null
|
||||
return (
|
||||
<tr key={a.account}>
|
||||
<td className="left">
|
||||
<Link className="numeral" to={href({ chain, account: a.address })}>
|
||||
{shortAddress(a.address)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="numeral">
|
||||
{endowment ? `${tokens(endowment, decimals)} ${symbol}` : '—'}
|
||||
</td>
|
||||
<td className="numeral">
|
||||
{/* Then against now. An endowed account that is empty
|
||||
spent it; one that has grown has been earning. */}
|
||||
{a.balance === null ? (
|
||||
<span className="runtime-modifier">gone</span>
|
||||
) : (
|
||||
`${tokens(a.balance, decimals)} ${symbol}`
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="panel-note">
|
||||
Read from <code>System::Account</code> in block zero's own state. There is no extrinsic
|
||||
for an endowment and no event — it is simply a balance the chain was born holding, which
|
||||
is why it is invisible to anyone who has not read the chainspec. <em>Holds now</em> is
|
||||
the same account today, so the two columns together say what each founding account did
|
||||
with what it was given.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* How the chain names an account.
|
||||
*
|
||||
* A chip states what the chain says and does not endorse it. `MintingAccount`
|
||||
* is a sentinel with no account entry at all, not a wallet; labelling it as what
|
||||
* the runtime calls it is a fact, and implying it holds funds or is trustworthy
|
||||
* would be the site vouching for an address it knows nothing about.
|
||||
*
|
||||
* The title carries the citation, because the chip is the claim and the
|
||||
* citation is the evidence — and the three kinds are different claims. A
|
||||
* constant is compiled in and immutable for that runtime; a storage value is
|
||||
* assigned and changeable; a genesis endowment is history and not a role at all.
|
||||
*/
|
||||
|
||||
import type { AccountRoleEntry } from '../api/generated/AccountRoleEntry'
|
||||
|
||||
function why(role: AccountRoleEntry): string {
|
||||
switch (role.source) {
|
||||
case 'constant':
|
||||
return `Named by the runtime constant ${role.cited} — compiled in, and fixed for as long as this runtime is in force.`
|
||||
case 'state':
|
||||
return `Named by ${role.cited} in chain state — assigned, and changeable by whatever call the pallet provides.`
|
||||
case 'genesis':
|
||||
return `Held a balance in block zero. History, not a job: an account can be endowed and have no role.`
|
||||
}
|
||||
}
|
||||
|
||||
export function RoleChips({ roles }: { roles: AccountRoleEntry[] }) {
|
||||
if (roles.length === 0) return null
|
||||
// One chip per label. Two constants naming the same address is two citations
|
||||
// and one fact, and rendering "Minting Minting" would say otherwise.
|
||||
const seen = new Set<string>()
|
||||
const shown = roles.filter((r) => !seen.has(r.label) && seen.add(r.label))
|
||||
return (
|
||||
<>
|
||||
{shown.map((r) => (
|
||||
<span
|
||||
key={r.label}
|
||||
className={r.source === 'genesis' ? 'chip chip-genesis' : 'chip chip-role'}
|
||||
title={[why(r), r.docs].filter(Boolean).join('\n\n')}
|
||||
>
|
||||
{r.label}
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -48,6 +48,17 @@ export function SectionNav({
|
||||
>
|
||||
Standings
|
||||
</Link>
|
||||
{/* Straight to block zero. Endowments happen there and are invisible to
|
||||
anyone who has not read the chainspec, so somebody who would never
|
||||
think to look is given an unmissable way to. Not a `SECTIONS` entry
|
||||
because it is not an index — it is one particular block. */}
|
||||
<Link
|
||||
to={href({ chain, block: '0' })}
|
||||
className="section-link"
|
||||
aria-current={route.block === '0' ? 'page' : undefined}
|
||||
>
|
||||
Genesis
|
||||
</Link>
|
||||
{SECTIONS.map((s) => (
|
||||
<Link
|
||||
key={s.id}
|
||||
|
||||
Reference in New Issue
Block a user