Commit Graph
90 Commits
Author SHA1 Message Date
zeekay e661decabb chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases
already ship it. The deployed gateways still answer the old prefix, so remote
reads break until they are redeployed; the estate moves together rather than
sitting half converted.

Lines that record an observation - a probe, a status code, a past release -
keep the prefix they were written against, as do URLs on hosts that are not
ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.
2026-09-01 08:20:22 -07:00
zeekay b36c67dd96 the face is Zen, and it ships from @hanzo/font
One family. Font stacks name Zen and fall back to the native faces, tokens
name the role rather than a face, and the packages that used to supply a
face now point at @hanzo/font.

Self-hosted @font-face blocks are removed rather than renamed: rewriting the
family in one would leave a src pointing at a file this repo no longer ships.
@hanzo/font carries the binaries and declares the faces, so the local copies
go with the declaration that used them.

Comments naming a face we no longer use are deleted, not reworded.
2026-09-01 08:19:35 -07:00
zeekay c8390b8b5c Restore the chain prefix the running nodes answer
The rename to /v1/chain is real in luxd source, but the deployed fleet has not
taken it yet: probed just now, api.lux.network, api.lux-test.network,
api.zoo.network and api.hanzo.network each answer 200 on /v1/bc/C/rpc and 404 on
/v1/chain/C/rpc. Composing /v1/chain against those hosts reaches nothing, which
is the failure the rename set out to prevent.

The move belongs with the node redeploy, not ahead of it.
2026-08-30 21:25:48 -07:00
zeekay 2a0a574b1e chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain; the /v1/bc prefix it replaced is
gone, so anything composing it reaches nothing. Lines that record an
observation - a probe, a status code, a past release - keep the prefix they
were written against, as do URLs on hosts that are not ours.
2026-08-30 21:06:27 -07:00
zeekay 35078161c5 docs: chain routes are /v1/chain
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181):

  /v1/bc/<alias>/...   -> /v1/chain/<alias>/...
  /ext/bc/<alias>/...  -> /v1/chain/<alias>/...

Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
2026-08-30 17:10:41 -07:00
zeekay 15e43615f1 bridge: connect 0.2.1, and Cardano is one of the seven
The dependency was pinned "^0.1.0", which under semver can never reach
0.2.x. So the bridge — a surface listed as a working consumer of the
shared connector — was carrying the bug 0.2.0 fixed.

Measured in the installed tree: 0.1.0's dist/connectors.js opens with
SEVEN STATIC IMPORTS, so every page load eagerly pulled sats-connect,
@tonconnect/sdk, @crossmarkio/sdk and @polkadot/util-crypto whether or
not anyone touched those chains. That is the eager-import shape that
blanked the exchange SPA. 0.2.1's connectors.js has zero: a chain's
wallet library arrives when that chain is asked for.

getConnector became async across that boundary, so resolveConnector is
async and its one caller awaits. Both call sites were already inside
async functions, which is why this is three lines rather than a refactor.

Cardano now maps to a connector instead of null. The comment beside it
said connect was unavailable — true when written, and 0.2.1 ships
./cardano/connect (CIP-30) with ./cardano/verify (CIP-8 COSE_Sign1).
Same day as the ADA custody address, from the other side: the register
can now name a Cardano deposit address, and a holder can now sign in
with the wallet that holds it.

EVM and Lux still return null and go through wagmi deliberately.
@luxwallet/connect has no transaction surface at all — no
sendTransaction, no chain switching, verified by grep — so it is a
connect-and-identity package, not a wagmi replacement. The comments say
that now instead of implying a gap.
2026-08-30 17:10:41 -07:00
zeekayandHanzo Dev 94d775889a Our nodes serve /v1/, so ask them for /v1/
luxd registers /v1/ and nothing else; api.lux.network, api.zoo.ngo,
api.hanzo.network, api.pars.network and api.osage.network all answer 200 on
/v1/bc/C/rpc and 404 on /ext/bc/C/rpc. Every /ext/ path aimed at one of our
hosts was a request that could only fail.

/ext/ is Avalanche's prefix. Endpoints on api.avax.network keep it — they are
someone else's convention and a real bridge corridor depends on them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 17:10:41 -07:00
zeekay 6ee87f25ce Say what a person can act on, and paint it in ink they can see
The header named the signing protocol — `MPC · CGGMP21` — and a transfer row
badged it again. Neither is something a visitor can do anything with, and the
header is the surface everybody reads. The copy now says what a person needs:
which network, that a deposit address is involved, that no single party can
release the funds. The cluster still runs whatever BRIDGE_MPC_PROTOCOL selects.

The pickers could not be operated from a keyboard. Every key was answered on
the menu and nothing ever focused the menu, so opening with Enter left the
arrows dead. They are answered on the trigger now, which holds focus for the
whole interaction, with aria-activedescendant naming the row the cursor is on.
Each picker takes a label bound to it or a name read with it, and the prop type
admits no third shape.

The brand primary is #000000 and the SDK painted its selected row, its live
phase dot, the refuel toggle and the header's only call to action with it —
black on a black page. Marking a thing is the neutral scale's job. The accent
stays as the published brand hook; nothing inside the SDK reads it.

