Replace the five hardcoded Lux strings/colors in the Lux tenant build
with reads from @luxfi/brand's brand.json:
name "Lux Bridge" -> `${shortName} Bridge`
primaryColor "#5b8def" -> brand.primaryColor (#000000)
secondaryColor "#7a9ff5" -> theme.dark.accent1
supportEmail "[email protected]" -> brand.supportEmail
apiHost / docsUrl literal lux.network -> derived from appDomain/docsDomain
Use the `@luxfi/brand/brand.json` subpath rather than the package's
main entry: the npm-published @luxfi/[email protected] omits its .d.ts shipping
artefacts, but brand.json is included and TypeScript infers full literal
types under `resolveJsonModule`. This also keeps the package as the
single source of truth — a brand block bump in @luxfi/brand propagates
here on the next install with no string copies.
Each lookup keeps a hard fallback string so the build never breaks when
@luxfi/brand omits a field in a future version. The fallbacks match the
pre-cutover hardcodes (lux.network defaults).
Tenant build verified:
pnpm --filter @luxbridge/lux-tenant typecheck clean
pnpm --filter @luxbridge/lux-tenant build clean (bundles brand.json)
pnpm --filter @luxfi/bridge typecheck clean (SDK unchanged)
SDK sweep clean: no hardcoded Lux brand overrides under pkg/bridge/src/app/.
The two remaining matches are:
- pkg/bridge/src/app/styles/theme.css header comment naming the SDK
- pkg/bridge/src/app/lib/chains.ts:29 `name: 'Lux Network'` chain identity
on `lux:96369` (structurally identical to `'Ethereum'` on `evm:1`).
DEADBEEF audit unchanged: 1 occurrence, gated behind `import.meta.env.DEV`
in pkg/bridge/src/app/hooks/useWallet.ts:32.
The patch-hanzogui-exports.cjs script is the only supported way to
make `pnpm install && pnpm exec vite build` succeed against the broken
@hanzo/[email protected] publish. Swallowing its exit code with `|| true`
hides the only signal we have that the workaround stopped applying —
e.g. if a future pnpm release changes traversal semantics or upstream
republishes the SDK and the patcher trips on an unexpected layout, the
install would still report success and the failure would surface much
later at the vite build step with a confusing module-resolution error.
Verified `rm -rf node_modules && pnpm install --frozen-lockfile`
completes end-to-end with the script exiting 0
(`patched 240 package(s); fixed 41 source file(s)`). Red Phase 3 R2 nit.
bridge-ui ships as a prebuilt Vite SPA inside ghcr.io/hanzoai/spa
(see app/bridge/Dockerfile final stage), not a Next.js dev container.
The compose + k8s manifests were still describing the old Next shape:
containerPort 3000, `npm run dev`, /app/.next volume mount, and
NEXT_PUBLIC_* env vars. None of that exists in the image anymore.
- compose.testnet.yml / compose.mainnet.yml: port 3001:80 (was 3001:3000);
drop command + .next mount; swap NEXT_PUBLIC_* for VITE_BRIDGE_* env
(the names app/bridge/src/bridge.config.ts reads).
- k8s/mpc-deployment.yaml: containerPort 80 (was 3000); Service
targetPort 80 (was 3000 — broken since the static-SPA cutover); swap
NEXT_PUBLIC_* for VITE_BRIDGE_* on the Deployment env.
- compose.yml: keep local hot-reload UX by mounting the repo root and
running `pnpm -C app/bridge dev --host 0.0.0.0 --port 3001` against
node:22-alpine. Dev port moves 3000 -> 3001 to match the published
SPA convention across envs.
- scripts/start-local.sh: update printed UI URL to :3001.
compose.infra.yml and compose.local.yml don't define bridge-ui — left
untouched. scripts/launch-mpc-bridge.sh already prints :3001 and has no
Next-specific config; check-ci-status.sh only references bridge-ui as a
GHCR package name (image preserved per spec). Both verified clean.
Validated:
- docker compose -f compose.{yml,testnet,mainnet,infra,local}.yml config -q
- kubectl --dry-run=client apply -f k8s/mpc-deployment.yaml
Adds a third matrix entry to docker.yml so app/bridge/Dockerfile is
built and pushed to ghcr.io/luxfi/bridge-ui on every main/dev/test push
and v* tag. Multi-arch (linux/amd64 + linux/arm64) and tag policy are
inherited from the shared hanzoai/.github docker-build.yml reusable
workflow (same as the existing bridge + bridge/server entries).
The bridge-ui image is referenced by compose.yml, compose.testnet.yml,
compose.mainnet.yml, and k8s/mpc-deployment.yaml. Without this job the
image was orphaned — the Makefile build-ui target was the only producer
and no workflow invoked it.
The Lux tenant config now imports getColorSVG() from @luxfi/logo
(https://github.com/luxfi/logo) and inlines it as a data URL on the
BrandConfig.logoUrl field. Replaces the prior env-var-only path with
a deterministic, single source of truth — the @luxfi/logo package.
Consumers can still override at deploy time via VITE_BRIDGE_LOGO_URL.
No other bridge code touches Lux branding — the SDK stays
jurisdiction-neutral.
Red flagged a P1 documentation gap: downstream tenants of @luxfi/bridge
(starting with zooai/bridge) need the same react-native-web alias +
TAMAGUI_TARGET define that app/bridge/vite.config.ts uses, or Rolldown
fails to resolve react-native from Tamagui's runtime. Adding:
- pkg/bridge/README.md: new "Consuming this SDK from a Vite app" stanza
with the canonical resolve.alias + dedupe + define block
- app/bridge/vite.config.ts: header note pointing downstream consumers
at the README
No code changes. No new dependencies.
Mechanical primitive rotation across the inlined bridge UI:
- 4 <button> sites -> @hanzo/gui Button
* WalletConnect: Connect Wallet button
* WalletConnect: Connected address pill (disconnect)
* SwapForm: Reverse-chains button (the central ⇄)
* SwapForm: Submit/bridge button
- 1 <input> site -> @hanzo/gui Input
* AssetInput: amount field (uses onChangeText, the Tamagui-native
string handler, instead of the React onChange(e) signature)
Inline-style props (style={...}) are preserved verbatim on the new
components -- they round-trip through Tamagui's web-mode style merge.
Phase 3 R2.5 will lift those into theme tokens.
Out-of-scope per the R2 dispatch:
- <select> (ChainSelector, AssetInput asset picker) -- stays native
- Card / Stack / Text / Header / Footer -- still plain HTML
- title="..." HTML tooltips dropped: Tamagui Button's stack-based
types do not expose the DOM title attribute. aria-label is
preserved on every site so screen-reader behavior is unchanged.
Verification:
- cd pkg/bridge && pnpm typecheck -> clean
- cd app/bridge && pnpm build -> clean (750 modules)
- Bundle size (app/bridge index chunk): 25.99 kB -> 106.83 kB raw
(gzip 9.01 -> 38.86 kB). Delta is the Tamagui runtime; expected
and within the dispatch's "~150 KB acceptable" budget.
The SDK now ships with @hanzo/gui + @hanzogui/{core,helpers,constants,
themes,animate,animate-presence,button,input} as direct dependencies
(moved out of the optional peerDependencies block). Phase 3 R2 starts
swapping native primitives onto Tamagui-based @hanzo/gui components,
so consumers no longer have to install those packages themselves.
Three plumbing changes make this actually bundle in a consumer:
1. scripts/patch-hanzogui-exports.cjs (vendored from lux/exchange):
the published @hanzo/[email protected] + @hanzogui/*@7.0.0 packages ship
without a populated dist/, so their package.json main/module/exports
point at folders that do not exist. The patch rewrites those entries
to point at the included src/ at install time. Root postinstall hook
runs it deterministically on every pnpm install.
2. app/bridge/vite.config.ts: alias react-native -> react-native-web,
dedupe react/react-dom/react-native-web, set TAMAGUI_TARGET=web and
__DEV__=false. Minimum viable Tamagui wiring (mirrors the wiring
lux/exchange ships in a much larger config).
3. app/bridge/package.json: add react-native-web@^0.19.0 dependency
so the alias above resolves.
Result: pnpm install in a fresh checkout produces a working build with
zero manual setup. `pnpm build` in app/bridge succeeds.
Sweep through configs, docs, and ignore files after the structural
collapse in the previous commit. Updates fall into three buckets:
1) Restore the Dockerfile for app/bridge/
- app/bridge/Dockerfile: new file, Vite SPA → ghcr.io/hanzoai/spa,
same shape as the pre-collapse legacy Dockerfile, build context
is repo root so @luxfi/bridge (workspace:*) resolves.
- cmd/bridge/Dockerfile: switch UI-build stage to the workspace
build context so the new app/bridge (which depends on the
sibling pkg/bridge package) actually installs.
- Makefile build-ui target: drop the per-app build context for
the same reason — context must be repo root.
2) Update workspace docs to the new layout
- README.md: rename @luxbridge/app row → @luxbridge/lux-tenant,
port 3000 → 3001 (the slim tenant's vite config uses 3001).
- docs/LLM.md: drop the bridge3 row, rename app/bridge row to
@luxbridge/lux-tenant, update dev port.
- docs/{BRIDGE-STATUS,LOCAL-SETUP,CI-CD-DOCKER-IMAGES}.md:
`docker build … ./app/bridge` → `… -f app/bridge/Dockerfile .`
so docs reflect the workspace-rooted build context.
- .github/workflows/publish.yml: drop @luxbridge/app and
@luxbridge/app-v3 from the private-packages comment.
3) Delete AI-slop status reports + test harness
- CI_STATUS_REPORT.md, DEPLOYMENT_STATUS.md, WORKFLOW_AUDIT.md,
WORKFLOWS_SUMMARY.md — stale and referenced bridge3.
- test-bridge-ui.js — top-level Puppeteer/Playwright probe of the
deleted legacy UI; not a workspace test.
- .dockerignore: drop the test-bridge-ui.js entry now that the
file no longer exists.
Out of scope (untouched): pkg/bridge/src/{Bridge,config,types,mount}
and pkg/bridge/src/app/ — Phase 3 R2 is working there. The
`@luxbridge/app` mentions in those files are historical comments
explaining the prior workspace cycle, not live references.
Phase 1.5 inlined the bridge UI into pkg/bridge/src/app/ and added a
slim consumer at app/lux-bridge/. With the @luxbridge/app workspace
cycle severed there is no longer any reason to keep two tenant
directories or the legacy 400-file UI under app/bridge/.
This commit picks Path A:
- delete legacy app/bridge/ (406-file @luxbridge/app, now lives in
pkg/bridge/src/app/ as 16-file inlined SDK UI)
- delete unused app/bridge3/ (Remix @luxbridge/app-v3, never shipped)
- move app/lux-bridge/ → app/bridge/ (canonical tenant dir matches
bridge.lux.network DNS; "lux-bridge" was a temporary worktree name
needed while the legacy dir still existed)
- delete root Dockerfile.bridge-ui (referenced deleted app/bridge3
paths and the old Next.js standalone output; superseded by
app/bridge/Dockerfile which builds the Vite SPA into ghcr.io/hanzoai/spa)
After this commit:
- one and only one bridge tenant: app/bridge/ (slim, consumes @luxfi/bridge)
- SDK: pkg/bridge/ (publishes @luxfi/bridge with inlined UI)
- zero references to @luxbridge/app or @luxbridge/app-v3
- zero references to app/bridge3 or app/lux-bridge
The new @luxbridge/lux-tenant package is a thin Vite SPA that imports
@luxfi/bridge + @luxfi/brand and renders <Bridge config={...} />. It
is the deployable artefact behind bridge.lux.network and the
smoke-test surface that proves the SDK bundle isn't a stub:
$ cd app/lux-bridge && pnpm build
dist/index.html 0.71 kB
dist/assets/index-yIcXWMx5.css 0.89 kB
dist/assets/index-CN78T0a_.js 25.96 kB ← real React, not 91 bytes
dist/assets/client-BZEUzgg-.js 134.47 kB
The 25.96 KB chunk contains real `createRoot`, `mountBridge`,
`BridgeApp`, `SwapForm`, `ChainSelector`, `AssetInput`,
`WalletConnect`, `TransferStatus`, `Header`, `useSwap`, `useWallet`,
`useTransfers` — verified via sourcemap inspection.
This package coexists with the legacy heavy `app/bridge` tree (65
components, full @luxbridge/app on `main`). Once Phase 2's branch
lands, the heavy tree is retired and `app/lux-bridge` (or whatever
Phase 2 renamed it to) becomes the canonical Lux-branded tenant.
Brand config (Lux-specific only):
- name: 'Lux Bridge'
- primaryColor: #5b8def
- secondaryColor: #7a9ff5
- supportEmail: [email protected]
- docsUrl: https://docs.lux.network/bridge
The brand block is the only Lux-specific surface in this package;
everything else is the jurisdiction-neutral @luxfi/bridge SDK.
Bridge.tsx no longer lazy-imports @luxbridge/app — it imports the
inlined `BridgeApp` from `./app/` directly. The workspace dep on
`@luxbridge/app` is removed from pkg/bridge/package.json, and the
type shim `src/luxbridge-app.d.ts` plus the tsconfig path alias are
gone with it.
Phase 2 produced a 91-byte JS bundle because its slimmed `app/bridge`
package re-exported `App: FC = () => null` to satisfy this lazy
import. With the cycle severed, that workaround is unnecessary and
the SDK's bundle is what a tenant build actually consumes — see the
new app/lux-bridge consumer (next commit) emitting a 25.96 KB chunk
with real `BridgeApp`, `createRoot`, and all UI strings.
package.json deltas:
- version: 1.0.0 -> 1.0.1
- dependencies: drop @luxbridge/app + @luxbridge/settings (both
workspace-only)
- peerDependencies: add @hanzo/gui ^7 + the @hanzogui/* v7
primitive set as OPTIONAL peers. Phase 3 R2 will swap the
inline-styled primitives in src/app/components/ for these; until
then they're optional so `pnpm add @luxfi/bridge` doesn't drag
in 30 unused packages.
tsconfig.json: drop the `@luxbridge/app` path mapping.
README.md: replace the workspace-internals diagram with the actual
inline layout under src/app/.
Phase 1.5 — sever the @luxbridge/app workspace cycle that Phase 2
exposed as a P0 blank-page bug. The SDK now ships its own bridge UI
inline; there is no runtime dependency on any sibling tenant package.
Layout:
pkg/bridge/src/app/
BridgeApp.tsx top-level <BridgeApp/>
components/ Header, SwapForm, ChainSelector, AssetInput,
WalletConnect, TransferStatus, Card
hooks/ useWallet, useSwap, useTransfers
lib/ chains, assets, format
styles/theme.css minimal CSS variables (brand-* hooks)
Features covered:
- Header with brand block + wallet connect
- Swap form: From/To chain selector, asset input + select, quote
panel (rate / fee / ETA), reverse button, submit
- Wallet connect button + connected-address pill
- Transfer status feed with MPC-phase rendering (pending → signing
→ broadcasting → completed)
Design constraints:
- PQ-safe: no classical-crypto assumptions anywhere. The MPC signer
layer (wired in Phase 3 R3) plugs Ringtail-lattice + ECDSA-CMP
hybrid; nothing here defeats post-quantum.
- Leaderless / permissionless: chain + asset registries are static
client-side data, not bound to any central authority. Quotes are
deterministic display mocks; the real backend (also leaderless)
arrives in Phase 3 R3.
- Phase 3 R2 swap target: components are seam-isolated so swapping
inline-styled `<div>` / native `<select>` for @hanzo/gui v7
primitives is mechanical. The shape of BridgeApp.tsx stays stable.
Stats: 15 new files, 1336 LoC. Renders identical output to the
former @luxbridge/app App component for the four MVP features.
Align the SDK Protocol union with the canonical thresholdd protocol set
(cggmp21, frost, pulsar, corona, bls, doerner) plus magnetar from the
study/ research variant. lss and ringtail are removed entirely — lss
never had a leaderless-safe round and ringtail folded into the pulsar/
corona lattice family.
Classical: cggmp21 (ECDSA secp256k1), frost (EdDSA ed25519), bls, doerner (2-of-2).
Post-quantum: pulsar (MLWE), corona (RLWE), magnetar (research variant).
- pkg/bridge/src/types.ts: BridgeMPCConfig.protocol widened with JSDoc
- pkg/threshold/src/types.ts: Protocol type widened with matching JSDoc
- pkg/threshold/src/client.test.ts: 'lss' callsites → 'cggmp21'
- pkg/bridge/src/__tests__/config.test.ts: parametric it.each test
walking all 7 protocols, confirms setConfig({mpc:{protocol}}) accepts each
Verified: pnpm typecheck + pnpm test green on pkg/bridge (14/14) and
pkg/threshold (8/8).
- BridgeMPCConfig.protocol now mirrors @luxfi/threshold Protocol union:
'lss' | 'cggmp21' | 'frost' | 'bls' | 'ringtail'
- setConfig throws on conflicting clientId / iamOrg between top-level
legacy fields and auth.{clientId,orgSlug} so misconfigured tenants
fail loudly at boot rather than silently picking one source.
- vitest wired into pkg/bridge with 7 tests covering precedence guard.
Extend the @luxfi/bridge SDK contract so tenant apps wire identical
auth/KMS/wallet/MPC config across `<Exchange>` and `<Bridge>`. The new
blocks intentionally mirror the canonical `<Exchange>` prop shape from
@luxfi/exchange — same field names, same semantics.
New optional types in pkg/bridge/src/types.ts:
- BridgeAuthConfig (provider, issuer, clientId, idHost?, orgSlug?)
- BridgeKMSConfig (url)
- BridgeWalletConfig (walletConnectProjectId?, defaultChainId?, supportedChainIds?)
- BridgeMPCConfig (publicUrl, privateUrl?, protocol?)
BridgeConfig gains optional `auth`, `kms`, `wallet`, `mpc` fields.
Legacy `clientId` / `iamOrg` flagged @deprecated but preserved — no
existing consumer breaks.
Bridge.tsx + mount.ts comments updated to note the full config (incl.
new blocks) is seeded synchronously via setConfig before children read.
No runtime code change required — setConfig already stores the whole
object, so new fields automatically flow through to getConfig consumers.
README updated with a complete tenant example wiring all four new
blocks, plus per-block field tables.
Typecheck: clean (tsc --noEmit).
Tenants now consume @luxfi/bridge as an SDK (see zooai/bridge for the
canonical pattern). No per-tenant overlay image, no brand.json runtime
fetch. Each tenant's repo is a thin React app that imports the SDK +
their brand package and renders <Bridge config={...} />.
Lux-side brand lives in @luxfi/brand and is consumed at build time by
the Lux tenant app (currently app/bridge3, soon a peer @luxfi/bridge
consumer tracking the same pattern as @zooai/bridge).
Mirrors zoo/bridge/app/ pattern — a thin Docker overlay on top of the
bridge3 base image that bakes the Lux brand into the deployable
artefact. The bridge3 app already defaults to Lux branding via
brand.ts; this shim makes the brand explicit at the image layer so
bridge.lux.network and bridge.zoo.network have identical structure
and one-file-only differences.
Produces ghcr.io/luxfi/bridge:latest. New tenant white labels follow
the same five-minute drop-in pattern: copy app/lux-shim/ to
<org>/bridge/app/, swap public/brand.json, update Dockerfile labels.
Adds a canonical precompile address constants module the UI can consume
when constructing chain-native bridge / PQ verification calls. Mirrors
the source-of-truth address scheme from ~/work/lux/precompile/dex/types.go
(0x0400-0x044F) and ~/work/lux/precompile/{mldsa,mlkem,slhdsa,pulsar,
p3q,corona,magnetar,hqc}/contract.go (LP-4200 unified PQCrypto block at
0x012201-0x012208). hasNativePrecompiles() guards the seven Lux chain
IDs that expose the native verifier set; non-Lux chains continue to use
the user-facing teleporter contracts from settings.ts unchanged.
LUX_STRICT_PQ_BRIDGE profile (v1.0.7) consumes the same address space
on the node side; the UI now has a matching constant set.
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.
Mirrors the luxfi/explorer pattern: cmd/bridge/main.go embeds the SPA
build via go:embed and exposes /v1/bridge/* on a single port. Read paths
(networks, tokens, exchanges, limits) are answered natively from a YAML
config — no Postgres. MPC-heavy paths (quote, rate, swaps, explorer)
reverse-proxy to the legacy Node backend until the native port lands.
Image: cmd/bridge/Dockerfile produces ~25 MB distroless.
Config: cmd/bridge/networks.example.yaml.
Migration target: Hanzo Base (SQLite) + luxfi/mpc direct calls.
Drop the Liquidity / Liquidity Testnet network entries (chain IDs
8675309/8675310 with rpc.*.redacted.com nodes and LQDTY/USDL/BTCL/ETHL
currency lists) from the Lux teleport bridge UI's mainnet and sandbox
network constants. Liquidity is an external white-label deployment
that can register itself at runtime; it must not be hardcoded into
the OSS Lux bridge UI.
- Add workflow-sanity.yml to enforce canonical CI contract
- Consolidate docker.yml to matrix-build bridge + bridge/server images
- Delete docker-publish.yml (duplicate of docker.yml with stale lux-build runner)
- Strip bespoke docker buildx from server.yml (now handled by docker.yml matrix)
Refs: hanzoai/.github canonical Docker CI contract.
- Add workflow-sanity.yml to enforce canonical CI contract
- Consolidate docker.yml to matrix-build bridge + bridge/server images
- Delete docker-publish.yml (duplicate of docker.yml with stale lux-build runner)
- Strip bespoke docker buildx from server.yml (now handled by docker.yml matrix)
Refs: hanzoai/.github canonical Docker CI contract.
- Convert app/bridge/ from Next.js 14 app-router to Vite 8 SPA.
- New entry: index.html → src/main.tsx → src/App.tsx (wouter router).
- src/pages/{HomePage,AuthPage,CampaignsPage,CampaignDetailPage,SalonPage,
SwapPage,TeleporterSwapPage,UtilaSwapPage,TransactionsPage,NotFoundPage}.tsx.
- src/shims/next-{link,image,navigation,dynamic,headers,server,font/google,mdx}
+ sentry-nextjs map Next.js APIs onto wouter + plain DOM so existing
components compile unchanged. Aliased in vite.config.ts.
- Dockerfile (root + app/bridge): FROM node:22-alpine AS build → vite build →
FROM ghcr.io/hanzoai/spa COPY dist /public.
- Delete app-router dir, next.config.js, next-env.d.ts, middleware.ts,
sentry.*.ts. Keep app/explorer/ and docs/ untouched (still Next.js).
Tenant route resolution:
src/app/api/tenant/route.ts was a 17-line edge handler that called
lib/tenant.fetchTenant(). fetchTenant() only reads env vars and fetches a
public IAM /api/get-organization endpoint. No secrets. Moved to client:
lib/tenant.ts now reads import.meta.env.VITE_* and is called directly from
main.tsx + TenantProvider. No sidecar needed.
TODO:
- `pnpm install` in CI to pick up wouter, vite, @vitejs/plugin-react,
@sentry/browser (added to package.json).
- Pre-existing viem/teleport tsc errors remain (ignored as before via esbuild).
- Storybook config still references @storybook/nextjs — rewire to
@storybook/react-vite separately.
Add pruneSeen() that caps nonce tracking at 10000 entries per chain.
Called on every checkpoint save. Since nonces are monotonic, only
recent nonces need tracking — old ones are definitionally confirmed.
2 new tests verify pruning behavior.