name = "bdk-cli"
version = "0.1.0"
edition = "2018"
-authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
+authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>", "Steve Myers <steve@notmandatory.org"]
+homepage = "https://bitcoindevkit.org"
+repository = "https://github.com/bitcoindevkit/bdk-cli"
+documentation = "https://docs.rs/bdk-cli"
+description = "A CLI library and example CLI tool based on the BDK descriptor-based wallet library"
+keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
+readme = "README.md"
+license-file = "LICENSE"
[dependencies]
bdk = { git = "https://github.com/bitcoindevkit/bdk.git" }
electrum = ["bdk/electrum"]
esplora = ["bdk/esplora"]
-
[[bin]]
-name = "repl"
-path = "src/repl.rs"
+name = "bdk-cli"
+path = "src/bdk_cli.rs"
required-features = ["repl"]
\ No newline at end of file
-# BDK CLI lib and REPL bin
+# bdk-cli lib and example bin tool


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.
-### REPL wallet usage examples
+### bdk-cli bin usage examples
-To get usage information for the `repl` wallet tool use the below command which
-returns a list of available wallet options and commands:
+To get usage information for the `bdk-cli` bin use the below command which returns a list of
+available wallet options and commands:
```shell
cargo run
--- /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::sync::Arc;
+
+use bitcoin::Network;
+use clap::AppSettings;
+use log::{debug, info, warn};
+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 CLI", 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() {
+ env_logger::init();
+
+ let cli_opt: WalletOpt = WalletOpt::from_args();
+
+ let network = cli_opt.network;
+ debug!("network: {:?}", network);
+ if network == Network::Bitcoin {
+ warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
+ }
+
+ let descriptor = cli_opt.descriptor.as_str();
+ let change_descriptor = cli_opt.change_descriptor.as_deref();
+ debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
+
+ 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());
+ }
+ }
+}
//! 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
+//! See the `bdk-cli` example bin for how to use this module to create a simple command line
//! wallet application.
//!
//! See [`WalletOpt`] for global wallet options and [`WalletSubCommand`] for supported sub-commands.
//! // to get args from cli use:
//! // let cli_opt = WalletOpt::from_args();
//!
-//! let cli_args = vec!["repl", "--network", "testnet", "--descriptor",
+//! let cli_args = vec!["bdk-cli", "--network", "testnet", "--descriptor",
//! "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
//! "sync", "--max_addresses", "50"];
//! let cli_opt = WalletOpt::from_iter(&cli_args);
/// # use structopt::StructOpt;
/// # use bdk_cli::{WalletSubCommand, WalletOpt};
///
-/// let cli_args = vec!["repl", "--network", "testnet",
+/// let cli_args = vec!["bdk-cli", "--network", "testnet",
/// "--descriptor", "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/44'/1'/0'/0/*)",
/// "sync", "--max_addresses", "50"];
///
/// ```
#[derive(Debug, StructOpt, Clone, PartialEq)]
-#[structopt(name = "BDK Wallet",
+#[structopt(name = "BDK CLI",
version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
pub struct WalletOpt {
/// 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.
+/// the command line or from a `String` vector, such as in the [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdkcli.rs)
+/// example cli wallet.
///
/// # Example
///
/// # use bdk_cli::WalletSubCommand;
/// # use structopt::StructOpt;
///
-/// let sync_sub_command = WalletSubCommand::from_iter(&["repl", "sync", "--max_addresses", "50"]);
+/// let sync_sub_command = WalletSubCommand::from_iter(&["bdk-cli", "sync", "--max_addresses", "50"]);
/// assert!(matches!(
/// sync_sub_command,
/// WalletSubCommand::Sync {
///
/// 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)
+/// enum. See also the [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli/blob/master/src/bdkcli.rs)
/// example app.
///
/// # Example
/// # use clap::AppSettings;
///
/// #[derive(Debug, StructOpt, Clone, PartialEq)]
-/// #[structopt(name = "BDK Wallet", setting = AppSettings::NoBinaryName,
+/// #[structopt(name = "BDK CLI", setting = AppSettings::NoBinaryName,
/// version = option_env ! ("CARGO_PKG_VERSION").unwrap_or("unknown"),
/// author = option_env ! ("CARGO_PKG_AUTHORS").unwrap_or(""))]
/// struct ReplOpt {
#[test]
fn test_get_new_address() {
- let cli_args = vec!["repl", "--network", "bitcoin",
+ let cli_args = vec!["bdk-cli", "--network", "bitcoin",
"--descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(xpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--esplora", "https://blockstream.info/api/",
#[test]
fn test_sync() {
- let cli_args = vec!["repl", "--network", "testnet",
+ let cli_args = vec!["bdk-cli", "--network", "testnet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"sync", "--max_addresses", "50"];
#[test]
fn test_create_tx() {
- let cli_args = vec!["repl", "--network", "testnet", "--proxy", "127.0.0.1:9150",
+ let cli_args = vec!["bdk-cli", "--network", "testnet", "--proxy", "127.0.0.1:9150",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"--change_descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/1/*)",
"--server","ssl://electrum.blockstream.info:50002",
#[test]
fn test_broadcast() {
- let cli_args = vec!["repl", "--network", "testnet",
+ let cli_args = vec!["bdk-cli", "--network", "testnet",
"--descriptor", "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)",
"broadcast",
"--psbt", "cHNidP8BAEICAAAAASWhGE1AhvtO+2GjJHopssFmgfbq+WweHd8zN/DeaqmDAAAAAAD/////AQAAAAAAAAAABmoEAAECAwAAAAAAAAA="];
+++ /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::sync::Arc;
-
-use bitcoin::Network;
-use clap::AppSettings;
-use log::{debug, info, warn};
-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() {
- env_logger::init();
-
- let cli_opt: WalletOpt = WalletOpt::from_args();
-
- let network = cli_opt.network;
- debug!("network: {:?}", network);
- if network == Network::Bitcoin {
- warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.")
- }
-
- let descriptor = cli_opt.descriptor.as_str();
- let change_descriptor = cli_opt.change_descriptor.as_deref();
- debug!("descriptors: {:?} {:?}", descriptor, change_descriptor);
-
- 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());
- }
- }
-}