feat(boltz): Boltz submarine & reverse swaps#116
Conversation
Integrate Boltz submarine (onchain -> Lightning) and reverse
(Lightning -> onchain) swaps behind the UniFFI surface.
Swap keys and reverse-swap preimages are derived deterministically from
the wallet seed via Boltz's BIP85 scheme (SwapMasterKey/derive_swapkey,
Preimage::from_swap_key). No key material is persisted: boltz.db stores
only a monotonic per-swap derivation index, so a leaked database cannot
move funds and swaps are recoverable from the seed alone (or via Boltz's
rescue API if boltz.db is lost).
- Submarine create/refund and reverse create/claim, with cooperative
key-path then script-path fallback (delegated to boltz-client).
- Managed WebSocket updates stream that auto-claims confirmed reverse
swaps; mnemonic held in memory only for the stream's lifetime.
- Atomic, collision-free swap-index reservation; schema user_version
anchor; input validation on create.
- Idempotent claim/refund (returns the recorded txid without
re-broadcasting).
- SQLite persistence, typed lifecycle status with forward-compatible
Unknown { raw }, and recovery/listing APIs.
- Unit tests for status mapping, DB round-trip, index reservation, and
deterministic derivation; ignored live E2E test against the Boltz API.
ovitrif
left a comment
There was a problem hiding this comment.
reviewed the current draft, added a few considerations in scoped review comments
| referral_id: None, | ||
| webhook: None, | ||
| }; | ||
| let response = client |
There was a problem hiding this comment.
Could we validate the Boltz create response before persisting or returning it? boltz-client exposes response.validate(...) for both submarine and reverse swaps, and that check proves the returned script/address matches our derived key and invoice/preimage before the app sends funds or pays the invoice.
|
|
||
| /// Fetch fees and limits for submarine swaps (onchain -> Lightning). | ||
| #[uniffi::export] | ||
| pub async fn boltz_get_submarine_limits( |
There was a problem hiding this comment.
Since this adds new UniFFI exports and public types, could we bump the package version and regenerate the mobile/Python bindings in this PR? Right now Rust builds, but downstream iOS/Android consumers would not get the new boltz* APIs from the checked-in artifacts.
| })?; | ||
| // Idempotent: don't re-broadcast a swap that already has a claim tx | ||
| // (e.g. an auto-claim that ran first). | ||
| if let Some(existing) = record.claim_tx_id.clone() { |
There was a problem hiding this comment.
Could we serialize claim/refund attempts per swap, or mark a claim/refund as in progress before broadcasting? As written, auto-claim and a manual recovery call can both see no recorded txid and try to broadcast before either path persists the result.
Summary
Adds a
boltzmodule integrating Boltz submarine (onchain → Lightning) and reverse (Lightning → onchain) swaps behind the UniFFI surface, for iOS/Android/Python.The dangerous cryptography (MuSig2 Taproot cooperative signing, swap scripts, claim/refund tx construction) is delegated to the
boltz-clientcrate. This module adds deterministic key management, SQLite persistence, lifecycle tracking, automatic claiming, and the FFI surface.Key design decision: deterministic keys, no stored secrets
Swap keys and reverse-swap preimages are derived from the wallet seed via Boltz's BIP85 scheme (
SwapMasterKey/derive_swapkey,Preimage::from_swap_key) — never random, never persisted.boltz.dbstores only a monotonic per-swap derivation index.Consequences:
boltz.dbis lost.mnemonic(+ optional BIP39 passphrase) now flows through the create/claim/refund/start-updates FFI calls. The background updates stream holds the mnemonic in memory only for its lifetime (dropped on stop) to auto-claim. The passphrase must match the wallet's, or derived keys won't control the funds.What's included
transaction.confirmed(not mempool, to avoid revealing the preimage against an unconfirmed lockup).PRAGMA user_versionmigration anchor; input validation on create.Unknown { raw }; recovery/listing APIs.Testing
cargo build, all 9 boltz unit tests, clippy, and fmt are clean. Tests cover status mapping, DB round-trip/recovery, monotonic index reservation, and deterministic derivation. An ignored live E2E test creates a real reverse swap and cryptographically validates the locally-derived redeem script + invoice against Boltz's response (no broadcast).Known follow-up: the claim/refund broadcast paths are not yet covered by an automated test — they need a regtest Boltz + Electrum stack. Recommended as a follow-up.