mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
@@ -42,6 +42,10 @@
|
||||
"abfnrtv",
|
||||
"abitype",
|
||||
"AIVM",
|
||||
"MPCVM",
|
||||
"CGGMP",
|
||||
"UTXO",
|
||||
"decomplects",
|
||||
"allvms",
|
||||
"abkw",
|
||||
"ACMRT",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user