25 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
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
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 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 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
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 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 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 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 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
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
venuswhispers 9a551a554a feat: base bridge functions like swap function has been set almost 2026-05-25 23:08:12 +02:00