/** * The chain nav. * * Only the chains this observer holds an RPC endpoint for, ordered by node * count descending — the busiest is the one most people came for, and the order * is a real fact about the network rather than a config ordering. * * **Mainnet and testnets are separated and labelled.** Side by side and * identically styled they had equal weight, and somebody reading a balance, a * reward or a transfer had no way to tell from the chrome whether the number * meant anything. Two headings say which is which without a legend and without * the reader having to know a convention; the dashed border on a testnet chip * is a second channel, because one visual difference alone reads as decoration * to anybody who has not been told what it means. * * `mainnet` is operator-asserted in the `[[chains]]` entry and never inferred * from the name — a chain publishing itself as "Quantus Staging Mainnet" must * not collect the heading by having the word in its title. * * The rest are **not advertised**. They were listed as disabled chips for a * while, on the reasoning that they are part of the network and hiding them * misrepresents its shape. In practice that put five dead chips above the * standings on every page, four of which had never had anything behind them, * and the one live chain was the smallest thing in the row. Nothing is * blocked: `/:chain/...` still resolves for any chain the backend knows, and * the API still reports every one of them with its `tracking`. This is only * about what the page offers unprompted. */ import { Link } from 'react-router-dom' import type { ChainInfo } from '../api/generated/ChainInfo' import type { Window as WindowName } from '../api/generated/Window' import { href } from '../lib/routes' function reason(chain: ChainInfo): string { const kind = chain.mainnet ? '' : ' (a testnet — its balances and rewards are not real)' switch (chain.status) { case 'awaiting': return `${chain.display_name}${kind} has not produced a block yet.` case 'unreachable': return `${chain.display_name}${kind} — its node is not answering; the last known standings are shown.` case 'syncing': return `${chain.display_name}${kind} — its node is still importing history.` default: return `${chain.display_name}${kind}` } } export function ChainSwitcher({ chains, active, window: windowName, }: { chains: ChainInfo[] active: string | null /** Carried across the switch: changing chain is not a reason to change span. */ window: WindowName }) { const navigable = chains.filter((chain) => chain.tracking === 'full') if (navigable.length === 0) return null const groups: { label: string; chains: ChainInfo[] }[] = [ { label: 'Mainnet', chains: navigable.filter((c) => c.mainnet) }, { label: 'Testnets', chains: navigable.filter((c) => !c.mainnet) }, ].filter((g) => g.chains.length > 0) return ( ) }