]> Untitled Git - bdk-cli/commitdiff
Refactor everything
authorrajarshimaitra <rajarshi149@gmail.com>
Wed, 15 Jun 2022 21:58:25 +0000 (03:28 +0530)
committerrajarshimaitra <rajarshi149@gmail.com>
Mon, 27 Jun 2022 18:36:33 +0000 (00:06 +0530)
This PR moves all the components into different module.
Checkout PR description for more details.

.github/workflows/cont_integration.yml
Cargo.lock
Cargo.toml
src/backend.rs [new file with mode: 0644]
src/bdk_cli.rs [deleted file]
src/commands.rs [new file with mode: 0644]
src/handlers.rs [new file with mode: 0644]
src/lib.rs [deleted file]
src/main.rs [new file with mode: 0644]
src/utils.rs [new file with mode: 0644]

index dc6ddb2bdd79e509edb3436f2a0688b52b72e2d6..935b7306faa0b98866e92a913b4d2bdf3f8bcfb0 100644 (file)
@@ -20,7 +20,6 @@ jobs:
           - compiler
           - compact_filters
           - rpc
-          - reserves
           - reserves,electrum
           - electrum,verify
           # regtest-* features are experimental and not fully usable
index bcd8a8fa881cddf2301127ed4ece6075e08658c0..23157ba54b9082f1237596bd1174bb367eea08b7 100644 (file)
@@ -8,6 +8,17 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
+[[package]]
+name = "ahash"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98"
+dependencies = [
+ "getrandom 0.2.7",
+ "once_cell",
+ "version_check",
+]
+
 [[package]]
 name = "aho-corasick"
 version = "0.7.18"
