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.
129 lines
3.6 KiB
TypeScript
129 lines
3.6 KiB
TypeScript
// React Query hook for platform.getCurrentValidators.
|
||
// Returns the validator list and aggregated network statistics.
|
||
// Uses the server-side /v1/node/p-chain proxy to bypass CORS.
|
||
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import React from 'react';
|
||
|
||
import type {
|
||
PChainValidator,
|
||
ValidatorStats,
|
||
} from './types';
|
||
|
||
const VALIDATORS_STALE_TIME_MS = 60_000;
|
||
const VALIDATORS_QUERY_KEY = 'pchain:currentValidators' as const;
|
||
const ZERO = BigInt(0);
|
||
|
||
function computeValidatorStats(
|
||
validators: ReadonlyArray<PChainValidator>,
|
||
): ValidatorStats {
|
||
let totalStake = ZERO;
|
||
let totalDelegatedStake = ZERO;
|
||
let connectedCount = 0;
|
||
let delegatorCount = 0;
|
||
let uptimeSum = 0;
|
||
|
||
for (const v of validators) {
|
||
totalStake += BigInt(v.stakeAmount ?? v.weight);
|
||
|
||
// connected field may be absent in some node configurations;
|
||
// infer connectivity from uptime > 0 when missing
|
||
if (v.connected ?? (parseFloat(v.uptime) > 0)) {
|
||
connectedCount += 1;
|
||
}
|
||
|
||
uptimeSum += parseFloat(v.uptime);
|
||
|
||
if (v.delegators) {
|
||
delegatorCount += v.delegators.length;
|
||
for (const d of v.delegators) {
|
||
totalDelegatedStake += BigInt(d.stakeAmount);
|
||
}
|
||
}
|
||
}
|
||
|
||
// uptime values from the API are already in percentage (0–100); no scaling needed
|
||
const averageUptime = validators.length > 0 ?
|
||
uptimeSum / validators.length :
|
||
0;
|
||
|
||
return {
|
||
totalStake,
|
||
validatorCount: validators.length,
|
||
connectedCount,
|
||
delegatorCount,
|
||
totalDelegatedStake,
|
||
averageUptime,
|
||
};
|
||
}
|
||
|
||
export interface UseCurrentValidatorsResult {
|
||
readonly validators: ReadonlyArray<PChainValidator>;
|
||
readonly stats: ValidatorStats;
|
||
}
|
||
|
||
async function fetchCurrentValidators(): Promise<UseCurrentValidatorsResult> {
|
||
const res = await fetch('/v1/node/p-chain', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
jsonrpc: '2.0',
|
||
method: 'platform.getCurrentValidators',
|
||
params: { netID: '11111111111111111111111111111111LpoYY' },
|
||
id: 1,
|
||
}),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error(`P-chain proxy returned ${ res.status }`);
|
||
}
|
||
|
||
const json = await res.json() as { result?: { validators?: ReadonlyArray<PChainValidator> } };
|
||
const validators = json.result?.validators ?? [];
|
||
const stats = computeValidatorStats(validators);
|
||
|
||
return { validators, stats };
|
||
}
|
||
|
||
export function useCurrentValidators() {
|
||
const query = useQuery({
|
||
queryKey: [ VALIDATORS_QUERY_KEY ],
|
||
queryFn: fetchCurrentValidators,
|
||
staleTime: VALIDATORS_STALE_TIME_MS,
|
||
retry: 2,
|
||
});
|
||
|
||
const validators = React.useMemo(
|
||
() => query.data?.validators ?? [],
|
||
[ query.data?.validators ],
|
||
);
|
||
|
||
const stats = React.useMemo(
|
||
() => query.data?.stats ?? {
|
||
totalStake: ZERO,
|
||
validatorCount: 0,
|
||
connectedCount: 0,
|
||
delegatorCount: 0,
|
||
totalDelegatedStake: ZERO,
|
||
averageUptime: 0,
|
||
},
|
||
[ query.data?.stats ],
|
||
);
|
||
|
||
return {
|
||
validators,
|
||
stats,
|
||
// Did the read actually answer? A failed read leaves `stats` zeroed, and a
|
||
// zeroed stat is indistinguishable from a chain with no validators — which
|
||
// is how this explorer came to print "0 validators / 0 LUX staked" about a
|
||
// network running five with 2,500,000,000 LUX bonded. Every surface asks
|
||
// this before rendering a validator figure, and renders an em dash when it
|
||
// is false. Deciding it HERE means the four surfaces cannot drift apart.
|
||
isKnown: !query.isError && stats.validatorCount > 0,
|
||
isLoading: query.isLoading,
|
||
isError: query.isError,
|
||
error: query.error,
|
||
refetch: query.refetch,
|
||
};
|
||
}
|