From a0ef1f96eccb33ace00f3f56630894b2bb52beaa Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 2 Feb 2021 10:39:34 +1100 Subject: [PATCH 1/7] Replace bitcoind wallet with bdk wallet The bitcoind wallet required the user to run a bitcoind node. It was replaced with a bdk wallet which allows the user to connect to an electrum instance hosted remotely. An electrum and bitcoind testcontainer were created to the test the bdk wallet. The electrum container reads the blockdata from the bitcoind testcontainer through a shared volume. bitcoind-harness was removed as bitcoind initialisation code was moved into test_utils. The bdk wallet differs from the bitcoind wallet in that it needs to be manually synced with an electrum node. We synchronise the wallet once upon initialisation to prevent a potentially long running blocking task from interrupting protocol execution. The electrum HTTP API was used to get the latest block height and the transaction block height as this functionality was not present in the bdk wallet API or it required the bdk wallet to be re-synced to get an up to date value. --- Cargo.lock | 298 +++++++++++++++++++++++-------- swap/Cargo.toml | 1 + swap/src/bin/nectar.rs | 21 ++- swap/src/bin/swap_cli.rs | 23 ++- swap/src/bitcoin.rs | 2 +- swap/src/bitcoin/lock.rs | 23 ++- swap/src/bitcoin/timelocks.rs | 7 +- swap/src/bitcoin/wallet.rs | 217 +++++++++++++++------- swap/src/cli/config.rs | 29 +-- swap/src/nectar/config.rs | 40 +++-- swap/tests/testutils/bitcoind.rs | 139 ++++++++++++++ swap/tests/testutils/electrs.rs | 156 ++++++++++++++++ swap/tests/testutils/mod.rs | 279 ++++++++++++++++++++++++++--- 13 files changed, 1010 insertions(+), 225 deletions(-) create mode 100644 swap/tests/testutils/bitcoind.rs create mode 100644 swap/tests/testutils/electrs.rs diff --git a/Cargo.lock b/Cargo.lock index 8e218318..d4b7b63c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -75,7 +75,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -132,7 +132,7 @@ dependencies = [ "polling", "vec-arena", "waker-fn", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -163,11 +163,11 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4401f0a3622dad2e0763fa79e0eb328bc70fb7dccfdd645341f00d671247d6" dependencies = [ - "bytes", + "bytes 1.0.1", "futures-sink", "futures-util", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.4", ] [[package]] @@ -187,7 +187,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -212,7 +212,7 @@ dependencies = [ "instant", "pin-project 1.0.4", "rand 0.8.2", - "tokio", + "tokio 1.0.2", ] [[package]] @@ -230,6 +230,15 @@ dependencies = [ "keccak-hash 0.1.2", ] +[[package]] +name = "base64" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" +dependencies = [ + "byteorder", +] + [[package]] name = "base64" version = "0.12.3" @@ -242,6 +251,37 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bdk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2fd4c84e2baef750794e7c3f317e37c0c611ef7b29c9a9f18c7e51940dbfdb5" +dependencies = [ + "async-trait", + "bdk-macros", + "bitcoin", + "electrum-client", + "js-sys", + "log", + "miniscript", + "rand 0.7.3", + "serde", + "serde_json", + "sled", + "tokio 0.2.25", +] + +[[package]] +name = "bdk-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62874901df222eb0fc3bad6e425bc2a935287b8110be0d1ad6d729af86cf6e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bech32" version = "0.7.2" @@ -283,7 +323,7 @@ dependencies = [ "serde_json", "testcontainers", "thiserror", - "tokio", + "tokio 1.0.2", "tracing", "url", ] @@ -428,6 +468,12 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "bytes" version = "1.0.1" @@ -553,7 +599,7 @@ dependencies = [ "regex", "terminal_size", "unicode-width", - "winapi", + "winapi 0.3.9", "winapi-util", ] @@ -807,7 +853,7 @@ checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" dependencies = [ "libc", "redox_users 0.3.5", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -818,7 +864,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users 0.4.0", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -867,6 +913,22 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "electrum-client" +version = "0.5.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedfb48f66ab17ba3b2c69f8ff32f68d8b5dbc7839c0ca4e94237b835ca608dd" +dependencies = [ + "bitcoin", + "log", + "rustls", + "serde", + "serde_json", + "socks", + "webpki", + "webpki-roots", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1013,7 +1075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1082,7 +1144,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite", + "pin-project-lite 0.2.4", "waker-fn", ] @@ -1132,7 +1194,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.4", "pin-utils", "proc-macro-hack", "proc-macro-nested", @@ -1180,8 +1242,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1211,7 +1275,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b67e66362108efccd8ac053abafc8b7a8d86a37e6e48fc4f6f7485eb5e9e6a5" dependencies = [ - "bytes", + "bytes 1.0.1", "fnv", "futures-core", "futures-sink", @@ -1219,7 +1283,7 @@ dependencies = [ "http", "indexmap", "slab", - "tokio", + "tokio 1.0.2", "tokio-util", "tracing", "tracing-futures", @@ -1246,7 +1310,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1679e6ea370dee694f91f1dc469bf94cf8f52051d147aec3e1f9497c6fc22461" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1335,7 +1399,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" dependencies = [ - "bytes", + "bytes 1.0.1", "fnv", "itoa", ] @@ -1346,7 +1410,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" dependencies = [ - "bytes", + "bytes 1.0.1", "http", ] @@ -1368,7 +1432,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12219dc884514cb4a6a03737f4413c0e01c23a1b059b0156004b23f1e19dccbe" dependencies = [ - "bytes", + "bytes 1.0.1", "futures-channel", "futures-core", "futures-util", @@ -1380,7 +1444,7 @@ dependencies = [ "itoa", "pin-project 1.0.4", "socket2", - "tokio", + "tokio 1.0.2", "tower-service", "tracing", "want", @@ -1392,10 +1456,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes", + "bytes 1.0.1", "hyper", "native-tls", - "tokio", + "tokio 1.0.2", "tokio-native-tls", ] @@ -1418,7 +1482,7 @@ checksum = "28538916eb3f3976311f5dfbe67b5362d0add1293d0a9cad17debf86f8e3aa48" dependencies = [ "if-addrs-sys", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1444,7 +1508,7 @@ dependencies = [ "ipnet", "libc", "log", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1579,7 +1643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5133112ce42be9482f6a87be92a605dd6bbc9e93c297aee77d172ff06908f3a" dependencies = [ "atomic", - "bytes", + "bytes 1.0.1", "futures", "lazy_static", "libp2p-core", @@ -1669,7 +1733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2705dc94b01ab9e3779b42a09bbf3712e637ed213e875c30face247291a85af0" dependencies = [ "asynchronous-codec", - "bytes", + "bytes 1.0.1", "futures", "libp2p-core", "log", @@ -1686,7 +1750,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aca322b52a0c5136142a7c3971446fb1e9964923a526c9cc6ef3b7c94e57778" dependencies = [ - "bytes", + "bytes 1.0.1", "curve25519-dalek 3.0.2", "futures", "lazy_static", @@ -1709,7 +1773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d37637a4b33b5390322ccc068a33897d0aa541daf4fec99f6a7efbf37295346e" dependencies = [ "async-trait", - "bytes", + "bytes 1.0.1", "futures", "libp2p-core", "libp2p-swarm", @@ -1754,7 +1818,7 @@ dependencies = [ "libp2p-core", "log", "socket2", - "tokio", + "tokio 1.0.2", ] [[package]] @@ -1901,7 +1965,7 @@ dependencies = [ "log", "miow", "ntapi", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1911,7 +1975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" dependencies = [ "socket2", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1945,7 +2009,7 @@ dependencies = [ "serde_json", "spectral", "testcontainers", - "tokio", + "tokio 1.0.2", "tracing", "tracing-log", "tracing-subscriber", @@ -1991,7 +2055,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10ddc0eb0117736f19d556355464fc87efc8ad98b29e3fd84f02531eb6e90840" dependencies = [ - "bytes", + "bytes 1.0.1", "futures", "log", "pin-project 1.0.4", @@ -2024,7 +2088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8123a81538e457d44b933a02faf885d3fe8408806b23fa700e8f01c6c3a98998" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2050,7 +2114,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2250,7 +2314,7 @@ dependencies = [ "libc", "redox_syscall 0.1.57", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2320,6 +2384,12 @@ dependencies = [ "syn", ] +[[package]] +name = "pin-project-lite" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" + [[package]] name = "pin-project-lite" version = "0.2.4" @@ -2348,7 +2418,7 @@ dependencies = [ "libc", "log", "wepoll-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2469,7 +2539,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e6984d2f1a23009bd270b8bb56d0926810a3d483f59c987d77969e9d8e840b2" dependencies = [ - "bytes", + "bytes 1.0.1", "prost-derive", ] @@ -2479,7 +2549,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32d3ebd75ac2679c2af3a92246639f9fcc8a442ee420719cc4fe195b98dd5fa3" dependencies = [ - "bytes", + "bytes 1.0.1", "heck", "itertools", "log", @@ -2510,7 +2580,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b518d7cdd93dab1d1122cf07fa9a60771836c668dde9d9e2a139f957f0d9f1bb" dependencies = [ - "bytes", + "bytes 1.0.1", "prost", ] @@ -2539,7 +2609,7 @@ dependencies = [ "libc", "rand_core 0.3.1", "rdrand", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2552,7 +2622,7 @@ dependencies = [ "fuchsia-cprng", "libc", "rand_core 0.3.1", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2571,7 +2641,7 @@ dependencies = [ "rand_os", "rand_pcg", "rand_xorshift", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2706,7 +2776,7 @@ checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" dependencies = [ "libc", "rand_core 0.4.2", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2720,7 +2790,7 @@ dependencies = [ "libc", "rand_core 0.4.2", "rdrand", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2818,7 +2888,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2828,7 +2898,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd281b1030aa675fb90aa994d07187645bb3c8fc756ca766e7c3070b439de9de" dependencies = [ "base64 0.13.0", - "bytes", + "bytes 1.0.1", "encoding_rs", "futures-core", "futures-util", @@ -2843,11 +2913,11 @@ dependencies = [ "mime", "native-tls", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.4", "serde", "serde_json", "serde_urlencoded", - "tokio", + "tokio 1.0.2", "tokio-native-tls", "url", "wasm-bindgen", @@ -2868,7 +2938,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2915,6 +2985,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +dependencies = [ + "base64 0.10.1", + "log", + "ring", + "sct", + "webpki", +] + [[package]] name = "rw-stream-sink" version = "0.2.1" @@ -2939,7 +3022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" dependencies = [ "lazy_static", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2948,6 +3031,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.19.0" @@ -3200,7 +3293,19 @@ checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" dependencies = [ "cfg-if 1.0.0", "libc", - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "socks" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30f86c7635fadf2814201a4f67efefb0007588ae7422ce299f354ab5c97f61ae" +dependencies = [ + "byteorder", + "libc", + "winapi 0.2.8", + "ws2_32-sys", ] [[package]] @@ -3371,6 +3476,7 @@ dependencies = [ "atty", "backoff", "base64 0.12.3", + "bdk", "bitcoin", "bitcoin-harness", "config", @@ -3410,7 +3516,7 @@ dependencies = [ "testcontainers", "thiserror", "time", - "tokio", + "tokio 1.0.2", "toml", "tracing", "tracing-core", @@ -3456,7 +3562,7 @@ dependencies = [ "rand 0.8.2", "redox_syscall 0.2.4", "remove_dir_all", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3467,7 +3573,7 @@ checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" dependencies = [ "byteorder", "dirs", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3477,7 +3583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3546,7 +3652,7 @@ dependencies = [ "stdweb", "time-macros", "version_check", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3596,6 +3702,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "tokio" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" +dependencies = [ + "bytes 0.5.6", + "pin-project-lite 0.1.11", + "slab", +] + [[package]] name = "tokio" version = "1.0.2" @@ -3603,12 +3720,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca04cec6ff2474c638057b65798f60ac183e5e79d3448bb7163d36a39cff6ec" dependencies = [ "autocfg 1.0.1", - "bytes", + "bytes 1.0.1", "libc", "memchr", "mio", "num_cpus", - "pin-project-lite", + "pin-project-lite 0.2.4", "tokio-macros", ] @@ -3630,7 +3747,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" dependencies = [ "native-tls", - "tokio", + "tokio 1.0.2", ] [[package]] @@ -3640,8 +3757,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76066865172052eb8796c686f0b441a93df8b08d40a950b062ffb9a426f00edd" dependencies = [ "futures-core", - "pin-project-lite", - "tokio", + "pin-project-lite 0.2.4", + "tokio 1.0.2", ] [[package]] @@ -3650,12 +3767,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12ae4751faa60b9f96dd8344d74592e5a17c0c9a220413dbc6942d14139bbfcc" dependencies = [ - "bytes", + "bytes 1.0.1", "futures-core", "futures-sink", "log", - "pin-project-lite", - "tokio", + "pin-project-lite 0.2.4", + "tokio 1.0.2", "tokio-stream", ] @@ -3681,7 +3798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" dependencies = [ "cfg-if 1.0.0", - "pin-project-lite", + "pin-project-lite 0.2.4", "tracing-attributes", "tracing-core", ] @@ -3840,7 +3957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35581ff83d4101e58b582e607120c7f5ffb17e632a980b1f38334d76b36908b2" dependencies = [ "asynchronous-codec", - "bytes", + "bytes 1.0.1", "futures-io", "futures-util", ] @@ -4025,6 +4142,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" +dependencies = [ + "webpki", +] + [[package]] name = "wepoll-sys" version = "3.0.1" @@ -4044,6 +4180,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -4054,6 +4196,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -4066,7 +4214,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4081,7 +4229,17 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "winapi", + "winapi 0.3.9", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", ] [[package]] diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 6bd8de00..23bafb11 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -18,6 +18,7 @@ async-trait = "0.1" atty = "0.2" backoff = { git = "https://github.com/ihrwein/backoff", rev = "9d03992a83dfdc596be26276d4e5c5254a4b11a2", features = ["tokio"] } base64 = "0.12" +bdk = { version = "0.3" } bitcoin = { version = "0.25", features = ["rand", "use-serde"] } bitcoin-harness = { git = "https://github.com/coblox/bitcoin-harness-rs", rev = "ae2f6cd547496e680941c0910018bbe884128799" } config = { version = "0.10", default-features = false, features = ["toml"] } diff --git a/swap/src/bin/nectar.rs b/swap/src/bin/nectar.rs index 3b649a1f..23af531e 100644 --- a/swap/src/bin/nectar.rs +++ b/swap/src/bin/nectar.rs @@ -15,7 +15,7 @@ use anyhow::{Context, Result}; use log::LevelFilter; use prettytable::{row, Table}; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use structopt::StructOpt; use swap::{ bitcoin, @@ -70,9 +70,13 @@ async fn main() -> Result<()> { config.data.dir.display() ); - let db = Database::open(config.data.dir.join("database").as_path()) + let db_path = config.data.dir.join("database"); + + let db = Database::open(config.data.dir.join(db_path).as_path()) .context("Could not open database")?; + let wallet_data_dir = config.data.dir.join("wallet"); + match opt.cmd { Command::Start => { let seed = Seed::from_file_or_generate(&config.data.dir) @@ -80,7 +84,8 @@ async fn main() -> Result<()> { let execution_params = execution_params::Testnet::get_execution_params(); - let (bitcoin_wallet, monero_wallet) = init_wallets(config.clone()).await?; + let (bitcoin_wallet, monero_wallet) = + init_wallets(config.clone(), &wallet_data_dir).await?; let (mut event_loop, _) = EventLoop::new( config.network.listen, @@ -113,11 +118,15 @@ async fn main() -> Result<()> { Ok(()) } -async fn init_wallets(config: Config) -> Result<(bitcoin::Wallet, monero::Wallet)> { +async fn init_wallets( + config: Config, + bitcoin_wallet_data_dir: &Path, +) -> Result<(bitcoin::Wallet, monero::Wallet)> { let bitcoin_wallet = bitcoin::Wallet::new( - config.bitcoin.wallet_name.as_str(), - config.bitcoin.bitcoind_url, + config.bitcoin.electrum_rpc_url, + config.bitcoin.electrum_http_url, BITCOIN_NETWORK, + bitcoin_wallet_data_dir, ) .await?; let bitcoin_balance = bitcoin_wallet.balance().await?; diff --git a/swap/src/bin/swap_cli.rs b/swap/src/bin/swap_cli.rs index 629d1061..67072d59 100644 --- a/swap/src/bin/swap_cli.rs +++ b/swap/src/bin/swap_cli.rs @@ -15,7 +15,7 @@ use anyhow::{Context, Result}; use log::LevelFilter; use prettytable::{row, Table}; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use structopt::StructOpt; use swap::{ bitcoin, @@ -75,6 +75,7 @@ async fn main() -> Result<()> { let db = Database::open(config.data.dir.join("database").as_path()) .context("Could not open database")?; + let wallet_data_dir = config.data.dir.join("wallet"); let seed = Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed"); @@ -90,7 +91,7 @@ async fn main() -> Result<()> { send_bitcoin, } => { let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, monero_network).await?; + init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; let swap_id = Uuid::new_v4(); @@ -132,7 +133,7 @@ async fn main() -> Result<()> { alice_addr, }) => { let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, monero_network).await?; + init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; let bob_factory = Builder::new( seed, @@ -157,7 +158,7 @@ async fn main() -> Result<()> { }) => { // TODO: Optimization: Only init the Bitcoin wallet, Monero wallet unnecessary let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, monero_network).await?; + init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; let bob_factory = Builder::new( seed, @@ -201,7 +202,7 @@ async fn main() -> Result<()> { force, }) => { let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, monero_network).await?; + init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; // TODO: Optimize to only use the Bitcoin wallet, Monero wallet is unnecessary let bob_factory = Builder::new( @@ -235,14 +236,22 @@ async fn main() -> Result<()> { async fn init_wallets( config: Config, bitcoin_network: bitcoin::Network, + bitcoin_wallet_data_dir: &Path, monero_network: monero::Network, ) -> Result<(bitcoin::Wallet, monero::Wallet)> { let bitcoin_wallet = bitcoin::Wallet::new( - config.bitcoin.wallet_name.as_str(), - config.bitcoin.bitcoind_url, + config.bitcoin.electrum_rpc_url, + config.bitcoin.electrum_http_url, bitcoin_network, + bitcoin_wallet_data_dir, ) .await?; + + bitcoin_wallet + .sync_wallet() + .await + .expect("Could not sync btc wallet"); + let bitcoin_balance = bitcoin_wallet.balance().await?; info!( "Connection to Bitcoin wallet succeeded, balance: {}", diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 61092574..602eaae9 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -245,7 +245,7 @@ pub trait GetRawTransaction { #[async_trait] pub trait GetNetwork { - fn get_network(&self) -> Network; + async fn get_network(&self) -> Network; } pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Result { diff --git a/swap/src/bitcoin/lock.rs b/swap/src/bitcoin/lock.rs index a00f6b20..2bc6eb14 100644 --- a/swap/src/bitcoin/lock.rs +++ b/swap/src/bitcoin/lock.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct TxLock { - inner: Transaction, + inner: PartiallySignedTransaction, pub(in crate::bitcoin) output_descriptor: Descriptor<::bitcoin::PublicKey>, } @@ -20,41 +20,38 @@ impl TxLock { { let lock_output_descriptor = build_shared_output_descriptor(A.0, B.0); let address = lock_output_descriptor - .address(wallet.get_network(), NullCtx) + .address(wallet.get_network().await, NullCtx) .expect("can derive address from descriptor"); - // We construct a psbt for convenience let psbt = wallet.build_tx_lock_psbt(address, amount).await?; - // We don't take advantage of psbt functionality yet, instead we convert to a - // raw transaction - let inner = psbt.extract_tx(); - Ok(Self { - inner, + inner: psbt, output_descriptor: lock_output_descriptor, }) } pub fn lock_amount(&self) -> Amount { - Amount::from_sat(self.inner.output[self.lock_output_vout()].value) + Amount::from_sat(self.inner.clone().extract_tx().output[self.lock_output_vout()].value) } pub fn txid(&self) -> Txid { - self.inner.txid() + self.inner.clone().extract_tx().txid() } pub fn as_outpoint(&self) -> OutPoint { // This is fine because a transaction that has that many outputs is not // realistic #[allow(clippy::cast_possible_truncation)] - OutPoint::new(self.inner.txid(), self.lock_output_vout() as u32) + OutPoint::new(self.txid(), self.lock_output_vout() as u32) } /// Retreive the index of the locked output in the transaction outputs /// vector fn lock_output_vout(&self) -> usize { self.inner + .clone() + .extract_tx() .output .iter() .position(|output| { @@ -78,7 +75,7 @@ impl TxLock { }; let tx_out = TxOut { - value: self.inner.output[self.lock_output_vout()].value - TX_FEE, + value: self.inner.clone().extract_tx().output[self.lock_output_vout()].value - TX_FEE, script_pubkey: spend_address.script_pubkey(), }; @@ -93,6 +90,6 @@ impl TxLock { impl From for PartiallySignedTransaction { fn from(from: TxLock) -> Self { - PartiallySignedTransaction::from_unsigned_tx(from.inner).expect("to be unsigned") + from.inner } } diff --git a/swap/src/bitcoin/timelocks.rs b/swap/src/bitcoin/timelocks.rs index cbde97a4..06dfc90c 100644 --- a/swap/src/bitcoin/timelocks.rs +++ b/swap/src/bitcoin/timelocks.rs @@ -18,11 +18,16 @@ impl BlockHeight { pub const fn new(block_height: u32) -> Self { Self(block_height) } + pub const fn checked_sub(self, rhs: Self) -> Option { + match self.0.checked_sub(rhs.0) { + Some(result) => Some(BlockHeight(result)), + None => None, + } + } } impl Add for BlockHeight { type Output = BlockHeight; - fn add(self, rhs: u32) -> Self::Output { BlockHeight(self.0 + rhs) } diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 584fe5cc..f37a4f69 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -7,55 +7,96 @@ use crate::{ execution_params::ExecutionParams, }; use ::bitcoin::{util::psbt::PartiallySignedTransaction, Txid}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, tokio::retry}; -use bitcoin_harness::{bitcoind_rpc::PsbtBase64, BitcoindRpcApi}; -use reqwest::Url; -use std::time::Duration; -use tokio::time::interval; +use bdk::{ + blockchain::{noop_progress, Blockchain, ElectrumBlockchain}, + electrum_client::{Client, ElectrumApi}, + keys::GeneratableDefaultOptions, + FeeRate, +}; +use reqwest::{Method, Url}; +use serde::{Deserialize, Serialize}; +use std::{path::Path, sync::Arc, time::Duration}; +use tokio::{sync::Mutex, time::interval}; + +const SLED_TREE_NAME: &str = "default_tree"; -#[derive(Debug)] pub struct Wallet { - pub inner: bitcoin_harness::Wallet, + pub inner: Arc>>, pub network: bitcoin::Network, + pub http_url: Url, + pub rpc_url: Url, } impl Wallet { - pub async fn new(name: &str, url: Url, network: bitcoin::Network) -> Result { - let wallet = bitcoin_harness::Wallet::new(name, url).await?; + pub async fn new( + electrum_rpc_url: Url, + electrum_http_url: Url, + network: bitcoin::Network, + waller_dir: &Path, + ) -> Result { + let client = Client::new(electrum_rpc_url.as_str()) + .map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?; + + let db = bdk::sled::open(waller_dir)?.open_tree(SLED_TREE_NAME)?; + + let p_key = ::bitcoin::PrivateKey::generate_default()?; + let bdk_wallet = bdk::Wallet::new( + bdk::template::P2WPKH(p_key), + None, + network, + db, + ElectrumBlockchain::from(client), + )?; Ok(Self { - inner: wallet, + inner: Arc::new(Mutex::new(bdk_wallet)), network, + http_url: electrum_http_url, + rpc_url: electrum_rpc_url, }) } pub async fn balance(&self) -> Result { - let balance = self.inner.balance().await?; - Ok(balance) + let balance = self.inner.lock().await.get_balance()?; + Ok(Amount::from_sat(balance)) } pub async fn new_address(&self) -> Result
{ - self.inner.new_address().await.map_err(Into::into) + self.inner + .lock() + .await + .get_new_address() + .map_err(Into::into) + } + + pub async fn get_tx(&self, txid: Txid) -> Result> { + let tx = self.inner.lock().await.client().get_tx(&txid)?; + Ok(tx) } pub async fn transaction_fee(&self, txid: Txid) -> Result { - let fee = self + let fees = self .inner - .get_wallet_transaction(txid) + .lock() .await - .map(|res| { - res.fee.map(|signed_amount| { - signed_amount - .abs() - .to_unsigned() - .expect("Absolute value is always positive") - }) + .list_transactions(true)? + .iter() + .find(|tx| tx.txid == txid) + .ok_or_else(|| { + anyhow!("Could not find tx in bdk wallet when trying to determine fees") })? - .context("Rpc response did not contain a fee")?; + .fees; - Ok(fee) + Ok(Amount::from_sat(fees)) + } + + pub async fn sync_wallet(&self) -> Result<()> { + tracing::debug!("syncing wallet"); + self.inner.lock().await.sync(noop_progress(), None)?; + Ok(()) } } @@ -66,11 +107,17 @@ impl BuildTxLockPsbt for Wallet { output_address: Address, output_amount: Amount, ) -> Result { - let psbt = self.inner.fund_psbt(output_address, output_amount).await?; - let as_hex = base64::decode(psbt)?; - - let psbt = bitcoin::consensus::deserialize(&as_hex)?; - + tracing::debug!("building tx lock"); + self.sync_wallet().await?; + let (psbt, _details) = self.inner.lock().await.create_tx( + bdk::TxBuilder::with_recipients(vec![( + output_address.script_pubkey(), + output_amount.as_sat(), + )]) + // todo: get actual fee + .fee_rate(FeeRate::from_sat_per_vb(5.0)), + )?; + tracing::debug!("tx lock built"); Ok(psbt) } } @@ -78,22 +125,15 @@ impl BuildTxLockPsbt for Wallet { #[async_trait] impl SignTxLock for Wallet { async fn sign_tx_lock(&self, tx_lock: TxLock) -> Result { + let txid = tx_lock.txid(); + tracing::debug!("signing tx lock: {}", txid); let psbt = PartiallySignedTransaction::from(tx_lock); - - let psbt = bitcoin::consensus::serialize(&psbt); - let as_base64 = base64::encode(psbt); - - let psbt = self - .inner - .wallet_process_psbt(PsbtBase64(as_base64)) - .await?; - let PsbtBase64(signed_psbt) = PsbtBase64::from(psbt); - - let as_hex = base64::decode(signed_psbt)?; - let psbt: PartiallySignedTransaction = bitcoin::consensus::deserialize(&as_hex)?; - - let tx = psbt.extract_tx(); - + let (signed_psbt, finalized) = self.inner.lock().await.sign(psbt, None)?; + if !finalized { + bail!("Could not finalize TxLock psbt") + } + let tx = signed_psbt.extract_tx(); + tracing::debug!("signed tx lock: {}", txid); Ok(tx) } } @@ -101,19 +141,22 @@ impl SignTxLock for Wallet { #[async_trait] impl BroadcastSignedTransaction for Wallet { async fn broadcast_signed_transaction(&self, transaction: Transaction) -> Result { - let txid = self.inner.send_raw_transaction(transaction).await?; - tracing::info!("Bitcoin tx broadcasted! TXID = {}", txid); - Ok(txid) + tracing::debug!("attempting to broadcast tx: {}", transaction.txid()); + self.inner.lock().await.broadcast(transaction.clone())?; + tracing::info!("Bitcoin tx broadcasted! TXID = {}", transaction.txid()); + Ok(transaction.txid()) } } -// TODO: For retry, use `backoff::ExponentialBackoff` in production as opposed -// to `ConstantBackoff`. #[async_trait] impl WatchForRawTransaction for Wallet { async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { + tracing::debug!("watching for tx: {}", txid); retry(ConstantBackoff::new(Duration::from_secs(1)), || async { - Ok(self.inner.get_raw_transaction(txid).await?) + let client = Client::new(self.rpc_url.as_ref())?; + let tx = client.transaction_get(&txid)?; + tracing::debug!("found tx: {}", txid); + Ok(tx) }) .await .expect("transient errors to be retried") @@ -122,17 +165,37 @@ impl WatchForRawTransaction for Wallet { #[async_trait] impl GetRawTransaction for Wallet { - // todo: potentially replace with option async fn get_raw_transaction(&self, txid: Txid) -> Result { - Ok(self.inner.get_raw_transaction(txid).await?) + self.get_tx(txid) + .await? + .ok_or_else(|| anyhow!("Could not get raw tx with id: {}", txid)) } } #[async_trait] impl GetBlockHeight for Wallet { async fn get_block_height(&self) -> BlockHeight { + // todo: create this url using the join() api in the Url type + let url = format!("{}{}", self.http_url.as_str(), "blocks/tip/height"); + #[derive(Debug)] + enum Error { + Io(reqwest::Error), + Parse(std::num::ParseIntError), + } let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { - Ok(self.inner.client.getblockcount().await?) + // todo: We may want to return early if we cannot connect to the electrum node + // rather than retrying + let height = reqwest::Client::new() + .request(Method::GET, &url) + .send() + .await + .map_err(Error::Io)? + .text() + .await + .map_err(Error::Io)? + .parse::() + .map_err(Error::Parse)?; + Result::<_, backoff::Error>::Ok(height) }) .await .expect("transient errors to be retried"); @@ -144,20 +207,36 @@ impl GetBlockHeight for Wallet { #[async_trait] impl TransactionBlockHeight for Wallet { async fn transaction_block_height(&self, txid: Txid) -> BlockHeight { + // todo: create this url using the join() api in the Url type + let url = format!("{}tx/{}/status", self.http_url, txid); + #[derive(Serialize, Deserialize, Debug, Clone)] + struct TransactionStatus { + block_height: Option, + confirmed: bool, + } + // todo: See if we can make this error handling more elegant + // errors #[derive(Debug)] enum Error { - Io, + Io(reqwest::Error), NotYetMined, + JsonDeserialisation(reqwest::Error), } - let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { - let block_height = self - .inner - .transaction_block_height(txid) + let resp = reqwest::Client::new() + .request(Method::GET, &url) + .send() .await - .map_err(|_| backoff::Error::Transient(Error::Io))?; + .map_err(|err| backoff::Error::Transient(Error::Io(err)))?; - let block_height = block_height.ok_or(backoff::Error::Transient(Error::NotYetMined))?; + let tx_status: TransactionStatus = resp + .json() + .await + .map_err(|err| backoff::Error::Permanent(Error::JsonDeserialisation(err)))?; + + let block_height = tx_status + .block_height + .ok_or(backoff::Error::Transient(Error::NotYetMined))?; Result::<_, backoff::Error>::Ok(block_height) }) @@ -175,16 +254,19 @@ impl WaitForTransactionFinality for Wallet { txid: Txid, execution_params: ExecutionParams, ) -> Result<()> { - // TODO(Franck): This assumes that bitcoind runs with txindex=1 - + tracing::debug!("waiting for tx finality: {}", txid); // Divide by 4 to not check too often yet still be aware of the new block early // on. let mut interval = interval(execution_params.bitcoin_avg_block_time / 4); loop { - let tx = self.inner.client.get_raw_transaction_verbose(txid).await?; - if let Some(confirmations) = tx.confirmations { - if confirmations >= execution_params.bitcoin_finality_confirmations { + let tx_block_height = self.transaction_block_height(txid).await; + tracing::debug!("tx_block_height: {:?}", tx_block_height); + let block_height = self.get_block_height().await; + tracing::debug!("latest_block_height: {:?}", block_height); + if let Some(confirmations) = block_height.checked_sub(tx_block_height) { + tracing::debug!("confirmations: {:?}", confirmations); + if u32::from(confirmations) >= execution_params.bitcoin_finality_confirmations { break; } } @@ -195,8 +277,9 @@ impl WaitForTransactionFinality for Wallet { } } +#[async_trait] impl GetNetwork for Wallet { - fn get_network(&self) -> bitcoin::Network { - self.network + async fn get_network(&self) -> bitcoin::Network { + self.inner.lock().await.network() } } diff --git a/swap/src/cli/config.rs b/swap/src/cli/config.rs index 8dd15741..1d04f917 100644 --- a/swap/src/cli/config.rs +++ b/swap/src/cli/config.rs @@ -11,7 +11,8 @@ use std::{ use tracing::info; use url::Url; -const DEFAULT_BITCOIND_TESTNET_URL: &str = "http://127.0.0.1:18332"; +const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/"; +const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] @@ -43,8 +44,8 @@ pub struct Data { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Bitcoin { - pub bitcoind_url: Url, - pub wallet_name: String, + pub electrum_http_url: Url, + pub electrum_rpc_url: Url, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -105,15 +106,17 @@ pub fn query_user_for_initial_testnet_config() -> Result { .interact_text()?; let data_dir = data_dir.as_str().parse()?; - let bitcoind_url = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter Bitcoind URL (including username and password if applicable) or hit return to use default") - .default(DEFAULT_BITCOIND_TESTNET_URL.to_owned()) + let electrum_http_url: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Electrum HTTP URL or hit return to use default") + .default(DEFAULT_ELECTRUM_HTTP_URL.to_owned()) .interact_text()?; - let bitcoind_url = bitcoind_url.as_str().parse()?; + let electrum_http_url = Url::parse(electrum_http_url.as_str())?; - let bitcoin_wallet_name = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter Bitcoind wallet name") + let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Electrum RPC URL or hit return to use default") + .default(DEFAULT_ELECTRUM_RPC_URL.to_owned()) .interact_text()?; + let electrum_rpc_url = Url::parse(electrum_rpc_url.as_str())?; let monero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter Monero Wallet RPC URL or hit enter to use default") @@ -125,8 +128,8 @@ pub fn query_user_for_initial_testnet_config() -> Result { Ok(Config { data: Data { dir: data_dir }, bitcoin: Bitcoin { - bitcoind_url, - wallet_name: bitcoin_wallet_name, + electrum_http_url, + electrum_rpc_url, }, monero: Monero { wallet_rpc_url: monero_wallet_rpc_url, @@ -150,8 +153,8 @@ mod tests { dir: Default::default(), }, bitcoin: Bitcoin { - bitcoind_url: Url::from_str("http://127.0.0.1:18332").unwrap(), - wallet_name: "alice".to_string(), + electrum_http_url: Url::from_str(DEFAULT_ELECTRUM_HTTP_URL).unwrap(), + electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), }, monero: Monero { wallet_rpc_url: Url::from_str("http://127.0.0.1:38083/json_rpc").unwrap(), diff --git a/swap/src/nectar/config.rs b/swap/src/nectar/config.rs index aa8d05a9..de3c8dd7 100644 --- a/swap/src/nectar/config.rs +++ b/swap/src/nectar/config.rs @@ -12,9 +12,10 @@ use std::{ use tracing::info; use url::Url; -const DEFAULT_BITCOIND_TESTNET_URL: &str = "http://127.0.0.1:18332"; -const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; const DEFAULT_LISTEN_ADDRESS: &str = "/ip4/0.0.0.0/tcp/9939"; +const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/"; +const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; +const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub struct Config { @@ -52,8 +53,8 @@ pub struct Network { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Bitcoin { - pub bitcoind_url: Url, - pub wallet_name: String, + pub electrum_http_url: Url, + pub electrum_rpc_url: Url, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -120,15 +121,17 @@ pub fn query_user_for_initial_testnet_config() -> Result { .interact_text()?; let listen_address = listen_address.as_str().parse()?; - let bitcoind_url = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter Bitcoind URL (including username and password if applicable) or hit return to use default") - .default(DEFAULT_BITCOIND_TESTNET_URL.to_owned()) + let electrum_http_url: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Electrum HTTP URL or hit return to use default") + .default(DEFAULT_ELECTRUM_HTTP_URL.to_owned()) .interact_text()?; - let bitcoind_url = bitcoind_url.as_str().parse()?; + let electrum_http_url = Url::parse(electrum_http_url.as_str())?; - let bitcoin_wallet_name = Input::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter Bitcoind wallet name") + let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter Electrum RPC URL or hit return to use default") + .default(DEFAULT_ELECTRUM_RPC_URL.to_owned()) .interact_text()?; + let electrum_rpc_url = Url::parse(electrum_rpc_url.as_str())?; let monero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter Monero Wallet RPC URL or hit enter to use default") @@ -143,8 +146,8 @@ pub fn query_user_for_initial_testnet_config() -> Result { listen: listen_address, }, bitcoin: Bitcoin { - bitcoind_url, - wallet_name: bitcoin_wallet_name, + electrum_http_url, + electrum_rpc_url, }, monero: Monero { wallet_rpc_url: monero_wallet_rpc_url, @@ -167,15 +170,16 @@ mod tests { data: Data { dir: Default::default(), }, - network: Network { - listen: "/ip4/0.0.0.0/tcp/9939".parse().unwrap(), - }, bitcoin: Bitcoin { - bitcoind_url: Url::from_str("http://127.0.0.1:18332").unwrap(), - wallet_name: "alice".to_string(), + electrum_http_url: Url::from_str(DEFAULT_ELECTRUM_HTTP_URL).unwrap(), + electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), }, + network: Network { + listen: DEFAULT_LISTEN_ADDRESS.parse().unwrap(), + }, + monero: Monero { - wallet_rpc_url: Url::from_str("http://127.0.0.1:38083/json_rpc").unwrap(), + wallet_rpc_url: Url::from_str(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL).unwrap(), }, }; diff --git a/swap/tests/testutils/bitcoind.rs b/swap/tests/testutils/bitcoind.rs new file mode 100644 index 00000000..3ab4f447 --- /dev/null +++ b/swap/tests/testutils/bitcoind.rs @@ -0,0 +1,139 @@ +use std::collections::HashMap; +use testcontainers::{ + core::{Container, Docker, Port, WaitForMessage}, + Image, +}; + +pub const RPC_USER: &str = "admin"; +pub const RPC_PASSWORD: &str = "123"; +pub const RPC_PORT: u16 = 18443; +pub const PORT: u16 = 18886; +pub const DATADIR: &str = "/home/bdk"; + +#[derive(Debug)] +pub struct Bitcoind { + tag: String, + args: BitcoindArgs, + entrypoint: Option, + volume: Option, +} + +impl Image for Bitcoind { + type Args = BitcoindArgs; + type EnvVars = HashMap; + type Volumes = HashMap; + type EntryPoint = str; + + fn descriptor(&self) -> String { + format!("coblox/bitcoin-core:{}", self.tag) + } + + fn wait_until_ready(&self, container: &Container<'_, D, Self>) { + container + .logs() + .stdout + .wait_for_message(&"init message: Done loading") + .unwrap(); + } + + fn args(&self) -> ::Args { + self.args.clone() + } + + fn volumes(&self) -> Self::Volumes { + let mut volumes = HashMap::new(); + match self.volume.clone() { + None => {} + Some(volume) => { + volumes.insert(volume, DATADIR.to_string()); + } + } + volumes + } + + fn env_vars(&self) -> Self::EnvVars { + HashMap::new() + } + + fn ports(&self) -> Option> { + None + } + + fn with_args(self, args: ::Args) -> Self { + Bitcoind { args, ..self } + } + + fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self { + Self { + entrypoint: Some(entrypoint.to_string()), + ..self + } + } + + fn entrypoint(&self) -> Option { + self.entrypoint.to_owned() + } +} + +impl Default for Bitcoind { + fn default() -> Self { + Bitcoind { + tag: "v0.19.1".into(), + args: BitcoindArgs::default(), + entrypoint: Some("/usr/bin/bitcoind".into()), + volume: None, + } + } +} + +impl Bitcoind { + pub fn with_tag(self, tag_str: &str) -> Self { + Bitcoind { + tag: tag_str.to_string(), + ..self + } + } + + pub fn with_volume(mut self, volume: String) -> Self { + self.volume = Some(volume); + self + } +} + +#[derive(Debug, Clone)] +pub struct BitcoindArgs; + +impl Default for BitcoindArgs { + fn default() -> Self { + BitcoindArgs + } +} + +impl IntoIterator for BitcoindArgs { + type Item = String; + type IntoIter = ::std::vec::IntoIter; + + // todo: these "defaults" are only suitable for our tests and need to be looked + // at + fn into_iter(self) -> ::IntoIter { + let args = vec![ + "-server".to_string(), + "-regtest".to_string(), + "-listen=1".to_string(), + "-prune=0".to_string(), + "-rpcallowip=0.0.0.0/0".to_string(), + "-rpcbind=0.0.0.0".to_string(), + format!("-rpcuser={}", RPC_USER), + format!("-rpcpassword={}", RPC_PASSWORD), + "-printtoconsole".to_string(), + "-rest".to_string(), + "-fallbackfee=0.0002".to_string(), + format!("-datadir={}", DATADIR), + format!("-rpcport={}", RPC_PORT), + format!("-port={}", PORT), + "-rest".to_string(), + ]; + + args.into_iter() + } +} diff --git a/swap/tests/testutils/electrs.rs b/swap/tests/testutils/electrs.rs new file mode 100644 index 00000000..5507f1e5 --- /dev/null +++ b/swap/tests/testutils/electrs.rs @@ -0,0 +1,156 @@ +use crate::testutils::bitcoind; +use bitcoin::Network; +use std::collections::HashMap; +use testcontainers::{ + core::{Container, Docker, Port, WaitForMessage}, + Image, +}; + +pub const HTTP_PORT: u16 = 60401; +pub const RPC_PORT: u16 = 3002; + +#[derive(Debug)] +pub struct Electrs { + tag: String, + args: ElectrsArgs, + entrypoint: Option, + wait_for_message: String, + volume: String, + bitcoind_container_name: String, +} + +impl Image for Electrs { + type Args = ElectrsArgs; + type EnvVars = HashMap; + type Volumes = HashMap; + type EntryPoint = str; + + fn descriptor(&self) -> String { + format!("vulpemventures/electrs:{}", self.tag) + } + + fn wait_until_ready(&self, container: &Container<'_, D, Self>) { + container + .logs() + .stderr + .wait_for_message(&self.wait_for_message) + .unwrap(); + } + + fn args(&self) -> ::Args { + self.args.clone() + } + + fn volumes(&self) -> Self::Volumes { + let mut volumes = HashMap::new(); + volumes.insert(self.volume.clone(), bitcoind::DATADIR.to_string()); + volumes + } + + fn env_vars(&self) -> Self::EnvVars { + HashMap::new() + } + + fn ports(&self) -> Option> { + None + } + + fn with_args(self, args: ::Args) -> Self { + Electrs { args, ..self } + } + + fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self { + Self { + entrypoint: Some(entrypoint.to_string()), + ..self + } + } + + fn entrypoint(&self) -> Option { + self.entrypoint.to_owned() + } +} + +impl Default for Electrs { + fn default() -> Self { + Electrs { + tag: "v0.16.0.3".into(), + args: ElectrsArgs::default(), + entrypoint: Some("/build/electrs".into()), + wait_for_message: "Running accept thread".to_string(), + volume: uuid::Uuid::new_v4().to_string(), + bitcoind_container_name: uuid::Uuid::new_v4().to_string(), + } + } +} + +impl Electrs { + pub fn with_tag(self, tag_str: &str) -> Self { + Electrs { + tag: tag_str.to_string(), + ..self + } + } + + pub fn with_volume(mut self, volume: String) -> Self { + self.volume = volume; + self + } + + pub fn with_daemon_rpc_addr(mut self, name: String) -> Self { + self.args.daemon_rpc_addr = name; + self + } +} + +#[derive(Debug, Clone)] +pub struct ElectrsArgs { + pub network: Network, + pub daemon_dir: String, + pub daemon_rpc_addr: String, + pub cookie: String, + pub http_addr: String, + pub electrum_rpc_addr: String, + pub cors: String, +} + +impl Default for ElectrsArgs { + fn default() -> Self { + // todo: these "defaults" are only suitable for our tests and need to be looked + // at + ElectrsArgs { + network: Network::Regtest, + daemon_dir: bitcoind::DATADIR.to_string(), + daemon_rpc_addr: format!("0.0.0.0:{}", bitcoind::RPC_PORT), + cookie: format!("{}:{}", bitcoind::RPC_USER, bitcoind::RPC_PASSWORD), + http_addr: format!("0.0.0.0:{}", HTTP_PORT), + electrum_rpc_addr: format!("0.0.0.0:{}", RPC_PORT), + cors: "*".to_string(), + } + } +} + +impl IntoIterator for ElectrsArgs { + type Item = String; + type IntoIter = ::std::vec::IntoIter; + + fn into_iter(self) -> ::IntoIter { + let mut args = Vec::new(); + + match self.network { + Network::Testnet => args.push("--network=testnet".to_string()), + Network::Regtest => args.push("--network=regtest".to_string()), + Network::Bitcoin => {} + } + + args.push("-vvvvv".to_string()); + args.push(format!("--daemon-dir=={}", self.daemon_dir.as_str())); + args.push(format!("--daemon-rpc-addr={}", self.daemon_rpc_addr)); + args.push(format!("--cookie={}", self.cookie)); + args.push(format!("--http-addr={}", self.http_addr)); + args.push(format!("--electrum-rpc-addr={}", self.electrum_rpc_addr)); + args.push(format!("--cors={}", self.cors)); + + args.into_iter() + } +} diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 1b6edd42..8797be6d 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -1,10 +1,18 @@ +mod bitcoind; +mod electrs; + use crate::testutils; -use bitcoin_harness::Bitcoind; +use anyhow::{Context, Result}; +use bitcoin_harness::{BitcoindRpcApi, Client}; use futures::{future::RemoteHandle, Future}; use get_port::get_port; use libp2p::{core::Multiaddr, PeerId}; use monero_harness::{image, Monero}; -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; use swap::{ bitcoin, bitcoin::{CancelTimelock, PunishTimelock}, @@ -21,19 +29,22 @@ use swap::{ seed::Seed, }; use tempfile::tempdir; -use testcontainers::{clients::Cli, Container}; -use tokio::{sync::mpsc, task::JoinHandle}; +use testcontainers::{clients::Cli, Container, Docker, RunArgs}; +use tokio::{sync::mpsc, task::JoinHandle, time::sleep}; use tracing_core::dispatcher::DefaultGuard; use tracing_log::LogTracer; +use url::Url; use uuid::Uuid; +const TEST_WALLET_NAME: &str = "testwallet"; + #[derive(Debug, Clone)] pub struct StartingBalances { pub xmr: monero::Amount, pub btc: bitcoin::Amount, } -#[derive(Debug, Clone)] +#[derive(Clone)] struct BobParams { seed: Seed, db_path: PathBuf, @@ -119,6 +130,11 @@ impl TestContext { assert!(matches!(state, AliceState::BtcRedeemed)); + self.alice_bitcoin_wallet + .sync_wallet() + .await + .expect("Could not sync wallet"); + let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap(); assert_eq!( btc_balance_after_swap, @@ -145,6 +161,11 @@ impl TestContext { state ); + self.alice_bitcoin_wallet + .sync_wallet() + .await + .expect("Could not sync wallet"); + let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap(); assert_eq!(btc_balance_after_swap, self.alice_starting_balances.btc); @@ -167,6 +188,11 @@ impl TestContext { pub async fn assert_alice_punished(&self, state: AliceState) { assert!(matches!(state, AliceState::BtcPunished)); + self.alice_bitcoin_wallet + .sync_wallet() + .await + .expect("Could not sync wallet"); + let btc_balance_after_swap = self.alice_bitcoin_wallet.as_ref().balance().await.unwrap(); assert_eq!( btc_balance_after_swap, @@ -184,6 +210,11 @@ impl TestContext { } pub async fn assert_bob_redeemed(&self, state: BobState) { + self.bob_bitcoin_wallet + .sync_wallet() + .await + .expect("Could not sync wallet"); + let lock_tx_id = if let BobState::XmrRedeemed { tx_lock_id } = state { tx_lock_id } else { @@ -217,6 +248,11 @@ impl TestContext { } pub async fn assert_bob_refunded(&self, state: BobState) { + self.bob_bitcoin_wallet + .sync_wallet() + .await + .expect("Could not sync wallet"); + let lock_tx_id = if let BobState::BtcRefunded(state4) = state { state4.tx_lock_id() } else { @@ -249,6 +285,11 @@ impl TestContext { } pub async fn assert_bob_punished(&self, state: BobState) { + self.bob_bitcoin_wallet + .sync_wallet() + .await + .expect("Could not sync wallet"); + let lock_tx_id = if let BobState::BtcPunished { tx_lock_id } = state { tx_lock_id } else { @@ -300,11 +341,23 @@ where .parse() .expect("failed to parse Alice's address"); + let electrs_rpc_port = containers + .electrs + .get_host_port(testutils::electrs::RPC_PORT) + .expect("Could not map electrs rpc port"); + let electrs_http_port = containers + .electrs + .get_host_port(testutils::electrs::HTTP_PORT) + .expect("Could not map electrs http port"); + let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( "alice", - &containers.bitcoind, + containers.bitcoind_url.clone(), &monero, alice_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + electrs_http_port, ) .await; @@ -320,9 +373,12 @@ where let (bob_bitcoin_wallet, bob_monero_wallet) = init_test_wallets( "bob", - &containers.bitcoind, + containers.bitcoind_url, &monero, bob_starting_balances.clone(), + tempdir().unwrap().path(), + electrs_rpc_port, + electrs_http_port, ) .await; @@ -369,56 +425,219 @@ where testfn(test).await; } +fn random_prefix() -> String { + use rand::{distributions::Alphanumeric, thread_rng, Rng}; + use std::iter; + const LEN: usize = 8; + let mut rng = thread_rng(); + let chars: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(LEN) + .collect(); + chars +} + async fn init_containers(cli: &Cli) -> (Monero, Containers<'_>) { - let bitcoind = Bitcoind::new(&cli, "0.19.1").unwrap(); - let _ = bitcoind.init(5).await; + let prefix = random_prefix(); + let bitcoind_name = format!("{}_{}", prefix, "bitcoind"); + let (bitcoind, bitcoind_url) = + init_bitcoind_container(&cli, prefix.clone(), bitcoind_name.clone(), prefix.clone()) + .await + .expect("could not init bitcoind"); + let electrs = init_electrs_container(&cli, prefix.clone(), bitcoind_name, prefix) + .await + .expect("could not init electrs"); + let (monero, monerods) = init_monero_container(&cli).await; + (monero, Containers { + bitcoind_url, + bitcoind, + monerods, + electrs, + }) +} + +async fn init_bitcoind_container( + cli: &Cli, + volume: String, + name: String, + network: String, +) -> Result<(Container<'_, Cli, bitcoind::Bitcoind>, Url)> { + let image = bitcoind::Bitcoind::default() + .with_volume(volume) + .with_tag("0.19.1"); + + let run_args = RunArgs::default().with_name(name).with_network(network); + + let docker = cli.run_with_args(image, run_args); + let a = docker + .get_host_port(testutils::bitcoind::RPC_PORT) + .context("Could not map bitcoind rpc port")?; + + let bitcoind_url = { + let input = format!( + "http://{}:{}@localhost:{}", + bitcoind::RPC_USER, + bitcoind::RPC_PASSWORD, + a + ); + Url::parse(&input).unwrap() + }; + + init_bitcoind(bitcoind_url.clone(), 5).await?; + + Ok((docker, bitcoind_url.clone())) +} + +pub async fn init_electrs_container( + cli: &Cli, + volume: String, + bitcoind_container_name: String, + network: String, +) -> Result> { + let bitcoind_rpc_addr = format!( + "{}:{}", + bitcoind_container_name, + testutils::bitcoind::RPC_PORT + ); + let image = electrs::Electrs::default() + .with_volume(volume) + .with_daemon_rpc_addr(bitcoind_rpc_addr) + .with_tag("latest"); + + let run_args = RunArgs::default().with_network(network); + + let docker = cli.run_with_args(image, run_args); + + Ok(docker) +} + +async fn mine(bitcoind_client: Client, reward_address: bitcoin::Address) -> Result<()> { + loop { + tokio::time::sleep(Duration::from_secs(1)).await; + bitcoind_client + .generatetoaddress(1, reward_address.clone(), None) + .await?; + } +} + +async fn init_bitcoind(node_url: Url, spendable_quantity: u32) -> Result { + let bitcoind_client = Client::new(node_url.clone()); + + bitcoind_client + .createwallet(TEST_WALLET_NAME, None, None, None, None) + .await?; + + let reward_address = bitcoind_client + .with_wallet(TEST_WALLET_NAME)? + .getnewaddress(None, None) + .await?; + + bitcoind_client + .generatetoaddress(101 + spendable_quantity, reward_address.clone(), None) + .await?; + let _ = tokio::spawn(mine(bitcoind_client.clone(), reward_address)); + Ok(bitcoind_client) +} + +/// Send Bitcoin to the specified address, limited to the spendable bitcoin +/// quantity. +pub async fn mint(node_url: Url, address: bitcoin::Address, amount: bitcoin::Amount) -> Result<()> { + let bitcoind_client = Client::new(node_url.clone()); + + bitcoind_client + .send_to_address(TEST_WALLET_NAME, address.clone(), amount) + .await?; + + // Confirm the transaction + let reward_address = bitcoind_client + .with_wallet(TEST_WALLET_NAME)? + .getnewaddress(None, None) + .await?; + bitcoind_client + .generatetoaddress(1, reward_address, None) + .await?; + + Ok(()) +} + +async fn init_monero_container( + cli: &Cli, +) -> ( + Monero, + Vec>, +) { let (monero, monerods) = Monero::new(&cli, None, vec!["alice".to_string(), "bob".to_string()]) .await .unwrap(); - (monero, Containers { bitcoind, monerods }) + (monero, monerods) } async fn init_test_wallets( name: &str, - bitcoind: &Bitcoind<'_>, + bitcoind_url: Url, monero: &Monero, starting_balances: StartingBalances, + datadir: &Path, + electrum_rpc_port: u16, + electrum_http_port: u16, ) -> (Arc, Arc) { monero .init(vec![(name, starting_balances.xmr.as_piconero())]) .await .unwrap(); - let xmr_wallet = Arc::new(swap::monero::Wallet { + let xmr_wallet = swap::monero::Wallet { inner: monero.wallet(name).unwrap().client(), network: monero::Network::default(), - }); + }; - let btc_wallet = Arc::new( - swap::bitcoin::Wallet::new(name, bitcoind.node_url.clone(), bitcoin::Network::Regtest) - .await - .unwrap(), - ); + let electrum_rpc_url = { + let input = format!("tcp://@localhost:{}", electrum_rpc_port); + Url::parse(&input).unwrap() + }; + let electrum_http_url = { + let input = format!("http://@localhost:{}", electrum_http_port); + Url::parse(&input).unwrap() + }; + + let btc_wallet = swap::bitcoin::Wallet::new( + electrum_rpc_url, + electrum_http_url, + bitcoin::Network::Regtest, + datadir, + ) + .await + .expect("could not init btc wallet"); if starting_balances.btc != bitcoin::Amount::ZERO { - bitcoind - .mint( - btc_wallet.inner.new_address().await.unwrap(), - starting_balances.btc, - ) - .await - .unwrap(); + mint( + bitcoind_url, + btc_wallet.new_address().await.unwrap(), + starting_balances.btc, + ) + .await + .expect("could not mint btc starting balance"); } - (btc_wallet, xmr_wallet) + sleep(Duration::from_secs(5)).await; + + btc_wallet + .sync_wallet() + .await + .expect("Could not sync btc wallet"); + + (Arc::new(btc_wallet), Arc::new(xmr_wallet)) } // This is just to keep the containers alive #[allow(dead_code)] struct Containers<'a> { - bitcoind: Bitcoind<'a>, + bitcoind_url: Url, + bitcoind: Container<'a, Cli, bitcoind::Bitcoind>, monerods: Vec>, + electrs: Container<'a, Cli, electrs::Electrs>, } /// Utility function to initialize logging in the test environment. @@ -427,7 +646,7 @@ struct Containers<'a> { /// ```rust /// let _guard = init_tracing(); /// ``` -fn init_tracing() -> DefaultGuard { +pub fn init_tracing() -> DefaultGuard { // converts all log records into tracing events // Note: Make sure to initialize without unwrapping, otherwise this causes // trouble when running multiple tests. @@ -438,16 +657,18 @@ fn init_tracing() -> DefaultGuard { let xmr_btc_filter = tracing::Level::DEBUG; let monero_harness_filter = tracing::Level::INFO; let bitcoin_harness_filter = tracing::Level::INFO; + let testcontainers_filter = tracing::Level::DEBUG; use tracing_subscriber::util::SubscriberInitExt as _; tracing_subscriber::fmt() .with_env_filter(format!( - "{},swap={},xmr_btc={},monero_harness={},bitcoin_harness={}", + "{},swap={},xmr_btc={},monero_harness={},bitcoin_harness={},testcontainers={}", global_filter, swap_filter, xmr_btc_filter, monero_harness_filter, bitcoin_harness_filter, + testcontainers_filter )) .set_default() } From 180e778df973241e7bb2eb75dfee21a13eaec8d3 Mon Sep 17 00:00:00 2001 From: rishflab Date: Mon, 15 Feb 2021 16:13:29 +1100 Subject: [PATCH 2/7] Allow blockchain calls to fail Prior to this change, functions could not fail early on permanent errors eg. parsing a url. Merged error enums. --- swap/src/bitcoin.rs | 19 ++++++----- swap/src/bitcoin/wallet.rs | 56 ++++++++++++++------------------ swap/src/protocol/alice/steps.rs | 9 ++--- swap/src/protocol/alice/swap.rs | 4 +-- swap/src/protocol/bob/state.rs | 2 +- swap/tests/testutils/mod.rs | 6 +--- 6 files changed, 43 insertions(+), 53 deletions(-) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 602eaae9..a2dbca29 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -211,7 +211,7 @@ pub trait BroadcastSignedTransaction { #[async_trait] pub trait WatchForRawTransaction { - async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction; + async fn watch_for_raw_transaction(&self, txid: Txid) -> Result; } #[async_trait] @@ -225,12 +225,12 @@ pub trait WaitForTransactionFinality { #[async_trait] pub trait GetBlockHeight { - async fn get_block_height(&self) -> BlockHeight; + async fn get_block_height(&self) -> Result; } #[async_trait] pub trait TransactionBlockHeight { - async fn transaction_block_height(&self, txid: Txid) -> BlockHeight; + async fn transaction_block_height(&self, txid: Txid) -> Result; } #[async_trait] @@ -259,13 +259,14 @@ pub fn recover(S: PublicKey, sig: Signature, encsig: EncryptedSignature) -> Resu Ok(s) } -pub async fn poll_until_block_height_is_gte(client: &B, target: BlockHeight) +pub async fn poll_until_block_height_is_gte(client: &B, target: BlockHeight) -> Result<()> where B: GetBlockHeight, { - while client.get_block_height().await < target { + while client.get_block_height().await? < target { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } + Ok(()) } pub async fn current_epoch( @@ -277,8 +278,8 @@ pub async fn current_epoch( where W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight, { - let current_block_height = bitcoin_wallet.get_block_height().await; - let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await; + let current_block_height = bitcoin_wallet.get_block_height().await?; + let lock_tx_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?; let cancel_timelock_height = lock_tx_height + cancel_timelock; let punish_timelock_height = cancel_timelock_height + punish_timelock; @@ -300,9 +301,9 @@ pub async fn wait_for_cancel_timelock_to_expire( where W: WatchForRawTransaction + TransactionBlockHeight + GetBlockHeight, { - let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await; + let tx_lock_height = bitcoin_wallet.transaction_block_height(lock_tx_id).await?; - poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await; + poll_until_block_height_is_gte(bitcoin_wallet, tx_lock_height + cancel_timelock).await?; Ok(()) } diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index f37a4f69..9615d180 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -23,6 +23,14 @@ use tokio::{sync::Mutex, time::interval}; const SLED_TREE_NAME: &str = "default_tree"; +#[derive(Debug)] +enum Error { + Io(reqwest::Error), + Parse(std::num::ParseIntError), + NotYetMined, + JsonDeserialisation(reqwest::Error), +} + pub struct Wallet { pub inner: Arc>>, pub network: bitcoin::Network, @@ -150,7 +158,7 @@ impl BroadcastSignedTransaction for Wallet { #[async_trait] impl WatchForRawTransaction for Wallet { - async fn watch_for_raw_transaction(&self, txid: Txid) -> Transaction { + async fn watch_for_raw_transaction(&self, txid: Txid) -> Result { tracing::debug!("watching for tx: {}", txid); retry(ConstantBackoff::new(Duration::from_secs(1)), || async { let client = Client::new(self.rpc_url.as_ref())?; @@ -159,7 +167,7 @@ impl WatchForRawTransaction for Wallet { Ok(tx) }) .await - .expect("transient errors to be retried") + .map_err(|err| anyhow!("transient errors to be retried: {:?}", err)) } } @@ -174,19 +182,11 @@ impl GetRawTransaction for Wallet { #[async_trait] impl GetBlockHeight for Wallet { - async fn get_block_height(&self) -> BlockHeight { - // todo: create this url using the join() api in the Url type - let url = format!("{}{}", self.http_url.as_str(), "blocks/tip/height"); - #[derive(Debug)] - enum Error { - Io(reqwest::Error), - Parse(std::num::ParseIntError), - } + async fn get_block_height(&self) -> Result { + let url = self.http_url.join("blocks/tip/height")?; let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { - // todo: We may want to return early if we cannot connect to the electrum node - // rather than retrying let height = reqwest::Client::new() - .request(Method::GET, &url) + .request(Method::GET, url.clone()) .send() .await .map_err(Error::Io)? @@ -194,37 +194,29 @@ impl GetBlockHeight for Wallet { .await .map_err(Error::Io)? .parse::() - .map_err(Error::Parse)?; + .map_err(|err| backoff::Error::Permanent(Error::Parse(err)))?; Result::<_, backoff::Error>::Ok(height) }) .await - .expect("transient errors to be retried"); + .map_err(|err| anyhow!("transient errors to be retried: {:?}", err))?; - BlockHeight::new(height) + Ok(BlockHeight::new(height)) } } #[async_trait] impl TransactionBlockHeight for Wallet { - async fn transaction_block_height(&self, txid: Txid) -> BlockHeight { - // todo: create this url using the join() api in the Url type - let url = format!("{}tx/{}/status", self.http_url, txid); + async fn transaction_block_height(&self, txid: Txid) -> Result { + let url = self.http_url.join(&format!("tx/{}/status", txid))?; + #[derive(Serialize, Deserialize, Debug, Clone)] struct TransactionStatus { block_height: Option, confirmed: bool, } - // todo: See if we can make this error handling more elegant - // errors - #[derive(Debug)] - enum Error { - Io(reqwest::Error), - NotYetMined, - JsonDeserialisation(reqwest::Error), - } let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { let resp = reqwest::Client::new() - .request(Method::GET, &url) + .request(Method::GET, url.clone()) .send() .await .map_err(|err| backoff::Error::Transient(Error::Io(err)))?; @@ -241,9 +233,9 @@ impl TransactionBlockHeight for Wallet { Result::<_, backoff::Error>::Ok(block_height) }) .await - .expect("transient errors to be retried"); + .map_err(|err| anyhow!("transient errors to be retried: {:?}", err))?; - BlockHeight::new(height) + Ok(BlockHeight::new(height)) } } @@ -260,9 +252,9 @@ impl WaitForTransactionFinality for Wallet { let mut interval = interval(execution_params.bitcoin_avg_block_time / 4); loop { - let tx_block_height = self.transaction_block_height(txid).await; + let tx_block_height = self.transaction_block_height(txid).await?; tracing::debug!("tx_block_height: {:?}", tx_block_height); - let block_height = self.get_block_height().await; + let block_height = self.get_block_height().await?; tracing::debug!("latest_block_height: {:?}", block_height); if let Some(confirmations) = block_height.checked_sub(tx_block_height) { tracing::debug!("confirmations: {:?}", confirmations); diff --git a/swap/src/protocol/alice/steps.rs b/swap/src/protocol/alice/steps.rs index bd1f9a2a..680b4f2b 100644 --- a/swap/src/protocol/alice/steps.rs +++ b/swap/src/protocol/alice/steps.rs @@ -42,7 +42,7 @@ where bitcoin_wallet.watch_for_raw_transaction(lock_bitcoin_txid), ) .await - .context("Failed to find lock Bitcoin tx")?; + .context("Failed to find lock Bitcoin tx")??; // // We saw the transaction in the mempool, waiting for it to be confirmed. bitcoin_wallet @@ -158,8 +158,9 @@ where // First wait for cancel timelock to expire let tx_lock_height = bitcoin_wallet .transaction_block_height(tx_lock.txid()) - .await; - poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), tx_lock_height + cancel_timelock).await; + .await?; + poll_until_block_height_is_gte(bitcoin_wallet.as_ref(), tx_lock_height + cancel_timelock) + .await?; let tx_cancel = bitcoin::TxCancel::new(&tx_lock, cancel_timelock, a.public(), B); @@ -216,7 +217,7 @@ where match select(punish_timelock_expired, seen_refund_tx).await { Either::Left(_) => Ok((tx_refund, None)), - Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx))), + Either::Right((published_refund_tx, _)) => Ok((tx_refund, Some(published_refund_tx?))), } } diff --git a/swap/src/protocol/alice/swap.rs b/swap/src/protocol/alice/swap.rs index 67875db7..7d8129d0 100644 --- a/swap/src/protocol/alice/swap.rs +++ b/swap/src/protocol/alice/swap.rs @@ -292,7 +292,7 @@ async fn run_until_internal( AliceState::BtcCancelled { state3, tx_cancel } => { let tx_cancel_height = bitcoin_wallet .transaction_block_height(tx_cancel.txid()) - .await; + .await?; let (tx_refund, published_refund_tx) = wait_for_bitcoin_refund( &tx_cancel, @@ -388,7 +388,7 @@ async fn run_until_internal( match select(refund_tx_seen, punish_tx_finalised).await { Either::Left((published_refund_tx, _)) => { let spend_key = extract_monero_private_key( - published_refund_tx, + published_refund_tx?, tx_refund, state3.s_a, state3.a.clone(), diff --git a/swap/src/protocol/bob/state.rs b/swap/src/protocol/bob/state.rs index 17fafa2f..aa32b5e4 100644 --- a/swap/src/protocol/bob/state.rs +++ b/swap/src/protocol/bob/state.rs @@ -480,7 +480,7 @@ impl State4 { let tx_redeem_candidate = bitcoin_wallet .watch_for_raw_transaction(tx_redeem.txid()) - .await; + .await?; let tx_redeem_sig = tx_redeem.extract_signature_by_key(tx_redeem_candidate, self.b.public())?; diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 8797be6d..96e0589c 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -155,11 +155,7 @@ impl TestContext { let swap_handle = self.alice_swap_handle.recv().await.unwrap(); let state = swap_handle.await.unwrap(); - assert!( - matches!(state, AliceState::XmrRefunded), - "Alice state is not XmrRefunded: {}", - state - ); + assert!(matches!(state, AliceState::XmrRefunded)); self.alice_bitcoin_wallet .sync_wallet() From a51194b9fad41c5e1279543ebf7bc3c6ef5850d0 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Mon, 15 Feb 2021 16:04:51 +1100 Subject: [PATCH 3/7] Instantiate electrum client with custom config with 2 retries The default number of retries is 1. Unfortunately, the way this config value is interpreted doesn't actually lead to a retry. We have to set it to 2 to actually make it retry. See https://github.com/bitcoindevkit/rust-electrum-client/issues/47. --- swap/src/bitcoin/wallet.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 9615d180..fc7448d1 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -12,7 +12,7 @@ use async_trait::async_trait; use backoff::{backoff::Constant as ConstantBackoff, tokio::retry}; use bdk::{ blockchain::{noop_progress, Blockchain, ElectrumBlockchain}, - electrum_client::{Client, ElectrumApi}, + electrum_client::{self, Client, ElectrumApi}, keys::GeneratableDefaultOptions, FeeRate, }; @@ -45,7 +45,10 @@ impl Wallet { network: bitcoin::Network, waller_dir: &Path, ) -> Result { - let client = Client::new(electrum_rpc_url.as_str()) + // Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47. + let config = electrum_client::ConfigBuilder::default().retry(2).build(); + + let client = Client::from_config(electrum_rpc_url.as_str(), config) .map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?; let db = bdk::sled::open(waller_dir)?.open_tree(SLED_TREE_NAME)?; From 4768c790702a6be9c5e1afea48f46d37fe01ae36 Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 9 Feb 2021 17:23:13 +1100 Subject: [PATCH 4/7] Derive bitcoin private key from seed --- swap/src/bin/nectar.rs | 10 +++++++-- swap/src/bin/swap_cli.rs | 42 ++++++++++++++++++++++++++++++------- swap/src/bitcoin/wallet.rs | 10 ++++----- swap/src/seed.rs | 7 +++++++ swap/tests/testutils/mod.rs | 10 +++++++++ 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/swap/src/bin/nectar.rs b/swap/src/bin/nectar.rs index 23af531e..ee3b3bb4 100644 --- a/swap/src/bin/nectar.rs +++ b/swap/src/bin/nectar.rs @@ -84,8 +84,12 @@ async fn main() -> Result<()> { let execution_params = execution_params::Testnet::get_execution_params(); - let (bitcoin_wallet, monero_wallet) = - init_wallets(config.clone(), &wallet_data_dir).await?; + let (bitcoin_wallet, monero_wallet) = init_wallets( + config.clone(), + &wallet_data_dir, + seed.extended_private_key(BITCOIN_NETWORK)?.private_key, + ) + .await?; let (mut event_loop, _) = EventLoop::new( config.network.listen, @@ -121,12 +125,14 @@ async fn main() -> Result<()> { async fn init_wallets( config: Config, bitcoin_wallet_data_dir: &Path, + private_key: ::bitcoin::PrivateKey, ) -> Result<(bitcoin::Wallet, monero::Wallet)> { let bitcoin_wallet = bitcoin::Wallet::new( config.bitcoin.electrum_rpc_url, config.bitcoin.electrum_http_url, BITCOIN_NETWORK, bitcoin_wallet_data_dir, + private_key, ) .await?; let bitcoin_balance = bitcoin_wallet.balance().await?; diff --git a/swap/src/bin/swap_cli.rs b/swap/src/bin/swap_cli.rs index 67072d59..6546accb 100644 --- a/swap/src/bin/swap_cli.rs +++ b/swap/src/bin/swap_cli.rs @@ -90,8 +90,14 @@ async fn main() -> Result<()> { alice_addr, send_bitcoin, } => { - let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; + let (bitcoin_wallet, monero_wallet) = init_wallets( + config, + bitcoin_network, + &wallet_data_dir, + monero_network, + seed, + ) + .await?; let swap_id = Uuid::new_v4(); @@ -132,8 +138,14 @@ async fn main() -> Result<()> { alice_peer_id, alice_addr, }) => { - let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; + let (bitcoin_wallet, monero_wallet) = init_wallets( + config, + bitcoin_network, + &wallet_data_dir, + monero_network, + seed, + ) + .await?; let bob_factory = Builder::new( seed, @@ -157,8 +169,14 @@ async fn main() -> Result<()> { force, }) => { // TODO: Optimization: Only init the Bitcoin wallet, Monero wallet unnecessary - let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; + let (bitcoin_wallet, monero_wallet) = init_wallets( + config, + bitcoin_network, + &wallet_data_dir, + monero_network, + seed, + ) + .await?; let bob_factory = Builder::new( seed, @@ -201,8 +219,14 @@ async fn main() -> Result<()> { alice_addr, force, }) => { - let (bitcoin_wallet, monero_wallet) = - init_wallets(config, bitcoin_network, &wallet_data_dir, monero_network).await?; + let (bitcoin_wallet, monero_wallet) = init_wallets( + config, + bitcoin_network, + &wallet_data_dir, + monero_network, + seed, + ) + .await?; // TODO: Optimize to only use the Bitcoin wallet, Monero wallet is unnecessary let bob_factory = Builder::new( @@ -238,12 +262,14 @@ async fn init_wallets( bitcoin_network: bitcoin::Network, bitcoin_wallet_data_dir: &Path, monero_network: monero::Network, + seed: Seed, ) -> Result<(bitcoin::Wallet, monero::Wallet)> { let bitcoin_wallet = bitcoin::Wallet::new( config.bitcoin.electrum_rpc_url, config.bitcoin.electrum_http_url, bitcoin_network, bitcoin_wallet_data_dir, + seed.extended_private_key(bitcoin_network)?.private_key, ) .await?; diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index fc7448d1..2c5751f1 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -13,7 +13,7 @@ use backoff::{backoff::Constant as ConstantBackoff, tokio::retry}; use bdk::{ blockchain::{noop_progress, Blockchain, ElectrumBlockchain}, electrum_client::{self, Client, ElectrumApi}, - keys::GeneratableDefaultOptions, + miniscript::bitcoin::PrivateKey, FeeRate, }; use reqwest::{Method, Url}; @@ -43,7 +43,8 @@ impl Wallet { electrum_rpc_url: Url, electrum_http_url: Url, network: bitcoin::Network, - waller_dir: &Path, + wallet_dir: &Path, + private_key: PrivateKey, ) -> Result { // Workaround for https://github.com/bitcoindevkit/rust-electrum-client/issues/47. let config = electrum_client::ConfigBuilder::default().retry(2).build(); @@ -51,11 +52,10 @@ impl Wallet { let client = Client::from_config(electrum_rpc_url.as_str(), config) .map_err(|e| anyhow!("Failed to init electrum rpc client: {:?}", e))?; - let db = bdk::sled::open(waller_dir)?.open_tree(SLED_TREE_NAME)?; + let db = bdk::sled::open(wallet_dir)?.open_tree(SLED_TREE_NAME)?; - let p_key = ::bitcoin::PrivateKey::generate_default()?; let bdk_wallet = bdk::Wallet::new( - bdk::template::P2WPKH(p_key), + bdk::template::P2WPKH(private_key), None, network, db, diff --git a/swap/src/seed.rs b/swap/src/seed.rs index 2a0b3836..48c9d039 100644 --- a/swap/src/seed.rs +++ b/swap/src/seed.rs @@ -1,5 +1,7 @@ use crate::fs::ensure_directory_exists; use ::bitcoin::secp256k1::{self, constants::SECRET_KEY_SIZE, SecretKey}; +use anyhow::Result; +use bdk::bitcoin::util::bip32::ExtendedPrivKey; use pem::{encode, Pem}; use rand::prelude::*; use std::{ @@ -26,6 +28,11 @@ impl Seed { Ok(Seed(bytes)) } + pub fn extended_private_key(&self, network: bitcoin::Network) -> Result { + let private_key = ExtendedPrivKey::new_master(network, &self.bytes())?; + Ok(private_key) + } + pub fn bytes(&self) -> [u8; SEED_LENGTH] { self.0 } diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 96e0589c..3f8fc49e 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -346,6 +346,9 @@ where .get_host_port(testutils::electrs::HTTP_PORT) .expect("Could not map electrs http port"); + let alice_seed = Seed::random().unwrap(); + let bob_seed = Seed::random().unwrap(); + let (alice_bitcoin_wallet, alice_monero_wallet) = init_test_wallets( "alice", containers.bitcoind_url.clone(), @@ -354,6 +357,7 @@ where tempdir().unwrap().path(), electrs_rpc_port, electrs_http_port, + alice_seed, ) .await; @@ -375,6 +379,7 @@ where tempdir().unwrap().path(), electrs_rpc_port, electrs_http_port, + bob_seed, ) .await; @@ -570,6 +575,7 @@ async fn init_monero_container( (monero, monerods) } +#[allow(clippy::too_many_arguments)] async fn init_test_wallets( name: &str, bitcoind_url: Url, @@ -578,6 +584,7 @@ async fn init_test_wallets( datadir: &Path, electrum_rpc_port: u16, electrum_http_port: u16, + seed: Seed, ) -> (Arc, Arc) { monero .init(vec![(name, starting_balances.xmr.as_piconero())]) @@ -603,6 +610,9 @@ async fn init_test_wallets( electrum_http_url, bitcoin::Network::Regtest, datadir, + seed.extended_private_key(bitcoin::Network::Regtest) + .expect("Could not create extended private key from seed") + .private_key, ) .await .expect("could not init btc wallet"); From d296c22ecff387c06f15e4976fc4051952105c4d Mon Sep 17 00:00:00 2001 From: rishflab Date: Mon, 15 Feb 2021 20:01:57 +1100 Subject: [PATCH 5/7] Log bitcoin deposit address on startup --- swap/src/bin/nectar.rs | 5 +++++ swap/src/bin/swap_cli.rs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/swap/src/bin/nectar.rs b/swap/src/bin/nectar.rs index ee3b3bb4..55aeaf2a 100644 --- a/swap/src/bin/nectar.rs +++ b/swap/src/bin/nectar.rs @@ -91,6 +91,11 @@ async fn main() -> Result<()> { ) .await?; + info!( + "BTC deposit address: {}", + bitcoin_wallet.new_address().await? + ); + let (mut event_loop, _) = EventLoop::new( config.network.listen, seed, diff --git a/swap/src/bin/swap_cli.rs b/swap/src/bin/swap_cli.rs index 6546accb..a6fb88b4 100644 --- a/swap/src/bin/swap_cli.rs +++ b/swap/src/bin/swap_cli.rs @@ -106,6 +106,11 @@ async fn main() -> Result<()> { send_bitcoin, swap_id ); + info!( + "BTC deposit address: {}", + bitcoin_wallet.new_address().await? + ); + let bob_factory = Builder::new( seed, db, From bc1d2bda54afc3894b7039e60a4cefeb36f2ff88 Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 16 Feb 2021 11:48:46 +1100 Subject: [PATCH 6/7] Test URL creation for default electrum HTTP API --- swap/src/bitcoin/wallet.rs | 44 +++++++++++++++++++++++++++++++++++--- swap/src/cli/config.rs | 2 +- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 2c5751f1..7b590007 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -186,7 +186,7 @@ impl GetRawTransaction for Wallet { #[async_trait] impl GetBlockHeight for Wallet { async fn get_block_height(&self) -> Result { - let url = self.http_url.join("blocks/tip/height")?; + let url = blocks_tip_height_url(&self.http_url)?; let height = retry(ConstantBackoff::new(Duration::from_secs(1)), || async { let height = reqwest::Client::new() .request(Method::GET, url.clone()) @@ -210,8 +210,7 @@ impl GetBlockHeight for Wallet { #[async_trait] impl TransactionBlockHeight for Wallet { async fn transaction_block_height(&self, txid: Txid) -> Result { - let url = self.http_url.join(&format!("tx/{}/status", txid))?; - + let url = tx_status_url(txid, &self.http_url)?; #[derive(Serialize, Deserialize, Debug, Clone)] struct TransactionStatus { block_height: Option, @@ -278,3 +277,42 @@ impl GetNetwork for Wallet { self.inner.lock().await.network() } } + +fn tx_status_url(txid: Txid, base_url: &Url) -> Result { + let url = base_url.join(&format!("tx/{}/status", txid))?; + Ok(url) +} + +fn blocks_tip_height_url(base_url: &Url) -> Result { + let url = base_url.join("blocks/tip/height")?; + Ok(url) +} + +#[cfg(test)] +mod tests { + use crate::{ + bitcoin::{ + wallet::{blocks_tip_height_url, tx_status_url}, + Txid, + }, + cli::config::DEFAULT_ELECTRUM_HTTP_URL, + }; + use reqwest::Url; + + #[test] + fn create_tx_status_url_from_default_base_url_success() { + let txid: Txid = Txid::default(); + let base_url = Url::parse(DEFAULT_ELECTRUM_HTTP_URL).expect("Could not parse url"); + let url = tx_status_url(txid, &base_url).expect("Could not create url"); + let expected = format!("https://blockstream.info/testnet/api/tx/{}/status", txid); + assert_eq!(url.as_str(), expected); + } + + #[test] + fn create_block_tip_height_url_from_default_base_url_success() { + let base_url = Url::parse(DEFAULT_ELECTRUM_HTTP_URL).expect("Could not parse url"); + let url = blocks_tip_height_url(&base_url).expect("Could not create url"); + let expected = "https://blockstream.info/testnet/api/blocks/tip/height"; + assert_eq!(url.as_str(), expected); + } +} diff --git a/swap/src/cli/config.rs b/swap/src/cli/config.rs index 1d04f917..2252f181 100644 --- a/swap/src/cli/config.rs +++ b/swap/src/cli/config.rs @@ -11,7 +11,7 @@ use std::{ use tracing::info; use url::Url; -const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/"; +pub const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/"; const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; From b66bb00c776adfa2d999b2d923f9fd3b03f3142b Mon Sep 17 00:00:00 2001 From: rishflab Date: Tue, 16 Feb 2021 13:32:21 +1100 Subject: [PATCH 7/7] Remove stale code --- swap/src/bitcoin.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index a2dbca29..ad5f4fc0 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -233,11 +233,6 @@ pub trait TransactionBlockHeight { async fn transaction_block_height(&self, txid: Txid) -> Result; } -#[async_trait] -pub trait WaitForBlockHeight { - async fn wait_for_block_height(&self, height: BlockHeight); -} - #[async_trait] pub trait GetRawTransaction { async fn get_raw_transaction(&self, txid: Txid) -> Result;