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

Lines that record an observation - a probe, a status code, a past release -
keep the prefix they were written against, as do URLs on hosts that are not
ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.
2026-09-01 08:20:22 -07:00
zeekay 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 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
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
venuswhispersandzeekay b24ee7f0a2 fix(bridge): BTC release confirmation gate + RBF rebuild on stale fee
Port of f3e25de from preserve/lux-bridge-golive onto main's
architecture. A BTC release now parks in
bridge_transfer_pending_confirmation on mempool admission and only
completes once mined; a release unconfirmed past --btc-confirm-timeout
(or fee-rejected at submit) is reset for re-sign at a bumped RBF
feerate — PreSign takes MinFeeRateSatPerVB as a floor over the live
estimate, and the paid rate persists on the swap (LastFeeRate) so each
rebuild strictly out-bids the last (BIP-125). Rebuild cycles share one
ceiling (default 5); past it the swap fails loudly. The broadcast
client answers confirmation status from the same mempool.space base it
posts to; /metrics gains bridge_btc_confirm_checks_total /
_timeouts_total / bridge_broadcast_rebuilds_total.
2026-08-07 22:19:29 -07:00
venuswhispersandzeekay 49bcf61703 feat(bridge): release-wallet signability monitoring + alert rules
Port of the wallet-health work from preserve/lux-bridge-golive onto
main's architecture: canary-sign poller (10m default) against every
minted release wallet, cached snapshot surfaced on /metrics as
bridge_release_wallet_signable / _sign_latency_ms / _last_check_age,
PrometheusRule + ServiceMonitor for the o11y stack, and the operator
runbook section (re-mint, don't retry). Catches the June key-share
desync failure mode before a real payout does.
2026-08-06 21:27:25 -07:00
zeekay 88af06da11 rpc: move every Lux/Zoo caller off the dead /ext/ prefix onto /v1/
luxd serves exactly one HTTP prefix — /v1 (node server/http/server.go
baseURL). /ext/ was removed some releases ago, so every Lux and Zoo URL
in this repo was pointing at a 404.

Measured 2026-07-27:
  api.lux.network/v1/bc/C/rpc       200  eth_chainId 0x17871 (96369)
  api.lux.network/ext/bc/C/rpc      404
  api.lux-test.network/v1/bc/C/rpc  200  eth_chainId 0x17870 (96368)
  api.zoo.network/v1/bc/C/rpc       200  eth_chainId 0x30e08 (200200)
  api.zoo-test.network/v1/bc/C/rpc  200  eth_chainId 0x30e09 (200201)
  control api.lux.network/v1/bc/ZZZNOPE/rpc  404

ZOO_MAINNET/ZOO_TESTNET were doubly wrong: legacy prefix AND a "Z" chain
alias that 404s on both prefixes. Zoo's EVM answers under bc/C.

Avalanche endpoints deliberately keep /ext/ — that is their API, not
ours (measured: api.avax.network/ext/bc/C/rpc 200 0xa86a, /v1 404).

B-chain/T-chain URLs move to /v1 as well; BridgeVM is still undeployed
(re-verified today: /v1/bc/B/rpc 404 while /v1/bc/C/rpc 200, so the 404
is "no such chain", not "no such prefix").
2026-07-27 17:56:13 -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 57b741cf31 feat(xrp): safe withdrawals — destination-tag plumbing + authoritative withdrawal gate
Makes XRP withdrawal safe to enable (testnet on; mainnet staged-gated) by closing
the fund-loss + gate-bypass vectors a red/blue loop surfaced over three rounds.

DESTINATION TAG (else a tag-less payout to an exchange pool is lost):
- internal/xrpl: ValidateDestination (reject X-addresses + validate r-address);
  parseAccount recover-wrapper (fixes a rippledata base58 panic-DoS reachable in
  the signing goroutine); tag set on the tx BEFORE SigningHash so it is committed
  into the SHA-512Half the MPC signs (unstrippable, proven).
- internal/txassembler: XRPSpec.DestinationTag *uint32 -> PaymentUnsigned.
- cmd/bridge: Swap.DestinationTag, createSwapReq accepts+range-checks it [0,2^32),
  signOneXRP threads it + re-validates the destination (terminal-fail on bad addr).

AUTHORITATIVE WITHDRAWAL GATE (was cosmetic — read only by the SPA list):
- config.go: WithdrawalEnabled/DepositEnabled now enforced server-side. Case-
  INSENSITIVE (network,asset) match + default-DENY for wired non-EVM families
  (BTC/SOL/TON/XRP/DOT) so an asset-case variant ('xrp'), empty asset, or network-
  case variant ('bitcoin_testnet') can't dodge a disabled row into a release pool
  (gate key now aligns with the network-only, case-folded signer routing). EVM
  stays default-on.
- Enforced at CREATE (swapsCreateNative 403), SIGN (signOne skip = kill-switch),
  BROADCAST (broadcastOne holds already-signed in-flight swaps), and the operator
  INJECT path (swapsInjectRawTxNative 403). signOne also gains a defer-recover
  backstop so one swap's panic can't crash the daemon.

Tests: tag carried+committed+on-wire; X-addr/bad-addr/panic-input rejected; tag
range boundaries; gate variant-bypass closed; create/sign/broadcast/inject all
honor the gate; kill-switch holds then resumes. cmd/bridge + xrpl + txassembler
green, full module green, vet clean.

KNOWN FOLLOW-UPS (pre-existing, bridge-wide, NOT XRP-specific; tracked):
- /admin/swaps/:id/{reset,inject-raw-tx} have no auth middleware (bounded by
  64-bit random swap IDs + the now-gated inject). Gate behind operator token/mTLS.
- GET /v1/bridge/swaps has no tenant scoping (IDOR). Both are mainnet-readiness
  items; mainnet XRP withdrawal stays isWithdrawalEnabled:false until addressed +
  a live testnet tag-carrying-payout proof.
2026-06-26 10:14:38 -07:00
zeekay 517aced181 fix(xrp): gate withdrawal on destination-tag plumbing; reserve minimum; honest trust docs
Red review of 2dd8da86 found the real blocker: XRP outbound has no DestinationTag
(internal/txassembler/xrp.go builds Payment without one; Swap/API have no tag field),
so a payout to an exchange's pooled deposit address is credited to nobody and is
irreversible. Set XRP isWithdrawalEnabled:false on both nets until the tag is plumbed
end-to-end + X-addresses are rejected — deposit-only, matching the BTC precedent.

Also from the review:
- per-XRP minUSD:30 floor — a fresh per-swap deposit address must hold the >=10-XRP
  AccountRoot base reserve or it never confirms (token-denominated min is the robust
  follow-up; USD floor is the interim proxy).
- Correct the depositcheck + deposit_watcher trust-model docstrings: depositcheck.Check
  is the SOLE runtime gate that advances a swap to a release-pool payout — the single
  upstream RPC is load-bearing/trusted, not 'secondary verification'. Documents the
  systemic multi-RPC-quorum hardening follow-up so operators point only at trusted RPCs.

Inbound checkXRP is unchanged (red verified it solid). cmd/bridge + depositcheck green.
2026-06-26 08:59:17 -07:00
zeekay 2dd8da86db feat(xrp): native inbound deposit detection + wire XRP into mainnet/testnet config
XRP was the one of {BTC,SOL,TON,XRP,ETH} with a genuine in-repo gap: outbound
(release pool + assembler + rippled submit) was complete, but inbound deposit
detection was a stub (ErrUnsupportedNetwork) and XRP was absent from all config.

- depositcheck.checkXRP: rippled account_info against the last *validated* ledger;
  balance read as decimal drops (1 XRP = 1e6 drops), compared via the shared
  compareBalance(decimals=6). An unfunded account (actNotFound) maps to the
  (false,nil) 'no deposit yet' contract, not an error. strict:true so account is
  read as an address. Same canonical rippled endpoints as the outbound broadcaster.
- networks.{mainnet,testnet}.yaml: XRP_{MAINNET,TESTNET} network + XRP token,
  deposit+withdrawal enabled. XRP settles on secp256k1 (same curve as the live
  ETH/BTC pipeline) so it has no mpcd FROST/Ed25519 gap like SOL/TON; r-address
  derives from the ecdsa_pub_key mpcd already returns. Empty release wallets fail
  payouts safe until an operator funds them (>=10 XRP reserve + fee).
- 7 new XRP deposit tests (funded/boundary/insufficient/actNotFound x2/rpc-error/
  malformed); retire the obsolete TestCheck_XRP_NotImplemented. depositcheck 36/36.
2026-06-26 08:44:12 -07:00
zeekay 2221f9af31 chore: scrub brand references from tenant config and comments
- Remove  /  /  doc-comment mentions
- Remove 'liquid' from SupportedChains allowlist (-specific
  chain identifier per CLAUDE.md, not a generic chain family)
- Update tenant config example strings and tests accordingly
- Trim BRAND-AUDIT.md to drop specific  references while
  keeping the foreign-brand rule
2026-06-10 11:45:08 -07:00
zeekay 70c294d771 fix: gofmt -s across repo (CI format check) 2026-06-02 11:38:14 -07:00
zeekay 0e809f51b0 merge: z/main-protocol-profile 2026-05-29 10:07:36 -07:00
zeekay 434d2dffc0 merge: z/mchain-btc-pool 2026-05-29 10:07:36 -07:00
zeekay 15e1cf8e09 feat(internal/mchain): wire Protocol through signViaDashboard + new public sign variants
PR#5 (z/mchain-protocol-enum) shipped the Protocol vocabulary. This PR
plumbs it through the actual sign call path so callers can opt into a
PQ-safe protocol (pulsar-m-65 / corona / magnetar) on a per-call basis.

Surface:

  NEW   SignForWalletOnProtocol(ctx, walletID, msg, curve, protocol)
        SignForWalletOnProtocolWithOrg(...) — canonical impl, all
        Sign* variants now funnel through here.

  SHIM  SignForWalletOnCurveWithOrg(ctx, walletID, msg, orgID, curve)
        — preserved verbatim; routes through the new canonical with
        ProtocolDefault, so every existing call site continues working
        unchanged.

Wire-shape rule (locked by tests):

  Protocol == ProtocolDefault  →  "protocol" field OMITTED from body.
                                  Older mpcd dashboards that pre-date
                                  the enum receive the exact body
                                  shape they were written against.
  Protocol == "pulsar-m-65"    →  body["protocol"] = "pulsar-m-65".
                                  Daemon dispatches on this; unknown
                                  values surface as 400 from mpcd.

Internal wiring (auth.go):

  signViaDashboard, signViaDashboardWithSession  now take Protocol arg;
  conditionally add "protocol" to the POST /v1/mpc/sign body.

Tests added (all green):

  TestSign_Dashboard_OmitsProtocolByDefault    backward-compat lock
  TestSign_Dashboard_PassesProtocolWhenSet     5 sub-tests
                                               (pulsar-m-65 / pulsar-m-87 /
                                               corona / cggmp21 explicit /
                                               frost explicit)
  TestSign_Dashboard_ProtocolDefaultOmits      explicit-default → omit

Full repo regression: ./... clean, no break.

Builds on z/mchain-protocol-enum (PR#5). Land PR#5 first, then this PR
can rebase to main. After this, signing_driver.go can consult
profile.go and pass the right Protocol — that's the third focused slice
(z/signing-driver-protocol, PR#7).
2026-05-28 23:07:10 -07:00
zeekay d56c21dbe8 feat(internal/mchain): Protocol enum — vocabulary for PQ + classical threshold protocols
Adds the missing primitive the bridge needs to opt swaps into a PQ-safe
threshold protocol (pulsar / corona / magnetar) instead of always going
through the daemon's classical defaults (cggmp21 / frost).

This PR is data-model only — establishes the vocabulary and the helpers
(IsPostQuantum, CompatibleCurve, ProtocolFor). The wire-plumbing
through mchain.Client.signViaDashboard + signing_driver gating off
bridge.BridgeProfile is the next focused PR; it touches the dashboard
call path consistently across auth.go and client.go and so wants its
own review.

Protocol catalogue mirrors mpcd's dispatch matrix
(luxfi/mpc/pkg/api/handlers_kms.go + luxfi/threshold/protocols/*):

  classical:
    cggmp21    secp256k1, default ECDSA
    frost      ed25519 / sr25519, threshold Schnorr/EdDSA
    bls        BLS12-381 aggregate (classical Beam in Warp 2.0)
    doerner    2-of-2 OT-based ECDSA

  post-quantum:
    pulsar-m-65    Module-LWE NIST L1 (~128-bit classical)
    pulsar-m-87    Module-LWE NIST L3 (~192-bit classical)
    corona         Ring-LWE (rank-1 MLWE collapse — production lane on
                   primary network, Warp 2.0 Pulse)
    magnetar       SLH-DSA / FIPS 205 hash-based (per-validator only —
                   true threshold is research-grade per Bonte-Smart-Tan
                   2023)

ProtocolDefault ("") is the wire sentinel for "let the daemon infer
from curve" — preserves backward compatibility with every current
caller. The empty-string contract is locked in a regression test.

Tests pass: ./internal/mchain (4.0s) + full repo green.

This is the foundation for the v1.2.0 PQ posture. The bridge's profile
gate (top-level profile.go) already names pulsar-m-65 / pulsar-m-87 /
ml-dsa-65 / ml-dsa-87 as the strict-PQ schemes; the Protocol enum is
the seam between that profile and the mpcd request body.
2026-05-28 20:25:44 -07:00
zeekay 2b28dc81b9 feat(bchain,mchain): LP-333 signer-set/epoch + mchain release-wallet pool
Two additive integrations from origin/whispers/bridgev2 — both pure
insertions, no consumer-contract changes.

LP-333 (internal/bchain/client.go):
  Adds SignerMember, SignerSetInfo, EpochInfo + GetSignerSetInfo /
  GetEpoch methods on the b-chain JSON-RPC client. The bridge becomes
  a CONSUMER of BridgeVM signer-set state: it observes which MPC
  nodes are in the active quorum, the threshold, and epoch
  transitions, then points new keygen/signing at the current epoch's
  members. mpcd remains authoritative on key share material; BridgeVM
  is the membership + epoch ledger.

  Methods are read-only — the bridge never proposes rotations. Node
  operators do that out-of-band via BridgeVM governance. Surface lives
  on the same `${nodeUrl}/ext/bc/B/rpc` endpoint as bridge_getInfo,
  not on ThresholdVM.

internal/mchain/pool.go:
  Adds Pool — round-robin release-wallet selector that the daemon
  drains for destination-chain broadcasts. Pure additive; existing
  callers don't see it until they choose to consume it.

Tests: bchain (2.3s) + mchain (2.6s) all green. Includes
TestGetSignerSetInfo_HappyPath + TestGetEpoch round-trips against the
mock node.

NOT included in this PR (deliberately scoped out):
  - internal/mchain/client.go modifications — branch's version is
    behind main (no DashboardURL/Curve/sessions). Main's BTC support
    already supersedes the branch's testnet-stopgap path.
  - internal/mchain/btc_address.go — branch's file is a TESTNET
    base58 stopgap; main's file is the production bech32 derivation
    (deriveBTCBech32Address, compressSecp256k1Pubkey). Different
    purposes, main's wins.

This is the second slice of the whispers/bridgev2 integration. Per
"x.x.x+1, never lazy major" the bridge's next tag is v1.2.0 once the
substantive features land.
2026-05-28 20:17:07 -07:00
zeekay 65979bf7a0 feat(internal): cosigners + secrets resolver — layered cosigner SDK Go port
Brings the SDK-v1.0.3 layered cosigner pattern from app/server/src/domain/cosigners.ts
across to Go. Adds:

  internal/secrets/        URI-scheme secret resolver (literal:/env:/file:/kms:)
                           with pluggable KMS providers (init-time Register).
                           Single source of truth for mpcd bearer tokens,
                           cosigner PEMs, and any future secret material.

  internal/cosigners/      Layered cosigner SDK — per-swap external custodian
                           attestation on TOP of the native MPC threshold sig.
                           Wire shape mirrors the TS reference exactly (snake-
                           case, `kind` discriminator). Families:
                             - fireblocks.go      Real REST integration; RAW
                                                  transaction approval flow,
                                                  per-tenant JWT (RSA), polls
                                                  /v1/transactions/{id} to
                                                  terminal status.
                             - utila.go           Connect-RPC scaffold; mirrors
                                                  FireblocksRESTFamily shape so
                                                  the future port is one
                                                  contained method swap.
                             - stub_family.go     Catch-all for unknown kinds.

Security invariant baked in: SECRET MATERIAL NEVER CROSSES THE WIRE. The
SDK sends only public identifiers (org_id, client_id, api_key, vault ids).
ValidateIntents rejects SECRET_FIELD_NAMES (api_secret, private_key, jwt,
token, …) as a defensive layer. Secrets are fetched at dispatch time via
secrets.Resolver, which is also where KMS plugs in.

Tests: cosigners (2.0s) + secrets (0.3s) all green. Self-contained — no
out-of-tree imports beyond stdlib and within-repo packages.

Extracted from origin/whispers/bridgev2 as the first of a series of focused
slices integrating that branch onto main without dragging in the noise
(export.txt transcript dump, bridge binary diff). Per "x.x.x+1" rule the
next bridge release tag is v1.2.0 — this is the first additive feature
that warrants the minor bump (new internal packages, no consumer break).
2026-05-28 20:14:51 -07:00
zeekay 784318262a feat(tenant): promote internal/tenant to pkg/tenant for external imports
The tenant.Config primitive is the public contract that downstream
shim repos (liquidity/bridge, hanzo/bridge-shim, zoo/bridge-shim,
pars/bridge-shim) consume to validate their own tenant.yaml. Keeping
it under internal/ blocked that — shims couldn't import it.

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

Tests:
- ./pkg/tenant 26 tests, PASS
- ./cmd/bridge full suite PASS
2026-05-28 01:36:40 -07:00
zeekay caa55ca609 feat(tenant): declarative tenant config — white-label shim pattern
Adds internal/tenant.Config: a single YAML primitive that drives brand,
IAM endpoint, KMS endpoint, MPC cluster, PQ profile, supported-chains
allowlist, basket allowlist, fee receiver, domain, and per-family
release-pool sizing. Same pattern Lux already uses for IAM (hanzo.id /
lux.id / zoo.id / pars.id) and KMS.

- internal/tenant/config.go (462 LOC): Config + per-section structs
  (Brand, Network, IAM, KMS, MPC, ReleasePool, FamilyPool); Load() +
  Parse() + Validate(); IsChainSupported() + IsBasketAllowed() helpers.
  Strict validation — daemon hard-fails on invalid config (no warn-
  and-continue path).

- internal/tenant/config_test.go (491 LOC): round-trip yaml, defaults
  applied, every required-field omission rejected, PQ profile must be
  one of strict-pq|classical-compat, FeeReceiverAddr must be 0x-prefixed
  20-byte hex, SupportedChains must be subset of the known family set,
  slug must be lowercase-hyphenated, IAM/KMS endpoints must be https://,
  MPC threshold can't exceed operators, basket allowlist validates.
  26 tests, all pass.

- cmd/bridge/main.go: --tenant-config flag (BRIDGE_TENANT_CONFIG env).
  When set, brand slug becomes the daemon log "service" tag + Fiber
  AppName; tenant defaults fill flags the operator didn't pass.
  Backwards-compatible: existing flag-driven deploys keep working.

- cmd/bridge/tenant_overrides.go: applyTenantOverrides — explicit
  flag wins, tenant fills gaps. flag.Visit-based, so we never
  clobber operator intent.

- cmd/bridge/main_test.go: TestTenantConfigFixture loads a complete
  tenant.yaml from disk and asserts it validates; TestTenantExampleYAMLLoads
  is a CI guardrail that the repo-root example file stays in sync
  with the schema.

- tenant.example.yaml: canonical commented schema. All four white-label
  shim repos (liquidity/bridge, hanzo/bridge-shim, zoo/bridge-shim,
  pars/bridge-shim) consume this shape.

Tests: ./... PASS — internal/tenant 0.282s, cmd/bridge 2.86s, full
suite green across 12 packages.
2026-05-28 01:24:15 -07:00
zeekay 421e170a80 merge: pull jackson's 4dd61fe+9664f6b (swap amount + coingecko)
Integrate whispers/bridgev2 into z/bridgev2-mpc-pool-gas:
- 4dd61fe 'fixed swap amount correctly' (introduces ReceiveAmount + releaseAmount helper, resolveReleaseSigning, FileReleaseStore)
- 9664f6b 'lux rice from live coingecko' (CoinGecko price feed)

Conflict resolution (preserving both sides):
- cmd/bridge/main.go: union of imports (math/big + net/http); both envOrInt+envOrInt64 (ours) and envBool (jackson)
- cmd/bridge/signing_driver.go: keep our BTC dispatch + multi-family pool acquire path; use jackson's resolveReleaseSigning as a fallback before the deposit-as-release legacy path; keep our SOL/DOT/TON switch cases AND jackson's releaseAmount helper used in the EVM PreSign call
- cmd/bridge/swap_store.go: collapse duplicate ReleaseWalletID/ReleaseAddress field declarations (both sides added the same fields with identical JSON tags); keep the more thorough doc block from jackson's side
- internal/mchain/client.go: keep our extra Wallet.ECDSAPubKey+EDDSAPubKeyHex fields AND add json tags from jackson's side so FileReleaseStore.persistLocked can serialize them
2026-05-28 00:59:41 -07:00
zeekay 0b5eff46fc merge: integrate TON broadcaster (#393)
Consolidates origin/z/bridgev2-ton-broadcast — final of the five chain
broadcaster branches. With TON wired, all six families (EVM, BTC, SOL,
DOT, XRP, TON) are now implemented end-to-end.

Conflicts resolved:
- internal/broadcast/client.go: TON case added alongside the existing
  five; ErrFamilyNotImplemented is now reserved for future families
- cmd/bridge/release_pool.go: dropped TON agent's separate Wallet field
  shape (ECDSAPubKey string + EDDSAPubKey string) in favor of the
  merged shape from earlier chains:
  * Wallet.ECDSAPubKey stays []byte (BTC's choice — XRP/DOT decode hex)
  * Wallet.EDDSAPubKeyHex added as hex string (TON's choice — uniform
    with the ed25519-only wallet families)
  ReleasePoolEntry now carries both ECDSAPubKey (hex) and EDDSAPubKey
  (hex), plus AddressType (mchain type — distinct from Family string)
- cmd/bridge/signing_driver.go: TON dispatch added to the family
  switch in signOne after the SOL+XRP branches but before the DOT/EVM
  assembler arm; threaded tonAssembler + tonUnsigned through the
  PreSign/MPC-sign/Finalize flow with TON-specific gas pre-check;
  lookupPoolEntry helper added for fetching the EDDSAPubKey alongside
  the address; tonAmountToNano + isTONNativeAsset utilities
- cmd/bridge/main.go: TON assembler wired through signer.SetTONAssembler
  alongside EVM/SOL/DOT; balance probe TON endpoints + API keys wired
  via networks.yaml + balanceProbe.TONBalanceURLs/TONAPIKeys
- cmd/bridge/balance_probe.go: TON branch added to BalanceAt switch
  (calls balanceAtTON via GET getAddressBalance); SOL kept in the
  dedicated branch; isLikelyNonEVMNetwork docstring updated to
  acknowledge SOL+TON are routed elsewhere
- internal/mchain/client.go: Wallet.ECDSAPubKey stays []byte; new
  EDDSAPubKeyHex string field for Ed25519 wallets (TON); pickAddress
  TON case derives v4r2 raw address from EDDSAPubKey when present,
  falls back to SOLAddress slot for legacy mpcd
- go.mod: added xssnick/tonutils-go + ed25519 dependency chain; took
  golang.org/x/crypto v0.51.0 + golang.org/x/sys v0.44.0 (TON's
  newer minimums)
- internal/broadcast/client_test.go: TestBroadcast_FamilyNotImplemented
  retired — every address type now has a concrete broadcaster
- Test refactors: ReleasePool tests use NewReleasePoolForFamily +
  PutEntry(family, idx, entry); FamilyTON constant added
2026-05-28 00:42:58 -07:00
zeekay da12749209 merge: integrate XRP broadcaster (#393)
Consolidates origin/z/bridgev2-xrp-broadcast. Conflicts resolved:
- internal/broadcast/client.go: XRP case added alongside EVM/BTC/SOL/DOT;
  ErrFamilyNotImplemented now lists only TON as pending
- cmd/bridge/release_pool.go: dropped XRP agent's PubKeyHex field +
  AcquireForFamily/SizeByFamily/Reload accessors that re-implemented
  family routing inside ReleasePool. Kept BTC's family-keyed store
  approach (LoadEntries(ctx, family)) as the single source of truth.
  XRP pubkey persistence rides on ReleasePoolEntry.ECDSAPubKey (hex
  string field added by DOT merge — XRP signing driver reads the same
  field for its SigningPubKey embedding). Added FamilyXRP constant.
- cmd/bridge/release_pool_set.go: added XRP slot to ReleasePoolSet +
  Size/FamilySizes/PoolFor/poolFor coverage
- cmd/bridge/signing_driver.go: XRP dispatch added before EVM/DOT
  assembler path; threaded poolEntry through both poolSet acquire and
  legacy fallback acquire so signOneXRP can read poolEntry.ECDSAPubKey
- cmd/bridge/main.go: XRP release pool wired through releasePoolSet.XRP
  via the same NewReleasePoolForFamily(store, FamilyXRP, …) shape as
  BTC/SOL/DOT — no more shared-store-with-different-indices hack
- internal/mchain/client.go: Wallet.ECDSAPubKey stays []byte (BTC's
  choice) — XRP consumers hex-encode when persisting. XRP r-address
  derivation runs in pickAddress when ECDSAPubKey present, falls back
  to ETHAddress otherwise (matches the merged-state shape-only tests)
- cmd/bridge/broadcast_driver.go: humanizeBroadcastErr merges BTC's
  txn-* rejections with XRP's tecINSUFFICIENT_FUNDS / tecUNFUNDED
- go.mod: added rubblelabs/ripple + bits-and-blooms/bitset to
  union of all branches' deps
- Test refactors: ECDSAPubKeyHex → ECDSAPubKey ([]byte); PutEntry takes
  family arg; XRP-specific tests using AcquireForFamily/SizeByFamily
  replaced with TestReleasePool_XRPFamily_BootstrapAndAcquire matching
  the BTC family-pool shape
2026-05-28 00:30:53 -07:00
zeekay 167c0c44dc merge: integrate DOT broadcaster (#393)
Consolidates origin/z/bridgev2-dot-broadcast. Conflicts resolved:
- internal/broadcast/client.go: DOT case added alongside EVM/BTC/SOL;
  ErrFamilyNotImplemented now lists only TON/XRP as pending
- cmd/bridge/release_pool.go: dropped DOT agent's keyspace + ReleasePool
  WithKey shape; kept BTC's family-based design (LoadEntries(ctx,family))
  as the single source of truth. Added FamilyDOT constant + dual Pubkey
  fields (raw []byte for BTC/XRP, hex string ECDSAPubKey for DOT)
- cmd/bridge/zap_store.go: kept HEAD's releasePoolKey(family,idx)
  signature; dropped DOT agent's NS variants; added defensive
  isAllDigits filter on the iterator suffix
- cmd/bridge/signing_driver.go: DOT assembler/chain-ctx slots threaded
  alongside SOL/BTC; pool acquisition picks dotPool for DOT destinations
  but defers to poolSet when present (routes by family)
- cmd/bridge/main.go: DOT release pool wired through releasePoolSet.DOT;
  signer wires all four assemblers (EVM, SOL, BTC, DOT)
- cmd/bridge/release_pool_set.go: added DOT slot to ReleasePoolSet +
  Size/FamilySizes/PoolFor/poolFor coverage
- internal/mchain/client.go: Wallet.ECDSAPubKey stays []byte (BTC's
  choice) — DOT consumers hex-encode when persisting
- release_pool_dot_test.go + dot_signing_test.go: rewritten to use
  PutEntry(family, idx, entry) family-based API
- internal/mchain/client_test.go: updated ECDSAPubKey assertion to
  hex.EncodeToString since field is []byte not string
2026-05-28 00:17:24 -07:00
zeekay 6aa0491351 merge: integrate SOL broadcaster (#393)
Consolidates origin/z/bridgev2-sol-broadcast. Conflicts resolved:
- internal/broadcast/client.go: keep BTC + SOL cases in family switch
- cmd/bridge/signing_driver.go: BTC dispatch first, then SOL branch
- cmd/bridge/main.go: BTC pool block + SOL pool block + multi-family
  router (releasePoolSet) that BOTH families register into
- go.mod/go.sum: union of dependencies, tidy applied
- Replaced SOL agent's NewInMemoryStoreFamily wrappers with BTC agent's
  family-scoped ReleasePoolStore (LoadEntries(ctx, family)) — single
  source of truth, no parallel wrappers
- release_pool_family_store.go: deleted (redundant with BTC design)
- FamilySOL constant added alongside FamilyEVM + FamilyBTC
2026-05-28 00:03:34 -07:00
venuswhispers 4dd61fedf6 feat: fixed swap amount correctly 2026-05-28 04:41:55 +02:00
zeekay 93e5078a1b feat(bridge): wire DOT signing path + parallel substrate release pool
End-to-end Polkadot integration in the cmd/bridge binary:

cmd/bridge/dot_signing.go (new):
- DOTChainContext interface: AccountNextIndex, RuntimeVersion,
  GenesisHash, FreeBalance — the four substrate RPC queries the
  signing payload + gas pre-check need.
- LiveDOTChainContext: production JSON-RPC impl. Caches GenesisHash
  forever (stable per chain), RuntimeVersion for 30s (refreshes on
  runtime upgrades). Reuses the broadcast URL overrides — single
  --source-rpc-overrides flag covers every leg.
- preSignDOT: wires PreSign(DOTSpec) on the signing driver — resolves
  pubkey via release pool, refreshes assembler config from live chain
  state, builds the signing-payload hash to feed the MPC.
- dotGasPrecheck: substrate analogue of the EVM eth_getBalance check.
  Required = transfer_value + fee + existential_deposit; insufficient
  short-circuits the swap to failed_insufficient_release_gas before
  burning the 75s MPC ceremony.
- extractDOTValuePlanck: parses a Compact<Balance> trailing the call
  bytes (so the gas pre-check has the exact transfer value).

signing_driver.go:
- New dotAssembler + dotChainCtx + dotPool slots.
- Family-aware dispatch in signOne: DOT swaps route through preSignDOT
  + the substrate Finalize path; EVM swaps stay on the EIP-155 path.
- Both ParseRSV (EVM) and ParseDOTSignature (substrate) live in
  txassembler — the driver picks the right one by checking which
  *Unsigned came out of PreSign.
- ReleasePubKey persisted at claim time so Finalize can determine the
  ECDSA recovery byte.

release_pool.go + zap_store.go + InMemoryStore:
- ReleasePoolEntry.ECDSAPubKey: persisted with each entry so the DOT
  signing path doesn't need a fresh keygen lookup.
- ReleasePoolStoreWithKeyspace: optional interface adding LoadEntriesNS /
  PutEntryNS. Lets the bridge run parallel pools without colliding on
  the legacy "releasepool:NNN" key prefix. ZapStore + InMemoryStore
  both implement it.
- NewReleasePoolWithKey: constructor for keyspace-isolated pools (the
  DOT pool uses "releasepool:dot" as its namespace).
- PubKeyHex(walletID): pool-side lookup the DOT signing path uses.

cmd/bridge/main.go:
- New CLI flags: --release-pool-dot-size (default 3),
  --release-pool-dot-mint-network (default POLKADOT_TESTNET),
  --release-pool-dot-threshold-planck (default 1e11 = 10 DOT).
- Per-network DOTAssembler config for POLKADOT_MAINNET (prefix 0,
  decimals 10, CallIndex 5/3), POLKADOT_TESTNET / Westend (prefix 42,
  decimals 12, CallIndex 4/3), KUSAMA_MAINNET (prefix 2).
- Wires LiveDOTChainContext + the DOT pool into the signing driver
  alongside the EVM pool.

txassembler/rlp.go:
- FloatToBaseUnits: exported alias for floatToWei. The DOT path
  reuses the same human-amount → integer scaling helper as the EVM
  path; this just makes it importable from cmd/bridge.

swap_store.go:
- Swap.ReleasePubKey: new field carrying the compressed secp256k1
  pubkey for the chosen release wallet. Required by the DOT signing
  path to derive AccountId + recovery byte at Finalize.

Tests:
- TestSigning_DOT_InsufficientBalance_ShortCircuits: end-to-end
  driver tick, balance 0 -> failed_insufficient_release_gas without
  hitting the MPC.
- TestSigning_DOT_WithoutDOTAssembler_FallsBackToPlaceholder:
  legacy behaviour preserved when DOT assembler unwired.
- TestSigning_DOT_PreSign_QueriesChainContextEachTick: verifies all
  three chain-context calls (nonce, runtime version, genesis hash)
  happen on every PreSign.
- TestSigning_DOT_ChainContextError_RollsBack: transient RPC errors
  re-queue the swap.
- TestReleasePool_KeyspaceIsolation: default + DOT pools see only
  their own entries.
- TestReleasePool_PubKeyHexRoundtrip: pubkey persistence works.
- TestZapStore_NamespacedPool: disk-side keyspace isolation works
  (including the legacy-prefix overlap fix in LoadEntriesNS).
- TestExtractDOTValuePlanck: compact-int decode all four modes.
2026-05-27 18:17:05 -07:00
zeekay cbc3e147ce feat(bridge): XRP end-to-end — txassembler, mchain pubkey, signing driver, release pool
internal/txassembler/xrp.go:
- XRPProvider interface + per-network XRPNetwork config (LastLedgerWindow,
  fee override, canonical-sig flag).
- Assembler.PreSignXRP: pulls Sequence + suggested fee from rippled,
  embeds the release wallet's compressed-secp256k1 pubkey, returns
  the 32-byte SHA-512Half (via internal/xrpl) the MPC quorum signs.
- Assembler.FinalizeXRP: parses (r, s) from the MPC's 65-byte R||S||V
  hex (ignores recovery byte — XRPL doesn't need ecrecover), DER-encodes
  with low-s canonicalization, attaches as TxnSignature, returns the
  wire blob (hex) + canonical XRPL tx hash.
- XRPRequiredDrops: amount + fee + 10-XRP reserve, used by the gas
  pre-check to refuse signing when the wallet can't cover.
- xrpFloatToDrops with the same string-format trick floatToWei uses,
  avoiding IEEE noise at 6-decimal native XRP precision.

internal/mchain/client.go:
- Wallet now carries ECDSAPubKeyHex (compressed-secp256k1 hex). EVM
  paths ignore it; XRP signing needs it on the wire.
- pickAddress(XRP) derives the canonical r-address from the ECDSAPubKey
  via internal/xrpl.AddressFromPubKey instead of falling back to the
  ETH slot. Fallback retained for legacy mocks without the pubkey field.

cmd/bridge/xrp_provider.go:
- HTTP wrapper over rippled JSON-RPC (account_info, fee). Returns
  (Sequence, validated_ledger_index) and the open_ledger_fee/median
  fee / base_fee chain. Treats actNotFound on balance lookup as
  "wallet not activated → balance 0" so the gas pre-check correctly
  reports an unactivated release wallet.

cmd/bridge/release_pool.go:
- ReleasePoolEntry gains Family + PubKeyHex fields (round-trips through
  JSON; pre-XRP entries leave them empty).
- AcquireForFamily filters entries by family — the signing driver now
  picks an XRP wallet for XRP destinations and an EVM wallet for EVM.
  Falls back to family-agnostic Acquire when no per-family match.
- SizeByFamily + Reload helpers so a parallel XRP-pool bootstrap can
  share the same store as the EVM pool without re-indexing.

cmd/bridge/signing_driver.go:
- signOne dispatches on mchain.AddressTypeFor(DestinationNetwork). XRP
  routes to signOneXRP, which mirrors the EVM ceremony but: builds a
  Payment via Assembler.PreSignXRP, runs an XRP gas pre-check (balance
  vs amount+fee+reserve) instead of eth_getBalance, hands the
  SHA-512Half digest to the MPC, then Assembler.FinalizeXRP emits the
  signed wire blob.
- xrpGasPrecheck short-circuits to SwapStatusFailedInsufficientReleaseGas
  with a humanized "balance=X drops, required=Y drops" reason. Probe
  failures DON'T block the swap (best-effort, broadcast layer still
  retries on transient rippled flakes).

cmd/bridge/main.go:
- New flags: --xrp-release-pool-size (default 5),
  --xrp-release-pool-mint-network (default XRP_TESTNET),
  --xrp-release-balance-threshold-drops (default 10 XRP).
- XRPProviderClient instantiated once, shared between the assembler
  (for PreSignXRP) and the gas pre-check (via assembler.XRPProviderFor).
- XRP pool minted on top of the existing EVM pool (via a helper-pool
  bound to the same store + Reload), so a single ReleasePool object
  serves both families.
- /health surfaces pool sizes broken out by family.

Tests:
- xrpl: address derivation round-trip, bad-pubkey rejection, DER
  encoding + low-s flip, PreSign digest matches SHA512Half(STX||body),
  full PreSign→Finalize round-trip through the rubblelabs parser.
- broadcast: tesSUCCESS happy path, terQUEUED retryable, tecINSUFFICIENT_FUNDS
  + tecNO_DST fatal, temBAD_AMOUNT malformed, telINSUF_FEE_P retryable,
  rippled high-level errors, classifyEngineResult table.
- txassembler: PreSignXRP populates Sequence / Fee / LastLedger,
  fee floor + pin, bad-input rejection, FinalizeXRP round-trip,
  XRPRequiredDrops math, xrpFloatToDrops table.
- mchain: XRP r-address derivation from ECDSAPubKey, legacy fallback
  to ETH slot, bad-hex rejection.
- release pool: XRP-family Bootstrap + entry shape + PubKey roundtrip,
  AcquireForFamily filtering, no-match returns ErrEmptyPool.
- signing driver: XRP happy path advances to broadcasting with
  populated DestRawTx, insufficient balance short-circuits, missing
  PubKeyHex rolls back with readable LastError.
- xrp_provider: account_info / fee / balance stubs, actNotFound → 0,
  rippled error surfacing.
2026-05-27 18:13:43 -07:00
zeekay 9f398bdf30 feat(broadcast): BTC sendrawtransaction via mempool.space + retryable error surface
BTC broadcaster pushing serialized P2WPKH txs to mempool.space's
REST POST /api/tx (or operator's Bitcoin Core via RPCURLOverrides).

Family dispatch in Broadcast() now routes AddressTypeBTC to
broadcastBTC alongside the existing EVM path; SOL/TON/XRP/DOT still
return ErrFamilyNotImplemented for callers to detect.

Error surface — BTCBroadcastError carries:
- HTTPStatus from the upstream response
- Message (chain-side error string)
- Retryable bit so the orchestrator decides:
  * retryable: leave swap at broadcasting (txn-already-known,
    rate limit, 503/502/504, "already in mempool")
  * fatal: advance to terminal failure (txn-mempool-conflict,
    bad-txns-inputs-missingorspent, dust, missing inputs)

Special handling:
- "txn-already-known" / "Transaction already in block chain" → recover
  the canonical txid locally from sha256d(raw) and surface as SUCCESS,
  not failure (the tx is already where we wanted it)
- Upstream txid validation: 64-hex-char check; non-conforming bodies
  fall back to local txid compute

Default URLs registered via init() in btc.go (not in client.go's
table) so the BTC defaults live next to the BTC broadcaster:
- BITCOIN_MAINNET → https://mempool.space/api/tx
- BITCOIN_TESTNET → https://mempool.space/testnet/api/tx

Tests: 13 across happy path (mainnet + testnet + 0x trimming),
already-known + already-in-chain success surface, mempool-conflict
+ missing-inputs fatal, 503/429 retryable, empty + malformed input,
malformed upstream body fallback, error stringification with
retryability label
2026-05-27 18:07:43 -07:00
zeekay e84b531b5b feat(txassembler): BTC P2WPKH build + BIP143 sighash + greedy UTXO selection
Wire-correct BTC release tx assembler that the MPC quorum can
threshold-sign and the broadcast driver can push. Mirrors the EVM
PreSign/Finalize shape but emits N per-input sighashes (one MPC
sign call per input) instead of one EIP-155 digest.

Scope (v1):
- P2WPKH witness-v0 only (bech32 bc1q.../tb1q...); P2TR deferred
- SIGHASH_ALL only (no anyonecanpay / single)
- Greedy largest-first UTXO selection — adequate for the bridge's
  own pool wallet, no churn concerns
- Single change output back to the release address (dropped under
  the 546 sat dust threshold)
- BIP143 sighash via btcd/txscript.CalcWitnessSigHash; scriptCode
  computed from the P2WPKH pkScript via the canonical
  OP_DUP OP_HASH160 <pkh> OP_EQUALVERIFY OP_CHECKSIG transform
- Fee estimation: mempool.space halfHourFee with fastestFee /
  minimumFee fallback; static fallback rate 10 sat/vB
- Low-s canonicalization in Finalize (BIP146); high-s sigs are
  refused by relay nodes
- DER encoding: canonical with 0x00 leading-pad when high bit set
- RBF-friendly sequence (0xfffffffd) on every input

What's NOT in this commit:
- PSBT wire format — we use raw wire internally; PSBT is a
  hardware-wallet convenience irrelevant in-process
- Bitcoin Core RPC — operators can override broadcast URL but the
  default uses mempool.space's REST API (zero-setup)

New types:
- BTCNetwork, BTCSpec, BTCUTXO, BTCInputCtx, BTCUnsigned, BTCECDSASig
- BTCAssembler with UTXOFetcher / FeeEstimator / BalanceProbe seams
- MempoolSpaceClient implements all three from public REST endpoints

Tests: 30+ across selectBTCUTXOs (greedy correctness, insufficient
funds, fee scaling), p2wpkhScriptCode, chainHashFromTxID round-trip,
encodeDERSignature (low-s + high-bit padding + zero rejection),
ParseBTCRSV, PreSign happy path + dust handling + insufficient funds
+ pubkey/address validation, Finalize witness layout + sig count
mismatch, MempoolSpaceClient UTXO/fee/balance shape parsing
2026-05-27 18:07:24 -07:00
zeekay 0e1ad73ace feat(mchain): BTC P2WPKH derivation from keygen ECDSAPubKey
Extend the MPC keygen client to surface the compressed secp256k1
public key alongside the chain-appropriate address, and derive a
bech32 P2WPKH locally for BITCOIN_* networks instead of relying on
mpcd's legacy P2PKH "1..." output.

Why: bridge release on BTC needs P2WPKH (witness-discount fees,
direct btcsuite/txscript compatibility, broader wallet support).
mpcd's pubKeyToBtcAddress emits Base58Check P2PKH; rather than
round-tripping a fix through mpcd, the bridge re-derives locally
from the same ECDSAPubKey the keygen already returns.

Changes:
- Wallet gains ECDSAPubKey []byte; 33-byte compressed form preferred,
  uncompressed (65/64) and x-only (32) inputs all compress canonically
- keygenResult parses both eth_address and evm_address (mpcd renamed)
- pickAddress() takes the network internal_name so BITCOIN_MAINNET vs
  BITCOIN_TESTNET selects the right bech32 hrp (bc / tb)
- new btc_address.go: deriveBTCBech32Address + compressSecp256k1Pubkey
  + bitcoinChainParams; uses btcd/btcutil for canonical HASH160 +
  segwit-v0 encoding
- 11 new tests covering mainnet/testnet hrp selection, 32/33/64/65
  byte pubkey shapes, empty/malformed input, keygen integration
2026-05-27 18:07:03 -07:00
zeekay c96474d7ff feat(bridge): wire TON end-to-end through mchain + pool + signing driver
internal/mchain:

- pickAddress for AddressTypeTON now derives the real v4r2 address
  from EDDSAPubKey when the keygen surfaces it (workchain=0 +
  DefaultSubwallet=698983191 + V4R2 code + pubkey → state_init.Hash).
  Falls back to the legacy SOL slot when the cluster hasn't yet
  emitted the Ed25519 pubkey, so existing mpcd deployments don't
  break.
- mchain.Wallet now carries ECDSAPubKey + EDDSAPubKey so downstream
  consumers (release_pool, signing_driver) can drive chain-specific
  flows without re-querying mpcd.
- New ton.go houses deriveTONAddressFromHex, delegating cell encoding
  to tonutils-go (same lib the txassembler depends on) so addresses
  surfaced at keygen time are bit-identical to those the txassembler
  rebuilds in state_init at first send.

cmd/bridge/release_pool.go:

- ReleasePoolEntry gains AddressType + ECDSAPubKey + EDDSAPubKey
  fields so the signing driver can route per-family. Legacy entries
  without these fields fall through to the EVM path (AddressType=
  empty equals "eth" via the dispatch default).

cmd/bridge/balance_probe.go:

- TON branch added to BalanceAt: GET <toncenter>/getAddressBalance,
  optional X-API-Key header from c.TONAPIKeys[network]. Returns
  *big.Int of nanoton. Same best-effort policy as the EVM path
  (probe failures log and skip rather than block signing).
- TONBalanceURLs + TONAPIKeys maps populated by main.go from
  networks.yaml's per-network Broadcast block.

cmd/bridge/signing_driver.go:

- New tonAssembler field + SetTONAssembler setter mirroring
  SetAssembler. Dispatch in signOne now branches on
  mchain.AddressTypeFor(DestinationNetwork): TON → PreSignTON path
  (looks up the pool entry's EDDSAPubKey, builds the v4r2 unsigned
  body, computes the nanoton-unit gas pre-check, finalizes via
  FinalizeTONHex into base64 BOC); otherwise → EVM path (unchanged).
- gasPrecheckTON: 0.05 TON gas floor for native transfers, 0.15 TON
  for jetton transfers (covers body value + forward TON + master
  call gas). Native transfers count the user amount against the
  release wallet; jetton transfers don't (the user value moves
  between jetton wallets, not out of ours).
- tonAmountToNano + isTONNativeAsset helpers — same fixed-point
  float→big.Int approach as the EVM floatToWei to avoid IEEE noise.

cmd/bridge/config.go:

- Network gains an optional Broadcast block (URL + APIKey) so
  operators configure TON Center endpoints per-network in
  networks.yaml without flag plumbing.

cmd/bridge/main.go:

- TON assembler instantiated at startup and wired into the signing
  driver via SetTONAssembler.
- Broadcast client picks up per-network sendBoc URLs + X-API-Keys
  from the new networks.yaml block.
- Balance probe picks up per-network getAddressBalance URLs (derived
  from sendBoc by path-swap) + the same X-API-Keys.
2026-05-27 18:04:24 -07:00
zeekay 84d4d56cf4 feat(mchain): derive substrate SS58 client-side; deprecate ErrSubstrateNotImplemented
The bridge previously fast-failed DOT keygen with ErrSubstrateNotImplemented.
The MPC cluster already returns a 33-byte compressed secp256k1 pubkey
(ecdsa_pub_key) — the only missing piece was the SS58 derivation, which
internal/substrate now provides.

Changes:
- Wallet.ECDSAPubKey: new field carrying the compressed pubkey hex.
  Required by the DOT signing path so the assembler can derive the
  AccountId and resolve the ECDSA recovery byte at Finalize.
- pickAddress: AddressTypeDOT now derives SS58 from ECDSAPubKey via
  blake2_256(pub) -> AccountId32 -> base58check with the per-network
  prefix (mainnet=0, Westend=42, Kusama=2). Returns ErrMissingPubKey
  if the cluster forgot to populate ecdsa_pub_key.
- networkAddressType: add POLKADOT_TESTNET, KUSAMA_MAINNET as DOT
  family (alongside the existing POLKADOT_MAINNET entry).
- ErrSubstrateNotImplemented: kept exported (callers may errors.Is)
  but documented as deprecated. KeygenForDeposit no longer returns it.

Tests updated:
- TestKeygen_DOT_DerivesSS58FromPubKey: pin a deterministic pubkey,
  verify the SS58 address starts with '1' (Polkadot prefix 0).
- TestKeygen_DOT_TestnetPrefix: Westend (prefix 42) addresses start
  with '5'.
- TestKeygen_DOT_MissingPubKeyErrors: empty ecdsa_pub_key -> ErrMissingPubKey.
- cmd/bridge swap handler test: TestSwapsCreate_DOTRequest_Succeeds
  replaces the old "returns 501" expectation.
2026-05-27 18:01:37 -07:00
zeekay 4a75d55b8d feat(broadcast): XRP submit via rippled JSON-RPC
internal/broadcast/xrp.go: XRP-family broadcaster targeting rippled's
`submit` method.

- Wire: POST {method:"submit", params:[{tx_blob:"<hex>"}]} to
  https://s1.ripple.com:51234/ (mainnet) or
  https://s.altnet.rippletest.net:51234/ (testnet). Strips a 0x prefix
  if the caller (every other broadcaster in this package emits 0x...)
  appends one — rippled rejects 0x-prefixed blobs.
- classifyEngineResult: maps rippled's engine_result codes to one of
  success / retryable / fatal:
    tesSUCCESS         → success → return result.tx_json.hash
    ter*               → retryable (broadcast driver re-submits)
    tel*               → retryable (local server, may resolve next round)
    tec* / tem* / tef* → fatal (consumed-fee failure, malformed, or
                          server-rejected past-seq / already / etc.)
    unknown / empty    → retryable (rippled wasn't sure yet)
  Behaviour matches xrpl.org/transaction-results.html.
- doPlainJSON helper added to client.go: rippled is JSON-RPC-shaped
  (method, params) but does NOT emit the {jsonrpc,id,result,error}
  2.0 envelope, so it needs its own POST/decode helper.
- Broadcast() dispatch grows an AddressTypeXRP arm.
- rpcURLs table grows XRP_MAINNET + XRP_TESTNET defaults; operators
  shadow per-env via --source-rpc-overrides.

Tests: tesSUCCESS happy path (returns tx_json.hash), terQUEUED
retryable, tecINSUFFICIENT_FUNDS + tecNO_DST fatal, temBAD_AMOUNT
malformed fatal, telINSUF_FEE_P retryable, high-level rippled error
surfaced as RPCError, network transport error, success-without-hash
rejection, 0x-prefix stripping, classifyEngineResult table.
2026-05-27 17:59:02 -07:00
zeekay bdfadf528c feat(txassembler): Polkadot ECDSA extrinsic builder + Finalize
Add internal/txassembler/dot.go — the DOT analogue of the EVM Assembler.

Pipeline:

  1. DOTSpec  ── PreSign ──>  DOTUnsigned (call bytes + signing payload)
  2. DOTUnsigned + (r,s,v) ── Finalize ──>  raw_extrinsic_hex + extrinsic_hash

PreSign behaviour:
  - decode recipient SS58, refuse cross-prefix transfers (operator
    typo'ing a mainnet → testnet address surfaces loudly).
  - SCALE-encode balances.transfer_keep_alive(MultiAddress::Id(dest),
    Compact<value>) using the per-network CallIndex.
  - derive signer AccountId32 from compressed pubkey via
    blake2_256(pub) — substrate ECDSA convention.
  - build ExtrinsicPayloadV4 = call || era(immortal) || nonce ||
    tip || spec_version || tx_version || genesis_hash || block_hash;
    blake2_256-hash it if it exceeds 256 bytes.

Finalize behaviour:
  - canonicalize low-s and (if hint was definite) flip v.
  - If recoveryHint ∈ {0,1}: try only that v.
    If 0xff: try both v=0 and v=1, pick the one whose recovered
    pubkey verifies the signature against the signer pubkey.
    Neither matching → "sign context wrong" (the MPC signed the
    wrong payload, or the wrong key share was used).
  - Wrap in MultiSignature::Ecdsa (tag 0x02) || 65-byte (r||s||v).
  - Assemble v4 signed extrinsic via substrate.AssembleSignedExtrinsic.

dot_crypto.go: self-contained pure-Go secp256k1 ECDSA verify (math/big
only). About 100 lines: curve params, affine point arithmetic,
compressed-pubkey decoding via y = x3^((P+1)/4), standard ECDSA verify.
No CGO; runs cross-arch.

Tests cover PreSign happy-path with golden call bytes, prefix-mismatch
refusal, bad-input rejection, Finalize round-trip with both an explicit
and an auto-discovered recovery byte, wrong-signer-fails-both,
ParseDOTSignature normalization (27/28 → 0/1, unknown → 0xff), and the
ECDSA verify self-check against known scalars.
2026-05-27 17:56:05 -07:00
zeekay 61d403336e test(bridge): TON broadcast + v4r2 assembler test coverage
internal/broadcast/ton_test.go:

- Happy path: hex payload → base64 on wire, X-API-Key header set
  per-network, response hash returned verbatim.
- Base64 payload pass-through (skip the hex re-encode).
- Error classification matrix:
    429 / "rate" → ErrTONRateLimited (retryable)
    "invalid boc" / "wrong signature" → ErrTONMalformedBOC (fatal)
    "account not initialized" → ErrTONAccountNotInitialized (re-build w/ state_init)
    HTTP 502/503/504 → TONError{Retryable:true}
- Family-dispatch parity: TON_TESTNET must NOT trip ErrFamilyNotImplemented.
- Direct parser tests (no HTTP) for non-JSON 5xx + hash-less success
  payloads (synthesized "ton:sendBoc:ok:200" identifier).
- isHexString + payload normalizer unit tests.

internal/txassembler/ton_test.go:

- Address derivation: same pubkey ⇒ same address (deterministic),
  different pubkeys ⇒ different addresses, UQ vs EQ flags surface
  correctly via TONAddressFromPubKey / …BounceableFromPubKey, raw
  form parses back to the same address (parse-roundtrip via
  ParseTONAddress).
- PreSign native transfer: deterministic body hash under a fixed
  clock, StateInit populated/skipped per SourceInitialized flag,
  rejects nil / zero / negative amounts + bad pubkey + spec
  SourceAddress mismatch.
- PreSign jetton: spec-provided JettonSourceWallet skips the
  JettonProvider, provider path is invoked with the right
  (network, master, owner) args, missing-provider + missing-master
  both error cleanly.
- PreSign + FinalizeTONHex round-trip: locally sign u.SigHash with
  Ed25519, finalize, decode the resulting BOC, verify
  ExternalMessage.Body's first 64 bytes match the signature.
- Body schema: 1 ref (the internal message), 112 bits of header
  data (subwallet 32 + valid_until 32 + seqno 32 + op 8 + mode 8).
- State_init hash matches the pubkey-derived address — confirms
  the wallet contract we deploy on first send IS the wallet
  contract whose address we keygen'd to.
- Static seqno provider drives v4r2 body's seqno field.
2026-05-27 17:55:34 -07:00
zeekay 5c4ae63411 feat(xrpl): Payment codec + address derivation + DER low-s signature
Introduces internal/xrpl, the bridge's XRP Ledger primitives package:

- AddressFromPubKey / AccountIDFromPubKey: derive the canonical r-address
  from a 33-byte compressed-secp256k1 public key. Hash chain is
  RIPEMD160(SHA256(pubkey)), base58check-encoded with the XRPL alphabet
  + 0x00 version byte. Round-trips through the rubblelabs codec.
- PreSign(PaymentUnsigned) → (32-byte digest, *SignCtx): builds a native
  XRP Payment, embeds the signing pubkey on the wire, returns the
  SHA-512Half of (0x53545800 || serialized_body) — exactly the message
  the MPC quorum must sign.
- Finalize(DER, *SignCtx) → (signedBlob, txHash): attaches the DER
  signature to TxnSignature, returns the full wire-format binary blob
  (suitable for submit's tx_blob param as hex) and the canonical XRPL
  tx hash (SHA-512Half of 0x54584E00 || blob).
- EncodeDERSignature(r, s): ASN.1 DER encode (r, s) with low-s
  normalization (rejects high-s as XRPL's tfFullyCanonicalSig requires)
  and the 0x00 leading-byte rule for INTEGERs with the MSB set.
- CanonicalizeS / SHA512Half exposed for tests + future TrustSet path.

Uses github.com/rubblelabs/ripple for the deep binary codec — every
field-id, type-byte, and length-prefix detail is the library's
responsibility. The bridge owns only the signing flow on top, so MPC
ECDSA can stand in for the library's local-key Sign helper.

Tests cover: canonical pubkey → r-address round-trip, rejection of
bad pubkey lengths/prefixes, DER encoding of standard and high-bit
cases, low-s flip for s > N/2, validation that high-s gets canonicalized
in EncodeDERSignature, PreSign digest matching independent SHA512Half
recomputation, full PreSign → Finalize round-trip via the parser.
2026-05-27 17:54:07 -07:00
zeekay 73aaa949fb feat(bridge): TON broadcast + v4r2 wallet tx assembler
Add the wire-correct TON destination handler so the bridge can release
into TON_MAINNET / TON_TESTNET via TON Center.

internal/broadcast/ton.go:

- broadcastTON pushes a signed external-message BOC to TON Center's
  sendBoc endpoint (mainnet: toncenter.com/api/v2/sendBoc; testnet:
  testnet.toncenter.com/api/v2/sendBoc).
- X-API-Key header sourced per-network from Client.TONAPIKeys (set via
  SetTONAPIKey or populated by main.go from networks.yaml). Required
  at production rates — public gateway throttles to ~1 req/s.
- normalizeTONPayload accepts the hex shape the txassembler emits (to
  match the EVM raw-tx convention) and converts to base64 for the
  toncenter wire. Pre-base64 payloads pass through.
- parseTONResponse classifies error responses: 429 / "rate" →
  ErrTONRateLimited, "account not initialized" / "state_init" →
  ErrTONAccountNotInitialized, "invalid boc" / "wrong signature" →
  ErrTONMalformedBOC (fatal — signer must re-build), other 5xx /
  transport → TONError{Retryable: true}.
- Returns the in-message hash as BroadcastResult.TxHash. Callers
  resolve the on-chain tx hash later via getTransactions polling.

internal/txassembler/ton.go:

- Two-phase PreSign / Finalize parallel to the EVM path. PreSignTON
  builds the wallet-v4r2 unsigned body cell (subwallet || valid_until
  || seqno || op=0 || message_mode || ref_to_internal_msg) and
  surfaces its representation hash for the MPC Ed25519 quorum to
  sign. FinalizeTON / FinalizeTONHex re-assemble with the 64-byte
  signature, wrap in an external-in message (with state_init when
  SourceInitialized=false so first sends self-deploy), and emit BOC
  bytes (or base64) for the broadcast layer.
- Native TON transfer + jetton transfer (TRC-3 op=0x0f8a7ea5) under
  one spec. Jetton path resolves the source's jetton-wallet address
  off the master either via TONJettonWalletAddressProvider (live
  runMethod) or operator-supplied TONSpec.JettonSourceWallet. Forward
  TON convention 0.05 TON (50_000_000 nanoton) for gas — same
  amount the body itself carries to the master.
- Wallet v4r2 (= tonwallet.V4R2) + sub-wallet 698983191 (=
  tonwallet.DefaultSubwallet). messages_ttl=180s matches upstream.
- Address helpers: TONAddressFromPubKey (UQ), …Bounceable… (EQ),
  TONRawAddress ("<workchain>:<hex>"), ParseTONAddress (accepts any
  form). Exported so mchain.pickAddress can derive a real TON
  address from EDDSAPubKey once mpcd surfaces it, and so the
  deposit-watcher can match incoming-deposit addresses.

internal/broadcast/client.go:

- Add TONAPIKeys map on Client + AddressTypeTON dispatch case
  routing to broadcastTON. ErrFamilyNotImplemented message updated
  (BTC/SOL/XRP/DOT remain not implemented; EVM + TON are in).
2026-05-27 17:52:24 -07:00
zeekay dd4a5e3d61 feat(bridge): SOL broadcaster, lamport balance probe, per-family pools
Three layered changes to drive Solana to the broadcast leg of the pipeline:

1. internal/broadcast/sol.go — sendTransaction over JSON-RPC.

   broadcastSOL packs the base64-encoded signed tx that the assembler
   produced into a Solana sendTransaction call with
   {encoding:"base64", preflightCommitment:"confirmed", skipPreflight:false}.
   preflightCommitment matches the assembler's blockhash commitment so
   the cluster's preflight simulation runs against a consistent
   snapshot.

   Two typed errors mirror the canonical upstream messages:
   - SOLBlockhashNotFoundError — Retryable() == true. The signing
     driver should drop the current message, rebuild PreSign with a
     fresh blockhash, re-run the MPC ceremony.
   - SOLSimulationError — Retryable() == false. Deterministic failure
     (insufficient SPL balance, malformed instruction, missing ATA);
     same MPC ceremony won't fix it. Logs extracted from error.data
     are surfaced for operator triage.
   Anything else falls through to the existing *RPCError shape.

   client.go: register SOLANA_{MAINNET,DEVNET,TESTNET} in rpcURLs +
   dispatch AddressTypeSOL → broadcastSOL.

2. balance_probe.go — getBalance for Solana (lamports).

   BalanceAt now branches on network family:
   - EVM: eth_getBalance, returns wei
   - SOL: getBalance with commitment=confirmed, returns lamports
   - Other non-EVM (BTC/TON/XRP/DOT): ErrFamilyNotSupportedForBalance,
     gas pre-check skipped (preserves existing behaviour)

   The new doRPC helper extracts shared HTTP transport so the EVM and
   SOL paths don't duplicate boilerplate. Lamport result decoded
   through json.Number → *big.Int for safety even though it always
   fits in uint64.

3. cmd/bridge/release_pool_set.go + release_pool_family_store.go —
   per-family release-wallet pool routing.

   The existing *ReleasePool is single-family by design (one mint
   network, one rotation cursor). Multi-family deployments need
   independent pools per family because the keygen call, address
   format, balance unit, and funding flow are all family-scoped.

   ReleasePoolSet wraps {EVM, SOL, BTC} *ReleasePool pointers and
   dispatches Acquire by mchain.AddressTypeFor(destinationNetwork). A
   nil per-family pointer surfaces ErrNoPoolForFamily so the signing
   driver falls back to the deposit-as-release legacy path.

   Family-scoped persistence (ZapStoreFamily / InMemoryStoreFamily)
   keys entries under releasepool:<family>:NNNNNN. The legacy
   releasepool:NNNNNN namespace stays intact for backward compat with
   pre-multi-family deployments. FamilyEVM/SOL/BTC constants are
   wire-stable (changing them breaks reload of existing pools).

   The InMemoryStore now holds an extra familyPools map alongside the
   single-pool slot — the legacy pool path stays exactly as it was;
   multi-family layers in.

Tests:
  - internal/broadcast/sol_test.go: happy path, malformed response,
    BlockhashNotFound retryable classification, SimulationError fatal
    classification + log extraction, empty raw tx, empty result,
    classifySOLError direct unit coverage, RPC URL table presence.
  - cmd/bridge/release_pool_set_test.go: family routing (EVM vs SOL),
    per-family round-robin isolation, Size + FamilySizes diagnostics,
    InMemoryStoreFamily namespace isolation + ascending-index iter,
    FamilyCode constant pinning.

All package tests green:
  ok  cmd/bridge        3.957s
  ok  internal/broadcast (cached)
  ok  internal/mchain   (cached)
  ok  internal/txassembler (cached)
2026-05-27 17:52:15 -07:00
zeekay 3663252fd8 feat(broadcast): wire Polkadot author_submitExtrinsic + error classification
Add the substrate broadcast handler at internal/broadcast/dot.go:

- POST {"method":"author_submitExtrinsic","params":["0x<hex>"]} against
  the network's substrate RPC, return the 0x-prefixed extrinsic hash
  on success.
- DOTBroadcastError classification (retryable vs fatal) by substrate
  error code + message:
    Invalid::Future / AlreadyImported / pool-full / runtime-upgrade
        -> retryable (orchestrator polls again next tick)
    Invalid::Stale / Invalid::BadProof / Module(...)
        -> fatal (re-sign needed or pallet rejection)
- Default RPC endpoints: rpc.polkadot.io (mainnet),
  westend-rpc.polkadot.io (testnet), kusama-rpc.polkadot.io (canary).

Wire dispatch in client.go Broadcast switch: AddressTypeDOT lands in
broadcastDOT. ErrFamilyNotImplemented now reads BTC/SOL/TON/XRP only.

Tests cover happy-path, Stale/BadProof fatal, Future retryable,
AlreadyImported retryable, Module error fatal, 4xx-with-envelope, 5xx
without envelope, empty result, and error-string formatting.
2026-05-27 17:50:56 -07:00
zeekay 4dfb24878c feat(txassembler): SOL message build + signature finalize
Adds SOLAssembler — companion to the EVM Assembler — that constructs
Solana transactions for the bridge's destination-release flow:

  - PreSign: fetch a recent blockhash via JSON-RPC getLatestBlockhash,
    build a v0 message (one signer = the MPC release wallet as fee
    payer, one recipient, lamport amount), serialize the bytes to be
    signed. SPL token mode (when SourceMint is set) derives source +
    destination ATAs via solana.FindAssociatedTokenAddress and probes
    the destination ATA via getAccountInfo — if absent, the assembler
    prepends a CreateAssociatedTokenAccount instruction so the release
    wallet pays the ~2_039_280 lamport rent + creates the account.
  - Finalize: slot the 64-byte Ed25519 signature into Transaction
    .Signatures[0], serialize the full wire tx with MarshalBinary, and
    base64-encode for JSON-RPC sendTransaction (encoding:base64). The
    signature itself doubles as the canonical Solana "tx hash" — return
    it base58-encoded for SDK consumption.

Dependency: github.com/gagliardetto/solana-go v1.20.0 — the de facto
standard Go client. The assembler stays decoupled from the broader
*rpc.Client beyond GetLatestBlockhash + GetAccountInfo, so the
broadcast layer can use its own JSON-RPC endpoint.

Helpers:
  - LamportsFromFloat(amount, decimals): safe float → uint64 conversion
    with overflow protection. 9dp for native SOL; varies for SPL.
  - ParseSOLSignatureHex(s): decodes a 128-char hex string to a fixed
    [64]byte buffer — used by the signing driver when the dashboard
    returns the Ed25519 sig in hex form.
  - SOLDefaultBlockhashURL(network): canonical RPC URL per network.

Tests (sol_test.go) cover:
  - Native transfer happy path: instruction is system.Transfer at
    Account[0] = payer; round-trip of MessageBytes through
    Message.Unmarshal yields an identical message.
  - SPL transfer with destination ATA missing: 2 instructions
    (ata.Create, then token.Transfer), destination ATA correctly
    derived and probed.
  - SPL transfer with destination ATA present: only 1 instruction.
  - Finalize round-trip: rawTx parses back through solana-go's
    Transaction decoder with matching signature + payer + blockhash.
  - Error paths: unknown network, zero amount, malformed payer addr.
  - ParseSOLSignatureHex + LamportsFromFloat: corner cases.

Total: 12 SOL tests, all green.
2026-05-27 17:45:02 -07:00
zeekay a560468888 feat(substrate): SCALE codec, SS58 encoder, signed extrinsic builder
Add internal/substrate package containing the minimal Polkadot wire
primitives the bridge needs to sign + broadcast balances.transfer_keep_alive
extrinsics:

- SCALE codec primitives (compact ints, fixed-width LE ints, byte slices
  bare + length-prefixed) — enough to encode ExtrinsicPayloadV4 + a v4
  signed extrinsic. Not a general reflection-driven encoder.
- AccountId32 derivation from a compressed secp256k1 pubkey via
  blake2_256 — matches sp_io::hashing::blake2_256 on the runtime side.
- SS58 base58check encoder + decoder with single-byte network prefixes
  (Polkadot=0, Kusama=2, generic=42). Verified against the canonical
  Alice fixture in both Polkadot mainnet ("15oF4u...") and substrate
  generic ("5Grwva...") forms.
- ExtrinsicPayloadV4 builder: SCALE(call) || era || nonce || tip ||
  spec_version || tx_version || genesis_hash || block_hash, with the
  >256-byte → blake2_256 hashing rule.
- v4 signed-extrinsic assembler: 0x84 version byte || MultiAddress::Id
  signer (33B) || MultiSignature(scheme, sig) || era || nonce || tip ||
  call, length-prefixed compact-style.

The substrate primitives are leaf code with no network I/O. Test vectors
are pinned against the Polkadot JS SDK and substrate runtime references.
2026-05-27 17:43:16 -07:00
zeekay 83f854f160 feat(mchain): per-curve keygen + sign — Ed25519 hint for SOL/TON/DOT
The lux-mpc cluster supports both ECDSA/secp256k1 and EdDSA/Ed25519
(FROST) keygens. The bridge previously hardcoded secp256k1 because
every active swap path was EVM/BTC. To wire Solana end-to-end we need
the cluster to mint an Ed25519 wallet and sign over raw Solana message
bytes — neither of which work under secp256k1.

Add a Curve type with two members (secp256k1, ed25519) and a
CurveFor(AddressType) mapping. Forward the curve hint on both the
keygen and dashboard /v1/mpc/sign request bodies; older daemons that
don't recognize the field fall back to their compile-time default
(secp256k1) — which produces an ECDSA-shaped signature the SOL
finalizer will reject with a clear error, so a deployment mismatch
fails fast instead of silently producing bad release txs.

SignForWallet and SignForWalletWithOrg stay on secp256k1 (default
behaviour preserved). The new SignForWalletOnCurve/OnCurveWithOrg
entry points let the signing driver pass an Ed25519 hint per swap.

Tests:
  - TestKeygen_SOL_SendsEd25519Curve: SOL keygen body has curve=ed25519
  - TestKeygen_ETH_SendsSecp256k1Curve: ETH stays on secp256k1
  - TestCurveFor: coverage across all six AddressTypes
2026-05-27 17:41:04 -07:00
zeekay 78ff44fe9d feat(mchain): wire SignForWallet to dashboard /v1/mpc/sign (real JWT/API-key path)
The live lux-mpc v2026-05 daemon does NOT expose /sign on the internal
port (:6000); only /keygen lives there. Signing is on the dashboard
listener (port 8081) at POST /v1/mpc/sign with {message, encoding,
walletId, sessionId, value} returning {signature, r, s}. That endpoint
requires a pre-created session minted by POST /v1/mpc/wallets/{id}/sessions.

This commit:

- Adds DashboardURL + DashboardToken (Bearer) + DashboardAPIKey
  (X-API-Key) fields to mchain.Client. Bearer wins when both are set.
- Adds internal/mchain/auth.go with EnsureDashboardSession() that
  lazily mints + caches one signing grant per (walletID, orgID) per
  process. Sessions live 24h with a 15-minute renewal skew.
- signViaDashboard() handles POST /v1/mpc/sign, including a single
  403-retry path that drops the cached session and re-mints. The
  dashboard sometimes returns only {r,s}; the client composes a
  canonical 65-byte EVM signature with v=27 (the assembler's
  ParseRSV tolerates the parity bit and any malleability would be
  caught at broadcast).
- pending_approval sessions are rejected as a hard error (the bridge
  cannot wait for a human approver mid-swap).
- SignForWalletWithOrg now routes through the dashboard when
  DashboardURL is set; APIURL-only configs keep the legacy /sign
  path for in-process mocks.
- New auth_test.go covers happy path, session caching across signs,
  403-triggered renew, Bearer vs API-key precedence, pending_approval,
  HTTP error surfacing, and r+s composition with zero-padding.

Builds on whispers/bridgev2 @ 2102068.
2026-05-27 17:02:08 -07:00
venuswhispers 21020685d9 feat: refunding has been working 2026-05-28 00:10:03 +02:00
venuswhispers a35e775173 feat: simple base swap is working now 2026-05-27 22:04:22 +02:00
venuswhispers b1241c9fda feat: added mpc sign option 2026-05-27 14:49:55 +02:00