]> Untitled Git - bdk-cli/commitdiff
Update cli module to use StructOpt and add docs
authorSteve Myers <steve@notmandatory.org>
Tue, 1 Dec 2020 06:03:39 +0000 (22:03 -0800)
committerSteve Myers <steve@notmandatory.org>
Fri, 4 Dec 2020 00:18:47 +0000 (16:18 -0800)
Cargo.toml
examples/repl.rs
src/cli.rs

index 5817b9794be3c7d062b28ce10aa54dfe6c55c45a..2515e63b4f8baf0a833690e7f28e314f7389c54a 100644 (file)
@@ -27,6 +27,7 @@ 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]
@@ -45,7 +46,7 @@ electrum = ["electrum-client"]
 esplora = ["reqwest", "futures"]
 compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
 key-value-db = ["sled"]
-cli-utils = ["clap", "base64"]
+cli-utils = ["clap", "base64", "structopt"]
 async-interface = ["async-trait"]
 all-keys = ["keys-bip39"]
 keys-bip39 = ["tiny-bip39"]
index 723b67868c49be2d3357cda1ff2572285b7bb0f1..f3b2887d41c49252fb0b61e2a18b35e6e9d85b03 100644 (file)
 
 use std::fs;
 use std::path::PathBuf;
+use std::str::FromStr;
 use std::sync::Arc;
 
+use bitcoin::Network;
+use clap::AppSettings;
+use log::{debug, error, info, trace, warn, LevelFilter};
 use rustyline::error::ReadlineError;
 use rustyline::Editor;
-
-use clap::AppSettings;
-
-#[allow(unused_imports)]
-use log::{debug, error, info, trace, LevelFilter};
-
-use bitcoin::Network;
+use structopt::StructOpt;
 
 use bdk::bitcoin;
+use bdk::blockchain::esplora::EsploraBlockchainConfig;
 use bdk::blockchain::{
     AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain, ElectrumBlockchainConfig,
 };
-use bdk::cli;
+use bdk::cli::{self, WalletOpt, WalletSubCommand};
 use bdk::sled;
 use bdk::Wallet;
 
-use bdk::blockchain::esplora::EsploraBlockchainConfig;
+#[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();
@@ -61,100 +68,96 @@ fn prepare_home_dir() -> PathBuf {
 }
 
 fn main() {
-    env_logger::init();
-
-    let app = cli::make_cli_subcommands();
-    let mut repl_app = app.clone().setting(AppSettings::NoBinaryName);
+    let cli_opt: WalletOpt = WalletOpt::from_args();
 
-    let app = cli::add_global_flags(app);
+    let level = LevelFilter::from_str(cli_opt.log_level.as_str()).unwrap_or(LevelFilter::Info);
+    env_logger::builder().filter_level(level).init();
 
-    let matches = app.get_matches();
-
-    // TODO
-    // let level = match matches.occurrences_of("v") {
-    //     0 => LevelFilter::Info,
-    //     1 => LevelFilter::Debug,
-    //     _ => LevelFilter::Trace,
-    // };
-
-    let network = match matches.value_of("network") {
-        Some("regtest") => Network::Regtest,
-        Some("testnet") | _ => Network::Testnet,
-    };
+    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 = matches.value_of("descriptor").unwrap();
-    let change_descriptor = matches.value_of("change_descriptor");
+    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(matches.value_of("wallet").unwrap())
-        .unwrap();
+    let tree = database.open_tree(cli_opt.wallet).unwrap();
     debug!("database opened successfully");
 
-    let config = match matches.value_of("esplora") {
+    let config = match cli_opt.esplora {
         Some(base_url) => AnyBlockchainConfig::Esplora(EsploraBlockchainConfig {
             base_url: base_url.to_string(),
-            concurrency: matches
-                .value_of("esplora_concurrency")
-                .and_then(|v| v.parse::<u8>().ok()),
+            concurrency: Some(cli_opt.esplora_concurrency),
         }),
         None => AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
-            url: matches.value_of("server").unwrap().to_string(),
-            socks5: matches.value_of("proxy").map(ToString::to_string),
+            url: cli_opt.electrum,
+            socks5: cli_opt.proxy,
             retry: 10,
             timeout: 10,
         }),
     };
