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