- 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.
Single LICENSING.md file referencing the canonical three-tier IP and
licensing strategy at github.com/luxfi/.github/blob/main/profile/README.md.
LICENSE file is unchanged; this only adds a navigational pointer.
The switch in walletSchemeName covers every WalletSchemeID enum
value, so TypeScript --strict narrows the default arm's `s` to
`never` and rejects `s.toString(16)`. Cast through `number` for
the forward-compat fallback path.
No behaviour change; KAT vector for AccountID derivation
(chainID=9000, scheme=0x42) still matches the Go side at
76a03630148103ec558cf4d8f7e8a2d8766ea205d96a4529249c3aaf9c5d078cc9f9fdb8f21a5e7ef6bb4c4ea29c9654.
TypeScript module under pkgs/wallet/src/features/wallet/pq/ that
mirrors the PQAccount shape, the cSHAKE-256 AccountID derivation, the
HD-path formatters, and the role/customization mappings defined on the
Go side in github.com/luxfi/sdk/wallet/account.
The TS side intentionally does NOT pull in an ML-DSA-65 implementation.
ML-DSA itself is software-in-browser; the cost of bundling FIPS 204 is
not worth paying for read-only consumers (UI list views, route
handlers). Consumers that need to sign in-browser supply a concrete
MLDSAProvider implementation (typically backed by @noble/post-quantum
mlDsa65 or a hardware-wallet bridge). Browser keys are session-scoped;
long-lived keys MUST live in a native wallet.
Wire compatibility: AccountID KAT cross-checked against the Go side.
For the canonical vector (chainID=9000, scheme=0x42, pubkey="test-
pubkey-for-accountid-kat-vector-stable-across-runs") both
implementations produce
76a03630148103ec558cf4d8f7e8a2d8766ea205d96a4529249c3aaf9c5d078cc9f9fdb8f21a5e7ef6bb4c4ea29c9654.
Adds @noble/hashes ^1.7.1 to pkgs/wallet dependencies (already
transitively present via @scure/bip32; declaration is for strict
node_modules resolution). pnpm-lock.yaml refresh on next install.
Patch-bump.
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.
Canonical home for static viem `defineChain` configs across the Lux
ecosystem (lux/build, lux/wallet, third-party integrators). Ships
lux/luxTestnet/luxDex, zoo/zooTestnet, hanzo/hanzoTestnet,
sparklePony/sparklePonyTestnet, pars/parsTestnet plus collection
re-exports (LUX_CHAINS, ZOO_CHAINS, ...). Published to npm as
@luxfi/wallet-chains@0.1.1 (tsup esm + dts, viem peerDep ^2.30).
White-label deployments overlay their own chain configs at runtime
via the brand.json layer in @luxfi/wallet-brand and do not consume
this package directly.
Walked every dir under wwallet/{app,web,components,sdk}. Zero files ported.
Canonical luxfi/wallet already covers every feature wwallet had with a
stricter security model and the SDK already published on npm.
- Local: mv ~/work/lux/wwallet ~/work/lux/.wwallet-archived-2026-04-30
- GitHub: gh repo archive luxfi/wwallet pending org admin permission
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.