mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
876 lines
52 KiB
Markdown
876 lines
52 KiB
Markdown
# Lux Network Explorer - AI Knowledge Base
|
||
|
||
**Last Updated**: 2024-12-24
|
||
**Project**: explore
|
||
**Organization**: luxfi
|
||
**Upstream**: [blockscout/frontend](https://github.com/blockscout/frontend)
|
||
|
||
## Project Overview
|
||
|
||
Lux Network Explorer is a fork of Blockscout Frontend, customized for the Lux blockchain ecosystem. It provides a web-based interface for exploring blocks, transactions, addresses, tokens, and smart contracts on Lux Network chains (C-Chain, Zoo chains).
|
||
|
||
## Essential Commands
|
||
|
||
**The package manager is pnpm** (`packageManager: [email protected]`, only `pnpm-lock.yaml`
|
||
exists). `yarn` in older notes below is wrong — `yarn install` would write a
|
||
second lockfile.
|
||
|
||
```bash
|
||
# Development — THE dev command
|
||
pnpm dev # tools/scripts/dev.sh, serves on $NEXT_PUBLIC_APP_PORT (3055), NOT 3000
|
||
|
||
pnpm build:next # Production build WITH assets/sprite/envs.js
|
||
pnpm start # Start production server
|
||
|
||
# Linting & Testing
|
||
pnpm vitest run # Unit suite — the only gate in .hanzo/workflows/ci.yml
|
||
pnpm lint:eslint # ESLint (repo-wide is red on main; keep YOUR files clean)
|
||
pnpm lint:tsc # TypeScript check (also red on main — see .hanzo/workflows/ci.yml)
|
||
pnpm test:pw # Playwright
|
||
```
|
||
|
||
### What `pnpm dev` actually does
|
||
|
||
`tools/scripts/dev.sh` deletes and regenerates the runtime env bundle each start:
|
||
|
||
1. `rm -rf public/assets/{configs,multichain,envs.js}`
|
||
2. `download_assets.sh` — skips every unset `NEXT_PUBLIC_*` asset URL, so it is
|
||
offline-safe with the committed local env files
|
||
3. `build_sprite.sh` — needs `jq` and `md5sum` (not macOS defaults)
|
||
4. **`make_envs_script.sh` → `public/assets/envs.js`** — this is the file the
|
||
browser reads. `pages/_document.tsx` loads it as a blocking `<script>` and
|
||
`configs/app/utils.ts:getEnvValue` reads `window.__envs` from it. Plain
|
||
`next dev` does NOT generate it, and without it every API-backed page throws
|
||
`API config for general not found`.
|
||
5. `next dev -p $NEXT_PUBLIC_APP_PORT`
|
||
|
||
Env precedence is **first `-e` wins** (dotenv-cli, `override: false`):
|
||
`.env.secrets` → `.env.development.local` → `.env.local` → `.env.development` → `.env`.
|
||
Missing files are skipped silently, which is why the absent `.env.secrets` is harmless.
|
||
⚠️ `.env.development.local` sets `NEXT_PUBLIC_APP_HOST=explore.lux.network`, so
|
||
the local app self-identifies as production for absolute links and OIDC redirects.
|
||
|
||
🪤 `.nvmrc` contains `>=22.14.0`, which is a semver range, not an nvm version —
|
||
`nvm use` fails in this directory. CI pins the real value, `22.14.0`.
|
||
|
||
🪤 **CSS comments do not nest.** A `/* … */` inside a block comment in
|
||
`nextjs/global.css` ends it early, and the remaining prose is parsed as CSS —
|
||
Turbopack then serves a full-page *Build Error* overlay instead of the app, so
|
||
`pnpm dev` "starts" but nothing renders. Fixed once; do not reintroduce.
|
||
|
||
```bash
|
||
# Docker
|
||
docker compose up # Dev environment
|
||
docker compose -f compose.prod.yml up # Production
|
||
```
|
||
|
||
## Architecture
|
||
|
||
```
|
||
explore/
|
||
├── pages/ # Next.js pages (file-based routing)
|
||
├── ui/ # UI components by feature
|
||
│ ├── address/ # Address page components
|
||
│ ├── block/ # Block page components
|
||
│ ├── tx/ # Transaction page components
|
||
│ ├── token/ # Token page components
|
||
│ ├── shared/ # Shared components
|
||
│ └── snippets/ # Header, footer, navigation
|
||
├── lib/ # Core business logic
|
||
│ ├── api/ # API client & services
|
||
│ ├── hooks/ # React hooks
|
||
│ ├── contexts/ # React contexts
|
||
│ └── web3/ # Web3 integration
|
||
├── toolkit/ # UI toolkit (Chakra-based)
|
||
│ ├── components/ # Reusable components
|
||
│ ├── theme/ # Theme configuration
|
||
│ └── chakra/ # Chakra UI customization
|
||
├── configs/ # Configuration files
|
||
│ ├── app/ # App features config
|
||
│ └── envs/ # Environment presets
|
||
├── deploy/ # Deployment tools
|
||
│ ├── scripts/ # Build scripts
|
||
│ └── tools/ # Deployment utilities
|
||
├── mocks/ # Mock data for testing
|
||
├── stubs/ # API type stubs
|
||
├── types/ # TypeScript types
|
||
└── nextjs/ # Next.js configuration
|
||
```
|
||
|
||
## Key Technologies
|
||
|
||
| Category | Technology |
|
||
|----------|------------|
|
||
| Framework | Next.js 14 (Pages Router) |
|
||
| Language | TypeScript |
|
||
| UI Library | Chakra UI v3 |
|
||
| State | React Query (TanStack) |
|
||
| Web3 | wagmi, viem, RainbowKit |
|
||
| Testing | Playwright, Vitest |
|
||
| Styling | Emotion CSS-in-JS |
|
||
| Build | Turbopack |
|
||
|
||
## Configuration
|
||
|
||
### Environment Variables
|
||
|
||
Key environment variables (see `docs/ENVS.md` for full list):
|
||
|
||
```bash
|
||
NEXT_PUBLIC_NETWORK_NAME=Lux Network
|
||
NEXT_PUBLIC_NETWORK_ID=96369
|
||
NEXT_PUBLIC_NETWORK_RPC_URL=https://api.lux.network/v1/chain/C/rpc
|
||
NEXT_PUBLIC_API_HOST=https://explore.lux.network
|
||
NEXT_PUBLIC_APP_HOST=https://explore.lux.network
|
||
```
|
||
|
||
### Lux Branding
|
||
|
||
Lux-specific branding is configured in:
|
||
- `configs/app/ui/` - UI customization
|
||
- `public/logos/` - Logo assets
|
||
- `toolkit/theme/` - Theme colors
|
||
|
||
## Development Workflow
|
||
|
||
### Syncing with Upstream
|
||
|
||
```bash
|
||
# Fetch upstream changes
|
||
git fetch upstream
|
||
|
||
# Merge upstream into main
|
||
git merge upstream/main
|
||
|
||
# Resolve conflicts, preserving Lux branding
|
||
git add . && git commit
|
||
```
|
||
|
||
### Adding New Features
|
||
|
||
1. Check if feature exists in upstream Blockscout
|
||
2. If not, implement in `ui/` or `lib/` directories
|
||
3. Add TypeScript types in `types/`
|
||
4. Add mock data in `mocks/` for testing
|
||
5. Test with Playwright
|
||
|
||
### Lux-Specific Changes
|
||
|
||
Key Lux customizations:
|
||
- `ui/snippets/networkLogo/` - Lux logo component
|
||
- `toolkit/theme/foundations/` - Lux color palette
|
||
- `public/logos/` - Lux brand assets
|
||
- `Dockerfile.lux` - Lux-specific Docker build
|
||
|
||
## Chain Configuration
|
||
|
||
| Chain | Chain ID | Explorer URL |
|
||
|-------|----------|--------------|
|
||
| Lux Mainnet (C-Chain) | 96369 | explore.lux.network |
|
||
| Lux Testnet | 96368 | testnet.explore.lux.network |
|
||
| Zoo Mainnet | 200200 | zoo.explore.lux.network |
|
||
| Zoo Testnet | 200201 | zoo-testnet.explore.lux.network |
|
||
|
||
## API Integration
|
||
|
||
The explorer connects to:
|
||
- **Blockscout API**: Block/tx/address data
|
||
- **Stats API**: Chain statistics
|
||
- **RPC Node**: Direct chain queries
|
||
|
||
API services are in `lib/api/services/`.
|
||
|
||
## Context for AI Assistants
|
||
|
||
This file (`LLM.md`) is symlinked as:
|
||
- `AGENTS.md`
|
||
- `CLAUDE.md`
|
||
- `QWEN.md`
|
||
- `GEMINI.md`
|
||
|
||
All symlinks reference this single source of truth.
|
||
|
||
## Rules for AI Assistants
|
||
|
||
1. **ALWAYS** update LLM.md with significant discoveries
|
||
2. **PRESERVE** Lux branding when merging upstream changes
|
||
3. **DO NOT** modify Blockscout-specific CI workflows
|
||
4. **USE** `pnpm` for package management (repo is `packageManager: [email protected]`; npm/yarn break the `@hanzogui` overrides)
|
||
5. **TEST** UI changes with Playwright before committing
|
||
|
||
## Recent Changes
|
||
|
||
### Duplicated tokens filter, colliding footer row, lower-case addresses (v1.1.27)
|
||
|
||
- **`ui/tokens/TokensActionBar.tsx` was the one action bar the Chakra→Tailwind
|
||
port stripped.** Upstream has two blocks on purpose — `<HStack display={{base:
|
||
'flex', lg:'none'}}>` (filter + sort + search, mobile) and a `display={{base:
|
||
'none', lg:'flex'}}` block inside the sticky `ActionBar` (filter + search,
|
||
desktop). The port turned both into bare `<div>`s, so *both* rendered at every
|
||
width (two Filter buttons, two search inputs) and, with no `flex`, their
|
||
children stacked instead of sitting in a row. Every sibling kept the pattern —
|
||
`VerifiedContracts`, `NameDomainsActionBar`, `ClustersActionBar` all use
|
||
`flex lg:hidden` / `hidden lg:flex` — so the fix restores it rather than
|
||
inventing a second way. 🪤 When a page-level control looks doubled, count the
|
||
elements before blaming CSS: two things at one spot are usually two things.
|
||
⚠️ `NameDomainsActionBar` writes its desktop block as `flex hidden lg:block` —
|
||
`block`, so its filter and input still stack at lg. Same family, not fixed here.
|
||
- **@luxfi/ui's `ButtonFrame` never sets `flexDirection`.** It is `styled(View)`
|
||
on a react-native `View`, whose default is `column`, while the frame sets
|
||
`inline-flex`, `alignItems`, `gap`, a fixed `height` and `overflow: hidden` —
|
||
all of which only make sense for a row. Any Button with **two or more children**
|
||
stacks icon over label and the fixed height clips the label off. Swept 39
|
||
visible LuxButtons over 9 pages × 2 viewports: only 3 have >1 child and only
|
||
`Filter` clipped, so this is fixed at `ui/shared/filters/FilterButton.tsx`
|
||
(the single component behind every filter in the app) with an inline
|
||
`style={{ flexDirection: 'row' }}` — inline because the frame's own
|
||
`is_View` class outranks a Tailwind `flex-row` utility. **The primitive is
|
||
still wrong**; fix `ButtonFrame` upstream and drop the call-site style.
|
||
- **`Footer`'s no-custom-links branch never wrapped its left column.** The grid is
|
||
`lg:grid-cols-[minmax(auto,470px)_1fr]` but that branch passes
|
||
`renderNetworkInfo() / renderProjectInfo() / renderRecaptcha()` as three
|
||
separate children, so auto-placement scattered them and the link row landed in
|
||
the **470px** track — where five 160px columns cannot fit. The two mobile
|
||
`grid-cols-2` tracks were also still the only *explicit* tracks at `lg`, so they
|
||
took `1fr` each, collapsed to width 0, and Contribute / X (Twitter) / Discord
|
||
painted on top of each other bottom-left. Both halves fixed: wrap the left
|
||
column in `<div className="min-w-0">` exactly as the custom-links branch already
|
||
does, and add `lg:grid-cols-none`. 🪤 A zero-width element does not register as
|
||
an overlap in a rect-intersection test — its text still paints over the
|
||
neighbour. Compare `max(width, scrollWidth)`, and treat width 0 as a defect.
|
||
- **The API case-folds address hashes, so never trust the payload's casing.** The
|
||
backend lower-cases `{hash}`/`{addr}` path params before lookup (deliberate — a
|
||
checksummed URL used to 404). `ui/pages/Address.tsx` and
|
||
`ui/multichain/address/MultichainAddress.tsx` both read
|
||
`addressQuery.data?.hash ?? getCheckedSummedAddress(hash)`, and the `??` took
|
||
the lower-case payload whenever the address existed. Both now checksum whatever
|
||
they display; `getAddress` accepts any case and is idempotent. **Display-side
|
||
only — the backend keeps folding.** The page `<title>` was always right (it is
|
||
built from the route), which is why this only ever showed in the header.
|
||
|
||
### Search adornments, ⌘K assistant, cross-L1 validator count (v1.1.23)
|
||
|
||
- **@luxfi/ui `InputGroup` never applied its measured padding.** It measures the
|
||
start/end adornments with a ResizeObserver and then discards the result: the
|
||
clone is guarded on `displayName === 'FieldInput'`, a component that does not
|
||
exist in the package, and the props it would set (`ps`/`pe`) are Chakra style
|
||
props that the Tailwind/gui `Input` spreads onto the DOM and ignores. Both are
|
||
Chakra→Tailwind port leftovers, so **every** `InputGroup` in the app was
|
||
unpadded — the header search icon sat on the placeholder's leading letter
|
||
(measured: 24px left overlap, 52px right, at 1440x900).
|
||
Fixed at the primitive via `pnpm patch` → `patches/@[email protected]`
|
||
(`pnpm install --frozen-lockfile` applies it, so CI gets it too). Publish the
|
||
same fix upstream and the patch can be dropped.
|
||
🪤 Removing the dead guard makes the *rest* of that clone live: it also blanked
|
||
`value`/`placeholder` on first render, which flips the field from controlled to
|
||
uncontrolled. Dropped, and measurement moved to a layout effect so there is no
|
||
unpadded first frame instead.
|
||
- **`ClearButton` spread `className` over its own visibility classes**, so any
|
||
caller passing one clobbered `invisible` — the header showed a clear button on
|
||
an empty field. Merged with `cn` at the component.
|
||
- **Validators counted one L1 of four.** `useCurrentValidators` asks a single
|
||
P-Chain (the one `NEXT_PUBLIC_NETWORK_RPC_URL` points at). Lux 96369, Hanzo
|
||
36963, Zoo 200200 and Pars 494949 each run their **own** P-Chain with their own
|
||
validator set — verified 2026-08-06, five each, four disjoint NodeID sets, so
|
||
20 not 5. `ChainEntry.nodeApiUrl` is the per-chain node origin;
|
||
`useNetworkValidators` fans out through the `/v1/pchain?chain=` proxy.
|
||
⚠️ **Pars DOES have a P-Chain** and answers `platform.getCurrentValidators` —
|
||
do not assume otherwise. SPC and Osage have no publicly reachable node and are
|
||
reported `unavailable`, never 0. Stake is **not** summed across chains: each
|
||
bonds its own currency.
|
||
- **⌘K assistant.** Nothing in this repo, `@luxfi/ui` or `@hanzo/gui` ships a
|
||
command palette or chat (no `cmdk`), so it is the existing Dialog + one global
|
||
listener + `pages/api/ai.ts`, which holds the credential server-side. Gated on
|
||
`NEXT_PUBLIC_AI_ASSISTANT_ENABLED`; `AI_API_KEY` is **not** a `NEXT_PUBLIC_`
|
||
var. 🪤 The gui Dialog's `size={{ lgDown: 'full', lg: 'md' }}` leaks `full`'s
|
||
`min-h-dvh rounded-none` into every width — `sizeClasses` only prefixes the
|
||
`lg` half. Use a plain size. 🪤 The gui `Input` sizes itself to 100% and
|
||
ignores flex utilities; wrap it to make the wrapper the flex item.
|
||
|
||
### 🔴 Neither CI plane builds this image — measured 2026-08-06/07
|
||
|
||
- `.github/workflows/*` is `runs-on: lux-build-amd64`, and
|
||
`gh api /orgs/luxfi/actions/runners` returns **0** — those jobs queue forever
|
||
(the runs on main sat 12–24h and were cancelled).
|
||
- `.hanzo/workflows/release.yml` names a label the fleet DOES carry, but those
|
||
runners register with **git.hanzo.ai**, and this repo is a **pull mirror**
|
||
there: `git push hanzo main` is rejected 3/3 while `git ls-remote hanzo` shows
|
||
GitHub pushes arriving within ~15s. A mirror sync raises no Actions event, so
|
||
the workflow never fires. `v1.1.23` sat tagged with no image for ~2h.
|
||
- The Gitea REST API 404s on **everything, including a repo that exists**, so it
|
||
cannot be used to check run status. Probe **GHCR** instead, always with a
|
||
negative control.
|
||
- Neither `v1.1.21` nor `v1.1.22` carries `NEXT_PUBLIC_GIT_TAG` or the OCI
|
||
labels the release workflow sets — more evidence they were built by hand.
|
||
|
||
**What works: build on our own amd64 nodes.**
|
||
🪤 A local `--platform linux/amd64` cross-build on an arm64 Mac **cannot** build
|
||
this image: colima emulates with QEMU, and `pnpm build` dies with
|
||
`QEMU internal SIGILL / uncaught target signal 4`.
|
||
🪤 buildx's `kubernetes` driver also fails here — its port-forward drops while
|
||
streaming (`load build context ... err="EOF"`), and when the client dies the
|
||
in-pod build keeps burning CPU but nothing is ever pushed.
|
||
✅ The reliable path is a **Job** running `buildctl` in-cluster, so no local
|
||
client is in the loop (`scratchpad/buildjob.yaml` shape): `moby/buildkit`
|
||
privileged, `nodeSelector: runner-pool=1tb` + the `dedicated=ci-runner`
|
||
toleration, `--opt context=https://github.com/luxfi/explore.git#<tag>` so
|
||
buildkit clones server-side, and `DOCKER_CONFIG` pointing at a mounted GHCR
|
||
secret so the push happens from inside the cluster.
|
||
🪤 **Do not read "stalled" from CPU graphs.** `kubectl top pod` reads ~3m CPU /
|
||
15Mi for a buildkit pod building flat out — it does not attribute the nested
|
||
build cgroup. `kubectl top node` is no better: the `runner-pool-1tb` nodes also
|
||
host 4 `git-runner-*` pods. The only honest check is inside the pod —
|
||
`kubectl exec <pod> -- ps -o pid,etime,time,rss,args`; a `next build` with
|
||
14 minutes of CPU TIME and 3.7 GB RSS is working, not hung. `Collecting build
|
||
traces` (`@vercel/nft`) is the long pole on this app, several minutes on its own.
|
||
|
||
📌 `hanzo-build` already runs a **`buildkitd-node` DaemonSet (7 nodes, amd64)** —
|
||
prefer wiring a build to that over creating a namespace and Job by hand as this
|
||
note's recipe does.
|
||
|
||
### Runtime ENV plumbing — three places, all required
|
||
|
||
Adding a `NEXT_PUBLIC_*` variable needs **all** of:
|
||
1. `configs/app/features/<name>.ts` (+ the `features/index.ts` export)
|
||
2. `deploy/tools/envs-validator/schemas/features/<name>.ts` (+ its `index.ts`
|
||
and a `.concat()` in `schema.ts`) — the schema is `.noUnknown(true)` and
|
||
`entrypoint.sh` validates at container start, so an undeclared variable
|
||
**stops the pod booting** (unless that deployment sets `SKIP_ENVS_VALIDATION`,
|
||
which `explore-env-lux` does)
|
||
3. `docs/ENVS.md` — `collect_envs.sh` greps it to build `.env.registry`, and
|
||
`checkPlaceholdersCongruity` **throws** for a runtime var with no placeholder
|
||
|
||
🪤 **`NEXT_PUBLIC_GIT_TAG` cannot be set from a ConfigMap.** The image bakes a
|
||
four-key `.env` (`NEXT_PUBLIC_BRAND`, `GIT_COMMIT_SHA`, `GIT_TAG`,
|
||
`ICON_SPRITE_HASH`) at build time, and `make_envs_script.sh` does `source .env`
|
||
before emitting `envs.js` — so the build-time value wins over the pod env. That
|
||
is correct: those four are properties of the image. Verified in-pod on
|
||
2026-08-06: `printenv NEXT_PUBLIC_GIT_TAG` = `v1.1.19` while `envs.js` carried
|
||
`""`. The stray key was the only drift on `explore-env-lux` (absent from
|
||
`last-applied-configuration` and from both git sources) and was **removed**,
|
||
not adopted — the version must come from the image or the footer can lie about
|
||
which build is running.
|
||
|
||
### Shell overflow + white-on-white values + broken Menu button (v1.1.12)
|
||
Three write-once-deploy-4 root causes from the 4-breakpoint UI audit. Built
|
||
on-cluster (buildkit Job in `hanzo-build` ns on do-sfo3-hanzo-k8s
|
||
runner-pool-32g, git-context clone of `luxfi/explore#main`), fanned out with
|
||
`crane` to ghcr.io/{luxfi,zooai,hanzoai,parsdao}/explore:v1.1.12 (same digest
|
||
`sha256:223b3ca2…`), rolled to all four explore-fe-* on lux-k8s/lux-mainnet.
|
||
**Verified LIVE (Playwright/Chromium) 4 brands × {390,768,1280,1920}, 16/16
|
||
pass**: `document.scrollWidth == innerWidth` everywhere, all four home values
|
||
≥4.5:1, Menu button 32px (lg+)/40px (mobile) with no clip, zero hamburgers at
|
||
lg+ (1 at <lg = the correct mobile nav trigger).
|
||
|
||
- **Horizontal overflow (454px @1280, 38px @1920).** `Container.tsx` forced
|
||
`min-w-[100vw] lg:min-w-[fit-content]`, which promoted the whole page to its
|
||
~1734px max-content width (the `MainColumn` grow shell measured 1734 at 1280)
|
||
so stat/block cards clipped past the viewport. Fix: fluid column —
|
||
`Container` → `w-full min-w-0`, and `MainColumn` gains `min-w-0` so the flex
|
||
chain shrinks and the block-row `overflow-x-auto` scrolls internally instead
|
||
of pushing the page. docSW 1734→1280 / 1958→1920 verified.
|
||
- **White-on-white values** (block Reward / tx Value / tx Fee /
|
||
network-utilization%, all computed `rgb(255,255,255)` @1.04:1). Root cause:
|
||
legacy Chakra dot-tokens (`color="text.secondary"`, `green.600`…) were passed
|
||
straight into an inline CSS `color`, which browsers drop as invalid → the text
|
||
inherited white. Fix: one `lib/utils/colorToken.ts` `resolveColorToken()`
|
||
(dot-token → `var(--color-*)`) applied in `SimpleValue` (the value chokepoint
|
||
for Reward/Value/Fee → `#737373` = 4.54:1 on the `#FAFAFA` card bg), plus new
|
||
semantic status vars `--color-status-good/warn/bad` (#1A7A47/#B45309/#B91C1C
|
||
light = 5.1/4.8/6.1:1; light shades for dark) that
|
||
`getNetworkUtilizationParams` now returns (utilization = 5.13:1).
|
||
NOTE: `@luxfi/ui` `Skeleton` sets `style.color = props.color` verbatim — pass
|
||
it a CSS value/var, never a Chakra dot-token. ~160 `color="x.y"` call sites
|
||
remain repo-wide; funnel value colors through `SimpleValue`.
|
||
- **Broken "Menu" button** (`UserWalletButton.tsx`): icon+label were stacked in
|
||
a bare `<div>` (20px icon over ~20px text) inside a 32px `overflow:hidden`
|
||
button → the "51px in 32px" clip that read as a stray hamburger at lg+. Fix:
|
||
`flex items-center gap-2` + sized icon (`w-5 h-5 shrink-0`). Lux shows "Sign
|
||
in" instead (OIDC auth) — that path was never broken.
|
||
|
||
### Org explorers un-stubbed + mobile detail pages fixed (v1.1.10)
|
||
Two mobile-QA defects across the block explorers. **DEPLOYED + VERIFIED**: image
|
||
built on-cluster via buildkit (a Job on do-sfo3-hanzo-k8s
|
||
runner-pool-32g — NO GHA; GitHub Actions is disabled for the account) from
|
||
commit 7e35a0a, fanned out with `crane` to ghcr.io/{luxfi,zooai,hanzoai,
|
||
parsdao}/explore:v1.1.10 (same digest 6f7c19d0), rolled to all four
|
||
explore-fe-* on lux-k8s/lux-mainnet. Verified 390px: docSW==390 (no clip),
|
||
labels legible, tab strip scrolls (sw 945, no overlap); desktop 1280px
|
||
unchanged; org branding intact (explore.zoo.network = Zoo, networkId 200200,
|
||
frontend 1.1.10). CI note: build-lux.yml only builds/deploys luxfi and its
|
||
deploy job is currently broken (expired DigitalOcean token → doctl 401);
|
||
org rollout is the buildkit+crane path above.
|
||
|
||
- **Defect 1 (org explorers showed goerli stub data, not their real chains).**
|
||
Root cause was purely TLS: `api-explore.{zoo,hanzo,pars}.network` served the
|
||
self-signed `CN=INGRESS DEFAULT CERT` (no cert-manager Ingress for those SNIs,
|
||
only lux had one). Real browsers reject it → every FE data fetch fails →
|
||
react-query falls back to the `stubs/` placeholders (block #8988736, "Stub
|
||
Token (goerli)") behind stuck skeletons. The FE wiring was already correct
|
||
(configmaps point at `api-explore.<org>.network` + right chainId) and the
|
||
backend already serves real per-chain data — the unified `explorer` pod has
|
||
all 6 chains (`{"chains":6}`) and the org IngressRoutes route by Host /
|
||
`blockscout-prefix-<brand>` → `/v1/indexer/<slug>` (proven: cchain 1.09M
|
||
blocks, zoo 13,381, hanzo 11, pars 63). Fix = add a dedicated cert-manager
|
||
`letsencrypt-prod` Ingress per org api host → `explorer-unified:8090`,
|
||
mirroring `api-explore-lux-network` (declared in `ingress-api-mainnet.yaml`,
|
||
in the kustomize resources). Certs issued in ~60s; all three now
|
||
`ssl_verify=0`. **If an org explorer ever goes blank/stubs again, check
|
||
`kubectl -n lux-mainnet get certificate api-explore-<org>-network-tls` first.**
|
||
The `@luxfi/ui` browser (Playwright) ignores TLS errors, so it renders real
|
||
data even when the cert is bad — verify Defect-1-class bugs with a REAL
|
||
cert-enforcing client (openssl/curl without -k), not the MCP browser.
|
||
|
||
- **Defect 2 (mobile 390px detail pages: dropped labels, horizontal clip, tab
|
||
overlap).** Three shared-UI fixes (fix all 4 explorers at once):
|
||
- *Labels invisible* was the v1.1.9 white-on-white color fix (already landed);
|
||
deploying v1.1.10 to the org images (they ran v1.0.14) is what makes it show.
|
||
- *Horizontal clip* — the real page-overflow driver at 390px was the FOOTER,
|
||
not the field rows. `ui/snippets/footer/Footer.tsx` used inline
|
||
`gridTemplateColumns: 'minmax(auto,470px) 1fr'` + `repeat(colNum,160px)` with
|
||
no mobile breakpoint → a 470px column past the 390 viewport. Made it
|
||
`grid-cols-1 lg:grid-cols-[minmax(auto,470px)_1fr]` (single column on mobile;
|
||
links `grid-cols-2` via a `--footer-cols` CSS var for the desktop count).
|
||
docSW 486→390 verified on a fresh `next dev` render.
|
||
- *Detail values* — `DetailedInfo.ItemValue` forced `whitespace-nowrap` with no
|
||
`min-w-0`, so a long address/hash couldn't shrink and its dynamic shortener
|
||
(`HashStringShortenDynamic`, which sizes off its parent width) never bounded.
|
||
Added `min-w-0 whitespace-normal break-words overflow-x-hidden lg:whitespace-nowrap
|
||
lg:overflow-x-visible` — desktop unchanged.
|
||
- *Tab bar overlap/collide* — **`@luxfi/ui` `TabsList`/`TabsTrigger` silently
|
||
DISCARD all Chakra style props** (`overflowX`, `w`, `mx`, `css` scroll-snap,
|
||
`flexShrink`, sticky…): they destructure them to `_overflowX` etc. and only
|
||
forward `className`/`style`. So the mobile scroll + `flexShrink={0}` the
|
||
consumer passed never applied → triggers shrank and collided, no scroll.
|
||
Fixed at the consumer (`AdaptiveTabsList.tsx`): express the mobile scroll as
|
||
Tailwind `className` on TabsList (`overflow-x-auto snap-x -mx-3 px-3
|
||
w-[calc(100%+24px)] lg:…`) and `className="shrink-0 snap-start"` on triggers.
|
||
Verified: tab strip becomes a scroll container (sw 373→921) with no overlap.
|
||
NOTE for future work: any Chakra-style prop on `@luxfi/ui` Tabs is a no-op —
|
||
use `className`. `DetailedInfo.Container`'s `templateColumns` prop is likewise
|
||
dropped (hardcoded grid); mobile is already single-column so it's benign.
|
||
- Dev caveat: `next dev` crashes tab pages in `AdaptiveTabsMenu` (pre-existing
|
||
dev-only `icons/dots.svg`/popover import resolving to object); prod build is
|
||
fine. Verify tab/detail rendering on the deployed prod URL, footer on dev.
|
||
|
||
### api-explore.lux.network TLS cert fix — explorer data now loads (infra)
|
||
The explorer FE was stuck in skeleton loaders ("0/5 CONNECTED", no blocks/txns)
|
||
because **every** `api-explore.lux.network` request failed in the browser with
|
||
`ERR_CERT_AUTHORITY_INVALID`. Root cause: the explorer Go backend answers by
|
||
Host header via the ingress default backend, so the API responded (200) even
|
||
with **no ingress rule** for that host — but with no TLS block for that SNI the
|
||
controller served the self-signed `CN=INGRESS DEFAULT CERT`, which browsers
|
||
reject. (`curl -k` worked; a real browser did not.) A stale
|
||
`api-explore-lux-network-tls` secret existed but nothing referenced/renewed it.
|
||
Fix: a dedicated Ingress for `api-explore.lux.network` → `explorer-unified:8090`
|
||
with a `letsencrypt-prod` TLS block (mirrors the working
|
||
`api-explore.osage.network`). cert-manager issued a real LE cert (CN=YR2) in
|
||
~60s; `curl ssl_verify_result=0`. Verified live (Playwright): explore.lux.network
|
||
now renders 1.09M blocks, 16.9k txns, live blocks + real tx hashes.
|
||
Declared in `deploy/k8s/explore-fe/ingress-api-mainnet.yaml` (in the kustomize
|
||
resources) so it survives redeploys. **If explorer data ever goes blank, check
|
||
this ingress + its cert first** (`kubectl -n lux-mainnet get certificate
|
||
api-explore-lux-network-tls`). Sibling brands (hanzo/pars/spc) have api-explore
|
||
TLS secrets too — if their explorers go blank, they likely need the same
|
||
dedicated api-explore ingress.
|
||
|
||
### Native all-chains wallet, lux.id login, menu/panel fix, real deploy path (v1.1.6)
|
||
Four owner-reported UI fixes + the deploy/runtime wiring to actually ship them.
|
||
(v1.1.4 shipped a crash; v1.1.5 fixed it; v1.1.6 fixes the chain switcher.)
|
||
Verified live on explore.lux.network (Playwright): homepage + /blocks render with
|
||
live blocks; header menus/panels open with solid backgrounds; Sign in → lux.id
|
||
login page; chain switcher lists Lux/Zoo/Hanzo/SPC/Pars/Osage.
|
||
|
||
- **Chain switcher didn't open (v1.1.6).** ChainSwitcher put onClick={handleToggle}
|
||
on the PopoverTrigger button, double-toggling against PopoverTrigger's own
|
||
handler (net: stays closed) — no chains were selectable. Removed the redundant
|
||
onClick + unused handleToggle (match UserProfileDesktop's pattern).
|
||
|
||
**Flagged, NOT fixed here (need owner input / secrets):**
|
||
- **Wallet connect is disabled**: blockchainInteraction only enables when
|
||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID is set (a real reown/WalletConnect
|
||
projectId, a vault secret per deploy/values/*.gotmpl). It's absent in the Lux
|
||
configmap → no wallet-connect UI. brandFamilyChains (the code side of fix c)
|
||
is in place; enable by adding the projectId secret. Don't use a placeholder.
|
||
- **Homepage intermittently error-boundaries** ("length" of undefined, after an
|
||
async update) — pre-existing (old build too); recovers on reload; header + all
|
||
other pages unaffected. No source maps, couldn't pin the exact widget.
|
||
- **CI deploy job**: build succeeds on ARC; the deploy job's KMS/DO-token step
|
||
still needs the DO token (fixed set -e / jq aborts + DIGITALOCEAN_ACCESS_TOKEN
|
||
fallback). Deploy is done via `kubectl set image explore-fe-lux …:sha-<short>`
|
||
(the documented method) meanwhile.
|
||
|
||
- **App-crash fix (v1.1.4 regression).** v1.1.4 changed `useProvider` to return
|
||
`null` (react-query v5 forbids `undefined`) but only switched the `lib/web3`
|
||
hooks; four UI consumers still did `const { data: { wallet } = {} } =
|
||
useProvider()`. `= {}` defaults only `undefined`, not `null`, so `null` data
|
||
crashed them → whole-page error boundary once account/OIDC put a wallet-using
|
||
popover in the header. Fixed to `useProvider().data ?? {}` in ChainWidget,
|
||
MultichainEcosystemsTableItem, NetworkAddToWallet, AddressAddToWallet.
|
||
|
||
- **Menu / floating panels (the big visible break).** The prior build threw
|
||
~7k `setReference` crashes — every popover/menu/drawer (chain switcher,
|
||
sign-in, mobile nav) rendered transparent (page bled through). Two causes: duplicate
|
||
`@hanzogui/*` React contexts (pinned to one 3.0.2 instance) and Tailwind v4
|
||
not scanning `@luxfi/ui` so the panel-surface utilities were purged. Fix:
|
||
`@hanzogui` pins + `@source "../node_modules/@luxfi/ui"` in `nextjs/global.css`
|
||
+ a `--color-popover-border` token.
|
||
- **Wallet / all-chains.** `lib/web3/chains.ts` derives `brandFamilyChains`
|
||
from `configs/app/chainRegistry` (+ matching wagmi transports); header
|
||
`ChainSwitcher` lists every registry chain on the primary C-Chain explorer.
|
||
`useProvider` returns `null` not `undefined` (react-query v5 crashed the
|
||
wallet hooks — that also broke the menu).
|
||
- **lux.id login is RUNTIME config, not code.** The account feature is gated on
|
||
env that was never in the deployed ConfigMap, so no sign-in button appeared.
|
||
Enabled on `explore-env-lux` (lux-mainnet) + `deploy/k8s/explore-fe/configmap-mainnet.yaml`:
|
||
`IS_ACCOUNT_SUPPORTED=true`, `ACCOUNT_AUTH_PROVIDER=oidc`,
|
||
`OIDC_SERVER_URL=https://lux.id`, `OIDC_CLIENT_ID=lux-explore-client-id`
|
||
(issuer live; authorize `https://lux.id/v1/iam/oauth/authorize`).
|
||
- **Deploy path (was broken).** explore.lux.network is served by Deployment
|
||
`explore-fe-lux` (ns `lux-mainnet`, `ghcr.io/luxfi/explore:sha-<short>`,
|
||
IfNotPresent). `build-lux.yml` builds on the `lux-build` ARC pool; the deploy
|
||
job now `kubectl set image explore-fe-lux` to the freshly built sha + waits on
|
||
rollout — it previously restarted a non-existent `lux-frontend-mainnet` and
|
||
its KMS `DO_API_TOKEN` fetch returned empty (deploys silently skipped). Added
|
||
a `DIGITALOCEAN_ACCESS_TOKEN` fallback; footer version now from package.json.
|
||
- **Manifest.** `scripts/generate-well-known.js` regenerates
|
||
`public/.well-known/explore.json` at build; now emits `<Brand> Network`, never
|
||
"Subnet".
|
||
|
||
**chainId reconciliation + RPC → /v1 (v1.1.7):** registry now matches the LIVE
|
||
chains AND the canonical genesis source of truth
|
||
(`~/work/lux/genesis/configs/lp182_chain_id_map.go`, the LP-018 (family,env)→
|
||
EVMChainID map). One value, one way.
|
||
- **Pars mainnet 494949** (was the stale 7070) — `api.pars.network` reports
|
||
`eth_chainId=0x78d65` / `net_version=494949`. Pars **testnet 494950**, **devnet
|
||
494951** (were 7071/7072) — matches the LP-018 map's 4949xx theme. Note the LP-018
|
||
map itself was ALSO stale on Pars mainnet (7070) and was corrected to 494949 in
|
||
the genesis repo; testnet/devnet 494950/494951 were already canonical there.
|
||
- **Lux devnet 96367** (was the stale 96370) — `api.lux-dev.network` reports
|
||
96367. Canonical Lux: mainnet 96369 / testnet 96368 / devnet 96367.
|
||
- `chainRegistry.spec.ts` now asserts Pars **494949** (was the stale-7070 assert).
|
||
- Fixed in: `configs/app/chainRegistry.ts`, `chainRegistry.spec.ts`,
|
||
`public/.well-known/explore.json`, `lib/web3/chains.ts` (comment),
|
||
`deploy/k8s/explore-fe/configmap-{mainnet,testnet}.yaml`,
|
||
`deploy/scripts/branded_build.sh`, `tools/send_test_txs.py`.
|
||
- **Chain RPC migrated `/v1/chain/<alias>/rpc` → `/v1/chain/C/rpc`** (gateway canonical;
|
||
every sovereign L1 exposes its own C-Chain at `/v1/chain/C/rpc`, so zoo/hanzo/pars
|
||
drop their old brand-aliased `/v1/bc/{zoo,hanzo,pars}/rpc` paths — which were
|
||
already 404ing). Verified returning blocks BEFORE switching: Lux/Zoo/Hanzo/Pars
|
||
**mainnet** + Lux **testnet**. Deliberately still on `/ext` (their `/v1` gateway
|
||
route is not up yet — do NOT switch until a block returns): Lux **devnet**,
|
||
Zoo/Hanzo/Pars **testnet**, and the local-node compose + `send_test_txs.py`
|
||
URLs (raw luxd serves `/ext` natively, no gateway). P-chain proxy
|
||
(`pages/api/pchain.ts`) now dials `/v1/bc/P` (verified). `useChainHeights.ts`
|
||
and `luxnet/instance.ts` no longer strip `/v1/bc/C/rpc`: they use the env RPC
|
||
URL (or its origin) directly, so the C-chain height widget + luxnet SDK work
|
||
under either path scheme.
|
||
|
||
### Chain-visibility + 10-VM/DEX handling (v1.0.14)
|
||
Two orthogonal correctness fixes shipped in one build cycle.
|
||
|
||
- **Chain-visibility rule (PR #7):** `isPrimaryNetworkExplorer()` now requires
|
||
`!isWhiteLabelMode() && vm === 'EVM'`, so unregistered/brand hosts stop
|
||
rendering the Lux primary panels + cross-L1 list. Registered
|
||
`explore.zoo.network`/`.ngo` + `explore.pars.network` as own-scoped L2s
|
||
(brand parity with Hanzo), added **Osage** L1, and set Pars chainId (interim
|
||
**7070**, since reconciled → **494949** in v1.1.7 above). Rule: **lux explorer =
|
||
ALL chains; hanzo/zoo/pars = ONLY their own.**
|
||
Proven by `configs/app/chainRegistry.spec.ts` (12 cases).
|
||
- **10-VM / DEX handling:** the Lux primary network is a family of VM-specialized
|
||
chains (C=EVM, X=UTXO, D=DEX, P=platform, + A/B/Q/T/Z/G/K/O/R/I/M), mirrored
|
||
from `~/work/lux/node/node/vms.go`. New single source of truth
|
||
`configs/app/primaryChains.ts` (`PRIMARY_VMS`, `getPrimaryVm`, `view`,
|
||
`hasBespokeView`, `chainsAwaitingBespokeView`) **decomplects** the two lists
|
||
that used to duplicate this (ChainsPage / ChainDetailPage now consume it).
|
||
Each VM routes at `/chains/<slug>`; `view` selects the render:
|
||
- **D-Chain → the DEX order-book UI** (`ui/dex/DexPage`, wired to the indexer
|
||
`./dex` adapter via `lib/api/dchain`) — `ChainDetailPage` renders `<DexPage/>`
|
||
for `view: 'dex'`, not a generic block list.
|
||
- **C-Chain** = the EVM explorer core; **P-Chain** = platform/validators data;
|
||
**X-Chain** + the custom VMs (A/B/Q/T/Z/G/K/O/R/I/M) render the generic
|
||
chain-detail fallback (info + indexer DAG stats + validators) — they do NOT
|
||
crash/blank. `chainsAwaitingBespokeView()` FLAGS the backlog: **X-Chain
|
||
(UTXO) and the 11 custom VMs still need a bespoke explorer view**; only
|
||
C/P/D have one today.
|
||
- **VMs are Lux-primary → lux-only:** the primary-VM surfaces (Chains / DEX /
|
||
Bridge / AI Compute nav + the `/chains`, `/chains/[slug]`, `/dex` pages) are
|
||
gated on `isPrimaryNetworkExplorer()`. Nav hides them on brand/white-label
|
||
hosts; `ui/shared/PrimaryNetworkGuard` backstops direct-URL access so the 10
|
||
VMs can never leak onto Hanzo/Zoo/Pars. Proven by
|
||
`configs/app/primaryChains.spec.ts` (26 cases).
|
||
- **Build/deploy:** single multi-destination kaniko Job (NO GHA), context
|
||
`git://github.com/luxfi/explore.git#main`, Dockerfile (base Next.js build),
|
||
fan-out `--destination=ghcr.io/{luxfi,zooai,hanzoai,parsdao}/explore:v1.0.14`
|
||
(parsdao ADDED — it had no v1.0.1x image). White-label is 100% runtime via the
|
||
`explore-env-{brand}` configmaps (APP_HOST), not baked per-brand. Deploy = bump
|
||
all four `explore-fe-*` images to v1.0.14 in
|
||
`luxfi/universe k8s/lux-mainnet/explore-fe.yaml`, operator reconcile.
|
||
|
||
### Realtime transport: Phoenix WebSocket → SSE (v1.0.13)
|
||
- **Bug:** the FE connected realtime via Phoenix Channels
|
||
(`wss://api-explore.lux.network/socket/v2/websocket`), but the Go backend
|
||
(`luxfi/explorer`) serves **no** Phoenix endpoint — its realtime is **SSE**
|
||
at `/v1/base/realtime` (multiplexed, envelope `{event, chain, data}`, initial
|
||
`event: CONNECT`, `: ping` keep-alive). Result: repeating `ws 404` →
|
||
"Live updates temporarily delayed". This is a frontend-only transport
|
||
mismatch; the chain/indexer are untouched.
|
||
- **Fix:** `lib/socket/sse.ts` reimplements the slice of the Phoenix
|
||
`Socket`/`Channel` API the 27 realtime consumers use, backed by ONE
|
||
`EventSource`. The seam is unchanged — `SocketProvider` / `useSocketChannel`
|
||
/ `useSocketMessage` and every consumer keep working; only the transport
|
||
underneath swapped. No more `phoenix` import in app code.
|
||
- **Routing:** envelope `{event, chain, data, topic?}`. With `topic` →
|
||
delivered straight to that channel/event (scoped `addresses:*`/`tokens:*`,
|
||
and the component-test harness). Without → the `TRANSLATIONS` table maps the
|
||
backend hub channel to a Blockscout `(topic, event)`; today the indexer
|
||
broadcasts only `blocks` → `blocks:new_block` / `new_block`. `join()` resolves
|
||
`ok` on stream open; EventSource auto-reconnects. URL normaliser collapses any
|
||
`wss://…/socket*` (or test `ws://host:port`) to `<https>/v1/base/realtime`.
|
||
- **Tests:** `lib/socket/sse.spec.ts` (12 vitest cases, all green) covers URL
|
||
normalisation, blocks→new_block translation, direct-topic routing, join `ok`,
|
||
on/off, onError, disconnect. CT harness `playwright/fixtures/socketServer.ts`
|
||
is now a tiny SSE server (same `createSocket`/`joinChannel`/`sendMessage`
|
||
signatures) so the 12 consumer specs are unchanged.
|
||
- **Still owner-gated (separate, NOT this fix):** data freshness on
|
||
explore.lux.network is blocked on the C-Chain rollback + indexer reorg
|
||
handling — no fresh blocks flow until that lands. When it does, align the
|
||
EVMBlock→Blockscout `Block` field shapes in the `blocks` translation (or have
|
||
the backend emit Blockscout-shaped blocks); that is the only remaining piece
|
||
and it can only be verified end-to-end once data flows again.
|
||
|
||
### Investor-grade data: real validators + honest-empty stats/DEX (v1.0.11)
|
||
Removed every fabricated placeholder; data is now real-from-chain or honestly
|
||
empty, never faked. Root causes + fixes:
|
||
- **Validators/Stake = 0 was a proxy bug, not missing data.** P-chain
|
||
`platform.getCurrentValidators` returns real validators (5, total weight
|
||
2.5e18 on hanzo). `pages/api/pchain.ts` derived its base URL by stripping
|
||
ONLY `/v1/chain/C/rpc` via regex; brand RPCs are `…/v1/chain/<chain>/rpc`
|
||
(hanzo: `/v1/chain/hanzo/rpc`), so the regex no-op'd and the proxy POSTed to
|
||
`…/v1/bc/hanzo/rpc/v1/bc/P` → HTML 404 → "Unexpected non-whitespace
|
||
character after JSON at position 4" → 502. Fix: `new URL(rpcUrl).origin`
|
||
(works for every brand) + defensive text→JSON parse with a clear error.
|
||
`ui/stats/lux/NetworkStats.tsx` (Network Overview: Total Chains / Validators
|
||
/ Connected / Total Stake / Avg Uptime) already had honest `—` fallbacks and
|
||
now renders REAL values. `lib/api/luxnet/instance.ts` was already safe (uses
|
||
`new URL().hostname`), so B/D-Chain SDK calls were unaffected.
|
||
- **"Placeholder Counter 9.074M ×8"** came from `stubs/stats.ts` `STATS_COUNTER`
|
||
(`value:'9074405', title:'Placeholder Counter'`) used as React-Query
|
||
`placeholderData` in `ui/stats/NumberWidgetsList.tsx`. When the Blockscout
|
||
**stats microservice** is absent (hanzo has NO `NEXT_PUBLIC_STATS_API_HOST`;
|
||
every `api-explore.hanzo.network/api/v1|v2/*` 404s) the stub leaked as "real".
|
||
Fix: neutralize the stub (`title:'', value:'0'`) AND gate the counters/charts
|
||
in `ui/pages/Stats.tsx` on `config.features.stats.isEnabled` — show the live
|
||
chain-sourced Network Overview always, with an honest "being indexed" note
|
||
when no stats service. Aggregate counters (total txns/addresses, gas-used-
|
||
today, history charts) genuinely need a stats indexer that isn't deployed.
|
||
- **DEX showed static demo numbers.** `lib/api/dchain/useDexData.ts` had
|
||
hardcoded `DEMO_SYMBOLS/ORDERS/TRADES/POOLS/OVERVIEW` (LUX/USDT 24.85, etc.)
|
||
served when the D-Chain (DexVM) is unavailable. Deleted ALL demo data → real
|
||
D-Chain data or honest-empty; `ui/dex/DexPage.tsx` renders an honest empty/
|
||
error message per tab.
|
||
- **Bridge signers = 0 is already honest** (`lib/api/bchain/useBridgeData.ts`
|
||
returns real B-Chain signer set or `EMPTY_STATS`; B-Chain not yet reporting).
|
||
- **Homepage is already investor-grade**: real RPC blocks (#8–#13), tx hashes,
|
||
gas (2.5 Gwei), Total blocks/txns/wallets — `ui/home/Stats.tsx` degrades to
|
||
RPC when the general indexer errors; `HOMEPAGE_STATS` stub is placeholder-only.
|
||
- **Blockchain dropdown clipped its last item** ("Verified Contracts"):
|
||
`@luxfi/ui` `Tooltip` `variant="popover"` Content defaults to
|
||
`overflow-hidden px-3 py-2`, clipping a tall menu `<ul>`. Fixed at the
|
||
consumer (`ui/snippets/navigation/horizontal/NavLinkGroup.tsx`) via
|
||
`contentProps={{ className:'overflow-visible px-1.5 py-2' }}` (twMerge wins)
|
||
+ `placement:'bottom-start'`. No node_modules patch.
|
||
- Still needs a real indexer (honest-empty until then): aggregate Stats
|
||
counters + history charts (Blockscout stats microservice), DEX live
|
||
markets/orders/trades (DexVM indexer), Bridge signer set (B-Chain).
|
||
- Built v1.0.11 via the kaniko Job pattern (NO GHA) → `ghcr.io/{hanzoai,zooai,
|
||
luxfi}/explore:v1.0.11`; deploy = image patch of `explore-fe-*` Deployments
|
||
on do-sfo3-lux-k8s ns `lux-mainnet` (plain Deployments, not operator-managed).
|
||
|
||
### Validators page: visible in light mode + brand currency (v1.0.12)
|
||
v1.0.11 fetched REAL P-chain validators (5, total weight 2.5e18 on hanzo) but
|
||
a Playwright pass found the `/validators` view rendered **white-on-white in
|
||
light mode** — the data was present in the DOM yet invisible to users (only the
|
||
tab bar showed because it alone used `text-[var(--color-text-primary)]`). The
|
||
three custom Lux components (`ui/validators/lux/{ValidatorsDashboard,
|
||
ValidatorsList,DelegatorsList}.tsx`) set NO text color, so they inherited a
|
||
white color that doesn't flip with the theme. Fixes:
|
||
- Set `text-[var(--color-text-primary)]` on each component's root container —
|
||
one place per component; the token flips per `styles/tokens.css` (light
|
||
`rgba(16,17,18,.80)` / dark `rgba(255,255,255,.80)`), so all descendants now
|
||
render the mode-correct color (dark on light, light on dark).
|
||
- Stake unit was hardcoded `LUX` on every brand; now `config.chain.currency.
|
||
symbol || 'LUX'` (= `AI` on Hanzo) — matches `NetworkStats.tsx`'s pattern,
|
||
DRY across all 6 stake labels in the three components.
|
||
- NOT changed: `formatStake` uses `LUX_DECIMALS = 6`, the codebase-wide
|
||
convention (NetworkStats + ChainDetailPage use the same); P-chain
|
||
`getMinStake` returns `minValidatorStake:2000000000` (= 2000 at 6dp),
|
||
consistent with 6. (Three copies of this helper is a latent DRY violation to
|
||
consolidate later, but the value is correct — left as-is to avoid drift.)
|
||
- `next.config.js` has `typescript.ignoreBuildErrors:true`; repo carries ~224
|
||
pre-existing tsc errors (all in `*.pw.tsx` / unrelated) — build is unaffected.
|
||
Dockerfile uses **pnpm** (not yarn). Edited files lint+typecheck clean.
|
||
- Built v1.0.12 via the kaniko Job (NO GHA) → `ghcr.io/{hanzoai,zooai,luxfi}/
|
||
explore:v1.0.12`; deployed by image patch of `explore-fe-{hanzo,lux,zoo}` on
|
||
do-sfo3-lux-k8s ns `lux-mainnet`. (Built on do-sfo3-hanzo-k8s ns `hanzo`.)
|
||
|
||
### Multi-brand hostname-driven header logo (explore v1.0.10)
|
||
- ONE image white-labels by request Host: `configs/app/chainRegistry.ts`
|
||
`getCurrentChain()` resolves the chain (and its brand) from the hostname,
|
||
and the header renders `chain.branding.logoContent` (an inline-SVG mark
|
||
using `currentColor`). NO hardcoded logo, no image-URL→triangle fallback.
|
||
- Per-brand marks live in `chainRegistry.ts`: Lux = downward triangle
|
||
(viewBox `0 0 100 100`), Hanzo = real blocky-H (`@hanzo/logo`, viewBox
|
||
`0 0 67 67`, 5 paths — `M22.21 67…`), Zoo = interlocking circles
|
||
(`0 0 1024 1024`), Pars = 8-pointed star, SPC = unicorn.
|
||
- The header logo renders in THREE spots, all reading `branding`:
|
||
`ui/snippets/topBar/TopBar.tsx` (desktop), and
|
||
`ui/snippets/networkLogo/{NetworkLogo,NetworkIcon}.tsx` (mobile header
|
||
`ui/snippets/header/HeaderMobile.tsx` + chain-switcher). The stale
|
||
pre-v1.0.10 mobile `NetworkIcon` hardcoded a `viewBox="0 0 50 50"`
|
||
`<polygon>` triangle mislabeled `aria-label="Hanzo icon"` — fixed in
|
||
4369780 (mobile) + 79dbe63 (desktop, single header per breakpoint).
|
||
- Multi-tenant L1/L2/L3: each `ChainEntry` has a `vm` field
|
||
(`EVM` = Lux primary L1, `L2` = brand sovereign chains) gating
|
||
`isPrimaryNetworkExplorer()` so brand explorers show ONLY their own chain.
|
||
mainnet/testnet/devnet/localnet entries per brand; hostnames cover both
|
||
canonical (`explore-<brand>.lux.network`) and brand-domain
|
||
(`explore.<brand>.network`, `explore.hanzo.ai`) hosts.
|
||
|
||
### Build + deploy (NO GitHub builders)
|
||
- Built on-cluster via kaniko (do-sfo3-hanzo-k8s ns `hanzo`, node pool
|
||
`runner-pool-32g`, secret `kaniko-ghcr-multi`, toleration
|
||
`dedicated=ci-runner`): context `git://github.com/luxfi/explore.git#main`,
|
||
`--dockerfile Dockerfile`, fan-out `--destination` to per-brand GHCR orgs
|
||
`ghcr.io/{luxfi,zooai,hanzoai}/explore:v1.0.10` (one digest,
|
||
`sha256:4f980291…`). pars (`ghcr.io/parsdao/explore`) not in the fan-out
|
||
yet → stays v1.0.4.
|
||
- Runs on do-sfo3-lux-k8s ns `lux-mainnet` as `explore-fe-<brand>`
|
||
Deployments (selector `{app:explore-fe,brand,network}`), env from
|
||
`explore-env-<brand>` ConfigMap, pull secret `ghcr-luxfi`, svc 80→3000.
|
||
NOT operator-managed (plain Deployments) → bump via image patch / apply.
|
||
Declared state: `luxfi/universe k8s/lux-mainnet/explore-fe.yaml`
|
||
(+ `k8s/lux-devnet/explore-fe.yaml`).
|
||
- App hydrates client-side (SSR HTML is an 8 KB shell) — verify the logo
|
||
with a real browser (Playwright), not `curl | grep`.
|
||
|
||
### Auth — Hanzo IAM is the only gate (do not add a second)
|
||
|
||
- Sign-in is OIDC against Hanzo IAM. **Never build local password/OTP/SIWE
|
||
auth, and never re-point Blockscout's `/account/*` auth at our backend —
|
||
rewriting it *is* building custom auth.** The auth0 (email + one-time-code +
|
||
reCAPTCHA), Dynamic.xyz and CSRF paths were deleted, not ported: every
|
||
`/account/*` endpoint 404s on `api-explore.*` (measured), so none of it could
|
||
ever have worked here.
|
||
- `lib/oidc.ts` is the ONE place that knows IAM's endpoints. Never rebuild a
|
||
path at a call site. IAM publishes discovery at
|
||
`<issuer>/.well-known/openid-configuration`; everything lives under
|
||
`/v1/iam/oauth/*` (`authorize`, `token`, `userinfo`, `logout`). The bare
|
||
`/oauth/*` and `/api/userinfo` paths are the IdP's SPA shell or a 404 and
|
||
fail *silently*.
|
||
- **PKCE is mandatory.** IAM rejects a public client without it:
|
||
`error=invalid_request, PKCE is required for public clients`. Our clients are
|
||
public (a browser holds no secret), so the S256 challenge is not optional
|
||
polish — it is the thing that makes login work at all.
|
||
- Client ids are `<org>-<app>` (HIP-0111): `lux-explore`, `zoo-explore`,
|
||
`hanzo-explore`, `pars-explore`. The tree shipped
|
||
`NEXT_PUBLIC_OIDC_CLIENT_ID=lux-explore-client-id`, a client that never
|
||
existed → `400 unknown client_id`. Issuers are `lux.id`, **`zoolabs.id`**
|
||
(NOT `zoo.id` — it does not resolve), `hanzo.id`, `pars.id`.
|
||
- IAM state is changed ONLY by `make iam-provision` from that org's
|
||
`universe/infra/k8s/iam/provision.yaml`. 🪤 The upsert **REPLACES**
|
||
`redirectUris` — it does not merge. Any live URI not derivable from `hosts:`
|
||
must be stated under `redirects:` or the next converge deletes it.
|
||
|
||
### UI stack — the migration is already done; Tailwind is NOT removable
|
||
|
||
- Measured: `@chakra-ui` 0 files, `@emotion` 0, `styled-system` 0, `@radix-ui`
|
||
**0 files in our code**. `@luxfi/ui` is imported by ~787 files.
|
||
- The ~17 `@radix-ui/*` entries in `package.json` have zero call sites — they
|
||
duplicate `@luxfi/ui`'s own 13 Radix dependencies. Dead declarations, safe to
|
||
drop, but they are our library's internals and Radix stays in the tree.
|
||
- **Tailwind v4 is structural, not legacy cruft.** `nextjs/global.css` has
|
||
`@source "../node_modules/@luxfi/ui"` precisely so Tailwind emits the
|
||
utilities `@luxfi/ui` itself uses; purge them and the library breaks. There is
|
||
no `tailwind.config` — v4 is configured in CSS (`@theme`, `@custom-variant`).
|
||
- Real remaining debt: ~137 files still pass Chakra-shaped style props
|
||
(`fontSize="sm"`, `w`, `borderRadius`) that `@luxfi/ui` absorbs via an
|
||
`any`-typed compat shim. If that shim goes, those files break.
|
||
|
||
### Deployment traps
|
||
|
||
- The four `ghcr.io/{luxfi,hanzoai,zooai,parsdao}/explore` tags are **byte-
|
||
identical copies** (same digest) — brand comes entirely from the
|
||
`explore-env-<brand>` ConfigMap at runtime. A version bump is therefore
|
||
`crane copy`, not four builds. Never mix org registries.
|
||
- 🪤 `tsconfig.json` has `incremental: true`. A stale `tsconfig.tsbuildinfo`
|
||
reports errors against file contents that no longer exist. Delete it before
|
||
trusting a typecheck diff.
|
||
- 🪤 `.cspell-words.txt` is in cspell's **ignorePaths**, not a dictionary. New
|
||
words go in the `words` array in `cspell.jsonc` or the pre-commit hook fails.
|
||
|
||
## The primary-network chains: what is real, and what only looks it
|
||
|
||
Measured 2026-08-08 against all three networks. These are facts about the
|
||
network, not about this app, so check them before believing any chain page.
|
||
|
||
- **The chain list is `platform.getBlockchains`, never a table in this repo.**
|
||
mainnet and testnet report nine chains (A B C D G K Q X Z); devnet reports
|
||
ten, adding **M**. `configs/app/primaryChains.ts` additionally names **T, R,
|
||
I and O, which are registered on NO network** — the node answers "there is no
|
||
ID with alias: T". That table is presentation metadata; the network is the
|
||
source of truth. `/chains` and `/chains/<slug>` join the two.
|
||
- **The public gateway routes exactly three chains: `/v1/chain/P`, `/v1/chain/X` and
|
||
`/v1/chain/C/rpc`.** Everything else 404s — including C-Chain's own blockchain
|
||
ID, `/v1/bc/25td8att…/rpc`, which proves the 404 is the route and not the
|
||
chain. D, A, B, Z, G, K are registered AND bootstrapped on the node; a
|
||
browser simply cannot reach them. Render that as unreachable, never as a
|
||
height of 0.
|
||
- **Q-Chain is `isBootstrapped: false` on all three networks** and is the sole
|
||
entry in `/v1/health`'s not-bootstrapped list — which is why all three
|
||
networks report `healthy: false`. Q's page says Syncing; that is correct.
|
||
- **P and X both report height 0** on all three. That is the RPC's real answer
|
||
(genesis-era P timestamps confirm it), not a failure.
|
||
- **X speaks `xvm.*`, not `avm.*`.** `avm` returns "can't find service".
|
||
`xvm.getBlockchainID` and `xvm.getTxFee` are not in this build either.
|
||
- C-Chain chain ids: mainnet **96369**, testnet **96368**, devnet **96367**.
|
||
- 🪤 `api-indexer-<x>chain.lux.network` — all fifteen were NXDOMAIN. Deleted.
|
||
Do not reintroduce a per-chain indexer host map without probing DNS first.
|
||
|
||
### Reaching the node
|
||
`/v1/node/<endpoint>` (`pages/api/node/[endpoint].ts`) is the ONE proxy. The
|
||
browser cannot call the node directly — the gateway 404s the OPTIONS preflight
|
||
for `/v1/chain/*`. The endpoint is resolved against the primary-chain table and
|
||
never used as a URL. A chain the gateway does not route answers 200 with a
|
||
JSON-RPC error, not 502: nothing is broken, and 502 painted a failed request
|
||
into the console of eight of the ten chain pages.
|
||
|
||
### 🪤 Deployment does not follow the tag
|
||
`.hanzo/workflows/release.yml` builds the image and copies it to each brand
|
||
registry, then **prints** "Pin all five brands to <tag> in luxfi/universe
|
||
deploy/lux-mainnet/". Nothing performs that pin. On 2026-08-08 GHCR held
|
||
v1.1.28, v1.1.29 and v1.1.26 while `universe` still declared **v1.1.26** — so
|
||
v1.1.27 (never built) and v1.1.28/29 (built, undeployed) had all been sitting.
|
||
Moving the pin is a hand commit in `luxfi/universe`; the history does exactly
|
||
that ("move all five brands to v1.1.26").
|
||
🪤 Only `deploy/lux-mainnet/explore-fe-*.yaml` drives CD (`cd.automated: true`,
|
||
"CD applies this file within ~90s"). The **testnet and devnet files are
|
||
snapshots generated from the live Deployment** by `hack/live2values.py` and say
|
||
so in their header — editing them deploys nothing. Those two clusters were on
|
||
`v1.0.4` / `sha-caf792d` while mainnet ran v1.1.26.
|
||
|
||
### 2024-12-24
|
||
- Merged upstream blockscout/frontend (up to commit 5a49ad8b1)
|
||
- Updated branding from Blockscout to Lux Network
|
||
- Added logo assets
|
||
|
||
### Cleanup
|
||
- **Removed** 8 stale `*.orig`/`*.bak` backups (~1.9 MB) left by the Blockscout→Lux
|
||
rebrand commits; two still carried raw `<<<<<<< HEAD` conflict markers. Nothing
|
||
referenced them. `*.orig`/`*.rej`/`*.bak` are now in `.gitignore` (upstream's had
|
||
`*.orig`; the fork dropped it, which is how they got committed).
|
||
|
||
---
|
||
|
||
**Note**: This file is tracked in git. Update it when making significant architectural changes or discoveries about the codebase.
|