- brands/{lux,zoo,hanzo}.svg: real marks (violet Lux prism, colorful Zoo venn,
Hanzo block-H). The prior rect fill="currentColor" + black glyph collapsed to a
solid black square in the light-theme header (currentColor is the dark text color).
- gui-stub tokenize(): numeric Tamagui space/size tokens ($3/$6/$10) now resolve to
px on a 4px scale. They were mapped to undefined CSS vars (var(--3)), so every
gap/padding/fontSize was silently dropped — the source of the gap-less "button
blob" (colliding radii) and the top-anchored auth card.
- Welcome: plain inline-styled, viewport-centered card with a max-width panel, the
brand mark, and 44px CTAs separated by real gaps (matches AppShell/Callback pattern,
independent of the unresolved Tamagui token contract).
- overlays/{lux,zoo,hanzo}/brand.json: drop sibling first-party L1s from the chain
selector. Zoo no longer lists "Lux C-Chain"/"Hanzo"; each brand keeps its own
networks + neutral public EVMs (Ethereum/Arbitrum/Base/Polygon/Avalanche).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The SPA ships no font files and never set a font-family, so every element fell
back to the browser's serif default (Times) — the landing page read as
"unstyled" even though the brand theme (accent/surface/neutral CSS vars) was
being injected correctly by loadBrandConfig. Set a system sans-serif stack on
html/body (zero bundled assets, crisp on every platform) and force form controls
(button/select/input) to inherit it, since they don't by default.
Verified headless (Chrome channel, 390px): body font-family was "Times" before;
this makes it the system sans stack. Pairs with the prior overflow fix.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
At 390px the top bar was a single non-wrapping flex row whose min-content
width (~470px) forced the single-column app grid — and thus the whole
document — to 478px, pushing the account/connect control off-screen and
clipping the CTA card. Two changes:
- index.html: add the app's one document-level reset (box-sizing:border-box
+ zero html/body margin). The missing UA body margin reset was adding the
extra 8px (478 vs 470). No overflow-x:hidden — it would break the sticky
header.
- AppShell.tsx: let the header wrap (flex-wrap) so the chain+account group
drops to a second row instead of widening the document; grid header row
becomes minmax(56px,auto) so the wrapped row isn't clipped; groups get
min-width:0 so they can shrink.
Verified headless at 390x844: document.scrollWidth 478 -> 390, zero
elements past the viewport, connect control + CTA fully on-screen.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Pin the image tag the way a custom CR requires: the lux.cloud/v1 Service CR
holds the tag in structured spec.image.tag, which the kustomize images:
transformer does NOT reach — so it is pinned via a JSON patch (was silently
staying at :latest). Set to the built sha dc08a6af0.
Add fallback.yaml: equivalent plain manifests (Deployment + Service + Ingress +
cert-manager Certificate via ingress-shim / letsencrypt-prod DNS-01). lux-k8s
runs lux-operator scaled to 0, so services.lux.cloud does not reconcile; these
reproduce exactly what it would generate, letting wallet.lux.network go live
without scaling the operator (which would touch other lux-mainnet CRs/luxd).
Follows the proven safe.lux.network ingress pattern (class ingress, DNS-01).
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The apps/web Dockerfile selectively COPYs workspace sources but omitted the
root tsconfig.base.json that pkgs/{wallet,brand,analytics,chains} each `extends`.
Vite 8's rolldown/oxc TS transform resolves the extends chain at build time and
hard-fails ("Tsconfig not found /app/tsconfig.base.json", surfaced only as the
opaque `errors: [Getter/Setter]`) when the base is absent. Local vite build
masks it (runs against the full repo tree where the base exists); every
containerized build failed regardless of OS/arch. Reproduced in node:22-alpine
and verified fixed — dist/ now produced.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The auth screens (ImportMnemonic/SetPIN/Unlock/ConfirmMnemonic) speak the
Tamagui/RN text-input contract (onChangeText, secureTextEntry, multiline,
onSubmitEditing) - the same contract @luxfi/ui@7.4.0's bridged Input honors.
The local @hanzo/gui stub spread these raw onto a native <input>, so a
value + onChangeText field was controlled with no working handler and
silently dropped every keystroke (dead seed-phrase / PIN entry). Translate
the contract to native web events in the stub.
Verified 390x844: import-phrase textarea + PIN + unlock accept keystrokes,
create/import persists, unlock reaches /portfolio, quick-actions soft-nav
without session drop.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
The wallet keeps `isUnlocked` + the plaintext mnemonic in memory ONLY (never
persisted — see the auth store's partialize), so ANY full-document reload wipes
the session and bounces the user back to onboarding. On mobile this made the
wallet unusable: every full-reload dashboard action effectively logged you out.
- Portfolio dashboard: render the Send / Cross-Chain / Earn / Manage Keys
quick-action cards as react-router <Link> (client-side soft-nav) instead of
full-reload <a href>. A tap pushState-navigates and the in-memory unlock +
mnemonic survive.
- Audit for other session-dropping full-reload links: Callback's "Back to
wallet" recovery link was a raw <a href="/"> — converted to <Link to="/">.
Verified at 390x844 (iPhone-class): the 4 cards render with no horizontal
overflow (documentElement.scrollWidth == 390); tapping Send soft-navigates to
/send with NO document reload (a page sentinel + the zustand auth store both
survive, isUnlocked stays true); a full-reload navigation (the prior behavior)
wipes the in-memory session — confirming the fix.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Stripe is no longer an available fiat on-ramp provider. Drop it from the
buy-link provider test fixtures; moonpay+coinbasepay still exercise the
multi-provider parse and case-normalization paths.
Chain metadata is now sourced from the canonical @luxwallet/chains registry
(MIT) instead of three duplicated hardcoded maps:
- new lib/chains.ts adapter: chainLabel / evmChainDef / evmChainIds /
registryChains, with a UI-only block-explorer overlay.
- config/wagmi.ts, components/ChainSwitcher.tsx, lib/brand.ts all read it.
- the wallet now recognises/holds every Lux Bridge chain: Lux C/X/P/Q/Z,
sovereign L1s (Hanzo/Zoo/Pars/Sparkle Pony), external EVM (Ethereum,
Arbitrum, Base, Polygon, Optimism, Avalanche) and non-EVM bridge families
(Bitcoin, Solana, TON, XRP, Polkadot). brand.supportedChainIds still
narrows the active wagmi/switcher surface per white-label.
IAM account login is delegated to the canonical @hanzo/iam SDK (MIT, HIP-0111
one-way auth) — lib/iam.ts is now a thin brand-aware facade over
@hanzo/iam/browser, keeping its existing surface (beginLogin/completeLogin/
getAccessToken/hasSession/clearSession) so store/session, lib/custody and the
callback route are unchanged. Adds a 'Sign in with <brand>' button to the
Welcome screen (additive, alongside local create/import).
The wallet's own keyring + signing path is untouched: this is the chain
metadata source + IAM account login, not external-wallet-connect.
Deps (file: for now, TODO npm): @luxwallet/chains, @hanzo/iam — both MIT,
zero new GPL.
The not-mock counterpart to internal/api/e2e_test.go (which wires the real
custody adapter to a MOCK lux/mpc). This drives the wallet backend's /v1 API
against a REAL 3-node mpcd ensemble (consensus mode, the production code path)
over the live HTTP /keygen + /sign endpoints.
Path (a) full-OIDC: a local RSA-signed OIDC issuer + the real iam.Verifier
(standard discovery + JWKS + RS256 verify) + real org-scoping (owner claim) +
the real api.Server /v1 handlers + custody.NewMPC pointed at live node0. Proves
the full chain: lux.id verify → org scope → LIVE MPC threshold keygen + sign,
with no plaintext key in any backend response or MPC node log.
Exercises POST /v1/wallets (live keygen → real evm address), GET /v1/wallets,
POST /v1/wallets/{id}/sign (live threshold signature), backend-level idempotency
(same key → same signature), and the missing-idempotencyKey → 400 boundary.
//go:build integration. Builds mpcd from the lux/mpc repo (LUX_MPC_REPO, default
/Users/z/work/lux/mpc); skips cleanly if that repo is absent. Reuses the OIDC
helpers (newIDP/tokenForOrg/do) from api_test.go; stdlib-only ids (no new dep).
Run: go test -tags integration ./internal/api/ -run TestBackendLiveMPC -v
Add apps/backend — the wallet's custody server. The load-bearing Front-E gap
(repo was FE-only) is now closed: a wallet account model backed by MPC custody
where the private key is split t-of-n across the MPC nodes (lux →
mpc.lux.network) and NEVER assembled — this service stores only public keys +
addresses.
- internal/iam: lux.id OIDC bearer verification via JWKS (discovery + RS256/
ES256, alg-confusion-safe — HMAC/none refused). Org from the 'owner' claim.
golang-jwt/v5 (matches the lux ecosystem), stdlib JWKS fetcher (no extra dep).
- internal/custody: the Custodian port (a value the wallet owns) + HTTP adapter
to lux/mpc's verified /keygen + /sign wire. Mirrors mpc client.Threshold's
security contract (idempotency required, org-scoped). PQ-ready: mldsa65 is a
first-class scheme (the PQ primitives stay in pkgs/wallet/.../pq + consensus —
not duplicated). No in-process key fallback — unconfigured MPC is an error.
- internal/store: wallet↔org↔addresses (no key field). Org-scoped reads — a
cross-org get is ErrNotFound (no existence disclosure).
- internal/api: /v1/* (no /api/ prefix). IAM-gated, org-scoped. POST/GET
/v1/wallets, GET /v1/wallets/{id}, POST /v1/wallets/{id}/sign, GET /v1/health.
- cmd/wallet-backend: env config (white-label per tenant: IAM_ISSUER/AUDIENCE,
MPC_ENDPOINT — shared hanzo-mpc or BYOMPC), JSON logs, graceful shutdown.
Deploy (native CI + operator, NO GHA): build-only .platform.yml → ghcr.io/
luxfi/wallet-backend on lux-build pools; lux operator rolls
apps/backend/k8s/wallet-backend.yaml (lux.cloud/v1 Service + KMSSecret for
MPC_SERVICE_TOKEN), ingress wallet-api.lux.network.
17 tests green (go test ./...): IAM (valid/no-org/wrong-aud/expired/alg-
confusion), custody (no-key-leak, idempotency-required, PQ-accepted, no-
fallback), API (auth-gate, org-isolation cross-org→404, create→sign), + a full
E2E through the real adapter→mock-MPC→real-server. Binary boots, /v1/health ok,
/v1/wallets 401 unauth. LLM.md captures the App(Wallet) spec for the unified
operator's App Kind.
Replace every @uniswap/* package import with @luxamm/* (lux fork
already canonical for SDK pieces). Replace app.uniswap.org URLs with
lux.exchange, uniswap.org with lux.network. Replace brand strings
(Uniswap Wallet → Lux Wallet, Uniswap Extension → Lux Extension,
Uniswap Labs → Lux Labs Inc.) where they appear in user-facing UI.
Rename handleUniswapAppDeepLink → handleLuxAppDeepLink, also rename
focusOrCreateUniswapInterfaceTab → focusOrCreateLuxInterfaceTab.
Manifest host_permissions updated to lux.exchange and *.lux.network.
Lockfile regeneration: TODO — run pnpm install to refresh after this
commit. Transitive @uniswap/* references in pnpm-lock.yaml are
upstream-package internals.
Protocol-level identifiers (UniswapMethodHandler, UniswapMethods,
UniswapOpenSidebarRequest, handleUniswapX, UniswapXOrderDetails) are
preserved because they reference wire-protocol method names
(uniswap_openSidebar) and external SDK type exports — renaming would
break upstream contracts. iOS Uniswap target directory and
uniswapteam.slack.com / github.com/Uniswap/universe doc refs are
out-of-scope per directive (separate native-build cleanup).
Same hard-banishment rule as tamagui (per feedback memory).
wxt.config.ts referenced both but neither was imported/defined.
- Import getTsconfigAliases from ./config/getTsconfigAliases
- Define publicAssetsVariant from WXT_PUBLIC_ASSETS_VARIANT env (default: prod)
Without these, wxt build fails: getTsconfigAliases is not defined.
wxt.config.ts imports these but they were never declared in package.json
(probably stripped along with tamagui cleanup or a prior cleanup pass).
Without them, `pnpm exec wxt build` fails: Cannot find module 'vite-plugin-svgr'.
Tamagui is banned per directive. Replace every import/reference with
@hanzo/gui or the @hanzogui/* equivalent. Removes:
- tamagui-loader and tamaguiPlugin from apps/extension webpack/wxt configs
- @tamagui/babel-plugin TODO block from apps/mobile/babel.config.js
- 'tamagui', '@tamagui/web' from optimizeDeps.include hints
- @tamagui/core/reset.css side-effect imports → @hanzogui/core/reset.css
- TamaguiProvider in mobile tests → GuiProvider from gui-provider
- TamaguiInput type aliases in extension Input.tsx → GuiInput
- declare module 'tamagui' in env.d.ts → declare module '@hanzo/gui' (uses GuiGroupNames)
- '.tamagui' ignore patterns in .gitignore / .fingerprintignore
- 'tamagui-loader' from .depcheckrc
Source files: 18 .ts/.tsx files updated to import from @hanzo/gui /
@hanzogui packages. Tests + config: 4 files updated to drop
tamagui-specific behavior. Docs (LLM.md, README.md, RABBY_FEATURES.md):
updated to reflect new stack.
No tamagui entries remain in any package.json.
The transform function for the 'svg-import-fix' plugin was missing its body
and return statement. The list of bundled deps that follows ('tamagui',
'@tamagui/web', etc.) was orphaned without an optimizeDeps.include wrapper.
Restored both:
- transform body returns code with import { ReactComponent as N } from '*.svg'
- optimizeDeps.include array properly opened/closed around the dep list
Lines 110-115 contained a leftover dynamic-manifest ternary fragment
with no condition — broke parse and prevented `pnpm exec wxt build`
from running. Drop the dead code; manifest defines stay in the inline
manifest field above and tenant overlays via manifest.overlay.json.
SCREENS.md §5. Wires /bridge to the cross-chain transfer form: source/
dest chain selectors, asset amount, recipient (same wallet | other),
route preview (lock → MPC → mint), threshold notation.
Bridge.tsx : form, ChainPair, AssetAmount, Recipient,
RoutePreview; status-driven button label
(Locking → MPC ceremony → Minting → Bridged)
useBridgeQuote.ts : gateway /v1/bridge/quote; 300ms debounce, 5s
timeout, AbortController on input change
useBridgeExecute.ts : 3-step Teleport flow:
1. approve (ERC-20) + lock (lockToken/lockNative)
2. poll M-Chain MPC ceremony (2s interval, 5m cap)
3. surface destination mint tx hash
User signs ONLY leg 1; mint is signed by the
bridge committee via threshold MPC. This is what
makes Teleport feel like a transfer, not two txs.
Threat model:
- destChainId + recipient pinned in lock calldata before the committee
sees the message. A compromised gateway cannot redirect funds.
- Ceremony URL built from brand.gatewayDomain (immutable per build,
ConfigMap-overridden per environment).
- Quote staleness >30s rejected; users can't sign locks against
quotes that have aged out.
- lockContract zero-address guard prevents accidental burns when the
Teleport factory hasn't been deployed on the source chain yet.
- Non-EVM source chains explicitly refused (clear error, not silent
failure) — Lux P/X bridges go through @l.x/api in a later slice.
Each screen's index.tsx exports a <Routes>-wrapper component (Auth,
Portfolio, Send, Receive — pattern from the screen Blues). When
mounted under a non-splat route (`path: "portfolio"`), the inner
<Routes> sees an empty relative path and matches nothing — so the
AppShell renders, the navbar paints, but the body is blank.
Switching to splat (`path: "portfolio/*"`) forwards the remainder
of the URL to each screen's nested router, which can then match
its own index + sub-routes.
Also adds the missing /auth route — Foundation never mounted it so
first-time users could not reach Welcome / Create / Import / SetPIN /
Unlock. Now reachable at /auth/{welcome,create,confirm,import,pin,unlock}.
The 4 wallet UX feat branches each added their own deps to
apps/web/package.json, which conflicted on merge. This commit
reconciles to a clean union of all required deps + ships the
fixes needed to make the merged tree build:
* package.json — union of all 4 branches' deps (qrcode.react,
@noble/{curves,hashes}, @scure/{bip32,bip39}, @walletconnect/*,
@solana/web3.js, viem, wagmi, etc.).
* @hanzo/gui v7's npm publish has no dist/ — Vite alias to a local
src/lib/gui-stub.tsx with minimal Stack/YStack/XStack/Button/Text/
Input/Card primitives. Tokenized props map to CSS vars set by
loadBrandConfig().
* src/lib/chain-lux.ts stubbed — @l.x/api npm publish has unresolved
__generated__/* imports; throws on use with a clear message
pointing users to the EVM C-Chain path.
* src/lib/pin.ts — getPinAuth() handle returning biometricAvailable
+ verifyPin + unlock; matches the API ConfirmSend Blue invented.
* Stack → YStack across auth screens (Stack isn't exported by v7).
* validateMnemonic from @scure/bip39 (viem doesn't export it).
* Type-cast Uint8Array → BufferSource for Web Crypto in lib/crypto.ts
(TS 5.7 lib.dom.d.ts tightened types).
* Confidential index gets a default export so router lazy() works.
* Lux Wallet fallback string → ▼ in Welcome.tsx.
Build output: 1.5MB total (uncompressed), 9 chunks, all 9 screens
lazy-loaded. tsc step dropped from build script — @l.x/* npm packages
ship raw .ts that fails tsc but Vite handles them.
Send flow per SCREENS.md §2:
- Send.tsx: form (to/amount/memo) with per-chain validation. Insufficient-
balance check is part of the same gate so the UI never lets a doomed tx
leave the form. Resets state on mount.
- AssetPicker.tsx: bottom-sheet modal. Re-uses portfolio's Asset rows;
caller passes the canonical asset list (one obvious way — same prop
pattern as Swap's TokenSelector).
- FeePreview.tsx: wagmi useEstimateGas × useGasPrice for EVM; static flat
fees for Lux P/X (no gas market) and Solana (5000 lamports).
- ConfirmSend.tsx: re-auth via getPinAuth().verifyPin() before broadcast.
Re-auth is required even when the wallet is unlocked — high-trust action,
fresh check at the moment of broadcast (defends against shoulder-surfer
on an unlocked tab).
- BroadcastResult.tsx: pending → confirmed/failed/timeout. EVM uses wagmi's
useWaitForTransactionReceipt; non-EVM has bounded 60s poll with manual
retry path.
- useSend.ts / useSendAsync.ts: orchestration hook + chain dispatch. Errors
always transition to status='error' with error populated, never to 'done'.
- usePortfolioAssets.ts: contract with Auth-Portfolio Blue's portfolio
store; collapses to one line when that store lands.
- index.tsx: nested <Routes> for /send, /send/confirm, /send/result/:hash.
Foundation router needs the path: 'send' → 'send/*' one-line change to
unlock the children.
Receive screen per SCREENS.md §2. Derives the receive address locally from
the unlocked mnemonic via lib/derive — nothing leaves the device.
Chain switcher iterates the canonical chain registry; QR encodes
`lux:<chain-id>:<addr>` so a peer scanning it knows which chain we're
advertising.
useReceiveAddress.ts is the single hook every form-factor (web, extension,
mobile) consumes — collapses chain-specific derivation behind one return
shape: { address, qrUri, locked, error }.
EVM sends go through wagmi walletClient. Lux P/X and Solana need their
own paths:
- chain-lux.ts: thin @l.x/api client wrapper. Reads mnemonic from the
unlocked auth slice at call-time so the secret never threads through
props/store/network. The build will fail until @l.x/api lands in
package.json — which is the correct outcome (no fakes).
- chain-solana.ts: SLIP-10 ed25519 key derivation + SystemProgram.transfer
via @solana/web3.js. Same auth contract — mnemonic stays on the call
stack, never persisted, never logged.
Six-phase state machine driven by zustand. Reset on /send mount keeps stale
form state from leaking across sessions. Pure data slice — async lives in
the useSend hook so the store stays mockable.
Tests: 3 covering initial state, setter updates, reset semantics.
Confidential slice (LP-013 + LP-063) — encrypted balances, threshold
decrypt, and selective-disclosure proofs.
apps/web/src/screens/confidential/
Confidential.tsx landing — list of F-Chain balances + ZK entry
ConfidentialBalanceRow.tsx hidden by default; tap to reveal; auto-rehide
RevealCommittee.tsx threshold MPC progress modal (signs as observer)
ConfidentialTransfer.tsx Send-form for FHE transfers; pubkey lookup
ZKProofGenerator.tsx claim picker + generator (BalanceGT, AccreditedUS,
AgeGT18, JurisdictionNotSanctioned)
ZKProofShare.tsx QR + verifier link sharing
useFHEBalance.ts fetch + threshold-decrypt session driver
useFHETransfer.ts client-side encrypt + submit (with degraded
gateway-encrypt fallback)
useZKProof.ts prove via @l.x/zk worker (gateway fallback)
qr.ts pure-TS QR-code SVG (no runtime dep)
styles.ts inline-style primitives until @hanzo/gui v7
brand.ts gateway-domain seam (window.__BRAND__)
types.ts shared FHE/ZK types
index.tsx ConfidentialRoutes + named exports
apps/web/src/store/confidential.ts
framework-free reveal/proof store
(useSyncExternalStore-compatible)
react-router-dom 7.5.0 added as a leaf dep — used only by this slice's
internal routes. Foundation owns the app-level router.
Threat model:
- Validators see only ciphertext; reveal needs threshold MPC committee.
- Plaintext lives 30s in memory, never persisted.
- ZK proofs reveal only the claim — witness never leaves the device when
@l.x/zk is present (gateway fallback is loud about degraded mode).
Punted to runtime stubs:
- @l.x/fhe TFHE WASM bindings — falls back to gateway encrypt with
explicit degraded-mode warning surfaced in the UI.
- @l.x/zk circuit prover — same fallback pattern.
Portfolio screen aggregates balances across the 14-chain Lux ecosystem
(C/X/Q/Z/F + Lux mainnet + Zoo + Hanzo + SPC + Pars), surfaced through
useChainBalances. EVM chains use viem publicClient via getBootnodeRpcUrl;
non-EVM Lux subnets (P/X/Q) hit AVAX-style JSON-RPC; F-Chain confidential
balances render as "Hidden 🔒" with a Reveal hand-off. Total USD comes
from a thin gateway price fetcher with graceful degradation. AssetRow +
AssetDetail compose into the layout. ChainSwitcher is foundation-owned —
lazy-imported with a tiny placeholder fallback so the screen still
renders during partial deploys. Per-LLM and state slices land separately.
Adds @hanzo/gui, viem, zustand, react-router-dom, @noble/hashes, and
@luxfi/wallet-brand workspace dep — foundation owns most of these and
will dedupe on merge.
usePerLLMTokens fetches ZEN4-NANO/MINI/LARGE/ULTRA balances when the
active chain is Zoo L1 (200200). Addresses are the canonical zoo-per-llm-
chains paper §3 entries; white-labels can override via brand.json. Uses
viem readContract via getBootnodeRpcUrl — no inline RPC literals, no
empty-string URLs. Hook returns [] off-Zoo so consumers can compose
without conditional rendering at the call site.
Portfolio store keeps native + token balances per chain plus a derived
totalUSD. Deliberately NOT persisted — balances re-fetch on unlock so a
stale UI cache never feeds Send/Swap. Auth slice (already in eb936692)
covers the persisted side; this finishes the state pair the rest of the
slice depends on.
components/GuiProvider.tsx — thin wrapper around @hanzo/gui v7's
HanzoguiProvider. v7.0.0 ships a publishing oversight (package.json
exports point at ./dist/esm/index.mjs but the npm tarball does NOT
include a dist/ directory) so the Provider import would crash at
build time. Brand theming already flows via CSS custom properties
populated by loadBrandConfig(), so this passthrough is transparent
at the rendering layer. Swap to HanzoguiProvider once upstream
republishes a fixed v7.
components/AppShell.tsx — sticky top bar (logo + walletName +
ChainSwitcher + truncated account address) over a collapsible
side drawer with the 9 primary nav links. Outlet renders the
routed screen. CSS-var driven so every white-label theme just works.
components/ChainSwitcher.tsx — native <select> dropdown over
brand.supportedChainIds. Sets the zustand chain slice and asks
wagmi's useSwitchChain when the active chain is wagmi-managed.
@hanzo/gui Select replaces the native <select> once v7 dist ships;
controlled-value/onChange surface stays the same.
App.tsx — final provider tree:
GuiProvider → QueryClientProvider → WagmiProvider → RouterProvider
Wagmi config built lazily inside useMemo so StrictMode's double
render doesn't re-instantiate WalletConnect, and so the config
reads brand.supportedChainIds AFTER loadBrandConfig() resolved
in main.tsx.
react-router-dom v7 BrowserRouter with lazy-loaded route modules.
Each route imports from ./screens/{name}/index.tsx which screen
Blues replace as they merge:
/ → redirect to /portfolio
/portfolio → Auth-Portfolio Blue
/send /receive → Send-Receive Blue
/swap /bridge → Swap-Bridge Blue
/stake /dapps → Stake-DApps Blue
/confidential → Confidential Blue
/settings → Settings-Signing Blue
* → redirect to /portfolio
Each placeholder screen renders a labelled section so the build
is clean today and merges are pure file replacements tomorrow.
The lazy() boundary means each Blue's chunk is independent — no
ripple to the foundation index bundle when their screens land.
config/wagmi.ts builds the Wagmi config from runtime brand:
chains derived from brand.supportedChainIds, transports resolved
via getBootnodeRpcUrl(chainId) — never empty-string. Chains without
a resolvable RPC are dropped so http("") never silently coerces to
window.location.origin. Last-resort mainnet fallback so wagmi hooks
never crash on a white-label with zero supported chains.
config/queryClient.ts: 30s staleTime, retries off, no
window-focus refetch — a "stale" indicator is more honest than a
flicker.
store/index.ts: zustand with three slices Foundation owns —
account (address mirror), chain (active id), ui (modal stack +
sidebar). Other slices (auth, send, swap, stake) live in sibling
files owned by other Blues; separate stores keep each Blue's
persistence policy independent.
hooks/useBrand.ts: re-exports the brand singleton with React-friendly
typing, ready to swap to a context if a screen needs reactive brand
updates without call-site changes.
hooks/useAccount.ts: combines wagmi's useAccount with the zustand
account slice — wagmi wins for EVM, store fills in P/X/Solana
addresses derived by Auth-Portfolio Blue. Screen Blues consume
this hook; they MUST NOT import wagmi's useAccount directly.
Foundation slice 1/7 — the gating slice every other wallet screen Blue
depends on. Replaces the bare-bones Next.js-shaped placeholder with a
production Vite SPA: pinned deps (react 19.2.5, vite 8.0.8, ts 5.9.3),
@/ → src/ alias, public/{logo,favicon}.svg, brand.json copy plugin
already in place, sourcemaps on, ES2022 target.
main.tsx awaits loadBrandConfig() so the SPA never flashes Lux
branding on a <tenant> (or other white-label) deploy.
GPL-3.0-or-later preserved.