Files
bridge/internal/txassembler
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
..