From 4ae47e57f95eb081977c9a143bb575e96281e318 Mon Sep 17 00:00:00 2001 From: Mohan <86064887+binarybaron@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:29:00 +0200 Subject: [PATCH] refactor: swap-core / swap-machine (#530) * progress * fix thread safety * move monero types from swap into swap_core * just fmt * move non test code above test code * revert removed tracing in bitcoin-wallet/src/primitives.rs * Use existing private_key_from_secp256k1_scalar * remove unused monero chose code * fix some clippy warnings due to imports * move state machine types into the new `swap-machine` crate * remove monero_c orphan submodule * rm bdk_test and sqlx_test from ci * move proptest.rs into swap-proptest * increase stack size to 12mb * properly increase stack size * fix merge conflict in ci.yml * don't increase stack size on mac * fix infinite recursion * fix integration tests * fix some compile errors * fix compilation errors * rustfmt * ignore unstaged patches we applied to monero submodule when running git status * fix some test compilation errors * use BitcoinWallet trait instead of concrete type everywhere * add just test command to run integration tests * remove test_utils features from bdk in swap-core --------- Co-authored-by: einliterflasche Co-authored-by: binarybaron --- .gitmodules | 1 + Cargo.lock | 983 ++++++++++-------- Cargo.toml | 19 +- bitcoin-wallet/Cargo.toml | 12 + bitcoin-wallet/src/lib.rs | 65 ++ bitcoin-wallet/src/primitives.rs | 255 +++++ justfile | 3 + .../src/bin/stress_test_downloader.rs | 2 +- monero-rpc-pool/src/proxy.rs | 1 - monero-seed/src/tests.rs | 2 +- monero-sys/src/lib.rs | 8 + monero-sys/tests/sign_message.rs | 5 +- monero-sys/tests/simple.rs | 5 +- monero-sys/tests/special_paths.rs | 5 +- monero-sys/tests/wallet_closing.rs | 5 +- swap-asb/Cargo.toml | 1 + swap-asb/src/main.rs | 2 +- swap-controller-api/Cargo.toml | 4 - swap-controller/Cargo.toml | 1 - swap-core/Cargo.toml | 44 + swap-core/src/bitcoin.rs | 801 ++++++++++++++ {swap => swap-core}/src/bitcoin/cancel.rs | 8 +- .../src/bitcoin/early_refund.rs | 6 +- {swap => swap-core}/src/bitcoin/lock.rs | 24 +- {swap => swap-core}/src/bitcoin/punish.rs | 4 +- {swap => swap-core}/src/bitcoin/redeem.rs | 14 +- {swap => swap-core}/src/bitcoin/refund.rs | 23 +- {swap => swap-core}/src/bitcoin/timelocks.rs | 0 swap-core/src/lib.rs | 2 + swap-core/src/monero.rs | 5 + .../src/monero/ext.rs | 2 +- swap-core/src/monero/primitives.rs | 871 ++++++++++++++++ swap-env/src/config.rs | 2 +- swap-env/src/prompt.rs | 8 +- swap-feed/src/rate.rs | 5 +- swap-machine/Cargo.toml | 29 + .../state.rs => swap-machine/src/alice/mod.rs | 205 ++-- .../state.rs => swap-machine/src/bob/mod.rs | 129 +-- swap-machine/src/common/mod.rs | 169 +++ swap-machine/src/lib.rs | 3 + swap-orchestrator/Cargo.toml | 2 +- swap-orchestrator/src/main.rs | 4 +- swap-orchestrator/src/prompt.rs | 2 +- swap-proptest/Cargo.toml | 12 + .../proptest.rs => swap-proptest/src/lib.rs | 0 swap-serde/src/electrum.rs | 2 +- swap-serde/src/libp2p.rs | 2 +- swap-serde/src/monero.rs | 6 +- swap/Cargo.toml | 15 +- swap/src/asb/event_loop.rs | 12 +- swap/src/asb/recovery/punish.rs | 7 +- swap/src/asb/recovery/redeem.rs | 7 +- swap/src/asb/recovery/refund.rs | 5 +- swap/src/asb/rpc/server.rs | 7 +- swap/src/bitcoin.rs | 789 +------------- swap/src/bitcoin/wallet.rs | 494 +++++---- swap/src/cli/api/request.rs | 10 +- swap/src/cli/api/tauri_bindings.rs | 5 +- swap/src/cli/behaviour.rs | 4 +- swap/src/cli/cancel_and_refund.rs | 3 +- swap/src/cli/command.rs | 2 +- swap/src/cli/event_loop.rs | 2 +- swap/src/cli/watcher.rs | 3 +- swap/src/database/alice.rs | 2 +- swap/src/lib.rs | 4 - swap/src/monero.rs | 841 +-------------- swap/src/monero/wallet.rs | 34 +- swap/src/monero/wallet_rpc.rs | 57 +- swap/src/network/encrypted_signature.rs | 2 +- swap/src/network/quote.rs | 3 +- swap/src/network/swap_setup/alice.rs | 7 +- swap/src/network/swap_setup/bob.rs | 12 +- swap/src/network/swarm.rs | 3 +- swap/src/protocol.rs | 165 +-- swap/src/protocol/alice.rs | 11 +- swap/src/protocol/alice/swap.rs | 150 ++- swap/src/protocol/bob.rs | 18 +- swap/src/protocol/bob/swap.rs | 176 +++- ..._refund_using_cancel_and_refund_command.rs | 2 +- ...refund_using_cancel_then_refund_command.rs | 2 +- .../alice_manually_punishes_after_bob_dead.rs | 4 +- ...punishes_after_bob_dead_and_bob_cancels.rs | 4 +- .../alice_punishes_after_restart_bob_dead.rs | 2 +- swap/tests/bdk.sh | 27 - ...ath_bob_offline_while_alice_redeems_btc.rs | 2 +- throttle/src/throttle.rs | 2 +- 86 files changed, 3651 insertions(+), 3007 deletions(-) create mode 100644 bitcoin-wallet/Cargo.toml create mode 100644 bitcoin-wallet/src/lib.rs create mode 100644 bitcoin-wallet/src/primitives.rs create mode 100644 swap-core/Cargo.toml create mode 100644 swap-core/src/bitcoin.rs rename {swap => swap-core}/src/bitcoin/cancel.rs (98%) rename {swap => swap-core}/src/bitcoin/early_refund.rs (95%) rename {swap => swap-core}/src/bitcoin/lock.rs (94%) rename {swap => swap-core}/src/bitcoin/punish.rs (96%) rename {swap => swap-core}/src/bitcoin/redeem.rs (93%) rename {swap => swap-core}/src/bitcoin/refund.rs (89%) rename {swap => swap-core}/src/bitcoin/timelocks.rs (100%) create mode 100644 swap-core/src/lib.rs create mode 100644 swap-core/src/monero.rs rename swap/src/monero_ext.rs => swap-core/src/monero/ext.rs (90%) create mode 100644 swap-core/src/monero/primitives.rs create mode 100644 swap-machine/Cargo.toml rename swap/src/protocol/alice/state.rs => swap-machine/src/alice/mod.rs (81%) rename swap/src/protocol/bob/state.rs => swap-machine/src/bob/mod.rs (90%) create mode 100644 swap-machine/src/common/mod.rs create mode 100644 swap-machine/src/lib.rs create mode 100644 swap-proptest/Cargo.toml rename swap/src/proptest.rs => swap-proptest/src/lib.rs (100%) delete mode 100755 swap/tests/bdk.sh diff --git a/.gitmodules b/.gitmodules index fa9ac517..558d6136 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "monero-sys/monero"] path = monero-sys/monero url = https://github.com/mrcyjanek/monero + ignore = dirty [submodule "monero-sys/monero-depends"] path = monero-sys/monero-depends url = https://github.com/eigenwallet/monero-depends.git diff --git a/Cargo.lock b/Cargo.lock index 3095dfcf..67d1588e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -193,9 +193,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -250,12 +250,12 @@ dependencies = [ "clipboard-win", "image", "log", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", - "parking_lot 0.12.4", + "objc2-foundation 0.3.2", + "parking_lot 0.12.5", "percent-encoding", "windows-sys 0.60.2", "wl-clipboard-rs", @@ -296,7 +296,7 @@ dependencies = [ "rand 0.9.2", "safelog", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "time 0.3.44", "tor-async-utils", "tor-basic-utils", @@ -395,7 +395,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -559,7 +559,7 @@ dependencies = [ "polling", "rustix 1.1.2", "slab", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -617,7 +617,7 @@ dependencies = [ "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -793,9 +793,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba2e2516bdf37af57fc6ff047855f54abad0066e5c4fdaaeb76dabb2e05bcf5" +checksum = "a2b715a6010afb9e457ca2b7c9d2b9c344baa8baed7b38dc476034c171b32575" dependencies = [ "bindgen", "cc", @@ -807,9 +807,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", "axum-macros", @@ -827,8 +827,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -842,9 +841,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -853,7 +852,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper 1.0.2", "tower-layer", "tower-service", @@ -897,7 +895,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -912,6 +910,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +[[package]] +name = "base256emoji" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e9430d9a245a77c92176e649af6e275f20839a48389859d1661e9a128d077c" +dependencies = [ + "const-str", + "match-lookup", +] + [[package]] name = "base58-monero" version = "0.3.2" @@ -1029,16 +1037,18 @@ dependencies = [ [[package]] name = "bdk_wallet" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d30b5dba770184863b5d966ccbc6a11d12c145450be3b6a4435308297e6a12dc" +checksum = "8b172f2caa6311b8172cf99559cd7f7a61cb58834e35e4ca208b3299e7be8bec" dependencies = [ + "anyhow", "bdk_chain", "bitcoin 0.32.7", "miniscript 12.3.5", "rand_core 0.6.4", "serde", "serde_json", + "tempfile", ] [[package]] @@ -1164,7 +1174,7 @@ dependencies = [ [[package]] name = "bitcoin-harness" version = "0.3.0" -source = "git+https://github.com/eigenwallet/bitcoin-harness-rs?branch=master#7a110c9f5e586a33cafe0015b9df0d94f2af9ddc" +source = "git+https://github.com/eigenwallet/bitcoin-harness-rs?branch=master#a909222a76b3967227956be97221e349721bf3c5" dependencies = [ "base64 0.12.3", "bitcoin 0.32.7", @@ -1210,6 +1220,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin-wallet" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bdk_wallet", + "bitcoin 0.32.7", + "tokio", + "tracing", +] + [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -1317,11 +1339,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -1484,9 +1506,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" [[package]] name = "byteorder" @@ -1565,9 +1587,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ "serde_core", ] @@ -1597,7 +1619,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1607,7 +1629,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.7", + "toml 0.9.8", ] [[package]] @@ -1627,9 +1649,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.39" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ "find-msvc-tools", "jobserver", @@ -1720,7 +1742,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -1747,7 +1769,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", - "half 2.6.0", + "half 2.7.0", ] [[package]] @@ -1887,7 +1909,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -1923,7 +1945,7 @@ checksum = "b03b7db8e0b4b2fdad6c551e634134e99ec000e5c8c3b6856c65e8bbaded7a3b" dependencies = [ "crossterm", "unicode-segmentation", - "unicode-width 0.2.1", + "unicode-width 0.2.2", ] [[package]] @@ -2014,7 +2036,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "windows-sys 0.59.0", ] @@ -2027,8 +2049,8 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", - "windows-sys 0.61.1", + "unicode-width 0.2.2", + "windows-sys 0.61.2", ] [[package]] @@ -2037,6 +2059,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-str" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f421161cb492475f1661ddc9815a745a1c894592070661180fdec3d4872e9c3" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -2277,7 +2305,7 @@ dependencies = [ "bitflags 2.9.4", "crossterm_winapi", "document-features", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rustix 1.1.2", "winapi", ] @@ -2369,29 +2397,29 @@ dependencies = [ [[package]] name = "cuprate-epee-encoding" version = "0.5.0" -source = "git+https://github.com/Cuprate/cuprate.git#267c98bcd8a62d70e450905266d05f809a1c2161" +source = "git+https://github.com/Cuprate/cuprate.git#065054996d4db3e86a02205ed01aa6a6286003a6" dependencies = [ "bytes", "cuprate-fixed-bytes", "cuprate-hex", "paste", "ref-cast", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "cuprate-fixed-bytes" version = "0.1.0" -source = "git+https://github.com/Cuprate/cuprate.git#267c98bcd8a62d70e450905266d05f809a1c2161" +source = "git+https://github.com/Cuprate/cuprate.git#065054996d4db3e86a02205ed01aa6a6286003a6" dependencies = [ "bytes", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "cuprate-hex" version = "0.0.0" -source = "git+https://github.com/Cuprate/cuprate.git#267c98bcd8a62d70e450905266d05f809a1c2161" +source = "git+https://github.com/Cuprate/cuprate.git#065054996d4db3e86a02205ed01aa6a6286003a6" dependencies = [ "hex", "serde", @@ -2722,9 +2750,9 @@ dependencies = [ [[package]] name = "deflate64" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "der" @@ -3072,7 +3100,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -3099,9 +3127,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] @@ -3384,7 +3412,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.7", + "toml 0.9.8", "vswhom", "winreg 0.55.0", ] @@ -3502,7 +3530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -3655,9 +3683,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "fixed-hash" @@ -3679,9 +3707,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "libz-rs-sys", @@ -3807,7 +3835,7 @@ dependencies = [ "libc", "pwd-grp", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "walkdir", ] @@ -3853,7 +3881,7 @@ version = "0.3.0" source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" dependencies = [ "fslock-arti-fork", - "thiserror 2.0.16", + "thiserror 2.0.17", "winapi", ] @@ -3934,7 +3962,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.4", + "parking_lot 0.12.5", ] [[package]] @@ -4157,9 +4185,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c8444bc9d71b935156cc0ccab7f622180808af7867b1daae6547d773591703" +checksum = "56cf091b3dc50a299896cb5fb7d9d1908500766a5de99a8d515e63cb4251de8f" dependencies = [ "typenum", ] @@ -4470,12 +4498,13 @@ checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" [[package]] name = "half" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "e54c115d4f30f52c67202f079c5f9d8b49db4691f460fdb0b4c2e838261b2ba5" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -4658,7 +4687,7 @@ dependencies = [ "ipconfig", "lru-cache", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "rand 0.8.5", "resolv-conf", "smallvec", @@ -4865,7 +4894,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] @@ -4919,7 +4948,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.1", + "windows-core 0.62.2", ] [[package]] @@ -5447,13 +5476,13 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "jsonrpsee-types", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.9.2", "rustc-hash", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tower 0.5.2", "tracing", @@ -5476,7 +5505,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tower 0.5.2", "url", @@ -5514,7 +5543,7 @@ dependencies = [ "serde", "serde_json", "soketto", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tokio-util", @@ -5531,7 +5560,7 @@ dependencies = [ "http 1.3.1", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -5659,9 +5688,9 @@ checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libgit2-sys" @@ -5692,14 +5721,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "liblzma" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bf66f4598dc77ff96677c8e763655494f00ff9c1cf79e2eb5bb07bc31f807d" +checksum = "73c36d08cad03a3fbe2c4e7bb3a9e84c57e4ee4135ed0b065cade3d98480c648" dependencies = [ "liblzma-sys", ] @@ -5799,7 +5828,7 @@ dependencies = [ "multihash", "multistream-select", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "quick-protobuf", "rand 0.8.5", @@ -5824,7 +5853,7 @@ dependencies = [ "hickory-resolver", "libp2p-core", "libp2p-identity", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "smallvec", "tracing", ] @@ -5900,7 +5929,7 @@ dependencies = [ "ring 0.17.14", "serde", "sha2 0.10.9", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "zeroize", ] @@ -6030,7 +6059,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-tls 0.4.1", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "quinn", "rand 0.8.5", "ring 0.17.14", @@ -6227,7 +6256,7 @@ dependencies = [ "futures-rustls 0.26.0", "libp2p-core", "libp2p-identity", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "rw-stream-sink", "soketto", @@ -6249,7 +6278,7 @@ dependencies = [ "thiserror 1.0.69", "tracing", "yamux 0.12.1", - "yamux 0.13.6", + "yamux 0.13.7", ] [[package]] @@ -6260,7 +6289,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.9.4", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", ] [[package]] @@ -6336,11 +6365,10 @@ checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -6394,6 +6422,17 @@ dependencies = [ "tendril", ] +[[package]] +name = "match-lookup" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "match_token" version = "0.1.0" @@ -6496,7 +6535,7 @@ checksum = "0209ec7c2b5573660a3c5d0f64c90f8ff286171b15ad2234e7a3fbf8f26180be" dependencies = [ "crypto-bigint", "ff", - "generic-array 1.2.0", + "generic-array 1.3.0", "group", "rand_core 0.6.4", "rustversion", @@ -6596,7 +6635,7 @@ dependencies = [ "equivalent", "event-listener", "futures-util", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "portable-atomic", "rustc_version", "smallvec", @@ -6623,19 +6662,20 @@ dependencies = [ [[package]] name = "monero-address" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "monero-base58", "monero-io", - "thiserror 2.0.16", + "monero-primitives", + "thiserror 2.0.17", "zeroize", ] [[package]] name = "monero-base58" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "monero-primitives", "std-shims 0.1.5", @@ -6644,7 +6684,7 @@ dependencies = [ [[package]] name = "monero-borromean" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", @@ -6657,7 +6697,7 @@ dependencies = [ [[package]] name = "monero-bulletproofs" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", @@ -6665,14 +6705,14 @@ dependencies = [ "monero-primitives", "rand_core 0.6.4", "std-shims 0.1.5", - "thiserror 2.0.16", + "thiserror 2.0.17", "zeroize", ] [[package]] name = "monero-clsag" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "group", @@ -6683,7 +6723,7 @@ dependencies = [ "rand_core 0.6.4", "std-shims 0.1.5", "subtle", - "thiserror 2.0.16", + "thiserror 2.0.17", "zeroize", ] @@ -6700,7 +6740,7 @@ dependencies = [ [[package]] name = "monero-generators" version = "0.4.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "crypto-bigint", "curve25519-dalek 4.1.3", @@ -6732,7 +6772,7 @@ dependencies = [ [[package]] name = "monero-io" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "std-shims 0.1.5", @@ -6742,21 +6782,21 @@ dependencies = [ [[package]] name = "monero-mlsag" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", "monero-io", "monero-primitives", "std-shims 0.1.5", - "thiserror 2.0.16", + "thiserror 2.0.17", "zeroize", ] [[package]] name = "monero-oxide" version = "0.1.4-alpha" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "hex-literal 1.0.0", @@ -6774,7 +6814,7 @@ dependencies = [ [[package]] name = "monero-primitives" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "monero-generators", @@ -6807,7 +6847,7 @@ dependencies = [ [[package]] name = "monero-rpc" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "curve25519-dalek 4.1.3", "hex", @@ -6816,7 +6856,7 @@ dependencies = [ "serde", "serde_json", "std-shims 0.1.5", - "thiserror 2.0.16", + "thiserror 2.0.17", "zeroize", ] @@ -6856,7 +6896,7 @@ dependencies = [ "typeshare", "url", "uuid", - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] @@ -6875,7 +6915,7 @@ dependencies = [ [[package]] name = "monero-simple-request-rpc" version = "0.1.0" -source = "git+https://github.com/monero-oxide/monero-oxide#020820470820dda2aba07a2d0b369631b01f9f9c" +source = "git+https://github.com/monero-oxide/monero-oxide#5b0da5135f738262667c72584df1a5a0c60acb8e" dependencies = [ "digest_auth", "hex", @@ -6916,9 +6956,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08" +checksum = "1cc7d85f3d741164e8972ad355e26ac6e51b20fcae5f911c7da8f2d8bbbb3f33" dependencies = [ "num-traits", "pxfm", @@ -6934,14 +6974,14 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "png 0.17.16", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "windows-sys 0.60.2", ] @@ -6966,11 +7006,12 @@ dependencies = [ [[package]] name = "multibase" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77" dependencies = [ "base-x", + "base256emoji", "data-encoding", "data-encoding-macro", ] @@ -7095,7 +7136,7 @@ dependencies = [ "log", "netlink-packet-core", "netlink-sys", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7335,9 +7376,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -7345,77 +7386,104 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-cloud-kit", "objc2-core-data", "objc2-core-foundation", "objc2-core-graphics", "objc2-core-image", - "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", ] [[package]] name = "objc2-cloud-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-core-data" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", ] [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.9.4", "dispatch2", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", "objc2-io-surface", ] [[package]] name = "objc2-core-image" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", ] [[package]] @@ -7447,22 +7515,22 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", + "block2 0.6.2", "libc", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] name = "objc2-io-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", @@ -7470,22 +7538,22 @@ dependencies = [ [[package]] name = "objc2-io-surface" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] name = "objc2-javascript-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] @@ -7503,14 +7571,14 @@ dependencies = [ [[package]] name = "objc2-osa-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", ] [[package]] @@ -7528,50 +7596,50 @@ dependencies = [ [[package]] name = "objc2-quartz-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-security" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", ] [[package]] name = "objc2-ui-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.9.4", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", ] [[package]] name = "objc2-web-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", - "objc2 0.6.2", + "block2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-javascript-core", "objc2-security", ] @@ -7741,12 +7809,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2 0.6.3", + "objc2-foundation 0.3.2", "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -7831,12 +7899,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", - "parking_lot_core 0.9.11", + "parking_lot_core 0.9.12", ] [[package]] @@ -7855,15 +7923,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -7915,20 +7983,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -7936,9 +8003,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", @@ -7949,9 +8016,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2 0.10.9", @@ -8292,7 +8359,7 @@ dependencies = [ "hermit-abi 0.5.2", "pin-project-lite", "rustix 1.1.2", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -8333,7 +8400,7 @@ dependencies = [ "atomic 0.5.3", "crossbeam-queue", "futures", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "static_assertions", "thiserror 1.0.69", @@ -8406,12 +8473,13 @@ dependencies = [ [[package]] name = "priority-queue" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e7f4ffd8645efad783fc2844ac842367aa2e912d484950192564d57dc039a3a" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" dependencies = [ "equivalent", "indexmap 2.11.4", + "serde", ] [[package]] @@ -8440,7 +8508,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.6", + "toml_edit 0.23.7", ] [[package]] @@ -8490,7 +8558,7 @@ checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" dependencies = [ "dtoa", "itoa", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "prometheus-client-derive-encode", ] @@ -8559,9 +8627,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" dependencies = [ "num-traits", ] @@ -8668,7 +8736,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.32", "socket2 0.6.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -8689,7 +8757,7 @@ dependencies = [ "rustls 0.23.32", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -8711,9 +8779,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -8937,9 +9005,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags 2.9.4", ] @@ -8963,23 +9031,23 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -9121,7 +9189,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots 1.0.3", ] [[package]] @@ -9152,17 +9220,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd", - "block2 0.6.1", + "block2 0.6.2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -9373,7 +9441,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -9424,7 +9492,7 @@ dependencies = [ "once_cell", "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.6", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] @@ -9450,7 +9518,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.0", + "security-framework 3.5.1", ] [[package]] @@ -9486,8 +9554,8 @@ dependencies = [ "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-platform-verifier-android", - "rustls-webpki 0.103.6", - "security-framework 3.5.0", + "rustls-webpki 0.103.7", + "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", "windows-sys 0.59.0", @@ -9511,9 +9579,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "aws-lc-rs", "ring 0.17.14", @@ -9529,9 +9597,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rusty-fork" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" dependencies = [ "fnv", "quick-error 1.2.3", @@ -9541,9 +9609,9 @@ dependencies = [ [[package]] name = "rustyline" -version = "17.0.1" +version = "17.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6614df0b6d4cfb20d1d5e295332921793ce499af3ebc011bf1e393380e1e492" +checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" dependencies = [ "bitflags 2.9.4", "cfg-if", @@ -9556,7 +9624,7 @@ dependencies = [ "nix 0.30.1", "radix_trie", "unicode-segmentation", - "unicode-width 0.2.1", + "unicode-width 0.2.2", "utf8parse", "windows-sys 0.60.2", ] @@ -9587,7 +9655,7 @@ dependencies = [ "educe", "either", "fluid-let", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -9623,7 +9691,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -9862,9 +9930,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags 2.9.4", "core-foundation 0.10.1", @@ -9913,9 +9981,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -9974,18 +10042,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.227" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -10059,9 +10127,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] @@ -10090,9 +10158,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" dependencies = [ "base64 0.22.1", "chrono", @@ -10101,10 +10169,9 @@ dependencies = [ "indexmap 2.11.4", "schemars 0.9.0", "schemars 1.0.4", - "serde", - "serde_derive", + "serde_core", "serde_json", - "serde_with_macros 3.14.1", + "serde_with_macros 3.15.0", "time 0.3.44", ] @@ -10122,9 +10189,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -10164,7 +10231,7 @@ dependencies = [ "futures", "log", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "scc", "serial_test_derive", ] @@ -10458,7 +10525,7 @@ dependencies = [ "paste", "serde", "slotmap", - "thiserror 2.0.16", + "thiserror 2.0.17", "void", ] @@ -10524,7 +10591,7 @@ dependencies = [ "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", "raw-window-handle", - "redox_syscall 0.5.17", + "redox_syscall 0.5.18", "wasm-bindgen", "web-sys", "windows-sys 0.59.0", @@ -10857,9 +10924,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -10870,7 +10937,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "std-shims" version = "0.1.4" -source = "git+https://github.com/serai-dex/serai#0066b94d38c34be3f407ee050e4f9340eb186258" +source = "git+https://github.com/serai-dex/serai#1b781b4b576d4481845604899ea1334a2cf18252" dependencies = [ "hashbrown 0.14.5", "rustversion", @@ -10895,7 +10962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "phf_shared 0.11.3", "precomputed-hash", "serde", @@ -11041,6 +11108,7 @@ dependencies = [ "big-bytes", "bitcoin 0.32.7", "bitcoin-harness", + "bitcoin-wallet", "bmrng", "comfy-table", "config", @@ -11092,9 +11160,11 @@ dependencies = [ "structopt", "strum 0.26.3", "swap-controller-api", + "swap-core", "swap-env", "swap-feed", "swap-fs", + "swap-machine", "swap-serde", "tauri", "tempfile", @@ -11140,6 +11210,7 @@ dependencies = [ "swap", "swap-env", "swap-feed", + "swap-machine", "swap-serde", "thiserror 1.0.69", "tokio", @@ -11159,7 +11230,6 @@ dependencies = [ "jsonrpsee", "monero", "rustyline", - "serde_json", "shell-words", "swap-controller-api", "tokio", @@ -11172,8 +11242,35 @@ dependencies = [ "bitcoin 0.32.7", "jsonrpsee", "serde", +] + +[[package]] +name = "swap-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "bdk_electrum", + "bdk_wallet", + "bitcoin 0.32.7", + "bitcoin-wallet", + "curve25519-dalek-ng", + "ecdsa_fun", + "electrum-pool", + "monero", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rust_decimal", + "serde", + "serde_cbor", "serde_json", + "sha2 0.10.9", + "swap-env", + "swap-serde", + "thiserror 1.0.69", "tokio", + "tracing", + "typeshare", + "uuid", ] [[package]] @@ -11194,7 +11291,7 @@ dependencies = [ "terminal_size", "thiserror 1.0.69", "time 0.3.44", - "toml 0.9.7", + "toml 0.9.8", "tracing", "url", ] @@ -11228,6 +11325,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "swap-machine" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bitcoin 0.32.7", + "bitcoin-wallet", + "conquer-once", + "curve25519-dalek-ng", + "ecdsa_fun", + "libp2p", + "monero", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "sha2 0.10.9", + "sigma_fun", + "swap-core", + "swap-env", + "swap-serde", + "thiserror 1.0.69", + "tracing", + "uuid", +] + [[package]] name = "swap-orchestrator" version = "0.1.0" @@ -11240,7 +11363,7 @@ dependencies = [ "monero", "serde_yaml", "swap-env", - "toml 0.9.7", + "toml 0.9.8", "url", "vergen 8.3.2", ] @@ -11413,7 +11536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" dependencies = [ "bitflags 2.9.4", - "block2 0.6.1", + "block2 0.6.2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", @@ -11430,11 +11553,11 @@ dependencies = [ "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "raw-window-handle", "scopeguard", "tao-macros", @@ -11502,9 +11625,9 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", @@ -11521,7 +11644,7 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tray-icon", "url", @@ -11550,7 +11673,7 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.7", + "toml 0.9.8", "walkdir", ] @@ -11574,7 +11697,7 @@ dependencies = [ "sha2 0.10.9", "syn 2.0.106", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "time 0.3.44", "url", "uuid", @@ -11608,7 +11731,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.7", + "toml 0.9.8", "walkdir", ] @@ -11624,7 +11747,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -11639,7 +11762,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -11656,7 +11779,7 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", ] @@ -11677,8 +11800,8 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.16", - "toml 0.9.7", + "thiserror 2.0.17", + "toml 0.9.8", "url", ] @@ -11691,14 +11814,14 @@ dependencies = [ "dunce", "glob", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "open", "schemars 0.8.22", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "windows 0.61.3", "zbus", @@ -11731,7 +11854,7 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", ] @@ -11744,7 +11867,7 @@ dependencies = [ "serde", "serde_json", "tauri", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "windows-sys 0.60.2", "zbus", @@ -11761,7 +11884,7 @@ dependencies = [ "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -11790,7 +11913,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "time 0.3.44", "tokio", "url", @@ -11809,14 +11932,14 @@ dependencies = [ "gtk", "http 1.3.1", "jni", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "webkit2gtk", "webview2-com", @@ -11833,9 +11956,9 @@ dependencies = [ "http 1.3.1", "jni", "log", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "percent-encoding", "raw-window-handle", @@ -11879,10 +12002,10 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "serde_with 3.14.1", + "serde_with 3.15.0", "swift-rs", - "thiserror 2.0.16", - "toml 0.9.7", + "thiserror 2.0.17", + "toml 0.9.8", "url", "urlpattern", "uuid", @@ -11896,7 +12019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" dependencies = [ "embed-resource", - "toml 0.9.7", + "toml 0.9.8", ] [[package]] @@ -11909,7 +12032,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -11979,11 +12102,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -11999,9 +12122,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -12032,7 +12155,7 @@ checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ "fax", "flate2", - "half 2.6.0", + "half 2.7.0", "quick-error 2.0.1", "weezl", "zune-jpeg", @@ -12137,7 +12260,7 @@ dependencies = [ "io-uring", "libc", "mio", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project-lite", "signal-hook-registry", "slab", @@ -12275,14 +12398,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap 2.11.4", "serde_core", - "serde_spanned 1.0.2", - "toml_datetime 0.7.2", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", "toml_parser", "toml_writer", "winnow 0.7.13", @@ -12299,9 +12422,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -12332,30 +12455,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap 2.11.4", - "toml_datetime 0.7.2", + "toml_datetime 0.7.3", "toml_parser", "winnow 0.7.13", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow 0.7.13", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tor-async-utils" @@ -12368,7 +12491,7 @@ dependencies = [ "oneshot-fused-workaround", "pin-project", "postage", - "thiserror 2.0.16", + "thiserror 2.0.17", "void", ] @@ -12387,7 +12510,7 @@ dependencies = [ "serde", "slab", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -12401,7 +12524,7 @@ dependencies = [ "educe", "getrandom 0.3.3", "safelog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-error", "tor-llcrypto", "zeroize", @@ -12423,7 +12546,7 @@ dependencies = [ "paste", "rand 0.9.2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-cert", @@ -12446,7 +12569,7 @@ dependencies = [ "derive_builder_fork_arti", "derive_more 2.0.1", "digest 0.10.7", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-bytes", "tor-checkable", "tor-llcrypto", @@ -12468,7 +12591,7 @@ dependencies = [ "rand 0.9.2", "safelog", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-cell", @@ -12494,7 +12617,7 @@ source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4 dependencies = [ "humantime", "signature 2.2.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-llcrypto", ] @@ -12524,7 +12647,7 @@ dependencies = [ "safelog", "serde", "static_assertions", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-cell", @@ -12570,8 +12693,8 @@ dependencies = [ "serde-value", "serde_ignored", "strum 0.27.2", - "thiserror 2.0.16", - "toml 0.9.7", + "thiserror 2.0.17", + "toml 0.9.8", "tor-basic-utils", "tor-error", "tor-rtcompat", @@ -12587,7 +12710,7 @@ dependencies = [ "directories", "serde", "shellexpand", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-error", "tor-general-addr", ] @@ -12599,7 +12722,7 @@ source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4 dependencies = [ "digest 0.10.7", "hex", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-llcrypto", ] @@ -12618,7 +12741,7 @@ dependencies = [ "httpdate", "itertools 0.14.0", "memchr", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-circmgr", "tor-error", "tor-hscrypto", @@ -12662,7 +12785,7 @@ dependencies = [ "signature 2.2.0", "static_assertions", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "time 0.3.44", "tor-async-utils", "tor-basic-utils", @@ -12694,7 +12817,7 @@ dependencies = [ "retry-error", "static_assertions", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "void", ] @@ -12705,7 +12828,7 @@ version = "0.34.0" source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4c14e048f72a088#d909ada56d6b2b7ed7b4d0edd4c14e048f72a088" dependencies = [ "derive_more 2.0.1", - "thiserror 2.0.16", + "thiserror 2.0.17", "void", ] @@ -12733,7 +12856,7 @@ dependencies = [ "safelog", "serde", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-config", @@ -12769,7 +12892,7 @@ dependencies = [ "safelog", "slotmap-careful", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-bytes", @@ -12812,7 +12935,7 @@ dependencies = [ "serde", "signature 2.2.0", "subtle", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-error", @@ -12853,9 +12976,9 @@ dependencies = [ "retry-error", "safelog", "serde", - "serde_with 3.14.1", + "serde_with 3.15.0", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-bytes", @@ -12893,7 +13016,7 @@ dependencies = [ "rand 0.9.2", "signature 2.2.0", "ssh-key", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-bytes", "tor-cert", "tor-checkable", @@ -12924,7 +13047,7 @@ dependencies = [ "serde", "signature 2.2.0", "ssh-key", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-config", @@ -12955,9 +13078,9 @@ dependencies = [ "itertools 0.14.0", "safelog", "serde", - "serde_with 3.14.1", + "serde_with 3.15.0", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-basic-utils", "tor-bytes", "tor-config", @@ -12997,7 +13120,7 @@ dependencies = [ "sha3", "signature 2.2.0", "subtle", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-memquota", "visibility", "x25519-dalek", @@ -13011,7 +13134,7 @@ source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4 dependencies = [ "futures", "humantime", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-error", "tor-rtcompat", "tracing", @@ -13036,7 +13159,7 @@ dependencies = [ "slotmap-careful", "static_assertions", "sysinfo", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-async-utils", "tor-basic-utils", "tor-config", @@ -13065,7 +13188,7 @@ dependencies = [ "serde", "static_assertions", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "time 0.3.44", "tor-basic-utils", "tor-error", @@ -13101,12 +13224,12 @@ dependencies = [ "phf 0.13.1", "rand 0.9.2", "serde", - "serde_with 3.14.1", + "serde_with 3.15.0", "signature 2.2.0", "smallvec", "strum 0.27.2", "subtle", - "thiserror 2.0.16", + "thiserror 2.0.17", "time 0.3.44", "tinystr", "tor-basic-utils", @@ -13144,7 +13267,7 @@ dependencies = [ "sanitize-filename", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "time 0.3.44", "tor-async-utils", "tor-basic-utils", @@ -13188,7 +13311,7 @@ dependencies = [ "static_assertions", "subtle", "sync_wrapper 1.0.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-util", "tor-async-utils", @@ -13222,8 +13345,8 @@ source = "git+https://github.com/eigenwallet/arti?rev=d909ada56d6b2b7ed7b4d0edd4 dependencies = [ "caret", "paste", - "serde_with 3.14.1", - "thiserror 2.0.16", + "serde_with 3.15.0", + "thiserror 2.0.17", "tor-bytes", ] @@ -13259,8 +13382,8 @@ dependencies = [ "paste", "pin-project", "rustls-pki-types", - "rustls-webpki 0.103.6", - "thiserror 2.0.16", + "rustls-webpki 0.103.7", + "thiserror 2.0.17", "tokio", "tokio-util", "tor-error", @@ -13288,7 +13411,7 @@ dependencies = [ "priority-queue", "slotmap-careful", "strum 0.27.2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-error", "tor-general-addr", "tor-rtcompat", @@ -13308,7 +13431,7 @@ dependencies = [ "educe", "safelog", "subtle", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-bytes", "tor-error", ] @@ -13321,7 +13444,7 @@ dependencies = [ "derive-deftly 1.2.0", "derive_more 2.0.1", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tor-memquota", ] @@ -13542,15 +13665,15 @@ dependencies = [ "dirs", "libappindicator", "muda", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "png 0.17.16", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "windows-sys 0.59.0", ] @@ -13612,9 +13735,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "typeshare" @@ -13770,9 +13893,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -14382,14 +14505,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.2", + "webpki-root-certs 1.0.3", ] [[package]] name = "webpki-root-certs" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" dependencies = [ "rustls-pki-types", ] @@ -14420,9 +14543,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" dependencies = [ "rustls-pki-types", ] @@ -14458,7 +14581,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.17", "windows 0.61.3", "windows-core 0.61.2", ] @@ -14481,9 +14604,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" [[package]] name = "winapi" @@ -14507,7 +14630,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -14522,10 +14645,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -14588,15 +14711,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -14612,9 +14735,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -14623,9 +14746,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -14640,9 +14763,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -14685,11 +14808,11 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -14703,11 +14826,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -14752,16 +14875,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.1" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -14812,19 +14935,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.0", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -14838,11 +14961,11 @@ dependencies = [ [[package]] name = "windows-version" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "700dad7c058606087f6fdc1f88da5841e06da40334413c6cd4367b25ef26d24e" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -14865,9 +14988,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -14889,9 +15012,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -14913,9 +15036,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -14925,9 +15048,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -14949,9 +15072,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -14973,9 +15096,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -14997,9 +15120,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -15021,9 +15144,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -15080,7 +15203,7 @@ dependencies = [ "os_pipe", "rustix 0.38.44", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -15096,12 +15219,12 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wry" -version = "0.53.3" +version = "0.53.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" +checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6" dependencies = [ "base64 0.22.1", - "block2 0.6.1", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs", @@ -15116,10 +15239,10 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2 0.6.2", + "objc2 0.6.3", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -15128,7 +15251,7 @@ dependencies = [ "sha2 0.10.9", "soup3", "tao-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "url", "webkit2gtk", "webkit2gtk-sys", @@ -15272,7 +15395,7 @@ dependencies = [ "futures", "log", "nohash-hasher", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.8.5", "static_assertions", @@ -15280,14 +15403,14 @@ dependencies = [ [[package]] name = "yamux" -version = "0.13.6" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dd50a6d6115feb3e5d7d0efd45e8ca364b6c83722c1e9c602f5764e0e9597" +checksum = "6927cfe0edfae4b26a369df6bad49cd0ef088c0ec48f4045b2084bcaedc10246" dependencies = [ "futures", "log", "nohash-hasher", - "parking_lot 0.12.4", + "parking_lot 0.12.5", "pin-project", "rand 0.9.2", "static_assertions", @@ -15431,9 +15554,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 640b1135..99e42c3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "bitcoin-wallet", "electrum-pool", "libp2p-rendezvous-server", "libp2p-tor", @@ -13,9 +14,11 @@ members = [ "swap-asb", "swap-controller", "swap-controller-api", + "swap-core", "swap-env", "swap-feed", "swap-fs", + "swap-machine", "swap-orchestrator", "swap-serde", "throttle", @@ -30,6 +33,21 @@ bdk_electrum = { version = "0.23.0", default-features = false } bdk_wallet = "2.0.0" bitcoin = { version = "0.32", features = ["rand", "serde"] } +# Cryptography +curve25519-dalek = { version = "4", package = "curve25519-dalek-ng" } +ecdsa_fun = { version = "0.10", default-features = false, features = ["libsecp_compat", "serde", "adaptor"] } +rand = "0.8" +# Randomness +rand_chacha = "0.3" +sha2 = "0.10" +sigma_fun = { version = "0.7", default-features = false } + +# Async +async-trait = "0.1" + +# Serialization +serde_cbor = "0.11" + anyhow = "1" backoff = { version = "0.4", features = ["futures", "tokio"] } futures = { version = "0.3", default-features = false, features = ["std"] } @@ -38,7 +56,6 @@ jsonrpsee = { version = "0.25", default-features = false } libp2p = { version = "0.53.2" } monero = { version = "0.12", features = ["serde_support"] } once_cell = "1.19" -rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["json"] } rust_decimal = { version = "1", features = ["serde-float"] } rust_decimal_macros = "1" diff --git a/bitcoin-wallet/Cargo.toml b/bitcoin-wallet/Cargo.toml new file mode 100644 index 00000000..8c52339f --- /dev/null +++ b/bitcoin-wallet/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bitcoin-wallet" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bdk_wallet = { workspace = true } +bitcoin = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/bitcoin-wallet/src/lib.rs b/bitcoin-wallet/src/lib.rs new file mode 100644 index 00000000..b98be65a --- /dev/null +++ b/bitcoin-wallet/src/lib.rs @@ -0,0 +1,65 @@ +pub mod primitives; + +pub use crate::primitives::{ScriptStatus, Subscription, Watchable}; +use anyhow::Result; +use bdk_wallet::{export::FullyNodedExport, Balance}; +use bitcoin::{Address, Amount, Network, Psbt, Txid, Weight}; + +#[async_trait::async_trait] +pub trait BitcoinWallet: Send + Sync { + async fn balance(&self) -> Result; + + async fn balance_info(&self) -> Result; + + async fn new_address(&self) -> Result
; + + async fn send_to_address( + &self, + address: Address, + amount: Amount, + spending_fee: Amount, + change_override: Option
, + ) -> Result; + + async fn send_to_address_dynamic_fee( + &self, + address: Address, + amount: Amount, + change_override: Option
, + ) -> Result; + + async fn sweep_balance_to_address_dynamic_fee( + &self, + address: Address, + ) -> Result; + + async fn sign_and_finalize(&self, psbt: bitcoin::psbt::Psbt) -> Result; + + async fn broadcast( + &self, + transaction: bitcoin::Transaction, + kind: &str, + ) -> Result<(Txid, Subscription)>; + + async fn sync(&self) -> Result<()>; + + async fn subscribe_to(&self, tx: Box) -> Subscription; + + async fn status_of_script(&self, tx: &dyn Watchable) -> Result; + + async fn get_raw_transaction( + &self, + txid: Txid, + ) -> Result>>; + + async fn max_giveable(&self, locking_script_size: usize) -> Result<(Amount, Amount)>; + + async fn estimate_fee(&self, weight: Weight, transfer_amount: Option) + -> Result; + + fn network(&self) -> Network; + + fn finality_confirmations(&self) -> u32; + + async fn wallet_export(&self, role: &str) -> Result; +} diff --git a/bitcoin-wallet/src/primitives.rs b/bitcoin-wallet/src/primitives.rs new file mode 100644 index 00000000..4755c136 --- /dev/null +++ b/bitcoin-wallet/src/primitives.rs @@ -0,0 +1,255 @@ +use anyhow::Result; +use bitcoin::{FeeRate, ScriptBuf, Txid}; + +/// An object that can estimate fee rates and minimum relay fees. +pub trait EstimateFeeRate { + /// Estimate the fee rate for a given target block. + fn estimate_feerate( + &self, + target_block: u32, + ) -> impl std::future::Future> + Send; + /// Get the minimum relay fee. + fn min_relay_fee(&self) -> impl std::future::Future> + Send; +} + +/// A subscription to the status of a given transaction +/// that can be used to wait for the transaction to be confirmed. +#[derive(Debug, Clone)] +pub struct Subscription { + /// A receiver used to await updates to the status of the transaction. + pub receiver: tokio::sync::watch::Receiver, + /// The number of confirmations we require for a transaction to be considered final. + pub finality_confirmations: u32, + /// The transaction ID we are subscribing to. + pub txid: Txid, +} + +impl Subscription { + pub fn new( + receiver: tokio::sync::watch::Receiver, + finality_confirmations: u32, + txid: Txid, + ) -> Self { + Self { + receiver, + finality_confirmations, + txid, + } + } + + pub async fn wait_until_final(&self) -> anyhow::Result<()> { + let conf_target = self.finality_confirmations; + let txid = self.txid; + + tracing::info!(%txid, required_confirmation=%conf_target, "Waiting for Bitcoin transaction finality"); + + let mut seen_confirmations = 0; + + self.wait_until(|status| match status { + ScriptStatus::Confirmed(inner) => { + let confirmations = inner.confirmations(); + + if confirmations > seen_confirmations { + tracing::info!(%txid, + seen_confirmations = %confirmations, + needed_confirmations = %conf_target, + "Waiting for Bitcoin transaction finality"); + seen_confirmations = confirmations; + } + + inner.meets_target(conf_target) + } + _ => false, + }) + .await + } + + pub async fn wait_until_seen(&self) -> anyhow::Result<()> { + self.wait_until(ScriptStatus::has_been_seen).await + } + + pub async fn wait_until_confirmed_with(&self, target: T) -> anyhow::Result<()> + where + T: Into, + T: Copy, + { + self.wait_until(|status| status.is_confirmed_with(target)) + .await + } + + pub async fn wait_until( + &self, + mut predicate: impl FnMut(&ScriptStatus) -> bool, + ) -> anyhow::Result<()> { + let mut receiver = self.receiver.clone(); + + while !predicate(&receiver.borrow()) { + receiver + .changed() + .await + .map_err(|_| anyhow::anyhow!("Failed while waiting for next status update"))?; + } + + Ok(()) + } +} + +/// The possible statuses of a script. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ScriptStatus { + Unseen, + InMempool, + Confirmed(Confirmed), + Retrying, +} + +/// The status of a confirmed transaction. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Confirmed { + /// The depth of this transaction within the blockchain. + /// + /// Zero if the transaction is included in the latest block. + pub depth: u32, +} + +impl Confirmed { + pub fn new(depth: u32) -> Self { + Self { depth } + } + + /// Compute the depth of a transaction based on its inclusion height and the + /// latest known block. + /// + /// Our information about the latest block might be outdated. To avoid an + /// overflow, we make sure the depth is 0 in case the inclusion height + /// exceeds our latest known block, + pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self { + let depth = latest_block.saturating_sub(inclusion_height); + + Self { depth } + } + + pub fn confirmations(&self) -> u32 { + self.depth + 1 + } + + pub fn meets_target(&self, target: T) -> bool + where + T: Into, + { + self.confirmations() >= target.into() + } + + pub fn blocks_left_until(&self, target: T) -> u32 + where + T: Into + Copy, + { + if self.meets_target(target) { + 0 + } else { + target.into() - self.confirmations() + } + } +} + +impl std::fmt::Display for ScriptStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ScriptStatus::Unseen => write!(f, "unseen"), + ScriptStatus::InMempool => write!(f, "in mempool"), + ScriptStatus::Retrying => write!(f, "retrying"), + ScriptStatus::Confirmed(inner) => { + write!(f, "confirmed with {} blocks", inner.confirmations()) + } + } + } +} + +/// Defines a watchable transaction. +/// +/// For a transaction to be watchable, we need to know two things: Its +/// transaction ID and the specific output script that is going to change. +/// A transaction can obviously have multiple outputs but our protocol purposes, +/// we are usually interested in a specific one. +pub trait Watchable: Send + Sync { + /// The transaction ID. + fn id(&self) -> Txid; + /// The script of the output we are interested in. + fn script(&self) -> ScriptBuf; + /// Convenience method to get both the script and the txid. + fn script_and_txid(&self) -> (ScriptBuf, Txid) { + (self.script(), self.id()) + } +} + +impl Watchable for (Txid, ScriptBuf) { + fn id(&self) -> Txid { + self.0 + } + + fn script(&self) -> ScriptBuf { + self.1.clone() + } +} + +impl Watchable for &dyn Watchable { + fn id(&self) -> Txid { + (*self).id() + } + + fn script(&self) -> ScriptBuf { + (*self).script() + } +} + +impl Watchable for Box { + fn id(&self) -> Txid { + (**self).id() + } + + fn script(&self) -> ScriptBuf { + (**self).script() + } +} + +impl ScriptStatus { + pub fn from_confirmations(confirmations: u32) -> Self { + match confirmations { + 0 => Self::InMempool, + confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)), + } + } +} + +impl ScriptStatus { + /// Check if the script has any confirmations. + pub fn is_confirmed(&self) -> bool { + matches!(self, ScriptStatus::Confirmed(_)) + } + + /// Check if the script has met the given confirmation target. + pub fn is_confirmed_with(&self, target: T) -> bool + where + T: Into, + { + match self { + ScriptStatus::Confirmed(inner) => inner.meets_target(target), + _ => false, + } + } + + // Calculate the number of blocks left until the target is met. + pub fn blocks_left_until(&self, target: T) -> u32 + where + T: Into + Copy, + { + match self { + ScriptStatus::Confirmed(inner) => inner.blocks_left_until(target), + _ => target.into(), + } + } + + pub fn has_been_seen(&self) -> bool { + matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_)) + } +} diff --git a/justfile b/justfile index f075c0fa..b70dc8ae 100644 --- a/justfile +++ b/justfile @@ -105,6 +105,9 @@ check_gui_eslint: check_gui_tsc: cd src-gui && yarn run tsc --noEmit +test test_name: + cargo test --test {{test_name}} -- --nocapture + # Run the checks for the GUI frontend check_gui: just check_gui_eslint || true diff --git a/monero-rpc-pool/src/bin/stress_test_downloader.rs b/monero-rpc-pool/src/bin/stress_test_downloader.rs index c6be2176..641d8b09 100644 --- a/monero-rpc-pool/src/bin/stress_test_downloader.rs +++ b/monero-rpc-pool/src/bin/stress_test_downloader.rs @@ -1,4 +1,4 @@ -use arti_client::{config::StreamTimeoutConfig, TorClient, TorClientConfig}; +use arti_client::{TorClient, TorClientConfig}; use clap::Parser; use cuprate_epee_encoding::{epee_object, from_bytes, to_bytes}; use futures::stream::{self, StreamExt}; diff --git a/monero-rpc-pool/src/proxy.rs b/monero-rpc-pool/src/proxy.rs index 85e37f7b..3dba5db1 100644 --- a/monero-rpc-pool/src/proxy.rs +++ b/monero-rpc-pool/src/proxy.rs @@ -16,7 +16,6 @@ use tokio::{ }; use tokio_rustls::rustls::{ - self, client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, pki_types::{CertificateDer, ServerName, UnixTime}, DigitallySignedStruct, Error as TlsError, SignatureScheme, diff --git a/monero-seed/src/tests.rs b/monero-seed/src/tests.rs index fb66dc49..152616ed 100644 --- a/monero-seed/src/tests.rs +++ b/monero-seed/src/tests.rs @@ -3,7 +3,7 @@ use zeroize::Zeroizing; use curve25519_dalek::scalar::Scalar; -use monero_primitives::keccak256; +use monero_oxide::primitives::keccak256; use crate::*; diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs index 6d934710..d84b1d48 100644 --- a/monero-sys/src/lib.rs +++ b/monero-sys/src/lib.rs @@ -177,6 +177,14 @@ impl TryFrom for Daemon { } } +impl<'a> TryFrom<&'a str> for Daemon { + type Error = anyhow::Error; + + fn try_from(address: &'a str) -> Result { + address.to_string().try_into() + } +} + impl Daemon { /// Try to convert the daemon configuration to a URL pub fn to_url_string(&self) -> String { diff --git a/monero-sys/tests/sign_message.rs b/monero-sys/tests/sign_message.rs index 48c31bf1..0f97ed85 100644 --- a/monero-sys/tests/sign_message.rs +++ b/monero-sys/tests/sign_message.rs @@ -10,10 +10,7 @@ async fn test_sign_message() { .init(); let temp_dir = tempfile::tempdir().unwrap(); - let daemon = Daemon { - address: PLACEHOLDER_NODE.into(), - ssl: false, - }; + let daemon = Daemon::try_from(PLACEHOLDER_NODE).unwrap(); let wallet_name = "test_signing_wallet"; let wallet_path = temp_dir.path().join(wallet_name).display().to_string(); diff --git a/monero-sys/tests/simple.rs b/monero-sys/tests/simple.rs index 4994900f..496d7635 100644 --- a/monero-sys/tests/simple.rs +++ b/monero-sys/tests/simple.rs @@ -15,10 +15,7 @@ async fn main() { .init(); let temp_dir = tempfile::tempdir().unwrap(); - let daemon = Daemon { - address: STAGENET_REMOTE_NODE.into(), - ssl: true, - }; + let daemon = Daemon::try_from(STAGENET_REMOTE_NODE).unwrap(); let wallet_name = "recovered_wallet"; let wallet_path = temp_dir.path().join(wallet_name).display().to_string(); diff --git a/monero-sys/tests/special_paths.rs b/monero-sys/tests/special_paths.rs index a3e6ed51..c3c3cb0d 100644 --- a/monero-sys/tests/special_paths.rs +++ b/monero-sys/tests/special_paths.rs @@ -13,10 +13,7 @@ async fn test_wallet_with_special_paths() { "path-with-hyphen", ]; - let daemon = Daemon { - address: "https://moneronode.org:18081".into(), - ssl: true, - }; + let daemon = Daemon::try_from("https://moneronode.org:18081").unwrap(); let futures = special_paths .into_iter() diff --git a/monero-sys/tests/wallet_closing.rs b/monero-sys/tests/wallet_closing.rs index 25d11f44..af719450 100644 --- a/monero-sys/tests/wallet_closing.rs +++ b/monero-sys/tests/wallet_closing.rs @@ -10,10 +10,7 @@ async fn main() { .init(); let temp_dir = tempfile::tempdir().unwrap(); - let daemon = Daemon { - address: STAGENET_REMOTE_NODE.into(), - ssl: true, - }; + let daemon = Daemon::try_from(STAGENET_REMOTE_NODE).unwrap(); { let wallet = WalletHandle::open_or_create( diff --git a/swap-asb/Cargo.toml b/swap-asb/Cargo.toml index df2ee114..1a96a163 100644 --- a/swap-asb/Cargo.toml +++ b/swap-asb/Cargo.toml @@ -25,6 +25,7 @@ swap = { path = "../swap" } swap-env = { path = "../swap-env" } swap-feed = { path = "../swap-feed" } swap-serde = { path = "../swap-serde" } +swap-machine = { path = "../swap-machine"} # Async tokio = { workspace = true, features = ["process", "fs", "net", "parking_lot", "rt"] } diff --git a/swap-asb/src/main.rs b/swap-asb/src/main.rs index c5acaba4..fd010b21 100644 --- a/swap-asb/src/main.rs +++ b/swap-asb/src/main.rs @@ -34,7 +34,6 @@ use swap::common::{self, get_logs, warn_if_outdated}; use swap::database::{open_db, AccessMode}; use swap::network::rendezvous::XmrBtcNamespace; use swap::network::swarm; -use swap::protocol::alice::swap::is_complete; use swap::protocol::alice::{run, AliceState, TipConfig}; use swap::protocol::{Database, State}; use swap::seed::Seed; @@ -43,6 +42,7 @@ use swap_env::config::{ initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized, }; use swap_feed; +use swap_machine::alice::is_complete; use tracing_subscriber::filter::LevelFilter; use uuid::Uuid; diff --git a/swap-controller-api/Cargo.toml b/swap-controller-api/Cargo.toml index c6bc7e76..6cc73f68 100644 --- a/swap-controller-api/Cargo.toml +++ b/swap-controller-api/Cargo.toml @@ -7,10 +7,6 @@ edition = "2021" bitcoin = { workspace = true } jsonrpsee = { workspace = true, features = ["macros", "server", "client-core", "http-client"] } serde = { workspace = true } -serde_json = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true, features = ["test-util"] } [lints] workspace = true diff --git a/swap-controller/Cargo.toml b/swap-controller/Cargo.toml index 5439f10d..124a173a 100644 --- a/swap-controller/Cargo.toml +++ b/swap-controller/Cargo.toml @@ -13,7 +13,6 @@ clap = { version = "4", features = ["derive"] } jsonrpsee = { workspace = true, features = ["client-core", "http-client"] } monero = { workspace = true } rustyline = "17.0.0" -serde_json = { workspace = true } shell-words = "1.1" swap-controller-api = { path = "../swap-controller-api" } tokio = { workspace = true } diff --git a/swap-core/Cargo.toml b/swap-core/Cargo.toml new file mode 100644 index 00000000..2be62693 --- /dev/null +++ b/swap-core/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "swap-core" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { workspace = true } + +# Bitcoin stuff +bdk_electrum = { workspace = true } +bdk_wallet = { workspace = true, features = ["rusqlite"] } +bitcoin = { workspace = true } +bitcoin-wallet = { path = "../bitcoin-wallet" } +curve25519-dalek = { workspace = true } +ecdsa_fun = { workspace = true } +electrum-pool = { path = "../electrum-pool" } +monero = { workspace = true } +sha2 = { workspace = true } + +# Serialization +rust_decimal = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +swap-serde = { path = "../swap-serde" } +typeshare = { workspace = true } + +# Tracing +tracing = { workspace = true } + +# Errors +thiserror = { workspace = true } + +# Randomness +rand = { workspace = true } +rand_chacha = { workspace = true } + +[dev-dependencies] +serde_cbor = { workspace = true } +swap-env = { path = "../swap-env" } +tokio = { workspace = true } +uuid = { workspace = true } + +[lints] +workspace = true diff --git a/swap-core/src/bitcoin.rs b/swap-core/src/bitcoin.rs new file mode 100644 index 00000000..42d4e480 --- /dev/null +++ b/swap-core/src/bitcoin.rs @@ -0,0 +1,801 @@ +mod cancel; +mod early_refund; +mod lock; +mod punish; +mod redeem; +mod refund; +mod timelocks; + +pub use crate::bitcoin::cancel::{CancelTimelock, PunishTimelock, TxCancel}; +pub use crate::bitcoin::early_refund::TxEarlyRefund; +pub use crate::bitcoin::lock::TxLock; +pub use crate::bitcoin::punish::TxPunish; +pub use crate::bitcoin::redeem::TxRedeem; +pub use crate::bitcoin::refund::TxRefund; +pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks}; +pub use ::bitcoin::amount::Amount; +pub use ::bitcoin::psbt::Psbt as PartiallySignedTransaction; +pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid}; +pub use ecdsa_fun::Signature; +pub use ecdsa_fun::adaptor::EncryptedSignature; +pub use ecdsa_fun::fun::Scalar; + +use ::bitcoin::hashes::Hash; +use ::bitcoin::secp256k1::ecdsa; +use ::bitcoin::sighash::SegwitV0Sighash as Sighash; +use anyhow::{Context, Result, bail}; +use bdk_wallet::miniscript::descriptor::Wsh; +use bdk_wallet::miniscript::{Descriptor, Segwitv0}; +use bitcoin_wallet::primitives::ScriptStatus; +use ecdsa_fun::ECDSA; +use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; +use ecdsa_fun::fun::Point; +use ecdsa_fun::nonce::Deterministic; +use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use std::str::FromStr; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct SecretKey { + inner: Scalar, + public: Point, +} + +impl SecretKey { + pub fn new_random(rng: &mut R) -> Self { + let scalar = Scalar::random(rng); + + let ecdsa = ECDSA::<()>::default(); + let public = ecdsa.verification_key_for(&scalar); + + Self { + inner: scalar, + public, + } + } + + pub fn public(&self) -> PublicKey { + PublicKey(self.public) + } + + pub fn to_bytes(&self) -> [u8; 32] { + self.inner.to_bytes() + } + + pub fn sign(&self, digest: Sighash) -> Signature { + let ecdsa = ECDSA::>::default(); + + ecdsa.sign(&self.inner, &digest.to_byte_array()) + } + + // TxRefund encsigning explanation: + // + // A and B, are the Bitcoin Public Keys which go on the joint output for + // TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the + // joint output for TxLock_Monero + + // tx_refund: multisig(A, B), published by bob + // bob can produce sig on B using b + // alice sends over an encrypted signature on A encrypted with S_b + // s_b is leaked to alice when bob publishes signed tx_refund allowing her to + // recover s_b: recover(encsig, S_b, sig_tx_refund) = s_b + // alice now has s_a and s_b and can refund monero + + // self = a, Y = S_b, digest = tx_refund + pub fn encsign(&self, Y: PublicKey, digest: Sighash) -> EncryptedSignature { + let adaptor = Adaptor::< + HashTranscript, + Deterministic, + >::default(); + + adaptor.encrypted_sign(&self.inner, &Y.0, &digest.to_byte_array()) + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PublicKey(Point); + +impl PublicKey { + #[cfg(test)] + pub fn random() -> Self { + Self(Point::random(&mut rand::thread_rng())) + } +} + +impl From for Point { + fn from(from: PublicKey) -> Self { + from.0 + } +} + +impl TryFrom for bitcoin::PublicKey { + type Error = bitcoin::key::FromSliceError; + + fn try_from(pubkey: PublicKey) -> Result { + let bytes = pubkey.0.to_bytes(); + bitcoin::PublicKey::from_slice(&bytes) + } +} + +impl TryFrom for PublicKey { + type Error = anyhow::Error; + + fn try_from(pubkey: bitcoin::PublicKey) -> Result { + let bytes = pubkey.to_bytes(); + let bytes_array: [u8; 33] = bytes + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid public key length"))?; + let point = Point::from_bytes(bytes_array) + .ok_or_else(|| anyhow::anyhow!("Invalid public key bytes"))?; + Ok(PublicKey(point)) + } +} + +impl From for PublicKey { + fn from(p: Point) -> Self { + Self(p) + } +} + +impl From for SecretKey { + fn from(scalar: Scalar) -> Self { + let ecdsa = ECDSA::<()>::default(); + let public = ecdsa.verification_key_for(&scalar); + + Self { + inner: scalar, + public, + } + } +} + +impl From for Scalar { + fn from(sk: SecretKey) -> Self { + sk.inner + } +} + +impl From for PublicKey { + fn from(scalar: Scalar) -> Self { + let ecdsa = ECDSA::<()>::default(); + PublicKey(ecdsa.verification_key_for(&scalar)) + } +} + +pub fn verify_sig( + verification_key: &PublicKey, + transaction_sighash: &Sighash, + sig: &Signature, +) -> Result<()> { + let ecdsa = ECDSA::verify_only(); + + if ecdsa.verify( + &verification_key.0, + &transaction_sighash.to_byte_array(), + sig, + ) { + Ok(()) + } else { + bail!(InvalidSignature) + } +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +#[error("signature is invalid")] +pub struct InvalidSignature; + +pub fn verify_encsig( + verification_key: PublicKey, + encryption_key: PublicKey, + digest: &Sighash, + encsig: &EncryptedSignature, +) -> Result<()> { + let adaptor = Adaptor::, Deterministic>::default(); + + if adaptor.verify_encrypted_signature( + &verification_key.0, + &encryption_key.0, + &digest.to_byte_array(), + encsig, + ) { + Ok(()) + } else { + bail!(InvalidEncryptedSignature) + } +} + +#[derive(Clone, Copy, Debug, thiserror::Error)] +#[error("encrypted signature is invalid")] +pub struct InvalidEncryptedSignature; + +pub fn build_shared_output_descriptor( + A: Point, + B: Point, +) -> Result> { + const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; + + let miniscript = MINISCRIPT_TEMPLATE + .replace('A', &A.to_string()) + .replace('B', &B.to_string()); + + let miniscript = + bdk_wallet::miniscript::Miniscript::::from_str(&miniscript) + .expect("a valid miniscript"); + + Ok(Descriptor::Wsh(Wsh::new(miniscript)?)) +} + +pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { + let adaptor = Adaptor::, Deterministic>::default(); + + let s = adaptor + .recover_decryption_key(&S.0, &sig, &encsig) + .map(SecretKey::from) + .context("Failed to recover secret from adaptor signature")?; + + Ok(s) +} + +pub fn current_epoch( + cancel_timelock: CancelTimelock, + punish_timelock: PunishTimelock, + tx_lock_status: ScriptStatus, + tx_cancel_status: ScriptStatus, +) -> ExpiredTimelocks { + if tx_cancel_status.is_confirmed_with(punish_timelock) { + return ExpiredTimelocks::Punish; + } + + if tx_lock_status.is_confirmed_with(cancel_timelock) { + return ExpiredTimelocks::Cancel { + blocks_left: tx_cancel_status.blocks_left_until(punish_timelock), + }; + } + + ExpiredTimelocks::None { + blocks_left: tx_lock_status.blocks_left_until(cancel_timelock), + } +} + +pub mod bitcoin_address { + use anyhow::{Context, Result}; + use bitcoin::{ + Address, + address::{NetworkChecked, NetworkUnchecked}, + }; + use serde::Serialize; + use std::str::FromStr; + + #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)] + #[error( + "Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}" + )] + pub struct BitcoinAddressNetworkMismatch { + #[serde(with = "swap_serde::bitcoin::network")] + expected: bitcoin::Network, + #[serde(with = "swap_serde::bitcoin::network")] + actual: bitcoin::Network, + } + + pub fn parse(addr_str: &str) -> Result> { + let address = bitcoin::Address::from_str(addr_str)?; + + if address.assume_checked_ref().address_type() != Some(bitcoin::AddressType::P2wpkh) { + anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!") + } + + Ok(address) + } + + /// Parse the address and validate the network. + pub fn parse_and_validate_network( + address: &str, + expected_network: bitcoin::Network, + ) -> Result { + let addres = bitcoin::Address::from_str(address)?; + let addres = addres.require_network(expected_network).with_context(|| { + format!("Bitcoin address network mismatch, expected `{expected_network:?}`") + })?; + Ok(addres) + } + + /// Parse the address and validate the network. + pub fn parse_and_validate(address: &str, is_testnet: bool) -> Result { + let expected_network = if is_testnet { + bitcoin::Network::Testnet + } else { + bitcoin::Network::Bitcoin + }; + parse_and_validate_network(address, expected_network) + } + + /// Validate the address network. + pub fn validate( + address: Address, + is_testnet: bool, + ) -> Result> { + let expected_network = if is_testnet { + bitcoin::Network::Testnet + } else { + bitcoin::Network::Bitcoin + }; + validate_network(address, expected_network) + } + + /// Validate the address network. + pub fn validate_network( + address: Address, + expected_network: bitcoin::Network, + ) -> Result> { + address + .require_network(expected_network) + .context("Bitcoin address network mismatch") + } + + /// Validate the address network even though the address is already checked. + pub fn revalidate_network( + address: Address, + expected_network: bitcoin::Network, + ) -> Result
{ + address + .as_unchecked() + .clone() + .require_network(expected_network) + .context("bitcoin address network mismatch") + } + + /// Validate the address network even though the address is already checked. + pub fn revalidate(address: Address, is_testnet: bool) -> Result
{ + revalidate_network( + address, + if is_testnet { + bitcoin::Network::Testnet + } else { + bitcoin::Network::Bitcoin + }, + ) + } +} + +// Transform the ecdsa der signature bytes into a secp256kfun ecdsa signature type. +pub fn extract_ecdsa_sig(sig: &[u8]) -> Result { + let data = &sig[..sig.len() - 1]; + let sig = ecdsa::Signature::from_der(data)?.serialize_compact(); + Signature::from_bytes(sig).ok_or(anyhow::anyhow!("invalid signature")) +} + +/// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23 +pub enum RpcErrorCode { + /// Transaction or block was rejected by network rules. Error code -26. + RpcVerifyRejected, + /// Transaction or block was rejected by network rules. Error code -27. + RpcVerifyAlreadyInChain, + /// General error during transaction or block submission + RpcVerifyError, + /// Invalid address or key. Error code -5. Is throwns when a transaction is not found. + /// See: + /// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/mempool.cpp#L470-L472 + /// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/rawtransaction.cpp#L352-L368 + RpcInvalidAddressOrKey, +} + +impl From for i64 { + fn from(code: RpcErrorCode) -> Self { + match code { + RpcErrorCode::RpcVerifyError => -25, + RpcErrorCode::RpcVerifyRejected => -26, + RpcErrorCode::RpcVerifyAlreadyInChain => -27, + RpcErrorCode::RpcInvalidAddressOrKey => -5, + } + } +} + +pub fn parse_rpc_error_code(error: &anyhow::Error) -> anyhow::Result { + // First try to extract an Electrum error from a MultiError if present + if let Some(multi_error) = error.downcast_ref::() { + // Try to find the first Electrum error in the MultiError + for single_error in multi_error.iter() { + if let bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String( + string, + )) = single_error + { + let json = serde_json::from_str( + &string + .replace("sendrawtransaction RPC error:", "") + .replace("daemon error:", ""), + )?; + + let json_map = match json { + serde_json::Value::Object(map) => map, + _ => continue, // Try next error if this one isn't a JSON object + }; + + let error_code_value = match json_map.get("code") { + Some(val) => val, + None => continue, // Try next error if no error code field + }; + + let error_code_number = match error_code_value { + serde_json::Value::Number(num) => num, + _ => continue, // Try next error if error code isn't a number + }; + + if let Some(int) = error_code_number.as_i64() { + return Ok(int); + } + } + } + // If we couldn't extract an RPC error code from any error in the MultiError + bail!( + "Error is of incorrect variant. We expected an Electrum error, but got: {}", + error + ); + } + + // Original logic for direct Electrum errors + let string = match error.downcast_ref::() { + Some(bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(string))) => { + string + } + _ => bail!( + "Error is of incorrect variant. We expected an Electrum error, but got: {}", + error + ), + }; + + let json = serde_json::from_str( + &string + .replace("sendrawtransaction RPC error:", "") + .replace("daemon error:", ""), + )?; + + let json_map = match json { + serde_json::Value::Object(map) => map, + _ => bail!("Json error is not json object "), + }; + + let error_code_value = match json_map.get("code") { + Some(val) => val, + None => bail!("No error code field"), + }; + + let error_code_number = match error_code_value { + serde_json::Value::Number(num) => num, + _ => bail!("Error code is not a number"), + }; + + if let Some(int) = error_code_number.as_i64() { + Ok(int) + } else { + bail!("Error code is not an unsigned integer") + } +} + +#[derive(Clone, Copy, thiserror::Error, Debug)] +#[error("transaction does not spend anything")] +pub struct NoInputs; + +#[derive(Clone, Copy, thiserror::Error, Debug)] +#[error("transaction has {0} inputs, expected 1")] +pub struct TooManyInputs(usize); + +#[derive(Clone, Copy, thiserror::Error, Debug)] +#[error("empty witness stack")] +pub struct EmptyWitnessStack; + +#[derive(Clone, Copy, thiserror::Error, Debug)] +#[error("input has {0} witnesses, expected 3")] +pub struct NotThreeWitnesses(usize); + +#[cfg(test)] +pub use crate::bitcoin::wallet::TestWalletBuilder; + +#[cfg(test)] +mod tests { + use super::*; + use crate::monero::TransferProof; + use crate::protocol::{alice, bob}; + use bitcoin::secp256k1; + use curve25519_dalek::scalar::Scalar; + use ecdsa_fun::fun::marker::{NonZero, Public}; + use monero::PrivateKey; + use rand::rngs::OsRng; + use std::matches; + use swap_env::env::{GetConfig, Regtest}; + use uuid::Uuid; + + #[test] + fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() { + let tx_lock_status = ScriptStatus::from_confirmations(4); + let tx_cancel_status = ScriptStatus::Unseen; + + let expired_timelock = current_epoch( + CancelTimelock::new(5), + PunishTimelock::new(5), + tx_lock_status, + tx_cancel_status, + ); + + assert!(matches!(expired_timelock, ExpiredTimelocks::None { .. })); + } + + #[test] + fn lock_confirmations_ge_to_cancel_timelock_cancel_timelock_expired() { + let tx_lock_status = ScriptStatus::from_confirmations(5); + let tx_cancel_status = ScriptStatus::Unseen; + + let expired_timelock = current_epoch( + CancelTimelock::new(5), + PunishTimelock::new(5), + tx_lock_status, + tx_cancel_status, + ); + + assert!(matches!(expired_timelock, ExpiredTimelocks::Cancel { .. })); + } + + #[test] + fn cancel_confirmations_ge_to_punish_timelock_punish_timelock_expired() { + let tx_lock_status = ScriptStatus::from_confirmations(10); + let tx_cancel_status = ScriptStatus::from_confirmations(5); + + let expired_timelock = current_epoch( + CancelTimelock::new(5), + PunishTimelock::new(5), + tx_lock_status, + tx_cancel_status, + ); + + assert_eq!(expired_timelock, ExpiredTimelocks::Punish) + } + + #[tokio::test] + async fn calculate_transaction_weights() { + let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) + .build() + .await; + let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) + .build() + .await; + let spending_fee = Amount::from_sat(1_000); + let btc_amount = Amount::from_sat(500_000); + let xmr_amount = crate::monero::Amount::from_piconero(10000); + + let tx_redeem_fee = alice_wallet + .estimate_fee(TxRedeem::weight(), Some(btc_amount)) + .await + .unwrap(); + let tx_punish_fee = alice_wallet + .estimate_fee(TxPunish::weight(), Some(btc_amount)) + .await + .unwrap(); + let tx_lock_fee = alice_wallet + .estimate_fee(TxLock::weight(), Some(btc_amount)) + .await + .unwrap(); + + let redeem_address = alice_wallet.new_address().await.unwrap(); + let punish_address = alice_wallet.new_address().await.unwrap(); + + let config = Regtest::get_config(); + let alice_state0 = alice::State0::new( + btc_amount, + xmr_amount, + config, + redeem_address, + punish_address, + tx_redeem_fee, + tx_punish_fee, + &mut OsRng, + ); + + let bob_state0 = bob::State0::new( + Uuid::new_v4(), + &mut OsRng, + btc_amount, + xmr_amount, + config.bitcoin_cancel_timelock, + config.bitcoin_punish_timelock, + bob_wallet.new_address().await.unwrap(), + config.monero_finality_confirmations, + spending_fee, + spending_fee, + tx_lock_fee, + ); + + let message0 = bob_state0.next_message(); + + let (_, alice_state1) = alice_state0.receive(message0).unwrap(); + let alice_message1 = alice_state1.next_message(); + + let bob_state1 = bob_state0 + .receive(&bob_wallet, alice_message1) + .await + .unwrap(); + let bob_message2 = bob_state1.next_message(); + + let alice_state2 = alice_state1.receive(bob_message2).unwrap(); + let alice_message3 = alice_state2.next_message(); + + let bob_state2 = bob_state1.receive(alice_message3).unwrap(); + let bob_message4 = bob_state2.next_message(); + + let alice_state3 = alice_state2.receive(bob_message4).unwrap(); + + let (bob_state3, _tx_lock) = bob_state2.lock_btc().await.unwrap(); + let bob_state4 = bob_state3.xmr_locked( + crate::monero::BlockHeight { height: 0 }, + // We use bogus values here, because they're irrelevant to this test + TransferProof::new( + crate::monero::TxHash("foo".into()), + PrivateKey::from_scalar(Scalar::one()), + ), + ); + let encrypted_signature = bob_state4.tx_redeem_encsig(); + let bob_state6 = bob_state4.cancel(); + + let cancel_transaction = alice_state3.signed_cancel_transaction().unwrap(); + let punish_transaction = alice_state3.signed_punish_transaction().unwrap(); + let redeem_transaction = alice_state3 + .signed_redeem_transaction(encrypted_signature) + .unwrap(); + let refund_transaction = bob_state6.signed_refund_transaction().unwrap(); + + assert_weight(redeem_transaction, TxRedeem::weight().to_wu(), "TxRedeem"); + assert_weight(cancel_transaction, TxCancel::weight().to_wu(), "TxCancel"); + assert_weight(punish_transaction, TxPunish::weight().to_wu(), "TxPunish"); + assert_weight(refund_transaction, TxRefund::weight().to_wu(), "TxRefund"); + + // Test TxEarlyRefund transaction + let early_refund_transaction = alice_state3 + .signed_early_refund_transaction() + .unwrap() + .unwrap(); + assert_weight( + early_refund_transaction, + TxEarlyRefund::weight() as u64, + "TxEarlyRefund", + ); + } + + #[tokio::test] + async fn tx_early_refund_can_be_constructed_and_signed() { + let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) + .build() + .await; + let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) + .build() + .await; + let spending_fee = Amount::from_sat(1_000); + let btc_amount = Amount::from_sat(500_000); + let xmr_amount = crate::monero::Amount::from_piconero(10000); + + let tx_redeem_fee = alice_wallet + .estimate_fee(TxRedeem::weight(), Some(btc_amount)) + .await + .unwrap(); + let tx_punish_fee = alice_wallet + .estimate_fee(TxPunish::weight(), Some(btc_amount)) + .await + .unwrap(); + + let refund_address = alice_wallet.new_address().await.unwrap(); + let punish_address = alice_wallet.new_address().await.unwrap(); + + let config = Regtest::get_config(); + let alice_state0 = alice::State0::new( + btc_amount, + xmr_amount, + config, + refund_address.clone(), + punish_address, + tx_redeem_fee, + tx_punish_fee, + &mut OsRng, + ); + + let bob_state0 = bob::State0::new( + Uuid::new_v4(), + &mut OsRng, + btc_amount, + xmr_amount, + config.bitcoin_cancel_timelock, + config.bitcoin_punish_timelock, + bob_wallet.new_address().await.unwrap(), + config.monero_finality_confirmations, + spending_fee, + spending_fee, + spending_fee, + ); + + // Complete the state machine up to State3 + let message0 = bob_state0.next_message(); + let (_, alice_state1) = alice_state0.receive(message0).unwrap(); + let alice_message1 = alice_state1.next_message(); + + let bob_state1 = bob_state0 + .receive(&bob_wallet, alice_message1) + .await + .unwrap(); + let bob_message2 = bob_state1.next_message(); + + let alice_state2 = alice_state1.receive(bob_message2).unwrap(); + let alice_message3 = alice_state2.next_message(); + + let bob_state2 = bob_state1.receive(alice_message3).unwrap(); + let bob_message4 = bob_state2.next_message(); + + let alice_state3 = alice_state2.receive(bob_message4).unwrap(); + + // Test TxEarlyRefund construction + let tx_early_refund = alice_state3.tx_early_refund(); + + // Verify basic properties + assert_eq!(tx_early_refund.txid(), tx_early_refund.txid()); // Should be deterministic + assert!(tx_early_refund.digest() != Sighash::all_zeros()); // Should have valid digest + + // Test that it can be signed and completed + let early_refund_transaction = alice_state3 + .signed_early_refund_transaction() + .unwrap() + .unwrap(); + + // Verify the transaction has expected structure + assert_eq!(early_refund_transaction.input.len(), 1); // One input from lock tx + assert_eq!(early_refund_transaction.output.len(), 1); // One output to refund address + assert_eq!( + early_refund_transaction.output[0].script_pubkey, + refund_address.script_pubkey() + ); + + // Verify the input is spending the lock transaction + assert_eq!( + early_refund_transaction.input[0].previous_output, + alice_state3.tx_lock.as_outpoint() + ); + + // Verify the amount is correct (lock amount minus fee) + let expected_amount = alice_state3.tx_lock.lock_amount() - alice_state3.tx_refund_fee; + assert_eq!(early_refund_transaction.output[0].value, expected_amount); + } + + #[test] + fn tx_early_refund_has_correct_weight() { + // TxEarlyRefund should have the same weight as other similar transactions + assert_eq!(TxEarlyRefund::weight(), 548); + + // It should be the same as TxRedeem and TxRefund weights since they have similar structure + assert_eq!(TxEarlyRefund::weight() as u64, TxRedeem::weight().to_wu()); + assert_eq!(TxEarlyRefund::weight() as u64, TxRefund::weight().to_wu()); + } + + // Weights fluctuate because of the length of the signatures. Valid ecdsa + // signatures can have 68, 69, 70, 71, or 72 bytes. Since most of our + // transactions have 2 signatures the weight can be up to 8 bytes less than + // the static weight (4 bytes per signature). + fn assert_weight(transaction: Transaction, expected_weight: u64, tx_name: &str) { + let is_weight = transaction.weight(); + + assert!( + expected_weight - is_weight.to_wu() <= 8, + "{} to have weight {}, but was {}. Transaction: {:#?}", + tx_name, + expected_weight, + is_weight, + transaction + ) + } + + #[test] + fn compare_point_hex() { + // secp256kfun Point and secp256k1 PublicKey should have the same bytes and hex representation + let secp = secp256k1::Secp256k1::default(); + let keypair = secp256k1::Keypair::new(&secp, &mut OsRng); + + let pubkey = keypair.public_key(); + let point: Point<_, Public, NonZero> = Point::from_bytes(pubkey.serialize()).unwrap(); + + assert_eq!(pubkey.to_string(), point.to_string()); + } +} diff --git a/swap/src/bitcoin/cancel.rs b/swap-core/src/bitcoin/cancel.rs similarity index 98% rename from swap/src/bitcoin/cancel.rs rename to swap-core/src/bitcoin/cancel.rs index 42dec976..e76686a1 100644 --- a/swap/src/bitcoin/cancel.rs +++ b/swap-core/src/bitcoin/cancel.rs @@ -1,17 +1,17 @@ use crate::bitcoin; -use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ - build_shared_output_descriptor, Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, + Address, Amount, BlockHeight, PublicKey, Transaction, TxLock, build_shared_output_descriptor, }; +use ::bitcoin::Weight; use ::bitcoin::sighash::SighashCache; use ::bitcoin::transaction::Version; -use ::bitcoin::Weight; use ::bitcoin::{ - locktime::absolute::LockTime as PackedLockTime, secp256k1, sighash::SegwitV0Sighash as Sighash, EcdsaSighashType, OutPoint, ScriptBuf, Sequence, TxIn, TxOut, Txid, + locktime::absolute::LockTime as PackedLockTime, secp256k1, sighash::SegwitV0Sighash as Sighash, }; use anyhow::Result; use bdk_wallet::miniscript::Descriptor; +use bitcoin_wallet::primitives::Watchable; use ecdsa_fun::Signature; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; diff --git a/swap/src/bitcoin/early_refund.rs b/swap-core/src/bitcoin/early_refund.rs similarity index 95% rename from swap/src/bitcoin/early_refund.rs rename to swap-core/src/bitcoin/early_refund.rs index bda19fd7..4d4be9dc 100644 --- a/swap/src/bitcoin/early_refund.rs +++ b/swap-core/src/bitcoin/early_refund.rs @@ -1,14 +1,14 @@ use crate::bitcoin; use ::bitcoin::sighash::SighashCache; -use ::bitcoin::{secp256k1, ScriptBuf}; -use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, EcdsaSighashType, Txid}; +use ::bitcoin::{EcdsaSighashType, Txid, sighash::SegwitV0Sighash as Sighash}; +use ::bitcoin::{ScriptBuf, secp256k1}; use anyhow::{Context, Result}; use bdk_wallet::miniscript::Descriptor; use bitcoin::{Address, Amount, Transaction}; use std::collections::HashMap; -use super::wallet::Watchable; use super::TxLock; +use bitcoin_wallet::primitives::Watchable; const TX_EARLY_REFUND_WEIGHT: usize = 548; diff --git a/swap/src/bitcoin/lock.rs b/swap-core/src/bitcoin/lock.rs similarity index 94% rename from swap/src/bitcoin/lock.rs rename to swap-core/src/bitcoin/lock.rs index cdba40ed..07ad653f 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap-core/src/bitcoin/lock.rs @@ -1,17 +1,13 @@ -use crate::bitcoin::wallet::Watchable; -use crate::bitcoin::{ - build_shared_output_descriptor, Address, Amount, PublicKey, Transaction, Wallet, -}; +use crate::bitcoin::{Address, Amount, PublicKey, Transaction, build_shared_output_descriptor}; use ::bitcoin::psbt::Psbt as PartiallySignedTransaction; use ::bitcoin::{OutPoint, TxIn, TxOut, Txid}; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use bdk_wallet::miniscript::Descriptor; use bdk_wallet::psbt::PsbtUtils; -use bitcoin::{locktime::absolute::LockTime as PackedLockTime, ScriptBuf, Sequence}; +use bitcoin::{ScriptBuf, Sequence, locktime::absolute::LockTime as PackedLockTime}; +use bitcoin_wallet::primitives::Watchable; use serde::{Deserialize, Serialize}; -use super::wallet::EstimateFeeRate; - const SCRIPT_SIZE: usize = 34; const TX_LOCK_WEIGHT: usize = 485; @@ -23,10 +19,7 @@ pub struct TxLock { impl TxLock { pub async fn new( - wallet: &Wallet< - bdk_wallet::rusqlite::Connection, - impl EstimateFeeRate + Send + Sync + 'static, - >, + wallet: &dyn bitcoin_wallet::BitcoinWallet, amount: Amount, spending_fee: Amount, A: PublicKey, @@ -202,8 +195,8 @@ impl Watchable for TxLock { #[cfg(test)] mod tests { use super::*; - use crate::bitcoin::wallet::TestWalletBuilder; use crate::bitcoin::Amount; + use crate::bitcoin::wallet::TestWalletBuilder; use ::bitcoin::psbt::Psbt as PartiallySignedTransaction; // Basic setup function for tests @@ -286,10 +279,7 @@ mod tests { async fn bob_make_psbt( A: PublicKey, B: PublicKey, - wallet: &Wallet< - bdk_wallet::rusqlite::Connection, - impl EstimateFeeRate + Send + Sync + 'static, - >, + wallet: &dyn bitcoin_wallet::BitcoinWallet, amount: Amount, spending_fee: Amount, ) -> PartiallySignedTransaction { diff --git a/swap/src/bitcoin/punish.rs b/swap-core/src/bitcoin/punish.rs similarity index 96% rename from swap/src/bitcoin/punish.rs rename to swap-core/src/bitcoin/punish.rs index b439fd1a..d4bd661a 100644 --- a/swap/src/bitcoin/punish.rs +++ b/swap-core/src/bitcoin/punish.rs @@ -1,10 +1,10 @@ -use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{self, Address, Amount, PunishTimelock, Transaction, TxCancel, Txid}; use ::bitcoin::sighash::SighashCache; -use ::bitcoin::{secp256k1, sighash::SegwitV0Sighash as Sighash, EcdsaSighashType}; +use ::bitcoin::{EcdsaSighashType, secp256k1, sighash::SegwitV0Sighash as Sighash}; use ::bitcoin::{ScriptBuf, Weight}; use anyhow::{Context, Result}; use bdk_wallet::miniscript::Descriptor; +use bitcoin_wallet::primitives::Watchable; use std::collections::HashMap; #[derive(Debug)] diff --git a/swap/src/bitcoin/redeem.rs b/swap-core/src/bitcoin/redeem.rs similarity index 93% rename from swap/src/bitcoin/redeem.rs rename to swap-core/src/bitcoin/redeem.rs index f4598d16..cfd8a805 100644 --- a/swap/src/bitcoin/redeem.rs +++ b/swap-core/src/bitcoin/redeem.rs @@ -1,18 +1,18 @@ -use crate::bitcoin::wallet::Watchable; use crate::bitcoin::{ - verify_encsig, verify_sig, Address, Amount, EmptyWitnessStack, EncryptedSignature, NoInputs, - NotThreeWitnesses, PublicKey, SecretKey, TooManyInputs, Transaction, TxLock, + Address, Amount, EmptyWitnessStack, EncryptedSignature, NoInputs, NotThreeWitnesses, PublicKey, + SecretKey, TooManyInputs, Transaction, TxLock, verify_encsig, verify_sig, }; -use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, Txid}; -use anyhow::{bail, Context, Result}; +use ::bitcoin::{Txid, sighash::SegwitV0Sighash as Sighash}; +use anyhow::{Context, Result, bail}; use bdk_wallet::miniscript::Descriptor; use bitcoin::sighash::SighashCache; -use bitcoin::{secp256k1, ScriptBuf}; use bitcoin::{EcdsaSighashType, Weight}; +use bitcoin::{ScriptBuf, secp256k1}; +use bitcoin_wallet::primitives::Watchable; +use ecdsa_fun::Signature; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::fun::Scalar; use ecdsa_fun::nonce::Deterministic; -use ecdsa_fun::Signature; use sha2::Sha256; use std::collections::HashMap; use std::sync::Arc; diff --git a/swap/src/bitcoin/refund.rs b/swap-core/src/bitcoin/refund.rs similarity index 89% rename from swap/src/bitcoin/refund.rs rename to swap-core/src/bitcoin/refund.rs index 470a37f3..23e98790 100644 --- a/swap/src/bitcoin/refund.rs +++ b/swap-core/src/bitcoin/refund.rs @@ -1,14 +1,15 @@ -use crate::bitcoin::wallet::Watchable; +use crate::bitcoin; use crate::bitcoin::{ - verify_sig, Address, Amount, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, - TooManyInputs, Transaction, TxCancel, + Address, Amount, EmptyWitnessStack, NoInputs, NotThreeWitnesses, PublicKey, TooManyInputs, + Transaction, TxCancel, verify_sig, }; -use crate::{bitcoin, monero}; use ::bitcoin::sighash::SighashCache; -use ::bitcoin::{secp256k1, ScriptBuf, Weight}; -use ::bitcoin::{sighash::SegwitV0Sighash as Sighash, EcdsaSighashType, Txid}; -use anyhow::{bail, Context, Result}; +use ::bitcoin::{EcdsaSighashType, Txid, sighash::SegwitV0Sighash as Sighash}; +use ::bitcoin::{ScriptBuf, Weight, secp256k1}; +use anyhow::{Context, Result, bail}; use bdk_wallet::miniscript::Descriptor; +use bitcoin_wallet::primitives::Watchable; +use curve25519_dalek::scalar::Scalar; use ecdsa_fun::Signature; use std::collections::HashMap; use std::sync::Arc; @@ -103,12 +104,10 @@ impl TxRefund { pub fn extract_monero_private_key( &self, published_refund_tx: Arc, - s_a: monero::Scalar, + s_a: Scalar, a: bitcoin::SecretKey, S_b_bitcoin: bitcoin::PublicKey, - ) -> Result { - let s_a = monero::PrivateKey { scalar: s_a }; - + ) -> Result { let tx_refund_sig = self .extract_signature_by_key(published_refund_tx, a.public()) .context("Failed to extract signature from Bitcoin refund tx")?; @@ -117,7 +116,7 @@ impl TxRefund { let s_b = bitcoin::recover(S_b_bitcoin, tx_refund_sig, tx_refund_encsig) .context("Failed to recover Monero secret key from Bitcoin signature")?; - let s_b = monero::private_key_from_secp256k1_scalar(s_b.into()); + let s_b = crate::monero::primitives::private_key_from_secp256k1_scalar(s_b.into()); let spend_key = s_a + s_b; diff --git a/swap/src/bitcoin/timelocks.rs b/swap-core/src/bitcoin/timelocks.rs similarity index 100% rename from swap/src/bitcoin/timelocks.rs rename to swap-core/src/bitcoin/timelocks.rs diff --git a/swap-core/src/lib.rs b/swap-core/src/lib.rs new file mode 100644 index 00000000..c2b89100 --- /dev/null +++ b/swap-core/src/lib.rs @@ -0,0 +1,2 @@ +pub mod bitcoin; +pub mod monero; diff --git a/swap-core/src/monero.rs b/swap-core/src/monero.rs new file mode 100644 index 00000000..34f8a18b --- /dev/null +++ b/swap-core/src/monero.rs @@ -0,0 +1,5 @@ +pub mod ext; +pub mod primitives; + +pub use ext::*; +pub use primitives::*; diff --git a/swap/src/monero_ext.rs b/swap-core/src/monero/ext.rs similarity index 90% rename from swap/src/monero_ext.rs rename to swap-core/src/monero/ext.rs index 96ca5116..1d12ec5f 100644 --- a/swap/src/monero_ext.rs +++ b/swap-core/src/monero/ext.rs @@ -5,7 +5,7 @@ pub trait ScalarExt { fn to_secpfun_scalar(&self) -> ecdsa_fun::fun::Scalar; } -impl ScalarExt for crate::monero::Scalar { +impl ScalarExt for curve25519_dalek::scalar::Scalar { fn to_secpfun_scalar(&self) -> Scalar { let mut little_endian_bytes = self.to_bytes(); diff --git a/swap-core/src/monero/primitives.rs b/swap-core/src/monero/primitives.rs new file mode 100644 index 00000000..63a34445 --- /dev/null +++ b/swap-core/src/monero/primitives.rs @@ -0,0 +1,871 @@ +use crate::bitcoin; +use anyhow::{Result, bail}; +pub use curve25519_dalek::scalar::Scalar; +use monero::Address; +use rand::{CryptoRng, RngCore}; +use rust_decimal::Decimal; +use rust_decimal::prelude::*; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; +use std::fmt; +use std::ops::{Add, Mul, Sub}; +use std::str::FromStr; +use typeshare::typeshare; + +use ::monero::network::Network; +pub use ::monero::{PrivateKey, PublicKey}; + +pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; + +/// A Monero block height. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct BlockHeight { + pub height: u64, +} + +impl fmt::Display for BlockHeight { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.height) + } +} + +pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> Scalar { + let mut bytes = scalar.to_bytes(); + + // we must reverse the bytes because a secp256k1 scalar is big endian, whereas a + // ed25519 scalar is little endian + bytes.reverse(); + + Scalar::from_bytes_mod_order(bytes) +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct PrivateViewKey(#[serde(with = "swap_serde::monero::private_key")] PrivateKey); + +impl fmt::Display for PrivateViewKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Delegate to the Display implementation of PrivateKey + write!(f, "{}", self.0) + } +} + +impl PrivateViewKey { + pub fn new_random(rng: &mut R) -> Self { + let scalar = Scalar::random(rng); + let private_key = PrivateKey::from_scalar(scalar); + + Self(private_key) + } + + pub fn public(&self) -> PublicViewKey { + PublicViewKey(PublicKey::from_private_key(&self.0)) + } +} + +impl Add for PrivateViewKey { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl From for PrivateKey { + fn from(from: PrivateViewKey) -> Self { + from.0 + } +} + +impl From for PublicKey { + fn from(from: PublicViewKey) -> Self { + from.0 + } +} + +#[derive(Clone, Copy, Debug)] +pub struct PublicViewKey(pub PublicKey); + +/// Our own monero amount type, which we need because the monero crate +/// doesn't implement Serialize and Deserialize. +#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)] +#[typeshare(serialized_as = "number")] +pub struct Amount(u64); + +// TX Fees on Monero can be found here: +// - https://www.monero.how/monero-transaction-fees +// - https://bitinfocharts.com/comparison/monero-transactionfees.html#1y +// +// In the last year the highest avg fee on any given day was around 0.00075 XMR +// We use a multiplier of 4x to stay safe +// 0.00075 XMR * 4 = 0.003 XMR (around $1 as of Jun. 4th 2025) +// We DO NOT use this fee to construct any transactions. It is only to **estimate** how much +// we need to reserve for the fee when determining our max giveable amount +// We use a VERY conservative value here to stay on the safe side. We want to avoid not being able +// to lock as much as we previously estimated. +pub const CONSERVATIVE_MONERO_FEE: Amount = Amount::from_piconero(3_000_000_000); + +impl Amount { + pub const ZERO: Self = Self(0); + pub const ONE_XMR: Self = Self(PICONERO_OFFSET); + /// Create an [Amount] with piconero precision and the given number of + /// piconeros. + /// + /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. + pub const fn from_piconero(amount: u64) -> Self { + Amount(amount) + } + + /// Return Monero Amount as Piconero. + pub fn as_piconero(&self) -> u64 { + self.0 + } + + /// Return Monero Amount as XMR. + pub fn as_xmr(&self) -> f64 { + let amount_decimal = Decimal::from(self.0); + let offset_decimal = Decimal::from(PICONERO_OFFSET); + let result = amount_decimal / offset_decimal; + + // Convert to f64 only at the end, after the division + result + .to_f64() + .expect("Conversion from piconero to XMR should not overflow f64") + } + + /// Calculate the conservative max giveable of Monero we can spent given [`self`] is the balance + /// of a Monero wallet + /// This is going to be LESS than we can really spent because we assume a high fee + pub fn max_conservative_giveable(&self) -> Self { + let pico_minus_fee = self + .as_piconero() + .saturating_sub(CONSERVATIVE_MONERO_FEE.as_piconero()); + + Self::from_piconero(pico_minus_fee) + } + + /// Calculate the Monero balance needed to send the [`self`] Amount to another address + /// E.g: Amount(1 XMR).min_conservative_balance_to_spend() with a fee of 0.1 XMR would be 1.1 XMR + /// This is going to be MORE than we really need because we assume a high fee + pub fn min_conservative_balance_to_spend(&self) -> Self { + let pico_minus_fee = self + .as_piconero() + .saturating_add(CONSERVATIVE_MONERO_FEE.as_piconero()); + + Self::from_piconero(pico_minus_fee) + } + + /// Calculate the maximum amount of Bitcoin that can be bought at a given + /// asking price for this amount of Monero including the median fee. + pub fn max_bitcoin_for_price(&self, ask_price: bitcoin::Amount) -> Option { + let pico_minus_fee = self.max_conservative_giveable(); + + if pico_minus_fee.as_piconero() == 0 { + return Some(bitcoin::Amount::ZERO); + } + + // safely convert the BTC/XMR rate to sat/pico + let ask_sats = Decimal::from(ask_price.to_sat()); + let pico_per_xmr = Decimal::from(PICONERO_OFFSET); + let ask_sats_per_pico = ask_sats / pico_per_xmr; + + let pico = Decimal::from(pico_minus_fee.as_piconero()); + let max_sats = pico.checked_mul(ask_sats_per_pico)?; + let satoshi = max_sats.to_u64()?; + + Some(bitcoin::Amount::from_sat(satoshi)) + } + + pub fn from_monero(amount: f64) -> Result { + let decimal = Decimal::try_from(amount)?; + Self::from_decimal(decimal) + } + + pub fn parse_monero(amount: &str) -> Result { + let decimal = Decimal::from_str(amount)?; + Self::from_decimal(decimal) + } + + pub fn as_piconero_decimal(&self) -> Decimal { + Decimal::from(self.as_piconero()) + } + + fn from_decimal(amount: Decimal) -> Result { + let piconeros_dec = + amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64")); + let piconeros = piconeros_dec + .to_u64() + .ok_or_else(|| OverflowError(amount.to_string()))?; + Ok(Amount(piconeros)) + } + + /// Subtract but throw an error on underflow. + pub fn checked_sub(self, rhs: Amount) -> Result { + if self.0 < rhs.0 { + bail!("checked sub would underflow"); + } + + Ok(Amount::from_piconero(self.0 - rhs.0)) + } +} + +/// A Monero address with an associated percentage and human-readable label. +/// +/// This structure represents a destination address for Monero transactions +/// along with the percentage of funds it should receive and a descriptive label. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[typeshare] +pub struct LabeledMoneroAddress { + // If this is None, we will use an address of the internal Monero wallet + #[typeshare(serialized_as = "string")] + address: Option, + #[typeshare(serialized_as = "number")] + percentage: Decimal, + label: String, +} + +impl LabeledMoneroAddress { + /// Creates a new labeled Monero address. + /// + /// # Arguments + /// + /// * `address` - The Monero address + /// * `percentage` - The percentage of funds (between 0.0 and 1.0) + /// * `label` - A human-readable label for this address + /// + /// # Errors + /// + /// Returns an error if the percentage is not between 0.0 and 1.0 inclusive. + fn new( + address: impl Into>, + percentage: Decimal, + label: String, + ) -> Result { + if percentage < Decimal::ZERO || percentage > Decimal::ONE { + bail!( + "Percentage must be between 0 and 1 inclusive, got: {}", + percentage + ); + } + + Ok(Self { + address: address.into(), + percentage, + label, + }) + } + + pub fn with_address( + address: monero::Address, + percentage: Decimal, + label: String, + ) -> Result { + Self::new(address, percentage, label) + } + + pub fn with_internal_address(percentage: Decimal, label: String) -> Result { + Self::new(None, percentage, label) + } + + /// Returns the Monero address. + pub fn address(&self) -> Option { + self.address.clone() + } + + /// Returns the percentage as a decimal. + pub fn percentage(&self) -> Decimal { + self.percentage + } + + /// Returns the human-readable label. + pub fn label(&self) -> &str { + &self.label + } +} + +/// A collection of labeled Monero addresses that can receive funds in a transaction. +/// +/// This structure manages multiple destination addresses with their associated +/// percentages and labels. It's used for splitting Monero transactions across +/// multiple recipients, such as for donations or multi-destination swaps. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[typeshare] +pub struct MoneroAddressPool(Vec); + +use rust_decimal::prelude::ToPrimitive; + +impl MoneroAddressPool { + /// Creates a new address pool from a vector of labeled addresses. + /// + /// # Arguments + /// + /// * `addresses` - Vector of labeled Monero addresses + pub fn new(addresses: Vec) -> Self { + Self(addresses) + } + + /// Returns a vector of all Monero addresses in the pool. + pub fn addresses(&self) -> Vec> { + self.0.iter().map(|address| address.address()).collect() + } + + /// Returns a vector of all percentages as f64 values (0-1 range). + pub fn percentages(&self) -> Vec { + self.0 + .iter() + .map(|address| { + address + .percentage() + .to_f64() + .expect("Decimal should convert to f64") + }) + .collect() + } + + /// Returns an iterator over the labeled addresses. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Validates that all addresses in the pool are on the expected network. + /// + /// # Arguments + /// + /// * `network` - The expected Monero network + /// + /// # Errors + /// + /// Returns an error if any address is on a different network than expected. + pub fn assert_network(&self, network: Network) -> Result<()> { + for address in self.0.iter() { + if let Some(address) = address.address { + if address.network != network { + bail!( + "Address pool contains addresses on the wrong network (address {} is on {:?}, expected {:?})", + address, + address.network, + network + ); + } + } + } + + Ok(()) + } + + /// Assert that the sum of the percentages in the address pool is 1 (allowing for a small tolerance) + pub fn assert_sum_to_one(&self) -> Result<()> { + let sum = self + .0 + .iter() + .map(|address| address.percentage()) + .sum::(); + + const TOLERANCE: f64 = 1e-6; + + if (sum - Decimal::ONE).abs() + > Decimal::from_f64(TOLERANCE).expect("TOLERANCE constant should be a valid f64") + { + bail!("Address pool percentages do not sum to 1"); + } + + Ok(()) + } + + /// Returns a vector of addresses with the empty addresses filled with the given primary address + pub fn fill_empty_addresses(&self, primary_address: monero::Address) -> Vec { + self.0 + .iter() + .map(|address| address.address().unwrap_or(primary_address)) + .collect() + } +} + +impl From<::monero::Address> for MoneroAddressPool { + fn from(address: ::monero::Address) -> Self { + Self(vec![ + LabeledMoneroAddress::new(address, Decimal::from(1), "user address".to_string()) + .expect("Percentage 1 is always valid"), + ]) + } +} + +/// A request to watch for a transfer. +pub struct WatchRequest { + pub public_view_key: super::PublicViewKey, + pub public_spend_key: monero::PublicKey, + /// The proof of the transfer. + pub transfer_proof: TransferProof, + /// The expected amount of the transfer. + pub expected_amount: monero::Amount, + /// The number of confirmations required for the transfer to be considered confirmed. + pub confirmation_target: u64, +} + +/// Transfer a specified amount of money to a specified address. +pub struct TransferRequest { + pub public_spend_key: monero::PublicKey, + pub public_view_key: super::PublicViewKey, + pub amount: monero::Amount, +} + +impl TransferRequest { + pub fn address_and_amount(&self, network: Network) -> (Address, monero::Amount) { + ( + Address::standard(network, self.public_spend_key, self.public_view_key.0), + self.amount, + ) + } +} + +impl Add for Amount { + type Output = Amount; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for Amount { + type Output = Amount; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl Mul for Amount { + type Output = Amount; + + fn mul(self, rhs: u64) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl From for u64 { + fn from(from: Amount) -> u64 { + from.0 + } +} + +impl From<::monero::Amount> for Amount { + fn from(from: ::monero::Amount) -> Self { + Amount::from_piconero(from.as_pico()) + } +} + +impl From for ::monero::Amount { + fn from(from: Amount) -> Self { + ::monero::Amount::from_pico(from.as_piconero()) + } +} + +impl fmt::Display for Amount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut decimal = Decimal::from(self.0); + decimal + .set_scale(12) + .expect("12 is smaller than max precision of 28"); + write!(f, "{} XMR", decimal) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct TransferProof { + pub tx_hash: TxHash, + #[serde(with = "swap_serde::monero::private_key")] + pub tx_key: PrivateKey, +} + +impl TransferProof { + pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self { + Self { tx_hash, tx_key } + } + pub fn tx_hash(&self) -> TxHash { + self.tx_hash.clone() + } + pub fn tx_key(&self) -> PrivateKey { + self.tx_key + } +} + +// TODO: add constructor/ change String to fixed length byte array +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TxHash(pub String); + +impl From for String { + fn from(from: TxHash) -> Self { + from.0 + } +} + +impl fmt::Debug for TxHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl fmt::Display for TxHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +#[error("expected {expected}, got {actual}")] +pub struct InsufficientFunds { + pub expected: Amount, + pub actual: Amount, +} + +#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] +#[error("Overflow, cannot convert {0} to u64")] +pub struct OverflowError(pub String); + +pub mod monero_amount { + use crate::monero::Amount; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(x: &Amount, s: S) -> Result + where + S: Serializer, + { + s.serialize_u64(x.as_piconero()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result>::Error> + where + D: Deserializer<'de>, + { + let picos = u64::deserialize(deserializer)?; + let amount = Amount::from_piconero(picos); + + Ok(amount) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn display_monero_min() { + let min_pics = 1; + let amount = Amount::from_piconero(min_pics); + let monero = amount.to_string(); + assert_eq!("0.000000000001 XMR", monero); + } + + #[test] + fn display_monero_one() { + let min_pics = 1000000000000; + let amount = Amount::from_piconero(min_pics); + let monero = amount.to_string(); + assert_eq!("1.000000000000 XMR", monero); + } + + #[test] + fn display_monero_max() { + let max_pics = 18_446_744_073_709_551_615; + let amount = Amount::from_piconero(max_pics); + let monero = amount.to_string(); + assert_eq!("18446744.073709551615 XMR", monero); + } + + #[test] + fn parse_monero_min() { + let monero_min = "0.000000000001"; + let amount = Amount::parse_monero(monero_min).unwrap(); + let pics = amount.0; + assert_eq!(1, pics); + } + + #[test] + fn parse_monero() { + let monero = "123"; + let amount = Amount::parse_monero(monero).unwrap(); + let pics = amount.0; + assert_eq!(123000000000000, pics); + } + + #[test] + fn parse_monero_max() { + let monero = "18446744.073709551615"; + let amount = Amount::parse_monero(monero).unwrap(); + let pics = amount.0; + assert_eq!(18446744073709551615, pics); + } + + #[test] + fn parse_monero_overflows() { + let overflow_pics = "18446744.073709551616"; + let error = Amount::parse_monero(overflow_pics).unwrap_err(); + assert_eq!( + error.downcast_ref::().unwrap(), + &OverflowError(overflow_pics.to_owned()) + ); + } + + #[test] + fn max_bitcoin_to_trade() { + // sanity check: if the asking price is 1 BTC / 1 XMR + // and we have μ XMR + fee + // then max BTC we can buy is μ + let ask = bitcoin::Amount::from_btc(1.0).unwrap(); + + let xmr = Amount::parse_monero("1.0").unwrap() + CONSERVATIVE_MONERO_FEE; + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap()); + + let xmr = Amount::parse_monero("0.5").unwrap() + CONSERVATIVE_MONERO_FEE; + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_btc(0.5).unwrap()); + + let xmr = Amount::parse_monero("2.5").unwrap() + CONSERVATIVE_MONERO_FEE; + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_btc(2.5).unwrap()); + + let xmr = Amount::parse_monero("420").unwrap() + CONSERVATIVE_MONERO_FEE; + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_btc(420.0).unwrap()); + + let xmr = Amount::parse_monero("0.00001").unwrap() + CONSERVATIVE_MONERO_FEE; + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_btc(0.00001).unwrap()); + + // other ask prices + + let ask = bitcoin::Amount::from_btc(0.5).unwrap(); + let xmr = Amount::parse_monero("2").unwrap() + CONSERVATIVE_MONERO_FEE; + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap()); + + let ask = bitcoin::Amount::from_btc(2.0).unwrap(); + let xmr = Amount::parse_monero("1").unwrap() + CONSERVATIVE_MONERO_FEE; + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_btc(2.0).unwrap()); + + let ask = bitcoin::Amount::from_sat(382_900); + let xmr = Amount::parse_monero("10").unwrap(); + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_sat(3_827_851)); + + // example from https://github.com/comit-network/xmr-btc-swap/issues/1084 + // with rate from kraken at that time + let ask = bitcoin::Amount::from_sat(685_800); + let xmr = Amount::parse_monero("0.826286435921").unwrap(); + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(btc, bitcoin::Amount::from_sat(564_609)); + } + + #[test] + fn max_bitcoin_to_trade_overflow() { + let xmr = Amount::from_monero(30.0).unwrap(); + let ask = bitcoin::Amount::from_sat(728_688); + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(bitcoin::Amount::from_sat(21_858_453), btc); + + let xmr = Amount::from_piconero(u64::MAX); + let ask = bitcoin::Amount::from_sat(u64::MAX); + let btc = xmr.max_bitcoin_for_price(ask); + + assert!(btc.is_none()); + } + + #[test] + fn geting_max_bitcoin_to_trade_with_balance_smaller_than_locking_fee() { + let ask = bitcoin::Amount::from_sat(382_900); + let xmr = Amount::parse_monero("0.00001").unwrap(); + let btc = xmr.max_bitcoin_for_price(ask).unwrap(); + + assert_eq!(bitcoin::Amount::ZERO, btc); + } + + use rand::rngs::OsRng; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + pub struct MoneroPrivateKey( + #[serde(with = "swap_serde::monero::private_key")] ::monero::PrivateKey, + ); + + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] + pub struct MoneroAmount(#[serde(with = "swap_serde::monero::amount")] ::monero::Amount); + + #[test] + fn serde_monero_private_key_json() { + let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng))); + let encoded = serde_json::to_vec(&key).unwrap(); + let decoded: MoneroPrivateKey = serde_json::from_slice(&encoded).unwrap(); + assert_eq!(key, decoded); + } + + #[test] + fn serde_monero_private_key_cbor() { + let key = MoneroPrivateKey(monero::PrivateKey::from_scalar(Scalar::random(&mut OsRng))); + let encoded = serde_cbor::to_vec(&key).unwrap(); + let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap(); + assert_eq!(key, decoded); + } + + #[test] + fn serde_monero_amount() { + let amount = MoneroAmount(::monero::Amount::from_pico(1000)); + let encoded = serde_cbor::to_vec(&amount).unwrap(); + let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap(); + assert_eq!(amount, decoded); + } + + #[test] + fn max_conservative_giveable_basic() { + // Test with balance larger than fee + let balance = Amount::parse_monero("1.0").unwrap(); + let giveable = balance.max_conservative_giveable(); + let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero(); + assert_eq!(giveable.as_piconero(), expected); + } + + #[test] + fn max_conservative_giveable_exact_fee() { + // Test with balance exactly equal to fee + let balance = CONSERVATIVE_MONERO_FEE; + let giveable = balance.max_conservative_giveable(); + assert_eq!(giveable, Amount::ZERO); + } + + #[test] + fn max_conservative_giveable_less_than_fee() { + // Test with balance less than fee (should saturate to 0) + let balance = Amount::from_piconero(CONSERVATIVE_MONERO_FEE.as_piconero() / 2); + let giveable = balance.max_conservative_giveable(); + assert_eq!(giveable, Amount::ZERO); + } + + #[test] + fn max_conservative_giveable_zero_balance() { + // Test with zero balance + let balance = Amount::ZERO; + let giveable = balance.max_conservative_giveable(); + assert_eq!(giveable, Amount::ZERO); + } + + #[test] + fn max_conservative_giveable_large_balance() { + // Test with large balance + let balance = Amount::parse_monero("100.0").unwrap(); + let giveable = balance.max_conservative_giveable(); + let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero(); + assert_eq!(giveable.as_piconero(), expected); + + // Ensure the result makes sense + assert!(giveable.as_piconero() > 0); + assert!(giveable < balance); + } + + #[test] + fn min_conservative_balance_to_spend_basic() { + // Test with 1 XMR amount to send + let amount_to_send = Amount::parse_monero("1.0").unwrap(); + let min_balance = amount_to_send.min_conservative_balance_to_spend(); + let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero(); + assert_eq!(min_balance.as_piconero(), expected); + } + + #[test] + fn min_conservative_balance_to_spend_zero() { + // Test with zero amount to send + let amount_to_send = Amount::ZERO; + let min_balance = amount_to_send.min_conservative_balance_to_spend(); + assert_eq!(min_balance, CONSERVATIVE_MONERO_FEE); + } + + #[test] + fn min_conservative_balance_to_spend_small_amount() { + // Test with small amount + let amount_to_send = Amount::from_piconero(1000); + let min_balance = amount_to_send.min_conservative_balance_to_spend(); + let expected = 1000 + CONSERVATIVE_MONERO_FEE.as_piconero(); + assert_eq!(min_balance.as_piconero(), expected); + } + + #[test] + fn min_conservative_balance_to_spend_large_amount() { + // Test with large amount + let amount_to_send = Amount::parse_monero("50.0").unwrap(); + let min_balance = amount_to_send.min_conservative_balance_to_spend(); + let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero(); + assert_eq!(min_balance.as_piconero(), expected); + + // Ensure the result makes sense + assert!(min_balance > amount_to_send); + assert!(min_balance > CONSERVATIVE_MONERO_FEE); + } + + #[test] + fn conservative_fee_functions_are_inverse() { + // Test that the functions are somewhat inverse of each other + let original_balance = Amount::parse_monero("5.0").unwrap(); + + // Get max giveable amount + let max_giveable = original_balance.max_conservative_giveable(); + + // Calculate min balance needed to send that amount + let min_balance_needed = max_giveable.min_conservative_balance_to_spend(); + + // The min balance needed should be equal to or slightly more than the original balance + // (due to the conservative nature of the fee estimation) + assert!(min_balance_needed >= original_balance); + + // The difference should be at most the conservative fee + let difference = min_balance_needed.as_piconero() - original_balance.as_piconero(); + assert!(difference <= CONSERVATIVE_MONERO_FEE.as_piconero()); + } + + #[test] + fn conservative_fee_edge_cases() { + // Test with maximum possible amount + let max_amount = Amount::from_piconero(u64::MAX - CONSERVATIVE_MONERO_FEE.as_piconero()); + let giveable = max_amount.max_conservative_giveable(); + assert!(giveable.as_piconero() > 0); + + // Test min balance calculation doesn't overflow + let large_amount = Amount::from_piconero(u64::MAX / 2); + let min_balance = large_amount.min_conservative_balance_to_spend(); + assert!(min_balance > large_amount); + } + + #[test] + fn labeled_monero_address_percentage_validation() { + use rust_decimal::Decimal; + + let address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse::().unwrap(); + + // Valid percentages should work (0-1 range) + assert!(LabeledMoneroAddress::new(address, Decimal::ZERO, "test".to_string()).is_ok()); + assert!(LabeledMoneroAddress::new(address, Decimal::ONE, "test".to_string()).is_ok()); + assert!(LabeledMoneroAddress::new(address, Decimal::new(5, 1), "test".to_string()).is_ok()); // 0.5 + assert!( + LabeledMoneroAddress::new(address, Decimal::new(9925, 4), "test".to_string()).is_ok() + ); // 0.9925 + + // Invalid percentages should fail + assert!( + LabeledMoneroAddress::new(address, Decimal::new(-1, 0), "test".to_string()).is_err() + ); + assert!( + LabeledMoneroAddress::new(address, Decimal::new(11, 1), "test".to_string()).is_err() + ); // 1.1 + assert!( + LabeledMoneroAddress::new(address, Decimal::new(2, 0), "test".to_string()).is_err() + ); // 2.0 + } +} diff --git a/swap-env/src/config.rs b/swap-env/src/config.rs index 60538eb5..9913b7b5 100644 --- a/swap-env/src/config.rs +++ b/swap-env/src/config.rs @@ -1,7 +1,7 @@ use crate::defaults::GetDefaults; use crate::env::{Mainnet, Testnet}; use crate::prompt; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use config::ConfigError; use libp2p::core::Multiaddr; use rust_decimal::Decimal; diff --git a/swap-env/src/prompt.rs b/swap-env/src/prompt.rs index 6a6c30fc..1c4a39e5 100644 --- a/swap-env/src/prompt.rs +++ b/swap-env/src/prompt.rs @@ -1,15 +1,15 @@ use std::path::{Path, PathBuf}; use crate::defaults::{ - default_rendezvous_points, DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, + DEFAULT_MAX_BUY_AMOUNT, DEFAULT_MIN_BUY_AMOUNT, DEFAULT_SPREAD, default_rendezvous_points, }; -use anyhow::{bail, Context, Result}; +use anyhow::{Context, Result, bail}; use console::Style; use dialoguer::Confirm; -use dialoguer::{theme::ColorfulTheme, Input, Select}; +use dialoguer::{Input, Select, theme::ColorfulTheme}; use libp2p::Multiaddr; -use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; +use rust_decimal::prelude::FromPrimitive; use url::Url; /// Prompt user for data directory diff --git a/swap-feed/src/rate.rs b/swap-feed/src/rate.rs index cb29ad95..6ad8f958 100644 --- a/swap-feed/src/rate.rs +++ b/swap-feed/src/rate.rs @@ -177,8 +177,9 @@ mod tests { .sell_quote(bitcoin::Amount::ONE_BTC) .unwrap(); - let xmr_factor = xmr_no_spread.into().as_piconero_decimal() - / xmr_with_spread.into().as_piconero_decimal() + let xmr_factor = Decimal::from_f64_retain(f64::from(xmr_no_spread.as_pico() as u32)) + .unwrap() + / Decimal::from_f64_retain(f64::from(xmr_with_spread.as_pico() as u32)).unwrap() - ONE; assert!(xmr_with_spread < xmr_no_spread); diff --git a/swap-machine/Cargo.toml b/swap-machine/Cargo.toml new file mode 100644 index 00000000..117e2376 --- /dev/null +++ b/swap-machine/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "swap-machine" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = { workspace = true } +async-trait = { workspace = true } +bitcoin = { workspace = true } +bitcoin-wallet = { path = "../bitcoin-wallet" } +conquer-once = "0.4" +curve25519-dalek = { workspace = true } +ecdsa_fun = { workspace = true } +libp2p = { workspace = true } +monero = { workspace = true } +rand = { workspace = true } +rand_chacha = { workspace = true } +serde = { workspace = true } +sha2 = { workspace = true } +sigma_fun = { workspace = true, default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] } +swap-core = { path = "../swap-core" } +swap-env = { path = "../swap-env" } +swap-serde = { path = "../swap-serde" } +thiserror = { workspace = true } +tracing = { workspace = true } +uuid = { workspace = true, features = ["serde"] } + +[lints] +workspace = true diff --git a/swap/src/protocol/alice/state.rs b/swap-machine/src/alice/mod.rs similarity index 81% rename from swap/src/protocol/alice/state.rs rename to swap-machine/src/alice/mod.rs index 6adb4874..d257c91e 100644 --- a/swap/src/protocol/alice/state.rs +++ b/swap-machine/src/alice/mod.rs @@ -1,19 +1,17 @@ -use crate::bitcoin::{ - current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, - TxEarlyRefund, TxPunish, TxRedeem, TxRefund, Txid, -}; -use crate::monero::wallet::{TransferRequest, WatchRequest}; -use crate::monero::BlockHeight; -use crate::monero::TransferProof; -use crate::monero_ext::ScalarExt; -use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM}; -use crate::{bitcoin, monero}; -use anyhow::{anyhow, bail, Context, Result}; +use crate::common::{CROSS_CURVE_PROOF_SYSTEM, Message0, Message1, Message2, Message3, Message4}; +use anyhow::{Context, Result, anyhow, bail}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof; use std::fmt; use std::sync::Arc; +use swap_core::bitcoin::{ + CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, TxEarlyRefund, + TxPunish, TxRedeem, TxRefund, Txid, current_epoch, +}; +use swap_core::monero; +use swap_core::monero::ScalarExt; +use swap_core::monero::primitives::{BlockHeight, TransferProof, TransferRequest, WatchRequest}; use swap_env::env::Config; use uuid::Uuid; @@ -49,7 +47,7 @@ pub enum AliceState { EncSigLearned { monero_wallet_restore_blockheight: BlockHeight, transfer_proof: TransferProof, - encrypted_signature: Box, + encrypted_signature: Box, state3: Box, }, BtcRedeemTransactionPublished { @@ -87,6 +85,17 @@ pub enum AliceState { SafelyAborted, } +pub fn is_complete(state: &AliceState) -> bool { + matches!( + state, + AliceState::XmrRefunded + | AliceState::BtcRedeemed + | AliceState::BtcPunished { .. } + | AliceState::SafelyAborted + | AliceState::BtcEarlyRefunded(_) + ) +} + impl fmt::Display for AliceState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -118,13 +127,14 @@ impl fmt::Display for AliceState { } } +#[allow(non_snake_case)] #[derive(Clone, Debug, PartialEq)] pub struct State0 { - a: bitcoin::SecretKey, + a: swap_core::bitcoin::SecretKey, s_a: monero::Scalar, v_a: monero::PrivateViewKey, S_a_monero: monero::PublicKey, - S_a_bitcoin: bitcoin::PublicKey, + S_a_bitcoin: swap_core::bitcoin::PublicKey, dleq_proof_s_a: CrossCurveDLEQProof, btc: bitcoin::Amount, xmr: monero::Amount, @@ -151,7 +161,7 @@ impl State0 { where R: RngCore + CryptoRng, { - let a = bitcoin::SecretKey::new_random(rng); + let a = swap_core::bitcoin::SecretKey::new_random(rng); let v_a = monero::PrivateViewKey::new_random(rng); let s_a = monero::Scalar::random(rng); @@ -224,15 +234,16 @@ impl State0 { } } +#[allow(non_snake_case)] #[derive(Clone, Debug)] pub struct State1 { - a: bitcoin::SecretKey, - B: bitcoin::PublicKey, - s_a: monero::Scalar, + a: swap_core::bitcoin::SecretKey, + B: swap_core::bitcoin::PublicKey, + s_a: swap_core::monero::Scalar, S_a_monero: monero::PublicKey, - S_a_bitcoin: bitcoin::PublicKey, + S_a_bitcoin: swap_core::bitcoin::PublicKey, S_b_monero: monero::PublicKey, - S_b_bitcoin: bitcoin::PublicKey, + S_b_bitcoin: swap_core::bitcoin::PublicKey, v: monero::PrivateViewKey, v_a: monero::PrivateViewKey, dleq_proof_s_a: CrossCurveDLEQProof, @@ -265,8 +276,9 @@ impl State1 { } pub fn receive(self, msg: Message2) -> Result { - let tx_lock = bitcoin::TxLock::from_psbt(msg.psbt, self.a.public(), self.B, self.btc) - .context("Failed to re-construct TxLock from received PSBT")?; + let tx_lock = + swap_core::bitcoin::TxLock::from_psbt(msg.psbt, self.a.public(), self.B, self.btc) + .context("Failed to re-construct TxLock from received PSBT")?; Ok(State2 { a: self.a, @@ -291,13 +303,14 @@ impl State1 { } } +#[allow(non_snake_case)] #[derive(Clone, Debug)] pub struct State2 { - a: bitcoin::SecretKey, - B: bitcoin::PublicKey, + a: swap_core::bitcoin::SecretKey, + B: swap_core::bitcoin::PublicKey, s_a: monero::Scalar, S_b_monero: monero::PublicKey, - S_b_bitcoin: bitcoin::PublicKey, + S_b_bitcoin: swap_core::bitcoin::PublicKey, v: monero::PrivateViewKey, btc: bitcoin::Amount, xmr: monero::Amount, @@ -306,7 +319,7 @@ pub struct State2 { refund_address: bitcoin::Address, redeem_address: bitcoin::Address, punish_address: bitcoin::Address, - tx_lock: bitcoin::TxLock, + tx_lock: swap_core::bitcoin::TxLock, tx_redeem_fee: bitcoin::Amount, tx_punish_fee: bitcoin::Amount, tx_refund_fee: bitcoin::Amount, @@ -315,7 +328,7 @@ pub struct State2 { impl State2 { pub fn next_message(&self) -> Message3 { - let tx_cancel = bitcoin::TxCancel::new( + let tx_cancel = swap_core::bitcoin::TxCancel::new( &self.tx_lock, self.cancel_timelock, self.a.public(), @@ -325,7 +338,7 @@ impl State2 { .expect("valid cancel tx"); let tx_refund = - bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); + swap_core::bitcoin::TxRefund::new(&tx_cancel, &self.refund_address, self.tx_refund_fee); // Alice encsigns the refund transaction(bitcoin) digest with Bob's monero // pubkey(S_b). The refund transaction spends the output of // tx_lock_bitcoin to Bob's refund address. @@ -342,7 +355,7 @@ impl State2 { pub fn receive(self, msg: Message4) -> Result { // Create the TxCancel transaction ourself - let tx_cancel = bitcoin::TxCancel::new( + let tx_cancel = swap_core::bitcoin::TxCancel::new( &self.tx_lock, self.cancel_timelock, self.a.public(), @@ -351,11 +364,11 @@ impl State2 { )?; // Check if the provided signature by Bob is valid for the transaction - bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig) + swap_core::bitcoin::verify_sig(&self.B, &tx_cancel.digest(), &msg.tx_cancel_sig) .context("Failed to verify cancel transaction")?; // Create the TxPunish transaction ourself - let tx_punish = bitcoin::TxPunish::new( + let tx_punish = swap_core::bitcoin::TxPunish::new( &tx_cancel, &self.punish_address, self.punish_timelock, @@ -363,16 +376,23 @@ impl State2 { ); // Check if the provided signature by Bob is valid for the transaction - bitcoin::verify_sig(&self.B, &tx_punish.digest(), &msg.tx_punish_sig) + swap_core::bitcoin::verify_sig(&self.B, &tx_punish.digest(), &msg.tx_punish_sig) .context("Failed to verify punish transaction")?; // Create the TxEarlyRefund transaction ourself - let tx_early_refund = - bitcoin::TxEarlyRefund::new(&self.tx_lock, &self.refund_address, self.tx_refund_fee); + let tx_early_refund = swap_core::bitcoin::TxEarlyRefund::new( + &self.tx_lock, + &self.refund_address, + self.tx_refund_fee, + ); // Check if the provided signature by Bob is valid for the transaction - bitcoin::verify_sig(&self.B, &tx_early_refund.digest(), &msg.tx_early_refund_sig) - .context("Failed to verify early refund transaction")?; + swap_core::bitcoin::verify_sig( + &self.B, + &tx_early_refund.digest(), + &msg.tx_early_refund_sig, + ) + .context("Failed to verify early refund transaction")?; Ok(State3 { a: self.a, @@ -400,13 +420,14 @@ impl State2 { } } +#[allow(non_snake_case)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] pub struct State3 { - a: bitcoin::SecretKey, - B: bitcoin::PublicKey, + a: swap_core::bitcoin::SecretKey, + B: swap_core::bitcoin::PublicKey, pub s_a: monero::Scalar, S_b_monero: monero::PublicKey, - S_b_bitcoin: bitcoin::PublicKey, + S_b_bitcoin: swap_core::bitcoin::PublicKey, pub v: monero::PrivateViewKey, #[serde(with = "::bitcoin::amount::serde::as_sat")] pub btc: bitcoin::Amount, @@ -419,9 +440,9 @@ pub struct State3 { redeem_address: bitcoin::Address, #[serde(with = "swap_serde::bitcoin::address_serde")] punish_address: bitcoin::Address, - pub tx_lock: bitcoin::TxLock, - tx_punish_sig_bob: bitcoin::Signature, - tx_cancel_sig_bob: bitcoin::Signature, + pub tx_lock: swap_core::bitcoin::TxLock, + tx_punish_sig_bob: swap_core::bitcoin::Signature, + tx_cancel_sig_bob: swap_core::bitcoin::Signature, /// This field was added in this pull request: /// https://github.com/eigenwallet/core/pull/344 /// @@ -432,7 +453,7 @@ pub struct State3 { /// to allow Alice to refund the Bitcoin early. If it is not present, Bob will have /// to wait for the timelock to expire. #[serde(default)] - tx_early_refund_sig_bob: Option, + tx_early_refund_sig_bob: Option, #[serde(with = "::bitcoin::amount::serde::as_sat")] tx_redeem_fee: bitcoin::Amount, #[serde(with = "::bitcoin::amount::serde::as_sat")] @@ -446,7 +467,7 @@ pub struct State3 { impl State3 { pub async fn expired_timelocks( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result { let tx_cancel = self.tx_cancel(); @@ -505,7 +526,11 @@ impl State3 { } pub fn tx_refund(&self) -> TxRefund { - bitcoin::TxRefund::new(&self.tx_cancel(), &self.refund_address, self.tx_refund_fee) + swap_core::bitcoin::TxRefund::new( + &self.tx_cancel(), + &self.refund_address, + self.tx_refund_fee, + ) } pub fn tx_redeem(&self) -> TxRedeem { @@ -513,24 +538,30 @@ impl State3 { } pub fn tx_early_refund(&self) -> TxEarlyRefund { - bitcoin::TxEarlyRefund::new(&self.tx_lock, &self.refund_address, self.tx_refund_fee) + swap_core::bitcoin::TxEarlyRefund::new( + &self.tx_lock, + &self.refund_address, + self.tx_refund_fee, + ) } pub fn extract_monero_private_key( &self, published_refund_tx: Arc, ) -> Result { - self.tx_refund().extract_monero_private_key( - published_refund_tx, - self.s_a, - self.a.clone(), - self.S_b_bitcoin, - ) + Ok(monero::PrivateKey::from_scalar( + self.tx_refund().extract_monero_private_key( + published_refund_tx, + self.s_a, + self.a.clone(), + self.S_b_bitcoin, + )?, + )) } pub async fn check_for_tx_cancel( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result>> { let tx_cancel = self.tx_cancel(); let tx = bitcoin_wallet @@ -543,7 +574,7 @@ impl State3 { pub async fn fetch_tx_refund( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result>> { let tx_refund = self.tx_refund(); let tx = bitcoin_wallet @@ -554,59 +585,19 @@ impl State3 { Ok(tx) } - pub async fn submit_tx_cancel(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { + pub async fn submit_tx_cancel( + &self, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, + ) -> Result { let transaction = self.signed_cancel_transaction()?; let (tx_id, _) = bitcoin_wallet.broadcast(transaction, "cancel").await?; Ok(tx_id) } - pub async fn refund_xmr( + pub async fn punish_btc( &self, - monero_wallet: Arc, - swap_id: Uuid, - spend_key: monero::PrivateKey, - transfer_proof: TransferProof, - ) -> Result<()> { - let view_key = self.v; - - // Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations - // on the lock transaction. - tracing::info!("Waiting for Monero lock transaction to be confirmed"); - let transfer_proof_2 = transfer_proof.clone(); - monero_wallet - .wait_until_confirmed( - self.lock_xmr_watch_request(transfer_proof_2, 10), - Some(move |(confirmations, target_confirmations)| { - tracing::debug!( - %confirmations, - %target_confirmations, - "Monero lock transaction got a confirmation" - ); - }), - ) - .await - .context("Failed to wait for Monero lock transaction to be confirmed")?; - - tracing::info!("Refunding Monero"); - - tracing::debug!(%swap_id, "Opening temporary Monero wallet from keys"); - let swap_wallet = monero_wallet - .swap_wallet(swap_id, spend_key, view_key, transfer_proof.tx_hash()) - .await - .context(format!("Failed to open/create swap wallet `{}`", swap_id))?; - - tracing::debug!(%swap_id, "Sweeping Monero to redeem address"); - let main_address = monero_wallet.main_wallet().await.main_address().await; - - swap_wallet - .sweep(&main_address) - .await - .context("Failed to sweep Monero to redeem address")?; - - Ok(()) - } - - pub async fn punish_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, + ) -> Result { let signed_tx_punish = self.signed_punish_transaction()?; let (txid, subscription) = bitcoin_wallet.broadcast(signed_tx_punish, "punish").await?; @@ -617,9 +608,9 @@ impl State3 { pub fn signed_redeem_transaction( &self, - sig: bitcoin::EncryptedSignature, + sig: swap_core::bitcoin::EncryptedSignature, ) -> Result { - bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee) + swap_core::bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee) .complete(sig, self.a.clone(), self.s_a.to_secpfun_scalar(), self.B) .context("Failed to complete Bitcoin redeem transaction") } @@ -653,7 +644,7 @@ impl State3 { } fn tx_punish(&self) -> TxPunish { - bitcoin::TxPunish::new( + swap_core::bitcoin::TxPunish::new( &self.tx_cancel(), &self.punish_address, self.punish_timelock, @@ -663,9 +654,11 @@ impl State3 { pub async fn watch_for_btc_tx_refund( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result { - let tx_refund_status = bitcoin_wallet.subscribe_to(self.tx_refund()).await; + let tx_refund_status = bitcoin_wallet + .subscribe_to(Box::new(self.tx_refund())) + .await; tx_refund_status .wait_until_seen() diff --git a/swap/src/protocol/bob/state.rs b/swap-machine/src/bob/mod.rs similarity index 90% rename from swap/src/protocol/bob/state.rs rename to swap-machine/src/bob/mod.rs index 94cbe695..6a6ea70d 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap-machine/src/bob/mod.rs @@ -1,17 +1,9 @@ -use crate::bitcoin::wallet::{EstimateFeeRate, Subscription}; -use crate::bitcoin::{ - self, current_epoch, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, - TxLock, Txid, Wallet, -}; -use crate::monero::wallet::WatchRequest; -use crate::monero::TransferProof; -use crate::monero::{self, MoneroAddressPool, TxHash}; -use crate::monero_ext::ScalarExt; -use crate::protocol::{Message0, Message1, Message2, Message3, Message4, CROSS_CURVE_PROOF_SYSTEM}; -use anyhow::{anyhow, bail, Context, Result}; +use crate::common::{CROSS_CURVE_PROOF_SYSTEM, Message0, Message1, Message2, Message3, Message4}; +use anyhow::{Context, Result, anyhow, bail}; +use bitcoin_wallet::primitives::Subscription; +use ecdsa_fun::Signature; use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; use ecdsa_fun::nonce::Deterministic; -use ecdsa_fun::Signature; use monero::BlockHeight; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; @@ -19,6 +11,13 @@ use sha2::Sha256; use sigma_fun::ext::dl_secp256k1_ed25519_eq::CrossCurveDLEQProof; use std::fmt; use std::sync::Arc; +use swap_core::bitcoin::{ + self, CancelTimelock, ExpiredTimelocks, PunishTimelock, Transaction, TxCancel, TxLock, Txid, + current_epoch, +}; +use swap_core::monero::ScalarExt; +use swap_core::monero::primitives::WatchRequest; +use swap_core::monero::{self, TransferProof}; use swap_serde::bitcoin::address_serde; use uuid::Uuid; @@ -98,7 +97,7 @@ impl BobState { /// Depending on the State, there are no locks to expire. pub async fn expired_timelocks( &self, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, ) -> Result> { Ok(match self.clone() { BobState::Started { .. } @@ -107,16 +106,16 @@ impl BobState { | BobState::SwapSetupCompleted(_) => None, BobState::BtcLocked { state3: state, .. } | BobState::XmrLockProofReceived { state, .. } => { - Some(state.expired_timelock(&bitcoin_wallet).await?) + Some(state.expired_timelock(bitcoin_wallet.as_ref()).await?) } BobState::XmrLocked(state) | BobState::EncSigSent(state) => { - Some(state.expired_timelock(&bitcoin_wallet).await?) + Some(state.expired_timelock(bitcoin_wallet.as_ref()).await?) } BobState::CancelTimelockExpired(state) | BobState::BtcCancelled(state) | BobState::BtcRefundPublished(state) | BobState::BtcEarlyRefundPublished(state) => { - Some(state.expired_timelock(&bitcoin_wallet).await?) + Some(state.expired_timelock(bitcoin_wallet.as_ref()).await?) } BobState::BtcPunished { .. } => Some(ExpiredTimelocks::Punish), BobState::BtcRefunded(_) @@ -127,6 +126,17 @@ impl BobState { } } +pub fn is_complete(state: &BobState) -> bool { + matches!( + state, + BobState::BtcRefunded(..) + | BobState::BtcEarlyRefunded { .. } + | BobState::XmrRedeemed { .. } + | BobState::SafelyAborted + ) +} + +#[allow(non_snake_case)] #[derive(Clone, Debug, PartialEq)] pub struct State0 { swap_id: Uuid, @@ -207,10 +217,7 @@ impl State0 { pub async fn receive( self, - wallet: &bitcoin::Wallet< - bdk_wallet::rusqlite::Connection, - impl EstimateFeeRate + Send + Sync + 'static, - >, + wallet: &dyn bitcoin_wallet::BitcoinWallet, msg: Message1, ) -> Result { let valid = CROSS_CURVE_PROOF_SYSTEM.verify( @@ -228,7 +235,7 @@ impl State0 { bail!("Alice's dleq proof doesn't verify") } - let tx_lock = bitcoin::TxLock::new( + let tx_lock = swap_core::bitcoin::TxLock::new( wallet, self.btc, self.tx_lock_fee, @@ -262,6 +269,7 @@ impl State0 { } } +#[allow(non_snake_case)] #[derive(Debug)] pub struct State1 { A: bitcoin::PublicKey, @@ -336,6 +344,7 @@ impl State1 { } } +#[allow(non_snake_case)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct State2 { A: bitcoin::PublicKey, @@ -427,6 +436,7 @@ impl State2 { } } +#[allow(non_snake_case)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct State3 { A: bitcoin::PublicKey, @@ -525,7 +535,7 @@ impl State3 { pub async fn expired_timelock( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result { let tx_cancel = TxCancel::new( &self.tx_lock, @@ -552,7 +562,7 @@ impl State3 { pub async fn check_for_tx_early_refund( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result>> { let tx_early_refund = self.construct_tx_early_refund(); let tx = bitcoin_wallet @@ -564,6 +574,7 @@ impl State3 { } } +#[allow(non_snake_case)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct State4 { A: bitcoin::PublicKey, @@ -594,7 +605,7 @@ pub struct State4 { impl State4 { pub async fn check_for_tx_redeem( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result> { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee); @@ -606,7 +617,9 @@ impl State4 { let tx_redeem_sig = tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?; let s_a = bitcoin::recover(self.S_a_bitcoin, tx_redeem_sig, tx_redeem_encsig)?; - let s_a = monero::private_key_from_secp256k1_scalar(s_a.into()); + let s_a = monero::PrivateKey::from_scalar(monero::private_key_from_secp256k1_scalar( + s_a.into(), + )); Ok(Some(State5 { s_a, @@ -628,12 +641,15 @@ impl State4 { self.b.encsign(self.S_a_bitcoin, tx_redeem.digest()) } - pub async fn watch_for_redeem_btc(&self, bitcoin_wallet: &bitcoin::Wallet) -> Result { + pub async fn watch_for_redeem_btc( + &self, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, + ) -> Result { let tx_redeem = bitcoin::TxRedeem::new(&self.tx_lock, &self.redeem_address, self.tx_redeem_fee); bitcoin_wallet - .subscribe_to(tx_redeem.clone()) + .subscribe_to(Box::new(tx_redeem)) .await .wait_until_seen() .await?; @@ -647,7 +663,7 @@ impl State4 { pub async fn expired_timelock( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result { let tx_cancel = TxCancel::new( &self.tx_lock, @@ -716,13 +732,13 @@ impl State5 { self.tx_lock.txid() } - pub fn lock_xmr_watch_request_for_sweep(&self) -> monero::wallet::WatchRequest { + pub fn lock_xmr_watch_request_for_sweep(&self) -> swap_core::monero::primitives::WatchRequest { let S_b_monero = monero::PublicKey::from_private_key(&monero::PrivateKey::from_scalar(self.s_b)); let S_a_monero = monero::PublicKey::from_private_key(&self.s_a); let S = S_a_monero + S_b_monero; - monero::wallet::WatchRequest { + swap_core::monero::primitives::WatchRequest { public_spend_key: S, public_view_key: self.v.public(), transfer_proof: self.lock_transfer_proof.clone(), @@ -732,50 +748,9 @@ impl State5 { expected_amount: self.xmr.into(), } } - - pub async fn redeem_xmr( - &self, - monero_wallet: &monero::Wallets, - swap_id: Uuid, - monero_receive_pool: MoneroAddressPool, - ) -> Result> { - let (spend_key, view_key) = self.xmr_keys(); - - tracing::info!(%swap_id, "Redeeming Monero from extracted keys"); - - tracing::debug!(%swap_id, "Opening temporary Monero wallet"); - - let wallet = monero_wallet - .swap_wallet( - swap_id, - spend_key, - view_key, - self.lock_transfer_proof.tx_hash(), - ) - .await - .context("Failed to open Monero wallet")?; - - tracing::debug!(%swap_id, receive_address=?monero_receive_pool, "Sweeping Monero to receive address"); - - let main_address = monero_wallet.main_wallet().await.main_address().await; - - let tx_hashes = wallet - .sweep_multi_destination( - &monero_receive_pool.fill_empty_addresses(main_address), - &monero_receive_pool.percentages(), - ) - .await - .context("Failed to redeem Monero")? - .into_iter() - .map(|tx_receipt| TxHash(tx_receipt.txid)) - .collect(); - - tracing::info!(%swap_id, txids=?tx_hashes, "Monero sweep completed"); - - Ok(tx_hashes) - } } +#[allow(non_snake_case)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct State6 { A: bitcoin::PublicKey, @@ -800,7 +775,7 @@ pub struct State6 { impl State6 { pub async fn expired_timelock( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result { let tx_cancel = TxCancel::new( &self.tx_lock, @@ -833,7 +808,7 @@ impl State6 { pub async fn check_for_tx_cancel( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result>> { let tx_cancel = self.construct_tx_cancel()?; @@ -847,7 +822,7 @@ impl State6 { pub async fn submit_tx_cancel( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result<(Txid, Subscription)> { let transaction = self .construct_tx_cancel()? @@ -861,7 +836,7 @@ impl State6 { pub async fn publish_refund_btc( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result { let signed_tx_refund = self.signed_refund_transaction()?; let signed_tx_refund_txid = signed_tx_refund.compute_txid(); @@ -922,7 +897,7 @@ impl State6 { pub async fn check_for_tx_early_refund( &self, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: &dyn bitcoin_wallet::BitcoinWallet, ) -> Result>> { let tx_early_refund = self.construct_tx_early_refund(); diff --git a/swap-machine/src/common/mod.rs b/swap-machine/src/common/mod.rs new file mode 100644 index 00000000..80bae536 --- /dev/null +++ b/swap-machine/src/common/mod.rs @@ -0,0 +1,169 @@ +use crate::alice::AliceState; +use crate::alice::is_complete as alice_is_complete; +use crate::bob::BobState; +use crate::bob::is_complete as bob_is_complete; +use anyhow::Result; +use async_trait::async_trait; +use conquer_once::Lazy; +use libp2p::{Multiaddr, PeerId}; +use serde::{Deserialize, Serialize}; +use sha2::Sha256; +use sigma_fun::HashTranscript; +use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof}; +use std::convert::TryInto; +use swap_core::bitcoin; +use swap_core::monero::{self, MoneroAddressPool}; +use uuid::Uuid; + +pub static CROSS_CURVE_PROOF_SYSTEM: Lazy< + CrossCurveDLEQ>, +> = Lazy::new(|| { + CrossCurveDLEQ::>::new( + (*ecdsa_fun::fun::G).normalize(), + curve25519_dalek::constants::ED25519_BASEPOINT_POINT, + ) +}); + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message0 { + pub swap_id: Uuid, + pub B: bitcoin::PublicKey, + pub S_b_monero: monero::PublicKey, + pub S_b_bitcoin: bitcoin::PublicKey, + pub dleq_proof_s_b: CrossCurveDLEQProof, + pub v_b: monero::PrivateViewKey, + #[serde(with = "swap_serde::bitcoin::address_serde")] + pub refund_address: bitcoin::Address, + #[serde(with = "::bitcoin::amount::serde::as_sat")] + pub tx_refund_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::amount::serde::as_sat")] + pub tx_cancel_fee: bitcoin::Amount, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message1 { + pub A: bitcoin::PublicKey, + pub S_a_monero: monero::PublicKey, + pub S_a_bitcoin: bitcoin::PublicKey, + pub dleq_proof_s_a: CrossCurveDLEQProof, + pub v_a: monero::PrivateViewKey, + #[serde(with = "swap_serde::bitcoin::address_serde")] + pub redeem_address: bitcoin::Address, + #[serde(with = "swap_serde::bitcoin::address_serde")] + pub punish_address: bitcoin::Address, + #[serde(with = "::bitcoin::amount::serde::as_sat")] + pub tx_redeem_fee: bitcoin::Amount, + #[serde(with = "::bitcoin::amount::serde::as_sat")] + pub tx_punish_fee: bitcoin::Amount, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message2 { + pub psbt: bitcoin::PartiallySignedTransaction, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message3 { + pub tx_cancel_sig: bitcoin::Signature, + pub tx_refund_encsig: bitcoin::EncryptedSignature, +} + +#[allow(non_snake_case)] +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message4 { + pub tx_punish_sig: bitcoin::Signature, + pub tx_cancel_sig: bitcoin::Signature, + pub tx_early_refund_sig: bitcoin::Signature, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Clone, Debug, PartialEq)] +pub enum State { + Alice(AliceState), + Bob(BobState), +} + +impl State { + pub fn swap_finished(&self) -> bool { + match self { + State::Alice(state) => alice_is_complete(state), + State::Bob(state) => bob_is_complete(state), + } + } +} + +impl From for State { + fn from(alice: AliceState) -> Self { + Self::Alice(alice) + } +} + +impl From for State { + fn from(bob: BobState) -> Self { + Self::Bob(bob) + } +} + +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] +#[error("Not in the role of Alice")] +pub struct NotAlice; + +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] +#[error("Not in the role of Bob")] +pub struct NotBob; + +impl TryInto for State { + type Error = NotBob; + + fn try_into(self) -> std::result::Result { + match self { + State::Alice(_) => Err(NotBob), + State::Bob(state) => Ok(state), + } + } +} + +impl TryInto for State { + type Error = NotAlice; + + fn try_into(self) -> std::result::Result { + match self { + State::Alice(state) => Ok(state), + State::Bob(_) => Err(NotAlice), + } + } +} + +#[async_trait] +pub trait Database { + async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()>; + async fn get_peer_id(&self, swap_id: Uuid) -> Result; + async fn insert_monero_address_pool( + &self, + swap_id: Uuid, + address: MoneroAddressPool, + ) -> Result<()>; + async fn get_monero_address_pool(&self, swap_id: Uuid) -> Result; + async fn get_monero_addresses(&self) -> Result>; + async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>; + async fn get_addresses(&self, peer_id: PeerId) -> Result>; + async fn get_all_peer_addresses(&self) -> Result)>>; + async fn get_swap_start_date(&self, swap_id: Uuid) -> Result; + async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>; + async fn get_state(&self, swap_id: Uuid) -> Result; + async fn get_states(&self, swap_id: Uuid) -> Result>; + async fn all(&self) -> Result>; + async fn insert_buffered_transfer_proof( + &self, + swap_id: Uuid, + proof: monero::TransferProof, + ) -> Result<()>; + async fn get_buffered_transfer_proof( + &self, + swap_id: Uuid, + ) -> Result>; +} diff --git a/swap-machine/src/lib.rs b/swap-machine/src/lib.rs new file mode 100644 index 00000000..e400f993 --- /dev/null +++ b/swap-machine/src/lib.rs @@ -0,0 +1,3 @@ +pub mod alice; +pub mod bob; +pub mod common; diff --git a/swap-orchestrator/Cargo.toml b/swap-orchestrator/Cargo.toml index f214518f..a30ab2a4 100644 --- a/swap-orchestrator/Cargo.toml +++ b/swap-orchestrator/Cargo.toml @@ -8,7 +8,6 @@ name = "orchestrator" path = "src/main.rs" [dependencies] -anyhow = { workspace = true } bitcoin = { workspace = true } chrono = "0.4.41" compose_spec = "0.3.0" @@ -18,6 +17,7 @@ serde_yaml = "0.9.34" swap-env = { path = "../swap-env" } toml = { workspace = true } url = { workspace = true } +anyhow = { workspace = true } [build-dependencies] anyhow = { workspace = true } diff --git a/swap-orchestrator/src/main.rs b/swap-orchestrator/src/main.rs index c981cf96..3638a29d 100644 --- a/swap-orchestrator/src/main.rs +++ b/swap-orchestrator/src/main.rs @@ -4,8 +4,8 @@ mod images; mod prompt; use crate::compose::{ - IntoSpec, OrchestratorDirectories, OrchestratorImage, OrchestratorImages, OrchestratorInput, - OrchestratorNetworks, ASB_DATA_DIR, DOCKER_COMPOSE_FILE, + ASB_DATA_DIR, DOCKER_COMPOSE_FILE, IntoSpec, OrchestratorDirectories, OrchestratorImage, + OrchestratorImages, OrchestratorInput, OrchestratorNetworks, }; use std::path::PathBuf; use swap_env::config::{ diff --git a/swap-orchestrator/src/prompt.rs b/swap-orchestrator/src/prompt.rs index 85085729..9b58fb55 100644 --- a/swap-orchestrator/src/prompt.rs +++ b/swap-orchestrator/src/prompt.rs @@ -1,4 +1,4 @@ -use dialoguer::{theme::ColorfulTheme, Select}; +use dialoguer::{Select, theme::ColorfulTheme}; use swap_env::prompt as config_prompt; use url::Url; diff --git a/swap-proptest/Cargo.toml b/swap-proptest/Cargo.toml new file mode 100644 index 00000000..edd2917d --- /dev/null +++ b/swap-proptest/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "swap-proptest" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitcoin = { workspace = true } +ecdsa_fun = { workspace = true } +proptest = { workspace = true } + +[lints] +workspace = true diff --git a/swap/src/proptest.rs b/swap-proptest/src/lib.rs similarity index 100% rename from swap/src/proptest.rs rename to swap-proptest/src/lib.rs diff --git a/swap-serde/src/electrum.rs b/swap-serde/src/electrum.rs index 9c36f12a..35e23070 100644 --- a/swap-serde/src/electrum.rs +++ b/swap-serde/src/electrum.rs @@ -1,6 +1,6 @@ pub mod urls { use serde::de::Unexpected; - use serde::{de, Deserialize, Deserializer}; + use serde::{Deserialize, Deserializer, de}; use serde_json::Value; use url::Url; diff --git a/swap-serde/src/libp2p.rs b/swap-serde/src/libp2p.rs index 23f4cf2a..2a96629b 100644 --- a/swap-serde/src/libp2p.rs +++ b/swap-serde/src/libp2p.rs @@ -1,7 +1,7 @@ pub mod multiaddresses { use libp2p::Multiaddr; use serde::de::Unexpected; - use serde::{de, Deserialize, Deserializer}; + use serde::{Deserialize, Deserializer, de}; use serde_json::Value; pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> diff --git a/swap-serde/src/monero.rs b/swap-serde/src/monero.rs index e317fc78..28e27b1f 100644 --- a/swap-serde/src/monero.rs +++ b/swap-serde/src/monero.rs @@ -12,11 +12,11 @@ pub enum network { pub mod private_key { use hex; - use monero::consensus::{Decodable, Encodable}; use monero::PrivateKey; + use monero::consensus::{Decodable, Encodable}; use serde::de::Visitor; use serde::ser::Error; - use serde::{de, Deserializer, Serializer}; + use serde::{Deserializer, Serializer, de}; use std::fmt; use std::io::Cursor; @@ -100,7 +100,7 @@ pub mod amount { } pub mod address { - use anyhow::{bail, Context, Result}; + use anyhow::{Context, Result, bail}; use std::str::FromStr; #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 0ce298f2..2279b28f 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -22,23 +22,24 @@ bdk_wallet = { workspace = true, features = ["rusqlite", "test-utils"] } anyhow = { workspace = true } arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service"] } async-compression = { version = "0.3", features = ["bzip2", "tokio"] } -async-trait = "0.1" +async-trait = { workspace = true } asynchronous-codec = "0.7.0" atty = "0.2" backoff = { workspace = true } base64 = "0.22" big-bytes = "1" bitcoin = { workspace = true } +bitcoin-wallet = { path = "../bitcoin-wallet" } bmrng = "0.5.2" comfy-table = "7.1" config = { version = "0.14", default-features = false, features = ["toml"] } conquer-once = "0.4" -curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" } +curve25519-dalek = { workspace = true } data-encoding = "2.6" derive_builder = "0.20.2" dfx-swiss-sdk = { workspace = true, optional = true } dialoguer = "0.11" -ecdsa_fun = { version = "0.10", default-features = false, features = ["libsecp_compat", "serde", "adaptor"] } +ecdsa_fun = { workspace = true, features = ["libsecp_compat", "serde", "adaptor"] } ed25519-dalek = "1" electrum-pool = { path = "../electrum-pool" } fns = "0.0.7" @@ -57,7 +58,7 @@ once_cell = { workspace = true } pem = "3.0" proptest = "1" rand = { workspace = true } -rand_chacha = "0.3" +rand_chacha = { workspace = true } regex = "1.10" reqwest = { workspace = true, features = ["http2", "rustls-tls-native-roots", "stream", "socks"] } rust_decimal = { version = "1", features = ["serde-float"] } @@ -68,15 +69,17 @@ serde = { workspace = true } serde_cbor = "0.11" serde_json = { workspace = true } serde_with = { version = "1", features = ["macros"] } -sha2 = "0.10" -sigma_fun = { version = "0.7", default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] } +sha2 = { workspace = true } +sigma_fun = { workspace = true, default-features = false, features = ["ed25519", "serde", "secp256k1", "alloc"] } sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] } structopt = "0.3" strum = { version = "0.26", features = ["derive"] } swap-controller-api = { path = "../swap-controller-api" } +swap-core = { path = "../swap-core" } swap-env = { path = "../swap-env" } swap-feed = { path = "../swap-feed" } swap-fs = { path = "../swap-fs" } +swap-machine = { path = "../swap-machine" } swap-serde = { path = "../swap-serde" } tauri = { version = "2.0", features = ["config-json5"], optional = true, default-features = false } thiserror = { workspace = true } diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 3de5d1b9..b6cf6c74 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -2,6 +2,7 @@ use self::quote::{ make_quote, unlocked_monero_balance_with_timeout, QuoteCacheKey, QUOTE_CACHE_TTL, }; use crate::asb::{Behaviour, OutEvent}; +use crate::monero; use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason; use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; use crate::network::quote::BidQuote; @@ -10,8 +11,8 @@ use crate::network::transfer_proof; use crate::protocol::alice::swap::has_already_processed_enc_sig; use crate::protocol::alice::{AliceState, State3, Swap, TipConfig}; use crate::protocol::{Database, State}; -use crate::{bitcoin, monero}; use anyhow::{anyhow, Context, Result}; +use bitcoin_wallet::BitcoinWallet; use futures::future; use futures::future::{BoxFuture, FutureExt}; use futures::stream::{FuturesUnordered, StreamExt}; @@ -26,6 +27,7 @@ use std::fmt::Debug; use std::io::Write; use std::sync::Arc; use std::time::Duration; +use swap_core::bitcoin; use swap_env::env; use swap_feed::LatestRate; use tokio::fs::{write, File}; @@ -41,7 +43,7 @@ where { swarm: libp2p::Swarm>, env_config: env::Config, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, monero_wallet: Arc, db: Arc, latest_rate: LR, @@ -132,7 +134,7 @@ where pub fn new( swarm: Swarm>, env_config: env::Config, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, monero_wallet: Arc, db: Arc, latest_rate: LR, @@ -245,7 +247,7 @@ where } }; - let wallet_snapshot = match WalletSnapshot::capture(&self.bitcoin_wallet, &self.monero_wallet, &self.external_redeem_address, btc).await { + let wallet_snapshot = match WalletSnapshot::capture(self.bitcoin_wallet.clone(), &self.monero_wallet, &self.external_redeem_address, btc).await { Ok(wallet_snapshot) => wallet_snapshot, Err(error) => { tracing::error!("Swap request will be ignored because we were unable to create wallet snapshot for swap: {:#}", error); @@ -1188,7 +1190,7 @@ mod tests { rate.clone(), || async { Ok(balance) }, || async { Ok(reserved_items) }, - None, + Decimal::ZERO, ) .await .unwrap(); diff --git a/swap/src/asb/recovery/punish.rs b/swap/src/asb/recovery/punish.rs index 8446c8f7..bd5596b6 100644 --- a/swap/src/asb/recovery/punish.rs +++ b/swap/src/asb/recovery/punish.rs @@ -1,9 +1,10 @@ -use crate::bitcoin::{self, Txid}; use crate::protocol::alice::AliceState; use crate::protocol::Database; use anyhow::{bail, Result}; +use bitcoin_wallet::BitcoinWallet; use std::convert::TryInto; use std::sync::Arc; +use swap_core::bitcoin::Txid; use uuid::Uuid; #[derive(Debug, thiserror::Error)] @@ -14,7 +15,7 @@ pub enum Error { pub async fn punish( swap_id: Uuid, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, db: Arc, ) -> Result<(Txid, AliceState)> { let state = db.get_state(swap_id).await?.try_into()?; @@ -47,7 +48,7 @@ pub async fn punish( tracing::info!(%swap_id, "Trying to manually punish swap"); - let txid = state3.punish_btc(&bitcoin_wallet).await?; + let txid = state3.punish_btc(bitcoin_wallet.as_ref()).await?; let state = AliceState::BtcPunished { state3: state3.clone(), diff --git a/swap/src/asb/recovery/redeem.rs b/swap/src/asb/recovery/redeem.rs index d2d300e8..c78bf164 100644 --- a/swap/src/asb/recovery/redeem.rs +++ b/swap/src/asb/recovery/redeem.rs @@ -1,9 +1,10 @@ -use crate::bitcoin::{Txid, Wallet}; +use crate::bitcoin::Wallet; use crate::protocol::alice::AliceState; use crate::protocol::Database; use anyhow::{bail, Result}; use std::convert::TryInto; use std::sync::Arc; +use swap_core::bitcoin::Txid; use uuid::Uuid; pub enum Finality { @@ -60,7 +61,9 @@ pub async fn redeem( Ok((txid, state)) } AliceState::BtcRedeemTransactionPublished { state3, .. } => { - let subscription = bitcoin_wallet.subscribe_to(state3.tx_redeem()).await; + let subscription = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_redeem())) + .await; if let Finality::Await = finality { subscription.wait_until_final().await?; diff --git a/swap/src/asb/recovery/refund.rs b/swap/src/asb/recovery/refund.rs index e144c346..33caabc7 100644 --- a/swap/src/asb/recovery/refund.rs +++ b/swap/src/asb/recovery/refund.rs @@ -1,9 +1,10 @@ -use crate::bitcoin::{self}; use crate::common::retry; use crate::monero; +use crate::protocol::alice::swap::XmrRefundable; use crate::protocol::alice::AliceState; use crate::protocol::Database; use anyhow::{bail, Result}; +use bitcoin_wallet::BitcoinWallet; use libp2p::PeerId; use std::convert::TryInto; use std::sync::Arc; @@ -27,7 +28,7 @@ pub enum Error { pub async fn refund( swap_id: Uuid, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, monero_wallet: Arc, db: Arc, ) -> Result { diff --git a/swap/src/asb/rpc/server.rs b/swap/src/asb/rpc/server.rs index 2b48d000..8e9237f1 100644 --- a/swap/src/asb/rpc/server.rs +++ b/swap/src/asb/rpc/server.rs @@ -1,7 +1,8 @@ use crate::asb::event_loop::EventLoopService; +use crate::monero; use crate::protocol::Database; -use crate::{bitcoin, monero}; use anyhow::{Context, Result}; +use bitcoin_wallet::BitcoinWallet; use jsonrpsee::server::{ServerBuilder, ServerHandle}; use jsonrpsee::types::error::ErrorCode; use jsonrpsee::types::ErrorObjectOwned; @@ -20,7 +21,7 @@ impl RpcServer { pub async fn start( host: String, port: u16, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, monero_wallet: Arc, event_loop_service: EventLoopService, db: Arc, @@ -54,7 +55,7 @@ impl RpcServer { } pub struct RpcImpl { - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, monero_wallet: Arc, event_loop_service: EventLoopService, db: Arc, diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 9de73e93..9d61ace8 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -1,788 +1,3 @@ pub mod wallet; - -mod cancel; -mod early_refund; -mod lock; -mod punish; -mod redeem; -mod refund; -mod timelocks; - -pub use crate::bitcoin::cancel::{CancelTimelock, PunishTimelock, TxCancel}; -pub use crate::bitcoin::early_refund::TxEarlyRefund; -pub use crate::bitcoin::lock::TxLock; -pub use crate::bitcoin::punish::TxPunish; -pub use crate::bitcoin::redeem::TxRedeem; -pub use crate::bitcoin::refund::TxRefund; -pub use crate::bitcoin::timelocks::{BlockHeight, ExpiredTimelocks}; -pub use ::bitcoin::amount::Amount; -pub use ::bitcoin::psbt::Psbt as PartiallySignedTransaction; -pub use ::bitcoin::{Address, AddressType, Network, Transaction, Txid}; -pub use ecdsa_fun::adaptor::EncryptedSignature; -pub use ecdsa_fun::fun::Scalar; -pub use ecdsa_fun::Signature; -pub use wallet::Wallet; - -#[cfg(test)] -pub use wallet::TestWalletBuilder; - -use crate::bitcoin::wallet::ScriptStatus; -use ::bitcoin::hashes::Hash; -use ::bitcoin::secp256k1::ecdsa; -use ::bitcoin::sighash::SegwitV0Sighash as Sighash; -use anyhow::{bail, Context, Result}; -use bdk_wallet::miniscript::descriptor::Wsh; -use bdk_wallet::miniscript::{Descriptor, Segwitv0}; -use ecdsa_fun::adaptor::{Adaptor, HashTranscript}; -use ecdsa_fun::fun::Point; -use ecdsa_fun::nonce::Deterministic; -use ecdsa_fun::ECDSA; -use rand::{CryptoRng, RngCore}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use std::str::FromStr; - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -pub struct SecretKey { - inner: Scalar, - public: Point, -} - -impl SecretKey { - pub fn new_random(rng: &mut R) -> Self { - let scalar = Scalar::random(rng); - - let ecdsa = ECDSA::<()>::default(); - let public = ecdsa.verification_key_for(&scalar); - - Self { - inner: scalar, - public, - } - } - - pub fn public(&self) -> PublicKey { - PublicKey(self.public) - } - - pub fn to_bytes(&self) -> [u8; 32] { - self.inner.to_bytes() - } - - pub fn sign(&self, digest: Sighash) -> Signature { - let ecdsa = ECDSA::>::default(); - - ecdsa.sign(&self.inner, &digest.to_byte_array()) - } - - // TxRefund encsigning explanation: - // - // A and B, are the Bitcoin Public Keys which go on the joint output for - // TxLock_Bitcoin. S_a and S_b, are the Monero Public Keys which go on the - // joint output for TxLock_Monero - - // tx_refund: multisig(A, B), published by bob - // bob can produce sig on B using b - // alice sends over an encrypted signature on A encrypted with S_b - // s_b is leaked to alice when bob publishes signed tx_refund allowing her to - // recover s_b: recover(encsig, S_b, sig_tx_refund) = s_b - // alice now has s_a and s_b and can refund monero - - // self = a, Y = S_b, digest = tx_refund - pub fn encsign(&self, Y: PublicKey, digest: Sighash) -> EncryptedSignature { - let adaptor = Adaptor::< - HashTranscript, - Deterministic, - >::default(); - - adaptor.encrypted_sign(&self.inner, &Y.0, &digest.to_byte_array()) - } -} - -#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct PublicKey(Point); - -impl PublicKey { - #[cfg(test)] - pub fn random() -> Self { - Self(Point::random(&mut rand::thread_rng())) - } -} - -impl From for Point { - fn from(from: PublicKey) -> Self { - from.0 - } -} - -impl TryFrom for bitcoin::PublicKey { - type Error = bitcoin::key::FromSliceError; - - fn try_from(pubkey: PublicKey) -> Result { - let bytes = pubkey.0.to_bytes(); - bitcoin::PublicKey::from_slice(&bytes) - } -} - -impl From for PublicKey { - fn from(p: Point) -> Self { - Self(p) - } -} - -impl From for SecretKey { - fn from(scalar: Scalar) -> Self { - let ecdsa = ECDSA::<()>::default(); - let public = ecdsa.verification_key_for(&scalar); - - Self { - inner: scalar, - public, - } - } -} - -impl From for Scalar { - fn from(sk: SecretKey) -> Self { - sk.inner - } -} - -impl From for PublicKey { - fn from(scalar: Scalar) -> Self { - let ecdsa = ECDSA::<()>::default(); - PublicKey(ecdsa.verification_key_for(&scalar)) - } -} - -pub fn verify_sig( - verification_key: &PublicKey, - transaction_sighash: &Sighash, - sig: &Signature, -) -> Result<()> { - let ecdsa = ECDSA::verify_only(); - - if ecdsa.verify( - &verification_key.0, - &transaction_sighash.to_byte_array(), - sig, - ) { - Ok(()) - } else { - bail!(InvalidSignature) - } -} - -#[derive(Debug, Clone, Copy, thiserror::Error)] -#[error("signature is invalid")] -pub struct InvalidSignature; - -pub fn verify_encsig( - verification_key: PublicKey, - encryption_key: PublicKey, - digest: &Sighash, - encsig: &EncryptedSignature, -) -> Result<()> { - let adaptor = Adaptor::, Deterministic>::default(); - - if adaptor.verify_encrypted_signature( - &verification_key.0, - &encryption_key.0, - &digest.to_byte_array(), - encsig, - ) { - Ok(()) - } else { - bail!(InvalidEncryptedSignature) - } -} - -#[derive(Clone, Copy, Debug, thiserror::Error)] -#[error("encrypted signature is invalid")] -pub struct InvalidEncryptedSignature; - -pub fn build_shared_output_descriptor( - A: Point, - B: Point, -) -> Result> { - const MINISCRIPT_TEMPLATE: &str = "c:and_v(v:pk(A),pk_k(B))"; - - let miniscript = MINISCRIPT_TEMPLATE - .replace('A', &A.to_string()) - .replace('B', &B.to_string()); - - let miniscript = - bdk_wallet::miniscript::Miniscript::::from_str(&miniscript) - .expect("a valid miniscript"); - - Ok(Descriptor::Wsh(Wsh::new(miniscript)?)) -} - -pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { - let adaptor = Adaptor::, Deterministic>::default(); - - let s = adaptor - .recover_decryption_key(&S.0, &sig, &encsig) - .map(SecretKey::from) - .context("Failed to recover secret from adaptor signature")?; - - Ok(s) -} - -pub fn current_epoch( - cancel_timelock: CancelTimelock, - punish_timelock: PunishTimelock, - tx_lock_status: ScriptStatus, - tx_cancel_status: ScriptStatus, -) -> ExpiredTimelocks { - if tx_cancel_status.is_confirmed_with(punish_timelock) { - return ExpiredTimelocks::Punish; - } - - if tx_lock_status.is_confirmed_with(cancel_timelock) { - return ExpiredTimelocks::Cancel { - blocks_left: tx_cancel_status.blocks_left_until(punish_timelock), - }; - } - - ExpiredTimelocks::None { - blocks_left: tx_lock_status.blocks_left_until(cancel_timelock), - } -} - -pub mod bitcoin_address { - use anyhow::{Context, Result}; - use bitcoin::{ - address::{NetworkChecked, NetworkUnchecked}, - Address, - }; - use serde::Serialize; - use std::str::FromStr; - - #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)] - #[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")] - pub struct BitcoinAddressNetworkMismatch { - #[serde(with = "swap_serde::bitcoin::network")] - expected: bitcoin::Network, - #[serde(with = "swap_serde::bitcoin::network")] - actual: bitcoin::Network, - } - - pub fn parse(addr_str: &str) -> Result> { - let address = bitcoin::Address::from_str(addr_str)?; - - if address.assume_checked_ref().address_type() != Some(bitcoin::AddressType::P2wpkh) { - anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!") - } - - Ok(address) - } - - /// Parse the address and validate the network. - pub fn parse_and_validate_network( - address: &str, - expected_network: bitcoin::Network, - ) -> Result { - let addres = bitcoin::Address::from_str(address)?; - let addres = addres.require_network(expected_network).with_context(|| { - format!("Bitcoin address network mismatch, expected `{expected_network:?}`") - })?; - Ok(addres) - } - - /// Parse the address and validate the network. - pub fn parse_and_validate(address: &str, is_testnet: bool) -> Result { - let expected_network = if is_testnet { - bitcoin::Network::Testnet - } else { - bitcoin::Network::Bitcoin - }; - parse_and_validate_network(address, expected_network) - } - - /// Validate the address network. - pub fn validate( - address: Address, - is_testnet: bool, - ) -> Result> { - let expected_network = if is_testnet { - bitcoin::Network::Testnet - } else { - bitcoin::Network::Bitcoin - }; - validate_network(address, expected_network) - } - - /// Validate the address network. - pub fn validate_network( - address: Address, - expected_network: bitcoin::Network, - ) -> Result> { - address - .require_network(expected_network) - .context("Bitcoin address network mismatch") - } - - /// Validate the address network even though the address is already checked. - pub fn revalidate_network( - address: Address, - expected_network: bitcoin::Network, - ) -> Result
{ - address - .as_unchecked() - .clone() - .require_network(expected_network) - .context("bitcoin address network mismatch") - } - - /// Validate the address network even though the address is already checked. - pub fn revalidate(address: Address, is_testnet: bool) -> Result
{ - revalidate_network( - address, - if is_testnet { - bitcoin::Network::Testnet - } else { - bitcoin::Network::Bitcoin - }, - ) - } -} - -// Transform the ecdsa der signature bytes into a secp256kfun ecdsa signature type. -pub fn extract_ecdsa_sig(sig: &[u8]) -> Result { - let data = &sig[..sig.len() - 1]; - let sig = ecdsa::Signature::from_der(data)?.serialize_compact(); - Signature::from_bytes(sig).ok_or(anyhow::anyhow!("invalid signature")) -} - -/// Bitcoin error codes: https://github.com/bitcoin/bitcoin/blob/97d3500601c1d28642347d014a6de1e38f53ae4e/src/rpc/protocol.h#L23 -pub enum RpcErrorCode { - /// Transaction or block was rejected by network rules. Error code -26. - RpcVerifyRejected, - /// Transaction or block was rejected by network rules. Error code -27. - RpcVerifyAlreadyInChain, - /// General error during transaction or block submission - RpcVerifyError, - /// Invalid address or key. Error code -5. Is throwns when a transaction is not found. - /// See: - /// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/mempool.cpp#L470-L472 - /// - https://github.com/bitcoin/bitcoin/blob/ae024137bda9fe189f4e7ccf26dbaffd44cbbeb6/src/rpc/rawtransaction.cpp#L352-L368 - RpcInvalidAddressOrKey, -} - -impl From for i64 { - fn from(code: RpcErrorCode) -> Self { - match code { - RpcErrorCode::RpcVerifyError => -25, - RpcErrorCode::RpcVerifyRejected => -26, - RpcErrorCode::RpcVerifyAlreadyInChain => -27, - RpcErrorCode::RpcInvalidAddressOrKey => -5, - } - } -} - -pub fn parse_rpc_error_code(error: &anyhow::Error) -> anyhow::Result { - // First try to extract an Electrum error from a MultiError if present - if let Some(multi_error) = error.downcast_ref::() { - // Try to find the first Electrum error in the MultiError - for single_error in multi_error.iter() { - if let bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String( - string, - )) = single_error - { - let json = serde_json::from_str( - &string - .replace("sendrawtransaction RPC error:", "") - .replace("daemon error:", ""), - )?; - - let json_map = match json { - serde_json::Value::Object(map) => map, - _ => continue, // Try next error if this one isn't a JSON object - }; - - let error_code_value = match json_map.get("code") { - Some(val) => val, - None => continue, // Try next error if no error code field - }; - - let error_code_number = match error_code_value { - serde_json::Value::Number(num) => num, - _ => continue, // Try next error if error code isn't a number - }; - - if let Some(int) = error_code_number.as_i64() { - return Ok(int); - } - } - } - // If we couldn't extract an RPC error code from any error in the MultiError - bail!( - "Error is of incorrect variant. We expected an Electrum error, but got: {}", - error - ); - } - - // Original logic for direct Electrum errors - let string = match error.downcast_ref::() { - Some(bdk_electrum::electrum_client::Error::Protocol(serde_json::Value::String(string))) => { - string - } - _ => bail!( - "Error is of incorrect variant. We expected an Electrum error, but got: {}", - error - ), - }; - - let json = serde_json::from_str( - &string - .replace("sendrawtransaction RPC error:", "") - .replace("daemon error:", ""), - )?; - - let json_map = match json { - serde_json::Value::Object(map) => map, - _ => bail!("Json error is not json object "), - }; - - let error_code_value = match json_map.get("code") { - Some(val) => val, - None => bail!("No error code field"), - }; - - let error_code_number = match error_code_value { - serde_json::Value::Number(num) => num, - _ => bail!("Error code is not a number"), - }; - - if let Some(int) = error_code_number.as_i64() { - Ok(int) - } else { - bail!("Error code is not an unsigned integer") - } -} - -#[derive(Clone, Copy, thiserror::Error, Debug)] -#[error("transaction does not spend anything")] -pub struct NoInputs; - -#[derive(Clone, Copy, thiserror::Error, Debug)] -#[error("transaction has {0} inputs, expected 1")] -pub struct TooManyInputs(usize); - -#[derive(Clone, Copy, thiserror::Error, Debug)] -#[error("empty witness stack")] -pub struct EmptyWitnessStack; - -#[derive(Clone, Copy, thiserror::Error, Debug)] -#[error("input has {0} witnesses, expected 3")] -pub struct NotThreeWitnesses(usize); - -#[cfg(test)] -mod tests { - use super::*; - use crate::monero::TransferProof; - use crate::protocol::{alice, bob}; - use bitcoin::secp256k1; - use curve25519_dalek::scalar::Scalar; - use ecdsa_fun::fun::marker::{NonZero, Public}; - use monero::PrivateKey; - use rand::rngs::OsRng; - use std::matches; - use swap_env::env::{GetConfig, Regtest}; - use uuid::Uuid; - - #[test] - fn lock_confirmations_le_to_cancel_timelock_no_timelock_expired() { - let tx_lock_status = ScriptStatus::from_confirmations(4); - let tx_cancel_status = ScriptStatus::Unseen; - - let expired_timelock = current_epoch( - CancelTimelock::new(5), - PunishTimelock::new(5), - tx_lock_status, - tx_cancel_status, - ); - - assert!(matches!(expired_timelock, ExpiredTimelocks::None { .. })); - } - - #[test] - fn lock_confirmations_ge_to_cancel_timelock_cancel_timelock_expired() { - let tx_lock_status = ScriptStatus::from_confirmations(5); - let tx_cancel_status = ScriptStatus::Unseen; - - let expired_timelock = current_epoch( - CancelTimelock::new(5), - PunishTimelock::new(5), - tx_lock_status, - tx_cancel_status, - ); - - assert!(matches!(expired_timelock, ExpiredTimelocks::Cancel { .. })); - } - - #[test] - fn cancel_confirmations_ge_to_punish_timelock_punish_timelock_expired() { - let tx_lock_status = ScriptStatus::from_confirmations(10); - let tx_cancel_status = ScriptStatus::from_confirmations(5); - - let expired_timelock = current_epoch( - CancelTimelock::new(5), - PunishTimelock::new(5), - tx_lock_status, - tx_cancel_status, - ); - - assert_eq!(expired_timelock, ExpiredTimelocks::Punish) - } - - #[tokio::test] - async fn calculate_transaction_weights() { - let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) - .build() - .await; - let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) - .build() - .await; - let spending_fee = Amount::from_sat(1_000); - let btc_amount = Amount::from_sat(500_000); - let xmr_amount = crate::monero::Amount::from_piconero(10000); - - let tx_redeem_fee = alice_wallet - .estimate_fee(TxRedeem::weight(), Some(btc_amount)) - .await - .unwrap(); - let tx_punish_fee = alice_wallet - .estimate_fee(TxPunish::weight(), Some(btc_amount)) - .await - .unwrap(); - let tx_lock_fee = alice_wallet - .estimate_fee(TxLock::weight(), Some(btc_amount)) - .await - .unwrap(); - - let redeem_address = alice_wallet.new_address().await.unwrap(); - let punish_address = alice_wallet.new_address().await.unwrap(); - - let config = Regtest::get_config(); - let alice_state0 = alice::State0::new( - btc_amount, - xmr_amount, - config, - redeem_address, - punish_address, - tx_redeem_fee, - tx_punish_fee, - &mut OsRng, - ); - - let bob_state0 = bob::State0::new( - Uuid::new_v4(), - &mut OsRng, - btc_amount, - xmr_amount, - config.bitcoin_cancel_timelock.into(), - config.bitcoin_punish_timelock.into(), - bob_wallet.new_address().await.unwrap(), - config.monero_finality_confirmations, - spending_fee, - spending_fee, - tx_lock_fee, - ); - - let message0 = bob_state0.next_message(); - - let (_, alice_state1) = alice_state0.receive(message0).unwrap(); - let alice_message1 = alice_state1.next_message(); - - let bob_state1 = bob_state0 - .receive(&bob_wallet, alice_message1) - .await - .unwrap(); - let bob_message2 = bob_state1.next_message(); - - let alice_state2 = alice_state1.receive(bob_message2).unwrap(); - let alice_message3 = alice_state2.next_message(); - - let bob_state2 = bob_state1.receive(alice_message3).unwrap(); - let bob_message4 = bob_state2.next_message(); - - let alice_state3 = alice_state2.receive(bob_message4).unwrap(); - - let (bob_state3, _tx_lock) = bob_state2.lock_btc().await.unwrap(); - let bob_state4 = bob_state3.xmr_locked( - crate::monero::BlockHeight { height: 0 }, - // We use bogus values here, because they're irrelevant to this test - TransferProof::new( - crate::monero::TxHash("foo".into()), - PrivateKey::from_scalar(Scalar::one()), - ), - ); - let encrypted_signature = bob_state4.tx_redeem_encsig(); - let bob_state6 = bob_state4.cancel(); - - let cancel_transaction = alice_state3.signed_cancel_transaction().unwrap(); - let punish_transaction = alice_state3.signed_punish_transaction().unwrap(); - let redeem_transaction = alice_state3 - .signed_redeem_transaction(encrypted_signature) - .unwrap(); - let refund_transaction = bob_state6.signed_refund_transaction().unwrap(); - - assert_weight(redeem_transaction, TxRedeem::weight().to_wu(), "TxRedeem"); - assert_weight(cancel_transaction, TxCancel::weight().to_wu(), "TxCancel"); - assert_weight(punish_transaction, TxPunish::weight().to_wu(), "TxPunish"); - assert_weight(refund_transaction, TxRefund::weight().to_wu(), "TxRefund"); - - // Test TxEarlyRefund transaction - let early_refund_transaction = alice_state3 - .signed_early_refund_transaction() - .unwrap() - .unwrap(); - assert_weight( - early_refund_transaction, - TxEarlyRefund::weight() as u64, - "TxEarlyRefund", - ); - } - - #[tokio::test] - async fn tx_early_refund_can_be_constructed_and_signed() { - let alice_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) - .build() - .await; - let bob_wallet = TestWalletBuilder::new(Amount::ONE_BTC.to_sat()) - .build() - .await; - let spending_fee = Amount::from_sat(1_000); - let btc_amount = Amount::from_sat(500_000); - let xmr_amount = crate::monero::Amount::from_piconero(10000); - - let tx_redeem_fee = alice_wallet - .estimate_fee(TxRedeem::weight(), Some(btc_amount)) - .await - .unwrap(); - let tx_punish_fee = alice_wallet - .estimate_fee(TxPunish::weight(), Some(btc_amount)) - .await - .unwrap(); - - let refund_address = alice_wallet.new_address().await.unwrap(); - let punish_address = alice_wallet.new_address().await.unwrap(); - - let config = Regtest::get_config(); - let alice_state0 = alice::State0::new( - btc_amount, - xmr_amount, - config, - refund_address.clone(), - punish_address, - tx_redeem_fee, - tx_punish_fee, - &mut OsRng, - ); - - let bob_state0 = bob::State0::new( - Uuid::new_v4(), - &mut OsRng, - btc_amount, - xmr_amount, - config.bitcoin_cancel_timelock.into(), - config.bitcoin_punish_timelock.into(), - bob_wallet.new_address().await.unwrap(), - config.monero_finality_confirmations, - spending_fee, - spending_fee, - spending_fee, - ); - - // Complete the state machine up to State3 - let message0 = bob_state0.next_message(); - let (_, alice_state1) = alice_state0.receive(message0).unwrap(); - let alice_message1 = alice_state1.next_message(); - - let bob_state1 = bob_state0 - .receive(&bob_wallet, alice_message1) - .await - .unwrap(); - let bob_message2 = bob_state1.next_message(); - - let alice_state2 = alice_state1.receive(bob_message2).unwrap(); - let alice_message3 = alice_state2.next_message(); - - let bob_state2 = bob_state1.receive(alice_message3).unwrap(); - let bob_message4 = bob_state2.next_message(); - - let alice_state3 = alice_state2.receive(bob_message4).unwrap(); - - // Test TxEarlyRefund construction - let tx_early_refund = alice_state3.tx_early_refund(); - - // Verify basic properties - assert_eq!(tx_early_refund.txid(), tx_early_refund.txid()); // Should be deterministic - assert!(tx_early_refund.digest() != Sighash::all_zeros()); // Should have valid digest - - // Test that it can be signed and completed - let early_refund_transaction = alice_state3 - .signed_early_refund_transaction() - .unwrap() - .unwrap(); - - // Verify the transaction has expected structure - assert_eq!(early_refund_transaction.input.len(), 1); // One input from lock tx - assert_eq!(early_refund_transaction.output.len(), 1); // One output to refund address - assert_eq!( - early_refund_transaction.output[0].script_pubkey, - refund_address.script_pubkey() - ); - - // Verify the input is spending the lock transaction - assert_eq!( - early_refund_transaction.input[0].previous_output, - alice_state3.tx_lock.as_outpoint() - ); - - // Verify the amount is correct (lock amount minus fee) - let expected_amount = alice_state3.tx_lock.lock_amount() - alice_state3.tx_refund_fee; - assert_eq!(early_refund_transaction.output[0].value, expected_amount); - } - - #[test] - fn tx_early_refund_has_correct_weight() { - // TxEarlyRefund should have the same weight as other similar transactions - assert_eq!(TxEarlyRefund::weight(), 548); - - // It should be the same as TxRedeem and TxRefund weights since they have similar structure - assert_eq!(TxEarlyRefund::weight() as u64, TxRedeem::weight().to_wu()); - assert_eq!(TxEarlyRefund::weight() as u64, TxRefund::weight().to_wu()); - } - - // Weights fluctuate because of the length of the signatures. Valid ecdsa - // signatures can have 68, 69, 70, 71, or 72 bytes. Since most of our - // transactions have 2 signatures the weight can be up to 8 bytes less than - // the static weight (4 bytes per signature). - fn assert_weight(transaction: Transaction, expected_weight: u64, tx_name: &str) { - let is_weight = transaction.weight(); - - assert!( - expected_weight - is_weight.to_wu() <= 8, - "{} to have weight {}, but was {}. Transaction: {:#?}", - tx_name, - expected_weight, - is_weight, - transaction - ) - } - - #[test] - fn compare_point_hex() { - // secp256kfun Point and secp256k1 PublicKey should have the same bytes and hex representation - let secp = secp256k1::Secp256k1::default(); - let keypair = secp256k1::Keypair::new(&secp, &mut OsRng); - - let pubkey = keypair.public_key(); - let point: Point<_, Public, NonZero> = Point::from_bytes(pubkey.serialize()).unwrap(); - - assert_eq!(pubkey.to_string(), point.to_string()); - } -} +pub use swap_core::bitcoin::*; +pub use wallet::*; diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 3aafdfc8..e8ed7c43 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -21,12 +21,14 @@ use bdk_wallet::{Balance, PersistedWallet}; use bitcoin::bip32::Xpriv; use bitcoin::{psbt::Psbt as PartiallySignedTransaction, Txid}; use bitcoin::{ScriptBuf, Weight}; +use derive_builder::Builder; +use electrum_pool::ElectrumBalancer; +use moka; use rust_decimal::prelude::*; use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::collections::BTreeMap; use std::collections::HashMap; -use std::fmt; use std::fmt::Debug; use std::path::Path; use std::path::PathBuf; @@ -34,17 +36,13 @@ use std::sync::Arc; use std::sync::Mutex as SyncMutex; use std::time::Duration; use std::time::Instant; +use swap_core::bitcoin::bitcoin_address::revalidate_network; +use swap_core::bitcoin::BlockHeight; use sync_ext::{CumulativeProgressHandle, InnerSyncCallback, SyncCallbackExt}; use tokio::sync::watch; use tokio::sync::Mutex as TokioMutex; use tracing::{debug_span, Instrument}; -use super::bitcoin_address::revalidate_network; -use super::BlockHeight; -use derive_builder::Builder; -use electrum_pool::ElectrumBalancer; -use moka; - /// We allow transaction fees of up to 20% of the transferred amount to ensure /// that lock transactions can always be published, even when fees are high. const MAX_RELATIVE_TX_FEE: Decimal = dec!(0.20); @@ -239,63 +237,9 @@ pub enum PersisterConfig { InMemorySqlite, } -/// A subscription to the status of a given transaction -/// that can be used to wait for the transaction to be confirmed. -#[derive(Debug, Clone)] -pub struct Subscription { - /// A receiver used to await updates to the status of the transaction. - receiver: watch::Receiver, - /// The number of confirmations we require for a transaction to be considered final. - finality_confirmations: u32, - /// The transaction ID we are subscribing to. - txid: Txid, -} - -/// The possible statuses of a script. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ScriptStatus { - Unseen, - InMempool, - Confirmed(Confirmed), - Retrying, -} - -/// The status of a confirmed transaction. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct Confirmed { - /// The depth of this transaction within the blockchain. - /// - /// Zero if the transaction is included in the latest block. - depth: u32, -} - -/// Defines a watchable transaction. -/// -/// For a transaction to be watchable, we need to know two things: Its -/// transaction ID and the specific output script that is going to change. -/// A transaction can obviously have multiple outputs but our protocol purposes, -/// we are usually interested in a specific one. -pub trait Watchable { - /// The transaction ID. - fn id(&self) -> Txid; - /// The script of the output we are interested in. - fn script(&self) -> ScriptBuf; - /// Convenience method to get both the script and the txid. - fn script_and_txid(&self) -> (ScriptBuf, Txid) { - (self.script(), self.id()) - } -} - -/// An object that can estimate fee rates and minimum relay fees. -pub trait EstimateFeeRate { - /// Estimate the fee rate for a given target block. - fn estimate_feerate( - &self, - target_block: u32, - ) -> impl std::future::Future> + Send; - /// Get the minimum relay fee. - fn min_relay_fee(&self) -> impl std::future::Future> + Send; -} +pub use bitcoin_wallet::primitives::{ + Confirmed, EstimateFeeRate, ScriptStatus, Subscription, Watchable, +}; /// A caching wrapper around EstimateFeeRate implementations. /// @@ -725,7 +669,10 @@ impl Wallet { // to watch for confirmations, watching a single output is enough let subscription = self - .subscribe_to((txid, transaction.output[0].script_pubkey.clone())) + .subscribe_to(Box::new(( + txid, + transaction.output[0].script_pubkey.clone(), + ))) .await; let client = self.electrum_client.lock().await; @@ -808,10 +755,7 @@ impl Wallet { Ok(last_tx.tx_node.txid) } - pub async fn status_of_script(&self, tx: &T) -> Result - where - T: Watchable, - { + pub async fn status_of_script(&self, tx: &dyn Watchable) -> Result { self.electrum_client .lock() .await @@ -819,7 +763,7 @@ impl Wallet { .await } - pub async fn subscribe_to(&self, tx: impl Watchable + Send + Sync + 'static) -> Subscription { + pub async fn subscribe_to(&self, tx: Box) -> Subscription { let txid = tx.id(); let script = tx.script(); @@ -1209,7 +1153,6 @@ where electrum_rate_sat_vb = electrum_rate.to_sat_per_vb_ceil(), mempool_space_rate_sat_vb = mempool_space_rate.to_sat_per_vb_ceil(), "Successfully fetched fee rates from both Electrum and mempool.space. We will use the higher one" - ); Ok(std::cmp::max(electrum_rate, mempool_space_rate)) } @@ -1372,7 +1315,8 @@ where change_override: Option
, ) -> Result { // Check address and change address for network equality. - let address = revalidate_network(address, self.network)?; + let address = + swap_core::bitcoin::bitcoin_address::revalidate_network(address, self.network)?; change_override .as_ref() @@ -1431,11 +1375,14 @@ where change_override: Option
, ) -> Result { // Check address and change address for network equality. - let address = revalidate_network(address, self.network)?; + let address = + swap_core::bitcoin::bitcoin_address::revalidate_network(address, self.network)?; change_override .as_ref() - .map(|a| revalidate_network(a.clone(), self.network)) + .map(|a| { + swap_core::bitcoin::bitcoin_address::revalidate_network(a.clone(), self.network) + }) .transpose() .context("Change address is not on the correct network")?; @@ -1687,7 +1634,7 @@ impl Client { /// As opposed to [`update_state`] this function does not /// check the time since the last update before refreshing /// It therefore also does not take a [`force`] parameter - pub async fn update_state_single(&mut self, script: &impl Watchable) -> Result<()> { + pub async fn update_state_single(&mut self, script: &dyn Watchable) -> Result<()> { self.update_script_history(script).await?; self.update_block_height().await?; @@ -1781,7 +1728,7 @@ impl Client { } /// Update the script history of a single script. - pub async fn update_script_history(&mut self, script: &impl Watchable) -> Result<()> { + pub async fn update_script_history(&mut self, script: &dyn Watchable) -> Result<()> { let (script_buf, _) = script.script_and_txid(); let script_clone = script_buf.clone(); @@ -1858,7 +1805,7 @@ impl Client { /// Get the status of a script. pub async fn status_of_script( &mut self, - script: &impl Watchable, + script: &dyn Watchable, force: bool, ) -> Result { let (script_buf, txid) = script.script_and_txid(); @@ -2106,6 +2053,122 @@ impl Client { } } +#[derive(Clone)] +pub struct SyncRequestBuilderFactory { + chain_tip: bdk_wallet::chain::CheckPoint, + spks: Vec<((KeychainKind, u32), ScriptBuf)>, +} + +impl SyncRequestBuilderFactory { + fn build(self) -> SyncRequestBuilder<(KeychainKind, u32)> { + SyncRequest::builder() + .chain_tip(self.chain_tip) + .spks_with_indexes(self.spks) + } +} + +#[async_trait::async_trait] +impl bitcoin_wallet::BitcoinWallet for Wallet { + async fn balance(&self) -> Result { + Wallet::balance(self).await + } + + async fn balance_info(&self) -> Result { + Wallet::balance_info(self).await + } + + async fn new_address(&self) -> Result
{ + Wallet::new_address(self).await + } + + async fn send_to_address( + &self, + address: Address, + amount: Amount, + spending_fee: Amount, + change_override: Option
, + ) -> Result { + Wallet::send_to_address(self, address, amount, spending_fee, change_override).await + } + + async fn send_to_address_dynamic_fee( + &self, + address: Address, + amount: Amount, + change_override: Option
, + ) -> Result { + Wallet::send_to_address_dynamic_fee(self, address, amount, change_override).await + } + + async fn sweep_balance_to_address_dynamic_fee( + &self, + address: Address, + ) -> Result { + Wallet::sweep_balance_to_address_dynamic_fee(self, address).await + } + + async fn sign_and_finalize(&self, psbt: bitcoin::psbt::Psbt) -> Result { + Wallet::sign_and_finalize(self, psbt).await + } + + async fn broadcast( + &self, + transaction: bitcoin::Transaction, + kind: &str, + ) -> Result<(Txid, bitcoin_wallet::Subscription)> { + Wallet::broadcast(self, transaction, kind).await + } + + async fn sync(&self) -> Result<()> { + Wallet::sync(self).await + } + + async fn subscribe_to( + &self, + tx: Box, + ) -> bitcoin_wallet::Subscription { + Wallet::subscribe_to(self, tx).await + } + + async fn status_of_script( + &self, + tx: &dyn bitcoin_wallet::Watchable, + ) -> Result { + Wallet::status_of_script(self, tx).await + } + + async fn get_raw_transaction( + &self, + txid: Txid, + ) -> Result>> { + Wallet::get_raw_transaction(self, txid).await + } + + async fn max_giveable(&self, locking_script_size: usize) -> Result<(Amount, Amount)> { + Wallet::max_giveable(self, locking_script_size).await + } + + async fn estimate_fee( + &self, + weight: Weight, + transfer_amount: Option, + ) -> Result { + Wallet::estimate_fee(self, weight, transfer_amount).await + } + + fn network(&self) -> Network { + self.network + } + + fn finality_confirmations(&self) -> u32 { + self.finality_confirmations + } + + async fn wallet_export(&self, role: &str) -> Result { + Wallet::wallet_export(self, role).await + } +} + impl EstimateFeeRate for Client { async fn estimate_feerate(&self, target_block: u32) -> Result { // Now that the Electrum client methods are async, we can parallelize the calls @@ -2342,61 +2405,6 @@ fn trace_status_change(txid: Txid, old: Option, new: ScriptStatus) new } -impl Subscription { - pub async fn wait_until_final(&self) -> Result<()> { - let conf_target = self.finality_confirmations; - let txid = self.txid; - - tracing::info!(%txid, required_confirmation=%conf_target, "Waiting for Bitcoin transaction finality"); - - let mut seen_confirmations = 0; - - self.wait_until(|status| match status { - ScriptStatus::Confirmed(inner) => { - let confirmations = inner.confirmations(); - - if confirmations > seen_confirmations { - tracing::info!(%txid, - seen_confirmations = %confirmations, - needed_confirmations = %conf_target, - "Waiting for Bitcoin transaction finality"); - seen_confirmations = confirmations; - } - - inner.meets_target(conf_target) - } - _ => false, - }) - .await - } - - pub async fn wait_until_seen(&self) -> Result<()> { - self.wait_until(ScriptStatus::has_been_seen).await - } - - pub async fn wait_until_confirmed_with(&self, target: T) -> Result<()> - where - T: Into, - T: Copy, - { - self.wait_until(|status| status.is_confirmed_with(target)) - .await - } - - pub async fn wait_until(&self, mut predicate: impl FnMut(&ScriptStatus) -> bool) -> Result<()> { - let mut receiver = self.receiver.clone(); - - while !predicate(&receiver.borrow()) { - receiver - .changed() - .await - .context("Failed while waiting for next status update")?; - } - - Ok(()) - } -} - /// Estimate the absolute fee for a transaction. /// /// This function takes the following parameters: @@ -2617,111 +2625,6 @@ mod mempool_client { } } -impl Watchable for (Txid, ScriptBuf) { - fn id(&self) -> Txid { - self.0 - } - - fn script(&self) -> ScriptBuf { - self.1.clone() - } -} - -impl ScriptStatus { - pub fn from_confirmations(confirmations: u32) -> Self { - match confirmations { - 0 => Self::InMempool, - confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)), - } - } -} - -impl Confirmed { - pub fn new(depth: u32) -> Self { - Self { depth } - } - - /// Compute the depth of a transaction based on its inclusion height and the - /// latest known block. - /// - /// Our information about the latest block might be outdated. To avoid an - /// overflow, we make sure the depth is 0 in case the inclusion height - /// exceeds our latest known block, - pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self { - let depth = latest_block.saturating_sub(inclusion_height); - - Self { depth } - } - - pub fn confirmations(&self) -> u32 { - self.depth + 1 - } - - pub fn meets_target(&self, target: T) -> bool - where - T: Into, - { - self.confirmations() >= target.into() - } - - pub fn blocks_left_until(&self, target: T) -> u32 - where - T: Into + Copy, - { - if self.meets_target(target) { - 0 - } else { - target.into() - self.confirmations() - } - } -} - -impl ScriptStatus { - /// Check if the script has any confirmations. - pub fn is_confirmed(&self) -> bool { - matches!(self, ScriptStatus::Confirmed(_)) - } - - /// Check if the script has met the given confirmation target. - pub fn is_confirmed_with(&self, target: T) -> bool - where - T: Into, - { - match self { - ScriptStatus::Confirmed(inner) => inner.meets_target(target), - _ => false, - } - } - - // Calculate the number of blocks left until the target is met. - pub fn blocks_left_until(&self, target: T) -> u32 - where - T: Into + Copy, - { - match self { - ScriptStatus::Confirmed(inner) => inner.blocks_left_until(target), - _ => target.into(), - } - } - - pub fn has_been_seen(&self) -> bool { - matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_)) - } -} - -impl fmt::Display for ScriptStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ScriptStatus::Unseen => write!(f, "unseen"), - ScriptStatus::InMempool => write!(f, "in mempool"), - ScriptStatus::Retrying => write!(f, "retrying"), - ScriptStatus::Confirmed(inner) => { - write!(f, "confirmed with {} blocks", inner.confirmations()) - } - } - } -} - pub mod pre_1_0_0_bdk { //! This module contains some code for creating a bdk wallet from before the update. //! We need to keep this around to be able to migrate the wallet. @@ -2989,11 +2892,114 @@ mod tests { use super::*; use crate::bitcoin::{PublicKey, TxLock}; use crate::tracing_ext::capture_logs; + use async_trait::async_trait; + use bdk::bitcoin::psbt::Psbt; use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::Hash; + use bitcoin_wallet::BitcoinWallet; use proptest::prelude::*; use tracing::level_filters::LevelFilter; + // Implement BitcoinWallet trait for a stub wallet and panic when the function is not implemented + #[async_trait] + impl BitcoinWallet for Wallet { + async fn balance(&self) -> Result { + unimplemented!("stub method called erroniosly") + } + + async fn balance_info(&self) -> Result { + unimplemented!("stub method called erroniosly") + } + + async fn new_address(&self) -> Result
{ + unimplemented!("stub method called erroniosly") + } + + async fn send_to_address( + &self, + address: Address, + amount: Amount, + spending_fee: Amount, + change_override: Option
, + ) -> Result { + unimplemented!("stub method called erroniosly") + } + + async fn send_to_address_dynamic_fee( + &self, + address: Address, + amount: Amount, + change_override: Option
, + ) -> Result { + unimplemented!("stub method called erroniosly") + } + + async fn sweep_balance_to_address_dynamic_fee( + &self, + address: Address, + ) -> Result { + unimplemented!("stub method called erroniosly") + } + + async fn sign_and_finalize( + &self, + psbt: bitcoin::psbt::Psbt, + ) -> Result { + unimplemented!("stub method called erroniosly") + } + + async fn broadcast( + &self, + transaction: bitcoin::Transaction, + kind: &str, + ) -> Result<(Txid, Subscription)> { + unimplemented!("stub method called erroniosly") + } + + async fn sync(&self) -> Result<()> { + unimplemented!("stub method called erroniosly") + } + + async fn subscribe_to(&self, tx: Box) -> Subscription { + unimplemented!("stub method called erroniosly") + } + + async fn status_of_script(&self, tx: &dyn Watchable) -> Result { + unimplemented!("stub method called erroniosly") + } + + async fn get_raw_transaction( + &self, + txid: Txid, + ) -> Result>> { + unimplemented!("stub method called erroniosly") + } + + async fn max_giveable(&self, locking_script_size: usize) -> Result<(Amount, Amount)> { + unimplemented!("stub method called erroniosly") + } + + async fn estimate_fee( + &self, + weight: Weight, + transfer_amount: Option, + ) -> Result { + unimplemented!("stub method called erroniosly") + } + + fn network(&self) -> Network { + unimplemented!("stub method called erroniosly") + } + + fn finality_confirmations(&self) -> u32 { + unimplemented!("stub method called erroniosly") + } + + async fn wallet_export(&self, role: &str) -> Result { + unimplemented!("stub method called erroniosly") + } + } + #[test] fn given_depth_0_should_meet_confirmation_target_one() { let script = ScriptStatus::Confirmed(Confirmed { depth: 0 }); @@ -3654,17 +3660,3 @@ TRACE swap::bitcoin::wallet: Bitcoin transaction status changed txid=00000000000 } } } - -#[derive(Clone)] -pub struct SyncRequestBuilderFactory { - chain_tip: bdk_wallet::chain::CheckPoint, - spks: Vec<((KeychainKind, u32), ScriptBuf)>, -} - -impl SyncRequestBuilderFactory { - fn build(self) -> SyncRequestBuilder<(KeychainKind, u32)> { - SyncRequest::builder() - .chain_tip(self.chain_tip) - .spks_with_indexes(self.spks) - } -} diff --git a/swap/src/cli/api/request.rs b/swap/src/cli/api/request.rs index f7515c57..19aad7ae 100644 --- a/swap/src/cli/api/request.rs +++ b/swap/src/cli/api/request.rs @@ -1,5 +1,5 @@ use super::tauri_bindings::TauriHandle; -use crate::bitcoin::{wallet, CancelTimelock, ExpiredTimelocks, PunishTimelock}; +use crate::bitcoin::wallet; use crate::cli::api::tauri_bindings::{ ApprovalRequestType, MoneroNodeConfig, SelectMakerDetails, SendMoneroDetails, TauriEmitter, TauriSwapProgressEvent, @@ -14,9 +14,9 @@ use crate::monero::MoneroAddressPool; use crate::network::quote::BidQuote; use crate::network::rendezvous::XmrBtcNamespace; use crate::network::swarm; -use crate::protocol::bob::{BobState, Swap}; -use crate::protocol::{bob, Database, State}; -use crate::{bitcoin, cli, monero}; +use crate::protocol::bob::{self, BobState, Swap}; +use crate::protocol::{Database, State}; +use crate::{cli, monero}; use ::bitcoin::address::NetworkUnchecked; use ::bitcoin::Txid; use ::monero::Network; @@ -36,6 +36,8 @@ use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; +use swap_core::bitcoin; +use swap_core::bitcoin::{CancelTimelock, ExpiredTimelocks, PunishTimelock}; use thiserror::Error; use tokio_util::task::AbortOnDropHandle; use tor_rtcompat::tokio::TokioRustlsRuntime; diff --git a/swap/src/cli/api/tauri_bindings.rs b/swap/src/cli/api/tauri_bindings.rs index 705957b9..2eaa3629 100644 --- a/swap/src/cli/api/tauri_bindings.rs +++ b/swap/src/cli/api/tauri_bindings.rs @@ -1,11 +1,10 @@ use super::request::BalanceResponse; -use crate::bitcoin; use crate::cli::api::request::{ GetMoneroBalanceResponse, GetMoneroHistoryResponse, GetMoneroSyncProgressResponse, }; use crate::cli::list_sellers::QuoteWithAddress; use crate::monero::MoneroAddressPool; -use crate::{bitcoin::ExpiredTimelocks, monero, network::quote::BidQuote}; +use crate::{monero, network::quote::BidQuote}; use anyhow::{anyhow, bail, Context, Result}; use async_trait::async_trait; use bitcoin::Txid; @@ -17,6 +16,8 @@ use std::sync::Arc; use std::sync::Mutex; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use strum::Display; +use swap_core::bitcoin; +use swap_core::bitcoin::ExpiredTimelocks; use tokio::sync::oneshot; use typeshare::typeshare; use uuid::Uuid; diff --git a/swap/src/cli/behaviour.rs b/swap/src/cli/behaviour.rs index c3b535dc..49ff1f40 100644 --- a/swap/src/cli/behaviour.rs +++ b/swap/src/cli/behaviour.rs @@ -1,4 +1,3 @@ -use crate::bitcoin; use crate::monero::{Scalar, TransferProof}; use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason; use crate::network::quote::BidQuote; @@ -9,6 +8,7 @@ use crate::network::{ }; use crate::protocol::bob::State2; use anyhow::{anyhow, Error, Result}; +use bitcoin_wallet::BitcoinWallet; use libp2p::request_response::{ InboundFailure, InboundRequestId, OutboundFailure, OutboundRequestId, ResponseChannel, }; @@ -104,7 +104,7 @@ impl Behaviour { pub fn new( alice: PeerId, env_config: env::Config, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, identify_params: (identity::Keypair, XmrBtcNamespace), ) -> Self { let agentVersion = format!("cli/{} ({})", env!("CARGO_PKG_VERSION"), identify_params.1); diff --git a/swap/src/cli/cancel_and_refund.rs b/swap/src/cli/cancel_and_refund.rs index 00ab9925..5fc2aa20 100644 --- a/swap/src/cli/cancel_and_refund.rs +++ b/swap/src/cli/cancel_and_refund.rs @@ -1,10 +1,11 @@ -use crate::bitcoin::{ExpiredTimelocks, Wallet}; +use crate::bitcoin::Wallet; use crate::monero::BlockHeight; use crate::protocol::bob::BobState; use crate::protocol::Database; use anyhow::{bail, Result}; use bitcoin::Txid; use std::sync::Arc; +use swap_core::bitcoin::ExpiredTimelocks; use uuid::Uuid; pub async fn cancel_and_refund( diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 7b385309..2af532e2 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,4 +1,3 @@ -use crate::bitcoin::{bitcoin_address, Amount}; use crate::cli::api::request::{ BalanceArgs, BuyXmrArgs, CancelAndRefundArgs, ExportBitcoinWalletArgs, GetConfigArgs, GetHistoryArgs, ListSellersArgs, MoneroRecoveryArgs, Request, ResumeSwapArgs, WithdrawBtcArgs, @@ -12,6 +11,7 @@ use std::ffi::OsString; use std::path::PathBuf; use std::sync::Arc; use structopt::{clap, StructOpt}; +use swap_core::bitcoin::{bitcoin_address, Amount}; use url::Url; use uuid::Uuid; diff --git a/swap/src/cli/event_loop.rs b/swap/src/cli/event_loop.rs index 0e36ecb6..f8d2f820 100644 --- a/swap/src/cli/event_loop.rs +++ b/swap/src/cli/event_loop.rs @@ -1,4 +1,3 @@ -use crate::bitcoin::EncryptedSignature; use crate::cli::behaviour::{Behaviour, OutEvent}; use crate::monero; use crate::network::cooperative_xmr_redeem_after_punish::{self, Request, Response}; @@ -18,6 +17,7 @@ use libp2p::{PeerId, Swarm}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use swap_core::bitcoin::EncryptedSignature; use uuid::Uuid; static REQUEST_RESPONSE_PROTOCOL_TIMEOUT: Duration = Duration::from_secs(60); diff --git a/swap/src/cli/watcher.rs b/swap/src/cli/watcher.rs index 7374c622..dafbd2ce 100644 --- a/swap/src/cli/watcher.rs +++ b/swap/src/cli/watcher.rs @@ -1,7 +1,7 @@ use super::api::tauri_bindings::{BackgroundRefundProgress, TauriBackgroundProgress, TauriEmitter}; use super::api::SwapLock; use super::cancel_and_refund; -use crate::bitcoin::{ExpiredTimelocks, Wallet}; +use crate::bitcoin::Wallet; use crate::cli::api::tauri_bindings::TauriHandle; use crate::protocol::bob::BobState; use crate::protocol::{Database, State}; @@ -9,6 +9,7 @@ use anyhow::{Context, Result}; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use swap_core::bitcoin::ExpiredTimelocks; use uuid::Uuid; /// A long running task which watches for changes to timelocks and balance diff --git a/swap/src/database/alice.rs b/swap/src/database/alice.rs index 63d71c49..dcb26b68 100644 --- a/swap/src/database/alice.rs +++ b/swap/src/database/alice.rs @@ -1,4 +1,3 @@ -use crate::bitcoin::EncryptedSignature; use crate::monero; use crate::monero::BlockHeight; use crate::monero::TransferProof; @@ -6,6 +5,7 @@ use crate::protocol::alice; use crate::protocol::alice::AliceState; use serde::{Deserialize, Serialize}; use std::fmt; +use swap_core::bitcoin::EncryptedSignature; // Large enum variant is fine because this is only used for database // and is dropped once written in DB. diff --git a/swap/src/lib.rs b/swap/src/lib.rs index b725294f..4d15f662 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -23,11 +23,7 @@ pub mod common; pub mod database; pub mod libp2p_ext; pub mod monero; -mod monero_ext; pub mod network; pub mod protocol; pub mod seed; pub mod tracing_ext; - -#[cfg(test)] -mod proptest; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 60086a9c..bbd16256 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -4,842 +4,5 @@ pub mod wallet_rpc; pub use ::monero::network::Network; pub use ::monero::{Address, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; -pub use wallet::{Daemon, Wallet, Wallets, WatchRequest}; - -use crate::bitcoin; -use anyhow::{bail, Result}; -use rand::{CryptoRng, RngCore}; -use rust_decimal::prelude::*; -use rust_decimal::Decimal; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; -use std::fmt; -use std::ops::{Add, Mul, Sub}; -use std::str::FromStr; -use typeshare::typeshare; - -pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; - -/// A Monero block height. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct BlockHeight { - pub height: u64, -} - -impl fmt::Display for BlockHeight { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.height) - } -} - -pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey { - let mut bytes = scalar.to_bytes(); - - // we must reverse the bytes because a secp256k1 scalar is big endian, whereas a - // ed25519 scalar is little endian - bytes.reverse(); - - PrivateKey::from_scalar(Scalar::from_bytes_mod_order(bytes)) -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct PrivateViewKey(#[serde(with = "swap_serde::monero::private_key")] PrivateKey); - -impl fmt::Display for PrivateViewKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Delegate to the Display implementation of PrivateKey - write!(f, "{}", self.0) - } -} - -impl PrivateViewKey { - pub fn new_random(rng: &mut R) -> Self { - let scalar = Scalar::random(rng); - let private_key = PrivateKey::from_scalar(scalar); - - Self(private_key) - } - - pub fn public(&self) -> PublicViewKey { - PublicViewKey(PublicKey::from_private_key(&self.0)) - } -} - -impl Add for PrivateViewKey { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl From for PrivateKey { - fn from(from: PrivateViewKey) -> Self { - from.0 - } -} - -impl From for PublicKey { - fn from(from: PublicViewKey) -> Self { - from.0 - } -} - -#[derive(Clone, Copy, Debug)] -pub struct PublicViewKey(PublicKey); - -/// Our own monero amount type, which we need because the monero crate -/// doesn't implement Serialize and Deserialize. -#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq, PartialOrd)] -#[typeshare(serialized_as = "number")] -pub struct Amount(u64); - -// TX Fees on Monero can be found here: -// - https://www.monero.how/monero-transaction-fees -// - https://bitinfocharts.com/comparison/monero-transactionfees.html#1y -// -// In the last year the highest avg fee on any given day was around 0.00075 XMR -// We use a multiplier of 4x to stay safe -// 0.00075 XMR * 4 = 0.003 XMR (around $1 as of Jun. 4th 2025) -// We DO NOT use this fee to construct any transactions. It is only to **estimate** how much -// we need to reserve for the fee when determining our max giveable amount -// We use a VERY conservative value here to stay on the safe side. We want to avoid not being able -// to lock as much as we previously estimated. -pub const CONSERVATIVE_MONERO_FEE: Amount = Amount::from_piconero(3_000_000_000); - -impl Amount { - pub const ZERO: Self = Self(0); - pub const ONE_XMR: Self = Self(PICONERO_OFFSET); - /// Create an [Amount] with piconero precision and the given number of - /// piconeros. - /// - /// A piconero (a.k.a atomic unit) is equal to 1e-12 XMR. - pub const fn from_piconero(amount: u64) -> Self { - Amount(amount) - } - - /// Return Monero Amount as Piconero. - pub fn as_piconero(&self) -> u64 { - self.0 - } - - /// Return Monero Amount as XMR. - pub fn as_xmr(&self) -> f64 { - let amount_decimal = Decimal::from(self.0); - let offset_decimal = Decimal::from(PICONERO_OFFSET); - let result = amount_decimal / offset_decimal; - - // Convert to f64 only at the end, after the division - result - .to_f64() - .expect("Conversion from piconero to XMR should not overflow f64") - } - - /// Calculate the conservative max giveable of Monero we can spent given [`self`] is the balance - /// of a Monero wallet - /// This is going to be LESS than we can really spent because we assume a high fee - pub fn max_conservative_giveable(&self) -> Self { - let pico_minus_fee = self - .as_piconero() - .saturating_sub(CONSERVATIVE_MONERO_FEE.as_piconero()); - - Self::from_piconero(pico_minus_fee) - } - - /// Calculate the Monero balance needed to send the [`self`] Amount to another address - /// E.g: Amount(1 XMR).min_conservative_balance_to_spend() with a fee of 0.1 XMR would be 1.1 XMR - /// This is going to be MORE than we really need because we assume a high fee - pub fn min_conservative_balance_to_spend(&self) -> Self { - let pico_minus_fee = self - .as_piconero() - .saturating_add(CONSERVATIVE_MONERO_FEE.as_piconero()); - - Self::from_piconero(pico_minus_fee) - } - - /// Calculate the maximum amount of Bitcoin that can be bought at a given - /// asking price for this amount of Monero including the median fee. - pub fn max_bitcoin_for_price(&self, ask_price: bitcoin::Amount) -> Option { - let pico_minus_fee = self.max_conservative_giveable(); - - if pico_minus_fee.as_piconero() == 0 { - return Some(bitcoin::Amount::ZERO); - } - - // safely convert the BTC/XMR rate to sat/pico - let ask_sats = Decimal::from(ask_price.to_sat()); - let pico_per_xmr = Decimal::from(PICONERO_OFFSET); - let ask_sats_per_pico = ask_sats / pico_per_xmr; - - let pico = Decimal::from(pico_minus_fee.as_piconero()); - let max_sats = pico.checked_mul(ask_sats_per_pico)?; - let satoshi = max_sats.to_u64()?; - - Some(bitcoin::Amount::from_sat(satoshi)) - } - - pub fn from_monero(amount: f64) -> Result { - let decimal = Decimal::try_from(amount)?; - Self::from_decimal(decimal) - } - - pub fn parse_monero(amount: &str) -> Result { - let decimal = Decimal::from_str(amount)?; - Self::from_decimal(decimal) - } - - pub fn as_piconero_decimal(&self) -> Decimal { - Decimal::from(self.as_piconero()) - } - - fn from_decimal(amount: Decimal) -> Result { - let piconeros_dec = - amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64")); - let piconeros = piconeros_dec - .to_u64() - .ok_or_else(|| OverflowError(amount.to_string()))?; - Ok(Amount(piconeros)) - } - - /// Subtract but throw an error on underflow. - pub fn checked_sub(self, rhs: Amount) -> Result { - if self.0 < rhs.0 { - bail!("checked sub would underflow"); - } - - Ok(Amount::from_piconero(self.0 - rhs.0)) - } -} - -/// A Monero address with an associated percentage and human-readable label. -/// -/// This structure represents a destination address for Monero transactions -/// along with the percentage of funds it should receive and a descriptive label. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[typeshare] -pub struct LabeledMoneroAddress { - // If this is None, we will use an address of the internal Monero wallet - #[typeshare(serialized_as = "string")] - address: Option, - #[typeshare(serialized_as = "number")] - percentage: Decimal, - label: String, -} - -impl LabeledMoneroAddress { - /// Creates a new labeled Monero address. - /// - /// # Arguments - /// - /// * `address` - The Monero address - /// * `percentage` - The percentage of funds (between 0.0 and 1.0) - /// * `label` - A human-readable label for this address - /// - /// # Errors - /// - /// Returns an error if the percentage is not between 0.0 and 1.0 inclusive. - fn new( - address: impl Into>, - percentage: Decimal, - label: String, - ) -> Result { - if percentage < Decimal::ZERO || percentage > Decimal::ONE { - bail!( - "Percentage must be between 0 and 1 inclusive, got: {}", - percentage - ); - } - - Ok(Self { - address: address.into(), - percentage, - label, - }) - } - - pub fn with_address( - address: monero::Address, - percentage: Decimal, - label: String, - ) -> Result { - Self::new(address, percentage, label) - } - - pub fn with_internal_address(percentage: Decimal, label: String) -> Result { - Self::new(None, percentage, label) - } - - /// Returns the Monero address. - pub fn address(&self) -> Option { - self.address.clone() - } - - /// Returns the percentage as a decimal. - pub fn percentage(&self) -> Decimal { - self.percentage - } - - /// Returns the human-readable label. - pub fn label(&self) -> &str { - &self.label - } -} - -/// A collection of labeled Monero addresses that can receive funds in a transaction. -/// -/// This structure manages multiple destination addresses with their associated -/// percentages and labels. It's used for splitting Monero transactions across -/// multiple recipients, such as for donations or multi-destination swaps. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[typeshare] -pub struct MoneroAddressPool(Vec); - -use rust_decimal::prelude::ToPrimitive; - -impl MoneroAddressPool { - /// Creates a new address pool from a vector of labeled addresses. - /// - /// # Arguments - /// - /// * `addresses` - Vector of labeled Monero addresses - pub fn new(addresses: Vec) -> Self { - Self(addresses) - } - - /// Returns a vector of all Monero addresses in the pool. - pub fn addresses(&self) -> Vec> { - self.0.iter().map(|address| address.address()).collect() - } - - /// Returns a vector of all percentages as f64 values (0-1 range). - pub fn percentages(&self) -> Vec { - self.0 - .iter() - .map(|address| { - address - .percentage() - .to_f64() - .expect("Decimal should convert to f64") - }) - .collect() - } - - /// Returns an iterator over the labeled addresses. - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } - - /// Validates that all addresses in the pool are on the expected network. - /// - /// # Arguments - /// - /// * `network` - The expected Monero network - /// - /// # Errors - /// - /// Returns an error if any address is on a different network than expected. - pub fn assert_network(&self, network: Network) -> Result<()> { - for address in self.0.iter() { - if let Some(address) = address.address { - if address.network != network { - bail!("Address pool contains addresses on the wrong network (address {} is on {:?}, expected {:?})", address, address.network, network); - } - } - } - - Ok(()) - } - - /// Assert that the sum of the percentages in the address pool is 1 (allowing for a small tolerance) - pub fn assert_sum_to_one(&self) -> Result<()> { - let sum = self - .0 - .iter() - .map(|address| address.percentage()) - .sum::(); - - const TOLERANCE: f64 = 1e-6; - - if (sum - Decimal::ONE).abs() - > Decimal::from_f64(TOLERANCE).expect("TOLERANCE constant should be a valid f64") - { - bail!("Address pool percentages do not sum to 1"); - } - - Ok(()) - } - - /// Returns a vector of addresses with the empty addresses filled with the given primary address - pub fn fill_empty_addresses(&self, primary_address: monero::Address) -> Vec { - self.0 - .iter() - .map(|address| address.address().unwrap_or(primary_address)) - .collect() - } -} - -impl From<::monero::Address> for MoneroAddressPool { - fn from(address: ::monero::Address) -> Self { - Self(vec![LabeledMoneroAddress::new( - address, - Decimal::from(1), - "user address".to_string(), - ) - .expect("Percentage 1 is always valid")]) - } -} - -impl Add for Amount { - type Output = Amount; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl Sub for Amount { - type Output = Amount; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl Mul for Amount { - type Output = Amount; - - fn mul(self, rhs: u64) -> Self::Output { - Self(self.0 * rhs) - } -} - -impl From for u64 { - fn from(from: Amount) -> u64 { - from.0 - } -} - -impl From<::monero::Amount> for Amount { - fn from(from: ::monero::Amount) -> Self { - Amount::from_piconero(from.as_pico()) - } -} - -impl From for ::monero::Amount { - fn from(from: Amount) -> Self { - ::monero::Amount::from_pico(from.as_piconero()) - } -} - -impl fmt::Display for Amount { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut decimal = Decimal::from(self.0); - decimal - .set_scale(12) - .expect("12 is smaller than max precision of 28"); - write!(f, "{} XMR", decimal) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct TransferProof { - tx_hash: TxHash, - #[serde(with = "swap_serde::monero::private_key")] - tx_key: PrivateKey, -} - -impl TransferProof { - pub fn new(tx_hash: TxHash, tx_key: PrivateKey) -> Self { - Self { tx_hash, tx_key } - } - pub fn tx_hash(&self) -> TxHash { - self.tx_hash.clone() - } - pub fn tx_key(&self) -> PrivateKey { - self.tx_key - } -} - -// TODO: add constructor/ change String to fixed length byte array -#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct TxHash(pub String); - -impl From for String { - fn from(from: TxHash) -> Self { - from.0 - } -} - -impl fmt::Debug for TxHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl fmt::Display for TxHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Debug, Clone, Copy, thiserror::Error)] -#[error("expected {expected}, got {actual}")] -pub struct InsufficientFunds { - pub expected: Amount, - pub actual: Amount, -} - -#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)] -#[error("Overflow, cannot convert {0} to u64")] -pub struct OverflowError(pub String); - -pub mod monero_amount { - use crate::monero::Amount; - use serde::{Deserialize, Deserializer, Serializer}; - - pub fn serialize(x: &Amount, s: S) -> Result - where - S: Serializer, - { - s.serialize_u64(x.as_piconero()) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, - { - let picos = u64::deserialize(deserializer)?; - let amount = Amount::from_piconero(picos); - - Ok(amount) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn display_monero_min() { - let min_pics = 1; - let amount = Amount::from_piconero(min_pics); - let monero = amount.to_string(); - assert_eq!("0.000000000001 XMR", monero); - } - - #[test] - fn display_monero_one() { - let min_pics = 1000000000000; - let amount = Amount::from_piconero(min_pics); - let monero = amount.to_string(); - assert_eq!("1.000000000000 XMR", monero); - } - - #[test] - fn display_monero_max() { - let max_pics = 18_446_744_073_709_551_615; - let amount = Amount::from_piconero(max_pics); - let monero = amount.to_string(); - assert_eq!("18446744.073709551615 XMR", monero); - } - - #[test] - fn parse_monero_min() { - let monero_min = "0.000000000001"; - let amount = Amount::parse_monero(monero_min).unwrap(); - let pics = amount.0; - assert_eq!(1, pics); - } - - #[test] - fn parse_monero() { - let monero = "123"; - let amount = Amount::parse_monero(monero).unwrap(); - let pics = amount.0; - assert_eq!(123000000000000, pics); - } - - #[test] - fn parse_monero_max() { - let monero = "18446744.073709551615"; - let amount = Amount::parse_monero(monero).unwrap(); - let pics = amount.0; - assert_eq!(18446744073709551615, pics); - } - - #[test] - fn parse_monero_overflows() { - let overflow_pics = "18446744.073709551616"; - let error = Amount::parse_monero(overflow_pics).unwrap_err(); - assert_eq!( - error.downcast_ref::().unwrap(), - &OverflowError(overflow_pics.to_owned()) - ); - } - - #[test] - fn max_bitcoin_to_trade() { - // sanity check: if the asking price is 1 BTC / 1 XMR - // and we have μ XMR + fee - // then max BTC we can buy is μ - let ask = bitcoin::Amount::from_btc(1.0).unwrap(); - - let xmr = Amount::parse_monero("1.0").unwrap() + CONSERVATIVE_MONERO_FEE; - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap()); - - let xmr = Amount::parse_monero("0.5").unwrap() + CONSERVATIVE_MONERO_FEE; - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_btc(0.5).unwrap()); - - let xmr = Amount::parse_monero("2.5").unwrap() + CONSERVATIVE_MONERO_FEE; - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_btc(2.5).unwrap()); - - let xmr = Amount::parse_monero("420").unwrap() + CONSERVATIVE_MONERO_FEE; - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_btc(420.0).unwrap()); - - let xmr = Amount::parse_monero("0.00001").unwrap() + CONSERVATIVE_MONERO_FEE; - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_btc(0.00001).unwrap()); - - // other ask prices - - let ask = bitcoin::Amount::from_btc(0.5).unwrap(); - let xmr = Amount::parse_monero("2").unwrap() + CONSERVATIVE_MONERO_FEE; - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_btc(1.0).unwrap()); - - let ask = bitcoin::Amount::from_btc(2.0).unwrap(); - let xmr = Amount::parse_monero("1").unwrap() + CONSERVATIVE_MONERO_FEE; - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_btc(2.0).unwrap()); - - let ask = bitcoin::Amount::from_sat(382_900); - let xmr = Amount::parse_monero("10").unwrap(); - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_sat(3_827_851)); - - // example from https://github.com/comit-network/xmr-btc-swap/issues/1084 - // with rate from kraken at that time - let ask = bitcoin::Amount::from_sat(685_800); - let xmr = Amount::parse_monero("0.826286435921").unwrap(); - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(btc, bitcoin::Amount::from_sat(564_609)); - } - - #[test] - fn max_bitcoin_to_trade_overflow() { - let xmr = Amount::from_monero(30.0).unwrap(); - let ask = bitcoin::Amount::from_sat(728_688); - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(bitcoin::Amount::from_sat(21_858_453), btc); - - let xmr = Amount::from_piconero(u64::MAX); - let ask = bitcoin::Amount::from_sat(u64::MAX); - let btc = xmr.max_bitcoin_for_price(ask); - - assert!(btc.is_none()); - } - - #[test] - fn geting_max_bitcoin_to_trade_with_balance_smaller_than_locking_fee() { - let ask = bitcoin::Amount::from_sat(382_900); - let xmr = Amount::parse_monero("0.00001").unwrap(); - let btc = xmr.max_bitcoin_for_price(ask).unwrap(); - - assert_eq!(bitcoin::Amount::ZERO, btc); - } - - use rand::rngs::OsRng; - use serde::{Deserialize, Serialize}; - - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - pub struct MoneroPrivateKey(#[serde(with = "monero_private_key")] crate::monero::PrivateKey); - - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - pub struct MoneroAmount(#[serde(with = "monero_amount")] crate::monero::Amount); - - #[test] - fn serde_monero_private_key_json() { - let key = MoneroPrivateKey(monero::PrivateKey::from_scalar( - crate::monero::Scalar::random(&mut OsRng), - )); - let encoded = serde_json::to_vec(&key).unwrap(); - let decoded: MoneroPrivateKey = serde_json::from_slice(&encoded).unwrap(); - assert_eq!(key, decoded); - } - - #[test] - fn serde_monero_private_key_cbor() { - let key = MoneroPrivateKey(monero::PrivateKey::from_scalar( - crate::monero::Scalar::random(&mut OsRng), - )); - let encoded = serde_cbor::to_vec(&key).unwrap(); - let decoded: MoneroPrivateKey = serde_cbor::from_slice(&encoded).unwrap(); - assert_eq!(key, decoded); - } - - #[test] - fn serde_monero_amount() { - let amount = MoneroAmount(crate::monero::Amount::from_piconero(1000)); - let encoded = serde_cbor::to_vec(&amount).unwrap(); - let decoded: MoneroAmount = serde_cbor::from_slice(&encoded).unwrap(); - assert_eq!(amount, decoded); - } - - #[test] - fn max_conservative_giveable_basic() { - // Test with balance larger than fee - let balance = Amount::parse_monero("1.0").unwrap(); - let giveable = balance.max_conservative_giveable(); - let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero(); - assert_eq!(giveable.as_piconero(), expected); - } - - #[test] - fn max_conservative_giveable_exact_fee() { - // Test with balance exactly equal to fee - let balance = CONSERVATIVE_MONERO_FEE; - let giveable = balance.max_conservative_giveable(); - assert_eq!(giveable, Amount::ZERO); - } - - #[test] - fn max_conservative_giveable_less_than_fee() { - // Test with balance less than fee (should saturate to 0) - let balance = Amount::from_piconero(CONSERVATIVE_MONERO_FEE.as_piconero() / 2); - let giveable = balance.max_conservative_giveable(); - assert_eq!(giveable, Amount::ZERO); - } - - #[test] - fn max_conservative_giveable_zero_balance() { - // Test with zero balance - let balance = Amount::ZERO; - let giveable = balance.max_conservative_giveable(); - assert_eq!(giveable, Amount::ZERO); - } - - #[test] - fn max_conservative_giveable_large_balance() { - // Test with large balance - let balance = Amount::parse_monero("100.0").unwrap(); - let giveable = balance.max_conservative_giveable(); - let expected = balance.as_piconero() - CONSERVATIVE_MONERO_FEE.as_piconero(); - assert_eq!(giveable.as_piconero(), expected); - - // Ensure the result makes sense - assert!(giveable.as_piconero() > 0); - assert!(giveable < balance); - } - - #[test] - fn min_conservative_balance_to_spend_basic() { - // Test with 1 XMR amount to send - let amount_to_send = Amount::parse_monero("1.0").unwrap(); - let min_balance = amount_to_send.min_conservative_balance_to_spend(); - let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero(); - assert_eq!(min_balance.as_piconero(), expected); - } - - #[test] - fn min_conservative_balance_to_spend_zero() { - // Test with zero amount to send - let amount_to_send = Amount::ZERO; - let min_balance = amount_to_send.min_conservative_balance_to_spend(); - assert_eq!(min_balance, CONSERVATIVE_MONERO_FEE); - } - - #[test] - fn min_conservative_balance_to_spend_small_amount() { - // Test with small amount - let amount_to_send = Amount::from_piconero(1000); - let min_balance = amount_to_send.min_conservative_balance_to_spend(); - let expected = 1000 + CONSERVATIVE_MONERO_FEE.as_piconero(); - assert_eq!(min_balance.as_piconero(), expected); - } - - #[test] - fn min_conservative_balance_to_spend_large_amount() { - // Test with large amount - let amount_to_send = Amount::parse_monero("50.0").unwrap(); - let min_balance = amount_to_send.min_conservative_balance_to_spend(); - let expected = amount_to_send.as_piconero() + CONSERVATIVE_MONERO_FEE.as_piconero(); - assert_eq!(min_balance.as_piconero(), expected); - - // Ensure the result makes sense - assert!(min_balance > amount_to_send); - assert!(min_balance > CONSERVATIVE_MONERO_FEE); - } - - #[test] - fn conservative_fee_functions_are_inverse() { - // Test that the functions are somewhat inverse of each other - let original_balance = Amount::parse_monero("5.0").unwrap(); - - // Get max giveable amount - let max_giveable = original_balance.max_conservative_giveable(); - - // Calculate min balance needed to send that amount - let min_balance_needed = max_giveable.min_conservative_balance_to_spend(); - - // The min balance needed should be equal to or slightly more than the original balance - // (due to the conservative nature of the fee estimation) - assert!(min_balance_needed >= original_balance); - - // The difference should be at most the conservative fee - let difference = min_balance_needed.as_piconero() - original_balance.as_piconero(); - assert!(difference <= CONSERVATIVE_MONERO_FEE.as_piconero()); - } - - #[test] - fn conservative_fee_edge_cases() { - // Test with maximum possible amount - let max_amount = Amount::from_piconero(u64::MAX - CONSERVATIVE_MONERO_FEE.as_piconero()); - let giveable = max_amount.max_conservative_giveable(); - assert!(giveable.as_piconero() > 0); - - // Test min balance calculation doesn't overflow - let large_amount = Amount::from_piconero(u64::MAX / 2); - let min_balance = large_amount.min_conservative_balance_to_spend(); - assert!(min_balance > large_amount); - } - - #[test] - fn labeled_monero_address_percentage_validation() { - use rust_decimal::Decimal; - - let address = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a".parse::().unwrap(); - - // Valid percentages should work (0-1 range) - assert!(LabeledMoneroAddress::new(address, Decimal::ZERO, "test".to_string()).is_ok()); - assert!(LabeledMoneroAddress::new(address, Decimal::ONE, "test".to_string()).is_ok()); - assert!(LabeledMoneroAddress::new(address, Decimal::new(5, 1), "test".to_string()).is_ok()); // 0.5 - assert!( - LabeledMoneroAddress::new(address, Decimal::new(9925, 4), "test".to_string()).is_ok() - ); // 0.9925 - - // Invalid percentages should fail - assert!( - LabeledMoneroAddress::new(address, Decimal::new(-1, 0), "test".to_string()).is_err() - ); - assert!( - LabeledMoneroAddress::new(address, Decimal::new(11, 1), "test".to_string()).is_err() - ); // 1.1 - assert!( - LabeledMoneroAddress::new(address, Decimal::new(2, 0), "test".to_string()).is_err() - ); // 2.0 - } -} +pub use swap_core::monero::primitives::*; +pub use wallet::{Daemon, Wallet, Wallets}; diff --git a/swap/src/monero/wallet.rs b/swap/src/monero/wallet.rs index 3e7ac469..788fc6bf 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -5,14 +5,14 @@ //! - wait for transactions to be confirmed //! - send money from one wallet to another. -use std::{path::PathBuf, sync::Arc, time::Duration}; +pub use monero_sys::{Daemon, WalletHandle as Wallet, WalletHandleListener}; use crate::common::throttle::{throttle, Throttle}; use anyhow::{Context, Result}; use monero::{Address, Network}; use monero_simple_request_rpc::SimpleRequestRpc; use monero_sys::WalletEventListener; -pub use monero_sys::{Daemon, WalletHandle as Wallet, WalletHandleListener}; +use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::sync::RwLock; use uuid::Uuid; @@ -21,7 +21,7 @@ use crate::cli::api::{ tauri_bindings::{MoneroWalletUpdate, TauriEmitter, TauriEvent, TauriHandle}, }; -use super::{BlockHeight, TransferProof, TxHash}; +use super::{BlockHeight, TxHash, WatchRequest}; /// Entrance point to the Monero blockchain. /// You can use this struct to open specific wallets and monitor the blockchain. @@ -45,25 +45,6 @@ pub struct Wallets { wallet_database: Option>, } -/// A request to watch for a transfer. -pub struct WatchRequest { - pub public_view_key: super::PublicViewKey, - pub public_spend_key: monero::PublicKey, - /// The proof of the transfer. - pub transfer_proof: TransferProof, - /// The expected amount of the transfer. - pub expected_amount: monero::Amount, - /// The number of confirmations required for the transfer to be considered confirmed. - pub confirmation_target: u64, -} - -/// Transfer a specified amount of money to a specified address. -pub struct TransferRequest { - pub public_spend_key: monero::PublicKey, - pub public_view_key: super::PublicViewKey, - pub amount: monero::Amount, -} - struct TauriWalletListener { // one throttle wrapper per expensive update balance_throttle: Throttle<()>, @@ -513,15 +494,6 @@ impl Wallets { } } -impl TransferRequest { - pub fn address_and_amount(&self, network: Network) -> (Address, monero::Amount) { - ( - Address::standard(network, self.public_spend_key, self.public_view_key.0), - self.amount, - ) - } -} - /// Pass this to [`Wallet::wait_until_confirmed`] or [`Wallet::wait_until_synced`] /// to not receive any confirmation callbacks. pub fn no_listener() -> Option { diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index 30a7c4b6..17fac777 100644 --- a/swap/src/monero/wallet_rpc.rs +++ b/swap/src/monero/wallet_rpc.rs @@ -1,29 +1,8 @@ use ::monero::Network; -use anyhow::{bail, Context, Error, Result}; -use once_cell::sync::Lazy; +use anyhow::{Context, Error, Result}; use serde::Deserialize; use std::fmt; use std::fmt::{Display, Formatter}; -use std::time::Duration; - -// See: https://www.moneroworld.com/#nodes, https://monero.fail -// We don't need any testnet nodes because we don't support testnet at all -static MONERO_DAEMONS: Lazy<[MoneroDaemon; 12]> = Lazy::new(|| { - [ - MoneroDaemon::new("http://xmr-node.cakewallet.com:18081", Network::Mainnet), - MoneroDaemon::new("http://nodex.monerujo.io:18081", Network::Mainnet), - MoneroDaemon::new("http://nodes.hashvault.pro:18081", Network::Mainnet), - MoneroDaemon::new("http://p2pmd.xmrvsbeast.com:18081", Network::Mainnet), - MoneroDaemon::new("http://node.monerodevs.org:18089", Network::Mainnet), - MoneroDaemon::new("http://xmr-node-uk.cakewallet.com:18081", Network::Mainnet), - MoneroDaemon::new("http://xmr.litepay.ch:18081", Network::Mainnet), - MoneroDaemon::new("http://stagenet.xmr-tw.org:38081", Network::Stagenet), - MoneroDaemon::new("http://node.monerodevs.org:38089", Network::Stagenet), - MoneroDaemon::new("http://singapore.node.xmr.pm:38081", Network::Stagenet), - MoneroDaemon::new("http://xmr-lux.boldsuck.org:38081", Network::Stagenet), - MoneroDaemon::new("http://stagenet.community.rino.io:38081", Network::Stagenet), - ] -}); #[derive(Debug, Clone)] pub struct MoneroDaemon { @@ -92,40 +71,6 @@ struct MoneroDaemonGetInfoResponse { testnet: bool, } -/// Chooses an available Monero daemon based on the specified network. -async fn choose_monero_daemon(network: Network) -> Result { - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .https_only(false) - .build()?; - - // We only want to check for daemons that match the specified network - let network_matching_daemons = MONERO_DAEMONS - .iter() - .filter(|daemon| daemon.network == network); - - for daemon in network_matching_daemons { - match daemon.is_available(&client).await { - Ok(true) => { - tracing::debug!(%daemon, "Found available Monero daemon"); - return Ok(daemon.clone()); - } - Err(err) => { - tracing::debug!(?err, %daemon, "Failed to connect to Monero daemon"); - continue; - } - Ok(false) => continue, - } - } - - bail!("No Monero daemon could be found. Please specify one manually or try again later.") -} - -/// Public wrapper around [`choose_monero_daemon`]. -pub async fn choose_monero_node(network: Network) -> Result { - choose_monero_daemon(network).await -} - #[cfg(test)] mod tests { use super::*; diff --git a/swap/src/network/encrypted_signature.rs b/swap/src/network/encrypted_signature.rs index ffd835a4..8f69328c 100644 --- a/swap/src/network/encrypted_signature.rs +++ b/swap/src/network/encrypted_signature.rs @@ -23,7 +23,7 @@ impl AsRef for EncryptedSignatureProtocol { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Request { pub swap_id: Uuid, - pub tx_redeem_encsig: crate::bitcoin::EncryptedSignature, + pub tx_redeem_encsig: swap_core::bitcoin::EncryptedSignature, } pub fn alice() -> Behaviour { diff --git a/swap/src/network/quote.rs b/swap/src/network/quote.rs index e3e3b1f9..dc5523f7 100644 --- a/swap/src/network/quote.rs +++ b/swap/src/network/quote.rs @@ -1,9 +1,10 @@ use std::time::Duration; -use crate::{asb, bitcoin, cli}; +use crate::{asb, cli}; use libp2p::request_response::{self, ProtocolSupport}; use libp2p::{PeerId, StreamProtocol}; use serde::{Deserialize, Serialize}; +use swap_core::bitcoin; use typeshare::typeshare; const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0"; diff --git a/swap/src/network/swap_setup/alice.rs b/swap/src/network/swap_setup/alice.rs index 440f9f8d..dcd9cf9b 100644 --- a/swap/src/network/swap_setup/alice.rs +++ b/swap/src/network/swap_setup/alice.rs @@ -5,8 +5,9 @@ use crate::network::swap_setup::{ }; use crate::protocol::alice::{State0, State3}; use crate::protocol::{Message0, Message2, Message4}; -use crate::{asb, bitcoin, monero}; +use crate::{asb, monero}; use anyhow::{anyhow, Context, Result}; +use bitcoin_wallet::BitcoinWallet; use futures::future::{BoxFuture, OptionFuture}; use futures::AsyncWriteExt; use futures::FutureExt; @@ -17,8 +18,10 @@ use libp2p::swarm::{ConnectionHandlerEvent, NetworkBehaviour, SubstreamProtocol, use libp2p::{Multiaddr, PeerId}; use std::collections::VecDeque; use std::fmt::Debug; +use std::sync::Arc; use std::task::Poll; use std::time::{Duration, Instant}; +use swap_core::bitcoin; use swap_env::env; use uuid::Uuid; @@ -55,7 +58,7 @@ pub struct WalletSnapshot { impl WalletSnapshot { pub async fn capture( - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: Arc, monero_wallet: &monero::Wallets, external_redeem_address: &Option, transfer_amount: bitcoin::Amount, diff --git a/swap/src/network/swap_setup/bob.rs b/swap/src/network/swap_setup/bob.rs index 84a82ac3..6530598f 100644 --- a/swap/src/network/swap_setup/bob.rs +++ b/swap/src/network/swap_setup/bob.rs @@ -1,8 +1,9 @@ use crate::network::swap_setup::{protocol, BlockchainNetwork, SpotPriceError, SpotPriceResponse}; use crate::protocol::bob::{State0, State2}; use crate::protocol::{Message1, Message3}; -use crate::{bitcoin, cli, monero}; +use crate::{cli, monero}; use anyhow::{Context, Result}; +use bitcoin_wallet::BitcoinWallet; use futures::future::{BoxFuture, OptionFuture}; use futures::AsyncWriteExt; use futures::FutureExt; @@ -16,6 +17,7 @@ use std::collections::VecDeque; use std::sync::Arc; use std::task::Poll; use std::time::Duration; +use swap_core::bitcoin; use swap_env::env; use uuid::Uuid; @@ -24,13 +26,13 @@ use super::{read_cbor_message, write_cbor_message, SpotPriceRequest}; #[allow(missing_debug_implementations)] pub struct Behaviour { env_config: env::Config, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, new_swaps: VecDeque<(PeerId, NewSwap)>, completed_swaps: VecDeque<(PeerId, Completed)>, } impl Behaviour { - pub fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { + pub fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { Self { env_config, bitcoin_wallet, @@ -116,12 +118,12 @@ pub struct Handler { env_config: env::Config, timeout: Duration, new_swaps: VecDeque, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, keep_alive: bool, } impl Handler { - fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { + fn new(env_config: env::Config, bitcoin_wallet: Arc) -> Self { Self { env_config, outbound_stream: OptionFuture::from(None), diff --git a/swap/src/network/swarm.rs b/swap/src/network/swarm.rs index 2e40668e..2e266f76 100644 --- a/swap/src/network/swarm.rs +++ b/swap/src/network/swarm.rs @@ -2,7 +2,7 @@ use crate::asb::{LatestRate, RendezvousNode}; use crate::libp2p_ext::MultiAddrExt; use crate::network::rendezvous::XmrBtcNamespace; use crate::seed::Seed; -use crate::{asb, bitcoin, cli}; +use crate::{asb, cli}; use anyhow::Result; use arti_client::TorClient; use libp2p::swarm::NetworkBehaviour; @@ -11,6 +11,7 @@ use libp2p::{identity, Multiaddr, Swarm}; use std::fmt::Debug; use std::sync::Arc; use std::time::Duration; +use swap_core::bitcoin; use swap_env::env; use tor_rtcompat::tokio::TokioRustlsRuntime; diff --git a/swap/src/protocol.rs b/swap/src/protocol.rs index ae0ca37e..860c94c0 100644 --- a/swap/src/protocol.rs +++ b/swap/src/protocol.rs @@ -1,167 +1,4 @@ -use crate::monero::MoneroAddressPool; -use crate::protocol::alice::swap::is_complete as alice_is_complete; -use crate::protocol::alice::AliceState; -use crate::protocol::bob::swap::is_complete as bob_is_complete; -use crate::protocol::bob::BobState; -use crate::{bitcoin, monero}; -use anyhow::Result; -use async_trait::async_trait; -use conquer_once::Lazy; -use libp2p::{Multiaddr, PeerId}; -use serde::{Deserialize, Serialize}; -use sha2::Sha256; -use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof}; -use sigma_fun::HashTranscript; -use std::convert::TryInto; -use uuid::Uuid; - pub mod alice; pub mod bob; -pub static CROSS_CURVE_PROOF_SYSTEM: Lazy< - CrossCurveDLEQ>, -> = Lazy::new(|| { - CrossCurveDLEQ::>::new( - (*ecdsa_fun::fun::G).normalize(), - curve25519_dalek::constants::ED25519_BASEPOINT_POINT, - ) -}); - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message0 { - swap_id: Uuid, - B: bitcoin::PublicKey, - S_b_monero: monero::PublicKey, - S_b_bitcoin: bitcoin::PublicKey, - dleq_proof_s_b: CrossCurveDLEQProof, - v_b: monero::PrivateViewKey, - #[serde(with = "swap_serde::bitcoin::address_serde")] - refund_address: bitcoin::Address, - #[serde(with = "::bitcoin::amount::serde::as_sat")] - tx_refund_fee: bitcoin::Amount, - #[serde(with = "::bitcoin::amount::serde::as_sat")] - tx_cancel_fee: bitcoin::Amount, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message1 { - A: bitcoin::PublicKey, - S_a_monero: monero::PublicKey, - S_a_bitcoin: bitcoin::PublicKey, - dleq_proof_s_a: CrossCurveDLEQProof, - v_a: monero::PrivateViewKey, - #[serde(with = "swap_serde::bitcoin::address_serde")] - redeem_address: bitcoin::Address, - #[serde(with = "swap_serde::bitcoin::address_serde")] - punish_address: bitcoin::Address, - #[serde(with = "::bitcoin::amount::serde::as_sat")] - tx_redeem_fee: bitcoin::Amount, - #[serde(with = "::bitcoin::amount::serde::as_sat")] - tx_punish_fee: bitcoin::Amount, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message2 { - psbt: bitcoin::PartiallySignedTransaction, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message3 { - tx_cancel_sig: bitcoin::Signature, - tx_refund_encsig: bitcoin::EncryptedSignature, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Message4 { - tx_punish_sig: bitcoin::Signature, - tx_cancel_sig: bitcoin::Signature, - tx_early_refund_sig: bitcoin::Signature, -} - -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, PartialEq)] -pub enum State { - Alice(AliceState), - Bob(BobState), -} - -impl State { - pub fn swap_finished(&self) -> bool { - match self { - State::Alice(state) => alice_is_complete(state), - State::Bob(state) => bob_is_complete(state), - } - } -} - -impl From for State { - fn from(alice: AliceState) -> Self { - Self::Alice(alice) - } -} - -impl From for State { - fn from(bob: BobState) -> Self { - Self::Bob(bob) - } -} - -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] -#[error("Not in the role of Alice")] -pub struct NotAlice; - -#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] -#[error("Not in the role of Bob")] -pub struct NotBob; - -impl TryInto for State { - type Error = NotBob; - - fn try_into(self) -> std::result::Result { - match self { - State::Alice(_) => Err(NotBob), - State::Bob(state) => Ok(state), - } - } -} - -impl TryInto for State { - type Error = NotAlice; - - fn try_into(self) -> std::result::Result { - match self { - State::Alice(state) => Ok(state), - State::Bob(_) => Err(NotAlice), - } - } -} - -#[async_trait] -pub trait Database { - async fn insert_peer_id(&self, swap_id: Uuid, peer_id: PeerId) -> Result<()>; - async fn get_peer_id(&self, swap_id: Uuid) -> Result; - async fn insert_monero_address_pool( - &self, - swap_id: Uuid, - address: MoneroAddressPool, - ) -> Result<()>; - async fn get_monero_address_pool(&self, swap_id: Uuid) -> Result; - async fn get_monero_addresses(&self) -> Result>; - async fn insert_address(&self, peer_id: PeerId, address: Multiaddr) -> Result<()>; - async fn get_addresses(&self, peer_id: PeerId) -> Result>; - async fn get_all_peer_addresses(&self) -> Result)>>; - async fn get_swap_start_date(&self, swap_id: Uuid) -> Result; - async fn insert_latest_state(&self, swap_id: Uuid, state: State) -> Result<()>; - async fn get_state(&self, swap_id: Uuid) -> Result; - async fn get_states(&self, swap_id: Uuid) -> Result>; - async fn all(&self) -> Result>; - async fn insert_buffered_transfer_proof( - &self, - swap_id: Uuid, - proof: monero::TransferProof, - ) -> Result<()>; - async fn get_buffered_transfer_proof( - &self, - swap_id: Uuid, - ) -> Result>; -} +pub use swap_machine::common::*; diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index 0c3d8db2..c48820df 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -1,22 +1,21 @@ //! Run an XMR/BTC swap in the role of Alice. //! Alice holds XMR and wishes receive BTC. +pub use crate::protocol::alice::swap::*; use crate::protocol::Database; -use crate::{asb, bitcoin, monero}; +use crate::{asb, monero}; +use bitcoin_wallet::BitcoinWallet; use rust_decimal::Decimal; use std::sync::Arc; use swap_env::env::Config; +pub use swap_machine::alice::*; use uuid::Uuid; -pub use self::state::*; -pub use self::swap::{run, run_until}; - -pub mod state; pub mod swap; pub struct Swap { pub state: AliceState, pub event_loop_handle: asb::EventLoopHandle, - pub bitcoin_wallet: Arc, + pub bitcoin_wallet: Arc, pub monero_wallet: Arc, pub env_config: Config, pub developer_tip: TipConfig, diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index aa35a0cb..b0f82708 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -5,15 +5,17 @@ use std::sync::Arc; use std::time::Duration; use crate::asb::{EventLoopHandle, LatestRate}; -use crate::bitcoin::ExpiredTimelocks; use crate::common::retry; +use crate::monero; use crate::monero::TransferProof; use crate::protocol::alice::{AliceState, Swap, TipConfig}; -use crate::{bitcoin, monero}; use ::bitcoin::consensus::encode::serialize_hex; use anyhow::{bail, Context, Result}; +use bitcoin_wallet::BitcoinWallet; use rust_decimal::Decimal; +use swap_core::bitcoin::ExpiredTimelocks; use swap_env::env::Config; +use swap_machine::alice::State3; use tokio::select; use tokio::time::timeout; use uuid::Uuid; @@ -36,12 +38,12 @@ where { let mut current_state = swap.state; - while !is_complete(¤t_state) && !exit_early(¤t_state) { + while !swap_machine::alice::is_complete(¤t_state) && !exit_early(¤t_state) { current_state = next_state( swap.swap_id, current_state, &mut swap.event_loop_handle, - swap.bitcoin_wallet.as_ref(), + swap.bitcoin_wallet.clone(), swap.monero_wallet.clone(), &swap.env_config, swap.developer_tip.clone(), @@ -61,7 +63,7 @@ async fn next_state( swap_id: Uuid, state: AliceState, event_loop_handle: &mut EventLoopHandle, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: Arc, monero_wallet: Arc, env_config: &Config, developer_tip: TipConfig, @@ -78,7 +80,9 @@ where Ok(match state { AliceState::Started { state3 } => { - let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; + let tx_lock_status = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_lock.clone())) + .await; match timeout( env_config.bitcoin_lock_mempool_timeout, @@ -100,7 +104,9 @@ where } } AliceState::BtcLockTransactionSeen { state3 } => { - let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; + let tx_lock_status = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_lock.clone())) + .await; match timeout( env_config.bitcoin_lock_confirmed_timeout, @@ -141,7 +147,7 @@ where // because there is no way for the swap to succeed. if !matches!( state3 - .expired_timelocks(bitcoin_wallet) + .expired_timelocks(&*bitcoin_wallet) .await .context("Failed to check for expired timelocks before locking Monero") .map_err(backoff::Error::transient)?, @@ -238,7 +244,9 @@ where let tx_early_refund_txid = tx_early_refund.compute_txid(); // Bob might cancel the swap and refund for himself. We won't need to early refund anymore. - let tx_cancel_status = bitcoin_wallet.subscribe_to(state3.tx_cancel()).await; + let tx_cancel_status = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_cancel())) + .await; let backoff = backoff::ExponentialBackoffBuilder::new() // We give up after 6 hours @@ -304,7 +312,7 @@ where monero_wallet_restore_blockheight, transfer_proof, state3, - } => match state3.expired_timelocks(bitcoin_wallet).await? { + } => match state3.expired_timelocks(&*bitcoin_wallet).await? { ExpiredTimelocks::None { .. } => { tracing::info!("Locked Monero, waiting for confirmations"); monero_wallet @@ -343,7 +351,9 @@ where transfer_proof, state3, } => { - let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; + let tx_lock_status = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_lock.clone())) + .await; tokio::select! { result = event_loop_handle.send_transfer_proof(transfer_proof.clone()) => { @@ -375,8 +385,9 @@ where transfer_proof, state3, } => { - let tx_lock_status_subscription = - bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; + let tx_lock_status_subscription = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_lock.clone())) + .await; select! { biased; // make sure the cancel timelock expiry future is polled first @@ -430,7 +441,9 @@ where "Waiting for cancellation timelock to expire", ); - let tx_lock_status = bitcoin_wallet.subscribe_to(state3.tx_lock.clone()).await; + let tx_lock_status = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_lock.clone())) + .await; tx_lock_status .wait_until_confirmed_with(state3.cancel_timelock) @@ -454,7 +467,7 @@ where match backoff::future::retry_notify(backoff.clone(), || async { // If the cancel timelock is expired, there is no need to try to publish the redeem transaction anymore if !matches!( - state3.expired_timelocks(bitcoin_wallet).await?, + state3.expired_timelocks(&*bitcoin_wallet).await?, ExpiredTimelocks::None { .. } ) { return Ok(None); @@ -502,7 +515,9 @@ where } } AliceState::BtcRedeemTransactionPublished { state3, .. } => { - let subscription = bitcoin_wallet.subscribe_to(state3.tx_redeem()).await; + let subscription = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_redeem())) + .await; match subscription.wait_until_final().await { Ok(_) => AliceState::BtcRedeemed, @@ -516,13 +531,17 @@ where transfer_proof, state3, } => { - if state3.check_for_tx_cancel(bitcoin_wallet).await?.is_none() { + if state3 + .check_for_tx_cancel(&*bitcoin_wallet) + .await? + .is_none() + { // If Bob hasn't yet broadcasted the cancel transaction, Alice has to publish it // to be able to eventually punish. Since the punish timelock is // relative to the publication of the cancel transaction we have to ensure it // gets published once the cancel timelock expires. - if let Err(e) = state3.submit_tx_cancel(bitcoin_wallet).await { + if let Err(e) = state3.submit_tx_cancel(&*bitcoin_wallet).await { // TODO: Actually ensure the transaction is published // What about a wrapper function ensure_tx_published that repeats the tx submission until // our subscription sees it in the mempool? @@ -545,10 +564,12 @@ where transfer_proof, state3, } => { - let tx_cancel_status = bitcoin_wallet.subscribe_to(state3.tx_cancel()).await; + let tx_cancel_status = bitcoin_wallet + .subscribe_to(Box::new(state3.tx_cancel())) + .await; select! { - spend_key = state3.watch_for_btc_tx_refund(bitcoin_wallet) => { + spend_key = state3.watch_for_btc_tx_refund(&*bitcoin_wallet) => { let spend_key = spend_key?; AliceState::BtcRefunded { @@ -603,7 +624,7 @@ where } => { // TODO: We should retry indefinitely here until we find the refund transaction // TODO: If we crash while we are waiting for the punish_tx to be confirmed (punish_btc waits until confirmation), we will remain in this state forever because we will attempt to re-publish the punish transaction - let punish = state3.punish_btc(bitcoin_wallet).await; + let punish = state3.punish_btc(&*bitcoin_wallet).await; match punish { Ok(_) => AliceState::BtcPunished { @@ -653,15 +674,84 @@ where }) } -pub fn is_complete(state: &AliceState) -> bool { - matches!( - state, - AliceState::XmrRefunded - | AliceState::BtcRedeemed - | AliceState::BtcPunished { .. } - | AliceState::SafelyAborted - | AliceState::BtcEarlyRefunded(_) - ) +#[allow(async_fn_in_trait)] +pub trait XmrRefundable { + async fn refund_xmr( + &self, + monero_wallet: Arc, + swap_id: Uuid, + spend_key: monero::PrivateKey, + transfer_proof: TransferProof, + ) -> Result<()>; +} + +impl XmrRefundable for State3 { + async fn refund_xmr( + &self, + monero_wallet: Arc, + swap_id: Uuid, + spend_key: monero::PrivateKey, + transfer_proof: TransferProof, + ) -> Result<()> { + let view_key = self.v; + + // Ensure that the XMR to be refunded are spendable by awaiting 10 confirmations + // on the lock transaction. + tracing::info!("Waiting for Monero lock transaction to be confirmed"); + let transfer_proof_2 = transfer_proof.clone(); + monero_wallet + .wait_until_confirmed( + self.lock_xmr_watch_request(transfer_proof_2, 10), + Some(move |(confirmations, target_confirmations)| { + tracing::debug!( + %confirmations, + %target_confirmations, + "Monero lock transaction got a confirmation" + ); + }), + ) + .await + .context("Failed to wait for Monero lock transaction to be confirmed")?; + + tracing::info!("Refunding Monero"); + + tracing::debug!(%swap_id, "Opening temporary Monero wallet from keys"); + let swap_wallet = monero_wallet + .swap_wallet(swap_id, spend_key, view_key, transfer_proof.tx_hash()) + .await + .context(format!("Failed to open/create swap wallet `{}`", swap_id))?; + + // Update blockheight to ensure that the wallet knows the funds are unlocked + tracing::debug!(%swap_id, "Updating temporary Monero wallet's blockheight"); + let _ = swap_wallet + .blockchain_height() + .await + .context("Couldn't get Monero blockheight")?; + + tracing::debug!(%swap_id, "Sweeping Monero to redeem address"); + let main_address = monero_wallet.main_wallet().await.main_address().await; + + swap_wallet + .sweep(&main_address) + .await + .context("Failed to sweep Monero to redeem address")?; + + Ok(()) + } +} + +impl XmrRefundable for Box { + async fn refund_xmr( + &self, + monero_wallet: Arc, + swap_id: Uuid, + spend_key: monero::PrivateKey, + transfer_proof: TransferProof, + ) -> Result<()> { + (**self) + .refund_xmr(monero_wallet, swap_id, spend_key, transfer_proof) + .await + } } /// Build transfer destinations for the Monero lock transaction, optionally including a developer tip. diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index df2cb3d9..d19abaca 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -1,26 +1,28 @@ use std::sync::Arc; use anyhow::Result; +use bitcoin_wallet::BitcoinWallet; +use std::convert::TryInto; use uuid::Uuid; use crate::cli::api::tauri_bindings::TauriHandle; use crate::monero::MoneroAddressPool; use crate::protocol::Database; -use crate::{bitcoin, cli, monero}; +use crate::{cli, monero}; + +use swap_core::bitcoin; use swap_env::env; -pub use self::state::*; -pub use self::swap::{run, run_until}; -use std::convert::TryInto; +pub use crate::protocol::bob::swap::*; +pub use swap_machine::bob::*; -pub mod state; pub mod swap; pub struct Swap { pub state: BobState, pub event_loop_handle: cli::EventLoopHandle, pub db: Arc, - pub bitcoin_wallet: Arc, + pub bitcoin_wallet: Arc, pub monero_wallet: Arc, pub env_config: env::Config, pub id: Uuid, @@ -33,7 +35,7 @@ impl Swap { pub fn new( db: Arc, id: Uuid, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, monero_wallet: Arc, env_config: env::Config, event_loop_handle: cli::EventLoopHandle, @@ -63,7 +65,7 @@ impl Swap { pub async fn from_db( db: Arc, id: Uuid, - bitcoin_wallet: Arc, + bitcoin_wallet: Arc, monero_wallet: Arc, env_config: env::Config, event_loop_handle: cli::EventLoopHandle, diff --git a/swap/src/protocol/bob/swap.rs b/swap/src/protocol/bob/swap.rs index 92bdca98..e9a58f1b 100644 --- a/swap/src/protocol/bob/swap.rs +++ b/swap/src/protocol/bob/swap.rs @@ -1,34 +1,25 @@ -use crate::bitcoin::wallet::ScriptStatus; -use crate::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; use crate::cli::api::tauri_bindings::LockBitcoinDetails; use crate::cli::api::tauri_bindings::{TauriEmitter, TauriHandle, TauriSwapProgressEvent}; use crate::cli::EventLoopHandle; use crate::common::retry; +use crate::monero; use crate::monero::MoneroAddressPool; use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; use crate::network::swap_setup::bob::NewSwap; -use crate::protocol::bob::state::*; +use crate::protocol::bob::*; use crate::protocol::{bob, Database}; -use crate::{bitcoin, monero}; use anyhow::{bail, Context as AnyContext, Result}; use std::sync::Arc; use std::time::Duration; +use swap_core::bitcoin::{ExpiredTimelocks, TxCancel, TxRefund}; +use swap_core::monero::TxHash; use swap_env::env; +use swap_machine::bob::State5; use tokio::select; use uuid::Uuid; const PRE_BTC_LOCK_APPROVAL_TIMEOUT_SECS: u64 = 60 * 3; -pub fn is_complete(state: &BobState) -> bool { - matches!( - state, - BobState::BtcRefunded(..) - | BobState::BtcEarlyRefunded { .. } - | BobState::XmrRedeemed { .. } - | BobState::SafelyAborted - ) -} - /// Identifies states that have already processed the transfer proof. /// This is used to be able to acknowledge the transfer proof multiple times (if it was already processed). /// This is necessary because sometimes our acknowledgement might not reach Alice. @@ -73,7 +64,7 @@ pub async fn run_until( current_state.clone(), &mut swap.event_loop_handle, swap.db.clone(), - swap.bitcoin_wallet.as_ref(), + swap.bitcoin_wallet.clone(), swap.monero_wallet.clone(), swap.monero_receive_pool.clone(), swap.event_emitter.clone(), @@ -101,7 +92,7 @@ async fn next_state( state: BobState, event_loop_handle: &mut EventLoopHandle, db: Arc, - bitcoin_wallet: &bitcoin::Wallet, + bitcoin_wallet: Arc, monero_wallet: Arc, monero_receive_pool: MoneroAddressPool, event_emitter: Option, @@ -267,16 +258,19 @@ async fn next_state( }, ); - let (tx_early_refund_status, tx_lock_status) = tokio::join!( - bitcoin_wallet.subscribe_to(state3.construct_tx_early_refund()), - bitcoin_wallet.subscribe_to(state3.tx_lock.clone()) + let (tx_early_refund_status, tx_lock_status): ( + bitcoin_wallet::Subscription, + bitcoin_wallet::Subscription, + ) = tokio::join!( + bitcoin_wallet.subscribe_to(Box::new(state3.construct_tx_early_refund())), + bitcoin_wallet.subscribe_to(Box::new(state3.tx_lock.clone())) ); // Check explicitly whether the cancel timelock has expired // (Most likely redundant but cannot hurt) // We only warn if this fails if let Ok(true) = state3 - .expired_timelock(bitcoin_wallet) + .expired_timelock(&*bitcoin_wallet) .await .inspect_err(|err| { tracing::warn!(?err, "Failed to check for cancel timelock expiration"); @@ -291,7 +285,7 @@ async fn next_state( // (Most likely redundant because we already do this below but cannot hurt) // We only warn if this fail here if let Ok(Some(_)) = state3 - .check_for_tx_early_refund(bitcoin_wallet) + .check_for_tx_early_refund(&*bitcoin_wallet) .await .inspect_err(|err| { tracing::warn!(?err, "Failed to check for early refund transaction"); @@ -324,7 +318,7 @@ async fn next_state( let cancel_timelock_expires = tx_lock_status.wait_until(|status| { // Emit a tauri event on new confirmations match status { - ScriptStatus::Confirmed(confirmed) => { + bitcoin_wallet::primitives::ScriptStatus::Confirmed(confirmed) => { event_emitter.emit_swap_progress_event( swap_id, TauriSwapProgressEvent::BtcLockTxInMempool { @@ -333,7 +327,7 @@ async fn next_state( }, ); } - ScriptStatus::InMempool => { + bitcoin_wallet::primitives::ScriptStatus::InMempool => { event_emitter.emit_swap_progress_event( swap_id, TauriSwapProgressEvent::BtcLockTxInMempool { @@ -342,7 +336,8 @@ async fn next_state( }, ); } - ScriptStatus::Unseen | ScriptStatus::Retrying => { + bitcoin_wallet::primitives::ScriptStatus::Unseen + | bitcoin_wallet::primitives::ScriptStatus::Retrying => { event_emitter.emit_swap_progress_event( swap_id, TauriSwapProgressEvent::BtcLockTxInMempool { @@ -402,7 +397,7 @@ async fn next_state( // Check if the cancel timelock has expired // If it has, we have to cancel the swap if state - .expired_timelock(bitcoin_wallet) + .expired_timelock(&*bitcoin_wallet) .await? .cancel_timelock_expired() { @@ -413,9 +408,12 @@ async fn next_state( let tx_early_refund = state.construct_tx_early_refund(); - let (tx_lock_status, tx_early_refund_status) = tokio::join!( - bitcoin_wallet.subscribe_to(state.tx_lock.clone()), - bitcoin_wallet.subscribe_to(tx_early_refund.clone()) + let (tx_lock_status, tx_early_refund_status): ( + bitcoin_wallet::Subscription, + bitcoin_wallet::Subscription, + ) = tokio::join!( + bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())), + bitcoin_wallet.subscribe_to(Box::new(tx_early_refund.clone())) ); // Clone these so that we can move them into the listener closure @@ -485,22 +483,25 @@ async fn next_state( // In case we send the encrypted signature to Alice, but she doesn't give us a confirmation // We need to check if she still published the Bitcoin redeem transaction // Otherwise we risk staying stuck in "XmrLocked" - if let Some(state5) = state.check_for_tx_redeem(bitcoin_wallet).await? { + if let Some(state5) = state.check_for_tx_redeem(&*bitcoin_wallet).await? { return Ok(BobState::BtcRedeemed(state5)); } // Check whether we can cancel the swap and do so if possible. if state - .expired_timelock(bitcoin_wallet) + .expired_timelock(&*bitcoin_wallet) .await? .cancel_timelock_expired() { return Ok(BobState::CancelTimelockExpired(state.cancel())); } - let (tx_lock_status, tx_early_refund_status) = tokio::join!( - bitcoin_wallet.subscribe_to(state.tx_lock.clone()), - bitcoin_wallet.subscribe_to(state.construct_tx_early_refund()) + let (tx_lock_status, tx_early_refund_status): ( + bitcoin_wallet::Subscription, + bitcoin_wallet::Subscription, + ) = tokio::join!( + bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())), + bitcoin_wallet.subscribe_to(Box::new(state.construct_tx_early_refund())) ); // Alice has locked her Monero @@ -538,27 +539,30 @@ async fn next_state( // We need to make sure that Alice did not publish the redeem transaction while we were offline // Even if the cancel timelock expired, if Alice published the redeem transaction while we were away we cannot miss it // If we do we cannot refund and will never be able to leave the "CancelTimelockExpired" state - if let Some(state5) = state.check_for_tx_redeem(bitcoin_wallet).await? { + if let Some(state5) = state.check_for_tx_redeem(&*bitcoin_wallet).await? { return Ok(BobState::BtcRedeemed(state5)); } if state - .expired_timelock(bitcoin_wallet) + .expired_timelock(&*bitcoin_wallet) .await? .cancel_timelock_expired() { return Ok(BobState::CancelTimelockExpired(state.cancel())); } - let (tx_lock_status, tx_early_refund_status) = tokio::join!( - bitcoin_wallet.subscribe_to(state.tx_lock.clone()), - bitcoin_wallet.subscribe_to(state.construct_tx_early_refund()) + let (tx_lock_status, tx_early_refund_status): ( + bitcoin_wallet::Subscription, + bitcoin_wallet::Subscription, + ) = tokio::join!( + bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())), + bitcoin_wallet.subscribe_to(Box::new(state.construct_tx_early_refund())) ); select! { // Wait for Alice to redeem the Bitcoin // We can then extract the key and redeem our Monero - state5 = state.watch_for_redeem_btc(bitcoin_wallet) => { + state5 = state.watch_for_redeem_btc(&*bitcoin_wallet) => { BobState::BtcRedeemed(state5?) }, // Wait for the cancel timelock to expire @@ -613,6 +617,7 @@ async fn next_state( "Redeeming Monero", || async { state + .clone() .redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone()) .await .map_err(backoff::Error::transient) @@ -639,16 +644,20 @@ async fn next_state( event_emitter .emit_swap_progress_event(swap_id, TauriSwapProgressEvent::CancelTimelockExpired); - if state6.check_for_tx_cancel(bitcoin_wallet).await?.is_none() { + if state6 + .check_for_tx_cancel(&*bitcoin_wallet) + .await? + .is_none() + { tracing::debug!("Couldn't find tx_cancel yet, publishing ourselves"); - if let Err(tx_cancel_err) = state6.submit_tx_cancel(bitcoin_wallet).await { + if let Err(tx_cancel_err) = state6.submit_tx_cancel(&*bitcoin_wallet).await { tracing::warn!(err = %tx_cancel_err, "Failed to publish tx_cancel even though it is not present in the chain. Did Alice already refund us our Bitcoin early?"); // If tx_cancel is not present in the chain and we fail to publish it. There's only one logical conclusion: // The tx_lock UTXO has been spent by the tx_early_refund transaction // Therefore we check for the early refund transaction - match state6.check_for_tx_early_refund(bitcoin_wallet).await? { + match state6.check_for_tx_early_refund(&*bitcoin_wallet).await? { Some(_) => { return Ok(BobState::BtcEarlyRefundPublished(state6)); } @@ -670,14 +679,14 @@ async fn next_state( ); // Bob has cancelled the swap - match state.expired_timelock(bitcoin_wallet).await? { + match state.expired_timelock(&*bitcoin_wallet).await? { ExpiredTimelocks::None { .. } => { bail!( "Internal error: canceled state reached before cancel timelock was expired" ); } ExpiredTimelocks::Cancel { .. } => { - let btc_refund_txid = state.publish_refund_btc(bitcoin_wallet).await?; + let btc_refund_txid = state.publish_refund_btc(&*bitcoin_wallet).await?; tracing::info!(%btc_refund_txid, "Refunded our Bitcoin"); @@ -702,9 +711,12 @@ async fn next_state( let tx_refund = state.construct_tx_refund()?; let tx_early_refund = state.construct_tx_early_refund(); - let (tx_refund_status, tx_early_refund_status) = tokio::join!( - bitcoin_wallet.subscribe_to(tx_refund.clone()), - bitcoin_wallet.subscribe_to(tx_early_refund.clone()), + let (tx_refund_status, tx_early_refund_status): ( + bitcoin_wallet::Subscription, + bitcoin_wallet::Subscription, + ) = tokio::join!( + bitcoin_wallet.subscribe_to(Box::new(tx_refund.clone())), + bitcoin_wallet.subscribe_to(Box::new(tx_early_refund.clone())), ); // Either of these two refund transactions could have been published @@ -753,9 +765,12 @@ async fn next_state( ); // Wait for confirmations - let (tx_lock_status, tx_early_refund_status) = tokio::join!( - bitcoin_wallet.subscribe_to(state.tx_lock.clone()), - bitcoin_wallet.subscribe_to(tx_early_refund_tx.clone()), + let (tx_lock_status, tx_early_refund_status): ( + bitcoin_wallet::Subscription, + bitcoin_wallet::Subscription, + ) = tokio::join!( + bitcoin_wallet.subscribe_to(Box::new(state.tx_lock.clone())), + bitcoin_wallet.subscribe_to(Box::new(tx_early_refund_tx.clone())), ); select! { @@ -852,6 +867,7 @@ async fn next_state( "Redeeming Monero", || async { state5 + .clone() .redeem_xmr(&monero_wallet, swap_id, monero_receive_pool.clone()) .await .map_err(backoff::Error::transient) @@ -941,3 +957,63 @@ async fn next_state( } }) } + +trait XmrRedeemable { + async fn redeem_xmr( + self, + monero_wallet: &monero::Wallets, + swap_id: Uuid, + monero_receive_pool: MoneroAddressPool, + ) -> Result>; +} + +impl XmrRedeemable for State5 { + async fn redeem_xmr( + self: State5, + monero_wallet: &monero::Wallets, + swap_id: Uuid, + monero_receive_pool: MoneroAddressPool, + ) -> Result> { + let (spend_key, view_key) = self.xmr_keys(); + + tracing::info!(%swap_id, "Redeeming Monero from extracted keys"); + + tracing::debug!(%swap_id, "Opening temporary Monero wallet"); + + let wallet = monero_wallet + .swap_wallet( + swap_id, + spend_key, + view_key, + self.lock_transfer_proof.tx_hash(), + ) + .await + .context("Failed to open Monero wallet")?; + + // Update blockheight to ensure that the wallet knows the funds are unlocked + tracing::debug!(%swap_id, "Updating temporary Monero wallet's blockheight"); + let _ = wallet + .blockchain_height() + .await + .context("Couldn't get Monero blockheight")?; + + tracing::debug!(%swap_id, receive_address=?monero_receive_pool, "Sweeping Monero to receive address"); + + let main_address = monero_wallet.main_wallet().await.main_address().await; + + let tx_hashes = wallet + .sweep_multi_destination( + &monero_receive_pool.fill_empty_addresses(main_address), + &monero_receive_pool.percentages(), + ) + .await + .context("Failed to redeem Monero")? + .into_iter() + .map(|tx_receipt| TxHash(tx_receipt.txid)) + .collect(); + + tracing::info!(%swap_id, txids=?tx_hashes, "Monero sweep completed"); + + Ok(tx_hashes) + } +} diff --git a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs index 568dc75e..01991be1 100644 --- a/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs +++ b/swap/tests/alice_and_bob_refund_using_cancel_and_refund_command.rs @@ -40,7 +40,7 @@ async fn given_alice_and_bob_manually_refund_after_funds_locked_both_refund() { if let BobState::BtcLocked { state3, .. } = bob_swap.state.clone() { bob_swap .bitcoin_wallet - .subscribe_to(state3.tx_lock) + .subscribe_to(Box::new(state3.tx_lock)) .await .wait_until_confirmed_with(state3.cancel_timelock) .await?; diff --git a/swap/tests/alice_and_bob_refund_using_cancel_then_refund_command.rs b/swap/tests/alice_and_bob_refund_using_cancel_then_refund_command.rs index a2c6b53e..62c339bc 100644 --- a/swap/tests/alice_and_bob_refund_using_cancel_then_refund_command.rs +++ b/swap/tests/alice_and_bob_refund_using_cancel_then_refund_command.rs @@ -40,7 +40,7 @@ async fn given_alice_and_bob_manually_cancel_and_refund_after_funds_locked_both_ if let BobState::BtcLocked { state3, .. } = bob_swap.state.clone() { bob_swap .bitcoin_wallet - .subscribe_to(state3.tx_lock) + .subscribe_to(Box::new(state3.tx_lock)) .await .wait_until_confirmed_with(state3.cancel_timelock) .await?; diff --git a/swap/tests/alice_manually_punishes_after_bob_dead.rs b/swap/tests/alice_manually_punishes_after_bob_dead.rs index e3ca76f9..9cd9f447 100644 --- a/swap/tests/alice_manually_punishes_after_bob_dead.rs +++ b/swap/tests/alice_manually_punishes_after_bob_dead.rs @@ -36,7 +36,7 @@ async fn alice_manually_punishes_after_bob_dead() { // Ensure cancel timelock is expired if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state { alice_bitcoin_wallet - .subscribe_to(state3.tx_lock) + .subscribe_to(Box::new(state3.tx_lock)) .await .wait_until_confirmed_with(state3.cancel_timelock) .await?; @@ -54,7 +54,7 @@ async fn alice_manually_punishes_after_bob_dead() { // Ensure punish timelock is expired if let AliceState::BtcCancelled { state3, .. } = alice_state { alice_bitcoin_wallet - .subscribe_to(state3.tx_cancel()) + .subscribe_to(Box::new(state3.tx_cancel())) .await .wait_until_confirmed_with(state3.punish_timelock) .await?; diff --git a/swap/tests/alice_manually_punishes_after_bob_dead_and_bob_cancels.rs b/swap/tests/alice_manually_punishes_after_bob_dead_and_bob_cancels.rs index 6c5e8e87..21d1cb10 100644 --- a/swap/tests/alice_manually_punishes_after_bob_dead_and_bob_cancels.rs +++ b/swap/tests/alice_manually_punishes_after_bob_dead_and_bob_cancels.rs @@ -36,7 +36,7 @@ async fn alice_manually_punishes_after_bob_dead_and_bob_cancels() { // Ensure cancel timelock is expired if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state { alice_bitcoin_wallet - .subscribe_to(state3.tx_lock) + .subscribe_to(Box::new(state3.tx_lock)) .await .wait_until_confirmed_with(state3.cancel_timelock) .await?; @@ -54,7 +54,7 @@ async fn alice_manually_punishes_after_bob_dead_and_bob_cancels() { // Ensure punish timelock is expired if let AliceState::BtcCancelled { state3, .. } = alice_state { alice_bitcoin_wallet - .subscribe_to(state3.tx_cancel()) + .subscribe_to(Box::new(state3.tx_cancel())) .await .wait_until_confirmed_with(state3.punish_timelock) .await?; diff --git a/swap/tests/alice_punishes_after_restart_bob_dead.rs b/swap/tests/alice_punishes_after_restart_bob_dead.rs index a2b4db4f..cabf76a0 100644 --- a/swap/tests/alice_punishes_after_restart_bob_dead.rs +++ b/swap/tests/alice_punishes_after_restart_bob_dead.rs @@ -35,7 +35,7 @@ async fn alice_punishes_after_restart_if_bob_dead() { // cancel transaction is not published at this point) if let AliceState::XmrLockTransactionSent { state3, .. } = alice_state { alice_bitcoin_wallet - .subscribe_to(state3.tx_lock) + .subscribe_to(Box::new(state3.tx_lock)) .await .wait_until_confirmed_with(state3.cancel_timelock) .await?; diff --git a/swap/tests/bdk.sh b/swap/tests/bdk.sh deleted file mode 100755 index abc9e57a..00000000 --- a/swap/tests/bdk.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -VERSION=1.0.0-rc.19 - -mkdir bdk -stat ./target/debug/swap || exit 1 -cp ./target/debug/swap bdk/swap-current -pushd bdk - -echo "download swap $VERSION" -curl -L "https://github.com/eigenwallet/core/releases/download/${VERSION}/swap_${VERSION}_Linux_x86_64.tar" | tar xv - -echo "create testnet wallet with $VERSION" -./swap --testnet --data-base-dir . --debug balance || exit 1 -echo "check testnet wallet with this version" -./swap-current --testnet --data-base-dir . --debug balance || exit 1 - -echo "create mainnet wallet with $VERSION" -./swap --version || exit 1 -./swap --data-base-dir . --debug balance || exit 1 -echo "check mainnet wallet with this version" -./swap-current --version || exit 1 -./swap-current --data-base-dir . --debug balance || exit 1 - -exit 0 diff --git a/swap/tests/happy_path_bob_offline_while_alice_redeems_btc.rs b/swap/tests/happy_path_bob_offline_while_alice_redeems_btc.rs index c02c7a81..1b4774ac 100644 --- a/swap/tests/happy_path_bob_offline_while_alice_redeems_btc.rs +++ b/swap/tests/happy_path_bob_offline_while_alice_redeems_btc.rs @@ -26,7 +26,7 @@ async fn given_bob_restarts_while_alice_redeems_btc() { if let BobState::EncSigSent(state4) = bob_swap.state.clone() { bob_swap .bitcoin_wallet - .subscribe_to(state4.tx_lock) + .subscribe_to(Box::new(state4.tx_lock)) .await .wait_until_confirmed_with(state4.cancel_timelock) .await?; diff --git a/throttle/src/throttle.rs b/throttle/src/throttle.rs index c092d643..eb08e9c6 100644 --- a/throttle/src/throttle.rs +++ b/throttle/src/throttle.rs @@ -2,7 +2,7 @@ // MIT License use std::pin::Pin; -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::{Arc, Mutex, mpsc}; use std::time::{self, /* SystemTime, UNIX_EPOCH, */ Duration}; pub fn throttle(closure: F, delay: Duration) -> Throttle