750 Commits
Author SHA1 Message Date
zeekayandHanzo Dev eb72965d75 Typecheck on the native compiler, keep 6 for the tools
typescript 7.0.2 is the native compiler and exports no JavaScript API, so
ts-jest and typescript-eslint cannot read it (an earlier bare-7 pin here
had to be reverted for exactly that). It comes in aliased as tsgo and does
the typechecking; typescript stays on the 6 line for the tools. tsgo
surfaced two native-compiler gaps not present under tsc 6.0.3: a JS-inference
miss on two @luxfi/utila exports (app/server), and a missing CSS side-effect
ambient declaration that also covers @luxdefi/ui's own unbuilt .tsx source
(app/explorer). Both fixed with narrow ambient declarations. 16 diagnostics
across the 11 workspace packages before and after.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 23:05:44 -07:00
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 13b6b73cbc bridge: the TypeScript twin, and Cardano can be watched
Two things, and the first is the one that mattered.

THE TWIN. app/server had the same default-to-eth I removed from the Go
side an hour ago — twice, in keygen and in the deposit check. Fixing one
language and leaving the other is the "one fact, two places, one fixed"
shape this session keeps turning up, and app/server is the one actually
deployed at bridge-api.lux.network. Both now refuse: minting a wallet
for a network with no known address format produces an address on the
wrong chain, and checking one with the EVM checker reads an 0x address
on a chain that has none and reports "no deposit" forever rather than
reporting that it cannot look.

CARDANO CAN NOW BE WATCHED, so it goes back into the offered set. A
checker over Koios — public, read-only, no key — on both sides. The test
that refused Cardano an hour ago passes it now, which is the test doing
its job in both directions rather than only saying no.

Two details worth keeping: an address that has never been used is ABSENT
from a Koios response rather than present with zero, so an empty array
is "nothing here" and not an error. And lovelace arrives as a decimal
STRING because the value exceeds what a JSON number carries exactly —
compared as integers, since a float parse introduces rounding exactly at
the threshold where a deposit is judged sufficient.
2026-08-30 17:10:41 -07:00
zeekay 2c1d44b07d bridge: XRP gets an r-address
The same defect as Cardano, in a different family, and also live. XRP
fell through to wallet.ethAddress with a comment saying derivation
"should be added when XRPL settlement ships" — so anyone bridging to XRP
was handed an 0x string to send XRP to.

XRPL's base58 alphabet is its own: `r` where Bitcoin has `1`. An address
built with the standard alphabet is well-formed and names a DIFFERENT
account, which is the trap — nothing rejects it, it simply points
somewhere else. There is a test asserting the alphabet is XRPL's.

Verified against the published genesis vector — public key
0330E7FC…FD020 encodes to rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh. That is
interoperability rather than self-consistency, which is what the Cardano
encoder still lacks and what its commit said to check before funds move.

Also asserted: a one-character change fails the checksum, an Ed25519 key
gains its 0xED prefix before hashing (hashing the bare 32 bytes gives a
valid-looking address for an account nobody controls), and a key that is
neither 32 nor 33 bytes is refused rather than padded into something
plausible.

This is the second placeholder address type found by looking. Both were
comments admitting the gap next to code that shipped anyway.
2026-08-30 17:10:41 -07:00
zeekay f21c8303d0 bridge: Cardano gets a Cardano address
You were right that the crypto is already there. Cardano signs with the
same Ed25519 key Solana does, through FROST, which luxfi/threshold and
luxfi/mpc support natively. The key was never the problem.

The address was. Solana's address IS the public key, base58. Cardano's
is blake2b-224 of that key inside a typed, network-tagged bech32
envelope. NETWORK_ADDRESS_TYPE mapped CARDANO_MAINNET to 'sol' — the
comment beside it said "placeholder" — so the bridge handed out a
Solana-shaped string and told people to send ADA to it. That is offered
on bridge-api.lux.network today.

cardano-address.ts builds the CIP-19 enterprise address: payment
credential only, no staking part, which is the right shape for custody —
a base address would commit to a staking credential the committee does
not hold.

blake2b-224 is NOT blake2b-512 truncated. Blake2b takes its digest
length into the parameter block, so they differ from the first byte, and
Node's fixed-length blake2b512 cannot be used. There is a test asserting
exactly that, so anyone substituting a truncating implementation finds
out immediately rather than after the funds move.

Also asserted: the address is not the public key (the bug this
replaces), the network tag distinguishes mainnet from testnet, a key
that is not 32 bytes is refused rather than hashed into a plausible
address for a key that does not exist, and encode/decode agree.

The type union gained 'ada' rather than the map being widened to string,
so the next family added has to be handled in the switch instead of
falling through to 'eth' — which is how Cardano would have got an
Ethereum address if the default had caught it.

25 networks in the config now. Nineteen EVM, all assemblable; Bitcoin,
Solana, TON, XRP, Polkadot and Cardano correctly excluded from the EVM
assembler.

NOT VERIFIED against a live Cardano node — the encoder is
self-consistent and matches CIP-19 as written, but no address produced
here has been used on chain. Worth one testnet deposit before mainnet
funds move through it.
2026-08-30 17:10:41 -07:00
zeekay 083c47ef07 bridge: the API default is same origin, not a different server
BRIDGE_API_HOST fell back to https://bridge-api.lux.network. That is a
different server — the older Express backend — and it serves a different
set of networks. So an unset environment variable moved every user onto
another backend, silently.

Measured rather than assumed. bridge-api offers 22 networks, this origin
offers 13, and the nine extra are not nine extra capabilities:

  Celo, Zora, Blast, Linea    real teleporter and vault. Genuinely
                              missing from this origin's config.
  Gnosis, Fantom, Aurora      teleporter and vault are EMPTY STRINGS.
                              Offered to users and cannot bridge.
  Cardano, Polkadot           no contract entry at all.

Three of those nine are the Arbitrum bug again: a chain a user can
select, send funds to, and then discover has nowhere to go. A default is
not the place to change which backend answers.

Same origin is the one answer that cannot be wrong — whatever serves the
page serves its API — and it removes the whole class rather than
correcting the hostname.

The end state is neither server. B-Chain is the bridge, and the SDK
already has BridgeConfig.rpc to reach it. Today B-Chain answers a canned
stub — identical bytes for a real method, a nonsense method, or an empty
body — so it is not yet somewhere to point. The comment says that
instead of implying the switch is available.

