diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 60d6905..d677e65 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -29,8 +29,6 @@ jobs: # regtest-* features are experimental and not fully usable - regtest-bitcoin - regtest-electrum - - regtest-esplora-ureq - - regtest-esplora-reqwest steps: - name: Checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb4ff9..99e5302 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Add distinct `key-value-db` and `sqlite-db` features, keep default as `key-value-db` +- Add experimental `regtest-*` features that will auto spawn bitcoind regtest node in the background. Exposing bitcoin-cli rpc calls via new bdk-cli command `bdk-cli node []`. +- Add integration testing framework in `tests/integrations.rs` with `regtest-*` features. ## [0.5.0] diff --git a/Cargo.lock b/Cargo.lock index bcd8a8f..d9184fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", "quote", @@ -48,6 +48,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -60,15 +69,6 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc19a4937b4fbd3fe3379793130e42060d10627a360f2127802b10b87e7baf74" -[[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.11.0" @@ -95,11 +95,29 @@ name = "bdk" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bdfb4ab7649b7ee7e170ea6dc3b7a3fc5d97671600fe40f6ffde3abe52ef4ae5" +dependencies = [ + "async-trait", + "bdk-macros", + "bitcoin 0.27.1", + "js-sys", + "log", + "miniscript 6.1.0", + "rand 0.7.3", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "bdk" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec2c4c49915b82a2576bc7dee83d2f274387904b79305e6ae8b622daa0b282c1" dependencies = [ "async-trait", "bdk-macros", "bip39", - "bitcoin", + "bitcoin 0.28.1", "bitcoinconsensus", "bitcoincore-rpc", "cc", @@ -108,8 +126,8 @@ dependencies = [ "js-sys", "lazy_static", "log", - "miniscript", - "rand", + "miniscript 7.0.0", + "rand 0.7.3", "reqwest", "rocksdb", "serde", @@ -125,7 +143,7 @@ name = "bdk-cli" version = "0.5.0" dependencies = [ "base64 0.11.0", - "bdk", + "bdk 0.19.0", "bdk-macros", "bdk-reserves", "clap", @@ -159,7 +177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ba3fcfd24950d05b99b88ab920214534e64f46a9a273c1bf90d56a64012b9f" dependencies = [ "base64 0.11.0", - "bdk", + "bdk 0.18.0", "bitcoinconsensus", "log", ] @@ -210,7 +228,20 @@ dependencies = [ "base64-compat", "bech32", "bitcoin_hashes 0.10.0", - "secp256k1", + "secp256k1 0.20.3", + "serde", +] + +[[package]] +name = "bitcoin" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" +dependencies = [ + "base64-compat", + "bech32", + "bitcoin_hashes 0.10.0", + "secp256k1 0.22.1", "serde", ] @@ -241,9 +272,9 @@ dependencies = [ [[package]] name = "bitcoincore-rpc" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8d99d58466295cb2bf72c6959b784d59f8f0d6977458d2ba3eb75c834f36c3" +checksum = "dd0e67dbf7a9971e7f4276f6089e9e814ce0f624a03216b7d92d00351ae7fb3e" dependencies = [ "bitcoincore-rpc-json", "jsonrpc", @@ -254,29 +285,31 @@ dependencies = [ [[package]] name = "bitcoincore-rpc-json" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce91de73c61f5776cf938bfa88378c5b404a70e3369b761dacbe6024fea79dd" +checksum = "2e2ae16202721ba8c3409045681fac790a5ddc791f05731a2df22c0c6bffc0f1" dependencies = [ - "bitcoin", + "bitcoin 0.28.1", "serde", "serde_json", ] [[package]] name = "bitcoind" -version = "0.20.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ddc41af9556a341c909bc71de33e16da52bf5f8dbda6b7a402054c60bdb722" +checksum = "0831b9721892ce845a6acadd111311bee84f9e1cc0c5017b8213ec4437ccdfe2" dependencies = [ "bitcoin_hashes 0.10.0", "bitcoincore-rpc", + "filetime", "flate2", "home", "log", "tar", "tempfile", "ureq 1.5.5", + "which", ] [[package]] @@ -287,9 +320,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byteorder" @@ -356,9 +389,9 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clang-sys" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf6b561dcf059c85bbe388e0a7b0a1469acb3934cc0cfa148613a830629e3049" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" dependencies = [ "glob", "libc", @@ -391,6 +424,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "const_fn" version = "0.4.9" @@ -439,7 +481,7 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cfg-if", "crossbeam-utils", "lazy_static", @@ -492,9 +534,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "electrsd" -version = "0.12.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "334abee7787b76757ac34b13a9a1cbf1ef0f2da35162d3ceb95a5b0bc34df80f" +checksum = "5ad65605e022b44ab8c1e489547311bb48b5c605a0aea9ba908e12cae2880111" dependencies = [ "bitcoin_hashes 0.10.0", "bitcoind", @@ -507,18 +549,18 @@ dependencies = [ [[package]] name = "electrum-client" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd12f125852d77980725243b2a8b3bea73cd4c7a22c33bc52b08b664c561dc7" +checksum = "8ef9b40020912229e947b45d91f9ff96b10d543e0eddd75ff41b9eda24d9c051" dependencies = [ - "bitcoin", + "bitcoin 0.28.1", "log", - "rustls 0.16.0", + "rustls 0.20.6", "serde", "serde_json", "socks", - "webpki", - "webpki-roots 0.19.0", + "webpki 0.22.0", + "webpki-roots 0.22.3", ] [[package]] @@ -590,9 +632,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", "libc", @@ -602,9 +644,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ "cfg-if", "crc32fast", @@ -638,6 +680,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "futures" version = "0.3.21" @@ -751,13 +799,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -820,9 +868,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", @@ -863,9 +911,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ "bytes", "futures-channel", @@ -898,11 +946,11 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown", ] @@ -1003,7 +1051,7 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ - "autocfg", + "autocfg 1.1.0", "scopeguard", ] @@ -1040,7 +1088,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -1061,17 +1109,28 @@ version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e292b58407dfbf1384e5aca8428d3b0f2eaa09d24cb17088f6db0b7ca31194a" dependencies = [ - "bitcoin", + "bitcoin 0.27.1", + "serde", +] + +[[package]] +name = "miniscript" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da39fc7a8adea97a677337b0091779dd86349226b869053af496584a9b9e5847" +dependencies = [ + "bitcoin 0.28.1", "serde", ] [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", + "autocfg 1.1.0", ] [[package]] @@ -1290,6 +1349,25 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + [[package]] name = "rand" version = "0.7.3" @@ -1298,9 +1376,19 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", ] [[package]] @@ -1313,6 +1401,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + [[package]] name = "rand_core" version = "0.4.2" @@ -1328,6 +1425,15 @@ dependencies = [ "getrandom 0.1.16", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -1337,6 +1443,68 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -1352,7 +1520,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "redox_syscall", "thiserror", ] @@ -1385,9 +1553,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64 0.13.0", "bytes", @@ -1410,6 +1578,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-socks", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1459,28 +1628,27 @@ dependencies = [ [[package]] name = "rustls" -version = "0.16.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64 0.10.1", + "base64 0.13.0", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", ] [[package]] name = "rustls" -version = "0.19.1" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" dependencies = [ - "base64 0.13.0", "log", "ring", - "sct", - "webpki", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -1529,13 +1697,34 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.4.2", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "rand 0.6.5", + "secp256k1-sys 0.5.2", "serde", ] @@ -1548,6 +1737,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + [[package]] name = "semver" version = "0.9.0" @@ -1751,9 +1949,9 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" [[package]] name = "str-buf" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strsim" @@ -1787,9 +1985,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", @@ -1903,7 +2101,7 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d0183f6f6001549ab68f8c7585093bb732beefbcf6d23a10b9b95c73a1dd49" dependencies = [ - "autocfg", + "autocfg 1.1.0", "bytes", "libc", "memchr", @@ -1946,9 +2144,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ "cfg-if", "pin-project-lite", @@ -1969,11 +2167,11 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -1990,9 +2188,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" @@ -2036,7 +2234,7 @@ dependencies = [ "qstring", "rustls 0.19.1", "url", - "webpki", + "webpki 0.21.4", "webpki-roots 0.21.1", ] @@ -2055,7 +2253,7 @@ dependencies = [ "serde_json", "socks", "url", - "webpki", + "webpki 0.21.4", "webpki-roots 0.21.1", ] @@ -2107,9 +2305,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" @@ -2198,12 +2396,13 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.19.0" +name = "webpki" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ - "webpki", + "ring", + "untrusted", ] [[package]] @@ -2212,7 +2411,27 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +dependencies = [ + "webpki 0.22.0", +] + +[[package]] +name = "which" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +dependencies = [ + "either", + "lazy_static", + "libc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4f95791..fa2c245 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT" [dependencies] -bdk = { version = "0.18", default-features = false, features = ["all-keys"]} +bdk = { version = "0.19", default-features = false, features = ["all-keys"]} bdk-macros = "0.6" structopt = "^0.3" serde_json = { version = "^1.0" } @@ -28,7 +28,7 @@ env_logger = { version = "0.7", optional = true } clap = { version = "2.33", optional = true } regex = { version = "1", optional = true } bdk-reserves = { version = "0.18", optional = true} -electrsd = { version= "0.12", features = ["trigger", "bitcoind_22_0"], optional = true} +electrsd = { version= "0.19", features = ["bitcoind_22_0"], optional = true} [features] default = ["cli", "repl", "key-value-db"] @@ -45,9 +45,11 @@ compact_filters = ["bdk/compact_filters"] rpc = ["bdk/rpc"] reserves = ["bdk-reserves"] verify = ["bdk/verify"] -regtest-node = [] -regtest-bitcoin = ["regtest-node" , "rpc", "electrsd"] +regtest-node = ["electrsd"] +regtest-bitcoin = ["regtest-node", "rpc"] regtest-electrum = ["regtest-node", "electrum", "electrsd/electrs_0_8_10"] +# regtest-esplora modes aren't operable currently, due to some issues in electrsd/esplora_a33e97e1 +# added for sake of code compleness. regtest-esplora-ureq = ["regtest-node", "esplora-ureq", "electrsd/esplora_a33e97e1"] regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"] diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index b3b0371..9a33160 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -12,11 +12,10 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::Network; -use std::fs; -use std::path::PathBuf; - use clap::AppSettings; use log::{debug, error, info, warn}; +use std::fs; +use std::path::PathBuf; #[cfg(feature = "repl")] use rustyline::error::ReadlineError; @@ -51,7 +50,7 @@ use bdk::database::{AnyDatabase, AnyDatabaseConfig, BatchDatabase, ConfigurableD use bdk::wallet::wallet_name_from_descriptor; use bdk::Wallet; use bdk::{bitcoin, Error}; -use bdk_cli::WalletSubCommand; +use bdk_cli::{Backend, WalletSubCommand}; use bdk_cli::{CliOpts, CliSubCommand, KeySubCommand, OfflineWalletSubCommand, WalletOpts}; #[cfg(any( @@ -86,12 +85,15 @@ enum ReplSubCommand { OfflineWalletSubCommand(OfflineWalletSubCommand), #[structopt(flatten)] KeySubCommand(KeySubCommand), + #[cfg(feature = "regtest-node")] + #[structopt(flatten)] + NodeSubCommand(bdk_cli::NodeSubCommand), /// Exit REPL loop Exit, } -/// prepare bdk_cli home and wallet directory -fn prepare_home_wallet_dir(wallet_name: &str) -> Result { +/// prepare bdk-cli home directory +fn prepare_home_dir() -> Result { let mut dir = PathBuf::new(); dir.push( &dirs_next::home_dir().ok_or_else(|| Error::Generic("home dir not found".to_string()))?, @@ -103,6 +105,13 @@ fn prepare_home_wallet_dir(wallet_name: &str) -> Result { fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; } + Ok(dir) +} + +/// prepare bdk_cli wallet directory +fn prepare_wallet_dir(wallet_name: &str) -> Result { + let mut dir = prepare_home_dir()?; + dir.push(wallet_name); if !dir.exists() { @@ -115,7 +124,7 @@ fn prepare_home_wallet_dir(wallet_name: &str) -> Result { /// Prepare wallet database directory fn prepare_wallet_db_dir(wallet_name: &str) -> Result { - let mut db_dir = prepare_home_wallet_dir(wallet_name)?; + let mut db_dir = prepare_wallet_dir(wallet_name)?; #[cfg(feature = "key-value-db")] db_dir.push("wallet.sled"); @@ -135,7 +144,7 @@ fn prepare_wallet_db_dir(wallet_name: &str) -> Result { /// Prepare blockchain data directory (for compact filters) #[cfg(feature = "compact_filters")] fn prepare_bc_dir(wallet_name: &str) -> Result { - let mut bc_dir = prepare_home_wallet_dir(wallet_name)?; + let mut bc_dir = prepare_wallet_dir(wallet_name)?; bc_dir.push("compact_filters"); @@ -150,6 +159,23 @@ fn prepare_bc_dir(wallet_name: &str) -> Result { Ok(bc_dir) } +// We create only a global single node directory. Because multiple +// wallets can access the same node datadir, and they will have separate +// wallet names in `~/.bdk-bitcoin/node-data/regtest/wallets`. +#[cfg(feature = "regtest-node")] +fn prepare_node_datadir() -> Result { + let mut dir = prepare_home_dir()?; + + dir.push("node-data"); + + if !dir.exists() { + info!("Creating node directory {}", dir.as_path().display()); + fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + } + + Ok(dir) +} + fn open_database(wallet_opts: &WalletOpts) -> Result { let wallet_name = wallet_opts.wallet.as_ref().expect("wallet name"); let database_path = prepare_wallet_db_dir(wallet_name)?; @@ -174,16 +200,6 @@ fn open_database(wallet_opts: &WalletOpts) -> Result { Ok(database) } -#[allow(dead_code)] -// Different Backend types activated with `regtest-*` mode. -// If `regtest-*` feature not activated, then default is `None`. -enum Backend { - None, - Bitcoin { rpc_url: String, rpc_auth: String }, - Electrum { electrum_url: String }, - Esplora { esplora_url: String }, -} - #[cfg(any( feature = "electrum", feature = "esplora", @@ -198,12 +214,13 @@ fn new_blockchain( #[cfg(feature = "electrum")] let config = { let url = match _backend { - Backend::Electrum { electrum_url } => electrum_url.to_owned(), - _ => wallet_opts.electrum_opts.server.clone(), + #[cfg(feature = "regtest-electrum")] + Backend::Electrum { electrsd, .. } => &electrsd.electrum_url, + _ => &wallet_opts.electrum_opts.server, }; AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { - url, + url: url.to_owned(), socks5: wallet_opts.proxy_opts.proxy.clone(), retry: wallet_opts.proxy_opts.retries, timeout: wallet_opts.electrum_opts.timeout, @@ -212,13 +229,21 @@ fn new_blockchain( }; #[cfg(feature = "esplora")] - let config = AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { - base_url: wallet_opts.esplora_opts.server.clone(), - timeout: Some(wallet_opts.esplora_opts.timeout), - concurrency: Some(wallet_opts.esplora_opts.conc), - stop_gap: wallet_opts.esplora_opts.stop_gap, - proxy: wallet_opts.proxy_opts.proxy.clone(), - }); + let config = { + let url = match _backend { + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + Backend::Esplora { esplorad } => esplorad.esplora_url.expect("Esplora url expected"), + _ => wallet_opts.esplora_opts.server.clone(), + }; + + AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { + base_url: url, + timeout: Some(wallet_opts.esplora_opts.timeout), + concurrency: Some(wallet_opts.esplora_opts.conc), + stop_gap: wallet_opts.esplora_opts.stop_gap, + proxy: wallet_opts.proxy_opts.proxy.clone(), + }) + }; #[cfg(feature = "compact_filters")] let config = { @@ -248,10 +273,11 @@ fn new_blockchain( #[cfg(feature = "rpc")] let config: AnyBlockchainConfig = { let (url, auth) = match _backend { - Backend::Bitcoin { rpc_url, rpc_auth } => ( - rpc_url, + #[cfg(feature = "regtest-node")] + Backend::Bitcoin { bitcoind } => ( + bitcoind.params.rpc_socket.to_string(), Auth::Cookie { - file: rpc_auth.into(), + file: bitcoind.params.cookie_file.clone(), }, ), _ => { @@ -265,16 +291,13 @@ fn new_blockchain( password: wallet_opts.rpc_opts.basic_auth.1.clone(), } }; - (&wallet_opts.rpc_opts.address, auth) + (wallet_opts.rpc_opts.address.clone(), auth) } }; - // Use deterministic wallet name derived from descriptor - let wallet_name = wallet_name_from_descriptor( - &wallet_opts.descriptor[..], - wallet_opts.change_descriptor.as_deref(), - _network, - &Secp256k1::new(), - )?; + let wallet_name = wallet_opts + .wallet + .to_owned() + .expect("Wallet name should be available this level"); let rpc_url = "http://".to_string() + &url; @@ -289,7 +312,7 @@ fn new_blockchain( AnyBlockchainConfig::Rpc(rpc_config) }; - Ok(AnyBlockchain::from_config(&config)?) + AnyBlockchain::from_config(&config) } fn new_wallet( @@ -319,10 +342,24 @@ fn main() { #[cfg(feature = "regtest-node")] let bitcoind = { - if network != Network::Regtest { - error!("Do not override default network value for `regtest-node` features"); - } - let bitcoind_conf = electrsd::bitcoind::Conf::default(); + // Configure node directory according to cli options + // nodes always have a persistent directory + let bitcoind_conf = { + match &cli_opts.datadir { + None => { + let datadir = prepare_node_datadir().unwrap(); + let mut conf = electrsd::bitcoind::Conf::default(); + conf.staticdir = Some(datadir); + conf + } + Some(path) => { + let mut conf = electrsd::bitcoind::Conf::default(); + conf.staticdir = Some(path.into()); + conf + } + } + }; + let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path() .expect("We should always have downloaded path"); electrsd::bitcoind::BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap() @@ -331,49 +368,39 @@ fn main() { #[cfg(feature = "regtest-bitcoin")] let backend = { Backend::Bitcoin { - rpc_url: bitcoind.params.rpc_socket.to_string(), - rpc_auth: bitcoind - .params - .cookie_file - .clone() - .into_os_string() - .into_string() - .unwrap(), + bitcoind: Box::new(bitcoind), } }; #[cfg(feature = "regtest-electrum")] - let (_electrsd, backend) = { + let backend = { let elect_conf = electrsd::Conf::default(); let elect_exe = electrsd::downloaded_exe_path().expect("We should always have downloaded path"); let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); - let backend = Backend::Electrum { - electrum_url: electrsd.electrum_url.clone(), - }; - (electrsd, backend) + Backend::Electrum { + bitcoind: Box::new(bitcoind), + electrsd: Box::new(electrsd), + } }; #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] - let (_electrsd, backend) = { + let backend = { let mut elect_conf = electrsd::Conf::default(); elect_conf.http_enabled = true; let elect_exe = electrsd::downloaded_exe_path().expect("Electrsd downloaded binaries not found"); let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); - let backend = Backend::Esplora { - esplora_url: electrsd - .esplora_url - .clone() - .expect("Esplora port not open in electrum"), - }; - (electrsd, backend) + Backend::Esplora { + bitcoind: Box::new(bitcoind), + esplorad: Box::new(electrsd), + } }; #[cfg(not(feature = "regtest-node"))] let backend = Backend::None; - match handle_command(cli_opts, network, backend) { + match handle_command(cli_opts, network, &backend) { Ok(result) => println!("{}", result), Err(e) => { match e { @@ -404,8 +431,16 @@ fn maybe_descriptor_wallet_name( Ok(wallet_opts) } -fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Result { +fn handle_command( + cli_opts: CliOpts, + network: Network, + _backend: &Backend, +) -> Result { let result = match cli_opts.subcommand { + #[cfg(feature = "regtest-node")] + CliSubCommand::Node { subcommand: cmd } => { + serde_json::to_string_pretty(&_backend.exec_cmd(cmd)?) + } #[cfg(any( feature = "electrum", feature = "esplora", @@ -418,11 +453,11 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res } => { let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?; let database = open_database(&wallet_opts)?; - let blockchain = new_blockchain(network, &wallet_opts, &_backend)?; + let blockchain = new_blockchain(network, &wallet_opts, _backend)?; let wallet = new_wallet(network, &wallet_opts, database)?; let result = bdk_cli::handle_online_wallet_subcommand(&wallet, &blockchain, online_subcommand)?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } CliSubCommand::Wallet { wallet_opts, @@ -436,13 +471,13 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res &wallet_opts, offline_subcommand, )?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } CliSubCommand::Key { subcommand: key_subcommand, } => { let result = bdk_cli::handle_key_subcommand(network, key_subcommand)?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } #[cfg(feature = "compiler")] CliSubCommand::Compile { @@ -450,7 +485,7 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res script_type, } => { let result = bdk_cli::handle_compile_subcommand(network, policy, script_type)?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } #[cfg(feature = "repl")] CliSubCommand::Repl { wallet_opts } => { @@ -496,6 +531,10 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res debug!("repl_subcommand = {:?}", repl_subcommand); let result = match repl_subcommand { + #[cfg(feature = "regtest-node")] + ReplSubCommand::NodeSubCommand(sub_command) => { + _backend.exec_cmd(sub_command) + } #[cfg(any( feature = "electrum", feature = "esplora", @@ -503,7 +542,7 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res feature = "rpc" ))] ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => { - let blockchain = new_blockchain(network, &wallet_opts, &_backend)?; + let blockchain = new_blockchain(network, &wallet_opts, _backend)?; bdk_cli::handle_online_wallet_subcommand( &wallet, &blockchain, @@ -534,7 +573,7 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res } } - "Exiting REPL".to_string() + Ok("Exiting REPL".to_string()) } #[cfg(all(feature = "reserves", feature = "electrum"))] CliSubCommand::ExternalReserves { @@ -552,10 +591,10 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res addresses, electrum_opts, )?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } }; - Ok(result) + result.map_err(|e| e.into()) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 108fe12..7ae33f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,6 +102,7 @@ use std::str::FromStr; pub use structopt; use structopt::StructOpt; +use crate::bdk::keys::GeneratableKey; use crate::OfflineWalletSubCommand::*; #[cfg(any( feature = "electrum", @@ -123,6 +124,8 @@ use bdk::bitcoin::hashes::hex::FromHex; use bdk::bitcoin::secp256k1::Secp256k1; use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, KeySource}; use bdk::bitcoin::util::psbt::PartiallySignedTransaction; +#[cfg(feature = "regtest-node")] +use bdk::bitcoin::Amount; use bdk::bitcoin::{Address, Network, OutPoint, Script, Txid}; #[cfg(all( feature = "reserves", @@ -150,7 +153,7 @@ use bdk::electrum_client::{Client, ElectrumApi}; use bdk::keys::bip39::{Language, Mnemonic, WordCount}; use bdk::keys::DescriptorKey::Secret; use bdk::keys::KeyError::{InvalidNetwork, Message}; -use bdk::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey}; +use bdk::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratedKey}; use bdk::miniscript::miniscript; #[cfg(feature = "compiler")] use bdk::miniscript::policy::Concrete; @@ -168,6 +171,8 @@ use bdk_reserves::reserves::verify_proof; ))] #[cfg(feature = "reserves")] use bdk_reserves::reserves::ProofOfReserves; +#[cfg(feature = "regtest-node")] +use electrsd::bitcoind::bitcoincore_rpc::{Client, RpcApi}; /// Global options /// @@ -203,6 +208,8 @@ use bdk_reserves::reserves::ProofOfReserves; /// /// let expected_cli_opts = CliOpts { /// network: Network::Testnet, +/// #[cfg(feature = "regtest-node")] +/// datadir: None, /// subcommand: CliSubCommand::Wallet { /// wallet_opts: WalletOpts { /// wallet: None, @@ -263,6 +270,11 @@ pub struct CliOpts { default_value = "testnet" )] pub network: Network, + /// Sets the backend node data directory. + /// Default value : "~/.bdk-bitcoin/node-data" + #[cfg(feature = "regtest-node")] + #[structopt(name = "DATADIR", short = "d", long = "datadir")] + pub datadir: Option, /// Top level cli sub-command #[structopt(subcommand)] pub subcommand: CliSubCommand, @@ -280,7 +292,15 @@ pub struct CliOpts { rename_all = "snake", long_about = "Top level options and command modes" )] +#[allow(clippy::large_enum_variant)] pub enum CliSubCommand { + /// Regtest node sub-commands + #[cfg(feature = "regtest-node")] + #[structopt(long_about = "Regtest Node mode")] + Node { + #[structopt(subcommand)] + subcommand: NodeSubCommand, + }, /// Wallet options and sub-commands #[structopt(long_about = "Wallet mode")] Wallet { @@ -334,6 +354,31 @@ pub enum CliSubCommand { }, } +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + doc, + doc = r#" +Node operation sub-commands + +Used as an API for the node backend. +"# +)] +#[derive(Debug, StructOpt, Clone, PartialEq)] +#[structopt(rename_all = "lower")] +#[cfg(any(feature = "regtest-node"))] +pub enum NodeSubCommand { + /// Get info + GetInfo, + /// Get new address from node's test wallet + GetNewAddress, + /// Generate blocks + Generate { block_num: u64 }, + /// Get Wallet balance + GetBalance, + /// Send to an external wallet address + SendToAddress { address: String, amount: u64 }, +} + #[cfg_attr(not(doc), allow(missing_docs))] #[cfg_attr( doc, @@ -880,6 +925,97 @@ fn parse_outpoint(s: &str) -> Result { OutPoint::from_str(s).map_err(|e| e.to_string()) } +#[allow(dead_code)] +// Different Backend types activated with `regtest-*` mode. +// If `regtest-*` feature not activated, then default is `None`. +// +// Box the backend to reduce size of Enum in memory. +pub enum Backend { + None, + #[cfg(feature = "regtest-bitcoin")] + // A pure core backend. Wallet connected to it via RPC. + Bitcoin { + bitcoind: Box, + }, + #[cfg(feature = "regtest-electrum")] + // An Electrum backend, with an underlying bitcoin core + // Wallet connected to it, via the electrum url + Electrum { + bitcoind: Box, + electrsd: Box, + }, + // An Esplora backend with underlying bitcoin core. + // Wallet connected to it, via the esplora url + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + Esplora { + bitcoind: Box, + esplorad: Box, + }, +} + +#[cfg(feature = "regtest-node")] +impl Backend { + /// Execute a [`NodeSubCommand`] in the backend + pub fn exec_cmd(&self, cmd: NodeSubCommand) -> Result { + let client = self.get_client()?; + match cmd { + NodeSubCommand::GetInfo => Ok(serde_json::to_value( + client + .get_blockchain_info() + .map_err(|e| Error::Generic(e.to_string()))?, + )?), + + NodeSubCommand::GetNewAddress => Ok(serde_json::to_value( + client + .get_new_address(None, None) + .map_err(|e| Error::Generic(e.to_string()))?, + )?), + + NodeSubCommand::Generate { block_num } => { + let core_addrs = client + .get_new_address(None, None) + .map_err(|e| Error::Generic(e.to_string()))?; + let block_hashes = client + .generate_to_address(block_num, &core_addrs) + .map_err(|e| Error::Generic(e.to_string()))?; + Ok(serde_json::to_value(block_hashes)?) + } + + NodeSubCommand::GetBalance => Ok(serde_json::to_value( + client + .get_balance(None, None) + .map_err(|e| Error::Generic(e.to_string()))? + .to_string(), + )?), + + NodeSubCommand::SendToAddress { address, amount } => { + let address = + Address::from_str(&address).map_err(|e| Error::Generic(e.to_string()))?; + let amount = Amount::from_sat(amount); + let txid = client + .send_to_address(&address, amount, None, None, None, None, None, None) + .map_err(|e| Error::Generic(e.to_string()))?; + Ok(serde_json::to_value(&txid)?) + } + } + } + + // Expose the underlying RPC client + pub fn get_client(&self) -> Result<&Client, Error> { + match self { + Self::None => Err(Error::Generic( + "No backend available. Cannot execute node commands".to_string(), + )), + #[cfg(feature = "regtest-bitcoin")] + Self::Bitcoin { bitcoind } => Ok(&bitcoind.client), + #[cfg(feature = "regtest-electrum")] + Self::Electrum { bitcoind, .. } => Ok(&bitcoind.client), + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + Self::Esplora { bitcoind, .. } => Ok(&bitcoind.client), + } + } +} + /// Execute an offline wallet sub-command /// /// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`]. @@ -1076,7 +1212,7 @@ where .try_fold::<_, _, Result>( init_psbt, |mut acc, x| { - acc.merge(x)?; + acc.combine(x)?; Ok(acc) }, )?; @@ -1457,6 +1593,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1518,6 +1656,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1557,6 +1697,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1597,6 +1739,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1638,6 +1782,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1675,6 +1821,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1715,6 +1863,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1789,6 +1939,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1858,6 +2010,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1928,6 +2082,8 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + #[cfg(feature = "regtest-node")] + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..12e2a3d --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,226 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! bdk-cli Integration Test Framework +//! +//! This modules performs the necessary integration test for bdk-cli +//! The tests can be run using `cargo test` + +#[cfg(feature = "regtest-node")] +mod test { + use electrsd::bitcoind::tempfile::TempDir; + use serde_json::{json, Value}; + use std::convert::From; + use std::path::PathBuf; + use std::process::Command; + + /// Testing errors for integration tests + #[derive(Debug)] + enum IntTestError { + // IO error + IO(std::io::Error), + // Command execution error + CmdExec(String), + // Json Data error + JsonData(String), + } + + impl From for IntTestError { + fn from(e: std::io::Error) -> Self { + IntTestError::IO(e) + } + } + + // Helper function + // Runs a system command with given args + fn run_cmd_with_args(cmd: &str, args: &[&str]) -> Result { + let output = Command::new(cmd).args(args).output().unwrap(); + let mut value = output.stdout; + let error = output.stderr; + if value.len() == 0 { + return Err(IntTestError::CmdExec(String::from_utf8(error).unwrap())); + } + value.pop(); // remove `\n` at end + let output_string = std::str::from_utf8(&value).unwrap(); + let json_value: serde_json::Value = match serde_json::from_str(output_string) { + Ok(value) => value, + Err(_) => json!(output_string), // bitcoin-cli will sometime return raw string + }; + Ok(json_value) + } + + // Helper Function + // Transforms a json value to string + fn value_to_string(value: &Value) -> Result { + match value { + Value::Bool(bool) => match bool { + true => Ok("true".to_string()), + false => Ok("false".to_string()), + }, + Value::Number(n) => Ok(n.to_string()), + Value::String(s) => Ok(s.to_string()), + _ => Err(IntTestError::JsonData( + "Value parsing not implemented for this type".to_string(), + )), + } + } + + // Helper Function + // Extracts value from a given json object and key + fn get_value(json: &Value, key: &str) -> Result { + let map = json + .as_object() + .ok_or(IntTestError::JsonData("Json is not an object".to_string()))?; + let value = map + .get(key) + .ok_or(IntTestError::JsonData("Invalid key".to_string()))? + .to_owned(); + let string_value = value_to_string(&value)?; + Ok(string_value) + } + + /// The bdk-cli command struct + /// Use it to perform all bdk-cli operations + struct BdkCli { + target: String, + network: String, + verbosity: bool, + recv_desc: Option, + chang_desc: Option, + node_datadir: PathBuf, + } + + impl BdkCli { + /// Construct a new [`BdkCli`] struct + fn new( + network: &str, + node_datadir: PathBuf, + verbosity: bool, + features: &[&str], + ) -> Result { + // Build bdk-cli with given features + let mut feat = "--features=".to_string(); + for item in features { + feat.push_str(item); + feat.push_str(","); + } + feat.pop(); // remove the last comma + println!("Building app with features : {}", feat); + let _build = Command::new("cargo").args(&["build", &feat]).output()?; + + let mut bdk_cli = Self { + target: "./target/debug/bdk-cli".to_string(), + network: network.to_string(), + verbosity, + recv_desc: None, + chang_desc: None, + node_datadir, + }; + + let bdk_master_key = bdk_cli.key_exec(&["generate"])?; + let bdk_xprv = get_value(&bdk_master_key, "xprv")?; + + let bdk_recv_desc = + bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/0", "--xprv", &bdk_xprv])?; + let bdk_recv_desc = get_value(&bdk_recv_desc, "xprv")?; + let bdk_recv_desc = format!("wpkh({})", bdk_recv_desc); + + let bdk_chng_desc = + bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/1", "--xprv", &bdk_xprv])?; + let bdk_chng_desc = get_value(&bdk_chng_desc, "xprv")?; + let bdk_chng_desc = format!("wpkh({})", bdk_chng_desc); + + bdk_cli.recv_desc = Some(bdk_recv_desc); + bdk_cli.chang_desc = Some(bdk_chng_desc); + + Ok(bdk_cli) + } + + /// Execute bdk-cli wallet commands with given args + fn wallet_exec(&self, args: &[&str]) -> Result { + let datadir = self.node_datadir.as_os_str().to_str().unwrap(); + let mut wallet_args = + ["--network", &self.network, "--datadir", datadir, "wallet"].to_vec(); + if self.verbosity { + wallet_args.push("-v"); + } + + wallet_args.push("-d"); + wallet_args.push(self.recv_desc.as_ref().unwrap()); + wallet_args.push("-c"); + wallet_args.push(&self.chang_desc.as_ref().unwrap()); + + for arg in args { + wallet_args.push(arg); + } + run_cmd_with_args(&self.target, &wallet_args) + } + + /// Execute bdk-cli key commands with given args + fn key_exec(&self, args: &[&str]) -> Result { + let mut key_args = ["key"].to_vec(); + for arg in args { + key_args.push(arg); + } + run_cmd_with_args(&self.target, &key_args) + } + + /// Execute bdk-cli node command + fn node_exec(&self, args: &[&str]) -> Result { + let datadir_path = self.node_datadir.as_os_str().to_str().unwrap(); + let mut node_args = ["--datadir", datadir_path, "node"].to_vec(); + for arg in args { + node_args.push(arg); + } + run_cmd_with_args(&self.target, &node_args) + } + } + + // Run A Basic wallet operation test, with given feature + #[cfg(test)] + fn basic_wallet_ops(feature: &str) { + // Create a temp file for testing env + let temp = TempDir::new().unwrap().path().to_path_buf(); + // Create bdk-cli instance + let bdk_cli = BdkCli::new("regtest", temp, false, &[feature]).unwrap(); + + // Generate 101 blocks + bdk_cli.node_exec(&["generate", "101"]).unwrap(); + + // Get a bdk address + let bdk_addr_json = bdk_cli.wallet_exec(&["get_new_address"]).unwrap(); + let bdk_addr = get_value(&bdk_addr_json, "address").unwrap(); + + // Send coins from core to bdk + bdk_cli + .node_exec(&["sendtoaddress", &bdk_addr, "1000000000"]) + .unwrap(); + + bdk_cli.node_exec(&["generate", "1"]).unwrap(); + + // Sync the bdk wallet + bdk_cli.wallet_exec(&["sync"]).unwrap(); + + // Get the balance + let balance_json = bdk_cli.wallet_exec(&["get_balance"]).unwrap(); + let balance = get_value(&balance_json, "satoshi").unwrap(); + assert_eq!(balance, "1000000000"); + } + + #[test] + #[cfg(feature = "regtest-bitcoin")] + fn test_basic_wallet_op_bitcoind() { + basic_wallet_ops("regtest-bitcoin") + } + + #[test] + #[cfg(feature = "regtest-electrum")] + fn test_basic_wallet_op_electrum() { + basic_wallet_ops("regtest-electrum") + } +}