mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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]>
458 lines
19 KiB
TypeScript
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);
|