diff --git a/src/bdk_cli.rs b/src/bdk_cli.rs index 265561d..ba53fff 100644 --- a/src/bdk_cli.rs +++ b/src/bdk_cli.rs @@ -12,11 +12,10 @@ use bitcoin::secp256k1::Secp256k1; use bitcoin::Network; -use std::fs; -use std::path::PathBuf; - use clap::AppSettings; use log::{debug, error, info, warn}; +use std::fs; +use std::path::PathBuf; #[cfg(feature = "repl")] use rustyline::error::ReadlineError; @@ -51,7 +50,7 @@ use bdk::database::{AnyDatabase, AnyDatabaseConfig, BatchDatabase, ConfigurableD use bdk::wallet::wallet_name_from_descriptor; use bdk::Wallet; use bdk::{bitcoin, Error}; -use bdk_cli::WalletSubCommand; +use bdk_cli::{Backend, WalletSubCommand}; use bdk_cli::{CliOpts, CliSubCommand, KeySubCommand, OfflineWalletSubCommand, WalletOpts}; #[cfg(any( @@ -86,6 +85,9 @@ enum ReplSubCommand { OfflineWalletSubCommand(OfflineWalletSubCommand), #[structopt(flatten)] KeySubCommand(KeySubCommand), + #[cfg(feature = "regtest-node")] + #[structopt(flatten)] + NodeSubCommand(bdk_cli::NodeSubCommand), /// Exit REPL loop Exit, } @@ -150,6 +152,33 @@ fn prepare_bc_dir(wallet_name: &str) -> Result { Ok(bc_dir) } +// We create only a global single node directory. Because multiple +// wallets can access the same node datadir, and they will have separate +// wallet names in `~/.bdk-bitcoin/node-data/regtest/wallets`. +// TODO: Fix the directory creation flow. +#[cfg(feature = "regtest-node")] +fn prepare_node_datadir() -> Result { + let mut dir = PathBuf::new(); + dir.push( + &dirs_next::home_dir().ok_or_else(|| Error::Generic("home dir not found".to_string()))?, + ); + dir.push(".bdk-bitcoin"); + + if !dir.exists() { + info!("Creating home directory {}", dir.as_path().display()); + fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + } + + dir.push("node-data"); + + if !dir.exists() { + info!("Creating node directory {}", dir.as_path().display()); + fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + } + + Ok(dir) +} + fn open_database(wallet_opts: &WalletOpts) -> Result { let wallet_name = wallet_opts.wallet.as_ref().expect("wallet name"); let database_path = prepare_wallet_db_dir(wallet_name)?; @@ -174,16 +203,6 @@ fn open_database(wallet_opts: &WalletOpts) -> Result { Ok(database) } -#[allow(dead_code)] -// Different Backend types activated with `regtest-*` mode. -// If `regtest-*` feature not activated, then default is `None`. -enum Backend { - None, - Bitcoin { rpc_url: String, rpc_auth: String }, - Electrum { electrum_url: String }, - Esplora { esplora_url: String }, -} - #[cfg(any( feature = "electrum", feature = "esplora", @@ -198,7 +217,7 @@ fn new_blockchain( #[cfg(feature = "electrum")] let config = { let url = match _backend { - Backend::Electrum { electrum_url } => electrum_url.to_owned(), + Backend::Electrum { electrsd } => electrsd.electrum_url, _ => wallet_opts.electrum_opts.server.clone(), }; @@ -212,13 +231,20 @@ fn new_blockchain( }; #[cfg(feature = "esplora")] - let config = AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { - base_url: wallet_opts.esplora_opts.server.clone(), - timeout: Some(wallet_opts.esplora_opts.timeout), - concurrency: Some(wallet_opts.esplora_opts.conc), - stop_gap: wallet_opts.esplora_opts.stop_gap, - proxy: wallet_opts.proxy_opts.proxy.clone(), - }); + let config = { + let url = match _backend { + Backend::Esplora { esplorad } => esplorad.esplora_url.expect("Esplora url expected"), + _ => wallet_opts.electrum_opts.server.clone(), + }; + + AnyBlockchainConfig::Esplora(EsploraBlockchainConfig { + base_url: url, + timeout: Some(wallet_opts.esplora_opts.timeout), + concurrency: Some(wallet_opts.esplora_opts.conc), + stop_gap: wallet_opts.esplora_opts.stop_gap, + proxy: wallet_opts.proxy_opts.proxy.clone(), + }); + }; #[cfg(feature = "compact_filters")] let config = { @@ -248,10 +274,10 @@ fn new_blockchain( #[cfg(feature = "rpc")] let config: AnyBlockchainConfig = { let (url, auth) = match _backend { - Backend::Bitcoin { rpc_url, rpc_auth } => ( - rpc_url, + Backend::Bitcoin { bitcoind } => ( + bitcoind.params.rpc_socket.to_string().clone(), Auth::Cookie { - file: rpc_auth.into(), + file: bitcoind.params.cookie_file.clone(), }, ), _ => { @@ -265,16 +291,13 @@ fn new_blockchain( password: wallet_opts.rpc_opts.basic_auth.1.clone(), } }; - (&wallet_opts.rpc_opts.address, auth) + (wallet_opts.rpc_opts.address.clone(), auth) } }; - // Use deterministic wallet name derived from descriptor - let wallet_name = wallet_name_from_descriptor( - &wallet_opts.descriptor[..], - wallet_opts.change_descriptor.as_deref(), - _network, - &Secp256k1::new(), - )?; + let wallet_name = wallet_opts + .wallet + .to_owned() + .expect("Wallet name should be available this level"); let rpc_url = "http://".to_string() + &url; @@ -319,10 +342,24 @@ fn main() { #[cfg(feature = "regtest-node")] let bitcoind = { - if network != Network::Regtest { - error!("Do not override default network value for `regtest-node` features"); - } - let bitcoind_conf = electrsd::bitcoind::Conf::default(); + // Configure node directory according to cli options + // nodes always have a persistent directory + let bitcoind_conf = { + match &cli_opts.data_dir { + None => { + let datadir = prepare_node_datadir().unwrap(); + let mut conf = electrsd::bitcoind::Conf::default(); + conf.staticdir = Some(datadir); + conf + } + Some(path) => { + let mut conf = electrsd::bitcoind::Conf::default(); + conf.staticdir = Some(path.into()); + conf + } + } + }; + let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path() .expect("We should always have downloaded path"); electrsd::bitcoind::BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap() @@ -331,14 +368,7 @@ fn main() { #[cfg(feature = "regtest-bitcoin")] let backend = { Backend::Bitcoin { - rpc_url: bitcoind.params.rpc_socket.to_string(), - rpc_auth: bitcoind - .params - .cookie_file - .clone() - .into_os_string() - .into_string() - .unwrap(), + bitcoind: &bitcoind, } }; @@ -349,7 +379,7 @@ fn main() { electrsd::downloaded_exe_path().expect("We should always have downloaded path"); let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); let backend = Backend::Electrum { - electrum_url: electrsd.electrum_url.clone(), + electrsd: &electrsd, }; (electrsd, backend) }; @@ -362,18 +392,15 @@ fn main() { electrsd::downloaded_exe_path().expect("Electrsd downloaded binaries not found"); let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); let backend = Backend::Esplora { - esplora_url: electrsd - .esplora_url - .clone() - .expect("Esplora port not open in electrum"), + esplorad: &electrsd, }; (electrsd, backend) }; #[cfg(not(feature = "regtest-node"))] - let backend = Backend::None; + let backend = Backend::None("no backend"); - match handle_command(cli_opts, network, backend) { + match handle_command(cli_opts, network, &backend) { Ok(result) => println!("{}", result), Err(e) => { match e { @@ -404,8 +431,16 @@ fn maybe_descriptor_wallet_name( Ok(wallet_opts) } -fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Result { +fn handle_command( + cli_opts: CliOpts, + network: Network, + _backend: &Backend, +) -> Result { let result = match cli_opts.subcommand { + #[cfg(feature = "regtest-node")] + CliSubCommand::Node { subcommand: cmd } => { + serde_json::to_string_pretty(&_backend.exec_cmd(cmd)?) + } #[cfg(any( feature = "electrum", feature = "esplora", @@ -422,7 +457,7 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res let wallet = new_wallet(network, &wallet_opts, database)?; let result = bdk_cli::handle_online_wallet_subcommand(&wallet, &blockchain, online_subcommand)?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } CliSubCommand::Wallet { wallet_opts, @@ -436,13 +471,13 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res &wallet_opts, offline_subcommand, )?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } CliSubCommand::Key { subcommand: key_subcommand, } => { let result = bdk_cli::handle_key_subcommand(network, key_subcommand)?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } #[cfg(feature = "compiler")] CliSubCommand::Compile { @@ -496,6 +531,10 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res debug!("repl_subcommand = {:?}", repl_subcommand); let result = match repl_subcommand { + #[cfg( feature = "regtest-node")] + ReplSubCommand::NodeSubCommand(sub_command) => { + _backend.exec_cmd(sub_command) + } #[cfg(any( feature = "electrum", feature = "esplora", @@ -534,7 +573,7 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res } } - "Exiting REPL".to_string() + Ok("Exiting REPL".to_string()) } #[cfg(all(feature = "reserves", feature = "electrum"))] CliSubCommand::ExternalReserves { @@ -555,7 +594,7 @@ fn handle_command(cli_opts: CliOpts, network: Network, _backend: Backend) -> Res serde_json::to_string_pretty(&result)? } }; - Ok(result) + result.map_err(|e| e.into()) } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 108fe12..6682b72 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,6 +102,7 @@ use std::str::FromStr; pub use structopt; use structopt::StructOpt; +use crate::bdk::keys::GeneratableKey; use crate::OfflineWalletSubCommand::*; #[cfg(any( feature = "electrum", @@ -124,6 +125,8 @@ use bdk::bitcoin::secp256k1::Secp256k1; use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey, KeySource}; use bdk::bitcoin::util::psbt::PartiallySignedTransaction; use bdk::bitcoin::{Address, Network, OutPoint, Script, Txid}; +#[cfg(feature = "regtest-bitcoin")] +use bdk::bitcoincore_rpc::RpcApi; #[cfg(all( feature = "reserves", any( @@ -150,7 +153,7 @@ use bdk::electrum_client::{Client, ElectrumApi}; use bdk::keys::bip39::{Language, Mnemonic, WordCount}; use bdk::keys::DescriptorKey::Secret; use bdk::keys::KeyError::{InvalidNetwork, Message}; -use bdk::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratableKey, GeneratedKey}; +use bdk::keys::{DerivableKey, DescriptorKey, ExtendedKey, GeneratedKey}; use bdk::miniscript::miniscript; #[cfg(feature = "compiler")] use bdk::miniscript::policy::Concrete; @@ -168,6 +171,12 @@ use bdk_reserves::reserves::verify_proof; ))] #[cfg(feature = "reserves")] use bdk_reserves::reserves::ProofOfReserves; +#[cfg(any( + feature = "regetst-electrum", + feature = "resgtest-esplora-ureq", + feature = "regtest-esplora-reqwest" +))] +use electrsd::ElectrsD; /// Global options /// @@ -263,6 +272,11 @@ pub struct CliOpts { default_value = "testnet" )] pub network: Network, + /// Sets the backend node data directory. + /// Default value : "~/.bdk-bitcoin/node-data" + #[cfg(feature = "regtest-node")] + #[structopt(name = "DATADIR", short = "d", long = "datadir")] + pub data_dir: Option, /// Top level cli sub-command #[structopt(subcommand)] pub subcommand: CliSubCommand, @@ -281,6 +295,13 @@ pub struct CliOpts { long_about = "Top level options and command modes" )] pub enum CliSubCommand { + /// Regtest node sub-commands + #[cfg(feature = "regtest-node")] + #[structopt(long_about = "Regtest Node mode")] + Node { + #[structopt(subcommand)] + subcommand: NodeSubCommand, + }, /// Wallet options and sub-commands #[structopt(long_about = "Wallet mode")] Wallet { @@ -334,6 +355,31 @@ pub enum CliSubCommand { }, } +#[cfg_attr(not(doc), allow(missing_docs))] +#[cfg_attr( + doc, + doc = r#" +Node operation sub-commands + +Used as an API for the node backend. +"# +)] +#[derive(Debug, StructOpt, Clone, PartialEq)] +#[structopt(rename_all = "lower")] +#[cfg(any(feature = "regtest-node"))] +pub enum NodeSubCommand { + /// Get info + GetInfo, + /// Get new address from node's test wallet + GetNewAddress, + /// Generate blocks + Generate { block_num: u64 }, + /// Get Wallet balance + GetBalance, + /// Send to an external wallet address + SendToAddress { address: String, amount: u64 }, +} + #[cfg_attr(not(doc), allow(missing_docs))] #[cfg_attr( doc, @@ -880,6 +926,70 @@ fn parse_outpoint(s: &str) -> Result { OutPoint::from_str(s).map_err(|e| e.to_string()) } +#[allow(dead_code)] +// Different Backend types activated with `regtest-*` mode. +// If `regtest-*` feature not activated, then default is `None`. +pub enum Backend<'node> { + None(&'node str), + #[cfg(feature = "regtest-bitcoin")] + Bitcoin { + bitcoind: &'node electrsd::bitcoind::BitcoinD, + }, + #[cfg(feature = "regtest-electrum")] + Electrum { + electrsd: &'node electrsd::ElectrsD, + }, + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + Esplora { + esplorad: &'node electrsd::ElectrsD, + }, +} + +impl<'node> Backend<'node> { + #[cfg(feature = "regtest-bitcoin")] + /// Execute a [`NodeSubCommand`] in the backend + pub fn exec_cmd(&self, cmd: NodeSubCommand) -> Result { + use bdk::bitcoin::Amount; + + match self { + Backend::Bitcoin { bitcoind } => match cmd { + NodeSubCommand::GetInfo => Ok(serde_json::to_value( + bitcoind.client.get_blockchain_info()?, + )?), + + NodeSubCommand::GetNewAddress => Ok(serde_json::to_value( + bitcoind.client.get_new_address(None, None)?, + )?), + + NodeSubCommand::Generate { block_num } => { + let core_addrs = bitcoind.client.get_new_address(None, None)?; + let block_hashes = bitcoind + .client + .generate_to_address(block_num, &core_addrs)?; + Ok(serde_json::to_value(block_hashes)?) + } + + NodeSubCommand::GetBalance => Ok(serde_json::to_value( + bitcoind.client.get_balance(None, None)?.to_string(), + )?), + + NodeSubCommand::SendToAddress { address, amount } => { + let address = + Address::from_str(&address).map_err(|e| Error::Generic(e.to_string()))?; + let amount = Amount::from_sat(amount); + let txid = bitcoind + .client + .send_to_address(&address, amount, None, None, None, None, None, None)?; + Ok(serde_json::to_value(&txid)?) + } + }, + _ => Err(Error::Generic( + "Pending: Other client implementations".to_string(), + )), + } + } +} + /// Execute an offline wallet sub-command /// /// Offline wallet sub-commands are described in [`OfflineWalletSubCommand`]. @@ -1076,7 +1186,7 @@ where .try_fold::<_, _, Result>( init_psbt, |mut acc, x| { - acc.merge(x)?; + acc.combine(x)?; Ok(acc) }, )?;