feat(explore/primary-vms): handle all 10 Lux-primary VM types + route D-Chain to the DEX UI

The Lux primary network is a family of VM-specialized chains (C=EVM, X=UTXO,
D=DEX, P=platform, + A/B/Q/T/Z/G/K/O/R/I/M), mirrored from node/node/vms.go.

- configs/app/primaryChains.ts: single source of truth (PRIMARY_VMS, getPrimaryVm,
  per-VM `view`, hasBespokeView, chainsAwaitingBespokeView). Decomplects the two
  lists that duplicated this — ChainsPage + ChainDetailPage now consume it
  (ChainDetailPage drops ~110 lines of copy).
- D-Chain (DexVM) renders the DEX order-book UI (ui/dex/DexPage) via view:'dex',
  not a generic block list. C-Chain=EVM core, P-Chain=platform/validators,
  X-Chain + the custom VMs render the generic detail fallback (info + indexer DAG
  stats + validators) — graceful, never blank. chainsAwaitingBespokeView() FLAGS
  the backlog: X-Chain (UTXO) + the 11 custom VMs still need a bespoke view.
- These VMs are Lux-primary, so the primary-VM surfaces (Chains/DEX/Bridge/AI
  Compute nav + /chains, /chains/[slug], /dex pages) gate on
  isPrimaryNetworkExplorer(). ui/shared/PrimaryNetworkGuard backstops direct-URL
  access so the VMs never leak onto a brand explorer (Hanzo/Zoo/Pars).
- Tests: configs/app/primaryChains.spec.ts (26 cases) — registry, per-VM view,
  bespoke-view flag, and the lux-only visibility keystone. 38/38 green with the
  existing chainRegistry.spec.ts.
