e2e/env.ts reads NETWORKS.yaml at runtime, so two things on main were fatal to
it and are fixed here rather than assumed:
- NETWORKS.yaml did not parse. `lux-explorer: Lux Explorer (single Go binary:
indexer+GraphQL+SPA) — 5 chains` is a plain scalar containing ": ", which
YAML rejects — js-yaml fails with "bad indentation of a mapping entry
(219:49)". Quoted.
- js-yaml / @types/js-yaml were not declared, so the import resolved only on a
machine that happened to have them.
Verified on the rebased tree: NETWORKS.yaml parses and the suite runs green
against mainnet.
Co-authored-by: Hanzo Dev <[email protected]>
Lux Universe
Infrastructure monorepo for Lux Network. Compose files, Kubernetes manifests, gateway configs, and deployment scripts.
Lux Ecosystem
| Repo | Purpose | Stack |
|---|---|---|
| lux/node | Core blockchain node (luxd) | Go |
| lux/wallet | @luxfi/wallet (extension, mobile, web) | TypeScript |
| lux/exchange | DEX at lux.exchange (apps/web, apps/mobile) | Vite + Tamagui |
| lux/web | lux.network marketing site | Next.js 14 |
| lux/ui | @luxfi/ui component library | React |
| lux/papers | 111 research papers | LaTeX |
| lux/proofs | Formal verification (Lean 4, TLA+, Halmos, Tamarin) | Multi |
Structure
compose*.yml Docker Compose for local dev (mainnet/testnet/devnet)
k8s/ Kubernetes manifests (lux-k8s cluster)
configs/ Node and gateway configuration
chain-configs/ Per-chain EVM configuration
scripts/ Deployment and management scripts
docker/ Dockerfiles
iam/ IAM init data for lux.id
e2e/ Playwright E2E tests (~488 cases)
Networks
All networks are namespaces on the single do-sfo3-lux-k8s cluster. There
is no lux-test-k8s or lux-dev-k8s cluster — no such kube context exists.
(k8s/lux-test-k8s/ and k8s/lux-dev-k8s/ are leftovers from when there were;
they still pin ghcr.io/luxfi/node:v1.28.16 against a v1.36.23 fleet.)
| Network | Network ID | C-Chain ID | Namespace (do-sfo3-lux-k8s) | luxd |
|---|---|---|---|---|
| Mainnet | 1 | 96369 | lux-mainnet |
5/5 |
| Testnet | 2 | 96368 | lux-testnet |
5/5 |
| Devnet | 3 | 96367 | lux-devnet |
5/5 |
| Storm | — | — | lux-storm |
5/5 |
Each network runs a 5-replica luxd StatefulSet in its namespace.
app chain EVM Chain IDs
| app chain EVM | Coin | Mainnet | Testnet | Devnet |
|---|---|---|---|---|
| Zoo | ZOO | 200200 | 200201 | 200202 |
| Hanzo | AI | 36963 | 36962 | 36964 |
| SPC | SPC | 36911 | 36910 | 36912 |
| Pars | PARS | 494949 | 494950 | 494951 |
Quick Start
git clone --recursive [email protected]:luxfi/universe.git
cd universe
# Local development
docker compose -f compose.mainnet.yml up -d
# Production: there is no working aggregate apply. See the note below.
kubectl apply -k k8s/lux-k8sis not the deploy path and never worked. The kustomization does not build (it pulls raw files from sibling directories, which kustomize refuses), and if it did build it would regress live — e.g.hanzo/cloudv1.801.210 → v1.785.19. Live workloads are applied per-file by hand; they carry kubectl's last-applied-configuration and no ownerReferences. Reconcile the pins ink8s/lux-k8s/against the cluster before trying to make it applyable.
Production (lux-k8s)
LB: 134.199.138.27. All *.lux.network services.
| Namespace | Services |
|---|---|
| lux-system | hanzo-ingress (Traefik), lux-operator |
| lux-mainnet | 5 luxd validators |
| lux-testnet | 5 luxd validators |
| lux-devnet | 5 luxd validators |
| lux-gateway | Gateway API gateway |
| lux-explorer | Lux Explorer (single Go binary: indexer+GraphQL+SPA, all 5 chains), exchange, indexers, faucets |
| lux-bridge | Bridge UI + server |
| lux-mpc | MPC dashboard + API (5 nodes, threshold 3) |
| lux-kms | KMS API |
Key Images (GHCR)
| Image | Purpose |
|---|---|
| ghcr.io/luxfi/node | Blockchain node |
| ghcr.io/luxfi/explorer | Lux Explorer (single Go binary: indexer+GraphQL+SPA) |
| ghcr.io/luxfi/operator | K8s operator |
| ghcr.io/luxfi/gateway | Gateway |
RPC Endpoints
Each network has its own host and serves a flat /v1 path. There is no
/mainnet or /ext path segment — never hand-build one.
mainnet https://api.lux.network/v1/bc/{C,P,X}/rpc chainId 96369
testnet https://api.lux-test.network/v1/bc/{C,P,X}/rpc chainId 96368
devnet https://api.lux-dev.network/v1/bc/{C,P,X}/rpc chainId 96367
App chains serve the same shape from their own host: api.zoo.network,
api.hanzo.network, api.pars.network (-test / -dev suffix per network).
E2E (Playwright)
One axis, one viewport set, one place where environments are defined.
make e2e-install # npm ci + chromium
make e2e-gate # the go-live gate — minutes, all three frames
make e2e # everything, mainnet (default), all three viewports
make e2e-testnet # same specs, testnet endpoints
make e2e-devnet
make e2e-local # needs: make start network=main + ./local/up.sh
make e2e vp=mobile # one viewport
make e2e spec=e2e/01-explorer.spec.ts
make e2e-health # per-surface targets honour LUX_ENV too
make e2e-report # HTML report
make e2e-shots # open the screenshots for this env
make e2e is ~1977 cases (659 × 3 frames) and takes hours: the very large
legacy broker/exchange specs are serial within their own file, so no amount of
workers shortens them. make e2e-gate is the subset you run before a go-live —
chain health, explorer, validator lifecycle, DeFi, bridge/MPC, DEX contracts,
network independence, app chains, plus the login proof when LUX_E2E_PASSWORD is
exported.
Concurrency is per-environment and measured (WORKERS in e2e/env.ts), because
the environments do not have the same front door. Mainnet's api host is
CDN-fronted and ran the gate at 4 workers with 342/342 green; one worker and four
returned an identical result set (154 passed / 2 failed) in 5.9m vs 2.3m. Testnet
and devnet resolve straight to the cluster LB with no CDN, and at 4 workers the
same gate lost 66 cases to gateway throttling that all passed at 2 — it is
aggregate load that tips it, so the budget belongs to the environment rather than
to any one spec.
LUX_ENV=local|devnet|testnet|mainnet (default mainnet) is the only
environment knob. e2e/env.ts resolves every base URL, RPC endpoint, network ID
and chain ID from NETWORKS.yaml — the config, helpers.ts and
the specs all derive from that one module, so changing a chain ID in the registry
changes the suite. The old two-axis ENV=production|local + LUX_NETWORK=… model
is retired and LUX_NETWORK now fails loudly instead of being half-honoured.
local targets the one local stack (127.0.0.1:9630 node, :8899 IAM) and reads
the mainnet column, because make start network=main boots a mainnet-genesis
chain. Only RPC and explorers are per-network: every app surface (cloud, id,
wallet, bridge, exchange, staking, mpc, safe) is a single deployment shared by all
remote envs — test.lux.cloud and dev.lux.cloud are 404.
Viewports are projects
mobile 390x844, tablet 834x1194, laptop 1440x900 — so "mobile and desktop"
means the same three frames on every run, and every screenshot is comparable
across runs and environments.
Screenshots
~/Desktop/lux-e2e/<env>/<surface>-<viewport>.png, written on pass and fail —
a failure you cannot look at is a failure you cannot fix. Deliberate frames come
from snap(page, surface); a failing test's dying state is copied out of
Playwright's run directory by e2e/shot-reporter.ts as
fail-<spec>-<test>-<viewport>.png.
Proving sign-in
export LUX_E2E_PASSWORD='…' # never committed; LUX_E2E_USER defaults to [email protected]
make e2e-login
e2e/50-login.spec.ts drives the real flow — lux.cloud → "Continue with Lux ID"
→ lux.id → /dashboard — and refuses the cheap answers, because every cheap
answer lies here:
- HTTP 200 on
/dashboardproves nothing: it is an SPA, so an anonymous visit is also 200 and also stays on the/dashboardURL. - "the password field vanished" proves nothing: the OIDC callback parks on an
interstitial that has no form either, and
/auth/callbackwith no code renders "Sign-in failed · Missing authorization code in callback URL" at HTTP 200.
So the proof is the access token: the run ends with a JWT in sessionStorage
whose iss is the IdP and whose email is the account that was typed, and
GET lux.id/v1/iam/oauth/userinfo accepts it (that endpoint returns 401 for
no token and 401 for a junk token, so a 200 is only reachable with a token the
IdP really minted). That is the exact leg that breaks in an invalid_client
regression — the IdP still issues a code and the browser still lands on
/dashboard, but the code→token exchange 401s and no token is ever stored. The
spec fails on Sign-in failed, invalid_client, PKCE is required, a missing
authorization code, a parked interstitial, or still being on the IdP.
It also asserts the handoff is a public PKCE client (client_id=lux-cloud,
code_challenge_method=S256, redirect https://lux.cloud/auth/callback, no
secret), and it carries its own negative control: a test that proves an anonymous
/dashboard holds no token and offers no sign-out, so the positive assertions
cannot go vacuous.
License
Proprietary - Lux Partners Limited. All rights reserved.