/** * What this economy is doing, in one place. * * Two kinds of number live on this page and they are not equally certain, which * is why they are in separate panels rather than one grid of tiles. * * **Supply and the map sizes are chain state**, read now and true now. Every one * is a value the runtime holds, not a figure derived by this observer. * * **The activity is our index**, and is only as complete as the range it prints * beside itself. On a chain indexed from genesis that is the whole history; on a * testnet still walking backwards it is not, and a daily chart whose axis claims * a month while the data covers a week is precisely the failure this repository * has already shipped twice. */ import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' import type { NetworkSummary } from '../api/generated/NetworkSummary' import { RequestFailed, fetchNetwork } from '../api/rest' import { height as fmtHeight, tokens } from '../lib/format' import { href } from '../lib/routes' import { DistributionPanel } from './DistributionPanel' import { IntentsPanel } from './IntentsPanel' import { Sparkline, type SparkPoint } from './Sparkline' import { Field } from './Field' /** A count, or an em dash where the figure is genuinely unknown — never a zero * standing in for "we could not read it". */ function count(value: number | null): string { return value === null ? '—' : fmtHeight(value) } export function NetworkPanel({ chain, decimals, symbol, }: { chain: string decimals: number symbol: string }) { const [summary, setSummary] = useState(null) const [error, setError] = useState(null) useEffect(() => { const controller = new AbortController() setSummary(null) setError(null) fetchNetwork(chain, controller.signal) .then(setSummary) .catch((e: unknown) => { if (controller.signal.aborted) return setError(e instanceof RequestFailed ? e.message : 'Could not reach the observer.') }) return () => controller.abort() }, [chain]) if (error) return

{error}

if (!summary) return

Reading the chain…

const amount = (raw: string | null) => (raw === null ? '—' : `${tokens(raw, decimals)} ${symbol}`) const locks = summary.locks const nothingLocked = locks !== null && locks.locks === 0 && locks.holds === 0 && locks.freezes === 0 && locks.reserves === 0 // The vesting pot, which on this chain holds almost everything. Rendered as a // percentage because two eighteen-digit strings do not compare at a glance, // and 99.5% is the whole point. // Only rendered when the account walk reached the end of the map: every share // here divides by a total, and a partial sum would make all of them wrong // while looking entirely reasonable. const conc = summary.concentration /** Parts per ten thousand as a percentage, or an em dash. */ const bps = (v: number | null) => (v === null ? '—' : `${(v / 100).toFixed(v > 9900 ? 2 : 1)}%`) const pot = summary.vesting_pot const potShare = pot && pot.share_bps !== null ? `${(pot.share_bps / 100).toFixed(pot.share_bps > 9900 ? 2 : 1)}%` : null const spark = (pick: (d: NetworkSummary['days'][number]) => number, unit: string): SparkPoint[] => summary.days.map((d) => ({ value: pick(d), label: `${fmtHeight(pick(d))} ${unit} · ${d.day.slice(0, 10)}`, })) // The indexed range against the chain's height, so the reader can see how // much of the chain these daily figures actually cover. const complete = summary.indexed_from === 1 && summary.height !== null && summary.indexed_to !== null ? summary.indexed_to >= summary.height - 2 : false return ( <>

Supply

read from chain state {summary.spec_version !== null && ` · runtime v${summary.spec_version}`}

