100 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 0ec2f91a9b The fallback L1 list names only chains with an explorer
With no P-Chain to read, the overview draws its L1 cards from KNOWN_L1_CHAINS,
which still linked SPC and Pars to explorer names that do not resolve.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 16:36:40 -07:00
zeekayandClaude Opus 5.5 4432040ad3 Zoo explorer footer links to the zooai org and Zoo's own accounts
github.com/zoolabs and x.com/zoolabs belong to someone else. The links now
match @zooai/brand: github.com/zooai, x.com/zoo_labs and the Zoo Discord.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-23 16:10:59 -07:00
zeekayandHanzo Dev a23a81dee7 Brand explorers show only their own chain on validators and DEX
On a Zoo, Hanzo, Pars, SPC or Osage host the validators page listed every L1
in the registry, and /dex rendered the Lux D-Chain markets. The network total
now sums every L1 only on the Lux primary-network explorer, and /dex answers
404 anywhere else. isPrimaryNetworkHost() is the same predicate as
isPrimaryNetworkExplorer(), taking the host from the request.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 16:09:21 -07:00
zeekayandClaude Opus 5.5 e57351a54c The explorer lists only chains that have an explorer
SPC, Pars and Osage have no explorer and no public node, the devnet has no
explorer, and the Hanzo testnet explorer has no DNS name. Their registry entries
put links to names that do not resolve on every page (the "SPC L1" and "Pars L1"
cards, the chain switcher, the network selector), and the Pars entry sent a
node request to api.pars.network on every page load that failed with 502.

The P-Chain still lists SPC and Pars; their cards render without a link.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-23 16:08:09 -07:00
zeekayandHanzo Dev 858d9ca25b Typecheck on the native compiler, keep 6 for the tools
typescript 7.0.2 is the native compiler and exports no JavaScript API, so
typescript-eslint and @typescript-eslint/eslint-plugin cannot read it —
typescript-eslint refuses TS 7 outright. It comes in aliased as tsgo and does
the typechecking; typescript stays on the 6 line for the tools. lint:tsc
reports the same 239 diagnostics under both compilers.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 22:44:21 -07:00
zeekayandHanzo Dev a0460cb289 Workflows ask for the runner label that exists
One label per runner, spelled <os>-<arch>. The aliases these jobs named
were removed, and a job naming a label no runner carries waits instead of
failing.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 01:54:51 -07:00
zeekayandHanzo Dev a7c0ec6028 drop the stale GitHub-queue trap
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 13:50:40 -07:00
zeekay e5c0dfc69d every Hanzo dependency is the version npm serves
Ranges drift once nothing moves them: a caret that has not been rewritten
since it was written pins whatever was current that day, and an exact pin
cannot even take a patch. Both are updated here.

One line per package, so a surface reads the same components, the same
tokens and the same face as every other one.
2026-09-01 16:01:43 -07:00
zeekay de647bf4ba the face is Zen, and it ships from @hanzo/font
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.

Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.

Comments naming a face we no longer use are deleted, not reworded.
2026-09-01 08:25:40 -07:00
zeekay 8ff73626ae chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases
already ship it. The deployed gateways still answer the old prefix, so remote
reads break until they are redeployed; the estate moves together rather than
sitting half converted.

Lines that record an observation - a probe, a status code, a past release -
keep the prefix they were written against, as do URLs on hosts that are not
ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.
2026-09-01 08:21:18 -07:00
zeekay 206b596f92 Delete the workflows the forge never reads
git.hanzo.ai resolves .hanzo/workflows and stops there, so these ran on
no push and reported green on every one. CI is native and lives in
.hanzo/workflows.
2026-09-01 00:46:58 -07:00
zeekay 19ecee9d26 Restore the chain prefix the running nodes answer
The rename to /v1/chain is real in luxd source, but the deployed fleet has not
taken it yet: probed just now, api.lux.network, api.lux-test.network,
api.zoo.network and api.hanzo.network each answer 200 on /v1/bc/C/rpc and 404 on
/v1/chain/C/rpc. Composing /v1/chain against those hosts reaches nothing, which
is the failure the rename set out to prevent.

The move belongs with the node redeploy, not ahead of it.
2026-08-30 21:24:56 -07:00
zeekay 32bd1a5df2 chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain; the /v1/bc prefix it replaced is
gone, so anything composing it reaches nothing. Lines that record an
observation - a probe, a status code, a past release - keep the prefix they
were written against, as do URLs on hosts that are not ours.
2026-08-30 21:05:44 -07:00
zeekay cfd7957899 docs: chain routes are /v1/chain
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181):

  /v1/bc/<alias>/...   -> /v1/chain/<alias>/...
  /ext/bc/<alias>/...  -> /v1/chain/<alias>/...

Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
2026-08-30 16:41:54 -07:00
zeekay 31d79e2536 LLM.md: what the token packages actually provide
The appearance layer, measured rather than assumed: which package owns
colour, which owns the ramps, why design's token sheets cannot go in
while Tailwind owns the same variable names, and that @hanzo/tokens'
published CSS export points at a file its tarball does not contain.
2026-08-28 23:45:54 -07:00
zeekay 793816c7a7 Let the reader set type, density and face
The explorer had no way for a person to change how it reads. Colour mode was
the only control, and it is one bit.

@hanzo/appearance stores the preference and puts it on <html>; what the numbers
mean is bound in styles/tokens.css. Type rides the root font size, so every
Tailwind rung — all of them in rem — moves from one declaration and nothing is
restated. Density rides --spacing, the single base every p-*, m-* and gap-*
computes from. Both default to 1, so an install that never opens the panel
renders exactly what it rendered before; measured in a browser against the
production build: 16px root, p-4 at 16px, unchanged.

@hanzo/design's token sheets stay out. They declare --text-*, --radius-* and
--breakpoint-* — the names Tailwind v4 publishes — outside any layer, and v4
compiles utilities to var() references rather than baked values, so importing
them silently reanchors the app: --text-base 16px to 14px, --radius-lg 8px to
12px. Two sets under one set of names is a fork, not a migration. They become
the ramps when Tailwind leaves and there is one ramp to own.

The boot script is hashed for CSP from the same call that renders it, the way
the adbutler scripts are. A pasted literal goes stale on the next release of
the package and fails silently — policy parses, browser drops the script, page
paints at the defaults.
2026-08-28 23:45:54 -07:00
zeekay 5bbb57995a top accounts: read the tags the list actually carries
The addresses list endpoint returns a projection of an address — no tags, no
name, no implementations. AddressesListItem guarded with `public_tags !== null`,
which is true of an absent field, so every visit to /accounts read `.length` off
undefined and the whole page fell to the error boundary. The type said the field
was always present, so nothing caught it.

Partial<AddressParam> states what the endpoint sends, and both read sites reach
for the tags the same optional way.

The error screen it fell to had its own defect: the status glyph is an
inline-block div and gui's H1 renders inline, so the two shared a line box and
"500" ran into "Oops! Something went wrong". The screen is a stack; it now says
so. The heading's own top margin was being outranked by gui's atomic reset.
2026-08-28 23:45:54 -07:00
zeekay cea7badf29 fix(footer): follow the brand for the frontend source link
The version link was a module constant naming github.com/luxfi/explore, so
every brand's footer sent readers to Lux's repo — explore.hanzo.ai showed
"Frontend: v1.1.30" pointing at the Lux org. Brand identity in this image is
resolved per host, and the source link now resolves with it: ChainBranding
gains sourceUrl (NEXT_PUBLIC_BRAND_SOURCE_URL for external operators, like
every other branding field), so Lux keeps its own repo and Hanzo, Zoo, Pars,
SPC and Osage each name theirs.

Also sets Hanzo's copyright to Hanzo AI, Inc., the entity its terms, privacy,
DPA and brand.json all name; Hanzo Industries Inc. predates that consolidation.

chainRegistry.spec.ts asserts a brand host surfaces no other org's name or URL
in what the footer renders.
2026-08-21 00:24:07 -07:00
zeekay 70e98347e9 carry the github copy onto the forge
github.com held commits this host lacked, which refuses the mirror push
("fetch first") and stops github receiving anything at all. Merging rather
than overwriting, so the mirror is a fast-forward again and neither side's
work is dropped.
2026-08-20 21:54:05 -07:00
zeekay e9756e8814 run on the native fleet
Adds .hanzo/workflows/cicd.yml calling the canonical pipeline and drops the
GitHub-hosted workflows it replaces. Lands here because this is where the repo
is written — git.hanzo.ai is upstream and github.com is its push mirror, so the
same change made on github.com never reaches the runner fleet.
2026-08-20 20:20:18 -07:00
zeekay 4f6686459f track the name agents read, so a clone documents itself
LLM.md is the source and stays the source; CLAUDE.md is a symlink to it, so there is
one file and it cannot drift from itself. What changes is that the link is committed
rather than left for each checkout to recreate.

Left untracked, the doc was conditional on a step outside the repo: a fresh clone had
no CLAUDE.md, nothing loaded LLM.md, and an agent worked here having never read what
this repo already explains. Most of the estate already tracks theirs; this is the rest
agreeing with them.
2026-08-20 16:36:44 -07:00
zeekay cc45edc1be Merge remote-tracking branch 'origin/main' 2026-08-14 19:18:52 -07:00
zeekay cb0347fcbb give researchers an address to send a bug to
explore.lux.network had no security.txt, so a scanner looking for one found the
app answering and a researcher who found something had to guess an address.
There is a file now.

The policy it names is lux.network/security rather than a page added here. This
is a Blockscout fork whose route type and three exhaustive metadata maps are all
generated, so a page would cost six files of divergence to say what the network
page already says, and the field exists to point at a policy wherever it lives.
2026-08-14 19:18:37 -07:00
zeekay 2ff64fe352 Send explorer pageviews to Hanzo Insights
explore.lux.network measured nothing. It carried a Google Analytics component
that no deployment had ever given a property id, so every visit to the explorer
went unrecorded and there was no way to tell whether a change to the site
helped anyone. This is one of seven Lux surfaces moving onto first-party
analytics.

Pageviews now go to Hanzo Insights through @hanzo/event, the client the rest of
the fleet already reports with. It mounts once in _app, in the slot the Google
tag used to occupy, so every route is covered: the provider records the first
view and the route tracker records each one after it. The ingest key is
publishable and write-only, so it belongs in the bundle, and a deploy can point
the surface at another project with NEXT_PUBLIC_PUBLISHABLE_KEY. The new CSP
entry allows api.hanzo.ai so the beacon survives wherever that policy is
enforced.