@@ -28,9 +39,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 +59,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 +80,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"
@@ -92,26 +103,28 @@ dependencies = [
 
 [[package]]
 name = "bdk"
-version = "0.18.0"
+version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdfb4ab7649b7ee7e170ea6dc3b7a3fc5d97671600fe40f6ffde3abe52ef4ae5"
+checksum = "ec2c4c49915b82a2576bc7dee83d2f274387904b79305e6ae8b622daa0b282c1"
 dependencies = [
+ "ahash",
  "async-trait",
  "bdk-macros",
  "bip39",
- "bitcoin",
+ "bitcoin 0.28.1",
  "bitcoinconsensus",
- "bitcoincore-rpc",
+ "bitcoincore-rpc 0.15.0",
  "cc",
- "electrum-client",
+ "electrum-client 0.10.1",
  "futures",
  "js-sys",
  "lazy_static",
  "log",
  "miniscript",
- "rand",
+ "rand 0.7.3",
  "reqwest",
  "rocksdb",
+ "rusqlite",
  "serde",
  "serde_json",
  "sled",
@@ -128,7 +141,6 @@ dependencies = [
  "bdk",
  "bdk-macros",
  "bdk-reserves",
- "clap",
  "dirs-next",
  "electrsd",
  "env_logger",
@@ -154,9 +166,9 @@ dependencies = [
 
 [[package]]
 name = "bdk-reserves"
-version = "0.18.0"
+version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2ba3fcfd24950d05b99b88ab920214534e64f46a9a273c1bf90d56a64012b9f"
+checksum = "64250f952cdfb473f80ec7e874677295b201fc9e0fc206d59982d8f9647c5b47"
 dependencies = [
  "base64 0.11.0",
  "bdk",
@@ -206,11 +218,23 @@ name = "bitcoin"
 version = "0.27.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d"
+dependencies = [
+ "bech32",
+ "bitcoin_hashes 0.10.0",
+ "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",
+ "secp256k1 0.22.1",
  "serde",
 ]
 
@@ -245,7 +269,20 @@ version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7b8d99d58466295cb2bf72c6959b784d59f8f0d6977458d2ba3eb75c834f36c3"
 dependencies = [
- "bitcoincore-rpc-json",
+ "bitcoincore-rpc-json 0.14.0",
+ "jsonrpc",
+ "log",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "bitcoincore-rpc"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0e67dbf7a9971e7f4276f6089e9e814ce0f624a03216b7d92d00351ae7fb3e"
+dependencies = [
+ "bitcoincore-rpc-json 0.15.0",
  "jsonrpc",
  "log",
  "serde",
@@ -258,7 +295,18 @@ version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dce91de73c61f5776cf938bfa88378c5b404a70e3369b761dacbe6024fea79dd"
 dependencies = [
- "bitcoin",
+ "bitcoin 0.27.1",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "bitcoincore-rpc-json"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e2ae16202721ba8c3409045681fac790a5ddc791f05731a2df22c0c6bffc0f1"
+dependencies = [
+ "bitcoin 0.28.1",
  "serde",
  "serde_json",
 ]
@@ -270,7 +318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "65ddc41af9556a341c909bc71de33e16da52bf5f8dbda6b7a402054c60bdb722"
 dependencies = [
  "bitcoin_hashes 0.10.0",
- "bitcoincore-rpc",
+ "bitcoincore-rpc 0.14.0",
  "flate2",
  "home",
  "log",
@@ -287,9 +335,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 +404,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 +439,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"
@@ -435,26 +492,26 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.8"
+version = "0.9.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c"
+checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d"
 dependencies = [
- "autocfg",
+ "autocfg 1.1.0",
  "cfg-if",
  "crossbeam-utils",
- "lazy_static",
  "memoffset",
+ "once_cell",
  "scopeguard",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.8"
+version = "0.8.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
+checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978"
 dependencies = [
  "cfg-if",
- "lazy_static",
+ "once_cell",
 ]
 
 [[package]]
@@ -498,7 +555,7 @@ checksum = "334abee7787b76757ac34b13a9a1cbf1ef0f2da35162d3ceb95a5b0bc34df80f"
 dependencies = [
  "bitcoin_hashes 0.10.0",
  "bitcoind",
- "electrum-client",
+ "electrum-client 0.8.0",
  "log",
  "nix",
  "ureq 2.2.0",
@@ -511,14 +568,26 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "edd12f125852d77980725243b2a8b3bea73cd4c7a22c33bc52b08b664c561dc7"
 dependencies = [
- "bitcoin",
+ "bitcoin 0.27.1",
+ "log",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "electrum-client"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ef9b40020912229e947b45d91f9ff96b10d543e0eddd75ff41b9eda24d9c051"
+dependencies = [
+ "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]]
@@ -568,6 +637,18 @@ dependencies = [
  "str-buf",
 ]
 
+[[package]]
+name = "fallible-iterator"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
 [[package]]
 name = "fastrand"
 version = "1.7.0"
@@ -585,14 +666,14 @@ checksum = "a16910e685088843d53132b04e0f10a571fdb193224fc589685b3ba1ce4cb03d"
 dependencies = [
  "cfg-if",
  "libc",
- "windows-sys",
+ "windows-sys 0.28.0",
 ]
 
 [[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 +683,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 +719,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 +838,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]]
@@ -790,6 +877,18 @@ name = "hashbrown"
 version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
+dependencies = [
+ "hashbrown",
+]
 
 [[package]]
 name = "heck"
@@ -820,9 +919,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 +962,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",
@@ -902,7 +1001,7 @@ version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
 dependencies = [
- "autocfg",
+ "autocfg 1.1.0",
  "hashbrown",
 ]
 
@@ -938,9 +1037,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.57"
+version = "0.3.58"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397"
+checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -997,13 +1096,23 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "libsqlite3-sys"
+version = "0.22.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
 [[package]]
 name = "lock_api"
 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 +1149,7 @@ version = "0.6.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
 dependencies = [
- "autocfg",
+ "autocfg 1.1.0",
 ]
 
 [[package]]
@@ -1057,43 +1166,34 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniscript"
-version = "6.1.0"
+version = "7.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e292b58407dfbf1384e5aca8428d3b0f2eaa09d24cb17088f6db0b7ca31194a"
+checksum = "da39fc7a8adea97a677337b0091779dd86349226b869053af496584a9b9e5847"
 dependencies = [
- "bitcoin",
+ "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]]
 name = "mio"
-version = "0.7.14"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
 dependencies = [
  "libc",
  "log",
- "miow",
- "ntapi",
- "winapi",
-]
-
-[[package]]
-name = "miow"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
-dependencies = [
- "winapi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "windows-sys 0.36.1",
 ]
 
 [[package]]
@@ -1128,15 +1228,6 @@ dependencies = [
  "minimal-lexical",
 ]
 
-[[package]]
-name = "ntapi"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
-dependencies = [
- "winapi",
-]
-
 [[package]]
 name = "once_cell"
 version = "1.12.0"
@@ -1290,6 +1381,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 +1408,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 +1433,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 +1457,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 +1475,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 +1552,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 +1585,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 +1610,7 @@ dependencies = [
  "serde_urlencoded",
  "tokio",
  "tokio-socks",
+ "tower-service",
  "url",
  "wasm-bindgen",
  "wasm-bindgen-futures",
@@ -1442,6 +1643,21 @@ dependencies = [
  "librocksdb-sys",
 ]
 
+[[package]]
+name = "rusqlite"
+version = "0.25.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c4b1eaf239b47034fb450ee9cdedd7d0226571689d8823030c4b6c2cb407152"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "memchr",
+ "smallvec 1.8.0",
+]
+
 [[package]]
 name = "rustc-hash"
 version = "1.1.0"
@@ -1459,28 +1675,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 +1744,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 +1784,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 +1996,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 +2032,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",
@@ -1899,16 +2144,17 @@ dependencies = [
 
 [[package]]
 name = "tokio"
-version = "1.14.1"
+version = "1.19.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9d0183f6f6001549ab68f8c7585093bb732beefbcf6d23a10b9b95c73a1dd49"
+checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
 dependencies = [
- "autocfg",
  "bytes",
  "libc",
  "memchr",
  "mio",
+ "once_cell",
  "pin-project-lite",
+ "socket2",
  "winapi",
 ]
 
@@ -1926,9 +2172,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.2"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c"
+checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45"
 dependencies = [
  "bytes",
  "futures-core",
@@ -1946,34 +2192,22 @@ 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",
- "tracing-attributes",
  "tracing-core",
 ]
 
-[[package]]
-name = "tracing-attributes"
-version = "0.1.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
 [[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 +2224,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 +2270,7 @@ dependencies = [
  "qstring",
  "rustls 0.19.1",
  "url",
- "webpki",
+ "webpki 0.21.4",
  "webpki-roots 0.21.1",
 ]
 
@@ -2055,7 +2289,7 @@ dependencies = [
  "serde_json",
  "socks",
  "url",
- "webpki",
+ "webpki 0.21.4",
  "webpki-roots 0.21.1",
 ]
 
@@ -2077,6 +2311,12 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372"
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "vec_map"
 version = "0.8.2"
@@ -2107,15 +2347,15 @@ 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"
-version = "0.2.80"
+version = "0.2.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
+checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -2123,9 +2363,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.80"
+version = "0.2.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4"
+checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a"
 dependencies = [
  "bumpalo",
  "lazy_static",
@@ -2138,9 +2378,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.30"
+version = "0.4.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2"
+checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -2150,9 +2390,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.80"
+version = "0.2.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
+checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -2160,9 +2400,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.80"
+version = "0.2.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
+checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2173,15 +2413,15 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.80"
+version = "0.2.81"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744"
+checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
 
 [[package]]
 name = "web-sys"
-version = "0.3.57"
+version = "0.3.58"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283"
+checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -2198,12 +2438,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 +2453,16 @@ 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]]
@@ -2252,11 +2502,24 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6"
 dependencies = [
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_msvc",
+ "windows_aarch64_msvc 0.28.0",
+ "windows_i686_gnu 0.28.0",
+ "windows_i686_msvc 0.28.0",
+ "windows_x86_64_gnu 0.28.0",
+ "windows_x86_64_msvc 0.28.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
 ]
 
 [[package]]
@@ -2265,30 +2528,60 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
 [[package]]
 name = "winreg"
 version = "0.10.1"
index 4f95791af9ba150b0113783ecc4311e6486a776a..f0a6c1efba2e46340b6b39914cd9bfdbbaec8b99 100644 (file)
@@ -2,59 +2,68 @@
 name = "bdk-cli"
 version = "0.5.0"
 edition = "2018"
-authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>", "Steve Myers <steve@notmandatory.org>"]
+authors = ["Bitcoin Dev Kit Developers"]
 homepage = "https://bitcoindevkit.org"
 repository = "https://github.com/bitcoindevkit/bdk-cli"
 documentation = "https://docs.rs/bdk-cli"
-description = "A CLI library and example CLI tool based on the BDK descriptor-based wallet library"
-keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
+description = "An experimental CLI wallet application and playground, powered by BDK"
+keywords = ["bitcoin", "wallet", "descriptor", "psbt", "taproot"]
 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" }
-log = "^0.4"
-base64 = "^0.11"
-zeroize = { version = "<1.4.0" }
+structopt = "0.3"
+serde_json = "1.0"
+log = "0.4"
+base64 = "0.11"
+zeroize = "<1.4.0"
+dirs-next = "2.0"
+env_logger = "0.7"
 
 # Optional dependencies
 rustyline = { version = "~9.0", optional = true }
 fd-lock = { version = "=3.0.2", optional = true }
-dirs-next = { version = "2.0", optional = true }
-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}
+bdk-reserves = { version = "0.19", optional = true}
 electrsd = { version= "0.12", features = ["trigger", "bitcoind_22_0"], optional = true}
 
 [features]
-default = ["cli", "repl", "key-value-db"]
-cli = ["clap", "dirs-next", "env_logger"]
+default = ["repl", "sqlite-db"]
+
+# To use the app in a REPL mode
 repl = ["regex", "rustyline", "fd-lock"]
+
+# Avaialable dataabse options
 key-value-db = ["bdk/key-value-db"]
-# sqlite-db = ["bdk/sqlite"] TODO add back with bdk version 0.19
+sqlite-db = ["bdk/sqlite"]
+
+# Avialable blockchain backend options
+rpc = ["bdk/rpc"]
 electrum = ["bdk/electrum"]
+compact_filters = ["bdk/compact_filters"]
 esplora = []
 esplora-ureq = ["esplora", "bdk/use-esplora-ureq"]
 esplora-reqwest = ["esplora", "bdk/use-esplora-reqwest"]
+
+# Use this to consensus verify transactions at sync time
+verify = ["bdk/verify"]
+
+# Extra utility tools
+# Compile policies
 compiler = ["bdk/compiler"]
-compact_filters = ["bdk/compact_filters"]
-rpc = ["bdk/rpc"]
+# Create/Verify proof of reserves as per BIP322
 reserves = ["bdk-reserves"]
-verify = ["bdk/verify"]
+
+# Following features auto deploys a regtest node in the background.
+# Connects the bdk wallet with that node.
+# And allows a new `bdk-cli node <command>` to operate the background node.
+#
+# This is most useful for integrations testing as well as quick demo testing
+# by devs using bdk and various types of background nodes.
 regtest-node = []
 regtest-bitcoin = ["regtest-node" , "rpc", "electrsd"]
 regtest-electrum = ["regtest-node", "electrum", "electrsd/electrs_0_8_10"]
 regtest-esplora-ureq = ["regtest-node", "esplora-ureq", "electrsd/esplora_a33e97e1"]
-regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"]
-
-[[bin]]
-name = "bdk-cli"
-path = "src/bdk_cli.rs"
-required-features = ["cli"]
-
-[package.metadata.docs.rs]
-features = ["compiler", "electrum"]
+regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"]
\ No newline at end of file
diff --git a/src/backend.rs b/src/backend.rs
new file mode 100644 (file)
index 0000000..f965b43
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! The Backend
+//!
+//! This module defines the Backend struct and associated operations
+
+#[allow(dead_code)]
+// Different Backend types activated with `regtest-*` mode.
+// If `regtest-*` feature not activated, then default is `None`.
+pub enum Backend {
+    None,
+    Bitcoin { rpc_url: String, rpc_auth: String },
+    Electrum { electrum_url: String },
+    Esplora { esplora_url: String },
+}
diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs
deleted file mode 100644 (file)
index b3b0371..0000000
+++ /dev/null
@@ -1,621 +0,0 @@
-// Bitcoin Dev Kit
-// Written in 2020 by
-//     Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-use bitcoin::secp256k1::Secp256k1;
-use bitcoin::Network;
-use std::fs;
-use std::path::PathBuf;
-
-use clap::AppSettings;
-use log::{debug, error, info, warn};
-
-#[cfg(feature = "repl")]
-use rustyline::error::ReadlineError;
-#[cfg(feature = "repl")]
-use rustyline::Editor;
-
-use structopt::StructOpt;
-
-#[cfg(feature = "compact_filters")]
-use bdk::blockchain::compact_filters::{BitcoinPeerConfig, CompactFiltersBlockchainConfig};
-#[cfg(feature = "electrum")]
-use bdk::blockchain::electrum::ElectrumBlockchainConfig;
-#[cfg(feature = "esplora")]
-use bdk::blockchain::esplora::EsploraBlockchainConfig;
-
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-use bdk::blockchain::{AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain};
-
-#[cfg(feature = "rpc")]
-use bdk::blockchain::rpc::{Auth, RpcConfig};
-
-#[cfg(feature = "key-value-db")]
-use bdk::database::any::SledDbConfiguration;
-#[cfg(feature = "sqlite-db")]
-use bdk::database::any::SqliteDbConfiguration;
-use bdk::database::{AnyDatabase, AnyDatabaseConfig, BatchDatabase, ConfigurableDatabase};
-use bdk::wallet::wallet_name_from_descriptor;
-use bdk::Wallet;
-use bdk::{bitcoin, Error};
-use bdk_cli::WalletSubCommand;
-use bdk_cli::{CliOpts, CliSubCommand, KeySubCommand, OfflineWalletSubCommand, WalletOpts};
-
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-use bdk_cli::OnlineWalletSubCommand;
-
-#[cfg(feature = "repl")]
-use regex::Regex;
-
-#[cfg(feature = "repl")]
-const REPL_LINE_SPLIT_REGEX: &str = r#""([^"]*)"|'([^']*)'|([\w\-]+)"#;
-
-/// REPL mode
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(name = "", setting = AppSettings::NoBinaryName,
-version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
-author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
-enum ReplSubCommand {
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "compact_filters",
-        feature = "rpc"
-    ))]
-    #[structopt(flatten)]
-    OnlineWalletSubCommand(OnlineWalletSubCommand),
-    #[structopt(flatten)]
-    OfflineWalletSubCommand(OfflineWalletSubCommand),
-    #[structopt(flatten)]
-    KeySubCommand(KeySubCommand),
-    /// Exit REPL loop
-    Exit,
-}
-
-/// prepare bdk_cli home and wallet directory
-fn prepare_home_wallet_dir(wallet_name: &str) -> Result<PathBuf, Error> {
-    let mut dir = PathBuf::new();
-    dir.push(
-        &dirs_next::home_dir().ok_or_else(|| Error::Generic("home dir not found".to_string()))?,
-    );
-    dir.push(".bdk-bitcoin");
-
-    if !dir.exists() {
-        info!("Creating home directory {}", dir.as_path().display());
-        fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
-    }
-
-    dir.push(wallet_name);
-
-    if !dir.exists() {
-        info!("Creating wallet directory {}", dir.as_path().display());
-        fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
-    }
-
-    Ok(dir)
-}
-
-/// Prepare wallet database directory
-fn prepare_wallet_db_dir(wallet_name: &str) -> Result<PathBuf, Error> {
-    let mut db_dir = prepare_home_wallet_dir(wallet_name)?;
-
-    #[cfg(feature = "key-value-db")]
-    db_dir.push("wallet.sled");
-
-    #[cfg(feature = "sqlite-db")]
-    db_dir.push("wallet.sqlite");
-
-    #[cfg(not(feature = "sqlite-db"))]
-    if !db_dir.exists() {
-        info!("Creating database directory {}", db_dir.as_path().display());
-        fs::create_dir(&db_dir).map_err(|e| Error::Generic(e.to_string()))?;
-    }
-
-    Ok(db_dir)
-}
-
-/// Prepare blockchain data directory (for compact filters)
-#[cfg(feature = "compact_filters")]
-fn prepare_bc_dir(wallet_name: &str) -> Result<PathBuf, Error> {
-    let mut bc_dir = prepare_home_wallet_dir(wallet_name)?;
-
-    bc_dir.push("compact_filters");
-
-    if !bc_dir.exists() {
-        info!(
-            "Creating blockchain directory {}",
-            bc_dir.as_path().display()
-        );
-        fs::create_dir(&bc_dir).map_err(|e| Error::Generic(e.to_string()))?;
-    }
-
-    Ok(bc_dir)
-}
-
-fn open_database(wallet_opts: &WalletOpts) -> Result<AnyDatabase, Error> {
-    let wallet_name = wallet_opts.wallet.as_ref().expect("wallet name");
-    let database_path = prepare_wallet_db_dir(wallet_name)?;
-
-    #[cfg(feature = "key-value-db")]
-    let config = AnyDatabaseConfig::Sled(SledDbConfiguration {
-        path: database_path
-            .into_os_string()
-            .into_string()
-            .expect("path string"),
-        tree_name: wallet_name.to_string(),
-    });
-    #[cfg(feature = "sqlite-db")]
-    let config = AnyDatabaseConfig::Sqlite(SqliteDbConfiguration {
-        path: database_path
-            .into_os_string()
-            .into_string()
-            .expect("path string"),
-    });
-    let database = AnyDatabase::from_config(&config)?;
-    debug!("database opened successfully");
-    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",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-fn new_blockchain(
-    _network: Network,
-    wallet_opts: &WalletOpts,
-    _backend: &Backend,
-) -> Result<AnyBlockchain, Error> {
-    #[cfg(feature = "electrum")]
-    let config = {
-        let url = match _backend {
-            Backend::Electrum { electrum_url } => electrum_url.to_owned(),
-            _ => wallet_opts.electrum_opts.server.clone(),
-        };
-
-        AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
-            url,
-            socks5: wallet_opts.proxy_opts.proxy.clone(),
-            retry: wallet_opts.proxy_opts.retries,
-            timeout: wallet_opts.electrum_opts.timeout,
-            stop_gap: wallet_opts.electrum_opts.stop_gap,
-        })
-    };
-
-    #[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(),
-    });
-
-    #[cfg(feature = "compact_filters")]
-    let config = {
-        let mut peers = vec![];
-        for addrs in wallet_opts.compactfilter_opts.address.clone() {
-            for _ in 0..wallet_opts.compactfilter_opts.conn_count {
-                peers.push(BitcoinPeerConfig {
-                    address: addrs.clone(),
-                    socks5: wallet_opts.proxy_opts.proxy.clone(),
-                    socks5_credentials: wallet_opts.proxy_opts.proxy_auth.clone(),
-                })
-            }
-        }
-
-        let wallet_name = wallet_opts.wallet.as_ref().expect("wallet name");
-        AnyBlockchainConfig::CompactFilters(CompactFiltersBlockchainConfig {
-            peers,
-            network: _network,
-            storage_dir: prepare_bc_dir(wallet_name)?
-                .into_os_string()
-                .into_string()
-                .map_err(|_| Error::Generic("Internal OS_String conversion error".to_string()))?,
-            skip_blocks: Some(wallet_opts.compactfilter_opts.skip_blocks),
-        })
-    };
-
-    #[cfg(feature = "rpc")]
-    let config: AnyBlockchainConfig = {
-        let (url, auth) = match _backend {
-            Backend::Bitcoin { rpc_url, rpc_auth } => (
-                rpc_url,
-                Auth::Cookie {
-                    file: rpc_auth.into(),
-                },
-            ),
-            _ => {
-                let auth = if let Some(cookie) = &wallet_opts.rpc_opts.cookie {
-                    Auth::Cookie {
-                        file: cookie.into(),
-                    }
-                } else {
-                    Auth::UserPass {
-                        username: wallet_opts.rpc_opts.basic_auth.0.clone(),
-                        password: wallet_opts.rpc_opts.basic_auth.1.clone(),
-                    }
-                };
-                (&wallet_opts.rpc_opts.address, 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 rpc_url = "http://".to_string() + &url;
-
-        let rpc_config = RpcConfig {
-            url: rpc_url,
-            auth,
-            network: _network,
-            wallet_name,
-            skip_blocks: wallet_opts.rpc_opts.skip_blocks,
-        };
-
-        AnyBlockchainConfig::Rpc(rpc_config)
-    };
-
-    Ok(AnyBlockchain::from_config(&config)?)
-}
-
-fn new_wallet<D>(
-    network: Network,
-    wallet_opts: &WalletOpts,
-    database: D,
-) -> Result<Wallet<D>, Error>
-where
-    D: BatchDatabase,
-{
-    let descriptor = wallet_opts.descriptor.as_str();
-    let change_descriptor = wallet_opts.change_descriptor.as_deref();
-    let wallet = Wallet::new(descriptor, change_descriptor, network, database)?;
-    Ok(wallet)
-}
-
-fn main() {
-    env_logger::init();
-
-    let cli_opts: CliOpts = CliOpts::from_args();
-
-    let network = cli_opts.network;
-    debug!("network: {:?}", network);
-    if network == Network::Bitcoin {
-        warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
-    }
-
-    #[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();
-        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()
-    };
-
-    #[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(),
-        }
-    };
-
-    #[cfg(feature = "regtest-electrum")]
-    let (_electrsd, 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)
-    };
-
-    #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))]
-    let (_electrsd, 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)
-    };
-
-    #[cfg(not(feature = "regtest-node"))]
-    let backend = Backend::None;
-
-    match handle_command(cli_opts, network, backend) {
-        Ok(result) => println!("{}", result),
-        Err(e) => {
-            match e {
-                Error::ChecksumMismatch => error!("Descriptor checksum mismatch. Are you using a different descriptor for an already defined wallet name? (if you are not specifying the wallet name it is automatically named based on the descriptor)"),
-                e => error!("{}", e.to_string()),
-            }
-        },
-    }
-}
-
-fn maybe_descriptor_wallet_name(
-    wallet_opts: WalletOpts,
-    network: Network,
-) -> Result<WalletOpts, Error> {
-    if wallet_opts.wallet.is_some() {
-        return Ok(wallet_opts);
-    }
-    // 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 mut wallet_opts = wallet_opts;
-    wallet_opts.wallet = Some(wallet_name);
-
-    Ok(wallet_opts)
-}
-
-fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Result<String, Error> {
-    let result = match cli_opts.subcommand {
-        #[cfg(any(
-            feature = "electrum",
-            feature = "esplora",
-            feature = "compact_filters",
-            feature = "rpc"
-        ))]
-        CliSubCommand::Wallet {
-            wallet_opts,
-            subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
-        } => {
-            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 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)?
-        }
-        CliSubCommand::Wallet {
-            wallet_opts,
-            subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
-        } => {
-            let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?;
-            let database = open_database(&wallet_opts)?;
-            let wallet = new_wallet(network, &wallet_opts, database)?;
-            let result = bdk_cli::handle_offline_wallet_subcommand(
-                &wallet,
-                &wallet_opts,
-                offline_subcommand,
-            )?;
-            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)?
-        }
-        #[cfg(feature = "compiler")]
-        CliSubCommand::Compile {
-            policy,
-            script_type,
-        } => {
-            let result = bdk_cli::handle_compile_subcommand(network, policy, script_type)?;
-            serde_json::to_string_pretty(&result)?
-        }
-        #[cfg(feature = "repl")]
-        CliSubCommand::Repl { wallet_opts } => {
-            let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?;
-            let database = open_database(&wallet_opts)?;
-
-            let wallet = new_wallet(network, &wallet_opts, database)?;
-
-            let mut rl = Editor::<()>::new();
-
-            // if rl.load_history("history.txt").is_err() {
-            //     println!("No previous history.");
-            // }
-
-            let split_regex =
-                Regex::new(REPL_LINE_SPLIT_REGEX).map_err(|e| Error::Generic(e.to_string()))?;
-
-            loop {
-                let readline = rl.readline(">> ");
-                match readline {
-                    Ok(line) => {
-                        if line.trim() == "" {
-                            continue;
-                        }
-                        rl.add_history_entry(line.as_str());
-                        let split_line: Vec<&str> = split_regex
-                            .captures_iter(&line)
-                            .map(|c| {
-                                Ok(c.get(1)
-                                    .or_else(|| c.get(2))
-                                    .or_else(|| c.get(3))
-                                    .ok_or_else(|| Error::Generic("Invalid commands".to_string()))?
-                                    .as_str())
-                            })
-                            .collect::<Result<Vec<_>, Error>>()?;
-                        let repl_subcommand = ReplSubCommand::from_iter_safe(split_line);
-                        if let Err(err) = repl_subcommand {
-                            println!("{}", err);
-                            continue;
-                        }
-                        // if error will be printed above
-                        let repl_subcommand = repl_subcommand.unwrap();
-                        debug!("repl_subcommand = {:?}", repl_subcommand);
-
-                        let result = match repl_subcommand {
-                            #[cfg(any(
-                                feature = "electrum",
-                                feature = "esplora",
-                                feature = "compact_filters",
-                                feature = "rpc"
-                            ))]
-                            ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => {
-                                let blockchain = new_blockchain(network, &wallet_opts, &_backend)?;
-                                bdk_cli::handle_online_wallet_subcommand(
-                                    &wallet,
-                                    &blockchain,
-                                    online_subcommand,
-                                )
-                            }
-                            ReplSubCommand::OfflineWalletSubCommand(offline_subcommand) => {
-                                bdk_cli::handle_offline_wallet_subcommand(
-                                    &wallet,
-                                    &wallet_opts,
-                                    offline_subcommand,
-                                )
-                            }
-                            ReplSubCommand::KeySubCommand(key_subcommand) => {
-                                bdk_cli::handle_key_subcommand(network, key_subcommand)
-                            }
-                            ReplSubCommand::Exit => break,
-                        };
-
-                        println!("{}", serde_json::to_string_pretty(&result?)?);
-                    }
-                    Err(ReadlineError::Interrupted) => continue,
-                    Err(ReadlineError::Eof) => break,
-                    Err(err) => {
-                        println!("{:?}", err);
-                        break;
-                    }
-                }
-            }
-
-            "Exiting REPL".to_string()
-        }
-        #[cfg(all(feature = "reserves", feature = "electrum"))]
-        CliSubCommand::ExternalReserves {
-            message,
-            psbt,
-            confirmations,
-            addresses,
-            electrum_opts,
-        } => {
-            let result = bdk_cli::handle_ext_reserves_subcommand(
-                network,
-                message,
-                psbt,
-                confirmations,
-                addresses,
-                electrum_opts,
-            )?;
-            serde_json::to_string_pretty(&result)?
-        }
-    };
-    Ok(result)
-}
-
-#[cfg(test)]
-mod test {
-    #[cfg(feature = "repl")]
-    use crate::REPL_LINE_SPLIT_REGEX;
-    #[cfg(feature = "repl")]
-    use regex::Regex;
-
-    #[cfg(feature = "repl")]
-    #[test]
-    fn test_regex_double_quotes() {
-        let split_regex = Regex::new(REPL_LINE_SPLIT_REGEX).unwrap();
-        let line = r#"restore -m "word1 word2 word3" -p 'test! 123 -test' "#;
-        let split_line: Vec<&str> = split_regex
-            .captures_iter(&line)
-            .map(|c| {
-                c.get(1)
-                    .or_else(|| c.get(2))
-                    .or_else(|| c.get(3))
-                    .unwrap()
-                    .as_str()
-            })
-            .collect();
-        assert_eq!(
-            vec!(
-                "restore",
-                "-m",
-                "word1 word2 word3",
-                "-p",
-                "test! 123 -test"
-            ),
-            split_line
-        );
-    }
-
-    #[cfg(feature = "repl")]
-    #[test]
-    fn test_regex_single_quotes() {
-        let split_regex = Regex::new(REPL_LINE_SPLIT_REGEX).unwrap();
-        let line = r#"restore -m 'word1 word2 word3' -p "test *123 -test" "#;
-        let split_line: Vec<&str> = split_regex
-            .captures_iter(&line)
-            .map(|c| {
-                c.get(1)
-                    .or_else(|| c.get(2))
-                    .or_else(|| c.get(3))
-                    .unwrap()
-                    .as_str()
-            })
-            .collect();
-        assert_eq!(
-            vec!(
-                "restore",
-                "-m",
-                "word1 word2 word3",
-                "-p",
-                "test *123 -test"
-            ),
-            split_line
-        );
-    }
-}
diff --git a/src/commands.rs b/src/commands.rs
new file mode 100644 (file)
index 0000000..015c3fd
--- /dev/null
@@ -0,0 +1,1594 @@
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! bdk-cli Command structure
+//!
+//! This module defines all the bdk-cli commands using [structopt]
+
+use structopt::StructOpt;
+
+use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey};
+use bdk::bitcoin::{Address, Network, OutPoint, Script};
+
+#[cfg(any(
+    feature = "compact_filters",
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc"
+))]
+use crate::utils::parse_proxy_auth;
+use crate::utils::{parse_outpoint, parse_recipient};
+
+#[derive(PartialEq, Clone, Debug, StructOpt)]
+/// The BDK Command Line Wallet App
+///
+/// bdk-cli is a light weight command line bitcoin wallet, powered by BDK.
+/// This app can be used as a playground as well as testing environment to simulate
+/// various wallet testing situations. If you are planning to use BDK in your wallet, bdk-cli
+/// is also a great intro tool to get familiar with the BDK API.
+///
+/// But this is not just any toy.
+/// bdk-cli is also a fully functioning Bitcoin wallet with taproot support!
+///
+/// For more information checkout https://bitcoindevkit.org/
+#[structopt(version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
+author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
+pub struct CliOpts {
+    /// Sets the network
+    #[structopt(
+        name = "NETWORK",
+        short = "n",
+        long = "network",
+        default_value = "testnet"
+    )]
+    pub network: Network,
+    #[structopt(subcommand)]
+    pub subcommand: CliSubCommand,
+}
+
+/// CLI sub-commands
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+#[structopt(rename_all = "snake")]
+pub enum CliSubCommand {
+    /// Wallet Operations
+    ///
+    /// bdk-cli wallet operations includes all the basic wallet level tasks.
+    /// Most commands can be used without connecting to any backend. To use commands that
+    /// needs backend like `sync` and `broadcast`, compile the binary with specific backend feature
+    /// and use the configuration options below to configure for that backend.
+    Wallet {
+        #[structopt(flatten)]
+        wallet_opts: WalletOpts,
+        #[structopt(subcommand)]
+        subcommand: WalletSubCommand,
+    },
+    /// Key Management Operations
+    ///
+    /// Provides basic key operations that are not related to a specific wallet such as generating a
+    /// new random master extended key or restoring a master extended key from mnemonic words.
+
+    /// These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this
+    /// feature to create keys that secure actual funds on the Bitcoin mainnet.
+    Key {
+        #[structopt(subcommand)]
+        subcommand: KeySubCommand,
+    },
+    /// Compile a miniscript policy to an output descriptor
+    #[cfg(feature = "compiler")]
+    #[structopt(long_about = "Miniscript policy compiler")]
+    Compile {
+        /// Sets the spending policy to compile
+        #[structopt(name = "POLICY", required = true, index = 1)]
+        policy: String,
+        /// Sets the script type used to embed the compiled policy
+        #[structopt(name = "TYPE", short = "t", long = "type", default_value = "wsh", possible_values = &["sh","wsh", "sh-wsh"])]
+        script_type: String,
+    },
+    #[cfg(feature = "repl")]
+    /// REPL command loop mode
+    ///
+    /// REPL command loop can be used to make recurring callbacks to an already loaded wallet.
+    /// This mode is useful for hands on live testing of wallet operations.
+    Repl {
+        #[structopt(flatten)]
+        wallet_opts: WalletOpts,
+    },
+    /// Proof of Reserves Operations
+    ///
+    /// This can be used to produce and verify Proof of Reserves (BIP 322) using bdk-cli
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    ExternalReserves {
+        /// Sets the challenge message with which the proof was produced
+        #[structopt(name = "MESSAGE", required = true, index = 1)]
+        message: String,
+        /// Sets the proof in form of a PSBT to verify
+        #[structopt(name = "PSBT", required = true, index = 2)]
+        psbt: String,
+        /// Sets the number of block confirmations for UTXOs to be considered.
+        #[structopt(name = "CONFIRMATIONS", required = true, index = 3)]
+        confirmations: usize,
+        /// Sets the addresses for which the proof was produced
+        #[structopt(name = "ADDRESSES", required = true, index = 4)]
+        addresses: Vec<String>,
+        #[structopt(flatten)]
+        electrum_opts: ElectrumOpts,
+    },
+}
+
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub enum WalletSubCommand {
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "compact_filters",
+        feature = "rpc"
+    ))]
+    #[structopt(flatten)]
+    OnlineWalletSubCommand(OnlineWalletSubCommand),
+    #[structopt(flatten)]
+    OfflineWalletSubCommand(OfflineWalletSubCommand),
+}
+
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub struct WalletOpts {
+    /// Selects the wallet to use
+    #[structopt(name = "WALLET_NAME", short = "w", long = "wallet")]
+    pub wallet: Option<String>,
+    /// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects
+    #[structopt(name = "VERBOSE", short = "v", long = "verbose")]
+    pub verbose: bool,
+    /// Sets the descriptor to use for the external addresses
+    #[structopt(name = "DESCRIPTOR", short = "d", long = "descriptor", required = true)]
+    pub descriptor: String,
+    /// Sets the descriptor to use for internal addresses
+    #[structopt(name = "CHANGE_DESCRIPTOR", short = "c", long = "change_descriptor")]
+    pub change_descriptor: Option<String>,
+    #[cfg(feature = "electrum")]
+    #[structopt(flatten)]
+    pub electrum_opts: ElectrumOpts,
+    #[cfg(feature = "esplora")]
+    #[structopt(flatten)]
+    pub esplora_opts: EsploraOpts,
+    #[cfg(feature = "compact_filters")]
+    #[structopt(flatten)]
+    pub compactfilter_opts: CompactFilterOpts,
+    #[cfg(feature = "rpc")]
+    #[structopt(flatten)]
+    pub rpc_opts: RpcOpts,
+    #[cfg(any(feature = "compact_filters", feature = "electrum", feature = "esplora"))]
+    #[structopt(flatten)]
+    pub proxy_opts: ProxyOpts,
+}
+
+#[cfg(any(feature = "compact_filters", feature = "electrum", feature = "esplora"))]
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub struct ProxyOpts {
+    /// Sets the SOCKS5 proxy for Blockchain backend
+    #[structopt(name = "PROXY_ADDRS:PORT", long = "proxy", short = "p")]
+    pub proxy: Option<String>,
+
+    /// Sets the SOCKS5 proxy credential
+    #[structopt(name="PROXY_USER:PASSWD", long="proxy_auth", short="a", parse(try_from_str = parse_proxy_auth))]
+    pub proxy_auth: Option<(String, String)>,
+
+    /// Sets the SOCKS5 proxy retries for the Electrum client
+    #[structopt(
+        name = "PROXY_RETRIES",
+        short = "r",
+        long = "retries",
+        default_value = "5"
+    )]
+    pub retries: u8,
+}
+
+#[cfg(feature = "compact_filters")]
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub struct CompactFilterOpts {
+    /// Sets the full node network address
+    #[structopt(
+        name = "ADDRESS:PORT",
+        short = "n",
+        long = "node",
+        default_value = "127.0.0.1:18444"
+    )]
+    pub address: Vec<String>,
+
+    /// Sets the number of parallel node connections
+    #[structopt(name = "CONNECTIONS", long = "conn_count", default_value = "4")]
+    pub conn_count: usize,
+
+    /// Optionally skip initial `skip_blocks` blocks
+    #[structopt(
+        name = "SKIP_BLOCKS",
+        short = "k",
+        long = "skip_blocks",
+        default_value = "0"
+    )]
+    pub skip_blocks: usize,
+}
+
+#[cfg(feature = "rpc")]
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub struct RpcOpts {
+    /// Sets the full node address for rpc connection
+    #[structopt(
+        name = "ADDRESS:PORT",
+        short = "n",
+        long = "node",
+        default_value = "127.0.0.1:18443"
+    )]
+    pub address: String,
+
+    /// Sets the rpc basic authentication
+    #[structopt(
+        name = "USER:PASSWD",
+        short = "a",
+        long = "basic-auth",
+        parse(try_from_str = parse_proxy_auth),
+        default_value = "user:password",
+    )]
+    pub basic_auth: (String, String),
+
+    /// Sets an optional cookie authentication
+    #[structopt(name = "COOKIE", long = "cookie")]
+    pub cookie: Option<String>,
+
+    /// Optionally skip initial `skip_blocks` blocks
+    #[structopt(name = "SKIP_BLOCKS", short = "s", long = "skip-blocks")]
+    pub skip_blocks: Option<u32>,
+}
+
+#[cfg(feature = "electrum")]
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub struct ElectrumOpts {
+    /// Sets the SOCKS5 proxy timeout for the Electrum client
+    #[structopt(name = "PROXY_TIMEOUT", short = "t", long = "timeout")]
+    pub timeout: Option<u8>,
+    /// Sets the Electrum server to use
+    #[structopt(
+        name = "ELECTRUM_URL",
+        short = "s",
+        long = "server",
+        default_value = "ssl://electrum.blockstream.info:60002"
+    )]
+    pub server: String,
+
+    /// Stop searching addresses for transactions after finding an unused gap of this length.
+    #[structopt(
+        name = "STOP_GAP",
+        long = "stop_gap",
+        short = "g",
+        default_value = "10"
+    )]
+    pub stop_gap: usize,
+}
+
+#[cfg(feature = "esplora")]
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub struct EsploraOpts {
+    /// Use the esplora server if given as parameter
+    #[structopt(
+        name = "ESPLORA_URL",
+        short = "s",
+        long = "server",
+        default_value = "https://blockstream.info/testnet/api/"
+    )]
+    pub server: String,
+
+    /// Socket timeout
+    #[structopt(name = "TIMEOUT", long = "timeout", default_value = "5")]
+    pub timeout: u64,
+
+    /// Stop searching addresses for transactions after finding an unused gap of this length.
+    #[structopt(
+        name = "STOP_GAP",
+        long = "stop_gap",
+        short = "g",
+        default_value = "10"
+    )]
+    pub stop_gap: usize,
+
+    /// Number of parallel requests sent to the esplora service (default: 4)
+    #[structopt(name = "CONCURRENCY", long = "conc", default_value = "4")]
+    pub conc: u8,
+}
+
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+#[structopt(rename_all = "snake")]
+pub enum OfflineWalletSubCommand {
+    /// Generates a new external address
+    GetNewAddress,
+    /// Lists the available spendable UTXOs
+    ListUnspent,
+    /// Lists all the incoming and outgoing transactions of the wallet
+    ListTransactions,
+    /// Returns the current wallet balance
+    GetBalance,
+    /// Creates a new unsigned transaction
+    CreateTx {
+        /// Adds a recipient to the transaction
+        #[structopt(name = "ADDRESS:SAT", long = "to", required = true, parse(try_from_str = parse_recipient))]
+        recipients: Vec<(Script, u64)>,
+        /// Sends all the funds (or all the selected utxos). Requires only one recipients of value 0
+        #[structopt(short = "all", long = "send_all")]
+        send_all: bool,
+        /// Enables Replace-By-Fee (BIP125)
+        #[structopt(short = "rbf", long = "enable_rbf")]
+        enable_rbf: bool,
+        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
+        #[structopt(long = "offline_signer")]
+        offline_signer: bool,
+        /// Selects which utxos *must* be spent
+        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
+        utxos: Option<Vec<OutPoint>>,
+        /// Marks a utxo as unspendable
+        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
+        unspendable: Option<Vec<OutPoint>>,
+        /// Fee rate to use in sat/vbyte
+        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
+        fee_rate: Option<f32>,
+        /// Selects which policy should be used to satisfy the external descriptor
+        #[structopt(name = "EXT_POLICY", long = "external_policy")]
+        external_policy: Option<String>,
+        /// Selects which policy should be used to satisfy the internal descriptor
+        #[structopt(name = "INT_POLICY", long = "internal_policy")]
+        internal_policy: Option<String>,
+    },
+    /// Bumps the fees of an RBF transaction
+    BumpFee {
+        /// TXID of the transaction to update
+        #[structopt(name = "TXID", short = "txid", long = "txid")]
+        txid: String,
+        /// Allows the wallet to reduce the amount to the specified address in order to increase fees.
+        #[structopt(name = "SHRINK_ADDRESS", short = "s", long = "shrink")]
+        shrink_address: Option<Address>,
+        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
+        #[structopt(long = "offline_signer")]
+        offline_signer: bool,
+        /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used
+        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
+        utxos: Option<Vec<OutPoint>>,
+        /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees
+        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
+        unspendable: Option<Vec<OutPoint>>,
+        /// The new targeted fee rate in sat/vbyte
+        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
+        fee_rate: f32,
+    },
+    /// Returns the available spending policies for the descriptor
+    Policies,
+    /// Returns the public version of the wallet's descriptor(s)
+    PublicDescriptor,
+    /// Signs and tries to finalize a PSBT
+    Sign {
+        /// Sets the PSBT to sign
+        #[structopt(name = "BASE64_PSBT", long = "psbt")]
+        psbt: String,
+        /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor
+        #[structopt(name = "HEIGHT", long = "assume_height")]
+        assume_height: Option<u32>,
+        /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided
+        #[structopt(name = "WITNESS", long = "trust_witness_utxo")]
+        trust_witness_utxo: Option<bool>,
+    },
+    /// Extracts a raw transaction from a PSBT
+    ExtractPsbt {
+        /// Sets the PSBT to extract
+        #[structopt(name = "BASE64_PSBT", long = "psbt")]
+        psbt: String,
+    },
+    /// Finalizes a PSBT
+    FinalizePsbt {
+        /// Sets the PSBT to finalize
+        #[structopt(name = "BASE64_PSBT", long = "psbt")]
+        psbt: String,
+        /// Assume the blockchain has reached a specific height
+        #[structopt(name = "HEIGHT", long = "assume_height")]
+        assume_height: Option<u32>,
+        /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided
+        #[structopt(name = "WITNESS", long = "trust_witness_utxo")]
+        trust_witness_utxo: Option<bool>,
+    },
+    /// Combines multiple PSBTs into one
+    CombinePsbt {
+        /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT
+        #[structopt(name = "BASE64_PSBT", long = "psbt", required = true)]
+        psbt: Vec<String>,
+    },
+}
+
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+#[structopt(rename_all = "snake")]
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+pub enum OnlineWalletSubCommand {
+    /// Syncs with the chosen blockchain server
+    Sync,
+    /// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract
+    Broadcast {
+        /// Sets the PSBT to sign
+        #[structopt(
+            name = "BASE64_PSBT",
+            long = "psbt",
+            required_unless = "RAWTX",
+            conflicts_with = "RAWTX"
+        )]
+        psbt: Option<String>,
+        /// Sets the raw transaction to broadcast
+        #[structopt(
+            name = "RAWTX",
+            long = "tx",
+            required_unless = "BASE64_PSBT",
+            conflicts_with = "BASE64_PSBT"
+        )]
+        tx: Option<String>,
+    },
+    /// Produce a proof of reserves
+    #[cfg(feature = "reserves")]
+    ProduceProof {
+        /// Sets the message
+        #[structopt(name = "MESSAGE", long = "message")]
+        msg: String,
+    },
+    /// Verify a proof of reserves for our wallet
+    #[cfg(feature = "reserves")]
+    VerifyProof {
+        /// Sets the PSBT to verify
+        #[structopt(name = "BASE64_PSBT", long = "psbt")]
+        psbt: String,
+        /// Sets the message to verify
+        #[structopt(name = "MESSAGE", long = "message")]
+        msg: String,
+        /// Sets the number of block confirmations for UTXOs to be considered. If nothing is specified, 6 is used.
+        #[structopt(name = "CONFIRMATIONS", long = "confirmations", default_value = "6")]
+        confirmations: u32,
+    },
+}
+
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub enum KeySubCommand {
+    /// Generates new random seed mnemonic phrase and corresponding master extended key
+    Generate {
+        /// Entropy level based on number of random seed mnemonic words
+        #[structopt(
+        name = "WORD_COUNT",
+        short = "e",
+        long = "entropy",
+        default_value = "24",
+        possible_values = &["12","24"],
+        )]
+        word_count: usize,
+        /// Seed password
+        #[structopt(name = "PASSWORD", short = "p", long = "password")]
+        password: Option<String>,
+    },
+    /// Restore a master extended key from seed backup mnemonic words
+    Restore {
+        /// Seed mnemonic words, must be quoted (eg. "word1 word2 ...")
+        #[structopt(name = "MNEMONIC", short = "m", long = "mnemonic")]
+        mnemonic: String,
+        /// Seed password
+        #[structopt(name = "PASSWORD", short = "p", long = "password")]
+        password: Option<String>,
+    },
+    /// Derive a child key pair from a master extended key and a derivation path string (eg. "m/84'/1'/0'/0" or "m/84h/1h/0h/0")
+    Derive {
+        /// Extended private key to derive from
+        #[structopt(name = "XPRV", short = "x", long = "xprv")]
+        xprv: ExtendedPrivKey,
+        /// Path to use to derive extended public key from extended private key
+        #[structopt(name = "PATH", short = "p", long = "path")]
+        path: DerivationPath,
+    },
+}
+
+#[cfg(feature = "repl")]
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+pub enum ReplSubCommand {
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "compact_filters",
+        feature = "rpc"
+    ))]
+    #[structopt(flatten)]
+    OnlineWalletSubCommand(OnlineWalletSubCommand),
+    #[structopt(flatten)]
+    OfflineWalletSubCommand(OfflineWalletSubCommand),
+    #[structopt(flatten)]
+    KeySubCommand(KeySubCommand),
+    /// Exit REPL loop
+    Exit,
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    #[cfg(feature = "compiler")]
+    use crate::handlers::handle_compile_subcommand;
+    use crate::handlers::handle_key_subcommand;
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    use crate::handlers::{handle_ext_reserves_subcommand, handle_online_wallet_subcommand};
+    use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey};
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    use bdk::bitcoin::{consensus::Encodable, util::psbt::PartiallySignedTransaction};
+    use bdk::bitcoin::{Address, Network, OutPoint};
+    use bdk::miniscript::bitcoin::network::constants::Network::Testnet;
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    use bdk::{
+        blockchain::ElectrumBlockchain, database::MemoryDatabase, electrum_client::Client,
+        SyncOptions, Wallet,
+    };
+    use std::str::{self, FromStr};
+    use structopt::StructOpt;
+
+    use super::OfflineWalletSubCommand::{BumpFee, CreateTx, GetNewAddress};
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "compact_filters",
+        feature = "rpc"
+    ))]
+    use super::OnlineWalletSubCommand::{Broadcast, Sync};
+    use super::WalletSubCommand::OfflineWalletSubCommand;
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "compact_filters",
+        feature = "rpc"
+    ))]
+    use super::WalletSubCommand::OnlineWalletSubCommand;
+    #[cfg(feature = "repl")]
+    use regex::Regex;
+
+    #[test]
+    fn test_parse_wallet_get_new_address() {
+        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
+                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "get_new_address"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    #[cfg(feature = "electrum")]
+                    electrum_opts: ElectrumOpts {
+                        timeout: None,
+                        server: "ssl://electrum.blockstream.info:60002".to_string(),
+                        stop_gap: 10,
+                    },
+                    #[cfg(feature = "esplora")]
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/testnet/api/".to_string(),
+                        timeout: 5,
+                        stop_gap: 10,
+                        conc: 4,
+                    },
+                    #[cfg(feature = "compact_filters")]
+                    compactfilter_opts: CompactFilterOpts{
+                        address: vec!["127.0.0.1:18444".to_string()],
+                        conn_count: 4,
+                        skip_blocks: 0,
+                    },
+                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
+                    proxy_opts: ProxyOpts{
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    },
+                    #[cfg(feature = "rpc")]
+                    rpc_opts: RpcOpts {
+                        address: "127.0.0.1:18443".to_string(),
+                        basic_auth: ("user".to_string(), "password".to_string()),
+                        cookie: None,
+                        skip_blocks: None,
+                    },
+                },
+                subcommand: OfflineWalletSubCommand(GetNewAddress),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(feature = "electrum")]
+    #[test]
+    fn test_parse_wallet_electrum() {
+        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
+                            "--proxy", "127.0.0.1:9150", "--retries", "3", "--timeout", "10",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "--server","ssl://electrum.blockstream.info:50002",
+                            "--stop_gap", "20",
+                            "get_new_address"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Testnet,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    electrum_opts: ElectrumOpts {
+                        timeout: Some(10),
+                        server: "ssl://electrum.blockstream.info:50002".to_string(),
+                        stop_gap: 20
+                    },
+                    proxy_opts: ProxyOpts{
+                        proxy: Some("127.0.0.1:9150".to_string()),
+                        proxy_auth: None,
+                        retries: 3,
+                    },
+                },
+                subcommand: OfflineWalletSubCommand(GetNewAddress),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(feature = "esplora-ureq")]
+    #[test]
+    fn test_parse_wallet_esplora() {
+        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
+                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "--server", "https://blockstream.info/api/",
+                            "--timeout", "10",
+                            "--stop_gap", "20",
+                            "get_new_address"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/api/".to_string(),
+                        timeout: 10,
+                        stop_gap: 20,
+                        conc: 4,
+                    },
+                    proxy_opts: ProxyOpts{
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    }
+                },
+                subcommand: OfflineWalletSubCommand(GetNewAddress),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(feature = "esplora-reqwest")]
+    #[test]
+    fn test_parse_wallet_esplora() {
+        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
+                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "--server", "https://blockstream.info/api/",
+                            "--conc", "10",
+                            "--stop_gap", "20",
+                            "get_new_address"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/api/".to_string(),
+                        conc: 10,
+                        stop_gap: 20,
+                        timeout: 5,
+                    },
+                    proxy_opts: ProxyOpts{
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    }
+                },
+                subcommand: OfflineWalletSubCommand(GetNewAddress),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(feature = "rpc")]
+    #[test]
+    fn test_parse_wallet_rpc() {
+        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
+                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "--node", "125.67.89.101:56678",
+                            "--basic-auth", "user:password",
+                            "--cookie", "/home/user/.bitcoin/regtest/.cookie",
+                            "--skip-blocks", "5",
+                            "get_new_address"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    rpc_opts: RpcOpts {
+                        address: "125.67.89.101:56678".to_string(),
+                        basic_auth: ("user".to_string(), "password".to_string()),
+                        cookie: Some("/home/user/.bitcoin/regtest/.cookie".to_string()),
+                        skip_blocks: Some(5),
+                    },
+                },
+                subcommand: OfflineWalletSubCommand(GetNewAddress),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(feature = "compact_filters")]
+    #[test]
+    fn test_parse_wallet_compact_filters() {
+        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
+                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "--proxy", "127.0.0.1:9005",
+                            "--proxy_auth", "random_user:random_passwd",
+                            "--node", "127.0.0.1:18444", "127.2.3.1:19695",
+                            "--conn_count", "4",
+                            "--skip_blocks", "5",
+                            "get_new_address"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    compactfilter_opts: CompactFilterOpts{
+                        address: vec!["127.0.0.1:18444".to_string(), "127.2.3.1:19695".to_string()],
+                        conn_count: 4,
+                        skip_blocks: 5,
+                    },
+                    proxy_opts: ProxyOpts{
+                        proxy: Some("127.0.0.1:9005".to_string()),
+                        proxy_auth: Some(("random_user".to_string(), "random_passwd".to_string())),
+                        retries: 5,
+                    }
+                },
+                subcommand: OfflineWalletSubCommand(GetNewAddress),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "compact_filters",
+        feature = "rpc"
+    ))]
+    #[test]
+    fn test_parse_wallet_sync() {
+        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "sync"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Testnet,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: None,
+                    #[cfg(feature = "electrum")]
+                    electrum_opts: ElectrumOpts {
+                        timeout: None,
+                        server: "ssl://electrum.blockstream.info:60002".to_string(),
+                        stop_gap: 10,
+                    },
+                    #[cfg(feature = "esplora")]
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/testnet/api/".to_string(),
+                        timeout: 5,
+                        stop_gap: 10,
+                        conc: 4,
+                    },
+                    #[cfg(feature = "compact_filters")]
+                    compactfilter_opts: CompactFilterOpts{
+                        address: vec!["127.0.0.1:18444".to_string()],
+                        conn_count: 4,
+                        skip_blocks: 0,
+                    },
+                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
+                    proxy_opts: ProxyOpts{
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    },
+                    #[cfg(feature = "rpc")]
+                    rpc_opts: RpcOpts {
+                        address: "127.0.0.1:18443".to_string(),
+                        basic_auth: ("user".to_string(), "password".to_string()),
+                        cookie: None,
+                        skip_blocks: None,
+                    },
+                },
+                subcommand: OnlineWalletSubCommand(Sync),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[test]
+    fn test_parse_wallet_create_tx() {
+        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "create_tx", "--to", "n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ:123456","mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf:78910",
+                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
+                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ")
+            .unwrap()
+            .script_pubkey();
+        let script2 = Address::from_str("mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf")
+            .unwrap()
+            .script_pubkey();
+        let outpoint1 = OutPoint::from_str(
+            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
+        )
+        .unwrap();
+        let outpoint2 = OutPoint::from_str(
+            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2",
+        )
+        .unwrap();
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Testnet,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    #[cfg(feature = "electrum")]
+                    electrum_opts: ElectrumOpts {
+                        timeout: None,
+                        server: "ssl://electrum.blockstream.info:60002".to_string(),
+                        stop_gap: 10,
+                    },
+                    #[cfg(feature = "esplora")]
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/testnet/api/".to_string(),
+                        timeout: 5,
+                        stop_gap: 10,
+                        conc: 4,
+                    },
+                    #[cfg(feature = "compact_filters")]
+                    compactfilter_opts: CompactFilterOpts{
+                        address: vec!["127.0.0.1:18444".to_string()],
+                        conn_count: 4,
+                        skip_blocks: 0,
+                    },
+                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
+                    proxy_opts: ProxyOpts{
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    },
+                    #[cfg(feature = "rpc")]
+                    rpc_opts: RpcOpts {
+                        address: "127.0.0.1:18443".to_string(),
+                        basic_auth: ("user".to_string(), "password".to_string()),
+                        cookie: None,
+                        skip_blocks: None,
+                    },
+                },
+                subcommand: WalletSubCommand::OfflineWalletSubCommand(CreateTx {
+                    recipients: vec![(script1, 123456), (script2, 78910)],
+                    send_all: false,
+                    enable_rbf: false,
+                    offline_signer: false,
+                    utxos: Some(vec!(outpoint1, outpoint2)),
+                    unspendable: None,
+                    fee_rate: None,
+                    external_policy: None,
+                    internal_policy: None,
+                }),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[test]
+    fn test_parse_wallet_bump_fee() {
+        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "bump_fee", "--fee_rate", "6.1",
+                            "--txid","35aab0d0213f8996f9e236a28630319b93109754819e8abf48a0835708d33506",
+                            "--shrink","tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Testnet,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+                    #[cfg(feature = "electrum")]
+                    electrum_opts: ElectrumOpts {
+                        timeout: None,
+                        server: "ssl://electrum.blockstream.info:60002".to_string(),
+                        stop_gap: 10,
+                    },
+                    #[cfg(feature = "esplora")]
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/testnet/api/".to_string(),
+                        timeout: 5,
+                        stop_gap: 10,
+                        conc: 4,
+                    },
+                    #[cfg(feature = "compact_filters")]
+                    compactfilter_opts: CompactFilterOpts{
+                        address: vec!["127.0.0.1:18444".to_string()],
+                        conn_count: 4,
+                        skip_blocks: 0,
+                    },
+                    #[cfg(feature = "rpc")]
+                    rpc_opts: RpcOpts {
+                        address: "127.0.0.1:18443".to_string(),
+                        basic_auth: ("user".to_string(), "password".to_string()),
+                        cookie: None,
+                        skip_blocks: None,
+                    },
+                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
+                    proxy_opts: ProxyOpts{
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    }
+                },
+                subcommand: OfflineWalletSubCommand(BumpFee {
+                    txid: "35aab0d0213f8996f9e236a28630319b93109754819e8abf48a0835708d33506".to_string(),
+                    shrink_address: Some(Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap()),
+                    offline_signer: false,
+                    utxos: None,
+                    unspendable: None,
+                    fee_rate: 6.1
+                }),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(any(
+        feature = "electrum",
+        feature = "esplora",
+        feature = "compact_filters",
+        feature = "rpc"
+    ))]
+    #[test]
+    fn test_parse_wallet_broadcast() {
+        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "broadcast",
+                            "--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Testnet,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+                    change_descriptor: None,
+                    #[cfg(feature = "electrum")]
+                    electrum_opts: ElectrumOpts {
+                        timeout: None,
+                        server: "ssl://electrum.blockstream.info:60002".to_string(),
+                        stop_gap: 10,
+                    },
+                    #[cfg(feature = "esplora")]
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/testnet/api/".to_string(),
+                        timeout: 5,
+                        stop_gap: 10,
+                        conc: 4,
+                    },
+                    #[cfg(feature = "compact_filters")]
+                    compactfilter_opts: CompactFilterOpts{
+                        address: vec!["127.0.0.1:18444".to_string()],
+                        conn_count: 4,
+                        skip_blocks: 0,
+                    },
+                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
+                    proxy_opts: ProxyOpts{
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    },
+                    #[cfg(feature = "rpc")]
+                    rpc_opts: RpcOpts {
+                        address: "127.0.0.1:18443".to_string(),
+                        basic_auth: ("user".to_string(), "password".to_string()),
+                        cookie: None,
+                        skip_blocks: None,
+                    },
+                },
+                subcommand: OnlineWalletSubCommand(Broadcast {
+                    psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()),
+                    tx: None
+                }),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[test]
+    fn test_parse_wrong_network() {
+        let cli_args = vec!["repl", "--network", "badnet", "wallet",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "sync"];
+
+        let cli_opts = CliOpts::from_iter_safe(&cli_args);
+        assert!(cli_opts.is_err());
+    }
+
+    #[test]
+    fn test_key_generate() {
+        let network = Testnet;
+        let key_generate_cmd = KeySubCommand::Generate {
+            word_count: 12,
+            password: Some("test123".to_string()),
+        };
+
+        let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
+        let result_obj = result.as_object().unwrap();
+
+        let mnemonic = result_obj.get("mnemonic").unwrap().as_str().unwrap();
+        let mnemonic: Vec<&str> = mnemonic.split(' ').collect();
+        let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
+
+        assert_eq!(mnemonic.len(), 12);
+        assert_eq!(&xprv[0..4], "tprv");
+    }
+
+    #[test]
+    fn test_key_restore() {
+        let network = Testnet;
+        let key_generate_cmd = KeySubCommand::Restore {
+            mnemonic: "payment battle unit sword token broccoli era violin purse trip blood hire"
+                .to_string(),
+            password: Some("test123".to_string()),
+        };
+
+        let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
+        let result_obj = result.as_object().unwrap();
+
+        let fingerprint = result_obj.get("fingerprint").unwrap().as_str().unwrap();
+        let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
+
+        assert_eq!(&fingerprint, &"828af366");
+        assert_eq!(&xprv, &"tprv8ZgxMBicQKsPd18TeiFknZKqaZFwpdX9tvvKh8eeHSSPBQi5g9xPHztBg411o78G8XkrhQb6Q1cVvBJ1a9xuFHpmWgvQsvkJkNxBjfGoqhK");
+    }
+
+    #[test]
+    fn test_key_derive() {
+        let network = Testnet;
+        let key_generate_cmd = KeySubCommand::Derive {
+            xprv: ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPfQjJy8ge2cvBfDjLxJSkvNLVQiw7BQ5gTjKadG2rrcQB5zjcdaaUTz5EDNJaS77q4DzjqjogQBfMsaXFFNP3UqoBnwt2kyT").unwrap(),
+            path: DerivationPath::from_str("m/84'/1'/0'/0").unwrap(),
+        };
+
+        let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
+        let result_obj = result.as_object().unwrap();
+
+        let xpub = result_obj.get("xpub").unwrap().as_str().unwrap();
+        let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
+
+        assert_eq!(&xpub, &"[566844c5/84'/1'/0'/0]tpubDFeqiDkfwR1tAhPxsXSZMfEmfpDhwhLyhLKZgmeBvuBkZQusoWeL62oGg2oTNGcENeKdwuGepAB85eMvyLemabYe9PSqv6cr5mFXktHc3Ka/*");
+        assert_eq!(&xprv, &"[566844c5/84'/1'/0'/0]tprv8ixoZoiRo3LDHENAysmxxFaf6nhmnNA582inQFbtWdPMivf7B7pjuYBQVuLC5bkM7tJZEDbfoivENsGZPBnQg1n52Kuc1P8X2Ei3XJuJX7c/*");
+    }
+
+    #[cfg(feature = "compiler")]
+    #[test]
+    fn test_parse_compile() {
+        let cli_args = vec![
+            "bdk-cli",
+            "compile",
+            "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))",
+            "--type",
+            "sh-wsh",
+        ];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Testnet,
+            subcommand: CliSubCommand::Compile {
+                policy: "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))".to_string(),
+                script_type: "sh-wsh".to_string(),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(feature = "compiler")]
+    #[test]
+    fn test_compile() {
+        let result = handle_compile_subcommand(
+            Network::Testnet,
+            "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))".to_string(),
+            "sh-wsh".to_string(),
+        )
+        .unwrap();
+        let result_obj = result.as_object().unwrap();
+
+        let descriptor = result_obj.get("descriptor").unwrap().as_str().unwrap();
+        assert_eq!(
+            &descriptor,
+            &"sh(wsh(thresh(3,pk(Alice),s:pk(Bob),s:pk(Carol),snl:older(2))))#rmef3s78"
+        );
+    }
+
+    #[cfg(all(feature = "reserves", feature = "compact_filters"))]
+    #[test]
+    fn test_parse_produce_proof() {
+        let message = "Those coins belong to Satoshi Nakamoto";
+        let cli_args = vec![
+            "bdk-cli",
+            "--network",
+            "bitcoin",
+            "wallet",
+            "--descriptor",
+            "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
+            "produce_proof",
+            "--message",
+            message.clone(),
+        ];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
+                        .to_string(),
+                    change_descriptor: None,
+                    compactfilter_opts: CompactFilterOpts {
+                        address: vec!["127.0.0.1:18444".to_string()],
+                        conn_count: 4,
+                        skip_blocks: 0,
+                    },
+                    proxy_opts: ProxyOpts {
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    },
+                },
+                subcommand: OnlineWalletSubCommand(ProduceProof {
+                    msg: message.to_string(),
+                }),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(all(feature = "reserves", feature = "esplora-ureq"))]
+    #[test]
+    fn test_parse_verify_proof_internal() {
+        let psbt = r#"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"#;
+        let message = "Those coins belong to Satoshi Nakamoto";
+        let cli_args = vec![
+            "bdk-cli",
+            "--network",
+            "bitcoin",
+            "wallet",
+            "--descriptor",
+            "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
+            "verify_proof",
+            "--psbt",
+            psbt.clone(),
+            "--message",
+            message.clone(),
+        ];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
+                        .to_string(),
+                    change_descriptor: None,
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/testnet/api/".to_string(),
+                        timeout: 5,
+                        stop_gap: 10,
+                        conc: 4,
+                    },
+                    proxy_opts: ProxyOpts {
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    },
+                },
+                subcommand: OnlineWalletSubCommand(VerifyProof {
+                    psbt: psbt.to_string(),
+                    msg: message.to_string(),
+                    confirmations: 6,
+                }),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(all(feature = "reserves", feature = "esplora-ureq"))]
+    #[test]
+    fn test_parse_verify_proof_internal_confirmation() {
+        let psbt = r#"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"#;
+        let message = "Those coins belong to Satoshi Nakamoto";
+        let cli_args = vec![
+            "bdk-cli",
+            "--network",
+            "bitcoin",
+            "wallet",
+            "--descriptor",
+            "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
+            "verify_proof",
+            "--psbt",
+            psbt.clone(),
+            "--message",
+            message.clone(),
+            "--confirmations",
+            "0",
+        ];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::Wallet {
+                wallet_opts: WalletOpts {
+                    wallet: None,
+                    verbose: false,
+                    descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
+                        .to_string(),
+                    change_descriptor: None,
+                    esplora_opts: EsploraOpts {
+                        server: "https://blockstream.info/testnet/api/".to_string(),
+                        timeout: 5,
+                        stop_gap: 10,
+                        conc: 4,
+                    },
+                    proxy_opts: ProxyOpts {
+                        proxy: None,
+                        proxy_auth: None,
+                        retries: 5,
+                    },
+                },
+                subcommand: OnlineWalletSubCommand(VerifyProof {
+                    psbt: psbt.to_string(),
+                    msg: message.to_string(),
+                    confirmations: 0,
+                }),
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    #[test]
+    fn test_parse_verify_proof_external() {
+        let psbt = r#"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"#.to_string();
+        let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d".to_string();
+        let message = "Those coins belong to Satoshi Nakamoto".to_string();
+        let cli_args = vec![
+            "bdk-cli",
+            "--network",
+            "bitcoin",
+            "external_reserves",
+            &message,
+            &psbt,
+            "6",
+            &address,
+            "--server",
+            "ssl://electrum.blockstream.info:60002",
+        ];
+
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let expected_cli_opts = CliOpts {
+            network: Network::Bitcoin,
+            subcommand: CliSubCommand::ExternalReserves {
+                message,
+                psbt,
+                confirmations: 6,
+                addresses: [address].to_vec(),
+                electrum_opts: ElectrumOpts {
+                    timeout: None,
+                    server: "ssl://electrum.blockstream.info:60002".to_string(),
+                    stop_gap: 10,
+                },
+            },
+        };
+
+        assert_eq!(expected_cli_opts, cli_opts);
+    }
+
+    /// Encodes a partially signed transaction as base64 and returns the  bytes of the resulting string.
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    fn encode_psbt(psbt: PartiallySignedTransaction) -> Vec<u8> {
+        let mut encoded = Vec::<u8>::new();
+        psbt.consensus_encode(&mut encoded).unwrap();
+        let base64_psbt = base64::encode(&encoded);
+
+        base64_psbt.as_bytes().to_vec()
+    }
+
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    #[test]
+    fn test_proof_of_reserves_wallet() {
+        let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string();
+        let message = "Those coins belong to Satoshi Nakamoto";
+
+        let client = Client::new("ssl://electrum.blockstream.info:60002").unwrap();
+        let blockchain = ElectrumBlockchain::from(client);
+        let wallet = Wallet::new(
+            &descriptor,
+            None,
+            Network::Testnet,
+            MemoryDatabase::default(),
+        )
+        .unwrap();
+
+        wallet.sync(&blockchain, SyncOptions::default()).unwrap();
+        let balance = wallet.get_balance().unwrap();
+
+        let addr = wallet.get_address(bdk::wallet::AddressIndex::New).unwrap();
+        assert_eq!(
+            "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d",
+            addr.to_string()
+        );
+
+        let cli_args = vec![
+            "bdk-cli",
+            "--network",
+            "bitcoin",
+            "wallet",
+            "--descriptor",
+            &descriptor,
+            "produce_proof",
+            "--message",
+            message.clone(),
+        ];
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let wallet_subcmd = match cli_opts.subcommand {
+            CliSubCommand::Wallet {
+                wallet_opts: _,
+                subcommand: OnlineWalletSubCommand(online_subcommand),
+            } => online_subcommand,
+            _ => panic!("unexpected subcommand"),
+        };
+        let result = handle_online_wallet_subcommand(&wallet, &blockchain, wallet_subcmd).unwrap();
+        let psbt: PartiallySignedTransaction =
+            serde_json::from_str(&result.as_object().unwrap().get("psbt").unwrap().to_string())
+                .unwrap();
+        let psbt = encode_psbt(psbt);
+        let psbt = str::from_utf8(&psbt).unwrap();
+        assert_eq!(format!("{}", psbt), "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");
+
+        let psbt_b64 = &result
+            .as_object()
+            .unwrap()
+            .get("psbt_base64")
+            .unwrap()
+            .to_string();
+        assert_eq!(&format!("{}", psbt), psbt_b64.trim_matches('\"'));
+
+        let cli_args = vec![
+            "bdk-cli",
+            "--network",
+            "bitcoin",
+            "wallet",
+            "--descriptor",
+            &descriptor,
+            "verify_proof",
+            "--psbt",
+            psbt,
+            "--message",
+            message.clone(),
+        ];
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let wallet_subcmd = match cli_opts.subcommand {
+            CliSubCommand::Wallet {
+                wallet_opts: _,
+                subcommand: OnlineWalletSubCommand(online_subcommand),
+            } => online_subcommand,
+            _ => panic!("unexpected subcommand"),
+        };
+        let result = handle_online_wallet_subcommand(&wallet, &blockchain, wallet_subcmd).unwrap();
+        let spendable = result
+            .as_object()
+            .unwrap()
+            .get("spendable")
+            .unwrap()
+            .as_u64()
+            .unwrap();
+        assert_eq!(spendable, balance);
+    }
+
+    #[cfg(all(feature = "reserves", feature = "electrum"))]
+    #[test]
+    fn test_proof_of_reserves_veryfy() {
+        let message = "Those coins belong to Satoshi Nakamoto";
+        let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d";
+        let psbt = "cHNidP8BAKcBAAAAA9A7AuVMuwIrkAuXtKDqw3BruovK2PH7E90MzCbyaAMlAAAAAAD/////sq+79fxJ+5F8951Xcbp8N5VYVJoesG1zd3AxStcGwPYAAAAAAP/////AUS4n/SkO9SSvIv8MI2Xdm3xdn77Sud6uX5PyqCganAEAAAAA/////wGwrQEAAAAAABl2qRSff9CW037SwOP38M/JJL7vT/zraIisAAAAAAABAQoAAAAAAAAAAAFRAQcAAAEBHxAnAAAAAAAAFgAU7OUmVxBT2oZFTJ4GZ3nq+dFQLmoiAgMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH40gwRQIhAPgByvkajQrNeQDSGik2gnxpo/P/owiEHR+0nWefkXurAiBgrAlDvwuTiaGEEWQW/Kd7L7u7YOQnqvrd46DR0A8yPgEBBwABCGwCSDBFAiEA+AHK+RqNCs15ANIaKTaCfGmj8/+jCIQdH7SdZ5+Re6sCIGCsCUO/C5OJoYQRZBb8p3svu7tg5Ceq+t3joNHQDzI+ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfoIYBAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjRzBEAiBSfiX0qP7vR+2Qx/mRJS8pwma8nTfOWKerzo6c0iSAfwIgEfX4Wt7YXd8MkKUEY627GWYCmKfMsJGcIC0U1wgc1vUBAQcAAQhrAkcwRAIgUn4l9Kj+70ftkMf5kSUvKcJmvJ03zlinq86OnNIkgH8CIBH1+Fre2F3fDJClBGOtuxlmApinzLCRnCAtFNcIHNb1ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAA==";
+
+        let cli_args = vec![
+            "bdk-cli",
+            "--network",
+            "bitcoin",
+            "external_reserves",
+            message,
+            psbt,
+            "6",
+            address,
+            address, // passing the address twice on purpose, to test passing of multiple addresses
+            "--server",
+            "ssl://electrum.blockstream.info:60002",
+        ];
+        let cli_opts = CliOpts::from_iter(&cli_args);
+
+        let (message, psbt, confirmations, addresses, electrum_opts) = match cli_opts.subcommand {
+            CliSubCommand::ExternalReserves {
+                message,
+                psbt,
+                confirmations,
+                addresses,
+                electrum_opts,
+            } => (message, psbt, confirmations, addresses, electrum_opts),
+            _ => panic!("unexpected subcommand"),
+        };
+        let result = handle_ext_reserves_subcommand(
+            Network::Bitcoin,
+            message,
+            psbt,
+            confirmations,
+            addresses,
+            electrum_opts,
+        )
+        .unwrap();
+        let spendable = result
+            .as_object()
+            .unwrap()
+            .get("spendable")
+            .unwrap()
+            .as_u64()
+            .unwrap();
+        assert!(spendable > 0);
+    }
+
+    #[cfg(feature = "repl")]
+    #[test]
+    fn test_regex_double_quotes() {
+        let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX).unwrap();
+        let line = r#"restore -m "word1 word2 word3" -p 'test! 123 -test' "#;
+        let split_line: Vec<&str> = split_regex
+            .captures_iter(&line)
+            .map(|c| {
+                c.get(1)
+                    .or_else(|| c.get(2))
+                    .or_else(|| c.get(3))
+                    .unwrap()
+                    .as_str()
+            })
+            .collect();
+        assert_eq!(
+            vec!(
+                "restore",
+                "-m",
+                "word1 word2 word3",
+                "-p",
+                "test! 123 -test"
+            ),
+            split_line
+        );
+    }
+
+    #[cfg(feature = "repl")]
+    #[test]
+    fn test_regex_single_quotes() {
+        let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX).unwrap();
+        let line = r#"restore -m 'word1 word2 word3' -p "test *123 -test" "#;
+        let split_line: Vec<&str> = split_regex
+            .captures_iter(&line)
+            .map(|c| {
+                c.get(1)
+                    .or_else(|| c.get(2))
+                    .or_else(|| c.get(3))
+                    .unwrap()
+                    .as_str()
+            })
+            .collect();
+        assert_eq!(
+            vec!(
+                "restore",
+                "-m",
+                "word1 word2 word3",
+                "-p",
+                "test *123 -test"
+            ),
+            split_line
+        );
+    }
+}
diff --git a/src/handlers.rs b/src/handlers.rs
new file mode 100644 (file)
index 0000000..4452997
--- /dev/null
@@ -0,0 +1,710 @@
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Command Handlers
+//!
+//! This module describes all the command handling logic used by bdk-cli
+
+use std::collections::BTreeMap;
+
+use crate::commands::OfflineWalletSubCommand::*;
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+use crate::commands::OnlineWalletSubCommand::*;
+use crate::commands::*;
+use crate::utils::*;
+use crate::Backend;
+use bdk::{database::BatchDatabase, wallet::AddressIndex, Error, FeeRate, KeychainKind, Wallet};
+
+use structopt::StructOpt;
+
+use bdk::bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+use bdk::bitcoin::hashes::hex::FromHex;
+use bdk::bitcoin::secp256k1::Secp256k1;
+use bdk::bitcoin::util::bip32::{DerivationPath, KeySource};
+use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
+use bdk::bitcoin::{Network, Txid};
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+use bdk::blockchain::{log_progress, Blockchain};
+use bdk::descriptor::Segwitv0;
+#[cfg(feature = "compiler")]
+use bdk::descriptor::{Descriptor, Legacy, Miniscript};
+#[cfg(all(feature = "reserves", feature = "electrum"))]
+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::miniscript::miniscript;
+#[cfg(feature = "compiler")]
+use bdk::miniscript::policy::Concrete;
+use bdk::SignOptions;
+#[cfg(all(feature = "reserves", feature = "electrum"))]
+use bdk::{
+    bitcoin::{Address, OutPoint, TxOut},
+    blockchain::Capability,
+};
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+use bdk_macros::{maybe_async, maybe_await};
+#[cfg(feature = "reserves")]
+use bdk_reserves::reserves::verify_proof;
+#[cfg(feature = "reserves")]
+use bdk_reserves::reserves::ProofOfReserves;
+#[cfg(feature = "repl")]
+use regex::Regex;
+#[cfg(feature = "repl")]
+use rustyline::error::ReadlineError;
+#[cfg(feature = "repl")]
+use rustyline::Editor;
+use serde_json::json;
+use std::str::FromStr;
+
+/// Execute an offline wallet sub-command
+///
+/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
+pub fn handle_offline_wallet_subcommand<D>(
+    wallet: &Wallet<D>,
+    wallet_opts: &WalletOpts,
+    offline_subcommand: OfflineWalletSubCommand,
+) -> Result<serde_json::Value, Error>
+where
+    D: BatchDatabase,
+{
+    match offline_subcommand {
+        GetNewAddress => {
+            let addr = wallet.get_address(AddressIndex::New)?;
+            if wallet_opts.verbose {
+                Ok(json!({
+                    "address": addr.address,
+                    "index": addr.index
+                }))
+            } else {
+                Ok(json!({
+                    "address": addr.address,
+                }))
+            }
+        }
+        ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?),
+        ListTransactions => Ok(serde_json::to_value(
+            &wallet.list_transactions(wallet_opts.verbose)?,
+        )?),
+        GetBalance => Ok(json!({"satoshi": wallet.get_balance()?})),
+        CreateTx {
+            recipients,
+            send_all,
+            enable_rbf,
+            offline_signer,
+            utxos,
+            unspendable,
+            fee_rate,
+            external_policy,
+            internal_policy,
+        } => {
+            let mut tx_builder = wallet.build_tx();
+
+            if send_all {
+                tx_builder.drain_wallet().drain_to(recipients[0].0.clone());
+            } else {
+                tx_builder.set_recipients(recipients);
+            }
+
+            if enable_rbf {
+                tx_builder.enable_rbf();
+            }
+
+            if offline_signer {
+                tx_builder.include_output_redeem_witness_script();
+            }
+
+            if let Some(fee_rate) = fee_rate {
+                tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
+            }
+
+            if let Some(utxos) = utxos {
+                tx_builder.add_utxos(&utxos[..])?.manually_selected_only();
+            }
+
+            if let Some(unspendable) = unspendable {
+                tx_builder.unspendable(unspendable);
+            }
+
+            let policies = vec![
+                external_policy.map(|p| (p, KeychainKind::External)),
+                internal_policy.map(|p| (p, KeychainKind::Internal)),
+            ];
+
+            for (policy, keychain) in policies.into_iter().flatten() {
+                let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)
+                    .map_err(|s| Error::Generic(s.to_string()))?;
+                tx_builder.policy_path(policy, keychain);
+            }
+
+            let (psbt, details) = tx_builder.finish()?;
+            if wallet_opts.verbose {
+                Ok(
+                    json!({"psbt": base64::encode(&serialize(&psbt)),"details": details, "serialized_psbt": psbt}),
+                )
+            } else {
+                Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details}))
+            }
+        }
+        BumpFee {
+            txid,
+            shrink_address,
+            offline_signer,
+            utxos,
+            unspendable,
+            fee_rate,
+        } => {
+            let txid = Txid::from_str(txid.as_str()).map_err(|s| Error::Generic(s.to_string()))?;
+
+            let mut tx_builder = wallet.build_fee_bump(txid)?;
+            tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
+
+            if let Some(address) = shrink_address {
+                let script_pubkey = address.script_pubkey();
+                tx_builder.allow_shrinking(script_pubkey)?;
+            }
+
+            if offline_signer {
+                tx_builder.include_output_redeem_witness_script();
+            }
+
+            if let Some(utxos) = utxos {
+                tx_builder.add_utxos(&utxos[..])?;
+            }
+
+            if let Some(unspendable) = unspendable {
+                tx_builder.unspendable(unspendable);
+            }
+
+            let (psbt, details) = tx_builder.finish()?;
+            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
+        }
+        Policies => Ok(json!({
+            "external": wallet.policies(KeychainKind::External)?,
+            "internal": wallet.policies(KeychainKind::Internal)?,
+        })),
+        PublicDescriptor => Ok(json!({
+            "external": wallet.public_descriptor(KeychainKind::External)?.map(|d| d.to_string()),
+            "internal": wallet.public_descriptor(KeychainKind::Internal)?.map(|d| d.to_string()),
+        })),
+        Sign {
+            psbt,
+            assume_height,
+            trust_witness_utxo,
+        } => {
+            let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
+            let mut psbt: PartiallySignedTransaction = deserialize(&psbt)?;
+            let signopt = SignOptions {
+                assume_height,
+                trust_witness_utxo: trust_witness_utxo.unwrap_or(false),
+                ..Default::default()
+            };
+            let finalized = wallet.sign(&mut psbt, signopt)?;
+            if wallet_opts.verbose {
+                Ok(
+                    json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized, "serialized_psbt": psbt}),
+                )
+            } else {
+                Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
+            }
+        }
+        ExtractPsbt { psbt } => {
+            let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
+            let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
+            Ok(json!({"raw_tx": serialize_hex(&psbt.extract_tx()),}))
+        }
+        FinalizePsbt {
+            psbt,
+            assume_height,
+            trust_witness_utxo,
+        } => {
+            let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
+            let mut psbt: PartiallySignedTransaction = deserialize(&psbt)?;
+
+            let signopt = SignOptions {
+                assume_height,
+                trust_witness_utxo: trust_witness_utxo.unwrap_or(false),
+                ..Default::default()
+            };
+            let finalized = wallet.finalize_psbt(&mut psbt, signopt)?;
+            if wallet_opts.verbose {
+                Ok(
+                    json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized, "serialized_psbt": psbt}),
+                )
+            } else {
+                Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
+            }
+        }
+        CombinePsbt { psbt } => {
+            let mut psbts = psbt
+                .iter()
+                .map(|s| {
+                    let psbt = base64::decode(&s).map_err(|e| Error::Generic(e.to_string()))?;
+                    let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
+                    Ok(psbt)
+                })
+                .collect::<Result<Vec<_>, Error>>()?;
+
+            let init_psbt = psbts
+                .pop()
+                .ok_or_else(|| Error::Generic("Invalid PSBT input".to_string()))?;
+            let final_psbt = psbts
+                .into_iter()
+                .try_fold::<_, _, Result<PartiallySignedTransaction, Error>>(
+                    init_psbt,
+                    |mut acc, x| {
+                        acc.combine(x)?;
+                        Ok(acc)
+                    },
+                )?;
+            Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) }))
+        }
+    }
+}
+
+/// Execute an online wallet sub-command
+///
+/// Online wallet sub-commands are described in [`OnlineWalletSubCommand`].
+#[maybe_async]
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+pub fn handle_online_wallet_subcommand<B, D>(
+    wallet: &Wallet<D>,
+    blockchain: &B,
+    online_subcommand: OnlineWalletSubCommand,
+) -> Result<serde_json::Value, Error>
+where
+    B: Blockchain,
+    D: BatchDatabase,
+{
+    use bdk::SyncOptions;
+
+    match online_subcommand {
+        Sync => {
+            maybe_await!(wallet.sync(
+                blockchain,
+                SyncOptions {
+                    progress: Some(Box::new(log_progress())),
+                }
+            ))?;
+            Ok(json!({}))
+        }
+        Broadcast { psbt, tx } => {
+            let tx = match (psbt, tx) {
+                (Some(psbt), None) => {
+                    let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
+                    let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
+                    psbt.extract_tx()
+                }
+                (None, Some(tx)) => deserialize(&Vec::<u8>::from_hex(&tx)?)?,
+                (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"),
+                (None, None) => panic!("Missing `psbt` and `tx` option"),
+            };
+            maybe_await!(blockchain.broadcast(&tx))?;
+            Ok(json!({ "txid": tx.txid() }))
+        }
+        #[cfg(feature = "reserves")]
+        ProduceProof { msg } => {
+            let mut psbt = maybe_await!(wallet.create_proof(&msg))?;
+
+            let _finalized = wallet.sign(
+                &mut psbt,
+                SignOptions {
+                    trust_witness_utxo: true,
+                    ..Default::default()
+                },
+            )?;
+
+            let psbt_ser = serialize(&psbt);
+            let psbt_b64 = base64::encode(&psbt_ser);
+
+            Ok(json!({ "psbt": psbt , "psbt_base64" : psbt_b64}))
+        }
+        #[cfg(feature = "reserves")]
+        VerifyProof {
+            psbt,
+            msg,
+            confirmations,
+        } => {
+            let psbt = base64::decode(&psbt).unwrap();
+            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+            let current_height = blockchain.get_height()?;
+            let max_confirmation_height = if confirmations == 0 {
+                None
+            } else {
+                if !blockchain
+                    .get_capabilities()
+                    .contains(&Capability::GetAnyTx)
+                {
+                    return Err(Error::Generic(
+                        "For validating a proof with a certain number of confirmations, we need a Blockchain with the GetAnyTx capability."
+                        .to_string()
+                    ));
+                }
+                Some(current_height - confirmations)
+            };
+
+            let spendable =
+                maybe_await!(wallet.verify_proof(&psbt, &msg, max_confirmation_height))?;
+            Ok(json!({ "spendable": spendable }))
+        }
+    }
+}
+
+/// Execute a key sub-command
+///
+/// Key sub-commands are described in [`KeySubCommand`].
+pub fn handle_key_subcommand(
+    network: Network,
+    subcommand: KeySubCommand,
+) -> Result<serde_json::Value, Error> {
+    let secp = Secp256k1::new();
+
+    match subcommand {
+        KeySubCommand::Generate {
+            word_count,
+            password,
+        } => {
+            let mnemonic_type = match word_count {
+                12 => WordCount::Words12,
+                _ => WordCount::Words24,
+            };
+            let mnemonic: GeneratedKey<_, miniscript::BareCtx> =
+                Mnemonic::generate((mnemonic_type, Language::English))
+                    .map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?;
+            let mnemonic = mnemonic.into_key();
+            let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?;
+            let xprv = xkey.into_xprv(network).ok_or_else(|| {
+                Error::Generic("Privatekey info not found (should not happen)".to_string())
+            })?;
+            let fingerprint = xprv.fingerprint(&secp);
+            let phrase = mnemonic
+                .word_iter()
+                .fold("".to_string(), |phrase, w| phrase + w + " ")
+                .trim()
+                .to_string();
+            Ok(
+                json!({ "mnemonic": phrase, "xprv": xprv.to_string(), "fingerprint": fingerprint.to_string() }),
+            )
+        }
+        KeySubCommand::Restore { mnemonic, password } => {
+            let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)
+                .map_err(|e| Error::Generic(e.to_string()))?;
+            let xkey: ExtendedKey = (mnemonic, password).into_extended_key()?;
+            let xprv = xkey.into_xprv(network).ok_or_else(|| {
+                Error::Generic("Privatekey info not found (should not happen)".to_string())
+            })?;
+            let fingerprint = xprv.fingerprint(&secp);
+
+            Ok(json!({ "xprv": xprv.to_string(), "fingerprint": fingerprint.to_string() }))
+        }
+        KeySubCommand::Derive { xprv, path } => {
+            if xprv.network != network {
+                return Err(Error::Key(InvalidNetwork));
+            }
+            let derived_xprv = &xprv.derive_priv(&secp, &path)?;
+
+            let origin: KeySource = (xprv.fingerprint(&secp), path);
+
+            let derived_xprv_desc_key: DescriptorKey<Segwitv0> =
+                derived_xprv.into_descriptor_key(Some(origin), DerivationPath::default())?;
+
+            if let Secret(desc_seckey, _, _) = derived_xprv_desc_key {
+                let desc_pubkey = desc_seckey
+                    .as_public(&secp)
+                    .map_err(|e| Error::Generic(e.to_string()))?;
+                Ok(json!({"xpub": desc_pubkey.to_string(), "xprv": desc_seckey.to_string()}))
+            } else {
+                Err(Error::Key(Message("Invalid key variant".to_string())))
+            }
+        }
+    }
+}
+
+/// Execute the miniscript compiler sub-command
+///
+/// Compiler options are described in [`CliSubCommand::Compile`].
+#[cfg(feature = "compiler")]
+pub fn handle_compile_subcommand(
+    _network: Network,
+    policy: String,
+    script_type: String,
+) -> Result<serde_json::Value, Error> {
+    let policy = Concrete::<String>::from_str(policy.as_str())?;
+    let legacy_policy: Miniscript<String, Legacy> = policy
+        .compile()
+        .map_err(|e| Error::Generic(e.to_string()))?;
+    let segwit_policy: Miniscript<String, Segwitv0> = policy
+        .compile()
+        .map_err(|e| Error::Generic(e.to_string()))?;
+
+    let descriptor = match script_type.as_str() {
+        "sh" => Descriptor::new_sh(legacy_policy),
+        "wsh" => Descriptor::new_wsh(segwit_policy),
+        "sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
+        _ => panic!("Invalid type"),
+    }
+    .map_err(Error::Miniscript)?;
+
+    Ok(json!({"descriptor": descriptor.to_string()}))
+}
+
+/// Proof of reserves verification sub-command
+///
+/// Proof of reserves options are described in [`CliSubCommand::ExternalReserves`].
+#[cfg(all(feature = "reserves", feature = "electrum"))]
+pub fn handle_ext_reserves_subcommand(
+    network: Network,
+    message: String,
+    psbt: String,
+    confirmations: usize,
+    addresses: Vec<String>,
+    electrum_opts: ElectrumOpts,
+) -> Result<serde_json::Value, Error> {
+    let psbt = base64::decode(&psbt)
+        .map_err(|e| Error::Generic(format!("Base64 decode error: {:?}", e)))?;
+    let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
+    let client = Client::new(&electrum_opts.server)?;
+
+    let current_block_height = client.block_headers_subscribe().map(|data| data.height)?;
+    let max_confirmation_height = Some(current_block_height - confirmations);
+
+    let outpoints_per_addr = addresses
+        .iter()
+        .map(|address| {
+            let address = Address::from_str(&address)
+                .map_err(|e| Error::Generic(format!("Invalid address: {:?}", e)))?;
+            get_outpoints_for_address(address, &client, max_confirmation_height)
+        })
+        .collect::<Result<Vec<Vec<_>>, Error>>()?;
+    let outpoints_combined = outpoints_per_addr
+        .iter()
+        .fold(Vec::new(), |mut outpoints, outs| {
+            outpoints.append(&mut outs.clone());
+            outpoints
+        });
+
+    let spendable = verify_proof(&psbt, &message, outpoints_combined, network)
+        .map_err(|e| Error::Generic(format!("{:?}", e)))?;
+
+    Ok(json!({ "spendable": spendable }))
+}
+
+#[cfg(all(feature = "reserves", feature = "electrum"))]
+pub fn get_outpoints_for_address(
+    address: Address,
+    client: &Client,
+    max_confirmation_height: Option<usize>,
+) -> Result<Vec<(OutPoint, TxOut)>, Error> {
+    let unspents = client
+        .script_list_unspent(&address.script_pubkey())
+        .map_err(Error::Electrum)?;
+
+    unspents
+        .iter()
+        .filter(|utxo| {
+            utxo.height > 0 && utxo.height <= max_confirmation_height.unwrap_or(usize::MAX)
+        })
+        .map(|utxo| {
+            let tx = match client.transaction_get(&utxo.tx_hash) {
+                Ok(tx) => tx,
+                Err(e) => {
+                    return Err(e).map_err(Error::Electrum);
+                }
+            };
+
+            Ok((
+                OutPoint {
+                    txid: utxo.tx_hash,
+                    vout: utxo.tx_pos as u32,
+                },
+                tx.output[utxo.tx_pos].clone(),
+            ))
+        })
+        .collect()
+}
+
+pub fn handle_command(
+    cli_opts: CliOpts,
+    network: Network,
+    _backend: Backend,
+) -> Result<String, Error> {
+    let result = match cli_opts.subcommand {
+        #[cfg(any(
+            feature = "electrum",
+            feature = "esplora",
+            feature = "compact_filters",
+            feature = "rpc"
+        ))]
+        CliSubCommand::Wallet {
+            wallet_opts,
+            subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
+        } => {
+            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 wallet = new_wallet(network, &wallet_opts, database)?;
+            let result = handle_online_wallet_subcommand(&wallet, &blockchain, online_subcommand)?;
+            serde_json::to_string_pretty(&result)?
+        }
+        CliSubCommand::Wallet {
+            wallet_opts,
+            subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand),
+        } => {
+            let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?;
+            let database = open_database(&wallet_opts)?;
+            let wallet = new_wallet(network, &wallet_opts, database)?;
+            let result =
+                handle_offline_wallet_subcommand(&wallet, &wallet_opts, offline_subcommand)?;
+            serde_json::to_string_pretty(&result)?
+        }
+        CliSubCommand::Key {
+            subcommand: key_subcommand,
+        } => {
+            let result = handle_key_subcommand(network, key_subcommand)?;
+            serde_json::to_string_pretty(&result)?
+        }
+        #[cfg(feature = "compiler")]
+        CliSubCommand::Compile {
+            policy,
+            script_type,
+        } => {
+            let result = handle_compile_subcommand(network, policy, script_type)?;
+            serde_json::to_string_pretty(&result)?
+        }
+        #[cfg(feature = "repl")]
+        CliSubCommand::Repl { wallet_opts } => {
+            let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?;
+            let database = open_database(&wallet_opts)?;
+
+            let wallet = new_wallet(network, &wallet_opts, database)?;
+
+            let mut rl = Editor::<()>::new();
+
+            // if rl.load_history("history.txt").is_err() {
+            //     println!("No previous history.");
+            // }
+
+            let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX)
+                .map_err(|e| Error::Generic(e.to_string()))?;
+
+            loop {
+                let readline = rl.readline(">> ");
+                match readline {
+                    Ok(line) => {
+                        if line.trim() == "" {
+                            continue;
+                        }
+                        rl.add_history_entry(line.as_str());
+                        let split_line: Vec<&str> = split_regex
+                            .captures_iter(&line)
+                            .map(|c| {
+                                Ok(c.get(1)
+                                    .or_else(|| c.get(2))
+                                    .or_else(|| c.get(3))
+                                    .ok_or_else(|| Error::Generic("Invalid commands".to_string()))?
+                                    .as_str())
+                            })
+                            .collect::<Result<Vec<_>, Error>>()?;
+                        let repl_subcommand = ReplSubCommand::from_iter_safe(split_line);
+                        if let Err(err) = repl_subcommand {
+                            println!("{}", err);
+                            continue;
+                        }
+                        // if error will be printed above
+                        let repl_subcommand = repl_subcommand.unwrap();
+                        log::debug!("repl_subcommand = {:?}", repl_subcommand);
+
+                        let result = match repl_subcommand {
+                            #[cfg(any(
+                                feature = "electrum",
+                                feature = "esplora",
+                                feature = "compact_filters",
+                                feature = "rpc"
+                            ))]
+                            ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => {
+                                let blockchain = new_blockchain(network, &wallet_opts, &_backend)?;
+                                handle_online_wallet_subcommand(
+                                    &wallet,
+                                    &blockchain,
+                                    online_subcommand,
+                                )
+                            }
+                            ReplSubCommand::OfflineWalletSubCommand(offline_subcommand) => {
+                                handle_offline_wallet_subcommand(
+                                    &wallet,
+                                    &wallet_opts,
+                                    offline_subcommand,
+                                )
+                            }
+                            ReplSubCommand::KeySubCommand(key_subcommand) => {
+                                handle_key_subcommand(network, key_subcommand)
+                            }
+                            ReplSubCommand::Exit => break,
+                        };
+
+                        println!("{}", serde_json::to_string_pretty(&result?)?);
+                    }
+                    Err(ReadlineError::Interrupted) => continue,
+                    Err(ReadlineError::Eof) => break,
+                    Err(err) => {
+                        println!("{:?}", err);
+                        break;
+                    }
+                }
+            }
+
+            "Exiting REPL".to_string()
+        }
+        #[cfg(all(feature = "reserves", feature = "electrum"))]
+        CliSubCommand::ExternalReserves {
+            message,
+            psbt,
+            confirmations,
+            addresses,
+            electrum_opts,
+        } => {
+            let result = handle_ext_reserves_subcommand(
+                network,
+                message,
+                psbt,
+                confirmations,
+                addresses,
+                electrum_opts,
+            )?;
+            serde_json::to_string_pretty(&result)?
+        }
+    };
+    Ok(result)
+}
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644 (file)
index 108fe12..0000000
+++ /dev/null
@@ -1,2436 +0,0 @@
-// Bitcoin Dev Kit
-// Written in 2020 by
-//     Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-//! BDK command line interface
-//!
-//! This lib provides [`structopt`] structs and enums that parse CLI options and sub-commands from
-//! the command line or from a `String` vector that can be used to access features of the [`bdk`]
-//! library. Functions are also provided to handle subcommands and options and provide results via
-//! the [`bdk`] lib.
-//!
-//! See the [`bdk-cli`] example bin for how to use this lib to create a simple command line
-//! application that demonstrates [`bdk`] wallet and key management features.
-//!
-//! See [`CliOpts`] for global cli options and [`CliSubCommand`] for supported top level sub-commands.
-//!
-//! [`structopt`]: https://docs.rs/crate/structopt
-//! [`bdk`]: https://github.com/bitcoindevkit/bdk
-//! [`bdk-cli`]: https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdk_cli.rs
-//!
-//! # Example
-//!
-//! ```no_run
-//! # #[cfg(feature = "electrum")]
-//! # {
-//! # use bdk::bitcoin::Network;
-//! # use bdk::blockchain::{AnyBlockchain, ConfigurableBlockchain};
-//! # use bdk::blockchain::{AnyBlockchainConfig, ElectrumBlockchainConfig};
-//! # use bdk_cli::{self, CliOpts, CliSubCommand, WalletOpts, OfflineWalletSubCommand, WalletSubCommand};
-//! # use bdk::database::MemoryDatabase;
-//! # use bdk::Wallet;
-//! # use std::sync::Arc;
-//! # use structopt::StructOpt;
-//! # use std::str::FromStr;
-//!
-//! // to get args from cli use:
-//! // let cli_opts = CliOpts::from_args();
-//!
-//! let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", "--descriptor",
-//!                     "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
-//!                     "sync"];
-//!
-//! let cli_opts = CliOpts::from_iter(&cli_args);
-//! let network = cli_opts.network;
-//!
-//! if let CliSubCommand::Wallet {
-//!         wallet_opts,
-//!         subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand)
-//!     } = cli_opts.subcommand {
-//!
-//!     let descriptor = wallet_opts.descriptor.as_str();
-//!     let change_descriptor = wallet_opts.change_descriptor.as_deref();
-//!
-//!     let database = MemoryDatabase::new();
-//!
-//!     let blockchain_config = AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
-//!         url: wallet_opts.electrum_opts.server.clone(),
-//!         socks5: wallet_opts.proxy_opts.proxy.clone(),
-//!         retry: wallet_opts.proxy_opts.retries,
-//!         timeout: wallet_opts.electrum_opts.timeout,
-//!         stop_gap: wallet_opts.electrum_opts.stop_gap,
-//!     });
-//!     let blockchain = AnyBlockchain::from_config(&blockchain_config).expect("blockchain");
-//!
-//!     let wallet = Wallet::new(
-//!         descriptor,
-//!         change_descriptor,
-//!         network,
-//!         database,
-//!     ).expect("wallet");
-//!
-//!     let result = bdk_cli::handle_online_wallet_subcommand(&wallet, &blockchain, online_subcommand).expect("result");
-//!     println!("{}", serde_json::to_string_pretty(&result).unwrap());
-//! }
-//! # }
-//! ```
-
-pub extern crate bdk;
-#[macro_use]
-extern crate serde_json;
-
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-#[macro_use]
-extern crate bdk_macros;
-
-use std::collections::BTreeMap;
-use std::str::FromStr;
-
-pub use structopt;
-use structopt::StructOpt;
-
-use crate::OfflineWalletSubCommand::*;
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-use crate::OnlineWalletSubCommand::*;
-#[cfg(all(feature = "reserves", feature = "electrum"))]
-use bdk::bitcoin::blockdata::transaction::TxOut;
-use bdk::bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-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;
-use bdk::bitcoin::{Address, Network, OutPoint, Script, Txid};
-#[cfg(all(
-    feature = "reserves",
-    any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "compact_filters",
-        feature = "rpc"
-    )
-))]
-use bdk::blockchain::Capability;
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-use bdk::blockchain::{log_progress, Blockchain};
-use bdk::database::BatchDatabase;
-use bdk::descriptor::Segwitv0;
-#[cfg(feature = "compiler")]
-use bdk::descriptor::{Descriptor, Legacy, Miniscript};
-#[cfg(all(feature = "reserves", feature = "electrum"))]
-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::miniscript::miniscript;
-#[cfg(feature = "compiler")]
-use bdk::miniscript::policy::Concrete;
-use bdk::wallet::AddressIndex;
-use bdk::Error;
-use bdk::SignOptions;
-use bdk::{FeeRate, KeychainKind, Wallet};
-#[cfg(all(feature = "reserves", feature = "electrum"))]
-use bdk_reserves::reserves::verify_proof;
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-#[cfg(feature = "reserves")]
-use bdk_reserves::reserves::ProofOfReserves;
-
-/// Global options
-///
-/// The global options and top level sub-command required for all subsequent [`CliSubCommand`]'s.
-///
-/// # Example
-///
-/// ```
-/// # #[cfg(any(feature = "electrum", feature = "esplora", feature = "compact_filters", feature = "rpc"))]
-/// # {
-/// # use bdk::bitcoin::Network;
-/// # use structopt::StructOpt;
-/// # use bdk_cli::{CliOpts, WalletOpts, CliSubCommand, WalletSubCommand};
-/// # #[cfg(feature = "electrum")]
-/// # use bdk_cli::ElectrumOpts;
-/// # #[cfg(feature = "esplora")]
-/// # use bdk_cli::EsploraOpts;
-/// # #[cfg(feature = "rpc")]
-/// # use bdk_cli::RpcOpts;
-/// # #[cfg(feature = "compact_filters")]
-/// # use bdk_cli::CompactFilterOpts;
-/// # #[cfg(any(feature = "compact_filters", feature = "electrum", feature="esplora"))]
-/// # use bdk_cli::ProxyOpts;
-/// # use bdk_cli::OnlineWalletSubCommand::Sync;
-///
-/// let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
-///                     "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)",
-///                     "sync"];
-///
-/// // to get CliOpts from the OS command line args use:
-/// // let cli_opts = CliOpts::from_args();
-/// let cli_opts = CliOpts::from_iter(&cli_args);
-///
-/// let expected_cli_opts = CliOpts {
-///             network: Network::Testnet,
-///             subcommand: CliSubCommand::Wallet {
-///                 wallet_opts: WalletOpts {
-///                     wallet: None,
-///                     verbose: false,
-///                     descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(),
-///                     change_descriptor: None,
-///               #[cfg(feature = "electrum")]
-///               electrum_opts: ElectrumOpts {
-///                   timeout: None,
-///                   server: "ssl://electrum.blockstream.info:60002".to_string(),
-///                   stop_gap: 10
-///               },
-///               #[cfg(feature = "esplora")]
-///               esplora_opts: EsploraOpts {
-///                   server: "https://blockstream.info/testnet/api/".to_string(),
-///                   timeout: 5,
-///                   stop_gap: 10,
-///                   conc: 4
-///               },
-///                 #[cfg(feature = "rpc")]
-///                 rpc_opts: RpcOpts{
-///                    address: "127.0.0.1:18443".to_string(),
-///                    basic_auth: ("user".to_string(), "password".to_string()),
-///                    skip_blocks: None,
-///                    cookie: None,
-///                },
-///                #[cfg(feature = "compact_filters")]
-///                compactfilter_opts: CompactFilterOpts{
-///                    address: vec!["127.0.0.1:18444".to_string()],
-///                    conn_count: 4,
-///                    skip_blocks: 0,
-///                },
-///                #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
-///                    proxy_opts: ProxyOpts{
-///                        proxy: None,
-///                        proxy_auth: None,
-///                        retries: 5,
-///                    },
-///                 },
-///                 subcommand: WalletSubCommand::OnlineWalletSubCommand(Sync),
-///             },
-///         };
-///
-/// assert_eq!(expected_cli_opts, cli_opts);
-/// # }
-/// ```
-
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(name = "BDK CLI",
-version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
-author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
-pub struct CliOpts {
-    /// Sets the network
-    #[structopt(
-        name = "NETWORK",
-        short = "n",
-        long = "network",
-        default_value = "testnet"
-    )]
-    pub network: Network,
-    /// Top level cli sub-command
-    #[structopt(subcommand)]
-    pub subcommand: CliSubCommand,
-}
-
-/// CLI sub-commands
-///
-/// The top level sub-commands, each may have different required options. For
-/// instance [`CliSubCommand::Wallet`] requires [`WalletOpts`] with a required descriptor but
-/// [`CliSubCommand::Key`] sub-command does not. [`CliSubCommand::Repl`] also requires
-/// [`WalletOpts`] and a descriptor because in this mode both [`WalletSubCommand`] and
-/// [`KeySubCommand`] sub-commands are available.
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(
-    rename_all = "snake",
-    long_about = "Top level options and command modes"
-)]
-pub enum CliSubCommand {
-    /// Wallet options and sub-commands
-    #[structopt(long_about = "Wallet mode")]
-    Wallet {
-        #[structopt(flatten)]
-        wallet_opts: WalletOpts,
-        #[structopt(subcommand)]
-        subcommand: WalletSubCommand,
-    },
-    /// Key management sub-commands
-    #[structopt(long_about = "Key management mode")]
-    Key {
-        #[structopt(subcommand)]
-        subcommand: KeySubCommand,
-    },
-    /// Compile a miniscript policy to an output descriptor
-    #[cfg(feature = "compiler")]
-    #[structopt(long_about = "Miniscript policy compiler")]
-    Compile {
-        /// Sets the spending policy to compile
-        #[structopt(name = "POLICY", required = true, index = 1)]
-        policy: String,
-        /// Sets the script type used to embed the compiled policy
-        #[structopt(name = "TYPE", short = "t", long = "type", default_value = "wsh", possible_values = &["sh","wsh", "sh-wsh"])]
-        script_type: String,
-    },
-    /// Enter REPL command loop mode
-    #[cfg(feature = "repl")]
-    #[structopt(long_about = "REPL command loop mode")]
-    Repl {
-        #[structopt(flatten)]
-        wallet_opts: WalletOpts,
-    },
-    /// Proof of reserves external sub-commands
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    #[structopt(long_about = "Proof of reserves external verification")]
-    ExternalReserves {
-        /// Sets the challenge message with which the proof was produced
-        #[structopt(name = "MESSAGE", required = true, index = 1)]
-        message: String,
-        /// Sets the proof in form of a PSBT to verify
-        #[structopt(name = "PSBT", required = true, index = 2)]
-        psbt: String,
-        /// Sets the number of block confirmations for UTXOs to be considered.
-        #[structopt(name = "CONFIRMATIONS", required = true, index = 3)]
-        confirmations: usize,
-        /// Sets the addresses for which the proof was produced
-        #[structopt(name = "ADDRESSES", required = true, index = 4)]
-        addresses: Vec<String>,
-        #[structopt(flatten)]
-        electrum_opts: ElectrumOpts,
-    },
-}
-
-#[cfg_attr(not(doc), allow(missing_docs))]
-#[cfg_attr(
-    doc,
-    doc = r#"
-Wallet sub-commands
-
-Can use either an online or offline wallet. An [`OnlineWalletSubCommand`] requires a blockchain
-client and network connection and an [`OfflineWalletSubCommand`] does not.
-"#
-)]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-pub enum WalletSubCommand {
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "compact_filters",
-        feature = "rpc"
-    ))]
-    #[structopt(flatten)]
-    OnlineWalletSubCommand(OnlineWalletSubCommand),
-    #[structopt(flatten)]
-    OfflineWalletSubCommand(OfflineWalletSubCommand),
-}
-
-#[cfg_attr(not(doc), allow(missing_docs))]
-#[cfg_attr(
-    doc,
-    doc = r#"
-Wallet options
-
-The wallet options required for all [`CliSubCommand::Wallet`] or [`CliSubCommand::Repl`]
-sub-commands. These options capture wallet descriptor and blockchain client information. The
-blockchain client details are only used for [`OnlineWalletSubCommand`]s.
-
-# Example
-
-```
-# use bdk::bitcoin::Network;
-# use structopt::StructOpt;
-# use bdk_cli::WalletOpts;
-# #[cfg(feature = "electrum")]
-# use bdk_cli::ElectrumOpts;
-# #[cfg(feature = "esplora")]
-# use bdk_cli::EsploraOpts;
-# #[cfg(feature = "compact_filters")]
-# use bdk_cli::CompactFilterOpts;
-# #[cfg(feature = "rpc")]
-# use bdk_cli::RpcOpts;
-# #[cfg(any(feature = "compact_filters", feature = "electrum", feature="esplora"))]
-# use bdk_cli::ProxyOpts;
-
-let cli_args = vec!["wallet",
-                     "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)"];
-
-// to get WalletOpt from OS command line args use:
-// let wallet_opt = WalletOpt::from_args();
-
-let wallet_opts = WalletOpts::from_iter(&cli_args);
-
-let expected_wallet_opts = WalletOpts {
-               wallet: None,
-                    verbose: false,
-               descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(),
-               change_descriptor: None,
-               #[cfg(feature = "electrum")]
-               electrum_opts: ElectrumOpts {
-                   timeout: None,
-                   server: "ssl://electrum.blockstream.info:60002".to_string(),
-                   stop_gap: 10
-               },
-               #[cfg(feature = "esplora")]
-               esplora_opts: EsploraOpts {
-                   server: "https://blockstream.info/testnet/api/".to_string(),
-                   timeout: 5,
-                   stop_gap: 10,
-                   conc: 4
-               },
-               #[cfg(feature = "compact_filters")]
-               compactfilter_opts: CompactFilterOpts{
-                    address: vec!["127.0.0.1:18444".to_string()],
-                    conn_count: 4,
-                    skip_blocks: 0,
-               },
-               #[cfg(feature = "rpc")]
-               rpc_opts: RpcOpts{
-                    address: "127.0.0.1:18443".to_string(),
-                    basic_auth: ("user".to_string(), "password".to_string()),
-                    skip_blocks: None,
-                    cookie: None,
-               },
-               #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-               },
- };
-
-assert_eq!(expected_wallet_opts, wallet_opts);
-```
-"#
-)]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-pub struct WalletOpts {
-    /// Selects the wallet to use
-    #[structopt(name = "WALLET_NAME", short = "w", long = "wallet")]
-    pub wallet: Option<String>,
-    /// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects
-    #[structopt(name = "VERBOSE", short = "v", long = "verbose")]
-    pub verbose: bool,
-    /// Sets the descriptor to use for the external addresses
-    #[structopt(name = "DESCRIPTOR", short = "d", long = "descriptor", required = true)]
-    pub descriptor: String,
-    /// Sets the descriptor to use for internal addresses
-    #[structopt(name = "CHANGE_DESCRIPTOR", short = "c", long = "change_descriptor")]
-    pub change_descriptor: Option<String>,
-    #[cfg(feature = "electrum")]
-    #[structopt(flatten)]
-    pub electrum_opts: ElectrumOpts,
-    #[cfg(feature = "esplora")]
-    #[structopt(flatten)]
-    pub esplora_opts: EsploraOpts,
-    #[cfg(feature = "compact_filters")]
-    #[structopt(flatten)]
-    pub compactfilter_opts: CompactFilterOpts,
-    #[cfg(feature = "rpc")]
-    #[structopt(flatten)]
-    pub rpc_opts: RpcOpts,
-    #[cfg(any(feature = "compact_filters", feature = "electrum", feature = "esplora"))]
-    #[structopt(flatten)]
-    pub proxy_opts: ProxyOpts,
-}
-
-#[cfg_attr(not(doc), allow(missing_docs))]
-#[cfg_attr(
-    doc,
-    doc = r#"
-Proxy Server options
-
-Only activated for `compact_filters` or `electrum`
-"#
-)]
-#[cfg(any(feature = "compact_filters", feature = "electrum", feature = "esplora"))]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-pub struct ProxyOpts {
-    /// Sets the SOCKS5 proxy for Blockchain backend
-    #[structopt(name = "PROXY_ADDRS:PORT", long = "proxy", short = "p")]
-    pub proxy: Option<String>,
-
-    /// Sets the SOCKS5 proxy credential
-    #[structopt(name="PROXY_USER:PASSWD", long="proxy_auth", short="a", parse(try_from_str = parse_proxy_auth))]
-    pub proxy_auth: Option<(String, String)>,
-
-    /// Sets the SOCKS5 proxy retries for the Electrum client
-    #[structopt(
-        name = "PROXY_RETRIES",
-        short = "r",
-        long = "retries",
-        default_value = "5"
-    )]
-    pub retries: u8,
-}
-
-/// Compact Filter options
-///
-/// Compact filter peer information used by [`OnlineWalletSubCommand`]s.
-#[cfg(feature = "compact_filters")]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-pub struct CompactFilterOpts {
-    /// Sets the full node network address
-    #[structopt(
-        name = "ADDRESS:PORT",
-        short = "n",
-        long = "node",
-        default_value = "127.0.0.1:18444"
-    )]
-    pub address: Vec<String>,
-
-    /// Sets the number of parallel node connections
-    #[structopt(name = "CONNECTIONS", long = "conn_count", default_value = "4")]
-    pub conn_count: usize,
-
-    /// Optionally skip initial `skip_blocks` blocks
-    #[structopt(
-        name = "SKIP_BLOCKS",
-        short = "k",
-        long = "skip_blocks",
-        default_value = "0"
-    )]
-    pub skip_blocks: usize,
-}
-
-#[cfg(feature = "rpc")]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-pub struct RpcOpts {
-    /// Sets the full node address for rpc connection
-    #[structopt(
-        name = "ADDRESS:PORT",
-        short = "n",
-        long = "node",
-        default_value = "127.0.0.1:18443"
-    )]
-    pub address: String,
-
-    /// Sets the rpc basic authentication
-    #[structopt(
-        name = "USER:PASSWD",
-        short = "a",
-        long = "basic-auth",
-        parse(try_from_str = parse_proxy_auth),
-        default_value = "user:password",
-    )]
-    pub basic_auth: (String, String),
-
-    /// Sets an optional cookie authentication
-    #[structopt(name = "COOKIE", long = "cookie")]
-    pub cookie: Option<String>,
-
-    /// Optionally skip initial `skip_blocks` blocks
-    #[structopt(name = "SKIP_BLOCKS", short = "s", long = "skip-blocks")]
-    pub skip_blocks: Option<u32>,
-}
-
-/// Electrum options
-///
-/// Electrum blockchain client information used by [`OnlineWalletSubCommand`]s.
-#[cfg(feature = "electrum")]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-pub struct ElectrumOpts {
-    /// Sets the SOCKS5 proxy timeout for the Electrum client
-    #[structopt(name = "PROXY_TIMEOUT", short = "t", long = "timeout")]
-    pub timeout: Option<u8>,
-    /// Sets the Electrum server to use
-    #[structopt(
-        name = "ELECTRUM_URL",
-        short = "s",
-        long = "server",
-        default_value = "ssl://electrum.blockstream.info:60002"
-    )]
-    pub server: String,
-
-    /// Stop searching addresses for transactions after finding an unused gap of this length.
-    #[structopt(
-        name = "STOP_GAP",
-        long = "stop_gap",
-        short = "g",
-        default_value = "10"
-    )]
-    pub stop_gap: usize,
-}
-
-#[cfg_attr(not(doc), allow(missing_docs))]
-#[cfg_attr(
-    doc,
-    doc = r#"
-Esplora options
-
-Esplora blockchain client information used by [`OnlineWalletSubCommand`]s.
-"#
-)]
-#[cfg(feature = "esplora")]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-pub struct EsploraOpts {
-    /// Use the esplora server if given as parameter
-    #[structopt(
-        name = "ESPLORA_URL",
-        short = "s",
-        long = "server",
-        default_value = "https://blockstream.info/testnet/api/"
-    )]
-    pub server: String,
-
-    /// Socket timeout
-    #[structopt(name = "TIMEOUT", long = "timeout", default_value = "5")]
-    pub timeout: u64,
-
-    /// Stop searching addresses for transactions after finding an unused gap of this length.
-    #[structopt(
-        name = "STOP_GAP",
-        long = "stop_gap",
-        short = "g",
-        default_value = "10"
-    )]
-    pub stop_gap: usize,
-
-    /// Number of parallel requests sent to the esplora service (default: 4)
-    #[structopt(name = "CONCURRENCY", long = "conc", default_value = "4")]
-    pub conc: u8,
-}
-
-// This is a workaround for `structopt` issue #333, #391, #418; see https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332
-#[cfg_attr(not(doc), allow(missing_docs))]
-#[cfg_attr(
-    doc,
-    doc = r#"
-Offline Wallet sub-command
-
-[`CliSubCommand::Wallet`] sub-commands that do not require a blockchain client and network
-connection. These sub-commands use only the provided descriptor or locally cached wallet
-information.
-
-# Example
-
-```
-# use bdk_cli::OfflineWalletSubCommand;
-# use structopt::StructOpt;
-
-let address_sub_command = OfflineWalletSubCommand::from_iter(&["wallet", "get_new_address"]);
-assert!(matches!(
-     address_sub_command,
-    OfflineWalletSubCommand::GetNewAddress
-));
-```
-
-To capture wallet sub-commands from a string vector without a preceeding binary name you can
-create a custom struct the includes the `NoBinaryName` clap setting and wraps the WalletSubCommand
-enum. See also the [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdkcli.rs)
-example app.
-"#
-)]
-#[cfg_attr(
-    all(doc, feature = "repl"),
-    doc = r#"
-
-# Example
-```
-# use bdk_cli::OfflineWalletSubCommand;
-# use structopt::StructOpt;
-# use clap::AppSettings;
-
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(name = "BDK CLI", setting = AppSettings::NoBinaryName,
-version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
-author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
-struct ReplOpts {
-    /// Wallet sub-command
-    #[structopt(subcommand)]
-    pub subcommand: OfflineWalletSubCommand,
-}
-
-let repl_opts = ReplOpts::from_iter(&["get_new_address"]);
-assert!(matches!(
-    repl_opts.subcommand,
-    OfflineWalletSubCommand::GetNewAddress
-));
-"#
-)]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(rename_all = "snake")]
-pub enum OfflineWalletSubCommand {
-    /// Generates a new external address
-    GetNewAddress,
-    /// Lists the available spendable UTXOs
-    ListUnspent,
-    /// Lists all the incoming and outgoing transactions of the wallet
-    ListTransactions,
-    /// Returns the current wallet balance
-    GetBalance,
-    /// Creates a new unsigned transaction
-    CreateTx {
-        /// Adds a recipient to the transaction
-        #[structopt(name = "ADDRESS:SAT", long = "to", required = true, parse(try_from_str = parse_recipient))]
-        recipients: Vec<(Script, u64)>,
-        /// Sends all the funds (or all the selected utxos). Requires only one recipients of value 0
-        #[structopt(short = "all", long = "send_all")]
-        send_all: bool,
-        /// Enables Replace-By-Fee (BIP125)
-        #[structopt(short = "rbf", long = "enable_rbf")]
-        enable_rbf: bool,
-        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
-        #[structopt(long = "offline_signer")]
-        offline_signer: bool,
-        /// Selects which utxos *must* be spent
-        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
-        utxos: Option<Vec<OutPoint>>,
-        /// Marks a utxo as unspendable
-        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
-        unspendable: Option<Vec<OutPoint>>,
-        /// Fee rate to use in sat/vbyte
-        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
-        fee_rate: Option<f32>,
-        /// Selects which policy should be used to satisfy the external descriptor
-        #[structopt(name = "EXT_POLICY", long = "external_policy")]
-        external_policy: Option<String>,
-        /// Selects which policy should be used to satisfy the internal descriptor
-        #[structopt(name = "INT_POLICY", long = "internal_policy")]
-        internal_policy: Option<String>,
-    },
-    /// Bumps the fees of an RBF transaction
-    BumpFee {
-        /// TXID of the transaction to update
-        #[structopt(name = "TXID", short = "txid", long = "txid")]
-        txid: String,
-        /// Allows the wallet to reduce the amount to the specified address in order to increase fees.
-        #[structopt(name = "SHRINK_ADDRESS", short = "s", long = "shrink")]
-        shrink_address: Option<Address>,
-        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
-        #[structopt(long = "offline_signer")]
-        offline_signer: bool,
-        /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used
-        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
-        utxos: Option<Vec<OutPoint>>,
-        /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees
-        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
-        unspendable: Option<Vec<OutPoint>>,
-        /// The new targeted fee rate in sat/vbyte
-        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
-        fee_rate: f32,
-    },
-    /// Returns the available spending policies for the descriptor
-    Policies,
-    /// Returns the public version of the wallet's descriptor(s)
-    PublicDescriptor,
-    /// Signs and tries to finalize a PSBT
-    Sign {
-        /// Sets the PSBT to sign
-        #[structopt(name = "BASE64_PSBT", long = "psbt")]
-        psbt: String,
-        /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor
-        #[structopt(name = "HEIGHT", long = "assume_height")]
-        assume_height: Option<u32>,
-        /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided
-        #[structopt(name = "WITNESS", long = "trust_witness_utxo")]
-        trust_witness_utxo: Option<bool>,
-    },
-    /// Extracts a raw transaction from a PSBT
-    ExtractPsbt {
-        /// Sets the PSBT to extract
-        #[structopt(name = "BASE64_PSBT", long = "psbt")]
-        psbt: String,
-    },
-    /// Finalizes a PSBT
-    FinalizePsbt {
-        /// Sets the PSBT to finalize
-        #[structopt(name = "BASE64_PSBT", long = "psbt")]
-        psbt: String,
-        /// Assume the blockchain has reached a specific height
-        #[structopt(name = "HEIGHT", long = "assume_height")]
-        assume_height: Option<u32>,
-        /// Whether the signer should trust the witness_utxo, if the non_witness_utxo hasn’t been provided
-        #[structopt(name = "WITNESS", long = "trust_witness_utxo")]
-        trust_witness_utxo: Option<bool>,
-    },
-    /// Combines multiple PSBTs into one
-    CombinePsbt {
-        /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT
-        #[structopt(name = "BASE64_PSBT", long = "psbt", required = true)]
-        psbt: Vec<String>,
-    },
-}
-
-#[cfg_attr(not(doc), allow(missing_docs))]
-#[cfg_attr(
-    doc,
-    doc = r#"
-Online Wallet sub-command
-
-[`CliSubCommand::Wallet`] sub-commands that require a blockchain client and network connection.
-These sub-commands use a provided descriptor, locally cached wallet information, and require a
-blockchain client and network connection.
-"#
-)]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(rename_all = "snake")]
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-pub enum OnlineWalletSubCommand {
-    /// Syncs with the chosen blockchain server
-    Sync,
-    /// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract
-    Broadcast {
-        /// Sets the PSBT to sign
-        #[structopt(
-            name = "BASE64_PSBT",
-            long = "psbt",
-            required_unless = "RAWTX",
-            conflicts_with = "RAWTX"
-        )]
-        psbt: Option<String>,
-        /// Sets the raw transaction to broadcast
-        #[structopt(
-            name = "RAWTX",
-            long = "tx",
-            required_unless = "BASE64_PSBT",
-            conflicts_with = "BASE64_PSBT"
-        )]
-        tx: Option<String>,
-    },
-    /// Produce a proof of reserves
-    #[cfg(feature = "reserves")]
-    ProduceProof {
-        /// Sets the message
-        #[structopt(name = "MESSAGE", long = "message")]
-        msg: String,
-    },
-    /// Verify a proof of reserves for our wallet
-    #[cfg(feature = "reserves")]
-    VerifyProof {
-        /// Sets the PSBT to verify
-        #[structopt(name = "BASE64_PSBT", long = "psbt")]
-        psbt: String,
-        /// Sets the message to verify
-        #[structopt(name = "MESSAGE", long = "message")]
-        msg: String,
-        /// Sets the number of block confirmations for UTXOs to be considered. If nothing is specified, 6 is used.
-        #[structopt(name = "CONFIRMATIONS", long = "confirmations", default_value = "6")]
-        confirmations: u32,
-    },
-}
-
-fn parse_recipient(s: &str) -> Result<(Script, u64), String> {
-    let parts: Vec<_> = s.split(':').collect();
-    if parts.len() != 2 {
-        return Err("Invalid format".to_string());
-    }
-    let addr = Address::from_str(parts[0]).map_err(|e| e.to_string())?;
-    let val = u64::from_str(parts[1]).map_err(|e| e.to_string())?;
-
-    Ok((addr.script_pubkey(), val))
-}
-#[cfg(any(
-    feature = "electrum",
-    feature = "compact_filters",
-    feature = "esplora",
-    feature = "rpc"
-))]
-fn parse_proxy_auth(s: &str) -> Result<(String, String), String> {
-    let parts: Vec<_> = s.split(':').collect();
-    if parts.len() != 2 {
-        return Err("Invalid format".to_string());
-    }
-
-    let user = parts[0].to_string();
-    let passwd = parts[1].to_string();
-
-    Ok((user, passwd))
-}
-
-fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
-    OutPoint::from_str(s).map_err(|e| e.to_string())
-}
-
-/// Execute an offline wallet sub-command
-///
-/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`].
-pub fn handle_offline_wallet_subcommand<D>(
-    wallet: &Wallet<D>,
-    wallet_opts: &WalletOpts,
-    offline_subcommand: OfflineWalletSubCommand,
-) -> Result<serde_json::Value, Error>
-where
-    D: BatchDatabase,
-{
-    match offline_subcommand {
-        GetNewAddress => {
-            let addr = wallet.get_address(AddressIndex::New)?;
-            if wallet_opts.verbose {
-                Ok(json!({
-                    "address": addr.address,
-                    "index": addr.index
-                }))
-            } else {
-                Ok(json!({
-                    "address": addr.address,
-                }))
-            }
-        }
-        ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?),
-        ListTransactions => Ok(serde_json::to_value(
-            &wallet.list_transactions(wallet_opts.verbose)?,
-        )?),
-        GetBalance => Ok(json!({"satoshi": wallet.get_balance()?})),
-        CreateTx {
-            recipients,
-            send_all,
-            enable_rbf,
-            offline_signer,
-            utxos,
-            unspendable,
-            fee_rate,
-            external_policy,
-            internal_policy,
-        } => {
-            let mut tx_builder = wallet.build_tx();
-
-            if send_all {
-                tx_builder.drain_wallet().drain_to(recipients[0].0.clone());
-            } else {
-                tx_builder.set_recipients(recipients);
-            }
-
-            if enable_rbf {
-                tx_builder.enable_rbf();
-            }
-
-            if offline_signer {
-                tx_builder.include_output_redeem_witness_script();
-            }
-
-            if let Some(fee_rate) = fee_rate {
-                tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
-            }
-
-            if let Some(utxos) = utxos {
-                tx_builder.add_utxos(&utxos[..])?.manually_selected_only();
-            }
-
-            if let Some(unspendable) = unspendable {
-                tx_builder.unspendable(unspendable);
-            }
-
-            let policies = vec![
-                external_policy.map(|p| (p, KeychainKind::External)),
-                internal_policy.map(|p| (p, KeychainKind::Internal)),
-            ];
-
-            for (policy, keychain) in policies.into_iter().flatten() {
-                let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)
-                    .map_err(|s| Error::Generic(s.to_string()))?;
-                tx_builder.policy_path(policy, keychain);
-            }
-
-            let (psbt, details) = tx_builder.finish()?;
-            if wallet_opts.verbose {
-                Ok(
-                    json!({"psbt": base64::encode(&serialize(&psbt)),"details": details, "serialized_psbt": psbt}),
-                )
-            } else {
-                Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details}))
-            }
-        }
-        BumpFee {
-            txid,
-            shrink_address,
-            offline_signer,
-            utxos,
-            unspendable,
-            fee_rate,
-        } => {
-            let txid = Txid::from_str(txid.as_str()).map_err(|s| Error::Generic(s.to_string()))?;
-
-            let mut tx_builder = wallet.build_fee_bump(txid)?;
-            tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
-
-            if let Some(address) = shrink_address {
-                let script_pubkey = address.script_pubkey();
-                tx_builder.allow_shrinking(script_pubkey)?;
-            }
-
-            if offline_signer {
-                tx_builder.include_output_redeem_witness_script();
-            }
-
-            if let Some(utxos) = utxos {
-                tx_builder.add_utxos(&utxos[..])?;
-            }
-
-            if let Some(unspendable) = unspendable {
-                tx_builder.unspendable(unspendable);
-            }
-
-            let (psbt, details) = tx_builder.finish()?;
-            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
-        }
-        Policies => Ok(json!({
-            "external": wallet.policies(KeychainKind::External)?,
-            "internal": wallet.policies(KeychainKind::Internal)?,
-        })),
-        PublicDescriptor => Ok(json!({
-            "external": wallet.public_descriptor(KeychainKind::External)?.map(|d| d.to_string()),
-            "internal": wallet.public_descriptor(KeychainKind::Internal)?.map(|d| d.to_string()),
-        })),
-        Sign {
-            psbt,
-            assume_height,
-            trust_witness_utxo,
-        } => {
-            let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
-            let mut psbt: PartiallySignedTransaction = deserialize(&psbt)?;
-            let signopt = SignOptions {
-                assume_height,
-                trust_witness_utxo: trust_witness_utxo.unwrap_or(false),
-                ..Default::default()
-            };
-            let finalized = wallet.sign(&mut psbt, signopt)?;
-            if wallet_opts.verbose {
-                Ok(
-                    json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized, "serialized_psbt": psbt}),
-                )
-            } else {
-                Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
-            }
-        }
-        ExtractPsbt { psbt } => {
-            let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
-            let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
-            Ok(json!({"raw_tx": serialize_hex(&psbt.extract_tx()),}))
-        }
-        FinalizePsbt {
-            psbt,
-            assume_height,
-            trust_witness_utxo,
-        } => {
-            let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
-            let mut psbt: PartiallySignedTransaction = deserialize(&psbt)?;
-
-            let signopt = SignOptions {
-                assume_height,
-                trust_witness_utxo: trust_witness_utxo.unwrap_or(false),
-                ..Default::default()
-            };
-            let finalized = wallet.finalize_psbt(&mut psbt, signopt)?;
-            if wallet_opts.verbose {
-                Ok(
-                    json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized, "serialized_psbt": psbt}),
-                )
-            } else {
-                Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
-            }
-        }
-        CombinePsbt { psbt } => {
-            let mut psbts = psbt
-                .iter()
-                .map(|s| {
-                    let psbt = base64::decode(&s).map_err(|e| Error::Generic(e.to_string()))?;
-                    let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
-                    Ok(psbt)
-                })
-                .collect::<Result<Vec<_>, Error>>()?;
-
-            let init_psbt = psbts
-                .pop()
-                .ok_or_else(|| Error::Generic("Invalid PSBT input".to_string()))?;
-            let final_psbt = psbts
-                .into_iter()
-                .try_fold::<_, _, Result<PartiallySignedTransaction, Error>>(
-                    init_psbt,
-                    |mut acc, x| {
-                        acc.merge(x)?;
-                        Ok(acc)
-                    },
-                )?;
-            Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) }))
-        }
-    }
-}
-
-/// Execute an online wallet sub-command
-///
-/// Online wallet sub-commands are described in [`OnlineWalletSubCommand`]. See [`crate`] for
-/// example usage.
-#[cfg(any(
-    feature = "electrum",
-    feature = "esplora",
-    feature = "compact_filters",
-    feature = "rpc"
-))]
-#[maybe_async]
-pub fn handle_online_wallet_subcommand<B, D>(
-    wallet: &Wallet<D>,
-    blockchain: &B,
-    online_subcommand: OnlineWalletSubCommand,
-) -> Result<serde_json::Value, Error>
-where
-    B: Blockchain,
-    D: BatchDatabase,
-{
-    use bdk::SyncOptions;
-
-    match online_subcommand {
-        Sync => {
-            maybe_await!(wallet.sync(
-                blockchain,
-                SyncOptions {
-                    progress: Some(Box::new(log_progress())),
-                }
-            ))?;
-            Ok(json!({}))
-        }
-        Broadcast { psbt, tx } => {
-            let tx = match (psbt, tx) {
-                (Some(psbt), None) => {
-                    let psbt = base64::decode(&psbt).map_err(|e| Error::Generic(e.to_string()))?;
-                    let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
-                    psbt.extract_tx()
-                }
-                (None, Some(tx)) => deserialize(&Vec::<u8>::from_hex(&tx)?)?,
-                (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"),
-                (None, None) => panic!("Missing `psbt` and `tx` option"),
-            };
-            maybe_await!(blockchain.broadcast(&tx))?;
-            Ok(json!({ "txid": tx.txid() }))
-        }
-        #[cfg(feature = "reserves")]
-        ProduceProof { msg } => {
-            let mut psbt = maybe_await!(wallet.create_proof(&msg))?;
-
-            let _finalized = wallet.sign(
-                &mut psbt,
-                SignOptions {
-                    trust_witness_utxo: true,
-                    ..Default::default()
-                },
-            )?;
-
-            let psbt_ser = serialize(&psbt);
-            let psbt_b64 = base64::encode(&psbt_ser);
-
-            Ok(json!({ "psbt": psbt , "psbt_base64" : psbt_b64}))
-        }
-        #[cfg(feature = "reserves")]
-        VerifyProof {
-            psbt,
-            msg,
-            confirmations,
-        } => {
-            let psbt = base64::decode(&psbt).unwrap();
-            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-            let current_height = blockchain.get_height()?;
-            let max_confirmation_height = if confirmations == 0 {
-                None
-            } else {
-                if !blockchain
-                    .get_capabilities()
-                    .contains(&Capability::GetAnyTx)
-                {
-                    return Err(Error::Generic(
-                        "For validating a proof with a certain number of confirmations, we need a Blockchain with the GetAnyTx capability."
-                        .to_string()
-                    ));
-                }
-                Some(current_height - confirmations)
-            };
-
-            let spendable =
-                maybe_await!(wallet.verify_proof(&psbt, &msg, max_confirmation_height))?;
-            Ok(json!({ "spendable": spendable }))
-        }
-    }
-}
-
-#[cfg_attr(not(doc), allow(missing_docs))]
-#[cfg_attr(
-    doc,
-    doc = r#"
-Key sub-command
-
-Provides basic key operations that are not related to a specific wallet such as generating a
-new random master extended key or restoring a master extended key from mnemonic words.
-
-These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this
-feature to create keys that secure actual funds on the Bitcoin mainnet.
-"#
-)]
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(rename_all = "snake")]
-pub enum KeySubCommand {
-    /// Generates new random seed mnemonic phrase and corresponding master extended key
-    Generate {
-        /// Entropy level based on number of random seed mnemonic words
-        #[structopt(
-        name = "WORD_COUNT",
-        short = "e",
-        long = "entropy",
-        default_value = "24",
-        possible_values = &["12","24"],
-        )]
-        word_count: usize,
-        /// Seed password
-        #[structopt(name = "PASSWORD", short = "p", long = "password")]
-        password: Option<String>,
-    },
-    /// Restore a master extended key from seed backup mnemonic words
-    Restore {
-        /// Seed mnemonic words, must be quoted (eg. "word1 word2 ...")
-        #[structopt(name = "MNEMONIC", short = "m", long = "mnemonic")]
-        mnemonic: String,
-        /// Seed password
-        #[structopt(name = "PASSWORD", short = "p", long = "password")]
-        password: Option<String>,
-    },
-    /// Derive a child key pair from a master extended key and a derivation path string (eg. "m/84'/1'/0'/0" or "m/84h/1h/0h/0")
-    Derive {
-        /// Extended private key to derive from
-        #[structopt(name = "XPRV", short = "x", long = "xprv")]
-        xprv: ExtendedPrivKey,
-        /// Path to use to derive extended public key from extended private key
-        #[structopt(name = "PATH", short = "p", long = "path")]
-        path: DerivationPath,
-    },
-}
-
-/// Execute a key sub-command
-///
-/// Key sub-commands are described in [`KeySubCommand`].
-pub fn handle_key_subcommand(
-    network: Network,
-    subcommand: KeySubCommand,
-) -> Result<serde_json::Value, Error> {
-    let secp = Secp256k1::new();
-
-    match subcommand {
-        KeySubCommand::Generate {
-            word_count,
-            password,
-        } => {
-            let mnemonic_type = match word_count {
-                12 => WordCount::Words12,
-                _ => WordCount::Words24,
-            };
-            let mnemonic: GeneratedKey<_, miniscript::BareCtx> =
-                Mnemonic::generate((mnemonic_type, Language::English))
-                    .map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?;
-            let mnemonic = mnemonic.into_key();
-            let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?;
-            let xprv = xkey.into_xprv(network).ok_or_else(|| {
-                Error::Generic("Privatekey info not found (should not happen)".to_string())
-            })?;
-            let fingerprint = xprv.fingerprint(&secp);
-            let phrase = mnemonic
-                .word_iter()
-                .fold("".to_string(), |phrase, w| phrase + w + " ")
-                .trim()
-                .to_string();
-            Ok(
-                json!({ "mnemonic": phrase, "xprv": xprv.to_string(), "fingerprint": fingerprint.to_string() }),
-            )
-        }
-        KeySubCommand::Restore { mnemonic, password } => {
-            let mnemonic = Mnemonic::parse_in(Language::English, mnemonic)
-                .map_err(|e| Error::Generic(e.to_string()))?;
-            let xkey: ExtendedKey = (mnemonic, password).into_extended_key()?;
-            let xprv = xkey.into_xprv(network).ok_or_else(|| {
-                Error::Generic("Privatekey info not found (should not happen)".to_string())
-            })?;
-            let fingerprint = xprv.fingerprint(&secp);
-
-            Ok(json!({ "xprv": xprv.to_string(), "fingerprint": fingerprint.to_string() }))
-        }
-        KeySubCommand::Derive { xprv, path } => {
-            if xprv.network != network {
-                return Err(Error::Key(InvalidNetwork));
-            }
-            let derived_xprv = &xprv.derive_priv(&secp, &path)?;
-
-            let origin: KeySource = (xprv.fingerprint(&secp), path);
-
-            let derived_xprv_desc_key: DescriptorKey<Segwitv0> =
-                derived_xprv.into_descriptor_key(Some(origin), DerivationPath::default())?;
-
-            if let Secret(desc_seckey, _, _) = derived_xprv_desc_key {
-                let desc_pubkey = desc_seckey
-                    .as_public(&secp)
-                    .map_err(|e| Error::Generic(e.to_string()))?;
-                Ok(json!({"xpub": desc_pubkey.to_string(), "xprv": desc_seckey.to_string()}))
-            } else {
-                Err(Error::Key(Message("Invalid key variant".to_string())))
-            }
-        }
-    }
-}
-
-/// Execute the miniscript compiler sub-command
-///
-/// Compiler options are described in [`CliSubCommand::Compile`].
-#[cfg(feature = "compiler")]
-pub fn handle_compile_subcommand(
-    _network: Network,
-    policy: String,
-    script_type: String,
-) -> Result<serde_json::Value, Error> {
-    let policy = Concrete::<String>::from_str(policy.as_str())?;
-    let legacy_policy: Miniscript<String, Legacy> = policy
-        .compile()
-        .map_err(|e| Error::Generic(e.to_string()))?;
-    let segwit_policy: Miniscript<String, Segwitv0> = policy
-        .compile()
-        .map_err(|e| Error::Generic(e.to_string()))?;
-
-    let descriptor = match script_type.as_str() {
-        "sh" => Descriptor::new_sh(legacy_policy),
-        "wsh" => Descriptor::new_wsh(segwit_policy),
-        "sh-wsh" => Descriptor::new_sh_wsh(segwit_policy),
-        _ => panic!("Invalid type"),
-    }
-    .map_err(Error::Miniscript)?;
-
-    Ok(json!({"descriptor": descriptor.to_string()}))
-}
-
-/// Proof of reserves verification sub-command
-///
-/// Proof of reserves options are described in [`CliSubCommand::ExternalReserves`].
-#[cfg(all(feature = "reserves", feature = "electrum"))]
-pub fn handle_ext_reserves_subcommand(
-    network: Network,
-    message: String,
-    psbt: String,
-    confirmations: usize,
-    addresses: Vec<String>,
-    electrum_opts: ElectrumOpts,
-) -> Result<serde_json::Value, Error> {
-    let psbt = base64::decode(&psbt)
-        .map_err(|e| Error::Generic(format!("Base64 decode error: {:?}", e)))?;
-    let psbt: PartiallySignedTransaction = deserialize(&psbt)?;
-    let client = Client::new(&electrum_opts.server)?;
-
-    let current_block_height = client.block_headers_subscribe().map(|data| data.height)?;
-    let max_confirmation_height = Some(current_block_height - confirmations);
-
-    let outpoints_per_addr = addresses
-        .iter()
-        .map(|address| {
-            let address = Address::from_str(&address)
-                .map_err(|e| Error::Generic(format!("Invalid address: {:?}", e)))?;
-            get_outpoints_for_address(address, &client, max_confirmation_height)
-        })
-        .collect::<Result<Vec<Vec<_>>, Error>>()?;
-    let outpoints_combined = outpoints_per_addr
-        .iter()
-        .fold(Vec::new(), |mut outpoints, outs| {
-            outpoints.append(&mut outs.clone());
-            outpoints
-        });
-
-    let spendable = verify_proof(&psbt, &message, outpoints_combined, network)
-        .map_err(|e| Error::Generic(format!("{:?}", e)))?;
-
-    Ok(json!({ "spendable": spendable }))
-}
-
-#[cfg(all(feature = "reserves", feature = "electrum"))]
-pub fn get_outpoints_for_address(
-    address: Address,
-    client: &Client,
-    max_confirmation_height: Option<usize>,
-) -> Result<Vec<(OutPoint, TxOut)>, Error> {
-    let unspents = client
-        .script_list_unspent(&address.script_pubkey())
-        .map_err(Error::Electrum)?;
-
-    unspents
-        .iter()
-        .filter(|utxo| {
-            utxo.height > 0 && utxo.height <= max_confirmation_height.unwrap_or(usize::MAX)
-        })
-        .map(|utxo| {
-            let tx = match client.transaction_get(&utxo.tx_hash) {
-                Ok(tx) => tx,
-                Err(e) => {
-                    return Err(e).map_err(Error::Electrum);
-                }
-            };
-
-            Ok((
-                OutPoint {
-                    txid: utxo.tx_hash,
-                    vout: utxo.tx_pos as u32,
-                },
-                tx.output[utxo.tx_pos].clone(),
-            ))
-        })
-        .collect()
-}
-
-#[cfg(test)]
-mod test {
-    use super::{CliOpts, WalletOpts};
-    #[cfg(feature = "compiler")]
-    use crate::handle_compile_subcommand;
-    #[cfg(feature = "compact_filters")]
-    use crate::CompactFilterOpts;
-    #[cfg(feature = "electrum")]
-    use crate::ElectrumOpts;
-    #[cfg(feature = "esplora")]
-    use crate::EsploraOpts;
-    use crate::OfflineWalletSubCommand::{BumpFee, CreateTx, GetNewAddress};
-    #[cfg(all(feature = "reserves", feature = "compact_filters"))]
-    use crate::OnlineWalletSubCommand::ProduceProof;
-    #[cfg(all(feature = "reserves", feature = "esplora-ureq"))]
-    use crate::OnlineWalletSubCommand::VerifyProof;
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "compact_filters",
-        feature = "rpc"
-    ))]
-    use crate::OnlineWalletSubCommand::{Broadcast, Sync};
-    #[cfg(any(feature = "compact_filters", feature = "electrum", feature = "esplora"))]
-    use crate::ProxyOpts;
-    #[cfg(feature = "rpc")]
-    use crate::RpcOpts;
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    use crate::{handle_ext_reserves_subcommand, handle_online_wallet_subcommand};
-    use crate::{handle_key_subcommand, CliSubCommand, KeySubCommand, WalletSubCommand};
-    use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey};
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    use bdk::bitcoin::{consensus::Encodable, util::psbt::PartiallySignedTransaction};
-    use bdk::bitcoin::{Address, Network, OutPoint};
-    use bdk::miniscript::bitcoin::network::constants::Network::Testnet;
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    use bdk::{
-        blockchain::ElectrumBlockchain, database::MemoryDatabase, electrum_client::Client,
-        SyncOptions, Wallet,
-    };
-    use std::str::{self, FromStr};
-    use structopt::StructOpt;
-
-    #[test]
-    fn test_parse_wallet_get_new_address() {
-        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
-                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "get_new_address"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    #[cfg(feature = "electrum")]
-                    electrum_opts: ElectrumOpts {
-                        timeout: None,
-                        server: "ssl://electrum.blockstream.info:60002".to_string(),
-                        stop_gap: 10,
-                    },
-                    #[cfg(feature = "esplora")]
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/testnet/api/".to_string(),
-                        timeout: 5,
-                        stop_gap: 10,
-                        conc: 4,
-                    },
-                    #[cfg(feature = "compact_filters")]
-                    compactfilter_opts: CompactFilterOpts{
-                        address: vec!["127.0.0.1:18444".to_string()],
-                        conn_count: 4,
-                        skip_blocks: 0,
-                    },
-                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    },
-                    #[cfg(feature = "rpc")]
-                    rpc_opts: RpcOpts {
-                        address: "127.0.0.1:18443".to_string(),
-                        basic_auth: ("user".to_string(), "password".to_string()),
-                        cookie: None,
-                        skip_blocks: None,
-                    },
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(feature = "electrum")]
-    #[test]
-    fn test_parse_wallet_electrum() {
-        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
-                            "--proxy", "127.0.0.1:9150", "--retries", "3", "--timeout", "10",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "--server","ssl://electrum.blockstream.info:50002",
-                            "--stop_gap", "20",
-                            "get_new_address"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Testnet,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    electrum_opts: ElectrumOpts {
-                        timeout: Some(10),
-                        server: "ssl://electrum.blockstream.info:50002".to_string(),
-                        stop_gap: 20
-                    },
-                    proxy_opts: ProxyOpts{
-                        proxy: Some("127.0.0.1:9150".to_string()),
-                        proxy_auth: None,
-                        retries: 3,
-                    },
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(feature = "esplora-ureq")]
-    #[test]
-    fn test_parse_wallet_esplora() {
-        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
-                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "--server", "https://blockstream.info/api/",
-                            "--timeout", "10",
-                            "--stop_gap", "20",
-                            "get_new_address"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/api/".to_string(),
-                        timeout: 10,
-                        stop_gap: 20,
-                        conc: 4,
-                    },
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    }
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(feature = "esplora-reqwest")]
-    #[test]
-    fn test_parse_wallet_esplora() {
-        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
-                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "--server", "https://blockstream.info/api/",
-                            "--conc", "10",
-                            "--stop_gap", "20",
-                            "get_new_address"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/api/".to_string(),
-                        conc: 10,
-                        stop_gap: 20,
-                        timeout: 5,
-                    },
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    }
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(feature = "rpc")]
-    #[test]
-    fn test_parse_wallet_rpc() {
-        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
-                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "--node", "125.67.89.101:56678",
-                            "--basic-auth", "user:password",
-                            "--cookie", "/home/user/.bitcoin/regtest/.cookie",
-                            "--skip-blocks", "5",
-                            "get_new_address"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    rpc_opts: RpcOpts {
-                        address: "125.67.89.101:56678".to_string(),
-                        basic_auth: ("user".to_string(), "password".to_string()),
-                        cookie: Some("/home/user/.bitcoin/regtest/.cookie".to_string()),
-                        skip_blocks: Some(5),
-                    },
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(feature = "compact_filters")]
-    #[test]
-    fn test_parse_wallet_compact_filters() {
-        let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet",
-                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "--proxy", "127.0.0.1:9005",
-                            "--proxy_auth", "random_user:random_passwd",
-                            "--node", "127.0.0.1:18444", "127.2.3.1:19695",
-                            "--conn_count", "4",
-                            "--skip_blocks", "5",
-                            "get_new_address"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    compactfilter_opts: CompactFilterOpts{
-                        address: vec!["127.0.0.1:18444".to_string(), "127.2.3.1:19695".to_string()],
-                        conn_count: 4,
-                        skip_blocks: 5,
-                    },
-                    proxy_opts: ProxyOpts{
-                        proxy: Some("127.0.0.1:9005".to_string()),
-                        proxy_auth: Some(("random_user".to_string(), "random_passwd".to_string())),
-                        retries: 5,
-                    }
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "compact_filters",
-        feature = "rpc"
-    ))]
-    #[test]
-    fn test_parse_wallet_sync() {
-        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "sync"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Testnet,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: None,
-                    #[cfg(feature = "electrum")]
-                    electrum_opts: ElectrumOpts {
-                        timeout: None,
-                        server: "ssl://electrum.blockstream.info:60002".to_string(),
-                        stop_gap: 10,
-                    },
-                    #[cfg(feature = "esplora")]
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/testnet/api/".to_string(),
-                        timeout: 5,
-                        stop_gap: 10,
-                        conc: 4,
-                    },
-                    #[cfg(feature = "compact_filters")]
-                    compactfilter_opts: CompactFilterOpts{
-                        address: vec!["127.0.0.1:18444".to_string()],
-                        conn_count: 4,
-                        skip_blocks: 0,
-                    },
-                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    },
-                    #[cfg(feature = "rpc")]
-                    rpc_opts: RpcOpts {
-                        address: "127.0.0.1:18443".to_string(),
-                        basic_auth: ("user".to_string(), "password".to_string()),
-                        cookie: None,
-                        skip_blocks: None,
-                    },
-                },
-                subcommand: WalletSubCommand::OnlineWalletSubCommand(Sync),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[test]
-    fn test_parse_wallet_create_tx() {
-        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "create_tx", "--to", "n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ:123456","mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf:78910",
-                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
-                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ")
-            .unwrap()
-            .script_pubkey();
-        let script2 = Address::from_str("mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf")
-            .unwrap()
-            .script_pubkey();
-        let outpoint1 = OutPoint::from_str(
-            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
-        )
-        .unwrap();
-        let outpoint2 = OutPoint::from_str(
-            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2",
-        )
-        .unwrap();
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Testnet,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    #[cfg(feature = "electrum")]
-                    electrum_opts: ElectrumOpts {
-                        timeout: None,
-                        server: "ssl://electrum.blockstream.info:60002".to_string(),
-                        stop_gap: 10,
-                    },
-                    #[cfg(feature = "esplora")]
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/testnet/api/".to_string(),
-                        timeout: 5,
-                        stop_gap: 10,
-                        conc: 4,
-                    },
-                    #[cfg(feature = "compact_filters")]
-                    compactfilter_opts: CompactFilterOpts{
-                        address: vec!["127.0.0.1:18444".to_string()],
-                        conn_count: 4,
-                        skip_blocks: 0,
-                    },
-                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    },
-                    #[cfg(feature = "rpc")]
-                    rpc_opts: RpcOpts {
-                        address: "127.0.0.1:18443".to_string(),
-                        basic_auth: ("user".to_string(), "password".to_string()),
-                        cookie: None,
-                        skip_blocks: None,
-                    },
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(CreateTx {
-                    recipients: vec![(script1, 123456), (script2, 78910)],
-                    send_all: false,
-                    enable_rbf: false,
-                    offline_signer: false,
-                    utxos: Some(vec!(outpoint1, outpoint2)),
-                    unspendable: None,
-                    fee_rate: None,
-                    external_policy: None,
-                    internal_policy: None,
-                }),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[test]
-    fn test_parse_wallet_bump_fee() {
-        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "bump_fee", "--fee_rate", "6.1",
-                            "--txid","35aab0d0213f8996f9e236a28630319b93109754819e8abf48a0835708d33506",
-                            "--shrink","tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt"];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Testnet,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-                    #[cfg(feature = "electrum")]
-                    electrum_opts: ElectrumOpts {
-                        timeout: None,
-                        server: "ssl://electrum.blockstream.info:60002".to_string(),
-                        stop_gap: 10,
-                    },
-                    #[cfg(feature = "esplora")]
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/testnet/api/".to_string(),
-                        timeout: 5,
-                        stop_gap: 10,
-                        conc: 4,
-                    },
-                    #[cfg(feature = "compact_filters")]
-                    compactfilter_opts: CompactFilterOpts{
-                        address: vec!["127.0.0.1:18444".to_string()],
-                        conn_count: 4,
-                        skip_blocks: 0,
-                    },
-                    #[cfg(feature = "rpc")]
-                    rpc_opts: RpcOpts {
-                        address: "127.0.0.1:18443".to_string(),
-                        basic_auth: ("user".to_string(), "password".to_string()),
-                        cookie: None,
-                        skip_blocks: None,
-                    },
-                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    }
-                },
-                subcommand: WalletSubCommand::OfflineWalletSubCommand(BumpFee {
-                    txid: "35aab0d0213f8996f9e236a28630319b93109754819e8abf48a0835708d33506".to_string(),
-                    shrink_address: Some(Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap()),
-                    offline_signer: false,
-                    utxos: None,
-                    unspendable: None,
-                    fee_rate: 6.1
-                }),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(any(
-        feature = "electrum",
-        feature = "esplora",
-        feature = "compact_filters",
-        feature = "rpc"
-    ))]
-    #[test]
-    fn test_parse_wallet_broadcast() {
-        let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "broadcast",
-                            "--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Testnet,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-                    change_descriptor: None,
-                    #[cfg(feature = "electrum")]
-                    electrum_opts: ElectrumOpts {
-                        timeout: None,
-                        server: "ssl://electrum.blockstream.info:60002".to_string(),
-                        stop_gap: 10,
-                    },
-                    #[cfg(feature = "esplora")]
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/testnet/api/".to_string(),
-                        timeout: 5,
-                        stop_gap: 10,
-                        conc: 4,
-                    },
-                    #[cfg(feature = "compact_filters")]
-                    compactfilter_opts: CompactFilterOpts{
-                        address: vec!["127.0.0.1:18444".to_string()],
-                        conn_count: 4,
-                        skip_blocks: 0,
-                    },
-                    #[cfg(any(feature="compact_filters", feature="electrum", feature="esplora"))]
-                    proxy_opts: ProxyOpts{
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    },
-                    #[cfg(feature = "rpc")]
-                    rpc_opts: RpcOpts {
-                        address: "127.0.0.1:18443".to_string(),
-                        basic_auth: ("user".to_string(), "password".to_string()),
-                        cookie: None,
-                        skip_blocks: None,
-                    },
-                },
-                subcommand: WalletSubCommand::OnlineWalletSubCommand(Broadcast {
-                    psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()),
-                    tx: None
-                }),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[test]
-    fn test_parse_wrong_network() {
-        let cli_args = vec!["repl", "--network", "badnet", "wallet",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "sync"];
-
-        let cli_opts = CliOpts::from_iter_safe(&cli_args);
-        assert!(cli_opts.is_err());
-    }
-
-    #[test]
-    fn test_key_generate() {
-        let network = Testnet;
-        let key_generate_cmd = KeySubCommand::Generate {
-            word_count: 12,
-            password: Some("test123".to_string()),
-        };
-
-        let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
-        let result_obj = result.as_object().unwrap();
-
-        let mnemonic = result_obj.get("mnemonic").unwrap().as_str().unwrap();
-        let mnemonic: Vec<&str> = mnemonic.split(' ').collect();
-        let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
-
-        assert_eq!(mnemonic.len(), 12);
-        assert_eq!(&xprv[0..4], "tprv");
-    }
-
-    #[test]
-    fn test_key_restore() {
-        let network = Testnet;
-        let key_generate_cmd = KeySubCommand::Restore {
-            mnemonic: "payment battle unit sword token broccoli era violin purse trip blood hire"
-                .to_string(),
-            password: Some("test123".to_string()),
-        };
-
-        let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
-        let result_obj = result.as_object().unwrap();
-
-        let fingerprint = result_obj.get("fingerprint").unwrap().as_str().unwrap();
-        let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
-
-        assert_eq!(&fingerprint, &"828af366");
-        assert_eq!(&xprv, &"tprv8ZgxMBicQKsPd18TeiFknZKqaZFwpdX9tvvKh8eeHSSPBQi5g9xPHztBg411o78G8XkrhQb6Q1cVvBJ1a9xuFHpmWgvQsvkJkNxBjfGoqhK");
-    }
-
-    #[test]
-    fn test_key_derive() {
-        let network = Testnet;
-        let key_generate_cmd = KeySubCommand::Derive {
-            xprv: ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPfQjJy8ge2cvBfDjLxJSkvNLVQiw7BQ5gTjKadG2rrcQB5zjcdaaUTz5EDNJaS77q4DzjqjogQBfMsaXFFNP3UqoBnwt2kyT").unwrap(),
-            path: DerivationPath::from_str("m/84'/1'/0'/0").unwrap(),
-        };
-
-        let result = handle_key_subcommand(network, key_generate_cmd).unwrap();
-        let result_obj = result.as_object().unwrap();
-
-        let xpub = result_obj.get("xpub").unwrap().as_str().unwrap();
-        let xprv = result_obj.get("xprv").unwrap().as_str().unwrap();
-
-        assert_eq!(&xpub, &"[566844c5/84'/1'/0'/0]tpubDFeqiDkfwR1tAhPxsXSZMfEmfpDhwhLyhLKZgmeBvuBkZQusoWeL62oGg2oTNGcENeKdwuGepAB85eMvyLemabYe9PSqv6cr5mFXktHc3Ka/*");
-        assert_eq!(&xprv, &"[566844c5/84'/1'/0'/0]tprv8ixoZoiRo3LDHENAysmxxFaf6nhmnNA582inQFbtWdPMivf7B7pjuYBQVuLC5bkM7tJZEDbfoivENsGZPBnQg1n52Kuc1P8X2Ei3XJuJX7c/*");
-    }
-
-    #[cfg(feature = "compiler")]
-    #[test]
-    fn test_parse_compile() {
-        let cli_args = vec![
-            "bdk-cli",
-            "compile",
-            "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))",
-            "--type",
-            "sh-wsh",
-        ];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Testnet,
-            subcommand: CliSubCommand::Compile {
-                policy: "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))".to_string(),
-                script_type: "sh-wsh".to_string(),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(feature = "compiler")]
-    #[test]
-    fn test_compile() {
-        let result = handle_compile_subcommand(
-            Network::Testnet,
-            "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))".to_string(),
-            "sh-wsh".to_string(),
-        )
-        .unwrap();
-        let result_obj = result.as_object().unwrap();
-
-        let descriptor = result_obj.get("descriptor").unwrap().as_str().unwrap();
-        assert_eq!(
-            &descriptor,
-            &"sh(wsh(thresh(3,pk(Alice),s:pk(Bob),s:pk(Carol),snl:older(2))))#rmef3s78"
-        );
-    }
-
-    #[cfg(all(feature = "reserves", feature = "compact_filters"))]
-    #[test]
-    fn test_parse_produce_proof() {
-        let message = "Those coins belong to Satoshi Nakamoto";
-        let cli_args = vec![
-            "bdk-cli",
-            "--network",
-            "bitcoin",
-            "wallet",
-            "--descriptor",
-            "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
-            "produce_proof",
-            "--message",
-            message.clone(),
-        ];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
-                        .to_string(),
-                    change_descriptor: None,
-                    compactfilter_opts: CompactFilterOpts {
-                        address: vec!["127.0.0.1:18444".to_string()],
-                        conn_count: 4,
-                        skip_blocks: 0,
-                    },
-                    proxy_opts: ProxyOpts {
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    },
-                },
-                subcommand: WalletSubCommand::OnlineWalletSubCommand(ProduceProof {
-                    msg: message.to_string(),
-                }),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(all(feature = "reserves", feature = "esplora-ureq"))]
-    #[test]
-    fn test_parse_verify_proof_internal() {
-        let psbt = r#"cHNidP8BAKcBAAAAA31Ko7U8mQMXxjrKhYvd5N06BrT2dBPwWVhZQYABZbdZAAAAAAD/////mAqA48Jx/UDORZswhCLAQiyCxhu4IZMXzWRUMx5PVIUAAAAAAP////+YCoDjwnH9QM5FmzCEIsBCLILGG7ghkxfNZFQzHk9UhQEAAAAA/////wHo7zMDAAAAABl2qRSff9CW037SwOP38M/JJL7vT/zraIisAAAAAAABAQoAAAAAAAAAAAFRAQMEAQAAAAEHAAABASAQJwAAAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FRzBEAiA3wllP5sFLWtT5NOthk2OaD42fNATjDzBVL4dPsG538QIgC7r4Hs2qQrKzY/WJOl2Idx7KAEY+J5xniJfEB1D7TzsBIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJIMEUCIQDETYrRs/Lamq1zew92oa2zFUFBeaWADxcKXmMf8/pMgAIgeQCUTF6jvi5iD9LxD54YKD3STmWy/Y4WwtVebZJWeh4BIgID9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNHMEQCIEIkdGA0m2sxDlRArMN5cVflkK3OZt0thfgntyqv8PuoAiBjtkZejhZ2YgB/C3oiGjZM2L7QA+QoXc7Ma677P7+87wEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zQEFAEcwRAIgN8JZT+bBS1rU+TTrYZNjmg+NnzQE4w8wVS+HT7Bud/ECIAu6+B7NqkKys2P1iTpdiHceygBGPiecZ4iXxAdQ+087AUgwRQIhAMRNitGz8tqarXN7D3ahrbMVQUF5pYAPFwpeYx/z+kyAAiB5AJRMXqO+LmIP0vEPnhgoPdJOZbL9jhbC1V5tklZ6HgFHMEQCIEIkdGA0m2sxDlRArMN5cVflkK3OZt0thfgntyqv8PuoAiBjtkZejhZ2YgB/C3oiGjZM2L7QA+QoXc7Ma677P7+87wHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgABASDYyDMDAAAAABepFBCNSAfpaNUWLsnOLKCLqO4EAl4UhyICAyS3XurSwfnGDoretecAn+x6Ka/Nsw2CnYLQlWL+i66FRzBEAiBER55YOumAJFkXvTrb1GSuXxYfenIqK+LRx7PPvoKGLQIgVp0yY/2YB63O2tzzjtEZpI+GVkHblhI/dWASuoKTUt4BIgIDdGj46pm2xkeIOYta0lSAytCPSw1lvlTOOlX9IGta5HJHMEQCIGjiLiZbmAJB6+x2D2K6FYWczwRx4XCKaBIsvvdyt1ouAiBTlhGF+7tXHXRWv4pWisXPlJ8oBvUN8c+CbdNxsfB8oQEiAgP3LT2WZjsOqZsK6w1/JzyrEajeN4hfHd3I2REq24cWk0gwRQIhAKxzC4IYfuSVMbIk1dkOgi+xCg/zEh7Drie9E1r0KKUPAiAEJM+oGgJw5CTKiLoO80uyWlHnNYXRt0bDLaM0OaoVtgEBBCIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQXxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgEHIyIAIHQQ4qnMe1dC7RoA6/AqOG53jareHaC0Fbqu6vBAL08NAQj9zQEFAEcwRAIgREeeWDrpgCRZF70629Rkrl8WH3pyKivi0cezz76Chi0CIFadMmP9mAetztrc847RGaSPhlZB25YSP3VgErqCk1LeAUcwRAIgaOIuJluYAkHr7HYPYroVhZzPBHHhcIpoEiy+93K3Wi4CIFOWEYX7u1cddFa/ilaKxc+UnygG9Q3xz4Jt03Gx8HyhAUgwRQIhAKxzC4IYfuSVMbIk1dkOgi+xCg/zEh7Drie9E1r0KKUPAiAEJM+oGgJw5CTKiLoO80uyWlHnNYXRt0bDLaM0OaoVtgHxUyECL1M7Zn4uo7NuIZYcn+nco0D74K9SEBc6g64DN6sgpXYhAmu1OpjoEL0O5hoO0RZLpsAkeG12VU55PiAtxs6ceMTqIQLVuKfWakH/229MU9YZlAIuiGtPRQAfsVi5XJFk1F+MoyEDJLde6tLB+cYOit615wCf7Hopr82zDYKdgtCVYv6LroUhAy00+JMiAIM0h70pSqIZ3L4AC5+bPYJHmVQUMACfD6VRIQN0aPjqmbbGR4g5i1rSVIDK0I9LDWW+VM46Vf0ga1rkciED9y09lmY7DqmbCusNfyc8qxGo3jeIXx3dyNkRKtuHFpNXrgAA"#;
-        let message = "Those coins belong to Satoshi Nakamoto";
-        let cli_args = vec![
-            "bdk-cli",
-            "--network",
-            "bitcoin",
-            "wallet",
-            "--descriptor",
-            "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
-            "verify_proof",
-            "--psbt",
-            psbt.clone(),
-            "--message",
-            message.clone(),
-        ];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
-                        .to_string(),
-                    change_descriptor: None,
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/testnet/api/".to_string(),
-                        timeout: 5,
-                        stop_gap: 10,
-                        conc: 4,
-                    },
-                    proxy_opts: ProxyOpts {
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    },
-                },
-                subcommand: WalletSubCommand::OnlineWalletSubCommand(VerifyProof {
-                    psbt: psbt.to_string(),
-                    msg: message.to_string(),
-                    confirmations: 6,
-                }),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(all(feature = "reserves", feature = "esplora-ureq"))]
-    #[test]
-    fn test_parse_verify_proof_internal_confirmation() {
-        let psbt = r#"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"#;
-        let message = "Those coins belong to Satoshi Nakamoto";
-        let cli_args = vec![
-            "bdk-cli",
-            "--network",
-            "bitcoin",
-            "wallet",
-            "--descriptor",
-            "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)",
-            "verify_proof",
-            "--psbt",
-            psbt.clone(),
-            "--message",
-            message.clone(),
-            "--confirmations",
-            "0",
-        ];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::Wallet {
-                wallet_opts: WalletOpts {
-                    wallet: None,
-                    verbose: false,
-                    descriptor: "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
-                        .to_string(),
-                    change_descriptor: None,
-                    esplora_opts: EsploraOpts {
-                        server: "https://blockstream.info/testnet/api/".to_string(),
-                        timeout: 5,
-                        stop_gap: 10,
-                        conc: 4,
-                    },
-                    proxy_opts: ProxyOpts {
-                        proxy: None,
-                        proxy_auth: None,
-                        retries: 5,
-                    },
-                },
-                subcommand: WalletSubCommand::OnlineWalletSubCommand(VerifyProof {
-                    psbt: psbt.to_string(),
-                    msg: message.to_string(),
-                    confirmations: 0,
-                }),
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    #[test]
-    fn test_parse_verify_proof_external() {
-        let psbt = r#"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"#.to_string();
-        let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d".to_string();
-        let message = "Those coins belong to Satoshi Nakamoto".to_string();
-        let cli_args = vec![
-            "bdk-cli",
-            "--network",
-            "bitcoin",
-            "external_reserves",
-            &message,
-            &psbt,
-            "6",
-            &address,
-            "--server",
-            "ssl://electrum.blockstream.info:60002",
-        ];
-
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let expected_cli_opts = CliOpts {
-            network: Network::Bitcoin,
-            subcommand: CliSubCommand::ExternalReserves {
-                message,
-                psbt,
-                confirmations: 6,
-                addresses: [address].to_vec(),
-                electrum_opts: ElectrumOpts {
-                    timeout: None,
-                    server: "ssl://electrum.blockstream.info:60002".to_string(),
-                    stop_gap: 10,
-                },
-            },
-        };
-
-        assert_eq!(expected_cli_opts, cli_opts);
-    }
-
-    /// Encodes a partially signed transaction as base64 and returns the  bytes of the resulting string.
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    fn encode_psbt(psbt: PartiallySignedTransaction) -> Vec<u8> {
-        let mut encoded = Vec::<u8>::new();
-        psbt.consensus_encode(&mut encoded).unwrap();
-        let base64_psbt = base64::encode(&encoded);
-
-        base64_psbt.as_bytes().to_vec()
-    }
-
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    #[test]
-    fn test_proof_of_reserves_wallet() {
-        let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string();
-        let message = "Those coins belong to Satoshi Nakamoto";
-
-        let client = Client::new("ssl://electrum.blockstream.info:60002").unwrap();
-        let blockchain = ElectrumBlockchain::from(client);
-        let wallet = Wallet::new(
-            &descriptor,
-            None,
-            Network::Testnet,
-            MemoryDatabase::default(),
-        )
-        .unwrap();
-
-        wallet.sync(&blockchain, SyncOptions::default()).unwrap();
-        let balance = wallet.get_balance().unwrap();
-
-        let addr = wallet.get_address(bdk::wallet::AddressIndex::New).unwrap();
-        assert_eq!(
-            "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d",
-            addr.to_string()
-        );
-
-        let cli_args = vec![
-            "bdk-cli",
-            "--network",
-            "bitcoin",
-            "wallet",
-            "--descriptor",
-            &descriptor,
-            "produce_proof",
-            "--message",
-            message.clone(),
-        ];
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let wallet_subcmd = match cli_opts.subcommand {
-            CliSubCommand::Wallet {
-                wallet_opts: _,
-                subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
-            } => online_subcommand,
-            _ => panic!("unexpected subcommand"),
-        };
-        let result = handle_online_wallet_subcommand(&wallet, &blockchain, wallet_subcmd).unwrap();
-        let psbt: PartiallySignedTransaction =
-            serde_json::from_str(&result.as_object().unwrap().get("psbt").unwrap().to_string())
-                .unwrap();
-        let psbt = encode_psbt(psbt);
-        let psbt = str::from_utf8(&psbt).unwrap();
-        assert_eq!(format!("{}", psbt), "cHNidP8BAP0YAgEAAAAM0DsC5Uy7AiuQC5e0oOrDcGu6i8rY8fsT3QzMJvJoAyUAAAAAAP////8IgYfaHR37CUDGQCaLj/QMLxAFteVTnYAskOVx6wHQLgEAAAAA/////wxNB645qLQXuZJoemip3ne14b5R5GWHEDL8o20m0oiHAAAAAAD/////UII10YAYjpnNzaXu1mPht5rsUF74nrz4anfwWykHepUAAAAAAP////+yr7v1/En7kXz3nVdxunw3lVhUmh6wbXN3cDFK1wbA9gAAAAAA/////7cV00FjL7mwDKa6bLd6TEoI1EI8OszcFUnlqT8j8a2HAQAAAAD/////u193IvDJvWzXUG6xaO8zqLBJK0wKKcVdgG74x+OYVOkAAAAAAP////+80K0TirJXCaMzD5VTAsfU35C3Xkawe26Ha2/vynAarQEAAAAA/////8BRLif9KQ71JK8i/wwjZd2bfF2fvtK53q5fk/KoKBqcAQAAAAD/////0BqoaKC7isw56cqwgPLMffSpGoSsuaycXuHMBc6W5/8AAAAAAP/////vDoSJCOCXfj+sO/p8S7w6AaPg2dbBaP0bAliB7X+3+wEAAAAA//////nwXYCb9rUnXsOz23U8xLrx6fhHcWbV2U2ItyzyqK4SAQAAAAD/////AWcFIAAAAAAAGXapFJ9/0JbTftLA4/fwz8kkvu9P/OtoiKwAAAAAAAEBCgAAAAAAAAAAAVEBBwAAAQEfio4BAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjRzBEAiBHtlGW6zZ+1K1GEKV4vv3QEuKCW/6FjChKpuHbBnW29QIgIxWSCMz8UE9tprl+purowf1svpD4DaLTPMgvLaXKCy8BAQcAAQhrAkcwRAIgR7ZRlus2ftStRhCleL790BLiglv+hYwoSqbh2wZ1tvUCICMVkgjM/FBPbaa5fqbq6MH9bL6Q+A2i0zzILy2lygsvASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfoIYBAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjSDBFAiEA1D0KbajwQJFu6vdMRYFIW6stdr8HE1gvtX+mV3zTq9QCIC063fGFpHdBd+JVd4okab/dIICWIR4whjMvyBKsEZPjAQEHAAEIbAJIMEUCIQDUPQptqPBAkW7q90xFgUhbqy12vwcTWC+1f6ZXfNOr1AIgLTrd8YWkd0F34lV3iiRpv90ggJYhHjCGMy/IEqwRk+MBIQMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH4wABAR8QJwAAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qIgIDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+NHMEQCICbBVORcPMOSnbtmd1Gd/b/QL0CS2S6D61qR2JFNoz1kAiAoR2S9aWv4vAtXkrWTpYjG8cRlGmikLozZ0HRdMnigFAEBBwABCGsCRzBEAiAmwVTkXDzDkp27ZndRnf2/0C9Aktkug+takdiRTaM9ZAIgKEdkvWlr+LwLV5K1k6WIxvHEZRpopC6M2dB0XTJ4oBQBIQMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH4wABAR8QJwAAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qIgIDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+NHMEQCIDDPltzRNQpO1DVfZ4ZsXGgpKyebQtV0kM3OFUr6AfOUAiBF1TgXEfd4EpJASYm6+TmHBapH3i65WRzpcJu6gfFTlwEBBwABCGsCRzBEAiAwz5bc0TUKTtQ1X2eGbFxoKSsnm0LVdJDNzhVK+gHzlAIgRdU4FxH3eBKSQEmJuvk5hwWqR94uuVkc6XCbuoHxU5cBIQMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH4wABAR8QJwAAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qIgIDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+NHMEQCIGkpWXofEClK3cvL39D+L+KzTVvHeJ8DRY98s0r496/mAiBlzWdO2fzGXwzlsLsjlKT8NsblLxU2NN668ZBkRUW7ZgEBBwABCGsCRzBEAiBpKVl6HxApSt3Ly9/Q/i/is01bx3ifA0WPfLNK+Pev5gIgZc1nTtn8xl8M5bC7I5Sk/DbG5S8VNjTeuvGQZEVFu2YBIQMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH4wABAR+ghgEAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qIgIDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+NHMEQCIDiggh2XrCL+4OrfdtF4XH9SCFqeSL6GMJJ8F5MIkQ70AiBWqXmxIflzSQDMXfS3J+GMV+CWBKIfLWRDEi1cujGFggEBBwABCGsCRzBEAiA4oIIdl6wi/uDq33bReFx/Ughanki+hjCSfBeTCJEO9AIgVql5sSH5c0kAzF30tyfhjFfglgSiHy1kQxItXLoxhYIBIQMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH4wABAR8QJwAAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qIgIDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+NIMEUCIQDuHCLXHy87WKdQtxz3r9nOWvQQ6c6QcgklSPCpXX0zSAIgI2UPlsB5ptVvVH+9L2Wkshd9pvqCo71fXkgYWBXt9oMBAQcAAQhsAkgwRQIhAO4cItcfLztYp1C3HPev2c5a9BDpzpByCSVI8KldfTNIAiAjZQ+WwHmm1W9Uf70vZaSyF32m+oKjvV9eSBhYFe32gwEhAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjAAEBH6CGAQAAAAAAFgAU7OUmVxBT2oZFTJ4GZ3nq+dFQLmoiAgMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH40cwRAIgBP4XC3UqeBdNcJjRJ/Sx7dhm0SDDa2wAuUwRqK0GkzICIC+gNAj6XgQuGtt+2gmxIykCuQ0GA1yI6XU2IzyyvH6XAQEHAAEIawJHMEQCIAT+Fwt1KngXTXCY0Sf0se3YZtEgw2tsALlMEaitBpMyAiAvoDQI+l4ELhrbftoJsSMpArkNBgNciOl1NiM8srx+lwEhAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjAAEBH534GAAAAAAAFgAU7OUmVxBT2oZFTJ4GZ3nq+dFQLmoiAgMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH40gwRQIhANmB3tuWZAOiFVFI6hR8Ag6ruuJjA6rANXVvQhYEhdYrAiAcjUdiOGPL4TfyzddaBuuPzpsyFV6DJGmyV1x2Cx0/NQEBBwABCGwCSDBFAiEA2YHe25ZkA6IVUUjqFHwCDqu64mMDqsA1dW9CFgSF1isCIByNR2I4Y8vhN/LN11oG64/OmzIVXoMkabJXXHYLHT81ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfECcAAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjRzBEAiAbOSAd6UBdDz7YKOUVE4M9uLeSk9LnSm+I9Dtm4Q4XKQIgHYPtZmV+Y6/F+un5QFnogg+B0QQARWzlsvh9GeKdD4oBAQcAAQhrAkcwRAIgGzkgHelAXQ8+2CjlFRODPbi3kpPS50pviPQ7ZuEOFykCIB2D7WZlfmOvxfrp+UBZ6IIPgdEEAEVs5bL4fRninQ+KASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfECcAAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjSDBFAiEAnC80m9Dho2bb4gGhG39WexAYV2UQ6LPMYNXHmlH3o0wCIADCLhvCB/wmz+fUx5J3neoOjoSLHpTc6/yawp7ExYpbAQEHAAEIbAJIMEUCIQCcLzSb0OGjZtviAaEbf1Z7EBhXZRDos8xg1ceaUfejTAIgAMIuG8IH/CbP59THkned6g6OhIselNzr/JrCnsTFilsBIQMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH4wAA");
-
-        let psbt_b64 = &result
-            .as_object()
-            .unwrap()
-            .get("psbt_base64")
-            .unwrap()
-            .to_string();
-        assert_eq!(&format!("{}", psbt), psbt_b64.trim_matches('\"'));
-
-        let cli_args = vec![
-            "bdk-cli",
-            "--network",
-            "bitcoin",
-            "wallet",
-            "--descriptor",
-            &descriptor,
-            "verify_proof",
-            "--psbt",
-            psbt,
-            "--message",
-            message.clone(),
-        ];
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let wallet_subcmd = match cli_opts.subcommand {
-            CliSubCommand::Wallet {
-                wallet_opts: _,
-                subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand),
-            } => online_subcommand,
-            _ => panic!("unexpected subcommand"),
-        };
-        let result = handle_online_wallet_subcommand(&wallet, &blockchain, wallet_subcmd).unwrap();
-        let spendable = result
-            .as_object()
-            .unwrap()
-            .get("spendable")
-            .unwrap()
-            .as_u64()
-            .unwrap();
-        assert_eq!(spendable, balance);
-    }
-
-    #[cfg(all(feature = "reserves", feature = "electrum"))]
-    #[test]
-    fn test_proof_of_reserves_veryfy() {
-        let message = "Those coins belong to Satoshi Nakamoto";
-        let address = "tb1qanjjv4cs20dgv32vncrxw702l8g4qtn2m9wn7d";
-        let psbt = "cHNidP8BAKcBAAAAA9A7AuVMuwIrkAuXtKDqw3BruovK2PH7E90MzCbyaAMlAAAAAAD/////sq+79fxJ+5F8951Xcbp8N5VYVJoesG1zd3AxStcGwPYAAAAAAP/////AUS4n/SkO9SSvIv8MI2Xdm3xdn77Sud6uX5PyqCganAEAAAAA/////wGwrQEAAAAAABl2qRSff9CW037SwOP38M/JJL7vT/zraIisAAAAAAABAQoAAAAAAAAAAAFRAQcAAAEBHxAnAAAAAAAAFgAU7OUmVxBT2oZFTJ4GZ3nq+dFQLmoiAgMrBVgHi+w4aUqEkz1lkwPiV12ufpFoWRFFQRW/1kSH40gwRQIhAPgByvkajQrNeQDSGik2gnxpo/P/owiEHR+0nWefkXurAiBgrAlDvwuTiaGEEWQW/Kd7L7u7YOQnqvrd46DR0A8yPgEBBwABCGwCSDBFAiEA+AHK+RqNCs15ANIaKTaCfGmj8/+jCIQdH7SdZ5+Re6sCIGCsCUO/C5OJoYQRZBb8p3svu7tg5Ceq+t3joNHQDzI+ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAQEfoIYBAAAAAAAWABTs5SZXEFPahkVMngZneer50VAuaiICAysFWAeL7DhpSoSTPWWTA+JXXa5+kWhZEUVBFb/WRIfjRzBEAiBSfiX0qP7vR+2Qx/mRJS8pwma8nTfOWKerzo6c0iSAfwIgEfX4Wt7YXd8MkKUEY627GWYCmKfMsJGcIC0U1wgc1vUBAQcAAQhrAkcwRAIgUn4l9Kj+70ftkMf5kSUvKcJmvJ03zlinq86OnNIkgH8CIBH1+Fre2F3fDJClBGOtuxlmApinzLCRnCAtFNcIHNb1ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAA==";
-
-        let cli_args = vec![
-            "bdk-cli",
-            "--network",
-            "bitcoin",
-            "external_reserves",
-            message,
-            psbt,
-            "6",
-            address,
-            address, // passing the address twice on purpose, to test passing of multiple addresses
-            "--server",
-            "ssl://electrum.blockstream.info:60002",
-        ];
-        let cli_opts = CliOpts::from_iter(&cli_args);
-
-        let (message, psbt, confirmations, addresses, electrum_opts) = match cli_opts.subcommand {
-            CliSubCommand::ExternalReserves {
-                message,
-                psbt,
-                confirmations,
-                addresses,
-                electrum_opts,
-            } => (message, psbt, confirmations, addresses, electrum_opts),
-            _ => panic!("unexpected subcommand"),
-        };
-        let result = handle_ext_reserves_subcommand(
-            Network::Bitcoin,
-            message,
-            psbt,
-            confirmations,
-            addresses,
-            electrum_opts,
-        )
-        .unwrap();
-        let spendable = result
-            .as_object()
-            .unwrap()
-            .get("spendable")
-            .unwrap()
-            .as_u64()
-            .unwrap();
-        assert!(spendable > 0);
-    }
-}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644 (file)
index 0000000..154b42a
--- /dev/null
@@ -0,0 +1,108 @@
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! BDK CLI APP
+//!
+//! This module describes the app's main() function
+
+mod backend;
+mod commands;
+mod handlers;
+mod utils;
+
+use backend::Backend;
+
+use bitcoin::Network;
+
+use log::{debug, error, warn};
+
+use crate::commands::CliOpts;
+use crate::handlers::*;
+use bdk::{bitcoin, Error};
+use structopt::StructOpt;
+
+#[cfg(feature = "repl")]
+const REPL_LINE_SPLIT_REGEX: &str = r#""([^"]*)"|'([^']*)'|([\w\-]+)"#;
+
+fn main() {
+    env_logger::init();
+
+    let cli_opts: CliOpts = CliOpts::from_args();
+
+    let network = cli_opts.network;
+    debug!("network: {:?}", network);
+    if network == Network::Bitcoin {
+        warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
+    }
+
+    #[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();
+        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()
+    };
+
+    #[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(),
+        }
+    };
+
+    #[cfg(feature = "regtest-electrum")]
+    let (_electrsd, 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)
+    };
+
+    #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))]
+    let (_electrsd, 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)
+    };
+
+    #[cfg(not(feature = "regtest-node"))]
+    let backend = Backend::None;
+
+    match handle_command(cli_opts, network, backend) {
+        Ok(result) => println!("{}", result),
+        Err(e) => {
+            match e {
+                Error::ChecksumMismatch => error!("Descriptor checksum mismatch. Are you using a different descriptor for an already defined wallet name? (if you are not specifying the wallet name it is automatically named based on the descriptor)"),
+                e => error!("{}", e.to_string()),
+            }
+        },
+    }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644 (file)
index 0000000..8e4251a
--- /dev/null
@@ -0,0 +1,314 @@
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Utility Tools
+//!
+//! This module includes all the utility tools used by the App.
+
+use std::path::PathBuf;
+use std::str::FromStr;
+
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+use crate::backend::Backend;
+use crate::commands::WalletOpts;
+use bdk::bitcoin::secp256k1::Secp256k1;
+use bdk::bitcoin::{Address, Network, OutPoint, Script};
+#[cfg(feature = "compact_filters")]
+use bdk::blockchain::compact_filters::{BitcoinPeerConfig, CompactFiltersBlockchainConfig};
+#[cfg(feature = "esplora")]
+use bdk::blockchain::esplora::EsploraBlockchainConfig;
+#[cfg(feature = "rpc")]
+use bdk::blockchain::rpc::{Auth, RpcConfig};
+#[cfg(feature = "electrum")]
+use bdk::blockchain::ElectrumBlockchainConfig;
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+use bdk::blockchain::{AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain};
+#[cfg(feature = "key-value-db")]
+use bdk::database::any::SledDbConfiguration;
+#[cfg(feature = "sqlite-db")]
+use bdk::database::any::SqliteDbConfiguration;
+use bdk::database::{AnyDatabase, AnyDatabaseConfig, BatchDatabase, ConfigurableDatabase};
+use bdk::wallet::wallet_name_from_descriptor;
+use bdk::{Error, Wallet};
+
+/// Create a randomized wallet name from the descriptor checksum.
+/// If wallet options already includes a name, use that instead.
+pub(crate) fn maybe_descriptor_wallet_name(
+    wallet_opts: WalletOpts,
+    network: Network,
+) -> Result<WalletOpts, Error> {
+    if wallet_opts.wallet.is_some() {
+        return Ok(wallet_opts);
+    }
+    // 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 mut wallet_opts = wallet_opts;
+    wallet_opts.wallet = Some(wallet_name);
+
+    Ok(wallet_opts)
+}
+
+/// Parse the recipient (Address,Amount) argument from cli input
+pub(crate) fn parse_recipient(s: &str) -> Result<(Script, u64), String> {
+    let parts: Vec<_> = s.split(':').collect();
+    if parts.len() != 2 {
+        return Err("Invalid format".to_string());
+    }
+    let addr = Address::from_str(parts[0]).map_err(|e| e.to_string())?;
+    let val = u64::from_str(parts[1]).map_err(|e| e.to_string())?;
+
+    Ok((addr.script_pubkey(), val))
+}
+#[cfg(any(
+    feature = "electrum",
+    feature = "compact_filters",
+    feature = "esplora",
+    feature = "rpc"
+))]
+/// Parse the proxy (Socket:Port) argument from the cli input
+pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), String> {
+    let parts: Vec<_> = s.split(':').collect();
+    if parts.len() != 2 {
+        return Err("Invalid format".to_string());
+    }
+
+    let user = parts[0].to_string();
+    let passwd = parts[1].to_string();
+
+    Ok((user, passwd))
+}
+
+/// Parse a outpoint (Txid:Vout) argument from cli input
+pub(crate) fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
+    OutPoint::from_str(s).map_err(|e| e.to_string())
+}
+
+/// prepare bdk_cli home and wallet directory
+pub(crate) fn prepare_home_wallet_dir(wallet_name: &str) -> Result<PathBuf, Error> {
+    let mut dir = PathBuf::new();
+    dir.push(
+        &dirs_next::home_dir().ok_or_else(|| Error::Generic("home dir not found".to_string()))?,
+    );
+    dir.push(".bdk-bitcoin");
+
+    if !dir.exists() {
+        log::info!("Creating home directory {}", dir.as_path().display());
+        std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
+    }
+
+    dir.push(wallet_name);
+
+    if !dir.exists() {
+        log::info!("Creating wallet directory {}", dir.as_path().display());
+        std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?;
+    }
+
+    Ok(dir)
+}
+
+/// Prepare wallet database directory
+pub(crate) fn prepare_wallet_db_dir(wallet_name: &str) -> Result<PathBuf, Error> {
+    let mut db_dir = prepare_home_wallet_dir(wallet_name)?;
+
+    #[cfg(feature = "key-value-db")]
+    db_dir.push("wallet.sled");
+
+    #[cfg(feature = "sqlite-db")]
+    db_dir.push("wallet.sqlite");
+
+    #[cfg(not(feature = "sqlite-db"))]
+    if !db_dir.exists() {
+        log::info!("Creating database directory {}", db_dir.as_path().display());
+        std::fs::create_dir(&db_dir).map_err(|e| Error::Generic(e.to_string()))?;
+    }
+
+    Ok(db_dir)
+}
+
+/// Prepare blockchain data directory (for compact filters)
+#[cfg(feature = "compact_filters")]
+pub(crate) fn prepare_bc_dir(wallet_name: &str) -> Result<PathBuf, Error> {
+    let mut bc_dir = prepare_home_wallet_dir(wallet_name)?;
+
+    bc_dir.push("compact_filters");
+
+    if !bc_dir.exists() {
+        log::info!(
+            "Creating blockchain directory {}",
+            bc_dir.as_path().display()
+        );
+        std::fs::create_dir(&bc_dir).map_err(|e| Error::Generic(e.to_string()))?;
+    }
+
+    Ok(bc_dir)
+}
+
+/// Open the wallet database
+pub(crate) fn open_database(wallet_opts: &WalletOpts) -> Result<AnyDatabase, Error> {
+    let wallet_name = wallet_opts.wallet.as_ref().expect("wallet name");
+    let database_path = prepare_wallet_db_dir(wallet_name)?;
+
+    #[cfg(feature = "key-value-db")]
+    let config = AnyDatabaseConfig::Sled(SledDbConfiguration {
+        path: database_path
+            .into_os_string()
+            .into_string()
+            .expect("path string"),
+        tree_name: wallet_name.to_string(),
+    });
+    #[cfg(feature = "sqlite-db")]
+    let config = AnyDatabaseConfig::Sqlite(SqliteDbConfiguration {
+        path: database_path
+            .into_os_string()
+            .into_string()
+            .expect("path string"),
+    });
+    let database = AnyDatabase::from_config(&config)?;
+    log::debug!("database opened successfully");
+    Ok(database)
+}
+
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "compact_filters",
+    feature = "rpc"
+))]
+/// Create a new blockchain for a given [Backend] if available
+/// Or else create one from the wallet configuration options
+pub(crate) fn new_blockchain(
+    _network: Network,
+    wallet_opts: &WalletOpts,
+    _backend: &Backend,
+) -> Result<AnyBlockchain, Error> {
+    #[cfg(feature = "electrum")]
+    let config = {
+        let url = match _backend {
+            Backend::Electrum { electrum_url } => electrum_url.to_owned(),
+            _ => wallet_opts.electrum_opts.server.clone(),
+        };
+
+        AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
+            url,
+            socks5: wallet_opts.proxy_opts.proxy.clone(),
+            retry: wallet_opts.proxy_opts.retries,
+            timeout: wallet_opts.electrum_opts.timeout,
+            stop_gap: wallet_opts.electrum_opts.stop_gap,
+        })
+    };
+
+    #[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(),
+    });
+
+    #[cfg(feature = "compact_filters")]
+    let config = {
+        let mut peers = vec![];
+        for addrs in wallet_opts.compactfilter_opts.address.clone() {
+            for _ in 0..wallet_opts.compactfilter_opts.conn_count {
+                peers.push(BitcoinPeerConfig {
+                    address: addrs.clone(),
+                    socks5: wallet_opts.proxy_opts.proxy.clone(),
+                    socks5_credentials: wallet_opts.proxy_opts.proxy_auth.clone(),
+                })
+            }
+        }
+
+        let wallet_name = wallet_opts.wallet.as_ref().expect("wallet name");
+        AnyBlockchainConfig::CompactFilters(CompactFiltersBlockchainConfig {
+            peers,
+            network: _network,
+            storage_dir: prepare_bc_dir(wallet_name)?
+                .into_os_string()
+                .into_string()
+                .map_err(|_| Error::Generic("Internal OS_String conversion error".to_string()))?,
+            skip_blocks: Some(wallet_opts.compactfilter_opts.skip_blocks),
+        })
+    };
+
+    #[cfg(feature = "rpc")]
+    let config: AnyBlockchainConfig = {
+        let (url, auth) = match _backend {
+            Backend::Bitcoin { rpc_url, rpc_auth } => (
+                rpc_url,
+                Auth::Cookie {
+                    file: rpc_auth.into(),
+                },
+            ),
+            _ => {
+                let auth = if let Some(cookie) = &wallet_opts.rpc_opts.cookie {
+                    Auth::Cookie {
+                        file: cookie.into(),
+                    }
+                } else {
+                    Auth::UserPass {
+                        username: wallet_opts.rpc_opts.basic_auth.0.clone(),
+                        password: wallet_opts.rpc_opts.basic_auth.1.clone(),
+                    }
+                };
+                (&wallet_opts.rpc_opts.address, 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 rpc_url = "http://".to_string() + &url;
+
+        let rpc_config = RpcConfig {
+            url: rpc_url,
+            auth,
+            network: _network,
+            wallet_name,
+            skip_blocks: wallet_opts.rpc_opts.skip_blocks,
+        };
+
+        AnyBlockchainConfig::Rpc(rpc_config)
+    };
+
+    Ok(AnyBlockchain::from_config(&config)?)
+}
+
+/// Create a new wallet from given wallet configuration options
+pub(crate) fn new_wallet<D>(
+    network: Network,
+    wallet_opts: &WalletOpts,
+    database: D,
+) -> Result<Wallet<D>, Error>
+where
+    D: BatchDatabase,
+{
+    let descriptor = wallet_opts.descriptor.as_str();
+    let change_descriptor = wallet_opts.change_descriptor.as_deref();
+    let wallet = Wallet::new(descriptor, change_descriptor, network, database)?;
+    Ok(wallet)
+}