Files
explore/ui/pages/NetworkOverview.tsx
zeekay 4045def1a0 A brand explorer's home reads nothing about the Lux primary network
Zoo's home rendered no P-Chain panel but still sent the validators, chain list
and height reads behind the Lux panels, because the hooks ran in the page body.
The metrics strip and the chain-health grid are now components the Lux explorer
alone renders, and the reads live inside them. The stats page's chain count,
shown only on Lux, reads the chain list the same way.

NetworkOverview.spec.tsx asserts a brand host sends no P-Chain request and the
Lux host sends all three; putting the reads back in the page body fails it.

Claude-Session: https://claude.ai/code/session_011Ge9UnfkEK37Lapx5nfPQK
2026-09-23 20:38:36 -07:00

453 lines
17 KiB
TypeScript

import { Heading } from '@luxfi/ui/heading';
import { Skeleton } from '@luxfi/ui/skeleton';
import { Tag } from '@luxfi/ui/tag';
import React from 'react';
import config from 'configs/app';
import { isPrimaryNetworkExplorer } from 'configs/app/chainRegistry';
import { useBlockchains, useChainHeights, useCurrentValidators } from 'lib/api/pchain';
import type { PChainBlockchain } from 'lib/api/pchain';
import { cn } from 'lib/utils/cn';
import { Link } from 'toolkit/next/link';
import { HomeRpcDataContextProvider } from 'ui/home/fallbacks/rpcDataContext';
import HeroBanner from 'ui/home/HeroBanner';
import LatestBlocks from 'ui/home/LatestBlocks';
import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions';
import FeeSplitPanel from 'ui/stats/lux/FeeSplitPanel';
const PRIMARY_NETWORK_ID = '11111111111111111111111111111111LpoYY';
// nLUX is NANO-LUX: 10^9 nLUX = 1 LUX. This read 6, inflating every LUX figure
// by 1000 — the validators page showed 2.5T LUX staked against a ~2T supply.
const LUX_DECIMALS = 9;
const PRIMARY_CHAINS = [
{ id: 'C', name: 'C-Chain', fullName: 'Contract Chain', vm: 'EVM', href: '/' },
{ id: 'P', name: 'P-Chain', fullName: 'Platform Chain', vm: 'PVM', href: '/validators' },
{ id: 'X', name: 'X-Chain', fullName: 'UTXO Chain', vm: 'XVM', href: '/chain/x-chain' },
{ id: 'D', name: 'D-Chain', fullName: 'DEX Chain', vm: 'DexVM', href: '/dex' },
{ id: 'A', name: 'A-Chain', fullName: 'AI Chain', vm: 'AIVM', href: '/ai' },
{ id: 'B', name: 'B-Chain', fullName: 'Bridge Chain', vm: 'BridgeVM', href: '/bridge' },
{ id: 'Q', name: 'Q-Chain', fullName: 'Quantum Chain', vm: 'QuantumVM', href: '/chain/q-chain' },
{ id: 'M', name: 'M-Chain', fullName: 'MPC Chain', vm: 'MPCVM', href: '/chain/m-chain' },
{ id: 'Z', name: 'Z-Chain', fullName: 'ZK Chain', vm: 'ZKVM', href: '/chain/z-chain' },
{ id: 'G', name: 'G-Chain', fullName: 'Graph Chain', vm: 'GraphVM', href: '/chain/g-chain' },
{ id: 'K', name: 'K-Chain', fullName: 'Key Chain', vm: 'KeyVM', href: '/chain/k-chain' },
{ id: 'O', name: 'O-Chain', fullName: 'Oracle Chain', vm: 'OracleVM', href: '/chain/o-chain' },
{ id: 'R', name: 'R-Chain', fullName: 'Relay Chain', vm: 'RelayVM', href: '/chain/r-chain' },
{ id: 'I', name: 'I-Chain', fullName: 'Identity Chain', vm: 'IdentityVM', href: '/chain/i-chain' },
] as const;
// Known L1 chains (fallback when P-chain API is unreachable from browser)
const KNOWN_L1_CHAINS: ReadonlyArray<{ readonly name: string; readonly href: string }> = [
{ name: 'Zoo', href: 'https://explore-zoo.lux.network' },
{ name: 'Hanzo', href: 'https://explore-hanzo.lux.network' },
];
const L1_EXPLORER_URLS: Readonly<Record<string, string>> = {
Zoo: 'https://explore-zoo.lux.network',
Hanzo: 'https://explore-hanzo.lux.network',
};
function formatStake(nanoLux: bigint): string {
const lux = Number(nanoLux) / Math.pow(10, LUX_DECIMALS);
if (lux >= 1_000_000_000) return `${ (lux / 1_000_000_000).toFixed(1) }B`;
if (lux >= 1_000_000) return `${ (lux / 1_000_000).toFixed(1) }M`;
if (lux >= 1_000) return `${ (lux / 1_000).toFixed(1) }K`;
return lux.toFixed(0);
}
// ── Hero metric (compact inline stat) ──
interface MetricProps {
readonly label: string;
readonly value: string;
readonly isLoading: boolean;
}
const Metric = ({ label, value, isLoading }: MetricProps) => (
<div className="flex flex-col items-center px-4 py-1">
<Skeleton loading={ isLoading }>
<span className="font-mono text-lg text-[var(--color-text-primary)] font-bold leading-tight">
{ value }
</span>
</Skeleton>
<span className="text-2xs uppercase tracking-[0.08em] text-[var(--color-text-secondary)] mt-1 font-medium">
{ label }
</span>
</div>
);
// ── Chain row (compact, for sidebar) ──
interface ChainRowProps {
readonly name: string;
readonly fullName: string;
readonly vm: string;
readonly href: string | undefined;
readonly tier?: string;
/** Absent: this chain shows no head. Null: its head could not be read. */
readonly height?: number | null;
readonly heightLoading?: boolean;
}
const ChainRow = ({
name, fullName, vm, href, tier, height, heightLoading,
}: ChainRowProps) => {
const content = (
<div className={ cn(
'flex items-center justify-between transition-all rounded-md px-3 py-2.5 gap-3',
href ?
'cursor-pointer hover:bg-[var(--color-gray-100)] dark:hover:bg-[var(--color-whiteAlpha-100)]' :
'cursor-default',
) }>
<div className="flex items-center gap-2 min-w-0">
<span className="text-sm text-[var(--color-text-primary)] font-semibold whitespace-nowrap">
{ name }
</span>
<span className="text-xs text-[var(--color-text-secondary)] hidden lg:inline whitespace-nowrap">
{ fullName }
</span>
</div>
<div className="flex items-center gap-2 shrink-0">
{ height !== undefined && (
<Skeleton loading={ heightLoading }>
<span className="font-mono text-xs text-[var(--color-text-secondary)]">
{ height === null ? '' : `#${ height.toLocaleString() }` }
</span>
</Skeleton>
) }
{ tier && <Tag size="sm" variant="subtle">{ tier }</Tag> }
<Tag size="sm" variant="subtle">{ vm }</Tag>
</div>
</div>
);
if (href) {
return <Link href={ href } variant="plain">{ content }</Link>;
}
return content;
};
// ── L1 Chain row ──
interface L1ChainRowProps {
readonly chain: PChainBlockchain;
}
const L1ChainRow = ({ chain }: L1ChainRowProps) => {
const explorerUrl = L1_EXPLORER_URLS[chain.name];
const slug = chain.name.toLowerCase();
const content = (
<div className={ cn(
'flex items-center justify-between cursor-pointer transition-colors rounded-md px-3 py-2',
'hover:bg-[var(--color-gray-100)] dark:hover:bg-[var(--color-whiteAlpha-100)]',
) }>
<div className="flex items-center gap-2">
<span className="text-sm text-[var(--color-text-primary)] font-semibold">
{ chain.name }
</span>
<span className="font-mono text-xs text-[var(--color-text-secondary)] hidden lg:inline">
{ chain.id.slice(0, 8) }...
</span>
</div>
<div className="flex items-center gap-1.5">
<Tag size="sm" variant="subtle">L1</Tag>
<span className="text-xs text-[var(--color-text-secondary)]">{ '\u2192' }</span>
</div>
</div>
);
if (explorerUrl) {
return <Link href={ explorerUrl } variant="plain" target="_blank">{ content }</Link>;
}
return <Link href={ `/chains/${ slug }` } variant="plain">{ content }</Link>;
};
// ── Known L1 chain row (fallback when API unavailable) ──
interface KnownL1RowProps {
readonly name: string;
readonly href: string;
}
const KnownL1Row = ({ name, href }: KnownL1RowProps) => (
<Link href={ href } variant="plain" target="_blank">
<div className={ cn(
'flex items-center justify-between cursor-pointer transition-colors rounded-md px-3 py-2',
'hover:bg-[var(--color-gray-100)] dark:hover:bg-[var(--color-whiteAlpha-100)]',
) }>
<span className="text-sm text-[var(--color-text-primary)] font-semibold">
{ name }
</span>
<div className="flex items-center gap-1.5">
<Tag size="sm" variant="subtle">L1</Tag>
<span className="text-xs text-[var(--color-text-secondary)]">{ '\u2192' }</span>
</div>
</div>
</Link>
);
// ── Section card ──
interface SectionCardProps {
readonly title: string;
readonly count?: number;
readonly isLoading?: boolean;
readonly action?: { label: string; href: string };
readonly children: React.ReactNode;
}
const SectionCard = ({ title, count, isLoading, action, children }: SectionCardProps) => (
<div className="rounded-lg p-5 border border-[var(--color-border-divider)] bg-[var(--color-stats-bg)]">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Heading level="3" className="text-sm">{ title }</Heading>
{ count !== undefined && (
<Skeleton loading={ isLoading }>
<Tag size="sm" variant="subtle">{ count }</Tag>
</Skeleton>
) }
</div>
{ action && (
<Link href={ action.href } className="text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
{ action.label }
</Link>
) }
</div>
{ children }
</div>
);
// ── Lux primary network panels ──
// The Lux primary network's figures. Only the panels the Lux explorer renders
// call this, so a brand explorer never reads what it does not show.
function usePrimaryNetwork() {
const validators = useCurrentValidators();
const { blockchains, isLoading: chainsLoading } = useBlockchains();
const l1Chains = React.useMemo(
() => blockchains.filter((c) => c.netID !== PRIMARY_NETWORK_ID),
[ blockchains ],
);
// Use known L1 chains as fallback when P-chain API is unreachable
const hasL1Data = l1Chains.length > 0;
const totalChains = PRIMARY_CHAINS.length + (hasL1Data ? l1Chains.length : KNOWN_L1_CHAINS.length);
return { ...validators, chainsLoading, l1Chains, hasL1Data, totalChains };
}
const PrimaryMetrics = () => {
const { stats, isLoading: validatorsLoading, isKnown: hasValidatorData, chainsLoading, totalChains } = usePrimaryNetwork();
const isLoading = validatorsLoading || chainsLoading;
return (
<div className={ cn(
'flex items-center justify-center flex-wrap overflow-hidden rounded-lg',
'py-4 px-4 gap-x-6 gap-y-3 border border-[var(--color-border-divider)]',
'bg-[var(--color-stats-bg)]',
) }>
{ /* Zoo is an L2 carried by these same validators, so this one set
is the whole of what secures the chains listed here. */ }
<Metric
label="Validators"
value={ hasValidatorData ? String(stats.validatorCount) : '\u2014' }
isLoading={ validatorsLoading }
/>
<div className="w-px h-7 hidden md:block bg-[var(--color-border-divider)]"/>
<Metric
label={ `Staked on ${ config.chain.name || 'primary' }` }
value={ hasValidatorData ?
`${ formatStake(stats.totalStake) } ${ config.chain.currency.symbol || 'LUX' }` :
'\u2014' }
isLoading={ validatorsLoading }
/>
<div className="w-px h-7 hidden md:block bg-[var(--color-border-divider)]"/>
<Metric
label="Chains"
value={ String(totalChains) }
isLoading={ isLoading }
/>
{ /*
Uptime / Connected are NOT shown here. They are the public API
node's view of its peers, and rendering them printed "0.0%" and
"0/5" on a healthy, fully-meshed network — the worst kind of wrong,
since it reads as an outage. Same reasoning as
ui/validators/lux/ValidatorsDashboard.tsx. Only chain-sourced,
verifiable metrics belong in this row.
*/ }
</div>
);
};
const PrimaryChainHealth = () => {
const {
stats, isLoading: validatorsLoading, isKnown: hasValidatorData, isError: validatorsError, chainsLoading, l1Chains, hasL1Data,
} = usePrimaryNetwork();
const { pChainHeight, cChainHeight, isLoading: heightsLoading } = useChainHeights();
const showFallbackL1 = !chainsLoading && !hasL1Data;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-6">
{ /* Primary Network chains */ }
<SectionCard
title="Primary Network"
count={ PRIMARY_CHAINS.length }
>
<div className="flex flex-col gap-0.5">
{ PRIMARY_CHAINS.map((chain) => {
const chainHeight = (() => {
if (chain.id === 'C') return cChainHeight ?? null;
if (chain.id === 'P') return pChainHeight ?? null;
return undefined;
})();
return (
<ChainRow
key={ chain.id }
name={ chain.name }
fullName={ chain.fullName }
vm={ chain.vm }
href={ chain.href }
height={ chainHeight }
heightLoading={ heightsLoading }
/>
);
}) }
</div>
</SectionCard>
{ /* L1 chains */ }
<SectionCard
title="Chains"
count={ hasL1Data ? l1Chains.length : KNOWN_L1_CHAINS.length }
isLoading={ chainsLoading }
action={{ label: 'View all', href: '/chains' }}
>
{ chainsLoading && (
<div className="flex flex-col gap-1">
{ Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={ i } loading h="40px" borderRadius="md"/>
)) }
</div>
) }
{ !chainsLoading && hasL1Data && (
<div className="flex flex-col gap-0.5">
{ l1Chains.map((chain) => (
<L1ChainRow key={ chain.id } chain={ chain }/>
)) }
</div>
) }
{ showFallbackL1 && (
<div className="flex flex-col gap-0.5">
{ KNOWN_L1_CHAINS.map((chain) => (
<KnownL1Row key={ chain.name } name={ chain.name } href={ chain.href }/>
)) }
</div>
) }
</SectionCard>
{ /* Validators summary card */ }
<SectionCard title="Validators">
{ !hasValidatorData && !validatorsLoading ? (
<div className="flex flex-col items-center py-4">
<span className="text-sm text-[var(--color-text-secondary)]">
{ validatorsError ? 'Unable to fetch validator data.' : 'No validator data available.' }
</span>
<Link href="/validators" className="text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] mt-2">
View validators
</Link>
</div>
) : (
<>
<div className="grid grid-cols-2 gap-4">
<div>
<Skeleton loading={ validatorsLoading }>
<span className="font-mono text-lg font-bold text-[var(--color-text-primary)]">
{ hasValidatorData ? stats.validatorCount : '\u2014' }
</span>
</Skeleton>
<span className="text-2xs text-[var(--color-text-secondary)]">Active</span>
</div>
<div>
<Skeleton loading={ validatorsLoading }>
<span className="font-mono text-lg font-bold text-[var(--color-text-primary)]">
{ hasValidatorData ? formatStake(stats.totalStake) : '\u2014' }
</span>
</Skeleton>
<span className="text-2xs text-[var(--color-text-secondary)]">Total Stake ({ config.chain.currency.symbol || 'LUX' })</span>
</div>
<div>
<Skeleton loading={ validatorsLoading }>
<span className="font-mono text-lg font-bold text-[var(--color-text-primary)]">
{ hasValidatorData ? stats.delegatorCount : '\u2014' }
</span>
</Skeleton>
<span className="text-2xs text-[var(--color-text-secondary)]">Delegators</span>
</div>
</div>
<div className="flex justify-center mt-4">
<Link href="/validators" className="text-xs text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]">
View validators
</Link>
</div>
</>
) }
</SectionCard>
</div>
);
};
// ── Main page ──
const NetworkOverview = () => {
// Primary-network panels (validator metrics strip + the primary-network
// chains + the cross-L1 list) belong ONLY on the Lux primary-network explorer.
// Brand explorers (Zoo / Hanzo, vm: 'L2') show ONLY their own chain — Hero +
// Stats + Latest blocks/txns — and read nothing about the parent network.
const isPrimary = isPrimaryNetworkExplorer();
return (
<HomeRpcDataContextProvider>
<div className="flex flex-col gap-6 lg:gap-8">
{ /* ── Hero ── */ }
<HeroBanner/>
{ /* ── Metrics strip (Lux primary network only) ── */ }
{ isPrimary && <PrimaryMetrics/> }
{ /* ── Stats widgets ── */ }
<Stats/>
{ /*
── Fee burn & staking rewards ──
Every chain, not just the primary network: the panel reads the chain
it is pointed at (vault balance + feeSplitTimestamp) and says so. It
lives here rather than on /stats because /stats 404s outright when the
stats microservice is not configured (getServerSideProps guards.stats),
and burn is a chain fact that owes nothing to an indexer.
*/ }
<FeeSplitPanel/>
{ /* ── Latest blocks ── */ }
<div className="rounded-lg border border-[var(--color-border-divider)] bg-[var(--color-stats-bg)] p-5 lg:p-6">
<LatestBlocks/>
</div>
{ /* ── Latest transactions ── */ }
<div className="rounded-lg border border-[var(--color-border-divider)] bg-[var(--color-stats-bg)] p-5 lg:p-6">
<Transactions/>
</div>
{ /* ── Chain Health (Lux primary network only) ── */ }
{ isPrimary && <PrimaryChainHealth/> }
</div>
</HomeRpcDataContextProvider>
);
};
export default React.memo(NetworkOverview);