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.
Document the provider tree, file layout, and the screen-Blue
contract: each Blue replaces src/screens/{name}/index.tsx; the
shell, router, store, hooks, and config remain owned by Foundation.
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.
Auth flow at /auth/* — first-launch wallet creation, BIP-39 import, PIN
setup, and returning-user unlock. Mnemonic generated/validated via
viem/accounts (BIP-39 + checksum); held only in an in-memory draft store
between screens; persisted only as AES-256-GCM ciphertext under a PIN-
derived scrypt key. PIN verifier hash uses an independent scrypt with a
domain-separated salt — verifier compromise does not yield the encryption
key. Unlock supports a passkey accelerator hook (no-op until Foundation
slice owns the platform-keychain handoff). Welcome/Create/Confirm/Import/
SetPIN/Unlock are self-contained components composed by AuthRoutes for
the foundation router to lazy-import.
Red review flagged that the 461 .ts files in pkgs/wallet/src/ are
fork-derived from upstream Uniswap Wallet (GPL-3 + BUSL hybrid) but
this repo had no LICENSE file at root and 4 of 7 pkg.jsons were
missing the license field. Distributing the binary without a LICENSE
file violates GPL-3 §4-5; downstream white-labels (<tenant>, Zoo)
inherit the violation.
LICENSE copied verbatim from luxfi/exchange (the canonical GPL-3
text already used across the lux ecosystem). license fields added
to: root package.json, apps/web/package.json, apps/extension/package.json,
pkgs/wallet/package.json. The other three (apps/mobile, pkgs/brand,
pkgs/analytics) already declared GPL-3.
Resolves Red finding RED-3 (HIGH severity).
LEGACY.md — sibling repos to archive after canonical absorption:
- wallet-legacy (OneKey-fork) → BRAND_PACKAGE pattern absorbed via pkgs/brand
- wwallet (bespoke SDK) → superseded by @l.x/api npm-published
- xwallet (OKX-fork) → hardware-wallet code already removed upstream
- dwallet → independent desktop product, NOT folded in
LLM.md — single source of truth for AI assistants on this repo. Documents:
- canonical structure (apps/{web,mobile,extension} + pkgs/{wallet,brand,analytics})
- what builds today (web clean; ext/mobile pending app refactor)
- white-label brand pattern, analytics pattern, bootnode RPC pattern
- the @l.x/* upstream publishing bug (~5000 type errors from raw .ts shipping)
- rules: no datadog/amplitude/uniswap direct deps, no Quicknode, no Tamagui
name in source, BIP44 9000 path preserved.
SCREENS.md — UX spec freeze 2025-12-15 (already written; now tracked).
Downstream consumption: a downstream wallet repo pins UPSTREAM_REF against
luxfi/wallet@<sha>, same pattern as a downstream swap repo shimming
luxfi/exchange.
@luxfi/wallet-analytics mirrors the abstraction the parallel exchange Blue
landed at ~/work/lux/exchange/pkgs/utilities/src/telemetry/analytics/backend.ts.
No third-party SDK is imported by this module — delivery is owned by an
AnalyticsDriver registered via setAnalyticsDriver(...). Hanzo Insights is
the intended default driver; white-labels swap drivers at boot.
Public surface (init/track/identify/Identify/getUserId/setDeviceId/flush)
matches what wallet call sites already expect, so swapping providers does
not require diff-ing 200+ sendAnalyticsEvent(...) sites.
Stripped from package.json (zero direct provider deps now in our shipped
manifests):
apps/extension: @datadog/browser-rum
apps/mobile: @amplitude/analytics-react-native
@datadog/mobile-react-native
@datadog/mobile-react-navigation
@datadog/datadog-ci
Both apps now declare workspace deps on @luxfi/wallet-analytics and
@luxfi/wallet-brand. The 6 source files that still call datadogRum / DdSdk
directly are upstream-shaped and broken on other axes (`@universe/*`,
`wallet/*` referenced as bare specifiers); they will be replaced with
@luxfi/wallet-analytics calls during the apps refactor.
Same pattern as ~/work/lux/exchange/pkgs/config/src/brand.ts. Replaces the
BRAND_PACKAGE env-var trick from wallet-legacy with a cleaner runtime
overlay: brand.json ships in the image, K8s ConfigMap overlays at deploy
time, no source fork required.
pkgs/brand/
src/index.ts — BrandConfig interface, mutable `brand` singleton,
loadBrandConfig(), getBootnodeRpcUrl(), getBrandUrl().
brand.json — Lux Wallet defaults (chains 96369/96368/200200/...).
package.json — @luxfi/wallet-brand workspace pkg.
apps/web/
vite.config.ts — copyBrandJson plugin: pkgs/brand/brand.json →
public/brand.json (dev) and dist/brand.json (build).
src/main.tsx — loadBrandConfig() before React renders.
src/App.tsx — read brand.walletName / brand.description.
White-labels (<tenant>, Zoo, Pars) override /brand.json via ConfigMap.
Theme tokens flow as CSS custom properties (--accent1, --surface1, ...) so
no React tree change is needed for a brand swap.
Bootnode RPC pattern: getBootnodeRpcUrl(chainId) defaults to
https://<gatewayDomain>/v1/rpc/<chainId> with rpc[<chainId>] overrides.
No Quicknode, no Alchemy direct.
The 01b6ad2b strip removed the upstream packages/* and config/tsconfig/* dirs
but left the old `references` arrays pointing at deleted paths, breaking
typecheck. Reset:
- pkgs/wallet/tsconfig.json now extends ../../tsconfig.base.json directly,
excludes test files (no @types/jest in this scope), no stale refs.
- apps/{extension,mobile}/tsconfig.json drop refs to packages/* (which never
existed in this monorepo — those were upstream paths) and reference
../../pkgs/wallet only.
apps/web builds clean. pkgs/wallet typecheck still surfaces ~5000 errors
from `@l.x/*` npm packages shipping raw .ts (upstream publishing bug —
documented in LLM.md, not lux/wallet's responsibility to patch).
- Delete pkgs/{analytics,api,config,eslint-config,gating,lx,notifications,
prices,sessions,ui,utilities} — all identical to ~/work/lux/exchange
canonical (MD5-matched). Now published to npm at @l.x/*; consume from
there instead of vendoring.
- pkgs/wallet remains — this IS @luxfi/wallet (the uniswap-style extension
+ mobile wallet bones). Published at 1.0.10.
- Rewrite 3553 bare-path imports (uniswap/src/, ui/src/, utilities/src/,
lx/src/, lux/src/) → explicit @l.x/* across 725 files.
- apps/{extension,mobile}/package.json: workspace:^ → npm ^versions for
all @l.x/* / @luxfi/eslint-config; only @luxfi/wallet stays workspace.
- apps/{extension,mobile}/.eslintrc.js: RULES_DIR → node_modules/@l.x/lx/eslint_rules.
~315K line deletion. One canonical home: ~/work/lux/exchange.
Default fallback brand shows ▼ Wallet / ▼ Extension instead of
Lux Wallet. Extension manifest defaults are now brand-neutral.
JSDoc examples cleaned to be brand-neutral.
Replace Next.js App Router with Vite 8 + @vitejs/plugin-react.
Fix @hanzo/gui 4.3.2 (does not exist) back to 4.3.1 with
pnpm override for @hanzogui/fake-react-native transitive dep.