fix(explore/validators): render real P-chain data in light mode + brand currency symbol

The /validators view fetched real validators (5, 2.5e18 weight) but rendered
white-on-white in light mode — data present in DOM, invisible to users. Set
text-[var(--color-text-primary)] on each of the three lux validator component
roots so all text inherits the theme-flipping color token. Also replace the
hardcoded "LUX" stake unit with config.chain.currency.symbol (AI on Hanzo),
matching NetworkStats. No decimals change (6dp is the codebase convention,
consistent with P-chain getMinStake).
This commit is contained in:
hanzo-dev
2026-06-24 05:16:12 -07:00
parent 340ce0dde8
commit 3eafdad6c9
4 changed files with 100 additions and 16 deletions
+75
View File
@@ -166,6 +166,81 @@ All symlinks reference this single source of truth.
## Recent Changes
### Investor-grade data: real validators + honest-empty stats/DEX (v1.0.11)
Removed every fabricated placeholder; data is now real-from-chain or honestly
empty, never faked. Root causes + fixes:
- **Validators/Stake = 0 was a proxy bug, not missing data.** P-chain
`platform.getCurrentValidators` returns real validators (5, total weight
2.5e18 on hanzo). `pages/api/pchain.ts` derived its base URL by stripping
ONLY `/ext/bc/C/rpc` via regex; brand RPCs are `…/ext/bc/<chain>/rpc`
(hanzo: `/ext/bc/hanzo/rpc`), so the regex no-op'd and the proxy POSTed to
`…/ext/bc/hanzo/rpc/ext/bc/P` → HTML 404 → "Unexpected non-whitespace
character after JSON at position 4" → 502. Fix: `new URL(rpcUrl).origin`
(works for every brand) + defensive text→JSON parse with a clear error.
`ui/stats/lux/NetworkStats.tsx` (Network Overview: Total Chains / Validators
/ Connected / Total Stake / Avg Uptime) already had honest `—` fallbacks and
now renders REAL values. `lib/api/luxnet/instance.ts` was already safe (uses
`new URL().hostname`), so B/D-Chain SDK calls were unaffected.
- **"Placeholder Counter 9.074M ×8"** came from `stubs/stats.ts` `STATS_COUNTER`
(`value:'9074405', title:'Placeholder Counter'`) used as React-Query
`placeholderData` in `ui/stats/NumberWidgetsList.tsx`. When the Blockscout
**stats microservice** is absent (hanzo has NO `NEXT_PUBLIC_STATS_API_HOST`;
every `api-explore.hanzo.network/api/v1|v2/*` 404s) the stub leaked as "real".
Fix: neutralize the stub (`title:'', value:'0'`) AND gate the counters/charts
in `ui/pages/Stats.tsx` on `config.features.stats.isEnabled` — show the live
chain-sourced Network Overview always, with an honest "being indexed" note
when no stats service. Aggregate counters (total txns/addresses, gas-used-
today, history charts) genuinely need a stats indexer that isn't deployed.
- **DEX showed static demo numbers.** `lib/api/dchain/useDexData.ts` had
hardcoded `DEMO_SYMBOLS/ORDERS/TRADES/POOLS/OVERVIEW` (LUX/USDT 24.85, etc.)
served when the D-Chain (DexVM) is unavailable. Deleted ALL demo data → real
D-Chain data or honest-empty; `ui/dex/DexPage.tsx` renders an honest empty/
error message per tab.
- **Bridge signers = 0 is already honest** (`lib/api/bchain/useBridgeData.ts`
returns real B-Chain signer set or `EMPTY_STATS`; B-Chain not yet reporting).
- **Homepage is already investor-grade**: real RPC blocks (#8#13), tx hashes,
gas (2.5 Gwei), Total blocks/txns/wallets — `ui/home/Stats.tsx` degrades to
RPC when the general indexer errors; `HOMEPAGE_STATS` stub is placeholder-only.
- **Blockchain dropdown clipped its last item** ("Verified Contracts"):
`@luxfi/ui` `Tooltip` `variant="popover"` Content defaults to
`overflow-hidden px-3 py-2`, clipping a tall menu `<ul>`. Fixed at the
consumer (`ui/snippets/navigation/horizontal/NavLinkGroup.tsx`) via
`contentProps={{ className:'overflow-visible px-1.5 py-2' }}` (twMerge wins)
+ `placement:'bottom-start'`. No node_modules patch.
- Still needs a real indexer (honest-empty until then): aggregate Stats
counters + history charts (Blockscout stats microservice), DEX live
markets/orders/trades (DexVM indexer), Bridge signer set (B-Chain).
- Built v1.0.11 via the kaniko Job pattern (NO GHA) → `ghcr.io/{hanzoai,zooai,
luxfi}/explore:v1.0.11`; deploy = image patch of `explore-fe-*` Deployments
on do-sfo3-lux-k8s ns `lux-mainnet` (plain Deployments, not operator-managed).
### Validators page: visible in light mode + brand currency (v1.0.12)
v1.0.11 fetched REAL P-chain validators (5, total weight 2.5e18 on hanzo) but
a Playwright pass found the `/validators` view rendered **white-on-white in
light mode** — the data was present in the DOM yet invisible to users (only the
tab bar showed because it alone used `text-[var(--color-text-primary)]`). The
three custom Lux components (`ui/validators/lux/{ValidatorsDashboard,
ValidatorsList,DelegatorsList}.tsx`) set NO text color, so they inherited a
white color that doesn't flip with the theme. Fixes:
- Set `text-[var(--color-text-primary)]` on each component's root container —
one place per component; the token flips per `styles/tokens.css` (light
`rgba(16,17,18,.80)` / dark `rgba(255,255,255,.80)`), so all descendants now
render the mode-correct color (dark on light, light on dark).
- Stake unit was hardcoded `LUX` on every brand; now `config.chain.currency.
symbol || 'LUX'` (= `AI` on Hanzo) — matches `NetworkStats.tsx`'s pattern,
DRY across all 6 stake labels in the three components.
- NOT changed: `formatStake` uses `LUX_DECIMALS = 6`, the codebase-wide
convention (NetworkStats + ChainDetailPage use the same); P-chain
`getMinStake` returns `minValidatorStake:2000000000` (= 2000 at 6dp),
consistent with 6. (Three copies of this helper is a latent DRY violation to
consolidate later, but the value is correct — left as-is to avoid drift.)
- `next.config.js` has `typescript.ignoreBuildErrors:true`; repo carries ~224
pre-existing tsc errors (all in `*.pw.tsx` / unrelated) — build is unaffected.
Dockerfile uses **pnpm** (not yarn). Edited files lint+typecheck clean.
- Built v1.0.12 via the kaniko Job (NO GHA) → `ghcr.io/{hanzoai,zooai,luxfi}/
explore:v1.0.12`; deployed by image patch of `explore-fe-{hanzo,lux,zoo}` on
do-sfo3-lux-k8s ns `lux-mainnet`. (Built on do-sfo3-hanzo-k8s ns `hanzo`.)
### Multi-brand hostname-driven header logo (explore v1.0.10)
- ONE image white-labels by request Host: `configs/app/chainRegistry.ts`
`getCurrentChain()` resolves the chain (and its brand) from the hostname,
+7 -4
View File
@@ -1,11 +1,14 @@
import { Skeleton } from '@luxfi/ui/skeleton';
import React from 'react';
import config from 'configs/app';
import type { PChainDelegator, PChainValidator } from 'lib/api/pchain';
import dayjs from 'lib/date/dayjs';
import { Skeleton } from '@luxfi/ui/skeleton';
import { formatStake, truncateNodeId } from './utils';
const CURRENCY = config.chain.currency.symbol || 'LUX';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
@@ -69,7 +72,7 @@ const DelegatorsList = ({ validators, isLoading }: DelegatorsListProps) => {
return (
<div
className="overflow-hidden"
className="overflow-hidden text-[var(--color-text-primary)]"
>
{ /* Header */ }
<div className="flex">
@@ -125,7 +128,7 @@ const DelegatorsList = ({ validators, isLoading }: DelegatorsListProps) => {
{ truncateNodeId(d.validatorNodeID) }
</div>
<div className="text-left lg:text-right">
{ formatStake(d.stakeAmount) } LUX
{ formatStake(d.stakeAmount) } { CURRENCY }
</div>
<div>
{ formatTimestamp(d.startTime) }
@@ -134,7 +137,7 @@ const DelegatorsList = ({ validators, isLoading }: DelegatorsListProps) => {
{ formatTimestamp(d.endTime) }
</div>
<div className="text-left lg:text-right">
{ formatStake(d.potentialReward) } LUX
{ formatStake(d.potentialReward) } { CURRENCY }
</div>
</div>
)) }
+10 -7
View File
@@ -1,10 +1,13 @@
import { Skeleton } from '@luxfi/ui/skeleton';
import React from 'react';
import config from 'configs/app';
import type { PChainValidator, ValidatorStats } from 'lib/api/pchain';
import { Skeleton } from '@luxfi/ui/skeleton';
import { formatStake, formatUptime, truncateNodeId } from './utils';
const CURRENCY = config.chain.currency.symbol || 'LUX';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
@@ -61,13 +64,13 @@ const StakeBreakdown = ({ stats, isLoading }: StakeBreakdownProps) => {
</div>
<Skeleton loading={ isLoading }>
<div className="font-bold">
{ formatStake(stats.totalStake) } LUX
{ formatStake(stats.totalStake) } { CURRENCY }
</div>
</Skeleton>
<Skeleton loading={ isLoading }>
<div className="flex gap-6 flex-wrap">
<div>Validators: { formatStake(validatorStake) } LUX ({ validatorPct }%)</div>
<div>Delegated: { formatStake(stats.totalDelegatedStake) } LUX ({ delegationPct }%)</div>
<div>Validators: { formatStake(validatorStake) } { CURRENCY } ({ validatorPct }%)</div>
<div>Delegated: { formatStake(stats.totalDelegatedStake) } { CURRENCY } ({ delegationPct }%)</div>
</div>
</Skeleton>
</div>
@@ -140,7 +143,7 @@ const ActiveValidatorsTable = ({ validators, isLoading }: ActiveValidatorsTableP
{ truncateNodeId(v.nodeID) }
</div>
<div className="flex-[2] text-right">
{ formatStake(v.stakeAmount ?? v.weight) } LUX
{ formatStake(v.stakeAmount ?? v.weight) } { CURRENCY }
</div>
<div className="flex-1 text-right">
{ v.delegationFee }%
@@ -169,11 +172,11 @@ interface ValidatorsDashboardProps {
const ValidatorsDashboard = ({ validators, stats, isLoading }: ValidatorsDashboardProps) => {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-6 text-[var(--color-text-primary)]">
{ /* Stat cards */ }
<div
className="grid gap-4"
style={{ gridTemplateColumns: "repeat(4, 1fr)" }}
style={{ gridTemplateColumns: 'repeat(4, 1fr)' }}
>
<StatCard
label="Validators"
+8 -5
View File
@@ -1,11 +1,14 @@
import React from 'react';
import type { PChainValidator } from 'lib/api/pchain';
import { Input } from '@luxfi/ui/input';
import { Skeleton } from '@luxfi/ui/skeleton';
import React from 'react';
import config from 'configs/app';
import type { PChainValidator } from 'lib/api/pchain';
import { formatStake, formatUptime, truncateNodeId } from './utils';
const CURRENCY = config.chain.currency.symbol || 'LUX';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
@@ -48,7 +51,7 @@ const ValidatorsList = ({ validators, isLoading }: ValidatorsListProps) => {
}, [ validators, search ]);
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 text-[var(--color-text-primary)]">
{ /* Search */ }
<div>
<Input
@@ -112,7 +115,7 @@ const ValidatorsList = ({ validators, isLoading }: ValidatorsListProps) => {
{ truncateNodeId(v.nodeID) }
</div>
<div className="flex-[2] text-right">
{ formatStake(v.stakeAmount ?? v.weight) } LUX
{ formatStake(v.stakeAmount ?? v.weight) } { CURRENCY }
</div>
<div className="flex-1 text-right">
{ v.delegationFee }%