The rest is the page reading like the exchange it sits beside: a flat ground,
panels separated by an edge rather than a shadow, one type scale, field names
in one case, and hints raised out of the ink you have to lean in for.

@luxfi/bridge 1.0.9.
2026-08-30 17:10:40 -07:00
zeekay 703bd4f9da license: relicense Lux Bridge to BSD-3-Clause
Lux Bridge is core network infrastructure meant for OSS developers to
build on, so it moves from the proprietary Lux Ecosystem License
(patent-protected tier) to BSD 3-Clause — the same permissive license
the rest of the Lux core stack carries (cli, sdk, genesis). Also
unifies the seven per-package license fields (MIT/ISC/BSD mixed) onto
the one repository license. No vendored modified upstream, so no
separate NOTICE is owed; third-party deps keep their licenses through
the module graph.
2026-08-08 03:15:36 -07:00
zeekayandHanzo Dev f60c28ece4 @luxfi/utila could not build — name its outDir
RollupError: [plugin typescript] @rollup/plugin-typescript: Path of
    Typescript compiler option 'outDir' must be located inside the same
    directory as the Rollup 'file' option.

No tsconfig in the chain sets outDir, so the plugin defaults it to the
tsconfig's own directory — pkg/utila — and then rejects it, because it requires
outDir to sit inside the directory each Rollup `file` writes to. Both outputs go
to dist/, so one value satisfies both.

This fails identically on a laptop, so it is not a container problem: the
package has simply been unbuildable since the toolchain moved under it, and
nobody noticed because dist/ was built once long ago and has been sitting there
older than its own sources. It only surfaced now because app/server's image
needs to build it from a clean checkout.

Verified: dist/index.cjs.js exports createGrpcClient and
serviceAccountAuthStrategy, which are what domain/utila.ts and
domain/cosigners.ts import.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 21:17:32 -07:00
zeekayandHanzo Dev d83c58ab7e say why a quote was refused, instead of "Quote failed (400)"
This is the UI that is actually deployed — the root Dockerfile builds app/bridge,
which mounts @luxfi/bridge, and lux-bridge, zoo-bridge and pars-bridge all run
that image. Its destination picker offers EVERY network for every source, so
choosing a pair the bridge cannot route is an ordinary thing to do, not an edge
case.

Until now such a pair returned 200 with a fabricated number. It now returns 400
and names the problem — "no bridge route between LBTC and ZETH" — and that
sentence is the whole diagnosis. The hook was throwing it away and rendering the
status code, which reads as the site being broken rather than as the route not
existing.

Falls back to the status when the body carries no message, so nothing ever
renders "undefined" or "[object Object]".

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 20:44:18 -07:00
venuswhispersandzeekay 4efdd7c631 test(bridge): cover ChainSelector's family-label mapping + reverse lookup
Small wrapper around Select, but two real bugs live in the mapping
layer if it regresses: the family->display-name table (with a
defensive fallback to the raw uppercased family for anything not in
the table -- covers a future ChainFamily addition landing before its
label does), and the SelectOption->Chain reverse lookup on commit,
where a wrong id match would silently call onChange with the wrong
chain entirely.
2026-08-06 21:21:42 -07:00
venuswhispersandzeekay 9da717cdad test(bridge): cover Select's keyboard nav + dismiss behavior
Select's own header comment documents a real interaction contract --
click-outside closes, Escape closes, arrow keys move the highlight,
Enter/Space commits -- none of which had a dedicated test.
AssetInput.test.tsx exercises Select only incidentally (open + click
one option); this covers the contract directly, on the component that
owns it.

20 tests: open/close via click and via ArrowDown/Enter/Space at the
trigger, click-outside dismissing without committing (vs. a click
inside the popover, which must NOT dismiss), Escape closing and
returning focus to the trigger, arrow-key highlight movement clamped
at both ends, Home/End jumping to the first/last option, Enter and
Space both committing the HIGHLIGHTED option (not just whatever was
originally selected -- these are different indices once you've
navigated), hover updating the highlight, aria-selected reflecting the
current value, the secondary-label render, and the renderTrigger /
label props.

pkg/bridge test suite 130 -> 150 tests, all passing; typecheck clean.
2026-08-06 21:21:41 -07:00
venuswhispersandzeekay 2a90d83c60 test(bridge): TransferStatus coverage
TransferStatus renders the live transfer feed -- phase banners, the
deposit-address panel, MPC session detail, layered-cosigner badges --
the surface a user watches while their swap is mid-flight. Zero test
coverage despite the conditional logic (which banner shows for
lastError vs refunding vs refunded vs a terminal failure) being exactly
the kind of thing that's easy to get subtly wrong in a way that only
surfaces as "why does this say refunding when it already refunded."

17 tests cover: empty state, chain-name resolution (including the
fallback to the raw chainId for an unregistered chain), the deposit
panel's visibility rule (depositAddress set AND phase not yet terminal)
plus its copy-to-clipboard button, the MPC session line (including the
"session aborted" special case), the two layered-cosigner badges
appearing independently per config, and all four phase-banner branches
mutually excluding each other correctly.

