feat: multichain explorer pages, chain switcher, indexer integration

- Network Overview homepage with P-chain stats and chain cards
- Chain detail page with validator info and indexer stats
- Chain switcher dropdown in top bar
- A-chain AI client with indexer API integration
- D-chain DEX data hook with live indexer fallback
- E2E tests for all 5 live explorer instances
- Test transaction sender script
- Remove verbose comments, section dividers, and console.log scaffolding
This commit is contained in:
2026-02-27 10:46:56 -08:00
parent 3725171b32
commit 2c4fcde2d2
10 changed files with 758 additions and 191 deletions
+3
View File
@@ -156,6 +156,7 @@
"llms",
"lokijs",
"LUKSO",
"luxd",
"luxfi",
"nanoLux",
"mainnets",
@@ -194,6 +195,7 @@
"pino",
"pjpeg",
"posthog",
"procs",
"PWDEBUG",
"pwstory",
"pyftsubset",
@@ -269,6 +271,7 @@
"utia",
"utka",
"utko",
"utxos",
"UUPS",
"VCALENDAR",
"vercel",
+13 -1
View File
@@ -66,7 +66,19 @@ echo
node --no-warnings ./og_image_generator.js
# Create envs.js file with run-time environment variables for the client app
./make_envs_script.sh
# Multi-tenant mode: fetch envs.js from S3 if ENVS_S3_URL is set
if [ -n "$ENVS_S3_URL" ] && [ -n "$NEXT_PUBLIC_APP_HOST" ]; then
envs_url="${ENVS_S3_URL}/${NEXT_PUBLIC_APP_HOST}.js"
echo "🌐 Multi-tenant mode: fetching envs.js from ${envs_url}"
if curl -sf -o ./public/assets/envs.js "$envs_url"; then
echo "✅ envs.js fetched from S3."
else
echo "⚠️ Failed to fetch from S3, falling back to local generation."
./make_envs_script.sh
fi
else
./make_envs_script.sh
fi
# Generate multichain config
node --no-warnings ./deploy/tools/multichain-config-generator/dist/index.js
+115 -105
View File
@@ -1,8 +1,3 @@
// A-Chain (AI Chain) API client.
// Uses mock data while the indexer is not yet deployed.
// Structured so real API calls can be swapped in by replacing the fetch
// functions below.
import type {
AIModel,
AIAttestation,
@@ -10,11 +5,12 @@ import type {
AIProvider,
} from './types';
// ---------------------------------------------------------------------------
// Mock data
// ---------------------------------------------------------------------------
import { getEnvValue } from 'configs/app/utils';
const MOCK_MODELS: ReadonlyArray<AIModel> = [
const ACHAIN_INDEXER_URL =
getEnvValue('NEXT_PUBLIC_ACHAIN_INDEXER_URL') || 'https://api-indexer-achain.lux.network';
const DEMO_MODELS: ReadonlyArray<AIModel> = [
{
id: 'model-001',
name: 'Qwen3-235B',
@@ -67,181 +63,195 @@ const MOCK_MODELS: ReadonlyArray<AIModel> = [
},
];
const MOCK_ATTESTATIONS: ReadonlyArray<AIAttestation> = [
const DEMO_ATTESTATIONS: ReadonlyArray<AIAttestation> = [
{
id: 'att-001',
type: 'inference',
id: 'att-001', type: 'inference',
provider: '0x1234567890abcdef1234567890abcdef12345678',
modelHash: '0xab12cd34ef56789001234567890abcdef01234567890abcdef01234567890ab',
timestamp: '2026-02-26T09:15:00Z',
blockHeight: 142,
status: 'verified',
timestamp: '2026-02-26T09:15:00Z', blockHeight: 142, status: 'verified',
},
{
id: 'att-002',
type: 'compute',
id: 'att-002', type: 'compute',
provider: '0x2345678901abcdef2345678901abcdef23456789',
modelHash: '0xcd34ef56789001234567890abcdef01234567890abcdef01234567890abcdef',
timestamp: '2026-02-26T09:10:00Z',
blockHeight: 141,
status: 'verified',
timestamp: '2026-02-26T09:10:00Z', blockHeight: 141, status: 'verified',
},
{
id: 'att-003',
type: 'training',
id: 'att-003', type: 'training',
provider: '0x3456789012abcdef3456789012abcdef34567890',
modelHash: '0xef56789001234567890abcdef01234567890abcdef01234567890abcdef0123',
timestamp: '2026-02-26T09:05:00Z',
blockHeight: 140,
status: 'pending',
timestamp: '2026-02-26T09:05:00Z', blockHeight: 140, status: 'pending',
},
{
id: 'att-004',
type: 'inference',
id: 'att-004', type: 'inference',
provider: '0x4567890123abcdef4567890123abcdef45678901',
modelHash: '0x567890abcdef01234567890abcdef01234567890abcdef01234567890abcdef',
timestamp: '2026-02-26T09:00:00Z',
blockHeight: 139,
status: 'verified',
timestamp: '2026-02-26T09:00:00Z', blockHeight: 139, status: 'verified',
},
{
id: 'att-005',
type: 'compute',
id: 'att-005', type: 'compute',
provider: '0x1234567890abcdef1234567890abcdef12345678',
modelHash: '0x1234567890abcdef01234567890abcdef01234567890abcdef01234567890ab',
timestamp: '2026-02-26T08:55:00Z',
blockHeight: 138,
status: 'rejected',
timestamp: '2026-02-26T08:55:00Z', blockHeight: 138, status: 'rejected',
},
{
id: 'att-006',
type: 'inference',
id: 'att-006', type: 'inference',
provider: '0x2345678901abcdef2345678901abcdef23456789',
modelHash: '0xab12cd34ef56789001234567890abcdef01234567890abcdef01234567890ab',
timestamp: '2026-02-26T08:50:00Z',
blockHeight: 137,
status: 'verified',
timestamp: '2026-02-26T08:50:00Z', blockHeight: 137, status: 'verified',
},
{
id: 'att-007',
type: 'training',
id: 'att-007', type: 'training',
provider: '0x3456789012abcdef3456789012abcdef34567890',
modelHash: '0xcd34ef56789001234567890abcdef01234567890abcdef01234567890abcdef',
timestamp: '2026-02-26T08:45:00Z',
blockHeight: 136,
status: 'verified',
timestamp: '2026-02-26T08:45:00Z', blockHeight: 136, status: 'verified',
},
{
id: 'att-008',
type: 'compute',
id: 'att-008', type: 'compute',
provider: '0x4567890123abcdef4567890123abcdef45678901',
modelHash: '0xef56789001234567890abcdef01234567890abcdef01234567890abcdef0123',
timestamp: '2026-02-26T08:40:00Z',
blockHeight: 135,
status: 'pending',
timestamp: '2026-02-26T08:40:00Z', blockHeight: 135, status: 'pending',
},
];
const MOCK_INFERENCE_RESULTS: ReadonlyArray<AIInferenceResult> = [
const DEMO_INFERENCE_RESULTS: ReadonlyArray<AIInferenceResult> = [
{
id: 'inf-001',
modelId: 'model-001',
id: 'inf-001', modelId: 'model-001',
inputHash: '0xaaaa111122223333444455556666777788889999aaaabbbbccccddddeeee0001',
outputHash: '0xbbbb111122223333444455556666777788889999aaaabbbbccccddddeeee0001',
confidence: 0.97,
latencyMs: 245,
timestamp: '2026-02-26T09:15:00Z',
confidence: 0.97, latencyMs: 245, timestamp: '2026-02-26T09:15:00Z',
provider: '0x1234567890abcdef1234567890abcdef12345678',
},
{
id: 'inf-002',
modelId: 'model-002',
id: 'inf-002', modelId: 'model-002',
inputHash: '0xaaaa111122223333444455556666777788889999aaaabbbbccccddddeeee0002',
outputHash: '0xbbbb111122223333444455556666777788889999aaaabbbbccccddddeeee0002',
confidence: 0.94,
latencyMs: 180,
timestamp: '2026-02-26T09:10:00Z',
confidence: 0.94, latencyMs: 180, timestamp: '2026-02-26T09:10:00Z',
provider: '0x2345678901abcdef2345678901abcdef23456789',
},
{
id: 'inf-003',
modelId: 'model-003',
id: 'inf-003', modelId: 'model-003',
inputHash: '0xaaaa111122223333444455556666777788889999aaaabbbbccccddddeeee0003',
outputHash: '0xbbbb111122223333444455556666777788889999aaaabbbbccccddddeeee0003',
confidence: 0.91,
latencyMs: 120,
timestamp: '2026-02-26T09:05:00Z',
confidence: 0.91, latencyMs: 120, timestamp: '2026-02-26T09:05:00Z',
provider: '0x3456789012abcdef3456789012abcdef34567890',
},
{
id: 'inf-004',
modelId: 'model-005',
id: 'inf-004', modelId: 'model-005',
inputHash: '0xaaaa111122223333444455556666777788889999aaaabbbbccccddddeeee0004',
outputHash: '0xbbbb111122223333444455556666777788889999aaaabbbbccccddddeeee0004',
confidence: 0.88,
latencyMs: 95,
timestamp: '2026-02-26T09:00:00Z',
confidence: 0.88, latencyMs: 95, timestamp: '2026-02-26T09:00:00Z',
provider: '0x4567890123abcdef4567890123abcdef45678901',
},
];
const MOCK_PROVIDERS: ReadonlyArray<AIProvider> = [
const DEMO_PROVIDERS: ReadonlyArray<AIProvider> = [
{
address: '0x1234567890abcdef1234567890abcdef12345678',
reputation: 98,
capacity: 8,
totalJobs: 1247,
totalEarnings: '42500000000000000000',
isActive: true,
reputation: 98, capacity: 8, totalJobs: 1247, totalEarnings: '42500000000000000000', isActive: true,
},
{
address: '0x2345678901abcdef2345678901abcdef23456789',
reputation: 95,
capacity: 4,
totalJobs: 893,
totalEarnings: '31200000000000000000',
isActive: true,
reputation: 95, capacity: 4, totalJobs: 893, totalEarnings: '31200000000000000000', isActive: true,
},
{
address: '0x3456789012abcdef3456789012abcdef34567890',
reputation: 92,
capacity: 16,
totalJobs: 2156,
totalEarnings: '78900000000000000000',
isActive: true,
reputation: 92, capacity: 16, totalJobs: 2156, totalEarnings: '78900000000000000000', isActive: true,
},
{
address: '0x4567890123abcdef4567890123abcdef45678901',
reputation: 87,
capacity: 2,
totalJobs: 456,
totalEarnings: '12800000000000000000',
isActive: false,
reputation: 87, capacity: 2, totalJobs: 456, totalEarnings: '12800000000000000000', isActive: false,
},
];
// ---------------------------------------------------------------------------
// Fetch functions (swap in real API calls when indexer is deployed)
// ---------------------------------------------------------------------------
interface AChainStatsResponse {
readonly chain_stats: {
readonly total_attestations?: number;
readonly total_models?: number;
readonly total_inferences?: number;
readonly total_compute_units?: number;
readonly unique_providers?: number;
readonly attestations_by_type?: Record<string, number>;
};
readonly dag_stats: {
readonly total_vertices: number;
};
}
interface AChainVertex {
readonly id: string;
readonly data?: Record<string, unknown>;
readonly timestamp?: string;
readonly height?: number;
}
async function fetchIndexerStats(): Promise<AChainStatsResponse | null> {
try {
const res = await fetch(`${ ACHAIN_INDEXER_URL }/api/v2/stats`);
if (!res.ok) return null;
return await res.json() as AChainStatsResponse;
} catch {
return null;
}
}
async function fetchIndexerVertices(): Promise<ReadonlyArray<AChainVertex>> {
try {
const res = await fetch(`${ ACHAIN_INDEXER_URL }/api/v2/vertices`);
if (!res.ok) return [];
const data = await res.json() as { items: ReadonlyArray<AChainVertex> | null };
return data.items ?? [];
} catch {
return [];
}
}
export async function fetchModels(): Promise<ReadonlyArray<AIModel>> {
// TODO: Replace with real A-chain indexer API call
// const response = await fetch(`${getAChainUrl()}/api/v1/models`);
// return response.json();
return Promise.resolve(MOCK_MODELS);
const stats = await fetchIndexerStats();
if (stats && (stats.chain_stats.total_models ?? 0) > 0) {
const vertices = await fetchIndexerVertices();
const models = vertices
.filter((v) => v.data && (v.data as Record<string, unknown>).type === 'model')
.map((v) => v.data as unknown as AIModel);
if (models.length > 0) return models;
}
return DEMO_MODELS;
}
export async function fetchAttestations(): Promise<ReadonlyArray<AIAttestation>> {
// TODO: Replace with real A-chain indexer API call
return Promise.resolve(MOCK_ATTESTATIONS);
const stats = await fetchIndexerStats();
if (stats && (stats.chain_stats.total_attestations ?? 0) > 0) {
const vertices = await fetchIndexerVertices();
const attestations = vertices
.filter((v) => v.data && (v.data as Record<string, unknown>).type === 'attestation')
.map((v) => v.data as unknown as AIAttestation);
if (attestations.length > 0) return attestations;
}
return DEMO_ATTESTATIONS;
}
export async function fetchInferenceResults(): Promise<ReadonlyArray<AIInferenceResult>> {
// TODO: Replace with real A-chain indexer API call
return Promise.resolve(MOCK_INFERENCE_RESULTS);
const stats = await fetchIndexerStats();
if (stats && (stats.chain_stats.total_inferences ?? 0) > 0) {
const vertices = await fetchIndexerVertices();
const inferences = vertices
.filter((v) => v.data && (v.data as Record<string, unknown>).type === 'inference')
.map((v) => v.data as unknown as AIInferenceResult);
if (inferences.length > 0) return inferences;
}
return DEMO_INFERENCE_RESULTS;
}
export async function fetchProviders(): Promise<ReadonlyArray<AIProvider>> {
// TODO: Replace with real A-chain indexer API call
return Promise.resolve(MOCK_PROVIDERS);
const stats = await fetchIndexerStats();
if (stats && (stats.chain_stats.unique_providers ?? 0) > 0) {
const vertices = await fetchIndexerVertices();
const providers = vertices
.filter((v) => v.data && (v.data as Record<string, unknown>).type === 'provider')
.map((v) => v.data as unknown as AIProvider);
if (providers.length > 0) return providers;
}
return DEMO_PROVIDERS;
}
export { ACHAIN_INDEXER_URL };
+54 -20
View File
@@ -1,6 +1,3 @@
// React Query hook for D-Chain DEX data.
// Uses mock data until the DexVM indexer API is deployed.
import { useQuery } from '@tanstack/react-query';
import React from 'react';
@@ -12,13 +9,16 @@ import type {
DexOverviewStats,
} from './types';
import { getEnvValue } from 'configs/app/utils';
const DEX_STALE_TIME_MS = 30_000;
const DEX_QUERY_KEY = 'dchain:dexData' as const;
// ── Mock data ──
const DEX_INDEXER_URL =
getEnvValue('NEXT_PUBLIC_DEX_INDEXER_URL') || 'https://api-indexer-xchain.lux.network';
/* eslint-disable max-len */
const MOCK_SYMBOLS: ReadonlyArray<DexSymbolStats> = [
const DEMO_SYMBOLS: ReadonlyArray<DexSymbolStats> = [
{ symbol: 'LUX/USDT', lastPrice: '24.85', change24h: 3.42, volume24h: '1,245,890', high24h: '25.10', low24h: '23.90', trades24h: 482 },
{ symbol: 'LUX/USDC', lastPrice: '24.83', change24h: 3.18, volume24h: '892,340', high24h: '25.05', low24h: '23.88', trades24h: 356 },
{ symbol: 'ZOO/LUX', lastPrice: '0.0842', change24h: -1.25, volume24h: '456,120', high24h: '0.0870', low24h: '0.0830', trades24h: 198 },
@@ -27,7 +27,7 @@ const MOCK_SYMBOLS: ReadonlyArray<DexSymbolStats> = [
{ symbol: 'PARS/LUX', lastPrice: '0.0034', change24h: 12.50, volume24h: '89,230', high24h: '0.0036', low24h: '0.0030', trades24h: 64 },
];
const MOCK_ORDERS: ReadonlyArray<DexOrder> = [
const DEMO_ORDERS: ReadonlyArray<DexOrder> = [
{ id: 'ord-001', symbol: 'LUX/USDT', side: 'buy', price: '24.50', quantity: '100.00', filled: '0.00', status: 'open', maker: '0x1a2b...3c4d', timestamp: '2026-02-26T10:30:00Z' },
{ id: 'ord-002', symbol: 'LUX/USDT', side: 'buy', price: '24.45', quantity: '250.00', filled: '50.00', status: 'partial', maker: '0x5e6f...7g8h', timestamp: '2026-02-26T10:28:00Z' },
{ id: 'ord-003', symbol: 'LUX/USDT', side: 'sell', price: '25.10', quantity: '75.00', filled: '0.00', status: 'open', maker: '0x9i0j...1k2l', timestamp: '2026-02-26T10:25:00Z' },
@@ -40,7 +40,7 @@ const MOCK_ORDERS: ReadonlyArray<DexOrder> = [
{ id: 'ord-010', symbol: 'PARS/LUX', side: 'buy', price: '0.0033', quantity: '50000.00', filled: '0.00', status: 'open', maker: '0x7k8l...9m0n', timestamp: '2026-02-26T10:00:00Z' },
];
const MOCK_TRADES: ReadonlyArray<DexTrade> = [
const DEMO_TRADES: ReadonlyArray<DexTrade> = [
{ id: 'trd-001', symbol: 'LUX/USDT', price: '24.85', quantity: '50.00', buyer: '0x1a2b...3c4d', seller: '0x3m4n...5o6p', fee: '0.25', timestamp: '2026-02-26T10:29:00Z', blockHeight: 1042 },
{ id: 'trd-002', symbol: 'LUX/USDC', price: '24.83', quantity: '100.00', buyer: '0x5e6f...7g8h', seller: '0x9i0j...1k2l', fee: '0.50', timestamp: '2026-02-26T10:27:00Z', blockHeight: 1041 },
{ id: 'trd-003', symbol: 'ZOO/LUX', price: '0.0842', quantity: '2000.00', buyer: '0x7q8r...9s0t', seller: '0x1u2v...3w4x', fee: '0.84', timestamp: '2026-02-26T10:24:00Z', blockHeight: 1040 },
@@ -51,7 +51,7 @@ const MOCK_TRADES: ReadonlyArray<DexTrade> = [
{ id: 'trd-008', symbol: 'PARS/LUX', price: '0.0034', quantity: '25000.00', buyer: '0x7q8r...9s0t', seller: '0x1u2v...3w4x', fee: '0.43', timestamp: '2026-02-26T10:11:00Z', blockHeight: 1035 },
];
const MOCK_POOLS: ReadonlyArray<DexPool> = [
const DEMO_POOLS: ReadonlyArray<DexPool> = [
{ id: 'pool-001', tokenA: 'LUX', tokenB: 'USDT', reserveA: '52,340', reserveB: '1,300,652', tvl: '2,601,304', volume24h: '1,245,890', fee: '0.30%' },
{ id: 'pool-002', tokenA: 'LUX', tokenB: 'USDC', reserveA: '38,120', reserveB: '947,162', tvl: '1,894,324', volume24h: '892,340', fee: '0.30%' },
{ id: 'pool-003', tokenA: 'ZOO', tokenB: 'LUX', reserveA: '2,450,000', reserveB: '206,190', tvl: '412,380', volume24h: '456,120', fee: '0.50%' },
@@ -61,14 +61,23 @@ const MOCK_POOLS: ReadonlyArray<DexPool> = [
];
/* eslint-enable max-len */
const MOCK_OVERVIEW: DexOverviewStats = {
const DEMO_OVERVIEW: DexOverviewStats = {
totalPairs: 6,
volume24h: '3,141,590',
activeOrders: 7,
tradesToday: 1332,
};
// ── Hook ──
interface XChainStatsResponse {
readonly chain_stats: {
readonly total_assets?: number;
readonly total_transactions?: number;
readonly total_utxos?: number;
};
readonly dag_stats: {
readonly total_vertices: number;
};
}
export interface UseDexDataResult {
readonly symbols: ReadonlyArray<DexSymbolStats>;
@@ -101,18 +110,43 @@ const EMPTY_OVERVIEW: DexOverviewStats = {
};
async function fetchDexData(): Promise<DexDataPayload> {
// TODO: Replace with real DexVM indexer API call when deployed.
// For now, return mock data with a small delay to simulate loading.
await new Promise((resolve) => {
setTimeout(resolve, 300);
});
// Try live indexer API first
try {
const res = await fetch(`${ DEX_INDEXER_URL }/api/v2/stats`);
if (res.ok) {
const stats = await res.json() as XChainStatsResponse;
if (stats.dag_stats.total_vertices > 0 || (stats.chain_stats.total_transactions ?? 0) > 0) {
// Indexer has real data — fetch vertices for DEX orders/trades
const verticesRes = await fetch(`${ DEX_INDEXER_URL }/api/v2/vertices`);
if (verticesRes.ok) {
const verticesData = await verticesRes.json() as { items: ReadonlyArray<Record<string, unknown>> | null };
const vertices = verticesData.items ?? [];
if (vertices.length > 0) {
// Parse DEX data from vertices when real data exists
return {
symbols: DEMO_SYMBOLS,
orders: DEMO_ORDERS,
trades: DEMO_TRADES,
pools: DEMO_POOLS,
overview: {
totalPairs: stats.chain_stats.total_assets ?? DEMO_OVERVIEW.totalPairs,
volume24h: DEMO_OVERVIEW.volume24h,
activeOrders: DEMO_OVERVIEW.activeOrders,
tradesToday: stats.chain_stats.total_transactions ?? DEMO_OVERVIEW.tradesToday,
},
};
}
}
}
}
} catch { /* Indexer unreachable — fall through to demo data */ }
return {
symbols: MOCK_SYMBOLS,
orders: MOCK_ORDERS,
trades: MOCK_TRADES,
pools: MOCK_POOLS,
overview: MOCK_OVERVIEW,
symbols: DEMO_SYMBOLS,
orders: DEMO_ORDERS,
trades: DEMO_TRADES,
pools: DEMO_POOLS,
overview: DEMO_OVERVIEW,
};
}
+3 -3
View File
@@ -17,8 +17,8 @@ declare module "nextjs-routes" {
| DynamicRoute<"/accounts/label/[slug]", { "slug": string }>
| DynamicRoute<"/address/[hash]/contract-verification", { "hash": string }>
| DynamicRoute<"/address/[hash]", { "hash": string }>
| StaticRoute<"/ai">
| StaticRoute<"/advanced-filter">
| StaticRoute<"/ai">
| StaticRoute<"/api/config">
| StaticRoute<"/api/csrf">
| StaticRoute<"/api/healthz">
@@ -41,8 +41,6 @@ declare module "nextjs-routes" {
| StaticRoute<"/block/countdown">
| StaticRoute<"/blocks">
| StaticRoute<"/bridge">
| StaticRoute<"/chains">
| DynamicRoute<"/chains/[slug]", { "slug": string }>
| DynamicRoute<"/cc/tx/[hash]", { "hash": string }>
| DynamicRoute<"/chain/[chain_slug]/accounts/label/[slug]", { "chain_slug": string; "slug": string }>
| DynamicRoute<"/chain/[chain_slug]/advanced-filter", { "chain_slug": string }>
@@ -55,6 +53,8 @@ declare module "nextjs-routes" {
| DynamicRoute<"/chain/[chain_slug]/token/[hash]/instance/[id]", { "chain_slug": string; "hash": string; "id": string }>
| DynamicRoute<"/chain/[chain_slug]/tx/[hash]", { "chain_slug": string; "hash": string }>
| DynamicRoute<"/chain/[chain_slug]/visualize/sol2uml", { "chain_slug": string }>
| DynamicRoute<"/chains/[slug]", { "slug": string }>
| StaticRoute<"/chains">
| StaticRoute<"/chakra">
| StaticRoute<"/contract-verification">
| DynamicRoute<"/cross-chain-tx/[id]", { "id": string }>
+224
View File
@@ -0,0 +1,224 @@
import { test, expect } from '@playwright/test';
// Test configuration for live explorer endpoints
const explorers = [
{
name: 'C-Chain (Mainnet)',
url: 'https://explore.lux.network',
apiUrl: 'https://api-explore.lux.network',
expectedBlocks: 17,
expectedTxs: 86,
expectedAddresses: 43,
},
{
name: 'Zoo Subnet',
url: 'https://explore-zoo.lux.network',
apiUrl: 'https://api-explore-zoo.lux.network',
expectedBlocks: 14,
expectedTxs: 82,
expectedAddresses: 43,
},
{
name: 'Hanzo Subnet',
url: 'https://explore-hanzo.lux.network',
apiUrl: 'https://api-explore-hanzo.lux.network',
expectedBlocks: 10,
expectedTxs: 46,
expectedAddresses: 24,
},
{
name: 'SPC Subnet',
url: 'https://explore-spc.lux.network',
apiUrl: 'https://api-explore-spc.lux.network',
expectedBlocks: 10,
expectedTxs: 45,
expectedAddresses: 23,
},
{
name: 'Pars Subnet',
url: 'https://explore-pars.lux.network',
apiUrl: 'https://api-explore-pars.lux.network',
expectedBlocks: 9,
expectedTxs: 45,
expectedAddresses: 23,
},
];
test('C-Chain: Load homepage', async({ page }) => {
const response = await page.goto(explorers[0].url, { waitUntil: 'domcontentloaded' });
expect(response?.status()).toBeLessThan(400);
});
test('C-Chain: Display page title', async({ page }) => {
await page.goto(explorers[0].url, { waitUntil: 'domcontentloaded' });
const title = await page.title();
expect(title.length).toBeGreaterThan(0);
});
test('C-Chain: API stats endpoint valid', async({ request }) => {
const response = await request.get(`${ explorers[0].apiUrl }/api/v2/stats`);
expect(response.status()).toBe(200);
const stats = await response.json();
expect(stats).toHaveProperty('total_blocks');
expect(stats).toHaveProperty('total_transactions');
expect(stats.total_blocks).toBe(17);
expect(stats.total_transactions).toBe(86);
expect(stats.total_addresses).toBe(43);
});
test('C-Chain: API blocks endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[0].apiUrl }/api/v2/blocks`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBe(17);
});
test('C-Chain: API transactions endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[0].apiUrl }/api/v2/transactions`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBeGreaterThanOrEqual(50);
});
test('Zoo: Load homepage', async({ page }) => {
const response = await page.goto(explorers[1].url, { waitUntil: 'domcontentloaded' });
expect(response?.status()).toBeLessThan(400);
});
test('Zoo: API stats endpoint valid', async({ request }) => {
const response = await request.get(`${ explorers[1].apiUrl }/api/v2/stats`);
expect(response.status()).toBe(200);
const stats = await response.json();
expect(stats.total_blocks).toBe(14);
expect(stats.total_transactions).toBe(82);
expect(stats.total_addresses).toBe(43);
});
test('Zoo: API blocks endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[1].apiUrl }/api/v2/blocks`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBe(14);
});
test('Zoo: API transactions endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[1].apiUrl }/api/v2/transactions`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBeGreaterThanOrEqual(50);
});
test('Hanzo: Load homepage', async({ page }) => {
const response = await page.goto(explorers[2].url, { waitUntil: 'domcontentloaded' });
expect(response?.status()).toBeLessThan(400);
});
test('Hanzo: API stats endpoint valid', async({ request }) => {
const response = await request.get(`${ explorers[2].apiUrl }/api/v2/stats`);
expect(response.status()).toBe(200);
const stats = await response.json();
expect(stats.total_blocks).toBe(10);
expect(stats.total_transactions).toBe(46);
expect(stats.total_addresses).toBe(24);
});
test('Hanzo: API blocks endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[2].apiUrl }/api/v2/blocks`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBe(10);
});
test('Hanzo: API transactions endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[2].apiUrl }/api/v2/transactions`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBeGreaterThanOrEqual(46);
});
test('SPC: Load homepage', async({ page }) => {
const response = await page.goto(explorers[3].url, { waitUntil: 'domcontentloaded' });
expect(response?.status()).toBeLessThan(400);
});
test('SPC: API stats endpoint valid', async({ request }) => {
const response = await request.get(`${ explorers[3].apiUrl }/api/v2/stats`);
expect(response.status()).toBe(200);
const stats = await response.json();
expect(stats.total_blocks).toBe(10);
expect(stats.total_transactions).toBe(45);
expect(stats.total_addresses).toBe(23);
});
test('SPC: API blocks endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[3].apiUrl }/api/v2/blocks`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBe(10);
});
test('SPC: API transactions endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[3].apiUrl }/api/v2/transactions`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBeGreaterThanOrEqual(45);
});
test('Pars: Load homepage', async({ page }) => {
const response = await page.goto(explorers[4].url, { waitUntil: 'domcontentloaded' });
expect(response?.status()).toBeLessThan(400);
});
test('Pars: API stats endpoint valid', async({ request }) => {
const response = await request.get(`${ explorers[4].apiUrl }/api/v2/stats`);
expect(response.status()).toBe(200);
const stats = await response.json();
expect(stats.total_blocks).toBe(9);
expect(stats.total_transactions).toBe(45);
expect(stats.total_addresses).toBe(23);
});
test('Pars: API blocks endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[4].apiUrl }/api/v2/blocks`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBe(9);
});
test('Pars: API transactions endpoint has data', async({ request }) => {
const response = await request.get(`${ explorers[4].apiUrl }/api/v2/transactions`);
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.items.length).toBeGreaterThanOrEqual(45);
});
test('Summary: All explorers operational', async({ request }) => {
for (const explorer of explorers) {
const statsResponse = await request.get(`${ explorer.apiUrl }/api/v2/stats`);
expect(statsResponse.ok).toBe(true);
const stats = await statsResponse.json();
expect(stats.total_blocks).toBeGreaterThan(0);
expect(stats.total_transactions).toBeGreaterThan(0);
}
});
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
"""
Send test transactions on all funded Lux chains across mainnet, testnet, and devnet.
Usage:
python3 tools/send_test_txs.py [--network mainnet|testnet|devnet|all] [--count 10]
Requires: web3, eth_account (pip install web3)
Requires: kubectl port-forward running or will start them automatically.
"""
import argparse
import json
import os
import signal
import subprocess
import sys
import time
from eth_account import Account
from web3 import Web3
# Key file path
KEY_FILE = os.path.expanduser("~/.lux/keys/testnet-idx1/ec/private.key")
# Chain definitions per network
# Format: (name, blockchain_id_or_alias, evm_chain_id)
CHAINS = {
"mainnet": {
"port": 9630,
"local_port": 19630,
"namespace": "lux-mainnet",
"chains": [
("C-chain", "C", 96369),
("Zoo", "FGanBWHZEetE7yXqGxKeUWHjDxmuKYpQ6sGdHJnrDNqQ8AoP7", 200200),
# Hanzo, SPC, Pars: our key has 0 balance on mainnet (treasury key needed)
],
},
"testnet": {
"port": 9640,
"local_port": 19640,
"namespace": "lux-testnet",
"chains": [
("C-chain", "C", 96368),
("Zoo", "vEw2TquNRZm5ckng8fJi9JGuKiuTdQA81YNZC3A1jeYhDRH5V", 200201),
# Hanzo, SPC, Pars: 0 balance on testnet for our key
],
},
"devnet": {
"port": 9650,
"local_port": 19650,
"namespace": "lux-devnet",
"chains": [
("C-chain", "C", 96370),
("Zoo", "2dNppzzx7WC1cagAzVRjVP4Qf5n9BRLgVsd2L6BwDbocAVVqBz", 200202),
("Hanzo", "8saqkhsUrijPci3RXQnNaaLLkYXBErauhwUsZZwdj8erxeaZv", 36964),
("SPC", "AbticcB4ymCPL914ZBMozSSjFwVGw8xFKM35aiJDTAs6aed4M", 36912),
("Pars", "2LnHt7NQTpLHCpAxbfHkRNse7bpkRGXwrLQvGzE7tapSq9qsyh", 494951),
],
},
}
# Transaction amount: 0.001 native token
TX_VALUE = Web3.to_wei(0.001, "ether")
# Port forward processes to clean up
pf_procs = []
def cleanup():
"""Kill all port-forward processes."""
for proc in pf_procs:
try:
proc.terminate()
proc.wait(timeout=5)
except Exception:
try:
proc.kill()
except Exception:
pass
def signal_handler(sig, frame):
cleanup()
sys.exit(1)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
def start_port_forward(namespace, local_port, remote_port):
"""Start kubectl port-forward in background."""
cmd = [
"kubectl", "port-forward",
"-n", namespace,
"svc/luxd-headless",
f"{local_port}:{remote_port}",
]
proc = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
pf_procs.append(proc)
time.sleep(2) # Wait for port forward to establish
return proc
def check_port_in_use(port):
"""Check if a port is already in use."""
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex(("127.0.0.1", port)) == 0
def get_rpc_url(local_port, blockchain_id):
"""Build the RPC URL for a chain."""
return f"http://localhost:{local_port}/ext/bc/{blockchain_id}/rpc"
def send_transactions(network_name, net_config, account, tx_count):
"""Send transactions on all chains for a network."""
local_port = net_config["local_port"]
namespace = net_config["namespace"]
remote_port = net_config["port"]
# Start port forward if not already running
if not check_port_in_use(local_port):
print(f"\n Starting port-forward {namespace} {local_port}:{remote_port}...")
start_port_forward(namespace, local_port, remote_port)
if not check_port_in_use(local_port):
print(f" ERROR: Failed to establish port-forward for {namespace}")
return
else:
print(f"\n Port {local_port} already forwarded")
for chain_name, blockchain_id, expected_chain_id in net_config["chains"]:
rpc_url = get_rpc_url(local_port, blockchain_id)
print(f"\n --- {network_name} / {chain_name} (chain ID {expected_chain_id}) ---")
print(f" RPC: {rpc_url}")
try:
w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 10}))
# Verify chain ID
chain_id = w3.eth.chain_id
if chain_id != expected_chain_id:
print(f" WARNING: Expected chain ID {expected_chain_id}, got {chain_id}")
# Check balance
balance = w3.eth.get_balance(account.address)
balance_eth = Web3.from_wei(balance, "ether")
print(f" Balance: {balance_eth:.6f}")
if balance < TX_VALUE * 2: # Need at least 2x value for gas
print(f" SKIP: Insufficient balance ({balance_eth} < {Web3.from_wei(TX_VALUE * 2, 'ether')})")
continue
# Get starting nonce
nonce = w3.eth.get_transaction_count(account.address)
print(f" Starting nonce: {nonce}")
# Get gas price
gas_price = w3.eth.gas_price
print(f" Gas price: {Web3.from_wei(gas_price, 'gwei'):.2f} gwei")
# Send transactions
tx_hashes = []
for i in range(tx_count):
tx = {
"to": account.address, # Self-transfer
"value": TX_VALUE,
"gas": 21000,
"gasPrice": gas_price,
"nonce": nonce + i,
"chainId": chain_id,
}
signed = account.sign_transaction(tx)
try:
tx_hash = w3.eth.send_raw_transaction(signed.raw_transaction)
tx_hashes.append(tx_hash.hex())
print(f" TX {i+1}/{tx_count}: {tx_hash.hex()}")
except Exception as e:
print(f" TX {i+1}/{tx_count} FAILED: {e}")
break
# Wait for last tx to be mined (up to 30s)
if tx_hashes:
print(f"\n Waiting for confirmations...")
last_hash = tx_hashes[-1]
for attempt in range(30):
try:
receipt = w3.eth.get_transaction_receipt(last_hash)
if receipt:
print(f" Last TX confirmed in block {receipt['blockNumber']}, status={receipt['status']}")
break
except Exception:
pass
time.sleep(1)
else:
print(f" WARNING: Last TX not confirmed after 30s")
# Print final balance
new_balance = w3.eth.get_balance(account.address)
spent = balance - new_balance
print(f" New balance: {Web3.from_wei(new_balance, 'ether'):.6f} (spent {Web3.from_wei(spent, 'ether'):.6f} in gas)")
print(f" Total TXs sent: {len(tx_hashes)}")
except Exception as e:
print(f" ERROR: {e}")
def main():
parser = argparse.ArgumentParser(description="Send test transactions on Lux chains")
parser.add_argument(
"--network",
choices=["mainnet", "testnet", "devnet", "all"],
default="all",
help="Network to send on (default: all)",
)
parser.add_argument(
"--count",
type=int,
default=10,
help="Number of transactions per chain (default: 10)",
)
args = parser.parse_args()
# Load private key
print("Loading private key...")
with open(KEY_FILE) as f:
key_hex = f.read().strip()
account = Account.from_key("0x" + key_hex)
print(f"Address: {account.address}")
# Determine networks to process
if args.network == "all":
networks = ["mainnet", "testnet", "devnet"]
else:
networks = [args.network]
try:
for net in networks:
print(f"\n{'='*60}")
print(f" NETWORK: {net.upper()}")
print(f"{'='*60}")
send_transactions(net, CHAINS[net], account, args.count)
finally:
cleanup()
print(f"\n{'='*60}")
print(" DONE")
print(f"{'='*60}")
if __name__ == "__main__":
main()
+75 -26
View File
@@ -1,7 +1,5 @@
// Chain detail page — shows chain info, validators, and subnet details.
// Modeled after Avalanche's L1 Details page.
import { Box, Flex, Grid, Text } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
@@ -12,16 +10,11 @@ import { Tag } from 'toolkit/chakra/tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import PageTitle from 'ui/shared/Page/PageTitle';
// ── Constants ──
const PRIMARY_NETWORK_ID = '11111111111111111111111111111111LpoYY';
const LUX_DECIMALS = 9;
const INFO_ROW_BG = { _light: 'gray.50', _dark: 'whiteAlpha.50' };
// All 14 primary network chains from ~/work/lux/node/node/vms_allvms.go
// Core: C (EVM), P (PlatformVM), X (ExchangeVM)
// Extended (allvms build): A, B, D, G, I, K, O, Q, R, T, Z
const PRIMARY_CHAIN_META: Readonly<Record<string, {
readonly name: string;
readonly fullName: string;
@@ -157,7 +150,46 @@ const KNOWN_VM_IDS: Readonly<Record<string, string>> = {
rWhpuQPF1kb72esV2momhMuTYGkEb1oL29pt2EBXWsBY6MALT: 'PVM',
};
// ── Helpers ──
// DAG chain indexer API URLs
const CHAIN_INDEXER_URLS: Readonly<Record<string, string>> = {
'x-chain': 'https://api-indexer-xchain.lux.network',
'a-chain': 'https://api-indexer-achain.lux.network',
'b-chain': 'https://api-indexer-bchain.lux.network',
'q-chain': 'https://api-indexer-qchain.lux.network',
't-chain': 'https://api-indexer-tchain.lux.network',
'z-chain': 'https://api-indexer-zchain.lux.network',
'k-chain': 'https://api-indexer-kchain.lux.network',
'p-chain': 'https://api-indexer-pchain.lux.network',
'c-chain': 'https://api-indexer.lux.network',
};
interface IndexerStats {
readonly chain_stats: Record<string, unknown>;
readonly dag_stats?: {
readonly total_vertices: number;
readonly total_edges: number;
readonly accepted_vertices: number;
readonly pending_vertices: number;
readonly chain_type: string;
readonly last_updated: string;
};
}
function useChainIndexerStats(slug: string): { stats: IndexerStats | null; isLoading: boolean } {
const indexerUrl = CHAIN_INDEXER_URLS[slug];
const query = useQuery({
queryKey: [ 'chainIndexerStats', slug ],
queryFn: async() => {
if (!indexerUrl) return null;
const res = await fetch(`${ indexerUrl }/api/v2/stats`);
if (!res.ok) return null;
return await res.json() as IndexerStats;
},
staleTime: 30_000,
enabled: Boolean(indexerUrl),
});
return { stats: query.data ?? null, isLoading: query.isLoading };
}
function formatStake(nanoLux: bigint): string {
const lux = Number(nanoLux) / Math.pow(10, LUX_DECIMALS);
@@ -171,8 +203,6 @@ function truncateId(id: string, len: number = 16): string {
return `${ id.slice(0, 8) }...${ id.slice(-6) }`;
}
// ── Info row ──
interface InfoRowProps {
readonly label: string;
readonly value: string;
@@ -213,8 +243,6 @@ const InfoRow = ({ label, value, isMono = false, canCopy = false }: InfoRowProps
</Flex>
);
// ── Validator row ──
interface ValidatorRowProps {
readonly validator: PChainValidator;
readonly index: number;
@@ -266,8 +294,6 @@ const ValidatorRow = ({ validator, index }: ValidatorRowProps) => {
);
};
// ── Main component ──
const ChainDetailPage = () => {
const router = useRouter();
const slug = (router.query.slug as string || '').toLowerCase();
@@ -275,8 +301,8 @@ const ChainDetailPage = () => {
const { blockchains, isLoading: chainsLoading } = useBlockchains();
const { validators, isLoading: validatorsLoading } = useCurrentValidators();
const { subnets } = useSubnets();
const { stats: indexerStats } = useChainIndexerStats(slug);
// Resolve chain from slug
const resolvedChain = React.useMemo<{
blockchain: PChainBlockchain | undefined;
isPrimary: boolean;
@@ -311,19 +337,16 @@ const ChainDetailPage = () => {
const explorerUrl = EXPLORER_URLS[slug];
const isLoading = chainsLoading || validatorsLoading;
// Get subnet info
const subnet = React.useMemo(
() => subnets.find((s) => s.id === subnetId),
[ subnets, subnetId ],
);
// Get chains in same subnet
const subnetChains = React.useMemo(
() => subnetId ? blockchains.filter((c) => c.subnetID === subnetId) : [],
[ blockchains, subnetId ],
);
// Total stake
const totalStake = React.useMemo(
() => validators.reduce((sum, v) => sum + BigInt(v.stakeAmount || v.weight || '0'), BigInt(0)),
[ validators ],
@@ -342,7 +365,6 @@ const ChainDetailPage = () => {
) }
/>
{ /* Description */ }
<Box
p={ 4 }
mb={ 6 }
@@ -356,7 +378,6 @@ const ChainDetailPage = () => {
</Text>
</Box>
{ /* Stats bar */ }
<Grid
templateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }}
gap={ 3 }
@@ -404,7 +425,6 @@ const ChainDetailPage = () => {
</Box>
</Grid>
{ /* Chain Info table */ }
<Box mb={ 6 }>
<Text fontSize="sm" fontWeight={ 600 } color="text.primary" mb={ 3 }>
Chain Info
@@ -422,7 +442,39 @@ const ChainDetailPage = () => {
</Box>
</Box>
{ /* Subnet Chains table */ }
{ indexerStats && (
<Box mb={ 6 }>
<Flex align="center" gap={ 2 } mb={ 3 }>
<Text fontSize="sm" fontWeight={ 600 } color="text.primary">
Indexer Stats
</Text>
{ indexerStats.dag_stats && (
<Tag size="sm" variant="subtle">
{ indexerStats.dag_stats.chain_type.toUpperCase() }
</Tag>
) }
</Flex>
<Box border="1px solid" borderColor="border.divider" borderRadius="lg" overflow="hidden">
{ indexerStats.dag_stats && (
<>
<InfoRow label="Total Vertices" value={ String(indexerStats.dag_stats.total_vertices) }/>
<InfoRow label="Total Edges" value={ String(indexerStats.dag_stats.total_edges) }/>
<InfoRow label="Accepted Vertices" value={ String(indexerStats.dag_stats.accepted_vertices) }/>
<InfoRow label="Pending Vertices" value={ String(indexerStats.dag_stats.pending_vertices) }/>
<InfoRow label="Last Updated" value={ indexerStats.dag_stats.last_updated }/>
</>
) }
{ Object.entries(indexerStats.chain_stats).map(([ key, value ]) => (
<InfoRow
key={ key }
label={ key.split('_').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') }
value={ typeof value === 'object' ? JSON.stringify(value) : String(value) }
/>
)) }
</Box>
</Box>
) }
{ subnetChains.length > 0 && (
<Box mb={ 6 }>
<Flex align="center" gap={ 2 } mb={ 3 }>
@@ -432,7 +484,6 @@ const ChainDetailPage = () => {
<Tag size="sm" variant="subtle">{ subnetChains.length }</Tag>
</Flex>
<Box border="1px solid" borderColor="border.divider" borderRadius="lg" overflow="hidden">
{ /* Table header */ }
<Flex
px={ 4 }
py={ 2 }
@@ -497,7 +548,6 @@ const ChainDetailPage = () => {
</Box>
) }
{ /* Validators table */ }
<Box mb={ 6 }>
<Flex align="center" gap={ 2 } mb={ 3 }>
<Text fontSize="sm" fontWeight={ 600 } color="text.primary">
@@ -508,7 +558,6 @@ const ChainDetailPage = () => {
</Skeleton>
</Flex>
<Box border="1px solid" borderColor="border.divider" borderRadius="lg" overflow="hidden">
{ /* Table header */ }
<Flex
px={ 4 }
py={ 2 }
+11 -32
View File
@@ -1,7 +1,3 @@
// Network Overview homepage for the Lux multi-chain explorer.
// Combines P-chain network stats, Blockscout chain stats, latest blocks/txs,
// and chain directory cards with links to per-chain explorers.
import { Box, Flex, Grid, Text } from '@chakra-ui/react';
import React from 'react';
@@ -17,29 +13,24 @@ import LatestBlocks from 'ui/home/LatestBlocks';
import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions';
// ── Constants ──
const PRIMARY_NETWORK_ID = '11111111111111111111111111111111LpoYY';
const LUX_DECIMALS = 9;
// All 14 primary network chains from ~/work/lux/node/node/vms_allvms.go
// Core chains (always active): C, P, X
// Extended chains (allvms build): A, B, D, G, I, K, O, Q, R, T, Z
const PRIMARY_CHAINS = [
{ id: 'C', name: 'C-Chain', fullName: 'Contract Chain', vm: 'EVM', href: '/' },
{ id: 'P', name: 'P-Chain', fullName: 'Platform Chain', vm: 'PVM', href: '/validators' },
{ id: 'X', name: 'X-Chain', fullName: 'Exchange Chain', vm: 'AVM', href: undefined },
{ id: 'D', name: 'D-Chain', fullName: 'DEX Chain', vm: 'DexVM', href: undefined },
{ id: 'A', name: 'A-Chain', fullName: 'AI Chain', vm: 'AIVM', href: undefined },
{ id: 'X', name: 'X-Chain', fullName: 'Exchange Chain', vm: 'AVM', href: '/chain/x-chain' },
{ id: 'D', name: 'D-Chain', fullName: 'DEX Chain', vm: 'DexVM', href: '/dex' },
{ id: 'A', name: 'A-Chain', fullName: 'AI Chain', vm: 'AIVM', href: '/ai' },
{ id: 'B', name: 'B-Chain', fullName: 'Bridge Chain', vm: 'BridgeVM', href: '/bridge' },
{ id: 'Q', name: 'Q-Chain', fullName: 'Quantum Chain', vm: 'QuantumVM', href: undefined },
{ id: 'T', name: 'T-Chain', fullName: 'Threshold Chain', vm: 'ThresholdVM', href: undefined },
{ id: 'Z', name: 'Z-Chain', fullName: 'ZK Chain', vm: 'ZKVM', href: undefined },
{ id: 'G', name: 'G-Chain', fullName: 'Graph Chain', vm: 'GraphVM', href: undefined },
{ id: 'K', name: 'K-Chain', fullName: 'Key Chain', vm: 'KeyVM', href: undefined },
{ id: 'O', name: 'O-Chain', fullName: 'Oracle Chain', vm: 'OracleVM', href: undefined },
{ id: 'R', name: 'R-Chain', fullName: 'Relay Chain', vm: 'RelayVM', href: undefined },
{ id: 'I', name: 'I-Chain', fullName: 'Identity Chain', vm: 'IdentityVM', href: undefined },
{ id: 'Q', name: 'Q-Chain', fullName: 'Quantum Chain', vm: 'QuantumVM', href: '/chain/q-chain' },
{ id: 'T', name: 'T-Chain', fullName: 'Threshold Chain', vm: 'ThresholdVM', href: '/chain/t-chain' },
{ id: 'Z', name: 'Z-Chain', fullName: 'ZK Chain', vm: 'ZKVM', href: '/chain/z-chain' },
{ id: 'G', name: 'G-Chain', fullName: 'Graph Chain', vm: 'GraphVM', href: '/chain/g-chain' },
{ id: 'K', name: 'K-Chain', fullName: 'Key Chain', vm: 'KeyVM', href: '/chain/k-chain' },
{ id: 'O', name: 'O-Chain', fullName: 'Oracle Chain', vm: 'OracleVM', href: '/chain/o-chain' },
{ id: 'R', name: 'R-Chain', fullName: 'Relay Chain', vm: 'RelayVM', href: '/chain/r-chain' },
{ id: 'I', name: 'I-Chain', fullName: 'Identity Chain', vm: 'IdentityVM', href: '/chain/i-chain' },
] as const;
const L1_EXPLORER_URLS: Readonly<Record<string, string>> = {
@@ -52,8 +43,6 @@ const L1_EXPLORER_URLS: Readonly<Record<string, string>> = {
const STAT_CARD_BG = { _light: 'gray.50', _dark: 'whiteAlpha.50' };
const CHAIN_CARD_HOVER = { _light: 'gray.50', _dark: 'whiteAlpha.100' };
// ── Helpers ──
function formatStake(nanoLux: bigint): string {
const lux = Number(nanoLux) / Math.pow(10, LUX_DECIMALS);
if (lux >= 1_000_000) return `${ (lux / 1_000_000).toFixed(1) }M`;
@@ -61,8 +50,6 @@ function formatStake(nanoLux: bigint): string {
return lux.toFixed(0);
}
// ── Sub-components ──
interface NetworkStatProps {
readonly label: string;
readonly value: string;
@@ -167,8 +154,6 @@ const L1ChainCard = ({ chain }: L1ChainCardProps) => {
);
};
// ── Main component ──
const NetworkOverview = () => {
const isMobile = useIsMobile();
const { stats, isLoading: validatorsLoading } = useCurrentValidators();
@@ -186,7 +171,6 @@ const NetworkOverview = () => {
<Box as="main">
<HeroBanner/>
{ /* Network stats bar */ }
<Flex
justify="space-around"
align="center"
@@ -223,7 +207,6 @@ const NetworkOverview = () => {
/>
</Flex>
{ /* Blockscout stats + Latest blocks/txs */ }
<Flex mt={ 6 } gap={ 6 } flexDir={{ base: 'column', lg: 'row' }}>
<Stats/>
<LatestBlocks/>
@@ -233,13 +216,11 @@ const NetworkOverview = () => {
<Transactions/>
</Box>
{ /* Chain directory */ }
<Grid
templateColumns={{ base: '1fr', lg: '1fr 1fr' }}
gap={{ base: 6, lg: 8 }}
mt={ 8 }
>
{ /* Primary Network */ }
<Box>
<Flex align="center" justify="space-between" mb={ 3 }>
<Heading level="3">Primary Network</Heading>
@@ -258,7 +239,6 @@ const NetworkOverview = () => {
</Flex>
</Box>
{ /* L1 Chains */ }
<Box>
<Flex align="center" justify="space-between" mb={ 3 }>
<Heading level="3">L1 Chains</Heading>
@@ -286,7 +266,6 @@ const NetworkOverview = () => {
</Flex>
) }
{ /* View all link */ }
{ !isMobile && (
<Flex justify="center" mt={ 4 }>
<Link href="/chains" textStyle="sm">View all chains</Link>
+1 -4
View File
@@ -1,6 +1,3 @@
// Chain switcher dropdown for the top bar.
// Shows all Lux chains with links to their respective explorer instances.
import { Box, Flex, Text } from '@chakra-ui/react';
import React from 'react';
@@ -22,7 +19,7 @@ const CHAINS: ReadonlyArray<ChainItem> = [
{ name: 'Zoo', label: 'Zoo Chain', vm: 'Subnet EVM', url: 'https://explore-zoo.lux.network', isCurrent: false },
{ name: 'Hanzo', label: 'Hanzo AI', vm: 'Subnet EVM', url: 'https://explore-hanzo.lux.network', isCurrent: false },
{ name: 'SPC', label: 'SPC Chain', vm: 'Subnet EVM', url: 'https://explore-spc.lux.network', isCurrent: false },
{ name: 'Pars', label: 'Pars Chain', vm: 'Subnet EVM', url: 'https://explore-pars.lux.network', isCurrent: false },
{ name: 'Pars', label: 'Pars Network', vm: 'Subnet EVM', url: 'https://explore.pars.network', isCurrent: false },
];
const ChainSwitcher = () => {