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.
pull/178/head
rishflab 3 years ago
parent 4d8e801c1e
commit a0ef1f96ec

298
Cargo.lock generated

@ -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]]

@ -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"] }

@ -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?;

@ -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: {}",

@ -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<SecretKey> {

@ -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<TxLock> for PartiallySignedTransaction {
fn from(from: TxLock) -> Self {
PartiallySignedTransaction::from_unsigned_tx(from.inner).expect("to be unsigned")
from.inner
}
}

@ -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<Self> {
match self.0.checked_sub(rhs.0) {
Some(result) => Some(BlockHeight(result)),
None => None,
}
}
}
impl Add<u32> for BlockHeight {
type Output = BlockHeight;
fn add(self, rhs: u32) -> Self::Output {
BlockHeight(self.0 + rhs)
}

@ -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<Mutex<bdk::Wallet<ElectrumBlockchain, bdk::sled::Tree>>>,
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<Self> {
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<Self> {
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<Amount> {
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<Address> {
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<Option<Transaction>> {
let tx = self.inner.lock().await.client().get_tx(&txid)?;
Ok(tx)
}
pub async fn transaction_fee(&self, txid: Txid) -> Result<Amount> {
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<PartiallySignedTransaction> {
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<Transaction> {
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<Txid> {
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<Transaction> {
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::<u32>()
.map_err(Error::Parse)?;
Result::<_, backoff::Error<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<u32>,
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<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()
}
}

@ -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<Config> {
.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<Config> {
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(),

@ -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<Config> {
.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<Config> {
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(),
},
};

@ -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<String>,
volume: Option<String>,
}
impl Image for Bitcoind {
type Args = BitcoindArgs;
type EnvVars = HashMap<String, String>;
type Volumes = HashMap<String, String>;
type EntryPoint = str;
fn descriptor(&self) -> String {
format!("coblox/bitcoin-core:{}", self.tag)
}
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stdout
.wait_for_message(&"init message: Done loading")
.unwrap();
}
fn args(&self) -> <Self as Image>::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<Vec<Port>> {
None
}
fn with_args(self, args: <Self as Image>::Args) -> Self {
Bitcoind { args, ..self }
}
fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self {
Self {
entrypoint: Some(entrypoint.to_string()),
..self
}
}
fn entrypoint(&self) -> Option<String> {
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<String>;
// todo: these "defaults" are only suitable for our tests and need to be looked
// at
fn into_iter(self) -> <Self as IntoIterator>::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()
}
}

@ -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<String>,
wait_for_message: String,
volume: String,
bitcoind_container_name: String,
}
impl Image for Electrs {
type Args = ElectrsArgs;
type EnvVars = HashMap<String, String>;
type Volumes = HashMap<String, String>;
type EntryPoint = str;
fn descriptor(&self) -> String {
format!("vulpemventures/electrs:{}", self.tag)
}
fn wait_until_ready<D: Docker>(&self, container: &Container<'_, D, Self>) {
container
.logs()
.stderr
.wait_for_message(&self.wait_for_message)
.unwrap();
}
fn args(&self) -> <Self as Image>::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<Vec<Port>> {
None
}
fn with_args(self, args: <Self as Image>::Args) -> Self {
Electrs { args, ..self }
}
fn with_entrypoint(self, entrypoint: &Self::EntryPoint) -> Self {
Self {
entrypoint: Some(entrypoint.to_string()),
..self
}
}
fn entrypoint(&self) -> Option<String> {
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<String>;
fn into_iter(self) -> <Self as IntoIterator>::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()
}
}

@ -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<Container<'_, Cli, electrs::Electrs>> {
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<Client> {
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<Container<'_, Cli, monero_harness::image::Monero>>,
) {
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<bitcoin::Wallet>, Arc<monero::Wallet>) {
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<Container<'a, Cli, image::Monero>>,
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()
}

Loading…
Cancel
Save