mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
chains: read each chain from that chain, and list the ones this network runs
/chains showed fifteen rows on every network, from a hardcoded table. Four of
them — T, R, I, O — are registered on no network at all; the node answers
"there is no ID with alias: T". A fifth, M, runs on devnet alone. And every
/chains/<slug> page printed the same four numbers: the primary network's
validator count, its total stake, its chain count, its threshold. Nothing on
Q-Chain's page was about Q-Chain. Fifteen pages, one set of facts.
The network already knows its own answer. platform.getBlockchains is the chain
list, and it is per-network for free: mainnet and testnet report nine chains,
devnet reports ten including M. That is what /chains lists now, joined with
configs/app/primaryChains.ts for presentation only — a chain the network
reports and we have no metadata for still gets a row, because the network is
the source of truth and our table is not.
Three facts per chain, each read from that chain:
* the blockchain ID, from the registry — the thing that actually tells one
chain from another, and it was already on screen while the numbers above
it were not
* whether the node has bootstrapped it, from info.isBootstrapped, which the
gateway routes for every chain. This is how Q-Chain's page now says
Syncing where the other nine say Bootstrapped: Q has not come up on any of
the three networks and is the sole reason all three report healthy:false
* its head, in its own dialect — eth_blockNumber for C, platform.getHeight
for P, xvm.getHeight for X. Not avm: that service is gone from this build,
and asking it returns "can't find service" rather than a height
Validators are the P-Chain's model, so they render on the P-Chain and nowhere
else. The head is labelled for the model it comes from — the X-Chain's is an
accepted height, not a block height.
Where a chain has no head to read, it says so. The gateway routes exactly P, X
and C on all three networks; D, A, B, Z, G, K and Q are registered and
bootstrapped on the node but their RPC 404s from a browser. Proof it is the
route and not the chain: C-Chain's own blockchain ID, /v1/bc/25td8att…/rpc,
404s while /v1/bc/C/rpc returns a height — same chain, same node. Those pages
read "No public RPC" and say plainly that the chain is registered and running
but not published. A zero there would have read as a measurement.
One proxy replaces the P-only one. /v1/pchain hardcoded /v1/bc/P; /v1/node/
<endpoint> resolves the endpoint against the primary-chain table and never
treats it as a URL. A chain the gateway does not route comes back 200 carrying
a JSON-RPC error rather than 502 — nothing is broken, and 502 put a red failed
request in the console of eight of the ten chain pages.
Deleted: CHAIN_INDEXER_URLS and the hook that read it — fifteen hostnames of
the shape api-indexer-<x>chain.lux.network, none of which resolve. Fifteen of
fifteen NXDOMAIN. It fetched them on every chain page and rendered the null.
Measured on mainnet, 1440x900 and 390x844, fresh context per URL: 20 of 20
chain/screen rows agree with RPC on both status and head — C-Chain 1,107,826
against a live head bracketed at 1,107,822..1,107,829, P and X at 0, seven
reading No public RPC, Q Syncing while the rest are Bootstrapped. /chains
lists the same ten chains the P-Chain reports, and 10 of 10 row links reach a
real page. Zero 4xx or 5xx from our own origin.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
// Per-chain facts for the primary-network chain pages.
|
||||
|
||||
export { useChainFacts } from './useChainFacts';
|
||||
export type { ChainFacts, Head } from './useChainFacts';
|
||||
@@ -0,0 +1,111 @@
|
||||
// What is true about ONE primary-network chain, read from that chain.
|
||||
//
|
||||
// Every /chains/<slug> page used to print the same four numbers — the
|
||||
// network-wide validator count, total stake, chain count and threshold — so
|
||||
// Q-Chain and Z-Chain and K-Chain were the same page with the title swapped.
|
||||
// None of it was about the chain you opened.
|
||||
//
|
||||
// Three facts are per-chain and real:
|
||||
// * the blockchain ID, from the P-Chain registry — unique, and the thing that
|
||||
// actually distinguishes one chain from another
|
||||
// * whether the node has bootstrapped it, from info.isBootstrapped, which the
|
||||
// gateway routes for every chain
|
||||
// * its head, from its own RPC, in whatever dialect it speaks
|
||||
//
|
||||
// The last one is only available where the gateway routes the chain. Today that
|
||||
// is P, X and C on all three networks; the rest are registered and bootstrapped
|
||||
// but answer 404 from the browser's only reachable origin. That is reported as
|
||||
// `unreachable`, never as a height of zero — a chain whose head we cannot read and
|
||||
// a chain sitting at genesis are different states and must not render alike.
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { PrimaryVm } from 'configs/app/primaryChains';
|
||||
|
||||
const STALE_MS = 15_000;
|
||||
|
||||
export type Head =
|
||||
{ readonly kind: 'height'; readonly height: number } |
|
||||
{ readonly kind: 'unreachable' } |
|
||||
{ readonly kind: 'unknown' };
|
||||
|
||||
export interface ChainFacts {
|
||||
readonly blockchainId: string | null;
|
||||
readonly bootstrapped: boolean | null;
|
||||
readonly head: Head;
|
||||
readonly chainId: number | null;
|
||||
}
|
||||
|
||||
async function rpc(endpoint: string, method: string, params: unknown): Promise<unknown> {
|
||||
const res = await fetch(`/v1/node/${ endpoint }`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
const body = await res.json() as { result?: unknown };
|
||||
return body.result ?? null;
|
||||
}
|
||||
|
||||
// Each dialect names its head differently, and asking the wrong one gets a
|
||||
// "can't find service" error rather than an answer — X speaks xvm, not avm, and
|
||||
// not platform.
|
||||
async function readHead(vm: PrimaryVm): Promise<Head> {
|
||||
try {
|
||||
if (vm.view === 'evm' || vm.view === 'dex') {
|
||||
const hex = await rpc(vm.slug, 'eth_blockNumber', []) as string | null;
|
||||
return hex ? { kind: 'height', height: parseInt(hex, 16) } : { kind: 'unreachable' };
|
||||
}
|
||||
const method = vm.view === 'platform' ? 'platform.getHeight' : 'xvm.getHeight';
|
||||
const out = await rpc(vm.slug, method, {}) as { height?: string } | null;
|
||||
return out?.height !== undefined ?
|
||||
{ kind: 'height', height: Number(out.height) } :
|
||||
{ kind: 'unreachable' };
|
||||
} catch {
|
||||
return { kind: 'unreachable' };
|
||||
}
|
||||
}
|
||||
|
||||
async function readChainId(vm: PrimaryVm): Promise<number | null> {
|
||||
if (vm.view !== 'evm' && vm.view !== 'dex') {
|
||||
return null;
|
||||
}
|
||||
const hex = await rpc(vm.slug, 'eth_chainId', []) as string | null;
|
||||
return hex ? parseInt(hex, 16) : null;
|
||||
}
|
||||
|
||||
export function useChainFacts(vm: PrimaryVm | undefined, blockchainId: string | null): {
|
||||
readonly facts: ChainFacts;
|
||||
readonly isLoading: boolean;
|
||||
} {
|
||||
const query = useQuery({
|
||||
queryKey: [ 'chain:facts', vm?.slug ],
|
||||
enabled: Boolean(vm),
|
||||
staleTime: STALE_MS,
|
||||
refetchInterval: STALE_MS,
|
||||
queryFn: async(): Promise<Omit<ChainFacts, 'blockchainId'>> => {
|
||||
if (!vm) {
|
||||
return { bootstrapped: null, head: { kind: 'unknown' }, chainId: null };
|
||||
}
|
||||
const alias = vm.slug.charAt(0).toUpperCase();
|
||||
const [ boot, head, chainId ] = await Promise.all([
|
||||
rpc('info', 'info.isBootstrapped', { chain: alias }) as Promise<{ isBootstrapped?: boolean } | null>,
|
||||
readHead(vm),
|
||||
readChainId(vm),
|
||||
]);
|
||||
return { bootstrapped: boot?.isBootstrapped ?? null, head, chainId };
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
facts: {
|
||||
blockchainId,
|
||||
bootstrapped: query.data?.bootstrapped ?? null,
|
||||
head: query.data?.head ?? { kind: 'unknown' },
|
||||
chainId: query.data?.chainId ?? vm?.chainId ?? null,
|
||||
},
|
||||
isLoading: query.isLoading,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// React Query hook for platform.getBlockchains.
|
||||
// Returns the list of all blockchains registered on the P-chain.
|
||||
// Uses the server-side /v1/pchain proxy to bypass CORS.
|
||||
// Uses the server-side /v1/node/p-chain proxy to bypass CORS.
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
@@ -12,7 +12,7 @@ const BLOCKCHAINS_QUERY_KEY = 'pchain:blockchains' as const;
|
||||
const EMPTY_BLOCKCHAINS: ReadonlyArray<PChainBlockchain> = [];
|
||||
|
||||
async function fetchBlockchains(): Promise<ReadonlyArray<PChainBlockchain>> {
|
||||
const res = await fetch('/v1/pchain', {
|
||||
const res = await fetch('/v1/node/p-chain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -15,7 +15,7 @@ async function fetchChainHeights(): Promise<{ pChain: number; cChain: number }>
|
||||
|
||||
const [ pRes, cRes ] = await Promise.allSettled([
|
||||
// P-chain: use server-side proxy to bypass CORS
|
||||
fetch('/v1/pchain', {
|
||||
fetch('/v1/node/p-chain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jsonrpc: '2.0', method: 'platform.getHeight', params: {}, id: 1 }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// React Query hook for platform.getCurrentValidators.
|
||||
// Returns the validator list and aggregated network statistics.
|
||||
// Uses the server-side /v1/pchain proxy to bypass CORS.
|
||||
// Uses the server-side /v1/node/p-chain proxy to bypass CORS.
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
@@ -63,7 +63,7 @@ export interface UseCurrentValidatorsResult {
|
||||
}
|
||||
|
||||
async function fetchCurrentValidators(): Promise<UseCurrentValidatorsResult> {
|
||||
const res = await fetch('/v1/pchain', {
|
||||
const res = await fetch('/v1/node/p-chain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// a sovereign L1 is a Network. The node's only wire method is
|
||||
// `platform.getNets` (vms/platformvm/service.go: GetNets/APINet) — the legacy
|
||||
// upstream method name does not exist here and returns JSON-RPC -32000.
|
||||
// Uses the server-side /v1/pchain proxy to bypass CORS.
|
||||
// Uses the server-side /v1/node/p-chain proxy to bypass CORS.
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
@@ -15,7 +15,7 @@ const NETS_QUERY_KEY = 'pchain:nets' as const;
|
||||
const EMPTY_NETS: ReadonlyArray<PChainNet> = [];
|
||||
|
||||
async function fetchNets(): Promise<ReadonlyArray<PChainNet>> {
|
||||
const res = await fetch('/v1/pchain', {
|
||||
const res = await fetch('/v1/node/p-chain', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -38,7 +38,7 @@ export interface NetworkValidators {
|
||||
}
|
||||
|
||||
async function fetchCount(chainName: string): Promise<number> {
|
||||
const res = await fetch(`/v1/pchain?chain=${ encodeURIComponent(chainName) }`, {
|
||||
const res = await fetch(`/v1/node/p-chain?chain=${ encodeURIComponent(chainName) }`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -100,7 +100,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/v1/csrf': 'Regular page',
|
||||
'/v1/healthz': 'Regular page',
|
||||
'/v1/config': 'Regular page',
|
||||
'/v1/pchain': 'Regular page',
|
||||
'/v1/node/[endpoint]': 'Regular page',
|
||||
};
|
||||
|
||||
export default function getPageOgType(pathname: Route['pathname']) {
|
||||
|
||||
@@ -103,7 +103,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/v1/csrf': DEFAULT_TEMPLATE,
|
||||
'/v1/healthz': DEFAULT_TEMPLATE,
|
||||
'/v1/config': DEFAULT_TEMPLATE,
|
||||
'/v1/pchain': DEFAULT_TEMPLATE,
|
||||
'/v1/node/[endpoint]': DEFAULT_TEMPLATE,
|
||||
};
|
||||
|
||||
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
|
||||
|
||||
@@ -105,7 +105,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/v1/csrf': '%network_name% node API CSRF token',
|
||||
'/v1/healthz': '%network_name% node API health check',
|
||||
'/v1/config': '%network_name% node API app config',
|
||||
'/v1/pchain': '%network_name% node API P-chain proxy',
|
||||
'/v1/node/[endpoint]': '%network_name% node API JSON-RPC proxy',
|
||||
};
|
||||
|
||||
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
|
||||
|
||||
@@ -98,7 +98,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/v1/csrf': 'Node API: CSRF token',
|
||||
'/v1/healthz': 'Node API: Health check',
|
||||
'/v1/config': 'Node API: App config',
|
||||
'/v1/pchain': 'Node API: P-chain proxy',
|
||||
'/v1/node/[endpoint]': 'Node API: JSON-RPC proxy',
|
||||
};
|
||||
|
||||
export default function getPageType(pathname: Route['pathname']) {
|
||||
|
||||
Vendored
+2
-4
@@ -19,20 +19,19 @@ declare module "nextjs-routes" {
|
||||
| DynamicRoute<"/address/[hash]", { "hash": string }>
|
||||
| StaticRoute<"/advanced-filter">
|
||||
| StaticRoute<"/ai">
|
||||
| StaticRoute<"/api/ai">
|
||||
| StaticRoute<"/api/config">
|
||||
| StaticRoute<"/api/csrf">
|
||||
| StaticRoute<"/api/healthz">
|
||||
| StaticRoute<"/api/log">
|
||||
| StaticRoute<"/api/metrics">
|
||||
| StaticRoute<"/api/monitoring/invalid-api-schema">
|
||||
| StaticRoute<"/api/pchain">
|
||||
| DynamicRoute<"/api/node/[endpoint]", { "endpoint": string }>
|
||||
| StaticRoute<"/api/proxy">
|
||||
| DynamicRoute<"/api/tokens/[hash]/instances/[id]/media-type", { "hash": string; "id": string }>
|
||||
| StaticRoute<"/api-docs">
|
||||
| DynamicRoute<"/apps/[id]", { "id": string }>
|
||||
| StaticRoute<"/apps">
|
||||
| StaticRoute<"/auth/callback">
|
||||
| StaticRoute<"/auth/profile">
|
||||
| DynamicRoute<"/batches/[number]", { "number": string }>
|
||||
| DynamicRoute<"/batches/celestia/[height]/[commitment]", { "height": string; "commitment": string }>
|
||||
| StaticRoute<"/batches">
|
||||
@@ -71,7 +70,6 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/">
|
||||
| StaticRoute<"/internal-txs">
|
||||
| StaticRoute<"/interop-messages">
|
||||
| StaticRoute<"/login">
|
||||
| StaticRoute<"/mud-worlds">
|
||||
| DynamicRoute<"/name-services/clusters/[name]", { "name": string }>
|
||||
| DynamicRoute<"/name-services/domains/[name]", { "name": string }>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@luxfi/explore",
|
||||
"version": "1.1.29",
|
||||
"version": "1.1.30",
|
||||
"private": false,
|
||||
"homepage": "https://github.com/luxfi/explore#readme",
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// JSON-RPC pass-through to one endpoint on our own node.
|
||||
//
|
||||
// The browser cannot call the node directly: the gateway answers 404 to the
|
||||
// OPTIONS preflight for /v1/bc/*, so a cross-origin RPC fails before it is
|
||||
// sent. Forwarding from the server side has no preflight.
|
||||
//
|
||||
// `endpoint` is resolved against a fixed table and never used as a URL. An
|
||||
// arbitrary value here would make this an open request forwarder for anything
|
||||
// the server can reach.
|
||||
//
|
||||
// Replaces /v1/pchain, which hardcoded /v1/bc/P. The P-Chain is not the only
|
||||
// chain a page needs to ask about — every primary-network chain has its own
|
||||
// identity and its own liveness, and reading them was the whole reason each
|
||||
// chain page showed the same network-wide validator count.
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { CHAINS, getCurrentChain } from 'configs/app/chainRegistry';
|
||||
import { PRIMARY_VMS } from 'configs/app/primaryChains';
|
||||
import { getEnvValue } from 'configs/app/utils';
|
||||
|
||||
const TIMEOUT_MS = 10_000;
|
||||
|
||||
// endpoint -> path on the node. `info` is the node's own API; every other
|
||||
// entry is a primary-network chain, addressed by its single-letter alias.
|
||||
// EVM chains answer under /rpc, the rest at the bare chain path.
|
||||
const PATHS: Readonly<Record<string, string>> = {
|
||||
info: '/v1/info',
|
||||
...Object.fromEntries(PRIMARY_VMS.map((vm) => [
|
||||
vm.slug,
|
||||
`/v1/bc/${ vm.slug.charAt(0).toUpperCase() }${ vm.view === 'evm' || vm.view === 'dex' ? '/rpc' : '' }`,
|
||||
])),
|
||||
};
|
||||
|
||||
// `?chain=<name>` reads another network's node — every sovereign L1 runs its
|
||||
// own. The name is resolved against the registry, never used as a URL.
|
||||
function getChainBase(name: string): string | undefined {
|
||||
const { network } = getCurrentChain();
|
||||
return CHAINS.find((c) => c.name === name && c.network === network)?.nodeApiUrl;
|
||||
}
|
||||
|
||||
// The node's API origin, derived from the chain RPC URL. That URL points at a
|
||||
// specific EVM chain (canonical `…/v1/bc/C/rpc`), so it must be reduced to
|
||||
// scheme+host — concatenating onto the full RPC path produced a malformed URL,
|
||||
// an HTML 404, and "Unexpected non-whitespace character after JSON" on every
|
||||
// non-C-chain brand.
|
||||
function getApiBase(): string {
|
||||
const rpcUrl = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL') ?? '';
|
||||
try {
|
||||
return rpcUrl ? new URL(rpcUrl).origin : '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const handler = async(req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).json({ error: 'Method not allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = String(req.query.endpoint ?? '').toLowerCase();
|
||||
const path = PATHS[endpoint];
|
||||
if (!path) {
|
||||
res.status(404).json({ error: `Unknown node endpoint "${ endpoint }"` });
|
||||
return;
|
||||
}
|
||||
|
||||
const chain = req.query.chain;
|
||||
if (typeof chain === 'string' && !getChainBase(chain)) {
|
||||
res.status(404).json({ error: `No node API is registered for chain "${ chain }"` });
|
||||
return;
|
||||
}
|
||||
|
||||
const base = typeof chain === 'string' ? getChainBase(chain) : getApiBase();
|
||||
if (!base) {
|
||||
res.status(500).json({ error: 'NEXT_PUBLIC_NETWORK_RPC_URL not configured' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(base + path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req.body),
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
});
|
||||
|
||||
// Valid RPC yields JSON; a chain the gateway does not route yields its HTML
|
||||
// 404. Most primary chains are registered on the P-Chain and bootstrapped
|
||||
// on the node but unreachable, so that is a normal answer about the network,
|
||||
// not a failure of this request — it comes back 200 carrying a JSON-RPC
|
||||
// error, the way JSON-RPC reports an unavailable method. Answering 502
|
||||
// instead put a red failed request in the console of eight of the ten
|
||||
// chain pages and claimed something was broken when nothing was.
|
||||
const raw = await response.text();
|
||||
try {
|
||||
res.status(200).json(JSON.parse(raw));
|
||||
} catch {
|
||||
res.status(200).json({
|
||||
jsonrpc: '2.0',
|
||||
id: null,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `${ path } is not routed by the gateway (HTTP ${ response.status })`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// A timeout or a DNS failure IS a transport fault, and stays one.
|
||||
res.status(502).json({
|
||||
error: 'unreachable',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -1,87 +0,0 @@
|
||||
// Server-side proxy for P-chain JSON-RPC calls.
|
||||
// Bypasses CORS issues with the KrakenD gateway which returns 404 on OPTIONS preflight for /v1/bc/P.
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { CHAINS, getCurrentChain } from 'configs/app/chainRegistry';
|
||||
import { getEnvValue } from 'configs/app/utils';
|
||||
|
||||
const TIMEOUT_MS = 10_000;
|
||||
|
||||
// `?chain=<name>` reads another network's P-Chain — every sovereign L1 runs its
|
||||
// own, so a cross-network validator count needs one hop per chain. The name is
|
||||
// resolved against the registry and never used as a URL: an arbitrary origin
|
||||
// here would make this endpoint a request forwarder for anything on our network.
|
||||
function getChainBase(name: string): string | undefined {
|
||||
const { network } = getCurrentChain();
|
||||
return CHAINS.find((c) => c.name === name && c.network === network)?.nodeApiUrl;
|
||||
}
|
||||
|
||||
// Derive the node's API origin from the chain RPC URL. The RPC URL points at a
|
||||
// specific EVM chain (canonical `…/v1/bc/C/rpc`); the P-chain
|
||||
// lives at `<origin>/v1/bc/P`, so we must reduce to the scheme+host origin and
|
||||
// never naively concatenate onto the full RPC path (which produced a malformed
|
||||
// URL → HTML 404 → "Unexpected non-whitespace character after JSON" for every
|
||||
// non-C-chain brand).
|
||||
function getApiBase(): string {
|
||||
const rpcUrl = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL') ?? '';
|
||||
if (!rpcUrl) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new URL(rpcUrl).origin;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const handler = async(req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
||||
if (req.method !== 'POST') {
|
||||
res.status(405).json({ error: 'Method not allowed' });
|
||||
return;
|
||||
}
|
||||
|
||||
const chain = req.query.chain;
|
||||
if (typeof chain === 'string' && !getChainBase(chain)) {
|
||||
res.status(404).json({ error: `No node API is registered for chain "${ chain }"` });
|
||||
return;
|
||||
}
|
||||
|
||||
const base = typeof chain === 'string' ? getChainBase(chain) : getApiBase();
|
||||
if (!base) {
|
||||
res.status(500).json({ error: 'NEXT_PUBLIC_NETWORK_RPC_URL not configured' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ base }/v1/bc/P`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(req.body),
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
});
|
||||
|
||||
// The node returns JSON for valid P-chain RPC; a wrong URL or gateway
|
||||
// error yields HTML. Read as text and parse defensively so the client gets a
|
||||
// clear error rather than an opaque "non-whitespace character" parse crash.
|
||||
const raw = await response.text();
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
res.status(502).json({
|
||||
error: 'P-chain request failed',
|
||||
message: `Upstream returned non-JSON (HTTP ${ response.status }) from ${ base }/v1/bc/P`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
res.status(502).json({
|
||||
error: 'P-chain request failed',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
+150
-175
@@ -1,13 +1,14 @@
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tag } from '@luxfi/ui/tag';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import type { PrimaryVm } from 'configs/app/primaryChains';
|
||||
import { getPrimaryVm } from 'configs/app/primaryChains';
|
||||
import { useBlockchains, useCurrentValidators, useNets } from 'lib/api/pchain';
|
||||
import type { Head } from 'lib/api/chain';
|
||||
import { useChainFacts } from 'lib/api/chain';
|
||||
import { useBlockchains, useCurrentValidators } from 'lib/api/pchain';
|
||||
import type { PChainBlockchain, PChainValidator } from 'lib/api/pchain';
|
||||
import { cn } from 'lib/utils/cn';
|
||||
import DexPage from 'ui/dex/DexPage';
|
||||
@@ -51,56 +52,6 @@ const KNOWN_VM_IDS: Readonly<Record<string, string>> = {
|
||||
rWhpuQPF1kb72esV2momhMuTYGkEb1oL29pt2EBXWsBY6MALT: 'PVM',
|
||||
};
|
||||
|
||||
// DAG chain indexer API URLs — one endpoint per primary network chain so
|
||||
// ChainDetailPage can fetch live stats (block height, vertex count, etc.)
|
||||
// for any chain the user opens. Missing entries here mean the detail page
|
||||
// falls back to P-Chain blockchain metadata only, with no realtime stats.
|
||||
const CHAIN_INDEXER_URLS: Readonly<Record<string, string>> = {
|
||||
'c-chain': 'https://api-indexer.lux.network',
|
||||
'p-chain': 'https://api-indexer-pchain.lux.network',
|
||||
'x-chain': 'https://api-indexer-xchain.lux.network',
|
||||
'a-chain': 'https://api-indexer-achain.lux.network',
|
||||
'b-chain': 'https://api-indexer-bchain.lux.network',
|
||||
'd-chain': 'https://api-indexer-dchain.lux.network',
|
||||
'g-chain': 'https://api-indexer-gchain.lux.network',
|
||||
'i-chain': 'https://api-indexer-ichain.lux.network',
|
||||
'k-chain': 'https://api-indexer-kchain.lux.network',
|
||||
'm-chain': 'https://api-indexer-mchain.lux.network',
|
||||
'o-chain': 'https://api-indexer-ochain.lux.network',
|
||||
'q-chain': 'https://api-indexer-qchain.lux.network',
|
||||
'r-chain': 'https://api-indexer-rchain.lux.network',
|
||||
't-chain': 'https://api-indexer-tchain.lux.network',
|
||||
'z-chain': 'https://api-indexer-zchain.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 }/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);
|
||||
if (lux >= 1_000_000) return `${ (lux / 1_000_000).toFixed(2) }M`;
|
||||
@@ -113,6 +64,49 @@ function truncateId(id: string, len: number = 16): string {
|
||||
return `${ id.slice(0, 8) }...${ id.slice(-6) }`;
|
||||
}
|
||||
|
||||
// A chain that we cannot reach and a chain sitting at genesis are different
|
||||
// states. Rendering the first as 0 is the fabricated zero — it reads as a
|
||||
// measurement and is not one.
|
||||
function headValue(head: Head): string {
|
||||
switch (head.kind) {
|
||||
case 'height': return head.height.toLocaleString();
|
||||
case 'unreachable': return 'No public RPC';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// Each VM names its head after its own data model. Calling the X-Chain's a
|
||||
// "block height" is the same category error as giving it a gas column.
|
||||
function headLabel(vm: PrimaryVm | undefined): string {
|
||||
switch (vm?.view) {
|
||||
case 'platform': return 'P-Chain Height';
|
||||
case 'utxo': return 'Accepted Height';
|
||||
default: return 'Block Height';
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapLabel(b: boolean | null): string {
|
||||
if (b === null) return 'Unknown';
|
||||
return b ? 'Bootstrapped' : 'Syncing';
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
readonly label: string;
|
||||
readonly value: string;
|
||||
readonly isLoading?: boolean;
|
||||
}
|
||||
|
||||
const StatCard = ({ label, value, isLoading = false }: StatCardProps) => (
|
||||
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
||||
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
||||
{ label }
|
||||
</span>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span className="text-xl font-bold text-[var(--color-text-primary)]">{ value }</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface InfoRowProps {
|
||||
readonly label: string;
|
||||
readonly value: string;
|
||||
@@ -180,10 +174,8 @@ const ChainDetailPage = () => {
|
||||
const router = useRouter();
|
||||
const slug = (router.query.slug as string || '').toLowerCase();
|
||||
|
||||
const { blockchains, isLoading: chainsLoading } = useBlockchains();
|
||||
const { blockchains } = useBlockchains();
|
||||
const { validators, isLoading: validatorsLoading } = useCurrentValidators();
|
||||
const { nets } = useNets();
|
||||
const { stats: indexerStats } = useChainIndexerStats(slug);
|
||||
|
||||
const resolvedChain = React.useMemo<{
|
||||
blockchain: PChainBlockchain | undefined;
|
||||
@@ -217,12 +209,22 @@ const ChainDetailPage = () => {
|
||||
const vmName = KNOWN_VM_IDS[vmId] ?? (vmId ? truncateId(vmId) : 'Unknown');
|
||||
const chainId = L1_EVM_CHAIN_IDS[slug];
|
||||
const explorerUrl = EXPLORER_URLS[slug];
|
||||
const isLoading = chainsLoading || validatorsLoading;
|
||||
|
||||
const net = React.useMemo(
|
||||
() => nets.find((n) => n.id === netId),
|
||||
[ nets, netId ],
|
||||
const { facts, isLoading: factsLoading } = useChainFacts(
|
||||
resolvedChain.meta,
|
||||
resolvedChain.blockchain?.id ?? null,
|
||||
);
|
||||
const isPlatform = resolvedChain.meta?.view === 'platform';
|
||||
// The P-Chain is the registry, so it never lists itself. Every other primary
|
||||
// chain has to appear in this network's own answer to be on this network:
|
||||
// T, R, I, O are in our metadata table and registered nowhere, and M runs on
|
||||
// devnet alone.
|
||||
const onThisNetwork = isPlatform ||
|
||||
!resolvedChain.isPrimary ||
|
||||
blockchains.some((c) => c.name.toLowerCase() === slug);
|
||||
const scopeLabel = facts.chainId != null ? 'EVM Chain ID' : 'Network';
|
||||
const networkScope = resolvedChain.isPrimary ? 'Primary' : 'Sovereign L1';
|
||||
const scopeValue = facts.chainId != null ? String(facts.chainId) : networkScope;
|
||||
|
||||
const netChains = React.useMemo(
|
||||
() => netId ? blockchains.filter((c) => c.netID === netId) : [],
|
||||
@@ -260,47 +262,33 @@ const ChainDetailPage = () => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{ !onThisNetwork && (
|
||||
<div className="p-4 mb-6 border border-[var(--color-border-divider)] rounded-lg">
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">
|
||||
{ chainName } is not registered on { config.chain.name || 'this network' }.
|
||||
Nothing below describes it.
|
||||
</span>
|
||||
</div>
|
||||
) }
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
|
||||
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
||||
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
||||
Validators
|
||||
</span>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
{ validators.length }
|
||||
</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
||||
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
||||
Total Stake
|
||||
</span>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
{ formatStake(totalStake) } { config.chain.currency.symbol || 'LUX' }
|
||||
</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
||||
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
||||
Chains on Network
|
||||
</span>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
{ netChains.length }
|
||||
</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<div className="p-4 border border-[var(--color-border-divider)] rounded-lg bg-[var(--color-gray-50)] dark:bg-[var(--color-whiteAlpha-50)]">
|
||||
<span className="block text-xs text-[var(--color-text-secondary)] font-semibold uppercase tracking-wider mb-1">
|
||||
Threshold
|
||||
</span>
|
||||
<Skeleton loading={ isLoading }>
|
||||
<span className="text-xl font-bold text-[var(--color-text-primary)]">
|
||||
{ net?.threshold ?? '-' }
|
||||
</span>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<StatCard label="Status" value={ bootstrapLabel(facts.bootstrapped) } isLoading={ factsLoading }/>
|
||||
<StatCard label={ headLabel(resolvedChain.meta) } value={ headValue(facts.head) } isLoading={ factsLoading }/>
|
||||
{ isPlatform ? (
|
||||
<>
|
||||
<StatCard label="Validators" value={ String(validators.length) } isLoading={ validatorsLoading }/>
|
||||
<StatCard
|
||||
label="Total Stake"
|
||||
value={ `${ formatStake(totalStake) } ${ config.chain.currency.symbol || 'LUX' }` }
|
||||
isLoading={ validatorsLoading }
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StatCard label="VM" value={ vmName }/>
|
||||
<StatCard label={ scopeLabel } value={ scopeValue }/>
|
||||
</>
|
||||
) }
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
@@ -320,39 +308,6 @@ const ChainDetailPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ indexerStats && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
Indexer Stats
|
||||
</span>
|
||||
{ indexerStats.dag_stats && (
|
||||
<Tag size="sm" variant="subtle">
|
||||
{ indexerStats.dag_stats.chain_type.toUpperCase() }
|
||||
</Tag>
|
||||
) }
|
||||
</div>
|
||||
<div className="border border-[var(--color-border-divider)] rounded-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) }
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ netChains.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -408,54 +363,74 @@ const ChainDetailPage = () => {
|
||||
</div>
|
||||
) }
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
Validators
|
||||
</span>
|
||||
<Skeleton loading={ validatorsLoading }>
|
||||
<Tag size="sm" variant="subtle">{ validators.length }</Tag>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
||||
<div className="hidden lg:flex px-4 py-2 gap-4 border-b border-[var(--color-border-divider)] items-center">
|
||||
<div className="w-10 shrink-0 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
#
|
||||
</div>
|
||||
<div className="flex-[2] text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Node ID
|
||||
</div>
|
||||
<div className="flex-1 text-right text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Stake
|
||||
</div>
|
||||
<div className="w-20 text-right shrink-0 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Uptime
|
||||
</div>
|
||||
<div className="w-10 shrink-0 text-center text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
{ '\u2022' }
|
||||
</div>
|
||||
{ /* Validators are the P-Chain's data model, not every chain's. Listing
|
||||
the same primary-network validator set under Q-Chain and Z-Chain and
|
||||
K-Chain is what made all fifteen pages read alike. */ }
|
||||
{ isPlatform && (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-sm font-semibold text-[var(--color-text-primary)]">
|
||||
Validators
|
||||
</span>
|
||||
<Skeleton loading={ validatorsLoading }>
|
||||
<Tag size="sm" variant="subtle">{ validators.length }</Tag>
|
||||
</Skeleton>
|
||||
</div>
|
||||
|
||||
{ validatorsLoading && (
|
||||
<div className="px-4 py-6">
|
||||
<Skeleton loading h="16px" mb={ 3 }/>
|
||||
<Skeleton loading h="16px" mb={ 3 }/>
|
||||
<Skeleton loading h="16px" mb={ 3 }/>
|
||||
<Skeleton loading h="16px"/>
|
||||
<div className="border border-[var(--color-border-divider)] rounded-lg overflow-hidden">
|
||||
<div className="hidden lg:flex px-4 py-2 gap-4 border-b border-[var(--color-border-divider)] items-center">
|
||||
<div className="w-10 shrink-0 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
#
|
||||
</div>
|
||||
<div className="flex-[2] text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Node ID
|
||||
</div>
|
||||
<div className="flex-1 text-right text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Stake
|
||||
</div>
|
||||
<div className="w-20 text-right shrink-0 text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
Uptime
|
||||
</div>
|
||||
<div className="w-10 shrink-0 text-center text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider">
|
||||
{ '\u2022' }
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ !validatorsLoading && validators.length === 0 && (
|
||||
<div className="px-4 py-8 text-center text-[var(--color-text-secondary)] text-sm">
|
||||
No validators found
|
||||
</div>
|
||||
) }
|
||||
{ validatorsLoading && (
|
||||
<div className="px-4 py-6">
|
||||
<Skeleton loading h="16px" mb={ 3 }/>
|
||||
<Skeleton loading h="16px" mb={ 3 }/>
|
||||
<Skeleton loading h="16px" mb={ 3 }/>
|
||||
<Skeleton loading h="16px"/>
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ !validatorsLoading && validators.map((v, i) => (
|
||||
<ValidatorRow key={ v.nodeID } validator={ v } index={ i }/>
|
||||
)) }
|
||||
{ !validatorsLoading && validators.length === 0 && (
|
||||
<div className="px-4 py-8 text-center text-[var(--color-text-secondary)] text-sm">
|
||||
No validators found
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ !validatorsLoading && validators.map((v, i) => (
|
||||
<ValidatorRow key={ v.nodeID } validator={ v } index={ i }/>
|
||||
)) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
|
||||
{ /* Say it plainly. The chain is registered on the P-Chain and running on
|
||||
the node, but the gateway routes only P, X and C, so no head can be
|
||||
read from a browser. An empty panel here would read as "nothing
|
||||
happening on this chain", which is a different and false claim. */ }
|
||||
{ facts.head.kind === 'unreachable' && (
|
||||
<div className="mb-6 p-4 border border-[var(--color-border-divider)] rounded-lg">
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">
|
||||
{ chainName } is registered on the primary network and
|
||||
{ facts.bootstrapped === true ? ' bootstrapped on the node' : ' not yet bootstrapped' }
|
||||
, but its RPC is not published on { config.chain.name || 'this network' }'s public gateway,
|
||||
so its height and contents cannot be read here.
|
||||
</span>
|
||||
</div>
|
||||
) }
|
||||
</PrimaryNetworkGuard>
|
||||
);
|
||||
};
|
||||
|
||||
+42
-10
@@ -17,9 +17,13 @@ import ChainRow from './ChainRow';
|
||||
|
||||
const PRIMARY_NETWORK_ID = '11111111111111111111111111111111LpoYY' as const;
|
||||
|
||||
// Primary-network VM chains come from the single source of truth in
|
||||
// configs/app/primaryChains.ts (mirrored from the node registry), so this page
|
||||
// and the chain-detail page can never drift apart.
|
||||
// Which primary chains EXIST is the network's answer, read live from
|
||||
// platform.getBlockchains; configs/app/primaryChains.ts only says how to
|
||||
// present one. Listing the hardcoded set gave every network the same fifteen
|
||||
// rows, four of which (T, R, I, O) are registered on no network at all — the
|
||||
// node answers "there is no ID with alias: T" — and one of which (M) exists
|
||||
// only on devnet. The P-Chain is added by hand because it is the registry, so
|
||||
// it never appears in its own list.
|
||||
|
||||
const L1_EVM_CHAIN_IDS: Readonly<Record<string, number>> = {
|
||||
Zoo: 200200,
|
||||
@@ -106,6 +110,21 @@ const ChainsPage = () => {
|
||||
return blockchains.filter((chain) => chain.netID !== PRIMARY_NETWORK_ID);
|
||||
}, [ blockchains ]);
|
||||
|
||||
// The primary chains this network actually runs, in the registry's order,
|
||||
// each paired with its presentation metadata. A chain the network reports
|
||||
// but we have no metadata for still gets a row — the network is the source
|
||||
// of truth, not our table.
|
||||
const primaryChains = React.useMemo(() => {
|
||||
const rows = blockchains
|
||||
.filter((chain) => chain.netID === PRIMARY_NETWORK_ID)
|
||||
.map((chain) => {
|
||||
const slug = chain.name.toLowerCase();
|
||||
return { chain, meta: PRIMARY_VMS.find((vm) => vm.slug === slug) };
|
||||
});
|
||||
const pChain = PRIMARY_VMS.find((vm) => vm.slug === 'p-chain');
|
||||
return pChain ? [ { chain: undefined, meta: pChain }, ...rows ] : rows;
|
||||
}, [ blockchains ]);
|
||||
|
||||
const handlePrimaryClick = React.useCallback(() => {
|
||||
setActiveTab(TAB_IDS.primary);
|
||||
}, []);
|
||||
@@ -143,15 +162,28 @@ const ChainsPage = () => {
|
||||
{ activeTab === TAB_IDS.primary && (
|
||||
<div className="border border-[var(--color-border-divider)] rounded-md overflow-hidden">
|
||||
<TableHeader showNetId={ false }/>
|
||||
{ PRIMARY_VMS.map((chain) => (
|
||||
{ isLoading && (
|
||||
<div className="px-4 py-6">
|
||||
<Skeleton loading={ true } h="20px" mb={ 3 }/>
|
||||
<Skeleton loading={ true } h="20px" mb={ 3 }/>
|
||||
<Skeleton loading={ true } h="20px"/>
|
||||
</div>
|
||||
) }
|
||||
{ !isLoading && primaryChains.length === 0 && (
|
||||
<div className="px-4 py-8 text-center text-[var(--color-text-secondary)] text-sm">
|
||||
The P-Chain did not answer, so this network's chain list is unknown
|
||||
</div>
|
||||
) }
|
||||
{ !isLoading && primaryChains.map(({ chain, meta }) => (
|
||||
<ChainRow
|
||||
key={ chain.name }
|
||||
name={ chain.name }
|
||||
fullName={ chain.fullName }
|
||||
vmLabel={ chain.vm }
|
||||
chainId={ chain.chainId }
|
||||
key={ meta?.slug ?? chain?.id }
|
||||
name={ meta?.name ?? chain?.name ?? '' }
|
||||
fullName={ meta?.fullName }
|
||||
blockchainId={ chain?.id }
|
||||
vmLabel={ meta?.vm ?? 'Unknown' }
|
||||
chainId={ meta?.chainId ?? null }
|
||||
isActive
|
||||
href={ `/chains/${ chain.slug }` }
|
||||
href={ `/chains/${ meta?.slug ?? (chain?.name ?? '').toLowerCase() }` }
|
||||
/>
|
||||
)) }
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user