-    let wallet = Arc::new(
-        Wallet::new(
-            descriptor,
-            change_descriptor,
-            network,
-            tree,
-            AnyBlockchain::from_config(&config).unwrap(),
-        )
-        .unwrap(),
-    );
-
-    if let Some(_sub_matches) = matches.subcommand_matches("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 matches = repl_app.get_matches_from_safe_borrow(line.split(" "));
-                    if let Err(err) = matches {
-                        println!("{}", err.message);
-                        continue;
+    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;
                     }
-
-                    let result =
-                        cli::handle_matches(&Arc::clone(&wallet), matches.unwrap()).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();
-    } else {
-        let result = cli::handle_matches(&wallet, matches).unwrap();
-        println!("{}", serde_json::to_string_pretty(&result).unwrap());
+            // 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());
+        }
     }
 }
index 882f6da72396c637b799a9a768bd441c65fe4479..b4f0b7321d2336ed0d7f3df2efcff1a19c06bb94 100644 (file)
 // 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 clap::{App, Arg, ArgMatches, SubCommand};
+use structopt::StructOpt;
 
 #[allow(unused_imports)]
 use log::{debug, error, info, trace, LevelFilter};
@@ -40,6 +107,289 @@ use crate::error::Error;
 use crate::types::ScriptType;
 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 {
@@ -62,565 +412,339 @@ fn parse_outpoint(s: &str) -> Result<OutPoint, String> {
     OutPoint::from_str(s).map_err(|e| format!("{:?}", e))
 }
 
-fn recipient_validator(s: String) -> Result<(), String> {
-    parse_recipient(&s).map(|_| ())
-}
-
-fn outpoint_validator(s: String) -> Result<(), String> {
-    parse_outpoint(&s).map(|_| ())
-}
-
-pub fn make_cli_subcommands<'a, 'b>() -> App<'a, 'b> {
-    App::new("Magical Bitcoin Wallet")
-        .version(option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"))
-        .author(option_env!("CARGO_PKG_AUTHORS").unwrap_or(""))
-        .about("A modern, lightweight, descriptor-based wallet")
-        .subcommand(
-            SubCommand::with_name("get_new_address").about("Generates a new external address"),
-        )
-        .subcommand(SubCommand::with_name("sync").about("Syncs with the chosen Electrum server").arg(
-            Arg::with_name("max_addresses")
-                .required(false)
-                .takes_value(true)
-                .long("max_addresses")
-                .help("max addresses to consider"),
-        ))
-        .subcommand(
-            SubCommand::with_name("list_unspent").about("Lists the available spendable UTXOs"),
-        )
-        .subcommand(
-            SubCommand::with_name("list_transactions").about("Lists all the incoming and outgoing transactions of the wallet"),
-        )
-        .subcommand(
-            SubCommand::with_name("get_balance").about("Returns the current wallet balance"),
-        )
-        .subcommand(
-            SubCommand::with_name("create_tx")
-                .about("Creates a new unsigned tranasaction")
-                .arg(
-                    Arg::with_name("to")
-                        .long("to")
-                        .value_name("ADDRESS:SAT")
-                        .help("Adds a recipient to the transaction")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .required(true)
-                        .multiple(true)
-                        .validator(recipient_validator),
-                )
-                .arg(
-                    Arg::with_name("send_all")
-                        .short("all")
-                        .long("send_all")
-                        .help("Sends all the funds (or all the selected utxos). Requires only one recipients of value 0"),
-                )
-                .arg(
-                    Arg::with_name("enable_rbf")
-                        .short("rbf")
-                        .long("enable_rbf")
-                        .help("Enables Replace-By-Fee (BIP125)"),
-                )
-                .arg(
-                    Arg::with_name("utxos")
-                        .long("utxos")
-                        .value_name("TXID:VOUT")
-                        .help("Selects which utxos *must* be spent")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .multiple(true)
-                        .validator(outpoint_validator),
-                )
-                .arg(
-                    Arg::with_name("unspendable")
-                        .long("unspendable")
-                        .value_name("TXID:VOUT")
-                        .help("Marks an utxo as unspendable")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .multiple(true)
-                        .validator(outpoint_validator),
-                )
-                .arg(
-                    Arg::with_name("fee_rate")
-                        .short("fee")
-                        .long("fee_rate")
-                        .value_name("SATS_VBYTE")
-                        .help("Fee rate to use in sat/vbyte")
-                        .takes_value(true),
-                )
-                .arg(
-                    Arg::with_name("external_policy")
-                        .long("external_policy")
-                        .value_name("POLICY")
-                        .help("Selects which policy should be used to satisfy the external descriptor")
-                        .takes_value(true)
-                        .number_of_values(1),
-                )
-                .arg(
-                    Arg::with_name("internal_policy")
-                        .long("internal_policy")
-                        .value_name("POLICY")
-                        .help("Selects which policy should be used to satisfy the internal descriptor")
-                        .takes_value(true)
-                        .number_of_values(1),
-                )
-                .arg(
-                    Arg::with_name("offline_signer")
-                        .long("offline_signer")
-                        .help("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.")
-                        .takes_value(false),
-                ),
-        )
-        .subcommand(
-            SubCommand::with_name("bump_fee")
-                .about("Bumps the fees of an RBF transaction")
-                .arg(
-                    Arg::with_name("txid")
-                        .required(true)
-                        .takes_value(true)
-                        .short("txid")
-                        .long("txid")
-                        .help("TXID of the transaction to update"),
-                )
-                .arg(
-                    Arg::with_name("send_all")
-                        .short("all")
-                        .long("send_all")
-                        .help("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`"),
-                )
-                .arg(
-                    Arg::with_name("utxos")
-                        .long("utxos")
-                        .value_name("TXID:VOUT")
-                        .help("Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .multiple(true)
-                        .validator(outpoint_validator),
-                )
-                .arg(
-                    Arg::with_name("unspendable")
-                        .long("unspendable")
-                        .value_name("TXID:VOUT")
-                        .help("Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .multiple(true)
-                        .validator(outpoint_validator),
-                )
-                .arg(
-                    Arg::with_name("fee_rate")
-                        .required(true)
-                        .short("fee")
-                        .long("fee_rate")
-                        .value_name("SATS_VBYTE")
-                        .help("The new targeted fee rate in sat/vbyte")
-                        .takes_value(true),
-                )
-                .arg(
-                    Arg::with_name("offline_signer")
-                        .long("offline_signer")
-                        .help("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.")
-                        .takes_value(false),
-                ),
-        )
-        .subcommand(
-            SubCommand::with_name("policies")
-                .about("Returns the available spending policies for the descriptor")
-            )
-        .subcommand(
-            SubCommand::with_name("public_descriptor")
-                .about("Returns the public version of the wallet's descriptor(s)")
-            )
-        .subcommand(
-            SubCommand::with_name("sign")
-                .about("Signs and tries to finalize a PSBT")
-                .arg(
-                    Arg::with_name("psbt")
-                        .long("psbt")
-                        .value_name("BASE64_PSBT")
-                        .help("Sets the PSBT to sign")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .required(true),
-                )
-                .arg(
-                    Arg::with_name("assume_height")
-                        .long("assume_height")
-                        .value_name("HEIGHT")
-                        .help("Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .required(false),
-                ))
-        .subcommand(
-            SubCommand::with_name("broadcast")
-                .about("Broadcasts a transaction to the network. Takes either a raw transaction or a PSBT to extract")
-                .arg(
-                    Arg::with_name("psbt")
-                        .long("psbt")
-                        .value_name("BASE64_PSBT")
-                        .help("Sets the PSBT to extract and broadcast")
-                        .takes_value(true)
-                        .required_unless("tx")
-                        .number_of_values(1))
-                .arg(
-                    Arg::with_name("tx")
-                        .long("tx")
-                        .value_name("RAWTX")
-                        .help("Sets the raw transaction to broadcast")
-                        .takes_value(true)
-                        .required_unless("psbt")
-                        .number_of_values(1))
-                )
-        .subcommand(
-            SubCommand::with_name("extract_psbt")
-                .about("Extracts a raw transaction from a PSBT")
-                .arg(
-                    Arg::with_name("psbt")
-                        .long("psbt")
-                        .value_name("BASE64_PSBT")
-                        .help("Sets the PSBT to extract")
-                        .takes_value(true)
-                        .required(true)
-                        .number_of_values(1))
-                )
-        .subcommand(
-            SubCommand::with_name("finalize_psbt")
-                .about("Finalizes a psbt")
-                .arg(
-                    Arg::with_name("psbt")
-                        .long("psbt")
-                        .value_name("BASE64_PSBT")
-                        .help("Sets the PSBT to finalize")
-                        .takes_value(true)
-                        .required(true)
-                        .number_of_values(1))
-                .arg(
-                    Arg::with_name("assume_height")
-                        .long("assume_height")
-                        .value_name("HEIGHT")
-                        .help("Assume the blockchain has reached a specific height")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .required(false))
-                )
-        .subcommand(
-            SubCommand::with_name("combine_psbt")
-                .about("Combines multiple PSBTs into one")
-                .arg(
-                    Arg::with_name("psbt")
-                        .long("psbt")
-                        .value_name("BASE64_PSBT")
-                        .help("Add one PSBT to comine. This option can be repeated multiple times, one for each PSBT")
-                        .takes_value(true)
-                        .number_of_values(1)
-                        .required(true)
-                        .multiple(true))
-                )
-}
-
-pub fn add_global_flags<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
-    let mut app = app
-        .arg(
-            Arg::with_name("network")
-                .short("n")
-                .long("network")
-                .value_name("NETWORK")
-                .help("Sets the network")
-                .takes_value(true)
-                .default_value("testnet")
-                .possible_values(&["testnet", "regtest"]),
-        )
-        .arg(
-            Arg::with_name("wallet")
-                .short("w")
-                .long("wallet")
-                .value_name("WALLET_NAME")
-                .help("Selects the wallet to use")
-                .takes_value(true)
-                .default_value("main"),
-        )
-        .arg(
-            Arg::with_name("proxy")
-                .short("p")
-                .long("proxy")
-                .value_name("SERVER:PORT")
-                .help("Sets the SOCKS5 proxy for the Electrum client")
-                .takes_value(true),
-        )
-        .arg(
-            Arg::with_name("descriptor")
-                .short("d")
-                .long("descriptor")
-                .value_name("DESCRIPTOR")
-                .help("Sets the descriptor to use for the external addresses")
-                .required(true)
-                .takes_value(true),
-        )
-        .arg(
-            Arg::with_name("change_descriptor")
-                .short("c")
-                .long("change_descriptor")
-                .value_name("DESCRIPTOR")
-                .help("Sets the descriptor to use for internal addresses")
-                .takes_value(true),
-        )
-        .arg(
-            Arg::with_name("v")
-                .short("v")
-                .multiple(true)
-                .help("Sets the level of verbosity"),
-        );
-
-    if cfg!(feature = "esplora") {
-        app = app
-            .arg(
-                Arg::with_name("esplora")
-                    .short("e")
-                    .long("esplora")
-                    .value_name("ESPLORA")
-                    .help("Use the esplora server if given as parameter")
-                    .takes_value(true),
-            )
-            .arg(
-                Arg::with_name("esplora_concurrency")
-                    .long("esplora_concurrency")
-                    .value_name("ESPLORA_CONCURRENCY")
-                    .help("Concurrency of requests made to the esplora server")
-                    .default_value("4")
-                    .takes_value(true),
-            )
-    }
-
-    if cfg!(feature = "electrum") {
-        app = app.arg(
-            Arg::with_name("server")
-                .short("s")
-                .long("server")
-                .value_name("SERVER:PORT")
-                .help("Sets the Electrum server to use")
-                .takes_value(true)
-                .default_value("ssl://electrum.blockstream.info:60002"),
-        );
-    }
-
-    app.subcommand(SubCommand::with_name("repl").about("Opens an interactive shell"))
-}
-
+/// 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_matches<C, D>(
+pub fn handle_wallet_subcommand<C, D>(
     wallet: &Wallet<C, D>,
-    matches: ArgMatches<'_>,
+    wallet_subcommand: WalletSubCommand,
 ) -> Result<serde_json::Value, Error>
 where
     C: crate::blockchain::Blockchain,
     D: crate::database::BatchDatabase,
 {
-    if let Some(_sub_matches) = matches.subcommand_matches("get_new_address") {
-        Ok(json!({
-            "address": wallet.get_new_address()?
-        }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("sync") {
-        let max_addresses: Option<u32> = sub_matches
-            .value_of("max_addresses")
-            .and_then(|m| m.parse().ok());
-        maybe_await!(wallet.sync(log_progress(), max_addresses))?;
-        Ok(json!({}))
-    } else if let Some(_sub_matches) = matches.subcommand_matches("list_unspent") {
-        Ok(serde_json::to_value(&wallet.list_unspent()?)?)
-    } else if let Some(_sub_matches) = matches.subcommand_matches("list_transactions") {
-        Ok(serde_json::to_value(&wallet.list_transactions(false)?)?)
-    } else if let Some(_sub_matches) = matches.subcommand_matches("get_balance") {
-        Ok(json!({
-            "satoshi": wallet.get_balance()?
-        }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("create_tx") {
-        let recipients = sub_matches
-            .values_of("to")
-            .unwrap()
-            .map(|s| parse_recipient(s))
-            .collect::<Result<Vec<_>, _>>()
-            .map_err(Error::Generic)?;
-        let mut tx_builder = TxBuilder::new();
-
-        if sub_matches.is_present("send_all") {
-            tx_builder = tx_builder
-                .drain_wallet()
-                .set_single_recipient(recipients[0].0.clone());
-        } else {
-            tx_builder = tx_builder.set_recipients(recipients);
+    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!({}))
         }
-
-        if sub_matches.is_present("enable_rbf") {
-            tx_builder = tx_builder.enable_rbf();
+        WalletSubCommand::ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?),
+        WalletSubCommand::ListTransactions => {
+            Ok(serde_json::to_value(&wallet.list_transactions(false)?)?)
         }
-
-        if sub_matches.is_present("offline_signer") {
-            tx_builder = tx_builder
-                .add_global_xpubs()
-                .force_non_witness_utxo()
-                .include_output_redeem_witness_script();
+        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, ScriptType::External)),
+                internal_policy.map(|p| (p, ScriptType::Internal)),
+            ];
+
+            for (policy, script_type) 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, script_type);
+            }
+
+            let (psbt, details) = wallet.create_tx(tx_builder)?;
+            Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,}))
         }
-
-        if let Some(fee_rate) = sub_matches.value_of("fee_rate") {
-            let fee_rate = f32::from_str(fee_rate).map_err(|s| Error::Generic(s.to_string()))?;
-            tx_builder = tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate));
+        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,}))
         }