This commit is contained in:
2026-06-28 15:08:34 -07:00
parent 72aa6a2ed4
commit eac4772aab
9 changed files with 471 additions and 158 deletions
+40
View File
@@ -166,6 +166,46 @@ All symlinks reference this single source of truth.
## Recent Changes
### Chain-visibility + 10-VM/DEX handling (v1.0.14)
Two orthogonal correctness fixes shipped in one build cycle.
- **Chain-visibility rule (PR #7):** `isPrimaryNetworkExplorer()` now requires
`!isWhiteLabelMode() && vm === 'EVM'`, so unregistered/brand hosts stop
rendering the Lux primary panels + cross-L1 list. Registered
`explore.zoo.network`/`.ngo` + `explore.pars.network` as own-scoped L2s
(brand parity with Hanzo), added **Osage** L1, and fixed Pars chainId →
**7070**. Rule: **lux explorer = ALL chains; hanzo/zoo/pars = ONLY their own.**
Proven by `configs/app/chainRegistry.spec.ts` (12 cases).
- **10-VM / DEX handling:** the Lux primary network is a family of VM-specialized
chains (C=EVM, X=UTXO, D=DEX, P=platform, + A/B/Q/T/Z/G/K/O/R/I/M), mirrored
from `~/work/lux/node/node/vms.go`. New single source of truth
`configs/app/primaryChains.ts` (`PRIMARY_VMS`, `getPrimaryVm`, `view`,
`hasBespokeView`, `chainsAwaitingBespokeView`) **decomplects** the two lists
that used to duplicate this (ChainsPage / ChainDetailPage now consume it).
Each VM routes at `/chains/<slug>`; `view` selects the render:
- **D-Chain → the DEX order-book UI** (`ui/dex/DexPage`, wired to the indexer
`./dex` adapter via `lib/api/dchain`) — `ChainDetailPage` renders `<DexPage/>`
for `view: 'dex'`, not a generic block list.
- **C-Chain** = the EVM explorer core; **P-Chain** = platform/validators data;
**X-Chain** + the custom VMs (A/B/Q/T/Z/G/K/O/R/I/M) render the generic
chain-detail fallback (info + indexer DAG stats + validators) — they do NOT
crash/blank. `chainsAwaitingBespokeView()` FLAGS the backlog: **X-Chain
(UTXO) and the 11 custom VMs still need a bespoke explorer view**; only
C/P/D have one today.
- **VMs are Lux-primary → lux-only:** the primary-VM surfaces (Chains / DEX /
Bridge / AI Compute nav + the `/chains`, `/chains/[slug]`, `/dex` pages) are
gated on `isPrimaryNetworkExplorer()`. Nav hides them on brand/white-label
hosts; `ui/shared/PrimaryNetworkGuard` backstops direct-URL access so the 10
VMs can never leak onto Hanzo/Zoo/Pars. Proven by
`configs/app/primaryChains.spec.ts` (26 cases).
- **Build/deploy:** single multi-destination kaniko Job (NO GHA), context
`git://github.com/luxfi/explore.git#main`, Dockerfile (base Next.js build),
fan-out `--destination=ghcr.io/{luxfi,zooai,hanzoai,parsdao}/explore:v1.0.14`
(parsdao ADDED — it had no v1.0.1x image). White-label is 100% runtime via the
`explore-env-{brand}` configmaps (APP_HOST), not baked per-brand. Deploy = bump
all four `explore-fe-*` images to v1.0.14 in
`luxfi/universe k8s/lux-mainnet/explore-fe.yaml`, operator reconcile.
### Realtime transport: Phoenix WebSocket → SSE (v1.0.13)
- **Bug:** the FE connected realtime via Phoenix Channels
(`wss://api-explore.lux.network/socket/v2/websocket`), but the Go backend
+113
View File
@@ -0,0 +1,113 @@
import { describe, expect, it, beforeEach } from 'vitest';
import { isPrimaryNetworkExplorer } from './chainRegistry';
import {
PRIMARY_VMS,
getPrimaryVm,
hasBespokeView,
chainsAwaitingBespokeView,
} from './primaryChains';
// Drives the registry off a simulated request host, exactly like the browser
// (getHostname() reads window.location.hostname). Mirrors chainRegistry.spec.ts.
function atHost(hostname: string) {
(globalThis as unknown as { window: unknown }).window = { location: { hostname }, __envs: {} };
}
describe('primary-network VM registry', () => {
// The 10 VM chains the Lux primary network must expose + route (the build
// directive). The registry is a superset (P/M/O/R/I as well), so we assert
// the mandated 10 are all present with the right VM type.
const REQUIRED = [
{ slug: 'c-chain', vm: 'EVM' },
{ slug: 'x-chain', vm: 'XVM' },
{ slug: 'd-chain', vm: 'DexVM' },
{ slug: 'a-chain', vm: 'AIVM' },
{ slug: 'z-chain', vm: 'ZKVM' },
{ slug: 'q-chain', vm: 'QuantumVM' },
{ slug: 'b-chain', vm: 'BridgeVM' },
{ slug: 'k-chain', vm: 'KeyVM' },
{ slug: 'g-chain', vm: 'GraphVM' },
{ slug: 't-chain', vm: 'ThresholdVM' },
];
it.each(REQUIRED)('registers $slug as $vm', ({ slug, vm }) => {
const entry = getPrimaryVm(slug);
expect(entry).toBeDefined();
expect(entry?.vm).toBe(vm);
});
it('exposes every VM at a unique slug routed under /chains/<slug>', () => {
const slugs = PRIMARY_VMS.map((c) => c.slug);
expect(new Set(slugs).size).toBe(slugs.length);
expect(PRIMARY_VMS.length).toBeGreaterThanOrEqual(REQUIRED.length);
});
it('resolves slugs case-insensitively', () => {
expect(getPrimaryVm('D-Chain')?.slug).toBe('d-chain');
expect(getPrimaryVm('C-CHAIN')?.slug).toBe('c-chain');
});
describe('each VM type maps to the correct render view', () => {
it('D-Chain renders the DEX order-book UI (not an EVM block list)', () => {
expect(getPrimaryVm('d-chain')?.view).toBe('dex');
});
it('C-Chain renders the EVM model', () => {
expect(getPrimaryVm('c-chain')?.view).toBe('evm');
});
it('X-Chain renders the UTXO model', () => {
expect(getPrimaryVm('x-chain')?.view).toBe('utxo');
});
it('P-Chain renders the platform/validators model', () => {
expect(getPrimaryVm('p-chain')?.view).toBe('platform');
});
it('custom VMs (A/Z/Q/B/K/G/T) fall back to the generic view', () => {
for (const slug of [ 'a-chain', 'z-chain', 'q-chain', 'b-chain', 'k-chain', 'g-chain', 't-chain' ]) {
expect(getPrimaryVm(slug)?.view).toBe('generic');
}
});
});
describe('bespoke-view flag — what still needs a native explorer', () => {
it('EVM / platform / DEX have a bespoke view today', () => {
expect(hasBespokeView('evm')).toBe(true);
expect(hasBespokeView('platform')).toBe(true);
expect(hasBespokeView('dex')).toBe(true);
});
it('UTXO + generic still fall back (flagged backlog)', () => {
expect(hasBespokeView('utxo')).toBe(false);
expect(hasBespokeView('generic')).toBe(false);
});
it('flags X-Chain and the custom VMs, never C/P/D', () => {
const flagged = chainsAwaitingBespokeView().map((c) => c.slug);
expect(flagged).toEqual(expect.arrayContaining([ 'x-chain', 'a-chain', 'z-chain', 'q-chain', 'b-chain', 'k-chain', 'g-chain', 't-chain' ]));
expect(flagged).not.toContain('c-chain');
expect(flagged).not.toContain('p-chain');
expect(flagged).not.toContain('d-chain');
});
});
});
// The primary-network VM surfaces (Chains / Chain detail / DEX nav + pages) are
// gated on isPrimaryNetworkExplorer(). This keystone proves the 10 VMs surface
// ONLY on the Lux explorer and never leak onto a brand explorer — the same
// predicate the nav and PrimaryNetworkGuard use.
describe('primary VMs surface only on the Lux primary explorer', () => {
beforeEach(() => atHost('localhost'));
it('shows the VM surfaces on the Lux explorer', () => {
atHost('explore.lux.network');
expect(isPrimaryNetworkExplorer()).toBe(true);
});
it.each([
'explore.hanzo.network',
'explore.zoo.network',
'explore.pars.network',
'explore.osage.network',
'explore.unknown.example',
])('hides the VM surfaces on %s (no leak)', (host) => {
atHost(host);
expect(isPrimaryNetworkExplorer()).toBe(false);
});
});
+217
View File
@@ -0,0 +1,217 @@
// Single source of truth for the Lux PRIMARY-network VM chains.
//
// The Lux primary network is not one chain but a family of VM-specialized
// blockchains that share one validator set — mirrored from the node registry
// at ~/work/lux/node/node/vms.go (P/X/C core + the extended allvms set:
// A,B,D,G,I,K,M,O,Q,R,T,Z). Each chain runs its OWN vmID and therefore its own
// data model: C is EVM blocks, X is a UTXO/DAG, D is a DEX order book, etc.
//
// This axis is ORTHOGONAL to the sovereign-L1 brand axis in chainRegistry.ts
// (Lux / Zoo / Hanzo / Pars / SPC / Osage). These VM chains belong to the Lux
// primary network ONLY, so they surface ONLY on the Lux primary explorer — see
// isPrimaryNetworkExplorer(). Brand explorers never list them.
//
// `view` selects which UI renders a chain's native model. `hasBespokeView`
// derives whether that view is VM-native today or a graceful generic fallback
// (so callers can flag the chains still awaiting a bespoke explorer).
export type PrimaryVmView = 'evm' | 'platform' | 'utxo' | 'dex' | 'generic';
export interface PrimaryVm {
/** Route slug — the chain renders at /chains/<slug>. */
readonly slug: string;
/** Short display name, e.g. "C-Chain". */
readonly name: string;
/** Full display name, e.g. "Contract Chain". */
readonly fullName: string;
/** VM label as the node registers it, e.g. "EVM", "DexVM". */
readonly vm: string;
/** P-Chain vmID where known (core chains), else '' (extended VMs). */
readonly vmId: string;
/** EVM chainId where the chain has one (C-Chain), else null. */
readonly chainId: number | null;
/** One-line description for the chain detail header. */
readonly description: string;
/** Which UI renders this chain's native data model. */
readonly view: PrimaryVmView;
}
// Views backed by a VM-native render today. Everything else falls back to the
// generic chain-detail view (info + indexer stats + validators) and is FLAGGED
// by chainsAwaitingBespokeView() as still needing a bespoke explorer.
const BESPOKE_VIEWS: ReadonlySet<PrimaryVmView> = new Set<PrimaryVmView>([ 'evm', 'platform', 'dex' ]);
export const PRIMARY_VMS: ReadonlyArray<PrimaryVm> = [
{
slug: 'c-chain',
name: 'C-Chain',
fullName: 'Contract Chain',
vm: 'EVM',
vmId: 'mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6',
chainId: 96369,
description: 'The C-Chain is an EVM-compatible blockchain on the network used for smart contracts and DeFi applications.',
view: 'evm',
},
{
slug: 'p-chain',
name: 'P-Chain',
fullName: 'Platform Chain',
vm: 'PVM',
vmId: 'rWhpuQPF1kb72esV2momhMuTYGkEb1oL29pt2EBXWsBY6MALT',
chainId: null,
description: 'The P-Chain manages validators, staking, and subnet creation across the network.',
view: 'platform',
},
{
slug: 'x-chain',
name: 'X-Chain',
fullName: 'UTXO Chain',
vm: 'XVM',
vmId: 'jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq',
chainId: null,
description: 'The X-Chain is a DAG-based chain for creating and exchanging digital assets on the network.',
view: 'utxo',
},
{
slug: 'd-chain',
name: 'D-Chain',
fullName: 'DEX Chain',
vm: 'DexVM',
vmId: '',
chainId: null,
description: 'The D-Chain is a decentralized exchange chain on the network for on-chain order books and token swaps.',
view: 'dex',
},
{
slug: 'a-chain',
name: 'A-Chain',
fullName: 'AI Chain',
vm: 'AIVM',
vmId: '',
chainId: null,
description: 'The A-Chain powers AI workloads on the network, providing decentralized inference and model serving.',
view: 'generic',
},
{
slug: 'b-chain',
name: 'B-Chain',
fullName: 'Bridge Chain',
vm: 'BridgeVM',
vmId: '',
chainId: null,
description: 'The B-Chain is the bridge relay chain on the network, enabling cross-chain asset transfers via Teleporter.',
view: 'generic',
},
{
slug: 'q-chain',
name: 'Q-Chain',
fullName: 'Quantum Chain',
vm: 'QuantumVM',
vmId: '',
chainId: null,
description: 'The Q-Chain provides post-quantum cryptographic primitives and quantum-resistant operations on the network.',
view: 'generic',
},
{
slug: 't-chain',
name: 'T-Chain',
fullName: 'Threshold Chain',
vm: 'ThresholdVM',
vmId: '',
chainId: null,
description: 'The T-Chain enables threshold signature schemes and distributed key generation on the network.',
view: 'generic',
},
{
slug: 'z-chain',
name: 'Z-Chain',
fullName: 'ZK Chain',
vm: 'ZKVM',
vmId: '',
chainId: null,
description: 'The Z-Chain handles zero-knowledge proof generation and verification on the network.',
view: 'generic',
},
{
slug: 'g-chain',
name: 'G-Chain',
fullName: 'Graph Chain',
vm: 'GraphVM',
vmId: '',
chainId: null,
description: 'The G-Chain provides decentralized graph indexing and query services on the network.',
view: 'generic',
},
{
slug: 'k-chain',
name: 'K-Chain',
fullName: 'Key Chain',
vm: 'KeyVM',
vmId: '',
chainId: null,
description: 'The K-Chain provides decentralized key management and custody services on the network.',
view: 'generic',
},
{
slug: 'o-chain',
name: 'O-Chain',
fullName: 'Oracle Chain',
vm: 'OracleVM',
vmId: '',
chainId: null,
description: 'The O-Chain provides decentralized oracle services, bringing off-chain data on-chain for the network.',
view: 'generic',
},
{
slug: 'r-chain',
name: 'R-Chain',
fullName: 'Relay Chain',
vm: 'RelayVM',
vmId: '',
chainId: null,
description: 'The R-Chain handles cross-chain message relay and interoperability routing on the network.',
view: 'generic',
},
{
slug: 'i-chain',
name: 'I-Chain',
fullName: 'Identity Chain',
vm: 'IdentityVM',
vmId: '',
chainId: null,
description: 'The I-Chain manages decentralized identity, DIDs, and verifiable credentials on the network.',
view: 'generic',
},
{
slug: 'm-chain',
name: 'M-Chain',
fullName: 'MPC Chain',
vm: 'MPCVM',
vmId: '',
chainId: null,
description: 'The M-Chain coordinates CGGMP21 threshold ECDSA and FROST threshold EdDSA signing sessions across validators.',
view: 'generic',
},
];
export function getPrimaryVm(slug: string): PrimaryVm | undefined {
return PRIMARY_VMS.find((vm) => vm.slug === slug.toLowerCase());
}
/** True when `view` has a VM-native render today (not the generic fallback). */
export function hasBespokeView(view: PrimaryVmView): boolean {
return BESPOKE_VIEWS.has(view);
}
/** Chains that still render via the generic fallback — the bespoke-view backlog. */
export function chainsAwaitingBespokeView(): ReadonlyArray<PrimaryVm> {
return PRIMARY_VMS.filter((vm) => !hasBespokeView(vm.view));
}
+4
View File
@@ -42,6 +42,10 @@
"abfnrtv",
"abitype",
"AIVM",
"MPCVM",
"CGGMP",
"UTXO",
"decomplects",
"allvms",
"abkw",
"ACMRT",
+15 -8
View File
@@ -4,6 +4,7 @@ import React from 'react';
import type { NavItemInternal, NavItem, NavGroupItem } from 'types/client/navigation';
import config from 'configs/app';
import { isPrimaryNetworkExplorer } from 'configs/app/chainRegistry';
import { getEnvValue } from 'configs/app/utils';
import { layerLabels } from 'lib/rollups/utils';
import { rightLineArrow } from 'toolkit/utils/htmlEntities';
@@ -99,30 +100,36 @@ export default function useNavItems(): ReturnType {
icon: 'navigation/validator',
isActive: pathname === '/validators' || pathname === '/validators/[id]',
};
const chains: NavItem = {
// Chains / DEX / Bridge / AI Compute are Lux PRIMARY-network VM surfaces
// (the C/X/D/A/B… VM family). They belong to the Lux primary network only,
// so they are hidden on brand explorers (Hanzo / Zoo / Pars / SPC / Osage)
// and on white-label hosts — enforcing the chain-visibility rule. Pages
// self-guard too (PrimaryNetworkGuard) so a direct URL can't leak them.
const isPrimaryNetwork = isPrimaryNetworkExplorer();
const chains: NavItem | null = isPrimaryNetwork ? {
text: 'Chains',
nextRoute: { pathname: '/chains' as const },
icon: 'navigation/blockchain',
isActive: pathname === '/chains',
};
const dex: NavItem = {
} : null;
const dex: NavItem | null = isPrimaryNetwork ? {
text: 'DEX',
nextRoute: { pathname: '/dex' as const },
icon: 'navigation/dex_tracker',
isActive: pathname === '/dex',
};
const aiCompute: NavItem = {
} : null;
const aiCompute: NavItem | null = isPrimaryNetwork ? {
text: 'AI Compute',
nextRoute: { pathname: '/ai' as const },
icon: 'navigation/blockchain',
isActive: pathname === '/ai',
};
const bridge: NavItem = {
} : null;
const bridge: NavItem | null = isPrimaryNetwork ? {
text: 'Bridge',
nextRoute: { pathname: '/bridge' as const },
icon: 'navigation/cross_chain_txs',
isActive: pathname === '/bridge',
};
} : null;
const rollupDeposits = {
text: `Deposits (${ layerLabels.parent }${ rightLineArrow }${ layerLabels.current })`,
nextRoute: { pathname: '/deposits' as const },
+35 -123
View File
@@ -1,132 +1,25 @@
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, useSubnets } from 'lib/api/pchain';
import type { PChainBlockchain, PChainValidator } from 'lib/api/pchain';
import { cn } from 'lib/utils/cn';
import { Skeleton } from '@luxfi/ui/skeleton';
import { Tag } from '@luxfi/ui/tag';
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';
const LUX_DECIMALS = 6;
const PRIMARY_CHAIN_META: Readonly<Record<string, {
readonly name: string;
readonly fullName: string;
readonly vm: string;
readonly vmId: string;
readonly description: string;
}>> = {
'c-chain': {
name: 'C-Chain',
fullName: 'Contract Chain',
vm: 'EVM',
vmId: 'mgj786NP7uDwBCcq6YwThhaN8FLyybkCa4zBWTQbNgmK6k9A6',
description: 'The C-Chain is an EVM-compatible blockchain on the network used for smart contracts and DeFi applications.',
},
'p-chain': {
name: 'P-Chain',
fullName: 'Platform Chain',
vm: 'PVM',
vmId: 'rWhpuQPF1kb72esV2momhMuTYGkEb1oL29pt2EBXWsBY6MALT',
description: 'The P-Chain manages validators, staking, and subnet creation across the network.',
},
'x-chain': {
name: 'X-Chain',
fullName: 'UTXO Chain',
vm: 'XVM',
vmId: 'jvYyfQTxGMJLuGWa55kdP2p2zSUYsQ5Raupu4TW34ZAUBAbtq',
description: 'The X-Chain is a DAG-based chain for creating and exchanging digital assets on the network.',
},
'd-chain': {
name: 'D-Chain',
fullName: 'DEX Chain',
vm: 'DexVM',
vmId: '',
description: 'The D-Chain is a decentralized exchange chain on the network for on-chain order books and token swaps.',
},
'a-chain': {
name: 'A-Chain',
fullName: 'AI Chain',
vm: 'AIVM',
vmId: '',
description: 'The A-Chain powers AI workloads on the network, providing decentralized inference and model serving.',
},
'b-chain': {
name: 'B-Chain',
fullName: 'Bridge Chain',
vm: 'BridgeVM',
vmId: '',
description: 'The B-Chain is the bridge relay chain on the network, enabling cross-chain asset transfers via Teleporter.',
},
'q-chain': {
name: 'Q-Chain',
fullName: 'Quantum Chain',
vm: 'QuantumVM',
vmId: '',
description: 'The Q-Chain provides post-quantum cryptographic primitives and quantum-resistant operations on the network.',
},
't-chain': {
name: 'T-Chain',
fullName: 'Threshold Chain',
vm: 'ThresholdVM',
vmId: '',
description: 'The T-Chain enables threshold signature schemes and distributed key generation on the network.',
},
'z-chain': {
name: 'Z-Chain',
fullName: 'ZK Chain',
vm: 'ZKVM',
vmId: '',
description: 'The Z-Chain handles zero-knowledge proof generation and verification on the network.',
},
'g-chain': {
name: 'G-Chain',
fullName: 'Graph Chain',
vm: 'GraphVM',
vmId: '',
description: 'The G-Chain provides decentralized graph indexing and query services on the network.',
},
'k-chain': {
name: 'K-Chain',
fullName: 'Key Chain',
vm: 'KeyVM',
vmId: '',
description: 'The K-Chain provides decentralized key management and custody services on the network.',
},
'o-chain': {
name: 'O-Chain',
fullName: 'Oracle Chain',
vm: 'OracleVM',
vmId: '',
description: 'The O-Chain provides decentralized oracle services, bringing off-chain data on-chain for the network.',
},
'r-chain': {
name: 'R-Chain',
fullName: 'Relay Chain',
vm: 'RelayVM',
vmId: '',
description: 'The R-Chain handles cross-chain message relay and interoperability routing on the network.',
},
'i-chain': {
name: 'I-Chain',
fullName: 'Identity Chain',
vm: 'IdentityVM',
vmId: '',
description: 'The I-Chain manages decentralized identity, DIDs, and verifiable credentials on the network.',
},
'm-chain': {
name: 'M-Chain',
fullName: 'MPC Chain',
vm: 'MPCVM',
vmId: '',
description: 'The M-Chain coordinates CGGMP21 threshold ECDSA and FROST threshold EdDSA signing sessions across validators.',
},
};
// 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>> = {
zoo: 200200,
@@ -226,7 +119,10 @@ interface InfoRowProps {
}
const InfoRow = ({ label, value, isMono = false, canCopy = false }: InfoRowProps) => (
<div className="flex py-3 px-4 border-b border-[var(--color-border-divider)] odd:bg-[var(--color-gray-50)] dark:odd:bg-[var(--color-whiteAlpha-50)] gap-4 flex-wrap lg:flex-nowrap">
<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>
@@ -249,7 +145,10 @@ const ValidatorRow = ({ validator, index }: ValidatorRowProps) => {
const uptime = parseFloat(validator.uptime || '0') * 100;
return (
<div className="flex 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 items-center flex-wrap lg:flex-nowrap">
<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>
@@ -287,10 +186,10 @@ const ChainDetailPage = () => {
const resolvedChain = React.useMemo<{
blockchain: PChainBlockchain | undefined;
isPrimary: boolean;
meta: typeof PRIMARY_CHAIN_META[string] | undefined;
meta: PrimaryVm | undefined;
}>(() => {
// Check primary chains first
const primaryMeta = PRIMARY_CHAIN_META[slug];
const primaryMeta = getPrimaryVm(slug);
if (primaryMeta) {
return { blockchain: undefined, isPrimary: true, meta: primaryMeta };
}
@@ -333,8 +232,15 @@ const ChainDetailPage = () => {
[ 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={ (
@@ -468,7 +374,10 @@ const ChainDetailPage = () => {
{ subnetChains.map((chain) => (
<div
key={ chain.id }
className="flex px-4 py-3 gap-4 border-b border-[var(--color-border-divider)] last:border-b-0 hover:bg-[var(--color-gray-50)] dark:hover:bg-[var(--color-whiteAlpha-50)] transition-colors duration-150 flex-wrap lg:flex-nowrap"
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)]">
@@ -484,7 +393,10 @@ const ChainDetailPage = () => {
</div>
</div>
<div className="w-[120px]">
<div className="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">
<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>
@@ -542,7 +454,7 @@ const ChainDetailPage = () => {
)) }
</div>
</div>
</>
</PrimaryNetworkGuard>
);
};
+12 -25
View File
@@ -1,11 +1,13 @@
import { Skeleton } from '@luxfi/ui/skeleton';
import React from 'react';
import config from 'configs/app';
import { PRIMARY_VMS } from 'configs/app/primaryChains';
import type { PChainBlockchain } from 'lib/api/pchain';
import { useBlockchains } from 'lib/api/pchain';
import { cn } from 'lib/utils/cn';
import { Skeleton } from '@luxfi/ui/skeleton';
import PageTitle from 'ui/shared/Page/PageTitle';
import PrimaryNetworkGuard from 'ui/shared/PrimaryNetworkGuard';
import ChainRow from './ChainRow';
@@ -15,26 +17,9 @@ import ChainRow from './ChainRow';
const PRIMARY_NETWORK_ID = '11111111111111111111111111111111LpoYY' as const;
// All 15 primary network chains from ~/work/lux/node/node/vms_allvms.go
// Core: C (EVM), P (PlatformVM), X (XVM)
// Extended (allvms build): A, B, D, G, I, K, M, O, Q, R, T, Z
const PRIMARY_CHAINS = [
{ name: 'C-Chain', fullName: 'Contract Chain', vm: 'EVM', chainId: 96369, slug: 'c-chain' },
{ name: 'P-Chain', fullName: 'Platform Chain', vm: 'PVM', chainId: null, slug: 'p-chain' },
{ name: 'X-Chain', fullName: 'UTXO Chain', vm: 'XVM', chainId: null, slug: 'x-chain' },
{ name: 'D-Chain', fullName: 'DEX Chain', vm: 'DexVM', chainId: null, slug: 'd-chain' },
{ name: 'A-Chain', fullName: 'AI Chain', vm: 'AIVM', chainId: null, slug: 'a-chain' },
{ name: 'B-Chain', fullName: 'Bridge Chain', vm: 'BridgeVM', chainId: null, slug: 'b-chain' },
{ name: 'Q-Chain', fullName: 'Quantum Chain', vm: 'QuantumVM', chainId: null, slug: 'q-chain' },
{ name: 'T-Chain', fullName: 'Threshold Chain', vm: 'ThresholdVM', chainId: null, slug: 't-chain' },
{ name: 'M-Chain', fullName: 'MPC Chain', vm: 'MPCVM', chainId: null, slug: 'm-chain' },
{ name: 'Z-Chain', fullName: 'ZK Chain', vm: 'ZKVM', chainId: null, slug: 'z-chain' },
{ name: 'G-Chain', fullName: 'Graph Chain', vm: 'GraphVM', chainId: null, slug: 'g-chain' },
{ name: 'K-Chain', fullName: 'Key Chain', vm: 'KeyVM', chainId: null, slug: 'k-chain' },
{ name: 'O-Chain', fullName: 'Oracle Chain', vm: 'OracleVM', chainId: null, slug: 'o-chain' },
{ name: 'R-Chain', fullName: 'Relay Chain', vm: 'RelayVM', chainId: null, slug: 'r-chain' },
{ name: 'I-Chain', fullName: 'Identity Chain', vm: 'IdentityVM', chainId: null, slug: 'i-chain' },
] as const;
// Primary-network VM chains come from the single source of truth in
// 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>> = {
Zoo: 200200,
@@ -96,7 +81,9 @@ const TabButton = ({ label, isActive, onClick }: TabButtonProps) => (
<button
className={ cn(
'px-4 py-2 text-sm bg-transparent cursor-pointer transition-all duration-150 border-b-2 hover:text-[var(--color-text-primary)]',
isActive ? 'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' : 'font-normal text-[var(--color-text-secondary)] border-transparent',
isActive ?
'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' :
'font-normal text-[var(--color-text-secondary)] border-transparent',
) }
onClick={ onClick }
>
@@ -128,7 +115,7 @@ const ChainsPage = () => {
}, []);
return (
<>
<PrimaryNetworkGuard title="Chains">
<PageTitle
title="Chains"
secondRow={ (
@@ -156,7 +143,7 @@ const ChainsPage = () => {
{ activeTab === TAB_IDS.primary && (
<div className="border border-[var(--color-border-divider)] rounded-md overflow-hidden">
<TableHeader showSubnetId={ false }/>
{ PRIMARY_CHAINS.map((chain) => (
{ PRIMARY_VMS.map((chain) => (
<ChainRow
key={ chain.name }
name={ chain.name }
@@ -202,7 +189,7 @@ const ChainsPage = () => {
)) }
</div>
) }
</>
</PrimaryNetworkGuard>
);
};
+3 -2
View File
@@ -10,6 +10,7 @@ import { useDexData } from 'lib/api/dchain';
import type { DexOrder, DexTrade, DexPool, DexSymbolStats } from 'lib/api/dchain';
import { cn } from 'lib/utils/cn';
import PageTitle from 'ui/shared/Page/PageTitle';
import PrimaryNetworkGuard from 'ui/shared/PrimaryNetworkGuard';
// ── Constants ──
@@ -255,7 +256,7 @@ const DexPage = () => {
const handlePoolsClick = React.useCallback(() => setActiveTab(TAB_IDS.pools), []);
return (
<>
<PrimaryNetworkGuard title="DEX">
<PageTitle
title="DEX"
secondRow={ (
@@ -360,7 +361,7 @@ const DexPage = () => {
)) }
</div>
) }
</>
</PrimaryNetworkGuard>
);
};
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import { isPrimaryNetworkExplorer } from 'configs/app/chainRegistry';
import PageTitle from 'ui/shared/Page/PageTitle';
// Primary-network VM surfaces (Chains / Chain detail / DEX) belong to the Lux
// primary network only. On a brand explorer (Hanzo / Zoo / Pars / SPC / Osage)
// these chains must never appear — see the chain-visibility rule in
// chainRegistry.ts. The nav hides the entries; this guard backstops direct-URL
// access so the primary VMs can never leak onto a brand explorer.
interface Props {
readonly title: string;
readonly children: React.ReactNode;
}
const PrimaryNetworkGuard = ({ title, children }: Props) => {
if (!isPrimaryNetworkExplorer()) {
return (
<>
<PageTitle title={ title }/>
<div className="px-4 py-10 text-center text-sm text-[var(--color-text-secondary)]">
This explorer shows only its own chain. The Lux primary-network chains
are available on the Lux explorer.
</div>
</>
);
}
return children;
};
export default PrimaryNetworkGuard;