mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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:
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Vendored
+3
-3
@@ -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 }>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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()
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user