65 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 75c529deb8 fix(wallet-web): visible brand marks, centered auth card, spaced CTAs, no cross-brand chain leak
- 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>
2026-07-19 01:30:36 -07:00
zeekayandHanzo Dev 6dc2c4e9b4 deploy(wallet-web): pin lux overlay to 21986289d (font + overflow fixes)
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-18 23:47:10 -07:00
zeekayandHanzo Dev 21986289db fix(web): global sans-serif font — wallet was rendering in UA serif (Times)
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>
2026-07-18 23:46:52 -07:00
zeekayandHanzo Dev 5dd2b16022 fix(web): stop mobile horizontal overflow on the wallet landing page
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>
2026-07-18 20:48:55 -07:00
zeekayandHanzo Dev db275f7487 deploy(wallet-web): lux overlay tag-pin fix + plain-manifest fallback
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>
2026-07-18 14:37:11 -07:00
zeekayandHanzo Dev dc08a6af0d fix(web): copy root tsconfig.base.json into Docker build stage
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>
2026-07-18 14:21:38 -07:00
zeekayandHanzo Dev 5754513482 fix(web): bridge onChangeText->onChange in gui-stub Input so onboarding accepts input
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>
2026-07-18 10:50:45 -07:00
zeekayandHanzo Dev 1ddc0df258 fix(web): dashboard quick-actions use SPA soft-nav so the session survives
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>
2026-07-18 10:43:17 -07:00
zeekay 83e2706fb6 remove Stripe from onramp deep-link test fixtures (Stripe account disabled)
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.
2026-06-24 12:34:20 -07:00
zeekay 48915e47db chore(deps): @luxwallet/chains + @hanzo/iam file: → published npm 2026-06-24 11:11:51 -07:00
zeekay 9d6c1d9a0c feat(web): chain registry from @luxwallet/chains + IAM login via @hanzo/iam
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.
2026-06-24 09:15:08 -07:00
zeekay 82e0d5512e fix(web): re-sync store chainId to brand.defaultChainId after loadBrandConfig (white-label default chain) 2026-06-23 16:27:05 -07:00
zeekay 3b1ad33433 test(backend): LIVE backend↔MPC custody e2e (not mock)
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
2026-06-23 16:24:27 -07:00
zeekay 10c926c22a feat(web): white-label custody wiring + per-brand deploy + download page 2026-06-23 15:51:02 -07:00
zeekay 64bd0f67c9 feat(backend): App(Wallet) MPC custody server (Go, no plaintext keys)
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.
2026-06-23 15:07:40 -07:00
Hanzo Devandzeekay aaae4e0278 scrub Avalanche naming residue: comment rename in non-EVM chain balance helper 2026-06-10 22:35:41 -07:00
Hanzo Devandzeekay 5d7a45aaec scrub: subnet/l2 → chain (canonical vocabulary, forward-only)
Wire-format codec IDs unchanged. CLI aliases deleted; chain is the
command. No backwards-compat shims, no deprecation comments.
2026-06-10 22:35:41 -07:00
Hanzo AIandzeekay 111b7f6276 wallet/pq: concrete ML-DSA / ML-KEM / SLH-DSA + hybrid + HD + precompile encoders
Wires the canonical post-quantum stack per LP-4200 into the wallet:

  • mldsa.ts    — ML-DSA-44/65/87 providers (FIPS 204) over @noble/post-quantum
  • mlkem.ts    — ML-KEM-512/768/1024 KEM providers (FIPS 203)
  • slhdsa.ts   — SLH-DSA-128f/192f/192s/256f providers (FIPS 205)
  • hybrid.ts   — secp256k1∥ML-DSA + Ed25519∥ML-DSA containers; wire format
                  byte-compatible with luxfi/sdk wallet/keychain PQSigner
  • hdPq.ts     — BIP-32 → cSHAKE expandChildSeed → ML-DSA/SLH-DSA keypair
  • domain.ts   — FIPS-204 §5.4 ctx strings:
                    lux-evm-precompile-mldsa-v1   (0x012202)
                    lux-x-chain-utxo-v1
                    lux-p-chain-platform-mldsa-v1
                    lux-recovery-slhdsa-v1        (0x012203)
                    lux-handshake-mlkem-v1
  • precompile.ts — eth_call encoders for 0x012201/02/03 LP-4200 verifiers

The pkgs/wallet/pq surface decomplects four concerns that were previously
tangled in luxfi/sdk's PQSigner: scheme dispatch (providerFor), domain
separation (contextBytesFor), hybrid composition (encode/decodeHybrid),
HD derivation (derivePQIdentity). Each module is independently complete;
the index re-exports them as a single PQ public API.