Still to do, and not done here: the four real networks belong in this
origin's config, and the three fictional ones should stop being
advertised anywhere.
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
zeekayandHanzo Dev d1a1ead5f5 Route and price the LUX<->ZOO and LUX<->ETH corridors
Quotes for the two flagship corridors returned "no bridge route": the
quote engine had only a 1:1 wrap table with no cross-asset edges. Add a
separate priced corridor table (LUX<->ZOO, LUX<->ETH, read symmetrically)
kept apart from the wrap table on purpose — a 1:1 cross-asset edge would
re-mint value. A corridor settles at amount * price(src)/price(dst) minus
the 1% Lux exit fee, pricing every core asset (LUX, ZOO, ETH, LBTC) from
the token price feed. It never invents a price: a missing leg throws and
surfaces as 503 price_unknown, cleanly distinct from the 400 of a
genuinely unroutable pair. Destination release still needs the live MPC
signer cluster.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 14:10:51 -07:00
zeekayandHanzo Dev 05ccf3ca8d favicon: serve a real icon file, and wear the outline mark
The app shipped its icon only as a data: URI in the markup, so nothing
existed at /favicon.ico. A browser asking for that path on its own fell
through the SPA catch-all and got index.html back with a text/html type,
which is not an icon by any reading.

Put the mark in public/ as favicon.svg and favicon.ico so the embedded
file server answers with an image type, and point the tags at them. The
tags stay in the static HTML because both the tenant rewrite in
frontend.go and bridge.config.ts need the elements to already be there.
The mark itself is @luxfi/[email protected] getFaviconSVG, the same hairline
triangle every other Lux surface now shows.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 21:01:51 -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 dfb777af99 don't rate-limit /health — it makes the kubelet kill the pod
Liveness probe failed: HTTP probe failed with statuscode: 429
    Container bridge-server failed liveness probe, will be restarted

Observed in lux-bridge as a restart every few minutes. `trust proxy` is set, so
real traffic keys on the client, but probes arrive straight at the pod IP with no
X-Forwarded-For and therefore all share one key. Readiness every 5s plus liveness
every 10s is ~270 requests per 15-minute window against a limit of 100, so
/health starts answering 429 and the platform reads that as a dead process.

Health is infrastructure asking whether the process is alive. Rate-limiting the
answer only teaches the kubelet to kill it.

Surfaced by removing the cors-patch ConfigMap: production's entry point had been
a March build of server.ts that predates this limiter, so the limiter had never
actually run in this deployment until now.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 22:21:24 -07:00
zeekayandHanzo Dev 5ffbaa9260 keep the workspace layout in the runtime image
Error: Cannot find module 'express'
    Require stack: - /app/dist/server.js

The image built, pushed and then could not start. pnpm links dependencies with
RELATIVE symlinks that count directories from where they sit —

    app/server/node_modules/express     -> ../../../node_modules/.pnpm/express@…
    app/server/node_modules/@luxfi/core -> ../../../../pkg/core

— and the runtime stage flattened app/server/node_modules into /app/node_modules,
which moves every one of those links two directories too high. They then resolve
above the filesystem root and the first require fails. Nothing about the copy
looks wrong; the links are simply measured from somewhere else now.

So the layout is reproduced exactly rather than tidied, and WORKDIR moves to meet
it. /app/pkg comes along because @luxfi/core and @luxfi/utila are workspace links
that point into it.

Caught by the smoke rather than by production, which is the whole reason it runs
the container instead of trusting a green build.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 21:29:09 -07:00
zeekayandHanzo Dev df36398766 copy scripts/ — the root postinstall needs it
Error: Cannot find module '/app/scripts/patch-hanzogui-exports.cjs'

The root package.json runs that script on postinstall, so pnpm install exits 1
without it and the deps stage never completes. Copied rather than skipped with
--ignore-scripts, which would also drop @prisma/client's postinstall and would
make the image's install a second, subtly different install from the one
everyone runs locally. It guards every path with existsSync, so with none of the
@hanzo/gui packages present it is a no-op.

Second failure in the same stage, found the same way: one broken step at a time,
now that the build actually reports.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 21:03:38 -07:00
zeekayandHanzo Dev bbbf7c90fe the server image could not build at all — copy and build @luxfi/utila
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  In app/server: "@luxfi/utila@workspace:^"
    is in the dependencies but no package named "@luxfi/utila" is present
    in the workspace

app/server took a dependency on @luxfi/utila and the Dockerfile was never told.
It copies a manifest per workspace dep, and pnpm resolves `workspace:` ranges
against the manifests present in the tree — a missing one is not a slow fallback
to the registry, it is a hard failure. So every build of this image since that
dependency landed has died in the deps stage, which is why ghcr's newest tag
predates it.

utila also has to be BUILT: its package.json points main at ./dist/index.cjs.js
and dist is gitignored, so an unbuilt dep fails at container start rather than at
build time. domain/utila.ts and domain/cosigners.ts import createGrpcClient and
serviceAccountAuthStrategy for real, not just as types.

Found because .hanzo/workflows/server.yml finally made the failure visible. It
was never a CI problem — .github/workflows queued forever against a dead pool and
reported nothing, so a broken Dockerfile looked exactly like a missing runner.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 21:00:56 -07:00
zeekayandHanzo Dev 2c178a9152 test the quote over the wire, not just the arithmetic
The unit tests cover convert(); these mount the real router on a real server and
speak HTTP to it, because the status code is part of the answer. The endpoint
used to return 200 with a confident number for a pair it could not honour, and
for an amount that was NaN — neither of which an arithmetic test can catch.

Six cases, each pinned to something production actually did: 990 where it
returned 900,000, the two directions agreeing where the ratio used to invert,
min_receive_amount equal to the quote, 400 for an unroutable pair, 400 for a
missing amount, and total_fee_in_usd null rather than the $1.00 that a caller
cannot tell from a real dollar.

axios is stubbed to fail. The only outbound call left on this path is the
display-only fee price, and "the lookup failed" is exactly the state under test
— it is also the common case, since the feed does not list 9 of the 38 symbols
the bridge asks about. Keeps the suite off the public internet.

No database: the quote path no longer touches one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:36:57 -07:00
zeekayandHanzo Dev 4d76c45e8d pre-bundle mobx-react-lite so the app can mount in dev
The dev server served a dead page: nav rendered, the swap form's asset pickers
stayed loading skeletons forever, and no request to /api/settings was ever made.
No error on screen.

    The requested module '/@fs/.../use-sync-external-store/shim/index.js'
    does not provide an export named 'useSyncExternalStore'

mobx-react-lite does a NAMED import from that shim, which is CommonJS
(`module.exports = require('../cjs/…')`). Vite can only discover the named
exports of a CJS module by pre-bundling it; left externalised it serves the file
raw and the import throws at module scope, taking out `observer()` and with it
every observed component — which is the whole swap form.

