Files
explore/ui/pages/NetworkOverview.tsx
T
zeekayandHanzo Dev a3caebadf2 search: the icon sat on the placeholder, and validators counted one L1 of four
The magnifying glass was drawn over the leading "S" of the placeholder. The
cause is not the search bar: @luxfi/ui's InputGroup measures its adornments
correctly and then throws the measurement away, because the clone is guarded on
`displayName === 'FieldInput'` — a component that does not exist in the package
— and the props it would have set (`ps`/`pe`) are Chakra style props that the
Tailwind/gui Input spreads onto the DOM and ignores. Both halves are leftovers
of the Chakra→Tailwind port, so EVERY InputGroup in the app is unpadded; the
header is just the one you look at every day. Patched at the primitive
(patches/@[email protected]) rather than hand-tuning padding at one call
site: the offset is the measured width of the actual adornment, so it stays
right when the adornment changes, and FilterInput / revoke's SearchInput / the
showcase are fixed by the same change. Measured at 1440x900: the icon overlapped
the text by 24px and the kbd chip by 52px; both are now 0.

The measurement also moves to a layout effect and stops blanking value and
placeholder on first render. That blanking was inert while the guard swallowed
the clone; with the clone live it flips the field from controlled to
uncontrolled on mount, which React warns about and which can eat a keystroke.

ClearButton spread `className` over its own visibility classes, so a caller
passing one clobbered `invisible` — the header showed a clear button on an empty
field. Merged instead, at the component.

Placeholder is now "Search or ask anything", and Cmd/Ctrl-K opens an assistant.
Nothing in this repo, @luxfi/ui or @hanzo/gui ships a command palette or chat
(no cmdk, no palette, one dialog primitive), so this is the smallest thing that
works: the existing Dialog, one global listener, and a server-side proxy that
holds the credential. The key is deliberately not NEXT_PUBLIC_, so a deployment
without one gets a dialog that says exactly that instead of failing silently.
Off unless NEXT_PUBLIC_AI_ASSISTANT_ENABLED is set — Ctrl+K is the browser's
address-bar shortcut and claiming it to show an error would be worse than not
claiming it.

Validators read 5 on a network running 20. useCurrentValidators asks ONE
P-Chain, the one this deployment points at, but Lux, Hanzo, Zoo and Pars are
each sovereign L1s with their own P-Chain and their own validator set — measured
2026-08-06, five each, with four disjoint NodeID sets. The registry gains the
node origin per chain; absence of it is the signal that a chain has no publicly
reachable node (SPC and Osage are in-cluster only), and those are reported
`unavailable` rather than counted as zero. Stake is deliberately NOT summed:
each chain bonds its own currency.

The proxy resolves `?chain=` through the registry and never from the request, so
it cannot be pointed at an arbitrary origin. The new envs are registered with
the envs-validator and documented in ENVS.md — the schema rejects unknown
NEXT_PUBLIC_* variables and runs at container start, so an undeclared one stops
the pod from booting.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 19:17:01 -07:00

458 lines
19 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, useNetworkValidators } 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' },
{ name: 'SPC', href: 'https://explore-spc.lux.network' },
{ name: 'Pars', href: 'https://explore-pars.lux.network' },
];
const L1_EXPLORER_URLS: Readonly<Record<string, string>> = {
Zoo: 'https://explore-zoo.lux.network',
Hanzo: 'https://explore-hanzo.lux.network',
SPC: 'https://explore-spc.lux.network',
Pars: 'https://explore-pars.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;
readonly height?: number;
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 > 0 ? `#${ 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>
);
// ── Main page ──
const NetworkOverview = () => {
const { stats, isLoading: validatorsLoading, isKnown: hasValidatorData, isError: validatorsError } = useCurrentValidators();
const allValidators = useNetworkValidators();
const { blockchains, isLoading: chainsLoading } = useBlockchains();
const { pChainHeight, cChainHeight, isLoading: heightsLoading } = useChainHeights();
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 showFallbackL1 = !chainsLoading && !hasL1Data;
const totalChains = PRIMARY_CHAINS.length + (hasL1Data ? l1Chains.length : KNOWN_L1_CHAINS.length);
const isLoading = validatorsLoading || chainsLoading;
// 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
// chain — Hero + Stats + Latest blocks/txns — never the parent's chains.
const isPrimary = isPrimaryNetworkExplorer();
return (
<HomeRpcDataContextProvider>
<div className="flex flex-col gap-6 lg:gap-8">
{ /* ── Hero ── */ }
<HeroBanner/>
{ /* ── Metrics strip (Lux primary network only) ── */ }
{ isPrimary && (
<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)]',
) }>
{ /* Every L1 in the registry, not just this chain. The breakdown is
in the Validators card below. Stake stays primary-network only:
each chain bonds its own currency and the sum of two currencies
is not a quantity. */ }
<Metric
label={ allValidators.answeredCount === allValidators.queriedCount ? 'Validators' : 'Validators (partial)' }
value={ allValidators.isKnown ? String(allValidators.total) : '\u2014' }
isLoading={ allValidators.isLoading }
/>
<div className="w-px h-7 hidden md:block bg-[var(--color-border-divider)]"/>
<Metric
label="Staked"
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. platform.getCurrentValidators
reports uptime=0 and connected=null for every validator on the public
RPC (the API node does not track peer uptime), so 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>
) }
{ /* ── 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 && (
<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;
if (chain.id === 'P') return pChainHeight;
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>
{ /* Per-L1 breakdown. The strip above sums these; without the
split, a chain that stopped answering just makes the
total quietly smaller. */ }
<div className="flex flex-col gap-1 mt-4 pt-3 border-t border-[var(--color-border-divider)]">
{ allValidators.networks.map((network) => (
<div key={ network.chainId } className="flex items-center justify-between text-xs">
<span className="text-[var(--color-text-secondary)]">{ network.name }</span>
<Skeleton loading={ network.isLoading }>
<span className={ cn(
'font-mono',
network.status === 'live' ?
'text-[var(--color-text-primary)]' :
'text-[var(--color-text-secondary)]',
) }>
{ network.status === 'live' ? network.validatorCount : network.status }
</span>
</Skeleton>
</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>
) }
</div>
</HomeRpcDataContextProvider>
);
};
export default React.memo(NetworkOverview);