From f74bfdd4938e75d2c7b779287c1767be7cec87bb Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Mon, 21 Dec 2020 18:38:35 -0800 Subject: [PATCH] Remove 'cli.rs' module, 'cli-utils' feature and 'repl.rs' example --- .github/workflows/code_coverage.yml | 2 +- .github/workflows/cont_integration.yml | 4 +- CHANGELOG.md | 6 + Cargo.toml | 15 +- examples/repl.rs | 174 ------ src/cli.rs | 750 ------------------------- src/lib.rs | 4 - 7 files changed, 13 insertions(+), 942 deletions(-) delete mode 100644 examples/repl.rs delete mode 100644 src/cli.rs diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index e5e88c6c..d61ab757 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -18,7 +18,7 @@ jobs: - name: Install tarpaulin run: cargo install cargo-tarpaulin - name: Tarpaulin - run: cargo tarpaulin --features all-keys,cli-utils,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml + run: cargo tarpaulin --features all-keys,compiler,esplora,compact_filters --run-types Tests,Doctests --exclude-files "testutils/*" --out Xml - name: Publish to codecov.io uses: codecov/codecov-action@v1.0.15 diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 25f4e45f..5d68cd6f 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -20,7 +20,7 @@ jobs: - key-value-db - electrum - compact_filters - - cli-utils,esplora,key-value-db,electrum + - esplora,key-value-db,electrum - compiler steps: - name: checkout @@ -130,7 +130,7 @@ jobs: - name: Add target wasm32 run: rustup target add wasm32-unknown-unknown - name: Check - run: cargo check --target wasm32-unknown-unknown --features cli-utils,esplora --no-default-features + run: cargo check --target wasm32-unknown-unknown --features esplora --no-default-features fmt: name: Rust fmt diff --git a/CHANGELOG.md b/CHANGELOG.md index 19da76e7..d410360c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Blockchain +#### Changed - Remove `BlockchainMarker`, `OfflineClient` and `OfflineWallet` in favor of just using the unit type to mark for a missing client. +### CLI +#### Changed +- Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository + ## [v0.2.0] - [0.1.0-beta.1] ### Project diff --git a/Cargo.toml b/Cargo.toml index d10ce59f..9464047e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,6 @@ sled = { version = "0.34", optional = true } electrum-client = { version = "0.4.0-beta.1", optional = true } reqwest = { version = "0.10", optional = true, features = ["json"] } futures = { version = "0.3", optional = true } -clap = { version = "2.33", optional = true } -base64 = { version = "^0.11", optional = true } async-trait = { version = "0.1", optional = true } rocksdb = { version = "0.14", optional = true } # pin cc version to 1.0.62 because 1.0.63 break rocksdb build @@ -34,7 +32,6 @@ cc = { version = "=1.0.62", optional = true } socks = { version = "0.3", optional = true } lazy_static = { version = "1.4", optional = true } tiny-bip39 = { version = "^0.8", optional = true } -structopt = { version = "^0.3", optional = true } # Platform-specific dependencies [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -47,13 +44,12 @@ rand = { version = "^0.7", features = ["wasm-bindgen"] } [features] minimal = [] -compiler = ["clap", "miniscript/compiler"] +compiler = ["miniscript/compiler"] default = ["key-value-db", "electrum"] electrum = ["electrum-client"] esplora = ["reqwest", "futures"] compact_filters = ["rocksdb", "socks", "lazy_static", "cc"] key-value-db = ["sled"] -cli-utils = ["clap", "base64", "structopt"] async-interface = ["async-trait"] all-keys = ["keys-bip39"] keys-bip39 = ["tiny-bip39"] @@ -61,20 +57,17 @@ keys-bip39 = ["tiny-bip39"] # Debug/Test features debug-proc-macros = ["bdk-macros/debug", "bdk-testutils-macros/debug"] test-electrum = ["electrum"] -test-md-docs = ["base64", "electrum"] +test-md-docs = ["electrum"] [dev-dependencies] bdk-testutils = "0.2" bdk-testutils-macros = "0.2" serial_test = "0.4" lazy_static = "1.4" -rustyline = "6.0" -dirs-next = "2.0" env_logger = "0.7" +base64 = "^0.11" +clap = "2.33" -[[example]] -name = "repl" -required-features = ["cli-utils"] [[example]] name = "parse_descriptor" [[example]] diff --git a/examples/repl.rs b/examples/repl.rs deleted file mode 100644 index bd04075d..00000000 --- a/examples/repl.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Magical Bitcoin Library -// Written in 2020 by -// Alekos Filini -// -// Copyright (c) 2020 Magical Bitcoin -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -use std::fs; -use std::path::PathBuf; -use std::str::FromStr; -use std::sync::Arc; - -use bitcoin::Network; -use clap::AppSettings; -use log::{debug, info, warn, LevelFilter}; -use rustyline::error::ReadlineError; -use rustyline::Editor; -use structopt::StructOpt; - -use bdk::bitcoin; -#[cfg(feature = "esplora")] -use bdk::blockchain::esplora::EsploraBlockchainConfig; -use bdk::blockchain::{ - AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain, ElectrumBlockchainConfig, -}; -use bdk::cli::{self, WalletOpt, WalletSubCommand}; -use bdk::sled; -use bdk::Wallet; - -#[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName, -version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), -author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] -struct ReplOpt { - /// Wallet sub-command - #[structopt(subcommand)] - pub subcommand: WalletSubCommand, -} - -fn prepare_home_dir() -> PathBuf { - let mut dir = PathBuf::new(); - dir.push(&dirs_next::home_dir().unwrap()); - dir.push(".bdk-bitcoin"); - - if !dir.exists() { - info!("Creating home directory {}", dir.as_path().display()); - fs::create_dir(&dir).unwrap(); - } - - dir.push("database.sled"); - dir -} - -fn main() { - let cli_opt: WalletOpt = WalletOpt::from_args(); - - let level = LevelFilter::from_str(cli_opt.log_level.as_str()).unwrap_or(LevelFilter::Info); - env_logger::builder().filter_level(level).init(); - - let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet); - debug!("network: {:?}", network); - if network == Network::Bitcoin { - warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.") - } - - let descriptor = cli_opt.descriptor.as_str(); - let change_descriptor = cli_opt.change_descriptor.as_deref(); - debug!("descriptors: {:?} {:?}", descriptor, change_descriptor); - - let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap(); - let tree = database.open_tree(cli_opt.wallet).unwrap(); - debug!("database opened successfully"); - - // Try to use Esplora config if "esplora" feature is enabled - #[cfg(feature = "esplora")] - let config_esplora: Option = { - let esplora_concurrency = cli_opt.esplora_concurrency; - cli_opt.esplora.map(|base_url| { - AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { - base_url: base_url.to_string(), - concurrency: Some(esplora_concurrency), - }) - }) - }; - #[cfg(not(feature = "esplora"))] - let config_esplora = None; - - // Fall back to Electrum config if Esplora config isn't provided - let config = - config_esplora.unwrap_or(AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { - url: cli_opt.electrum, - socks5: cli_opt.proxy, - retry: 10, - timeout: 10, - })); - - let wallet = Wallet::new( - descriptor, - change_descriptor, - network, - tree, - AnyBlockchain::from_config(&config).unwrap(), - ) - .unwrap(); - - let wallet = Arc::new(wallet); - - match cli_opt.subcommand { - WalletSubCommand::Other(external) if external.contains(&"repl".to_string()) => { - let mut rl = Editor::<()>::new(); - - // if rl.load_history("history.txt").is_err() { - // println!("No previous history."); - // } - - loop { - let readline = rl.readline(">> "); - match readline { - Ok(line) => { - if line.trim() == "" { - continue; - } - rl.add_history_entry(line.as_str()); - let split_line: Vec<&str> = line.split(" ").collect(); - let repl_subcommand: Result = - ReplOpt::from_iter_safe(split_line); - debug!("repl_subcommand = {:?}", repl_subcommand); - - if let Err(err) = repl_subcommand { - println!("{}", err.message); - continue; - } - - let result = cli::handle_wallet_subcommand( - &Arc::clone(&wallet), - repl_subcommand.unwrap().subcommand, - ) - .unwrap(); - println!("{}", serde_json::to_string_pretty(&result).unwrap()); - } - Err(ReadlineError::Interrupted) => continue, - Err(ReadlineError::Eof) => break, - Err(err) => { - println!("{:?}", err); - break; - } - } - } - - // rl.save_history("history.txt").unwrap(); - } - _ => { - let result = cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap(); - println!("{}", serde_json::to_string_pretty(&result).unwrap()); - } - } -} diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 44f11d7c..00000000 --- a/src/cli.rs +++ /dev/null @@ -1,750 +0,0 @@ -// Magical Bitcoin Library -// Written in 2020 by -// Alekos Filini -// -// Copyright (c) 2020 Magical Bitcoin -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -//! Command line interface -//! -//! This module provides a [structopt](https://docs.rs/crate/structopt) `struct` and `enum` that -//! parse global wallet options and wallet subcommand options needed for a wallet command line -//! interface. -//! -//! See the `repl.rs` example for how to use this module to create a simple command line REPL -//! wallet application. -//! -//! See [`WalletOpt`] for global wallet options and [`WalletSubCommand`] for supported sub-commands. -//! -//! # Example -//! -//! ``` -//! # use bdk::bitcoin::Network; -//! # use bdk::blockchain::esplora::EsploraBlockchainConfig; -//! # use bdk::blockchain::{AnyBlockchain, ConfigurableBlockchain}; -//! # use bdk::blockchain::{AnyBlockchainConfig, ElectrumBlockchainConfig}; -//! # use bdk::cli::{self, WalletOpt, WalletSubCommand}; -//! # use bdk::database::MemoryDatabase; -//! # use bdk::Wallet; -//! # use bitcoin::hashes::core::str::FromStr; -//! # use std::sync::Arc; -//! # use structopt::StructOpt; -//! -//! // to get args from cli use: -//! // let cli_opt = WalletOpt::from_args(); -//! -//! let cli_args = vec!["repl", "--network", "testnet", "--descriptor", -//! "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", -//! "sync", "--max_addresses", "50"]; -//! let cli_opt = WalletOpt::from_iter(&cli_args); -//! -//! let network = Network::from_str(cli_opt.network.as_str()).unwrap_or(Network::Testnet); -//! -//! let descriptor = cli_opt.descriptor.as_str(); -//! let change_descriptor = cli_opt.change_descriptor.as_deref(); -//! -//! let database = MemoryDatabase::new(); -//! -//! let config = match cli_opt.esplora { -//! Some(base_url) => AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { -//! base_url: base_url.to_string(), -//! concurrency: Some(cli_opt.esplora_concurrency), -//! }), -//! None => AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { -//! url: cli_opt.electrum, -//! socks5: cli_opt.proxy, -//! retry: 3, -//! timeout: 5, -//! }), -//! }; -//! -//! let wallet = Wallet::new( -//! descriptor, -//! change_descriptor, -//! network, -//! database, -//! AnyBlockchain::from_config(&config).unwrap(), -//! ).unwrap(); -//! -//! let wallet = Arc::new(wallet); -//! -//! let result = cli::handle_wallet_subcommand(&wallet, cli_opt.subcommand).unwrap(); -//! println!("{}", serde_json::to_string_pretty(&result).unwrap()); -//! ``` - -use std::collections::BTreeMap; -use std::str::FromStr; - -use structopt::StructOpt; - -#[allow(unused_imports)] -use log::{debug, error, info, trace, LevelFilter}; - -use bitcoin::consensus::encode::{deserialize, serialize, serialize_hex}; -use bitcoin::hashes::hex::FromHex; -use bitcoin::util::psbt::PartiallySignedTransaction; -use bitcoin::{Address, OutPoint, Script, Txid}; - -use crate::blockchain::log_progress; -use crate::error::Error; -use crate::types::KeychainKind; -use crate::{FeeRate, TxBuilder, Wallet}; - -/// Wallet global options and sub-command -/// -/// A [structopt](https://docs.rs/crate/structopt) `struct` that parses wallet global options and -/// sub-command from the command line or from a `String` vector. See [`WalletSubCommand`] for details -/// on parsing sub-commands. -/// -/// # Example -/// -/// ``` -/// # use bdk::cli::{WalletOpt, WalletSubCommand}; -/// # use structopt::StructOpt; -/// -/// let cli_args = vec!["repl", "--network", "testnet", -/// "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)", -/// "sync", "--max_addresses", "50"]; -/// -/// // to get WalletOpt from OS command line args use: -/// // let wallet_opt = WalletOpt::from_args(); -/// -/// let wallet_opt = WalletOpt::from_iter(&cli_args); -/// -/// let expected_wallet_opt = WalletOpt { -/// network: "testnet".to_string(), -/// wallet: "main".to_string(), -/// proxy: None, -/// descriptor: "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)".to_string(), -/// change_descriptor: None, -/// log_level: "info".to_string(), -/// #[cfg(feature = "esplora")] -/// esplora: None, -/// #[cfg(feature = "esplora")] -/// esplora_concurrency: 4, -/// electrum: "ssl://electrum.blockstream.info:60002".to_string(), -/// subcommand: WalletSubCommand::Sync { -/// max_addresses: Some(50) -/// }, -/// }; -/// -/// assert_eq!(expected_wallet_opt, wallet_opt); -/// ``` - -#[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt(name = "BDK Wallet", -version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), -author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] -pub struct WalletOpt { - /// Sets the network - #[structopt( - name = "NETWORK", - short = "n", - long = "network", - default_value = "testnet" - )] - pub network: String, - /// Selects the wallet to use - #[structopt( - name = "WALLET_NAME", - short = "w", - long = "wallet", - default_value = "main" - )] - pub wallet: String, - #[cfg(feature = "electrum")] - /// Sets the SOCKS5 proxy for the Electrum client - #[structopt(name = "PROXY_SERVER:PORT", short = "p", long = "proxy")] - pub proxy: Option, - /// 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, - /// Sets the logging level filter (off, error, warn, info, debug, trace) - #[structopt(long = "log_level", short = "l", default_value = "info")] - pub log_level: String, - #[cfg(feature = "esplora")] - /// Use the esplora server if given as parameter - #[structopt(name = "ESPLORA_URL", short = "e", long = "esplora")] - pub esplora: Option, - #[cfg(feature = "esplora")] - /// Concurrency of requests made to the esplora server - #[structopt( - name = "ESPLORA_CONCURRENCY", - long = "esplora_concurrency", - default_value = "4" - )] - pub esplora_concurrency: u8, - #[cfg(feature = "electrum")] - /// Sets the Electrum server to use - #[structopt( - name = "SERVER:PORT", - short = "s", - long = "server", - default_value = "ssl://electrum.blockstream.info:60002" - )] - pub electrum: String, - /// Wallet sub-command - #[structopt(subcommand)] - pub subcommand: WalletSubCommand, -} - -/// Wallet sub-command -/// -/// A [structopt](https://docs.rs/crate/structopt) enum that parses wallet sub-command arguments from -/// the command line or from a `String` vector, such as in the [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs) -/// example app. -/// -/// Additional "external" sub-commands can be captured via the [`WalletSubCommand::Other`] enum and passed to a -/// custom `structopt` or another parser. See [structopt "External subcommands"](https://docs.rs/structopt/0.3.21/structopt/index.html#external-subcommands) -/// for more information. -/// -/// # Example -/// -/// ``` -/// # use bdk::cli::WalletSubCommand; -/// # use structopt::StructOpt; -/// -/// let sync_sub_command = WalletSubCommand::from_iter(&["repl", "sync", "--max_addresses", "50"]); -/// assert!(matches!( -/// sync_sub_command, -/// WalletSubCommand::Sync { -/// max_addresses: Some(50) -/// } -/// )); -/// -/// let other_sub_command = WalletSubCommand::from_iter(&["repl", "custom", "--param1", "20"]); -/// let external_args: Vec = vec!["custom".to_string(), "--param1".to_string(), "20".to_string()]; -/// assert!(matches!( -/// other_sub_command, -/// WalletSubCommand::Other(v) if v == external_args -/// )); -/// ``` -/// -/// To capture wallet sub-commands from a string vector without a preceeding binary name you can -/// create a custom struct the includes the `NoBinaryName` clap setting and wraps the WalletSubCommand -/// enum. See also the [`repl`](https://github.com/bitcoindevkit/bdk/blob/master/examples/repl.rs) -/// example app. -/// -/// # Example -/// ``` -/// # use bdk::cli::WalletSubCommand; -/// # use structopt::StructOpt; -/// # use clap::AppSettings; -/// -/// #[derive(Debug, StructOpt, Clone, PartialEq)] -/// #[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName, -/// version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"), -/// author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))] -/// struct ReplOpt { -/// /// Wallet sub-command -/// #[structopt(subcommand)] -/// pub subcommand: WalletSubCommand, -/// } -/// ``` -#[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt( - rename_all = "snake", - long_about = "A modern, lightweight, descriptor-based wallet" -)] -pub enum WalletSubCommand { - /// Generates a new external address - GetNewAddress, - /// Syncs with the chosen blockchain server - Sync { - /// max addresses to consider - #[structopt(short = "v", long = "max_addresses")] - max_addresses: Option, - }, - /// Lists the available spendable UTXOs - ListUnspent, - /// Lists all the incoming and outgoing transactions of the wallet - ListTransactions, - /// Returns the current wallet balance - GetBalance, - /// Creates a new unsigned transaction - CreateTx { - /// Adds a recipient to the transaction - #[structopt(name = "ADDRESS:SAT", long = "to", required = true, parse(try_from_str = parse_recipient))] - recipients: Vec<(Script, u64)>, - /// Sends all the funds (or all the selected utxos). Requires only one recipients of value 0 - #[structopt(short = "all", long = "send_all")] - send_all: bool, - /// Enables Replace-By-Fee (BIP125) - #[structopt(short = "rbf", long = "enable_rbf")] - enable_rbf: bool, - /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. - #[structopt(long = "offline_signer")] - offline_signer: bool, - /// Selects which utxos *must* be spent - #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))] - utxos: Option>, - /// Marks a utxo as unspendable - #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))] - unspendable: Option>, - /// Fee rate to use in sat/vbyte - #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")] - fee_rate: Option, - /// Selects which policy should be used to satisfy the external descriptor - #[structopt(name = "EXT_POLICY", long = "external_policy")] - external_policy: Option, - /// Selects which policy should be used to satisfy the internal descriptor - #[structopt(name = "INT_POLICY", long = "internal_policy")] - internal_policy: Option, - }, - /// Bumps the fees of an RBF transaction - BumpFee { - /// TXID of the transaction to update - #[structopt(name = "TXID", short = "txid", long = "txid")] - txid: String, - /// Allows the wallet to reduce the amount of the only output in order to increase fees. This is generally the expected behavior for transactions originally created with `send_all` - #[structopt(short = "all", long = "send_all")] - send_all: bool, - /// Make a PSBT that can be signed by offline signers and hardware wallets. Forces the addition of `non_witness_utxo` and more details to let the signer identify the change output. - #[structopt(long = "offline_signer")] - offline_signer: bool, - /// Selects which utxos *must* be added to the tx. Unconfirmed utxos cannot be used - #[structopt(name = "MUST_SPEND_TXID:VOUT", long = "utxos", parse(try_from_str = parse_outpoint))] - utxos: Option>, - /// Marks an utxo as unspendable, in case more inputs are needed to cover the extra fees - #[structopt(name = "CANT_SPEND_TXID:VOUT", long = "unspendable", parse(try_from_str = parse_outpoint))] - unspendable: Option>, - /// The new targeted fee rate in sat/vbyte - #[structopt(name = "SATS_VBYTE", short = "fee", long = "fee_rate")] - fee_rate: f32, - }, - /// Returns the available spending policies for the descriptor - Policies, - /// Returns the public version of the wallet's descriptor(s) - PublicDescriptor, - /// Signs and tries to finalize a PSBT - Sign { - /// Sets the PSBT to sign - #[structopt(name = "BASE64_PSBT", long = "psbt")] - psbt: String, - /// Assume the blockchain has reached a specific height. This affects the transaction finalization, if there are timelocks in the descriptor - #[structopt(name = "HEIGHT", long = "assume_height")] - assume_height: Option, - }, - /// 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 - #[structopt(name = "BASE64_PSBT", long = "psbt")] - psbt: String, - }, - /// Finalizes a PSBT - FinalizePsbt { - /// Sets the PSBT to finalize - #[structopt(name = "BASE64_PSBT", long = "psbt")] - psbt: String, - /// Assume the blockchain has reached a specific height - #[structopt(name = "HEIGHT", long = "assume_height")] - assume_height: Option, - }, - /// Combines multiple PSBTs into one - CombinePsbt { - /// Add one PSBT to combine. This option can be repeated multiple times, one for each PSBT - #[structopt(name = "BASE64_PSBT", long = "psbt", required = true)] - psbt: Vec, - }, - /// Put any extra arguments into this Vec - #[structopt(external_subcommand)] - Other(Vec), -} - -fn parse_recipient(s: &str) -> Result<(Script, u64), String> { - let parts: Vec<_> = s.split(':').collect(); - if parts.len() != 2 { - return Err("Invalid format".to_string()); - } - - let addr = Address::from_str(&parts[0]); - if let Err(e) = addr { - return Err(format!("{:?}", e)); - } - let val = u64::from_str(&parts[1]); - if let Err(e) = val { - return Err(format!("{:?}", e)); - } - - Ok((addr.unwrap().script_pubkey(), val.unwrap())) -} - -fn parse_outpoint(s: &str) -> Result { - OutPoint::from_str(s).map_err(|e| format!("{:?}", e)) -} - -/// Execute a wallet sub-command with a given [`Wallet`]. -/// -/// Wallet sub-commands are described in [`WalletSubCommand`]. See [`super::cli`] for example usage. -#[maybe_async] -pub fn handle_wallet_subcommand( - wallet: &Wallet, - wallet_subcommand: WalletSubCommand, -) -> Result -where - C: crate::blockchain::Blockchain, - D: crate::database::BatchDatabase, -{ - match wallet_subcommand { - WalletSubCommand::GetNewAddress => Ok(json!({"address": wallet.get_new_address()?})), - WalletSubCommand::Sync { max_addresses } => { - maybe_await!(wallet.sync(log_progress(), max_addresses))?; - Ok(json!({})) - } - WalletSubCommand::ListUnspent => Ok(serde_json::to_value(&wallet.list_unspent()?)?), - WalletSubCommand::ListTransactions => { - Ok(serde_json::to_value(&wallet.list_transactions(false)?)?) - } - WalletSubCommand::GetBalance => Ok(json!({"satoshi": wallet.get_balance()?})), - WalletSubCommand::CreateTx { - recipients, - send_all, - enable_rbf, - offline_signer, - utxos, - unspendable, - fee_rate, - external_policy, - internal_policy, - } => { - let mut tx_builder = TxBuilder::new(); - - if send_all { - tx_builder = tx_builder - .drain_wallet() - .set_single_recipient(recipients[0].0.clone()); - } else { - tx_builder = tx_builder.set_recipients(recipients); - } - - if enable_rbf { - tx_builder = tx_builder.enable_rbf(); - } - - if offline_signer { - tx_builder = tx_builder - .force_non_witness_utxo() - .include_output_redeem_witness_script(); - } - - if let Some(fee_rate) = fee_rate { - tx_builder = tx_builder.fee_rate(FeeRate::from_sat_per_vb(fee_rate)); - } - - if let Some(utxos) = utxos { - tx_builder = tx_builder.utxos(utxos).manually_selected_only(); - } - - if let Some(unspendable) = unspendable { - tx_builder = tx_builder.unspendable(unspendable); - } - - let policies = vec![ - external_policy.map(|p| (p, KeychainKind::External)), - internal_policy.map(|p| (p, KeychainKind::Internal)), - ]; - - for (policy, keychain) in policies.into_iter().filter_map(|x| x) { - let policy = serde_json::from_str::>>(&policy) - .map_err(|s| Error::Generic(s.to_string()))?; - tx_builder = tx_builder.policy_path(policy, keychain); - } - - let (psbt, details) = wallet.create_tx(tx_builder)?; - Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,})) - } - WalletSubCommand::BumpFee { - txid, - send_all, - offline_signer, - utxos, - unspendable, - fee_rate, - } => { - let txid = Txid::from_str(txid.as_str()).map_err(|s| Error::Generic(s.to_string()))?; - - let mut tx_builder = TxBuilder::new().fee_rate(FeeRate::from_sat_per_vb(fee_rate)); - - if send_all { - tx_builder = tx_builder.maintain_single_recipient(); - } - - if offline_signer { - tx_builder = tx_builder - .force_non_witness_utxo() - .include_output_redeem_witness_script(); - } - - if let Some(utxos) = utxos { - tx_builder = tx_builder.utxos(utxos); - } - - if let Some(unspendable) = unspendable { - tx_builder = tx_builder.unspendable(unspendable); - } - - let (psbt, details) = wallet.bump_fee(&txid, tx_builder)?; - Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"details": details,})) - } - WalletSubCommand::Policies => Ok(json!({ - "external": wallet.policies(KeychainKind::External)?, - "internal": wallet.policies(KeychainKind::Internal)?, - })), - WalletSubCommand::PublicDescriptor => Ok(json!({ - "external": wallet.public_descriptor(KeychainKind::External)?.map(|d| d.to_string()), - "internal": wallet.public_descriptor(KeychainKind::Internal)?.map(|d| d.to_string()), - })), - WalletSubCommand::Sign { - psbt, - assume_height, - } => { - let psbt = base64::decode(&psbt).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - let (psbt, finalized) = wallet.sign(psbt, assume_height)?; - Ok(json!({"psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,})) - } - WalletSubCommand::Broadcast { psbt, tx } => { - let tx = match (psbt, tx) { - (Some(psbt), None) => { - let psbt = base64::decode(&psbt).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - psbt.extract_tx() - } - (None, Some(tx)) => deserialize(&Vec::::from_hex(&tx).unwrap()).unwrap(), - (Some(_), Some(_)) => panic!("Both `psbt` and `tx` options not allowed"), - (None, None) => panic!("Missing `psbt` and `tx` option"), - }; - - let txid = maybe_await!(wallet.broadcast(tx))?; - Ok(json!({ "txid": txid })) - } - WalletSubCommand::ExtractPsbt { psbt } => { - let psbt = base64::decode(&psbt).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - Ok(json!({"raw_tx": serialize_hex(&psbt.extract_tx()),})) - } - WalletSubCommand::FinalizePsbt { - psbt, - assume_height, - } => { - let psbt = base64::decode(&psbt).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - - let (psbt, finalized) = wallet.finalize_psbt(psbt, assume_height)?; - Ok(json!({ "psbt": base64::encode(&serialize(&psbt)),"is_finalized": finalized,})) - } - WalletSubCommand::CombinePsbt { psbt } => { - let mut psbts = psbt - .iter() - .map(|s| { - let psbt = base64::decode(&s).unwrap(); - let psbt: PartiallySignedTransaction = deserialize(&psbt).unwrap(); - psbt - }) - .collect::>(); - - let init_psbt = psbts.pop().unwrap(); - let final_psbt = psbts - .into_iter() - .try_fold::<_, _, Result>( - init_psbt, - |mut acc, x| { - acc.merge(x)?; - Ok(acc) - }, - )?; - - Ok(json!({ "psbt": base64::encode(&serialize(&final_psbt)) })) - } - WalletSubCommand::Other(_) => Ok(json!({})), - } -} - -#[cfg(test)] -mod test { - use super::{WalletOpt, WalletSubCommand}; - use bitcoin::hashes::core::str::FromStr; - use bitcoin::{Address, OutPoint}; - use structopt::StructOpt; - - #[test] - fn test_get_new_address() { - let cli_args = vec!["repl", "--network", "bitcoin", - "--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", - "--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)", - "--esplora", "https://blockstream.info/api/", - "--esplora_concurrency", "5", - "get_new_address"]; - - let wallet_opt = WalletOpt::from_iter(&cli_args); - - let expected_wallet_opt = WalletOpt { - network: "bitcoin".to_string(), - wallet: "main".to_string(), - proxy: None, - descriptor: "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), - change_descriptor: Some("wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), - log_level: "info".to_string(), - #[cfg(feature = "esplora")] - esplora: Some("https://blockstream.info/api/".to_string()), - #[cfg(feature = "esplora")] - esplora_concurrency: 5, - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - subcommand: WalletSubCommand::GetNewAddress, - }; - - assert_eq!(expected_wallet_opt, wallet_opt); - } - - #[test] - fn test_sync() { - let cli_args = vec!["repl", "--network", "testnet", - "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", - "sync", "--max_addresses", "50"]; - - let wallet_opt = WalletOpt::from_iter(&cli_args); - - let expected_wallet_opt = WalletOpt { - network: "testnet".to_string(), - wallet: "main".to_string(), - proxy: None, - descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), - change_descriptor: None, - log_level: "info".to_string(), - #[cfg(feature = "esplora")] - esplora: None, - #[cfg(feature = "esplora")] - esplora_concurrency: 4, - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - subcommand: WalletSubCommand::Sync { - max_addresses: Some(50) - }, - }; - - assert_eq!(expected_wallet_opt, wallet_opt); - } - - #[test] - fn test_create_tx() { - let cli_args = vec!["repl", "--network", "testnet", "--proxy", "127.0.0.1:9150", - "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", - "--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)", - "--server","ssl://electrum.blockstream.info:50002", - "create_tx", "--to", "n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ:123456","mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf:78910", - "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1", - "--utxos","87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2"]; - - let wallet_opt = WalletOpt::from_iter(&cli_args); - - let script1 = Address::from_str("n2Z3YNXtceeJhFkTknVaNjT1mnCGWesykJ") - .unwrap() - .script_pubkey(); - let script2 = Address::from_str("mjDZ34icH4V2k9GmC8niCrhzVuR3z8Mgkf") - .unwrap() - .script_pubkey(); - let outpoint1 = OutPoint::from_str( - "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:1", - ) - .unwrap(); - let outpoint2 = OutPoint::from_str( - "87345e46bfd702d24d54890cc094d08a005f773b27c8f965dfe0eb1e23eef88e:2", - ) - .unwrap(); - - let expected_wallet_opt = WalletOpt { - network: "testnet".to_string(), - wallet: "main".to_string(), - proxy: Some("127.0.0.1:9150".to_string()), - descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), - change_descriptor: Some("wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)".to_string()), - log_level: "info".to_string(), - #[cfg(feature = "esplora")] - esplora: None, - #[cfg(feature = "esplora")] - esplora_concurrency: 4, - electrum: "ssl://electrum.blockstream.info:50002".to_string(), - subcommand: WalletSubCommand::CreateTx { - recipients: vec![(script1, 123456), (script2, 78910)], - send_all: false, - enable_rbf: false, - offline_signer: false, - utxos: Some(vec!(outpoint1, outpoint2)), - unspendable: None, - fee_rate: None, - external_policy: None, - internal_policy: None, - }, - }; - - assert_eq!(expected_wallet_opt, wallet_opt); - } - - #[test] - fn test_broadcast() { - let cli_args = vec!["repl", "--network", "testnet", - "--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)", - "broadcast", - "--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="]; - - let wallet_opt = WalletOpt::from_iter(&cli_args); - - let expected_wallet_opt = WalletOpt { - network: "testnet".to_string(), - wallet: "main".to_string(), - proxy: None, - descriptor: "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)".to_string(), - change_descriptor: None, - log_level: "info".to_string(), - #[cfg(feature = "esplora")] - esplora: None, - #[cfg(feature = "esplora")] - esplora_concurrency: 4, - electrum: "ssl://electrum.blockstream.info:60002".to_string(), - subcommand: WalletSubCommand::Broadcast { - psbt: Some("cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA=".to_string()), - tx: None - }, - }; - - assert_eq!(expected_wallet_opt, wallet_opt); - } -} diff --git a/src/lib.rs b/src/lib.rs index 163f4c3d..1712296e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,6 @@ //! //! * `all-keys`: all features for working with bitcoin keys //! * `async-interface`: async functions in bdk traits -//! * `cli-utils`: utilities for creating a command line interface wallet //! * `keys-bip39`: [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic codes for generating deterministic keys //! //! ## Internal features @@ -233,9 +232,6 @@ pub extern crate reqwest; #[cfg(feature = "key-value-db")] pub extern crate sled; -#[cfg(feature = "cli-utils")] -pub mod cli; - #[allow(unused_imports)] #[cfg(test)] #[macro_use]