useNetworks and getConfig are mocked (react-query + a mountBridge-gated
singleton this component's own logic doesn't need exercised); Card
renders for real -- it's a 37-line style wrapper with no logic.
2026-08-06 21:21:41 -07:00
zeekay 83f826794a kms.lux.cloud is the brand host; kms.lux.network is retired
The .network host was retired 2026-07-27 and no longer resolves at all, so every
reference to it named an address that cannot answer. The brand host is .cloud,
matching kms.zoo.cloud and the rest of the white-label convention.

Left alone deliberately, because rewriting them would invert what they say: the
LLM.md line that RECORDS the retirement, the e2e spec that ASSERTS
kms.lux.network must not resolve, and the recorded applies + cluster backups,
which are faithful accounts of what was applied and are not config.
2026-08-05 07:51:13 -07:00
zeekayandHanzo Dev b554dea13a release: @luxfi/bridge 1.0.8, @luxfi/threshold 1.0.0
1.0.7 shipped `"@luxfi/threshold": "workspace:*"` in dependencies, so no
consumer outside this monorepo could install it — `npm publish` does not
rewrite the workspace protocol and `pnpm publish` does. npm versions are
immutable, so correcting it needs a bump.

@luxfi/threshold had never been published at ALL, which is why fixing the
specifier alone would not have been enough: the rewrite points at the
sibling's real version, and that version has to exist on the registry.
Published it first (1.0.0), then bridge, so the graph resolves.

Both verified before publishing — 0 workspace: refs, real dist (133 files
for bridge, 24 for threshold), and bridge's dependency rewritten to the
concrete "@luxfi/threshold": "1.0.0".

The fail-closed guard added in d7ec4757 ran on this publish and passed under
pnpm, which is what it is for.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 00:28:20 -07:00
zeekayandHanzo Dev d7ec4757da fix(publish): refuse to publish under npm — workspace:* must be rewritten
@luxfi/[email protected] is broken on npm right now: its published manifest
carries `"@luxfi/threshold": "workspace:*"`, a specifier that only resolves
inside this monorepo. Every external consumer fails at install.

Cause: `workspace:*` is rewritten to a real version by `pnpm publish` and
NOT by `npm publish`. This package had no publish script, so it was
published by hand with npm and shipped the specifier verbatim. The source
still declares it, so the next hand-publish breaks again the same way.

The defect is invisible in CI and inside the monorepo — pnpm resolves
workspace:* fine locally, and only an external consumer ever sees it. So
the check belongs at publish time and has to fail closed: prepublishOnly
now refuses unless the publishing client is pnpm, including when the user
agent is absent.

Verified: refuses under npm, refuses with no user agent, passes under pnpm.

Not done here: republishing 1.0.7. That is outward-facing and affects
anyone already pinned, so it needs an owner.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 23:41:14 -07:00
zeekayandHanzo Dev f2f4d8a33d fix: complete the TypeScript 7 tsconfig migration
The first pass left three classes of config that BOTH tsc 5.9 and tsc 7
reject. Each was proven against both compilers before changing:

  TS5090  A `paths` target must be relative once `baseUrl` is gone. The
          first pass skipped the "./" prefix wherever baseUrl pointed at
          the config's own directory, reasoning it was semantically
          equivalent. It is not — without baseUrl a non-relative target
          is rejected outright, by 5.9 as well as 7.

  TS5110  `moduleResolution: node16` requires `module: node16`. The first
          pass mapped commonjs projects to node16 resolution alone, which
          BROKE those configs for the current toolchain. Both are now set.

  TS5102  `downlevelIteration` is also removed in TS7; it was missing from
          the dead-flag list.

Verified: repos that tsc 7 previously refused (base-studio, js-sdk, kv-js)
now report zero config errors on tsc 7 AND tsc 5.9.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 20:17:34 -07:00
zeekayandHanzo Dev a702429811 Bridge and threshold RPC on /v1; @luxfi/bridge 1.0.7
luxd serves exactly one prefix, /v1 (node/server/http/server.go:33), and
has never served /ext. createBridgeRPCClient's two defaults are the SDK's
whole contract for a caller who passes only nodeUrl — both pointed at a
404, and a caller had no way to see it from the outside.

Two Zoo endpoints were also aimed at the wrong network entirely:
api.lux.network/ext/bc/zoo/rpc and api.zoo.network/ext/bc/Z/rpc. Zoo is
sovereign on api.zoo.network (200200) and api.zoo-test.network (200201),
both confirmed serving /v1/bc/C/rpc; /v1/bc/Z/rpc and /v1/bc/zoo/rpc are
404. Env is the hostname, never a path segment.

Patch-bumps @luxfi/bridge 1.0.6 -> 1.0.7 so the fixed defaults ship. Note
1.0.6 was never actually on the registry — `npm view @luxfi/bridge` is a
hard 404 with no versions, while @luxfi/ui resolves fine from the same
scope, so this is a first publish rather than a re-publish.

Tests pin both defaults and assert the overrides still win. They fail on
/ext and pass on /v1. Full suite: 11 files, 94 tests.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 19:24:56 -07:00
zeekayandHanzo Dev e4caa184ee build: migrate tsconfig to TypeScript 7 (native compiler)
TypeScript 7 is the native Go compiler and removes `baseUrl` and
`moduleResolution: node|node10`. Both appear here, so `tsc` from TS7
refuses the config outright (TS5102 / TS5108) and cannot typecheck.

