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(Amount::from_sat(fees)) + } - Ok(fee) + 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() }