mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
The magnifying glass was drawn over the leading "S" of the placeholder. The cause is not the search bar: @luxfi/ui's InputGroup measures its adornments correctly and then throws the measurement away, because the clone is guarded on `displayName === 'FieldInput'` — a component that does not exist in the package — and the props it would have set (`ps`/`pe`) are Chakra style props that the Tailwind/gui Input spreads onto the DOM and ignores. Both halves are leftovers of the Chakra→Tailwind port, so EVERY InputGroup in the app is unpadded; the header is just the one you look at every day. Patched at the primitive (patches/@[email protected]) rather than hand-tuning padding at one call site: the offset is the measured width of the actual adornment, so it stays right when the adornment changes, and FilterInput / revoke's SearchInput / the showcase are fixed by the same change. Measured at 1440x900: the icon overlapped the text by 24px and the kbd chip by 52px; both are now 0. The measurement also moves to a layout effect and stops blanking value and placeholder on first render. That blanking was inert while the guard swallowed the clone; with the clone live it flips the field from controlled to uncontrolled on mount, which React warns about and which can eat a keystroke. ClearButton spread `className` over its own visibility classes, so a caller passing one clobbered `invisible` — the header showed a clear button on an empty field. Merged instead, at the component. Placeholder is now "Search or ask anything", and Cmd/Ctrl-K opens an assistant. Nothing in this repo, @luxfi/ui or @hanzo/gui ships a command palette or chat (no cmdk, no palette, one dialog primitive), so this is the smallest thing that works: the existing Dialog, one global listener, and a server-side proxy that holds the credential. The key is deliberately not NEXT_PUBLIC_, so a deployment without one gets a dialog that says exactly that instead of failing silently. Off unless NEXT_PUBLIC_AI_ASSISTANT_ENABLED is set — Ctrl+K is the browser's address-bar shortcut and claiming it to show an error would be worse than not claiming it. Validators read 5 on a network running 20. useCurrentValidators asks ONE P-Chain, the one this deployment points at, but Lux, Hanzo, Zoo and Pars are each sovereign L1s with their own P-Chain and their own validator set — measured 2026-08-06, five each, with four disjoint NodeID sets. The registry gains the node origin per chain; absence of it is the signal that a chain has no publicly reachable node (SPC and Osage are in-cluster only), and those are reported `unavailable` rather than counted as zero. Stake is deliberately NOT summed: each chain bonds its own currency. The proxy resolves `?chain=` through the registry and never from the request, so it cannot be pointed at an arbitrary origin. The new envs are registered with the envs-validator and documented in ENVS.md — the schema rejects unknown NEXT_PUBLIC_* variables and runs at container start, so an undeclared one stops the pod from booting. Co-authored-by: Hanzo Dev <[email protected]>
114 lines
3.9 KiB
TypeScript
114 lines
3.9 KiB
TypeScript
// Validator counts across every network in the registry, not just the one this
|
|
// explorer is pointed at.
|
|
//
|
|
// Lux, Hanzo, Zoo and Pars are each a sovereign L1 with its OWN P-Chain and its
|
|
// own validator set, so "how many validators" is a sum over chains and there is
|
|
// one read per chain. Chains with no publicly reachable node (SPC, Osage — see
|
|
// ChainEntry.nodeApiUrl) cannot be read at all, and are reported as unknown
|
|
// rather than folded in as zero.
|
|
//
|
|
// Stake is deliberately NOT summed: each chain bonds its own currency, so
|
|
// adding nLUX to nZOO would produce a number that means nothing.
|
|
|
|
import { useQueries } from '@tanstack/react-query';
|
|
import React from 'react';
|
|
|
|
import type { PChainValidator } from './types';
|
|
|
|
import { CHAINS, getCurrentChain } from 'configs/app/chainRegistry';
|
|
|
|
const STALE_TIME_MS = 60_000;
|
|
const PRIMARY_NETWORK_ID = '11111111111111111111111111111111LpoYY';
|
|
|
|
export interface NetworkValidators {
|
|
readonly name: string;
|
|
readonly chainId: number;
|
|
|
|
/** Undefined when the chain did not answer — never 0 standing in for it. */
|
|
readonly validatorCount: number | undefined;
|
|
readonly isLoading: boolean;
|
|
|
|
/**
|
|
* Why the count is undefined. 'unavailable' = no publicly reachable node, so
|
|
* it was never asked; 'unknown' = it was asked and did not answer. Callers
|
|
* render the difference, because one is a deployment fact and the other is an
|
|
* outage.
|
|
*/
|
|
readonly status: 'live' | 'unknown' | 'unavailable';
|
|
}
|
|
|
|
async function fetchCount(chainName: string): Promise<number> {
|
|
const res = await fetch(`/v1/pchain?chain=${ encodeURIComponent(chainName) }`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
method: 'platform.getCurrentValidators',
|
|
params: { netID: PRIMARY_NETWORK_ID },
|
|
id: 1,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`P-chain proxy returned ${ res.status } for ${ chainName }`);
|
|
}
|
|
|
|
const json = await res.json() as { result?: { validators?: ReadonlyArray<PChainValidator> } };
|
|
const validators = json.result?.validators;
|
|
if (!validators) {
|
|
throw new Error(`${ chainName } did not return a validator set`);
|
|
}
|
|
return validators.length;
|
|
}
|
|
|
|
export function useNetworkValidators() {
|
|
// Same network as this deployment: a mainnet explorer must not count testnet
|
|
// validators.
|
|
const { all, readable } = React.useMemo(() => {
|
|
const { network } = getCurrentChain();
|
|
const chains = CHAINS.filter((c) => c.network === network);
|
|
return { all: chains, readable: chains.filter((c) => c.nodeApiUrl) };
|
|
}, []);
|
|
|
|
const results = useQueries({
|
|
queries: readable.map((chain) => ({
|
|
queryKey: [ 'pchain:validatorCount', chain.name ],
|
|
queryFn: () => fetchCount(chain.name),
|
|
staleTime: STALE_TIME_MS,
|
|
retry: 1,
|
|
})),
|
|
});
|
|
|
|
// Every chain is listed, including the ones that cannot be read: a chain
|
|
// missing from the list would read as a chain with no validators.
|
|
const networks: ReadonlyArray<NetworkValidators> = all.map((chain) => {
|
|
const i = readable.indexOf(chain);
|
|
if (i < 0) {
|
|
return { name: chain.name, chainId: chain.chainId, validatorCount: undefined, isLoading: false, status: 'unavailable' };
|
|
}
|
|
const result = results[i];
|
|
return {
|
|
name: chain.name,
|
|
chainId: chain.chainId,
|
|
validatorCount: result.data,
|
|
isLoading: result.isLoading,
|
|
status: result.data !== undefined ? 'live' : 'unknown',
|
|
};
|
|
});
|
|
|
|
const answered = networks.filter((n) => n.validatorCount !== undefined);
|
|
|
|
return {
|
|
networks,
|
|
total: answered.reduce((sum, n) => sum + (n.validatorCount ?? 0), 0),
|
|
|
|
/** How many of the queried chains answered — the total means little without it. */
|
|
answeredCount: answered.length,
|
|
queriedCount: readable.length,
|
|
|
|
/** A partial total is still a real number, but zero answers is not a zero. */
|
|
isKnown: answered.length > 0,
|
|
isLoading: results.some((r) => r.isLoading),
|
|
};
|
|
}
|