]> Untitled Git - bdk-cli/commitdiff
Reorganize bdk cli module into new stand alone lib and repl bin
authorSteve Myers <steve@notmandatory.org>
Fri, 18 Dec 2020 04:38:48 +0000 (20:38 -0800)
committerSteve Myers <steve@notmandatory.org>
Fri, 18 Dec 2020 04:46:46 +0000 (20:46 -0800)
.github/pull_request_template.md
Cargo.toml
README.md [new file with mode: 0644]
examples/repl.rs [deleted file]
src/cli.rs [deleted file]
src/lib.rs [new file with mode: 0644]
src/repl.rs [new file with mode: 0644]

index 572282fccf7bbcb0d1fb8473b7874f799135e460..3dbc5fd3c94bf2b4d141167c30cd835df46f98f7 100644 (file)
@@ -13,18 +13,18 @@ of the PR were done in a specific way -->
 
 #### All Submissions:
 
-* [] I've signed all my commits
-* [] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
-* [] I ran `cargo fmt` and `cargo clippy` before committing
+* [ ] I've signed all my commits
+* [ ] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
+* [ ] I ran `cargo fmt` and `cargo clippy` before committing
 
 #### New Features:
 
-* [] I've added tests for the new feature
-* [] I've added docs for the new feature
-* [] I've updated `CHANGELOG.md`
+* [ ] I've added tests for the new feature
+* [ ] I've added docs for the new feature
+* [ ] I've updated `CHANGELOG.md`
 
 #### Bugfixes:
 
-* [] This pull request breaks the existing API
-* [] I've added tests to reproduce the issue which are now passing
-* [] I'm linking the issue being fixed by this PR
+* [ ] This pull request breaks the existing API
+* [ ] I've added tests to reproduce the issue which are now passing
+* [ ] I'm linking the issue being fixed by this PR
index 84c1fa531b01eaf426607fcbbee65e0aed122bf2..110f73e237a5d8e87218222fa3972b6cd5dfe4c9 100644 (file)
@@ -1,89 +1,32 @@
 [package]
-name = "bdk"
+name = "bdk-cli"
 version = "0.1.0"
 edition = "2018"
 authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
 
 [dependencies]
-bdk-macros = { version = "0.1.0-beta.1", path = "./macros" }
-log = "^0.4"
-miniscript = "4.0"
-bitcoin = { version = "^0.25.2", features = ["use-serde"] }
-serde = { version = "^1.0", features = ["derive"] }
+bdk = { git = "https://github.com/bitcoindevkit/bdk.git" }
+bdk-macros = { git = "https://github.com/bitcoindevkit/bdk.git" }
+structopt = "^0.3"
 serde_json = { version = "^1.0" }
-rand = "^0.7"
+log = "^0.4"
+base64 = "^0.11"
 
 # Optional dependencies
-sled = { version = "0.34", optional = true }
-electrum-client = { version = "0.4.0-beta.1", optional = true }
-reqwest = { version = "0.10", optional = true, features = ["json"] }
-futures = { version = "0.3", optional = true }
-clap = { version = "2.33", optional = true }
-base64 = { version = "^0.11", optional = true }
 async-trait = { version = "0.1", optional = true }
-rocksdb = { version = "0.14", optional = true }
-# pin cc version to 1.0.62 because 1.0.63 break rocksdb build
-cc = { version = "=1.0.62", optional = true }
-socks = { version = "0.3", optional = true }
-lazy_static = { version = "1.4", optional = true }
-tiny-bip39 = { version = "^0.8", optional = true }
-structopt = { version = "^0.3", optional = true }
-
-# Platform-specific dependencies
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-tokio = { version = "0.2", features = ["rt-core"] }
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-async-trait = "0.1"
-js-sys = "0.3"
-rand = { version = "^0.7", features = ["wasm-bindgen"] }
+rustyline = { version = "6.0", optional = true }
+dirs-next = { version = "2.0", optional = true }
+env_logger = { version = "0.7", optional = true }
+clap = { version = "2.33", optional = true }
 
 [features]
-minimal = []
-compiler = ["clap", "miniscript/compiler"]
-default = ["key-value-db", "electrum"]
-electrum = ["electrum-client"]
-esplora = ["reqwest", "futures"]
-compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
-key-value-db = ["sled"]
-cli-utils = ["clap", "base64", "structopt"]
-async-interface = ["async-trait"]
-all-keys = ["keys-bip39"]
-keys-bip39 = ["tiny-bip39"]
-
-# Debug/Test features
-debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"]
-test-electrum = ["electrum"]
-test-md-docs = ["base64", "electrum"]
+default = ["repl", "esplora"]
+repl = ["async-trait", "rustyline", "dirs-next", "env_logger", "clap", "electrum"]
+electrum = ["bdk/electrum"]
+esplora = ["bdk/esplora"]
 
-[dev-dependencies]
-bdk-testutils = { version = "0.1.0-beta.1", path = "./testutils" }
-bdk-testutils-macros = { version = "0.1.0-beta.1", path = "./testutils-macros" }
-serial_test = "0.4"
-lazy_static = "1.4"
-rustyline = "6.0"
-dirs-next = "2.0"
-env_logger = "0.7"
 
-[[example]]
+[[bin]]
 name = "repl"
