This PR moves all the components into different module.
Checkout PR description for more details.
- compiler
- compact_filters
- rpc
- - reserves
- reserves,electrum
- electrum,verify
# regtest-* features are experimental and not fully usable
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"
[[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",
"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"
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"
[[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",
"bdk",
"bdk-macros",
"bdk-reserves",
- "clap",
"dirs-next",
"electrsd",
"env_logger",
[[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",
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",
]
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",
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",
]
checksum = "65ddc41af9556a341c909bc71de33e16da52bf5f8dbda6b7a402054c60bdb722"
dependencies = [
"bitcoin_hashes 0.10.0",
- "bitcoincore-rpc",
+ "bitcoincore-rpc 0.14.0",
"flate2",
"home",
"log",
[[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"
[[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",
"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"
[[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]]
dependencies = [
"bitcoin_hashes 0.10.0",
"bitcoind",
- "electrum-client",
+ "electrum-client 0.8.0",
"log",
"nix",
"ureq 2.2.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]]
"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"
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",
[[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",
"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"
[[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]]
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"
[[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",
[[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",
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee"
dependencies = [
- "autocfg",
+ "autocfg 1.1.0",
"hashbrown",
]
[[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",
]
"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",
]
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
dependencies = [
- "autocfg",
+ "autocfg 1.1.0",
]
[[package]]
[[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]]
"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"
"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"
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]]
"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"
"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"
"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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
- "getrandom 0.2.6",
+ "getrandom 0.2.7",
"redox_syscall",
"thiserror",
]
[[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",
"serde_urlencoded",
"tokio",
"tokio-socks",
+ "tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"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"
[[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]]
"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",
]
"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"
[[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"
[[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",
[[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",
]
[[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",
[[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]]
[[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"
"qstring",
"rustls 0.19.1",
"url",
- "webpki",
+ "webpki 0.21.4",
"webpki-roots 0.21.1",
]
"serde_json",
"socks",
"url",
- "webpki",
+ "webpki 0.21.4",
"webpki-roots 0.21.1",
]
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"
[[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",
[[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",
[[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",
[[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",
[[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",
[[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",
]
[[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]]
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]]
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]]
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"
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
--- /dev/null
+// 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 },
+}
+++ /dev/null
-// 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
- );
- }
-}
--- /dev/null
+// 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#"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(),
+ "--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
+ );
+ }
+}
--- /dev/null
+// 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)
+}
+++ /dev/null
-// 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#"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"#.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), "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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: 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);
- }
-}
--- /dev/null
+// 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()),
+ }
+ },
+ }
+}
--- /dev/null
+// 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)
+}