{pot && potShare !== null && ( <> {potShare} of all issuance sits in one account — {pot.address}, which the vesting pallet owns and nobody holds a key to. This runtime vests from a pot rather than with locks: undistributed funds wait there and are paid out from it.{' '} )} {nothingLocked ? ( <> All four of the balances pallet's immobilising maps — locks, holds, freezes and reserves — are empty, counted rather than assumed.{' '} That is not the same as nothing being withheld: this chain withholds with the pot above, which no lock records. There is still no “circulating supply” figure here, because funds leave the pot on a schedule this observer does not read and an address outside it may be unspendable for other reasons — but issuance alone would overstate what is in play by a factor of several hundred.{' '} ) : ( locks && ( <> Supply is immobilised in {count(locks.locks)} locks, {count(locks.holds)} holds,{' '} {count(locks.freezes)} freezes and {count(locks.reserves)} reserves.{' '} ) )} {summary.vesting_schedules !== null && summary.vesting_schedules > 0 && ( <> The {count(summary.vesting_schedules)} vesting schedules are a count rather than a sum; the sum that matters is the pot, and it is read directly.{' '} )} Account balances are not added up anywhere on this site. On this chain they exceed total issuance and the gap grows: a wormhole exit credits its destination without debiting the note's source, so value that moves privately appears twice in any such total. Total issuance is the figure that counts each unit once.

{conc !== null && conc.complete && (

Concentration

outside the vesting pot

The vesting pot is excluded from every figure above. It holds{' '} {conc.pot?.share_bps === null || conc.pot === null ? 'almost all' : bps(conc.pot.share_bps)}{' '} of the chain's counted balance, and a concentration measure that included it would describe an undistributed reserve — it would sit near 1.000 and move only when the pot pays out. These figures divide by what is outside it.

The denominator is the counted balance, not total issuance. The two differ: a wormhole exit credits its destination without debiting the note's source, so balances summed across the chain exceed issuance by however much has ever been taken out. Shares of issuance would add up to more than everything.

Read the tail carefully. A mining reward is paid to an address derived from the miner's own preimage, which nothing can sign for — so those balances are cumulative receipts that can only rise, and never fall however much their operator pays out. Concentration across them is in part a statement about who has mined the most rather than about who holds the most.

)}

Capability

what is shipped, and what is used

A zero here is a finding, not a gap: a pallet shipped and never touched says something about a chain that its absence would not. The usage counts are over this observer's index rather than all of history —{' '} the call index has the detail, including every dispatchable nobody has ever used.

{/* A fixed window, not the route's. The window selector is hidden on this page, so carrying the route's would mean the shares silently reflect whatever span the reader last picked on the standings — invisible, and different for two people looking at the same page. The longest window is also the right one for the question: what a miner has done with its rewards is a question about the chain's history, not its last hour. */}

Activity

{summary.indexed_from !== null && summary.indexed_to !== null ? `blocks ${fmtHeight(summary.indexed_from)}–${fmtHeight(summary.indexed_to)}${ complete ? ' · the whole chain' : ' · a partial index' }` : 'nothing indexed yet'}
{[ { label: 'Signed extrinsics', pick: (d: NetworkSummary['days'][number]) => d.signed, unit: 'signed', }, { label: 'Distinct signers', pick: (d: NetworkSummary['days'][number]) => d.signers, unit: 'signers', }, { label: 'New accounts', pick: (d: NetworkSummary['days'][number]) => d.new_accounts, unit: 'accounts', }, { label: 'Blocks', pick: (d: NetworkSummary['days'][number]) => d.blocks, unit: 'blocks', }, ].map((s) => (
{s.label}
{fmtHeight(s.pick(summary.days[summary.days.length - 1]!))}
{/* No hover readout here: each tile already prints its latest value above the line, and five probe handlers competing over one row of 200px tiles is a pile rather than a hover layer — the same reason the headline tiles borrow their note line. */} {}} />
))}
{[...summary.days].reverse().map((d) => ( ))}
Activity per day, newest first
Day Blocks Miners Extrinsics Signed Signers Events New accounts
{d.day.slice(0, 10)} {fmtHeight(d.blocks)} {fmtHeight(d.miners)} {fmtHeight(d.extrinsics)} {fmtHeight(d.signed)} {fmtHeight(d.signers)} {fmtHeight(d.events)} {fmtHeight(d.new_accounts)}

Bucketed by the chain's own clock, never by when this observer saw anything: a stretch caught up on carries observation times minutes apart for blocks spanning days, and a daily series built on that reports a fortnight as one afternoon.{' '} Signed is separated from the total because most extrinsics on this chain are inherents — the timestamp every block carries — so a single “transactions per day” line would be mostly clockwork, a number that looks like adoption and is not. {!complete && ' These counts are of what has been read, not of what happened: this chain is still being indexed backwards.'}

) }