mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
styles/tokens.css was a 494-line FORK of @luxfi/ui/tokens.css, which is how it drifted its own browns: --color-status-warn #B45309 / #FBBF7A and the whole Chakra orange + yellow badge ramp (#FEEBCB #FFFAF0 #DD6B20 #FFFFF0 #D69E2E #FEFCBF #B7791F #C05621 #7B341E #744210 #FAF089). It imports the umbrella now and adds nothing of its own. next.config.js hand-listed SEVEN of the engine's 55 packages in transpilePackages and deduped none of them, which is how this app's store holds seven physical copies of @hanzogui/popper (3.0.0, 3.0.1, 3.0.2 x3, 7.3.0 x2). A context published at module scope is per-copy, so PopperAnchor read the empty default and called refs.setReference on undefined once per tooltip trigger. It uses withLuxUi now — the one place that wiring lives. 47 files reached for Tailwind's chromatic ramps directly (108 occurrences). They resolve through tokens now, under short semantic names registered in the @theme block: text-good / text-bad / text-warn, where warn is ACHROMATIC — attention in Lux is the loudest neutral, not a hue. The Quasar certificate tooltip stops naming colours it no longer paints ("BLS (green)", "Corona (blue)", "ML-DSA (purple)") — copy that describes a palette has to change with it. Measured on the rendered pages: warm 0%, tinted-neutral 0.0003%, mean chroma 0.004 (/) and 0.003 (/txs) at 1440x900, 0.000 at 390x844, both modes; zero uncaught setReference errors. Two pre-existing defects surfaced because lint-staged now lints these files and are fixed here rather than skipped: `Box` was used but never imported in AddressEntity.pw.tsx, and eight lines already exceeded the 160-column limit. Co-authored-by: Hanzo Dev <[email protected]>
464 lines
20 KiB
TypeScript
464 lines
20 KiB
TypeScript
import { Skeleton } from '@luxfi/ui/skeleton';
|
|
import { Tag } from '@luxfi/ui/tag';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useRouter } from 'next/router';
|
|
import React from 'react';
|
|
|
|
import config from 'configs/app';
|
|
import type { PrimaryVm } from 'configs/app/primaryChains';
|
|
import { getPrimaryVm } from 'configs/app/primaryChains';
|
|
import { useBlockchains, useCurrentValidators, useNets } from 'lib/api/pchain';
|
|
import type { PChainBlockchain, PChainValidator } from 'lib/api/pchain';
|
|
import { cn } from 'lib/utils/cn';
|
|
import DexPage from 'ui/dex/DexPage';
|
|
import CopyToClipboard from 'ui/shared/CopyToClipboard';
|
|
import PageTitle from 'ui/shared/Page/PageTitle';
|
|
import PrimaryNetworkGuard from 'ui/shared/PrimaryNetworkGuard';
|
|
|
|
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;
|
|
|
|
// Primary-network VM identity (name / vm / vmId / description / view) comes from
|
|
// the single source of truth in configs/app/primaryChains.ts.
|
|
|
|
const L1_EVM_CHAIN_IDS: Readonly<Record<string, number>> = {
|
|
zoo: 200200,
|
|
hanzo: 36963,
|
|
spc: 36911,
|
|
pars: 494949,
|
|
};
|
|
|
|
const L1_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
|
zoo: 'Zoo is an L1 blockchain on the network for the Zoo Labs Foundation open AI research network.',
|
|
hanzo: 'Hanzo is an L1 blockchain on the network for Hanzo AI infrastructure and agent frameworks.',
|
|
spc: 'SPC is an L1 blockchain on the network.',
|
|
pars: 'Pars is an L1 blockchain on the network.',
|
|
};
|
|
|
|
const 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',
|
|
};
|
|
|
|
const KNOWN_VM_IDS: Readonly<Record<string, string>> = {
|
|
mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6: 'EVM',
|
|
ag3GReYPNuSR17rUP8acMdZipQBikdXNRKDyFszAysmy3vDXE: 'L2',
|
|
jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq: 'XVM',
|
|
rWhpuQPF1kb72esV2momhMuTYGkEb1oL29pt2EBXWsBY6MALT: 'PVM',
|
|
};
|
|
|
|
// DAG chain indexer API URLs — one endpoint per primary network chain so
|
|
// ChainDetailPage can fetch live stats (block height, vertex count, etc.)
|
|
// for any chain the user opens. Missing entries here mean the detail page
|
|
// falls back to P-Chain blockchain metadata only, with no realtime stats.
|
|
const CHAIN_INDEXER_URLS: Readonly<Record<string, string>> = {
|
|
'c-chain': 'https://api-indexer.lux.network',
|
|
'p-chain': 'https://api-indexer-pchain.lux.network',
|
|
'x-chain': 'https://api-indexer-xchain.lux.network',
|
|
'a-chain': 'https://api-indexer-achain.lux.network',
|
|
'b-chain': 'https://api-indexer-bchain.lux.network',
|
|
'd-chain': 'https://api-indexer-dchain.lux.network',
|
|
'g-chain': 'https://api-indexer-gchain.lux.network',
|
|
'i-chain': 'https://api-indexer-ichain.lux.network',
|
|
'k-chain': 'https://api-indexer-kchain.lux.network',
|
|
'm-chain': 'https://api-indexer-mchain.lux.network',
|
|
'o-chain': 'https://api-indexer-ochain.lux.network',
|
|
'q-chain': 'https://api-indexer-qchain.lux.network',
|
|
'r-chain': 'https://api-indexer-rchain.lux.network',
|
|
't-chain': 'https://api-indexer-tchain.lux.network',
|
|
'z-chain': 'https://api-indexer-zchain.lux.network',
|
|
};
|
|
|
|
interface IndexerStats {
|
|
readonly chain_stats: Record<string, unknown>;
|
|
readonly dag_stats?: {
|
|
readonly total_vertices: number;
|
|
readonly total_edges: number;
|
|
readonly accepted_vertices: number;
|
|
readonly pending_vertices: number;
|
|
readonly chain_type: string;
|
|
readonly last_updated: string;
|
|
};
|
|
}
|
|
|
|
function useChainIndexerStats(slug: string): { stats: IndexerStats | null; isLoading: boolean } {
|
|
const indexerUrl = CHAIN_INDEXER_URLS[slug];
|
|
const query = useQuery({
|
|
queryKey: [ 'chainIndexerStats', slug ],
|
|
queryFn: async() => {
|
|
if (!indexerUrl) return null;
|
|
const res = await fetch(`${ indexerUrl }/stats`);
|
|
if (!res.ok) return null;
|
|
return await res.json() as IndexerStats;
|
|
},
|
|
staleTime: 30_000,
|
|
enabled: Boolean(indexerUrl),
|
|
});
|
|
return { stats: query.data ?? null, isLoading: query.isLoading };
|
|
}
|
|
|
|
function formatStake(nanoLux: bigint): string {
|
|
const lux = Number(nanoLux) / Math.pow(10, LUX_DECIMALS);
|
|
if (lux >= 1_000_000) return `${ (lux / 1_000_000).toFixed(2) }M`;
|
|
if (lux >= 1_000) return `${ (lux / 1_000).toFixed(2) }K`;
|
|
return lux.toFixed(2);
|
|
}
|
|
|
|
function truncateId(id: string, len: number = 16): string {
|
|
if (id.length <= len) return id;
|
|
return `${ id.slice(0, 8) }...${ id.slice(-6) }`;
|
|
}
|
|
|
|
interface InfoRowProps {
|
|
readonly label: string;
|
|
readonly value: string;
|
|
readonly isMono?: boolean;
|
|
readonly canCopy?: boolean;
|
|
}
|
|
|
|
const InfoRow = ({ label, value, isMono = false, canCopy = false }: InfoRowProps) => (
|
|
<div className={ cn(
|
|
'flex py-3 px-4 border-b border-[var(--color-border-divider)] gap-4 flex-wrap lg:flex-nowrap',
|
|
'odd:bg-[var(--color-gray-50)] dark:odd:bg-[var(--color-whiteAlpha-50)]',
|
|
) }>
|
|
<div className="min-w-full lg:min-w-[200px] shrink-0 text-[var(--color-text-secondary)] text-sm font-medium">
|
|
{ label }
|
|
</div>
|
|
<div className="flex-1 flex items-center gap-1 min-w-0">
|
|
<span className={ cn('text-sm text-[var(--color-text-primary)] break-all', isMono && 'font-mono') }>
|
|
{ value }
|
|
</span>
|
|
{ canCopy && <CopyToClipboard text={ value }/> }
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
interface ValidatorRowProps {
|
|
readonly validator: PChainValidator;
|
|
readonly index: number;
|
|
}
|
|
|
|
const ValidatorRow = ({ validator, index }: ValidatorRowProps) => {
|
|
const stake = BigInt(validator.stakeAmount || validator.weight || '0');
|
|
const uptime = parseFloat(validator.uptime || '0') * 100;
|
|
|
|
return (
|
|
<div className={ cn(
|
|
'flex py-3 px-4 border-b border-[var(--color-border-divider)] gap-4 items-center flex-wrap lg:flex-nowrap',
|
|
'transition-colors duration-150 hover:bg-[var(--color-gray-50)] dark:hover:bg-[var(--color-whiteAlpha-50)]',
|
|
) }>
|
|
<div className="w-10 shrink-0 text-[var(--color-text-secondary)] text-sm">
|
|
{ index + 1 }
|
|
</div>
|
|
<div className="flex-[2] min-w-0">
|
|
<span className="text-sm font-mono text-[var(--color-text-primary)]" title={ validator.nodeID }>
|
|
{ truncateId(validator.nodeID, 24) }
|
|
</span>
|
|
</div>
|
|
<div className="flex-1 text-right">
|
|
<span className="text-sm text-[var(--color-text-primary)] font-medium">
|
|
{ formatStake(stake) } { config.chain.currency.symbol || 'LUX' }
|
|
</span>
|
|
</div>
|
|
<div className="w-20 text-right shrink-0">
|
|
<span className="text-sm text-[var(--color-text-primary)]">
|
|
{ uptime.toFixed(1) }%
|
|
</span>
|
|
</div>
|
|
<div className="w-10 shrink-0 text-center">
|
|
<div className={ cn('w-2 h-2 rounded-full inline-block', validator.connected ? 'bg-good' : 'bg-gray-400') }/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const ChainDetailPage = () => {
|
|
const router = useRouter();
|
|
const slug = (router.query.slug as string || '').toLowerCase();
|
|
|
|
const { blockchains, isLoading: chainsLoading } = useBlockchains();
|
|
const { validators, isLoading: validatorsLoading } = useCurrentValidators();
|
|
const { nets } = useNets();
|
|
const { stats: indexerStats } = useChainIndexerStats(slug);
|
|
|
|
const resolvedChain = React.useMemo<{
|
|
blockchain: PChainBlockchain | undefined;
|
|
isPrimary: boolean;
|
|
meta: PrimaryVm | undefined;
|
|
}>(() => {
|
|
// Check primary chains first
|
|
const primaryMeta = getPrimaryVm(slug);
|
|
if (primaryMeta) {
|
|
return { blockchain: undefined, isPrimary: true, meta: primaryMeta };
|
|
}
|
|
|
|
// Check L1 chains
|
|
const chain = blockchains.find((c) =>
|
|
c.name.toLowerCase() === slug ||
|
|
c.id.toLowerCase() === slug,
|
|
);
|
|
return { blockchain: chain, isPrimary: false, meta: undefined };
|
|
}, [ slug, blockchains ]);
|
|
|
|
const chainName = resolvedChain.meta?.name ??
|
|
resolvedChain.blockchain?.name ??
|
|
slug;
|
|
const chainDescription = resolvedChain.meta?.description ??
|
|
L1_DESCRIPTIONS[slug] ??
|
|
`${ chainName } is a blockchain on the network.`;
|
|
|
|
const blockchainId = resolvedChain.blockchain?.id ?? '';
|
|
const netId = resolvedChain.blockchain?.netID ?? (resolvedChain.isPrimary ? PRIMARY_NETWORK_ID : '');
|
|
const vmId = resolvedChain.blockchain?.vmID ?? resolvedChain.meta?.vmId ?? '';
|
|
const vmName = KNOWN_VM_IDS[vmId] ?? (vmId ? truncateId(vmId) : 'Unknown');
|
|
const chainId = L1_EVM_CHAIN_IDS[slug];
|
|
const explorerUrl = EXPLORER_URLS[slug];
|
|
const isLoading = chainsLoading || validatorsLoading;
|
|
|
|
const net = React.useMemo(
|
|
() => nets.find((n) => n.id === netId),
|
|
[ nets, netId ],
|
|
);
|
|
|
|
const netChains = React.useMemo(
|
|
() => netId ? blockchains.filter((c) => c.netID === netId) : [],
|
|
[ blockchains, netId ],
|
|
);
|
|
|
|
const totalStake = React.useMemo(
|
|
() => validators.reduce((sum, v) => sum + BigInt(v.stakeAmount || v.weight || '0'), BigInt(0)),
|
|
[ validators ],
|
|
);
|
|
|
|
// D-Chain (DexVM) renders its native order-book model — the DEX UI — instead
|
|
// of the generic validators/stats detail. DexPage self-guards to the primary
|
|
// explorer, so /chains/d-chain can never leak onto a brand explorer.
|
|
if (resolvedChain.meta?.view === 'dex') {
|
|
return <DexPage/>;
|
|
}
|
|
|
|
return (
|
|
<PrimaryNetworkGuard title={ chainName }>
|
|
<PageTitle
|
|
title={ chainName }
|
|
secondRow={ (
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-[var(--color-text-secondary)]">Chain Details</span>
|
|
{ resolvedChain.isPrimary && <Tag size="sm" variant="subtle">Primary Network</Tag> }
|
|
{ !resolvedChain.isPrimary && resolvedChain.blockchain && <Tag size="sm" variant="subtle">L1</Tag> }
|
|
</div>
|
|
) }
|
|
/>
|
|
|
|
<div className="p-4 mb-6 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
|
<span className="text-sm text-[var(--color-text-secondary)] leading-relaxed">
|
|
{ chainDescription }
|
|
</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
|
|
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
|
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
|
Validators
|
|
</span>
|
|
<Skeleton loading={ isLoading }>
|
|
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
|
{ validators.length }
|
|
</span>
|
|
</Skeleton>
|
|
</div>
|
|
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
|
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
|
Total Stake
|
|
</span>
|
|
<Skeleton loading={ isLoading }>
|
|
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
|
{ formatStake(totalStake) } { config.chain.currency.symbol || 'LUX' }
|
|
</span>
|
|
</Skeleton>
|
|
</div>
|
|
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
|
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
|
Chains on Network
|
|
</span>
|
|
<Skeleton loading={ isLoading }>
|
|
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
|
{ netChains.length }
|
|
</span>
|
|
</Skeleton>
|
|
</div>
|
|
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
|
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
|
Threshold
|
|
</span>
|
|
<Skeleton loading={ isLoading }>
|
|
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
|
{ net?.threshold ?? '-' }
|
|
</span>
|
|
</Skeleton>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
<span className="block text-sm font-semibold text-[var(--color-text-primary)] mb-3">
|
|
Chain Info
|
|
</span>
|
|
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
|
<InfoRow label="Chain Name" value={ chainName }/>
|
|
{ blockchainId && <InfoRow label="Blockchain ID" value={ blockchainId } isMono canCopy/> }
|
|
{ netId && <InfoRow label="Network ID" value={ netId } isMono canCopy/> }
|
|
<InfoRow label="VM Name" value={ vmName }/>
|
|
{ vmId && <InfoRow label="VM ID" value={ vmId } isMono canCopy/> }
|
|
{ chainId != null && <InfoRow label="EVM Chain ID" value={ String(chainId) }/> }
|
|
{ explorerUrl && <InfoRow label="Explorer" value={ explorerUrl }/> }
|
|
{ resolvedChain.isPrimary && <InfoRow label="Network" value="Primary Network"/> }
|
|
{ !resolvedChain.isPrimary && <InfoRow label="Network" value="Sovereign L1"/> }
|
|
</div>
|
|
</div>
|
|
|
|
{ indexerStats && (
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
|
Indexer Stats
|
|
</span>
|
|
{ indexerStats.dag_stats && (
|
|
<Tag size="sm" variant="subtle">
|
|
{ indexerStats.dag_stats.chain_type.toUpperCase() }
|
|
</Tag>
|
|
) }
|
|
</div>
|
|
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
|
{ indexerStats.dag_stats && (
|
|
<>
|
|
<InfoRow label="Total Vertices" value={ String(indexerStats.dag_stats.total_vertices) }/>
|
|
<InfoRow label="Total Edges" value={ String(indexerStats.dag_stats.total_edges) }/>
|
|
<InfoRow label="Accepted Vertices" value={ String(indexerStats.dag_stats.accepted_vertices) }/>
|
|
<InfoRow label="Pending Vertices" value={ String(indexerStats.dag_stats.pending_vertices) }/>
|
|
<InfoRow label="Last Updated" value={ indexerStats.dag_stats.last_updated }/>
|
|
</>
|
|
) }
|
|
{ Object.entries(indexerStats.chain_stats).map(([ key, value ]) => (
|
|
<InfoRow
|
|
key={ key }
|
|
label={ key.split('_').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') }
|
|
value={ typeof value === 'object' ? JSON.stringify(value) : String(value) }
|
|
/>
|
|
)) }
|
|
</div>
|
|
</div>
|
|
) }
|
|
|
|
{ netChains.length > 0 && (
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
|
Chains on This Network
|
|
</span>
|
|
<Tag size="sm" variant="subtle">{ netChains.length }</Tag>
|
|
</div>
|
|
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
|
<div className="hidden lg:flex px-4 py-2 gap-4 border-b border-[var(--color-border-divider)]">
|
|
<div className="flex-1 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
Name
|
|
</div>
|
|
<div className="flex-[2] text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
Blockchain ID
|
|
</div>
|
|
<div className="w-[120px] text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
VM
|
|
</div>
|
|
</div>
|
|
{ netChains.map((chain) => (
|
|
<div
|
|
key={ chain.id }
|
|
className={ cn(
|
|
'flex px-4 py-3 gap-4 border-b border-[var(--color-border-divider)] last:border-b-0 flex-wrap lg:flex-nowrap',
|
|
'transition-colors duration-150 hover:bg-[var(--color-gray-50)] dark:hover:bg-[var(--color-whiteAlpha-50)]',
|
|
) }
|
|
>
|
|
<div className="flex-1">
|
|
<span className="text-sm font-medium text-[var(--color-text-primary)]">
|
|
{ chain.name }
|
|
</span>
|
|
</div>
|
|
<div className="flex-[2] min-w-0">
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-sm font-mono text-[var(--color-text-secondary)]" title={ chain.id }>
|
|
{ truncateId(chain.id, 24) }
|
|
</span>
|
|
<CopyToClipboard text={ chain.id } size="2xs"/>
|
|
</div>
|
|
</div>
|
|
<div className="w-[120px]">
|
|
<div className={ cn(
|
|
'inline-block bg-[var(--color-gray-100)] dark:bg-[var(--color-whiteAlpha-100)]',
|
|
'text-[var(--color-text-secondary)] rounded-sm px-2 py-0.5 text-xs font-mono',
|
|
) }>
|
|
{ KNOWN_VM_IDS[chain.vmID] ?? truncateId(chain.vmID, 12) }
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)) }
|
|
</div>
|
|
</div>
|
|
) }
|
|
|
|
<div className="mb-6">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
|
Validators
|
|
</span>
|
|
<Skeleton loading={ validatorsLoading }>
|
|
<Tag size="sm" variant="subtle">{ validators.length }</Tag>
|
|
</Skeleton>
|
|
</div>
|
|
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
|
<div className="hidden lg:flex px-4 py-2 gap-4 border-b border-[var(--color-border-divider)] items-center">
|
|
<div className="w-10 shrink-0 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
#
|
|
</div>
|
|
<div className="flex-[2] text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
Node ID
|
|
</div>
|
|
<div className="flex-1 text-right text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
Stake
|
|
</div>
|
|
<div className="w-20 text-right shrink-0 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
Uptime
|
|
</div>
|
|
<div className="w-10 shrink-0 text-center text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
|
{ '\u2022' }
|
|
</div>
|
|
</div>
|
|
|
|
{ validatorsLoading && (
|
|
<div className="px-4 py-6">
|
|
<Skeleton loading h="16px" mb={ 3 }/>
|
|
<Skeleton loading h="16px" mb={ 3 }/>
|
|
<Skeleton loading h="16px" mb={ 3 }/>
|
|
<Skeleton loading h="16px"/>
|
|
</div>
|
|
) }
|
|
|
|
{ !validatorsLoading && validators.length === 0 && (
|
|
<div className="px-4 py-8 text-center text-[var(--color-text-secondary)] text-sm">
|
|
No validators found
|
|
</div>
|
|
) }
|
|
|
|
{ !validatorsLoading && validators.map((v, i) => (
|
|
<ValidatorRow key={ v.nodeID } validator={ v } index={ i }/>
|
|
)) }
|
|
</div>
|
|
</div>
|
|
</PrimaryNetworkGuard>
|
|
);
|
|
};
|
|
|
|
export default ChainDetailPage;
|