-        if let Some(utxos) = sub_matches.values_of("utxos") {
-            let utxos = utxos
-                .map(|i| parse_outpoint(i))
-                .collect::<Result<Vec<_>, _>>()
-                .map_err(Error::Generic)?;
-            tx_builder = tx_builder.utxos(utxos).manually_selected_only();
+        WalletSubCommand::Policies => Ok(json!({
+            "external": wallet.policies(ScriptType::External)?,
+            "internal": wallet.policies(ScriptType::Internal)?,
+        })),
+        WalletSubCommand::PublicDescriptor => Ok(json!({
+            "external": wallet.public_descriptor(ScriptType::External)?.map(|d| d.to_string()),
+            "internal": wallet.public_descriptor(ScriptType::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,}))
         }
-
-        if let Some(unspendable) = sub_matches.values_of("unspendable") {
-            let unspendable = unspendable
-                .map(|i| parse_outpoint(i))
-                .collect::<Result<Vec<_>, _>>()
-                .map_err(Error::Generic)?;
-            tx_builder = tx_builder.unspendable(unspendable);
+        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 }))
         }
-
-        let policies = vec![
-            sub_matches
-                .value_of("external_policy")
-                .map(|p| (p, ScriptType::External)),
-            sub_matches
-                .value_of("internal_policy")
-                .map(|p| (p, ScriptType::Internal)),
-        ];
-        for (policy, script_type) 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, script_type);
+        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, details) = wallet.create_tx(tx_builder)?;
-        Ok(json!({
-            "psbt": base64::encode(&serialize(&psbt)),
-            "details": details,
-        }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("bump_fee") {
-        let txid = Txid::from_str(sub_matches.value_of("txid").unwrap())
-            .map_err(|s| Error::Generic(s.to_string()))?;
-
-        let fee_rate = f32::from_str(sub_matches.value_of("fee_rate").unwrap())
-            .map_err(|s| Error::Generic(s.to_string()))?;
-        let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate));
-
-        if sub_matches.is_present("send_all") {
-            tx_builder = tx_builder.maintain_single_recipient();
+            let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?;
+            Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,}))
         }
