fix(explore): a failed validator read printed 0, not "unknown"

/validators reported "0 validators / 0 LUX staked" about a network running
five with 2,500,000,000 LUX bonded. Measured now against the live P-Chain:

  POST https://api.lux.network/ext/bc/P platform.getCurrentValidators
    -> 5 validators, sum(weight) 2500000000000000000 nLUX = 2,500,000,000 LUX

(P-chain figures are nLUX, 1e9 — and the rows carry `weight`, not
`stakeAmount`, which is why the reader already falls back.)

useCurrentValidators returns a ZEROED ValidatorStats when the query fails,
and a zeroed stat is indistinguishable from a chain with no validators.
NetworkOverview and NetworkStats each guarded against that with their own
copy of `!isError && validatorCount > 0`; ValidatorsPage and
ValidatorsDashboard had no guard at all, so they rendered the zeros —
"Validators (0)", "Delegators (0)", "0 LUX Total Stake" — as if measured.

The question "did the read answer?" now has exactly one answer, computed
where the read happens: the hook exports `isKnown`. The two ad-hoc copies
are gone and the two unguarded surfaces use it, showing an em dash and
naming the method that did not answer.

Also restores module resolution. 258bd1a48 dropped `baseUrl: "."` from
tsconfig on the grounds that `paths` already covered it — but `paths` was
`{}`, so baseUrl alone was resolving every absolute import in the repo
("configs/app", "lib/api/pchain", "ui/..."). Without it tsc could not
resolve a single one. `"*": ["./*"]` is the same rule in the form TS7
accepts: tsc goes from thousands of TS2307 to 1.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-07-27 14:32:37 -07:00
co-authored by Hanzo Dev
parent 863add6f6b
commit 521e47ab47
6 changed files with 47 additions and 19 deletions
+7
View File
@@ -113,6 +113,13 @@ export function useCurrentValidators() {
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,
+6 -1
View File
@@ -18,7 +18,12 @@
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {},
// TS7 removed `baseUrl`, and this project's absolute imports ("configs/app",
// "lib/api/pchain", "ui/...") were resolved by `baseUrl: "."` alone — `paths`
// was empty, so dropping baseUrl left nothing to resolve them and tsc could
// not typecheck a single file. `"*": ["./*"]` is the same rule expressed the
// way TS7 accepts: targets resolve relative to this tsconfig's directory.
"paths": { "*": [ "./*" ] },
"types": [
"node"
]
+1 -4
View File
@@ -226,7 +226,7 @@ const SectionCard = ({ title, count, isLoading, action, children }: SectionCardP
// ── Main page ──
const NetworkOverview = () => {
const { stats, isLoading: validatorsLoading, isError: validatorsError } = useCurrentValidators();
const { stats, isLoading: validatorsLoading, isKnown: hasValidatorData, isError: validatorsError } = useCurrentValidators();
const { blockchains, isLoading: chainsLoading } = useBlockchains();
const { pChainHeight, cChainHeight, isLoading: heightsLoading } = useChainHeights();
@@ -242,9 +242,6 @@ const NetworkOverview = () => {
const totalChains = PRIMARY_CHAINS.length + (hasL1Data ? l1Chains.length : KNOWN_L1_CHAINS.length);
const isLoading = validatorsLoading || chainsLoading;
// Hide validator metrics when API returned an error (show dashes instead of 0)
const hasValidatorData = !validatorsError && stats.validatorCount > 0;
// Primary-network panels (validator metrics strip + the 15 primary-network
// chains + the cross-L1 list) belong ONLY on the Lux primary-network explorer.
// Brand explorers (Hanzo / Zoo / SPC / Pars, vm: 'L2') show ONLY their own
+1 -2
View File
@@ -51,7 +51,7 @@ const StatCard = ({ label, value, isLoading }: StatCardProps) => (
// ── Main component ──
const NetworkStats = () => {
const { stats, isLoading: validatorsLoading, isError: validatorsError } = useCurrentValidators();
const { stats, isLoading: validatorsLoading, isKnown: hasValidatorData } = useCurrentValidators();
const { blockchains, isLoading: chainsLoading } = useBlockchains();
const isLoading = validatorsLoading || chainsLoading;
@@ -62,7 +62,6 @@ const NetworkStats = () => {
);
const totalChains = PRIMARY_CHAIN_COUNT + l1Count;
const hasValidatorData = !validatorsError && stats.validatorCount > 0;
return (
<div>
+25 -8
View File
@@ -8,6 +8,11 @@ import { formatStake, truncateNodeId } from './utils';
const CURRENCY = config.chain.currency.symbol || 'LUX';
// What an unanswered read looks like. Never a zero: this page printed
// "0 validators / 0 LUX staked" about a network running five with
// 2,500,000,000 LUX bonded, because a failed read leaves the stats zeroed.
const UNKNOWN = '\u2014';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
@@ -45,9 +50,10 @@ const StatCard = ({ label, value, isLoading }: StatCardProps) => (
interface StakeBreakdownProps {
readonly stats: ValidatorStats;
readonly isLoading: boolean;
readonly isKnown: boolean;
}
const StakeBreakdown = ({ stats, isLoading }: StakeBreakdownProps) => {
const StakeBreakdown = ({ stats, isLoading, isKnown }: StakeBreakdownProps) => {
const validatorStake = stats.totalStake - stats.totalDelegatedStake;
const totalNumber = Number(stats.totalStake);
const validatorPct = totalNumber > 0 ?
@@ -64,13 +70,21 @@ const StakeBreakdown = ({ stats, isLoading }: StakeBreakdownProps) => {
</div>
<Skeleton loading={ isLoading }>
<div className="font-bold">
{ formatStake(stats.totalStake) } { CURRENCY }
{ isKnown ? `${ formatStake(stats.totalStake) } ${ CURRENCY }` : UNKNOWN }
</div>
</Skeleton>
<Skeleton loading={ isLoading }>
<div className="flex gap-6 flex-wrap">
<div>Validators: { formatStake(validatorStake) } { CURRENCY } ({ validatorPct }%)</div>
<div>Delegated: { formatStake(stats.totalDelegatedStake) } { CURRENCY } ({ delegationPct }%)</div>
{ isKnown ? (
<>
<div>Validators: { formatStake(validatorStake) } { CURRENCY } ({ validatorPct }%)</div>
<div>Delegated: { formatStake(stats.totalDelegatedStake) } { CURRENCY } ({ delegationPct }%)</div>
</>
) : (
<div className="text-[var(--color-text-secondary)]">
platform.getCurrentValidators did not answer — no stake figure to show.
</div>
) }
</div>
</Skeleton>
</div>
@@ -156,9 +170,12 @@ interface ValidatorsDashboardProps {
readonly validators: ReadonlyArray<PChainValidator>;
readonly stats: ValidatorStats;
readonly isLoading: boolean;
/** False when the P-chain read failed. A zeroed stat is not a measurement. */
readonly isKnown: boolean;
}
const ValidatorsDashboard = ({ validators, stats, isLoading }: ValidatorsDashboardProps) => {
const ValidatorsDashboard = ({ validators, stats, isLoading, isKnown }: ValidatorsDashboardProps) => {
return (
<div className="flex flex-col gap-6 text-[var(--color-text-primary)]">
{ /* Stat cards */ }
@@ -174,18 +191,18 @@ const ValidatorsDashboard = ({ validators, stats, isLoading }: ValidatorsDashboa
>
<StatCard
label="Validators"
value={ stats.validatorCount.toLocaleString() }
value={ isKnown ? stats.validatorCount.toLocaleString() : UNKNOWN }
isLoading={ isLoading }
/>
<StatCard
label="Delegators"
value={ stats.delegatorCount.toLocaleString() }
value={ isKnown ? stats.delegatorCount.toLocaleString() : UNKNOWN }
isLoading={ isLoading }
/>
</div>
{ /* Stake breakdown */ }
<StakeBreakdown stats={ stats } isLoading={ isLoading }/>
<StakeBreakdown stats={ stats } isLoading={ isLoading } isKnown={ isKnown }/>
{ /* Active validators table */ }
<ActiveValidatorsTable validators={ validators } isLoading={ isLoading }/>
+7 -4
View File
@@ -35,7 +35,9 @@ const TabButton = ({ label, isActive, onClick }: TabButtonProps) => (
<button
className={ cn(
'px-4 py-2 text-sm border-b-2 bg-transparent cursor-pointer transition-all hover:text-[var(--color-text-primary)]',
isActive ? 'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' : 'font-normal text-[var(--color-text-secondary)] border-transparent',
isActive ?
'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' :
'font-normal text-[var(--color-text-secondary)] border-transparent',
) }
onClick={ onClick }
>
@@ -49,7 +51,7 @@ const TabButton = ({ label, isActive, onClick }: TabButtonProps) => (
const ValidatorsPage = () => {
const [ activeTab, setActiveTab ] = React.useState<TabId>(TABS.dashboard);
const { validators, stats, isLoading } = useCurrentValidators();
const { validators, stats, isLoading, isKnown } = useCurrentValidators();
const handleDashboardClick = React.useCallback(() => {
setActiveTab(TABS.dashboard);
@@ -83,12 +85,12 @@ const ValidatorsPage = () => {
onClick={ handleDashboardClick }
/>
<TabButton
label={ `Validators (${ stats.validatorCount })` }
label={ isKnown ? `Validators (${ stats.validatorCount })` : 'Validators' }
isActive={ activeTab === TABS.validators }
onClick={ handleValidatorsClick }
/>
<TabButton
label={ `Delegators (${ stats.delegatorCount })` }
label={ isKnown ? `Delegators (${ stats.delegatorCount })` : 'Delegators' }
isActive={ activeTab === TABS.delegators }
onClick={ handleDelegatorsClick }
/>
@@ -100,6 +102,7 @@ const ValidatorsPage = () => {
validators={ validators }
stats={ stats }
isLoading={ isLoading }
isKnown={ isKnown }
/>
) }