mirror of
https://github.com/luxfi/explore.git
synced 2026-07-27 03:13:50 +00:00
refactor(pchain): Subnet -> Net; add the mainnet explorer ingress
Drops "subnet" from the P-chain surface, which is not our vocabulary:
useSubnets -> useNets (useSubnets.ts deleted)
PChainSubnet -> PChainNet
GetSubnetsResponse -> GetNetsResponse
One "subnet" deliberately survives — `subnetID` in lib/api/pchain/wire.spec.ts.
That is the UPSTREAM P-chain wire field, read through a raw
Record<string,string> cast, so renaming it would break response parsing. The
rule is about our own language, not about lying to the wire.
Also adds deploy/k8s/explore-fe/ingress-mainnet.yaml. explore.lux.network is
served by the Next.js deployment explore-fe-lux (ghcr.io/luxfi/explore), not
the Go `explorer` binary, which only backs api-explore.lux.network. This
replaces drift where the plain Ingress pointed explore.lux.network at
explorer:80 — the Go binary's embedded, stale build.
Two pre-commit gates had to be satisfied, both legitimate:
- eslint max-len: three className lines (BridgePage 230 chars, ChainRow 188 x2)
exceeded the 160 limit; wrapped.
- cspell: `platformvm` flagged unknown. It is the P-chain VM's actual name, so
it is added to cspell.jsonc `words` rather than renamed in code. (Note for
the next person: .cspell-words.txt is listed under ignorePaths — it is a file
excluded from checking, NOT the dictionary. Adding a word there does nothing.)
Verified: tsc --noEmit reports ZERO errors in every file this commit touches and
zero stale-symbol errors, so the rename resolves completely. The project's 223
pre-existing errors are all in untouched files and unchanged by this commit.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
ECDL
|
||||
Kaniko
|
||||
parsdao
|
||||
platformvm
|
||||
|
||||
@@ -67,7 +67,7 @@ export const PRIMARY_VMS: ReadonlyArray<PrimaryVm> = [
|
||||
vm: 'PVM',
|
||||
vmId: 'rWhpuQPF1kb72esV2momhMuTYGkEb1oL29pt2EBXWsBY6MALT',
|
||||
chainId: null,
|
||||
description: 'The P-Chain manages validators, staking, and subnet creation across the network.',
|
||||
description: 'The P-Chain manages validators, staking, and L1 creation across the network.',
|
||||
view: 'platform',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
],
|
||||
// words - list of words to be always considered correct
|
||||
"words": [
|
||||
"platformvm",
|
||||
"aatx",
|
||||
"CLOB",
|
||||
"hanzoai",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
# Public ingress for the Lux mainnet explorer frontend.
|
||||
#
|
||||
# explore.lux.network is served by the dedicated Next.js deployment
|
||||
# `explore-fe-lux` (ghcr.io/luxfi/explore), NOT the Go `explorer` binary. The
|
||||
# Go binary only backs the API host api-explore.lux.network (separate ingress,
|
||||
# ingress-api-mainnet.yaml). explore-fe-lux is self-contained: it renders every
|
||||
# app page, serves /assets/envs.js from its own make_envs at startup, and
|
||||
# proxies P-chain reads via its /api/pchain Next.js route — so the whole host
|
||||
# routes to it. This replaces the earlier drift where the plain Ingress pointed
|
||||
# explore.lux.network at explorer:80 (the Go binary's embedded, stale build).
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: explore-lux-network
|
||||
namespace: lux-mainnet
|
||||
labels: {app: explore-fe, brand: lux, network: mainnet}
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
ingressClassName: ingress
|
||||
tls:
|
||||
- hosts: [explore.lux.network]
|
||||
secretName: explore-lux-network-tls
|
||||
rules:
|
||||
- host: explore.lux.network
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: explore-fe-lux
|
||||
port:
|
||||
number: 80
|
||||
@@ -3,7 +3,7 @@
|
||||
export { getPChain, getInfo, getHealth } from './client';
|
||||
export { useCurrentValidators } from './useCurrentValidators';
|
||||
export { useBlockchains } from './useBlockchains';
|
||||
export { useSubnets } from './useSubnets';
|
||||
export { useNets } from './useNets';
|
||||
export { useChainHeights } from './useChainHeights';
|
||||
|
||||
export type {
|
||||
@@ -11,10 +11,10 @@ export type {
|
||||
PChainDelegator,
|
||||
PChainRewardOwner,
|
||||
PChainBlockchain,
|
||||
PChainSubnet,
|
||||
PChainNet,
|
||||
GetCurrentValidatorsResponse,
|
||||
GetBlockchainsResponse,
|
||||
GetSubnetsResponse,
|
||||
GetNetsResponse,
|
||||
ValidatorStats,
|
||||
} from './types';
|
||||
|
||||
|
||||
@@ -30,14 +30,18 @@ export interface PChainValidator {
|
||||
readonly delegators: ReadonlyArray<PChainDelegator> | null;
|
||||
}
|
||||
|
||||
// Wire field is `netID` — the node's platform.getBlockchains emits netID
|
||||
// (Lux nomenclature: a sovereign L1 is a Network). See
|
||||
// ~/work/lux/node/vms/platformvm/service.go GetBlockchainsResponse.
|
||||
export interface PChainBlockchain {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly subnetID: string;
|
||||
readonly netID: string;
|
||||
readonly vmID: string;
|
||||
}
|
||||
|
||||
export interface PChainSubnet {
|
||||
// Wire shape of platform.getNets → APINet.
|
||||
export interface PChainNet {
|
||||
readonly id: string;
|
||||
readonly controlKeys: ReadonlyArray<string>;
|
||||
readonly threshold: string;
|
||||
@@ -53,8 +57,8 @@ export interface GetBlockchainsResponse {
|
||||
readonly blockchains: ReadonlyArray<PChainBlockchain>;
|
||||
}
|
||||
|
||||
export interface GetSubnetsResponse {
|
||||
readonly subnets: ReadonlyArray<PChainSubnet>;
|
||||
export interface GetNetsResponse {
|
||||
readonly nets: ReadonlyArray<PChainNet>;
|
||||
}
|
||||
|
||||
// Aggregated validator statistics
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// React Query hook for platform.getSubnets.
|
||||
// Returns the list of all subnets on the P-chain.
|
||||
// Uses the server-side /api/pchain proxy to bypass CORS.
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { PChainSubnet } from './types';
|
||||
|
||||
const SUBNETS_STALE_TIME_MS = 300_000;
|
||||
const SUBNETS_QUERY_KEY = 'pchain:subnets' as const;
|
||||
const EMPTY_SUBNETS: ReadonlyArray<PChainSubnet> = [];
|
||||
|
||||
async function fetchSubnets(): Promise<ReadonlyArray<PChainSubnet>> {
|
||||
const res = await fetch('/api/pchain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'platform.getSubnets',
|
||||
params: {},
|
||||
id: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`P-chain proxy returned ${ res.status }`);
|
||||
}
|
||||
|
||||
const json = await res.json() as { result?: { subnets?: ReadonlyArray<PChainSubnet> } };
|
||||
return json.result?.subnets ?? [];
|
||||
}
|
||||
|
||||
export function useSubnets() {
|
||||
const query = useQuery({
|
||||
queryKey: [ SUBNETS_QUERY_KEY ],
|
||||
queryFn: fetchSubnets,
|
||||
staleTime: SUBNETS_STALE_TIME_MS,
|
||||
});
|
||||
|
||||
const subnets = React.useMemo(
|
||||
() => query.data ?? EMPTY_SUBNETS,
|
||||
[ query.data ],
|
||||
);
|
||||
|
||||
return {
|
||||
subnets,
|
||||
isLoading: query.isLoading,
|
||||
isError: query.isError,
|
||||
error: query.error,
|
||||
refetch: query.refetch,
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
// This store captures `req.headers.host` at the start of each SSR render
|
||||
// (see `_document.tsx`), so `getHostname()` on the server returns the
|
||||
// actual requested host. The same docker image serves all chains on the
|
||||
// Lux primary network (C, Zoo, Hanzo, SPC, Pars subnets) at distinct
|
||||
// Lux primary network (C, Zoo, Hanzo, SPC, Pars L1s) at distinct
|
||||
// explore-*.lux.network hosts — one image, N hosts.
|
||||
//
|
||||
// Client side is unaffected: `window.location.hostname` continues to be
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
// White-label brand detection.
|
||||
//
|
||||
// Source code in this repo is Lux-only. Per-chain identity (Zoo / Hanzo / SPC /
|
||||
// Pars subnets on Lux primary network) is rendered with each chain's logo and
|
||||
// Pars L1s on Lux primary network) is rendered with each chain's logo and
|
||||
// name as network metadata — see `configs/app/chainRegistry.ts`. The explorer
|
||||
// brand itself is always Lux.
|
||||
//
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@luxfi/explore",
|
||||
"version": "1.1.13",
|
||||
"version": "1.1.14",
|
||||
"private": false,
|
||||
"homepage": "https://github.com/luxfi/explore#readme",
|
||||
"engines": {
|
||||
|
||||
@@ -41,7 +41,8 @@ function parseChainEntries(src) {
|
||||
|
||||
/** Display name for a federated chain. C-Chain (Contract Chain) is handled
|
||||
* separately by the caller; other chains use the short `name` field plus
|
||||
* " Network". Sovereign L1s are Networks — never "Subnets" (Lux nomenclature). */
|
||||
* " Network". Sovereign L1s are Networks (Lux nomenclature; never the
|
||||
* legacy upstream term for them). */
|
||||
function networkDisplayName(rawName) {
|
||||
return `${ rawName.trim() } Network`;
|
||||
}
|
||||
|
||||
@@ -11,28 +11,28 @@ const explorers = [
|
||||
minTxs: 45,
|
||||
},
|
||||
{
|
||||
name: 'Zoo Subnet',
|
||||
name: 'Zoo L1',
|
||||
url: 'https://explore-zoo.lux.network',
|
||||
apiUrl: 'https://api-explore-zoo.lux.network',
|
||||
minBlocks: 10,
|
||||
minTxs: 45,
|
||||
},
|
||||
{
|
||||
name: 'Hanzo Subnet',
|
||||
name: 'Hanzo L1',
|
||||
url: 'https://explore-hanzo.lux.network',
|
||||
apiUrl: 'https://api-explore-hanzo.lux.network',
|
||||
minBlocks: 9,
|
||||
minTxs: 45,
|
||||
},
|
||||
{
|
||||
name: 'SPC Subnet',
|
||||
name: 'SPC L1',
|
||||
url: 'https://explore-spc.lux.network',
|
||||
apiUrl: 'https://api-explore-spc.lux.network',
|
||||
minBlocks: 9,
|
||||
minTxs: 45,
|
||||
},
|
||||
{
|
||||
name: 'Pars Subnet',
|
||||
name: 'Pars L1',
|
||||
url: 'https://explore-pars.lux.network',
|
||||
apiUrl: 'https://api-explore-pars.lux.network',
|
||||
minBlocks: 9,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// Cross-chain bridge/teleporter page for the Lux multi-chain explorer.
|
||||
// Shows cross-chain transfers between Primary Network chains and L1 subnets.
|
||||
// Shows cross-chain transfers between Primary Network chains and sovereign L1s.
|
||||
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tag } from '@luxfi/ui/tag';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { useBridgeData } from 'lib/api/bchain';
|
||||
import { useBlockchains, useCurrentValidators } from 'lib/api/pchain';
|
||||
import type { PChainBlockchain } from 'lib/api/pchain';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tag } from '@luxfi/ui/tag';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
// ── Constants ──
|
||||
@@ -61,7 +61,10 @@ interface ChainPairCardProps {
|
||||
}
|
||||
|
||||
const ChainPairCard = ({ source, destination, status }: ChainPairCardProps) => (
|
||||
<div className="flex items-center py-3 px-4 border-b border-[var(--color-border-divider)] hover:bg-[var(--color-gray-50)] dark:hover:bg-[var(--color-whiteAlpha-50)] transition-colors duration-150 gap-4 flex-wrap lg:flex-nowrap">
|
||||
<div className={ `
|
||||
flex items-center py-3 px-4 border-b border-[var(--color-border-divider)] hover:bg-[var(--color-gray-50)]
|
||||
dark:hover:bg-[var(--color-whiteAlpha-50)] transition-colors duration-150 gap-4 flex-wrap lg:flex-nowrap
|
||||
` }>
|
||||
<div className="min-w-[160px] shrink-0">
|
||||
<span className="font-medium text-sm text-[var(--color-text-primary)]">
|
||||
{ source }
|
||||
@@ -93,13 +96,13 @@ const BridgePage = () => {
|
||||
const { stats: bridgeStats, isLoading: bridgeLoading } = useBridgeData();
|
||||
|
||||
const l1Chains = React.useMemo<ReadonlyArray<PChainBlockchain>>(
|
||||
() => blockchains.filter((c) => c.subnetID !== PRIMARY_NETWORK_ID),
|
||||
() => blockchains.filter((c) => c.netID !== PRIMARY_NETWORK_ID),
|
||||
[ blockchains ],
|
||||
);
|
||||
|
||||
const isLoading = chainsLoading || validatorsLoading || bridgeLoading;
|
||||
|
||||
// Build bridge pairs: Primary chain <-> L1 subnets
|
||||
// Build bridge pairs: Primary chain <-> sovereign L1s
|
||||
const bridgePairs = React.useMemo(() => {
|
||||
const pairs: Array<{ source: string; destination: string; status: 'active' | 'coming_soon' }> = [];
|
||||
|
||||
@@ -211,8 +214,8 @@ const BridgePage = () => {
|
||||
</span>
|
||||
<span className="text-sm text-[var(--color-text-secondary)] leading-relaxed">
|
||||
The bridge enables cross-chain asset transfers between the Primary Network
|
||||
chains and L1 subnet chains. Atomic swaps between core chains (C, P, X) are
|
||||
currently active. Teleporter-based transfers to L1 subnets are
|
||||
chains and sovereign L1 chains. Atomic swaps between core chains (C, P, X) are
|
||||
currently active. Transfers to sovereign L1s are
|
||||
coming soon via the B-Chain bridge relay.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ 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, useSubnets } from 'lib/api/pchain';
|
||||
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';
|
||||
@@ -21,14 +21,14 @@ const LUX_DECIMALS = 6;
|
||||
// Primary-network VM identity (name / vm / vmId / description / view) comes from
|
||||
// the single source of truth in configs/app/primaryChains.ts.
|
||||
|
||||
const SUBNET_CHAIN_IDS: Readonly<Record<string, number>> = {
|
||||
const L1_EVM_CHAIN_IDS: Readonly<Record<string, number>> = {
|
||||
zoo: 200200,
|
||||
hanzo: 36963,
|
||||
spc: 36911,
|
||||
pars: 494949,
|
||||
};
|
||||
|
||||
const SUBNET_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
||||
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.',
|
||||
@@ -180,7 +180,7 @@ const ChainDetailPage = () => {
|
||||
|
||||
const { blockchains, isLoading: chainsLoading } = useBlockchains();
|
||||
const { validators, isLoading: validatorsLoading } = useCurrentValidators();
|
||||
const { subnets } = useSubnets();
|
||||
const { nets } = useNets();
|
||||
const { stats: indexerStats } = useChainIndexerStats(slug);
|
||||
|
||||
const resolvedChain = React.useMemo<{
|
||||
@@ -206,25 +206,25 @@ const ChainDetailPage = () => {
|
||||
resolvedChain.blockchain?.name ??
|
||||
slug;
|
||||
const chainDescription = resolvedChain.meta?.description ??
|
||||
SUBNET_DESCRIPTIONS[slug] ??
|
||||
L1_DESCRIPTIONS[slug] ??
|
||||
`${ chainName } is a blockchain on the network.`;
|
||||
|
||||
const blockchainId = resolvedChain.blockchain?.id ?? '';
|
||||
const subnetId = resolvedChain.blockchain?.subnetID ?? (resolvedChain.isPrimary ? PRIMARY_NETWORK_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 = SUBNET_CHAIN_IDS[slug];
|
||||
const chainId = L1_EVM_CHAIN_IDS[slug];
|
||||
const explorerUrl = EXPLORER_URLS[slug];
|
||||
const isLoading = chainsLoading || validatorsLoading;
|
||||
|
||||
const subnet = React.useMemo(
|
||||
() => subnets.find((s) => s.id === subnetId),
|
||||
[ subnets, subnetId ],
|
||||
const net = React.useMemo(
|
||||
() => nets.find((n) => n.id === netId),
|
||||
[ nets, netId ],
|
||||
);
|
||||
|
||||
const subnetChains = React.useMemo(
|
||||
() => subnetId ? blockchains.filter((c) => c.subnetID === subnetId) : [],
|
||||
[ blockchains, subnetId ],
|
||||
const netChains = React.useMemo(
|
||||
() => netId ? blockchains.filter((c) => c.netID === netId) : [],
|
||||
[ blockchains, netId ],
|
||||
);
|
||||
|
||||
const totalStake = React.useMemo(
|
||||
@@ -281,11 +281,11 @@ const ChainDetailPage = () => {
|
||||
</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">
|
||||
Subnet Chains
|
||||
Chains on Network
|
||||
</span>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
{ subnetChains.length }
|
||||
{ netChains.length }
|
||||
</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
@@ -295,7 +295,7 @@ const ChainDetailPage = () => {
|
||||
</span>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
{ subnet?.threshold ?? '-' }
|
||||
{ net?.threshold ?? '-' }
|
||||
</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
@@ -308,13 +308,13 @@ const ChainDetailPage = () => {
|
||||
<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/> }
|
||||
{ subnetId && <InfoRow label="Subnet ID" value={ subnetId } 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="L1 Subnet"/> }
|
||||
{ !resolvedChain.isPrimary && <InfoRow label="Network" value="Sovereign L1"/> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -351,13 +351,13 @@ const ChainDetailPage = () => {
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ subnetChains.length > 0 && (
|
||||
{ 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 in Subnet
|
||||
Chains on This Network
|
||||
</span>
|
||||
<Tag size="sm" variant="subtle">{ subnetChains.length }</Tag>
|
||||
<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)]">
|
||||
@@ -371,7 +371,7 @@ const ChainDetailPage = () => {
|
||||
VM
|
||||
</div>
|
||||
</div>
|
||||
{ subnetChains.map((chain) => (
|
||||
{ netChains.map((chain) => (
|
||||
<div
|
||||
key={ chain.id }
|
||||
className={ cn(
|
||||
|
||||
+14
-8
@@ -1,14 +1,14 @@
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from 'lib/utils/cn';
|
||||
import { Link } from 'toolkit/next/link';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
|
||||
interface ChainRowProps {
|
||||
readonly name: string;
|
||||
readonly fullName?: string;
|
||||
readonly blockchainId?: string;
|
||||
readonly subnetId?: string;
|
||||
readonly netId?: string;
|
||||
readonly vmId?: string;
|
||||
readonly vmLabel?: string;
|
||||
readonly chainId?: number | null;
|
||||
@@ -31,7 +31,7 @@ const ChainRow = ({
|
||||
name,
|
||||
fullName,
|
||||
blockchainId,
|
||||
subnetId,
|
||||
netId,
|
||||
vmLabel,
|
||||
chainId,
|
||||
isActive = true,
|
||||
@@ -68,23 +68,29 @@ const ChainRow = ({
|
||||
{ blockchainId ? truncateId(blockchainId) : '\u2014' }
|
||||
</div>
|
||||
|
||||
{ /* Subnet ID column */ }
|
||||
{ /* Network ID column */ }
|
||||
<div
|
||||
className="flex-1 min-w-0 font-mono text-sm text-[var(--color-text-secondary)] overflow-hidden text-ellipsis whitespace-nowrap hidden lg:block"
|
||||
title={ subnetId }
|
||||
title={ netId }
|
||||
>
|
||||
{ subnetId ? truncateId(subnetId) : '\u2014' }
|
||||
{ netId ? truncateId(netId) : '\u2014' }
|
||||
</div>
|
||||
|
||||
{ /* VM badge */ }
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{ vmLabel && (
|
||||
<div className="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 whitespace-nowrap">
|
||||
<div className={ `
|
||||
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 whitespace-nowrap
|
||||
` }>
|
||||
{ vmLabel }
|
||||
</div>
|
||||
) }
|
||||
{ chainId != null && (
|
||||
<div className="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 whitespace-nowrap">
|
||||
<div className={ `
|
||||
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 whitespace-nowrap
|
||||
` }>
|
||||
{ chainId }
|
||||
</div>
|
||||
) }
|
||||
|
||||
+16
-16
@@ -21,7 +21,7 @@ const PRIMARY_NETWORK_ID = '11111111111111111111111111111111LpoYY' as const;
|
||||
// configs/app/primaryChains.ts (mirrored from the node registry), so this page
|
||||
// and the chain-detail page can never drift apart.
|
||||
|
||||
const SUBNET_CHAIN_IDS: Readonly<Record<string, number>> = {
|
||||
const L1_EVM_CHAIN_IDS: Readonly<Record<string, number>> = {
|
||||
Zoo: 200200,
|
||||
Hanzo: 36963,
|
||||
SPC: 36911,
|
||||
@@ -30,7 +30,7 @@ const SUBNET_CHAIN_IDS: Readonly<Record<string, number>> = {
|
||||
|
||||
const TAB_IDS = {
|
||||
primary: 'primary',
|
||||
subnets: 'subnets',
|
||||
l1: 'l1',
|
||||
} as const;
|
||||
|
||||
type TabId = typeof TAB_IDS[keyof typeof TAB_IDS];
|
||||
@@ -42,10 +42,10 @@ const EMPTY_L1_CHAINS: ReadonlyArray<PChainBlockchain> = [];
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface TableHeaderProps {
|
||||
readonly showSubnetId: boolean;
|
||||
readonly showNetId: boolean;
|
||||
}
|
||||
|
||||
const TableHeader = ({ showSubnetId }: TableHeaderProps) => (
|
||||
const TableHeader = ({ showNetId }: TableHeaderProps) => (
|
||||
<div className="hidden lg:flex px-4 py-2 gap-4 border-b border-[var(--color-border-divider)]">
|
||||
<div className="min-w-[180px] max-w-[220px] shrink-0 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Chain
|
||||
@@ -53,9 +53,9 @@ const TableHeader = ({ showSubnetId }: TableHeaderProps) => (
|
||||
<div className="flex-1 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Blockchain ID
|
||||
</div>
|
||||
{ showSubnetId && (
|
||||
{ showNetId && (
|
||||
<div className="flex-1 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Subnet ID
|
||||
Network ID
|
||||
</div>
|
||||
) }
|
||||
<div className="shrink-0 w-[120px] text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
@@ -103,15 +103,15 @@ const ChainsPage = () => {
|
||||
if (!blockchains.length) {
|
||||
return EMPTY_L1_CHAINS;
|
||||
}
|
||||
return blockchains.filter((chain) => chain.subnetID !== PRIMARY_NETWORK_ID);
|
||||
return blockchains.filter((chain) => chain.netID !== PRIMARY_NETWORK_ID);
|
||||
}, [ blockchains ]);
|
||||
|
||||
const handlePrimaryClick = React.useCallback(() => {
|
||||
setActiveTab(TAB_IDS.primary);
|
||||
}, []);
|
||||
|
||||
const handleSubnetsClick = React.useCallback(() => {
|
||||
setActiveTab(TAB_IDS.subnets);
|
||||
const handleL1Click = React.useCallback(() => {
|
||||
setActiveTab(TAB_IDS.l1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -134,15 +134,15 @@ const ChainsPage = () => {
|
||||
/>
|
||||
<TabButton
|
||||
label={ `L1 / L2 / L3${ l1Chains.length > 0 ? ` (${ l1Chains.length })` : '' }` }
|
||||
isActive={ activeTab === TAB_IDS.subnets }
|
||||
onClick={ handleSubnetsClick }
|
||||
isActive={ activeTab === TAB_IDS.l1 }
|
||||
onClick={ handleL1Click }
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ /* Primary Network tab */ }
|
||||
{ activeTab === TAB_IDS.primary && (
|
||||
<div className="border border-[var(--color-border-divider)] rounded-md overflow-hidden">
|
||||
<TableHeader showSubnetId={ false }/>
|
||||
<TableHeader showNetId={ false }/>
|
||||
{ PRIMARY_VMS.map((chain) => (
|
||||
<ChainRow
|
||||
key={ chain.name }
|
||||
@@ -158,9 +158,9 @@ const ChainsPage = () => {
|
||||
) }
|
||||
|
||||
{ /* L1/L2/L3 tab */ }
|
||||
{ activeTab === TAB_IDS.subnets && (
|
||||
{ activeTab === TAB_IDS.l1 && (
|
||||
<div className="border border-[var(--color-border-divider)] rounded-md overflow-hidden">
|
||||
<TableHeader showSubnetId/>
|
||||
<TableHeader showNetId/>
|
||||
{ isLoading && (
|
||||
<div className="px-4 py-6">
|
||||
<Skeleton loading={ true } h="20px" mb={ 3 }/>
|
||||
@@ -179,10 +179,10 @@ const ChainsPage = () => {
|
||||
key={ chain.id }
|
||||
name={ chain.name }
|
||||
blockchainId={ chain.id }
|
||||
subnetId={ chain.subnetID }
|
||||
netId={ chain.netID }
|
||||
vmId={ chain.vmID }
|
||||
vmLabel={ resolveVmLabel(chain.vmID) }
|
||||
chainId={ SUBNET_CHAIN_IDS[chain.name] ?? null }
|
||||
chainId={ L1_EVM_CHAIN_IDS[chain.name] ?? null }
|
||||
isActive
|
||||
href={ `/chains/${ chain.name.toLowerCase() }` }
|
||||
/>
|
||||
|
||||
@@ -38,7 +38,7 @@ const ChainSwitcher = () => {
|
||||
const currentNetwork = getCurrentNetwork();
|
||||
const isMainChain = current.name === 'C-Chain';
|
||||
|
||||
// For white-label subnets (Zoo, Pars, Hanzo, SPC), only show that chain's
|
||||
// For white-label L1s (Zoo, Pars, Hanzo, SPC), only show that chain's
|
||||
// mainnet/testnet entries — don't show the full Lux ecosystem.
|
||||
// For the main C-Chain explorer, show all chains.
|
||||
const chains = React.useMemo(() => {
|
||||
@@ -46,11 +46,11 @@ const ChainSwitcher = () => {
|
||||
if (isMainChain) {
|
||||
return allChains;
|
||||
}
|
||||
// Filter to only chains with the same branding (same subnet)
|
||||
// Filter to only chains with the same branding (same network)
|
||||
return allChains.filter((c) => c.branding.brandName === current.branding.brandName);
|
||||
}, [ currentNetwork.network, isMainChain, current.branding.brandName ]);
|
||||
|
||||
// For subnets, show network switcher (mainnet/testnet) instead of chain switcher
|
||||
// For L1s, show network switcher (mainnet/testnet) instead of chain switcher
|
||||
const availableNetworks = React.useMemo(() => {
|
||||
if (isMainChain) return [];
|
||||
return NETWORKS.filter((net) => {
|
||||
@@ -100,7 +100,7 @@ const ChainSwitcher = () => {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="240px">
|
||||
<PopoverBody className="p-1">
|
||||
{ /* Network switcher for subnets (e.g. Zoo Mainnet / Zoo Testnet) */ }
|
||||
{ /* Network switcher for L1s (e.g. Zoo Mainnet / Zoo Testnet) */ }
|
||||
{ availableNetworks.length > 1 && (
|
||||
<>
|
||||
<div className="px-2 py-1.5">
|
||||
|
||||
@@ -55,7 +55,7 @@ const NetworkStats = () => {
|
||||
const isLoading = validatorsLoading || chainsLoading;
|
||||
|
||||
const l1Count = React.useMemo(
|
||||
() => blockchains.filter((c) => c.subnetID !== PRIMARY_NETWORK_ID).length,
|
||||
() => blockchains.filter((c) => c.netID !== PRIMARY_NETWORK_ID).length,
|
||||
[ blockchains ],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user