feat: mark the accounts a chain names, and make genesis unmissable
All checks were successful
deploy / build (push) Successful in 8m4s
deploy / deploy-web (push) Successful in 5s
deploy / deploy-api (push) Successful in 15s

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:
2026-09-10 09:04:45 +03:00
parent c08d3c4e04
commit dd8086df0d
24 changed files with 1087 additions and 45 deletions

View File

@@ -1,5 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AccountEvent } from "./AccountEvent";
import type { AccountRoleEntry } from "./AccountRoleEntry";
import type { BigUintDec } from "./BigUintDec";
import type { ChainId } from "./ChainId";
import type { MinerId } from "./MinerId";
@@ -36,6 +37,12 @@ miner: MinerId | null,
* The telemetry name held for that miner, if one is.
*/
display: string | null,
/**
* Every reason this account is special, if any — named by a runtime
* constant, named by a storage value, or endowed at genesis. Empty for
* almost every account, which is why an entry here is worth marking.
*/
roles: Array<AccountRoleEntry>,
/**
* The account's free balance right now, read from chain state rather than
* inferred from the flows below.

View File

@@ -0,0 +1,29 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BigUintDec } from "./BigUintDec";
import type { RoleSource } from "./RoleSource";
/**
* One reason an account is special.
*/
export type AccountRoleEntry = {
/**
* Which kind of claim this is.
*/
source: RoleSource,
/**
* The chain's own name for it — `TreasuryPallet::TreasuryAccount`,
* `MiningRewards::MintingAccount`, or `genesis`. The citation, kept whole.
*/
cited: string,
/**
* A short label for a chip, derived from `cited` and never invented.
*/
label: string,
/**
* The runtime's documentation for the constant or entry that names it.
*/
docs: string | null,
/**
* What it was endowed with, for a genesis role.
*/
endowment: BigUintDec | null, };

View File

@@ -1,4 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AccountRoleEntry } from "./AccountRoleEntry";
import type { AttributionSource } from "./AttributionSource";
import type { BigUintDec } from "./BigUintDec";
import type { MinerId } from "./MinerId";
@@ -53,4 +54,9 @@ blocks: number,
/**
* Their sum, in the chain's smallest unit.
*/
total: BigUintDec, };
total: BigUintDec,
/**
* Any reason the chain names this account, so a founding or role-holding
* address is marked here as it is everywhere else.
*/
roles: Array<AccountRoleEntry>, };

View File

@@ -0,0 +1,26 @@
// 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 { NamedAccount } from "./NamedAccount";
/**
* Every account this chain names.
*/
export type ChainRoles = {
/**
* Which chain.
*/
chain: ChainId,
/**
* The runtime the constants and entries were read from.
*/
spec_version: number,
/**
* The named accounts.
*/
accounts: Array<NamedAccount>,
/**
* Whether genesis state could be read at all. An archive node holds block
* zero; a pruning one does not, and the endowed accounts would then be
* missing rather than absent.
*/
genesis_readable: boolean, };

View File

@@ -0,0 +1,33 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { BigUintDec } from "./BigUintDec";
import type { ChainId } from "./ChainId";
import type { NamedAccount } from "./NamedAccount";
/**
* Block zero, and what it handed out.
*/
export type GenesisDetail = {
/**
* Which chain.
*/
chain: ChainId,
/**
* The genesis hash, which is the chain's identity.
*/
hash: string | null,
/**
* Accounts endowed in block zero, largest first.
*/
endowed: Array<NamedAccount>,
/**
* Their sum — the chain's starting issuance, as far as accounts go.
*/
total_endowed: BigUintDec,
/**
* When the chain started, if the observer knows.
*/
started_at: string | null,
/**
* Whether genesis state could be read.
*/
readable: boolean, };

View File

@@ -0,0 +1,25 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AccountRoleEntry } from "./AccountRoleEntry";
import type { BigUintDec } from "./BigUintDec";
/**
* One account and every reason it is special.
*/
export type NamedAccount = {
/**
* The raw account id, `0x` hex.
*/
account: string,
/**
* Its SS58 address.
*/
address: string,
/**
* Every reason, most specific first.
*/
roles: Array<AccountRoleEntry>,
/**
* What it holds now. `null` when the account does not exist on chain —
* which for a named role is a finding rather than a gap.
*/
balance: BigUintDec | null, };

View File

@@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Where an account's specialness comes from.
*
* Three different claims, deliberately not flattened into one badge.
*/
export type RoleSource = "constant" | "state" | "genesis";

View File

@@ -14,7 +14,9 @@ import type { BlockDetail } from './generated/BlockDetail'
import type { BlockExtrinsic } from './generated/BlockExtrinsic'
import type { CallIndex } from './generated/CallIndex'
import type { ChainSeries } from './generated/ChainSeries'
import type { ChainRoles } from './generated/ChainRoles'
import type { ChainState } from './generated/ChainState'
import type { GenesisDetail } from './generated/GenesisDetail'
import type { ReversibleState } from './generated/ReversibleState'
import type { RecentBlock } from './generated/RecentBlock'
import type { MinerDetail } from './generated/MinerDetail'
@@ -169,6 +171,16 @@ export function fetchReversible(chain: string, signal?: AbortSignal): Promise<Re
return get<ReversibleState>(`/chains/${encodeURIComponent(chain)}/reversible`, signal)
}
/** Every account the chain names, and how. */
export function fetchRoles(chain: string, signal?: AbortSignal): Promise<ChainRoles> {
return get<ChainRoles>(`/chains/${encodeURIComponent(chain)}/roles`, signal)
}
/** Block zero, and what it handed out. */
export function fetchGenesis(chain: string, signal?: AbortSignal): Promise<GenesisDetail> {
return get<GenesisDetail>(`/chains/${encodeURIComponent(chain)}/genesis`, signal)
}
/** Accounts ranked by what they have been paid, most first. */
export function fetchAccounts(chain: string, signal?: AbortSignal): Promise<AccountRow[]> {
return get<AccountRow[]>(`/chains/${encodeURIComponent(chain)}/accounts`, signal)