1677 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 5e8d4fb57e Ship core's dist, whatever packs it
10.0.6 went out with 13 files and no dist/, so every import of it failed: the
repo gitignores **/dist and the package named no files, which left the packed
contents up to whichever tool packed it. A files field settles that. types also
pointed at dist/index.d.ts, which tsc never emits — the declarations are under
dist/esm — so exports now names the types beside the code it describes.

10.0.7 is published with all 61 files and installs clean; 10.0.6 is deprecated.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 08:51:01 -07:00
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
zeekayandHanzo Dev d7037ab546 Keep TypeScript 6 where the build reads its API
typescript 7.0.2 exports only its version: no ModuleKind, no readConfigFile,
no program API. next build reads those while preparing tsconfig and fails with
"Cannot read properties of undefined (reading 'getCurrentDirectory')"; ts-jest
and typescript-eslint read them too. This reverts f5396049 until those tools
take the native compiler.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:11:04 -07:00
zeekayandHanzo Dev f5396049b5 Compile with TypeScript 7
typescript 7.0.2 is the native compiler, so the move is a catalog pin and no
second toolchain. app/bridge3 reports the same three diagnostics on 5.7.2 and
on 7.0.2 (a vite config overload and two in a dependency's types), so nothing
about what this code is told changed.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 18:37:03 -07:00
zeekayandHanzo Dev 7859c66a7a Workflows ask for the runner label that exists
One label per runner, spelled <os>-<arch>. The aliases these jobs named
were removed, and a job naming a label no runner carries waits instead of
failing.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 01:54:35 -07:00
zeekayandHanzo Dev 711ec6d893 gofmt six files, and untrack the laptop build the merge brought back
The merge from upstream restored ./bridge as a tracked file — upstream still
carried it — so the ignore rule added alongside the earlier untracking had
nothing to act on and the next local build dirtied the tree again. Removed from
the index once more; the file stays on disk.

The six source files were not gofmt-clean and a formatter pass reordered their
imports. Nothing else changed in them.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.8
2026-09-03 13:30:59 -07:00
zeekay 8ada5f14c8 A nil client in an interface is not a nil interface
Starting the bridge without MPC configured killed the process with a nil pointer
dereference in release-pool bootstrap. The guard it was supposed to hit was
already there, and already said the useful thing: "cannot grow 0→10 without
Keygener (mpc client not configured)".

It could not fire. mchainClient is declared as *mchain.Client and assigned only
when MPC is configured, so without it the variable is a nil pointer — and a nil
pointer passed into an interface parameter produces an interface that is not
nil, because it carries a type. `kg == nil` is false, the next line calls a
method on nothing, and the process dies where a sentence had been written for
exactly this case.

Five call sites now pass keygener(mchainClient), which returns a genuinely nil
interface when there is no client. Running without MPC is an ordinary local
case; it now says so and carries on to the next pool instead of taking the
process down.
2026-09-02 11:15:45 -07:00
zeekay 61960e4457 mpcsign was an operator action on a public path
The proxy branch forwards /api/swaps/+ whole, and among those legs is
mpcsign/:swapId, which asks the MPC to sign for a swap id the caller names. The
backend router it arrives at holds no authentication of any kind
(app/server/src/routes/swaps.ts), and no hook in the SPA calls it — the client
reaches getsig, payout and transfer. So it was an operator action reachable by
anyone on the port the ingress publishes. It moves to the operator listener,
declared before the wildcard because routes match in registration order.

The rig now builds the SPA catch-all main.go installs at /*, which changes what
the listener tests can honestly assert. Three of them read status == 404, and
production never answers 404 on an unrouted path — the catch-all answers 200
with HTML first, so those assertions held only because the rig was missing a
route. They assert the substance instead: no swap id or signed destination tx
in the body, no scrape, no deposit poll. Each still fails when its route is put
back on the public listener.
2026-09-01 20:55:46 -07:00
zeekay f050efbaf0 Merge branch 'main' of github-zeekay:luxfi/bridge
# Conflicts:
#	cmd/bridge/admin_routes_test.go
#	cmd/bridge/api.go
#	cmd/bridge/estate_test.go
#	cmd/bridge/main.go
#	cmd/bridge/swaps_handler_test.go
#	internal/broadcast/client.go
#	internal/depositcheck/client.go
#	internal/txassembler/rpc_provider.go
#	pkg/bridge/package.json
#	pkg/bridge/src/app/styles/theme.css
2026-09-01 20:07:24 -07:00
zeekay e08b050109 Stop tracking two laptop builds
./bridge and ./opnet-watcher/opnet-watcher are Mach-O arm64 executables that
rode into feature commits from someone's machine. Nothing deploys them — every
target is linux — and 24MB of the two is carried in every clone. What they
actually cost is the shared checkout: a local `go build ./cmd/bridge/` dirties
a tracked file, and a dirty tracked file refuses the next person's pull.

Untracked and ignored. The files stay on disk; only git stops watching them.
2026-09-01 20:04:35 -07:00
zeekay c817afd5c0 bridge: the operator surface gets its own listener
The swap list answered anonymously with every swap id, the MPC signature
minted for it and the signed destination tx, on the port the Ingress
publishes. /metrics named the release wallets and reported which of them
could not sign. The deposit poll turned an anonymous request into a
source-chain RPC call with caller-chosen arguments.

All three move to a second listener, BRIDGE_ADMIN_ADDR, along with the
swap mutators, which still need BRIDGE_ADMIN_ROUTES on top. The edge
routes a port, so a port is the thing that can be left unrouted: k8s
publishes 8080 and gives the operator surface 8081, and the ServiceMonitor
scrapes there. A swap read by id keeps the swap's progress and drops the
signature and the raw tx; the list on the operator port carries both.

The legacy proxy exposed the same list twice. `/api/swaps/*` matched the
empty remainder and so forwarded GET /api/swaps to the backend's list;
`+` requires a sub-path. `/v1/bridge/swaps/*` forwarded arbitrary
sub-paths into /swaps/… after the prefix strip — the backend mounts its
swap router at /api/swaps, so that path space holds no routes and the
forwarder only ever produced a 404.
2026-09-01 19:39:22 -07:00
zeekay 6c51c3541d BRIDGE_ADMIN_ROUTES=false switched the admin routes on
The flag's default was envOr("BRIDGE_ADMIN_ROUTES", "") != "", which asks
whether the variable is set, not what it says. Writing false, 0, off or no —
the way an operator records in a manifest that a switch is off — set it, and
so mounted /admin/swaps/:id/reset and /admin/swaps/:id/inject-raw-tx, which
take any swap id and no credential.

envBool was already in this file, already handled every spelling, and was
already used by the flag directly below. Only this call site did not reach for
it, so the fix is the expression rather than any new code.

The regression test is on the shape, not on envBool, since envBool was never
the thing that was wrong: a boolean flag in this file may not be built from a
set-ness test at all, and the test names the file and line when one is.
2026-09-01 16:10:08 -07:00
zeekay 11a850d7b6 every Hanzo dependency is the version npm serves
Ranges drift once nothing moves them: a caret that has not been rewritten
since it was written pins whatever was current that day, and an exact pin
cannot even take a patch. Both are updated here.

One line per package, so a surface reads the same components, the same
tokens and the same face as every other one.
2026-09-01 15:58:44 -07:00
zeekay 1f84c0617f Pin past the versions the proxy can no longer serve
The bulk retag moved published tags. go.sum and sum.golang.org hold the
original bytes, proxy.golang.org answers not-found, and the direct fetch
disagrees, so builds stopped at a checksum before reaching a compiler.
Pin forward to versions the proxy still serves.
2026-09-01 12:20:56 -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 cab29b7700 Delete the workflows the forge never reads
git.hanzo.ai resolves .hanzo/workflows and stops there, so these ran on
no push and reported green on every one. CI is native and lives in
.hanzo/workflows.
2026-09-01 00:43:20 -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 1863dc3653 bridge: mount /admin/swaps only when asked
Both handlers rewind or overwrite any swap by id and authenticate
nobody. Their own comments said "operator-only ... no auth ... do not
expose externally", but they were registered beside the public routes,
so what actually kept them unreachable was that they sit at the app
root while the edge forwards /v1/bridge and /api. That is a property of
the ingress rules rather than of this program.

BRIDGE_ADMIN_ROUTES mounts them. Off by default, so an unrouted path
answers 404 because the route does not exist.

Tested as a pair: absent by default, present when enabled. Either alone
would pass if the flag mounted nothing. Both use a seeded swap, since
the handler answers an unknown id with the same 404 the router gives an
unmounted route.
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 cea8ef5ad6 bridge: an unknown network is unknown, not Ethereum
AddressTypeFor returned AddressTypeETH for any network it did not name,
on the reasoning in its own comment: "safe because every
BridgeVM-supported chain is at minimum an EVM". That stopped being true
at Bitcoin. An unlisted non-EVM network silently became an EVM one, so
custody minted an 0x address for a chain that has never seen one, the
watcher looked for an EVM deposit that could not exist, and the
broadcaster reached for the EVM arm.

It returns (type, known) now, and the compiler walked every caller:
keygen refuses to mint for an unknown family, the deposit checker
refuses to look, the broadcaster refuses to send, pool routing returns
no pool, and the withdrawal gate HOLDS an unknown family rather than
waving it through — a family nobody has named is the one to hold.

A test asserted the old behaviour by name —
TestKeygen_UnknownNetwork_DefaultsToETH. It encoded the bug as the
specification, which is how a bug survives a test suite.

Cardano was mapped to AddressTypeSOL here, the same defect fixed in
mpc-wallet.ts earlier today. Same fact, two places, one fixed. It has
its own AddressTypeADA now.

AND THE ONE I CAUSED. Adding nine networks earlier, I filled the network
rows and the asset rows and missed internal/depositcheck's RPC table —
seven of eight EVM additions had no deposit RPC, so a Celo deposit would
have been watched for on a chain the watcher could not reach. That is
precisely the failure the five-table shape produces, committed by
someone who had just written a test against it.

Fixed, and made structural:
TestEveryOfferedNetworkIsCompleteAcrossTheTables walks the config and
requires an address family, a deposit RPC and an asset row for every
network offered. Verified by mutation — dropping Celo's RPC names it.

That test refuses Cardano, correctly. Its address encoder is right and
internal/mchain names the family, but depositcheck has no Cardano
checker, so a deposit to a correct address is observed by nobody and the
swap waits forever on money that already arrived. Held back with the
reason in the config rather than shipped and discovered.
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
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 0c73fe50a0 bridge: eight more networks, reached by custody
An empty teleporter is a mode, not a gap. Ethereum and Polygon carry
empty ones and cross fine, because the committee holds an address on the
chain and a deposit to it is the lock. I had read those empty strings as
"cannot bridge" and called three real networks fictional on the strength
of it. They are real; what was missing was our row, not their existence.

Custody already spans all of these — CGGMP21 on secp256k1 for every EVM,
FROST on Ed25519 for substrate — and the address type defaults to `eth`,
so every EVM network added here gets a correct deposit address with no
new key material and no new ceremony.

Added: Celo, Zora, Blast, Linea, Gnosis, Fantom, Aurora (EVM, chain ids
from the live registry rather than memory) and Polkadot (substrate, no
EIP-155 id, SS58 via encodeSubstrateAddress). Nineteen EVM networks are
now offered and all nineteen assemble.

Each carries its native asset row. A network in four of the five tables
a crossing needs is one whose gap is found after the user has sent
funds, so TestEveryOfferedNetworkHasItsNativeAsset now walks the config
and refuses that state.

CARDANO IS DELIBERATELY LEFT OUT, and this is the one to look at:
mpc-wallet.ts maps CARDANO_MAINNET to address type `sol`, with the
comment "placeholder — needs Ed25519". Both are Ed25519 so the key is
fine, but a Cardano address is bech32 addr1... and a Solana address is a
base58 public key. Anyone bridging to Cardano is handed a Solana-shaped
address to send ADA to. It is offered on bridge-api.lux.network today.
Adding it here would have spread a funds-losing bug rather than fixed
it; it goes in when it has an encoder.
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 b8cbf65607 bridge: the assembler reads the networks the operator offers
Arbitrum, Optimism and Avalanche were in the picker on
bridge.lux.network and the assembler had never heard of them. A swap to
any of the three is accepted, takes the user's funds, and then cannot
build the release — the failure lands after the money has moved, which
is the worst place in the flow to find a gap.

The cause was a second copy of data that already existed. The networks
file declares `chainId` and `type: evm` per network; evmChainIDs said it
again in Go, and the two drifted. configureEVM now reads the config, so
a network the operator offers is a network the assembler knows by
construction. The table stays as a fallback for a config that declares
no id, and only for that.

A network with no usable id stays unregistered rather than guessing. An
EIP-155 id is signed into the transaction: a wrong one produces a
signature valid on some OTHER chain, so refusing beats inventing.

TestEveryOfferedEVMNetworkCanBeAssembled walks the config and fails on
any EVM network the assembler lacks or disagrees with about the id.
Verified by mutation — dropping Arbitrum reports it by name. The
companion test checks the other direction: Bitcoin, Solana, TON and XRP
must NOT be registered as EVM, because giving a chain an EIP-155 id it
does not have is inventing a fact about it.

TestEstateChainIDsAreConfigured now passes nil networks, keeping the
fallback table as its subject rather than silently testing the config.
2026-08-30 17:10:41 -07:00
zeekayandHanzo Dev 85aba53d5f bridge: reach Hanzo, Pars and Osage, and hold the chain ids in one table
The bridge settled to Lux and Zoo but not to the other three chains Lux
runs, so the set it offered was smaller than the set the exchange lists.
Each is an EVM L1 whose custody address is the same secp256k1 key at the
same address it already derives, so a crossing needs no new key material.

A network reaches a user through five tables that are edited separately —
address family, deposit endpoint, broadcast endpoint, assembler endpoint,
asset row — and a chain in four of them fails after the user has already
sent funds. estate_test reads all five and the served config, so the next
chain either arrives everywhere or fails here.

The assembler's chain ids move out of main into evm.go: an EIP-155 id is
what goes into the signature, and a wrong one is valid somewhere else.

Native symbols are the ones each chain's own registry reports — Hanzo's
coin is AI. Zoo was serving live but missing from the checked-in config.

Co-authored-by: Hanzo Dev <[email protected]>
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 ee6a8f0fa2 bridge: mount /admin/swaps only when asked
Both handlers rewind or overwrite any swap by id and authenticate
nobody. Their own comments said "operator-only ... no auth ... do not
expose externally", but they were registered beside the public routes,
so what actually kept them unreachable was that they sit at the app
root while the edge forwards /v1/bridge and /api. That is a property of
the ingress rules rather than of this program.

BRIDGE_ADMIN_ROUTES mounts them. Off by default, so an unrouted path
answers 404 because the route does not exist.

Tested as a pair: absent by default, present when enabled. Either alone
would pass if the flag mounted nothing. Both use a seeded swap, since
the handler answers an unknown id with the same 404 the router gives an
unmounted route.
2026-08-29 21:05:09 -07:00
zeekay f3af7910ab GitHub Pages is not where these sites live
This fleet does not publish to GitHub Pages, and the plan does not offer it
for a private repository — creating a Pages site answers 422 — so the job
could not have succeeded on any push it ran for.

Every job in this workflow was the Pages deploy, so the file goes with it.
Sites publish from their hanzo.yml declaration onto the static plane.
2026-08-29 18:18:50 -07:00
zeekay 887491bbdf 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-29 13:35:37 -07:00
zeekay c9c131ec85 bridge: an unknown network is unknown, not Ethereum
AddressTypeFor returned AddressTypeETH for any network it did not name,
on the reasoning in its own comment: "safe because every
BridgeVM-supported chain is at minimum an EVM". That stopped being true
at Bitcoin. An unlisted non-EVM network silently became an EVM one, so
custody minted an 0x address for a chain that has never seen one, the
watcher looked for an EVM deposit that could not exist, and the
broadcaster reached for the EVM arm.

It returns (type, known) now, and the compiler walked every caller:
keygen refuses to mint for an unknown family, the deposit checker
refuses to look, the broadcaster refuses to send, pool routing returns
no pool, and the withdrawal gate HOLDS an unknown family rather than
waving it through — a family nobody has named is the one to hold.

A test asserted the old behaviour by name —
TestKeygen_UnknownNetwork_DefaultsToETH. It encoded the bug as the
specification, which is how a bug survives a test suite.

Cardano was mapped to AddressTypeSOL here, the same defect fixed in
mpc-wallet.ts earlier today. Same fact, two places, one fixed. It has
its own AddressTypeADA now.

AND THE ONE I CAUSED. Adding nine networks earlier, I filled the network
rows and the asset rows and missed internal/depositcheck's RPC table —
seven of eight EVM additions had no deposit RPC, so a Celo deposit would
have been watched for on a chain the watcher could not reach. That is
precisely the failure the five-table shape produces, committed by
someone who had just written a test against it.

Fixed, and made structural:
TestEveryOfferedNetworkIsCompleteAcrossTheTables walks the config and
requires an address family, a deposit RPC and an asset row for every
network offered. Verified by mutation — dropping Celo's RPC names it.

That test refuses Cardano, correctly. Its address encoder is right and
internal/mchain names the family, but depositcheck has no Cardano
checker, so a deposit to a correct address is observed by nobody and the
swap waits forever on money that already arrived. Held back with the
reason in the config rather than shipped and discovered.
2026-08-29 13:27:15 -07:00
zeekay 049190a9ec 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-29 12:22:46 -07:00
zeekay a6c33e86bd 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-29 12:08:17 -07:00
zeekay b3cbd87d0e 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-29 12:00:08 -07:00
zeekay dbf4c0acf2 bridge: eight more networks, reached by custody
An empty teleporter is a mode, not a gap. Ethereum and Polygon carry
empty ones and cross fine, because the committee holds an address on the
chain and a deposit to it is the lock. I had read those empty strings as
"cannot bridge" and called three real networks fictional on the strength
of it. They are real; what was missing was our row, not their existence.

Custody already spans all of these — CGGMP21 on secp256k1 for every EVM,
FROST on Ed25519 for substrate — and the address type defaults to `eth`,
so every EVM network added here gets a correct deposit address with no
new key material and no new ceremony.

Added: Celo, Zora, Blast, Linea, Gnosis, Fantom, Aurora (EVM, chain ids
from the live registry rather than memory) and Polkadot (substrate, no
EIP-155 id, SS58 via encodeSubstrateAddress). Nineteen EVM networks are
now offered and all nineteen assemble.

Each carries its native asset row. A network in four of the five tables
a crossing needs is one whose gap is found after the user has sent
funds, so TestEveryOfferedNetworkHasItsNativeAsset now walks the config
and refuses that state.

CARDANO IS DELIBERATELY LEFT OUT, and this is the one to look at:
mpc-wallet.ts maps CARDANO_MAINNET to address type `sol`, with the
comment "placeholder — needs Ed25519". Both are Ed25519 so the key is
fine, but a Cardano address is bech32 addr1... and a Solana address is a
base58 public key. Anyone bridging to Cardano is handed a Solana-shaped
address to send ADA to. It is offered on bridge-api.lux.network today.
Adding it here would have spread a funds-losing bug rather than fixed
it; it goes in when it has an encoder.
2026-08-29 11:52:44 -07:00
zeekay 1db09c1f72 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-29 11:08:46 -07:00
zeekay 68af71224c bridge: the assembler reads the networks the operator offers
Arbitrum, Optimism and Avalanche were in the picker on
bridge.lux.network and the assembler had never heard of them. A swap to
any of the three is accepted, takes the user's funds, and then cannot
build the release — the failure lands after the money has moved, which
is the worst place in the flow to find a gap.

The cause was a second copy of data that already existed. The networks
file declares `chainId` and `type: evm` per network; evmChainIDs said it
again in Go, and the two drifted. configureEVM now reads the config, so
a network the operator offers is a network the assembler knows by
construction. The table stays as a fallback for a config that declares
no id, and only for that.

A network with no usable id stays unregistered rather than guessing. An
EIP-155 id is signed into the transaction: a wrong one produces a
signature valid on some OTHER chain, so refusing beats inventing.

TestEveryOfferedEVMNetworkCanBeAssembled walks the config and fails on
any EVM network the assembler lacks or disagrees with about the id.
Verified by mutation — dropping Arbitrum reports it by name. The
companion test checks the other direction: Bitcoin, Solana, TON and XRP
must NOT be registered as EVM, because giving a chain an EIP-155 id it
does not have is inventing a fact about it.

TestEstateChainIDsAreConfigured now passes nil networks, keeping the
fallback table as its subject rather than silently testing the config.
2026-08-29 11:04:27 -07:00
zeekayandHanzo Dev ac5b8293e0 bridge: reach Hanzo, Pars and Osage, and hold the chain ids in one table
The bridge settled to Lux and Zoo but not to the other three chains Lux
runs, so the set it offered was smaller than the set the exchange lists.
Each is an EVM L1 whose custody address is the same secp256k1 key at the
same address it already derives, so a crossing needs no new key material.

A network reaches a user through five tables that are edited separately —
address family, deposit endpoint, broadcast endpoint, assembler endpoint,
asset row — and a chain in four of them fails after the user has already
sent funds. estate_test reads all five and the served config, so the next
chain either arrives everywhere or fails here.

The assembler's chain ids move out of main into evm.go: an EIP-155 id is
what goes into the signature, and a wrong one is valid somewhere else.

Native symbols are the ones each chain's own registry reports — Hanzo's
coin is AI. Zoo was serving live but missing from the checked-in config.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-29 10:56:29 -07:00
zeekayandHanzo Dev a5a691e31b 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-29 10:45:22 -07:00
zeekay a91d9a507f 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-29 10:38:06 -07:00
zeekay 963781e962 the corporate name is Lux Industries Inc
Lux Partners Limited is not the entity behind this work. Every place the
old name stood — copyright lines, site footers, brand config, legal and
compliance text — now names the company that actually holds it.

lux.fund is the exception and is not renamed to match: that site belongs
to Lux Assets Management LLC, a different company, and it says so.

Two kinds of line were left for a person rather than rewritten. A dated
news item recording that Lux Partners Limited was established in the Isle
of Man is a historical fact, and renaming it would falsify the record. And
a sentence asserting that the company is domiciled in the Isle of Man is a
claim about a jurisdiction, not a name — an Inc or an LLC is not an Isle
of Man company, so swapping the name there would assert something untrue.
v1.2.7
2026-08-28 10:59:57 -07:00
zeekayandHanzo Dev 45a8883a9f ci: build the SPA image from .hanzo/workflows so the plain tag moves again
The SPA build lived in .github/workflows/docker.yml, which the forge no longer
dispatches, so ghcr.io/luxfi/bridge stopped publishing on the plain semver line
while the `-server` line kept building from .hanzo/workflows. Deployments that
pin a plain tag were left on a June bundle whose chain RPCs still used the
retired /ext/ prefix and addressed zoo's EVM as `zoo` rather than `C`, so every
RPC call 404s. brand.json on main already carries the /v1/bc/C/rpc form; this
just gets it built.

Adds a sha- tag so a deployment can pin a digest, and drops the two superseded
.github workflows (docker.yml, and cmd-bridge-image.yml which bridge.yml
already replaced).

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 09:10:43 -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]>
v1.2.6
2026-08-21 14:10:51 -07:00
zeekay 43521de8fc carry the github copy onto the forge
github.com held commits this host lacked, which refuses the mirror push
("fetch first") and stops github receiving anything at all. Merging rather
than overwriting, so the mirror is a fast-forward again and neither side's
work is dropped.
2026-08-20 21:51:38 -07:00
zeekay dc76f7b5f9 run on the native fleet
Adds .hanzo/workflows/cicd.yml calling the canonical pipeline and drops the
GitHub-hosted workflows it replaces. Lands here because this is where the repo
is written — git.hanzo.ai is upstream and github.com is its push mirror, so the
same change made on github.com never reaches the runner fleet.
2026-08-20 20:11:43 -07:00