mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
/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.
112 lines
4.1 KiB
TypeScript
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,
|
|
};
|
|
}
|