It started when use-sync-external-store 1.6.0 added an `exports` map. The shim's
code is byte-identical to 1.4.0; only its resolution changed, and the optimizer
stopped picking it up. So this names mobx-react-lite in optimizeDeps.include
rather than pinning the version back: the CJS-ness is permanent, and a pin would
just defer the next resolution bump.

It must be mobx-react-lite and not the shim itself — under pnpm the shim exists
only inside mobx-react-lite's own node_modules, so Vite cannot resolve it from
this app and rejects the entry outright ("Failed to resolve dependency ...
present in optimizeDeps.include"). Pre-bundling the package that owns the import
resolves it from where it actually lives; the shim's source ends up inlined.

optimizeDeps is dev-only — the production build goes through rollup's commonjs
plugin, which never had this problem.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:33:59 -07:00
zeekayandHanzo Dev e2dd5cffa7 settle the terms before minting a custody address
Moving the conversion into handleSwapCreation left it downstream of two side
effects that outlive the request: createMPCWalletForDeposit mints a real
custody address, and prisma.swap.create writes the row the payout path later
reads back. Since convert refuses a pair the bridge has no route for, an
unroutable request would have minted an address and written a swap and only
then thrown — leaving a funded-looking address behind for input that was never
going to settle. The old code did not have this shape because it never refused
anything; it computed NaN and stored 0.

convert is pure — no clock, no network, no db — so it can run before any of
that, and now does. A test pins the purity, because the ordering is only safe
while it holds.

The route reports UnsupportedPair and a nonsense amount as 400. They are bad
input, and answering 500 would put them in the same bucket as a chain outage.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:25:36 -07:00
zeekayandHanzo Dev 39b85c0979 one pair table, read in both directions
The destination picker was built from a local copy of the swap-pair table that
the client overwrote the server's response with — and the two had drifted. The
local copy was missing DOT and XRP entirely and did not list native SOL or TON
as destinations, so six assets the bridge routes could not be selected at all.
The server already sends the table it validates against; the client now uses
it. A second copy of a table is a copy that is already stale.

Reading it in one direction was the other half. 33 rows name a destination that
does not name them back — BTC lists LBTC, while LBTC lists WBTC, ZBTC and cbBTC
but not BTC — so the same route was offered inbound and refused outbound, with
nothing in the UI to say why. Wrapping and unwrapping are one route, so the
relation is symmetric by nature and is now read that way rather than written
twice; adding 33 reverse entries by hand would just re-open the gap the next
time an asset is added. 169 declared edges, 202 usable routes.

`swapPairs[src.asset].includes(...)` also threw outright for an asset with no
row of its own (LSIXR and ZSIXR are named as destinations but have none). The
throw escaped the mobx reaction, so setToNetworks never ran and the picker
rendered EMPTY — a crash that presented as a blank UI.

.env.example pointed at api-bridge.lux.network, which answers 530. The deployed
app has always called bridge-api.lux.network; copying the example into a .env
built an app that could not load its settings at all.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:15:00 -07:00
zeekayandHanzo Dev c330427ce7 bridging is a wrap, not a trade — stop pricing it
1000 ZLUX  ->        900,000 LUX
    1000 LZOO  ->     47,142,857 ZOO
    1000 LBTC  ->     64,267,132 cbBTC
    1000 LUX   ->          1.089 ZLUX

measured against production. Every one should be 990. This is not a display
bug: on an exit, handlerUtilaPayoutAction sets payoutAmount from the stored
quote's receive_amount and bridgeMints exactly it, so the first three rows are
an unbacked mint and the fourth silently destroys 99.9% of the user's balance.

The quote was `amount * sourcePrice / destinationPrice`, priced per request off
Coinbase, and getTokenPrice answered 1 on any failure. Coinbase lists 29 of the
38 symbols the bridge asks about; the other 9 threw, so a third of lookups
returned an invented $1.00 that is indistinguishable from a real dollar peg.
A pair with one real price and one invented one gives a ratio that inverts when
you swap the legs — which is why the same route was wrong in opposite
directions, and why that looked like two bugs instead of one.

The formula was the mistake, not the feed. Take the connected components of the
pair table and all 33 are one asset wearing three names — {BTC, LBTC, WBTC,
ZBTC, cbBTC}, {ETH, LETH, WETH, ZETH}, {LUX, ZLUX}. There is no edge between
two different assets anywhere in it, so the rate is 1, exactly, both ways, and
only the fee comes off. No price is consulted, which retires the whole class:
an outage, a delisting, a rate-limit or a renamed ticker now change nothing
here, because nothing here asks. The executor already agreed — its entry path
is 1:1 and its fallback is amount*(1-fee); only the quote disputed it.

One `convert` in quote.ts; getQuote, getRate and the stored swap row project
from it. Three copies of the arithmetic could not promise that a swap settles
at the number the user was shown.

Also, because they were the same defect wearing other masks:

- Quoting an unroutable pair returned a confident number. `convert` refuses,
  and the routes answer 400 rather than inventing a trade the bridge cannot
  make.
- getTokenPrice returns undefined, not 1, when nobody quotes an asset. It is
  display-only now, but a caller still has to be able to tell a price from the
  absence of one.
- min_receive_amount advertised a 2.5% slippage band on an exact wrap, asking
  the user to consent to less than the bridge can ever pay.

The peg classes are pinned in the test rather than derived: recovering them
means stripping an L/Z/W/cb prefix and comparing the remainder, and inferring
an asset's identity from the spelling of its ticker is the exact reasoning that
caused this (it also reads LUX as a wrapped "UX").

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 17:15:00 -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 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 df9000f990 fix(bridge): one icon set — Lucide, one version, no hand-drawn second set
The repo resolved three majors of the same set at once — 0.307.0 (via
@luxdefi/ui), 0.456.0 (the catalog, used by bridge3) and 1.16.0 (pinned
directly by explorer). Same import name, three different drawings. Every
app now takes lucide from `catalog:`, and an override collapses the
transitive copy, so one version resolves for the whole tree.

app/bridge3/app/components/icons/ was a second set wearing the first
one's clothes: avatar/bag/left-arrow/right-arrow were hand-drawn, typed
as LucideProps, off-grid (17x21, 9x16, 1019x1281) and filled rather than
stroked — three of them with `fill="white"` baked into the path, so they
were invisible on a light background and could never take the theme.
None had a consumer. Deleted; Lucide draws all four (User, ShoppingBag,
ChevronLeft, ChevronRight).

What stays in that directory is not an icon set: the Lux mark and the
third-party network marks are logos, and logos are not glyphs.

explorer also declared @heroicons/react and heroicons with zero imports
of either — a set you cannot see is still a set you can reach for.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:04:22 -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 b4fd32d903 fix(bridge-server): allow every white-label tenant origin in CORS
The backend origin allow-list only permitted bridge.lux.network + *.lux.*,
so a swap POST from a tenant bridge (Zoo/Pars/Hanzo) — which carries an
Origin header even same-origin — was rejected (500). Add the explicit
tenant hosts (zoo.network, zoo.ngo, pars.network, hanzo.ai) alongside the
Lux regex. Networks (GET) already load same-origin via the ingress /api
route; this completes the swap path for the next bridge-server release.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-20 14:56:42 -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 4da032adaa bridge(spa): complete white-label env plumbing (brand name/favicon/color/support/docs)
bridge.config.ts already reads BRIDGE_BRAND_NAME, BRIDGE_FAVICON_URL,
BRIDGE_PRIMARY_COLOR, BRIDGE_SUPPORT_EMAIL, BRIDGE_DOCS_URL from
window.__ENV (added f4e17d69), but the /__ENV.js default template and
the docker entrypoint only wrote BRIDGE_LOGO_URL — so an env-based
tenant deploy could not set the brand name, favicon, footer email or
docs URL, leaving non-Lux tenants (zoo, pars) showing the Lux fallback.

Template all five keys at container boot and list them in the default
__ENV.js so one image white-labels by env: brand name -> header +
document.title, favicon URL -> tab icon, support email + docs -> footer.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-19 01:35:22 -07:00
zeekay f4e17d69cf bridge: wire tenant brand to the SPA at runtime (white-label)
The app/bridge SPA baked @luxfi/brand at build time and the Go server served
brand at /envs.js while the SPA loads /__ENV.js — so a white-label tenant
(e.g. zooai/bridge-shim) rendered the Lux brand. Complete the stated
'jurisdiction-neutral, tenant supplies brand' design:

- bridge.config.ts: brand name/favicon/primaryColor/supportEmail/docsUrl are now
  window.__ENV-overridable (logoUrl already was); @luxfi/brand stays the
  fallback so the Lux tenant is byte-unchanged when no override is set.
- frontend.go: serve /__ENV.js (the endpoint the SPA actually loads) projecting
  the tenant brand+IAM into the BRIDGE_* keys; applyBrandMetadata() then themes
  title/favicon/logo/color at mount. Empty when no tenant → Lux fallback.
- main.go: pass the tenant config into NewFrontend.
2026-07-08 06:53:45 -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 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 1f5dd99c12 brand: audit, rip non-lux references, add .well-known/bridge.json
Rule: luxfi/bridge ships ONLY the Lux brand. Foreign-org bridge tenants
(Hanzo, Zoo, Pars, SPC, ) live in their own repos as thin
shims that consume @luxfi/bridge plus their own brand package.

- rm manifests/tenants/{hanzo,zoo,pars,spc}.yaml (carried foreign-brand
  logos/colors/names into a Lux repo)
- REQUIREMENTS.md: drop  tenant goal, drop Phase 2
  Zoo-tenant stand-up + Hanzo-as-tenant deliverables, drop
  @zooai/brand / @hanzoai/brand publication tasks; scope tenant work
  to Lux only and embed target to luxfi/exchange
- docs/LLM.md (symlinked as CLAUDE.md/AGENTS.md/QWEN.md/GEMINI.md):
  drop @zooai/brand / @hanzoai/brand from the architecture legend;
  rewrite "Hanzo-side bridge" section as "foreign-brand tenants live
  in their own repos"
- add app/bridge/public/.well-known/bridge.json declaring brandId=lux,
  domain bridge.lux.network, peers [] (federation peers via runtime
  ConfigMap), capabilities [bridge, mpc], apiVersion 1
- add BRAND-AUDIT.md documenting findings + actions

Kept (not org brand): "Liquid <TOKEN>" wrapped-asset names are Lux's
own bridge token brand (Liquid BTC, Liquid ETH, ...), wrapper minted
on Lux C-Chain per manifests/schema.yaml. Kept (chain identity):
manifests/tenants/lux.yaml, manifests/chains/*, chain RPC config,
teleporter contracts.
2026-05-29 14:25:39 -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 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
zeekay cf5b4e558b test(app/server): 21 specs for refactored mpc-signer + mpc-wallet (MPCClient flow)
Verifies the v1.1.24 refactor that routed every bridge → mpcd call
through the canonical MPCClient. Mocks @/clients/mpc.MPCClient + a
minimal ethers JsonRpcProvider; no real network.

mpc-signer.test.ts — 11 specs covering:
  isMPCSigningEnabled():
    - true when full BRIDGE_MPC_* + BRIDGE_IAM_* + BRIDGE_MPC_WALLET_ID set
    - false when any required env var missing (5 variants)
  mpcSignAndSend():
    - getWallet → unsigned tx → sign → broadcast happy path
    - v < 27 normalized to v + 27
    - compact 65-byte signature splits to r/s/v fallback
    - too-short signature throws
    - clean error when env incomplete
    - clean error when BRIDGE_MPC_WALLET_ID unset
    - clean error when wallet has no eth_address
  mpcBridgeMint(): encodes bridgeMint(address, uint256) correctly
  mpcSendNative(): posts empty-data tx with requested value

mpc-wallet.test.ts — 10 specs covering createMPCWalletForDeposit:
  EVM:        ETHEREUM_MAINNET + BASE_MAINNET → cggmp21 + secp256k1 + ethAddress
              (BASE_MAINNET also exercises BRIDGE_MPC_VAULT_ID env)
  BTC:        BITCOIN_MAINNET → cggmp21 + secp256k1 + btcAddress
  ed25519:    SOLANA_MAINNET + TON_MAINNET → frost + ed25519 + solAddress
  XRP:        XRP_MAINNET → cggmp21 + eth fallback (rAddress TODO)
  Substrate:  POLKADOT_MAINNET → SS58 derivation from eddsaPubKey
              (+ throws cleanly when eddsa pubkey missing)
  failure:    env incomplete → standalone error; no chain-family address → throws

Mock-shape pitfall avoided: vi.restoreAllMocks() in afterEach would
have nulled the MPCClient constructor's mock implementation (set once
in vi.mock factory at module load). Replaced with explicit per-fn
mockReset() in beforeEach.

Browser smoke (Playwright): http://localhost:3001 still renders the
'LUX Bridge / Connect Wallet' header with #bridge-root populated and
ZERO page errors.

Tests: 58 SDK + 144 server = 202 total. Typecheck clean on both packages.
2026-05-23 22:23:48 -07:00
zeekay d6f127dc26 feat(app/server): mpc-signer + mpc-wallet through MPCClient (eliminate legacy paths)
Final NATS/per-bridge-MPC residue swept. All bridge → mpcd traffic now
flows through the canonical MPCClient HTTP wire protocol — same one as
~/work/lux/kms/pkg/mpc/client.go in Go.

────────────────────────────────────────────────────────────────────────
 mpc-signer.ts (153 → 184 LOC; net simpler — one round-trip vs polling)
────────────────────────────────────────────────────────────────────────
Was: axios.get(/api/v1/wallets/{id}) + axios.post(/api/v1/transactions)
     + 30-iter 2s poll loop reading /api/v1/transactions/{id} for status
     transitions. Static MPC_API_TOKEN bearer (no IAM-minted JWT).

Now: MPCClient.getWallet(walletId)  → resolves ETH address (single GET)
     MPCClient.sign({ walletId, keyType, message })  → single POST,
                                                      threshold round
                                                      completes inside
                                                      mpcd; returns
                                                      r/s/v atomically.

Env (brand-neutral): MPC_API_URL/TOKEN/WALLET_ID replaced with
  BRIDGE_MPC_URL + BRIDGE_IAM_ISSUER + BRIDGE_IAM_CLIENT_ID +
  BRIDGE_IAM_CLIENT_SECRET + BRIDGE_MPC_WALLET_ID. isMPCSigningEnabled()
  short-circuits to false when any are missing; swaps.ts falls back to
  LUX_SIGNER (legacy private key) cleanly in standalone mode.

Robustness: if mpcd returns the compact 65-byte signature in
result.signature (hex) instead of separate r/s/v, mpc-signer splits it
manually as a fallback. v < 27 is normalized to v + 27 for EIP-1559
ethers compatibility.

────────────────────────────────────────────────────────────────────────
 mpc-wallet.ts (createMPCWalletForDeposit refactor)
────────────────────────────────────────────────────────────────────────
Was: raw fetch(`${MPC_API_URL}/keygen`) — POST with { wallet_id },
     auth via no header (or relying on cluster ingress), parsed a
     bridge-specific MPCKeygenResult shape.

Now: MPCClient.keygen({ vaultId, name, keyType, protocol }) then
     MPCClient.getWallet(walletId) to resolve per-chain-family
     addresses (eth/btc/sol/ton/dot/xrp). Protocol pinned per
     chain family:
       eth/btc/xrp → cggmp21 (secp256k1)
       sol/ton/dot → frost   (ed25519)

Env: drops MPC_API_URL / MPC_DASHBOARD_URL. Adds BRIDGE_MPC_VAULT_ID
(default 'bridge') for vault scoping.

The Utila-compatible return shape ({ name, addresses: [{ address }] })
is preserved so call-sites in domain/swaps.ts stay unchanged.

────────────────────────────────────────────────────────────────────────
 MPCClient.getWallet (new method)
────────────────────────────────────────────────────────────────────────
GET /v1/wallets/{walletId}. Returns the full WalletInfo type
(id/walletId/vaultId/keyType/protocol/ethAddress/btcAddress/solAddress
/ecdsaPubKey/eddsaPubKey/status). Accepts both camelCase and snake_case
fields from the wire so mpcd flavors that emit either shape work.

────────────────────────────────────────────────────────────────────────
 Net effect
────────────────────────────────────────────────────────────────────────
Bridge backend has zero per-bridge MPC HTTP client code remaining.
Every threshold sign + keygen call goes through src/clients/mpc.ts
which implements the canonical Lux MPC HTTP API (matches the Go
reference at ~/work/lux/kms/pkg/mpc/client.go). Brand-neutral env vars
(BRIDGE_*) throughout.

Tests: 58 SDK + 123 server = 181. Typecheck clean on both packages.
2026-05-23 21:05:58 -07:00
zeekay 8a8942ca89 feat(app/server): brand-neutral env + classes; drop NATS/Consul; m-chain via lux/mpc HTTP
User directive: 'drop LUX_ keep it brand neutral do not use nats/consul
must use standalone ~/work/lux/mpc with our native ~/work/lux/consensus
or b/m-chains'.

This commit lands all three asks.

────────────────────────────────────────────────────────────────────────
 Brand neutralization
────────────────────────────────────────────────────────────────────────
Env vars renamed (LUX_* → BRIDGE_*):
  LUX_IAM_ISSUER           → BRIDGE_IAM_ISSUER
  LUX_KMS_URL              → BRIDGE_KMS_URL
  LUX_KMS_ORG              → BRIDGE_KMS_ORG
  LUX_KMS_CLIENT_ID        → BRIDGE_IAM_CLIENT_ID
  LUX_KMS_CLIENT_SECRET    → BRIDGE_IAM_CLIENT_SECRET
  LUX_MPC_URL              → BRIDGE_MPC_URL
  (+ BRIDGE_MPC_WALLET_ID new — default wallet for native sign)

Client classes renamed (Lux* → brand-neutral):
  LuxIAMClient   → IAMClient
  LuxKMSClient   → KMSSecretClient   (avoids collision w/ AWS SDK's KMSClient)
  LuxMPCClient   → MPCClient

Audience strings renamed (lux-* → bare service name):
  'lux-kms' → 'kms'
  'lux-mpc' → 'mpc'

File renames:
  src/clients/lux-iam.ts  → src/clients/iam.ts
  src/clients/lux-kms.ts  → src/clients/kms.ts
  src/clients/lux-mpc.ts  → src/clients/mpc.ts
  (+ matching __tests__/ files)

────────────────────────────────────────────────────────────────────────
 NATS + Consul: gone
────────────────────────────────────────────────────────────────────────
Deletions:
  - app/server/src/services/mpc-service.ts      (498 LOC NATS+Consul wrapper)
  - app/server/src/utils/trigger-keygen.ts      (dev script, NATS)
  - app/server/src/scripts/trigger-keygen.ts    (dev script, NATS)
  - app/server/src/scripts/test-signing.ts      (dev script, NATS)
  - app/server/src/types/nats.d.ts              (vendored type stubs)
  - consul + nats sections of app/server/src/types/index.d.ts
  - nats + consul devDependencies in app/server/package.json
  - NATS + Consul + Vault containers in compose.local.yml

Rewritten:
  - app/server/src/domain/mpc-bridge.ts (497 → 197 LOC)
      Preserves the public surface (bridgeMPC.initialize() /
      .generateMPCSignature() / .completeSwap() / .getHealthStatus())
      so domain/mpc-modern.ts callers stay untouched. Internally:
        - One MPCClient HTTP call replaces the NATS publish + subscribe
          pub/sub dance. mpcd handles the threshold round atomically.
        - Consul KV completion marker replaced with Prisma's
          Swap.metadata_sequence_number update (single source of truth).
        - Lazy IAM + MPCClient construction so the bridge boots cleanly
          when BRIDGE_MPC_URL / BRIDGE_IAM_* are unset (standalone mode).
          Sign calls return a clear error rather than crashing the
          process.
  - app/server/src/server.ts startup: no more mpcService.initialize();
      bridgeMPC.initialize() now soft-fails (logger.warn) on missing
      env. /health endpoint reads through bridgeMPC.getHealthStatus()
      which reports { online: 0, total: 0, standalone: true } when
      MPCClient isn't configured.
  - compose.local.yml: replaces 6-service NATS/Consul/Vault topology
      with bare 3-service (iam + iam-db + bridge postgres). Out-of-band
      mpcd + KMS instructions documented in the file header.

────────────────────────────────────────────────────────────────────────
 docs/LLM.md
────────────────────────────────────────────────────────────────────────
All LUX_* env mentions retargeted to BRIDGE_*. The 'Native Lux services'
section now describes the HTTP-only, env-neutral integration pattern.
Standalone mode section unchanged in shape but with refreshed env names.

────────────────────────────────────────────────────────────────────────
 Tests + typecheck
────────────────────────────────────────────────────────────────────────
  SDK pkg/bridge:    58 / 58
  Server app/server: 123 / 123 (no regression — same set as v1.1.21
                                with renamed env vars)
  Typecheck:         clean on both packages

────────────────────────────────────────────────────────────────────────
 Net effect
────────────────────────────────────────────────────────────────────────
Bridge backend is now genuinely brand-neutral:
  - No 'LUX_' in any env var (the BRIDGE_ prefix is service-scoped, not
    brand-scoped; same prefix the SDK uses for runtime config)
  - No NATS, no Consul anywhere in the codebase or compose files
  - Native m-chain signing goes through HTTP to mpcd (the canonical
    ~/work/lux/mpc daemon, github.com/luxfi/mpc), one round-trip per
    sign, with the same wire protocol as ~/work/lux/kms/pkg/mpc/client.go
  - Standalone mode preserved: when BRIDGE_MPC_URL is unset, the bridge
    runs without crashing; sign calls return a clear error
2026-05-23 20:06:36 -07:00
zeekay a16981d8e1 feat(app/server): native Lux IAM + KMS + MPC clients — m-chain wired (no env-var stubs)
User asked for native ~/work/lux/mpc + ~/work/lux/kms integration and
'pure b-chain/m-chain via mpc/kms software frontend'. Delivers the
canonical-Lux-services HTTP clients on the bridge backend.

Three new client modules in app/server/src/clients/:

  lux-iam.ts   LuxIAMClient — OAuth2 client_credentials minter against
               Lux IAM (https://iam.lux.network). Per-audience cache
               with early refresh; one cache slot per (aud=lux-kms,
               aud=lux-mpc) downstream service. invalidate() on 401.
               Mirrors ~/work/lux/kms/pkg/iamclient/client.go in TS.

  lux-kms.ts   LuxKMSClient.getSecret(path) — reads from ~/work/lux/kms
               (https://kms.lux.network) at
               /v1/kms/orgs/{org}/secrets/{path...}. Bearer JWT auth via
               LuxIAMClient. Unwraps either raw text/* or JSON envelope
               { value | secret } responses. KMSError carries httpStatus
               + path for the caller to switch on.

  lux-mpc.ts   LuxMPCClient — the canonical m-chain entrypoint. Speaks
               the same wire protocol as ~/work/lux/kms/pkg/mpc/client.go
               (~/work/lux/mpc/cmd/mpcd HTTP API). Methods:
                 keygen({ vaultId, name, keyType, protocol })
                          → POST /v1/vaults/{id}/wallets (DKG)
                 sign({ walletId, keyType, message })
                          → POST /v1/transactions  (type=sign,
                            base64 payload)
                 status()  → GET /v1/status
               Supports the full Lux protocol set: cggmp21 / frost /
               bls / sr25519 / doerner / pulsar / corona / magnetar.
               Per-call 120s timeout matches mpcd's threshold ceiling.

────────────────────────────────────────────────────────────────────────
 fetchCosignerSecret — now KMS-backed
────────────────────────────────────────────────────────────────────────
domain/cosigners.ts::fetchCosignerSecret routes through LuxKMSClient
when LUX_KMS_URL + LUX_IAM_* are set. Path layout:

  utila:      bridge/cosigners/utila/{envSafe(org_id)}/sa_pem
  fireblocks: bridge/cosigners/fireblocks/{envSafe(api_key)}/secret_pem

404 from KMS → quietly falls back to UTILA_COSIGNER_PEM__/
FIREBLOCKS_COSIGNER_PEM__ env vars (dev-only). Any other KMS error is
rethrown so the dispatcher surfaces it as a 'failed' (retryable)
CosignResult. _resetKmsClientForTests() lets tests rebuild the cached
client after mutating process.env.

────────────────────────────────────────────────────────────────────────
 docs/LLM.md
────────────────────────────────────────────────────────────────────────
- New 'Native Lux services' section: documents IAM + KMS + MPC env
  config and the audience scoping. Production MUST set the LUX_KMS_*
  block; env-var fallback is dev-only.
- New 'Lux software frontends' section: lists the three operator UIs
  (~/work/lux/mpc/dashboard Next.js, ~/work/lux/kms/frontend Vite) and
  notes the f-chain frontend does NOT exist yet.
- New 'f-chain frontend — what it would look like' section: scaffolds
  the proposed @luxfi/fchain-dashboard layout (cluster status, pubkey
  registry, encrypt-session history, reshare history, health probe)
  with the exact /v1/fhe/* endpoints each widget would consume. Note
  that the bridge tenant's TRANSFERS panel currently surfaces per-swap
  fchain attestation status (no separate UI needed for that scope).

────────────────────────────────────────────────────────────────────────
 tests — 18 new
────────────────────────────────────────────────────────────────────────
  + clients/__tests__/lux-iam.test.ts  (7 cases)
  + clients/__tests__/lux-kms.test.ts  (6 cases)
  + clients/__tests__/lux-mpc.test.ts  (8 cases — keygen / sign / status
                                                 / 401 invalidate / abort)
  + domain/__tests__/cosigners-kms-secret-fetch.test.ts  (7 cases —
        KMS happy path, env-var fallback, 404 fallthrough, 5xx propagate,
        cloud_hsm/fchain defensive guard)

Updated two pre-existing tests (cosigners-utila + cosigners-fireblocks
env-var-missing cases) to match the new 'cosigner secret unavailable'
error string from the refactored fetchCosignerSecret.

────────────────────────────────────────────────────────────────────────
 final counts
────────────────────────────────────────────────────────────────────────
  pkg/bridge SDK:    58 / 58
  app/server:        95 → 123 (+28)
  Typecheck:         clean

  Every cosigner adapter (utila / fireblocks / gcp_kms / aws_kms /
  azure_key_vault / vault_transit / fchain) is a real network call.
  fetchCosignerSecret routes through real Lux KMS.
  Native m-chain signing wired via LuxMPCClient (caller integration in
  domain/mpc-modern.ts is the next planned step — clients are now in
  place and tested).
2026-05-23 17:06:17 -07:00
zeekay 66d80e9984 feat(app/server): real AWS KMS + Azure KV + Vault Transit + f-chain adapters (no stubs)
'no stubs all real killer no filler' — every TODO in cosigners.ts replaced
with a real, network-touching adapter. Cloud HSM layer is now production-
grade across all four providers; f-chain attests via the Lux FHE daemon.

Dependencies added to app/server/package.json:
  @aws-sdk/client-kms       AWS SDK v3, IAM role / IRSA workload identity
  @azure/keyvault-keys      CryptographyClient with sign('ES256K'|'PS256', …)
  @azure/identity           DefaultAzureCredential (MSI / WI / Azure CLI)
  (no client lib for Vault — direct HTTP via fetch + X-Vault-Token)
  (no client lib for f-chain — direct HTTP to /v1/fhe/encrypt)

────────────────────────────────────────────────────────────────────────
 AWS KMS — real
────────────────────────────────────────────────────────────────────────
- KMSClient({ region }) — region from identityHint or parsed from key ARN
- SignCommand(KeyId, Message: digest, MessageType: DIGEST, SigningAlgorithm)
- AWS_ALGORITHM_MAP: secp256k1_ecdsa_sha256 → ECDSA_SHA_256,
                    rsa_pss_sha256          → RSASSA_PSS_SHA_256
- ed25519 NOT in map → algorithm_mismatch rejected before any RPC
- mapAwsError covers AWS exception names + HTTP status:
    AccessDeniedException / 403           → permission_denied
    NotFoundException / 404               → key_not_found
    KMSInvalidStateException / Disabled   → key_disabled
    InvalidKeyUsageException              → algorithm_mismatch
    ThrottlingException / 429             → rate_limited
    DependencyTimeout / 5xx               → provider_unavailable
    other                                  → network_error

────────────────────────────────────────────────────────────────────────
 Azure Key Vault — real
────────────────────────────────────────────────────────────────────────
- CryptographyClient(keyRef, DefaultAzureCredential())
- AZURE_ALGORITHM_MAP: secp256k1_ecdsa_sha256 → 'ES256K',
                      rsa_pss_sha256          → 'PS256'
- Returns key version from response.keyID (audit trail per CosignerStep row)
- mapAzureError covers Azure code + statusCode:
    Forbidden / AuthenticationFailed / 403  → permission_denied
    KeyNotFound / 404                       → key_not_found
    KeyDisabled                             → key_disabled
    UnsupportedKeyOperation                 → algorithm_mismatch
    TooManyRequests / 429                   → rate_limited
    ServiceUnavailable / 5xx                → provider_unavailable

────────────────────────────────────────────────────────────────────────
 Vault Transit — real
────────────────────────────────────────────────────────────────────────
- POST {VAULT_ADDR}/v1/{mount}/sign/{name} with X-Vault-Token
- Auto-splits keyRef on '/keys/' so tenants pin a non-default mount
- Body shape per algorithm:
    secp256k1_ecdsa_sha256: hash_algorithm=sha2-256, prehashed=true,
                            marshaling_algorithm=asn1 (DER, parity with AWS/GCP)
    rsa_pss_sha256:         hash_algorithm=sha2-256, signature_algorithm=pss
    ed25519:                no prehash (Vault rejects prehashed ed25519)
- Parses 'vault:v{N}:<base64>' response shape — extracts both signature
  bytes and key_version (returned as externalId='v3' etc.)
- mapVaultError maps status + body substrings:
    401/403 / 'permission denied'           → permission_denied
    404 / 'no such key'                     → key_not_found
    'key version disabled'                  → key_disabled
    'unsupported' / 'invalid input'         → algorithm_mismatch
    429 / 'rate limit'                      → rate_limited
    5xx / 'unavailable' / 'sealed'          → provider_unavailable

────────────────────────────────────────────────────────────────────────
 f-chain (Lux FHE attestation) — real
────────────────────────────────────────────────────────────────────────
- POST {publicUrl}/v1/fhe/encrypt with { plaintext_hex, scheme? }
- Endpoint contract from luxfi/fhe::cmd/fhed/main.go (the FHE daemon
  already running in the Lux ecosystem; no new server to deploy)
- 30s timeout (FCHAIN_COSIGNER_TIMEOUT_MS env override) with AbortController
- Stores the FHE ciphertext as CosignerStep.signature (0x-prefixed hex)
  — the cosign property: only f-chain (with its FHE key shares) could
  produce that ciphertext for that txHash; downstream verifier obtains
  f-chain's public key via /v1/fhe/publickey and checks equivalence
- 401/403 → rejected (policy denial)
- 5xx → failed (retryable infra)
- network/abort → failed

────────────────────────────────────────────────────────────────────────
 Other touches
────────────────────────────────────────────────────────────────────────
- fetchCosignerSecret narrows kind 'fireblocks' explicitly so the union
  with cloud_hsm + fchain typechecks
- cosigners-gcp-kms.test.ts: removes the four 'not yet implemented'
  marker assertions (those now live as positive assertions in the new
  cosigners-aws-azure-vault-fchain.test.ts)

Test counts:
  SDK  pkg/bridge:                   58/58 unchanged
  Server app/server:                 77 → 95 (+18)
    16 wire-shape validator
    10 Fireblocks
     7 persistence + dispatch-for-swap
    13 Utila
    13 GCP KMS + cloudHsmDigest + algorithm allow-list
    18 NEW: 6 AWS + 3 Azure + 3 Vault Transit + 5 f-chain + 1 ed25519 reject
  Typecheck:                          clean on both packages

No STUBs remain. Every adapter is a real network call with a real error
mapping. Workload-identity-only by design (no SA-JSON-key fallback).
2026-05-23 16:34:29 -07: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 a7030ec59b feat(app/server): real Utila cosigner implementation + 13 tests (issue #386)
Replaces the runUtila STUB with a real evm_personal_sign attestation
flow against @luxfi/utila Connect-RPC. After this commit the layered
cosigner feature is feature-complete for BOTH providers — no stubs
remain in app/server/src/domain/cosigners.ts.

Flow:
  1. Build per-tenant gRPC client via createGrpcClient + serviceAccount-
     AuthStrategy with { email: intent.org_id, privateKey: () => secretPem }
     — mirrors the env-singleton client in domain/utila.ts but isolates
     each tenant's auth context so multiple tenants cosign in parallel
     without singleton contention.

  2. initiateTransaction({
       parent: vaults/<intent.vault_id>,
       details: { details: { case: 'evmPersonalSign', value: {
         fromAddress: intent.client_id, messageHex: opts.txHash
       } } },
       priority: NORMAL, note: 'lux-bridge cosign swap=<id>'
     })

     vault_id is REQUIRED — utila scopes initiateTransaction by parent
     vault. Missing vault_id returns 'failed' before any RPC.

  3. Poll getTransaction(name) every 1.5s, partition Utila's
     TransactionState_Enum (proto path:
     pkg/utila/src/lib/gen/utila/api/v1/transactions_pb.ts) into:

       approved   SIGNED=4, AWAITING_PUBLISH=5, PUBLISHED=6,
                  MINED=7, CONFIRMED=13
       rejected   DECLINED=9, CANCELED=11, DROPPED=12, EXPIRED=15
       failed     FAILED=8, REPLACED=10

     If state is in 'approved' but evmMessage.signature isn't populated
     yet (Utila publishes the sig in a later state transition), keep
     polling — don't bail. Intermediates (AWAITING_APPROVAL=2,
     AWAITING_POLICY_CHECK=14, AWAITING_SIGNATURE=3) also keep polling.

  4. 60s timeout ceiling (UTILA_COSIGNER_TIMEOUT_MS env override).
     Dynamic read on each call so config picks up without a restart.

  5. Return CosignResult with signature converted from Uint8Array proto
     bytes to 0x-prefixed hex; externalId = Utila transaction.name.

Edge cases covered by 13 tests (mocked @luxfi/utila):
  - happy: SIGNED + signature → approved
  - happy: CONFIRMED (later state, sig already attested) → approved
  - happy: SIGNED-without-sig keeps polling, picks up next tick
  - rejected: DECLINED, CANCELED, EXPIRED, DROPPED — all map to rejected
  - failed: FAILED proto state
  - failed: initiateTransaction throws (auth: bad jwt)
  - failed: initiateTransaction returns no transaction.name
  - failed: timeout (AWAITING_* indefinitely)
  - failed: missing vault_id → fail BEFORE any RPC call
  - failed: env-var fallback unset → clear KMS-TODO error
  - wire-shape assertion: initiateTransaction is called with the right
    parent + evmPersonalSign + fromAddress + messageHex + note

Test infra:
  - vitest.config.ts gains a resolve.alias mapping @luxfi/utila to its
    workspace src/grpc-client.ts. The published 3.0.0 rollup-built
    dist/ is what npm consumers see, but the workspace package's
    local rollup config has a long-standing outDir-vs-file-path issue
    that prevents in-place builds. Vitest resolves the TS source
    directly and runtime production is unaffected.

Tests: 91 → 104 total (58 SDK + 46 server).
  Server breakdown: 16 validator + 10 Fireblocks + 7 persistence + 13 Utila.

After this commit there are no STUBs in the cosigner module. The
remaining work on #386 is the KMS round-trip in fetchCosignerSecret
(env-var fallback works for dev; production needs KMS path read).

User correction: 'utila was already 100% complete... please make sure
it's done' — addressed by replacing the runUtila STUB with the real
implementation that reuses the @luxfi/utila Connect-RPC machinery
already in the workspace.
2026-05-22 17:34:34 -07:00
zeekay dd406080d9 feat(app/server): CosignerStep Prisma model + state-machine gate (issue #386)
Closes the persistence + state-machine gating gap from v1.1.12. After
this commit, layered cosigners are 2-of-2 enforced end-to-end on the
backend.

Prisma:
  + model CosignerStep — one row per intent. Public IDs only:
      kind         'utila' | 'fireblocks'
      public_id    org_id (utila) or api_key (fireblocks)
      api_host?, vault_id?
      status       'pending' (default) | 'approved' | 'rejected' | 'failed'
      signature?, reason?, external_id?
  + Swap.cosigner_steps relation (1:N)
  Indexed on swap_id + status for the dispatch query.

Persistence path:
  handleSwapCreation(...)
    └─ persistCosignerIntents(swapId, cosigners) — Prisma ,
        one create() per intent, all status='pending'. No-ops when no
        intents listed (backwards compat — zero cost for native-MPC-only
        tenants).

Dispatch path (issue #386 trust-boundary enforcement):
  handlerUpdateMpcSignAction(swapId, txHash, ...)
    └─ dispatchCosignersForSwap(swapId, signature, txHash)
        ├─ findMany({ swap_id, status: 'pending' })
        ├─ rowToIntent(...) — invert persistence
        ├─ dispatchCosigners(...) — runs Fireblocks (real) / Utila (stub)
        ├─ update each step with terminal state
        └─ aggregate: 'all_approved' | 'any_rejected' | 'any_failed' | 'no_steps'

If aggregate is rejected/failed:
  - Swap.status → SwapStatus.Failed
  - Swap.fail_reason → 'Layered cosigner rejection: <kind>: <reason>'
  - Never advances to UserPayoutPending; user payout step is unreachable
    without 2-of-2 (native + every listed cosigner).
  - cosigner_steps included in the response payload for traceability.

If aggregate is no_steps (native-only swap):
  Existing code path runs unchanged — proves out the additive, opt-in
  design from the SDK side.

Tests: 7 new persistence + dispatch tests (vi.hoisted Prisma mock).
  - SDK:    58/58 unchanged
  - Server: 26 → 33 (16 validator + 10 Fireblocks + 7 persistence)

What's still STUB after this commit:
  - runUtila — waiting on the first Utila tenant to model the
    InitiateTransaction subtype against their TAP policy
  - fetchCosignerSecret KMS round-trip — env-var fallback works for
    local dev; production needs the KMS path read

The Prisma schema change requires `prisma migrate dev` (in dev) +
`prisma migrate deploy` (in CI) to apply the CosignerStep table to
existing databases. The migration file itself is left for the new
dev to generate against the target Postgres instance — autogenerated
migrations need a connected DB and shouldn't be committed blind.
2026-05-22 17:24:24 -07: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