Files
zeekay 87aa0fa367 chains: read each chain from that chain, and list the ones this network runs
/chains showed fifteen rows on every network, from a hardcoded table. Four of
them — T, R, I, O — are registered on no network at all; the node answers
"there is no ID with alias: T". A fifth, M, runs on devnet alone. And every
/chains/<slug> page printed the same four numbers: the primary network's
validator count, its total stake, its chain count, its threshold. Nothing on
Q-Chain's page was about Q-Chain. Fifteen pages, one set of facts.

The network already knows its own answer. platform.getBlockchains is the chain
list, and it is per-network for free: mainnet and testnet report nine chains,
devnet reports ten including M. That is what /chains lists now, joined with
configs/app/primaryChains.ts for presentation only — a chain the network
reports and we have no metadata for still gets a row, because the network is
the source of truth and our table is not.

Three facts per chain, each read from that chain:

  * the blockchain ID, from the registry — the thing that actually tells one
    chain from another, and it was already on screen while the numbers above
    it were not
  * whether the node has bootstrapped it, from info.isBootstrapped, which the
    gateway routes for every chain. This is how Q-Chain's page now says
    Syncing where the other nine say Bootstrapped: Q has not come up on any of
    the three networks and is the sole reason all three report healthy:false
  * its head, in its own dialect — eth_blockNumber for C, platform.getHeight
    for P, xvm.getHeight for X. Not avm: that service is gone from this build,
    and asking it returns "can't find service" rather than a height

Validators are the P-Chain's model, so they render on the P-Chain and nowhere
else. The head is labelled for the model it comes from — the X-Chain's is an
accepted height, not a block height.

Where a chain has no head to read, it says so. The gateway routes exactly P, X
and C on all three networks; D, A, B, Z, G, K and Q are registered and
bootstrapped on the node but their RPC 404s from a browser. Proof it is the
route and not the chain: C-Chain's own blockchain ID, /v1/bc/25td8att…/rpc,
404s while /v1/bc/C/rpc returns a height — same chain, same node. Those pages
read "No public RPC" and say plainly that the chain is registered and running
but not published. A zero there would have read as a measurement.

One proxy replaces the P-only one. /v1/pchain hardcoded /v1/bc/P; /v1/node/
<endpoint> resolves the endpoint against the primary-chain table and never
treats it as a URL. A chain the gateway does not route comes back 200 carrying
a JSON-RPC error rather than 502 — nothing is broken, and 502 put a red failed
request in the console of eight of the ten chain pages.

Deleted: CHAIN_INDEXER_URLS and the hook that read it — fifteen hostnames of
the shape api-indexer-<x>chain.lux.network, none of which resolve. Fifteen of
fifteen NXDOMAIN. It fetched them on every chain page and rendered the null.

Measured on mainnet, 1440x900 and 390x844, fresh context per URL: 20 of 20
chain/screen rows agree with RPC on both status and head — C-Chain 1,107,826
against a live head bracketed at 1,107,822..1,107,829, P and X at 0, seven
reading No public RPC, Q Syncing while the rest are Bootstrapped. /chains
lists the same ten chains the P-Chain reports, and 10 of 10 row links reach a
real page. Zero 4xx or 5xx from our own origin.
2026-08-08 07:32:01 -07:00

112 lines
4.1 KiB
TypeScript

// What is true about ONE primary-network chain, read from that chain.
//
// Every /chains/<slug> page used to print the same four numbers — the
// network-wide validator count, total stake, chain count and threshold — so
// Q-Chain and Z-Chain and K-Chain were the same page with the title swapped.
// None of it was about the chain you opened.
//
// Three facts are per-chain and real:
// * the blockchain ID, from the P-Chain registry — unique, and the thing that
// actually distinguishes one chain from another
// * whether the node has bootstrapped it, from info.isBootstrapped, which the
// gateway routes for every chain
// * its head, from its own RPC, in whatever dialect it speaks
//
// The last one is only available where the gateway routes the chain. Today that
// is P, X and C on all three networks; the rest are registered and bootstrapped
// but answer 404 from the browser's only reachable origin. That is reported as
// `unreachable`, never as a height of zero — a chain whose head we cannot read and
// a chain sitting at genesis are different states and must not render alike.
import { useQuery } from '@tanstack/react-query';
import type { PrimaryVm } from 'configs/app/primaryChains';
const STALE_MS = 15_000;
export type Head =
{ readonly kind: 'height'; readonly height: number } |
{ readonly kind: 'unreachable' } |
{ readonly kind: 'unknown' };
export interface ChainFacts {
readonly blockchainId: string | null;
readonly bootstrapped: boolean | null;
readonly head: Head;
readonly chainId: number | null;
}
async function rpc(endpoint: string, method: string, params: unknown): Promise<unknown> {
const res = await fetch(`/v1/node/${ endpoint }`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
});
if (!res.ok) {
return null;
}
const body = await res.json() as { result?: unknown };
return body.result ?? null;
}
// Each dialect names its head differently, and asking the wrong one gets a
// "can't find service" error rather than an answer — X speaks xvm, not avm, and
// not platform.
async function readHead(vm: PrimaryVm): Promise<Head> {
try {
if (vm.view === 'evm' || vm.view === 'dex') {
const hex = await rpc(vm.slug, 'eth_blockNumber', []) as string | null;
return hex ? { kind: 'height', height: parseInt(hex, 16) } : { kind: 'unreachable' };
}
const method = vm.view === 'platform' ? 'platform.getHeight' : 'xvm.getHeight';
const out = await rpc(vm.slug, method, {}) as { height?: string } | null;
return out?.height !== undefined ?
{ kind: 'height', height: Number(out.height) } :
{ kind: 'unreachable' };
} catch {
return { kind: 'unreachable' };
}
}
async function readChainId(vm: PrimaryVm): Promise<number | null> {
if (vm.view !== 'evm' && vm.view !== 'dex') {
return null;
}
const hex = await rpc(vm.slug, 'eth_chainId', []) as string | null;
return hex ? parseInt(hex, 16) : null;
}
export function useChainFacts(vm: PrimaryVm | undefined, blockchainId: string | null): {
readonly facts: ChainFacts;
readonly isLoading: boolean;
} {
const query = useQuery({
queryKey: [ 'chain:facts', vm?.slug ],
enabled: Boolean(vm),
staleTime: STALE_MS,
refetchInterval: STALE_MS,
queryFn: async(): Promise<Omit<ChainFacts, 'blockchainId'>> => {
if (!vm) {
return { bootstrapped: null, head: { kind: 'unknown' }, chainId: null };
}
const alias = vm.slug.charAt(0).toUpperCase();
const [ boot, head, chainId ] = await Promise.all([
rpc('info', 'info.isBootstrapped', { chain: alias }) as Promise<{ isBootstrapped?: boolean } | null>,
readHead(vm),
readChainId(vm),
]);
return { bootstrapped: boot?.isBootstrapped ?? null, head, chainId };
},
});
return {
facts: {
blockchainId,
bootstrapped: query.data?.bootstrapped ?? null,
head: query.data?.head ?? { kind: 'unknown' },
chainId: query.data?.chainId ?? vm?.chainId ?? null,
},
isLoading: query.isLoading,
};
}