-
-        if sub_matches.is_present("offline_signer") {
-            tx_builder = tx_builder
-                .add_global_xpubs()
-                .force_non_witness_utxo()
-                .include_output_redeem_witness_script();
+        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!({})),
+    }
+}
 
-        if let Some(utxos) = sub_matches.values_of("utxos") {
-            let utxos = utxos
-                .map(|i| parse_outpoint(i))
-                .collect::<Result<Vec<_>, _>>()
-                .map_err(Error::Generic)?;
-            tx_builder = tx_builder.utxos(utxos);
-        }
+#[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,
+        };
 
-        if let Some(unspendable) = sub_matches.values_of("unspendable") {
-            let unspendable = unspendable
-                .map(|i| parse_outpoint(i))
-                .collect::<Result<Vec<_>, _>>()
-                .map_err(Error::Generic)?;
-            tx_builder = tx_builder.unspendable(unspendable);
-        }
+        assert_eq!(expected_wallet_opt, wallet_opt);
+    }
 
-        let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?;
-        Ok(json!({
-            "psbt": base64::encode(&serialize(&psbt)),
-            "details": details,
-        }))
-    } else if let Some(_sub_matches) = matches.subcommand_matches("policies") {
-        Ok(json!({
-            "external": wallet.policies(ScriptType::External)?,
-            "internal": wallet.policies(ScriptType::Internal)?,
-        }))
-    } else if let Some(_sub_matches) = matches.subcommand_matches("public_descriptor") {
-        Ok(json!({
-            "external": wallet.public_descriptor(ScriptType::External)?.map(|d| d.to_string()),
-            "internal": wallet.public_descriptor(ScriptType::Internal)?.map(|d| d.to_string()),
-        }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("sign") {
-        let psbt = base64::decode(sub_matches.value_of("psbt").unwrap()).unwrap();
-        let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-        let assume_height = sub_matches
-            .value_of("assume_height")
-            .map(|s| s.parse().unwrap());
-        let (psbt, finalized) = wallet.sign(psbt, assume_height)?;
-        Ok(json!({
-            "psbt": base64::encode(&serialize(&psbt)),
-            "is_finalized": finalized,
-        }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("broadcast") {
-        let tx = if sub_matches.value_of("psbt").is_some() {
-            let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap();
-            let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-            psbt.extract_tx()
-        } else if sub_matches.value_of("tx").is_some() {
-            deserialize(&Vec::<u8>::from_hex(&sub_matches.value_of("tx").unwrap()).unwrap())
-                .unwrap()
-        } else {
-            panic!("Missing `psbt` and `tx` option");
+    #[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)
+            },
         };
 
-        let txid = maybe_await!(wallet.broadcast(tx))?;
-        Ok(json!({ "txid": txid }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("extract_psbt") {
-        let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap();
-        let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-        Ok(json!({
-            "raw_tx": serialize_hex(&psbt.extract_tx()),
-        }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("finalize_psbt") {
-        let psbt = base64::decode(&sub_matches.value_of("psbt").unwrap()).unwrap();
-        let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap();
-
-        let assume_height = sub_matches
-            .value_of("assume_height")
-            .map(|s| s.parse().unwrap());
-
-        let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?;
-        Ok(json!({
-            "psbt": base64::encode(&serialize(&psbt)),
-            "is_finalized": finalized,
-        }))
-    } else if let Some(sub_matches) = matches.subcommand_matches("combine_psbt") {
-        let mut psbts = sub_matches
-            .values_of("psbt")
+        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()
-            .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)) }))
-    } else {
-        Ok(serde_json::Value::Null)
+            .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);
     }
 }