Web facade lives at apps/web/src/lib/pq.ts; usePQIdentity() hook derives
the canonical ML-DSA-65 identity at m/44'/9000'/<chainID>'/0'/0'/0' from
the unlocked auth-slice mnemonic.

Tests: 71 passing across 8 suites
  domain: 4   mldsa: 11   mlkem: 7   slhdsa: 6
  hybrid: 9   hdPq: 9     precompile: 8   pqAccount: 17 (pre-existing)

Test runner: jest.pq.config.js (standalone — bypasses upstream-shaped
jest-expo preset still pending refactor; see LLM.md).

Domain separators pinned by domain.test.ts; cSHAKE customizations pinned
by pqAccount.test.ts (pre-existing). Replay-across-chains explicitly
tested via mldsa.test.ts and hybrid.test.ts cross-ctx verify cases.
2026-06-10 22:35:41 -07:00
Hanzo AIandzeekay 9e53916781 chore: scrub uniswap — use @luxamm/* (lux fork) only
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).
2026-06-10 22:35:41 -07:00
Hanzo AIandzeekay dc12b4deed fix(extension): add missing getTsconfigAliases import + publicAssetsVariant const
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.
2026-06-10 22:35:40 -07:00
Hanzo AIandzeekay 2b7990fa0c fix(extension): add missing vite-plugin-svgr + commonjs + node-polyfills + tsconfig-paths
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'.
2026-06-10 22:35:40 -07:00
Hanzo AIandzeekay 58e646e718 chore: scrub tamagui — use @hanzo/gui (and @hanzogui/* forks) only
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.
2026-06-10 22:35:40 -07:00
Hanzo AIandzeekay 64268180bf fix(extension): repair svg-import-fix transform body + restore optimizeDeps wrapper
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
2026-06-10 22:35:40 -07:00
Hanzo AIandzeekay 79a4645aac fix(extension): remove orphaned ternary fragment from wxt.config.ts
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.
2026-06-10 22:35:40 -07:00
Hanzo AIandzeekay d8f73616f0 wallet: scrub Avalabs IP brand refs 2026-06-10 22:35:40 -07:00
Hanzo AI 29eab77bd2 feat(bridge): Bridge form + Lux Teleport quote/execute hooks
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.
2026-04-30 17:54:01 -07:00
Hanzo AI 8bb1343757 feat(swap): SwapForm + TokenSelector + quote/execute hooks
SCREENS.md §3. Wires /swap and /swap/confirm to the canonical token-swap
form: from/to TokenSelector × 2, decimal amount, slippage popover (10/50/
100bps presets + custom), QuoteRoute panel with route kind badge.

  Swap.tsx           : main form, flip button, slippage popover, submit gate
  TokenSelector.tsx  : modal w/ search + popular pinned + inline balances
  QuoteRoute.tsx     : route kind ("AMM v2/v3" | "DEX v4") + path + impact
  useSwapQuote.ts    : gateway /v1/quote (preferred) + on-chain QuoterV2
                       fallback via useReadContract; 300ms debounce, 5s
                       timeout, AbortController on input change
  useSwapExecute.ts  : approve (max-uint, ERC-20 only) + swap via wagmi
                       writeContract; amountOutMin = amountOut * (1 - bps)
  contracts.ts       : minimal QuoterV2 / SwapRouter02 / Teleport ABIs +
                       per-chain address table (gateway is source of truth
                       at runtime; addresses are fallback-only)

Threat model:
  - Slippage enforced on-chain (router reverts on amountOutMin); UI gate is
    defense-in-depth, not the canonical control.
  - Quote staleness rejected client-side (>30s) to prevent stale-price
    execution after the user steps away.
  - All RPC URLs come from getBootnodeRpcUrl(chainId); no empty-string URLs
    (Solana Connection ctor crash mitigation per v0.3.39 lesson).
  - Token names/symbols rendered as React text (default-escaped) — no
    dangerouslySetInnerHTML anywhere in the slice.
2026-04-30 17:53:43 -07:00
Hanzo AI afe7ce1f12 feat(state): zustand swap+bridge slices
Pure data slices for Swap and Bridge state machines. Async lives in the
hooks; the store keeps the screen + flow phases in sync.

  swap   : idle → quoting → ready → approving → swapping → done | error
  bridge : idle → quoting → ready → locking → signing → minting → done | error

Quote shapes carry a `ts` timestamp; consumers reject quotes >30s old.
Route kinds locked: V2/V3 = AMM, V4 = DEX (rendered verbatim in UI).
2026-04-30 17:53:23 -07:00
Hanzo AI f028efd393 feat(web): settings + signing slice (7/7 — wallet UX complete)
Settings sub-router (`/settings/*`) with eight screens:

  Settings.tsx — top-level menu
  Networks.tsx — per-chain RPC override (HTTPS-only, persisted)
  Security.tsx — PIN change, biometric toggle, auto-lock 1/5/15/60/never
  Backup.tsx   — PIN re-auth → reveal mnemonic → 30s auto-redact + ack
  Language.tsx — 10 locales, en default
  Theme.tsx    — system / dark / light
  Currency.tsx — USD / EUR / JPY / GBP / CHF / CAD
  About.tsx    — VERSION + /.upstream-sha + GPL-3 + brand legal links

Signing modal — global tx confirmation triggered by Send / Swap /
Bridge / dApps before broadcast:

  SigningModal.tsx   — backdrop + ESC + reject-on-close
  TxSummary.tsx      — from / to / amount / gas / chain + ContractDecode
                       (local ABI + opt-in 4byte fallback)
  RiskIndicator.tsx  — green / yellow / red heuristics
                       (known contract → green, MAX_UINT approve → yellow,
                        unverified contract → red)
  useSigningModal.ts — hook + imperative API; promise-resolving queue,
                       single in-flight request enforced

State (`store/settings.ts`):
  { networks: { [chainId]: { rpcOverride } },
    security: { biometric, autoLockMin },
    locale, theme, currency }
  Persisted via zustand `persist` middleware, partialize boundary
  excludes nothing (pure UI prefs, no secrets).

State (`store/signing.ts`):
  Single-slot queue keyed by `pending`. `open(tx)` returns
  `Promise<{approved, reason?}>`. `beforeunload` rejects pending so
  caller awaits unwind cleanly.

Wired at app root: `App.tsx` mounts `<SigningModal />` lazily inside
`<Suspense>` so the modal is reachable from any slice but stays out
of the auth bootstrap bundle.

Constraints honoured:
  - ▼ brand defaults via `lib/brand.ts` + `getBootnodeRpcUrl()`,
    no empty-string URLs, white-label-ready.
  - Mnemonic reveal: PIN re-auth required, 30s auto-redact timer,
    overwrites on unmount, never logged or auto-clipboarded.
  - Risk heuristics never block; user always confirms.
  - Currency conversions through `https://${brand.gatewayDomain}/v1/prices`
    with 30s in-memory cache, no calldata leaks.

Build: 23.0 KB settings chunk (6.2 KB gzipped), 13.9 KB SigningModal
chunk (4.8 KB gzipped). Total dist 7.6 MB.
2026-04-30 17:52:26 -07:00
Hanzo AI 61332c02de fix(router): mount screens with /* splat so nested <Routes> match
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}.
2026-04-30 16:46:31 -07:00
Hanzo AI ab7ebb1c8f fix(web): unblock build — gui-stub, pin lib stubs, package.json union
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.
2026-04-30 14:10:11 -07:00
Hanzo AI 795d09ff11 merge: wallet feat/confidential slice
# Conflicts:
#	apps/web/package.json
#	apps/web/src/screens/confidential/index.tsx
#	pnpm-lock.yaml
2026-04-30 14:03:22 -07:00
Hanzo AI eb6ffb9277 merge: wallet feat/stake-dapps slice
# Conflicts:
#	apps/web/package.json
#	apps/web/src/screens/dapps/index.tsx
#	apps/web/src/screens/stake/index.tsx
#	pnpm-lock.yaml
2026-04-30 14:03:11 -07:00
Hanzo AI 77775cbb3e merge: wallet feat/send-receive slice
# Conflicts:
#	apps/web/package.json
#	apps/web/src/screens/receive/index.tsx
#	apps/web/src/screens/send/index.tsx
#	pnpm-lock.yaml
2026-04-30 14:03:10 -07:00
Hanzo AI 089d862bac merge: wallet feat/auth-portfolio slice
# Conflicts:
#	apps/web/package.json
#	apps/web/src/screens/portfolio/index.tsx
2026-04-30 14:02:56 -07:00
Hanzo AI 40262dfc3d feat(web/send): form, asset picker, fee preview, confirm-with-reauth, broadcast result
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.
2026-04-30 13:16:18 -07:00
Hanzo AI 7cd3500c8d feat(web/receive): chain-switcher, QR, copy/share
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 }.
2026-04-30 13:16:03 -07:00
Hanzo AI d72cbad2a7 feat(web/lib): non-EVM chain send adapters (Lux P/X, Solana)
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.
2026-04-30 13:15:55 -07:00
Hanzo AI 7e777bebc8 feat(web/lib): chain registry, address validators, BIP-44 derivation
Shared modules consumed by Send + Receive (and re-usable by Swap/Bridge):

- asset.ts: Chain registry (lux-c/p/x/b/z/f, zoo-l1, ethereum, …, solana),
  Asset interface, parseUnits/formatUnits (bigint-safe).

- address.ts: Per-chain validation. EIP-55 checksum (no viem dep), BIP-173
  bech32 with HRP and "P-"/"X-" UX prefix, base58 decode-and-length-32 for
  Solana. Discriminated Validation result so the form can both gate the
  Sign button and surface a precise error inline.

- derive.ts: BIP-39 → BIP-32/SLIP-10 → per-chain encoding pipeline.
  EVM uses m/44'/60'/0'/0/0 → keccak256(uncompressed_pub[1:])[12:].
  Lux P/X uses m/44'/9000'/0'/0/0 → ripemd160(sha256(compressed_pub)) →
  bech32 with "P-"/"X-" UX prefix.
  Solana uses SLIP-10 ed25519 m/44'/501'/0'/0' → base58.

Tests: 41 — EIP-55 reference vectors, BIP-173 reference vector, base58
length / charset, parse/format round-trip, BIP-39 reference seed, EVM-vs-Lux
key sharing across coin_type 60/9000, deterministic re-derivation.
2026-04-30 13:15:48 -07:00
Hanzo AI 00ecfdb35a feat(web/store): send-flow state machine (idle → preview → broadcasting → done|error)
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.
2026-04-30 13:15:34 -07:00
Hanzo AI a0c48969bf deps(web): qrcode.react, @noble/hashes, @noble/curves, @scure/bip32, @scure/bip39, zustand
Add the cryptographic primitives Send and Receive depend on:

- @scure/bip39 — mnemonic → seed (BIP-39 PBKDF2)
- @scure/bip32 — secp256k1 BIP-32 / BIP-44 (m/44'/60'/* and m/44'/9000'/*)
- @noble/curves — secp256k1 + ed25519 keypair derivation
- @noble/hashes — keccak256 (EIP-55), sha256+ripemd160 (Lux bech32 payload),
  hmac-sha512 (SLIP-10), pbkdf2 inside @scure/bip39
- qrcode.react — Receive QR
- zustand — Send-flow store
2026-04-30 13:15:29 -07:00
Hanzo AI 4c7dd80ac8 feat(web): stake + dApp connect screens
- screens/stake/{Stake,ValidatorList,StakeForm,index}.tsx +
  useValidators/useStake hooks for P-Chain delegation. NodeID regex
  validation, BigInt nLUX math (no floats), 14-day min lock period,
  jailed validator block, capacity check.
- screens/dapps/{DApps,Connect,ActiveSessions,index}.tsx +
  useWalletConnect (sign-client v2.21) and useTrustedDApps. wc:// URI
  shape validator, method+chain allowlist, no auto-approval, hostname
  -keyed trusted dApp fallback.
- store/{stake,dapps}.ts: useSyncExternalStore-based stores.
- screens/_shared/{runtime,account}.ts: typed shims for Foundation's
  config and account contracts so screens are independently buildable.
- deps: @walletconnect/sign-client@^2.21, @walletconnect/utils@^2.21,
  react-router-dom@^7.5.

tsc clean, vite build clean, URI validator unit run 8/8 pass.
2026-04-30 13:14:39 -07:00
Hanzo AI 12d12682c2 feat(confidential): F-Chain FHE balances + Z-Chain ZK proofs
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.
2026-04-30 13:10:46 -07:00
Hanzo AI 058d09b51e feat(portfolio): main view + asset list + chain switcher integration
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.
2026-04-30 13:10:12 -07:00
Hanzo AI 5bd2daccb5 feat(portfolio): per-LLM token row for Zoo chain
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.
2026-04-30 13:08:07 -07:00
Hanzo AI e3f995c405 feat(state): zustand portfolio slice
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.
2026-04-30 13:07:46 -07:00
Hanzo AI 959b0fb68d feat(web): @hanzo/gui v7 web AppShell + ChainSwitcher
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.
2026-04-30 13:06:00 -07:00
Hanzo AI 8886825cce feat(web): router placeholders for 9 screens (filled by screen blues)
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.
2026-04-30 13:05:46 -07:00
Hanzo AI 7193d539f5 feat(web): wagmi + react-query + zustand + brand wiring
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.
2026-04-30 13:05:38 -07:00
Hanzo AI f91fe1e35c feat(web): replace Next.js skeleton with Vite SPA shell
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.
2026-04-30 13:05:22 -07:00