Files
zeekay 0ac8927c3d The P-Chain is read through typed GETs, from the P-Chain that secures the chain
luxd serves no P-Chain JSON-RPC, so every platform.* read the explorer sent
answered 404 and the validators, chains and heights widgets were empty on Lux
and Zoo. Each read is now GET /v1/chain/P/ops/{validators,blockchains,height},
coded against the node's OpenAPI 3.1 document, through GET
/v1/node/p-chain?op=<op>. The proxy's fixed table allows those three ops and one
NodeID parameter; a POST there answers 405.

ChainEntry.pChain is where the P-Chain that secures a chain is read: Lux reads
its own; Zoo, an L2 carried by Lux's validator set, reads Lux's and never its
own node's; Hanzo, on its own committee until hanzod joins as an L2, has none.
P-Chain UI names whose it is ("Lux primary network validators securing Zoo
Network") and prices stake in LUX. The cross-network validator sum is gone: it
counted Lux's validators again under Zoo.

The top bar shows a link only where its page has content: Chains, Bridge and
DEX on the Lux C-Chain host (their pages now 404 elsewhere), Validators where a
P-Chain secures the chain, Stats where there is a P-Chain panel or a stats
service. A P-Chain at genesis renders height #0 rather than nothing. The chain
page's uptime is no longer multiplied by 100.

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

232 lines
9.0 KiB
TypeScript

import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from '@luxfi/ui/menu';
import { useRouter } from 'next/router';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import { getCurrentChain, getPChain, isChainSelectorEnabled, isNetworkSelectorEnabled, isPrimaryNetworkExplorer } from 'configs/app/chainRegistry';
import { cn } from 'lib/utils/cn';
import { Link } from 'toolkit/next/link';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import SearchBarAssistant from 'ui/snippets/searchBar/SearchBarAssistant';
import SearchBar from 'ui/snippets/searchBar/SearchBarDesktop';
import UserProfileDesktop from 'ui/snippets/user/UserProfileDesktop';
import ChainSwitcher from './ChainSwitcher';
import NetworkSelector from './NetworkSelector';
// -- Nav link --
interface NavLinkProps {
readonly text: string;
readonly href: string;
readonly isActive: boolean;
}
const NavLinkItem = ({ text, href, isActive }: NavLinkProps) => (
<Link
href={ href }
className={ cn(
'px-2 py-1 text-xs rounded-sm no-underline whitespace-nowrap transition-all duration-150',
'hover:text-[var(--color-text-primary)] hover:bg-[var(--color-blackAlpha-50)] dark:hover:bg-[var(--color-whiteAlpha-50)]',
isActive ? 'font-semibold text-[var(--color-text-primary)]' : 'font-medium text-[var(--color-text-secondary)]',
) }
variant="plain"
>
{ text }
</Link>
);
// -- Blockchain sub-menu items --
const BLOCKCHAIN_ITEMS = [
{ text: 'Blocks', pathname: '/blocks' as const },
{ text: 'Transactions', pathname: '/txs' as const },
{ text: 'Tokens', pathname: '/tokens' as const },
{ text: 'Accounts', pathname: '/accounts' as const },
{ text: 'Verified Contracts', pathname: '/verified-contracts' as const },
] as const;
// -- Main component --
const TopBar = () => {
const router = useRouter();
const pathname = router.pathname;
const chain = getCurrentChain();
// A link is shown only where its page has something to show. Chains, Bridge
// and DEX are about the Lux primary network's own chains; Validators reads the
// P-Chain that secures this chain; Stats is the P-Chain panel or the stats
// service. Chains, Bridge, DEX and Validators answer 404 on the same condition.
const isPrimary = isPrimaryNetworkExplorer();
const pChain = getPChain();
const isHome = pathname === '/';
const isBlockchainActive = pathname === '/blocks' || pathname.startsWith('/block/') ||
pathname === '/txs' || pathname.startsWith('/tx/') ||
pathname === '/tokens' || pathname.startsWith('/token/') ||
pathname === '/accounts' || pathname.startsWith('/address/') ||
pathname === '/verified-contracts';
return (
<div
className={ cn(
// Desktop-only: HeaderMobile (block lg:hidden) is the sole header below
// lg. Without this, both render < lg → a double header (two sign-ins).
'hidden lg:block sticky top-0 left-0 w-full max-w-[100vw] z-sticky',
'border-b border-[var(--color-border-divider)]',
'backdrop-blur-[16px] bg-[rgba(255,255,255,0.97)] dark:bg-[rgba(16,17,18,0.97)]',
) }
>
<div
className="flex py-2 px-4 lg:px-6 mx-auto items-center gap-2"
style={{ maxWidth: `${ CONTENT_MAX_WIDTH }px` }}
>
{ /* -- Logo + Brand + Chain/Network selectors (grouped) -- */ }
<div className="flex items-center gap-2 shrink-0">
<a
href={ route({ pathname: '/' as const }) }
className="group flex items-center gap-[6px] shrink-0 no-underline hover:no-underline transition-all duration-300"
aria-label={ `${ chain.branding.brandName } home` }
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox={ chain.branding.logoViewBox }
className={ cn(
'shrink-0 transition-all duration-300 ease-out',
'group-hover:scale-110 group-hover:drop-shadow-[0_0_8px_rgba(74,158,255,0.4)]',
isHome ? 'w-[26px] h-[26px]' : 'w-5 h-5',
) }
dangerouslySetInnerHTML={{ __html: chain.branding.logoContent }}
/>
<div className={ cn(
'flex flex-col leading-none overflow-hidden transition-all duration-300 ease-out',
isHome ? 'max-w-[200px] opacity-100' : 'max-w-[80px] opacity-100',
) }>
<span className={ cn(
'font-bold whitespace-nowrap text-[var(--color-text-primary)] transition-all duration-300',
isHome ? 'text-[14px] tracking-[-0.02em]' : 'text-[13px] tracking-[-0.03em]',
) }>
{ isHome ? chain.branding.brandName : chain.branding.brandName.split(' ')[0] }
</span>
<span className={ cn(
'text-[10px] tracking-[0.03em] whitespace-nowrap text-[var(--color-text-secondary)] transition-all duration-300',
isHome ? 'max-h-[14px] opacity-100 mt-[2px]' : 'max-h-0 opacity-0 mt-0',
) }>
{ chain.branding.productName }
</span>
</div>
</a>
{ /* Network & Chain selectors — adjacent to logo */ }
{ isNetworkSelectorEnabled() && <NetworkSelector/> }
{ isChainSelectorEnabled() && <ChainSwitcher/> }
</div>
{ /* -- Divider -- */ }
<div className="h-4 w-px bg-[var(--color-border-divider)] shrink-0 mx-1"/>
{ /* -- Navigation -- */ }
<nav className="hidden lg:flex items-center gap-1 shrink-0 min-w-0">
{ /* Blockchain dropdown */ }
<MenuRoot>
<MenuTrigger asChild>
<button
className={ cn(
'px-2 py-1 text-xs rounded-sm cursor-pointer transition-all duration-150',
'flex items-center gap-0.5 bg-transparent border-none',
'hover:text-[var(--color-text-primary)] hover:bg-[var(--color-blackAlpha-50)] dark:hover:bg-[var(--color-whiteAlpha-50)]',
isBlockchainActive ? 'font-semibold text-[var(--color-text-primary)]' : 'font-medium text-[var(--color-text-secondary)]',
) }
>
Blockchain
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="w-[14px] h-[14px]"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</button>
</MenuTrigger>
<MenuContent minW="180px">
{ BLOCKCHAIN_ITEMS.map((item) => (
<MenuItem
key={ item.pathname }
value={ item.pathname }
asChild
>
<Link
href={ route({ pathname: item.pathname }) }
className="no-underline hover:no-underline"
variant="plain"
>
{ item.text }
</Link>
</MenuItem>
)) }
</MenuContent>
</MenuRoot>
{ isPrimary && (
<NavLinkItem
text="Chains"
href={ route({ pathname: '/chains' as const }) }
isActive={ pathname === '/chains' || pathname.startsWith('/chain/') }
/>
) }
{ pChain && (
<NavLinkItem
text="Validators"
href={ route({ pathname: '/validators' as const }) }
isActive={ pathname === '/validators' || pathname.startsWith('/validators/') }
/>
) }
{ (pChain || config.features.stats.isEnabled) && (
<NavLinkItem
text="Stats"
href={ route({ pathname: '/stats' as const }) }
isActive={ pathname.startsWith('/stats') }
/>
) }
{ isPrimary && (
<>
<NavLinkItem
text="Bridge"
href={ route({ pathname: '/bridge' as const }) }
isActive={ pathname === '/bridge' }
/>
<NavLinkItem
text="DEX"
href={ route({ pathname: '/dex' as const }) }
isActive={ pathname === '/dex' }
/>
</>
) }
</nav>
{ /* -- Search bar (center, flexible) -- */ }
<div className="flex-1 mx-3 hidden lg:block max-w-[480px] min-w-[180px]">
<SearchBar isHeroBanner={ false }/>
</div>
<SearchBarAssistant/>
{ /* -- Spacer -- */ }
<div className="flex-1 block lg:hidden"/>
{ /* -- User profile / Sign in -- */ }
<div className="shrink-0 ml-auto">
<UserProfileDesktop buttonSize="sm"/>
</div>
</div>
</div>
);
};
export default React.memo(TopBar);