From 2f5f76638cb7a486924df0aac6e43dd929aaca05 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 22:10:43 -0800 Subject: [PATCH 01/18] Add regex dependency, remove esplora from default feature --- Cargo.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 958b00c..0bfd826 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT" [dependencies] -bdk = { version = "^0.3", default-features = false } +bdk = { git = "https://github.com/afilini/bdk.git", branch = "fix/better-derivable-key-api", features = ["all-keys"]} bdk-macros = "^0.2" structopt = "^0.3" serde_json = { version = "^1.0" } @@ -25,10 +25,11 @@ 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 } +regex = {version = "1", optional = true } [features] -default = ["repl", "esplora", "electrum", "bdk/key-value-db"] -repl = ["async-trait", "rustyline", "dirs-next", "env_logger", "clap", "electrum"] +default = ["repl", "electrum"] +repl = ["async-trait", "rustyline", "dirs-next", "env_logger", "clap", "regex"] electrum = ["bdk/electrum"] esplora = ["bdk/esplora"] compiler = ["bdk/compiler"] From 5cd79bd756619cab230e850346b5514f9880db24 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 22:45:58 -0800 Subject: [PATCH 02/18] Add CliOpts, CliSubCommand, WalletSubCommand, WalletOpts --- src/lib.rs | 286 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 201 insertions(+), 85 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bc8f0c4..955f95f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,25 +22,29 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -//! BDK Command line interface +//! 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. +//! This lib provides [`structopt`] structs and enums that parse CLI options and sub-commands from +//! the command line or from a `String` vector that can be used to access features of the [`bdk`] +//! library. Functions are also provided to handle subcommands and options and provide results via +//! the [`bdk`] lib. //! -//! See the `bdk-cli` example bin for how to use this module to create a simple command line -//! wallet application. +//! See the [`bdk-cli`] example bin for how to use this lib to create a simple command line +//! application that demonstrates [`bdk`] wallet and key management features. //! -//! See [`WalletOpt`] for global wallet options and [`WalletSubCommand`] for supported sub-commands. +//! See [`CliOpts`] for global cli options and [`CliSubCommand`] for supported top level sub-commands. +//! +//! [`structopt`]: https://docs.rs/crate/structopt +//! [`bdk`]: https://github.com/bitcoindevkit/bdk +//! [`bdk-cli`]: https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdk_cli.rs //! //! # Example //! //! ``` //! # 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_cli::{self, CliOpts, CliSubCommand, WalletOpts, OfflineWalletSubCommand, WalletSubCommand}; //! # use bdk::database::MemoryDatabase; //! # use bdk::Wallet; //! # use std::sync::Arc; @@ -48,49 +52,46 @@ //! # use std::str::FromStr; //! //! // to get args from cli use: -//! // let cli_opt = WalletOpt::from_args(); +//! // let cli_opts = CliOpts::from_args(); //! -//! let cli_args = vec!["bdk-cli", "--network", "testnet", "--descriptor", +//! let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", "--descriptor", //! "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", //! "sync", "--max_addresses", "50"]; -//! let cli_opt = WalletOpt::from_iter(&cli_args); //! -//! let network = cli_opt.network; +//! let cli_opts = CliOpts::from_iter(&cli_args); +//! let network = cli_opts.network; //! -//! let descriptor = cli_opt.descriptor.as_str(); -//! let change_descriptor = cli_opt.change_descriptor.as_deref(); +//! if let CliSubCommand::Wallet { +//! wallet_opts, +//! subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand) +//! } = cli_opts.subcommand { //! -//! let database = MemoryDatabase::new(); +//! let descriptor = wallet_opts.descriptor.as_str(); +//! let change_descriptor = wallet_opts.change_descriptor.as_deref(); //! -//! 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: None, -//! }), -//! }; +//! let database = MemoryDatabase::new(); //! -//! let wallet = Wallet::new( -//! descriptor, -//! change_descriptor, -//! network, -//! database, -//! AnyBlockchain::from_config(&config).unwrap(), -//! ).unwrap(); +//! let config = AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { +//! url: wallet_opts.electrum, +//! socks5: wallet_opts.proxy, +//! retry: 3, +//! timeout: None, +//! }); //! -//! let wallet = Arc::new(wallet); +//! let wallet = Wallet::new( +//! descriptor, +//! change_descriptor, +//! network, +//! database, +//! AnyBlockchain::from_config(&config).unwrap(), +//! ).unwrap(); //! -//! let result = bdk_cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap(); -//! println!("{}", serde_json::to_string_pretty(&result).unwrap()); +//! let result = bdk_cli::handle_online_wallet_subcommand(&wallet, online_subcommand).unwrap(); +//! println!("{}", serde_json::to_string_pretty(&result).unwrap()); +//! } //! ``` pub extern crate bdk; -pub extern crate structopt; #[macro_use] extern crate serde_json; #[cfg(any(target_arch = "wasm32", feature = "async-interface"))] @@ -104,60 +105,75 @@ use std::str::FromStr; use structopt::StructOpt; +use crate::OfflineWalletSubCommand::*; +use crate::OnlineWalletSubCommand::*; use bdk::bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; use bdk::bitcoin::hashes::hex::FromHex; +use bdk::bitcoin::secp256k1::Secp256k1; +use bdk::bitcoin::util::bip32::ExtendedPubKey; use bdk::bitcoin::util::psbt::PartiallySignedTransaction; use bdk::bitcoin::{Address, Network, OutPoint, Script, Txid}; -use bdk::blockchain::log_progress; +use bdk::blockchain::{log_progress, Blockchain}; +use bdk::database::BatchDatabase; +use bdk::keys::bip39::{Language, Mnemonic, MnemonicType}; +use bdk::keys::{DerivableKey, ExtendedKey, GeneratableKey, GeneratedKey}; +use bdk::miniscript::miniscript; use bdk::Error; use bdk::{FeeRate, KeychainKind, TxBuilder, Wallet}; -/// Wallet global options and sub-command +/// Global options /// -/// 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. +/// The global options and top level sub-command required for all subsequent [`CliSubCommand`]'s. /// /// # Example /// /// ``` /// # use bdk::bitcoin::Network; /// # use structopt::StructOpt; -/// # use bdk_cli::{WalletSubCommand, WalletOpt}; +/// # use bdk_cli::{CliOpts, WalletOpts, CliSubCommand, WalletSubCommand}; +/// # use bdk_cli::OnlineWalletSubCommand::Sync; /// -/// let cli_args = vec!["bdk-cli", "--network", "testnet", +/// let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", /// "--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(); +/// // to get CliOpts from the OS command line args use: +/// // let cli_opts = CliOpts::from_args(); +/// let cli_opts = CliOpts::from_iter(&cli_args); /// -/// let wallet_opt = WalletOpt::from_iter(&cli_args); -/// -/// let expected_wallet_opt = WalletOpt { -/// network: Network::Testnet, -/// wallet: "main".to_string(), -/// proxy: None, -/// descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(), -/// change_descriptor: None, -/// #[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 expected_cli_opts = CliOpts { +/// network: Network::Testnet, +/// subcommand: CliSubCommand::Wallet { +/// wallet_opts: WalletOpts { +/// wallet: "main".to_string(), +/// descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(), +/// change_descriptor: None, +/// #[cfg(feature = "electrum")] +/// proxy: None, +/// #[cfg(feature = "electrum")] +/// retries: 5, +/// #[cfg(feature = "electrum")] +/// timeout: None, +/// #[cfg(feature = "electrum")] +/// electrum: "ssl://electrum.blockstream.info:60002".to_string(), +/// #[cfg(feature = "esplora")] +/// esplora: None, +/// #[cfg(feature = "esplora")] +/// esplora_concurrency: 4, +/// }, +/// subcommand: WalletSubCommand::OnlineWalletSubCommand(Sync { +/// max_addresses: Some(50) +/// }), +/// }, +/// }; /// -/// assert_eq!(expected_wallet_opt, wallet_opt); +/// assert_eq!(expected_cli_opts, cli_opts); /// ``` - #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(name = "BDK CLI", version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] -pub struct WalletOpt { +pub struct CliOpts { /// Sets the network #[structopt( name = "NETWORK", @@ -166,6 +182,96 @@ pub struct WalletOpt { default_value = "testnet" )] pub network: Network, + /// Top level cli sub-command + #[structopt(subcommand)] + pub subcommand: CliSubCommand, +} + +/// CLI sub-commands +/// +/// The top level mode for subsequent sub-commands, each may have different required options. For +/// instance [`CliSubCommand::Wallet`] requires [`WalletOpts`] with a required descriptor but +/// [`CliSubCommand::Key`] sub-command does not. [`CliSubCommand::Repl`] also requires +/// [`WalletOpts`] and a descriptor because in this mode both [`WalletSubCommand`] and +/// [`KeySubCommand`] sub-commands are available. +/// +#[derive(Debug, StructOpt, Clone, PartialEq)] +pub enum CliSubCommand { + /// Wallet options and sub-commands + Wallet { + #[structopt(flatten)] + wallet_opts: WalletOpts, + #[structopt(subcommand)] + subcommand: WalletSubCommand, + }, + /// Key management sub-commands + Key { + #[structopt(subcommand)] + subcommand: KeySubCommand, + }, + /// Enter REPL command loop mode + Repl { + #[structopt(flatten)] + wallet_opts: WalletOpts, + }, +} + +/// Wallet sub-commands +/// +/// Can use either an online or offline wallet. An [`OnlineWalletSubCommand`] requires a blockchain +/// client and network connection and an [`OfflineWalletSubCommand`] does not. +/// +#[derive(Debug, StructOpt, Clone, PartialEq)] +pub enum WalletSubCommand { + #[structopt(flatten)] + OnlineWalletSubCommand(OnlineWalletSubCommand), + #[structopt(flatten)] + OfflineWalletSubCommand(OfflineWalletSubCommand), +} + +/// Wallet options +/// +/// The wallet options required for all [`CliSubCommand::Wallet`] or [`CliSubCommand::Repl`] +/// sub-commands. These options capture wallet descriptor and blockchain client information. The \ +/// blockchain client details are only used for [`OnlineWalletSubCommand`]s. +/// +/// # Example +/// +/// ``` +/// # use bdk::bitcoin::Network; +/// # use structopt::StructOpt; +/// # use bdk_cli:: WalletOpts; +/// +/// let cli_args = vec!["wallet", +/// "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)"]; +/// +/// // to get WalletOpt from OS command line args use: +/// // let wallet_opt = WalletOpt::from_args(); +/// +/// let wallet_opts = WalletOpts::from_iter(&cli_args); +/// +/// let expected_wallet_opts = WalletOpts { +/// wallet: "main".to_string(), +/// descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(), +/// change_descriptor: None, +/// #[cfg(feature = "electrum")] +/// proxy: None, +/// #[cfg(feature = "electrum")] +/// retries: 5, +/// #[cfg(feature = "electrum")] +/// timeout: None, +/// #[cfg(feature = "electrum")] +/// electrum: "ssl://electrum.blockstream.info:60002".to_string(), +/// #[cfg(feature = "esplora")] +/// esplora: None, +/// #[cfg(feature = "esplora")] +/// esplora_concurrency: 4, +/// }; +/// +/// assert_eq!(expected_wallet_opts, wallet_opts); +/// ``` +#[derive(Debug, StructOpt, Clone, PartialEq)] +pub struct WalletOpts { /// Selects the wallet to use #[structopt( name = "WALLET_NAME", @@ -174,30 +280,31 @@ pub struct WalletOpt { 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, /// 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, - #[cfg(feature = "esplora")] - /// Use the esplora server if given as parameter - #[structopt(name = "ESPLORA_URL", short = "e", long = "esplora")] - pub esplora: Option, - #[cfg(feature = "esplora")] - /// Concurrency of requests made to the esplora server + /// Sets the SOCKS5 proxy for the Electrum client + #[cfg(feature = "electrum")] + #[structopt(name = "PROXY_SERVER:PORT", short = "p", long = "proxy")] + pub proxy: Option, + /// Sets the SOCKS5 proxy retries for the Electrum client + #[cfg(feature = "electrum")] #[structopt( - name = "ESPLORA_CONCURRENCY", - long = "esplora_concurrency", - default_value = "4" + name = "PROXY_RETRIES", + short = "r", + long = "retries", + default_value = "5" )] - pub esplora_concurrency: u8, + pub retries: u8, + /// Sets the SOCKS5 proxy timeout for the Electrum client #[cfg(feature = "electrum")] + #[structopt(name = "PROXY_TIMEOUT", short = "t", long = "timeout")] + pub timeout: Option, /// Sets the Electrum server to use + #[cfg(feature = "electrum")] #[structopt( name = "SERVER:PORT", short = "s", @@ -205,9 +312,18 @@ pub struct WalletOpt { default_value = "ssl://electrum.blockstream.info:60002" )] pub electrum: String, - /// Wallet sub-command - #[structopt(subcommand)] - pub subcommand: WalletSubCommand, + /// Use the esplora server if given as parameter + #[cfg(feature = "esplora")] + #[structopt(name = "ESPLORA_URL", short = "e", long = "esplora")] + pub esplora: Option, + /// Concurrency of requests made to the esplora server + #[cfg(feature = "esplora")] + #[structopt( + name = "ESPLORA_CONCURRENCY", + long = "esplora_concurrency", + default_value = "4" + )] + pub esplora_concurrency: u8, } /// Wallet sub-command From df6e0819a8800865a63b4907ffe15b2bdcb28a30 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 22:50:20 -0800 Subject: [PATCH 03/18] Split WalletSubCommand into OfflineWalletSubCommand and OnlineWalletSubcommand --- src/lib.rs | 100 +++++++++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 955f95f..8de6c43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -326,24 +326,22 @@ pub struct WalletOpts { pub esplora_concurrency: u8, } -/// Wallet sub-command +/// Offline 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 [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdkcli.rs) -/// example cli wallet. +/// [`CliSubCommand::Wallet`] sub-commands that do not require a blockchain client and network +/// connection. These sub-commands use only the provided descriptor or locally cached wallet +/// information. /// /// # Example /// /// ``` -/// # use bdk_cli::WalletSubCommand; +/// # use bdk_cli::OfflineWalletSubCommand; /// # use structopt::StructOpt; /// -/// let sync_sub_command = WalletSubCommand::from_iter(&["bdk-cli", "sync", "--max_addresses", "50"]); +/// let address_sub_command = OfflineWalletSubCommand::from_iter(&["wallet", "get_new_address"]); /// assert!(matches!( -/// sync_sub_command, -/// WalletSubCommand::Sync { -/// max_addresses: Some(50) -/// } +/// address_sub_command, +/// OfflineWalletSubCommand::GetNewAddress /// )); /// ``` /// @@ -354,7 +352,7 @@ pub struct WalletOpts { /// /// # Example /// ``` -/// # use bdk_cli::WalletSubCommand; +/// # use bdk_cli::OfflineWalletSubCommand; /// # use structopt::StructOpt; /// # use clap::AppSettings; /// @@ -362,26 +360,23 @@ pub struct WalletOpts { /// #[structopt(name = "BDK CLI", setting = AppSettings::NoBinaryName, /// version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), /// author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] -/// struct ReplOpt { +/// struct ReplOpts { /// /// Wallet sub-command /// #[structopt(subcommand)] -/// pub subcommand: WalletSubCommand, +/// pub subcommand: OfflineWalletSubCommand, /// } +/// +/// let repl_opts = ReplOpts::from_iter(&["get_new_address"]); +/// assert!(matches!( +/// repl_opts.subcommand, +/// OfflineWalletSubCommand::GetNewAddress +/// )); /// ``` #[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt( - rename_all = "snake", - long_about = "A modern, lightweight, descriptor-based wallet" -)] -pub enum WalletSubCommand { +#[structopt(rename_all = "snake")] +pub enum OfflineWalletSubCommand { /// 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, - }, /// Lists the available spendable UTXOs ListUnspent, /// Lists all the incoming and outgoing transactions of the wallet @@ -452,25 +447,6 @@ pub enum WalletSubCommand { #[structopt(name = "HEIGHT", long = "assume_height")] assume_height: Option, }, - /// 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, - /// Sets the raw transaction to broadcast - #[structopt( - name = "RAWTX", - long = "tx", - required_unless = "BASE64_PSBT", - conflicts_with = "BASE64_PSBT" - )] - tx: Option, - }, /// Extracts a raw transaction from a PSBT ExtractPsbt { /// Sets the PSBT to extract @@ -492,8 +468,42 @@ pub enum WalletSubCommand { #[structopt(name = "BASE64_PSBT", long = "psbt", required = true)] psbt: Vec, }, - /// Enter REPL command loop mode - Repl, +} + +/// Online Wallet sub-command +/// +/// [`CliSubCommand::Wallet`] sub-commands that require a blockchain client and network connection. +/// These sub-commands use a provided descriptor, locally cached wallet information, and require a +/// blockchain client and network connection. +/// +#[derive(Debug, StructOpt, Clone, PartialEq)] +#[structopt(rename_all = "snake")] +pub enum OnlineWalletSubCommand { + /// Syncs with the chosen blockchain server + Sync { + /// max addresses to consider + #[structopt(short = "v", long = "max_addresses")] + max_addresses: Option, + }, + /// 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, + /// Sets the raw transaction to broadcast + #[structopt( + name = "RAWTX", + long = "tx", + required_unless = "BASE64_PSBT", + conflicts_with = "BASE64_PSBT" + )] + tx: Option, + }, } fn parse_recipient(s: &str) -> Result<(Script, u64), String> { From ca8ea93a95b68bbb5bc6bb59606c8c14ed8da8fb Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 22:54:47 -0800 Subject: [PATCH 04/18] Split handle_wallet_subcommand into handle_offline_wallet_subcommand and handle_online_wallet_subcommand, Add KeySubCommand and handle_key_subcommand --- src/lib.rs | 185 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 142 insertions(+), 43 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8de6c43..4386d3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -528,30 +528,24 @@ fn parse_outpoint(s: &str) -> Result { OutPoint::from_str(s).map_err(|e| format!("{:?}", e)) } -/// Execute a wallet sub-command with a given [`Wallet`]. +/// Execute an offline wallet sub-command +/// +/// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`]. /// -/// Wallet sub-commands are described in [`WalletSubCommand`]. See [`crate`] for example usage. #[maybe_async] -pub fn handle_wallet_subcommand( - wallet: &Wallet, - wallet_subcommand: WalletSubCommand, +pub fn handle_offline_wallet_subcommand( + wallet: &Wallet<(), D>, + offline_subcommand: OfflineWalletSubCommand, ) -> Result where - C: bdk::blockchain::Blockchain, - D: bdk::database::BatchDatabase, + D: 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 { + match offline_subcommand { + GetNewAddress => Ok(json!({"address": wallet.get_new_address()?})), + ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?), + ListTransactions => Ok(serde_json::to_value(&wallet.list_transactions(false)?)?), + GetBalance => Ok(json!({"satoshi": wallet.get_balance()?})), + CreateTx { recipients, send_all, enable_rbf, @@ -608,7 +602,7 @@ where let (psbt, details) = wallet.create_tx(tx_builder)?; Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,})) } - WalletSubCommand::BumpFee { + BumpFee { txid, send_all, offline_signer, @@ -641,15 +635,15 @@ where let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?; Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,})) } - WalletSubCommand::Policies => Ok(json!({ + Policies => Ok(json!({ "external": wallet.policies(KeychainKind::External)?, "internal": wallet.policies(KeychainKind::Internal)?, })), - WalletSubCommand::PublicDescriptor => Ok(json!({ + 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 { + Sign { psbt, assume_height, } => { @@ -658,27 +652,12 @@ where 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::::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 } => { + 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 { + FinalizePsbt { psbt, assume_height, } => { @@ -688,7 +667,7 @@ where let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?; Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,})) } - WalletSubCommand::CombinePsbt { psbt } => { + CombinePsbt { psbt } => { let mut psbts = psbt .iter() .map(|s| { @@ -708,10 +687,130 @@ where Ok(acc) }, )?; - Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) })) } - WalletSubCommand::Repl => Ok(json!({})), + } +} + +/// Execute an online wallet sub-command +/// +/// Online wallet sub-commands are described in [`OnlineWalletSubCommand`]. See [`crate`] for +/// example usage. +/// +#[maybe_async] +pub fn handle_online_wallet_subcommand( + wallet: &Wallet, + online_subcommand: OnlineWalletSubCommand, +) -> Result +where + C: Blockchain, + D: BatchDatabase, +{ + match online_subcommand { + Sync { max_addresses } => { + maybe_await!(wallet.sync(log_progress(), max_addresses))?; + Ok(json!({})) + } + 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::::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 })) + } + } +} + +/// Key sub-command +/// +/// Provides basic key operations that are not related to a specific wallet such as generating a +/// new random master extended key or restoring a master extended key from mnemonic words. +/// +/// These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this +/// feature to create keys that secure actual funds on the Bitcoin mainnet. +/// +#[derive(Debug, StructOpt, Clone, PartialEq)] +#[structopt(rename_all = "snake")] +pub enum KeySubCommand { + /// Generates new random seed mnemonic phrase and corresponding master extended keys + Generate { + /// Entropy level based on number of random seed mnemonic words + #[structopt( + name = "WORD_COUNT", + short = "e", + long = "entropy", + default_value = "24", + possible_values = &["12","24"], + )] + word_count: usize, + /// Seed password + #[structopt(name = "PASSWORD", short = "p", long = "password")] + password: Option, + }, + /// Restore a master extended keys from seed backup mnemonic words + Restore { + /// Seed mnemonic words, must be quoted (eg. "word1 word2 ...") + #[structopt(name = "MNEMONIC", short = "m", long = "mnemonic")] + mnemonic: String, + /// Seed password + #[structopt(name = "PASSWORD", short = "p", long = "password")] + password: Option, + }, +} + +/// Execute a key sub-command +/// +/// Key sub-commands are described in [`KeySubCommand`]. +/// +pub fn handle_key_subcommand( + network: Network, + subcommand: KeySubCommand, +) -> Result { + let secp = Secp256k1::new(); + + match subcommand { + KeySubCommand::Generate { + word_count, + password, + } => { + let mnemonic_type = match word_count { + 12 => MnemonicType::Words12, + _ => MnemonicType::Words24, + }; + let mnemonic: GeneratedKey<_, miniscript::Bare> = + Mnemonic::generate((mnemonic_type, Language::English)).unwrap(); + //.map_err(|e| KeyError::from(e.unwrap()))?; + let mnemonic = mnemonic.into_key(); + let xkey: ExtendedKey = (mnemonic.clone(), password).into_extended_key()?; + let xprv = xkey.into_xprv(network).unwrap(); + let xpub = ExtendedPubKey::from_private(&secp, &xprv); + let fingerprint = xprv.fingerprint(&secp); + Ok( + json!({ "mnemonic": mnemonic.phrase(), "xprv": xprv.to_string(),"xpub": xpub.to_string(), "fingerprint": fingerprint.to_string() }), + ) + } + KeySubCommand::Restore { mnemonic, password } => { + let mnemonic = Mnemonic::from_phrase(mnemonic.as_ref(), Language::English).unwrap(); + // .map_err(|e| { + // KeyError::from(e.downcast::().unwrap()) + // })?; + let xkey: ExtendedKey = (mnemonic, password).into_extended_key()?; + let xprv = xkey.into_xprv(network).unwrap(); + let xpub = ExtendedPubKey::from_private(&secp, &xprv); + let fingerprint = xprv.fingerprint(&secp); + + Ok( + json!({ "xprv": xprv.to_string(),"xpub": xpub.to_string(), "fingerprint": fingerprint.to_string() }), + ) + } } } From 51a2f424a764175c0904716fa141f4c8a7773436 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 22:57:23 -0800 Subject: [PATCH 05/18] Update tests --- src/lib.rs | 266 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 189 insertions(+), 77 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4386d3f..0b24766 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -816,77 +816,169 @@ pub fn handle_key_subcommand( #[cfg(test)] mod test { - use super::{WalletOpt, WalletSubCommand}; + use super::{CliOpts, WalletOpts}; + use crate::OfflineWalletSubCommand::{CreateTx, GetNewAddress}; + use crate::OnlineWalletSubCommand::{Broadcast, Sync}; + use crate::{CliSubCommand, WalletSubCommand}; use bdk::bitcoin::{Address, Network, OutPoint}; use std::str::FromStr; use structopt::StructOpt; #[test] - fn test_get_new_address() { - let cli_args = vec!["bdk-cli", "--network", "bitcoin", + fn test_wallet_get_new_address() { + let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet", "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)", + "get_new_address"]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Bitcoin, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), + change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), + #[cfg(feature = "electrum")] + proxy: None, + #[cfg(feature = "electrum")] + retries: 5, + #[cfg(feature = "electrum")] + timeout: None, + #[cfg(feature = "electrum")] + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + #[cfg(feature = "esplora")] + esplora: None, + #[cfg(feature = "esplora")] + esplora_concurrency: 4 + }, + subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + #[cfg(feature = "electrum")] + #[test] + fn test_wallet_electrum() { + let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", + "--proxy", "127.0.0.1:9150", "--retries", "3", "--timeout", "10", + "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", + "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)", + "--server","ssl://electrum.blockstream.info:50002", + "get_new_address"]; + + let cli_opts = CliOpts::from_iter(&cli_args); + + let expected_cli_opts = CliOpts { + network: Network::Testnet, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), + change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), + proxy: Some("127.0.0.1:9150".to_string()), + retries: 3, + timeout: Some(10), + electrum: "ssl://electrum.blockstream.info:50002".to_string(), + #[cfg(feature = "esplora")] + esplora: None, + #[cfg(feature = "esplora")] + esplora_concurrency: 4, + }, + subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + }, + }; + + assert_eq!(expected_cli_opts, cli_opts); + } + + #[cfg(feature = "esplora")] + #[test] + fn test_wallet_esplora() { + let cli_args = vec!["bdk-cli", "--network", "bitcoin", "wallet", + "--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 cli_opts = CliOpts::from_iter(&cli_args); - let expected_wallet_opt = WalletOpt { + let expected_cli_opts = CliOpts { network: Network::Bitcoin, - wallet: "main".to_string(), - proxy: None, - descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), - change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".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, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), + change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), + #[cfg(feature = "electrum")] + proxy: None, + #[cfg(feature = "electrum")] + retries: 5, + #[cfg(feature = "electrum")] + timeout: None, + #[cfg(feature = "electrum")] + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + esplora: Some("https://blockstream.info/api/".to_string()), + esplora_concurrency: 5, + }, + subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), + }, }; - assert_eq!(expected_wallet_opt, wallet_opt); + assert_eq!(expected_cli_opts, cli_opts); } #[test] - fn test_sync() { - let cli_args = vec!["bdk-cli", "--network", "testnet", + fn test_wallet_sync() { + let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", "sync", "--max_addresses", "50"]; - let wallet_opt = WalletOpt::from_iter(&cli_args); + let cli_opts = CliOpts::from_iter(&cli_args); - let expected_wallet_opt = WalletOpt { + let expected_cli_opts = CliOpts { network: Network::Testnet, - wallet: "main".to_string(), - proxy: None, - descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), - change_descriptor: None, - #[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) + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), + change_descriptor: None, + #[cfg(feature = "electrum")] + proxy: None, + #[cfg(feature = "electrum")] + retries: 5, + #[cfg(feature = "electrum")] + timeout: None, + #[cfg(feature = "electrum")] + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + #[cfg(feature = "esplora")] + esplora: None, + #[cfg(feature = "esplora")] + esplora_concurrency: 4, + }, + subcommand: WalletSubCommand::OnlineWalletSubCommand(Sync { + max_addresses: Some(50) + }), }, }; - assert_eq!(expected_wallet_opt, wallet_opt); + assert_eq!(expected_cli_opts, cli_opts); } #[test] - fn test_create_tx() { - let cli_args = vec!["bdk-cli", "--network", "testnet", "--proxy", "127.0.0.1:9150", + fn test_wallet_create_tx() { + let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", "--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 cli_opts = CliOpts::from_iter(&cli_args); let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ") .unwrap() @@ -903,69 +995,89 @@ mod test { ) .unwrap(); - let expected_wallet_opt = WalletOpt { + let expected_cli_opts = CliOpts { network: Network::Testnet, - 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()), - #[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, + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), + change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), + #[cfg(feature = "electrum")] + proxy: None, + #[cfg(feature = "electrum")] + retries: 5, + #[cfg(feature = "electrum")] + timeout: None, + #[cfg(feature = "electrum")] + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + #[cfg(feature = "esplora")] + esplora: None, + #[cfg(feature = "esplora")] + esplora_concurrency: 4, + }, + subcommand: WalletSubCommand::OfflineWalletSubCommand(CreateTx { + recipients: vec![(script1, 123456), (script2, 78910)], + send_all: false, + enable_rbf: false, + offline_signer: false, + utxos: Some(vec!(outpoint1, outpoint2)), + unspendable: None, + fee_rate: None, + external_policy: None, + internal_policy: None, + }), }, }; - assert_eq!(expected_wallet_opt, wallet_opt); + assert_eq!(expected_cli_opts, cli_opts); } #[test] - fn test_broadcast() { - let cli_args = vec!["bdk-cli", "--network", "testnet", + fn test_wallet_broadcast() { + let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", "broadcast", "--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="]; - let wallet_opt = WalletOpt::from_iter(&cli_args); + let cli_opts = CliOpts::from_iter(&cli_args); - let expected_wallet_opt = WalletOpt { + let expected_cli_opts = CliOpts { network: Network::Testnet, - wallet: "main".to_string(), - proxy: None, - descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), - change_descriptor: None, - #[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 + subcommand: CliSubCommand::Wallet { + wallet_opts: WalletOpts { + wallet: "main".to_string(), + descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), + change_descriptor: None, + #[cfg(feature = "electrum")] + proxy: None, + #[cfg(feature = "electrum")] + retries: 5, + #[cfg(feature = "electrum")] + timeout: None, + #[cfg(feature = "electrum")] + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + #[cfg(feature = "esplora")] + esplora: None, + #[cfg(feature = "esplora")] + esplora_concurrency: 4, + }, + subcommand: WalletSubCommand::OnlineWalletSubCommand(Broadcast { + psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()), + tx: None + }), }, }; - assert_eq!(expected_wallet_opt, wallet_opt); + assert_eq!(expected_cli_opts, cli_opts); } #[test] fn test_wrong_network() { - let cli_args = vec!["repl", "--network", "badnet", + let cli_args = vec!["repl", "--network", "badnet", "wallet", "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", "sync", "--max_addresses", "50"]; - let wallet_opt = WalletOpt::from_iter_safe(&cli_args); - assert!(wallet_opt.is_err()); + let cli_opts = CliOpts::from_iter_safe(&cli_args); + assert!(cli_opts.is_err()); } } From 0ed2219e47a9d8221a606c9300132a2db56df3d7 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 22:59:42 -0800 Subject: [PATCH 06/18] Add ReplOpt and ReplSubCommand --- src/bdk_cli.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 8dfc632..439c420 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -33,24 +33,43 @@ 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::database::BatchDatabase; use bdk::sled; +use bdk::sled::Tree; use bdk::Wallet; -use bdk_cli::{self, WalletOpt, WalletSubCommand}; +use bdk::{bitcoin, Error}; +use bdk_cli::WalletSubCommand; +use bdk_cli::{ + CliOpts, CliSubCommand, KeySubCommand, OfflineWalletSubCommand, OnlineWalletSubCommand, + WalletOpts, +}; +use regex::Regex; #[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt(name = "BDK CLI", setting = AppSettings::NoBinaryName, +#[structopt(name = "", 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, + pub subcommand: ReplSubCommand, +} + +#[derive(Debug, StructOpt, Clone, PartialEq)] +pub enum ReplSubCommand { + #[structopt(flatten)] + OnlineWalletSubCommand(OnlineWalletSubCommand), + #[structopt(flatten)] + OfflineWalletSubCommand(OfflineWalletSubCommand), + #[structopt(flatten)] + KeySubCommand(KeySubCommand), + /// Exit REPL loop + Exit, } fn prepare_home_dir() -> PathBuf { From 0d3e641bbbf2ad08152eff59a952684ebe761c37 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 23:04:37 -0800 Subject: [PATCH 07/18] Create online and offline wallets, use regex to split and filter repl commands --- src/bdk_cli.rs | 201 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 154 insertions(+), 47 deletions(-) diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 439c420..e8c20b7 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -86,30 +86,26 @@ fn prepare_home_dir() -> PathBuf { dir } -fn main() { - env_logger::init(); - - let cli_opt: WalletOpt = WalletOpt::from_args(); - - let network = cli_opt.network; - debug!("network: {:?}", network); - if network == Network::Bitcoin { - warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.") - } - - let descriptor = cli_opt.descriptor.as_str(); - let change_descriptor = cli_opt.change_descriptor.as_deref(); - debug!("descriptors: {:?} {:?}", descriptor, change_descriptor); - +fn open_database(wallet_opts: &WalletOpts) -> Tree { let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap(); - let tree = database.open_tree(cli_opt.wallet).unwrap(); + let tree = database.open_tree(&wallet_opts.wallet).unwrap(); debug!("database opened successfully"); + tree +} +fn new_online_wallet( + network: Network, + wallet_opts: &WalletOpts, + database: D, +) -> Result, Error> +where + D: BatchDatabase, +{ // Try to use Esplora config if "esplora" feature is enabled #[cfg(feature = "esplora")] let config_esplora: Option = { - let esplora_concurrency = cli_opt.esplora_concurrency; - cli_opt.esplora.map(|base_url| { + let esplora_concurrency = wallet_opts.esplora_concurrency; + wallet_opts.esplora.clone().map(|base_url| { AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { base_url, concurrency: Some(esplora_concurrency), @@ -119,34 +115,97 @@ fn main() { #[cfg(not(feature = "esplora"))] let config_esplora = None; + let config_electrum = AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { + url: wallet_opts.electrum.clone(), + socks5: wallet_opts.proxy.clone(), + retry: wallet_opts.retries, + timeout: wallet_opts.timeout, + }); + // 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: None, - })); + let config = config_esplora.unwrap_or(config_electrum); + let descriptor = wallet_opts.descriptor.as_str(); + let change_descriptor = wallet_opts.change_descriptor.as_deref(); let wallet = Wallet::new( descriptor, change_descriptor, network, - tree, - AnyBlockchain::from_config(&config).unwrap(), - ) - .unwrap(); + database, + AnyBlockchain::from_config(&config)?, + )?; + Ok(wallet) +} + +fn new_offline_wallet( + network: Network, + wallet_opts: &WalletOpts, + database: D, +) -> Result, Error> +where + D: BatchDatabase, +{ + let descriptor = wallet_opts.descriptor.as_str(); + let change_descriptor = wallet_opts.change_descriptor.as_deref(); + let wallet = Wallet::new_offline(descriptor, change_descriptor, network, database)?; + Ok(wallet) +} + +fn main() { + env_logger::init(); + + let cli_opts: CliOpts = CliOpts::from_args(); - let wallet = Arc::new(wallet); + let network = cli_opts.network; + debug!("network: {:?}", network); + if network == Network::Bitcoin { + warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.") + } + + //println!("cli_opts = {:?}", cli_opts); + + let result = match cli_opts.subcommand { + CliSubCommand::Wallet { + wallet_opts, + subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + } => { + let database = open_database(&wallet_opts); + let wallet = new_online_wallet(network, &wallet_opts, database).unwrap(); + let result = bdk_cli::handle_online_wallet_subcommand(&wallet, online_subcommand); + serde_json::to_string_pretty(&result.unwrap()).unwrap() + } + CliSubCommand::Wallet { + wallet_opts, + subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), + } => { + let database = open_database(&wallet_opts); + let wallet = new_offline_wallet(network, &wallet_opts, database).unwrap(); + let result = bdk_cli::handle_offline_wallet_subcommand(&wallet, offline_subcommand); + serde_json::to_string_pretty(&result.unwrap()).unwrap() + } + CliSubCommand::Key { + subcommand: key_subcommand, + } => { + let result = bdk_cli::handle_key_subcommand(network, key_subcommand); + serde_json::to_string_pretty(&result.unwrap()).unwrap() + } + CliSubCommand::Repl { wallet_opts } => { + let database = open_database(&wallet_opts); + let online_wallet = new_online_wallet(network, &wallet_opts, database.clone()).unwrap(); + let online_wallet = Arc::new(online_wallet); + + let offline_wallet = new_offline_wallet(network, &wallet_opts, database).unwrap(); + let offline_wallet = Arc::new(offline_wallet); - match cli_opt.subcommand { - WalletSubCommand::Repl => { let mut rl = Editor::<()>::new(); // if rl.load_history("history.txt").is_err() { // println!("No previous history."); // } + let split_regex = Regex::new(r#"[\w\-]+|"[\w\s]*""#).unwrap(); + let filter_regex = Regex::new(r#"[\w\s\-]+"#).unwrap(); + loop { let readline = rl.readline(">> "); match readline { @@ -155,22 +214,46 @@ fn main() { continue; } rl.add_history_entry(line.as_str()); - let split_line: Vec<&str> = line.split(' ').collect(); - let repl_subcommand: Result = - ReplOpt::from_iter_safe(split_line); - debug!("repl_subcommand = {:?}", repl_subcommand); + let split_line: Vec<&str> = + split_regex.find_iter(&line).map(|m| m.as_str()).collect(); + let filtered_line: Vec<&str> = split_line + .iter() + .flat_map(|s| filter_regex.find_iter(s).map(|m| m.as_str())) + .collect(); + let repl_opt: Result = + ReplOpt::from_iter_safe(filtered_line); + debug!("repl_opt = {:?}", repl_opt); - if let Err(err) = repl_subcommand { + if let Err(err) = repl_opt { 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()); + let repl_subcommand = repl_opt.unwrap().subcommand; + + let result = match repl_subcommand { + ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => { + bdk_cli::handle_online_wallet_subcommand( + &Arc::clone(&online_wallet), + online_subcommand, + ) + } + ReplSubCommand::OfflineWalletSubCommand(offline_subcommand) => { + bdk_cli::handle_offline_wallet_subcommand( + &Arc::clone(&offline_wallet), + offline_subcommand, + ) + } + ReplSubCommand::KeySubCommand(key_subcommand) => { + bdk_cli::handle_key_subcommand(network, key_subcommand) + } + ReplSubCommand::Exit => break, + }; + + println!( + "{}", + serde_json::to_string_pretty(&result.unwrap()).unwrap() + ); } Err(ReadlineError::Interrupted) => continue, Err(ReadlineError::Eof) => break, @@ -182,10 +265,34 @@ fn main() { } // rl.save_history("history.txt").unwrap(); + "Exiting REPL".to_string() } - _ => { - let result = bdk_cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap(); - println!("{}", serde_json::to_string_pretty(&result).unwrap()); - } + }; + + println!("{}", result); +} + +#[cfg(test)] +mod test { + use regex::Regex; + + #[test] + fn test_regex() { + let split_regex = Regex::new(r#"[\w\-]+|"[\w\s]*""#).unwrap(); + //println!("split_regex = {:?}", &split_regex); + let filter_regex = Regex::new(r#"[\w\s\-]+"#).unwrap(); + //println!("filter_regex = {:?}", &filter_regex); + let line = r#"restore -m "word1 word2 word3" -p test"#; + let split_line: Vec<&str> = split_regex.find_iter(&line).map(|m| m.as_str()).collect(); + //println!("split_line({}) = {:?}", &line, &split_line); + let filtered_line: Vec<&str> = split_line + .iter() + .flat_map(|s| filter_regex.find_iter(s).map(|m| m.as_str())) + .collect(); + //println!("filtered_line({:?}) = {:?}", &split_line, &filtered_line); + assert_eq!( + vec!("restore", "-m", "word1 word2 word3", "-p", "test"), + filtered_line + ); } } From 6601ed42b29f2c67d073601241eaac4c1bac0f43 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 2 Feb 2021 23:05:01 -0800 Subject: [PATCH 08/18] Update CHANGELOG --- CHANGELOG.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ccf7c8..a440715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Project -- Upgrade BDK to `0.3` #### Added - Add support for `wasm` +- `CliOpts` struct and `CliSubCommand` enum representing top level cli options and commands +- `KeySubCommand` enum +- `handle_key_subcommand` function + +#### Changed +- Upgrade BDK to `0.3` +- Renamed `WalletOpt` struct to `WalletOpts` +- `WalletSubCommand` enum split into `OfflineWalletSubCommand` and `OnlineWalletSubCommand` +- Split `handle_wallet_subcommand` into two functions, `handle_offline_wallet_subcommand` and `handle_online_wallet_subcommand` +- A wallet without a `Blockchain` is used when handling offline wallet sub-commands + +### `bdk-cli` bin + +#### Added +- Top level commands "wallet", "key", and "repl" +- "key" command has sub-commands to "generate" and "restore" a master extended key +- "repl" command now has an "exit" sub-command + +#### Changed +- "wallet" sub-commands and options must be proceeded by "wallet" command +- "repl" command loop now includes both "wallet" and "key" sub-commands ## [0.1.0] From 0bed7e95476d3e8fbcfcdccbfe6c470b5e4bce3f Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 3 Feb 2021 10:55:16 -0800 Subject: [PATCH 09/18] Update to current bdk rev with new 'DerivableKey' and 'TxBuilder' APIs --- Cargo.toml | 4 ++-- src/lib.rs | 35 ++++++++++++++++++----------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0bfd826..70fd6dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,8 @@ readme = "README.md" license = "MIT" [dependencies] -bdk = { git = "https://github.com/afilini/bdk.git", branch = "fix/better-derivable-key-api", features = ["all-keys"]} -bdk-macros = "^0.2" +bdk = { git = "https://github.com/bitcoindevkit/bdk.git", commit = "4c36020e", features = ["all-keys"]} +bdk-macros = { git = "https://github.com/bitcoindevkit/bdk.git", commit = "4c36020e" } structopt = "^0.3" serde_json = { version = "^1.0" } log = "^0.4" diff --git a/src/lib.rs b/src/lib.rs index 0b24766..4edc80c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,7 +119,7 @@ use bdk::keys::bip39::{Language, Mnemonic, MnemonicType}; use bdk::keys::{DerivableKey, ExtendedKey, GeneratableKey, GeneratedKey}; use bdk::miniscript::miniscript; use bdk::Error; -use bdk::{FeeRate, KeychainKind, TxBuilder, Wallet}; +use bdk::{FeeRate, KeychainKind, Wallet}; /// Global options /// @@ -556,36 +556,36 @@ where external_policy, internal_policy, } => { - let mut tx_builder = TxBuilder::new(); + let mut tx_builder = wallet.build_tx(); if send_all { - tx_builder = tx_builder + tx_builder .drain_wallet() .set_single_recipient(recipients[0].0.clone()); } else { - tx_builder = tx_builder.set_recipients(recipients); + tx_builder.set_recipients(recipients); } if enable_rbf { - tx_builder = tx_builder.enable_rbf(); + tx_builder.enable_rbf(); } if offline_signer { - tx_builder = 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)); + 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(); + tx_builder.add_utxos(&utxos[..])?.manually_selected_only(); } if let Some(unspendable) = unspendable { - tx_builder = tx_builder.unspendable(unspendable); + tx_builder.unspendable(unspendable); } let policies = vec![ @@ -596,10 +596,10 @@ where for (policy, keychain) in policies.into_iter().filter_map(|x| x) { let policy = serde_json::from_str::>>(&policy) .map_err(|s| Error::Generic(s.to_string()))?; - tx_builder = tx_builder.policy_path(policy, keychain); + tx_builder.policy_path(policy, keychain); } - let (psbt, details) = wallet.create_tx(tx_builder)?; + let (psbt, details) = tx_builder.finish()?; Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,})) } BumpFee { @@ -612,27 +612,28 @@ where } => { 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)); + let mut tx_builder = wallet.build_fee_bump(txid)?; + tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate)); if send_all { - tx_builder = tx_builder.maintain_single_recipient(); + tx_builder.maintain_single_recipient()?; } if offline_signer { - tx_builder = tx_builder + tx_builder .force_non_witness_utxo() .include_output_redeem_witness_script(); } if let Some(utxos) = utxos { - tx_builder = tx_builder.utxos(utxos); + tx_builder.add_utxos(&utxos[..])?; } if let Some(unspendable) = unspendable { - tx_builder = tx_builder.unspendable(unspendable); + tx_builder.unspendable(unspendable); } - let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?; + let (psbt, details) = tx_builder.finish()?; Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,})) } Policies => Ok(json!({ From a64086e0042b46c3b6864ccc28793c950ae696dd Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 3 Feb 2021 14:12:57 -0800 Subject: [PATCH 10/18] Add key subcommand tests --- src/lib.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 4edc80c..8e84031 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -820,10 +820,11 @@ mod test { use super::{CliOpts, WalletOpts}; use crate::OfflineWalletSubCommand::{CreateTx, GetNewAddress}; use crate::OnlineWalletSubCommand::{Broadcast, Sync}; - use crate::{CliSubCommand, WalletSubCommand}; + use crate::{CliSubCommand, WalletSubCommand, KeySubCommand, handle_key_subcommand}; use bdk::bitcoin::{Address, Network, OutPoint}; use std::str::FromStr; use structopt::StructOpt; + use bdk::miniscript::bitcoin::network::constants::Network::Testnet; #[test] fn test_wallet_get_new_address() { @@ -1081,4 +1082,45 @@ mod test { let cli_opts = CliOpts::from_iter_safe(&cli_args); assert!(cli_opts.is_err()); } + + #[test] + fn test_key_generate() { + let network = Testnet; + let key_generate_cmd = KeySubCommand::Generate { + word_count: 12, + password: Some("test123".to_string()) + }; + + let result = handle_key_subcommand(network, key_generate_cmd).unwrap(); + let result_obj = result.as_object().unwrap(); + + let mnemonic = result_obj.get("mnemonic").unwrap().as_str().unwrap(); + let mnemonic:Vec<&str> = mnemonic.split(' ').collect(); + let xprv = result_obj.get("xprv").unwrap().as_str().unwrap(); + let xpub = result_obj.get("xpub").unwrap().as_str().unwrap(); + + assert_eq!(mnemonic.len(), 12); + assert_eq!(&xprv[0..4], "tprv"); + assert_eq!(&xpub[0..4], "tpub"); + } + + #[test] + fn test_key_restore() { + let network = Testnet; + let key_generate_cmd = KeySubCommand::Restore { + mnemonic: "payment battle unit sword token broccoli era violin purse trip blood hire".to_string(), + password: Some("test123".to_string()) + }; + + let result = handle_key_subcommand(network, key_generate_cmd).unwrap(); + let result_obj = result.as_object().unwrap(); + + let fingerprint = result_obj.get("fingerprint").unwrap().as_str().unwrap(); + let xprv = result_obj.get("xprv").unwrap().as_str().unwrap(); + let xpub = result_obj.get("xpub").unwrap().as_str().unwrap(); + + assert_eq!(&fingerprint, &"828af366"); + assert_eq!(&xprv, &"tprv8ZgxMBicQKsPd18TeiFknZKqaZFwpdX9tvvKh8eeHSSPBQi5g9xPHztBg411o78G8XkrhQb6Q1cVvBJ1a9xuFHpmWgvQsvkJkNxBjfGoqhK"); + assert_eq!(&xpub, &"tpubD6NzVbkrYhZ4WUAFYMvMBxyx9amsyxi4UEX6yegwhiEn1txrJYmyUVW3rABPz2emcVT5H8PqBoMmWHmJG8fWi9a4iiGLDquUDtyYDKe9cqk"); + } } From adbc3b6956fea7829d6c00df78deffc573cf953c Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 3 Feb 2021 15:04:44 -0800 Subject: [PATCH 11/18] Cleanup cli help messages --- src/bdk_cli.rs | 2 +- src/lib.rs | 37 +++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index e8c20b7..6179620 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -55,7 +55,7 @@ use regex::Regex; version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] struct ReplOpt { - /// Wallet sub-command + /// Repl sub-command #[structopt(subcommand)] pub subcommand: ReplSubCommand, } diff --git a/src/lib.rs b/src/lib.rs index 8e84031..767d356 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -189,15 +189,19 @@ pub struct CliOpts { /// CLI sub-commands /// -/// The top level mode for subsequent sub-commands, each may have different required options. For +/// The top level sub-commands, each may have different required options. For /// instance [`CliSubCommand::Wallet`] requires [`WalletOpts`] with a required descriptor but /// [`CliSubCommand::Key`] sub-command does not. [`CliSubCommand::Repl`] also requires /// [`WalletOpts`] and a descriptor because in this mode both [`WalletSubCommand`] and /// [`KeySubCommand`] sub-commands are available. -/// #[derive(Debug, StructOpt, Clone, PartialEq)] +#[structopt( + rename_all = "snake", + long_about = "Top level options and command modes" +)] pub enum CliSubCommand { /// Wallet options and sub-commands + #[structopt(long_about = "Wallet mode")] Wallet { #[structopt(flatten)] wallet_opts: WalletOpts, @@ -205,11 +209,13 @@ pub enum CliSubCommand { subcommand: WalletSubCommand, }, /// Key management sub-commands + #[structopt(long_about = "Key management mode")] Key { #[structopt(subcommand)] subcommand: KeySubCommand, }, /// Enter REPL command loop mode + #[structopt(long_about = "REPL command loop mode")] Repl { #[structopt(flatten)] wallet_opts: WalletOpts, @@ -220,7 +226,6 @@ pub enum CliSubCommand { /// /// Can use either an online or offline wallet. An [`OnlineWalletSubCommand`] requires a blockchain /// client and network connection and an [`OfflineWalletSubCommand`] does not. -/// #[derive(Debug, StructOpt, Clone, PartialEq)] pub enum WalletSubCommand { #[structopt(flatten)] @@ -475,7 +480,6 @@ pub enum OfflineWalletSubCommand { /// [`CliSubCommand::Wallet`] sub-commands that require a blockchain client and network connection. /// These sub-commands use a provided descriptor, locally cached wallet information, and require a /// blockchain client and network connection. -/// #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(rename_all = "snake")] pub enum OnlineWalletSubCommand { @@ -531,7 +535,6 @@ fn parse_outpoint(s: &str) -> Result { /// Execute an offline wallet sub-command /// /// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`]. -/// #[maybe_async] pub fn handle_offline_wallet_subcommand( wallet: &Wallet<(), D>, @@ -697,7 +700,6 @@ where /// /// Online wallet sub-commands are described in [`OnlineWalletSubCommand`]. See [`crate`] for /// example usage. -/// #[maybe_async] pub fn handle_online_wallet_subcommand( wallet: &Wallet, @@ -737,7 +739,6 @@ where /// /// These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this /// feature to create keys that secure actual funds on the Bitcoin mainnet. -/// #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(rename_all = "snake")] pub enum KeySubCommand { @@ -770,7 +771,6 @@ pub enum KeySubCommand { /// Execute a key sub-command /// /// Key sub-commands are described in [`KeySubCommand`]. -/// pub fn handle_key_subcommand( network: Network, subcommand: KeySubCommand, @@ -820,11 +820,11 @@ mod test { use super::{CliOpts, WalletOpts}; use crate::OfflineWalletSubCommand::{CreateTx, GetNewAddress}; use crate::OnlineWalletSubCommand::{Broadcast, Sync}; - use crate::{CliSubCommand, WalletSubCommand, KeySubCommand, handle_key_subcommand}; + use crate::{handle_key_subcommand, CliSubCommand, KeySubCommand, WalletSubCommand}; use bdk::bitcoin::{Address, Network, OutPoint}; + use bdk::miniscript::bitcoin::network::constants::Network::Testnet; use std::str::FromStr; use structopt::StructOpt; - use bdk::miniscript::bitcoin::network::constants::Network::Testnet; #[test] fn test_wallet_get_new_address() { @@ -1082,23 +1082,23 @@ mod test { let cli_opts = CliOpts::from_iter_safe(&cli_args); assert!(cli_opts.is_err()); } - + #[test] fn test_key_generate() { let network = Testnet; let key_generate_cmd = KeySubCommand::Generate { word_count: 12, - password: Some("test123".to_string()) + password: Some("test123".to_string()), }; - + let result = handle_key_subcommand(network, key_generate_cmd).unwrap(); let result_obj = result.as_object().unwrap(); - + let mnemonic = result_obj.get("mnemonic").unwrap().as_str().unwrap(); - let mnemonic:Vec<&str> = mnemonic.split(' ').collect(); + let mnemonic: Vec<&str> = mnemonic.split(' ').collect(); let xprv = result_obj.get("xprv").unwrap().as_str().unwrap(); let xpub = result_obj.get("xpub").unwrap().as_str().unwrap(); - + assert_eq!(mnemonic.len(), 12); assert_eq!(&xprv[0..4], "tprv"); assert_eq!(&xpub[0..4], "tpub"); @@ -1108,8 +1108,9 @@ mod test { fn test_key_restore() { let network = Testnet; let key_generate_cmd = KeySubCommand::Restore { - mnemonic: "payment battle unit sword token broccoli era violin purse trip blood hire".to_string(), - password: Some("test123".to_string()) + mnemonic: "payment battle unit sword token broccoli era violin purse trip blood hire" + .to_string(), + password: Some("test123".to_string()), }; let result = handle_key_subcommand(network, key_generate_cmd).unwrap(); From 616f18030532d84b4799503894b90dfd21b75ad5 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 3 Feb 2021 16:43:40 -0800 Subject: [PATCH 12/18] Update README.md --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d698abf..46deea3 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,17 @@ cargo run To sync a wallet to the default electrum server: ```shell -cargo run -- --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" sync +cargo run -- wallet --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" sync ``` To get a wallet balance with customized logging: ```shell -RUST_LOG=debug,sled=info,rustls=info cargo run -- --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" get_balance +RUST_LOG=debug,sled=info,rustls=info cargo run -- wallet --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" get_balance +``` + +To generate a new extended master key, suitable for using in a descriptor: + +```shell +cargo run -- key generate ``` \ No newline at end of file From 503095c91bd83ed656a0b0c1044573de4773f491 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Thu, 4 Feb 2021 16:56:18 -0800 Subject: [PATCH 13/18] Remove ReplOpt --- src/bdk_cli.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 6179620..4e553b7 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -51,16 +51,9 @@ use bdk_cli::{ use regex::Regex; #[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt(name = "", setting = AppSettings::NoBinaryName, +#[structopt(name = "", long_about = "REPL mode", setting = AppSettings::NoBinaryName, version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] -struct ReplOpt { - /// Repl sub-command - #[structopt(subcommand)] - pub subcommand: ReplSubCommand, -} - -#[derive(Debug, StructOpt, Clone, PartialEq)] pub enum ReplSubCommand { #[structopt(flatten)] OnlineWalletSubCommand(OnlineWalletSubCommand), @@ -162,8 +155,6 @@ fn main() { warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.") } - //println!("cli_opts = {:?}", cli_opts); - let result = match cli_opts.subcommand { CliSubCommand::Wallet { wallet_opts, @@ -220,16 +211,16 @@ fn main() { .iter() .flat_map(|s| filter_regex.find_iter(s).map(|m| m.as_str())) .collect(); - let repl_opt: Result = - ReplOpt::from_iter_safe(filtered_line); - debug!("repl_opt = {:?}", repl_opt); + let repl_subcommand: Result = + ReplSubCommand::from_iter_safe(filtered_line); + debug!("repl_subcommand = {:?}", repl_subcommand); - if let Err(err) = repl_opt { + if let Err(err) = repl_subcommand { println!("{}", err.message); continue; } - let repl_subcommand = repl_opt.unwrap().subcommand; + let repl_subcommand = repl_subcommand.unwrap(); let result = match repl_subcommand { ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => { From c96bf2a03b8503207c49a7ec8248f77548d6fc2a Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Fri, 5 Feb 2021 10:52:25 -0800 Subject: [PATCH 14/18] Add workaround for 'structopt' about issue --- src/bdk_cli.rs | 3 +- src/lib.rs | 134 ++++++++++++++++++++++++++++--------------------- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 4e553b7..b5bb2c2 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -50,8 +50,9 @@ use bdk_cli::{ }; use regex::Regex; +/// REPL mode #[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt(name = "", long_about = "REPL mode", setting = AppSettings::NoBinaryName, +#[structopt(name = "", setting = AppSettings::NoBinaryName, version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] pub enum ReplSubCommand { diff --git a/src/lib.rs b/src/lib.rs index 767d356..49232f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -331,52 +331,58 @@ pub struct WalletOpts { pub esplora_concurrency: u8, } -/// Offline Wallet sub-command -/// -/// [`CliSubCommand::Wallet`] sub-commands that do not require a blockchain client and network -/// connection. These sub-commands use only the provided descriptor or locally cached wallet -/// information. -/// -/// # Example -/// -/// ``` -/// # use bdk_cli::OfflineWalletSubCommand; -/// # use structopt::StructOpt; -/// -/// let address_sub_command = OfflineWalletSubCommand::from_iter(&["wallet", "get_new_address"]); -/// assert!(matches!( -/// address_sub_command, -/// OfflineWalletSubCommand::GetNewAddress -/// )); -/// ``` -/// -/// To capture wallet sub-commands from a string vector without a preceeding binary name you can -/// create a custom struct the includes the `NoBinaryName` clap setting and wraps the WalletSubCommand -/// enum. See also the [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdkcli.rs) -/// example app. -/// -/// # Example -/// ``` -/// # use bdk_cli::OfflineWalletSubCommand; -/// # use structopt::StructOpt; -/// # use clap::AppSettings; -/// -/// #[derive(Debug, StructOpt, Clone, PartialEq)] -/// #[structopt(name = "BDK CLI", setting = AppSettings::NoBinaryName, -/// version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), -/// author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] -/// struct ReplOpts { -/// /// Wallet sub-command -/// #[structopt(subcommand)] -/// pub subcommand: OfflineWalletSubCommand, -/// } -/// -/// let repl_opts = ReplOpts::from_iter(&["get_new_address"]); -/// assert!(matches!( -/// repl_opts.subcommand, -/// OfflineWalletSubCommand::GetNewAddress -/// )); -/// ``` +// This is a workaround for `structopt` issue #333, #391, #418; see https://github.com/TeXitoi/structopt/issues/333#issuecomment-712265332 +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + doc, + doc = r#" +Offline Wallet sub-command + +[`CliSubCommand::Wallet`] sub-commands that do not require a blockchain client and network +connection. These sub-commands use only the provided descriptor or locally cached wallet +information. + +# Example + +``` +# use bdk_cli::OfflineWalletSubCommand; +# use structopt::StructOpt; + +let address_sub_command = OfflineWalletSubCommand::from_iter(&["wallet", "get_new_address"]); +assert!(matches!( + address_sub_command, + OfflineWalletSubCommand::GetNewAddress +)); +``` + +To capture wallet sub-commands from a string vector without a preceeding binary name you can +create a custom struct the includes the `NoBinaryName` clap setting and wraps the WalletSubCommand +enum. See also the [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdkcli.rs) +example app. + +# Example +``` +# use bdk_cli::OfflineWalletSubCommand; +# use structopt::StructOpt; +# use clap::AppSettings; + +#[derive(Debug, StructOpt, Clone, PartialEq)] +#[structopt(name = "BDK CLI", setting = AppSettings::NoBinaryName, +version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), +author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] +struct ReplOpts { + /// Wallet sub-command + #[structopt(subcommand)] + pub subcommand: OfflineWalletSubCommand, +} + +let repl_opts = ReplOpts::from_iter(&["get_new_address"]); +assert!(matches!( + repl_opts.subcommand, + OfflineWalletSubCommand::GetNewAddress +)); +"# +)] #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(rename_all = "snake")] pub enum OfflineWalletSubCommand { @@ -475,11 +481,17 @@ pub enum OfflineWalletSubCommand { }, } -/// Online Wallet sub-command -/// -/// [`CliSubCommand::Wallet`] sub-commands that require a blockchain client and network connection. -/// These sub-commands use a provided descriptor, locally cached wallet information, and require a -/// blockchain client and network connection. +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + doc, + doc = r#" +Online Wallet sub-command + +[`CliSubCommand::Wallet`] sub-commands that require a blockchain client and network connection. +These sub-commands use a provided descriptor, locally cached wallet information, and require a +blockchain client and network connection. +"# +)] #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(rename_all = "snake")] pub enum OnlineWalletSubCommand { @@ -732,13 +744,19 @@ where } } -/// Key sub-command -/// -/// Provides basic key operations that are not related to a specific wallet such as generating a -/// new random master extended key or restoring a master extended key from mnemonic words. -/// -/// These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this -/// feature to create keys that secure actual funds on the Bitcoin mainnet. +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + doc, + doc = r#" +Key sub-command + +Provides basic key operations that are not related to a specific wallet such as generating a +new random master extended key or restoring a master extended key from mnemonic words. + +These sub-commands are **EXPERIMENTAL** and should only be used for testing. Do not use this +feature to create keys that secure actual funds on the Bitcoin mainnet. +"# +)] #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(rename_all = "snake")] pub enum KeySubCommand { From 1cec70927ea5973b899f128dea681cf5de89ee3d Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Fri, 5 Feb 2021 17:08:48 -0800 Subject: [PATCH 15/18] Update to latest 'bdk' commit --- Cargo.toml | 4 ++-- src/lib.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 70fd6dd..1e09f81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,8 @@ readme = "README.md" license = "MIT" [dependencies] -bdk = { git = "https://github.com/bitcoindevkit/bdk.git", commit = "4c36020e", features = ["all-keys"]} -bdk-macros = { git = "https://github.com/bitcoindevkit/bdk.git", commit = "4c36020e" } +bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "c4f2179", features = ["all-keys"]} +bdk-macros = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "c4f2179" } structopt = "^0.3" serde_json = { version = "^1.0" } log = "^0.4" diff --git a/src/lib.rs b/src/lib.rs index 49232f4..75089f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -760,7 +760,7 @@ feature to create keys that secure actual funds on the Bitcoin mainnet. #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(rename_all = "snake")] pub enum KeySubCommand { - /// Generates new random seed mnemonic phrase and corresponding master extended keys + /// Generates new random seed mnemonic phrase and corresponding master extended key Generate { /// Entropy level based on number of random seed mnemonic words #[structopt( @@ -775,7 +775,7 @@ pub enum KeySubCommand { #[structopt(name = "PASSWORD", short = "p", long = "password")] password: Option, }, - /// Restore a master extended keys from seed backup mnemonic words + /// Restore a master extended key from seed backup mnemonic words Restore { /// Seed mnemonic words, must be quoted (eg. "word1 word2 ...") #[structopt(name = "MNEMONIC", short = "m", long = "mnemonic")] @@ -804,7 +804,7 @@ pub fn handle_key_subcommand( 12 => MnemonicType::Words12, _ => MnemonicType::Words24, }; - let mnemonic: GeneratedKey<_, miniscript::Bare> = + let mnemonic: GeneratedKey<_, miniscript::BareCtx> = Mnemonic::generate((mnemonic_type, Language::English)).unwrap(); //.map_err(|e| KeyError::from(e.unwrap()))?; let mnemonic = mnemonic.into_key(); From dc00b80862fa2e41c77ea64013a5654d6162e891 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Mon, 8 Feb 2021 13:40:10 -0800 Subject: [PATCH 16/18] Simplify repl line parsing regex --- src/bdk_cli.rs | 84 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index b5bb2c2..80661c7 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -50,9 +50,11 @@ use bdk_cli::{ }; use regex::Regex; +const REPL_LINE_SPLIT_REGEX: &str = r#""([^"]*)"|'([^']*)'|([\w\-]+)"#; + /// REPL mode #[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt(name = "", setting = AppSettings::NoBinaryName, +#[structopt(name = "", setting = AppSettings::NoBinaryName, version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] pub enum ReplSubCommand { @@ -195,8 +197,7 @@ fn main() { // println!("No previous history."); // } - let split_regex = Regex::new(r#"[\w\-]+|"[\w\s]*""#).unwrap(); - let filter_regex = Regex::new(r#"[\w\s\-]+"#).unwrap(); + let split_regex = Regex::new(REPL_LINE_SPLIT_REGEX).unwrap(); loop { let readline = rl.readline(">> "); @@ -206,14 +207,18 @@ fn main() { continue; } rl.add_history_entry(line.as_str()); - let split_line: Vec<&str> = - split_regex.find_iter(&line).map(|m| m.as_str()).collect(); - let filtered_line: Vec<&str> = split_line - .iter() - .flat_map(|s| filter_regex.find_iter(s).map(|m| m.as_str())) + let split_line: Vec<&str> = split_regex + .captures_iter(&line) + .map(|c| { + c.get(1) + .or_else(|| c.get(2)) + .or_else(|| c.get(3)) + .unwrap() + .as_str() + }) .collect(); let repl_subcommand: Result = - ReplSubCommand::from_iter_safe(filtered_line); + ReplSubCommand::from_iter_safe(split_line); debug!("repl_subcommand = {:?}", repl_subcommand); if let Err(err) = repl_subcommand { @@ -266,25 +271,58 @@ fn main() { #[cfg(test)] mod test { + use crate::REPL_LINE_SPLIT_REGEX; use regex::Regex; #[test] - fn test_regex() { - let split_regex = Regex::new(r#"[\w\-]+|"[\w\s]*""#).unwrap(); - //println!("split_regex = {:?}", &split_regex); - let filter_regex = Regex::new(r#"[\w\s\-]+"#).unwrap(); - //println!("filter_regex = {:?}", &filter_regex); - let line = r#"restore -m "word1 word2 word3" -p test"#; - let split_line: Vec<&str> = split_regex.find_iter(&line).map(|m| m.as_str()).collect(); - //println!("split_line({}) = {:?}", &line, &split_line); - let filtered_line: Vec<&str> = split_line - .iter() - .flat_map(|s| filter_regex.find_iter(s).map(|m| m.as_str())) + fn test_regex_double_quotes() { + let split_regex = Regex::new(REPL_LINE_SPLIT_REGEX).unwrap(); + let line = r#"restore -m "word1 word2 word3" -p 'test! 123 -test' "#; + let split_line: Vec<&str> = split_regex + .captures_iter(&line) + .map(|c| { + c.get(1) + .or_else(|| c.get(2)) + .or_else(|| c.get(3)) + .unwrap() + .as_str() + }) + .collect(); + assert_eq!( + vec!( + "restore", + "-m", + "word1 word2 word3", + "-p", + "test! 123 -test" + ), + split_line + ); + } + + #[test] + fn test_regex_single_quotes() { + let split_regex = Regex::new(REPL_LINE_SPLIT_REGEX).unwrap(); + let line = r#"restore -m 'word1 word2 word3' -p "test *123 -test" "#; + let split_line: Vec<&str> = split_regex + .captures_iter(&line) + .map(|c| { + c.get(1) + .or_else(|| c.get(2)) + .or_else(|| c.get(3)) + .unwrap() + .as_str() + }) .collect(); - //println!("filtered_line({:?}) = {:?}", &split_line, &filtered_line); assert_eq!( - vec!("restore", "-m", "word1 word2 word3", "-p", "test"), - filtered_line + vec!( + "restore", + "-m", + "word1 word2 word3", + "-p", + "test *123 -test" + ), + split_line ); } } From 3211af06021f0382400ab8f02d7f347a50185eaf Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Mon, 8 Feb 2021 19:16:01 -0800 Subject: [PATCH 17/18] Add ElectrumOpts and EsploraOpts structops; set no default dependencies --- .github/workflows/cont_integration.yml | 5 +- Cargo.toml | 11 +- README.md | 17 +- src/bdk_cli.rs | 12 +- src/lib.rs | 221 +++++++++++++++---------- 5 files changed, 162 insertions(+), 104 deletions(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index c5e7536..d1c5203 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -14,7 +14,10 @@ jobs: - 1.45.0 # MSRV features: - default - - default,esplora + - repl + - electrum + - esplora + - repl,electrum,esplora steps: - name: Checkout uses: actions/checkout@v2 diff --git a/Cargo.toml b/Cargo.toml index 1e09f81..056f512 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT" [dependencies] -bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "c4f2179", features = ["all-keys"]} +bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "c4f2179", default-features = false, features = ["all-keys"]} bdk-macros = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "c4f2179" } structopt = "^0.3" serde_json = { version = "^1.0" } @@ -28,8 +28,8 @@ clap = { version = "2.33", optional = true } regex = {version = "1", optional = true } [features] -default = ["repl", "electrum"] -repl = ["async-trait", "rustyline", "dirs-next", "env_logger", "clap", "regex"] +default = [] +repl = ["async-trait", "bdk/key-value-db", "clap", "dirs-next", "env_logger", "regex", "rustyline"] electrum = ["bdk/electrum"] esplora = ["bdk/esplora"] compiler = ["bdk/compiler"] @@ -37,4 +37,7 @@ compiler = ["bdk/compiler"] [[bin]] name = "bdk-cli" path = "src/bdk_cli.rs" -required-features = ["repl"] +required-features = ["repl", "electrum"] + +[package.metadata.docs.rs] +all-features = true diff --git a/README.md b/README.md index 46deea3..e5d4886 100644 --- a/README.md +++ b/README.md @@ -14,23 +14,32 @@ To get usage information for the `bdk-cli` bin use the below command which retur available wallet options and commands: ```shell -cargo run +cargo run --features repl,electrum ``` To sync a wallet to the default electrum server: ```shell -cargo run -- wallet --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" sync +cargo run --features repl,electrum,esplora -- wallet --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" sync ``` To get a wallet balance with customized logging: ```shell -RUST_LOG=debug,sled=info,rustls=info cargo run -- wallet --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" get_balance +RUST_LOG=debug,sled=info,rustls=info cargo run --features repl,electrum,esplora -- wallet --descriptor "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)" get_balance ``` To generate a new extended master key, suitable for using in a descriptor: ```shell -cargo run -- key generate +cargo run --features repl,electrum -- key generate +``` + +To install dev version of `bdk-cli` from local git repo: + +```shell +cd +cargo install --path . --features repl,electrum,esplora + +bdk-cli help # to verify it worked ``` \ No newline at end of file diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 80661c7..4e63b1a 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -100,8 +100,8 @@ where // Try to use Esplora config if "esplora" feature is enabled #[cfg(feature = "esplora")] let config_esplora: Option = { - let esplora_concurrency = wallet_opts.esplora_concurrency; - wallet_opts.esplora.clone().map(|base_url| { + let esplora_concurrency = wallet_opts.esplora_opts.esplora_concurrency; + wallet_opts.esplora_opts.esplora.clone().map(|base_url| { AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { base_url, concurrency: Some(esplora_concurrency), @@ -112,10 +112,10 @@ where let config_esplora = None; let config_electrum = AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { - url: wallet_opts.electrum.clone(), - socks5: wallet_opts.proxy.clone(), - retry: wallet_opts.retries, - timeout: wallet_opts.timeout, + url: wallet_opts.electrum_opts.electrum.clone(), + socks5: wallet_opts.electrum_opts.proxy.clone(), + retry: wallet_opts.electrum_opts.retries, + timeout: wallet_opts.electrum_opts.timeout, }); // Fall back to Electrum config if Esplora config isn't provided diff --git a/src/lib.rs b/src/lib.rs index 75089f4..7eaab72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,7 +40,9 @@ //! //! # Example //! -//! ``` +//! ```no_run +//! # #[cfg(feature = "electrum")] +//! # { //! # use bdk::bitcoin::Network; //! # use bdk::blockchain::{AnyBlockchain, ConfigurableBlockchain}; //! # use bdk::blockchain::{AnyBlockchainConfig, ElectrumBlockchainConfig}; @@ -72,8 +74,8 @@ //! let database = MemoryDatabase::new(); //! //! let config = AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { -//! url: wallet_opts.electrum, -//! socks5: wallet_opts.proxy, +//! url: wallet_opts.electrum_opts.electrum, +//! socks5: wallet_opts.electrum_opts.proxy, //! retry: 3, //! timeout: None, //! }); @@ -89,6 +91,7 @@ //! let result = bdk_cli::handle_online_wallet_subcommand(&wallet, online_subcommand).unwrap(); //! println!("{}", serde_json::to_string_pretty(&result).unwrap()); //! } +//! # } //! ``` pub extern crate bdk; @@ -131,6 +134,10 @@ use bdk::{FeeRate, KeychainKind, Wallet}; /// # use bdk::bitcoin::Network; /// # use structopt::StructOpt; /// # use bdk_cli::{CliOpts, WalletOpts, CliSubCommand, WalletSubCommand}; +/// # #[cfg(feature = "electrum")] +/// # use bdk_cli::ElectrumOpts; +/// # #[cfg(feature = "esplora")] +/// # use bdk_cli::EsploraOpts; /// # use bdk_cli::OnlineWalletSubCommand::Sync; /// /// let cli_args = vec!["bdk-cli", "--network", "testnet", "wallet", @@ -148,18 +155,18 @@ use bdk::{FeeRate, KeychainKind, Wallet}; /// wallet: "main".to_string(), /// descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(), /// change_descriptor: None, -/// #[cfg(feature = "electrum")] -/// proxy: None, -/// #[cfg(feature = "electrum")] -/// retries: 5, -/// #[cfg(feature = "electrum")] -/// timeout: None, -/// #[cfg(feature = "electrum")] -/// electrum: "ssl://electrum.blockstream.info:60002".to_string(), -/// #[cfg(feature = "esplora")] -/// esplora: None, -/// #[cfg(feature = "esplora")] -/// esplora_concurrency: 4, +/// #[cfg(feature = "electrum")] +/// electrum_opts: ElectrumOpts { +/// proxy: None, +/// retries: 5, +/// timeout: None, +/// electrum: "ssl://electrum.blockstream.info:60002".to_string(), +/// }, +/// #[cfg(feature = "esplora")] +/// esplora_opts: EsploraOpts { +/// esplora: None, +/// esplora_concurrency: 4, +/// } /// }, /// subcommand: WalletSubCommand::OnlineWalletSubCommand(Sync { /// max_addresses: Some(50) @@ -237,7 +244,7 @@ pub enum WalletSubCommand { /// Wallet options /// /// The wallet options required for all [`CliSubCommand::Wallet`] or [`CliSubCommand::Repl`] -/// sub-commands. These options capture wallet descriptor and blockchain client information. The \ +/// sub-commands. These options capture wallet descriptor and blockchain client information. The /// blockchain client details are only used for [`OnlineWalletSubCommand`]s. /// /// # Example @@ -245,7 +252,11 @@ pub enum WalletSubCommand { /// ``` /// # use bdk::bitcoin::Network; /// # use structopt::StructOpt; -/// # use bdk_cli:: WalletOpts; +/// # use bdk_cli::WalletOpts; +/// # #[cfg(feature = "electrum")] +/// # use bdk_cli::ElectrumOpts; +/// # #[cfg(feature = "esplora")] +/// # use bdk_cli::EsploraOpts; /// /// let cli_args = vec!["wallet", /// "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)"]; @@ -260,17 +271,17 @@ pub enum WalletSubCommand { /// descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(), /// change_descriptor: None, /// #[cfg(feature = "electrum")] -/// proxy: None, -/// #[cfg(feature = "electrum")] -/// retries: 5, -/// #[cfg(feature = "electrum")] -/// timeout: None, -/// #[cfg(feature = "electrum")] -/// electrum: "ssl://electrum.blockstream.info:60002".to_string(), -/// #[cfg(feature = "esplora")] -/// esplora: None, +/// electrum_opts: ElectrumOpts { +/// proxy: None, +/// retries: 5, +/// timeout: None, +/// electrum: "ssl://electrum.blockstream.info:60002".to_string(), +/// }, /// #[cfg(feature = "esplora")] -/// esplora_concurrency: 4, +/// esplora_opts: EsploraOpts { +/// esplora: None, +/// esplora_concurrency: 4, +/// } /// }; /// /// assert_eq!(expected_wallet_opts, wallet_opts); @@ -291,12 +302,24 @@ pub struct WalletOpts { /// Sets the descriptor to use for internal addresses #[structopt(name = "CHANGE_DESCRIPTOR", short = "c", long = "change_descriptor")] pub change_descriptor: Option, - /// Sets the SOCKS5 proxy for the Electrum client #[cfg(feature = "electrum")] + #[structopt(flatten)] + pub electrum_opts: ElectrumOpts, + #[cfg(feature = "esplora")] + #[structopt(flatten)] + pub esplora_opts: EsploraOpts, +} + +/// Electrum options +/// +/// Electrum blockchain client information used by [`OnlineWalletSubCommand`]s. +#[cfg(feature = "electrum")] +#[derive(Debug, StructOpt, Clone, PartialEq)] +pub struct ElectrumOpts { + /// Sets the SOCKS5 proxy for the Electrum client #[structopt(name = "PROXY_SERVER:PORT", short = "p", long = "proxy")] pub proxy: Option, /// Sets the SOCKS5 proxy retries for the Electrum client - #[cfg(feature = "electrum")] #[structopt( name = "PROXY_RETRIES", short = "r", @@ -305,11 +328,9 @@ pub struct WalletOpts { )] pub retries: u8, /// Sets the SOCKS5 proxy timeout for the Electrum client - #[cfg(feature = "electrum")] #[structopt(name = "PROXY_TIMEOUT", short = "t", long = "timeout")] pub timeout: Option, /// Sets the Electrum server to use - #[cfg(feature = "electrum")] #[structopt( name = "SERVER:PORT", short = "s", @@ -317,12 +338,18 @@ pub struct WalletOpts { default_value = "ssl://electrum.blockstream.info:60002" )] pub electrum: String, +} + +/// Esplora options +/// +/// Esplora blockchain client information used by [`OnlineWalletSubCommand`]s. +#[cfg(feature = "esplora")] +#[derive(Debug, StructOpt, Clone, PartialEq)] +pub struct EsploraOpts { /// Use the esplora server if given as parameter - #[cfg(feature = "esplora")] #[structopt(name = "ESPLORA_URL", short = "e", long = "esplora")] pub esplora: Option, /// Concurrency of requests made to the esplora server - #[cfg(feature = "esplora")] #[structopt( name = "ESPLORA_CONCURRENCY", long = "esplora_concurrency", @@ -359,6 +386,11 @@ To capture wallet sub-commands from a string vector without a preceeding binary create a custom struct the includes the `NoBinaryName` clap setting and wraps the WalletSubCommand enum. See also the [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdkcli.rs) example app. +"# +)] +#[cfg_attr( + all(doc, feature = "repl"), + doc = r#" # Example ``` @@ -836,9 +868,14 @@ pub fn handle_key_subcommand( #[cfg(test)] mod test { use super::{CliOpts, WalletOpts}; + #[cfg(feature = "electrum")] + use crate::ElectrumOpts; + #[cfg(feature = "esplora")] + use crate::EsploraOpts; use crate::OfflineWalletSubCommand::{CreateTx, GetNewAddress}; use crate::OnlineWalletSubCommand::{Broadcast, Sync}; use crate::{handle_key_subcommand, CliSubCommand, KeySubCommand, WalletSubCommand}; + use bdk::bitcoin::{Address, Network, OutPoint}; use bdk::miniscript::bitcoin::network::constants::Network::Testnet; use std::str::FromStr; @@ -861,17 +898,17 @@ mod test { descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), #[cfg(feature = "electrum")] - proxy: None, - #[cfg(feature = "electrum")] - retries: 5, - #[cfg(feature = "electrum")] - timeout: None, - #[cfg(feature = "electrum")] - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - #[cfg(feature = "esplora")] - esplora: None, + electrum_opts: ElectrumOpts { + proxy: None, + retries: 5, + timeout: None, + electrum: "ssl://electrum.blockstream.info:60002".to_string() + }, #[cfg(feature = "esplora")] - esplora_concurrency: 4 + esplora_opts: EsploraOpts { + esplora: None, + esplora_concurrency: 4 + }, }, subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), }, @@ -899,14 +936,18 @@ mod test { wallet: "main".to_string(), descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), - proxy: Some("127.0.0.1:9150".to_string()), - retries: 3, - timeout: Some(10), - electrum: "ssl://electrum.blockstream.info:50002".to_string(), - #[cfg(feature = "esplora")] - esplora: None, + #[cfg(feature = "electrum")] + electrum_opts: ElectrumOpts { + proxy: Some("127.0.0.1:9150".to_string()), + retries: 3, + timeout: Some(10), + electrum: "ssl://electrum.blockstream.info:50002".to_string(), + }, #[cfg(feature = "esplora")] - esplora_concurrency: 4, + esplora_opts: EsploraOpts { + esplora: None, + esplora_concurrency: 4, + } }, subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), }, @@ -935,15 +976,17 @@ mod test { descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), #[cfg(feature = "electrum")] - proxy: None, - #[cfg(feature = "electrum")] - retries: 5, - #[cfg(feature = "electrum")] - timeout: None, - #[cfg(feature = "electrum")] - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - esplora: Some("https://blockstream.info/api/".to_string()), - esplora_concurrency: 5, + electrum_opts: ElectrumOpts { + proxy: None, + retries: 5, + timeout: None, + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + }, + #[cfg(feature = "esplora")] + esplora_opts: EsploraOpts { + esplora: Some("https://blockstream.info/api/".to_string()), + esplora_concurrency: 5, + } }, subcommand: WalletSubCommand::OfflineWalletSubCommand(GetNewAddress), }, @@ -968,17 +1011,17 @@ mod test { descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), change_descriptor: None, #[cfg(feature = "electrum")] - proxy: None, - #[cfg(feature = "electrum")] - retries: 5, - #[cfg(feature = "electrum")] - timeout: None, - #[cfg(feature = "electrum")] - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - #[cfg(feature = "esplora")] - esplora: None, + electrum_opts: ElectrumOpts { + proxy: None, + retries: 5, + timeout: None, + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + }, #[cfg(feature = "esplora")] - esplora_concurrency: 4, + esplora_opts: EsploraOpts { + esplora: None, + esplora_concurrency: 4, + } }, subcommand: WalletSubCommand::OnlineWalletSubCommand(Sync { max_addresses: Some(50) @@ -1023,17 +1066,17 @@ mod test { descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), #[cfg(feature = "electrum")] - proxy: None, - #[cfg(feature = "electrum")] - retries: 5, - #[cfg(feature = "electrum")] - timeout: None, - #[cfg(feature = "electrum")] - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - #[cfg(feature = "esplora")] - esplora: None, + electrum_opts: ElectrumOpts { + proxy: None, + retries: 5, + timeout: None, + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + }, #[cfg(feature = "esplora")] - esplora_concurrency: 4, + esplora_opts: EsploraOpts { + esplora: None, + esplora_concurrency: 4, + } }, subcommand: WalletSubCommand::OfflineWalletSubCommand(CreateTx { recipients: vec![(script1, 123456), (script2, 78910)], @@ -1069,17 +1112,17 @@ mod test { descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), change_descriptor: None, #[cfg(feature = "electrum")] - proxy: None, - #[cfg(feature = "electrum")] - retries: 5, - #[cfg(feature = "electrum")] - timeout: None, - #[cfg(feature = "electrum")] - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - #[cfg(feature = "esplora")] - esplora: None, + electrum_opts: ElectrumOpts { + proxy: None, + retries: 5, + timeout: None, + electrum: "ssl://electrum.blockstream.info:60002".to_string(), + }, #[cfg(feature = "esplora")] - esplora_concurrency: 4, + esplora_opts: EsploraOpts { + esplora: None, + esplora_concurrency: 4, + } }, subcommand: WalletSubCommand::OnlineWalletSubCommand(Broadcast { psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()), From 9fb50b1e7bd16683c65d77ece61bcc32e0f445e8 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 9 Feb 2021 10:21:22 -0800 Subject: [PATCH 18/18] Update handle offline fn and repl to work with online wallets --- src/bdk_cli.rs | 9 ++------- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 4e63b1a..17ed3aa 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -24,7 +24,6 @@ use std::fs; use std::path::PathBuf; -use std::sync::Arc; use bitcoin::Network; use clap::AppSettings; @@ -186,10 +185,6 @@ fn main() { CliSubCommand::Repl { wallet_opts } => { let database = open_database(&wallet_opts); let online_wallet = new_online_wallet(network, &wallet_opts, database.clone()).unwrap(); - let online_wallet = Arc::new(online_wallet); - - let offline_wallet = new_offline_wallet(network, &wallet_opts, database).unwrap(); - let offline_wallet = Arc::new(offline_wallet); let mut rl = Editor::<()>::new(); @@ -231,13 +226,13 @@ fn main() { let result = match repl_subcommand { ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => { bdk_cli::handle_online_wallet_subcommand( - &Arc::clone(&online_wallet), + &online_wallet, online_subcommand, ) } ReplSubCommand::OfflineWalletSubCommand(offline_subcommand) => { bdk_cli::handle_offline_wallet_subcommand( - &Arc::clone(&offline_wallet), + &online_wallet, offline_subcommand, ) } diff --git a/src/lib.rs b/src/lib.rs index 7eaab72..b5c05d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -580,8 +580,8 @@ fn parse_outpoint(s: &str) -> Result { /// /// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`]. #[maybe_async] -pub fn handle_offline_wallet_subcommand( - wallet: &Wallet<(), D>, +pub fn handle_offline_wallet_subcommand( + wallet: &Wallet, offline_subcommand: OfflineWalletSubCommand, ) -> Result where