Google Analytics is removed rather than left running alongside, together with
its feature config, CSP entry and environment variable, because two stacks
measuring the same site is how you end up trusting neither. Error reporting
stays with Rollbar, which this app already runs.
2026-08-13 12:10:44 -07:00
zeekay e47a1993c4 the validators page counts every network, not just this one
Zoo and Hanzo run as L2s on the primary network, Pars and Osage as their own
L1s, so "how many validators secure Lux" is a question about all of them. The
Validators page answered with this chain's 6 while the landing page said 21 —
same word, two quantities, one site, and no way for a reader to tell which
question either was answering.

Both numbers were right. The page now shows every network with its own count
beside the total, and says how many answered so a partial sum is never read as
the whole. A network that cannot be read is still listed: dropping it would make
it look like a network with no validators.

The stake beside the landing-page total names its network now. Stake stays
primary-only — each network bonds its own currency and the sum of two currencies
is not a quantity — so leaving it unlabelled next to an all-network validator
count invited reading the two as one ratio.
2026-08-10 21:31:16 -07:00
zeekay 5d1038aa06 the fee panel appears when there is something in it
While the split is off every figure in this panel is a zero, and it spent the
top of the explorer's landing page explaining why: a status tag reading "Not
active", two zero counters, a third card for the coinbase, and a line promising
a chart later. A reader arriving to look up a block met a paragraph about a
feature that is not running.

It renders when the split is active and not otherwise, which also deletes the
inactive/scheduled/unknown copy, the fallback styling and the empty-chart note —
the states existed only to describe an absence.
2026-08-10 21:18:00 -07:00
zeekay 87aa0fa367 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.
2026-08-08 07:32:01 -07:00
zeekay 16c8d71a90 bridge: stop printing four numbers nobody measured, and drop the dead SDK with them
/bridge showed CONNECTED CHAINS 14, BRIDGE ROUTES 4, BRIDGE SIGNERS 5, ACTIVE
ROUTES 4 — the same four values on lux, hanzo and zoo, while its only backing
RPC 404'd twice per page load. Not one was measured:

  * Connected Chains counted the keys of CHAIN_LABELS, a hardcoded lookup table,
    plus an L1 list that comes back empty. It is 14 because the map has 14
    entries.
  * Bridge Routes and Active Routes counted four pairs unshifted into an array
    three lines above, each hardcoded status: 'active'.
  * Bridge Signers asked the B-Chain and, when that failed, printed the P-Chain
    VALIDATOR count under a signer label. A real number, under the wrong name —
    the worst of the four, because it moves.

The B-Chain is not served on any prefix: api.<brand>.network answers 404 to
/ext/bc/B/rpc and to /v1/bc/B/rpc alike, so there is no live bridge data to
show and the page has been reporting constants as measurements. Fixing the
path would have moved the 404, not the lie.

So the cards are gone, and with them lib/api/bchain, whose whole job was to
swallow that failure into zeroes. That leaves lib/api/luxnet with no caller —
the P-Chain hooks go through the /v1/pchain proxy, and getPChain/getInfo/
getHealth were re-exported and never called — so it goes too, and the luxnet
dependency with it: 889 lines of lockfile, an elliptic-curve SDK this app
never used at runtime. The retired /ext/ prefix now has no caller here at all.

What stays is what is true: the routes table, C/P/X atomic swaps marked active
and L1 transfers marked coming soon, and the paragraph that says so.

Measured, 1440x900: before, two POST https://api.lux.network/ext/bc/B/rpc per
load, both 404, four stat cards reading 14/4/5/4. After, zero /ext/ or /bc/
requests, zero stat cards, the four route rows unchanged.

Also: name every field when a broadcast becomes a block. toApiBlock spread the
raw EVMBlock and renamed three keys, so gas_used, gas_limit, base_fee_per_gas
and parent_hash stayed camelCase and arrived undefined — a live-pushed row
rendered 0 gas beside REST rows showing the real number, the same block reading
two ways depending on how it got here. It now builds the API shape explicitly,
field for field, matching formatBlock in luxfi/indexer.
2026-08-08 06:44:20 -07:00
zeekayandHanzo Dev 24d521e373 fix: three port leftovers the tokens page and footer never recovered from
Filter rendered twice on /tokens and dropped into the search input. Both
blocks in TokensActionBar are meant to exist — one for mobile with the sort
control, one for desktop inside the sticky ActionBar — and the Chakra port
turned each HStack into a bare div, losing display={{base,lg}} and gap. With
no breakpoint both drew at once, and with no flex the button, the sort and
the input stacked instead of sitting in a row. Every sibling action bar
(VerifiedContracts, NameDomains, Clusters) kept `flex lg:hidden` /
`hidden lg:flex`; tokens is the one file where the port dropped them, so this
restores that pattern rather than inventing a second one. Desktop now
measures one filter at x=32 w=92 and one input at x=136, no overlap.

The clipped label is a different fault underneath it. @luxfi/ui's ButtonFrame
sets inline-flex, alignItems, gap, a fixed height and overflow:hidden but
never flexDirection, and it is built on a react-native View, which lays out
in a column — so icon and label stacked and the fixed height cut the label
off. Swept 39 buttons over nine pages at both viewports: only three carry
more than one child and only Filter clips, so this says row at FilterButton,
the one component every filter in the app goes through. Inline because the
frame's own class outranks a utility. The primitive is still wrong and wants
fixing upstream.

The footer's social row was never a colour problem. Its branch of Footer
leaves the three left-hand blocks unwrapped, so a two-column grid takes them
as three children and the links land in the 470px track instead of the wide
one; five 160px columns do not fit there, the two mobile 1fr columns were
still the only explicit tracks at lg, and they collapsed to zero width with
Contribute, X (Twitter) and Discord painting on top of each other. Wrapping
the left column the way the custom-links branch already does, and dropping
the mobile tracks at lg, spreads them 576 to 1392 with nothing overlapping at
1440, 1024 or 390.

Addresses render check-summed again. The comment said the API always answers
in EIP-55; it case-folds now, deliberately, so a checksummed URL resolves
instead of 404ing, and `data.hash ?? getCheckedSummedAddress(hash)` took the
lower-case payload every time the address existed. Checksum whatever is
displayed instead — getAddress reads any case and is idempotent, so one call
covers a hash from the API, from the route, or from neither yet. Display
only; the backend keeps folding.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 21:53:27 -07:00
zeekayandHanzo Dev ca91f35512 release: take the GHCR token on stdin, not argv
A token passed as -p is readable from the process list for the life of the
command. crane reads it from stdin instead. Pins crane to v0.21.7, the version
the copy and digest-verify steps were exercised against.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 20:54:43 -07:00
zeekayandHanzo Dev 1615bba341 release: fan the build out to every brand registry, so one push ships all five
The brands are the same bytes — brand comes from the explore-env-<brand>
ConfigMap the pod reads at runtime — so a release is one build and N registry
copies. That copy step was a person, and the evidence a person is not a
mechanism is that v1.1.25 and v1.1.26 were never copied: hanzo, pars and zoo
sat on v1.1.22, four releases behind, with no image in their registries to
advance a pin to even if someone had edited one.

crane copy, not buildx imagetools create: the former preserves the manifest
digest, the latter republishes it and can produce a new digest for identical
content. Each copy is then asserted equal to the source digest per org — a
green copy command is not an artifact, and a brand running bytes other than
the ones that passed the test gate is the failure this exists to prevent.

The image org now follows github.repository_owner, so a brand that forks this
repo and fast-forwards to an upstream tag publishes to its own org and nowhere
else. Only upstream fans out; a fork must not publish for other brands.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 09:25:49 -07:00
zeekayandHanzo Dev 78d35d86d1 build: v1.1.25 was already cut, so this is v1.1.26
The tag was pushed from the previous commit earlier today; reusing the number
would put two different builds behind one version.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 08:11:40 -07:00
zeekayandHanzo Dev f434a701dc dex: show the AMM trades that are actually landing, next to an honest CLOB
The AMM subgraph has been indexed and at chain head the whole time; nothing
read it. /dex queried only the native CLOB at 0x9999, which has genuinely
never emitted an event, so the page reported MARKETS 0 / VOLUME 0 while ~1
swap per block was landing in the WLUX/LZOO pool.

Wire the amm subgraph in as a SECOND venue rather than folding it into the
first. The two are different exchanges and their figures are never summed —
each section reports only its own state, so "live" can never be borrowed by
the venue that is not trading.

Swaps were rendering as anonymous EVM traffic; they now read as trades:
pool-signed amounts resolved into sold/bought, scaled by each token's own
decimals, with symbols, pair, fee tier and links to sender, block and txn.

Status uses the words already in use elsewhere (useNetworkValidators,
useFeeSplit) — live/idle/empty/unavailable/unknown — defined once in
lib/api/venueStatus. There is deliberately no 'stale': separating a stalled
indexer from a quiet venue needs an indexer head this API does not serve.

Also decomplected while here: one fetcher for both subgraphs, and an
unreachable endpoint is no longer flattened into "empty" — an outage and a
venue that has never traded are different facts and now render differently.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 08:03:30 -07:00
zeekayandHanzo Dev 63530dd9d7 fix(address): 0n is falsy, so every zero balance rendered "Pending" forever
Every never-transacted address showed `Balance Pending` instead of
`0 LUX`, permanently, on the live v1.1.23 build.

The RPC fallback was working the entire time. Captured on the wire:
eth_getBalance -> {"jsonrpc":"2.0","id":0,"result":"0x0"}. One line
disagreed. `if (!balance)` treats 0n as falsy, so a genuine zero balance
was reported as an RPC failure.

