diff --git a/Cargo.lock b/Cargo.lock index b491c2e6..c16ac080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.4.3" @@ -34,6 +40,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "aes-gcm" version = "0.9.2" @@ -41,7 +58,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead 0.4.3", - "aes", + "aes 0.7.5", "cipher 0.3.0", "ctr", "ghash", @@ -119,6 +136,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arrayvec" version = "0.7.6" @@ -142,6 +165,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -192,6 +221,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bdk-cli" version = "3.0.0" @@ -201,6 +236,7 @@ dependencies = [ "bdk_esplora", "bdk_kyoto", "bdk_redb", + "bdk_testenv", "bdk_wallet", "clap", "clap_complete", @@ -263,7 +299,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b59a3f7fbe678874fa34354097644a171276e02a49934c13b3d61c54610ddf39" dependencies = [ "bdk_core", - "electrum-client", + "electrum-client 0.24.1", ] [[package]] @@ -301,6 +337,16 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "bdk_testenv" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543dc273dab3b9ec329772bcb15741a948cc3510deae2a30af3a116f03505fee" +dependencies = [ + "bdk_chain", + "electrsd", +] + [[package]] name = "bdk_wallet" version = "2.1.0" @@ -532,6 +578,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoin_hashes 0.14.1", + "bitcoincore-rpc", + "flate2", + "log", + "minreq", + "tar", + "tempfile", + "which", + "zip", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -574,6 +644,26 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cc" version = "1.2.52" @@ -787,6 +877,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" version = "0.9.4" @@ -821,6 +917,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.4" @@ -878,6 +989,15 @@ dependencies = [ "cipher 0.3.0", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" version = "0.9.0" @@ -936,6 +1056,40 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "electrsd" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c3c57645202a05a47206ed81cc179cf32bf4c3ca5a9e60c06c49d0222d5844" +dependencies = [ + "bitcoin_hashes 0.14.1", + "bitcoind", + "electrum-client 0.20.0", + "log", + "minreq", + "nix", + "which", + "zip", +] + +[[package]] +name = "electrum-client" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7b1f8783238bb18e6e137875b0a66f3dffe6c7ea84066e05d033cf180b150f" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] + [[package]] name = "electrum-client" version = "0.24.1" @@ -1025,12 +1179,33 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -1307,6 +1482,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -1647,8 +1831,9 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", + "redox_syscall 0.7.3", ] [[package]] @@ -1662,6 +1847,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1701,6 +1892,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniscript" version = "12.3.5" @@ -1712,6 +1912,16 @@ dependencies = [ "serde", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "minreq" version = "2.14.1" @@ -1754,6 +1964,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1763,6 +1987,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "once_cell" version = "1.21.3" @@ -1787,7 +2017,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1849,11 +2079,22 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "payjoin" version = "1.0.0-rc.1" @@ -1873,6 +2114,18 @@ dependencies = [ "url", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "password-hash", + "sha2 0.10.9", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1961,6 +2214,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2123,7 +2382,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.10.0", ] [[package]] @@ -2256,7 +2524,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2270,16 +2538,29 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -2407,7 +2688,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -2488,6 +2769,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2537,6 +2829,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.11" @@ -2608,6 +2906,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -2617,7 +2926,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -2679,6 +2988,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tinystr" version = "0.8.2" @@ -2821,7 +3149,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -3122,6 +3450,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3333,6 +3673,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.3", +] + [[package]] name = "yoke" version = "0.8.1" @@ -3450,8 +3800,57 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes 0.8.4", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac 0.12.1", + "pbkdf2", + "sha1", + "time", + "zstd", +] + [[package]] name = "zmij" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 7a1a31b4..3d6c9b9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,3 +63,6 @@ verify = [] # Extra utility tools # Compile policies compiler = [] + +[dev-dependencies] +bdk_testenv = { version = "0.13.1", default-features = true } diff --git a/src/handlers.rs b/src/handlers.rs index a98b172d..6615c9ac 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -604,7 +604,7 @@ pub fn handle_offline_wallet_subcommand( feature = "cbf", feature = "rpc" ))] -pub(crate) async fn handle_online_wallet_subcommand( +pub async fn handle_online_wallet_subcommand( wallet: &mut Wallet, client: &BlockchainClient, online_subcommand: OnlineWalletSubCommand, @@ -1196,7 +1196,7 @@ pub fn handle_wallets_subcommand(datadir: &Path, pretty: bool) -> Result Result { +pub async fn handle_command(cli_opts: CliOpts) -> Result { let pretty = cli_opts.pretty; let subcommand = cli_opts.subcommand.clone(); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..82e2b759 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,28 @@ +// Copyright (c) 2020-2025 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. + +#![doc = include_str!("../README.md")] +#![doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")] + +pub mod commands; +mod config; +pub mod error; +pub mod handlers; +#[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "cbf", + feature = "rpc" +))] +mod payjoin; +#[cfg(any(feature = "sqlite", feature = "redb"))] +mod persister; +pub mod utils; + +pub use commands::CliOpts; +pub use handlers::handle_command; diff --git a/src/main.rs b/src/main.rs index 90d701b0..0b65b39a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,27 +10,10 @@ #![doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")] #![warn(missing_docs)] -mod commands; -mod config; -mod error; -mod handlers; -#[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "cbf", - feature = "rpc" -))] -mod payjoin; -#[cfg(any(feature = "sqlite", feature = "redb"))] -mod persister; -mod utils; - +use bdk_cli::{CliOpts, handle_command}; use bdk_wallet::bitcoin::Network; -use log::{debug, error, warn}; - -use crate::commands::CliOpts; -use crate::handlers::*; use clap::Parser; +use log::{debug, error, warn}; #[tokio::main] async fn main() { diff --git a/src/utils.rs b/src/utils.rs index 76e56a0f..34e414fc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -141,7 +141,7 @@ pub(crate) fn prepare_wallet_db_dir( feature = "rpc", feature = "cbf", ))] -pub(crate) enum BlockchainClient { +pub enum BlockchainClient { #[cfg(feature = "electrum")] Electrum { client: Box>, @@ -176,7 +176,7 @@ pub struct KyotoClientHandle { feature = "cbf", ))] /// Create a new blockchain from the wallet configuration options. -pub(crate) fn new_blockchain_client( +pub fn new_blockchain_client( wallet_opts: &WalletOpts, _wallet: &Wallet, _datadir: PathBuf, diff --git a/tests/integration.rs b/tests/integration.rs index a45cf8aa..5812fedc 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -6,254 +6,199 @@ // 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` +//! bdk-cli integration tests -#[cfg(feature = "rpc")] +#[cfg(feature = "electrum")] mod test { - use serde_json::{Value, json}; - use std::convert::From; - use std::env::temp_dir; - use std::path::PathBuf; - use std::process::Command; - - /// Testing errors for integration tests - #[allow(dead_code)] - #[derive(Debug)] - enum IntTestError { - // IO error - IO(std::io::Error), - // Command execution error - CmdExec(String), - // Json Data error - JsonData(String), + use std::{env, str::FromStr, time::Duration}; + + #[cfg(feature = "cbf")] + use bdk_cli::commands::CompactFilterOpts; + #[cfg(any(feature = "sqlite", feature = "redb"))] + use bdk_cli::commands::DatabaseType; + use bdk_cli::commands::{ + CliOpts, CliSubCommand, ClientType, OfflineWalletSubCommand, OnlineWalletSubCommand, + WalletOpts, + }; + use bdk_cli::handlers::{handle_offline_wallet_subcommand, handle_online_wallet_subcommand}; + use bdk_cli::utils::new_blockchain_client; + use bdk_testenv::{ + TestEnv, + anyhow::{Context, Result, anyhow}, + bitcoincore_rpc::RpcApi, + }; + use bdk_wallet::{ + Wallet, + bitcoin::{Address, Amount, Network}, + }; + use serde_json::Value; + + const EXTERNAL_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/0/*)#429nsxmg"; + const INTERNAL_DESCRIPTOR: &str = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/1/*)#y7qjdnts"; + + fn test_env() -> Result { + TestEnv::new() } - impl From for IntTestError { - fn from(e: std::io::Error) -> Self { - IntTestError::IO(e) + fn mine_blocks_in_batches(env: &TestEnv, count: usize, address: &Address) -> Result<()> { + let mut remaining = count; + while remaining > 0 { + let batch_size = remaining.min(10); + let mut attempt = 0; + loop { + match env.mine_blocks(batch_size, Some(address.clone())) { + Ok(_) => break, + Err(_) if attempt < 2 => { + attempt += 1; + std::thread::sleep(Duration::from_millis(250)); + continue; + } + Err(error) => { + return Err(error).context(format!( + "failed to mine a batch of {batch_size} blocks after {attempt} retries" + )); + } + } + } + remaining -= batch_size; } - } - // Helper function - // Runs a system command with given args - #[allow(dead_code)] - 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.is_empty() { - 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) + Ok(()) } - // Helper Function - // Transforms a json value to string - #[allow(dead_code)] - 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(), - )), + fn cli_opts() -> CliOpts { + CliOpts { + network: Network::Regtest, + datadir: None, + pretty: false, + subcommand: CliSubCommand::Wallets, } } - // Helper Function - // Extracts value from a given json object and key - #[allow(dead_code)] - 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 - #[allow(dead_code)] - #[derive(Debug)] - struct BdkCli { - target: String, - network: String, - verbosity: bool, - recv_desc: Option, - chang_desc: Option, - node_datadir: Option, - } - - impl BdkCli { - /// Construct a new [`BdkCli`] struct - fn new( - network: &str, - node_datadir: Option, - 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(','); + fn wallet_opts(electrum_url: String) -> WalletOpts { + #[cfg(any(feature = "sqlite", feature = "redb"))] + let database_type = { + #[cfg(feature = "sqlite")] + { + DatabaseType::Sqlite } - feat.pop(); // remove the last comma - 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, - }; - - println!("BDK-CLI Config : {bdk_cli:#?}"); - 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 { - // Check if data directory is specified - let mut wallet_args = if let Some(datadir) = &self.node_datadir { - let datadir = datadir.as_os_str().to_str().unwrap(); - ["--network", &self.network, "--datadir", datadir, "wallet"].to_vec() - } else { - ["--network", &self.network, "wallet"].to_vec() - }; - - if self.verbosity { - wallet_args.push("-v"); + #[cfg(all(not(feature = "sqlite"), feature = "redb"))] + { + DatabaseType::Redb } + }; - 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 { - // Check if data directory is specified - let mut node_args = if let Some(datadir) = &self.node_datadir { - let datadir = datadir.as_os_str().to_str().unwrap(); - ["--network", &self.network, "--datadir", datadir, "node"].to_vec() - } else { - ["--network", &self.network, "node"].to_vec() - }; - - for arg in args { - node_args.push(arg); - } - run_cmd_with_args(&self.target, &node_args) + WalletOpts { + wallet: Some("integration-test".to_string()), + verbose: false, + ext_descriptor: EXTERNAL_DESCRIPTOR.to_string(), + int_descriptor: Some(INTERNAL_DESCRIPTOR.to_string()), + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "rpc", + feature = "cbf" + ))] + client_type: ClientType::Electrum, + #[cfg(any(feature = "sqlite", feature = "redb"))] + database_type, + #[cfg(any(feature = "electrum", feature = "esplora", feature = "rpc"))] + url: electrum_url, + #[cfg(feature = "electrum")] + batch_size: 10, + #[cfg(feature = "esplora")] + parallel_requests: 5, + #[cfg(feature = "rpc")] + basic_auth: ("user".to_string(), "password".to_string()), + #[cfg(feature = "rpc")] + cookie: None, + #[cfg(feature = "cbf")] + compactfilter_opts: CompactFilterOpts { conn_count: 2 }, } } - // Run A Basic wallet operation test, with given feature - #[cfg(test)] - #[allow(dead_code)] - fn basic_wallet_ops(feature: &str) { - // Create a temporary directory for testing env - let mut test_dir = std::env::current_dir().unwrap(); - test_dir.push("bdk-testing"); - - let test_dir = temp_dir(); - // let test_dir = test_temp_dir.into_path().to_path_buf(); - - // Create bdk-cli instance - let bdk_cli = BdkCli::new("regtest", Some(test_dir), 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(); + fn parse_json(output: &str) -> Result { + Ok(serde_json::from_str(output)?) + } - // Sync the bdk wallet - bdk_cli.wallet_exec(&["sync"]).unwrap(); + fn address_from_output(output: &str) -> Result
{ + let parsed_output = parse_json(output)?; + let address = parsed_output + .get("address") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("address missing from handler output"))?; + Ok(Address::from_str(address)?.assume_checked()) + } - // Get the balance - let balance_json = bdk_cli.wallet_exec(&["get_balance"]).unwrap(); - let confirmed_balance = balance_json - .as_object() - .unwrap() + fn confirmed_balance_from_output(output: &str) -> Result { + parse_json(output)? .get("satoshi") - .unwrap() - .as_object() - .unwrap() - .get("confirmed") - .unwrap() - .as_u64() - .unwrap(); - assert_eq!(confirmed_balance, 1000000000u64); + .and_then(|balance| balance.get("confirmed")) + .and_then(Value::as_u64) + .ok_or_else(|| anyhow!("confirmed balance missing from handler output")) } - // #[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") - // } + #[tokio::test] + async fn sync_updates_balance_for_a_funded_wallet() -> Result<()> { + let env = test_env().context("failed to start test environment")?; + let cli_opts = cli_opts(); + let wallet_opts = wallet_opts(env.electrsd.electrum_url.clone()); + let mut wallet = Wallet::create(EXTERNAL_DESCRIPTOR, INTERNAL_DESCRIPTOR) + .network(Network::Regtest) + .create_wallet_no_persist() + .context("failed to create in-memory wallet")?; + + let address_output = handle_offline_wallet_subcommand( + &mut wallet, + &wallet_opts, + &cli_opts, + OfflineWalletSubCommand::NewAddress, + ) + .context("failed to derive a receive address from the offline handler")?; + let receive_address = + address_from_output(&address_output).context("failed to parse receive address")?; + + let miner = env + .rpc_client() + .get_new_address(None, None) + .context("failed to get a mining address from bitcoind")? + .assume_checked(); + mine_blocks_in_batches(&env, 101, &miner) + .context("failed to mine initial spendable coins")?; + + let sent_amount = Amount::from_sat(50_000); + let txid = env + .send(&receive_address, sent_amount) + .context("failed to fund the test wallet")?; + mine_blocks_in_batches(&env, 1, &miner) + .context("failed to confirm the funding transaction")?; + env.wait_until_electrum_sees_block(Duration::from_secs(15)) + .context("electrs did not observe the confirmation block in time")?; + env.wait_until_electrum_sees_txid(txid, Duration::from_secs(15)) + .context("electrs did not index the funding transaction in time")?; + + let blockchain_client = new_blockchain_client(&wallet_opts, &wallet, env::temp_dir()) + .context("failed to build the electrum blockchain client")?; + handle_online_wallet_subcommand( + &mut wallet, + &blockchain_client, + OnlineWalletSubCommand::Sync, + ) + .await + .context("wallet sync handler failed")?; + + let balance_output = handle_offline_wallet_subcommand( + &mut wallet, + &wallet_opts, + &cli_opts, + OfflineWalletSubCommand::Balance, + ) + .context("balance handler failed after sync")?; + let confirmed_balance = confirmed_balance_from_output(&balance_output) + .context("failed to parse confirmed balance from handler output")?; + + assert_eq!(confirmed_balance, sent_amount.to_sat()); + + Ok(()) + } }