`paths` targets resolve relative to `baseUrl` when it is set and relative
to the tsconfig file otherwise. Every `baseUrl` folded here already
pointed at the config's own directory, so dropping it moves nothing and
the targets are left byte-identical. Where a baseUrl pointed elsewhere,
each affected target was rewritten as join(baseUrl, target).

`moduleResolution` was chosen from the declared `module`: commonjs ->
node16, esnext/preserve -> bundler. Configs whose `module` is unset or
exotic were left alone rather than guessed at.

The result is accepted by BOTH toolchains, so nothing has to upgrade
TypeScript in lockstep. Verified on hanzo/chat packages/api: tsc 5.9
779 -> 778 errors (no regression), and tsc 7.0.2 now runs the project
in 2s where it previously refused the config.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 14:18:44 -07:00
zeekayandHanzo Dev 4d3a799e6d fix(bridge): white-label description meta + ship Basel Grotesk webfont
Two white-label defects surfaced by the universe branded e2e suite:

1. HEAD <meta name="description"> stayed "Cross-chain bridge for the Lux
   Network." on Zoo/Pars surfaces (title/favicon/logo were already
   white-labeled) — a foreign brand in a non-Lux head. applyBrandMetadata
   now rewrites description + og:description + twitter:description from the
   brand block (creating them if absent). bridge.config.ts derives the
   description from the already-white-labeled brand name (single source;
   BRIDGE_DESCRIPTION overrides), so a non-Lux surface never leaks "Lux".
   Extended the __ENV.js contract (docker-entrypoint.sh + public/__ENV.js).

2. Body font fell back to Times serif — the intended face never loaded and
   .bridge-root only styles the mount subtree, leaving <body> at the UA
   default. Ship Basel Grotesk (the shared BRAND_TOKENS face) as a bundled
   webfont, prepend it to --bridge-font, and apply the font to html/body so
   the page owner inherits the brand sans stack.

