From 1e70ff911cfde7677b377c21b3c787eb73b4531e Mon Sep 17 00:00:00 2001 From: rajarshimaitra Date: Thu, 16 Jun 2022 03:28:25 +0530 Subject: [PATCH] Refactor everything This PR moves all the components into different module. Checkout PR description for more details. --- .github/workflows/cont_integration.yml | 1 - Cargo.lock | 607 +++++++++---- Cargo.toml | 65 +- src/backend.rs | 21 + src/bdk_cli.rs | 621 ------------- src/{lib.rs => commands.rs} | 1134 +++--------------------- src/handlers.rs | 710 +++++++++++++++ src/main.rs | 108 +++ src/utils.rs | 314 +++++++ 9 files changed, 1786 insertions(+), 1795 deletions(-) create mode 100644 src/backend.rs delete mode 100644 src/bdk_cli.rs rename src/{lib.rs => commands.rs} (67%) create mode 100644 src/handlers.rs create mode 100644 src/main.rs create mode 100644 src/utils.rs diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index dc6ddb2..935b730 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -20,7 +20,6 @@ jobs: - compiler - compact_filters - rpc - - reserves - reserves,electrum - electrum,verify # regtest-* features are experimental and not fully usable diff --git a/Cargo.lock b/Cargo.lock index bcd8a8f..23157ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb833f0bf979d8475d38fbf09ed3b8a55e1885fe93ad3f93239fc6a4f17b98" +dependencies = [ + "getrandom 0.2.7", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -28,9 +39,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", "quote", @@ -48,6 +59,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dde43e75fd43e8a1bf86103336bc699aa8d17ad1be60c76c0bdfd4828e19b78" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -60,15 +80,6 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc19a4937b4fbd3fe3379793130e42060d10627a360f2127802b10b87e7baf74" -[[package]] -name = "base64" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" -dependencies = [ - "byteorder", -] - [[package]] name = "base64" version = "0.11.0" @@ -92,26 +103,28 @@ dependencies = [ [[package]] name = "bdk" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb4ab7649b7ee7e170ea6dc3b7a3fc5d97671600fe40f6ffde3abe52ef4ae5" +checksum = "ec2c4c49915b82a2576bc7dee83d2f274387904b79305e6ae8b622daa0b282c1" dependencies = [ + "ahash", "async-trait", "bdk-macros", "bip39", - "bitcoin", + "bitcoin 0.28.1", "bitcoinconsensus", - "bitcoincore-rpc", + "bitcoincore-rpc 0.15.0", "cc", - "electrum-client", + "electrum-client 0.10.1", "futures", "js-sys", "lazy_static", "log", "miniscript", - "rand", + "rand 0.7.3", "reqwest", "rocksdb", + "rusqlite", "serde", "serde_json", "sled", @@ -128,7 +141,6 @@ dependencies = [ "bdk", "bdk-macros", "bdk-reserves", - "clap", "dirs-next", "electrsd", "env_logger", @@ -154,9 +166,9 @@ dependencies = [ [[package]] name = "bdk-reserves" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ba3fcfd24950d05b99b88ab920214534e64f46a9a273c1bf90d56a64012b9f" +checksum = "64250f952cdfb473f80ec7e874677295b201fc9e0fc206d59982d8f9647c5b47" dependencies = [ "base64 0.11.0", "bdk", @@ -206,11 +218,23 @@ name = "bitcoin" version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" +dependencies = [ + "bech32", + "bitcoin_hashes 0.10.0", + "secp256k1 0.20.3", + "serde", +] + +[[package]] +name = "bitcoin" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bba324e6baf655b882df672453dbbc527bc938cadd27750ae510aaccc3a66a" dependencies = [ "base64-compat", "bech32", "bitcoin_hashes 0.10.0", - "secp256k1", + "secp256k1 0.22.1", "serde", ] @@ -245,7 +269,20 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b8d99d58466295cb2bf72c6959b784d59f8f0d6977458d2ba3eb75c834f36c3" dependencies = [ - "bitcoincore-rpc-json", + "bitcoincore-rpc-json 0.14.0", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0e67dbf7a9971e7f4276f6089e9e814ce0f624a03216b7d92d00351ae7fb3e" +dependencies = [ + "bitcoincore-rpc-json 0.15.0", "jsonrpc", "log", "serde", @@ -258,7 +295,18 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dce91de73c61f5776cf938bfa88378c5b404a70e3369b761dacbe6024fea79dd" dependencies = [ - "bitcoin", + "bitcoin 0.27.1", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e2ae16202721ba8c3409045681fac790a5ddc791f05731a2df22c0c6bffc0f1" +dependencies = [ + "bitcoin 0.28.1", "serde", "serde_json", ] @@ -270,7 +318,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65ddc41af9556a341c909bc71de33e16da52bf5f8dbda6b7a402054c60bdb722" dependencies = [ "bitcoin_hashes 0.10.0", - "bitcoincore-rpc", + "bitcoincore-rpc 0.14.0", "flate2", "home", "log", @@ -287,9 +335,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byteorder" @@ -356,9 +404,9 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clang-sys" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf6b561dcf059c85bbe388e0a7b0a1469acb3934cc0cfa148613a830629e3049" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" dependencies = [ "glob", "libc", @@ -391,6 +439,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + [[package]] name = "const_fn" version = "0.4.9" @@ -435,26 +492,26 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" dependencies = [ - "autocfg", + "autocfg 1.1.0", "cfg-if", "crossbeam-utils", - "lazy_static", "memoffset", + "once_cell", "scopeguard", ] [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" dependencies = [ "cfg-if", - "lazy_static", + "once_cell", ] [[package]] @@ -498,7 +555,7 @@ checksum = "334abee7787b76757ac34b13a9a1cbf1ef0f2da35162d3ceb95a5b0bc34df80f" dependencies = [ "bitcoin_hashes 0.10.0", "bitcoind", - "electrum-client", + "electrum-client 0.8.0", "log", "nix", "ureq 2.2.0", @@ -511,14 +568,26 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd12f125852d77980725243b2a8b3bea73cd4c7a22c33bc52b08b664c561dc7" dependencies = [ - "bitcoin", + "bitcoin 0.27.1", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "electrum-client" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef9b40020912229e947b45d91f9ff96b10d543e0eddd75ff41b9eda24d9c051" +dependencies = [ + "bitcoin 0.28.1", "log", - "rustls 0.16.0", + "rustls 0.20.6", "serde", "serde_json", "socks", - "webpki", - "webpki-roots 0.19.0", + "webpki 0.22.0", + "webpki-roots 0.22.3", ] [[package]] @@ -568,6 +637,18 @@ dependencies = [ "str-buf", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "1.7.0" @@ -585,14 +666,14 @@ checksum = "a16910e685088843d53132b04e0f10a571fdb193224fc589685b3ba1ce4cb03d" dependencies = [ "cfg-if", "libc", - "windows-sys", + "windows-sys 0.28.0", ] [[package]] name = "filetime" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", "libc", @@ -602,9 +683,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ "cfg-if", "crc32fast", @@ -638,6 +719,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "futures" version = "0.3.21" @@ -751,13 +838,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -790,6 +877,18 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" @@ -820,9 +919,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", @@ -863,9 +962,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ "bytes", "futures-channel", @@ -902,7 +1001,7 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ - "autocfg", + "autocfg 1.1.0", "hashbrown", ] @@ -938,9 +1037,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" dependencies = [ "wasm-bindgen", ] @@ -997,13 +1096,23 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290b64917f8b0cb885d9de0f9959fe1f775d7fa12f1da2db9001c1c8ab60f89d" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "lock_api" version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ - "autocfg", + "autocfg 1.1.0", "scopeguard", ] @@ -1040,7 +1149,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" dependencies = [ - "autocfg", + "autocfg 1.1.0", ] [[package]] @@ -1057,43 +1166,34 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniscript" -version = "6.1.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e292b58407dfbf1384e5aca8428d3b0f2eaa09d24cb17088f6db0b7ca31194a" +checksum = "da39fc7a8adea97a677337b0091779dd86349226b869053af496584a9b9e5847" dependencies = [ - "bitcoin", + "bitcoin 0.28.1", "serde", ] [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", + "autocfg 1.1.0", ] [[package]] name = "mio" -version = "0.7.14" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ "libc", "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.36.1", ] [[package]] @@ -1128,15 +1228,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", -] - [[package]] name = "once_cell" version = "1.12.0" @@ -1290,6 +1381,25 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.8", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + [[package]] name = "rand" version = "0.7.3" @@ -1298,9 +1408,19 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", - "rand_hc", + "rand_hc 0.2.0", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.3.1", ] [[package]] @@ -1313,6 +1433,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + [[package]] name = "rand_core" version = "0.4.2" @@ -1328,6 +1457,15 @@ dependencies = [ "getrandom 0.1.16", ] +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -1337,6 +1475,68 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.8", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -1352,7 +1552,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "redox_syscall", "thiserror", ] @@ -1385,9 +1585,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64 0.13.0", "bytes", @@ -1410,6 +1610,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-socks", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1442,6 +1643,21 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "rusqlite" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4b1eaf239b47034fb450ee9cdedd7d0226571689d8823030c4b6c2cb407152" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec 1.8.0", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1459,28 +1675,27 @@ dependencies = [ [[package]] name = "rustls" -version = "0.16.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25a18b1bf7387f0145e7f8324e700805aade3842dd3db2e74e4cdeb4677c09e" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64 0.10.1", + "base64 0.13.0", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", ] [[package]] name = "rustls" -version = "0.19.1" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" dependencies = [ - "base64 0.13.0", "log", "ring", - "sct", - "webpki", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -1529,13 +1744,34 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.4.2", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "rand 0.6.5", + "secp256k1-sys 0.5.2", "serde", ] @@ -1548,6 +1784,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + [[package]] name = "semver" version = "0.9.0" @@ -1751,9 +1996,9 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" [[package]] name = "str-buf" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strsim" @@ -1787,9 +2032,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", @@ -1899,16 +2144,17 @@ dependencies = [ [[package]] name = "tokio" -version = "1.14.1" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d0183f6f6001549ab68f8c7585093bb732beefbcf6d23a10b9b95c73a1dd49" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" dependencies = [ - "autocfg", "bytes", "libc", "memchr", "mio", + "once_cell", "pin-project-lite", + "socket2", "winapi", ] @@ -1926,9 +2172,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", @@ -1946,34 +2192,22 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ "cfg-if", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -1990,9 +2224,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" @@ -2036,7 +2270,7 @@ dependencies = [ "qstring", "rustls 0.19.1", "url", - "webpki", + "webpki 0.21.4", "webpki-roots 0.21.1", ] @@ -2055,7 +2289,7 @@ dependencies = [ "serde_json", "socks", "url", - "webpki", + "webpki 0.21.4", "webpki-roots 0.21.1", ] @@ -2077,6 +2311,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vec_map" version = "0.8.2" @@ -2107,15 +2347,15 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2123,9 +2363,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" dependencies = [ "bumpalo", "lazy_static", @@ -2138,9 +2378,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" dependencies = [ "cfg-if", "js-sys", @@ -2150,9 +2390,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2160,9 +2400,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" dependencies = [ "proc-macro2", "quote", @@ -2173,15 +2413,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" dependencies = [ "js-sys", "wasm-bindgen", @@ -2198,12 +2438,13 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.19.0" +name = "webpki" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8eff4b7516a57307f9349c64bf34caa34b940b66fed4b2fb3136cb7386e5739" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ - "webpki", + "ring", + "untrusted", ] [[package]] @@ -2212,7 +2453,16 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +dependencies = [ + "webpki 0.22.0", ] [[package]] @@ -2252,11 +2502,24 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82ca39602d5cbfa692c4b67e3bcbb2751477355141c1ed434c94da4186836ff6" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.28.0", + "windows_i686_gnu 0.28.0", + "windows_i686_msvc 0.28.0", + "windows_x86_64_gnu 0.28.0", + "windows_x86_64_msvc 0.28.0", +] + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] [[package]] @@ -2265,30 +2528,60 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52695a41e536859d5308cc613b4a022261a274390b25bd29dfff4bf08505f3c2" +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + [[package]] name = "windows_i686_gnu" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f54725ac23affef038fecb177de6c9bf065787c2f432f79e3c373da92f3e1d8a" +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + [[package]] name = "windows_i686_msvc" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d5158a43cc43623c0729d1ad6647e62fa384a3d135fd15108d37c683461f64" +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + [[package]] name = "windows_x86_64_gnu" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc31f409f565611535130cfe7ee8e6655d3fa99c1c61013981e491921b5ce954" +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + [[package]] name = "windows_x86_64_msvc" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f2b8c7cbd3bfdddd9ab98769f9746a7fad1bca236554cd032b78d768bc0e89f" +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 4f95791..f0a6c1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,59 +2,68 @@ name = "bdk-cli" version = "0.5.0" edition = "2018" -authors = ["Alekos Filini ", "Riccardo Casatta ", "Steve Myers "] +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 ` to operate the background node. +# +# This is most useful for integrations testing as well as quick demo testing +# by devs using bdk and various types of background nodes. regtest-node = [] regtest-bitcoin = ["regtest-node" , "rpc", "electrsd"] regtest-electrum = ["regtest-node", "electrum", "electrsd/electrs_0_8_10"] regtest-esplora-ureq = ["regtest-node", "esplora-ureq", "electrsd/esplora_a33e97e1"] -regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"] - -[[bin]] -name = "bdk-cli" -path = "src/bdk_cli.rs" -required-features = ["cli"] - -[package.metadata.docs.rs] -features = ["compiler", "electrum"] +regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"] \ No newline at end of file diff --git a/src/backend.rs b/src/backend.rs new file mode 100644 index 0000000..f965b43 --- /dev/null +++ b/src/backend.rs @@ -0,0 +1,21 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! The Backend +//! +//! This module defines the Backend struct and associated operations + +#[allow(dead_code)] +// Different Backend types activated with `regtest-*` mode. +// If `regtest-*` feature not activated, then default is `None`. +pub enum Backend { + None, + Bitcoin { rpc_url: String, rpc_auth: String }, + Electrum { electrum_url: String }, + Esplora { esplora_url: String }, +} diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs deleted file mode 100644 index b3b0371..0000000 --- a/src/bdk_cli.rs +++ /dev/null @@ -1,621 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by -// Alekos Filini -// -// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -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 { - 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 { - 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 { - 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 { - 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 { - #[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( - network: Network, - wallet_opts: &WalletOpts, - database: D, -) -> Result, 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 { - 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 { - 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::, Error>>()?; - let repl_subcommand = ReplSubCommand::from_iter_safe(split_line); - if let Err(err) = repl_subcommand { - println!("{}", err); - continue; - } - // if error will be printed above - let repl_subcommand = repl_subcommand.unwrap(); - debug!("repl_subcommand = {:?}", repl_subcommand); - - let result = match repl_subcommand { - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "compact_filters", - feature = "rpc" - ))] - ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => { - let blockchain = new_blockchain(network, &wallet_opts, &_backend)?; - bdk_cli::handle_online_wallet_subcommand( - &wallet, - &blockchain, - online_subcommand, - ) - } - ReplSubCommand::OfflineWalletSubCommand(offline_subcommand) => { - bdk_cli::handle_offline_wallet_subcommand( - &wallet, - &wallet_opts, - offline_subcommand, - ) - } - ReplSubCommand::KeySubCommand(key_subcommand) => { - bdk_cli::handle_key_subcommand(network, key_subcommand) - } - ReplSubCommand::Exit => break, - }; - - println!("{}", serde_json::to_string_pretty(&result?)?); - } - Err(ReadlineError::Interrupted) => continue, - Err(ReadlineError::Eof) => break, - Err(err) => { - println!("{:?}", err); - break; - } - } - } - - "Exiting REPL".to_string() - } - #[cfg(all(feature = "reserves", feature = "electrum"))] - CliSubCommand::ExternalReserves { - message, - psbt, - confirmations, - addresses, - electrum_opts, - } => { - let result = bdk_cli::handle_ext_reserves_subcommand( - network, - message, - psbt, - confirmations, - addresses, - electrum_opts, - )?; - serde_json::to_string_pretty(&result)? - } - }; - Ok(result) -} - -#[cfg(test)] -mod test { - #[cfg(feature = "repl")] - use crate::REPL_LINE_SPLIT_REGEX; - #[cfg(feature = "repl")] - use regex::Regex; - - #[cfg(feature = "repl")] - #[test] - fn test_regex_double_quotes() { - let split_regex = Regex::new(REPL_LINE_SPLIT_REGEX).unwrap(); - let line = r#"restore -m "word1 word2 word3" -p 'test! 123 -test' "#; - let split_line: Vec<&str> = split_regex - .captures_iter(&line) - .map(|c| { - c.get(1) - .or_else(|| c.get(2)) - .or_else(|| c.get(3)) - .unwrap() - .as_str() - }) - .collect(); - assert_eq!( - vec!( - "restore", - "-m", - "word1 word2 word3", - "-p", - "test! 123 -test" - ), - split_line - ); - } - - #[cfg(feature = "repl")] - #[test] - fn test_regex_single_quotes() { - let split_regex = Regex::new(REPL_LINE_SPLIT_REGEX).unwrap(); - let line = r#"restore -m 'word1 word2 word3' -p "test *123 -test" "#; - let split_line: Vec<&str> = split_regex - .captures_iter(&line) - .map(|c| { - c.get(1) - .or_else(|| c.get(2)) - .or_else(|| c.get(3)) - .unwrap() - .as_str() - }) - .collect(); - assert_eq!( - vec!( - "restore", - "-m", - "word1 word2 word3", - "-p", - "test *123 -test" - ), - split_line - ); - } -} diff --git a/src/lib.rs b/src/commands.rs similarity index 67% rename from src/lib.rs rename to src/commands.rs index 108fe12..015c3fd 100644 --- a/src/lib.rs +++ b/src/commands.rs @@ -1,8 +1,4 @@ -// Bitcoin Dev Kit -// Written in 2020 by -// Alekos Filini -// -// Copyright (c) 2020-2022 Bitcoin Dev Kit Developers +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers // // This file is licensed under the Apache License, Version 2.0 or the MIT license @@ -10,249 +6,37 @@ // 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(); +//! bdk-cli Command structure //! -//! 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; +//! This module defines all the bdk-cli commands using [structopt] -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}; +use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; +use bdk::bitcoin::{Address, Network, OutPoint, Script}; + #[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; +use crate::utils::parse_proxy_auth; +use crate::utils::{parse_outpoint, parse_recipient}; -/// 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; +#[derive(PartialEq, Clone, Debug, StructOpt)] +/// The BDK Command Line Wallet App /// -/// let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", -/// "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)", -/// "sync"]; +/// 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. /// -/// // to get CliOpts from the OS command line args use: -/// // let cli_opts = CliOpts::from_args(); -/// let cli_opts = CliOpts::from_iter(&cli_args); +/// But this is not just any toy. +/// bdk-cli is also a fully functioning Bitcoin wallet with taproot support! /// -/// 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"), +/// 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 @@ -263,34 +47,33 @@ pub struct CliOpts { 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" -)] +#[structopt(rename_all = "snake")] pub enum CliSubCommand { - /// Wallet options and sub-commands - #[structopt(long_about = "Wallet mode")] + /// 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 sub-commands - #[structopt(long_about = "Key management mode")] + /// 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, @@ -306,16 +89,19 @@ pub enum CliSubCommand { #[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 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 external sub-commands + /// 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"))] - #[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)] @@ -334,16 +120,6 @@ pub enum CliSubCommand { }, } -#[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( @@ -358,84 +134,6 @@ pub enum WalletSubCommand { 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 @@ -467,15 +165,6 @@ pub struct WalletOpts { 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 { @@ -497,9 +186,6 @@ pub struct ProxyOpts { 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 { @@ -557,9 +243,6 @@ pub struct RpcOpts { pub skip_blocks: Option, } -/// Electrum options -/// -/// Electrum blockchain client information used by [`OnlineWalletSubCommand`]s. #[cfg(feature = "electrum")] #[derive(Debug, StructOpt, Clone, PartialEq)] pub struct ElectrumOpts { @@ -585,15 +268,6 @@ pub struct ElectrumOpts { 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 { @@ -624,63 +298,6 @@ pub struct EsploraOpts { 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 { @@ -785,17 +402,6 @@ pub enum OfflineWalletSubCommand { }, } -#[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( @@ -848,352 +454,7 @@ pub enum OnlineWalletSubCommand { }, } -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::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( - wallet: &Wallet, - wallet_opts: &WalletOpts, - offline_subcommand: OfflineWalletSubCommand, -) -> Result -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::>>(&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::, 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>( - 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( - wallet: &Wallet, - blockchain: &B, - online_subcommand: OnlineWalletSubCommand, -) -> Result -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::::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 { @@ -1230,209 +491,33 @@ pub enum KeySubCommand { }, } -/// Execute a key sub-command -/// -/// Key sub-commands are described in [`KeySubCommand`]. -pub fn handle_key_subcommand( - network: Network, - subcommand: KeySubCommand, -) -> Result { - 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 = - 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 { - let policy = Concrete::::from_str(policy.as_str())?; - let legacy_policy: Miniscript = policy - .compile() - .map_err(|e| Error::Generic(e.to_string()))?; - let segwit_policy: Miniscript = 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, - electrum_opts: ElectrumOpts, -) -> Result { - 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::>, 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, -) -> Result, 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(feature = "repl")] +#[derive(Debug, StructOpt, Clone, PartialEq)] +pub enum ReplSubCommand { #[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; + #[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::{handle_ext_reserves_subcommand, handle_online_wallet_subcommand}; - use crate::{handle_key_subcommand, CliSubCommand, KeySubCommand, WalletSubCommand}; + 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}; @@ -1446,6 +531,25 @@ mod test { 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", @@ -1496,7 +600,7 @@ mod test { skip_blocks: None, }, }, - subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + subcommand: OfflineWalletSubCommand(GetNewAddress), }, }; @@ -1535,7 +639,7 @@ mod test { retries: 3, }, }, - subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + subcommand: OfflineWalletSubCommand(GetNewAddress), }, }; @@ -1575,7 +679,7 @@ mod test { retries: 5, } }, - subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + subcommand: OfflineWalletSubCommand(GetNewAddress), }, }; @@ -1615,7 +719,7 @@ mod test { retries: 5, } }, - subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + subcommand: OfflineWalletSubCommand(GetNewAddress), }, }; @@ -1651,7 +755,7 @@ mod test { skip_blocks: Some(5), }, }, - subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + subcommand: OfflineWalletSubCommand(GetNewAddress), }, }; @@ -1692,7 +796,7 @@ mod test { retries: 5, } }, - subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + subcommand: OfflineWalletSubCommand(GetNewAddress), }, }; @@ -1754,7 +858,7 @@ mod test { skip_blocks: None, }, }, - subcommand: WalletSubCommand::OnlineWalletSubCommand(Sync), + subcommand: OnlineWalletSubCommand(Sync), }, }; @@ -1897,7 +1001,7 @@ mod test { retries: 5, } }, - subcommand: WalletSubCommand::OfflineWalletSubCommand(BumpFee { + subcommand: OfflineWalletSubCommand(BumpFee { txid: "35aab0d0213f8996f9e236a28630319b93109754819e8abf48a0835708d33506".to_string(), shrink_address: Some(Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap()), offline_signer: false, @@ -1967,7 +1071,7 @@ mod test { skip_blocks: None, }, }, - subcommand: WalletSubCommand::OnlineWalletSubCommand(Broadcast { + subcommand: OnlineWalletSubCommand(Broadcast { psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()), tx: None }), @@ -2123,7 +1227,7 @@ mod test { retries: 5, }, }, - subcommand: WalletSubCommand::OnlineWalletSubCommand(ProduceProof { + subcommand: OnlineWalletSubCommand(ProduceProof { msg: message.to_string(), }), }, @@ -2174,7 +1278,7 @@ mod test { retries: 5, }, }, - subcommand: WalletSubCommand::OnlineWalletSubCommand(VerifyProof { + subcommand: OnlineWalletSubCommand(VerifyProof { psbt: psbt.to_string(), msg: message.to_string(), confirmations: 6, @@ -2229,7 +1333,7 @@ mod test { retries: 5, }, }, - subcommand: WalletSubCommand::OnlineWalletSubCommand(VerifyProof { + subcommand: OnlineWalletSubCommand(VerifyProof { psbt: psbt.to_string(), msg: message.to_string(), confirmations: 0, @@ -2330,7 +1434,7 @@ mod test { let wallet_subcmd = match cli_opts.subcommand { CliSubCommand::Wallet { wallet_opts: _, - subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + subcommand: OnlineWalletSubCommand(online_subcommand), } => online_subcommand, _ => panic!("unexpected subcommand"), }; @@ -2368,7 +1472,7 @@ mod test { let wallet_subcmd = match cli_opts.subcommand { CliSubCommand::Wallet { wallet_opts: _, - subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + subcommand: OnlineWalletSubCommand(online_subcommand), } => online_subcommand, _ => panic!("unexpected subcommand"), }; @@ -2433,4 +1537,58 @@ mod test { .unwrap(); assert!(spendable > 0); } + + #[cfg(feature = "repl")] + #[test] + fn test_regex_double_quotes() { + let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX).unwrap(); + let line = r#"restore -m "word1 word2 word3" -p 'test! 123 -test' "#; + let split_line: Vec<&str> = split_regex + .captures_iter(&line) + .map(|c| { + c.get(1) + .or_else(|| c.get(2)) + .or_else(|| c.get(3)) + .unwrap() + .as_str() + }) + .collect(); + assert_eq!( + vec!( + "restore", + "-m", + "word1 word2 word3", + "-p", + "test! 123 -test" + ), + split_line + ); + } + + #[cfg(feature = "repl")] + #[test] + fn test_regex_single_quotes() { + let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX).unwrap(); + let line = r#"restore -m 'word1 word2 word3' -p "test *123 -test" "#; + let split_line: Vec<&str> = split_regex + .captures_iter(&line) + .map(|c| { + c.get(1) + .or_else(|| c.get(2)) + .or_else(|| c.get(3)) + .unwrap() + .as_str() + }) + .collect(); + assert_eq!( + vec!( + "restore", + "-m", + "word1 word2 word3", + "-p", + "test *123 -test" + ), + split_line + ); + } } diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..4452997 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,710 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! 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( + wallet: &Wallet, + wallet_opts: &WalletOpts, + offline_subcommand: OfflineWalletSubCommand, +) -> Result +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::>>(&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::, 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>( + 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( + wallet: &Wallet, + blockchain: &B, + online_subcommand: OnlineWalletSubCommand, +) -> Result +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::::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 { + 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 = + 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 { + let policy = Concrete::::from_str(policy.as_str())?; + let legacy_policy: Miniscript = policy + .compile() + .map_err(|e| Error::Generic(e.to_string()))?; + let segwit_policy: Miniscript = 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, + electrum_opts: ElectrumOpts, +) -> Result { + 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::>, 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, +) -> Result, 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 { + 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::, Error>>()?; + let repl_subcommand = ReplSubCommand::from_iter_safe(split_line); + if let Err(err) = repl_subcommand { + println!("{}", err); + continue; + } + // if error will be printed above + let repl_subcommand = repl_subcommand.unwrap(); + log::debug!("repl_subcommand = {:?}", repl_subcommand); + + let result = match repl_subcommand { + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "compact_filters", + feature = "rpc" + ))] + ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => { + let blockchain = new_blockchain(network, &wallet_opts, &_backend)?; + handle_online_wallet_subcommand( + &wallet, + &blockchain, + online_subcommand, + ) + } + ReplSubCommand::OfflineWalletSubCommand(offline_subcommand) => { + handle_offline_wallet_subcommand( + &wallet, + &wallet_opts, + offline_subcommand, + ) + } + ReplSubCommand::KeySubCommand(key_subcommand) => { + handle_key_subcommand(network, key_subcommand) + } + ReplSubCommand::Exit => break, + }; + + println!("{}", serde_json::to_string_pretty(&result?)?); + } + Err(ReadlineError::Interrupted) => continue, + Err(ReadlineError::Eof) => break, + Err(err) => { + println!("{:?}", err); + break; + } + } + } + + "Exiting REPL".to_string() + } + #[cfg(all(feature = "reserves", feature = "electrum"))] + CliSubCommand::ExternalReserves { + message, + psbt, + confirmations, + addresses, + electrum_opts, + } => { + let result = handle_ext_reserves_subcommand( + network, + message, + psbt, + confirmations, + addresses, + electrum_opts, + )?; + serde_json::to_string_pretty(&result)? + } + }; + Ok(result) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..154b42a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,108 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! BDK CLI APP +//! +//! This module describes the app's main() function + +mod backend; +mod commands; +mod handlers; +mod utils; + +use backend::Backend; + +use bitcoin::Network; + +use log::{debug, error, warn}; + +use crate::commands::CliOpts; +use crate::handlers::*; +use bdk::{bitcoin, Error}; +use structopt::StructOpt; + +#[cfg(feature = "repl")] +const REPL_LINE_SPLIT_REGEX: &str = r#""([^"]*)"|'([^']*)'|([\w\-]+)"#; + +fn main() { + env_logger::init(); + + let cli_opts: CliOpts = CliOpts::from_args(); + + let network = cli_opts.network; + debug!("network: {:?}", network); + if network == Network::Bitcoin { + warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.") + } + + #[cfg(feature = "regtest-node")] + let bitcoind = { + if network != Network::Regtest { + error!("Do not override default network value for `regtest-node` features"); + } + let bitcoind_conf = electrsd::bitcoind::Conf::default(); + let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path() + .expect("We should always have downloaded path"); + electrsd::bitcoind::BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap() + }; + + #[cfg(feature = "regtest-bitcoin")] + let backend = { + Backend::Bitcoin { + rpc_url: bitcoind.params.rpc_socket.to_string(), + rpc_auth: bitcoind + .params + .cookie_file + .clone() + .into_os_string() + .into_string() + .unwrap(), + } + }; + + #[cfg(feature = "regtest-electrum")] + let (_electrsd, backend) = { + let elect_conf = electrsd::Conf::default(); + let elect_exe = + electrsd::downloaded_exe_path().expect("We should always have downloaded path"); + let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); + let backend = Backend::Electrum { + electrum_url: electrsd.electrum_url.clone(), + }; + (electrsd, backend) + }; + + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + let (_electrsd, backend) = { + let mut elect_conf = electrsd::Conf::default(); + elect_conf.http_enabled = true; + let elect_exe = + electrsd::downloaded_exe_path().expect("Electrsd downloaded binaries not found"); + let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); + let backend = Backend::Esplora { + esplora_url: electrsd + .esplora_url + .clone() + .expect("Esplora port not open in electrum"), + }; + (electrsd, backend) + }; + + #[cfg(not(feature = "regtest-node"))] + let backend = Backend::None; + + match handle_command(cli_opts, network, backend) { + Ok(result) => println!("{}", result), + Err(e) => { + match e { + Error::ChecksumMismatch => error!("Descriptor checksum mismatch. Are you using a different descriptor for an already defined wallet name? (if you are not specifying the wallet name it is automatically named based on the descriptor)"), + e => error!("{}", e.to_string()), + } + }, + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..8e4251a --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,314 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! 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 { + 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::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 { + 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 { + 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 { + 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 { + 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 { + #[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( + network: Network, + wallet_opts: &WalletOpts, + database: D, +) -> Result, 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) +} -- 2.49.0