From 4ab4071849aa21863891f679e6eea7a3b46a004c Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Thu, 5 Nov 2026 11:10:46 -0500 Subject: [PATCH 1/2] Removes the Boltz swap module and everything that only existed to serve it, since the Boltz service has shut down. --- AGENTS.md | 4 +- CHANGELOG.md | 1 + Cargo.lock | 395 +------ Cargo.toml | 1 - bindings/ios/bitkitcore.swift | 1821 +------------------------------ bindings/ios/bitkitcoreFFI.h | 165 --- src/lib.rs | 282 +---- src/modules/activity/types.rs | 12 - src/modules/boltz/README.md | 317 ------ src/modules/boltz/api.rs | 242 ---- src/modules/boltz/claim.rs | 128 --- src/modules/boltz/client.rs | 70 -- src/modules/boltz/db.rs | 252 ----- src/modules/boltz/errors.rs | 56 - src/modules/boltz/guard.rs | 32 - src/modules/boltz/listener.rs | 526 --------- src/modules/boltz/mod.rs | 23 - src/modules/boltz/models.rs | 177 --- src/modules/boltz/refund.rs | 108 -- src/modules/boltz/tests.rs | 727 ------------ src/modules/boltz/types.rs | 290 ----- src/modules/boltz/validation.rs | 219 ---- src/modules/mod.rs | 1 - tests/init_db.rs | 24 + 24 files changed, 36 insertions(+), 5837 deletions(-) delete mode 100644 src/modules/boltz/README.md delete mode 100644 src/modules/boltz/api.rs delete mode 100644 src/modules/boltz/claim.rs delete mode 100644 src/modules/boltz/client.rs delete mode 100644 src/modules/boltz/db.rs delete mode 100644 src/modules/boltz/errors.rs delete mode 100644 src/modules/boltz/guard.rs delete mode 100644 src/modules/boltz/listener.rs delete mode 100644 src/modules/boltz/mod.rs delete mode 100644 src/modules/boltz/models.rs delete mode 100644 src/modules/boltz/refund.rs delete mode 100644 src/modules/boltz/tests.rs delete mode 100644 src/modules/boltz/types.rs delete mode 100644 src/modules/boltz/validation.rs create mode 100644 tests/init_db.rs diff --git a/AGENTS.md b/AGENTS.md index 7577cf8d..e582fb8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Android bindings are built and published by `.github/workflows/gradle-publish.ym ```bash cargo test # All tests -cargo test modules:: # Single module (scanner, lnurl, onchain, activity, blocktank, boltz, trezor, jade, hardware_wallet, ur, pubky, paykit_route, pubky_swaps, usdt) +cargo test modules:: # Single module (scanner, lnurl, onchain, activity, blocktank, trezor, jade, hardware_wallet, ur, pubky, paykit_route, pubky_swaps, usdt) ``` ## Lint & Format @@ -35,7 +35,7 @@ Android bindings use ktlint via Gradle plugin (`org.jlleitschuh.gradle.ktlint`), ## Architecture - `src/lib.rs` — UniFFI exports and module re-exports -- `src/modules/`: core modules: scanner, lnurl, onchain, activity, blocktank, boltz, trezor, jade, hardware_wallet, ur, pubky, paykit_route, pubky_swaps, usdt +- `src/modules/`: core modules: scanner, lnurl, onchain, activity, blocktank, trezor, jade, hardware_wallet, ur, pubky, paykit_route, pubky_swaps, usdt - `bindings/` — Platform-specific binding outputs (ios/, android/, python/) - `build.sh`, `build_ios.sh`, `build_android.sh`, `build_python.sh` — Build scripts diff --git a/CHANGELOG.md b/CHANGELOG.md index b80abcd2..6fa67041 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Upgrade `trezor-connect-rs` to 10.0.1. The THP handshake now accepts a paired state only with a verified stored credential. A paired state without one, or an unknown state, returns `TrezorError::ConnectionError` instead of continuing to session creation. A malformed completion frame is now an error too, and the callback transport retries it like other temporary failures. A stored credential with a low-order device key no longer verifies, so the device pairs again. - `onchain_compose_transaction` keeps the synced watch-only wallet per extended key, script type, network and Electrum URL, so later composes for the same account run an incremental sync that reuses stored transactions instead of a full sync. Every compose still syncs before building PSBTs. Composes with and without a fingerprint share one wallet. Against the staging regtest Electrum with the Trezor emulator account, a compose drops from about 7.5 s to about 3.4 s after the first one. - Add a `wake` module for the wake push gateway: device key, secret and install id generation, a signed registration (`wake_prepare_registration`, `wake_sign_pubky_proof`, `wake_register`), decryption of legacy (v0) and v1 envelopes from any delivered push (`wake_decrypt_push`), and the device routes for acks, presence, topics and unregistering. Grants let a peer wake the device (`wake_create_grant`, `wake_list_grants`, `wake_revoke_grant`), and `wake_send_peer_wake` sends a peer wake with a grant token another device handed over. The v1 wire format is experimental until the apps adopt it. `register_device` and `test_notification` are unchanged. +- Remove the Boltz swap module, since the Boltz service has shut down. All `boltz*` functions and the Boltz types (`BoltzNetwork`, `BoltzSwapType`, `BoltzSwapStatus`, `BoltzPairInfo`, `SubmarineSwapResponse`, `ReverseSwapResponse`, `BoltzSwap`, `BoltzSwapEvent`, `BoltzError`, `BoltzEventListener`) are gone, along with the `boltz-client` dependency. `init_db` no longer creates or opens `boltz.db`, and leaves an existing one on disk untouched. Note that removing the `DbError.DbBoltzError` case changes exhaustive Kotlin `when` and Swift `switch` statements over `DbError`, which is source breaking for consumers. ## 0.5.16 - 2026-09-14 diff --git a/Cargo.lock b/Cargo.lock index 75333b0b..9e275be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,7 @@ dependencies = [ "rustc-hash", "secp256k1 0.31.1", "serde", - "sha3 0.11.0", + "sha3", ] [[package]] @@ -153,7 +153,7 @@ dependencies = [ "proc-macro-error3", "proc-macro2", "quote", - "sha3 0.11.0", + "sha3", "syn 2.0.117", "syn-solidity", ] @@ -209,7 +209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" dependencies = [ "android_log-sys", - "env_filter 0.1.4", + "env_filter", "log", ] @@ -222,56 +222,12 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - [[package]] name = "anyhow" version = "1.0.102" @@ -787,15 +743,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "base85" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36915bbaca237c626689b5bd14d02f2ba7a5a359d30a2a08be697392e3718079" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "basic-toml" version = "0.1.10" @@ -883,19 +830,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "bip85_extended" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f300582d6cf7cb4b1f59971f14b4353015a3538b3aa048e3de85c7ef80ac34c9" -dependencies = [ - "base64 0.22.1", - "base85", - "bip39", - "bitcoin 0.32.8", - "sha3 0.10.9", -] - [[package]] name = "bitcoin" version = "0.30.2" @@ -918,7 +852,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", - "base64 0.21.7", "bech32 0.11.1", "bitcoin-internals 0.3.0", "bitcoin-io", @@ -1054,7 +987,6 @@ dependencies = [ "bip39", "bitcoin 0.32.8", "bitcoin-address-generator", - "boltz-client", "btleplug", "chrono", "ed25519-dalek 2.2.0", @@ -1064,7 +996,7 @@ dependencies = [ "jade-client-rs", "jni 0.19.0", "lazy-regex", - "lightning-invoice 0.32.0", + "lightning-invoice", "lnurl-rs", "log", "minicbor", @@ -1207,50 +1139,6 @@ dependencies = [ "dbus", ] -[[package]] -name = "boltz-client" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "889efede8a28a19c3f6088ab5406bf1b8379a21a3470f24a5cef4cc6c7122346" -dependencies = [ - "async-trait", - "bip39", - "bip85_extended", - "bitcoin 0.32.8", - "boltz-client-macros", - "electrum-client 0.25.0", - "elements", - "env_logger", - "futures-util", - "getrandom 0.2.17", - "gloo-timers", - "hex", - "js-sys", - "lightning", - "lightning-invoice 0.34.2", - "log", - "reqwest 0.12.28", - "secp256k1 0.32.0-beta.2", - "serde", - "serde_json", - "tokio", - "tokio-tungstenite-wasm", - "url", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "boltz-client-macros" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bae3bbd82eb8c315b3b120d7cb1a8adef85003216315c42e23abead43ae3835" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "borsh" version = "1.6.0" @@ -1582,12 +1470,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - [[package]] name = "combine" version = "4.6.7" @@ -2161,12 +2043,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dnssec-prover" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4f825369fc7134da70ca4040fddc8e03b80a46d249ae38d9c1c39b7b4476bf" - [[package]] name = "document-features" version = "0.2.12" @@ -2317,19 +2193,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "elements" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1210978e2686f892318085f806866014d4788479a37f0a634db97fcb39454bde" -dependencies = [ - "bech32 0.11.1", - "bitcoin 0.32.8", - "secp256k1-zkp", - "serde", - "serde_json", -] - [[package]] name = "elliptic-curve" version = "0.13.8" @@ -2411,29 +2274,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_filter" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" -dependencies = [ - "anstream", - "anstyle", - "env_filter 2.0.0", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -2944,12 +2784,6 @@ dependencies = [ "ahash 0.7.8", ] -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" - [[package]] name = "hashbrown" version = "0.14.5" @@ -3638,12 +3472,6 @@ dependencies = [ "ws_stream_wasm", ] -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - [[package]] name = "itertools" version = "0.10.5" @@ -3875,15 +3703,6 @@ dependencies = [ "sha2 0.10.9", ] -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures 0.2.17", -] - [[package]] name = "keccak" version = "0.2.2" @@ -3998,23 +3817,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "lightning" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb355abfb61d8ffe201cd5ce6b3e4304c9a897b05988ceed71987f9514fb642" -dependencies = [ - "bech32 0.11.1", - "bitcoin 0.32.8", - "dnssec-prover", - "hashbrown 0.13.2", - "libm", - "lightning-invoice 0.34.2", - "lightning-macros", - "lightning-types 0.3.5", - "possiblyrandom", -] - [[package]] name = "lightning-invoice" version = "0.32.0" @@ -4023,29 +3825,7 @@ checksum = "90ab9f6ea77e20e3129235e62a2e6bd64ed932363df104e864ee65ccffb54a8f" dependencies = [ "bech32 0.9.1", "bitcoin 0.32.8", - "lightning-types 0.1.0", -] - -[[package]] -name = "lightning-invoice" -version = "0.34.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e874476e461bcca94ae30335bc5263f126f9219fdbd7bde6e02b530dbaa6567" -dependencies = [ - "bech32 0.11.1", - "bitcoin 0.32.8", - "lightning-types 0.3.5", -] - -[[package]] -name = "lightning-macros" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c717494cdc2c8bb85bee7113031248f5f6c64f8802b33c1c9e2d98e594aa71" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", + "lightning-types", ] [[package]] @@ -4059,15 +3839,6 @@ dependencies = [ "hex-conservative", ] -[[package]] -name = "lightning-types" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77c676d4a34cceb2ae3756916e446b4d17f9430a24107e099981f0f9aec77e6" -dependencies = [ - "bitcoin 0.32.8", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4878,12 +4649,6 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - [[package]] name = "opaque-debug" version = "0.3.1" @@ -5345,15 +5110,6 @@ dependencies = [ "url", ] -[[package]] -name = "possiblyrandom" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c564dbf654befd49035528299f1208a40508f6e07efb11c163444e304e4484f" -dependencies = [ - "getrandom 0.2.17", -] - [[package]] name = "postcard" version = "1.1.3" @@ -6621,16 +6377,6 @@ dependencies = [ "secp256k1-sys 0.11.0", ] -[[package]] -name = "secp256k1" -version = "0.32.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5fdc7d6e800869d3fd60ff857c479bf0a83ea7bf44b389e64461e844204994" -dependencies = [ - "rand 0.9.2", - "secp256k1-sys 0.12.0", -] - [[package]] name = "secp256k1-sys" version = "0.8.2" @@ -6658,38 +6404,6 @@ dependencies = [ "cc", ] -[[package]] -name = "secp256k1-sys" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d3be00697c88c00fe102af8dc316038cc2062eab8da646e7463f4c0e70ca9fd" -dependencies = [ - "cc", -] - -[[package]] -name = "secp256k1-zkp" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a44aed3002b5ae975f8624c5df3a949cfbf00479e18778b6058fcd213b76e3" -dependencies = [ - "bitcoin-private", - "rand 0.8.5", - "secp256k1 0.29.1", - "secp256k1-zkp-sys", - "serde", -] - -[[package]] -name = "secp256k1-zkp-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f08b2d0b143a22e07f798ae4f0ab20d5590d7c68e0d090f2088a48a21d1654" -dependencies = [ - "cc", - "secp256k1-sys 0.10.1", -] - [[package]] name = "security-framework" version = "3.7.0" @@ -6931,17 +6645,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha1_smol" version = "1.0.1" @@ -6970,16 +6673,6 @@ dependencies = [ "digest 0.11.3", ] -[[package]] -name = "sha3" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" -dependencies = [ - "digest 0.10.7", - "keccak 0.1.6", -] - [[package]] name = "sha3" version = "0.11.0" @@ -6987,7 +6680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ "digest 0.11.3", - "keccak 0.2.2", + "keccak", ] [[package]] @@ -7600,42 +7293,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "tokio-tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "rustls 0.23.46", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tungstenite", - "webpki-roots 0.26.11", -] - -[[package]] -name = "tokio-tungstenite-wasm" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4585aa997e4afb43c64f9101c27411b8e1bf9dde49b22e3e47acdde3055b325c" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "httparse", - "js-sys", - "rustls 0.23.46", - "thiserror 2.0.18", - "tokio", - "tokio-tungstenite", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -7860,25 +7517,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" -dependencies = [ - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.9.2", - "rustls 0.23.46", - "rustls-pki-types", - "sha1", - "thiserror 2.0.18", - "utf-8", -] - [[package]] name = "typenum" version = "1.19.0" @@ -8127,24 +7765,12 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - [[package]] name = "uuid" version = "1.22.0" @@ -8379,15 +8005,6 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.11", -] - [[package]] name = "webpki-roots" version = "1.0.11" diff --git a/Cargo.toml b/Cargo.toml index d069fed7..48d43c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ uuid = { version = "1.16.0", features = ["v4"] } hex = "0.4.3" bip39 = { version = "2.0", features = ["zeroize"] } bdk = { version = "0.30.2", features = ["all-keys"] } -boltz-client = { version = "0.4.1", default-features = false, features = ["electrum", "ws"] } base64 = "0.22" # Paykit Capabilities §5 signatures on providers' delegations (already in the lockfile). ed25519-dalek = "2" diff --git a/bindings/ios/bitkitcore.swift b/bindings/ios/bitkitcore.swift index 6b646266..77ea9748 100644 --- a/bindings/ios/bitkitcore.swift +++ b/bindings/ios/bitkitcore.swift @@ -601,195 +601,6 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer { -/** - * Callback interface for receiving Boltz swap lifecycle events. - * - * Implement this in Swift/Kotlin/Python and register it via - * `boltz_start_swap_updates` to receive typed notifications as swaps progress. - * Reverse swaps are claimed automatically; the [`BoltzSwapEvent::Claimed`] - * event reports the resulting transaction id. - */ -public protocol BoltzEventListener: AnyObject, Sendable { - - func onEvent(event: BoltzSwapEvent) - -} -/** - * Callback interface for receiving Boltz swap lifecycle events. - * - * Implement this in Swift/Kotlin/Python and register it via - * `boltz_start_swap_updates` to receive typed notifications as swaps progress. - * Reverse swaps are claimed automatically; the [`BoltzSwapEvent::Claimed`] - * event reports the resulting transaction id. - */ -open class BoltzEventListenerImpl: BoltzEventListener, @unchecked Sendable { - fileprivate let pointer: UnsafeMutableRawPointer! - - /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. -#if swift(>=5.8) - @_documentation(visibility: private) -#endif - public struct NoPointer { - public init() {} - } - - // TODO: We'd like this to be `private` but for Swifty reasons, - // we can't implement `FfiConverter` without making this `required` and we can't - // make it `required` without making it `public`. -#if swift(>=5.8) - @_documentation(visibility: private) -#endif - required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { - self.pointer = pointer - } - - // This constructor can be used to instantiate a fake object. - // - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. - // - // - Warning: - // Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. -#if swift(>=5.8) - @_documentation(visibility: private) -#endif - public init(noPointer: NoPointer) { - self.pointer = nil - } - -#if swift(>=5.8) - @_documentation(visibility: private) -#endif - public func uniffiClonePointer() -> UnsafeMutableRawPointer { - return try! rustCall { uniffi_bitkitcore_fn_clone_boltzeventlistener(self.pointer, $0) } - } - // No primary constructor declared for this class. - - deinit { - guard let pointer = pointer else { - return - } - - try! rustCall { uniffi_bitkitcore_fn_free_boltzeventlistener(pointer, $0) } - } - - - - -open func onEvent(event: BoltzSwapEvent) {try! rustCall() { - uniffi_bitkitcore_fn_method_boltzeventlistener_on_event(self.uniffiClonePointer(), - FfiConverterTypeBoltzSwapEvent_lower(event),$0 - ) -} -} - - -} - - -// Put the implementation in a struct so we don't pollute the top-level namespace -fileprivate struct UniffiCallbackInterfaceBoltzEventListener { - - // Create the VTable using a series of closures. - // Swift automatically converts these into C callback functions. - // - // This creates 1-element array, since this seems to be the only way to construct a const - // pointer that we can pass to the Rust code. - static let vtable: [UniffiVTableCallbackInterfaceBoltzEventListener] = [UniffiVTableCallbackInterfaceBoltzEventListener( - onEvent: { ( - uniffiHandle: UInt64, - event: RustBuffer, - uniffiOutReturn: UnsafeMutableRawPointer, - uniffiCallStatus: UnsafeMutablePointer - ) in - let makeCall = { - () throws -> () in - guard let uniffiObj = try? FfiConverterTypeBoltzEventListener.handleMap.get(handle: uniffiHandle) else { - throw UniffiInternalError.unexpectedStaleHandle - } - return uniffiObj.onEvent( - event: try FfiConverterTypeBoltzSwapEvent_lift(event) - ) - } - - - let writeReturn = { () } - uniffiTraitInterfaceCall( - callStatus: uniffiCallStatus, - makeCall: makeCall, - writeReturn: writeReturn - ) - }, - uniffiFree: { (uniffiHandle: UInt64) -> () in - let result = try? FfiConverterTypeBoltzEventListener.handleMap.remove(handle: uniffiHandle) - if result == nil { - print("Uniffi callback interface BoltzEventListener: handle missing in uniffiFree") - } - } - )] -} - -private func uniffiCallbackInitBoltzEventListener() { - uniffi_bitkitcore_fn_init_callback_vtable_boltzeventlistener(UniffiCallbackInterfaceBoltzEventListener.vtable) -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzEventListener: FfiConverter { - fileprivate static let handleMap = UniffiHandleMap() - - typealias FfiType = UnsafeMutableRawPointer - typealias SwiftType = BoltzEventListener - - public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> BoltzEventListener { - return BoltzEventListenerImpl(unsafeFromRawPointer: pointer) - } - - public static func lower(_ value: BoltzEventListener) -> UnsafeMutableRawPointer { - guard let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: handleMap.insert(obj: value))) else { - fatalError("Cast to UnsafeMutableRawPointer failed") - } - return ptr - } - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzEventListener { - let v: UInt64 = try readInt(&buf) - // The Rust code won't compile if a pointer won't fit in a UInt64. - // We have to go via `UInt` because that's the thing that's the size of a pointer. - let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) - if (ptr == nil) { - throw UniffiInternalError.unexpectedNullPointer - } - return try lift(ptr!) - } - - public static func write(_ value: BoltzEventListener, into buf: inout [UInt8]) { - // This fiddling is because `Int` is the thing that's the same size as a pointer. - // The Rust code won't compile if a pointer won't fit in a `UInt64`. - writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzEventListener_lift(_ pointer: UnsafeMutableRawPointer) throws -> BoltzEventListener { - return try FfiConverterTypeBoltzEventListener.lift(pointer) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzEventListener_lower(_ value: BoltzEventListener) -> UnsafeMutableRawPointer { - return FfiConverterTypeBoltzEventListener.lower(value) -} - - - - - - /** * Callback interface for receiving watcher events. * @@ -4109,376 +3920,6 @@ public func FfiConverterTypeAddressInfo_lower(_ value: AddressInfo) -> RustBuffe } -/** - * Fees and limits for a swap pair, used to size a swap and present costs. - */ -public struct BoltzPairInfo { - /** - * Pair hash identifying the current terms (passed back to Boltz if needed). - */ - public var hash: String - /** - * Exchange rate of the pair. - */ - public var rate: Double - /** - * Minimum swap amount in satoshis. - */ - public var minimalSat: UInt64 - /** - * Maximum swap amount in satoshis. - */ - public var maximalSat: UInt64 - /** - * Boltz service fee as a percentage of the swap amount. - */ - public var feePercentage: Double - /** - * Estimated absolute miner fees in satoshis. - */ - public var minerFeesSat: UInt64 - - // Default memberwise initializers are never public by default, so we - // declare one manually. - public init( - /** - * Pair hash identifying the current terms (passed back to Boltz if needed). - */hash: String, - /** - * Exchange rate of the pair. - */rate: Double, - /** - * Minimum swap amount in satoshis. - */minimalSat: UInt64, - /** - * Maximum swap amount in satoshis. - */maximalSat: UInt64, - /** - * Boltz service fee as a percentage of the swap amount. - */feePercentage: Double, - /** - * Estimated absolute miner fees in satoshis. - */minerFeesSat: UInt64) { - self.hash = hash - self.rate = rate - self.minimalSat = minimalSat - self.maximalSat = maximalSat - self.feePercentage = feePercentage - self.minerFeesSat = minerFeesSat - } -} - -#if compiler(>=6) -extension BoltzPairInfo: Sendable {} -#endif - - -extension BoltzPairInfo: Equatable, Hashable { - public static func ==(lhs: BoltzPairInfo, rhs: BoltzPairInfo) -> Bool { - if lhs.hash != rhs.hash { - return false - } - if lhs.rate != rhs.rate { - return false - } - if lhs.minimalSat != rhs.minimalSat { - return false - } - if lhs.maximalSat != rhs.maximalSat { - return false - } - if lhs.feePercentage != rhs.feePercentage { - return false - } - if lhs.minerFeesSat != rhs.minerFeesSat { - return false - } - return true - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(hash) - hasher.combine(rate) - hasher.combine(minimalSat) - hasher.combine(maximalSat) - hasher.combine(feePercentage) - hasher.combine(minerFeesSat) - } -} - -extension BoltzPairInfo: Codable {} - - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzPairInfo: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzPairInfo { - return - try BoltzPairInfo( - hash: FfiConverterString.read(from: &buf), - rate: FfiConverterDouble.read(from: &buf), - minimalSat: FfiConverterUInt64.read(from: &buf), - maximalSat: FfiConverterUInt64.read(from: &buf), - feePercentage: FfiConverterDouble.read(from: &buf), - minerFeesSat: FfiConverterUInt64.read(from: &buf) - ) - } - - public static func write(_ value: BoltzPairInfo, into buf: inout [UInt8]) { - FfiConverterString.write(value.hash, into: &buf) - FfiConverterDouble.write(value.rate, into: &buf) - FfiConverterUInt64.write(value.minimalSat, into: &buf) - FfiConverterUInt64.write(value.maximalSat, into: &buf) - FfiConverterDouble.write(value.feePercentage, into: &buf) - FfiConverterUInt64.write(value.minerFeesSat, into: &buf) - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzPairInfo_lift(_ buf: RustBuffer) throws -> BoltzPairInfo { - return try FfiConverterTypeBoltzPairInfo.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzPairInfo_lower(_ value: BoltzPairInfo) -> RustBuffer { - return FfiConverterTypeBoltzPairInfo.lower(value) -} - - -/** - * A persisted swap and its current state, returned by the listing/query APIs. - */ -public struct BoltzSwap { - public var id: String - public var swapType: BoltzSwapType - public var status: BoltzSwapStatus - public var network: BoltzNetwork - /** - * Deterministic BIP85 index used to derive this swap's key and preimage - * from the wallet seed. The recovery handle: given the seed and this index - * (or by scanning indices), the swap's secrets can be reconstructed. - */ - public var swapIndex: UInt64 - /** - * For submarine swaps: the amount to lock onchain. For reverse swaps: the - * Lightning invoice amount. - */ - public var amountSat: UInt64 - /** - * For reverse swaps: the onchain amount that will be received. - */ - public var onchainAmountSat: UInt64? - /** - * Lightning invoice associated with the swap (the hold invoice for reverse - * swaps, the invoice Boltz pays for submarine swaps). - */ - public var invoice: String? - /** - * Onchain lockup address. - */ - public var lockupAddress: String? - /** - * The address funds are claimed to (reverse) or refunded to (submarine). - */ - public var onchainAddress: String? - public var timeoutBlockHeight: UInt64 - public var createdAt: UInt64 - /** - * Txid of the claim transaction once broadcast (reverse swaps). - */ - public var claimTxId: String? - /** - * Txid of the refund transaction once broadcast (submarine swaps). - */ - public var refundTxId: String? - - // Default memberwise initializers are never public by default, so we - // declare one manually. - public init(id: String, swapType: BoltzSwapType, status: BoltzSwapStatus, network: BoltzNetwork, - /** - * Deterministic BIP85 index used to derive this swap's key and preimage - * from the wallet seed. The recovery handle: given the seed and this index - * (or by scanning indices), the swap's secrets can be reconstructed. - */swapIndex: UInt64, - /** - * For submarine swaps: the amount to lock onchain. For reverse swaps: the - * Lightning invoice amount. - */amountSat: UInt64, - /** - * For reverse swaps: the onchain amount that will be received. - */onchainAmountSat: UInt64?, - /** - * Lightning invoice associated with the swap (the hold invoice for reverse - * swaps, the invoice Boltz pays for submarine swaps). - */invoice: String?, - /** - * Onchain lockup address. - */lockupAddress: String?, - /** - * The address funds are claimed to (reverse) or refunded to (submarine). - */onchainAddress: String?, timeoutBlockHeight: UInt64, createdAt: UInt64, - /** - * Txid of the claim transaction once broadcast (reverse swaps). - */claimTxId: String?, - /** - * Txid of the refund transaction once broadcast (submarine swaps). - */refundTxId: String?) { - self.id = id - self.swapType = swapType - self.status = status - self.network = network - self.swapIndex = swapIndex - self.amountSat = amountSat - self.onchainAmountSat = onchainAmountSat - self.invoice = invoice - self.lockupAddress = lockupAddress - self.onchainAddress = onchainAddress - self.timeoutBlockHeight = timeoutBlockHeight - self.createdAt = createdAt - self.claimTxId = claimTxId - self.refundTxId = refundTxId - } -} - -#if compiler(>=6) -extension BoltzSwap: Sendable {} -#endif - - -extension BoltzSwap: Equatable, Hashable { - public static func ==(lhs: BoltzSwap, rhs: BoltzSwap) -> Bool { - if lhs.id != rhs.id { - return false - } - if lhs.swapType != rhs.swapType { - return false - } - if lhs.status != rhs.status { - return false - } - if lhs.network != rhs.network { - return false - } - if lhs.swapIndex != rhs.swapIndex { - return false - } - if lhs.amountSat != rhs.amountSat { - return false - } - if lhs.onchainAmountSat != rhs.onchainAmountSat { - return false - } - if lhs.invoice != rhs.invoice { - return false - } - if lhs.lockupAddress != rhs.lockupAddress { - return false - } - if lhs.onchainAddress != rhs.onchainAddress { - return false - } - if lhs.timeoutBlockHeight != rhs.timeoutBlockHeight { - return false - } - if lhs.createdAt != rhs.createdAt { - return false - } - if lhs.claimTxId != rhs.claimTxId { - return false - } - if lhs.refundTxId != rhs.refundTxId { - return false - } - return true - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(swapType) - hasher.combine(status) - hasher.combine(network) - hasher.combine(swapIndex) - hasher.combine(amountSat) - hasher.combine(onchainAmountSat) - hasher.combine(invoice) - hasher.combine(lockupAddress) - hasher.combine(onchainAddress) - hasher.combine(timeoutBlockHeight) - hasher.combine(createdAt) - hasher.combine(claimTxId) - hasher.combine(refundTxId) - } -} - -extension BoltzSwap: Codable {} - - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzSwap: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzSwap { - return - try BoltzSwap( - id: FfiConverterString.read(from: &buf), - swapType: FfiConverterTypeBoltzSwapType.read(from: &buf), - status: FfiConverterTypeBoltzSwapStatus.read(from: &buf), - network: FfiConverterTypeBoltzNetwork.read(from: &buf), - swapIndex: FfiConverterUInt64.read(from: &buf), - amountSat: FfiConverterUInt64.read(from: &buf), - onchainAmountSat: FfiConverterOptionUInt64.read(from: &buf), - invoice: FfiConverterOptionString.read(from: &buf), - lockupAddress: FfiConverterOptionString.read(from: &buf), - onchainAddress: FfiConverterOptionString.read(from: &buf), - timeoutBlockHeight: FfiConverterUInt64.read(from: &buf), - createdAt: FfiConverterUInt64.read(from: &buf), - claimTxId: FfiConverterOptionString.read(from: &buf), - refundTxId: FfiConverterOptionString.read(from: &buf) - ) - } - - public static func write(_ value: BoltzSwap, into buf: inout [UInt8]) { - FfiConverterString.write(value.id, into: &buf) - FfiConverterTypeBoltzSwapType.write(value.swapType, into: &buf) - FfiConverterTypeBoltzSwapStatus.write(value.status, into: &buf) - FfiConverterTypeBoltzNetwork.write(value.network, into: &buf) - FfiConverterUInt64.write(value.swapIndex, into: &buf) - FfiConverterUInt64.write(value.amountSat, into: &buf) - FfiConverterOptionUInt64.write(value.onchainAmountSat, into: &buf) - FfiConverterOptionString.write(value.invoice, into: &buf) - FfiConverterOptionString.write(value.lockupAddress, into: &buf) - FfiConverterOptionString.write(value.onchainAddress, into: &buf) - FfiConverterUInt64.write(value.timeoutBlockHeight, into: &buf) - FfiConverterUInt64.write(value.createdAt, into: &buf) - FfiConverterOptionString.write(value.claimTxId, into: &buf) - FfiConverterOptionString.write(value.refundTxId, into: &buf) - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwap_lift(_ buf: RustBuffer) throws -> BoltzSwap { - return try FfiConverterTypeBoltzSwap.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwap_lower(_ value: BoltzSwap) -> RustBuffer { - return FfiConverterTypeBoltzSwap.lower(value) -} - - public struct ChannelLiquidityOptions { public var defaultLspBalanceSat: UInt64 public var minLspBalanceSat: UInt64 @@ -12947,132 +12388,6 @@ public func FfiConverterTypePubkySwapSentPayment_lower(_ value: PubkySwapSentPay } -/** - * Result of creating a reverse swap (Lightning -> onchain). - * - * The caller pays `invoice` from its Lightning node; once Boltz locks funds at - * `lockup_address`, the module claims them to the provided onchain address. - */ -public struct ReverseSwapResponse { - public var id: String - /** - * Hold invoice the caller must pay via Lightning. - */ - public var invoice: String - /** - * Address Boltz locks the onchain funds to. - */ - public var lockupAddress: String - /** - * Amount in satoshis that will be received onchain (after Boltz fees). - */ - public var onchainAmountSat: UInt64 - /** - * Onchain timeout height for the swap. - */ - public var timeoutBlockHeight: UInt64 - - // Default memberwise initializers are never public by default, so we - // declare one manually. - public init(id: String, - /** - * Hold invoice the caller must pay via Lightning. - */invoice: String, - /** - * Address Boltz locks the onchain funds to. - */lockupAddress: String, - /** - * Amount in satoshis that will be received onchain (after Boltz fees). - */onchainAmountSat: UInt64, - /** - * Onchain timeout height for the swap. - */timeoutBlockHeight: UInt64) { - self.id = id - self.invoice = invoice - self.lockupAddress = lockupAddress - self.onchainAmountSat = onchainAmountSat - self.timeoutBlockHeight = timeoutBlockHeight - } -} - -#if compiler(>=6) -extension ReverseSwapResponse: Sendable {} -#endif - - -extension ReverseSwapResponse: Equatable, Hashable { - public static func ==(lhs: ReverseSwapResponse, rhs: ReverseSwapResponse) -> Bool { - if lhs.id != rhs.id { - return false - } - if lhs.invoice != rhs.invoice { - return false - } - if lhs.lockupAddress != rhs.lockupAddress { - return false - } - if lhs.onchainAmountSat != rhs.onchainAmountSat { - return false - } - if lhs.timeoutBlockHeight != rhs.timeoutBlockHeight { - return false - } - return true - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(invoice) - hasher.combine(lockupAddress) - hasher.combine(onchainAmountSat) - hasher.combine(timeoutBlockHeight) - } -} - -extension ReverseSwapResponse: Codable {} - - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeReverseSwapResponse: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ReverseSwapResponse { - return - try ReverseSwapResponse( - id: FfiConverterString.read(from: &buf), - invoice: FfiConverterString.read(from: &buf), - lockupAddress: FfiConverterString.read(from: &buf), - onchainAmountSat: FfiConverterUInt64.read(from: &buf), - timeoutBlockHeight: FfiConverterUInt64.read(from: &buf) - ) - } - - public static func write(_ value: ReverseSwapResponse, into buf: inout [UInt8]) { - FfiConverterString.write(value.id, into: &buf) - FfiConverterString.write(value.invoice, into: &buf) - FfiConverterString.write(value.lockupAddress, into: &buf) - FfiConverterUInt64.write(value.onchainAmountSat, into: &buf) - FfiConverterUInt64.write(value.timeoutBlockHeight, into: &buf) - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeReverseSwapResponse_lift(_ buf: RustBuffer) throws -> ReverseSwapResponse { - return try FfiConverterTypeReverseSwapResponse.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeReverseSwapResponse_lower(_ value: ReverseSwapResponse) -> RustBuffer { - return FfiConverterTypeReverseSwapResponse.lower(value) -} - - /** * A one-leg route through a service the wallet holds a balance on, such as a Spark withdrawal * to the contact's on-chain address. The app quotes it and runs it. @@ -13346,146 +12661,6 @@ public func FfiConverterTypeSingleAddressInfoResult_lower(_ value: SingleAddress } -/** - * Result of creating a submarine swap (onchain -> Lightning). - * - * The caller funds `address` with `expected_amount_sat` from its onchain - * wallet; Boltz then pays the Lightning invoice supplied at creation. - */ -public struct SubmarineSwapResponse { - public var id: String - /** - * Onchain lockup address to send funds to. - */ - public var address: String - /** - * BIP21 URI for the lockup payment. - */ - public var bip21: String - /** - * Exact amount in satoshis the caller must send to `address`. - */ - public var expectedAmountSat: UInt64 - /** - * Whether Boltz will accept a zero-conf lockup. - */ - public var acceptZeroConf: Bool - /** - * Onchain timeout height after which a refund is possible. - */ - public var timeoutBlockHeight: UInt64 - - // Default memberwise initializers are never public by default, so we - // declare one manually. - public init(id: String, - /** - * Onchain lockup address to send funds to. - */address: String, - /** - * BIP21 URI for the lockup payment. - */bip21: String, - /** - * Exact amount in satoshis the caller must send to `address`. - */expectedAmountSat: UInt64, - /** - * Whether Boltz will accept a zero-conf lockup. - */acceptZeroConf: Bool, - /** - * Onchain timeout height after which a refund is possible. - */timeoutBlockHeight: UInt64) { - self.id = id - self.address = address - self.bip21 = bip21 - self.expectedAmountSat = expectedAmountSat - self.acceptZeroConf = acceptZeroConf - self.timeoutBlockHeight = timeoutBlockHeight - } -} - -#if compiler(>=6) -extension SubmarineSwapResponse: Sendable {} -#endif - - -extension SubmarineSwapResponse: Equatable, Hashable { - public static func ==(lhs: SubmarineSwapResponse, rhs: SubmarineSwapResponse) -> Bool { - if lhs.id != rhs.id { - return false - } - if lhs.address != rhs.address { - return false - } - if lhs.bip21 != rhs.bip21 { - return false - } - if lhs.expectedAmountSat != rhs.expectedAmountSat { - return false - } - if lhs.acceptZeroConf != rhs.acceptZeroConf { - return false - } - if lhs.timeoutBlockHeight != rhs.timeoutBlockHeight { - return false - } - return true - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - hasher.combine(address) - hasher.combine(bip21) - hasher.combine(expectedAmountSat) - hasher.combine(acceptZeroConf) - hasher.combine(timeoutBlockHeight) - } -} - -extension SubmarineSwapResponse: Codable {} - - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeSubmarineSwapResponse: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SubmarineSwapResponse { - return - try SubmarineSwapResponse( - id: FfiConverterString.read(from: &buf), - address: FfiConverterString.read(from: &buf), - bip21: FfiConverterString.read(from: &buf), - expectedAmountSat: FfiConverterUInt64.read(from: &buf), - acceptZeroConf: FfiConverterBool.read(from: &buf), - timeoutBlockHeight: FfiConverterUInt64.read(from: &buf) - ) - } - - public static func write(_ value: SubmarineSwapResponse, into buf: inout [UInt8]) { - FfiConverterString.write(value.id, into: &buf) - FfiConverterString.write(value.address, into: &buf) - FfiConverterString.write(value.bip21, into: &buf) - FfiConverterUInt64.write(value.expectedAmountSat, into: &buf) - FfiConverterBool.write(value.acceptZeroConf, into: &buf) - FfiConverterUInt64.write(value.timeoutBlockHeight, into: &buf) - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeSubmarineSwapResponse_lift(_ buf: RustBuffer) throws -> SubmarineSwapResponse { - return try FfiConverterTypeSubmarineSwapResponse.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeSubmarineSwapResponse_lower(_ value: SubmarineSwapResponse) -> RustBuffer { - return FfiConverterTypeSubmarineSwapResponse.lower(value) -} - - /** * A hardware-wallet model Bitkit supports. */ @@ -22454,675 +21629,6 @@ extension BlocktankError: Foundation.LocalizedError { -/** - * Errors surfaced by the Boltz swaps module. - */ -public enum BoltzError: Swift.Error { - - - - case InitializationError(errorDetails: String - ) - case ConnectionError(errorDetails: String - ) - case DatabaseError(errorDetails: String - ) - case ApiError(errorDetails: String - ) - case SwapError(errorDetails: String - ) - case BroadcastError(errorDetails: String - ) - case InvalidInput(errorDetails: String - ) - case SerializationError(errorDetails: String - ) - case NotFound(errorDetails: String - ) -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzError: FfiConverterRustBuffer { - typealias SwiftType = BoltzError - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzError { - let variant: Int32 = try readInt(&buf) - switch variant { - - - - - case 1: return .InitializationError( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 2: return .ConnectionError( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 3: return .DatabaseError( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 4: return .ApiError( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 5: return .SwapError( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 6: return .BroadcastError( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 7: return .InvalidInput( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 8: return .SerializationError( - errorDetails: try FfiConverterString.read(from: &buf) - ) - case 9: return .NotFound( - errorDetails: try FfiConverterString.read(from: &buf) - ) - - default: throw UniffiInternalError.unexpectedEnumCase - } - } - - public static func write(_ value: BoltzError, into buf: inout [UInt8]) { - switch value { - - - - - - case let .InitializationError(errorDetails): - writeInt(&buf, Int32(1)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .ConnectionError(errorDetails): - writeInt(&buf, Int32(2)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .DatabaseError(errorDetails): - writeInt(&buf, Int32(3)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .ApiError(errorDetails): - writeInt(&buf, Int32(4)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .SwapError(errorDetails): - writeInt(&buf, Int32(5)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .BroadcastError(errorDetails): - writeInt(&buf, Int32(6)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .InvalidInput(errorDetails): - writeInt(&buf, Int32(7)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .SerializationError(errorDetails): - writeInt(&buf, Int32(8)) - FfiConverterString.write(errorDetails, into: &buf) - - - case let .NotFound(errorDetails): - writeInt(&buf, Int32(9)) - FfiConverterString.write(errorDetails, into: &buf) - - } - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzError_lift(_ buf: RustBuffer) throws -> BoltzError { - return try FfiConverterTypeBoltzError.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzError_lower(_ value: BoltzError) -> RustBuffer { - return FfiConverterTypeBoltzError.lower(value) -} - - -extension BoltzError: Equatable, Hashable {} - -extension BoltzError: Codable {} - - - - -extension BoltzError: Foundation.LocalizedError { - public var errorDescription: String? { - String(reflecting: self) - } -} - - - - -// Note that we don't yet support `indirect` for enums. -// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. -/** - * Bitcoin network selection for Boltz swaps. Maps to the networks Boltz - * operates on (mainnet, testnet, regtest). - */ - -public enum BoltzNetwork { - - case mainnet - case testnet - case regtest -} - - -#if compiler(>=6) -extension BoltzNetwork: Sendable {} -#endif - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzNetwork: FfiConverterRustBuffer { - typealias SwiftType = BoltzNetwork - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzNetwork { - let variant: Int32 = try readInt(&buf) - switch variant { - - case 1: return .mainnet - - case 2: return .testnet - - case 3: return .regtest - - default: throw UniffiInternalError.unexpectedEnumCase - } - } - - public static func write(_ value: BoltzNetwork, into buf: inout [UInt8]) { - switch value { - - - case .mainnet: - writeInt(&buf, Int32(1)) - - - case .testnet: - writeInt(&buf, Int32(2)) - - - case .regtest: - writeInt(&buf, Int32(3)) - - } - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzNetwork_lift(_ buf: RustBuffer) throws -> BoltzNetwork { - return try FfiConverterTypeBoltzNetwork.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzNetwork_lower(_ value: BoltzNetwork) -> RustBuffer { - return FfiConverterTypeBoltzNetwork.lower(value) -} - - -extension BoltzNetwork: Equatable, Hashable {} - -extension BoltzNetwork: Codable {} - - - - - - -// Note that we don't yet support `indirect` for enums. -// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. -/** - * Events emitted to a registered [`crate::modules::boltz::BoltzEventListener`] - * as swaps progress through their lifecycle. - */ - -public enum BoltzSwapEvent { - - /** - * The swap transitioned to a new status. - */ - case statusUpdate(swapId: String, status: BoltzSwapStatus - ) - /** - * A reverse swap was claimed onchain. `txid` is the claim transaction. - */ - case claimed(swapId: String, txid: String - ) - /** - * A submarine swap was refunded onchain. `txid` is the refund transaction. - */ - case refunded(swapId: String, txid: String - ) - /** - * An error occurred while processing the swap (e.g. an auto-claim failed). - */ - case error(swapId: String, message: String - ) -} - - -#if compiler(>=6) -extension BoltzSwapEvent: Sendable {} -#endif - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzSwapEvent: FfiConverterRustBuffer { - typealias SwiftType = BoltzSwapEvent - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzSwapEvent { - let variant: Int32 = try readInt(&buf) - switch variant { - - case 1: return .statusUpdate(swapId: try FfiConverterString.read(from: &buf), status: try FfiConverterTypeBoltzSwapStatus.read(from: &buf) - ) - - case 2: return .claimed(swapId: try FfiConverterString.read(from: &buf), txid: try FfiConverterString.read(from: &buf) - ) - - case 3: return .refunded(swapId: try FfiConverterString.read(from: &buf), txid: try FfiConverterString.read(from: &buf) - ) - - case 4: return .error(swapId: try FfiConverterString.read(from: &buf), message: try FfiConverterString.read(from: &buf) - ) - - default: throw UniffiInternalError.unexpectedEnumCase - } - } - - public static func write(_ value: BoltzSwapEvent, into buf: inout [UInt8]) { - switch value { - - - case let .statusUpdate(swapId,status): - writeInt(&buf, Int32(1)) - FfiConverterString.write(swapId, into: &buf) - FfiConverterTypeBoltzSwapStatus.write(status, into: &buf) - - - case let .claimed(swapId,txid): - writeInt(&buf, Int32(2)) - FfiConverterString.write(swapId, into: &buf) - FfiConverterString.write(txid, into: &buf) - - - case let .refunded(swapId,txid): - writeInt(&buf, Int32(3)) - FfiConverterString.write(swapId, into: &buf) - FfiConverterString.write(txid, into: &buf) - - - case let .error(swapId,message): - writeInt(&buf, Int32(4)) - FfiConverterString.write(swapId, into: &buf) - FfiConverterString.write(message, into: &buf) - - } - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwapEvent_lift(_ buf: RustBuffer) throws -> BoltzSwapEvent { - return try FfiConverterTypeBoltzSwapEvent.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwapEvent_lower(_ value: BoltzSwapEvent) -> RustBuffer { - return FfiConverterTypeBoltzSwapEvent.lower(value) -} - - -extension BoltzSwapEvent: Equatable, Hashable {} - -extension BoltzSwapEvent: Codable {} - - - - - - -// Note that we don't yet support `indirect` for enums. -// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. -/** - * Typed view of the Boltz swap lifecycle. `Unknown` carries the raw status so - * new server-side states don't break the bindings. - * - * See . - */ - -public enum BoltzSwapStatus { - - /** - * `swap.created` — initial state. - */ - case swapCreated - /** - * `invoice.set` — invoice attached to a submarine swap. - */ - case invoiceSet - /** - * `transaction.mempool` — a lockup transaction is in the mempool. - */ - case transactionMempool - /** - * `transaction.confirmed` — a lockup transaction confirmed. - */ - case transactionConfirmed - /** - * `invoice.pending` — Boltz is paying the submarine swap invoice. - */ - case invoicePending - /** - * `invoice.paid` — submarine swap invoice paid by Boltz. - */ - case invoicePaid - /** - * `invoice.settled` — reverse swap invoice settled (preimage revealed). - */ - case invoiceSettled - /** - * `invoice.failedToPay` — submarine swap invoice could not be paid; refund. - */ - case invoiceFailedToPay - /** - * `invoice.expired` — reverse swap invoice expired before payment. - */ - case invoiceExpired - /** - * `transaction.claim.pending` — Boltz ready for a cooperative claim. - */ - case transactionClaimPending - /** - * `transaction.claimed` — onchain funds claimed. - */ - case transactionClaimed - /** - * `transaction.refunded` — onchain funds refunded. - */ - case transactionRefunded - /** - * `transaction.lockupFailed` — wrong amount locked; can refund. - */ - case transactionLockupFailed - /** - * `transaction.failed` — Boltz failed to lock the agreed funds. - */ - case transactionFailed - /** - * `swap.expired` — swap expired without completing. - */ - case swapExpired - /** - * Any status not yet modelled. `raw` holds the verbatim Boltz status. - */ - case unknown(raw: String - ) -} - - -#if compiler(>=6) -extension BoltzSwapStatus: Sendable {} -#endif - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzSwapStatus: FfiConverterRustBuffer { - typealias SwiftType = BoltzSwapStatus - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzSwapStatus { - let variant: Int32 = try readInt(&buf) - switch variant { - - case 1: return .swapCreated - - case 2: return .invoiceSet - - case 3: return .transactionMempool - - case 4: return .transactionConfirmed - - case 5: return .invoicePending - - case 6: return .invoicePaid - - case 7: return .invoiceSettled - - case 8: return .invoiceFailedToPay - - case 9: return .invoiceExpired - - case 10: return .transactionClaimPending - - case 11: return .transactionClaimed - - case 12: return .transactionRefunded - - case 13: return .transactionLockupFailed - - case 14: return .transactionFailed - - case 15: return .swapExpired - - case 16: return .unknown(raw: try FfiConverterString.read(from: &buf) - ) - - default: throw UniffiInternalError.unexpectedEnumCase - } - } - - public static func write(_ value: BoltzSwapStatus, into buf: inout [UInt8]) { - switch value { - - - case .swapCreated: - writeInt(&buf, Int32(1)) - - - case .invoiceSet: - writeInt(&buf, Int32(2)) - - - case .transactionMempool: - writeInt(&buf, Int32(3)) - - - case .transactionConfirmed: - writeInt(&buf, Int32(4)) - - - case .invoicePending: - writeInt(&buf, Int32(5)) - - - case .invoicePaid: - writeInt(&buf, Int32(6)) - - - case .invoiceSettled: - writeInt(&buf, Int32(7)) - - - case .invoiceFailedToPay: - writeInt(&buf, Int32(8)) - - - case .invoiceExpired: - writeInt(&buf, Int32(9)) - - - case .transactionClaimPending: - writeInt(&buf, Int32(10)) - - - case .transactionClaimed: - writeInt(&buf, Int32(11)) - - - case .transactionRefunded: - writeInt(&buf, Int32(12)) - - - case .transactionLockupFailed: - writeInt(&buf, Int32(13)) - - - case .transactionFailed: - writeInt(&buf, Int32(14)) - - - case .swapExpired: - writeInt(&buf, Int32(15)) - - - case let .unknown(raw): - writeInt(&buf, Int32(16)) - FfiConverterString.write(raw, into: &buf) - - } - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwapStatus_lift(_ buf: RustBuffer) throws -> BoltzSwapStatus { - return try FfiConverterTypeBoltzSwapStatus.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwapStatus_lower(_ value: BoltzSwapStatus) -> RustBuffer { - return FfiConverterTypeBoltzSwapStatus.lower(value) -} - - -extension BoltzSwapStatus: Equatable, Hashable {} - -extension BoltzSwapStatus: Codable {} - - - - - - -// Note that we don't yet support `indirect` for enums. -// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. -/** - * The direction of a Boltz swap. - * - * - `Submarine`: onchain Bitcoin -> Lightning (the user locks onchain funds, - * Boltz pays a Lightning invoice). - * - `Reverse`: Lightning -> onchain Bitcoin (the user pays a Boltz hold - * invoice, Boltz locks onchain funds the user then claims). - */ - -public enum BoltzSwapType { - - case submarine - case reverse -} - - -#if compiler(>=6) -extension BoltzSwapType: Sendable {} -#endif - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public struct FfiConverterTypeBoltzSwapType: FfiConverterRustBuffer { - typealias SwiftType = BoltzSwapType - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BoltzSwapType { - let variant: Int32 = try readInt(&buf) - switch variant { - - case 1: return .submarine - - case 2: return .reverse - - default: throw UniffiInternalError.unexpectedEnumCase - } - } - - public static func write(_ value: BoltzSwapType, into buf: inout [UInt8]) { - switch value { - - - case .submarine: - writeInt(&buf, Int32(1)) - - - case .reverse: - writeInt(&buf, Int32(2)) - - } - } -} - - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwapType_lift(_ buf: RustBuffer) throws -> BoltzSwapType { - return try FfiConverterTypeBoltzSwapType.lift(buf) -} - -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -public func FfiConverterTypeBoltzSwapType_lower(_ value: BoltzSwapType) -> RustBuffer { - return FfiConverterTypeBoltzSwapType.lower(value) -} - - -extension BoltzSwapType: Equatable, Hashable {} - -extension BoltzSwapType: Codable {} - - - - - - - public enum BroadcastError: Swift.Error { @@ -24237,8 +22743,6 @@ public enum DbError: Swift.Error { ) case DbBlocktankError(errorDetails: BlocktankError ) - case DbBoltzError(errorDetails: BoltzError - ) case InitializationError(errorDetails: String ) } @@ -24263,10 +22767,7 @@ public struct FfiConverterTypeDbError: FfiConverterRustBuffer { case 2: return .DbBlocktankError( errorDetails: try FfiConverterTypeBlocktankError.read(from: &buf) ) - case 3: return .DbBoltzError( - errorDetails: try FfiConverterTypeBoltzError.read(from: &buf) - ) - case 4: return .InitializationError( + case 3: return .InitializationError( errorDetails: try FfiConverterString.read(from: &buf) ) @@ -24291,13 +22792,8 @@ public struct FfiConverterTypeDbError: FfiConverterRustBuffer { FfiConverterTypeBlocktankError.write(errorDetails, into: &buf) - case let .DbBoltzError(errorDetails): - writeInt(&buf, Int32(3)) - FfiConverterTypeBoltzError.write(errorDetails, into: &buf) - - case let .InitializationError(errorDetails): - writeInt(&buf, Int32(4)) + writeInt(&buf, Int32(3)) FfiConverterString.write(errorDetails, into: &buf) } @@ -29963,30 +28459,6 @@ fileprivate struct FfiConverterOptionData: FfiConverterRustBuffer { } } -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -fileprivate struct FfiConverterOptionTypeBoltzSwap: FfiConverterRustBuffer { - typealias SwiftType = BoltzSwap? - - public static func write(_ value: SwiftType, into buf: inout [UInt8]) { - guard let value = value else { - writeInt(&buf, Int8(0)) - return - } - writeInt(&buf, Int8(1)) - FfiConverterTypeBoltzSwap.write(value, into: &buf) - } - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { - switch try readInt(&buf) as Int8 { - case 0: return nil - case 1: return try FfiConverterTypeBoltzSwap.read(from: &buf) - default: throw UniffiInternalError.unexpectedOptionalTag - } - } -} - #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -31336,31 +29808,6 @@ fileprivate struct FfiConverterSequenceTypeAddressInfo: FfiConverterRustBuffer { } } -#if swift(>=5.8) -@_documentation(visibility: private) -#endif -fileprivate struct FfiConverterSequenceTypeBoltzSwap: FfiConverterRustBuffer { - typealias SwiftType = [BoltzSwap] - - public static func write(_ value: [BoltzSwap], into buf: inout [UInt8]) { - let len = Int32(value.count) - writeInt(&buf, len) - for item in value { - FfiConverterTypeBoltzSwap.write(item, into: &buf) - } - } - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [BoltzSwap] { - let len: Int32 = try readInt(&buf) - var seq = [BoltzSwap]() - seq.reserveCapacity(Int(len)) - for _ in 0 ..< len { - seq.append(try FfiConverterTypeBoltzSwap.read(from: &buf)) - } - return seq - } -} - #if swift(>=5.8) @_documentation(visibility: private) #endif @@ -32765,233 +31212,6 @@ public func blocktankWipeAll()async throws { errorHandler: FfiConverterTypeBlocktankError_lift ) } -/** - * Claim a reverse swap's onchain funds to its claim address, returning the - * broadcast claim transaction id. Normally happens automatically via the - * updates stream; exposed for manual recovery. The claim key is re-derived from - * `mnemonic`. Claims are serialized per swap, so calling this while the updates - * stream is auto-claiming the same swap waits for that claim and returns its - * txid rather than broadcasting a second transaction. - */ -public func boltzClaimReverseSwap(swapId: String, mnemonic: String, bip39Passphrase: String?, feeRateSatPerVb: Double?)async throws -> String { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_claim_reverse_swap(FfiConverterString.lower(swapId),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(bip39Passphrase),FfiConverterOptionDouble.lower(feeRateSatPerVb) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterString.lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Create a reverse swap (Lightning -> onchain). - * - * The caller pays the returned hold invoice from its Lightning node; - * `claim_address` is the onchain address the received funds are claimed to. - * The claim key and preimage are derived deterministically from `mnemonic` - * (only the derivation index is persisted, never the secrets) so the claim can - * be made automatically once Boltz locks the funds. `bip39_passphrase` must - * match the wallet's, or claims will derive the wrong key. - */ -public func boltzCreateReverseSwap(network: BoltzNetwork, electrumUrl: String, amountSat: UInt64, claimAddress: String, mnemonic: String, bip39Passphrase: String?)async throws -> ReverseSwapResponse { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_create_reverse_swap(FfiConverterTypeBoltzNetwork_lower(network),FfiConverterString.lower(electrumUrl),FfiConverterUInt64.lower(amountSat),FfiConverterString.lower(claimAddress),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(bip39Passphrase) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterTypeReverseSwapResponse_lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Create a submarine swap (onchain -> Lightning). - * - * `invoice` is a BOLT11 invoice the caller's Lightning node generated. The - * caller funds the returned lockup address from its onchain wallet. The refund - * key is derived deterministically from `mnemonic` (only the derivation index - * is persisted, never the key), and the swap is tracked if an updates stream is - * running. `bip39_passphrase` must match the wallet's, or refunds will derive - * the wrong key. - */ -public func boltzCreateSubmarineSwap(network: BoltzNetwork, electrumUrl: String, invoice: String, mnemonic: String, bip39Passphrase: String?)async throws -> SubmarineSwapResponse { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_create_submarine_swap(FfiConverterTypeBoltzNetwork_lower(network),FfiConverterString.lower(electrumUrl),FfiConverterString.lower(invoice),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(bip39Passphrase) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterTypeSubmarineSwapResponse_lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Fetch fees and limits for reverse swaps (Lightning -> onchain). - */ -public func boltzGetReverseLimits(network: BoltzNetwork)async throws -> BoltzPairInfo { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_get_reverse_limits(FfiConverterTypeBoltzNetwork_lower(network) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterTypeBoltzPairInfo_lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Fetch fees and limits for submarine swaps (onchain -> Lightning). - */ -public func boltzGetSubmarineLimits(network: BoltzNetwork)async throws -> BoltzPairInfo { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_get_submarine_limits(FfiConverterTypeBoltzNetwork_lower(network) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterTypeBoltzPairInfo_lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Fetch a single swap by id. - */ -public func boltzGetSwap(swapId: String)async throws -> BoltzSwap? { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_get_swap(FfiConverterString.lower(swapId) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterOptionTypeBoltzSwap.lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * List swaps that have not reached a terminal state (for recovery/resume). - */ -public func boltzListPendingSwaps()async throws -> [BoltzSwap] { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_list_pending_swaps( - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterSequenceTypeBoltzSwap.lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * List every persisted swap, newest first. - */ -public func boltzListSwaps()async throws -> [BoltzSwap] { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_list_swaps( - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterSequenceTypeBoltzSwap.lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Refund a submarine swap's locked funds to `refund_address`, returning the - * broadcast refund transaction id. Used when Boltz fails to pay the invoice or - * the swap expires. The refund key is re-derived from `mnemonic`. Refunds are - * serialized per swap, so two concurrent calls cannot both broadcast: the second - * waits for the first and returns its txid. - */ -public func boltzRefundSubmarineSwap(swapId: String, refundAddress: String, mnemonic: String, bip39Passphrase: String?, feeRateSatPerVb: Double?)async throws -> String { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_refund_submarine_swap(FfiConverterString.lower(swapId),FfiConverterString.lower(refundAddress),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(bip39Passphrase),FfiConverterOptionDouble.lower(feeRateSatPerVb) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_rust_buffer, - completeFunc: ffi_bitkitcore_rust_future_complete_rust_buffer, - freeFunc: ffi_bitkitcore_rust_future_free_rust_buffer, - liftFunc: FfiConverterString.lift, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Open a Boltz WebSocket for `network`, subscribe to all pending swaps, and - * drive their lifecycle (auto-claiming reverse swaps) until stopped. Replaces - * any previously running updates stream (only one network is tracked at a - * time). `mnemonic` is held in memory for the lifetime of the stream so - * confirmed reverse swaps can be auto-claimed; it is never persisted. Events - * are delivered to `listener`. - * - * `fee_rate_sat_per_vb` is the fee rate used for automatic claim transactions. - * Bitkit owns fee estimation and should pass its current recommended rate; when - * `None`, a conservative built-in default is used. To auto-claim at an updated - * fee rate, call this again (it restarts the stream). - * - * `accept_zero_conf` claims reverse swaps as soon as Boltz's lockup enters the - * mempool instead of waiting for its confirmation. That reveals the preimage - * against an unconfirmed lockup: if the lockup were replaced before - * confirming, the user would be debited on Lightning without receiving - * onchain funds. Pass `false` to keep the confirmation-gated default. - */ -public func boltzStartSwapUpdates(network: BoltzNetwork, listener: BoltzEventListener, mnemonic: String, bip39Passphrase: String?, feeRateSatPerVb: Double?, acceptZeroConf: Bool)async throws { - return - try await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_start_swap_updates(FfiConverterTypeBoltzNetwork_lower(network),FfiConverterTypeBoltzEventListener_lower(listener),FfiConverterString.lower(mnemonic),FfiConverterOptionString.lower(bip39Passphrase),FfiConverterOptionDouble.lower(feeRateSatPerVb),FfiConverterBool.lower(acceptZeroConf) - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_void, - completeFunc: ffi_bitkitcore_rust_future_complete_void, - freeFunc: ffi_bitkitcore_rust_future_free_void, - liftFunc: { $0 }, - errorHandler: FfiConverterTypeBoltzError_lift - ) -} -/** - * Stop the running Boltz updates stream, if any. - */ -public func boltzStopSwapUpdates()async { - return - try! await uniffiRustCallAsync( - rustFutureFunc: { - uniffi_bitkitcore_fn_func_boltz_stop_swap_updates( - ) - }, - pollFunc: ffi_bitkitcore_rust_future_poll_void, - completeFunc: ffi_bitkitcore_rust_future_complete_void, - freeFunc: ffi_bitkitcore_rust_future_free_void, - liftFunc: { $0 }, - errorHandler: nil - - ) -} public func broadcastSweepTransaction(psbt: String, mnemonicPhrase: String, network: Network?, bip39Passphrase: String?, electrumUrl: String)async throws -> SweepResult { return try await uniffiRustCallAsync( @@ -35854,39 +34074,6 @@ private let initializationResult: InitializationResult = { if (uniffi_bitkitcore_checksum_func_blocktank_wipe_all() != 41797) { return InitializationResult.apiChecksumMismatch } - if (uniffi_bitkitcore_checksum_func_boltz_claim_reverse_swap() != 52516) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_create_reverse_swap() != 20570) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_create_submarine_swap() != 27933) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_get_reverse_limits() != 59203) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_get_submarine_limits() != 5900) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_get_swap() != 17473) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_list_pending_swaps() != 20926) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_list_swaps() != 16447) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_refund_submarine_swap() != 24549) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_start_swap_updates() != 168) { - return InitializationResult.apiChecksumMismatch - } - if (uniffi_bitkitcore_checksum_func_boltz_stop_swap_updates() != 63683) { - return InitializationResult.apiChecksumMismatch - } if (uniffi_bitkitcore_checksum_func_broadcast_sweep_transaction() != 43422) { return InitializationResult.apiChecksumMismatch } @@ -36535,9 +34722,6 @@ private let initializationResult: InitializationResult = { if (uniffi_bitkitcore_checksum_func_wipe_all_transaction_details() != 65339) { return InitializationResult.apiChecksumMismatch } - if (uniffi_bitkitcore_checksum_method_boltzeventlistener_on_event() != 11294) { - return InitializationResult.apiChecksumMismatch - } if (uniffi_bitkitcore_checksum_method_eventlistener_on_event() != 35531) { return InitializationResult.apiChecksumMismatch } @@ -36680,7 +34864,6 @@ private let initializationResult: InitializationResult = { return InitializationResult.apiChecksumMismatch } - uniffiCallbackInitBoltzEventListener() uniffiCallbackInitEventListener() uniffiCallbackInitJadeTransportCallback() uniffiCallbackInitTrezorTransportCallback() diff --git a/bindings/ios/bitkitcoreFFI.h b/bindings/ios/bitkitcoreFFI.h index 10a0c056..24c753d4 100644 --- a/bindings/ios/bitkitcoreFFI.h +++ b/bindings/ios/bitkitcoreFFI.h @@ -250,13 +250,6 @@ typedef struct UniffiForeignFutureStructVoid { typedef void (*UniffiForeignFutureCompleteVoid)(uint64_t, UniffiForeignFutureStructVoid ); -#endif -#ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_BOLTZ_EVENT_LISTENER_METHOD0 -#define UNIFFI_FFIDEF_CALLBACK_INTERFACE_BOLTZ_EVENT_LISTENER_METHOD0 -typedef void (*UniffiCallbackInterfaceBoltzEventListenerMethod0)(uint64_t, RustBuffer, void* _Nonnull, - RustCallStatus *_Nonnull uniffiCallStatus - ); - #endif #ifndef UNIFFI_FFIDEF_CALLBACK_INTERFACE_EVENT_LISTENER_METHOD0 #define UNIFFI_FFIDEF_CALLBACK_INTERFACE_EVENT_LISTENER_METHOD0 @@ -403,14 +396,6 @@ typedef void (*UniffiCallbackInterfaceTrezorUiCallbackMethod1)(uint64_t, int8_t, typedef void (*UniffiCallbackInterfaceUsdtBackupMethod0)(uint64_t, RustBuffer, UniffiForeignFutureCompleteVoid _Nonnull, uint64_t, UniffiForeignFuture* _Nonnull ); -#endif -#ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_BOLTZ_EVENT_LISTENER -#define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_BOLTZ_EVENT_LISTENER -typedef struct UniffiVTableCallbackInterfaceBoltzEventListener { - UniffiCallbackInterfaceBoltzEventListenerMethod0 _Nonnull onEvent; - UniffiCallbackInterfaceFree _Nonnull uniffiFree; -} UniffiVTableCallbackInterfaceBoltzEventListener; - #endif #ifndef UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_EVENT_LISTENER #define UNIFFI_FFIDEF_V_TABLE_CALLBACK_INTERFACE_EVENT_LISTENER @@ -467,26 +452,6 @@ typedef struct UniffiVTableCallbackInterfaceUsdtBackup { UniffiCallbackInterfaceFree _Nonnull uniffiFree; } UniffiVTableCallbackInterfaceUsdtBackup; -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_BOLTZEVENTLISTENER -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_BOLTZEVENTLISTENER -void*_Nonnull uniffi_bitkitcore_fn_clone_boltzeventlistener(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FREE_BOLTZEVENTLISTENER -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FREE_BOLTZEVENTLISTENER -void uniffi_bitkitcore_fn_free_boltzeventlistener(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_INIT_CALLBACK_VTABLE_BOLTZEVENTLISTENER -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_INIT_CALLBACK_VTABLE_BOLTZEVENTLISTENER -void uniffi_bitkitcore_fn_init_callback_vtable_boltzeventlistener(const UniffiVTableCallbackInterfaceBoltzEventListener* _Nonnull vtable -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_BOLTZEVENTLISTENER_ON_EVENT -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_METHOD_BOLTZEVENTLISTENER_ON_EVENT -void uniffi_bitkitcore_fn_method_boltzeventlistener_on_event(void*_Nonnull ptr, RustBuffer event, RustCallStatus *_Nonnull out_status -); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_EVENTLISTENER #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_CLONE_EVENTLISTENER @@ -891,64 +856,6 @@ uint64_t uniffi_bitkitcore_fn_func_blocktank_remove_all_orders(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BLOCKTANK_WIPE_ALL uint64_t uniffi_bitkitcore_fn_func_blocktank_wipe_all(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_CLAIM_REVERSE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_CLAIM_REVERSE_SWAP -uint64_t uniffi_bitkitcore_fn_func_boltz_claim_reverse_swap(RustBuffer swap_id, RustBuffer mnemonic, RustBuffer bip39_passphrase, RustBuffer fee_rate_sat_per_vb -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_CREATE_REVERSE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_CREATE_REVERSE_SWAP -uint64_t uniffi_bitkitcore_fn_func_boltz_create_reverse_swap(RustBuffer network, RustBuffer electrum_url, uint64_t amount_sat, RustBuffer claim_address, RustBuffer mnemonic, RustBuffer bip39_passphrase -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_CREATE_SUBMARINE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_CREATE_SUBMARINE_SWAP -uint64_t uniffi_bitkitcore_fn_func_boltz_create_submarine_swap(RustBuffer network, RustBuffer electrum_url, RustBuffer invoice, RustBuffer mnemonic, RustBuffer bip39_passphrase -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_GET_REVERSE_LIMITS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_GET_REVERSE_LIMITS -uint64_t uniffi_bitkitcore_fn_func_boltz_get_reverse_limits(RustBuffer network -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_GET_SUBMARINE_LIMITS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_GET_SUBMARINE_LIMITS -uint64_t uniffi_bitkitcore_fn_func_boltz_get_submarine_limits(RustBuffer network -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_GET_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_GET_SWAP -uint64_t uniffi_bitkitcore_fn_func_boltz_get_swap(RustBuffer swap_id -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_LIST_PENDING_SWAPS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_LIST_PENDING_SWAPS -uint64_t uniffi_bitkitcore_fn_func_boltz_list_pending_swaps(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_LIST_SWAPS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_LIST_SWAPS -uint64_t uniffi_bitkitcore_fn_func_boltz_list_swaps(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_REFUND_SUBMARINE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_REFUND_SUBMARINE_SWAP -uint64_t uniffi_bitkitcore_fn_func_boltz_refund_submarine_swap(RustBuffer swap_id, RustBuffer refund_address, RustBuffer mnemonic, RustBuffer bip39_passphrase, RustBuffer fee_rate_sat_per_vb -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_START_SWAP_UPDATES -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_START_SWAP_UPDATES -uint64_t uniffi_bitkitcore_fn_func_boltz_start_swap_updates(RustBuffer network, void*_Nonnull listener, RustBuffer mnemonic, RustBuffer bip39_passphrase, RustBuffer fee_rate_sat_per_vb, int8_t accept_zero_conf -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_STOP_SWAP_UPDATES -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BOLTZ_STOP_SWAP_UPDATES -uint64_t uniffi_bitkitcore_fn_func_boltz_stop_swap_updates(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_FN_FUNC_BROADCAST_SWEEP_TRANSACTION @@ -2419,72 +2326,6 @@ uint16_t uniffi_bitkitcore_checksum_func_blocktank_remove_all_orders(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BLOCKTANK_WIPE_ALL uint16_t uniffi_bitkitcore_checksum_func_blocktank_wipe_all(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_CLAIM_REVERSE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_CLAIM_REVERSE_SWAP -uint16_t uniffi_bitkitcore_checksum_func_boltz_claim_reverse_swap(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_CREATE_REVERSE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_CREATE_REVERSE_SWAP -uint16_t uniffi_bitkitcore_checksum_func_boltz_create_reverse_swap(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_CREATE_SUBMARINE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_CREATE_SUBMARINE_SWAP -uint16_t uniffi_bitkitcore_checksum_func_boltz_create_submarine_swap(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_GET_REVERSE_LIMITS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_GET_REVERSE_LIMITS -uint16_t uniffi_bitkitcore_checksum_func_boltz_get_reverse_limits(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_GET_SUBMARINE_LIMITS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_GET_SUBMARINE_LIMITS -uint16_t uniffi_bitkitcore_checksum_func_boltz_get_submarine_limits(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_GET_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_GET_SWAP -uint16_t uniffi_bitkitcore_checksum_func_boltz_get_swap(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_LIST_PENDING_SWAPS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_LIST_PENDING_SWAPS -uint16_t uniffi_bitkitcore_checksum_func_boltz_list_pending_swaps(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_LIST_SWAPS -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_LIST_SWAPS -uint16_t uniffi_bitkitcore_checksum_func_boltz_list_swaps(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_REFUND_SUBMARINE_SWAP -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_REFUND_SUBMARINE_SWAP -uint16_t uniffi_bitkitcore_checksum_func_boltz_refund_submarine_swap(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_START_SWAP_UPDATES -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_START_SWAP_UPDATES -uint16_t uniffi_bitkitcore_checksum_func_boltz_start_swap_updates(void - -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_STOP_SWAP_UPDATES -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BOLTZ_STOP_SWAP_UPDATES -uint16_t uniffi_bitkitcore_checksum_func_boltz_stop_swap_updates(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_BROADCAST_SWEEP_TRANSACTION @@ -3781,12 +3622,6 @@ uint16_t uniffi_bitkitcore_checksum_func_wipe_all_databases(void #define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_FUNC_WIPE_ALL_TRANSACTION_DETAILS uint16_t uniffi_bitkitcore_checksum_func_wipe_all_transaction_details(void -); -#endif -#ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_BOLTZEVENTLISTENER_ON_EVENT -#define UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_BOLTZEVENTLISTENER_ON_EVENT -uint16_t uniffi_bitkitcore_checksum_method_boltzeventlistener_on_event(void - ); #endif #ifndef UNIFFI_FFIDEF_UNIFFI_BITKITCORE_CHECKSUM_METHOD_EVENTLISTENER_ON_EVENT diff --git a/src/lib.rs b/src/lib.rs index af33b748..dccee990 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,10 +38,6 @@ use crate::modules::blocktank::{ IBt0ConfMinTxFeeWindow, IBtBolt11Invoice, IBtEstimateFeeResponse, IBtEstimateFeeResponse2, IBtInfo, IBtOrder, ICJitEntry, IGift, }; -use crate::modules::boltz::{ - self, BoltzDB, BoltzError, BoltzEventListener, BoltzNetwork, BoltzPairInfo, BoltzSwap, - ReverseSwapResponse, SubmarineSwapResponse, -}; pub use crate::modules::hardware_wallet::{ get_supported_hardware_wallets, HardwareWalletTransport, HardwareWalletVendor, SupportedHardwareWallet, @@ -94,7 +90,6 @@ use crate::onchain::{ start_watcher, stop_all_watchers, stop_watcher, EventListener, WatcherParams, }; pub use modules::activity; -pub use modules::boltz as boltz_swaps; pub use modules::lnurl; pub use modules::onchain; pub use modules::paykit_route::{ @@ -125,7 +120,6 @@ pub struct DatabaseConnections { pub struct AsyncDatabaseConnections { blocktank_db: Option, - boltz_db: Option>, } // Two separate global states for sync and async connections static DB: OnceCell> = OnceCell::new(); @@ -507,12 +501,7 @@ pub fn init_db(base_path: String) -> Result { DB.get_or_init(|| StdMutex::new(DatabaseConnections { activity_db: None })); // Initialize async database state - ASYNC_DB.get_or_init(|| { - TokioMutex::new(AsyncDatabaseConnections { - blocktank_db: None, - boltz_db: None, - }) - }); + ASYNC_DB.get_or_init(|| TokioMutex::new(AsyncDatabaseConnections { blocktank_db: None })); // Create runtime for async operations let rt = ensure_runtime(); @@ -520,7 +509,6 @@ pub fn init_db(base_path: String) -> Result { let activity_db = ActivityDB::new(&format!("{}/activity.db", base_path))?; let blocktank_db = rt .block_on(async { BlocktankDB::new(&format!("{}/blocktank.db", base_path), None).await })?; - let boltz_db = rt.block_on(async { BoltzDB::new(&format!("{}/boltz.db", base_path)).await })?; modules::pubky_swaps::init(&base_path).map_err(|e| DbError::InitializationError { error_details: e.to_string(), })?; @@ -537,7 +525,6 @@ pub fn init_db(base_path: String) -> Result { rt.block_on(async { let mut guard = async_db.lock().await; guard.blocktank_db = Some(blocktank_db); - guard.boltz_db = Some(Arc::new(boltz_db)); }); } @@ -3526,273 +3513,6 @@ pub fn onchain_stop_all_watchers() { stop_all_watchers(); } -// ============================================================================ -// Boltz swaps -// ============================================================================ - -/// Clone the shared Boltz database handle. The handle is held only briefly -/// while cloning, so long-running swap operations don't block other callers. -async fn get_boltz_db() -> Result, BoltzError> { - let cell = ASYNC_DB.get().ok_or(BoltzError::ConnectionError { - error_details: "Database not initialized. Call init_db first.".to_string(), - })?; - let guard = cell.lock().await; - guard.boltz_db.clone().ok_or(BoltzError::ConnectionError { - error_details: "Database not initialized. Call init_db first.".to_string(), - }) -} - -fn boltz_runtime_err(e: tokio::task::JoinError) -> BoltzError { - BoltzError::ConnectionError { - error_details: format!("Runtime error: {}", e), - } -} - -/// Fetch fees and limits for submarine swaps (onchain -> Lightning). -#[uniffi::export] -pub async fn boltz_get_submarine_limits( - network: BoltzNetwork, -) -> Result { - let rt = ensure_runtime(); - rt.spawn(async move { boltz::get_submarine_limits(network).await }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Fetch fees and limits for reverse swaps (Lightning -> onchain). -#[uniffi::export] -pub async fn boltz_get_reverse_limits(network: BoltzNetwork) -> Result { - let rt = ensure_runtime(); - rt.spawn(async move { boltz::get_reverse_limits(network).await }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Create a submarine swap (onchain -> Lightning). -/// -/// `invoice` is a BOLT11 invoice the caller's Lightning node generated. The -/// caller funds the returned lockup address from its onchain wallet. The refund -/// key is derived deterministically from `mnemonic` (only the derivation index -/// is persisted, never the key), and the swap is tracked if an updates stream is -/// running. `bip39_passphrase` must match the wallet's, or refunds will derive -/// the wrong key. -#[uniffi::export] -pub async fn boltz_create_submarine_swap( - network: BoltzNetwork, - electrum_url: String, - invoice: String, - mnemonic: String, - bip39_passphrase: Option, -) -> Result { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - let response = db - .create_submarine_swap(network, electrum_url, invoice, mnemonic, bip39_passphrase) - .await?; - boltz::subscribe_if_active(network, &response.id).await; - Ok(response) - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Create a reverse swap (Lightning -> onchain). -/// -/// The caller pays the returned hold invoice from its Lightning node; -/// `claim_address` is the onchain address the received funds are claimed to. -/// The claim key and preimage are derived deterministically from `mnemonic` -/// (only the derivation index is persisted, never the secrets) so the claim can -/// be made automatically once Boltz locks the funds. `bip39_passphrase` must -/// match the wallet's, or claims will derive the wrong key. -#[uniffi::export] -pub async fn boltz_create_reverse_swap( - network: BoltzNetwork, - electrum_url: String, - amount_sat: u64, - claim_address: String, - mnemonic: String, - bip39_passphrase: Option, -) -> Result { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - let response = db - .create_reverse_swap( - network, - electrum_url, - amount_sat, - claim_address, - mnemonic, - bip39_passphrase, - ) - .await?; - boltz::subscribe_if_active(network, &response.id).await; - Ok(response) - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// List every persisted swap, newest first. -#[uniffi::export] -pub async fn boltz_list_swaps() -> Result, BoltzError> { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - Ok(db - .list_swaps() - .await? - .iter() - .map(|r| r.to_boltz_swap()) - .collect()) - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// List swaps that have not reached a terminal state (for recovery/resume). -#[uniffi::export] -pub async fn boltz_list_pending_swaps() -> Result, BoltzError> { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - Ok(db - .list_pending_swaps() - .await? - .iter() - .map(|r| r.to_boltz_swap()) - .collect()) - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Fetch a single swap by id. -#[uniffi::export] -pub async fn boltz_get_swap(swap_id: String) -> Result, BoltzError> { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - Ok(db.get_swap(&swap_id).await?.map(|r| r.to_boltz_swap())) - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Claim a reverse swap's onchain funds to its claim address, returning the -/// broadcast claim transaction id. Normally happens automatically via the -/// updates stream; exposed for manual recovery. The claim key is re-derived from -/// `mnemonic`. Claims are serialized per swap, so calling this while the updates -/// stream is auto-claiming the same swap waits for that claim and returns its -/// txid rather than broadcasting a second transaction. -#[uniffi::export] -pub async fn boltz_claim_reverse_swap( - swap_id: String, - mnemonic: String, - bip39_passphrase: Option, - fee_rate_sat_per_vb: Option, -) -> Result { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - let outcome = boltz::claim_reverse_swap_guarded( - &db, - &swap_id, - &mnemonic, - bip39_passphrase.as_deref(), - fee_rate_sat_per_vb, - ) - .await?; - Ok(outcome.txid()) - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Refund a submarine swap's locked funds to `refund_address`, returning the -/// broadcast refund transaction id. Used when Boltz fails to pay the invoice or -/// the swap expires. The refund key is re-derived from `mnemonic`. Refunds are -/// serialized per swap, so two concurrent calls cannot both broadcast: the second -/// waits for the first and returns its txid. -#[uniffi::export] -pub async fn boltz_refund_submarine_swap( - swap_id: String, - refund_address: String, - mnemonic: String, - bip39_passphrase: Option, - fee_rate_sat_per_vb: Option, -) -> Result { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - boltz::refund_submarine_swap_guarded( - &db, - &swap_id, - refund_address, - &mnemonic, - bip39_passphrase.as_deref(), - fee_rate_sat_per_vb, - ) - .await - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Open a Boltz WebSocket for `network`, subscribe to all pending swaps, and -/// drive their lifecycle (auto-claiming reverse swaps) until stopped. Replaces -/// any previously running updates stream (only one network is tracked at a -/// time). `mnemonic` is held in memory for the lifetime of the stream so -/// confirmed reverse swaps can be auto-claimed; it is never persisted. Events -/// are delivered to `listener`. -/// -/// `fee_rate_sat_per_vb` is the fee rate used for automatic claim transactions. -/// Bitkit owns fee estimation and should pass its current recommended rate; when -/// `None`, a conservative built-in default is used. To auto-claim at an updated -/// fee rate, call this again (it restarts the stream). -/// -/// `accept_zero_conf` claims reverse swaps as soon as Boltz's lockup enters the -/// mempool instead of waiting for its confirmation. That reveals the preimage -/// against an unconfirmed lockup: if the lockup were replaced before -/// confirming, the user would be debited on Lightning without receiving -/// onchain funds. Pass `false` to keep the confirmation-gated default. -#[uniffi::export] -pub async fn boltz_start_swap_updates( - network: BoltzNetwork, - listener: Arc, - mnemonic: String, - bip39_passphrase: Option, - fee_rate_sat_per_vb: Option, - accept_zero_conf: bool, -) -> Result<(), BoltzError> { - let rt = ensure_runtime(); - rt.spawn(async move { - let db = get_boltz_db().await?; - boltz::start_swap_updates( - db, - network, - listener, - mnemonic, - bip39_passphrase, - fee_rate_sat_per_vb, - accept_zero_conf, - ) - .await - }) - .await - .unwrap_or_else(|e| Err(boltz_runtime_err(e))) -} - -/// Stop the running Boltz updates stream, if any. -#[uniffi::export] -pub async fn boltz_stop_swap_updates() { - let rt = ensure_runtime(); - let _ = rt - .spawn(async move { boltz::stop_swap_updates().await }) - .await; -} - // ============================================================================ // Pubky Swaps as Paykit routes // ============================================================================ diff --git a/src/modules/activity/types.rs b/src/modules/activity/types.rs index 29418497..b2acdb6f 100644 --- a/src/modules/activity/types.rs +++ b/src/modules/activity/types.rs @@ -1,6 +1,5 @@ use crate::activity::ActivityError; use crate::modules::blocktank::BlocktankError; -use crate::modules::boltz::BoltzError; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -308,9 +307,6 @@ pub enum DbError { #[error("DB Blocktank Error: {error_details}")] DbBlocktankError { error_details: BlocktankError }, - #[error("DB Boltz Error: {error_details}")] - DbBoltzError { error_details: BoltzError }, - #[error("Initialization Error: {error_details}")] InitializationError { error_details: String }, } @@ -330,11 +326,3 @@ impl From for DbError { } } } - -impl From for DbError { - fn from(error: BoltzError) -> Self { - DbError::DbBoltzError { - error_details: error, - } - } -} diff --git a/src/modules/boltz/README.md b/src/modules/boltz/README.md deleted file mode 100644 index 20bfde8b..00000000 --- a/src/modules/boltz/README.md +++ /dev/null @@ -1,317 +0,0 @@ -# Boltz Module - -This module integrates [Boltz](https://boltz.exchange) submarine and reverse -swaps so funds can move between onchain Bitcoin and Lightning channels: - -- **Submarine swap** (onchain → Lightning): you lock onchain BTC, Boltz pays a - Lightning invoice your node generated. Use this to **add** Lightning balance - from onchain funds. -- **Reverse swap** (Lightning → onchain): you pay a Boltz hold invoice over - Lightning, Boltz locks onchain BTC, and this module claims it to your onchain - address. Use this to **drain** Lightning balance to onchain. - -The dangerous cryptography (MuSig2 Taproot cooperative signing, swap scripts, -claim/refund transaction construction) is handled by the -[`boltz-client`](https://crates.io/crates/boltz-client) crate. This module adds -persistence, lifecycle tracking, automatic claiming, and the FFI surface. - -## Responsibility split with the app - -bitkit-core does **not** own the Lightning node. The app (via LDK Node) and -bitkit-core cooperate: - -| Step | Owner | -|------|-------| -| Generate the BOLT11 invoice (submarine) | **App** — `node.bolt11Payment().receive(...)` | -| Pay the hold invoice (reverse) | **App** — `node.bolt11Payment().send(...)` | -| Provide an onchain claim address (reverse) | **App** — `node.onchainPayment().newAddress()` | -| Send the onchain lockup (submarine) | **App** — `node.onchainPayment().sendToAddress(...)` | -| Call Boltz, derive keys & preimage, track status | **bitkit-core** | -| Build, sign and broadcast claim/refund transactions | **bitkit-core** | - -The app passes its **wallet mnemonic** (and BIP39 passphrase, if any) to the -create/claim/refund/start-updates calls. - -## Keys, secrets & recovery - -Swap keys are **derived deterministically from the wallet seed**, never randomly -generated and never stored. Each swap uses a unique index under Boltz's BIP85 -scheme (`m/26589'/0'/0'/{index}`); for reverse swaps the preimage is -`sha256(swapKey)`. `boltz.db` persists only that **index** (plus status and -metadata) — it holds **no key material**, so a leaked database cannot move funds. - -This makes swaps recoverable two independent ways: - -1. **Same device / restored `boltz.db`** — the index is on disk; combine it with - the in-memory seed to re-derive keys. Use `boltzListPendingSwaps` after - `boltzStartSwapUpdates` on startup. -2. **Seed only (`boltz.db` lost)** — because keys derive from the seed, an - in-flight swap can still be recovered: the same BIP85 swap mnemonic can be - registered with Boltz's rescue API - (`https://boltz.exchange/rescue/external?mode=rescue-key`) to re-enumerate - swaps and re-derive their keys by scanning indices. - -> **The BIP39 passphrase must match the wallet's.** Keys derived under the wrong -> passphrase (or a typo'd mnemonic) will not control the locked funds. Pass the -> exact same `mnemonic`/`bip39Passphrase` used by the wallet to every Boltz call. - -Secrets exist in process memory only while a swap is being created, claimed or -refunded; the background updates stream additionally holds the mnemonic in memory -for its lifetime (dropped on `boltzStopSwapUpdates`) so it can auto-claim. As -elsewhere in the app, `boltz.db` relies on the platform's app-sandbox/filesystem -encryption at rest. - -## Lifecycle - -Status strings mirror the [Boltz lifecycle](https://api.docs.boltz.exchange/lifecycle.html) -and are surfaced as the typed `BoltzSwapStatus` enum (unknown future states fall -back to `Unknown { raw }`). Register a `BoltzEventListener` via -`boltzStartSwapUpdates` to receive `BoltzSwapEvent`s over a managed WebSocket. - -**Reverse swaps are claimed automatically**: once Boltz's lockup reaches -`transaction.confirmed`, this module builds and broadcasts the claim transaction -(cooperative key-path first, script-path fallback) and emits -`BoltzSwapEvent.Claimed { txid }`. Claiming on confirmation (not mempool) avoids -revealing the preimage before the lockup is final; pass `acceptZeroConf: true` -to `boltzStartSwapUpdates` to claim on mempool acceptance instead, if you accept -the 0-conf risk (`false` is the safe default). The auto-claim fee rate is the -`feeRateSatPerVb` passed to `boltzStartSwapUpdates` — **Bitkit owns fee -estimation** and should pass its current recommended rate so the claim confirms -before the swap times out; restart the stream to apply an updated rate. - -**Submarine refunds are manual** (the module needs a destination address): on -`invoice.failedToPay` / `transaction.lockupFailed` / `swap.expired`, call -`boltzRefundSubmarineSwap` with an onchain address. - -## FFI surface - -``` -boltzGetSubmarineLimits(network) -> BoltzPairInfo -boltzGetReverseLimits(network) -> BoltzPairInfo -boltzCreateSubmarineSwap(network, electrumUrl, invoice, mnemonic, bip39Passphrase?) -> SubmarineSwapResponse -boltzCreateReverseSwap(network, electrumUrl, amountSat, claimAddress, mnemonic, bip39Passphrase?) -> ReverseSwapResponse -boltzListSwaps() -> [BoltzSwap] -boltzListPendingSwaps() -> [BoltzSwap] -boltzGetSwap(swapId) -> BoltzSwap? -boltzClaimReverseSwap(swapId, mnemonic, bip39Passphrase?, feeRateSatPerVb?) -> String (txid) -boltzRefundSubmarineSwap(swapId, refundAddress, mnemonic, bip39Passphrase?, feeRateSatPerVb?) -> String (txid) -boltzStartSwapUpdates(network, listener, mnemonic, bip39Passphrase?, feeRateSatPerVb?, acceptZeroConf) // managed WebSocket -boltzStopSwapUpdates() -``` - -`network` is `BoltzNetwork.{Mainnet, Testnet, Regtest}`. `electrumUrl` accepts -`ssl://host:port`, `tcp://host:port`, or a bare `host:port` (treated as TLS) and -is stored per-swap for later claim/refund broadcasting. `mnemonic` is the wallet -mnemonic; pass the same `bip39Passphrase` the wallet uses (omit/`null` if none). - -`boltzClaimReverseSwap` and `boltzRefundSubmarineSwap` are **idempotent**: if the -swap already has a recorded claim/refund tx, the existing txid is returned -without re-broadcasting. - -**Only one updates stream runs at a time.** `boltzStartSwapUpdates` stops any -previous stream, so a single network is tracked at once; call it again to switch -networks. - -## Usage Examples - -### Reverse swap — Lightning → onchain - -#### iOS (Swift) -```swift -import BitkitCore - -// 1. Register a listener once (auto-claims reverse swaps). -final class SwapListener: BoltzEventListener { - func onEvent(event: BoltzSwapEvent) { - switch event { - case .statusUpdate(let swapId, let status): - print("swap \(swapId): \(status)") - case .claimed(let swapId, let txid): - print("reverse swap \(swapId) claimed in \(txid)") - case .refunded(let swapId, let txid): - print("swap \(swapId) refunded in \(txid)") - case .error(let swapId, let message): - print("swap \(swapId) error: \(message)") - } - } -} -// `mnemonic` is the wallet's seed phrase; pass the wallet's BIP39 passphrase too -// (or nil). It's held in memory for the stream's lifetime to auto-claim. -try await boltzStartSwapUpdates( - network: .mainnet, - listener: SwapListener(), - mnemonic: wallet.mnemonic, - bip39Passphrase: nil, - feeRateSatPerVb: feeService.currentSatPerVb(), // Bitkit-provided fee for auto-claims - acceptZeroConf: false // claim only after the lockup confirms -) - -func drainToOnchain(amountSat: UInt64) async throws { - // 2. A fresh onchain address from the LDK node receives the funds. - let claimAddress = try lightningService.node.onchainPayment().newAddress() - - // 3. Create the swap and pay its hold invoice over Lightning. - let swap = try await boltzCreateReverseSwap( - network: .mainnet, - electrumUrl: "ssl://electrum.blockstream.info:50002", - amountSat: amountSat, - claimAddress: claimAddress, - mnemonic: wallet.mnemonic, - bip39Passphrase: nil - ) - _ = try lightningService.node.bolt11Payment().send(invoice: swap.invoice, sendingParameters: nil) - - // 4. Once Boltz locks & confirms onchain, the module auto-claims and the - // listener reports `.claimed`. Nothing else to do. -} -``` - -#### Android (Kotlin) -```kotlin -import com.synonym.bitkitcore.* - -class SwapListener : BoltzEventListener { - override fun onEvent(event: BoltzSwapEvent) { - when (event) { - is BoltzSwapEvent.StatusUpdate -> println("swap ${event.swapId}: ${event.status}") - is BoltzSwapEvent.Claimed -> println("reverse swap ${event.swapId} claimed in ${event.txid}") - is BoltzSwapEvent.Refunded -> println("swap ${event.swapId} refunded in ${event.txid}") - is BoltzSwapEvent.Error -> println("swap ${event.swapId} error: ${event.message}") - } - } -} - -suspend fun drainToOnchain(amountSat: ULong) { - // mnemonic = wallet seed phrase; pass the wallet's BIP39 passphrase or null. - // feeRateSatPerVb is Bitkit's fee rate for auto-claims; acceptZeroConf = - // false claims only after the lockup confirms (the safe default). - boltzStartSwapUpdates(BoltzNetwork.MAINNET, SwapListener(), wallet.mnemonic, null, feeService.currentSatPerVb(), false) - - val claimAddress = lightningService.node.onchainPayment().newAddress() - val swap = boltzCreateReverseSwap( - network = BoltzNetwork.MAINNET, - electrumUrl = "ssl://electrum.blockstream.info:50002", - amountSat = amountSat, - claimAddress = claimAddress, - mnemonic = wallet.mnemonic, - bip39Passphrase = null, - ) - lightningService.node.bolt11Payment().send(swap.invoice, null) - // Auto-claimed on confirmation; listener emits Claimed. -} -``` - -#### Python -```python -from bitkitcore import ( - boltz_create_reverse_swap, boltz_start_swap_updates, - BoltzNetwork, BoltzEventListener, -) - -class SwapListener(BoltzEventListener): - def on_event(self, event): - print(event) - -# accept_zero_conf=False claims only after the lockup confirms (the safe default). -await boltz_start_swap_updates(BoltzNetwork.MAINNET, SwapListener(), wallet_mnemonic, None, 5.0, False) -swap = await boltz_create_reverse_swap( - network=BoltzNetwork.MAINNET, - electrum_url="ssl://electrum.blockstream.info:50002", - amount_sat=50_000, - claim_address=claim_address, # from your onchain wallet - mnemonic=wallet_mnemonic, - bip39_passphrase=None, -) -# Pay swap.invoice over Lightning; the module claims onchain automatically. -``` - -### Submarine swap — onchain → Lightning - -#### iOS (Swift) -```swift -func topUpLightning(amountSat: UInt64) async throws { - // 1. Your LDK node issues the invoice Boltz will pay. - let invoice = try lightningService.node.bolt11Payment() - .receive(amountMsat: amountSat * 1000, description: "Boltz top-up", expirySecs: 3600) - - // 2. Create the swap; Boltz returns the lockup address & exact amount. - let swap = try await boltzCreateSubmarineSwap( - network: .mainnet, - electrumUrl: "ssl://electrum.blockstream.info:50002", - invoice: invoice, - mnemonic: wallet.mnemonic, - bip39Passphrase: nil - ) - - // 3. Fund the lockup from your onchain wallet. - _ = try lightningService.node.onchainPayment() - .sendToAddress(address: swap.address, amountSats: swap.expectedAmountSat) - - // 4. Boltz pays the invoice on confirmation (.invoicePaid → .transactionClaimed). - // If it fails, refund onchain (key re-derived from the mnemonic): - // try await boltzRefundSubmarineSwap( - // swapId: swap.id, refundAddress: addr, - // mnemonic: wallet.mnemonic, bip39Passphrase: nil, feeRateSatPerVb: nil) -} -``` - -#### Android (Kotlin) -```kotlin -suspend fun topUpLightning(amountSat: ULong) { - val invoice = lightningService.node.bolt11Payment() - .receive(amountSat * 1000u, "Boltz top-up", 3600u) - - val swap = boltzCreateSubmarineSwap( - network = BoltzNetwork.MAINNET, - electrumUrl = "ssl://electrum.blockstream.info:50002", - invoice = invoice, - mnemonic = wallet.mnemonic, - bip39Passphrase = null, - ) - lightningService.node.onchainPayment().sendToAddress(swap.address, swap.expectedAmountSat) - // On failure: boltzRefundSubmarineSwap(swap.id, refundAddress, wallet.mnemonic, null, null) -} -``` - -## Recovery after restart - -On startup (after `initDb`), resume tracking and surface anything actionable: - -```kotlin -// re-subscribes all pending swaps; holds the mnemonic to auto-claim -boltzStartSwapUpdates(BoltzNetwork.MAINNET, SwapListener(), wallet.mnemonic, null, feeService.currentSatPerVb(), false) -val pending = boltzListPendingSwaps() // for UI / manual refunds -``` - -`boltzListPendingSwaps` returns every swap that is not locally complete. A -terminal server status alone does not end recovery: a reverse swap whose invoice -Boltz reports as settled stays pending until its claim txid is recorded locally, -because the cooperative claim flow discloses the preimage before broadcast and -the claim transaction may never have reached the chain (the updates stream -retries such claims automatically). Combined with the wallet seed (keys are -re-derived from each swap's index), an interrupted reverse swap can still be -claimed and a failed submarine swap refunded. If `boltz.db` itself was -lost, recover via Boltz's rescue API using the seed (see *Keys, secrets & -recovery* above). - -## Testing - -```bash -cargo test modules::boltz # unit tests (offline) -cargo test modules::boltz -- --ignored --nocapture # live E2E (testnet) -BOLTZ_LIVE_NETWORK=mainnet cargo test modules::boltz -- --ignored --nocapture -``` - -The offline tests cover status mapping, DB round-trip/recovery, monotonic index -reservation, and deterministic key/preimage derivation (same seed+index → -same key; distinct indices/passphrases → distinct keys). The **claim/refund -broadcast paths are not yet covered by an automated test** — they require a -regtest Boltz + Electrum stack; the live test exercises swap creation and -cryptographically validates the locally-derived redeem script and invoice -against Boltz's response, but does not broadcast. A regtest end-to-end test that -actually claims and refunds is the recommended follow-up. - -The live test creates a real reverse swap and asserts the locally-derived redeem -script and invoice match Boltz's response (the guarantee that a later claim is -valid). The swap is never paid and simply expires — no funds move. It skips -gracefully if the Boltz endpoint is temporarily unavailable. diff --git a/src/modules/boltz/api.rs b/src/modules/boltz/api.rs deleted file mode 100644 index b9c81980..00000000 --- a/src/modules/boltz/api.rs +++ /dev/null @@ -1,242 +0,0 @@ -use crate::modules::boltz::client::build_boltz_client; -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::models::{derive_swap_keypair, BoltzDB, SwapRecord}; -use crate::modules::boltz::types::{ - BoltzNetwork, BoltzPairInfo, BoltzSwapType, ReverseSwapResponse, SubmarineSwapResponse, -}; -use crate::modules::boltz::validation::{ - validate_onchain_address, validate_reverse_response, validate_submarine_response, -}; -use boltz_client::swaps::boltz::{CreateReverseRequest, CreateSubmarineRequest}; -use boltz_client::util::secrets::Preimage; - -/// Raw status string a freshly created swap starts in. -const STATUS_CREATED: &str = "swap.created"; - -/// Fetch fees and limits for submarine swaps (onchain -> Lightning) on `network`. -pub async fn get_submarine_limits(network: BoltzNetwork) -> Result { - let client = build_boltz_client(network); - let pairs = client - .get_submarine_pairs() - .await - .map_err(map_api_err("fetch submarine pairs"))?; - let pair = pairs.get_btc_to_btc_pair().ok_or(BoltzError::ApiError { - error_details: "BTC submarine pair unavailable".to_string(), - })?; - Ok(BoltzPairInfo { - hash: pair.hash, - rate: pair.rate, - minimal_sat: pair.limits.minimal, - maximal_sat: pair.limits.maximal, - fee_percentage: pair.fees.percentage, - miner_fees_sat: pair.fees.miner_fees, - }) -} - -/// Fetch fees and limits for reverse swaps (Lightning -> onchain) on `network`. -pub async fn get_reverse_limits(network: BoltzNetwork) -> Result { - let client = build_boltz_client(network); - let pairs = client - .get_reverse_pairs() - .await - .map_err(map_api_err("fetch reverse pairs"))?; - let pair = pairs.get_btc_to_btc_pair().ok_or(BoltzError::ApiError { - error_details: "BTC reverse pair unavailable".to_string(), - })?; - Ok(BoltzPairInfo { - hash: pair.hash, - rate: pair.rate, - minimal_sat: pair.limits.minimal, - maximal_sat: pair.limits.maximal, - fee_percentage: pair.fees.percentage, - miner_fees_sat: pair.fees.miner_fees.lockup + pair.fees.miner_fees.claim, - }) -} - -impl BoltzDB { - /// Create a submarine swap (onchain -> Lightning). - /// - /// `invoice` is the BOLT11 invoice the caller's Lightning node generated for - /// the amount it wants to receive. Boltz returns a lockup address the caller - /// funds from its onchain wallet. The refund key is derived deterministically - /// from `mnemonic` at a freshly reserved index (persisted, not the key), so - /// the swap can be refunded — and recovered from the seed — if Boltz fails to - /// pay the invoice. - pub async fn create_submarine_swap( - &self, - network: BoltzNetwork, - electrum_url: String, - invoice: String, - mnemonic: String, - bip39_passphrase: Option, - ) -> Result { - if invoice.trim().is_empty() { - return Err(BoltzError::InvalidInput { - error_details: "invoice must not be empty".to_string(), - }); - } - - let swap_index = self.reserve_swap_index().await?; - let keypair = - derive_swap_keypair(&mnemonic, bip39_passphrase.as_deref(), network, swap_index)?; - let refund_public_key = bitcoin::PublicKey::new(keypair.public_key()); - - let client = build_boltz_client(network); - let request = CreateSubmarineRequest { - from: "BTC".to_string(), - to: "BTC".to_string(), - invoice: invoice.clone(), - refund_public_key, - pair_hash: None, - referral_id: None, - webhook: None, - }; - let response = client - .post_swap_req(&request) - .await - .map_err(map_api_err("create submarine swap"))?; - - // Prove Boltz's response before persisting it or handing the caller a - // lockup address to fund: the returned address must commit to a script - // holding our refund key, and that script's hashlock must match the - // invoice's payment hash. Without both, a malicious or buggy server - // could hand back an address we cannot refund from, or one it can - // claim without ever paying the invoice. - validate_submarine_response(&response, &invoice, &refund_public_key, network)?; - - let record = SwapRecord { - id: response.id.clone(), - swap_type: BoltzSwapType::Submarine, - status: STATUS_CREATED.to_string(), - network, - electrum_url, - swap_index, - invoice: Some(invoice), - lockup_address: Some(response.address.clone()), - onchain_address: None, - amount_sat: response.expected_amount, - onchain_amount_sat: None, - timeout_block_height: response.timeout_block_height, - create_response_json: serde_json::to_string(&response)?, - claim_tx_id: None, - refund_tx_id: None, - created_at: now_secs(), - }; - self.insert_swap(&record).await?; - - Ok(SubmarineSwapResponse { - id: response.id, - address: response.address, - bip21: response.bip21, - expected_amount_sat: response.expected_amount, - accept_zero_conf: response.accept_zero_conf, - timeout_block_height: response.timeout_block_height, - }) - } - - /// Create a reverse swap (Lightning -> onchain). - /// - /// `amount_sat` is the Lightning amount the caller will pay; `claim_address` - /// is the onchain address the received funds are claimed to (typically a - /// fresh address from the caller's onchain wallet). The claim key and the - /// preimage are derived deterministically from `mnemonic` at a freshly - /// reserved index (the preimage is `sha256(swap_key)`), so the claim can be - /// made — and recovered from the seed — once Boltz locks the onchain funds. - pub async fn create_reverse_swap( - &self, - network: BoltzNetwork, - electrum_url: String, - amount_sat: u64, - claim_address: String, - mnemonic: String, - bip39_passphrase: Option, - ) -> Result { - if amount_sat == 0 { - return Err(BoltzError::InvalidInput { - error_details: "amount_sat must be greater than 0".to_string(), - }); - } - // The claim address is persisted at creation and cannot be replaced - // once the caller has paid the invoice, so a parse or wrong-network - // failure must surface now, not at claim time. - let claim_address = validate_onchain_address(&claim_address, network)?; - - let swap_index = self.reserve_swap_index().await?; - let keypair = - derive_swap_keypair(&mnemonic, bip39_passphrase.as_deref(), network, swap_index)?; - let claim_public_key = bitcoin::PublicKey::new(keypair.public_key()); - let preimage = Preimage::from_swap_key(&keypair); - - let client = build_boltz_client(network); - let request = CreateReverseRequest { - from: "BTC".to_string(), - to: "BTC".to_string(), - claim_public_key, - invoice: None, - invoice_amount: Some(amount_sat), - preimage_hash: Some(preimage.sha256), - description: None, - description_hash: None, - address: None, - address_signature: None, - referral_id: None, - webhook: None, - }; - let response = client - .post_reverse_req(request) - .await - .map_err(map_api_err("create reverse swap"))?; - - // Prove Boltz's response before persisting it or handing the caller an - // invoice to pay: the invoice must commit to our preimage, the lockup - // address must commit to a script holding our claim key whose hashlock - // matches that preimage, and the invoice amount must be what was - // requested. Without those, the caller could pay an invoice for funds - // it cannot claim, or pay more than it asked to. - validate_reverse_response(&response, &preimage, &claim_public_key, amount_sat, network)?; - - let invoice = response.invoice.clone().ok_or(BoltzError::ApiError { - error_details: "Reverse swap response missing invoice".to_string(), - })?; - let timeout_block_height = response.timeout_block_height as u64; - let onchain_amount = response.onchain_amount; - - let record = SwapRecord { - id: response.id.clone(), - swap_type: BoltzSwapType::Reverse, - status: STATUS_CREATED.to_string(), - network, - electrum_url, - swap_index, - invoice: Some(invoice.clone()), - lockup_address: Some(response.lockup_address.clone()), - onchain_address: Some(claim_address), - amount_sat, - onchain_amount_sat: Some(onchain_amount), - timeout_block_height, - create_response_json: serde_json::to_string(&response)?, - claim_tx_id: None, - refund_tx_id: None, - created_at: now_secs(), - }; - self.insert_swap(&record).await?; - - Ok(ReverseSwapResponse { - id: response.id, - invoice, - lockup_address: response.lockup_address, - onchain_amount_sat: onchain_amount, - timeout_block_height, - }) - } -} - -fn now_secs() -> u64 { - chrono::Utc::now().timestamp().max(0) as u64 -} - -fn map_api_err(context: &'static str) -> impl Fn(boltz_client::error::Error) -> BoltzError { - move |e| BoltzError::ApiError { - error_details: format!("Failed to {}: {}", context, e), - } -} diff --git a/src/modules/boltz/claim.rs b/src/modules/boltz/claim.rs deleted file mode 100644 index 64eb3fe7..00000000 --- a/src/modules/boltz/claim.rs +++ /dev/null @@ -1,128 +0,0 @@ -use crate::modules::boltz::client::{build_boltz_client, build_chain_client}; -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::guard::lock_swap; -use crate::modules::boltz::models::{BoltzDB, SwapRecord}; -use crate::modules::boltz::validation::validate_fee_rate; -use boltz_client::swaps::{SwapScript, SwapTransactionParams, TransactionOptions}; -use boltz_client::util::fees::Fee; - -/// Default claim fee rate in sat/vByte used when the caller doesn't specify one. -pub(crate) const DEFAULT_FEERATE_SAT_PER_VB: f64 = 2.0; - -/// Result of a guarded claim attempt. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ClaimOutcome { - /// A claim transaction was built and broadcast by this call. - Broadcast(String), - /// The swap already had a claim txid recorded, so nothing was broadcast. - AlreadyClaimed(String), -} - -impl ClaimOutcome { - /// The claim transaction id, however it was arrived at. - pub fn txid(self) -> String { - match self { - ClaimOutcome::Broadcast(txid) | ClaimOutcome::AlreadyClaimed(txid) => txid, - } - } -} - -/// Claim a reverse swap, serialized against any other claim of the same swap. -/// -/// This is the only path that should broadcast a claim. It holds the swap's lock -/// across the whole read-broadcast-record sequence, so the automatic claim from -/// the updates stream and a manual recovery call cannot both broadcast: whichever -/// arrives second re-reads the swap under the lock, finds the first one's txid, -/// and returns it as [`ClaimOutcome::AlreadyClaimed`]. -pub async fn claim_reverse_swap_guarded( - db: &BoltzDB, - swap_id: &str, - mnemonic: &str, - bip39_passphrase: Option<&str>, - fee_rate_sat_per_vb: Option, -) -> Result { - validate_fee_rate(fee_rate_sat_per_vb)?; - let _guard = lock_swap(swap_id).await; - - // Re-read under the lock. The record the caller checked may be stale: a - // concurrent claim can have completed while we waited to acquire. - let record = db - .get_swap(swap_id) - .await? - .ok_or_else(|| BoltzError::NotFound { - error_details: format!("Swap {} not found", swap_id), - })?; - if let Some(existing) = record.claim_tx_id { - return Ok(ClaimOutcome::AlreadyClaimed(existing)); - } - - let txid = claim_reverse_swap(&record, mnemonic, bip39_passphrase, fee_rate_sat_per_vb).await?; - db.set_claim_tx(swap_id, &txid).await?; - Ok(ClaimOutcome::Broadcast(txid)) -} - -/// Claim a reverse swap's onchain funds to the address captured at creation, -/// revealing the preimage so Boltz can settle the Lightning invoice. -/// -/// A cooperative (key-path) claim is attempted first for a smaller, cheaper -/// transaction; if Boltz declines to cooperate it falls back to the script-path -/// spend, which is always available while the lockup is unspent. Returns the -/// broadcast claim transaction id. -pub async fn claim_reverse_swap( - record: &SwapRecord, - mnemonic: &str, - bip39_passphrase: Option<&str>, - fee_rate_sat_per_vb: Option, -) -> Result { - let reverse_resp = record.reverse_response()?; - let keypair = record.keypair(mnemonic, bip39_passphrase)?; - let preimage = record.preimage(mnemonic, bip39_passphrase)?; - let our_pubkey = bitcoin::PublicKey::new(keypair.public_key()); - - let claim_address = record - .onchain_address - .clone() - .ok_or_else(|| BoltzError::InvalidInput { - error_details: "Reverse swap is missing a claim address".to_string(), - })?; - - let chain = record.network.as_chain(); - let swap_script = SwapScript::reverse_from_swap_resp(chain, &reverse_resp, our_pubkey)?; - let chain_client = build_chain_client(record.network, &record.electrum_url)?; - let boltz_client = build_boltz_client(record.network); - let fee = Fee::Relative(fee_rate_sat_per_vb.unwrap_or(DEFAULT_FEERATE_SAT_PER_VB)); - - let make_params = |cooperative: bool| SwapTransactionParams { - keys: keypair, - output_address: claim_address.clone(), - fee, - swap_id: record.id.clone(), - chain_client: &chain_client, - boltz_client: &boltz_client, - options: Some(TransactionOptions::default().with_cooperative(cooperative)), - }; - - // Try the cooperative key-path claim first; fall back to the script path. - let tx = match swap_script - .construct_claim(&preimage, make_params(true)) - .await - { - Ok(tx) => tx, - Err(coop_err) => swap_script - .construct_claim(&preimage, make_params(false)) - .await - .map_err(|script_err| BoltzError::SwapError { - error_details: format!( - "Claim failed (cooperative: {}; script-path: {})", - coop_err, script_err - ), - })?, - }; - - chain_client - .broadcast_tx(&tx) - .await - .map_err(|e| BoltzError::BroadcastError { - error_details: format!("Failed to broadcast claim transaction: {}", e), - }) -} diff --git a/src/modules/boltz/client.rs b/src/modules/boltz/client.rs deleted file mode 100644 index 719eb01f..00000000 --- a/src/modules/boltz/client.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::types::BoltzNetwork; -use boltz_client::network::electrum::ElectrumBitcoinClient; -use boltz_client::swaps::boltz::BoltzApiClientV2; -use boltz_client::swaps::ChainClient; - -/// Electrum socket timeout, in seconds, used when building swap chain clients. -const ELECTRUM_TIMEOUT_SECS: u8 = 30; - -/// Build a Boltz REST client for the given network. -pub fn build_boltz_client(network: BoltzNetwork) -> BoltzApiClientV2 { - BoltzApiClientV2::default(network.as_client_network()) -} - -/// Build an Electrum-backed [`ChainClient`] for broadcasting and fetching swap -/// UTXOs. `electrum_url` accepts the same `ssl://`/`tcp://` scheme conventions -/// used elsewhere in the crate; a bare `host:port` is treated as TLS. -pub fn build_chain_client( - network: BoltzNetwork, - electrum_url: &str, -) -> Result { - let (host_port, tls) = parse_electrum_url(electrum_url); - let client = ElectrumBitcoinClient::new( - network.as_bitcoin_chain(), - &host_port, - tls, - tls, - ELECTRUM_TIMEOUT_SECS, - ) - .map_err(|e| BoltzError::ConnectionError { - error_details: format!("Failed to connect to Electrum server: {}", e), - })?; - Ok(ChainClient::new().with_bitcoin(client)) -} - -/// Split an Electrum URL into a `host:port` and a TLS flag. `ssl://` and -/// `tls://` mean TLS; `tcp://` means plaintext; a scheme-less URL defaults to -/// TLS (the safe default for public servers). -fn parse_electrum_url(url: &str) -> (String, bool) { - if let Some(rest) = url.strip_prefix("ssl://") { - (rest.to_string(), true) - } else if let Some(rest) = url.strip_prefix("tls://") { - (rest.to_string(), true) - } else if let Some(rest) = url.strip_prefix("tcp://") { - (rest.to_string(), false) - } else { - (url.to_string(), true) - } -} - -#[cfg(test)] -mod tests { - use super::parse_electrum_url; - - #[test] - fn parses_schemes() { - assert_eq!( - parse_electrum_url("ssl://electrum.example.com:50002"), - ("electrum.example.com:50002".to_string(), true) - ); - assert_eq!( - parse_electrum_url("tcp://10.0.0.1:50001"), - ("10.0.0.1:50001".to_string(), false) - ); - assert_eq!( - parse_electrum_url("electrum.example.com:50002"), - ("electrum.example.com:50002".to_string(), true) - ); - } -} diff --git a/src/modules/boltz/db.rs b/src/modules/boltz/db.rs deleted file mode 100644 index c5d9937f..00000000 --- a/src/modules/boltz/db.rs +++ /dev/null @@ -1,252 +0,0 @@ -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::models::{ - BoltzDB, SwapRecord, CREATE_META_TABLE, CREATE_SWAPS_TABLE, SCHEMA_VERSION, -}; -use crate::modules::boltz::types::{BoltzNetwork, BoltzSwapType}; -use rusqlite::{params, Connection, OptionalExtension, Row}; - -/// Counter key in `swap_meta` for the next deterministic swap index. -const NEXT_SWAP_INDEX_KEY: &str = "next_swap_index"; - -impl BoltzDB { - /// Open (or create) the swaps database at `db_path` and run migrations. - pub async fn new(db_path: &str) -> Result { - let conn = Connection::open(db_path).map_err(|e| BoltzError::InitializationError { - error_details: format!("Error opening database: {}", e), - })?; - let db = BoltzDB { - conn: tokio::sync::Mutex::new(conn), - }; - db.initialize().await?; - Ok(db) - } - - async fn initialize(&self) -> Result<(), BoltzError> { - let conn = self.conn.lock().await; - conn.execute(CREATE_SWAPS_TABLE, []) - .map_err(|e| BoltzError::InitializationError { - error_details: format!("Failed to create swaps table: {}", e), - })?; - conn.execute(CREATE_META_TABLE, []) - .map_err(|e| BoltzError::InitializationError { - error_details: format!("Failed to create swap_meta table: {}", e), - })?; - conn.pragma_update(None, "user_version", SCHEMA_VERSION) - .map_err(|e| BoltzError::InitializationError { - error_details: format!("Failed to set schema version: {}", e), - })?; - Ok(()) - } - - /// Atomically reserve the next deterministic swap index. The connection - /// mutex serializes all access, so the read-then-write below cannot - /// interleave with another reservation. Indices are monotonic and never - /// reused, so each swap derives a unique key even if a creation later fails. - pub async fn reserve_swap_index(&self) -> Result { - let conn = self.conn.lock().await; - let current: Option = conn - .query_row( - "SELECT value FROM swap_meta WHERE key = ?1", - params![NEXT_SWAP_INDEX_KEY], - |row| row.get(0), - ) - .optional()?; - let index = current.unwrap_or(0); - conn.execute( - "INSERT INTO swap_meta (key, value) VALUES (?1, ?2) - ON CONFLICT(key) DO UPDATE SET value = ?2", - params![NEXT_SWAP_INDEX_KEY, index + 1], - )?; - Ok(index as u64) - } - - /// Insert a newly-created swap. - pub async fn insert_swap(&self, record: &SwapRecord) -> Result<(), BoltzError> { - let conn = self.conn.lock().await; - conn.execute( - "INSERT INTO swaps ( - id, swap_type, status, network, electrum_url, swap_index, - invoice, lockup_address, onchain_address, amount_sat, onchain_amount_sat, - timeout_block_height, create_response_json, claim_tx_id, refund_tx_id, created_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)", - params![ - record.id, - record.swap_type.as_str(), - record.status, - record.network.as_str(), - record.electrum_url, - record.swap_index as i64, - record.invoice, - record.lockup_address, - record.onchain_address, - record.amount_sat as i64, - record.onchain_amount_sat.map(|v| v as i64), - record.timeout_block_height as i64, - record.create_response_json, - record.claim_tx_id, - record.refund_tx_id, - record.created_at as i64, - ], - ) - .map_err(|e| BoltzError::DatabaseError { - error_details: format!("Failed to insert swap: {}", e), - })?; - Ok(()) - } - - /// Update the raw status string of a swap. - /// - /// Once a claim or refund txid has been recorded locally, the locally set - /// terminal status is ground truth for what happened onchain; a delayed or - /// re-ordered server update (the WebSocket and the reconcile loop both feed - /// this) must not regress it, so completed swaps are left untouched. - pub async fn update_status(&self, swap_id: &str, status: &str) -> Result<(), BoltzError> { - let conn = self.conn.lock().await; - conn.execute( - "UPDATE swaps SET status = ?1 - WHERE id = ?2 AND claim_tx_id IS NULL AND refund_tx_id IS NULL", - params![status, swap_id], - )?; - Ok(()) - } - - /// Record the broadcast claim transaction id for a swap and mark it claimed. - /// - /// The `status` column is normally advanced only by the live updates stream, so - /// claiming (manual or automatic) would otherwise leave a swap showing its - /// pre-claim status. Setting the terminal `transaction.claimed` status here keeps - /// the persisted state truthful and drops the swap from the pending set. - pub async fn set_claim_tx(&self, swap_id: &str, txid: &str) -> Result<(), BoltzError> { - let conn = self.conn.lock().await; - conn.execute( - "UPDATE swaps SET claim_tx_id = ?1, status = 'transaction.claimed' WHERE id = ?2", - params![txid, swap_id], - )?; - Ok(()) - } - - /// Record the broadcast refund transaction id and destination for a swap - /// and mark it refunded. - /// - /// The destination is stored as the swap's `onchain_address`: a submarine - /// swap is created without one, and the public field is documented as the - /// refund destination, so queries after a refund must return it. - pub async fn set_refund_tx( - &self, - swap_id: &str, - txid: &str, - refund_address: &str, - ) -> Result<(), BoltzError> { - let conn = self.conn.lock().await; - conn.execute( - "UPDATE swaps - SET refund_tx_id = ?1, status = 'transaction.refunded', onchain_address = ?2 - WHERE id = ?3", - params![txid, refund_address, swap_id], - )?; - Ok(()) - } - - /// Fetch a single swap by id. - pub async fn get_swap(&self, swap_id: &str) -> Result, BoltzError> { - let conn = self.conn.lock().await; - let record = conn - .query_row( - "SELECT id, swap_type, status, network, electrum_url, swap_index, - invoice, lockup_address, onchain_address, amount_sat, onchain_amount_sat, - timeout_block_height, create_response_json, claim_tx_id, refund_tx_id, - created_at - FROM swaps WHERE id = ?1", - params![swap_id], - row_to_record, - ) - .optional()?; - record.transpose() - } - - /// List every persisted swap, newest first. - pub async fn list_swaps(&self) -> Result, BoltzError> { - self.query_swaps( - "SELECT id, swap_type, status, network, electrum_url, swap_index, - invoice, lockup_address, onchain_address, amount_sat, onchain_amount_sat, - timeout_block_height, create_response_json, claim_tx_id, refund_tx_id, created_at - FROM swaps ORDER BY created_at DESC", - ) - .await - } - - /// List swaps that are not locally complete, for recovery and for - /// resubscribing to status updates after a restart. Completion is judged by - /// [`SwapRecord::is_locally_complete`], not server status alone, so a - /// reverse swap whose invoice settled but whose claim never broadcast stays - /// recoverable. - pub async fn list_pending_swaps(&self) -> Result, BoltzError> { - Ok(self - .list_swaps() - .await? - .into_iter() - .filter(|r| !r.is_locally_complete()) - .collect()) - } - - async fn query_swaps(&self, sql: &str) -> Result, BoltzError> { - let conn = self.conn.lock().await; - let mut stmt = conn.prepare(sql)?; - let rows = stmt.query_map([], row_to_record)?; - let mut records = Vec::new(); - for row in rows { - records.push(row??); - } - Ok(records) - } -} - -/// Map a SQLite row to a [`SwapRecord`]. The outer `rusqlite::Result` covers -/// column-access failures; the inner `Result` covers -/// decoding of persisted enum strings (`swap_type`, `network`). No secrets are -/// stored, so there is no key material to decode here. -fn row_to_record(row: &Row) -> rusqlite::Result> { - let swap_type_str: String = row.get(1)?; - let network_str: String = row.get(3)?; - let amount_sat: i64 = row.get(9)?; - let onchain_amount_sat: Option = row.get(10)?; - let timeout_block_height: i64 = row.get(11)?; - let created_at: i64 = row.get(15)?; - let swap_index: i64 = row.get(5)?; - - let swap_type = match BoltzSwapType::from_str(&swap_type_str) { - Some(t) => t, - None => { - return Ok(Err(BoltzError::DatabaseError { - error_details: format!("Unknown swap_type: {}", swap_type_str), - })) - } - }; - let network = match BoltzNetwork::from_str(&network_str) { - Some(n) => n, - None => { - return Ok(Err(BoltzError::DatabaseError { - error_details: format!("Unknown network: {}", network_str), - })) - } - }; - - Ok(Ok(SwapRecord { - id: row.get(0)?, - swap_type, - status: row.get(2)?, - network, - electrum_url: row.get(4)?, - swap_index: swap_index as u64, - invoice: row.get(6)?, - lockup_address: row.get(7)?, - onchain_address: row.get(8)?, - amount_sat: amount_sat as u64, - onchain_amount_sat: onchain_amount_sat.map(|v| v as u64), - timeout_block_height: timeout_block_height as u64, - create_response_json: row.get(12)?, - claim_tx_id: row.get(13)?, - refund_tx_id: row.get(14)?, - created_at: created_at as u64, - })) -} diff --git a/src/modules/boltz/errors.rs b/src/modules/boltz/errors.rs deleted file mode 100644 index f4d7a89a..00000000 --- a/src/modules/boltz/errors.rs +++ /dev/null @@ -1,56 +0,0 @@ -use thiserror::Error; - -/// Errors surfaced by the Boltz swaps module. -#[derive(uniffi::Error, Debug, Error)] -pub enum BoltzError { - #[error("Database initialization failed: {error_details}")] - InitializationError { error_details: String }, - - #[error("Database connection error: {error_details}")] - ConnectionError { error_details: String }, - - #[error("Database error: {error_details}")] - DatabaseError { error_details: String }, - - #[error("Boltz API error: {error_details}")] - ApiError { error_details: String }, - - #[error("Swap error: {error_details}")] - SwapError { error_details: String }, - - #[error("Broadcast error: {error_details}")] - BroadcastError { error_details: String }, - - #[error("Invalid input: {error_details}")] - InvalidInput { error_details: String }, - - #[error("Serialization error: {error_details}")] - SerializationError { error_details: String }, - - #[error("Swap not found: {error_details}")] - NotFound { error_details: String }, -} - -impl From for BoltzError { - fn from(err: boltz_client::error::Error) -> Self { - BoltzError::SwapError { - error_details: err.to_string(), - } - } -} - -impl From for BoltzError { - fn from(err: serde_json::Error) -> Self { - BoltzError::SerializationError { - error_details: err.to_string(), - } - } -} - -impl From for BoltzError { - fn from(err: rusqlite::Error) -> Self { - BoltzError::DatabaseError { - error_details: err.to_string(), - } - } -} diff --git a/src/modules/boltz/guard.rs b/src/modules/boltz/guard.rs deleted file mode 100644 index d782ad61..00000000 --- a/src/modules/boltz/guard.rs +++ /dev/null @@ -1,32 +0,0 @@ -use once_cell::sync::Lazy; -use std::collections::HashMap; -use std::sync::{Arc, Mutex as StdMutex}; -use tokio::sync::{Mutex as TokioMutex, OwnedMutexGuard}; - -/// Per-swap locks that serialize claim and refund attempts. -/// -/// Claiming reads the swap, broadcasts, then records the txid. Those steps span -/// `.await` points (Boltz and Electrum round trips), so two callers can -/// interleave: the automatic claim driven by the updates stream and a manual -/// `boltz_claim_reverse_swap` recovery call can both observe no recorded txid -/// and both broadcast. Holding the swap's lock across the whole -/// read-broadcast-record sequence makes it atomic, so the second caller sees the -/// first caller's txid and returns it instead of broadcasting again. -/// -/// The map only ever holds one small entry per swap that has had a claim or -/// refund attempted, so it is left to grow rather than reference-counted down; -/// pruning would race with a caller that is waiting to acquire the same lock. -static SWAP_LOCKS: Lazy>>>> = - Lazy::new(|| StdMutex::new(HashMap::new())); - -/// Acquire the lock for `swap_id`, waiting for any in-flight claim or refund on -/// the same swap to finish. The returned guard releases the lock when dropped. -pub(crate) async fn lock_swap(swap_id: &str) -> OwnedMutexGuard<()> { - let lock = { - let mut locks = SWAP_LOCKS - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - locks.entry(swap_id.to_string()).or_default().clone() - }; - lock.lock_owned().await -} diff --git a/src/modules/boltz/listener.rs b/src/modules/boltz/listener.rs deleted file mode 100644 index 84718602..00000000 --- a/src/modules/boltz/listener.rs +++ /dev/null @@ -1,526 +0,0 @@ -use crate::modules::boltz::claim::{claim_reverse_swap_guarded, ClaimOutcome}; -use crate::modules::boltz::client::build_boltz_client; -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::models::{BoltzDB, SwapRecord}; -use crate::modules::boltz::types::{BoltzNetwork, BoltzSwapEvent, BoltzSwapStatus, BoltzSwapType}; -use crate::modules::boltz::validation::validate_fee_rate; -use boltz_client::swaps::boltz::{BoltzWsApi, BoltzWsConfig, SwapStatus}; -use once_cell::sync::OnceCell; -use std::sync::Arc; -use tokio::sync::broadcast::error::RecvError; -use tokio::sync::Mutex as TokioMutex; -use tokio::task::JoinHandle; - -/// Callback interface for receiving Boltz swap lifecycle events. -/// -/// Implement this in Swift/Kotlin/Python and register it via -/// `boltz_start_swap_updates` to receive typed notifications as swaps progress. -/// Reverse swaps are claimed automatically; the [`BoltzSwapEvent::Claimed`] -/// event reports the resulting transaction id. -#[uniffi::export(with_foreign)] -pub trait BoltzEventListener: Send + Sync { - fn on_event(&self, event: BoltzSwapEvent); -} - -struct UpdatesHandle { - ws: Arc, - network: BoltzNetwork, - db: Arc, - listener: Arc, - config: AutoClaimConfig, - ws_task: JoinHandle<()>, - process_task: JoinHandle<()>, - reconcile_task: JoinHandle<()>, -} - -impl UpdatesHandle { - /// Abort the stream's background tasks and release its WebSocket. Dropping - /// a [`JoinHandle`] alone does NOT stop the task (it detaches), so a - /// superseded stream must be shut down explicitly or its tasks would keep - /// running, processing events, forever. - fn shutdown(self) { - self.reconcile_task.abort(); - self.process_task.abort(); - self.ws_task.abort(); - // Dropping the last `Arc` triggers its shutdown. - drop(self.ws); - } -} - -/// Count of live background tasks across all update streams, maintained by the -/// [`TaskAlive`] markers each task future holds. Lets tests (and debugging) -/// verify that superseded or stopped streams' tasks are actually torn down. -static LIVE_STREAM_TASKS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); - -/// RAII marker moved into every spawned stream task. Created (and counted) -/// before the spawn, dropped when the task's future is dropped, whether it ran -/// to completion or was aborted. -struct TaskAlive; - -impl TaskAlive { - fn new() -> TaskAlive { - LIVE_STREAM_TASKS.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - TaskAlive - } -} - -impl Drop for TaskAlive { - fn drop(&mut self) { - LIVE_STREAM_TASKS.fetch_sub(1, std::sync::atomic::Ordering::SeqCst); - } -} - -/// How often the running updates stream re-checks every pending swap against -/// Boltz's REST status, so a missed live event self-heals without a restart. -const RECONCILE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60); - -/// Configuration the background stream needs to perform automatic -/// reverse-swap claims. The wallet credentials (used to re-derive swap keys) -/// are held in memory only for the lifetime of the updates stream (dropped on -/// [`stop_swap_updates`]); never persisted. -#[derive(Clone)] -struct AutoClaimConfig { - mnemonic: String, - bip39_passphrase: Option, - /// Fee rate, in sat/vByte, used for the automatic claim transaction. Supplied - /// by the caller (Bitkit owns fee estimation); falls back to - /// [`crate::modules::boltz::claim::DEFAULT_FEERATE_SAT_PER_VB`] when `None`. - fee_rate_sat_per_vb: Option, - /// When `true`, reverse swaps are auto-claimed as soon as the lockup enters - /// the mempool instead of waiting for its confirmation. See - /// [`should_auto_claim`] for the risk trade-off. - accept_zero_conf: bool, -} - -static SWAP_UPDATES: OnceCell>> = OnceCell::new(); - -fn updates_cell() -> &'static TokioMutex> { - SWAP_UPDATES.get_or_init(|| TokioMutex::new(None)) -} - -/// Open a Boltz WebSocket for `network`, subscribe to every pending swap, and -/// drive their lifecycle until [`stop_swap_updates`] is called. -/// -/// Any previously running updates stream is stopped first; only one stream (for -/// one network) runs at a time. `mnemonic` is held in memory for the lifetime of -/// the stream so confirmed reverse swaps can be auto-claimed (their keys are -/// re-derived on demand). `fee_rate_sat_per_vb` is the fee rate used for those -/// automatic claims (Bitkit supplies the current rate; `None` falls back to a -/// conservative default). `accept_zero_conf` claims reverse swaps as soon as -/// the lockup hits the mempool instead of waiting for its confirmation (see -/// [`should_auto_claim`] for the risk trade-off). Must be invoked from within -/// a Tokio runtime context (it spawns background tasks). -pub async fn start_swap_updates( - db: Arc, - network: BoltzNetwork, - listener: Arc, - mnemonic: String, - bip39_passphrase: Option, - fee_rate_sat_per_vb: Option, - accept_zero_conf: bool, -) -> Result<(), BoltzError> { - // An invalid auto-claim fee rate must fail here, before the stream exists, - // not later inside a claim attempt. - validate_fee_rate(fee_rate_sat_per_vb)?; - - // Hold the stream slot's lock across the entire replacement. Two concurrent - // starts would otherwise both tear down the old stream, both spawn tasks, - // and then overwrite each other's handle; dropping a handle does not stop - // its tasks, so the loser's stream would keep running detached. - let mut guard = updates_cell().lock().await; - if let Some(handle) = guard.take() { - handle.shutdown(); - } - - // The only fallible step, done before anything is spawned so a failed start - // cannot leak a task. - let pending = db.list_pending_swaps().await?; - - let config = AutoClaimConfig { - mnemonic, - bip39_passphrase, - fee_rate_sat_per_vb, - accept_zero_conf, - }; - - let boltz_client = build_boltz_client(network); - let ws = Arc::new(BoltzWsApi::new( - boltz_client.get_ws_url(), - BoltzWsConfig::default(), - )); - - let ws_task = { - let alive = TaskAlive::new(); - let ws = ws.clone(); - tokio::spawn(async move { - let _alive = alive; - ws.run_ws_loop().await; - }) - }; - - // Subscribe to every pending swap for this network. Reconciliation is - // handled by the periodic task below, whose immediate first tick catches up - // any status the live stream missed while it was down. - for record in pending.iter().filter(|r| r.network == network) { - if let Err(e) = ws.subscribe_swap(&record.id).await { - log::warn!("Failed to subscribe to swap {}: {}", record.id, e); - } - } - - // Reconcile every pending swap against Boltz's REST status once per minute - // for as long as the stream runs, so a live event that was dropped or lagged - // (the process loop skips `RecvError::Lagged`) self-heals without an app - // restart. Swaps are reconciled serially to rate-limit the REST calls. - let reconcile_task = { - let alive = TaskAlive::new(); - let db = db.clone(); - let listener = listener.clone(); - let config = config.clone(); - tokio::spawn(async move { - let _alive = alive; - let mut interval = tokio::time::interval(RECONCILE_INTERVAL); - loop { - interval.tick().await; - let pending = match db.list_pending_swaps().await { - Ok(pending) => pending, - Err(e) => { - log::warn!("Failed to list pending swaps for reconcile: {}", e); - continue; - } - }; - for record in pending.iter().filter(|r| r.network == network) { - reconcile_swap(&db, &listener, &config, network, &record.id).await; - } - } - }) - }; - - let process_task = { - let alive = TaskAlive::new(); - let ws = ws.clone(); - let db = db.clone(); - let listener = listener.clone(); - let config = config.clone(); - tokio::spawn(async move { - let _alive = alive; - let mut updates = ws.updates(); - loop { - match updates.recv().await { - Ok(status) => process_status(&db, listener.as_ref(), &config, status).await, - Err(RecvError::Lagged(_)) => continue, - Err(RecvError::Closed) => break, - } - } - }) - }; - - *guard = Some(UpdatesHandle { - ws, - network, - db, - listener, - config, - ws_task, - process_task, - reconcile_task, - }); - Ok(()) -} - -/// Stop the running updates stream, if any, and tear down its tasks. -pub async fn stop_swap_updates() { - let mut guard = updates_cell().lock().await; - if let Some(handle) = guard.take() { - handle.shutdown(); - } -} - -/// If an updates stream is running for `network`, subscribe it to `swap_id` so -/// newly created swaps are tracked without restarting the stream, then reconcile -/// the swap against Boltz's REST status so a status that predates the -/// subscription is not missed. -pub async fn subscribe_if_active(network: BoltzNetwork, swap_id: &str) { - // Clone the context we need, then release the global lock before any network - // round-trip so the lock is not held across REST/WebSocket calls. - let ctx = { - let guard = updates_cell().lock().await; - match guard.as_ref() { - Some(handle) if handle.network == network => Some(( - handle.ws.clone(), - handle.db.clone(), - handle.listener.clone(), - handle.config.clone(), - )), - _ => None, - } - }; - let Some((ws, db, listener, config)) = ctx else { - return; - }; - if let Err(e) = ws.subscribe_swap(swap_id).await { - log::warn!("Failed to subscribe to swap {}: {}", swap_id, e); - } - reconcile_swap(&db, &listener, &config, network, swap_id).await; -} - -/// Fetch a swap's current status from Boltz over REST and run it through -/// [`process_status`], catching up any status the live WebSocket did not deliver -/// (for example a `transaction.confirmed` lockup that fired while the stream was -/// down). This makes every (re)subscribe self-healing: a confirmed reverse-swap -/// lockup is auto-claimed even when its live event was never received. -async fn reconcile_swap( - db: &Arc, - listener: &Arc, - config: &AutoClaimConfig, - network: BoltzNetwork, - swap_id: &str, -) { - let boltz_client = build_boltz_client(network); - match boltz_client.get_swap(swap_id).await { - Ok(resp) => { - let status = SwapStatus { - id: swap_id.to_string(), - status: resp.status, - ..Default::default() - }; - process_status(db, listener.as_ref(), config, status).await; - } - Err(e) => log::warn!("Failed to reconcile swap {}: {}", swap_id, e), - } -} - -/// Handle a single status update: persist it, notify the listener, and trigger -/// an automatic claim for reverse swaps whose lockup is now spendable. -async fn process_status( - db: &Arc, - listener: &dyn BoltzEventListener, - config: &AutoClaimConfig, - status: SwapStatus, -) { - let swap_id = status.id.clone(); - let raw = status.status.clone(); - - if let Err(e) = db.update_status(&swap_id, &raw).await { - log::warn!("Failed to persist status for swap {}: {}", swap_id, e); - } - - listener.on_event(BoltzSwapEvent::StatusUpdate { - swap_id: swap_id.clone(), - status: BoltzSwapStatus::from_raw(&raw), - }); - - let record = match db.get_swap(&swap_id).await { - Ok(Some(record)) => record, - Ok(None) => return, - Err(e) => { - log::warn!("Failed to load swap {} after update: {}", swap_id, e); - return; - } - }; - - if should_auto_claim(&record, &raw, config.accept_zero_conf) { - auto_claim(db, listener, config, &record).await; - } -} - -/// A reverse swap is auto-claimed once Boltz's lockup *confirms*, provided it -/// hasn't already been claimed. When `accept_zero_conf` is set it is claimed -/// as soon as the lockup enters the mempool. -/// -/// Claiming reveals the preimage, which lets Boltz settle the Lightning -/// invoice. Doing that against an unconfirmed (mempool-only) lockup risks the -/// preimage leaking before the lockup confirms: if the lockup were then -/// replaced, the user could be debited on Lightning without receiving onchain -/// funds. Callers opt into that trade-off via `accept_zero_conf`; the -/// confirmed arm always claims so a swap whose mempool event was missed is -/// still claimed on confirmation. -/// -/// `invoice.settled` with no locally recorded claim txid also claims: the -/// cooperative flow discloses the preimage before broadcast, so Boltz can -/// settle the invoice while our claim transaction failed to broadcast. The -/// preimage is already public at that point, so retrying the claim carries no -/// added risk and recovers the onchain funds. -fn should_auto_claim(record: &SwapRecord, raw_status: &str, accept_zero_conf: bool) -> bool { - record.swap_type == BoltzSwapType::Reverse - && record.claim_tx_id.is_none() - && (raw_status == "transaction.confirmed" - || raw_status == "invoice.settled" - || (accept_zero_conf && raw_status == "transaction.mempool")) -} - -/// Claim through the guarded path, which serializes against a concurrent manual -/// claim and persists the txid while still holding the swap's lock. If that -/// manual claim won the race, this call broadcasts nothing and stays quiet: -/// its caller already received the txid, so re-emitting [`BoltzSwapEvent::Claimed`] -/// would double-report the same claim. -async fn auto_claim( - db: &Arc, - listener: &dyn BoltzEventListener, - config: &AutoClaimConfig, - record: &SwapRecord, -) { - match claim_reverse_swap_guarded( - db, - &record.id, - &config.mnemonic, - config.bip39_passphrase.as_deref(), - config.fee_rate_sat_per_vb, - ) - .await - { - Ok(ClaimOutcome::Broadcast(txid)) => listener.on_event(BoltzSwapEvent::Claimed { - swap_id: record.id.clone(), - txid, - }), - Ok(ClaimOutcome::AlreadyClaimed(txid)) => { - log::info!("Swap {} was already claimed by tx {}", record.id, txid); - } - Err(e) => listener.on_event(BoltzSwapEvent::Error { - swap_id: record.id.clone(), - message: e.to_string(), - }), - } -} - -#[cfg(test)] -mod tests { - use super::should_auto_claim; - use crate::modules::boltz::models::SwapRecord; - use crate::modules::boltz::types::{BoltzNetwork, BoltzSwapType}; - - fn record(swap_type: BoltzSwapType, claim_tx_id: Option) -> SwapRecord { - SwapRecord { - id: "swap-id".to_string(), - swap_type, - status: "swap.created".to_string(), - network: BoltzNetwork::Testnet, - electrum_url: "ssl://electrum.example.com:50002".to_string(), - swap_index: 0, - invoice: Some("lnbc1...".to_string()), - lockup_address: Some("bc1qlockup".to_string()), - onchain_address: Some("bc1qclaim".to_string()), - amount_sat: 100_000, - onchain_amount_sat: Some(99_000), - timeout_block_height: 800_000, - create_response_json: "{}".to_string(), - claim_tx_id, - refund_tx_id: None, - created_at: 1_700_000_000, - } - } - - #[test] - fn auto_claims_a_confirmed_unclaimed_reverse_swap() { - let record = record(BoltzSwapType::Reverse, None); - assert!(should_auto_claim(&record, "transaction.confirmed", false)); - assert!(should_auto_claim(&record, "transaction.confirmed", true)); - } - - #[test] - fn auto_claims_a_mempool_lockup_only_with_zero_conf() { - let record = record(BoltzSwapType::Reverse, None); - assert!(should_auto_claim(&record, "transaction.mempool", true)); - assert!(!should_auto_claim(&record, "transaction.mempool", false)); - } - - #[test] - fn does_not_auto_claim_before_lockup() { - let record = record(BoltzSwapType::Reverse, None); - assert!(!should_auto_claim(&record, "swap.created", false)); - assert!(!should_auto_claim(&record, "swap.created", true)); - } - - #[test] - fn does_not_auto_claim_an_already_claimed_swap() { - let record = record(BoltzSwapType::Reverse, Some("claim-txid".to_string())); - assert!(!should_auto_claim(&record, "transaction.confirmed", false)); - assert!(!should_auto_claim(&record, "transaction.mempool", true)); - assert!(!should_auto_claim(&record, "invoice.settled", false)); - } - - /// The cooperative claim flow discloses the preimage before broadcast, so a - /// settled invoice with no locally recorded claim txid means the claim may - /// never have reached the chain; it must be retried. - #[test] - fn auto_claims_a_settled_swap_without_a_local_claim_txid() { - let record = record(BoltzSwapType::Reverse, None); - assert!(should_auto_claim(&record, "invoice.settled", false)); - assert!(should_auto_claim(&record, "invoice.settled", true)); - } - - #[test] - fn does_not_auto_claim_a_submarine_swap() { - let record = record(BoltzSwapType::Submarine, None); - assert!(!should_auto_claim(&record, "transaction.confirmed", false)); - assert!(!should_auto_claim(&record, "transaction.mempool", true)); - assert!(!should_auto_claim(&record, "invoice.settled", false)); - } - - struct NoopListener; - - impl super::BoltzEventListener for NoopListener { - fn on_event(&self, _event: crate::modules::boltz::types::BoltzSwapEvent) {} - } - - /// Wait until the live-task gauge reaches `expected`. Aborted task futures - /// are dropped asynchronously by the runtime, so the count converges rather - /// than changing at the abort call itself. - async fn wait_for_live_tasks(expected: usize) { - use std::sync::atomic::Ordering; - for _ in 0..200 { - if super::LIVE_STREAM_TASKS.load(Ordering::SeqCst) == expected { - return; - } - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - } - panic!( - "expected {} live stream tasks, found {}", - expected, - super::LIVE_STREAM_TASKS.load(Ordering::SeqCst) - ); - } - - /// Concurrent starts must serialize the stream replacement: whichever start - /// wins, exactly one stream (three tasks) may remain, every superseded - /// stream's tasks must be aborted, and a stop must tear down the survivor. - /// This is the only test that touches the global stream slot, so it cannot - /// race with other tests. - #[tokio::test] - async fn concurrent_starts_leave_exactly_one_stream() { - use crate::modules::boltz::models::BoltzDB; - use std::sync::Arc; - - let dir = tempfile::tempdir().unwrap(); - let db = Arc::new( - BoltzDB::new(dir.path().join("boltz.db").to_str().unwrap()) - .await - .unwrap(), - ); - - let starts: Vec<_> = (0..4) - .map(|_| { - let db = db.clone(); - tokio::spawn(super::start_swap_updates( - db, - BoltzNetwork::Regtest, - Arc::new(NoopListener), - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), - None, - None, - false, - )) - }) - .collect(); - for start in starts { - start.await.unwrap().unwrap(); - } - - // All but the last-written stream must have been fully aborted. - wait_for_live_tasks(3).await; - assert!(super::updates_cell().lock().await.is_some()); - - super::stop_swap_updates().await; - wait_for_live_tasks(0).await; - assert!(super::updates_cell().lock().await.is_none()); - } -} diff --git a/src/modules/boltz/mod.rs b/src/modules/boltz/mod.rs deleted file mode 100644 index f6866ed8..00000000 --- a/src/modules/boltz/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod api; -mod claim; -mod client; -mod db; -mod errors; -mod guard; -mod listener; -mod models; -mod refund; -#[cfg(test)] -mod tests; -mod types; -mod validation; - -pub use api::{get_reverse_limits, get_submarine_limits}; -pub use claim::{claim_reverse_swap_guarded, ClaimOutcome}; -pub use errors::BoltzError; -pub use listener::{ - start_swap_updates, stop_swap_updates, subscribe_if_active, BoltzEventListener, -}; -pub use models::{BoltzDB, SwapRecord}; -pub use refund::refund_submarine_swap_guarded; -pub use types::*; diff --git a/src/modules/boltz/models.rs b/src/modules/boltz/models.rs deleted file mode 100644 index 331abd3e..00000000 --- a/src/modules/boltz/models.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::types::{BoltzNetwork, BoltzSwap, BoltzSwapStatus, BoltzSwapType}; -use boltz_client::swaps::boltz::{CreateReverseResponse, CreateSubmarineResponse}; -use boltz_client::util::secrets::{Preimage, SwapMasterKey}; -use boltz_client::Keypair; -use rusqlite::Connection; -use tokio::sync::Mutex; - -/// SQLite-backed store for Boltz swaps. -/// -/// Wraps a single connection behind an async mutex (mirroring the blocktank -/// module). No swap secrets are persisted: each swap's key and (for reverse -/// swaps) preimage are *re-derived on demand* from the wallet mnemonic and the -/// swap's deterministic [`SwapRecord::swap_index`] via BIP85 -/// ([`derive_swap_keypair`]). The database therefore holds no key material — -/// only the index needed to reconstruct it given the seed. -pub struct BoltzDB { - pub(crate) conn: Mutex, -} - -/// Re-derive a swap's secp256k1 keypair from the wallet mnemonic. -/// -/// Keys are derived via Boltz's BIP85 scheme: the wallet mnemonic yields a -/// per-wallet swap master key, and each swap uses a unique `index` under it -/// (`m/26589'/0'/0'/{index}`). The same `(mnemonic, passphrase, index)` always -/// reproduces the same key, so swaps are recoverable from the seed alone — the -/// derived swap mnemonic can also be registered with Boltz's rescue API. -/// -/// The key value is independent of `network`; the parameter is accepted for -/// symmetry with the rest of the API and to match how the swap was created. -pub(crate) fn derive_swap_keypair( - mnemonic: &str, - passphrase: Option<&str>, - network: BoltzNetwork, - index: u64, -) -> Result { - let master = - SwapMasterKey::new(mnemonic, passphrase, network.as_client_network()).map_err(|e| { - BoltzError::InvalidInput { - error_details: format!("Invalid mnemonic or key derivation failed: {}", e), - } - })?; - master.derive_swapkey(index).map_err(BoltzError::from) -} - -pub const CREATE_SWAPS_TABLE: &str = "CREATE TABLE IF NOT EXISTS swaps ( - id TEXT PRIMARY KEY, - swap_type TEXT NOT NULL, - status TEXT NOT NULL, - network TEXT NOT NULL, - electrum_url TEXT NOT NULL, - swap_index INTEGER NOT NULL, - invoice TEXT, - lockup_address TEXT, - onchain_address TEXT, - amount_sat INTEGER NOT NULL, - onchain_amount_sat INTEGER, - timeout_block_height INTEGER NOT NULL, - create_response_json TEXT NOT NULL, - claim_tx_id TEXT, - refund_tx_id TEXT, - created_at INTEGER NOT NULL -)"; - -/// Single-row counter table backing monotonic [`BoltzDB::reserve_swap_index`] -/// allocation. Keeping the counter in its own table (rather than `MAX(index)+1`) -/// guarantees an index is never reused even if a later swap creation fails after -/// the index was reserved. -pub const CREATE_META_TABLE: &str = "CREATE TABLE IF NOT EXISTS swap_meta ( - key TEXT PRIMARY KEY, - value INTEGER NOT NULL -)"; - -/// Current `boltz.db` schema version, written to `PRAGMA user_version` so future -/// changes have a migration anchor. -pub const SCHEMA_VERSION: i64 = 1; - -/// Internal, fully-detailed representation of a persisted swap, including -/// secrets. This is never exposed across the FFI boundary — use -/// [`SwapRecord::to_boltz_swap`] to produce the public [`BoltzSwap`]. -#[derive(Debug, Clone)] -pub struct SwapRecord { - pub id: String, - pub swap_type: BoltzSwapType, - /// Raw Boltz status string (mapped to [`BoltzSwapStatus`] on the way out). - pub status: String, - pub network: BoltzNetwork, - pub electrum_url: String, - /// Deterministic BIP85 derivation index for this swap's key (and, for - /// reverse swaps, its preimage). The secrets themselves are never stored; - /// they are re-derived from the wallet mnemonic and this index. - pub swap_index: u64, - pub invoice: Option, - pub lockup_address: Option, - pub onchain_address: Option, - pub amount_sat: u64, - pub onchain_amount_sat: Option, - pub timeout_block_height: u64, - /// Serialized `CreateSubmarineResponse` or `CreateReverseResponse`, used to - /// reconstruct the swap script for claims/refunds. - pub create_response_json: String, - pub claim_tx_id: Option, - pub refund_tx_id: Option, - pub created_at: u64, -} - -impl SwapRecord { - /// Re-derive the client keypair from the wallet mnemonic and this swap's - /// [`SwapRecord::swap_index`]. - pub fn keypair(&self, mnemonic: &str, passphrase: Option<&str>) -> Result { - derive_swap_keypair(mnemonic, passphrase, self.network, self.swap_index) - } - - /// Re-derive the preimage (reverse swaps only) from the swap key. The - /// preimage is `sha256(swap_private_key)`, so it is reproducible from the - /// seed without ever being stored. - pub fn preimage( - &self, - mnemonic: &str, - passphrase: Option<&str>, - ) -> Result { - let keypair = self.keypair(mnemonic, passphrase)?; - Ok(Preimage::from_swap_key(&keypair)) - } - - /// Deserialize the stored submarine swap creation response. - pub fn submarine_response(&self) -> Result { - serde_json::from_str(&self.create_response_json).map_err(BoltzError::from) - } - - /// Deserialize the stored reverse swap creation response. - pub fn reverse_response(&self) -> Result { - serde_json::from_str(&self.create_response_json).map_err(BoltzError::from) - } - - /// Whether the swap is complete from this wallet's perspective, i.e. no - /// further local action is possible or required. - /// - /// A terminal server status alone does not prove that: a reverse swap's - /// cooperative claim discloses the preimage to Boltz *before* the claim - /// transaction is broadcast, so Boltz can report `invoice.settled` while - /// our claim never made it onchain. Such a swap still holds claimable - /// funds and must stay recoverable until a claim txid is recorded locally. - pub fn is_locally_complete(&self) -> bool { - let status = BoltzSwapStatus::from_raw(&self.status); - if !status.is_terminal() { - return false; - } - if self.swap_type == BoltzSwapType::Reverse - && status == BoltzSwapStatus::InvoiceSettled - && self.claim_tx_id.is_none() - { - return false; - } - true - } - - /// Project to the public FFI type. - pub fn to_boltz_swap(&self) -> BoltzSwap { - BoltzSwap { - id: self.id.clone(), - swap_type: self.swap_type, - status: BoltzSwapStatus::from_raw(&self.status), - network: self.network, - swap_index: self.swap_index, - amount_sat: self.amount_sat, - onchain_amount_sat: self.onchain_amount_sat, - invoice: self.invoice.clone(), - lockup_address: self.lockup_address.clone(), - onchain_address: self.onchain_address.clone(), - timeout_block_height: self.timeout_block_height, - created_at: self.created_at, - claim_tx_id: self.claim_tx_id.clone(), - refund_tx_id: self.refund_tx_id.clone(), - } - } -} diff --git a/src/modules/boltz/refund.rs b/src/modules/boltz/refund.rs deleted file mode 100644 index 219adff2..00000000 --- a/src/modules/boltz/refund.rs +++ /dev/null @@ -1,108 +0,0 @@ -use crate::modules::boltz::claim::DEFAULT_FEERATE_SAT_PER_VB; -use crate::modules::boltz::client::{build_boltz_client, build_chain_client}; -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::guard::lock_swap; -use crate::modules::boltz::models::{BoltzDB, SwapRecord}; -use crate::modules::boltz::validation::{validate_fee_rate, validate_onchain_address}; -use boltz_client::swaps::{SwapScript, SwapTransactionParams, TransactionOptions}; -use boltz_client::util::fees::Fee; - -/// Refund a submarine swap, serialized against any other refund of the same swap. -/// -/// The counterpart to [`crate::modules::boltz::claim::claim_reverse_swap_guarded`]: -/// it holds the swap's lock across the read-broadcast-record sequence so two -/// concurrent recovery calls cannot both broadcast a refund. If the swap already -/// has a refund txid recorded, that txid is returned without re-broadcasting. -pub async fn refund_submarine_swap_guarded( - db: &BoltzDB, - swap_id: &str, - refund_address: String, - mnemonic: &str, - bip39_passphrase: Option<&str>, - fee_rate_sat_per_vb: Option, -) -> Result { - validate_fee_rate(fee_rate_sat_per_vb)?; - let _guard = lock_swap(swap_id).await; - - // Re-read under the lock: a concurrent refund may have completed while we waited. - let record = db - .get_swap(swap_id) - .await? - .ok_or_else(|| BoltzError::NotFound { - error_details: format!("Swap {} not found", swap_id), - })?; - if let Some(existing) = record.refund_tx_id { - return Ok(existing); - } - - // The refund destination receives the funds directly; reject a malformed - // or wrong-network address before any transaction is constructed. - let refund_address = validate_onchain_address(&refund_address, record.network)?; - - let txid = refund_submarine_swap( - &record, - refund_address.clone(), - mnemonic, - bip39_passphrase, - fee_rate_sat_per_vb, - ) - .await?; - db.set_refund_tx(swap_id, &txid, &refund_address).await?; - Ok(txid) -} - -/// Refund a submarine swap's locked onchain funds back to `refund_address`. -/// -/// Used when Boltz fails to pay the invoice (`invoice.failedToPay`), the wrong -/// amount was locked (`transaction.lockupFailed`), or the swap expired. A -/// cooperative refund is attempted first; if unavailable it falls back to the -/// script-path refund, which becomes spendable after the swap's onchain -/// timeout. Returns the broadcast refund transaction id. -pub async fn refund_submarine_swap( - record: &SwapRecord, - refund_address: String, - mnemonic: &str, - bip39_passphrase: Option<&str>, - fee_rate_sat_per_vb: Option, -) -> Result { - let submarine_resp = record.submarine_response()?; - let keypair = record.keypair(mnemonic, bip39_passphrase)?; - let our_pubkey = bitcoin::PublicKey::new(keypair.public_key()); - - let chain = record.network.as_chain(); - let swap_script = SwapScript::submarine_from_swap_resp(chain, &submarine_resp, our_pubkey)?; - let chain_client = build_chain_client(record.network, &record.electrum_url)?; - let boltz_client = build_boltz_client(record.network); - let fee = Fee::Relative(fee_rate_sat_per_vb.unwrap_or(DEFAULT_FEERATE_SAT_PER_VB)); - - let make_params = |cooperative: bool| SwapTransactionParams { - keys: keypair, - output_address: refund_address.clone(), - fee, - swap_id: record.id.clone(), - chain_client: &chain_client, - boltz_client: &boltz_client, - options: Some(TransactionOptions::default().with_cooperative(cooperative)), - }; - - // Prefer a cooperative refund; fall back to the timeout script path. - let tx = match swap_script.construct_refund(make_params(true)).await { - Ok(tx) => tx, - Err(coop_err) => swap_script - .construct_refund(make_params(false)) - .await - .map_err(|script_err| BoltzError::SwapError { - error_details: format!( - "Refund failed (cooperative: {}; script-path: {})", - coop_err, script_err - ), - })?, - }; - - chain_client - .broadcast_tx(&tx) - .await - .map_err(|e| BoltzError::BroadcastError { - error_details: format!("Failed to broadcast refund transaction: {}", e), - }) -} diff --git a/src/modules/boltz/tests.rs b/src/modules/boltz/tests.rs deleted file mode 100644 index 82724724..00000000 --- a/src/modules/boltz/tests.rs +++ /dev/null @@ -1,727 +0,0 @@ -#![allow(clippy::print_stdout)] - -use crate::modules::boltz::api::{get_reverse_limits, get_submarine_limits}; -use crate::modules::boltz::claim::{claim_reverse_swap_guarded, ClaimOutcome}; -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::guard::lock_swap; -use crate::modules::boltz::models::{derive_swap_keypair, BoltzDB, SwapRecord}; -use crate::modules::boltz::refund::refund_submarine_swap_guarded; -use crate::modules::boltz::types::{BoltzNetwork, BoltzSwapStatus, BoltzSwapType}; -use crate::modules::boltz::validation::{validate_reverse_response, validate_submarine_response}; -use bitcoin::absolute::LockTime; -use bitcoin::hashes::{hash160, sha256, Hash}; -use bitcoin::opcodes::all::{ - OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CLTV, OP_EQUALVERIFY, OP_HASH160, OP_SIZE, -}; -use bitcoin::script::Builder; -use boltz_client::network::BitcoinChain; -use boltz_client::swaps::bitcoin::BtcSwapScript; -use boltz_client::swaps::boltz::{ - CreateReverseResponse, CreateSubmarineResponse, Leaf, SwapTree, SwapType, -}; -use boltz_client::util::secrets::Preimage; -use lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret}; - -/// A throwaway BIP39 mnemonic used only by the offline tests. -const TEST_MNEMONIC: &str = - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - -fn sample_record(id: &str, swap_type: BoltzSwapType) -> SwapRecord { - SwapRecord { - id: id.to_string(), - swap_type, - status: "swap.created".to_string(), - network: BoltzNetwork::Testnet, - electrum_url: "ssl://electrum.example.com:50002".to_string(), - swap_index: 0, - invoice: Some("lnbc1...".to_string()), - lockup_address: Some("bc1qexample".to_string()), - onchain_address: Some("bc1qclaim".to_string()), - amount_sat: 100_000, - onchain_amount_sat: Some(99_000), - timeout_block_height: 800_000, - create_response_json: "{}".to_string(), - claim_tx_id: None, - refund_tx_id: None, - created_at: 1_700_000_000, - } -} - -/// Block height used for the fixture responses' refund timelock. -const FIXTURE_LOCKTIME: u32 = 800_000; - -/// Build a signed BOLT11 testnet invoice with the given payment hash and -/// amount. The signing key is arbitrary; the validators under test only read -/// the payment hash and amount. -fn build_test_invoice(payment_hash: sha256::Hash, amount_msat: u64) -> String { - let secp = bitcoin::secp256k1::Secp256k1::new(); - let key = bitcoin::secp256k1::SecretKey::from_slice(&[41; 32]).unwrap(); - InvoiceBuilder::new(Currency::BitcoinTestnet) - .description("fixture".to_string()) - .payment_hash(payment_hash) - .payment_secret(PaymentSecret([42; 32])) - .duration_since_epoch(std::time::Duration::from_secs(1_700_000_000)) - .min_final_cltv_expiry_delta(80) - .amount_milli_satoshis(amount_msat) - .build_signed(|hash| secp.sign_ecdsa_recoverable(hash, &key)) - .unwrap() - .to_string() -} - -fn x_only(pubkey: &bitcoin::PublicKey) -> bitcoin::secp256k1::XOnlyPublicKey { - pubkey.inner.x_only_public_key().0 -} - -/// Canonical reverse-swap claim leaf: the hashlock guards the preimage reveal, -/// the key is ours. -fn reverse_claim_leaf(hashlock: hash160::Hash, claim_pubkey: &bitcoin::PublicKey) -> Leaf { - let script = Builder::new() - .push_opcode(OP_SIZE) - .push_int(32) - .push_opcode(OP_EQUALVERIFY) - .push_opcode(OP_HASH160) - .push_slice(hashlock.to_byte_array()) - .push_opcode(OP_EQUALVERIFY) - .push_x_only_key(&x_only(claim_pubkey)) - .push_opcode(OP_CHECKSIG) - .into_script(); - Leaf { - output: format!("{:x}", script), - version: 192, - } -} - -/// Canonical submarine-swap claim leaf (Boltz's side of the script). -fn submarine_claim_leaf(hashlock: hash160::Hash, claim_pubkey: &bitcoin::PublicKey) -> Leaf { - let script = Builder::new() - .push_opcode(OP_HASH160) - .push_slice(hashlock.to_byte_array()) - .push_opcode(OP_EQUALVERIFY) - .push_x_only_key(&x_only(claim_pubkey)) - .push_opcode(OP_CHECKSIG) - .into_script(); - Leaf { - output: format!("{:x}", script), - version: 192, - } -} - -/// Canonical refund leaf, shared by both swap directions. -fn refund_leaf(refund_pubkey: &bitcoin::PublicKey) -> Leaf { - let script = Builder::new() - .push_x_only_key(&x_only(refund_pubkey)) - .push_opcode(OP_CHECKSIGVERIFY) - .push_lock_time(LockTime::from_consensus(FIXTURE_LOCKTIME)) - .push_opcode(OP_CLTV) - .into_script(); - Leaf { - output: format!("{:x}", script), - version: 192, - } -} - -/// The taproot lockup address the given script terms commit to. Fixtures use -/// this to build *internally consistent* responses (script and address match, -/// so `boltz-client`'s own validation passes) whose terms are nonetheless -/// wrong for the swap, which is exactly the malicious-server shape the extra -/// validation must catch. -fn taproot_address_for( - swap_type: SwapType, - hashlock: hash160::Hash, - receiver: &bitcoin::PublicKey, - sender: &bitcoin::PublicKey, -) -> String { - let script = BtcSwapScript { - swap_type, - side: None, - funding_addrs: None, - hashlock, - receiver_pubkey: *receiver, - locktime: LockTime::from_consensus(FIXTURE_LOCKTIME), - sender_pubkey: *sender, - }; - script - .to_address(BitcoinChain::BitcoinTestnet) - .unwrap() - .to_string() -} - -/// A reverse-swap creation response whose script hashlock is `hashlock` and -/// whose invoice carries `invoice_msat`. Script, tree and lockup address are -/// kept consistent with each other so only the terms under test vary. -fn reverse_response_fixture( - hashlock: hash160::Hash, - our_pubkey: &bitcoin::PublicKey, - boltz_pubkey: &bitcoin::PublicKey, - payment_hash: sha256::Hash, - invoice_msat: u64, -) -> CreateReverseResponse { - CreateReverseResponse { - id: "rev-fixture".to_string(), - invoice: Some(build_test_invoice(payment_hash, invoice_msat)), - swap_tree: SwapTree { - claim_leaf: reverse_claim_leaf(hashlock, our_pubkey), - refund_leaf: refund_leaf(boltz_pubkey), - }, - lockup_address: taproot_address_for( - SwapType::ReverseSubmarine, - hashlock, - our_pubkey, - boltz_pubkey, - ), - refund_public_key: *boltz_pubkey, - timeout_block_height: FIXTURE_LOCKTIME, - onchain_amount: 99_000, - blinding_key: None, - } -} - -/// A submarine-swap creation response whose script hashlock is `hashlock`. -fn submarine_response_fixture( - hashlock: hash160::Hash, - our_pubkey: &bitcoin::PublicKey, - boltz_pubkey: &bitcoin::PublicKey, -) -> CreateSubmarineResponse { - let address = taproot_address_for(SwapType::Submarine, hashlock, boltz_pubkey, our_pubkey); - CreateSubmarineResponse { - accept_zero_conf: false, - bip21: format!("bitcoin:{}?amount=0.001", address), - address, - claim_public_key: *boltz_pubkey, - expected_amount: 100_000, - id: "sub-fixture".to_string(), - referral_id: None, - swap_tree: SwapTree { - claim_leaf: submarine_claim_leaf(hashlock, boltz_pubkey), - refund_leaf: refund_leaf(our_pubkey), - }, - timeout_block_height: FIXTURE_LOCKTIME as u64, - blinding_key: None, - } -} - -/// Our claim keypair/preimage plus a distinct "Boltz" pubkey for fixtures. -fn fixture_keys() -> (bitcoin::PublicKey, Preimage, bitcoin::PublicKey) { - let keypair = derive_swap_keypair(TEST_MNEMONIC, None, BoltzNetwork::Testnet, 0).unwrap(); - let our_pubkey = bitcoin::PublicKey::new(keypair.public_key()); - let preimage = Preimage::from_swap_key(&keypair); - let boltz_keypair = derive_swap_keypair(TEST_MNEMONIC, None, BoltzNetwork::Testnet, 1).unwrap(); - let boltz_pubkey = bitcoin::PublicKey::new(boltz_keypair.public_key()); - (our_pubkey, preimage, boltz_pubkey) -} - -#[test] -fn reverse_response_with_consistent_terms_validates() { - let (our_pubkey, preimage, boltz_pubkey) = fixture_keys(); - let amount_sat = 100_000; - let response = reverse_response_fixture( - preimage.hash160, - &our_pubkey, - &boltz_pubkey, - preimage.sha256, - amount_sat * 1000, - ); - validate_reverse_response( - &response, - &preimage, - &our_pubkey, - amount_sat, - BoltzNetwork::Testnet, - ) - .expect("a consistent response must validate"); -} - -#[test] -fn reverse_response_with_mismatched_script_hashlock_is_rejected() { - let (our_pubkey, preimage, boltz_pubkey) = fixture_keys(); - let amount_sat = 100_000; - // Script and address agree with each other (so the address check passes), - // but the hashlock is not our preimage's: our script-path claim would be - // unspendable, leaving the funds claimable only if Boltz cooperates. - let wrong_hashlock = hash160::Hash::hash(b"not our preimage"); - let response = reverse_response_fixture( - wrong_hashlock, - &our_pubkey, - &boltz_pubkey, - preimage.sha256, - amount_sat * 1000, - ); - let err = validate_reverse_response( - &response, - &preimage, - &our_pubkey, - amount_sat, - BoltzNetwork::Testnet, - ) - .unwrap_err(); - assert!( - err.to_string().contains("hashlock"), - "unexpected error: {}", - err - ); -} - -#[test] -fn reverse_response_with_mismatched_invoice_amount_is_rejected() { - let (our_pubkey, preimage, boltz_pubkey) = fixture_keys(); - let amount_sat = 100_000; - // The invoice commits to our preimage but asks for more than requested. - let response = reverse_response_fixture( - preimage.hash160, - &our_pubkey, - &boltz_pubkey, - preimage.sha256, - (amount_sat + 1) * 1000, - ); - let err = validate_reverse_response( - &response, - &preimage, - &our_pubkey, - amount_sat, - BoltzNetwork::Testnet, - ) - .unwrap_err(); - assert!( - err.to_string().contains("invoice amount"), - "unexpected error: {}", - err - ); -} - -#[test] -fn submarine_response_with_consistent_terms_validates() { - let (our_pubkey, _, boltz_pubkey) = fixture_keys(); - let payment_hash = sha256::Hash::hash(b"submarine payment preimage"); - let invoice = build_test_invoice(payment_hash, 100_000_000); - // The correct hashlock is hash160(preimage) = ripemd160(payment_hash). - let hashlock = Preimage::from_sha256_str(&payment_hash.to_string()) - .unwrap() - .hash160; - let response = submarine_response_fixture(hashlock, &our_pubkey, &boltz_pubkey); - validate_submarine_response(&response, &invoice, &our_pubkey, BoltzNetwork::Testnet) - .expect("a consistent response must validate"); -} - -#[test] -fn submarine_response_with_mismatched_script_hashlock_is_rejected() { - let (our_pubkey, _, boltz_pubkey) = fixture_keys(); - let payment_hash = sha256::Hash::hash(b"submarine payment preimage"); - let invoice = build_test_invoice(payment_hash, 100_000_000); - // Consistent script/address pair whose hashlock does not match the - // invoice: Boltz could claim the lockup with its own preimage without - // ever paying the invoice. - let wrong_hashlock = hash160::Hash::hash(b"a preimage boltz controls"); - let response = submarine_response_fixture(wrong_hashlock, &our_pubkey, &boltz_pubkey); - let err = validate_submarine_response(&response, &invoice, &our_pubkey, BoltzNetwork::Testnet) - .unwrap_err(); - assert!( - err.to_string().contains("hashlock"), - "unexpected error: {}", - err - ); -} - -#[test] -fn status_round_trips_through_raw() { - let cases = [ - ("swap.created", BoltzSwapStatus::SwapCreated), - ("transaction.mempool", BoltzSwapStatus::TransactionMempool), - ( - "transaction.claim.pending", - BoltzSwapStatus::TransactionClaimPending, - ), - ("invoice.failedToPay", BoltzSwapStatus::InvoiceFailedToPay), - ("transaction.claimed", BoltzSwapStatus::TransactionClaimed), - ]; - for (raw, expected) in cases { - let mapped = BoltzSwapStatus::from_raw(raw); - assert_eq!(mapped, expected); - assert_eq!(mapped.as_raw(), raw); - } -} - -#[test] -fn unknown_status_preserves_raw() { - let mapped = BoltzSwapStatus::from_raw("some.future.state"); - assert_eq!( - mapped, - BoltzSwapStatus::Unknown { - raw: "some.future.state".to_string() - } - ); - assert_eq!(mapped.as_raw(), "some.future.state"); -} - -#[test] -fn terminal_states_are_terminal() { - assert!(BoltzSwapStatus::TransactionClaimed.is_terminal()); - assert!(BoltzSwapStatus::TransactionRefunded.is_terminal()); - assert!(BoltzSwapStatus::SwapExpired.is_terminal()); - assert!(!BoltzSwapStatus::TransactionMempool.is_terminal()); - assert!(!BoltzSwapStatus::SwapCreated.is_terminal()); -} - -#[test] -fn derivation_is_deterministic_and_index_unique() { - // Same (mnemonic, index) reproduces the same key and preimage — the - // property that makes swaps recoverable from the seed alone. - let k0a = derive_swap_keypair(TEST_MNEMONIC, None, BoltzNetwork::Mainnet, 0).unwrap(); - let k0b = derive_swap_keypair(TEST_MNEMONIC, None, BoltzNetwork::Mainnet, 0).unwrap(); - assert_eq!(k0a.secret_bytes(), k0b.secret_bytes()); - assert_eq!( - Preimage::from_swap_key(&k0a).bytes, - Preimage::from_swap_key(&k0b).bytes - ); - - // Different indices yield different keys (no key reuse across swaps). - let k1 = derive_swap_keypair(TEST_MNEMONIC, None, BoltzNetwork::Mainnet, 1).unwrap(); - assert_ne!(k0a.secret_bytes(), k1.secret_bytes()); - - // A different passphrase yields a different key (passphrase must match). - let k0p = derive_swap_keypair(TEST_MNEMONIC, Some("pass"), BoltzNetwork::Mainnet, 0).unwrap(); - assert_ne!(k0a.secret_bytes(), k0p.secret_bytes()); -} - -#[test] -fn rejects_invalid_mnemonic() { - assert!(derive_swap_keypair("not a valid mnemonic", None, BoltzNetwork::Mainnet, 0).is_err()); -} - -#[tokio::test] -async fn reserve_swap_index_is_monotonic_and_persists() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - - assert_eq!(db.reserve_swap_index().await.unwrap(), 0); - assert_eq!(db.reserve_swap_index().await.unwrap(), 1); - assert_eq!(db.reserve_swap_index().await.unwrap(), 2); - drop(db); - - // The counter survives reopening the database — no index is ever reused. - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - assert_eq!(db.reserve_swap_index().await.unwrap(), 3); -} - -#[tokio::test] -async fn db_round_trip_and_recovery() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - - let mut reverse = sample_record("rev-1", BoltzSwapType::Reverse); - reverse.swap_index = 3; - let submarine = sample_record("sub-1", BoltzSwapType::Submarine); - db.insert_swap(&reverse).await.unwrap(); - db.insert_swap(&submarine).await.unwrap(); - - // get_swap reconstructs the record faithfully (no secrets are stored, only - // the derivation index needed to reconstruct them from the seed). - let loaded = db.get_swap("rev-1").await.unwrap().expect("swap exists"); - assert_eq!(loaded.swap_type, BoltzSwapType::Reverse); - assert_eq!(loaded.swap_index, 3); - assert_eq!(loaded.amount_sat, 100_000); - assert_eq!(loaded.onchain_amount_sat, Some(99_000)); - - // Both swaps are pending until they reach a terminal state. - assert_eq!(db.list_swaps().await.unwrap().len(), 2); - assert_eq!(db.list_pending_swaps().await.unwrap().len(), 2); - - // A non-terminal status persists and leaves the swap in the pending set. - db.update_status("rev-1", "transaction.mempool") - .await - .unwrap(); - let loaded = db.get_swap("rev-1").await.unwrap().unwrap(); - assert_eq!(loaded.status, "transaction.mempool"); - assert_eq!(db.list_pending_swaps().await.unwrap().len(), 2); - - // Recording the claim tx id stores it and advances the swap to the terminal - // claimed status, which drops it from the pending set. - db.set_claim_tx("rev-1", "abc123").await.unwrap(); - let loaded = db.get_swap("rev-1").await.unwrap().unwrap(); - assert_eq!(loaded.status, "transaction.claimed"); - assert_eq!(loaded.claim_tx_id, Some("abc123".to_string())); - let pending = db.list_pending_swaps().await.unwrap(); - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].id, "sub-1"); - - // Recording the refund tx id does the same for the submarine swap, and - // persists the refund destination as the swap's onchain address. - db.set_refund_tx("sub-1", "def456", "bc1qrefund") - .await - .unwrap(); - let loaded = db.get_swap("sub-1").await.unwrap().unwrap(); - assert_eq!(loaded.status, "transaction.refunded"); - assert_eq!(loaded.refund_tx_id, Some("def456".to_string())); - assert_eq!(loaded.onchain_address, Some("bc1qrefund".to_string())); - assert!(db.list_pending_swaps().await.unwrap().is_empty()); - - // Terminal swaps are still listed, just no longer pending. - assert_eq!(db.list_swaps().await.unwrap().len(), 2); -} - -#[tokio::test] -async fn settled_reverse_swap_without_local_claim_stays_recoverable() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - - let record = sample_record("rev-settled", BoltzSwapType::Reverse); - db.insert_swap(&record).await.unwrap(); - - // Boltz reporting the invoice settled proves the Lightning leg, not our - // onchain claim: the cooperative flow discloses the preimage before - // broadcast, so the claim tx may never have made it out. Without a local - // claim txid the swap must stay recoverable. - db.update_status("rev-settled", "invoice.settled") - .await - .unwrap(); - let pending = db.list_pending_swaps().await.unwrap(); - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].id, "rev-settled"); - - // Once the claim txid is recorded locally, the swap is complete. - db.set_claim_tx("rev-settled", "claim-txid").await.unwrap(); - assert!(db.list_pending_swaps().await.unwrap().is_empty()); -} - -#[tokio::test] -async fn local_completion_survives_late_server_updates() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - - let reverse = sample_record("rev-1", BoltzSwapType::Reverse); - let submarine = sample_record("sub-1", BoltzSwapType::Submarine); - db.insert_swap(&reverse).await.unwrap(); - db.insert_swap(&submarine).await.unwrap(); - - // A delayed or re-ordered server status (WebSocket lag, reconcile) must - // not regress a locally recorded completion. - db.set_claim_tx("rev-1", "claim-txid").await.unwrap(); - db.update_status("rev-1", "transaction.confirmed") - .await - .unwrap(); - let loaded = db.get_swap("rev-1").await.unwrap().unwrap(); - assert_eq!(loaded.status, "transaction.claimed"); - assert_eq!(loaded.claim_tx_id, Some("claim-txid".to_string())); - - db.set_refund_tx("sub-1", "refund-txid", "bc1qrefund") - .await - .unwrap(); - db.update_status("sub-1", "invoice.failedToPay") - .await - .unwrap(); - let loaded = db.get_swap("sub-1").await.unwrap().unwrap(); - assert_eq!(loaded.status, "transaction.refunded"); - assert_eq!(loaded.refund_tx_id, Some("refund-txid".to_string())); -} - -#[tokio::test] -async fn guarded_claim_returns_recorded_txid_without_rebroadcasting() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - - let mut record = sample_record("rev-claimed", BoltzSwapType::Reverse); - record.claim_tx_id = Some("already-broadcast".to_string()); - db.insert_swap(&record).await.unwrap(); - - // The record's electrum_url is unreachable, so reaching the broadcast path at - // all would fail the call rather than return the recorded txid. - let outcome = claim_reverse_swap_guarded(&db, "rev-claimed", TEST_MNEMONIC, None, None) - .await - .unwrap(); - assert!(matches!(outcome, ClaimOutcome::AlreadyClaimed(_))); - assert_eq!(outcome.txid(), "already-broadcast"); -} - -#[tokio::test] -async fn guarded_refund_returns_recorded_txid_without_rebroadcasting() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - - let mut record = sample_record("sub-refunded", BoltzSwapType::Submarine); - record.refund_tx_id = Some("already-refunded".to_string()); - db.insert_swap(&record).await.unwrap(); - - let txid = refund_submarine_swap_guarded( - &db, - "sub-refunded", - "bc1qrefund".to_string(), - TEST_MNEMONIC, - None, - None, - ) - .await - .unwrap(); - assert_eq!(txid, "already-refunded"); -} - -#[tokio::test] -async fn guarded_claim_reports_a_missing_swap() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - - let err = claim_reverse_swap_guarded(&db, "nope", TEST_MNEMONIC, None, None) - .await - .unwrap_err(); - assert!(matches!(err, BoltzError::NotFound { .. })); -} - -/// The lock is what stops an auto-claim and a manual claim of the same swap from -/// both broadcasting, so assert it actually excludes: holders of one swap's lock -/// never overlap, while a different swap's lock stays free. -#[tokio::test] -async fn swap_lock_excludes_per_swap_and_not_across_swaps() { - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - - let live = Arc::new(AtomicUsize::new(0)); - let peak = Arc::new(AtomicUsize::new(0)); - - let mut handles = Vec::new(); - for _ in 0..4 { - let live = live.clone(); - let peak = peak.clone(); - handles.push(tokio::spawn(async move { - let _guard = lock_swap("contended").await; - let in_section = live.fetch_add(1, Ordering::SeqCst) + 1; - peak.fetch_max(in_section, Ordering::SeqCst); - // Yield while holding the lock: a claim awaits here, and this is - // exactly where an unguarded second caller would slip through. - tokio::task::yield_now().await; - live.fetch_sub(1, Ordering::SeqCst); - })); - } - for handle in handles { - handle.await.unwrap(); - } - assert_eq!( - peak.load(Ordering::SeqCst), - 1, - "two claims of the same swap must never run concurrently" - ); - - // Holding one swap's lock must not stall an unrelated swap. - let held = lock_swap("swap-a").await; - let other = tokio::time::timeout(std::time::Duration::from_secs(5), lock_swap("swap-b")).await; - assert!(other.is_ok(), "locks must be independent across swaps"); - drop(held); -} - -/// Returns true if a Boltz API error indicates the endpoint is temporarily -/// unreachable (gateway/timeout), so the live test can skip rather than fail. -fn api_unavailable(err: &crate::modules::boltz::BoltzError) -> bool { - let msg = err.to_string(); - msg.contains("502") - || msg.contains("503") - || msg.contains("504") - || msg.contains("Bad Gateway") - || msg.contains("timed out") - || msg.contains("error sending request") -} - -/// Live end-to-end test against the Boltz API. Ignored by default (needs -/// network); run with: -/// `cargo test modules::boltz -- --ignored --nocapture` -/// -/// Targets testnet by default; set `BOLTZ_LIVE_NETWORK=mainnet` (or `regtest`) -/// to override. A reverse swap is ideal for an unattended live test: Boltz -/// generates the hold invoice itself, so no Lightning node or onchain funds are -/// required, and an unpaid swap simply expires. The test asserts the -/// locally-derived redeem script and invoice match what Boltz returned — the -/// core cryptographic guarantee that a later claim/refund will be valid. If the -/// endpoint is temporarily down it skips instead of failing. -#[tokio::test] -#[ignore = "hits the live Boltz API"] -async fn live_reverse_swap() { - let network = match std::env::var("BOLTZ_LIVE_NETWORK").as_deref() { - Ok("mainnet") => BoltzNetwork::Mainnet, - Ok("regtest") => BoltzNetwork::Regtest, - _ => BoltzNetwork::Testnet, - }; - println!("live test network: {}", network.as_str()); - - // 1. Real fees/limits round-trip for both directions. - let reverse = match get_reverse_limits(network).await { - Ok(limits) => limits, - Err(e) if api_unavailable(&e) => { - println!("SKIP: Boltz API unavailable ({e})"); - return; - } - Err(e) => panic!("fetch reverse limits: {e:?}"), - }; - let submarine = get_submarine_limits(network) - .await - .expect("fetch submarine limits"); - println!( - "submarine: min={} max={} fee%={}", - submarine.minimal_sat, submarine.maximal_sat, submarine.fee_percentage - ); - println!( - "reverse: min={} max={} fee%={}", - reverse.minimal_sat, reverse.maximal_sat, reverse.fee_percentage - ); - assert!(reverse.maximal_sat > reverse.minimal_sat); - - // 2. Create a real reverse swap at the minimum amount. - let dir = tempfile::tempdir().unwrap(); - let db = BoltzDB::new(dir.path().join("boltz.db").to_str().unwrap()) - .await - .unwrap(); - // A valid address for the selected network, only stored locally as the - // claim destination, not sent to Boltz (we never broadcast in this test). - let claim_address = match network { - BoltzNetwork::Mainnet => "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", - BoltzNetwork::Testnet => "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", - BoltzNetwork::Regtest => "bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080", - } - .to_string(); - let response = db - .create_reverse_swap( - network, - "ssl://blockstream.info:993".to_string(), - reverse.minimal_sat, - claim_address, - TEST_MNEMONIC.to_string(), - None, - ) - .await - .expect("create reverse swap"); - - println!("swap id: {}", response.id); - println!("hold invoice: {}", response.invoice); - println!("lockup address: {}", response.lockup_address); - assert!( - response.invoice.starts_with("lnbc") || response.invoice.starts_with("lntb"), - "expected a BOLT11 invoice, got: {}", - response.invoice - ); - assert!(!response.lockup_address.is_empty()); - - // 3. Reconstruct the swap from persisted secrets and verify the invoice's - // payment hash and the redeem script match — proving our claim key, - // preimage and locally-built swap script are correct. - let record = db.get_swap(&response.id).await.unwrap().expect("persisted"); - let keypair = record.keypair(TEST_MNEMONIC, None).unwrap(); - let preimage = record.preimage(TEST_MNEMONIC, None).unwrap(); - let our_pubkey = bitcoin::PublicKey::new(keypair.public_key()); - let reverse_resp = record.reverse_response().unwrap(); - reverse_resp - .validate(&preimage, &our_pubkey, network.as_chain()) - .expect("invoice + redeem script validate against our keys"); - - // The swap is left to expire on testnet; nothing is broadcast. - println!("reverse swap created and cryptographically validated ✅"); -} - -#[tokio::test] -async fn get_missing_swap_returns_none() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("boltz.db"); - let db = BoltzDB::new(path.to_str().unwrap()).await.unwrap(); - assert!(db.get_swap("nope").await.unwrap().is_none()); -} diff --git a/src/modules/boltz/types.rs b/src/modules/boltz/types.rs deleted file mode 100644 index cc38757c..00000000 --- a/src/modules/boltz/types.rs +++ /dev/null @@ -1,290 +0,0 @@ -use boltz_client::network::{BitcoinChain, Chain, Network as BoltzClientNetwork}; -use serde::{Deserialize, Serialize}; - -/// Bitcoin network selection for Boltz swaps. Maps to the networks Boltz -/// operates on (mainnet, testnet, regtest). -#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum, Serialize, Deserialize)] -pub enum BoltzNetwork { - Mainnet, - Testnet, - Regtest, -} - -impl BoltzNetwork { - pub(crate) fn as_client_network(self) -> BoltzClientNetwork { - match self { - BoltzNetwork::Mainnet => BoltzClientNetwork::Mainnet, - BoltzNetwork::Testnet => BoltzClientNetwork::Testnet, - BoltzNetwork::Regtest => BoltzClientNetwork::Regtest, - } - } - - pub(crate) fn as_bitcoin_chain(self) -> BitcoinChain { - match self { - BoltzNetwork::Mainnet => BitcoinChain::Bitcoin, - BoltzNetwork::Testnet => BitcoinChain::BitcoinTestnet, - BoltzNetwork::Regtest => BitcoinChain::BitcoinRegtest, - } - } - - pub(crate) fn as_chain(self) -> Chain { - Chain::Bitcoin(self.as_bitcoin_chain()) - } - - pub(crate) fn as_bitcoin_network(self) -> bitcoin::Network { - match self { - BoltzNetwork::Mainnet => bitcoin::Network::Bitcoin, - BoltzNetwork::Testnet => bitcoin::Network::Testnet, - BoltzNetwork::Regtest => bitcoin::Network::Regtest, - } - } - - pub(crate) fn as_str(self) -> &'static str { - match self { - BoltzNetwork::Mainnet => "mainnet", - BoltzNetwork::Testnet => "testnet", - BoltzNetwork::Regtest => "regtest", - } - } - - pub(crate) fn from_str(s: &str) -> Option { - match s { - "mainnet" => Some(BoltzNetwork::Mainnet), - "testnet" => Some(BoltzNetwork::Testnet), - "regtest" => Some(BoltzNetwork::Regtest), - _ => None, - } - } -} - -/// The direction of a Boltz swap. -/// -/// - `Submarine`: onchain Bitcoin -> Lightning (the user locks onchain funds, -/// Boltz pays a Lightning invoice). -/// - `Reverse`: Lightning -> onchain Bitcoin (the user pays a Boltz hold -/// invoice, Boltz locks onchain funds the user then claims). -#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum, Serialize, Deserialize)] -pub enum BoltzSwapType { - Submarine, - Reverse, -} - -impl BoltzSwapType { - pub(crate) fn as_str(self) -> &'static str { - match self { - BoltzSwapType::Submarine => "submarine", - BoltzSwapType::Reverse => "reverse", - } - } - - pub(crate) fn from_str(s: &str) -> Option { - match s { - "submarine" => Some(BoltzSwapType::Submarine), - "reverse" => Some(BoltzSwapType::Reverse), - _ => None, - } - } -} - -/// Typed view of the Boltz swap lifecycle. `Unknown` carries the raw status so -/// new server-side states don't break the bindings. -/// -/// See . -#[derive(Debug, Clone, PartialEq, Eq, uniffi::Enum, Serialize, Deserialize)] -pub enum BoltzSwapStatus { - /// `swap.created` — initial state. - SwapCreated, - /// `invoice.set` — invoice attached to a submarine swap. - InvoiceSet, - /// `transaction.mempool` — a lockup transaction is in the mempool. - TransactionMempool, - /// `transaction.confirmed` — a lockup transaction confirmed. - TransactionConfirmed, - /// `invoice.pending` — Boltz is paying the submarine swap invoice. - InvoicePending, - /// `invoice.paid` — submarine swap invoice paid by Boltz. - InvoicePaid, - /// `invoice.settled` — reverse swap invoice settled (preimage revealed). - InvoiceSettled, - /// `invoice.failedToPay` — submarine swap invoice could not be paid; refund. - InvoiceFailedToPay, - /// `invoice.expired` — reverse swap invoice expired before payment. - InvoiceExpired, - /// `transaction.claim.pending` — Boltz ready for a cooperative claim. - TransactionClaimPending, - /// `transaction.claimed` — onchain funds claimed. - TransactionClaimed, - /// `transaction.refunded` — onchain funds refunded. - TransactionRefunded, - /// `transaction.lockupFailed` — wrong amount locked; can refund. - TransactionLockupFailed, - /// `transaction.failed` — Boltz failed to lock the agreed funds. - TransactionFailed, - /// `swap.expired` — swap expired without completing. - SwapExpired, - /// Any status not yet modelled. `raw` holds the verbatim Boltz status. - Unknown { raw: String }, -} - -impl BoltzSwapStatus { - /// Map a raw Boltz status string to a typed status. - pub fn from_raw(raw: &str) -> BoltzSwapStatus { - match raw { - "swap.created" => BoltzSwapStatus::SwapCreated, - "invoice.set" => BoltzSwapStatus::InvoiceSet, - "transaction.mempool" => BoltzSwapStatus::TransactionMempool, - "transaction.confirmed" => BoltzSwapStatus::TransactionConfirmed, - "invoice.pending" => BoltzSwapStatus::InvoicePending, - "invoice.paid" => BoltzSwapStatus::InvoicePaid, - "invoice.settled" => BoltzSwapStatus::InvoiceSettled, - "invoice.failedToPay" => BoltzSwapStatus::InvoiceFailedToPay, - "invoice.expired" => BoltzSwapStatus::InvoiceExpired, - "transaction.claim.pending" => BoltzSwapStatus::TransactionClaimPending, - "transaction.claimed" => BoltzSwapStatus::TransactionClaimed, - "transaction.refunded" => BoltzSwapStatus::TransactionRefunded, - "transaction.lockupFailed" => BoltzSwapStatus::TransactionLockupFailed, - "transaction.failed" => BoltzSwapStatus::TransactionFailed, - "swap.expired" => BoltzSwapStatus::SwapExpired, - other => BoltzSwapStatus::Unknown { - raw: other.to_string(), - }, - } - } - - /// The raw Boltz status string this typed status was derived from. - pub fn as_raw(&self) -> String { - match self { - BoltzSwapStatus::SwapCreated => "swap.created".to_string(), - BoltzSwapStatus::InvoiceSet => "invoice.set".to_string(), - BoltzSwapStatus::TransactionMempool => "transaction.mempool".to_string(), - BoltzSwapStatus::TransactionConfirmed => "transaction.confirmed".to_string(), - BoltzSwapStatus::InvoicePending => "invoice.pending".to_string(), - BoltzSwapStatus::InvoicePaid => "invoice.paid".to_string(), - BoltzSwapStatus::InvoiceSettled => "invoice.settled".to_string(), - BoltzSwapStatus::InvoiceFailedToPay => "invoice.failedToPay".to_string(), - BoltzSwapStatus::InvoiceExpired => "invoice.expired".to_string(), - BoltzSwapStatus::TransactionClaimPending => "transaction.claim.pending".to_string(), - BoltzSwapStatus::TransactionClaimed => "transaction.claimed".to_string(), - BoltzSwapStatus::TransactionRefunded => "transaction.refunded".to_string(), - BoltzSwapStatus::TransactionLockupFailed => "transaction.lockupFailed".to_string(), - BoltzSwapStatus::TransactionFailed => "transaction.failed".to_string(), - BoltzSwapStatus::SwapExpired => "swap.expired".to_string(), - BoltzSwapStatus::Unknown { raw } => raw.clone(), - } - } - - /// Whether the swap has reached a terminal state and no further action is - /// possible or required. - pub fn is_terminal(&self) -> bool { - matches!( - self, - BoltzSwapStatus::TransactionClaimed - | BoltzSwapStatus::TransactionRefunded - | BoltzSwapStatus::InvoiceSettled - | BoltzSwapStatus::TransactionFailed - | BoltzSwapStatus::InvoiceExpired - | BoltzSwapStatus::SwapExpired - ) - } -} - -/// Fees and limits for a swap pair, used to size a swap and present costs. -#[derive(Debug, Clone, uniffi::Record)] -pub struct BoltzPairInfo { - /// Pair hash identifying the current terms (passed back to Boltz if needed). - pub hash: String, - /// Exchange rate of the pair. - pub rate: f64, - /// Minimum swap amount in satoshis. - pub minimal_sat: u64, - /// Maximum swap amount in satoshis. - pub maximal_sat: u64, - /// Boltz service fee as a percentage of the swap amount. - pub fee_percentage: f64, - /// Estimated absolute miner fees in satoshis. - pub miner_fees_sat: u64, -} - -/// Result of creating a submarine swap (onchain -> Lightning). -/// -/// The caller funds `address` with `expected_amount_sat` from its onchain -/// wallet; Boltz then pays the Lightning invoice supplied at creation. -#[derive(Debug, Clone, uniffi::Record)] -pub struct SubmarineSwapResponse { - pub id: String, - /// Onchain lockup address to send funds to. - pub address: String, - /// BIP21 URI for the lockup payment. - pub bip21: String, - /// Exact amount in satoshis the caller must send to `address`. - pub expected_amount_sat: u64, - /// Whether Boltz will accept a zero-conf lockup. - pub accept_zero_conf: bool, - /// Onchain timeout height after which a refund is possible. - pub timeout_block_height: u64, -} - -/// Result of creating a reverse swap (Lightning -> onchain). -/// -/// The caller pays `invoice` from its Lightning node; once Boltz locks funds at -/// `lockup_address`, the module claims them to the provided onchain address. -#[derive(Debug, Clone, uniffi::Record)] -pub struct ReverseSwapResponse { - pub id: String, - /// Hold invoice the caller must pay via Lightning. - pub invoice: String, - /// Address Boltz locks the onchain funds to. - pub lockup_address: String, - /// Amount in satoshis that will be received onchain (after Boltz fees). - pub onchain_amount_sat: u64, - /// Onchain timeout height for the swap. - pub timeout_block_height: u64, -} - -/// A persisted swap and its current state, returned by the listing/query APIs. -#[derive(Debug, Clone, uniffi::Record)] -pub struct BoltzSwap { - pub id: String, - pub swap_type: BoltzSwapType, - pub status: BoltzSwapStatus, - pub network: BoltzNetwork, - /// Deterministic BIP85 index used to derive this swap's key and preimage - /// from the wallet seed. The recovery handle: given the seed and this index - /// (or by scanning indices), the swap's secrets can be reconstructed. - pub swap_index: u64, - /// For submarine swaps: the amount to lock onchain. For reverse swaps: the - /// Lightning invoice amount. - pub amount_sat: u64, - /// For reverse swaps: the onchain amount that will be received. - pub onchain_amount_sat: Option, - /// Lightning invoice associated with the swap (the hold invoice for reverse - /// swaps, the invoice Boltz pays for submarine swaps). - pub invoice: Option, - /// Onchain lockup address. - pub lockup_address: Option, - /// The address funds are claimed to (reverse) or refunded to (submarine). - pub onchain_address: Option, - pub timeout_block_height: u64, - pub created_at: u64, - /// Txid of the claim transaction once broadcast (reverse swaps). - pub claim_tx_id: Option, - /// Txid of the refund transaction once broadcast (submarine swaps). - pub refund_tx_id: Option, -} - -/// Events emitted to a registered [`crate::modules::boltz::BoltzEventListener`] -/// as swaps progress through their lifecycle. -#[derive(Debug, Clone, uniffi::Enum)] -pub enum BoltzSwapEvent { - /// The swap transitioned to a new status. - StatusUpdate { - swap_id: String, - status: BoltzSwapStatus, - }, - /// A reverse swap was claimed onchain. `txid` is the claim transaction. - Claimed { swap_id: String, txid: String }, - /// A submarine swap was refunded onchain. `txid` is the refund transaction. - Refunded { swap_id: String, txid: String }, - /// An error occurred while processing the swap (e.g. an auto-claim failed). - Error { swap_id: String, message: String }, -} diff --git a/src/modules/boltz/validation.rs b/src/modules/boltz/validation.rs deleted file mode 100644 index 682dfd73..00000000 --- a/src/modules/boltz/validation.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::modules::boltz::errors::BoltzError; -use crate::modules::boltz::types::BoltzNetwork; -use boltz_client::swaps::bitcoin::BtcSwapScript; -use boltz_client::swaps::boltz::{CreateReverseResponse, CreateSubmarineResponse}; -use boltz_client::util::secrets::Preimage; -use boltz_client::Bolt11Invoice; -use std::str::FromStr; - -/// Validate an optional fee rate before any fund-moving work begins. -/// -/// The rate flows into `Fee::Relative(f64)` unchecked otherwise: negative -/// values and `NaN` silently become a zero fee and infinity saturates, so an -/// invalid configuration would only surface after a claim or refund is -/// already being constructed. -pub(crate) fn validate_fee_rate(fee_rate_sat_per_vb: Option) -> Result<(), BoltzError> { - if let Some(rate) = fee_rate_sat_per_vb { - if !rate.is_finite() || rate <= 0.0 { - return Err(BoltzError::InvalidInput { - error_details: format!( - "fee_rate_sat_per_vb must be a finite, positive sat/vB value, got {}", - rate - ), - }); - } - } - Ok(()) -} - -/// Parse `address` and require it to belong to `network`, returning its -/// canonical string form. -/// -/// Fund destinations must be checked before they are acted on: a reverse -/// swap's claim address is persisted at creation and cannot be replaced after -/// the caller has paid the invoice, and a refund address receives the funds -/// directly. -pub(crate) fn validate_onchain_address( - address: &str, - network: BoltzNetwork, -) -> Result { - let trimmed = address.trim(); - if trimmed.is_empty() { - return Err(BoltzError::InvalidInput { - error_details: "address must not be empty".to_string(), - }); - } - let parsed = trimmed - .parse::>() - .map_err(|e| BoltzError::InvalidInput { - error_details: format!("Invalid Bitcoin address: {}", e), - })?; - let address = parsed - .require_network(network.as_bitcoin_network()) - .map_err(|_| BoltzError::InvalidInput { - error_details: format!( - "Address does not belong to the {} network", - network.as_str() - ), - })?; - Ok(address.to_string()) -} - -/// Fully validate a submarine swap creation response before it is persisted -/// or the caller is handed a lockup address to fund. -/// -/// `response.validate` proves the returned address commits to the script in -/// the response (with our refund key), but on Bitcoin it leaves the script's -/// hashlock unbound: Boltz could return a consistent script/address pair whose -/// hashlock does not match the invoice, letting it claim the lockup with a -/// preimage of its own choosing without ever paying the invoice. Binding the -/// hashlock to the invoice's payment hash closes that. -pub(crate) fn validate_submarine_response( - response: &CreateSubmarineResponse, - invoice: &str, - refund_public_key: &bitcoin::PublicKey, - network: BoltzNetwork, -) -> Result<(), BoltzError> { - response - .validate(invoice, refund_public_key, network.as_chain()) - .map_err(|e| BoltzError::SwapError { - error_details: format!("Boltz submarine response failed validation: {}", e), - })?; - - let invoice_preimage = - Preimage::from_invoice_str(invoice).map_err(|e| BoltzError::InvalidInput { - error_details: format!("Invalid BOLT11 invoice: {}", e), - })?; - let script = - BtcSwapScript::submarine_from_swap_resp(response, *refund_public_key).map_err(|e| { - BoltzError::SwapError { - error_details: format!("Boltz submarine response failed validation: {}", e), - } - })?; - if script.hashlock != invoice_preimage.hash160 { - return Err(BoltzError::SwapError { - error_details: format!( - "Boltz submarine response failed validation: script hashlock {} does not match the invoice payment hash160 {}", - script.hashlock, invoice_preimage.hash160 - ), - }); - } - Ok(()) -} - -/// Fully validate a reverse swap creation response before it is persisted or -/// the caller is handed an invoice to pay. -/// -/// `response.validate` proves the invoice commits to our preimage's sha256 and -/// that the lockup address commits to the script in the response (with our -/// claim key), but it leaves two terms unbound: the script's hashlock is never -/// compared with the preimage (a mismatch would silently break our script-path -/// claim, leaving the funds claimable only if Boltz cooperates), and the -/// invoice amount is never compared with the requested `amount_sat`. -pub(crate) fn validate_reverse_response( - response: &CreateReverseResponse, - preimage: &Preimage, - claim_public_key: &bitcoin::PublicKey, - amount_sat: u64, - network: BoltzNetwork, -) -> Result<(), BoltzError> { - response - .validate(preimage, claim_public_key, network.as_chain()) - .map_err(|e| BoltzError::SwapError { - error_details: format!("Boltz reverse response failed validation: {}", e), - })?; - - let script = - BtcSwapScript::reverse_from_swap_resp(response, *claim_public_key).map_err(|e| { - BoltzError::SwapError { - error_details: format!("Boltz reverse response failed validation: {}", e), - } - })?; - if script.hashlock != preimage.hash160 { - return Err(BoltzError::SwapError { - error_details: format!( - "Boltz reverse response failed validation: script hashlock {} does not match our preimage hash160 {}", - script.hashlock, preimage.hash160 - ), - }); - } - - if let Some(invoice) = &response.invoice { - let parsed = Bolt11Invoice::from_str(invoice).map_err(|e| BoltzError::SwapError { - error_details: format!("Boltz returned an unparseable invoice: {}", e), - })?; - let expected_msat = - amount_sat - .checked_mul(1000) - .ok_or_else(|| BoltzError::InvalidInput { - error_details: format!("amount_sat {} is too large", amount_sat), - })?; - match parsed.amount_milli_satoshis() { - Some(msat) if msat == expected_msat => {} - other => { - return Err(BoltzError::SwapError { - error_details: format!( - "Boltz reverse response failed validation: invoice amount {:?} msat does not match the requested {} msat", - other, expected_msat - ), - }); - } - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::{validate_fee_rate, validate_onchain_address}; - use crate::modules::boltz::types::BoltzNetwork; - - #[test] - fn accepts_valid_fee_rates_and_none() { - assert!(validate_fee_rate(None).is_ok()); - assert!(validate_fee_rate(Some(1.5)).is_ok()); - assert!(validate_fee_rate(Some(500.0)).is_ok()); - } - - #[test] - fn rejects_non_finite_and_non_positive_fee_rates() { - for rate in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { - assert!( - validate_fee_rate(Some(rate)).is_err(), - "rate {} must be rejected", - rate - ); - } - } - - #[test] - fn accepts_an_address_on_the_selected_network() { - let mainnet = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"; - let canonical = validate_onchain_address(mainnet, BoltzNetwork::Mainnet).unwrap(); - assert_eq!(canonical, mainnet); - - // Surrounding whitespace is tolerated and stripped. - let padded = format!(" {}\n", mainnet); - assert_eq!( - validate_onchain_address(&padded, BoltzNetwork::Mainnet).unwrap(), - mainnet - ); - } - - #[test] - fn rejects_an_address_from_another_network() { - // A valid mainnet address is not acceptable on testnet, and vice versa. - let mainnet = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"; - let testnet = "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx"; - assert!(validate_onchain_address(mainnet, BoltzNetwork::Testnet).is_err()); - assert!(validate_onchain_address(testnet, BoltzNetwork::Mainnet).is_err()); - assert!(validate_onchain_address(testnet, BoltzNetwork::Regtest).is_err()); - } - - #[test] - fn rejects_garbage_and_empty_addresses() { - assert!(validate_onchain_address("", BoltzNetwork::Mainnet).is_err()); - assert!(validate_onchain_address(" ", BoltzNetwork::Mainnet).is_err()); - assert!(validate_onchain_address("notanaddress", BoltzNetwork::Mainnet).is_err()); - } -} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 42c1f76d..1609fdae 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,6 +1,5 @@ pub mod activity; pub mod blocktank; -pub mod boltz; pub mod hardware_wallet; pub mod jade; pub mod lnurl; diff --git a/tests/init_db.rs b/tests/init_db.rs new file mode 100644 index 00000000..02916cde --- /dev/null +++ b/tests/init_db.rs @@ -0,0 +1,24 @@ +//! Lives outside `src` so the crate itself keeps no reference to the removed +//! swap module. A separate test binary also keeps `init_db`'s global +//! connections away from the unit tests. + +use bitkitcore::init_db; + +/// Older releases kept swap data in `boltz.db`. With the wallet seed it is what +/// a manual refund of a locked swap needs, so `init_db` must neither create +/// nor touch it. +#[test] +fn init_db_leaves_boltz_db_alone() { + let fresh = tempfile::tempdir().unwrap(); + init_db(fresh.path().to_string_lossy().into_owned()).unwrap(); + assert!(fresh.path().join("activity.db").exists()); + assert!(fresh.path().join("blocktank.db").exists()); + assert!(!fresh.path().join("boltz.db").exists()); + + let existing = tempfile::tempdir().unwrap(); + let legacy_db = existing.path().join("boltz.db"); + let legacy_bytes = b"legacy boltz.db contents".to_vec(); + std::fs::write(&legacy_db, &legacy_bytes).unwrap(); + init_db(existing.path().to_string_lossy().into_owned()).unwrap(); + assert_eq!(std::fs::read(&legacy_db).unwrap(), legacy_bytes); +} From 7aeca6b7749ab1c69e0ae7187fc1956af0b71fc9 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Wed, 11 Nov 2026 17:34:08 -0500 Subject: [PATCH 2/2] chore: drop the lightning update from the changelog Removing boltz-client also removes `lightning`, so the Unreleased line from #207 no longer describes a dependency. --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa67041..5b1dd9cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## Unreleased -- Update `lightning` to 0.2.8. - `regtest_mine`, `regtest_deposit`, `regtest_pay`, `regtest_get_payment` and `regtest_close_channel` take an optional `auth` (`user:password`), sent as basic auth to Blocktank servers that protect the regtest routes; a 401 returns a clear error. - Wake: a default registration `timestamp` is one past the last in this process when needed, so two quick registrations no longer get 409 `stale_registration`. `WakeError::GatewayRejected` gains `server_time` and `retry_after_secs`. Add `wake_get_device` and `wake_send_test`. `wake_set_topics` now returns `WakeSubscription` with `unknown_topics` (breaking). - Add `paykit_route_payment`, which picks how to pay a Paykit contact: directly when a held balance reaches one of the contact's endpoints, otherwise through the cheapest quoted service leg, such as a Spark withdrawal to the contact's on-chain address.