mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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:
@@ -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
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }/>
|
||||
|
||||
@@ -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 }
|
||||
/>
|
||||
) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user