-required-features = ["cli-utils"]
-[[example]]
-name = "parse_descriptor"
-[[example]]
-name = "address_validator"
-
-[[example]]
-name = "miniscriptc"
-path = "examples/compiler.rs"
-required-features = ["compiler"]
-
-[workspace]
-members = ["macros", "testutils", "testutils-macros"]
-
-# Generate docs with nightly to add the "features required" badge
-# https://stackoverflow.com/questions/61417452/how-to-get-a-feature-requirement-tag-in-the-documentation-generated-by-cargo-do
-[package.metadata.docs.rs]
-features = ["compiler", "electrum", "esplora", "compact_filters", "key-value-db", "all-keys"]
-# defines the configuration attribute `docsrs`
-rustdoc-args = ["--cfg", "docsrs"]
+path = "src/repl.rs"
+required-features = ["repl"]
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..73fc745
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+## About
+
+This project provides a command line interface (cli) Bitcoin wallet library and [`REPL`](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop)
+wallet tool based on the [bdk](https://github.com/bitcoindevkit/bdk) library.
+
+### How to run the REPL tool
+
+To run the REPL tool use the below command which returns the list of available wallet options and
+commands:
+
+```shell
+cargo run
+```
\ No newline at end of file
diff --git a/examples/repl.rs b/examples/repl.rs
deleted file mode 100644 (file)
index bd04075..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-// Magical Bitcoin Library
-// Written in 2020 by
-//     Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020 Magical Bitcoin
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-use std::fs;
-use std::path::PathBuf;
-use std::str::FromStr;
-use std::sync::Arc;
-
-use bitcoin::Network;
-use clap::AppSettings;
-use log::{debug, info, warn, LevelFilter};
-use rustyline::error::ReadlineError;
-use rustyline::Editor;
-use structopt::StructOpt;
-
-use bdk::bitcoin;
-#[cfg(feature = "esplora")]
-use bdk::blockchain::esplora::EsploraBlockchainConfig;
-use bdk::blockchain::{
-    AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain, ElectrumBlockchainConfig,
-};
-use bdk::cli::{self, WalletOpt, WalletSubCommand};
-use bdk::sled;
-use bdk::Wallet;
-
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName,
-version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
-author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
-struct ReplOpt {
-    /// Wallet sub-command
-    #[structopt(subcommand)]
-    pub subcommand: WalletSubCommand,
-}
-
-fn prepare_home_dir() -> PathBuf {
-    let mut dir = PathBuf::new();
-    dir.push(&dirs_next::home_dir().unwrap());
-    dir.push(".bdk-bitcoin");
-
-    if !dir.exists() {
-        info!("Creating home directory {}", dir.as_path().display());
-        fs::create_dir(&dir).unwrap();
-    }
-
-    dir.push("database.sled");
-    dir
-}
-
-fn main() {
-    let cli_opt: WalletOpt = WalletOpt::from_args();
-
-    let level = LevelFilter::from_str(cli_opt.log_level.as_str()).unwrap_or(LevelFilter::Info);
-    env_logger::builder().filter_level(level).init();
-
-    let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet);
-    debug!("network: {:?}", network);
-    if network == Network::Bitcoin {
-        warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
-    }
-
-    let descriptor = cli_opt.descriptor.as_str();
-    let change_descriptor = cli_opt.change_descriptor.as_deref();
-    debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
-
-    let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
-    let tree = database.open_tree(cli_opt.wallet).unwrap();
-    debug!("database opened successfully");
-
-    // Try to use Esplora config if "esplora" feature is enabled
-    #[cfg(feature = "esplora")]
-    let config_esplora: Option<AnyBlockchainConfig> = {
-        let esplora_concurrency = cli_opt.esplora_concurrency;
-        cli_opt.esplora.map(|base_url| {
-            AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
-                base_url: base_url.to_string(),
-                concurrency: Some(esplora_concurrency),
-            })
-        })
-    };
-    #[cfg(not(feature = "esplora"))]
-    let config_esplora = None;
-
-    // Fall back to Electrum config if Esplora config isn't provided
-    let config =
-        config_esplora.unwrap_or(AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
-            url: cli_opt.electrum,
-            socks5: cli_opt.proxy,
-            retry: 10,
-            timeout: 10,
-        }));
-
-    let wallet = Wallet::new(
-        descriptor,
-        change_descriptor,
-        network,
-        tree,
-        AnyBlockchain::from_config(&config).unwrap(),
-    )
-    .unwrap();
-
-    let wallet = Arc::new(wallet);
-
-    match cli_opt.subcommand {
-        WalletSubCommand::Other(external) if external.contains(&"repl".to_string()) => {
-            let mut rl = Editor::<()>::new();
-
-            // if rl.load_history("history.txt").is_err() {
-            //     println!("No previous history.");
-            // }
-
-            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> = line.split(" ").collect();
-                        let repl_subcommand: Result<ReplOpt, clap::Error> =
-                            ReplOpt::from_iter_safe(split_line);
-                        debug!("repl_subcommand = {:?}", repl_subcommand);
-
-                        if let Err(err) = repl_subcommand {
-                            println!("{}", err.message);
-                            continue;
-                        }
-
-                        let result = cli::handle_wallet_subcommand(
-                            &Arc::clone(&wallet),
-                            repl_subcommand.unwrap().subcommand,
-                        )
-                        .unwrap();
-                        println!("{}", serde_json::to_string_pretty(&result).unwrap());
-                    }
-                    Err(ReadlineError::Interrupted) => continue,
-                    Err(ReadlineError::Eof) => break,
-                    Err(err) => {
-                        println!("{:?}", err);
-                        break;
-                    }
-                }
-            }
-
-            // rl.save_history("history.txt").unwrap();
-        }
-        _ => {
-            let result = cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap();
-            println!("{}", serde_json::to_string_pretty(&result).unwrap());
-        }
-    }
-}
diff --git a/src/cli.rs b/src/cli.rs
deleted file mode 100644 (file)
index 44f11d7..0000000
+++ /dev/null
@@ -1,750 +0,0 @@
-// Magical Bitcoin Library
-// Written in 2020 by
-//     Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020 Magical Bitcoin
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-//! Command line interface
-//!
-//! This module provides a [structopt](https://docs.rs/crate/structopt) `struct` and `enum` that
-//! parse global wallet options and wallet subcommand options needed for a wallet command line
-//! interface.
-//!
-//! See the `repl.rs` example for how to use this module to create a simple command line REPL
-//! wallet application.
-//!
-//! See [`WalletOpt`] for global wallet options and [`WalletSubCommand`] for supported sub-commands.
-//!
-//! # Example
-//!
-//! ```
-//! # use bdk::bitcoin::Network;
-//! # use bdk::blockchain::esplora::EsploraBlockchainConfig;
-//! # use bdk::blockchain::{AnyBlockchain, ConfigurableBlockchain};
-//! # use bdk::blockchain::{AnyBlockchainConfig, ElectrumBlockchainConfig};
-//! # use bdk::cli::{self, WalletOpt, WalletSubCommand};
-//! # use bdk::database::MemoryDatabase;
-//! # use bdk::Wallet;
-//! # use bitcoin::hashes::core::str::FromStr;
-//! # use std::sync::Arc;
-//! # use structopt::StructOpt;
-//!
-//! // to get args from cli use:
-//! // let cli_opt = WalletOpt::from_args();
-//!
-//! let cli_args = vec!["repl", "--network", "testnet", "--descriptor",
-//!                     "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
-//!                     "sync", "--max_addresses", "50"];
-//! let cli_opt = WalletOpt::from_iter(&cli_args);
-//!
-//! let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet);
-//!
-//! let descriptor = cli_opt.descriptor.as_str();
-//! let change_descriptor = cli_opt.change_descriptor.as_deref();
-//!
-//! let database = MemoryDatabase::new();
-//!
-//! let config = match cli_opt.esplora {
-//!     Some(base_url) => AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
-//!         base_url: base_url.to_string(),
-//!         concurrency: Some(cli_opt.esplora_concurrency),
-//!     }),
-//!     None => AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
-//!         url: cli_opt.electrum,
-//!         socks5: cli_opt.proxy,
-//!         retry: 3,
-//!         timeout: 5,
-//!     }),
-//! };
-//!
-//! let wallet = Wallet::new(
-//!     descriptor,
-//!     change_descriptor,
-//!     network,
-//!     database,
-//!     AnyBlockchain::from_config(&config).unwrap(),
-//! ).unwrap();
-//!
-//! let wallet = Arc::new(wallet);
-//!
-//! let result = cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap();
-//! println!("{}", serde_json::to_string_pretty(&result).unwrap());
-//! ```
-
-use std::collections::BTreeMap;
-use std::str::FromStr;
-
-use structopt::StructOpt;
-
-#[allow(unused_imports)]
-use log::{debug, error, info, trace, LevelFilter};
-
-use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
-use bitcoin::hashes::hex::FromHex;
-use bitcoin::util::psbt::PartiallySignedTransaction;
-use bitcoin::{Address, OutPoint, Script, Txid};
-
-use crate::blockchain::log_progress;
-use crate::error::Error;
-use crate::types::KeychainKind;
-use crate::{FeeRate, TxBuilder, Wallet};
-
-/// Wallet global options and sub-command
-///
-/// A [structopt](https://docs.rs/crate/structopt) `struct` that parses wallet global options and
-/// sub-command from the command line or from a `String` vector. See [`WalletSubCommand`] for details
-/// on parsing sub-commands.
-///
-/// # Example
-///
-/// ```
-/// # use bdk::cli::{WalletOpt, WalletSubCommand};
-/// # use structopt::StructOpt;
-///
-/// let cli_args = vec!["repl", "--network", "testnet",
-///                     "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)",
-///                     "sync", "--max_addresses", "50"];
-///
-/// // to get WalletOpt from OS command line args use:
-/// // let wallet_opt = WalletOpt::from_args();
-///
-/// let wallet_opt = WalletOpt::from_iter(&cli_args);
-///
-/// let expected_wallet_opt = WalletOpt {
-///         network: "testnet".to_string(),
-///         wallet: "main".to_string(),
-///         proxy: None,
-///         descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(),
-///         change_descriptor: None,
-///         log_level: "info".to_string(),
-///         #[cfg(feature = "esplora")]
-///         esplora: None,
-///         #[cfg(feature = "esplora")]
-///         esplora_concurrency: 4,
-///         electrum: "ssl://electrum.blockstream.info:60002".to_string(),
-///         subcommand: WalletSubCommand::Sync {
-///             max_addresses: Some(50)
-///         },
-/// };
-///
-/// assert_eq!(expected_wallet_opt, wallet_opt);
-/// ```
-
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(name = "BDK Wallet",
-version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
-author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
-pub struct WalletOpt {
-    /// Sets the network
-    #[structopt(
-        name = "NETWORK",
-        short = "n",
-        long = "network",
-        default_value = "testnet"
-    )]
-    pub network: String,
-    /// Selects the wallet to use
-    #[structopt(
-        name = "WALLET_NAME",
-        short = "w",
-        long = "wallet",
-        default_value = "main"
-    )]
-    pub wallet: String,
-    #[cfg(feature = "electrum")]
-    /// Sets the SOCKS5 proxy for the Electrum client
-    #[structopt(name = "PROXY_SERVER:PORT", short = "p", long = "proxy")]
-    pub proxy: Option<String>,
-    /// Sets the descriptor to use for the external addresses
-    #[structopt(name = "DESCRIPTOR", short = "d", long = "descriptor", required = true)]
-    pub descriptor: String,
-    /// Sets the descriptor to use for internal addresses
-    #[structopt(name = "CHANGE_DESCRIPTOR", short = "c", long = "change_descriptor")]
-    pub change_descriptor: Option<String>,
-    /// Sets the logging level filter (off, error, warn, info, debug, trace)
-    #[structopt(long = "log_level", short = "l", default_value = "info")]
-    pub log_level: String,
-    #[cfg(feature = "esplora")]
-    /// Use the esplora server if given as parameter
-    #[structopt(name = "ESPLORA_URL", short = "e", long = "esplora")]
-    pub esplora: Option<String>,
-    #[cfg(feature = "esplora")]
-    /// Concurrency of requests made to the esplora server
-    #[structopt(
-        name = "ESPLORA_CONCURRENCY",
-        long = "esplora_concurrency",
-        default_value = "4"
-    )]
-    pub esplora_concurrency: u8,
-    #[cfg(feature = "electrum")]
-    /// Sets the Electrum server to use
-    #[structopt(
-        name = "SERVER:PORT",
-        short = "s",
-        long = "server",
-        default_value = "ssl://electrum.blockstream.info:60002"
-    )]
-    pub electrum: String,
-    /// Wallet sub-command
-    #[structopt(subcommand)]
-    pub subcommand: WalletSubCommand,
-}
-
-/// Wallet sub-command
-///
-/// A [structopt](https://docs.rs/crate/structopt) enum that parses wallet sub-command arguments from
-/// the command line or from a `String` vector, such as in the [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs)
-/// example app.
-///
-/// Additional "external" sub-commands can be captured via the [`WalletSubCommand::Other`] enum and passed to a
-/// custom `structopt` or another parser. See [structopt "External subcommands"](https://docs.rs/structopt/0.3.21/structopt/index.html#external-subcommands)
-/// for more information.
-///
-/// # Example
-///
-/// ```
-/// # use bdk::cli::WalletSubCommand;
-/// # use structopt::StructOpt;
-///
-/// let sync_sub_command = WalletSubCommand::from_iter(&["repl", "sync", "--max_addresses", "50"]);
-/// assert!(matches!(
-///         sync_sub_command,
-///         WalletSubCommand::Sync {
-///             max_addresses: Some(50)
-///         }
-///     ));
-///
-/// let other_sub_command = WalletSubCommand::from_iter(&["repl", "custom", "--param1", "20"]);
-/// let external_args: Vec<String> = vec!["custom".to_string(), "--param1".to_string(), "20".to_string()];
-/// assert!(matches!(
-///         other_sub_command,
-///         WalletSubCommand::Other(v) if v == external_args
-///     ));
-/// ```
-///
-/// 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 [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs)
-/// example app.
-///
-/// # Example
-/// ```
-/// # use bdk::cli::WalletSubCommand;
-/// # use structopt::StructOpt;
-/// # use clap::AppSettings;
-///
-/// #[derive(Debug, StructOpt, Clone, PartialEq)]
-/// #[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName,
-/// version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
-/// author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
-/// struct ReplOpt {
-///     /// Wallet sub-command
-///     #[structopt(subcommand)]
-///     pub subcommand: WalletSubCommand,
-/// }
-/// ```
-#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(
-    rename_all = "snake",
-    long_about = "A modern, lightweight, descriptor-based wallet"
-)]
-pub enum WalletSubCommand {
-    /// Generates a new external address
-    GetNewAddress,
-    /// Syncs with the chosen blockchain server
-    Sync {
-        /// max addresses to consider
-        #[structopt(short = "v", long = "max_addresses")]
-        max_addresses: Option<u32>,
-    },
-    /// Lists the available spendable UTXOs
-    ListUnspent,
-    /// Lists all the incoming and outgoing transactions of the wallet
-    ListTransactions,
-    /// Returns the current wallet balance
-    GetBalance,
-    /// Creates a new unsigned transaction
-    CreateTx {
-        /// Adds a recipient to the transaction
-        #[structopt(name = "ADDRESS:SAT", long = "to", required = true, parse(try_from_str = parse_recipient))]
-        recipients: Vec<(Script, u64)>,
-        /// Sends all the funds (or all the selected utxos). Requires only one recipients of value 0
-        #[structopt(short = "all", long = "send_all")]
-        send_all: bool,
-        /// Enables Replace-By-Fee (BIP125)
-        #[structopt(short = "rbf", long = "enable_rbf")]
-        enable_rbf: bool,
-        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
-        #[structopt(long = "offline_signer")]
-        offline_signer: bool,
-        /// Selects which utxos *must* be spent
-        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
-        utxos: Option<Vec<OutPoint>>,
-        /// Marks a utxo as unspendable
-        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
-        unspendable: Option<Vec<OutPoint>>,
-        /// Fee rate to use in sat/vbyte
-        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
-        fee_rate: Option<f32>,
-        /// Selects which policy should be used to satisfy the external descriptor
-        #[structopt(name = "EXT_POLICY", long = "external_policy")]
-        external_policy: Option<String>,
-        /// Selects which policy should be used to satisfy the internal descriptor
-        #[structopt(name = "INT_POLICY", long = "internal_policy")]
-        internal_policy: Option<String>,
-    },
-    /// Bumps the fees of an RBF transaction
-    BumpFee {
-        /// TXID of the transaction to update
-        #[structopt(name = "TXID", short = "txid", long = "txid")]
-        txid: String,
-        /// Allows the wallet to reduce the amount of the only output in order to increase fees. This is generally the expected behavior for transactions originally created with `send_all`
-        #[structopt(short = "all", long = "send_all")]
-        send_all: bool,
-        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
-        #[structopt(long = "offline_signer")]
-        offline_signer: bool,
-        /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used
-        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
-        utxos: Option<Vec<OutPoint>>,
-        /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees
-        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
-        unspendable: Option<Vec<OutPoint>>,
-        /// The new targeted fee rate in sat/vbyte
-        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
-        fee_rate: f32,
-    },
-    /// Returns the available spending policies for the descriptor
-    Policies,
-    /// Returns the public version of the wallet's descriptor(s)
-    PublicDescriptor,
-    /// Signs and tries to finalize a PSBT
-    Sign {
-        /// Sets the PSBT to sign
-        #[structopt(name = "BASE64_PSBT", long = "psbt")]
-        psbt: String,
-        /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor
-        #[structopt(name = "HEIGHT", long = "assume_height")]
-        assume_height: Option<u32>,
-    },
-    /// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract
-    Broadcast {
-        /// Sets the PSBT to sign
-        #[structopt(
-            name = "BASE64_PSBT",
-            long = "psbt",
-            required_unless = "RAWTX",
-            conflicts_with = "RAWTX"
-        )]
-        psbt: Option<String>,
-        /// Sets the raw transaction to broadcast
-        #[structopt(
-            name = "RAWTX",
-            long = "tx",
-            required_unless = "BASE64_PSBT",
-            conflicts_with = "BASE64_PSBT"
-        )]
-        tx: Option<String>,
-    },
-    /// Extracts a raw transaction from a PSBT
-    ExtractPsbt {
-        /// Sets the PSBT to extract
-        #[structopt(name = "BASE64_PSBT", long = "psbt")]
-        psbt: String,
-    },
-    /// Finalizes a PSBT
-    FinalizePsbt {
-        /// Sets the PSBT to finalize
-        #[structopt(name = "BASE64_PSBT", long = "psbt")]
-        psbt: String,
-        /// Assume the blockchain has reached a specific height
-        #[structopt(name = "HEIGHT", long = "assume_height")]
-        assume_height: Option<u32>,
-    },
-    /// Combines multiple PSBTs into one
-    CombinePsbt {
-        /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT
-        #[structopt(name = "BASE64_PSBT", long = "psbt", required = true)]
-        psbt: Vec<String>,
-    },
-    /// Put any extra arguments into this Vec
-    #[structopt(external_subcommand)]
-    Other(Vec<String>),
-}
-
-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]);
-    if let Err(e) = addr {
-        return Err(format!("{:?}", e));
-    }
-    let val = u64::from_str(&parts[1]);
-    if let Err(e) = val {
-        return Err(format!("{:?}", e));
-    }
-
-    Ok((addr.unwrap().script_pubkey(), val.unwrap()))
-}
-
-fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
-    OutPoint::from_str(s).map_err(|e| format!("{:?}", e))
-}
-
-/// Execute a wallet sub-command with a given [`Wallet`].
-///
-/// Wallet sub-commands are described in [`WalletSubCommand`]. See [`super::cli`] for example usage.
-#[maybe_async]
-pub fn handle_wallet_subcommand<C, D>(
-    wallet: &Wallet<C, D>,
-    wallet_subcommand: WalletSubCommand,
-) -> Result<serde_json::Value, Error>
-where
-    C: crate::blockchain::Blockchain,
-    D: crate::database::BatchDatabase,
-{
-    match wallet_subcommand {
-        WalletSubCommand::GetNewAddress => Ok(json!({"address": wallet.get_new_address()?})),
-        WalletSubCommand::Sync { max_addresses } => {
-            maybe_await!(wallet.sync(log_progress(), max_addresses))?;
-            Ok(json!({}))
-        }
-        WalletSubCommand::ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?),
-        WalletSubCommand::ListTransactions => {
-            Ok(serde_json::to_value(&wallet.list_transactions(false)?)?)
-        }
-        WalletSubCommand::GetBalance => Ok(json!({"satoshi": wallet.get_balance()?})),
-        WalletSubCommand::CreateTx {
-            recipients,
-            send_all,
-            enable_rbf,
-            offline_signer,
-            utxos,
-            unspendable,
-            fee_rate,
-            external_policy,
-            internal_policy,
-        } => {
-            let mut tx_builder = TxBuilder::new();
-
-            if send_all {
-                tx_builder = tx_builder
-                    .drain_wallet()
-                    .set_single_recipient(recipients[0].0.clone());
-            } else {
-                tx_builder = tx_builder.set_recipients(recipients);
-            }
-
-            if enable_rbf {
-                tx_builder = tx_builder.enable_rbf();
-            }
-
-            if offline_signer {
-                tx_builder = tx_builder
-                    .force_non_witness_utxo()
-                    .include_output_redeem_witness_script();
-            }
-
-            if let Some(fee_rate) = fee_rate {
-                tx_builder = tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
-            }
-
-            if let Some(utxos) = utxos {
-                tx_builder = tx_builder.utxos(utxos).manually_selected_only();
-            }
-
-            if let Some(unspendable) = unspendable {
-                tx_builder = 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().filter_map(|x| x) {
-                let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)
-                    .map_err(|s| Error::Generic(s.to_string()))?;
-                tx_builder = tx_builder.policy_path(policy, keychain);
-            }
-
-            let (psbt, details) = wallet.create_tx(tx_builder)?;
-            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
-        }
-        WalletSubCommand::BumpFee {
-            txid,
-            send_all,
-            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 = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate));
-
-            if send_all {
-                tx_builder = tx_builder.maintain_single_recipient();
-            }
-
-            if offline_signer {
-                tx_builder = tx_builder
-                    .force_non_witness_utxo()
-                    .include_output_redeem_witness_script();
-            }
-
-            if let Some(utxos) = utxos {
-                tx_builder = tx_builder.utxos(utxos);
-            }
-
-            if let Some(unspendable) = unspendable {
-                tx_builder = tx_builder.unspendable(unspendable);
-            }
-
-            let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?;
-            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
-        }
-        WalletSubCommand::Policies => Ok(json!({
-            "external": wallet.policies(KeychainKind::External)?,
-            "internal": wallet.policies(KeychainKind::Internal)?,
-        })),
-        WalletSubCommand::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()),
-        })),
-        WalletSubCommand::Sign {
-            psbt,
-            assume_height,
-        } => {
-            let psbt = base64::decode(&psbt).unwrap();
-            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-            let (psbt, finalized) = wallet.sign(psbt, assume_height)?;
-            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
-        }
-        WalletSubCommand::Broadcast { psbt, tx } => {
-            let tx = match (psbt, tx) {
-                (Some(psbt), None) => {
-                    let psbt = base64::decode(&psbt).unwrap();
-                    let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-                    psbt.extract_tx()
-                }
-                (None, Some(tx)) => deserialize(&Vec::<u8>::from_hex(&tx).unwrap()).unwrap(),
-                (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"),
-                (None, None) => panic!("Missing `psbt` and `tx` option"),
-            };
-
-            let txid = maybe_await!(wallet.broadcast(tx))?;
-            Ok(json!({ "txid": txid }))
-        }
-        WalletSubCommand::ExtractPsbt { psbt } => {
-            let psbt = base64::decode(&psbt).unwrap();
-            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-            Ok(json!({"raw_tx": serialize_hex(&psbt.extract_tx()),}))
-        }
-        WalletSubCommand::FinalizePsbt {
-            psbt,
-            assume_height,
-        } => {
-            let psbt = base64::decode(&psbt).unwrap();
-            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-
-            let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?;
-            Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
-        }
-        WalletSubCommand::CombinePsbt { psbt } => {
-            let mut psbts = psbt
-                .iter()
-                .map(|s| {
-                    let psbt = base64::decode(&s).unwrap();
-                    let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-                    psbt
-                })
-                .collect::<Vec<_>>();
-
-            let init_psbt = psbts.pop().unwrap();
-            let final_psbt = psbts
-                .into_iter()
-                .try_fold::<_, _, Result<PartiallySignedTransaction, Error>>(
-                    init_psbt,
-                    |mut acc, x| {
-                        acc.merge(x)?;
-                        Ok(acc)
-                    },
-                )?;
-
-            Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) }))
-        }
-        WalletSubCommand::Other(_) => Ok(json!({})),
-    }
-}
-
-#[cfg(test)]
-mod test {
-    use super::{WalletOpt, WalletSubCommand};
-    use bitcoin::hashes::core::str::FromStr;
-    use bitcoin::{Address, OutPoint};
-    use structopt::StructOpt;
-
-    #[test]
-    fn test_get_new_address() {
-        let cli_args = vec!["repl", "--network", "bitcoin",
-                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "--esplora", "https://blockstream.info/api/",
-                            "--esplora_concurrency", "5",
-                            "get_new_address"];
-
-        let wallet_opt = WalletOpt::from_iter(&cli_args);
-
-        let expected_wallet_opt = WalletOpt {
-            network: "bitcoin".to_string(),
-            wallet: "main".to_string(),
-            proxy: None,
-            descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-            change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-            log_level: "info".to_string(),
-            #[cfg(feature = "esplora")]
-            esplora: Some("https://blockstream.info/api/".to_string()),
-            #[cfg(feature = "esplora")]
-            esplora_concurrency: 5,
-            electrum: "ssl://electrum.blockstream.info:60002".to_string(),
-            subcommand: WalletSubCommand::GetNewAddress,
-        };
-
-        assert_eq!(expected_wallet_opt, wallet_opt);
-    }
-
-    #[test]
-    fn test_sync() {
-        let cli_args = vec!["repl", "--network", "testnet",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "sync", "--max_addresses", "50"];
-
-        let wallet_opt = WalletOpt::from_iter(&cli_args);
-
-        let expected_wallet_opt = WalletOpt {
-            network: "testnet".to_string(),
-            wallet: "main".to_string(),
-            proxy: None,
-            descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-            change_descriptor: None,
-            log_level: "info".to_string(),
-            #[cfg(feature = "esplora")]
-            esplora: None,
-            #[cfg(feature = "esplora")]
-            esplora_concurrency: 4,
-            electrum: "ssl://electrum.blockstream.info:60002".to_string(),
-            subcommand: WalletSubCommand::Sync {
-                max_addresses: Some(50)
-            },
-        };
-
-        assert_eq!(expected_wallet_opt, wallet_opt);
-    }
-
-    #[test]
-    fn test_create_tx() {
-        let cli_args = vec!["repl", "--network", "testnet", "--proxy", "127.0.0.1:9150",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
-                            "--server","ssl://electrum.blockstream.info:50002",
-                            "create_tx", "--to", "n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ:123456","mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf:78910",
-                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
-                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2"];
-
-        let wallet_opt = WalletOpt::from_iter(&cli_args);
-
-        let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ")
-            .unwrap()
-            .script_pubkey();
-        let script2 = Address::from_str("mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf")
-            .unwrap()
-            .script_pubkey();
-        let outpoint1 = OutPoint::from_str(
-            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
-        )
-        .unwrap();
-        let outpoint2 = OutPoint::from_str(
-            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2",
-        )
-        .unwrap();
-
-        let expected_wallet_opt = WalletOpt {
-            network: "testnet".to_string(),
-            wallet: "main".to_string(),
-            proxy: Some("127.0.0.1:9150".to_string()),
-            descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-            change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
-            log_level: "info".to_string(),
-            #[cfg(feature = "esplora")]
-            esplora: None,
-            #[cfg(feature = "esplora")]
-            esplora_concurrency: 4,
-            electrum: "ssl://electrum.blockstream.info:50002".to_string(),
-            subcommand: WalletSubCommand::CreateTx {
-                recipients: vec![(script1, 123456), (script2, 78910)],
-                send_all: false,
-                enable_rbf: false,
-                offline_signer: false,
-                utxos: Some(vec!(outpoint1, outpoint2)),
-                unspendable: None,
-                fee_rate: None,
-                external_policy: None,
-                internal_policy: None,
-            },
-        };
-
-        assert_eq!(expected_wallet_opt, wallet_opt);
-    }
-
-    #[test]
-    fn test_broadcast() {
-        let cli_args = vec!["repl", "--network", "testnet",
-                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
-                            "broadcast",
-                            "--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="];
-
-        let wallet_opt = WalletOpt::from_iter(&cli_args);
-
-        let expected_wallet_opt = WalletOpt {
-            network: "testnet".to_string(),
-            wallet: "main".to_string(),
-            proxy: None,
-            descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
-            change_descriptor: None,
-            log_level: "info".to_string(),
-            #[cfg(feature = "esplora")]
-            esplora: None,
-            #[cfg(feature = "esplora")]
-            esplora_concurrency: 4,
-            electrum: "ssl://electrum.blockstream.info:60002".to_string(),
-            subcommand: WalletSubCommand::Broadcast {
-                psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()),
-                tx: None
-            },
-        };
-
-        assert_eq!(expected_wallet_opt, wallet_opt);
-    }
-}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644 (file)
index 0000000..aa82585
--- /dev/null
@@ -0,0 +1,742 @@
+// Magical Bitcoin Library
+// Written in 2020 by
+//     Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020 Magical Bitcoin
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+//! BDK Command line interface
+//!
+//! This lib provides a [structopt](https://docs.rs/crate/structopt) `struct` and `enum` that
+//! parse global wallet options and wallet subcommand options needed for a wallet command line
+//! interface.
+//!
+//! See the `repl.rs` example for how to use this module to create a simple command line REPL
+//! wallet application.
+//!
+//! See [`WalletOpt`] for global wallet options and [`WalletSubCommand`] for supported sub-commands.
+//!
+//! # Example
+//!
+//! ```
+//! # use bdk::bitcoin::Network;
+//! # use bdk::blockchain::esplora::EsploraBlockchainConfig;
+//! # use bdk::blockchain::{AnyBlockchain, ConfigurableBlockchain};
+//! # use bdk::blockchain::{AnyBlockchainConfig, ElectrumBlockchainConfig};
+//! # use bdk_cli::{self, WalletOpt, 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_opt = WalletOpt::from_args();
+//!
+//! let cli_args = vec!["repl", "--network", "testnet", "--descriptor",
+//!                     "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
+//!                     "sync", "--max_addresses", "50"];
+//! let cli_opt = WalletOpt::from_iter(&cli_args);
+//!
+//! let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet);
+//!
+//! let descriptor = cli_opt.descriptor.as_str();
+//! let change_descriptor = cli_opt.change_descriptor.as_deref();
+//!
+//! let database = MemoryDatabase::new();
+//!
+//! let config = match cli_opt.esplora {
+//!     Some(base_url) => AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
+//!         base_url: base_url.to_string(),
+//!         concurrency: Some(cli_opt.esplora_concurrency),
+//!     }),
+//!     None => AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
+//!         url: cli_opt.electrum,
+//!         socks5: cli_opt.proxy,
+//!         retry: 3,
+//!         timeout: 5,
+//!     }),
+//! };
+//!
+//! let wallet = Wallet::new(
+//!     descriptor,
+//!     change_descriptor,
+//!     network,
+//!     database,
+//!     AnyBlockchain::from_config(&config).unwrap(),
+//! ).unwrap();
+//!
+//! let wallet = Arc::new(wallet);
+//!
+//! let result = bdk_cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap();
+//! println!("{}", serde_json::to_string_pretty(&result).unwrap());
+//! ```
+
+pub extern crate bdk;
+#[macro_use]
+extern crate serde_json;
+#[cfg(any(target_arch = "wasm32", feature = "async-interface"))]
+#[macro_use]
+extern crate async_trait;
+#[macro_use]
+extern crate bdk_macros;
+
+use std::collections::BTreeMap;
+use std::str::FromStr;
+
+use structopt::StructOpt;
+
+use bdk::bitcoin::consensus::encode::{deserialize, serialize, serialize_hex};
+use bdk::bitcoin::hashes::hex::FromHex;
+use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
+use bdk::bitcoin::{Address, OutPoint, Script, Txid};
+use bdk::blockchain::log_progress;
+use bdk::Error;
+use bdk::{FeeRate, KeychainKind, TxBuilder, Wallet};
+
+/// Wallet global options and sub-command
+///
+/// A [structopt](https://docs.rs/crate/structopt) `struct` that parses wallet global options and
+/// sub-command from the command line or from a `String` vector. See [`WalletSubCommand`] for details
+/// on parsing sub-commands.
+///
+/// # Example
+///
+/// ```
+/// # use structopt::StructOpt;
+/// # use bdk_cli::{WalletSubCommand, WalletOpt};
+///
+/// let cli_args = vec!["repl", "--network", "testnet",
+///                     "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)",
+///                     "sync", "--max_addresses", "50"];
+///
+/// // to get WalletOpt from OS command line args use:
+/// // let wallet_opt = WalletOpt::from_args();
+///
+/// let wallet_opt = WalletOpt::from_iter(&cli_args);
+///
+/// let expected_wallet_opt = WalletOpt {
+///         network: "testnet".to_string(),
+///         wallet: "main".to_string(),
+///         proxy: None,
+///         descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(),
+///         change_descriptor: None,
+///         log_level: "info".to_string(),
+///         #[cfg(feature = "esplora")]
+///         esplora: None,
+///         #[cfg(feature = "esplora")]
+///         esplora_concurrency: 4,
+///         electrum: "ssl://electrum.blockstream.info:60002".to_string(),
+///         subcommand: WalletSubCommand::Sync {
+///             max_addresses: Some(50)
+///         },
+/// };
+///
+/// assert_eq!(expected_wallet_opt, wallet_opt);
+/// ```
+
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+#[structopt(name = "BDK Wallet",
+version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
+author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
+pub struct WalletOpt {
+    /// Sets the network
+    #[structopt(
+        name = "NETWORK",
+        short = "n",
+        long = "network",
+        default_value = "testnet"
+    )]
+    pub network: String,
+    /// Selects the wallet to use
+    #[structopt(
+        name = "WALLET_NAME",
+        short = "w",
+        long = "wallet",
+        default_value = "main"
+    )]
+    pub wallet: String,
+    #[cfg(feature = "electrum")]
+    /// Sets the SOCKS5 proxy for the Electrum client
+    #[structopt(name = "PROXY_SERVER:PORT", short = "p", long = "proxy")]
+    pub proxy: Option<String>,
+    /// Sets the descriptor to use for the external addresses
+    #[structopt(name = "DESCRIPTOR", short = "d", long = "descriptor", required = true)]
+    pub descriptor: String,
+    /// Sets the descriptor to use for internal addresses
+    #[structopt(name = "CHANGE_DESCRIPTOR", short = "c", long = "change_descriptor")]
+    pub change_descriptor: Option<String>,
+    /// Sets the logging level filter (off, error, warn, info, debug, trace)
+    #[structopt(long = "log_level", short = "l", default_value = "info")]
+    pub log_level: String,
+    #[cfg(feature = "esplora")]
+    /// Use the esplora server if given as parameter
+    #[structopt(name = "ESPLORA_URL", short = "e", long = "esplora")]
+    pub esplora: Option<String>,
+    #[cfg(feature = "esplora")]
+    /// Concurrency of requests made to the esplora server
+    #[structopt(
+        name = "ESPLORA_CONCURRENCY",
+        long = "esplora_concurrency",
+        default_value = "4"
+    )]
+    pub esplora_concurrency: u8,
+    #[cfg(feature = "electrum")]
+    /// Sets the Electrum server to use
+    #[structopt(
+        name = "SERVER:PORT",
+        short = "s",
+        long = "server",
+        default_value = "ssl://electrum.blockstream.info:60002"
+    )]
+    pub electrum: String,
+    /// Wallet sub-command
+    #[structopt(subcommand)]
+    pub subcommand: WalletSubCommand,
+}
+
+/// Wallet sub-command
+///
+/// A [structopt](https://docs.rs/crate/structopt) enum that parses wallet sub-command arguments from
+/// the command line or from a `String` vector, such as in the [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs)
+/// example app.
+///
+/// # Example
+///
+/// ```
+/// # use bdk_cli::WalletSubCommand;
+/// # use structopt::StructOpt;
+///
+/// let sync_sub_command = WalletSubCommand::from_iter(&["repl", "sync", "--max_addresses", "50"]);
+/// assert!(matches!(
+///         sync_sub_command,
+///         WalletSubCommand::Sync {
+///             max_addresses: Some(50)
+///         }
+///     ));
+/// ```
+///
+/// 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 [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs)
+/// example app.
+///
+/// # Example
+/// ```
+/// # use bdk_cli::WalletSubCommand;
+/// # use structopt::StructOpt;
+/// # use clap::AppSettings;
+///
+/// #[derive(Debug, StructOpt, Clone, PartialEq)]
+/// #[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName,
+/// version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
+/// author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
+/// struct ReplOpt {
+///     /// Wallet sub-command
+///     #[structopt(subcommand)]
+///     pub subcommand: WalletSubCommand,
+/// }
+/// ```
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+#[structopt(
+    rename_all = "snake",
+    long_about = "A modern, lightweight, descriptor-based wallet"
+)]
+pub enum WalletSubCommand {
+    /// Generates a new external address
+    GetNewAddress,
+    /// Syncs with the chosen blockchain server
+    Sync {
+        /// max addresses to consider
+        #[structopt(short = "v", long = "max_addresses")]
+        max_addresses: Option<u32>,
+    },
+    /// Lists the available spendable UTXOs
+    ListUnspent,
+    /// Lists all the incoming and outgoing transactions of the wallet
+    ListTransactions,
+    /// Returns the current wallet balance
+    GetBalance,
+    /// Creates a new unsigned transaction
+    CreateTx {
+        /// Adds a recipient to the transaction
+        #[structopt(name = "ADDRESS:SAT", long = "to", required = true, parse(try_from_str = parse_recipient))]
+        recipients: Vec<(Script, u64)>,
+        /// Sends all the funds (or all the selected utxos). Requires only one recipients of value 0
+        #[structopt(short = "all", long = "send_all")]
+        send_all: bool,
+        /// Enables Replace-By-Fee (BIP125)
+        #[structopt(short = "rbf", long = "enable_rbf")]
+        enable_rbf: bool,
+        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
+        #[structopt(long = "offline_signer")]
+        offline_signer: bool,
+        /// Selects which utxos *must* be spent
+        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
+        utxos: Option<Vec<OutPoint>>,
+        /// Marks a utxo as unspendable
+        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
+        unspendable: Option<Vec<OutPoint>>,
+        /// Fee rate to use in sat/vbyte
+        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
+        fee_rate: Option<f32>,
+        /// Selects which policy should be used to satisfy the external descriptor
+        #[structopt(name = "EXT_POLICY", long = "external_policy")]
+        external_policy: Option<String>,
+        /// Selects which policy should be used to satisfy the internal descriptor
+        #[structopt(name = "INT_POLICY", long = "internal_policy")]
+        internal_policy: Option<String>,
+    },
+    /// Bumps the fees of an RBF transaction
+    BumpFee {
+        /// TXID of the transaction to update
+        #[structopt(name = "TXID", short = "txid", long = "txid")]
+        txid: String,
+        /// Allows the wallet to reduce the amount of the only output in order to increase fees. This is generally the expected behavior for transactions originally created with `send_all`
+        #[structopt(short = "all", long = "send_all")]
+        send_all: bool,
+        /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output.
+        #[structopt(long = "offline_signer")]
+        offline_signer: bool,
+        /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used
+        #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))]
+        utxos: Option<Vec<OutPoint>>,
+        /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees
+        #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))]
+        unspendable: Option<Vec<OutPoint>>,
+        /// The new targeted fee rate in sat/vbyte
+        #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")]
+        fee_rate: f32,
+    },
+    /// Returns the available spending policies for the descriptor
+    Policies,
+    /// Returns the public version of the wallet's descriptor(s)
+    PublicDescriptor,
+    /// Signs and tries to finalize a PSBT
+    Sign {
+        /// Sets the PSBT to sign
+        #[structopt(name = "BASE64_PSBT", long = "psbt")]
+        psbt: String,
+        /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor
+        #[structopt(name = "HEIGHT", long = "assume_height")]
+        assume_height: Option<u32>,
+    },
+    /// Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract
+    Broadcast {
+        /// Sets the PSBT to sign
+        #[structopt(
+            name = "BASE64_PSBT",
+            long = "psbt",
+            required_unless = "RAWTX",
+            conflicts_with = "RAWTX"
+        )]
+        psbt: Option<String>,
+        /// Sets the raw transaction to broadcast
+        #[structopt(
+            name = "RAWTX",
+            long = "tx",
+            required_unless = "BASE64_PSBT",
+            conflicts_with = "BASE64_PSBT"
+        )]
+        tx: Option<String>,
+    },
+    /// Extracts a raw transaction from a PSBT
+    ExtractPsbt {
+        /// Sets the PSBT to extract
+        #[structopt(name = "BASE64_PSBT", long = "psbt")]
+        psbt: String,
+    },
+    /// Finalizes a PSBT
+    FinalizePsbt {
+        /// Sets the PSBT to finalize
+        #[structopt(name = "BASE64_PSBT", long = "psbt")]
+        psbt: String,
+        /// Assume the blockchain has reached a specific height
+        #[structopt(name = "HEIGHT", long = "assume_height")]
+        assume_height: Option<u32>,
+    },
+    /// Combines multiple PSBTs into one
+    CombinePsbt {
+        /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT
+        #[structopt(name = "BASE64_PSBT", long = "psbt", required = true)]
+        psbt: Vec<String>,
+    },
+    /// Enter REPL command loop mode
+    Repl,
+}
+
+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]);
+    if let Err(e) = addr {
+        return Err(format!("{:?}", e));
+    }
+    let val = u64::from_str(&parts[1]);
+    if let Err(e) = val {
+        return Err(format!("{:?}", e));
+    }
+
+    Ok((addr.unwrap().script_pubkey(), val.unwrap()))
+}
+
+fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
+    OutPoint::from_str(s).map_err(|e| format!("{:?}", e))
+}
+
+/// Execute a wallet sub-command with a given [`Wallet`].
+///
+/// Wallet sub-commands are described in [`WalletSubCommand`]. See [`super::cli`] for example usage.
+#[maybe_async]
+pub fn handle_wallet_subcommand<C, D>(
+    wallet: &Wallet<C, D>,
+    wallet_subcommand: WalletSubCommand,
+) -> Result<serde_json::Value, Error>
+where
+    C: bdk::blockchain::Blockchain,
+    D: bdk::database::BatchDatabase,
+{
+    match wallet_subcommand {
+        WalletSubCommand::GetNewAddress => Ok(json!({"address": wallet.get_new_address()?})),
+        WalletSubCommand::Sync { max_addresses } => {
+            maybe_await!(wallet.sync(log_progress(), max_addresses))?;
+            Ok(json!({}))
+        }
+        WalletSubCommand::ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?),
+        WalletSubCommand::ListTransactions => {
+            Ok(serde_json::to_value(&wallet.list_transactions(false)?)?)
+        }
+        WalletSubCommand::GetBalance => Ok(json!({"satoshi": wallet.get_balance()?})),
+        WalletSubCommand::CreateTx {
+            recipients,
+            send_all,
+            enable_rbf,
+            offline_signer,
+            utxos,
+            unspendable,
+            fee_rate,
+            external_policy,
+            internal_policy,
+        } => {
+            let mut tx_builder = TxBuilder::new();
+
+            if send_all {
+                tx_builder = tx_builder
+                    .drain_wallet()
+                    .set_single_recipient(recipients[0].0.clone());
+            } else {
+                tx_builder = tx_builder.set_recipients(recipients);
+            }
+
+            if enable_rbf {
+                tx_builder = tx_builder.enable_rbf();
+            }
+
+            if offline_signer {
+                tx_builder = tx_builder
+                    .force_non_witness_utxo()
+                    .include_output_redeem_witness_script();
+            }
+
+            if let Some(fee_rate) = fee_rate {
+                tx_builder = tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
+            }
+
+            if let Some(utxos) = utxos {
+                tx_builder = tx_builder.utxos(utxos).manually_selected_only();
+            }
+
+            if let Some(unspendable) = unspendable {
+                tx_builder = 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().filter_map(|x| x) {
+                let policy = serde_json::from_str::<BTreeMap<String, Vec<usize>>>(&policy)
+                    .map_err(|s| Error::Generic(s.to_string()))?;
+                tx_builder = tx_builder.policy_path(policy, keychain);
+            }
+
+            let (psbt, details) = wallet.create_tx(tx_builder)?;
+            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
+        }
+        WalletSubCommand::BumpFee {
+            txid,
+            send_all,
+            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 = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate));
+
+            if send_all {
+                tx_builder = tx_builder.maintain_single_recipient();
+            }
+
+            if offline_signer {
+                tx_builder = tx_builder
+                    .force_non_witness_utxo()
+                    .include_output_redeem_witness_script();
+            }
+
+            if let Some(utxos) = utxos {
+                tx_builder = tx_builder.utxos(utxos);
+            }
+
+            if let Some(unspendable) = unspendable {
+                tx_builder = tx_builder.unspendable(unspendable);
+            }
+
+            let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?;
+            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
+        }
+        WalletSubCommand::Policies => Ok(json!({
+            "external": wallet.policies(KeychainKind::External)?,
+            "internal": wallet.policies(KeychainKind::Internal)?,
+        })),
+        WalletSubCommand::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()),
+        })),
+        WalletSubCommand::Sign {
+            psbt,
+            assume_height,
+        } => {
+            let psbt = base64::decode(&psbt).unwrap();
+            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+            let (psbt, finalized) = wallet.sign(psbt, assume_height)?;
+            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
+        }
+        WalletSubCommand::Broadcast { psbt, tx } => {
+            let tx = match (psbt, tx) {
+                (Some(psbt), None) => {
+                    let psbt = base64::decode(&psbt).unwrap();
+                    let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+                    psbt.extract_tx()
+                }
+                (None, Some(tx)) => deserialize(&Vec::<u8>::from_hex(&tx).unwrap()).unwrap(),
+                (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"),
+                (None, None) => panic!("Missing `psbt` and `tx` option"),
+            };
+
+            let txid = maybe_await!(wallet.broadcast(tx))?;
+            Ok(json!({ "txid": txid }))
+        }
+        WalletSubCommand::ExtractPsbt { psbt } => {
+            let psbt = base64::decode(&psbt).unwrap();
+            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+            Ok(json!({"raw_tx": serialize_hex(&psbt.extract_tx()),}))
+        }
+        WalletSubCommand::FinalizePsbt {
+            psbt,
+            assume_height,
+        } => {
+            let psbt = base64::decode(&psbt).unwrap();
+            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+
+            let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?;
+            Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
+        }
+        WalletSubCommand::CombinePsbt { psbt } => {
+            let mut psbts = psbt
+                .iter()
+                .map(|s| {
+                    let psbt = base64::decode(&s).unwrap();
+                    let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
+                    psbt
+                })
+                .collect::<Vec<_>>();
+
+            let init_psbt = psbts.pop().unwrap();
+            let final_psbt = psbts
+                .into_iter()
+                .try_fold::<_, _, Result<PartiallySignedTransaction, Error>>(
+                    init_psbt,
+                    |mut acc, x| {
+                        acc.merge(x)?;
+                        Ok(acc)
+                    },
+                )?;
+
+            Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) }))
+        }
+        WalletSubCommand::Repl => Ok(json!({})),
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::{WalletOpt, WalletSubCommand};
+    use bdk::bitcoin::{Address, OutPoint};
+    use std::str::FromStr;
+    use structopt::StructOpt;
+
+    #[test]
+    fn test_get_new_address() {
+        let cli_args = vec!["repl", "--network", "bitcoin",
+                            "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "--esplora", "https://blockstream.info/api/",
+                            "--esplora_concurrency", "5",
+                            "get_new_address"];
+
+        let wallet_opt = WalletOpt::from_iter(&cli_args);
+
+        let expected_wallet_opt = WalletOpt {
+            network: "bitcoin".to_string(),
+            wallet: "main".to_string(),
+            proxy: None,
+            descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+            change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+            log_level: "info".to_string(),
+            #[cfg(feature = "esplora")]
+            esplora: Some("https://blockstream.info/api/".to_string()),
+            #[cfg(feature = "esplora")]
+            esplora_concurrency: 5,
+            electrum: "ssl://electrum.blockstream.info:60002".to_string(),
+            subcommand: WalletSubCommand::GetNewAddress,
+        };
+
+        assert_eq!(expected_wallet_opt, wallet_opt);
+    }
+
+    #[test]
+    fn test_sync() {
+        let cli_args = vec!["repl", "--network", "testnet",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "sync", "--max_addresses", "50"];
+
+        let wallet_opt = WalletOpt::from_iter(&cli_args);
+
+        let expected_wallet_opt = WalletOpt {
+            network: "testnet".to_string(),
+            wallet: "main".to_string(),
+            proxy: None,
+            descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+            change_descriptor: None,
+            log_level: "info".to_string(),
+            #[cfg(feature = "esplora")]
+            esplora: None,
+            #[cfg(feature = "esplora")]
+            esplora_concurrency: 4,
+            electrum: "ssl://electrum.blockstream.info:60002".to_string(),
+            subcommand: WalletSubCommand::Sync {
+                max_addresses: Some(50)
+            },
+        };
+
+        assert_eq!(expected_wallet_opt, wallet_opt);
+    }
+
+    #[test]
+    fn test_create_tx() {
+        let cli_args = vec!["repl", "--network", "testnet", "--proxy", "127.0.0.1:9150",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
+                            "--server","ssl://electrum.blockstream.info:50002",
+                            "create_tx", "--to", "n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ:123456","mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf:78910",
+                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
+                            "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2"];
+
+        let wallet_opt = WalletOpt::from_iter(&cli_args);
+
+        let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ")
+            .unwrap()
+            .script_pubkey();
+        let script2 = Address::from_str("mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf")
+            .unwrap()
+            .script_pubkey();
+        let outpoint1 = OutPoint::from_str(
+            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1",
+        )
+        .unwrap();
+        let outpoint2 = OutPoint::from_str(
+            "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2",
+        )
+        .unwrap();
+
+        let expected_wallet_opt = WalletOpt {
+            network: "testnet".to_string(),
+            wallet: "main".to_string(),
+            proxy: Some("127.0.0.1:9150".to_string()),
+            descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+            change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()),
+            log_level: "info".to_string(),
+            #[cfg(feature = "esplora")]
+            esplora: None,
+            #[cfg(feature = "esplora")]
+            esplora_concurrency: 4,
+            electrum: "ssl://electrum.blockstream.info:50002".to_string(),
+            subcommand: WalletSubCommand::CreateTx {
+                recipients: vec![(script1, 123456), (script2, 78910)],
+                send_all: false,
+                enable_rbf: false,
+                offline_signer: false,
+                utxos: Some(vec!(outpoint1, outpoint2)),
+                unspendable: None,
+                fee_rate: None,
+                external_policy: None,
+                internal_policy: None,
+            },
+        };
+
+        assert_eq!(expected_wallet_opt, wallet_opt);
+    }
+
+    #[test]
+    fn test_broadcast() {
+        let cli_args = vec!["repl", "--network", "testnet",
+                            "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
+                            "broadcast",
+                            "--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="];
+
+        let wallet_opt = WalletOpt::from_iter(&cli_args);
+
+        let expected_wallet_opt = WalletOpt {
+            network: "testnet".to_string(),
+            wallet: "main".to_string(),
+            proxy: None,
+            descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(),
+            change_descriptor: None,
+            log_level: "info".to_string(),
+            #[cfg(feature = "esplora")]
+            esplora: None,
+            #[cfg(feature = "esplora")]
+            esplora_concurrency: 4,
+            electrum: "ssl://electrum.blockstream.info:60002".to_string(),
+            subcommand: WalletSubCommand::Broadcast {
+                psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()),
+                tx: None
+            },
+        };
+
+        assert_eq!(expected_wallet_opt, wallet_opt);
+    }
+}
diff --git a/src/repl.rs b/src/repl.rs
new file mode 100644 (file)
index 0000000..f23c384
--- /dev/null
@@ -0,0 +1,174 @@
+// Magical Bitcoin Library
+// Written in 2020 by
+//     Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020 Magical Bitcoin
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+use std::fs;
+use std::path::PathBuf;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use bitcoin::Network;
+use clap::AppSettings;
+use log::{debug, info, warn, LevelFilter};
+use rustyline::error::ReadlineError;
+use rustyline::Editor;
+use structopt::StructOpt;
+
+use bdk::bitcoin;
+#[cfg(feature = "esplora")]
+use bdk::blockchain::esplora::EsploraBlockchainConfig;
+use bdk::blockchain::{
+    AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain, ElectrumBlockchainConfig,
+};
+use bdk::sled;
+use bdk::Wallet;
+use bdk_cli::{self, WalletOpt, WalletSubCommand};
+
+#[derive(Debug, StructOpt, Clone, PartialEq)]
+#[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName,
+version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
+author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
+struct ReplOpt {
+    /// Wallet sub-command
+    #[structopt(subcommand)]
+    pub subcommand: WalletSubCommand,
+}
+
+fn prepare_home_dir() -> PathBuf {
+    let mut dir = PathBuf::new();
+    dir.push(&dirs_next::home_dir().unwrap());
+    dir.push(".bdk-bitcoin");
+
+    if !dir.exists() {
+        info!("Creating home directory {}", dir.as_path().display());
+        fs::create_dir(&dir).unwrap();
+    }
+
+    dir.push("database.sled");
+    dir
+}
+
+fn main() {
+    let cli_opt: WalletOpt = WalletOpt::from_args();
+
+    let level = LevelFilter::from_str(cli_opt.log_level.as_str()).unwrap_or(LevelFilter::Info);
+    env_logger::builder().filter_level(level).init();
+
+    let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet);
+    debug!("network: {:?}", network);
+    if network == Network::Bitcoin {
+        warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
+    }
+
+    let descriptor = cli_opt.descriptor.as_str();
+    let change_descriptor = cli_opt.change_descriptor.as_deref();
+    debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
+
+    let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
+    let tree = database.open_tree(cli_opt.wallet).unwrap();
+    debug!("database opened successfully");
+
+    // Try to use Esplora config if "esplora" feature is enabled
+    #[cfg(feature = "esplora")]
+    let config_esplora: Option<AnyBlockchainConfig> = {
+        let esplora_concurrency = cli_opt.esplora_concurrency;
+        cli_opt.esplora.map(|base_url| {
+            AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
+                base_url,
+                concurrency: Some(esplora_concurrency),
+            })
+        })
+    };
+    #[cfg(not(feature = "esplora"))]
+    let config_esplora = None;
+
+    // Fall back to Electrum config if Esplora config isn't provided
+    let config =
+        config_esplora.unwrap_or(AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
+            url: cli_opt.electrum,
+            socks5: cli_opt.proxy,
+            retry: 10,
+            timeout: 10,
+        }));
+
+    let wallet = Wallet::new(
+        descriptor,
+        change_descriptor,
+        network,
+        tree,
+        AnyBlockchain::from_config(&config).unwrap(),
+    )
+    .unwrap();
+
+    let wallet = Arc::new(wallet);
+
+    match cli_opt.subcommand {
+        WalletSubCommand::Repl => {
+            let mut rl = Editor::<()>::new();
+
+            // if rl.load_history("history.txt").is_err() {
+            //     println!("No previous history.");
+            // }
+
+            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> = line.split(' ').collect();
+                        let repl_subcommand: Result<ReplOpt, clap::Error> =
+                            ReplOpt::from_iter_safe(split_line);
+                        debug!("repl_subcommand = {:?}", repl_subcommand);
+
+                        if let Err(err) = repl_subcommand {
+                            println!("{}", err.message);
+                            continue;
+                        }
+
+                        let result = bdk_cli::handle_wallet_subcommand(
+                            &Arc::clone(&wallet),
+                            repl_subcommand.unwrap().subcommand,
+                        )
+                        .unwrap();
+                        println!("{}", serde_json::to_string_pretty(&result).unwrap());
+                    }
+                    Err(ReadlineError::Interrupted) => continue,
+                    Err(ReadlineError::Eof) => break,
+                    Err(err) => {
+                        println!("{:?}", err);
+                        break;
+                    }
+                }
+            }
+
+            // rl.save_history("history.txt").unwrap();
+        }
+        _ => {
+            let result = bdk_cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap();
+            println!("{}", serde_json::to_string_pretty(&result).unwrap());
+        }
+    }
+}