Tests: pkg/bridge 91/91 pass (config.test.ts adds DOM coverage for the
description rewrite + the zero-foreign-leak invariant).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-20 14:48:50 -07:00
zeekayandHanzo Dev ff351f7ac1 build(threshold,core): silence TS7 node10 moduleResolution deprecation
The bridge image build (root Dockerfile) compiles pkg/threshold + pkg/core
to dist/ before the Vite app build. Under the runner's TypeScript 7 the
deprecated moduleResolution:node (node10) is a hard error (TS5107), so
'Build & push' failed at 'pnpm -C pkg/threshold build'. Add the prescribed
ignoreDeprecations:6.0 escape (inherited by the .cjs.json configs via
extends) — no emit change. Unblocks ghcr.io/luxfi/bridge:main.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-19 01:46:53 -07:00
fd3bc75a29 bridge: sync latest — docs trim, tenant config, internal broadcast/substrate/txassembler fixes (#406)
Squash of the local main lineage onto the canonical origin/main. Net tree only
(62 files): stale docs pages removed, tenant config + tests updated, minor Go
fixes, refreshed opnet-watcher build. No third-party white-label branding in
code or history.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-07 12:39:25 -07:00
zeekay 9490537379 chore(deps): @luxwallet/* file: → published npm ^0.1.0 2026-06-24 11:11:46 -07:00
zeekay 316ba17273 feat(bridge): @luxwallet/connect multi-chain wallet + @luxwallet/chains registry
Connect a wallet on every bridge ecosystem, not just EVM. wagmi stays the
EVM/Lux deposit signer; @luxwallet/connect adds the connect/identity layer
for Solana, Bitcoin, TON, XRP and Polkadot — chains wagmi cannot reach.

- useLuxWallet: non-EVM connect via getConnector(chain).connect() + SIWx login
- useBridgeWallet: composes wagmi + luxwallet, routes by chain family
- WalletConnect: family-aware — EVM picker vs non-EVM single-tap connect
- chains/registry: DEFAULT_CHAINS derived from @luxwallet/chains (one source
  of truth for the selector); covers all six connectable ecosystems
- wallet-family: the one place mapping bridge family -> wallet stack
- SwapForm: source/dest address resolved across both stacks
- TON manifest for the tenant app; deps via file: (TODO -> npm)

Additive + non-breaking: existing EVM flow unchanged. No WalletConnect
projectId on the non-EVM path. MIT/Apache/ISC deps only, zero GPL.

87 tests pass (8 new); tsc clean for SDK + tenant app.
2026-06-24 09:20:14 -07:00
zeekay 34f82eaa71 scrub branding + corona→corona (OSS brand hygiene) 2026-06-10 23:44:29 -07:00
zeekay 2221f9af31 chore: scrub brand references from tenant config and comments
- Remove  /  /  doc-comment mentions
- Remove 'liquid' from SupportedChains allowlist (-specific
  chain identifier per CLAUDE.md, not a generic chain family)
- Update tenant config example strings and tests accordingly
- Trim BRAND-AUDIT.md to drop specific  references while
  keeping the foreign-brand rule
2026-06-10 11:45:08 -07:00
zeekay ce78badda9 WIP: session checkpoint 2026-06-05 (codec rip in flight) 2026-06-05 15:33:51 -07:00
zeekay 70c294d771 fix: gofmt -s across repo (CI format check) 2026-06-02 11:38:14 -07:00
zeekay 784318262a feat(tenant): promote internal/tenant to pkg/tenant for external imports
The tenant.Config primitive is the public contract that downstream
shim repos (liquidity/bridge, hanzo/bridge-shim, zoo/bridge-shim,
pars/bridge-shim) consume to validate their own tenant.yaml. Keeping
it under internal/ blocked that — shims couldn't import it.

This patch moves the package one directory level (internal/tenant →
pkg/tenant), updates the three call-sites in cmd/bridge, and changes
nothing else. All tests still pass.

Tests:
- ./pkg/tenant 26 tests, PASS
- ./cmd/bridge full suite PASS
2026-05-28 01:36:40 -07:00
zeekay 7282219e12 merge: pull main (CI/docker/docs cleanup, brand-neutral)
# Conflicts:
#	.dockerignore
#	app/bridge/package.json
#	compose.local.yml
#	pkg/bridge/src/app/hooks/useTransfers.ts
#	pnpm-lock.yaml
2026-05-28 01:07:23 -07:00
venuswhispers 21020685d9 feat: refunding has been working 2026-05-28 00:10:03 +02:00
venuswhispers a35e775173 feat: simple base swap is working now 2026-05-27 22:04:22 +02:00
zeekay 8f075365ae chore: brand-neutral cleanup — remove cross-tenant references 2026-05-25 15:12:52 -07:00
venuswhispers 202785aff8 feat: testnet configuration set, updated deposit workflow with mpc workflow, not teleport 2026-05-25 18:20:04 +02:00
venuswhispers 0ff373526e feat: wallet connect function updated 2026-05-23 23:43:10 +02:00
zeekay ad4421264b feat(pkg/bridge): @hanzo/gui 7.0.0 -> 7.2.2 + dual-name compat (SDK 1.0.5 stable)
User asked to confirm the rebuild is up to date with @hanzo/gui framework
and to run both Vite tenant + Next.js explorer locally to verify. Playwright
caught a regression: bumping @hanzo/gui to 7.2.2 (latest published) made
the bridge tenant render a blank page with 'createGui is not a function'.

Root cause: @hanzo/gui's published dist artefacts use the upstream Tamagui
symbol names verbatim (createHanzogui / HanzoguiProvider / getDefault-
HanzoguiConfig). The consumer-side scripts/patch-hanzogui-exports.cjs
ONLY runs when dist/ is empty — which was true for 7.0.0 (broken
publish) but is false for 7.2.x (real dist). So 7.2.2 exposes the
upstream Hanzogui* names and the bridge SDK code (which calls the
renamed createGui / GuiProvider / getDefaultGuiConfig) finds undefined
exports.

Fix: SDK accepts BOTH naming surfaces. pkg/bridge/src/mount.ts and
Bridge.tsx try the renamed symbol first (works on 7.0.0 + patch), fall
back to the upstream Hanzogui* name (works on 7.2.x dist). Throw a
clear error if neither is exported.

Playwright in-browser verification after the fix:
  Title:           'LUX Bridge'
  #bridge-root:    1 child rendered (Tamagui tree)
  Console errors:  0  (only theme-token warnings, harmless)
  UI rendering:    header (LUX Bridge + Connect Wallet) ✓
                   swap form (FROM Lux Network → TO Ethereum) ✓
                   TRANSFERS panel ✓
                   footer (Network: mainnet · Docs · [email protected]) ✓

Also bumped:
  - @hanzo/gui  ^7.0.0 → ^7.2.2 (latest published)
  - @luxfi/bridge  1.0.4 → 1.0.5

Plus an incidental fix to app/explorer/components/SwapHistory/StatusIcons.tsx
(pre-existing operator-precedence bug: 'a' + b ?? '' had its ?? branch
unreachable; wrapped className ?? '' in parens). Found while attempting
to run the legacy explorer locally; we're deprecating that whole app
per #387 but the type error blocked next dev / next build cold.

Tests:
  - SDK pkg/bridge:   58/58 passing
  - SDK typecheck:    clean
  - app/server:       77/77 passing (unchanged)
2026-05-22 21:18:56 -07:00
zeekay 2f0312d6de feat: Cloud HSM (GCP real, AWS/Azure/Vault stubs) + f-chain native cosigner (#386)
User feedback was that my first Cloud HSM pass had three architectural
weaknesses: (1) per-provider top-level kinds vs a single cloud_hsm
discriminator, (2) SA-JSON-in-KMS auth pattern, (3) missing first-class
algorithm + structured error codes. Plus the orthogonal point that for
bridge.lux.network specifically the right topology is m-chain (native
MPC) + optional f-chain (FHE attestation) and NO external cosigners.

This commit applies all of that.

SDK (@luxfi/bridge 1.0.4 → 1.0.5):

  - BridgeCloudHsmConfig — single, uniform shape:
      provider: 'gcp_kms' | 'aws_kms' | 'azure_key_vault' | 'vault_transit'
      keyRef: <self-describing per-provider URI form>
      algorithm: 'secp256k1_ecdsa_sha256' | 'ed25519' | 'rsa_pss_sha256'
      identityHint?: <non-secret workload-identity scope hint>
    Replaces the three separate gcpKms / awsKms / azureKv blocks from
    the first pass. Tenants layer multiple HSMs via the cloudHsm array
    on BridgeMPCConfig.

  - BridgeFChainConfig — Lux Network native FHE attestation:
      publicUrl: <f-chain cluster URL>
      scheme?: 'ckks' | 'bgv' | 'bfv'
    Lives alongside m-chain on the Lux primary network. Cosigns the
    same txHash that m-chain signed via FHE-secured key material so
    the message never crosses a plaintext threshold quorum. PQ-safe.

  - Wire shape (bridge-api.ts CosignerIntent) collapses the three
    cloud_hsm variants into one canonical record with the provider
    sub-discriminator. Adds the fchain wire kind.

  - useTransfers iterates cfg.mpc.cloudHsm[] + emits the optional
    fchain intent.

Backend (app/server):

  - CloudSigner interface matches user's exact spec:
      readonly provider: CloudHsmProvider
      sign(input: CloudSignInput): Promise<CloudSignResult>
    where CloudSignInput / CloudSignResult carry the structured metadata
    (keyRef, digest, algorithm, identityHint, requestId / signature,
    publicKeyRef, provider, keyVersion, signedAt).

  - CloudSignerError class with stable CloudSignerErrorCode union:
      permission_denied | key_not_found | key_disabled
        → maps to CosignResult.status = 'rejected'
      algorithm_mismatch | invalid_digest
        → maps to 'rejected' (terminal config errors)
      rate_limited | network_error | provider_unavailable
        → maps to 'failed' (retryable infra errors)
    statusForErrorCode() does the mapping; the dispatcher never has to
    string-match on error messages again.

  - Real GCP CKMS adapter:
      ✅ Workload-identity ONLY (no SA-JSON-in-KMS fallback). The
         backend trusts the cloud's native identity layer.
      ✅ identityHint is non-secret context, not credentials.
      ✅ Algorithm allow-list — only secp256k1_ecdsa_sha256 wired today;
         ed25519 / rsa_pss_sha256 reject early with algorithm_mismatch.
      ✅ Error mapping covers gRPC codes 5/7/8/9/14/16 + string
         fallbacks. mapGcpError centralizes it.

  - AWS KMS / Azure Key Vault / Vault Transit adapters are stubs that
    return CloudSignerError 'provider_unavailable' with a clear marker.
    Each stub has a full implementation sketch in comments so the next
    dev (or me, next session) drops in @aws-sdk/client-kms / etc. and
    wires through cleanly.

  - f-chain (FHE attestation) stub — waiting on @luxfi/fchain workspace.

  - Persistence: cloud_hsm rows persist as 'cloud_hsm:<provider>' in
    CosignerStep.kind to avoid a Prisma migration. public_id = key_ref;
    api_host = identity_hint; vault_id = algorithm. rowToIntent inverts
    cleanly. First schema migration round will split into dedicated
    columns; the compound kind is a stable round-trippable hack.

Tests: 77/77 passing.
  pkg/bridge:   58/58 (unchanged)
  app/server:   16 validator (now covers all 4 cloud providers + fchain)
              + 10 Fireblocks
              + 13 Utila
              +  7 persistence
              + 19 GCP KMS + Cloud HSM stubs + fchain
              + 12 new cloud-HSM validator cases

Lux Network topology (bridge.lux.network and all Lux-branded tenants):
  - Primary: m-chain (native CGGMP21 / FROST threshold MPC)
  - Optional: f-chain (FHE attestation, also native to Lux)
  - NO external cosigners (Utila / Fireblocks / Cloud HSM) by default
  - These cosigner blocks exist on BridgeMPCConfig for white-label
    tenants whose policy requires institutional / cloud-HSM cosign.

User correction record:
  > 'Use a provider sub-discriminator, not separate top-level kinds' ✓
  > 'Do not fetch SA JSON from KMS at signing time' ✓
  > 'Keep browser SDK config declarative only' ✓
  > 'Separate SDK config from backend signer runtime config' ✓
  > 'Normalize algorithms explicitly' ✓
  > 'Shared interface should return structured metadata' ✓
  > 'Preserve provider-specific error classification' ✓
  > 'rely on just b-chain and m-chain ... f-chain for FHE' ✓ (fchain
    config is optional + native; external cosigners are opt-in only)
2026-05-22 20:27:02 -07:00
zeekay a5f9a05008 feat(pkg/bridge): jurisdiction-neutralize SDK (1.0.3 -> 1.0.4)
User pointed out that @luxfi/brand should be used 'for all lux apps /
projects' — confirming the canonical brand sourcing principle. Audit
pass against the bridge stack found two leaks of Lux-specific identity
INTO the jurisdiction-neutral SDK:

1. `pkg/bridge/package.json` declared `@luxfi/brand` as a direct
   dependency — but the SDK source never imports it. Dead dep and a
   layering violation: Zoo / Hanzo / any white-label consumer of
   `@luxfi/bridge` shouldn't pull Lux brand metadata into their bundle.
   Dropped.

2. `pkg/bridge/src/app/lib/wagmi-config.ts` had a hardcoded SSR
   fallback URL `https://bridge.lux.network` for the WalletConnect
   metadata block. On client-side hydration this is overridden by
   `window.location.origin`, but the first SSR-render frame was
   leaking the Lux hostname into Zoo / Hanzo tenant builds.

   Replaced with `deriveAppOrigin(cfg.apiHost)` — strips the leading
   `api.` subdomain from the configured API host so the fallback is
   tenant-derived. `api.bridge.lux.network` → `bridge.lux.network`,
   `api.bridge.zoo.network` → `bridge.zoo.network`. Catch-all
   fallback is `https://localhost` if the URL fails to parse.

3. `docs/LLM.md` gains a 'Canonical brand sourcing' subsection
   documenting the principle:
   - `@luxfi/brand` (~/work/lux/brand) is the ONLY source of Lux
     brand identity for ALL Lux-ecosystem apps (bridge, exchange,
     id, pay, etc.)
   - SDK is jurisdiction-neutral; tenants supply brand at mount time
   - App-specific endpoints (api.bridge.lux.network) are independent
     of brand.json's appDomain (lux.exchange)
   - Other Lux apps follow the same pattern; any hardcoded brand
     string in a Lux app is a leak — file as a follow-up issue.

SDK 1.0.3 -> 1.0.4. Additive + neutralization only — no functional
behavior change for Lux consumers; Zoo / Hanzo SSR frames now show
their own hostname instead of bridge.lux.network.

Tests: 58/58 SDK unchanged.
2026-05-22 17:41:39 -07:00
venuswhispers 07e5ec6cb1 feat: UI bundle svg logo 2026-05-22 23:02:57 +02:00
venuswhispers 5216f4ce14 feat: UI- mobile responsive 2026-05-22 22:54:00 +02:00
venuswhispers d4ced5d3d5 feat: bridge UI part is motly finished 2026-05-22 22:05:31 +02:00
venuswhispers f4c6a47e51 chore: env has been set up successfully 2026-05-22 15:41:58 +02:00
zeekay 9dc0a48578 test(pkg/bridge): 5 cosigner SDK tests — wire-shape + no-secret-leak (issue #390 partial)
Covers the SDK side of the layered cosigner contract while issue #386's
backend implementation is in progress:

  1. Omits cosigners[] entirely when no layered providers configured
     (backwards compat — no behavior change for existing tenants).
  2. Forwards utila intent in snake_case, public IDs only.
  3. Forwards fireblocks intent in snake_case, public IDs only.
  4. Forwards both utila + fireblocks together (belt + suspenders).
  5. Omits optional sub-fields (api_host, vault_id) when unset — no
     'foo: undefined' on the wire.

Each test that forwards an intent also asserts the full request body
contains zero secret-like substrings (private_key, api_secret, jwt,
token, service_account_private_key) — defense in depth against any
future regression where a consumer's bridge.config.ts accidentally
forwards a secret.

Pairs with the 16 server-side validator tests in app/server/src/domain/
__tests__/cosigners.test.ts — together they pin the wire contract from
both ends.

Total SDK tests: 53 → 58. All passing.
2026-05-22 00:26:50 -07:00
zeekay f6841b2863 feat(pkg/bridge): optional Utila + Fireblocks layered cosigners (1.0.2 -> 1.0.3)
External MPC custodians can now be layered ON TOP of the native Lux
threshold network as additional cosigners. The bridge backend enforces
2-of-2 (native + listed cosigner) before releasing settlement. Use when
tenants are already on Utila or Fireblocks for institutional custody and
need regulated-cosigner gating without giving up the native threshold
property.

Browser-safe by construction: the SDK declares only PUBLIC identifiers
(orgId, clientId, apiKey id, vault ids). The Utila JWT and the
Fireblocks secret key live on app/server/ (sourced from KMS at boot).
No secret material is ever shipped in the page bundle.

Surface:

  BridgeMPCConfig {
    publicUrl?, privateUrl?, protocol?       (existing native MPC)
    utila?:      BridgeUtilaConfig           (new, optional)
    fireblocks?: BridgeFireblocksConfig      (new, optional)
  }

publicUrl is now optional too — pure-external custody (utila/fireblocks
only, no native threshold) is supported. The runMpcSignSession helper
throws if it's called without publicUrl, and useTransfers gates the
client-side sign session on its presence.

Per-swap, the SDK forwards a cosigners[] array to POST /api/swaps
carrying just the public IDs. The backend pairs each entry with its
KMS-held secret half and completes the cosign. Both blocks may be set
together (belt + suspenders) — backend enforces all listed.

Tests: 53/53 passing. Typecheck clean. No new runtime deps in pkg/bridge
(utila/fireblocks SDKs live on the server, not the browser bundle).
2026-05-21 18:40:15 -07:00
zeekay 4b70d86d97 pkg/bridge: bump @luxfi/bridge 1.0.1 -> 1.0.2 (Phase 3 R3 — real MPC wallet wiring) 2026-05-21 17:36:00 -07:00
zeekay 1aa7d499f4 pkg/bridge: useTransfers POSTs real swaps + polls server, BridgeApp installs WagmiProvider
useTransfers replaces the setTimeout phase progression with the real
flow:
  1. POST ${cfg.apiHost}/api/swaps with the order (Idempotency-Key
     header set to the local transfer id, server snake_case body).
  2. Subscribe via GET /api/swaps/:id polling at 2s; map the server's
     free-form status string → TransferPhase via statusToPhase()
     (signing/broadcasting/completed/failed detection).
  3. On entry into the signing phase, start an MPC threshold session
     via runMpcSignSession() when the server record carries the
     keyId + messageHash. Progress is patched onto the Transfer.mpc
     field so the UI can render real signer activity.
  4. On unmount or clear(): abort every per-transfer controller so
     polls + MPC sessions stop cleanly.

Destination address defaults to the connected wallet (self-send across
chains is the common case); tenants needing sweep-to-treasury flows
pass an explicit address. New cases marked failed cleanly: no wallet
connected, unsupported chain pair, server hard-error.

submit() kept as a back-compat alias for initiate() so SwapForm.tsx
(off-limits — Phase 3 R2.5 is in parallel on components/) doesn't have
to retouch. The richer destinationAddress field is optional.

BridgeApp.tsx wraps its inner tree in <WagmiProvider> +
<QueryClientProvider>. The Config is built once per mount from
BridgeConfig.wallet via buildWagmiConfig(); the QueryClient defaults
disable refetch-on-focus/reconnect (bridge quotes are request-scoped
and shouldn't refresh behind the user's back).

4 tests cover initiate POST shape, the wallet-not-connected failure
mode, the unsupported-chain-pair failure mode, and clear() aborting
polls.
2026-05-21 17:28:05 -07:00
zeekay 68cfe9a289 pkg/bridge: replace useSwap static rate matrix with real backend quotes
useSwap now hits ${cfg.apiHost}/api/quote (GET, query params per the
server's app/server/src/routes/quote.ts) with a 300ms debounce. Maps
bridge chain ids → server internal_name (ETHEREUM_MAINNET / LUX_MAINNET
/ etc, suffix-swapped for testnet/devnet env). In-flight requests are
aborted via AbortController on every keystroke so the backend isn't
flooded.

New SwapState fields: quoting (boolean) + quoteError (string | null).
quoteError surfaces 5xx + network errors to the UI rather than blanking
the quote silently. AbortError is suppressed (newer keystroke).

The previous static USD-rate matrix is gone — pricing now comes from
the server's getTokenPrice oracle. The Quote shape is preserved
(outAmount/feeUsd/destGas/etaText/rate) so SwapForm doesn't need to
change; minOut is added for slippage protection on submission.

5 tests cover debounce, same-chain noop, invalid input, server error,
and reverse-swap state.

Also flips vitest config to happy-dom so hook tests can mount under
a real DOM-ish environment (jsdom would also work; happy-dom starts
faster on cold init).
2026-05-21 17:28:05 -07:00
zeekay 764b32317f pkg/bridge: replace useWallet stub with real wagmi-backed wallet hook
useWallet now wraps wagmi's useAccount + useConnect + useDisconnect +
useSignMessage + useSwitchChain. Connector preference: WalletConnect
(when wallet.walletConnectProjectId is supplied) → injected (MetaMask,
Rabby, etc.) → Coinbase Wallet → first available.

Adds disconnect(), switchChain(bridgeId), signMessage(msg) to
WalletState — capabilities the SwapForm submission flow + future MPC
co-signing will need.

DEV fallback preserved: when import.meta.env.DEV is true AND no
connector is registered, the hook still throws a clear error
("install MetaMask, or configure wallet.walletConnectProjectId") rather
than silently faking a connection. The display-only DEADBEEF address
literal is kept only for the gated DEV branch — production builds
tree-shake it out (the gate is statically evaluable).

7 tests cover the wagmi delegation and the EVM-only switchChain guard.
2026-05-21 17:28:05 -07:00
zeekay b01fed3a07 pkg/bridge: add bridge backend API client + MPC threshold session helper
bridge-api.ts wraps the Express bridge server's /api/{quote,swaps,...}
routes (see app/server/src/routes). Pure functions, no React. Errors
throw BridgeApiError({ status, body }) so callers can surface or
retry. chainIdToInternalName maps SDK chain ids → server enum
("evm:1" → "ETHEREUM_MAINNET", suffix-rewritten for testnet/devnet).

mpc-session.ts wraps @luxfi/threshold's ThresholdClient with a
progress-reporting wrapper used by useTransfers to surface real
MPC ceremony state to the UI instead of a setTimeout placeholder.
Protocol defaults to cggmp21 (secp256k1, EVM-compatible); respects
BridgeMPCConfig.protocol when set (pulsar/corona/magnetar for the
post-quantum path).

14 tests across both modules: fetch shape, query/body construction,
error mapping, JSON-RPC happy-path + failure + abort.
2026-05-21 17:28:05 -07:00