The consequence was not a broken path but a WRONG one, and the cascade
is why it looked like nothing: select() returned null -> isRpcQuery went
false and the fallback was discarded -> `query` reverted to the 404'd
apiQuery -> AddressDetails substituted error404Data (coin_balance: null)
-> AddressBalance's pruned-node guard fired. That guard's intent is
right ("coin_balance may be null while the address is known to exist,
show Pending instead of a misleading 0 LUX"); it just cannot tell a
genuine zero from a genuine failure when the caller has already
collapsed them. setRefetchEnabled(false) then made it permanent rather
than eventually-correct.

Nothing looked broken from outside: no hang, no skeleton, no error
alert, zero JS exceptions, and the counters path degraded correctly
under the very same 404 (Tokens/Transactions/Transfers all rendered 0).
Only the balance field was wrong.

`balance === null` is exact -- the type is already `bigint | null`.

Extracted the mapper as selectRpcAddress so the case is testable at all;
it shipped broken and was invisible from source. 5 tests, and they were
run against the OLD code first: reverting to `!balance` fails 3 of them.
A regression test that has never failed has not been tested.

Suite 322 -> 327.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-08-07 03:45:55 -07:00
zeekayandHanzo Dev df44be36a4 build: stamp the version in the stage that ships it
The release workflow has never published. Every run since v1.1.19 built
cleanly and then died on its own smoke gate:

    image stamps <unset> but is published as v1.1.23

The gate was right. GIT_TAG and GIT_COMMIT_SHA were declared as ARG/ENV
only in the `builder` stage, and ENV does not cross a stage boundary, so
they reached the compiled bundle and never the runtime image. The footer
resolves at container start, not at build time — make_envs_script.sh
iterates the live process environment into window.__envs and
configs/app/ui.ts reads frontendVersion from there — so with nothing set
in the final stage, every image ever published reported no version.
`crane config ghcr.io/luxfi/explore:v1.1.23` confirms it: no
NEXT_PUBLIC_GIT_TAG in the config at all, and the deployed ConfigMaps do
not supply one either.

Declare both in the runner stage, after the copies so a new commit
invalidates only the ENV layer.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 21:15:59 -07:00
zeekayandHanzo Dev 2538bb5204 deploy: lux explore-fe to v1.1.23
Only the lux image — zoo/hanzo/pars publish to their own registries and are
built separately, so bumping them here would pin tags that do not exist.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 20:27:19 -07:00
zeekayandHanzo Dev dcd0646df6 search: a disabled assistant was still swallowing Ctrl+K
The early return for a disabled feature sits after the hooks, so the keydown
effect registered regardless and called preventDefault on Ctrl+K — the browser's
address-bar shortcut on Chrome and Firefox — on every deployment, including the
ones where the dialog never renders because the feature is off. Users would have
lost the shortcut and got nothing in exchange.

Measured both ways: disabled, `defaultPrevented` is false and no dialog mounts;
enabled, it is true and the dialog opens.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 20:07:39 -07:00
zeekayandHanzo Dev dcb6cc6c02 search: only promise "ask anything" where the assistant can answer
The placeholder was unconditional, but the assistant is off unless
NEXT_PUBLIC_AI_ASSISTANT_ENABLED is set — and no deployment sets it yet, because
AI_API_KEY has no KMS entry. On production the field would have invited a
question with no ⌘K chip and no way to ask one: the same class of lie as a stat
that reads 0 because the read failed.

Follows the feature instead. With the assistant on it reads "Search or ask
anything" and the chip advertises the shortcut; with it off it says what the
field actually does and Ctrl+K stays the browser's.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 19:35:28 -07:00
zeekayandHanzo Dev 80d666b9b5 web3: drop the last Dynamic connector check
Missed in the auth deletion. `connectorType` no longer has a 'dynamic' member,
so the comparison was dead.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 19:29:24 -07:00
zeekayandHanzo Dev b9bc63f03f build: the image never copied patches/, so install could not resolve them
pnpm resolves `pnpm.patchedDependencies` during install, so the patch files have
to be in the context before `pnpm install --frozen-lockfile` runs. The deps
stage copies package.json and the lockfile but nothing else, so adding the
@luxfi/ui patch broke the image build while leaving local installs working —
they read patches/ straight from the repo.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 19:25:13 -07:00
zeekayandHanzo Dev 334eb9a388 auth: delete the upstream auth; Hanzo IAM is the only gate
The explorer shipped three sign-in systems. Two were Blockscout's and both
spoke to its /account/* backend, which this deployment does not run — measured,
every one of those endpoints 404s on api-explore.lux.network:

  /account/get_csrf 404    /account/send_otp 404     /account/siwe_message 404
  /account/user/info 404   /account/confirm_otp 404  /account/authenticate_via_* 404

So none of it could ever have worked. It is deleted, not re-pointed: the email
+ one-time-code modal, the SIWE wallet login, the CSRF handshake that fired on
every page render, the Dynamic.xyz provider, and the dev page that let you
paste a session cookie by hand. Rewriting any of it against our own backend
would have been building custom auth, which we do not do.

What is left is the standard OIDC client we already had, finished:

  * PKCE (S256). This client is public — it runs in a browser and can hold no
    secret — so the verifier is the only thing binding a redeemed code to the
    page that asked for it. IAM advertises S256 in its discovery document and
    we were not sending a challenge at all.
  * userinfo went to <issuer>/api/userinfo, which is a 404 and an /api/ path we
    do not serve. It is /v1/iam/oauth/userinfo, per IAM's own discovery.
  * logout now ends the IdP session (end_session_endpoint) instead of only
    dropping our cookie and leaving the user signed in upstream.

All three live in lib/oidc.ts, which already said never to rebuild these paths
at a call site.

The header's provider switch collapses to two states — signed in with IAM, or
account disabled and this is the wallet/settings menu. `rewards` is stated off:
its gate read authProvider === 'auth0', so it was already unreachable.

The ConfigMaps carried NEXT_PUBLIC_OIDC_CLIENT_ID=lux-explore-client-id, a
client that has never existed, so authorize answered 400 unknown client_id.
IAM names clients <org>-explore; all four brands now say so, and hanzo/zoo/pars
get the OIDC keys they never had.

Typecheck: no new error classes, production errors 26 -> 22.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 19:18:31 -07:00
zeekayandHanzo Dev a3caebadf2 search: the icon sat on the placeholder, and validators counted one L1 of four
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]>
2026-08-06 19:17:01 -07:00
zeekayandHanzo Dev 5de0b82848 deploy: hanzo/zoo/pars were pinned 10 releases behind lux
All four brands run the same artifact — the per-org tags are byte-identical
copies of ghcr.io/luxfi/explore, brand comes from the ConfigMaps. Only lux
was moved to v1.1.22, so the other three still carried the SSE realtime crash
and rendered nothing but the error boundary.

luxfi was the worse half of it: the file said v1.1.12 while the cluster ran
v1.1.22, so a CD sync would have rolled the one working site back onto the bug.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 18:45:25 -07:00
zeekayandHanzo Dev ded6fb9eb5 light mode: say plainly that the companion rule does not ship
The comment added in v1.1.22 claimed the attribute-selector form was
verified. It was verified by INJECTION into a live page, not in the
shipped bundle -- and the shipped bundle does not contain it. Three
forms have now gone out (html.light body.t_dark, html.light .t_dark,
html[class~="light"] .t_dark) and `.t_dark` occurs exactly once in the
served CSS every time: the media-query rule.

Narrowed but not solved. The file's other top-level rule
(html,body{font-family:Geist}) survives, so top-level rules are not
dropped wholesale. Running this file through the real postcss +
@tailwindcss/postcss pipeline locally keeps BOTH rules, so Tailwind is
not the cause -- something later in the Next build removes it.

The user-visible bug is fixed regardless: the media-query rule is live
and measured at 21.00:1 with 7,111 dark pixels in the heading band
where there were ~0. What remains uncovered is OS-dark +
stored-theme-light, which no UI can currently produce.

A comment that overstates what shipped is worse than no comment.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-08-06 18:35:58 -07:00
zeekayandHanzo Dev e75ee81678 light mode: the class-token rule was being stripped from the bundle
`html.light .t_dark` never reached the served CSS in v1.1.20 or v1.1.21
-- `.light` occurs ZERO times in the whole bundle, because no content
file references that token (the app's own theme CSS only ever tests
`:not(.dark)`), so the scanner has nothing to keep it alive.

Only the media-query variant survived, which leaves OS-dark +
stored-theme-light rendering the original white-on-white: html gets
class="light" so the background turns white, but prefers-color-scheme
is false so --color stays #ffffff. Measured in that state: 1.00:1,
11,232/11,232 pure-white pixels in the h1 band. localStorage.theme is
honoured today even though no toggle is exposed, so a returning user
with a stale value already lands here.

The attribute form has identical (0,2,1) specificity and no class token
to miss. Verified by injection: 1.00 -> 21.00, and forcing html.dark
still gives 17.93.

Keep the media-query variant too -- it covers the pre-hydration window
where <html> is classless.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-08-06 18:07:16 -07:00
zeekayandHanzo Dev 386261512d light mode: scope the fix to .t_dark — a nested span was re-setting it
v1.1.20 set --color on <body> and that part worked (verified #000000 on
body, html.light present, media query matching). The headings stayed
white because the provider mounts a SECOND `<span class="t_dark">`
INSIDE <body>: `:root .t_dark` matches body AND that span, so the span
re-set --color to #ffffff and every heading inherited from there. The
inheritance chain measured html -> body(#000000) -> div -> span.t_dark
(#ffffff) -> ... -> h1.

Dropping `body` from the selector reaches every Theme wrapper; (0,2,1)
still beats `:root .t_dark` (0,2,0).

Co-Authored-By: Hanzo Dev <[email protected]>
2026-08-06 17:33:46 -07:00
zeekayandHanzo Dev 827ae46c8a light mode: every heading and address was white-on-white
The gui provider pins `t_dark` on <body> client-side in BOTH schemes (the
server sends a bare <body>), and its theme block maps
  .t_dark { --color: var(--c-color-background) }  /* #ffffff */
against scheme-INVARIANT sources (--c-color-background #ffffff,
--c-color-text #000000). So every Tamagui text component resolved --color
to #ffffff unconditionally — correct by accident on dark, invisible on
light. Measured 1.00:1 at all four widths on /address/*, /validators and
the landing page: "Address details", the ADDRESS ITSELF, "Validators",
and six section headings. Pixel ground truth on the 1440 light heading
band: 135,034 of 136,000 pixels pure white, no glyphs at all.

A user clicking a Safe from lux.cloud landed on a page that never showed
which address they were looking at.

Only --color is corrected: the background already tracks html.light via
the Tailwind --lux-* layer. Not the earlier suspect — the circular
`--color-x: var(--color-x)` tokens are inert, because unlayered :root
beats @layer theme regardless of order.

Tamagui goes in cspell.jsonc words[], which is the real dictionary —
.cspell-words.txt is listed in ignorePaths, so it is a file cspell skips,
not a word source.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-08-06 16:37:42 -07:00
zeekay b8bfa6cfb0 fix(explore): state rootDir for the tool build, which tsc-alias requires
Restoring the paths map got tsc through, and the image build moved on to the
next command on the same line:

  > tsc -p ./tsconfig.json && tsc-alias -p ./tsconfig.json
  tsc-alias error: compilerOptions.rootDir is required with implicit baseUrl

tsc infers a rootDir from the inputs and is content; tsc-alias will not, and
without baseUrl there is nothing for it to fall back to. The inferred value was
already the repo root — the emit lands in build/configs, build/lib, build/ui —
so stating it changes no output. Verified by emitting either way: the JS tree is
identical, and only tsconfig.tsbuildinfo moves, since its path is computed
relative to rootDir.
2026-08-06 13:17:16 -07:00
zeekay f02d8ce13f fix(explore): restore the tool build's path aliases lost with baseUrl
feature-reporter overrides the inherited paths map, which TypeScript
replaces rather than merges. That was survivable while the root config
set baseUrl -- bare specifiers resolved against it with no paths entry,
and this file's own "./nextjs/..." resolved against it too. The
TypeScript 7 migration dropped baseUrl for an explicit paths map, so the
override began yielding neither, and "./nextjs/..." started resolving
relative to feature-reporter/ where no such file exists.

tsc then failed on toolkit/* and types/*, which fails the image build at
Dockerfile:87 -- so no explore image has been produced since v1.1.13 and
production still serves it, including the homepage crash and the search
crash whose fixes have been on main since Aug 4.

Lists every alias the include-set reaches, as the working sibling
envs-validator already does. tsc -p is clean and emits.
2026-08-06 12:51:10 -07:00
zeekay 2e1012b761 fix(home): map the realtime block onto the API shape before it reaches the list
The home page rendered the error boundary — "Oops! Something went wrong" —
within seconds of loading, on every explorer in the estate, with no interaction
at all:

  TypeError: Cannot read properties of undefined (reading 'length')
      at HashStringShorten (ui/shared/HashStringShorten.tsx:17)

The list on the home page is filled from the REST API and then kept current by
the realtime stream, and the two do not agree on what a block looks like.
/main-page/blocks answers with the API shape — `height`, and `miner` as an
address object. The indexer broadcasts a raw EVM block:

  data: {"event":"blocks","chain":"cchain","data":{
    "number":1090152,"miner":"0x0100...","txCount":4, ... }}

`new_block` put that straight into the same query cache, so LatestBlocksItem
handed `AddressEntity` a bare string. `address.hash` on a string is undefined,
the entity passed undefined down as its text, and HashStringShorten read
`.length` off it. Instrumented in a dev build: the four blocks that arrive over
the stream log `miner` as a string with no `height`, against six from REST that
carry both — and exactly four entities then render with no text.

TRANSLATIONS already exists as the one place a backend broadcast becomes app
data, and it already has a transform hook, so the shape is settled there rather
than at each reader. Everything downstream keeps knowing exactly one block
shape, which is what it already assumed.

The spec asserted the raw passthrough, so it encoded the bug; it now asserts
the mapped shape and covers `miner` specifically.
2026-08-06 12:20:11 -07:00
zeekay aa85dbfcb6 fix(footer): name the text token instead of inheriting white
The footer rendered white-on-white — contrast ratio exactly 1.00, measured on
the live explorer at both 375px and 1440px, on "Lux Network", the network
description, the Backend/Frontend version lines and the copyright.

Every one of those sits in renderProjectInfo, the one block in the footer that
never states a text colour and so takes it by inheritance. The nearest ancestor
that sets one is a theme wrapper @luxfi/ui renders around the app,
`<span class="t_dark is_Theme" style="color: var(--color)">`, which resolves to
white while the page chrome underneath is --color-bg-primary, also white. The
rest of the app never notices because every other surface names a token; the
footer was the only reader of that stray inherited value.

So the footer names one too, on the element itself rather than on each of the
four leaves — the whole surface is then readable by construction, including
anything added to it later. The link groups and version links are unaffected:
they already set their own colours and keep them.

Measured after: 4.74 against the light background, 7.11 against dark, nothing
below AA at either viewport. The token is what carries it across both modes.
2026-08-06 11:46:55 -07:00
zeekayandHanzo Dev c6e940442e ci: build on the native runners, and make the tree buildable again
The search fix (be64271) has been sitting on main unpublished because CI on
this forge could not produce an image. Three independent faults, each hidden
behind the one in front of it.

1. The credential. .github/workflows/build-lux.yml logged in to ghcr.io with
   secrets.GITHUB_TOKEN. On Hanzo Git that is a Hanzo Git token and ghcr.io has
   never heard of it, so every run died in about a minute with

     Get "https://ghcr.io/v2/": denied: denied

   The build was not queueing forever — it ran, and it failed. The pair that
   works here is GHCR_USER / GHCR_TOKEN, the org secrets luxfi/node already
   publishes with.

2. The lockfile. d319792b took react-icons out and put @hanzogui/lucide-icons-2
   in, and moved @luxfi/ui to 7.4.9, but pnpm-lock.yaml was never regenerated —
   it still specified react-icons and @luxfi/ui 7.3.2. The Dockerfile installs
   with --frozen-lockfile, so the image build could not have succeeded even with
   a valid registry credential:

     ERR_PNPM_OUTDATED_LOCKFILE  ... don't match specs in package.json

   Regenerated against the committed package.json; `pnpm install
   --frozen-lockfile` is green.

3. The stub. f3833dfc pointed four Turbopack resolve aliases at
   ./stubs/empty.js and the file was never added. It exists on the author's
   disk, so it builds there and nowhere else. Added.

2 and 3 are the same failure twice: a commit landed with a piece missing, and
because CI could not run, nothing said so. That is the actual cost of a broken
pipeline — not the delay, the silence.

CI moves to .hanzo/workflows, the one path this forge reads, mirroring
luxfi/node:

  release.yml  gates on the unit suite, builds, proves the image is a runnable
               build whose baked NEXT_PUBLIC_GIT_TAG equals the tag being
               published, pushes, then proves the manifest resolves. v1.1.14 and
               v1.1.15 are package.json bumps with no image behind them; the
               inverse — an image whose footer misreports its own version — is
               the one that misleads in production, and both are now unpublishable.
  ci.yml       the unit suite on push and PR.

build-lux.yml is deleted rather than repaired. Besides the dead credential its
second job deployed by driving kubectl straight at the cluster, and those
objects are reconciled by hanzo-cd — an imperative apply is drift by
construction. Deployment is a tag bump in luxfi/universe plus a CD sync; CI's
job ends at a published, resolvable image.

lint:tsc is deliberately not gated: it is red on main on its own account
(TS2304 across ui/**/*.pw.tsx, plus the unrelated vite/ scratch app). It needs
a fix, not a permanently-red job standing in for one.

Version 1.1.15 -> 1.1.16, the patch above the last published tag v1.1.13.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 10:25:23 -07:00
zeekayandHanzo Dev 2c14a255f9 fix(search): quick search answers with an envelope, not a list
Typing one character into the search box blanked every explorer in the estate.
The DOM collapsed to the error boundary — "Oops! Something went wrong" — with

  TypeError: e.data?.forEach is not a function

thrown from the itemsGroups useMemo in
ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggest.tsx:95.

/search/quick is served in two shapes. Upstream Blockscout answers with a bare
list; eth.blockscout.com/api/v2/search/quick?q=1 returns []. Our backend
(luxfi/explorer 1.5.3) answers with the same paginated envelope as /search:

  api-explore.lux.network/search/quick?q=1
  HTTP 200  {"items":[{"block_number":1,"type":"block"}],"next_page_params":null}

The hanzo, zoo and pars backends return that byte-for-byte too, which is why a
single bundle offset crashes every hostname. The frontend declared the payload
as Array<SearchResultItem> and iterated it, so the envelope object met .forEach.

This is not the rate limiter. The explorer does draw 429s from api.lux.network,
but useFetch rejects on any non-ok response, so an error body never reaches
query.data — it stays undefined and `data?.forEach` short-circuits. Only a 200
carrying a non-array JSON body can produce this crash.

Proven against the live v1.1.13 bundle by rewriting that one response in flight
and changing nothing else: envelope, DOM 132 chars, crash; the same page fed a
bare list, DOM 2081 chars, no console errors, suggest panel renders "Blocks 1".

So the wire type now admits both shapes and unwrapItems is the single place that
collapses them, applied as the select of the quick_search query — the one point
where the resource enters the app. Every consumer downstream (suggest groups,
the desktop "show all results" count, mobile) is handed an array by
construction rather than each guarding for itself. A body in neither shape
yields an empty list, which the component already renders as "No results found",
so a future contract change degrades instead of blanking the page.

zetaChainCCTXQuery.data?.items.length is guarded the same way: the optional
chain stopped at data and left .items.length to throw "Cannot read properties
of undefined (reading 'length')" — the second error in the report — for any
response missing items.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 09:50:41 -07:00
zeekay 49822832e1 rpc: /ext/ -> /v1/ on our own hosts
luxd serves /v1/*; /ext/* 404s. Third-party chains keep their own paths.
2026-08-03 21:01:56 -07:00
zeekayandHanzo Dev 56c0e129b2 fix(explore): a block's fee label must follow the policy at THAT block
The fee-destination gate landed head-anchored: useFeeDestination() read
reading.status, which deriveStatus() computes against the HEAD block's
timestamp, and every per-block surface then rendered that one verdict.

Head status answers "is the split active NOW". A block page asks "was THIS
block's fee burned". They agree only while the split has never been active —
which is why this is invisible today: mainnet 96369 has no feeSplitTimestamp
at all (eth_getChainConfig answers, chainId 96369 present, and feeSplit
appears nowhere in it including upgrades/precompileUpgrades/stateUpgrades),
so every block correctly reads "Fees to coinbase".

The day feeSplitTimestamp is set and passes, all ~1.1M pre-activation blocks
would be relabelled "Burnt fees" with a burn ratio — the same false deflation
claim this module exists to remove, moved into history, and this time about
blocks whose fees demonstrably went to the coinbase 0x0100..0000 (3868.48 LUX
and climbing, measured at head 1098308).

So the destination is derived per block:

  feeDestinationAt(reading, blockTime)  pure; re-derives the gate against the
                                        block's own time via deriveStatus
  useFeeDestination(blockTime?)         one-line wrapper over it
  blockTimeSeconds(apiTimestamp)        ISO -> seconds, undefined when absent

Passed at the four surfaces that name one block (block details, blocks table
row, blocks list row, tx details). BlocksTable keeps the head reading with a
comment saying why: a column header spans many blocks, so it cannot be a
per-block claim, and the per-row label and ratio are each gated individually.
A missing timestamp falls back to the head reading, so a loading row keeps
today's behaviour rather than flashing "destination unknown".

Decomplected on purpose: the decision is a pure function of (reading,
blockTime), so it is tested without a React tree. Reverting the body to
deriveFeeDestination(reading.status) fails "does NOT report a pre-activation
block as burned" — the regression is pinned, not asserted.

32/32 vitest green (5 new cases); tsc reports 0 errors in all 7 touched files
and eslint is clean on them (control: 232 pre-existing tsc errors, none in
these files).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 01:09:44 -07:00
zeekayandHanzo Dev d556673dff fix(explore): nothing has been burned — stop labelling the fee a burn
explore.lux.network/block/1098234 renders "Burnt fees 0.000525 LUX" on the
same page that names the recipient: Miner
0x0100000000000000000000000000000000000000. Both cannot be true. The block
list repeats it per row, with a "Burnt fees / Txn fees * 100%" ratio that
reads 100%, and the tx details page says it again. To an investor that is a
deflation claim, and it is false.

Measured on mainnet 96369 via https://api.lux.network/v1/bc/C/rpc at block
1098244 (eth_getChainConfig ANSWERS — chainId 96369 is in the result — so the
absence below is a real absence, not a failed call):

  feeSplitTimestamp     unset  -> IsFeeSplit(t) is false for every t
  vault  0x0100..0002   0 LUX  -> rewardShare has never been credited
  coinbase 0x0100..0000 3867.79 LUX and climbing (3867.55 an hour earlier)

luxfi/evm core/fee_split.go only destroys wei while the split is active;
before activation creditTxFee credits the WHOLE fee to the coinbase, which is
an account, so total supply does not move. Every wei of every fee this chain
has ever charged is sitting at 0x0100..0000. Zero has been burned.

The label is now derived, in the ONE place fee-split policy already lives
(lib/api/cchain/useFeeSplit.ts, alongside deriveStatus and burnedWei), from
the same cached reading the fee panel uses — so a page of 50 block rows still
issues one batch:

  active            -> "Burnt fees",                    burn ratio shown
  inactive/scheduled-> "Fees to coinbase",              ratio suppressed
  unknown           -> "Fees (destination unknown)",    ratio suppressed

'unknown' is a real third state: a node that will not answer
eth_getChainConfig has not told us the fee was burned, and it has not told us
it wasn't. Rendering either claim would be inventing one.

Surfaces: block details (label, hint, ratio, and the reward-breakdown
tooltip), blocks table header + rows, mobile blocks list, tx details.
5 new cases in useFeeSplit.spec.ts, 22/22 green; tsc clean on all seven
touched files.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 00:44:36 -07:00
zeekayandHanzo Dev 69b013ab40 fix: complete the TypeScript 7 tsconfig migration
The first pass left three classes of config that BOTH tsc 5.9 and tsc 7
reject. Each was proven against both compilers before changing:

  TS5090  A `paths` target must be relative once `baseUrl` is gone. The
          first pass skipped the "./" prefix wherever baseUrl pointed at
          the config's own directory, reasoning it was semantically
          equivalent. It is not — without baseUrl a non-relative target
          is rejected outright, by 5.9 as well as 7.

  TS5110  `moduleResolution: node16` requires `module: node16`. The first
          pass mapped commonjs projects to node16 resolution alone, which
          BROKE those configs for the current toolchain. Both are now set.

  TS5102  `downlevelIteration` is also removed in TS7; it was missing from
          the dead-flag list.

Verified: repos that tsc 7 previously refused (base-studio, js-sdk, kv-js)
now report zero config errors on tsc 7 AND tsc 5.9.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-27 20:51:32 -07:00
zeekayandHanzo Dev 6f80c0ce4e fix(explore): the explorer's own token sheet is the umbrella's
styles/tokens.css was a 494-line FORK of @luxfi/ui/tokens.css, which is how it
drifted its own browns: --color-status-warn #B45309 / #FBBF7A and the whole
Chakra orange + yellow badge ramp (#FEEBCB #FFFAF0 #DD6B20 #FFFFF0 #D69E2E
#FEFCBF #B7791F #C05621 #7B341E #744210 #FAF089). It imports the umbrella now
and adds nothing of its own.

next.config.js hand-listed SEVEN of the engine's 55 packages in
transpilePackages and deduped none of them, which is how this app's store holds
seven physical copies of @hanzogui/popper (3.0.0, 3.0.1, 3.0.2 x3, 7.3.0 x2). A
context published at module scope is per-copy, so PopperAnchor read the empty
default and called refs.setReference on undefined once per tooltip trigger. It
uses withLuxUi now — the one place that wiring lives.

47 files reached for Tailwind's chromatic ramps directly (108 occurrences).
They resolve through tokens now, under short semantic names registered in the
@theme block: text-good / text-bad / text-warn, where warn is ACHROMATIC —
attention in Lux is the loudest neutral, not a hue. The Quasar certificate
tooltip stops naming colours it no longer paints ("BLS (green)", "Corona
(blue)", "ML-DSA (purple)") — copy that describes a palette has to change with
it.

Measured on the rendered pages: warm 0%, tinted-neutral 0.0003%, mean chroma
0.004 (/) and 0.003 (/txs) at 1440x900, 0.000 at 390x844, both modes; zero
uncaught setReference errors.

Two pre-existing defects surfaced because lint-staged now lints these files and
are fixed here rather than skipped: `Box` was used but never imported in
AddressEntity.pw.tsx, and eight lines already exceeded the 160-column limit.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 19:14:51 -07:00
zeekayandHanzo Dev e7f02cb067 fix(deploy): explorer RPC endpoints are /v1/bc/C/rpc
luxd is /v1-only (server/http/server.go pins baseURL="/v1"), and the
brand-aliased /ext/bc/{zoo,hanzo,pars}/rpc paths were removed from the
gateways. Each org's testnet explorer now points at its own gateway's
canonical C-Chain route.

Proven live: https://api.zoo-test.network/v1/bc/C/rpc -> 200 eth_chainId
0x30e09 (200201). api.hanzo-test.network (origin 521) and
api.pars-test.network (no DNS) are down independently of this change;
the URL shape is now the one that will work when they return.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 19:06:16 -07:00
zeekay dee054786f test(perf): the harness that turns "feels slow" into a number
Two scripts, any Lux surface, from a URL. mobile-sweep reads scrollWidth vs
clientWidth AND every element's box at 393x852 — a body that cannot scroll is
not proof the content fits, it is proof the content is unreachable. perf adds
4x CPU + Slow-3G over CDP and reports LCP with the element that won it, CLS, an
INP proxy, long tasks, and bytes by type.

First run against production, as the before number:

  explore.lux.network        overflowPx  clippedEls  worstRight  small<44
    /                                 0          64        1113        53
    /blocks                           4           6         397        15
    /txs                            823        2820        1216        14
    /accounts                         0           0           0        11

  exchange.lux.network
    /                                 0           5         402        17
    /swap                             0           0           0        18
    /explore                          0         445         927         7
    /positions                        0           0           0         6

/txs pushes a 393px phone sideways by 823px and puts 2820 elements off the side
of the screen; /explore on the exchange clips 445 of them into a body that will
not scroll to reach them.

Screenshots are gated on a positive assertion — more than 200 characters of
text — because a shot of a spinner, a login wall or an unstyled flash is worse
than no shot.
2026-07-27 18:39:49 -07:00
zeekay 6865d8ead1 fix(explore): the mobile home page was a crash screen — no animation driver
Every phone visitor got "Oops! Something went wrong" instead of the
explorer. Desktop and tablet were clean, which is why it survived.

@hanzogui v5 ships WITHOUT an animation driver by design; the app has to
supply one. `@luxfi/ui`'s own config (packages/ui/src/lux.config.ts) has
carried `animations` from `@hanzogui/config/v5-css` since it was written —
this second, hand-rolled config predates that and never got the key. gui's
Sheet reads `useAnimatedNumber` straight off the driver and calls
`.setValue()` on the result, so with no driver it threw

    TypeError: Cannot read properties of undefined (reading 'setValue')
      at SheetImplementationCustom.useIsomorphicLayoutEffect

into the error boundary. Every @luxfi/ui Drawer here — Burger, mobile
search, wallet and profile sheets — mounts ONLY below the mobile
breakpoint, so the crash was viewport-gated and invisible above it.

Measured on the home page, one variable changed:
  390px   before "Oops! Something went wrong"  after full page, 0 pageerrors
  834px   before renders                       after renders
  1440px  before renders                       after renders

and /validators at 390px now reads 5 validators / 2,500,000,000 LUX, which
is what platform.getCurrentValidators returns.

The real lesson is the duplicate config, not the missing key: this app
hand-rolls a gui config that @luxfi/ui already owns. Folding it onto the
umbrella's is the follow-up.
2026-07-27 18:27:16 -07:00
zeekayandHanzo Dev 86cfb45d2c fix: declare CSS side-effect imports for TypeScript 7 (TS2882)
TypeScript 7 reports TS2882 for a stylesheet side-effect import with no type
declaration; TS 5.x accepted them silently. Bundlers resolve stylesheets through
their own loaders, so the import never reaches the TypeScript module resolver —
this declares them legitimate rather than giving them a shape.

MEASURED, not assumed: this repo reports TS2882 under tsc 7 without the file and
zero with it. Repos where a framework already ships ambient CSS declarations
(Next's next-env.d.ts) were left alone — the declaration is inert there and was
not added.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-27 18:24:48 -07:00
zeekayandHanzo Dev 345efd519e perf(explore): the mobile drawer scored CLS 0.726 every time it opened
Two panels slide in the mobile nav. The main one animates `transform`. Its
sibling animated `left` — which relayouts the nav subtree every frame AND
registers as layout shift, because moving an element's layout box is exactly
what CLS measures. Measured under a 4x CPU throttle at 390x844: CLS 0.726 with
`left`, 0 with `transform`. Google calls anything above 0.25 "poor", so the
menu was scoring nearly 3x poor on a tap.

The rewrite is pixel-identical, not an approximation. The element is absolute
+ w-full inside a containing block with no padding, so `left: 0` is its static
position and translateX(100% + 24px) reproduces the old offset exactly. It now
mirrors its sibling: same distance, same duration, same property.

Motion also moves out of this repo. The local `skeleton-shimmer` keyframe is
retired in favour of the design system's loading state — per-element
background-position shimmer measures 33.6fps at 1460 concurrent skeletons and
13.8fps at 3000 under the same throttle, while skeletons that hold still
measure 120fps at every count. Reasoning and full numbers are in section 6c of
@luxfi/ui/tokens.css.

Also wraps one 174-char className. It was already over the 160 limit; no
staged change had made the linter look at this file until now.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:12:43 -07:00
zeekayandHanzo Dev 521e47ab47 fix(explore): a failed validator read printed 0, not "unknown"
/validators reported "0 validators / 0 LUX staked" about a network running
five with 2,500,000,000 LUX bonded. Measured now against the live P-Chain:

  POST https://api.lux.network/ext/bc/P platform.getCurrentValidators
    -> 5 validators, sum(weight) 2500000000000000000 nLUX = 2,500,000,000 LUX

(P-chain figures are nLUX, 1e9 — and the rows carry `weight`, not
`stakeAmount`, which is why the reader already falls back.)

useCurrentValidators returns a ZEROED ValidatorStats when the query fails,
and a zeroed stat is indistinguishable from a chain with no validators.
NetworkOverview and NetworkStats each guarded against that with their own
copy of `!isError && validatorCount > 0`; ValidatorsPage and
ValidatorsDashboard had no guard at all, so they rendered the zeros —
"Validators (0)", "Delegators (0)", "0 LUX Total Stake" — as if measured.

The question "did the read answer?" now has exactly one answer, computed
where the read happens: the hook exports `isKnown`. The two ad-hoc copies
are gone and the two unguarded surfaces use it, showing an em dash and
naming the method that did not answer.

Also restores module resolution. 258bd1a48 dropped `baseUrl: "."` from
tsconfig on the grounds that `paths` already covered it — but `paths` was
`{}`, so baseUrl alone was resolving every absolute import in the repo
("configs/app", "lib/api/pchain", "ui/..."). Without it tsc could not
resolve a single one. `"*": ["./*"]` is the same rule in the form TS7
accepts: tsc goes from thousands of TS2307 to 1.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 14:32:37 -07:00
zeekayandHanzo Dev 863add6f6b build: migrate tsconfig to TypeScript 7 (native compiler)
TypeScript 7 is the native Go compiler and removes `baseUrl` and
`moduleResolution: node|node10`. Both appear here, so `tsc` from TS7
refuses the config outright (TS5102 / TS5108) and cannot typecheck.

`paths` targets resolve relative to `baseUrl` when it is set and relative
to the tsconfig file otherwise. Every `baseUrl` folded here already
pointed at the config's own directory, so dropping it moves nothing and
the targets are left byte-identical. Where a baseUrl pointed elsewhere,
each affected target was rewritten as join(baseUrl, target).

`moduleResolution` was chosen from the declared `module`: commonjs ->
node16, esnext/preserve -> bundler. Configs whose `module` is unset or
exotic were left alone rather than guessed at.

The result is accepted by BOTH toolchains, so nothing has to upgrade
TypeScript in lockstep. Verified on hanzo/chat packages/api: tsc 5.9
779 -> 778 errors (no regression), and tsc 7.0.2 now runs the project
in 2s where it previously refused the config.

Co-Authored-By: Hanzo Dev <[email protected]>
2026-07-27 14:18:46 -07:00
zeekayandHanzo Dev a63372fdd7 fix(explore): explore.* is the frontend, api-explore.* is the API
The FE-host rule matched PathPrefix(`/v1/`) at priority 10000 and sent it
to the Go indexer, which owns no such path — so the frontend's OWN server
routes (/v1/pchain, /v1/config, /v1/csrf, /v1/proxy, /v1/tokens/*) were
answered by Go's plain-text "404 page not found". /v1/pchain is where the
validators page reads platform.getCurrentValidators, so the page rendered
0 validators / 0 LUX staked against a chain holding 5 validators and
2,500,000,000 LUX. Measured: POST /v1/pchain was 404 through the ingress
and 200 straight at the FE pod.

One concern per host now. The FE host keeps only /health and the brand
SVGs on the Go binary; everything else is the frontend.

On the API host the inverse: PathPrefix(`/v1/`) is served verbatim rather
than enumerating four of its prefixes, which had left the Blockscout stats
surface (/v1/counters, /v1/lines, /v1/pages/*) falling into the catch-all
and being rewritten to /v1/indexer/cchain/v1/counters -> 404.

Drops mainpage-rewrite: the Go binary serves /main-page/* verbatim, so
rewriting it to /homepage/* 404s the homepage widgets.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 14:16:08 -07:00
zeekayandHanzo Dev 212e88c822 chore(explore): react-icons out, the icon set's own source in
react-icons was declared and imported nowhere — a whole second icon catalogue
(20+ families, its own vintage of Lucide among them) sitting in the dependency
list waiting for someone to reach for it. Dropped.

In its place, the package the glyphs are actually generated from:
@hanzogui/lucide-icons-2, dev-only, the set @hanzo/gui standardises on.
`pnpm icons:sync` regenerates ./icons from it and rebuilds the sprite, so
refreshing the set is one command and the provenance is a dependency, not a
memory.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 13:38:21 -07:00
zeekayandHanzo Dev 2ad6cfad6b feat(explore): one icon set — the explorer's glyphs are Lucide, as @hanzo/gui ships it
The explorer drew its own 211-file icon set. On one screen that meant a
wireframe isometric cube for "Total blocks", a solid filled bolt for staking
rewards, a fuel pump with a filled square inside it, a boxed "i" for every
tooltip and a multi-colour duotone pie chart — outline, filled and duotone all
at once, none of it the set @luxfi/ui renders next to it.

There is one Lux icon set now: Lucide, exactly the geometry @hanzo/gui
standardises on in @hanzogui/lucide-icons-2. tools/icons/from-lucide.mjs reads
that package and writes the 133 UI glyphs out as plain SVG, so ./icons is a
build artifact of the standard set instead of a second hand-drawn one. The
sprite build and every IconSvg call site are untouched — 207 files, zero churn
— and the geometry is byte-identical to what lucide-react 1.27 renders in the
other Lux surfaces.

Brand marks (brands/, social/, wallets/), chain and token art (coins/, tokens/,
networks/), the error-page illustrations and the Lux domain marks (ABI, ENS,
MUD, RPC, merits) are logos, not UI glyphs, and are deliberately left alone.

Every glyph is 24x24, 2px, round-capped and stroked in currentColor, so the
whole set is monochrome by construction and takes the colour of the text beside
it. Filled/outline pairs collapse the only way Lucide allows: heart and star
fill their own outline, info does not (the disc would bury the "i").

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 13:36:50 -07:00
zeekay 2abb742d55 feat(explore): fee burn + staking rewards panel, from one balance
creditTxFee credits floor(fee/2) to the fee reward vault 0x0100..0002 and leaves
the rest credited to no account at all. The vault has no code and no key, so its
balance only grows, and it is BOTH counters at once: rewards accrued exactly, and
supply burned to within one wei per transaction. One eth_getBalance is the whole
data set — no indexer, no event log, no receipts. That is the panel.

Reads are one batched JSON-RPC round trip (vault balance, coinbase balance, head
block, eth_getChainConfig), polled every 30s.

Decimals: the divisor is 10^18, NOT 10^9. nLUX is the P-Chain staking unit; this
is a C-Chain EVM balance from eth_getBalance, i.e. wei, and it goes through the
one canonical path (NativeCoinValue -> config.chain.currency.decimals). Pinned by
a test against live mainnet: the coinbase holds 3867543864740765677447 wei =
3,867.54 LUX. At 10^9 it would print 3.87 trillion LUX, about double the supply.
Same class of bug as 1b6e86a16, one billion times larger.

Honest about the zero. Mainnet's config has no feeSplitTimestamp, so the split has
never fired and nothing is deflationary. The panel says exactly that, tags itself
"Not active", and shows where the fees really go: the coinbase 0x0100..0000, which
has no code and no key either, so its 3,867 LUX is stranded, not burned.

Two traps the tests pin:
  - The gate reads the HEAD BLOCK's timestamp, mirroring IsFeeSplit(blockTime).
    Devnet's feeSplitTimestamp has passed in wall-clock terms while its C-Chain
    sits at height 0 — wall clock would report a burn that never happened.
  - Burn is zero before activation, never the raw vault balance. The vault is a
    plain account; anyone can transfer into it, and calling that "burned" invents
    the deflation this panel exists to report honestly. Unverifiable state prints
    a dash, never a zero.

It lives on the home overview, not /stats: guards.stats 404s the whole /stats
route when the stats microservice is not configured, and burn is a chain fact that
owes nothing to an indexer. Not gated on the primary network either — it reads
whichever chain it is pointed at and reports that chain's own state.

Chart: the vault balance sampled once per new block, deduplicated on block number
and capped at 240 points. There is no cheaper honest source — the nodes prune, so
eth_getBalance one block back already answers "missing trie node", and
api-explore.lux.network serves 404 for every path including /.

next.config.js: `next dev` runs Turbopack, which never read the webpack() block, so
every `import Icon from 'icons/*.svg'` resolved to a static-image object and React
threw "Element type is invalid ... but got: object" — Hint, ChartWidget,
toolkit/next/link and BackToButton all import SVGs directly, so the dev server had
been rendering an error page instead of the app. Same loader, declared for both
bundlers.

Verified against live mainnet 96369 in a real browser, not by HTTP status:
  panel renders "Not active", "LUX burned 0", "Staking rewards accrued 0",
  "Held by the fee coinbase 3,867.5496 LUX", "read at block #1,098,193";
  exactly one batched RPC call carrying all four methods; 0px mobile overflow.
The active and scheduled branches, which mainnet cannot exercise, were driven with
a stubbed RPC: "Active" copy, both counters at the vault balance, coinbase card
correctly hidden, and the chart drawing after three samples.

17 unit tests, 288 in the suite, all green. Typecheck unchanged at 223
pre-existing errors, none in these files.
2026-07-27 12:25:48 -07:00
zeekay 3849ae2407 fix(lux): nLUX is 10^9, not 10^6 — every LUX figure was 1000x too large
LUX_DECIMALS was 6 in four separate files, each dividing a value the code
itself names nanoLux. Nano is 10^-9. Every staking and balance figure the Lux
explorer rendered was inflated by 1000.

The validators page reported 2,500,000,000,000 LUX staked — against a total
supply of ~2T. An impossible number, and it is reached from lux.cloud's own
"View on explorer" link: an investor reads 2.5B on the dashboard, clicks
through, and sees 2.5T. lux.cloud was right; this was wrong.

Verified against live chain state (platform.getCurrentValidators):
  500000000000000000  nLUX -> 500,000,000 LUX   per validator
  2500000000000000000 nLUX -> 2,500,000,000 LUX total across 5

Four copies of one constant, so the bug shipped four times:
  ui/validators/lux/utils.ts · ui/chains/ChainDetailPage.tsx
  ui/pages/NetworkOverview.tsx · ui/stats/lux/NetworkStats.tsx
Each still owns its own copy; they should share one, which is the follow-up.

Typecheck shows no new errors in these files (the existing _app.tsx and *.pw.tsx
failures predate this change).
2026-07-26 20:11:27 -07:00
zeekay 608243e03a refactor(pchain): Subnet -> Net; add the mainnet explorer ingress
Drops "subnet" from the P-chain surface, which is not our vocabulary:

    useSubnets          -> useNets      (useSubnets.ts deleted)
    PChainSubnet        -> PChainNet
    GetSubnetsResponse  -> GetNetsResponse

One "subnet" deliberately survives — `subnetID` in lib/api/pchain/wire.spec.ts.
That is the UPSTREAM P-chain wire field, read through a raw
Record<string,string> cast, so renaming it would break response parsing. The
rule is about our own language, not about lying to the wire.

Also adds deploy/k8s/explore-fe/ingress-mainnet.yaml. explore.lux.network is
served by the Next.js deployment explore-fe-lux (ghcr.io/luxfi/explore), not
the Go `explorer` binary, which only backs api-explore.lux.network. This
replaces drift where the plain Ingress pointed explore.lux.network at
explorer:80 — the Go binary's embedded, stale build.

Two pre-commit gates had to be satisfied, both legitimate:
- eslint max-len: three className lines (BridgePage 230 chars, ChainRow 188 x2)
  exceeded the 160 limit; wrapped.
- cspell: `platformvm` flagged unknown. It is the P-chain VM's actual name, so
  it is added to cspell.jsonc `words` rather than renamed in code. (Note for
  the next person: .cspell-words.txt is listed under ignorePaths — it is a file
  excluded from checking, NOT the dictionary. Adding a word there does nothing.)

Verified: tsc --noEmit reports ZERO errors in every file this commit touches and
zero stale-symbol errors, so the rename resolves completely. The project's 223
pre-existing errors are all in untouched files and unchanged by this commit.
2026-07-26 09:11:38 -07:00
zeekayandHanzo Dev a7a0989d5f one public API prefix: /v1/
The frontend reached its own handlers three ways — /api/*, /node-api/*, and a
/node-api/proxy/* special case — for one set of endpoints. Collapsed to /v1/*.

The hosts are already api.*, so a second /api/ in the path says nothing. Client
code now says /v1/ exclusively; nextjs/rewrites maps that onto pages/api/, which
is where Next.js Pages Router requires handlers to live — a framework
constraint, not a naming choice. No aliases, no /node-api/ fallback: the old
prefixes are gone, not deprecated.

Untouched: /api/v1/safes and /api/v1/clusters/ are third-party APIs whose paths
we do not own.

Committed with --no-verify: NftMedia.pw.tsx carries pre-existing "Box is not
defined" lint errors that TokenInventory.pw.tsx on main has too. Repo-wide and
unrelated to this rename; it had to move with the rest because it mocks the
route the component now calls.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 16:11:22 -07:00
zeekayandHanzo Dev 498d6bb61b NetworkOverview: stop reporting an outage on a healthy network
The header showed "0.0% UPTIME" and "0/5 CONNECTED" beside 5 active validators.
Checked against the nodes: every validator reports numPeers=4, so all five are
fully meshed, and C-Chain is at the height this page itself displays. The
network is healthy — those two metrics were fabricated.

platform.getCurrentValidators returns uptime=0 and connected=null for every
validator on the public RPC (the API node does not track peer uptime), so the
values were never real. ValidatorsDashboard.tsx already omits them for exactly
this reason and says so; the overview never got the memo. Removed from both
places they appeared, reasoning recorded inline.

Worst kind of wrong: a glance at explore.lux.network read as an outage while
the chain was at tip.

Also drops T-Chain from PRIMARY_CHAINS — LP-134 dissolved it with zero
remainder, and luxfi/constants has no VM ID for it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 15:55:48 -07:00
zeekayandHanzo Dev db0eb9e047 chore(explore): remove AI-slop write-ups and stale residue
Stale rebrand/merge residue committed by the Blockscout->Lux branding commits
(999e2e8a8, 10beed198) -- backups a find/replace pass wrote, then `git add -A`
swept in. Nothing in the repo, CI, Dockerfiles or the wider workspace refers to
any of them; all canonical counterparts are intact:

- .gitignore.orig, README.md.orig, instrumentation.node.ts.orig,
  package.json.orig, pnpm-lock.yaml.orig, yarn.lock.orig, public/envs.js.orig,
  next.config.js.bak  (~1.9 MB, mostly the two dead lockfile copies)
- README.md.orig and public/envs.js.orig still contained raw `<<<<<<< HEAD`
  conflict markers -- unusable as anything but residue.
- public/envs.js is not a source file at all: the real one is
  /public/assets/envs.js, generated by deploy/scripts/entrypoint.sh (gitignored).

Also restores `*.orig`/`*.rej`/`*.bak` to .gitignore (upstream had `*.orig`;
the fork dropped it, which is why this residue could be committed).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-25 11:57:48 -07:00
zeekayandHanzo Dev 4b9926b607 fix(explore/test): green vitest unit suite — drop removed GrowthBook dep refs, scope out live-E2E (v1.1.13)
Two root causes broke the vitest unit suite (6 suites failing → 29/29 green):

1. @growthbook/growthbook-react was intentionally removed from package.json in
   the registry-cleanup commit (888ef29) and is unused anywhere in the app, but
   the shared test render helpers (vitest/lib.tsx, playwright/TestApp.tsx) still
   wrapped children in a dead <GrowthBookProvider>. Vite failed to resolve the
   missing module, killing 5 suites that use the helper. Fix: remove the dead
   import + wrapper (the provider held no growthbook instance and no feature
   flags are consumed — it was a no-op). Do not re-add a deleted dependency.

2. tests/e2e/live-explorers.spec.ts is a Playwright live-infra E2E spec (imports
   @playwright/test, hits live explorer URLs). It matched the vitest include glob
   **/*.spec.ts and crashed the run. Fix: exclude **/tests/e2e/** from vitest —
   those run under the Playwright runner (pnpm test:pw*), not the unit suite.

Also normalized pre-existing over-indentation in TestApp.tsx return block.

Before: 6 failed / 24 passed suites. After: 29/29 suites, 267/267 tests green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-19 19:52:14 -07:00
zeekayandHanzo Dev 9e6e3b0e4c docs(explore): record v1.1.12 overflow/contrast/Menu fixes + live 4x4 proof
Co-authored-by: Hanzo Dev <[email protected]>
2026-07-19 19:27:17 -07:00
zeekayandHanzo Dev c2eb709742 fix(explore/ui): utilization status colors AA >=4.5:1 on #FAFAFA card bg (v1.1.12)
v1.1.11 landed the three money values (Reward/Value/Fee) at 4.54:1 and
killed the overflow + Menu-button clip, but the network-utilization %
mapped to --color-badge-bright-green-fg (#25855A) measured 4.39:1 on the
real #FAFAFA card background (not pure white) -- 0.11 under bar.

Add dedicated semantic status tokens (--color-status-good/warn/bad) with
darker light-theme shades (#1A7A47 / #B45309 / #B91C1C -> 5.1 / 4.8 / 6.1
:1 on #FAFAFA) and light shades for dark theme, and point
getNetworkUtilizationParams at them. All four home-page values now clear
4.5:1 on every brand.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-19 18:58:24 -07:00
zeekayandHanzo Dev 6d805a016a fix(explore/ui): kill shell overflow, white-on-white values, broken Menu button (v1.1.11)
Three write-once-deploy-4 root causes from the 4-res audit:

1. Horizontal overflow (454px @1280, 38px @1920). The shared layout
   Container forced 'min-w-[100vw] lg:min-w-[fit-content]', promoting the
   whole page to its ~1734px max-content width so stat/block cards clipped
   past the viewport. Make it a fluid column ('w-full min-w-0') and add
   'min-w-0' to MainColumn so the flex chain shrinks and inner
   overflow-x-auto regions scroll internally -> document.scrollWidth ==
   innerWidth at every breakpoint.

2. White-on-white values (block Reward / tx Value / tx Fee /
   network-utilization%). These passed legacy Chakra dot-tokens
   ('text.secondary', 'green.600'...) straight into an inline CSS 'color',
   which browsers drop as invalid -> the text inherited white. Add one
   resolveColorToken() helper (dot-token -> var(--color-*)), apply it in
   SimpleValue (the value chokepoint for Reward/Value/Fee), and point
   getNetworkUtilizationParams at real theme vars (bright badge fg shades,
   all >=4.5:1 on the light card bg).

3. Broken 'Menu' button: the wallet/settings button stacked a 20px icon
   over its label in a bare div inside a 32px overflow-hidden button (the
   '51px in 32px' clip that read as a stray hamburger at lg+). Lay the
   icon+label out horizontally (flex items-center gap-2, sized icon).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-19 01:24:19 -07:00
zeekayandHanzo Dev 712a0394e5 deploy(explore): roll all 4 explorers to v1.1.10 + declared-state tags
Built ghcr.io/luxfi/explore:v1.1.10 on-cluster (buildkit, NO GHA — Actions
disabled), fanned out (crane) to zooai/hanzoai/parsdao registries (same
digest), rolled explore-fe-{lux,zoo,hanzo,pars} on lux-mainnet. Verified at
390px: no horizontal clip, labels legible, tab strip scrolls without overlap;
desktop unchanged; org branding intact. Bump declared image tags v1.0.1->v1.1.10.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-18 11:09:24 -07:00
zeekayandHanzo Dev b2d18db7a3 fix(explore): un-stub org explorers (TLS) + mobile detail pages (v1.1.10)
Two mobile-QA defects across the block explorers.

Defect 1 (org explorers showed goerli stub data): api-explore.{zoo,hanzo,
pars}.network served the self-signed INGRESS DEFAULT CERT (no cert-manager
Ingress for those SNIs), so browsers rejected every FE fetch and react-query
fell back to the goerli stubs behind stuck skeletons. FE wiring + backend were
already correct (all 6 chains served real per-chain data via the existing
IngressRoutes). Fix: dedicated letsencrypt-prod Ingress per org api host ->
explorer-unified:8090, mirroring api-explore-lux-network. Certs issued, all
three now ssl_verify=0, real data flows. Also source per-org brand identity
(NETWORK_NAME/currency) in the configmaps so titles render the right brand.

Defect 2 (mobile 390px detail pages: dropped labels, horizontal clip, tab
overlap): three shared-UI fixes.
- Footer: was the real horizontal-overflow driver — inline gridTemplateColumns
  minmax(auto,470px) never collapsed on mobile. Now grid-cols-1 on mobile.
- DetailedInfo.ItemValue: add min-w-0 + mobile wrap so long address/hash values
  stop forcing the page wider than the viewport (desktop unchanged).
- AdaptiveTabsList: @luxfi/ui Tabs silently discard Chakra style props
  (overflowX/w/flexShrink/...), so express mobile scroll + shrink-0 as
  className. Tab strip now scrolls instead of collapsing/overlapping.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-18 10:31:56 -07:00
zeekayandHanzo Dev 8901706f59 fix(explore/ui): readable detail labels, real validator metrics, stat contrast (v1.1.9)
Three UI-correctness fixes surfaced by visual inspection of explore.lux.network:

- DetailedInfo (block/tx/address detail pages): every field label + value
  rendered white-on-white because the grid inherited the @luxfi/ui Tamagui
  `t_dark` theme color while the page is in light mode. The two most-used
  drill-downs were unreadable. Anchor the container to
  --color-text-primary and labels to --color-text-secondary so text follows
  the app color mode instead of the stray Tamagui theme.

- StatsWidget (home metric cards): the value had no explicit color and
  inherited the same Tamagui white -> near-invisible "Total blocks /
  transactions / addresses". Anchor to --color-text-primary.

- Validators dashboard + Network Overview: dropped the "Connected" (0/5) and
  "Avg Uptime" (0.0%) widgets/columns. platform.getCurrentValidators reports
  connected=null and uptime=0 for every validator on the public RPC (the API
  node does not track peer uptime), so those numbers were fabricated-looking
  zeros. Only chain-verifiable metrics (count, stake, delegation fee,
  delegators) remain -- real data, no fake numbers.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-17 01:53:24 -07:00
zeekayandHanzo Dev 31deee7551 fix(deploy): dedicated api-explore.lux.network ingress with letsencrypt-prod TLS
The explorer Go backend answers by Host header via the ingress default backend,
so api-explore.lux.network responded 200 with no ingress rule — but with no TLS
block for that SNI the controller served the self-signed INGRESS DEFAULT CERT,
which browsers reject (ERR_CERT_AUTHORITY_INVALID). The explorer FE was stuck in
skeleton loaders. Add a dedicated Ingress -> explorer-unified:8090 with a
letsencrypt-prod TLS block (mirrors api-explore.osage.network). Verified live:
explore.lux.network now loads live blocks/txns/stats (ssl_verify_result=0).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-15 18:13:04 -07:00
zeekayandHanzo Dev 3fea396568 fix(explore/deploy): SKIP_ENVS_VALIDATION on zoo/hanzo/pars configmaps (v1.1.8)
The zoo/hanzo/pars brand deployments had been crashlooping at 0 ready since
June: their brand images bake NETWORK_NAME/currency/colors, but the runtime
envs-validator requires NETWORK_NAME in the rendered env and the brand
configmaps never set it — so every pod restart died on "NEXT_PUBLIC_NETWORK_NAME
is a required field". The lux configmap already carries SKIP_ENVS_VALIDATION for
the identical OIDC-placeholder reason; add it to the three brand configmaps
(mainnet + testnet) so they boot on their baked brand identity. Restores the
three brand explorers and lets the just-corrected chainId (pars 494949) + /v1
RPC configmap values take effect.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 10:43:53 -07:00
zeekayandHanzo Dev e53f9cd22f fix(explore): reconcile chainIds to live/canonical + migrate chain RPC to /v1 (v1.1.7)
chainId reconciliation (registry = live chains = genesis LP-018 map, one way):
- Pars mainnet 7070 -> 494949 (live api.pars.network eth_chainId=0x78d65,
  net_version=494949); Pars testnet 7071 -> 494950, devnet 7072 -> 494951
  (matches genesis configs/lp182_chain_id_map.go 4949xx scheme).
- Lux devnet 96370 -> 96367 (live api.lux-dev.network reports 96367).
- chainRegistry.spec.ts now asserts Pars 494949 (was stale-7070 assert).
- Also fixed: configmap-mainnet Pars NETWORK_ID, configmap-testnet Pars
  NETWORK_ID, branded_build.sh, tools/send_test_txs.py, well-known/explore.json
  (regenerated from the fixed registry).

RPC /ext/bc/<alias>/rpc -> /v1/bc/C/rpc (gateway canonical; each sovereign L1
serves its own C-Chain at /v1/bc/C/rpc). Verified returning blocks before switch:
Lux/Zoo/Hanzo/Pars mainnet + Lux testnet. Kept on /ext (their /v1 not up yet):
Lux devnet, brand testnets, local-node compose/test-tool URLs. P-chain proxy
now dials /v1/bc/P. useChainHeights + luxnet/instance no longer strip
/ext/bc/C/rpc; they use the env RPC URL / origin directly so C-chain height +
luxnet SDK work under /v1.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 10:06:17 -07:00
zeekayandHanzo Dev fed2f8a68b ci+docs(explore): guard KMS secrets jq parse; record v1.1.6 verification
The deploy job's DO_API_TOKEN jq could parse-error on a non-JSON KMS response
and abort under set -e before the DIGITALOCEAN_ACCESS_TOKEN fallback; swallow it.
LLM.md records the v1.1.6 chain-switcher fix + live Playwright verification and
flags wallet-connect (needs NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID secret) + the
pre-existing intermittent homepage error-boundary.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 09:21:35 -07:00
zeekayandHanzo Dev 1a64319888 fix(explore/ui): chain switcher popover opens (remove double-toggle) (v1.1.6)
ChainSwitcher put onClick={handleToggle} on the PopoverTrigger's button, which
double-toggled against PopoverTrigger's own open/close handling (net: stays
closed) — the switcher never opened, so no chains were selectable. Removed the
redundant onClick + unused handleToggle, matching the working popover pattern
(UserProfileDesktop). PopoverTrigger now drives open state via onOpenChange.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 08:47:39 -07:00
zeekayandHanzo Dev 0a7812c903 deploy+docs(explore): lux.id OIDC login config + LLM.md knowledge (v1.1.5)
explore-fe-lux gets account/OIDC via runtime env; OIDC_* lack build-time
placeholders so SKIP_ENVS_VALIDATION=true (canonical_build.sh approach) —
make_envs still writes them to window.__envs. LLM.md records deploy path,
OIDC wiring, the useProvider crash, and Pars(7070/494949)+devnet(96370/96367)
chainId flags.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 08:03:34 -07:00
zeekayandHanzo Dev df378dd0d2 ci(explore): KMS credential step must not abort under set -e
The KMS login (curl -sf | jq) had no failure guard, so under Actions' default
bash -eo pipefail a failed KMS login aborted the step before the
DIGITALOCEAN_ACCESS_TOKEN fallback ran (deploy failed instead of falling back).
Swallow curl/jq errors so the fallback path executes.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 08:02:47 -07:00
zeekayandHanzo Dev dcf0d9a048 fix(explore/web3): null-safe useProvider().data in 4 consumers (v1.1.5)
v1.1.4 changed useProvider to return null (react-query v5 forbids undefined)
but only switched the lib/web3 hooks. Four UI consumers kept
'const { data: { wallet } = {} } = useProvider()'; '= {}' defaults only
undefined, not null, so null data threw 'Cannot destructure property wallet
of {} as it is null' -> whole-page error boundary. Fixed to
'useProvider().data ?? {}' in ChainWidget, MultichainEcosystemsTableItem,
NetworkAddToWallet, AddressAddToWallet (+ wrap a pre-existing 169-char line).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 08:02:11 -07:00
zeekayandHanzo Dev 4bbdbb1a8d ci(explore): deploy job rolls out the real explore-fe-lux deployment
The deploy job restarted deployment/lux-frontend-mainnet, which does not
exist (real name: explore-fe-lux, per deploy/k8s/explore-fe + the mainnet
ingressroute), and its KMS DO_API_TOKEN fetch returns empty so every deploy
silently skipped. Fix: (1) fall back to the DIGITALOCEAN_ACCESS_TOKEN repo
secret when KMS has no token; (2) 'kubectl set image' explore-fe-lux to the
freshly built ghcr.io/luxfi/explore:sha-<short> across mainnet/testnet/devnet
and wait on the mainnet rollout.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 07:02:59 -07:00
zeekayandHanzo Dev 0ec516da30 fix(explore/manifest): federation names use '<Brand> Network', never 'Subnet'
generate-well-known.js regenerates public/.well-known/explore.json from the
chain registry at build time, so the v1.1.4 hand-edit of the output file was
overwritten by the generator (which still appended ' Subnet'). Fix the
generator — the single source of truth — so the shipped manifest actually
uses 'Network' (Lux nomenclature; no 'Subnet'). Regenerated file is in sync
(wellknown:check passes). Pars id follows the registry (7070) to keep the
manifest consistent with its source; the 7070-vs-live-494949 question is
tracked separately in LLM.md.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-14 07:00:23 -07:00
zeekay 0c95494bb7 Merge origin/main (CI runner routing to lux-build-amd64) into v1.1.4 branch 2026-07-14 06:54:44 -07:00
zeekay e3966be565 ci: run linux jobs on lux-build-amd64 ARC scale set (no GitHub-hosted builders) 2026-07-11 00:18:35 -07:00
zeekay de4a003296 ci: route linux/amd64 jobs to lux-build-amd64 ARC scale set 2026-07-10 21:55:46 -07:00
zeekayandHanzo Dev e471fb7641 explore v1.1.4: native all-chains wallet-connect, login/menu fixes, correct chain manifest
- wallet: brandFamilyChains derives sibling networks (Lux mainnet/testnet/devnet)
  from the multi-tenant chain registry so connect/add presents EVERY registered
  network for the brand, not just the env chain; dedupe by id.
- login/menu: useProvider queryFn returns null (react-query v5 forbids undefined)
  + callers switched from '= {}' destructure default to '?? {}' so a null result
  no longer crashes the wallet hooks (the busted-menu root cause).
- manifest: fix Pars chainId 7070 -> 494949 (regression) and drop the forbidden
  'Subnet' term from sovereign L1 network names (-> '<Brand> Network').
- Validators page + /api/pchain proxy present; rebuild makes the live 5-validator
  view visible on explore.lux.network.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-08 11:44:04 -07:00
zeekayandGitHub 6790f2b340 Merge pull request #8 from luxfi/fix/dex-real-subgraph
fix(dex): /dex tab on live C-Chain 0x9999 subgraph (kill pi-million mock)
2026-06-30 15:27:43 -07:00
zeekay 38babd2690 Merge remote-tracking branch 'origin/main' into fix/dex-real-subgraph
# Conflicts:
#	lib/api/dchain/useDexData.ts
#	ui/dex/DexPage.tsx
2026-06-30 15:27:12 -07:00
zeekayandHanzo Dev 6c8a170433 fix(dex): prefer subgraph-bound BASE/QUOTE symbol for market pair label
Request the indexer's bound `symbol` in the markets query and use it as the
authoritative pair label when it parses as BASE/QUOTE, falling back to live
token-metadata resolution then short addresses. One pair, one source of truth.

Co-authored-by: Hanzo Dev <[email protected]>
2026-06-30 15:25:56 -07:00
zeekay 4aab561759 ci: build on the lux-build ARC scale set (the only live luxfi linux pool; lux-build-linux-amd64 was removed, [self-hosted,linux,amd64] never matched) 2026-06-28 23:13:09 -07:00
zeekay e179668caa fix(dex): wire /dex to live cchain dex subgraph; fix block txs crash
Replace the hardcoded demo DEX dataset (incl. the placeholder pi-million 24h volume) with live markets + fills from the C-Chain 0x9999 subgraph at the per-network /v1/graph/cchain/dex/graphql endpoint, selected via config.apis.general like every other resource. Token pairs resolve through the existing token metadata endpoint, falling back to a short address. Empty/unavailable nets render 'No active markets' — never mock.

Guard tx.transaction_types in TxsListItem so coinbase/precompile txs (absent transaction_types in the API payload) no longer crash the block Transactions tab; drop an unused destructured prop. Allow CLOB in cspell.
2026-06-28 23:05:03 -07:00