diff --git a/CHANGELOG.md b/CHANGELOG.md index 215cba3..de884f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Change default database to `sqlite`. - Change the `esplora-reqwest` feature to always use async mode - Change rpc `--skip-blocks` option to `--start-time` which specifies time initial sync will start scanning from. +- Add new `bdk-cli node []` to control the backend node deployed by `regtest-*` features. +- Add an integration testing framework in `src/tests/integration.rs`. This framework uses the `regtest-*` feature to run automated testing with bdk-cli. ## [0.5.0] diff --git a/Cargo.lock b/Cargo.lock index 2ea14ff..3075154 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,9 +105,9 @@ dependencies = [ "async-trait", "bdk-macros", "bip39", - "bitcoin 0.28.1", + "bitcoin", "bitcoinconsensus", - "bitcoincore-rpc 0.15.0", + "bitcoincore-rpc", "cc", "electrum-client 0.11.0", "futures", @@ -206,18 +206,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "bitcoin" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" -dependencies = [ - "bech32", - "bitcoin_hashes 0.10.0", - "secp256k1 0.20.3", - "serde", -] - [[package]] name = "bitcoin" version = "0.28.1" @@ -227,7 +215,7 @@ dependencies = [ "base64-compat", "bech32", "bitcoin_hashes 0.10.0", - "secp256k1 0.22.1", + "secp256k1", "serde", ] @@ -256,68 +244,46 @@ dependencies = [ "libc", ] -[[package]] -name = "bitcoincore-rpc" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8d99d58466295cb2bf72c6959b784d59f8f0d6977458d2ba3eb75c834f36c3" -dependencies = [ - "bitcoincore-rpc-json 0.14.0", - "jsonrpc", - "log", - "serde", - "serde_json", -] - [[package]] name = "bitcoincore-rpc" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0e67dbf7a9971e7f4276f6089e9e814ce0f624a03216b7d92d00351ae7fb3e" dependencies = [ - "bitcoincore-rpc-json 0.15.0", + "bitcoincore-rpc-json", "jsonrpc", "log", "serde", "serde_json", ] -[[package]] -name = "bitcoincore-rpc-json" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce91de73c61f5776cf938bfa88378c5b404a70e3369b761dacbe6024fea79dd" -dependencies = [ - "bitcoin 0.27.1", - "serde", - "serde_json", -] - [[package]] name = "bitcoincore-rpc-json" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e2ae16202721ba8c3409045681fac790a5ddc791f05731a2df22c0c6bffc0f1" dependencies = [ - "bitcoin 0.28.1", + "bitcoin", "serde", "serde_json", ] [[package]] name = "bitcoind" -version = "0.20.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ddc41af9556a341c909bc71de33e16da52bf5f8dbda6b7a402054c60bdb722" +checksum = "0831b9721892ce845a6acadd111311bee84f9e1cc0c5017b8213ec4437ccdfe2" dependencies = [ "bitcoin_hashes 0.10.0", - "bitcoincore-rpc 0.14.0", + "bitcoincore-rpc", + "filetime", "flate2", "home", "log", "tar", "tempfile", "ureq 1.5.5", + "which", ] [[package]] @@ -558,13 +524,13 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "electrsd" -version = "0.12.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "334abee7787b76757ac34b13a9a1cbf1ef0f2da35162d3ceb95a5b0bc34df80f" +checksum = "5ad65605e022b44ab8c1e489547311bb48b5c605a0aea9ba908e12cae2880111" dependencies = [ "bitcoin_hashes 0.10.0", "bitcoind", - "electrum-client 0.8.0", + "electrum-client 0.10.2", "log", "nix", "ureq 2.2.0", @@ -573,11 +539,11 @@ dependencies = [ [[package]] name = "electrum-client" -version = "0.8.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd12f125852d77980725243b2a8b3bea73cd4c7a22c33bc52b08b664c561dc7" +checksum = "25ae36f27655f7705dd8e9105600a79e4f23d390649abbbc57aa87adbc57245d" dependencies = [ - "bitcoin 0.27.1", + "bitcoin", "log", "serde", "serde_json", @@ -589,7 +555,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af53c415260dcb220fa02182669727c730535bfed4edbfde42e1291342af95cd" dependencies = [ - "bitcoin 0.28.1", + "bitcoin", "byteorder", "libc", "log", @@ -682,23 +648,25 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.17" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", "libc", "redox_syscall", - "windows-sys 0.36.1", + "winapi", ] [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ + "cfg-if", "crc32fast", + "libc", "miniz_oxide", ] @@ -1212,17 +1180,18 @@ version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da39fc7a8adea97a677337b0091779dd86349226b869053af496584a9b9e5847" dependencies = [ - "bitcoin 0.28.1", + "bitcoin", "serde", ] [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", + "autocfg 1.1.0", ] [[package]] @@ -1881,16 +1850,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "secp256k1" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" -dependencies = [ - "secp256k1-sys 0.4.2", - "serde", -] - [[package]] name = "secp256k1" version = "0.22.1" @@ -1898,19 +1857,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" dependencies = [ "rand 0.6.5", - "secp256k1-sys 0.5.2", + "secp256k1-sys", "serde", ] -[[package]] -name = "secp256k1-sys" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" -dependencies = [ - "cc", -] - [[package]] name = "secp256k1-sys" version = "0.5.2" @@ -2641,6 +2591,17 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "which" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index d749962..ba7a46b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ rustyline = { version = "~9.0", optional = true } fd-lock = { version = "=3.0.2", optional = true } regex = { version = "1", optional = true } bdk-reserves = { version = "0.22", optional = true } -electrsd = { version= "0.12", features = ["trigger", "bitcoind_22_0"], optional = true} +electrsd = { version= "0.19", features = ["bitcoind_22_0"], optional = true} tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"], optional = true } [features] @@ -63,8 +63,9 @@ reserves = ["bdk-reserves"] # # This is most useful for integrations testing as well as quick demo testing # by devs using bdk and various types of background nodes. -regtest-node = [] -regtest-bitcoin = ["regtest-node" , "rpc", "electrsd"] +regtest-node = ["electrsd"] +regtest-bitcoin = ["regtest-node" , "rpc"] regtest-electrum = ["regtest-node", "electrum", "electrsd/electrs_0_8_10"] -regtest-esplora-ureq = ["regtest-node", "esplora-ureq", "electrsd/esplora_a33e97e1"] -regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"] +#TODO: Check why esplora in electrsd isn't working. +#regtest-esplora-ureq = ["regtest-node", "esplora-ureq", "electrsd/esplora_a33e97e1"] +#regtest-esplora-reqwest = ["regtest-node", "esplora-reqwest", "electrsd/esplora_a33e97e1"] \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs index 2e7eac4..fafa5ad 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -10,7 +10,9 @@ //! //! This module defines all the bdk-cli commands using [structopt] +#![allow(clippy::large_enum_variant)] use structopt::clap::AppSettings; + use structopt::StructOpt; use bdk::bitcoin::util::bip32::{DerivationPath, ExtendedPrivKey}; @@ -48,6 +50,11 @@ pub struct CliOpts { default_value = "testnet" )] pub network: Network, + /// Sets the wallet data directory. + /// Default value : "~/.bdk-bitcoin + #[structopt(name = "DATADIR", short = "d", long = "datadir")] + pub datadir: Option, + /// Top level cli sub-command #[structopt(subcommand)] pub subcommand: CliSubCommand, } @@ -56,6 +63,20 @@ pub struct CliOpts { #[derive(Debug, StructOpt, Clone, PartialEq)] #[structopt(rename_all = "snake")] pub enum CliSubCommand { + /// Node operation subcommands + /// + /// These commands can be used to control the backend bitcoin-core node + /// launched automatically with the `regtest-*` feature sets. The commands issues + /// bitcoin-cli rpc calls on the demon, in the background. + /// + /// Feel free to open feature-request in https://github.com/bitcoindevkit/bdk-cli + /// if you need extra rpc calls not covered in the command list. + #[cfg(feature = "regtest-node")] + #[structopt(long_about = "Regtest Node mode")] + Node { + #[structopt(subcommand)] + subcommand: NodeSubCommand, + }, /// Wallet Operations /// /// bdk-cli wallet operations includes all the basic wallet level tasks. @@ -121,6 +142,25 @@ pub enum CliSubCommand { }, } +#[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 given number of blocks and fund the internal wallet with coinbases. + Generate { block_num: u64 }, + /// Get Wallet balance + GetBalance, + /// Send to an external wallet address + SendToAddress { address: String, amount: u64 }, + /// Execute any bitcoin-cli commands + #[structopt(external_subcommand)] + BitcoinCli(Vec), +} + #[derive(Debug, StructOpt, Clone, PartialEq)] pub enum WalletSubCommand { #[cfg(any( @@ -499,20 +539,24 @@ pub enum KeySubCommand { #[cfg(feature = "repl")] #[derive(Debug, StructOpt, Clone, PartialEq)] -#[structopt(global_settings =&[AppSettings::NoBinaryName])] +#[structopt(global_settings =&[AppSettings::NoBinaryName], rename_all = "lower")] pub enum ReplSubCommand { - #[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "compact_filters", - feature = "rpc" - ))] - #[structopt(flatten)] - OnlineWalletSubCommand(OnlineWalletSubCommand), - #[structopt(flatten)] - OfflineWalletSubCommand(OfflineWalletSubCommand), - #[structopt(flatten)] - KeySubCommand(KeySubCommand), + /// Execute wallet Commands + Wallet { + #[structopt(subcommand)] + subcommand: WalletSubCommand, + }, + /// Execute Key Commands + Key { + #[structopt(subcommand)] + subcommand: KeySubCommand, + }, + /// Execute Node Commands + #[cfg(feature = "regtest-node")] + Node { + #[structopt(subcommand)] + subcommand: NodeSubCommand, + }, /// Exit REPL loop Exit, } @@ -568,6 +612,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -629,6 +674,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -668,6 +714,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -708,6 +755,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -749,6 +797,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -786,6 +835,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -826,6 +876,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -900,6 +951,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -969,6 +1021,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1039,6 +1092,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + datadir: None, subcommand: CliSubCommand::Wallet { wallet_opts: WalletOpts { wallet: None, @@ -1169,6 +1223,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Testnet, + datadir: None, subcommand: CliSubCommand::Compile { policy: "thresh(3,pk(Alice),pk(Bob),pk(Carol),older(2))".to_string(), script_type: "sh-wsh".to_string(), @@ -1374,6 +1429,7 @@ mod test { let expected_cli_opts = CliOpts { network: Network::Bitcoin, + datadir: None, subcommand: CliSubCommand::ExternalReserves { message, psbt, diff --git a/src/handlers.rs b/src/handlers.rs index aa25b5e..536ce7d 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -22,7 +22,6 @@ use crate::commands::OfflineWalletSubCommand::*; use crate::commands::OnlineWalletSubCommand::*; use crate::commands::*; use crate::utils::*; -use crate::Nodes; use bdk::{database::BatchDatabase, wallet::AddressIndex, Error, FeeRate, KeychainKind, Wallet}; use structopt::StructOpt; @@ -61,10 +60,7 @@ use bdk::miniscript::miniscript; use bdk::miniscript::policy::Concrete; use bdk::SignOptions; #[cfg(all(feature = "reserves", feature = "electrum"))] -use bdk::{ - bitcoin::{Address, OutPoint, TxOut}, - blockchain::Capability, -}; +use bdk::{bitcoin::Address, blockchain::Capability}; use bdk_macros::maybe_async; #[cfg(any( feature = "electrum", @@ -301,7 +297,7 @@ where feature = "compact_filters", feature = "rpc" ))] -pub fn handle_online_wallet_subcommand( +pub(crate) fn handle_online_wallet_subcommand( wallet: &Wallet, blockchain: &B, online_subcommand: OnlineWalletSubCommand, @@ -387,7 +383,7 @@ where /// Execute a key sub-command /// /// Key sub-commands are described in [`KeySubCommand`]. -pub fn handle_key_subcommand( +pub(crate) fn handle_key_subcommand( network: Network, subcommand: KeySubCommand, ) -> Result { @@ -458,7 +454,7 @@ pub fn handle_key_subcommand( /// /// Compiler options are described in [`CliSubCommand::Compile`]. #[cfg(feature = "compiler")] -pub fn handle_compile_subcommand( +pub(crate) fn handle_compile_subcommand( _network: Network, policy: String, script_type: String, @@ -486,7 +482,7 @@ pub fn handle_compile_subcommand( /// /// Proof of reserves options are described in [`CliSubCommand::ExternalReserves`]. #[cfg(all(feature = "reserves", feature = "electrum"))] -pub fn handle_ext_reserves_subcommand( +pub(crate) fn handle_ext_reserves_subcommand( network: Network, message: String, psbt: String, @@ -523,47 +519,16 @@ pub fn handle_ext_reserves_subcommand( Ok(json!({ "spendable": spendable })) } -#[cfg(all(feature = "reserves", feature = "electrum"))] -pub fn get_outpoints_for_address( - address: Address, - client: &Client, - max_confirmation_height: Option, -) -> Result, Error> { - let unspents = client - .script_list_unspent(&address.script_pubkey()) - .map_err(Error::Electrum)?; - - unspents - .iter() - .filter(|utxo| { - utxo.height > 0 && utxo.height <= max_confirmation_height.unwrap_or(usize::MAX) - }) - .map(|utxo| { - let tx = match client.transaction_get(&utxo.tx_hash) { - Ok(tx) => tx, - Err(e) => { - return Err(e).map_err(Error::Electrum); - } - }; - - Ok(( - OutPoint { - txid: utxo.tx_hash, - vout: utxo.tx_pos as u32, - }, - tx.output[utxo.tx_pos].clone(), - )) - }) - .collect() -} - #[maybe_async] -pub fn handle_command( - cli_opts: CliOpts, - network: Network, - _backend: Nodes, -) -> Result { +pub(crate) fn handle_command(cli_opts: CliOpts) -> Result { + let network = cli_opts.network; + let home_dir = prepare_home_dir(cli_opts.datadir)?; let result = match cli_opts.subcommand { + #[cfg(feature = "regtest-node")] + CliSubCommand::Node { subcommand: cmd } => { + let backend = new_backend(&home_dir)?; + serde_json::to_string_pretty(&backend.exec_cmd(cmd)?) + } #[cfg(any( feature = "electrum", feature = "esplora", @@ -574,48 +539,49 @@ pub fn handle_command( wallet_opts, subcommand: WalletSubCommand::OnlineWalletSubCommand(online_subcommand), } => { - let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?; - let database = open_database(&wallet_opts)?; - let blockchain = new_blockchain(network, &wallet_opts, &_backend)?; + let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, cli_opts.network)?; + let database = open_database(&wallet_opts, &home_dir)?; + let backend = new_backend(&home_dir)?; + let blockchain = new_blockchain(network, &wallet_opts, &backend, &home_dir)?; let wallet = new_wallet(network, &wallet_opts, database)?; let result = maybe_await!(handle_online_wallet_subcommand( &wallet, &blockchain, online_subcommand ))?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } CliSubCommand::Wallet { wallet_opts, subcommand: WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), } => { let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?; - let database = open_database(&wallet_opts)?; + let database = open_database(&wallet_opts, &home_dir)?; let wallet = new_wallet(network, &wallet_opts, database)?; let result = handle_offline_wallet_subcommand(&wallet, &wallet_opts, offline_subcommand)?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } CliSubCommand::Key { subcommand: key_subcommand, } => { - let result = handle_key_subcommand(network, key_subcommand)?; - serde_json::to_string_pretty(&result)? + let result = handle_key_subcommand(cli_opts.network, key_subcommand)?; + serde_json::to_string_pretty(&result) } #[cfg(feature = "compiler")] CliSubCommand::Compile { policy, script_type, } => { - let result = handle_compile_subcommand(network, policy, script_type)?; - serde_json::to_string_pretty(&result)? + let result = handle_compile_subcommand(cli_opts.network, policy, script_type)?; + serde_json::to_string_pretty(&result) } #[cfg(feature = "repl")] CliSubCommand::Repl { wallet_opts } => { - let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, network)?; - let database = open_database(&wallet_opts)?; + let wallet_opts = maybe_descriptor_wallet_name(wallet_opts, cli_opts.network)?; + let database = open_database(&wallet_opts, &home_dir)?; - let wallet = new_wallet(network, &wallet_opts, database)?; + let wallet = new_wallet(cli_opts.network, &wallet_opts, database)?; let mut rl = Editor::<()>::new(); @@ -626,6 +592,14 @@ pub fn handle_command( let split_regex = Regex::new(crate::REPL_LINE_SPLIT_REGEX) .map_err(|e| Error::Generic(e.to_string()))?; + #[cfg(any( + feature = "electrum", + feature = "esplora", + feature = "compact_filters", + feature = "rpc" + ))] + let backend = new_backend(&home_dir)?; + loop { let readline = rl.readline(">> "); match readline { @@ -654,29 +628,45 @@ pub fn handle_command( log::debug!("repl_subcommand = {:?}", repl_subcommand); let result = match repl_subcommand { + #[cfg(feature = "regtest-node")] + ReplSubCommand::Node { subcommand } => { + match backend.exec_cmd(subcommand) { + Ok(result) => Ok(result), + Err(e) => Ok(serde_json::Value::String(e.to_string())), + } + } #[cfg(any( feature = "electrum", feature = "esplora", feature = "compact_filters", feature = "rpc" ))] - ReplSubCommand::OnlineWalletSubCommand(online_subcommand) => { - let blockchain = new_blockchain(network, &wallet_opts, &_backend)?; + ReplSubCommand::Wallet { + subcommand: + WalletSubCommand::OnlineWalletSubCommand(online_subcommand), + } => { + let blockchain = new_blockchain( + cli_opts.network, + &wallet_opts, + &backend, + &home_dir, + )?; maybe_await!(handle_online_wallet_subcommand( &wallet, &blockchain, online_subcommand, )) } - ReplSubCommand::OfflineWalletSubCommand(offline_subcommand) => { - handle_offline_wallet_subcommand( - &wallet, - &wallet_opts, - offline_subcommand, - ) - } - ReplSubCommand::KeySubCommand(key_subcommand) => { - handle_key_subcommand(network, key_subcommand) + ReplSubCommand::Wallet { + subcommand: + WalletSubCommand::OfflineWalletSubCommand(offline_subcommand), + } => handle_offline_wallet_subcommand( + &wallet, + &wallet_opts, + offline_subcommand, + ), + ReplSubCommand::Key { subcommand } => { + handle_key_subcommand(cli_opts.network, subcommand) } ReplSubCommand::Exit => break, }; @@ -692,7 +682,7 @@ pub fn handle_command( } } - "Exiting REPL".to_string() + Ok("Exiting REPL".to_string()) } #[cfg(all(feature = "reserves", feature = "electrum"))] CliSubCommand::ExternalReserves { @@ -703,15 +693,15 @@ pub fn handle_command( electrum_opts, } => { let result = handle_ext_reserves_subcommand( - network, + cli_opts.network, message, psbt, confirmations, addresses, electrum_opts, )?; - serde_json::to_string_pretty(&result)? + serde_json::to_string_pretty(&result) } }; - Ok(result) + result.map_err(|e| e.into()) } diff --git a/src/main.rs b/src/main.rs index fced154..66e53f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,9 +14,6 @@ mod commands; mod handlers; mod nodes; mod utils; - -use nodes::Nodes; - use bitcoin::Network; use log::{debug, error, warn}; @@ -43,63 +40,7 @@ fn main() { warn!("This is experimental software and not currently recommended for use on Bitcoin mainnet, proceed with caution.") } - #[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(); - 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() - }; - - #[cfg(feature = "regtest-bitcoin")] - let backend = { - Nodes::Bitcoin { - rpc_url: bitcoind.params.rpc_socket.to_string(), - rpc_auth: bitcoind - .params - .cookie_file - .clone() - .into_os_string() - .into_string() - .unwrap(), - } - }; - - #[cfg(feature = "regtest-electrum")] - let (_electrsd, backend) = { - let elect_conf = electrsd::Conf::default(); - let elect_exe = - 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 = Nodes::Electrum { - electrum_url: electrsd.electrum_url.clone(), - }; - (electrsd, backend) - }; - - #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] - let (_electrsd, backend) = { - let mut elect_conf = electrsd::Conf::default(); - elect_conf.http_enabled = true; - let elect_exe = - electrsd::downloaded_exe_path().expect("Electrsd downloaded binaries not found"); - let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); - let backend = Nodes::Esplora { - esplora_url: electrsd - .esplora_url - .clone() - .expect("Esplora port not open in electrum"), - }; - (electrsd, nodes) - }; - - #[cfg(not(feature = "regtest-node"))] - let backend = Nodes::None; - - match maybe_await!(handle_command(cli_opts, network, backend)) { + match maybe_await!(handle_command(cli_opts)) { Ok(result) => println!("{}", result), Err(e) => { match e { diff --git a/src/nodes.rs b/src/nodes.rs index f3873e8..fd2686f 100644 --- a/src/nodes.rs +++ b/src/nodes.rs @@ -8,14 +8,120 @@ //! The Node structures //! -//! This module defines the the backend node structures for `regtest-*` features +//! This module defines containers for different backend clients. +//! These Backends are auto-deployed in `regtest-*` features to spawn a blockchain +//! interface of selected types, and connects the bdk-cli wallet to it. +//! +//! For more information check TODO: [Add readme section for `regtest-*` features.] + +#[cfg(feature = "regtest-node")] +use { + crate::commands::NodeSubCommand, + bdk::{ + bitcoin::{Address, Amount}, + Error, + }, + electrsd::bitcoind::bitcoincore_rpc::{Client, RpcApi}, + serde_json::Value, + std::str::FromStr, +}; #[allow(dead_code)] // Different regtest node types activated with `regtest-*` mode. // If `regtest-*` feature not activated, then default is `None`. pub enum Nodes { None, - Bitcoin { rpc_url: String, rpc_auth: String }, - Electrum { electrum_url: String }, - Esplora { esplora_url: String }, + #[cfg(feature = "regtest-bitcoin")] + // A bitcoin core backend. Wallet connected to it via RPC. + Bitcoin { + bitcoind: Box, + }, + #[cfg(feature = "regtest-electrum")] + // An Electrum backend, with an underlying bitcoin core + // Wallet connected to it, via the electrum url + Electrum { + bitcoind: Box, + electrsd: Box, + }, + // An Esplora backend with underlying bitcoin core. + // Wallet connected to it, via the esplora url + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + Esplora { + bitcoind: Box, + esplorad: Box, + }, +} + +#[cfg(feature = "regtest-node")] +impl Nodes { + /// Execute a [`NodeSubCommand`] in the backend + pub fn exec_cmd(&self, cmd: NodeSubCommand) -> Result { + let client = self.get_client()?; + match cmd { + NodeSubCommand::GetInfo => Ok(serde_json::to_value( + client + .get_blockchain_info() + .map_err(|e| Error::Generic(e.to_string()))?, + )?), + + NodeSubCommand::GetNewAddress => Ok(serde_json::to_value( + client + .get_new_address(None, None) + .map_err(|e| Error::Generic(e.to_string()))?, + )?), + + NodeSubCommand::Generate { block_num } => { + let core_addrs = client + .get_new_address(None, None) + .map_err(|e| Error::Generic(e.to_string()))?; + let block_hashes = client + .generate_to_address(block_num, &core_addrs) + .map_err(|e| Error::Generic(e.to_string()))?; + Ok(serde_json::to_value(block_hashes)?) + } + + NodeSubCommand::GetBalance => Ok(serde_json::to_value( + client + .get_balance(None, None) + .map_err(|e| Error::Generic(e.to_string()))? + .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 = client + .send_to_address(&address, amount, None, None, None, None, None, None) + .map_err(|e| Error::Generic(e.to_string()))?; + Ok(serde_json::to_value(&txid)?) + } + + NodeSubCommand::BitcoinCli(args) => { + let cmd = &args[0]; + let args = args[1..] + .iter() + .map(|arg| serde_json::Value::from_str(arg)) + .collect::, _>>()?; + client + .call::(cmd, &args) + .map_err(|e| Error::Generic(e.to_string())) + } + } + } + + // Expose the underlying RPC client + pub fn get_client(&self) -> Result<&Client, Error> { + match self { + Self::None => Err(Error::Generic( + "No backend available. Cannot execute node commands".to_string(), + )), + #[cfg(feature = "regtest-bitcoin")] + Self::Bitcoin { bitcoind } => Ok(&bitcoind.client), + #[cfg(feature = "regtest-electrum")] + Self::Electrum { bitcoind, .. } => Ok(&bitcoind.client), + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + Self::Esplora { bitcoind, .. } => Ok(&bitcoind.client), + } + } } diff --git a/src/utils.rs b/src/utils.rs index aecaad5..8333ce5 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -10,16 +10,16 @@ //! //! This module includes all the utility tools used by the App. -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk::electrum_client::{Client, ElectrumApi}; + +#[cfg(all(feature = "reserves", feature = "electrum"))] +use bdk::bitcoin::TxOut; + use crate::commands::WalletOpts; -#[cfg(any( - feature = "electrum", - feature = "esplora", - feature = "compact_filters", - feature = "rpc" -))] use crate::nodes::Nodes; use bdk::bitcoin::secp256k1::Secp256k1; use bdk::bitcoin::{Address, Network, OutPoint, Script}; @@ -79,6 +79,7 @@ pub(crate) fn parse_recipient(s: &str) -> Result<(Script, u64), String> { Ok((addr.script_pubkey(), val)) } + #[cfg(any( feature = "electrum", feature = "compact_filters", @@ -98,24 +99,73 @@ pub(crate) fn parse_proxy_auth(s: &str) -> Result<(String, String), String> { Ok((user, passwd)) } +#[cfg(all(feature = "reserves", feature = "electrum"))] +pub fn get_outpoints_for_address( + address: Address, + client: &Client, + max_confirmation_height: Option, +) -> Result, Error> { + let unspents = client + .script_list_unspent(&address.script_pubkey()) + .map_err(Error::Electrum)?; + + unspents + .iter() + .filter(|utxo| { + utxo.height > 0 && utxo.height <= max_confirmation_height.unwrap_or(usize::MAX) + }) + .map(|utxo| { + let tx = match client.transaction_get(&utxo.tx_hash) { + Ok(tx) => tx, + Err(e) => { + return Err(e).map_err(Error::Electrum); + } + }; + + Ok(( + OutPoint { + txid: utxo.tx_hash, + vout: utxo.tx_pos as u32, + }, + tx.output[utxo.tx_pos].clone(), + )) + }) + .collect() +} + /// Parse a outpoint (Txid:Vout) argument from cli input pub(crate) fn parse_outpoint(s: &str) -> Result { OutPoint::from_str(s).map_err(|e| e.to_string()) } -/// prepare bdk_cli home and wallet directory -pub(crate) fn prepare_home_wallet_dir(wallet_name: &str) -> 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"); +/// prepare bdk-cli home directory +/// +/// This function is called to check if [`crate::CliOpts`] datadir is set. +/// If not the default home directory is created at `~/.bdk-bitcoin +pub(crate) fn prepare_home_dir(home_path: Option) -> Result { + let dir = home_path.unwrap_or_else(|| { + let mut dir = PathBuf::new(); + dir.push( + &dirs_next::home_dir() + .ok_or_else(|| Error::Generic("home dir not found".to_string())) + .unwrap(), + ); + dir.push(".bdk-bitcoin"); + dir + }); if !dir.exists() { log::info!("Creating home directory {}", dir.as_path().display()); std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; } + Ok(dir) +} + +/// prepare bdk_cli wallet directory +fn prepare_wallet_dir(wallet_name: &str, home_path: &Path) -> Result { + let mut dir = home_path.to_owned(); + dir.push(wallet_name); if !dir.exists() { @@ -127,8 +177,8 @@ pub(crate) fn prepare_home_wallet_dir(wallet_name: &str) -> Result Result { - let mut db_dir = prepare_home_wallet_dir(wallet_name)?; +fn prepare_wallet_db_dir(wallet_name: &str, home_path: &Path) -> Result { + let mut db_dir = prepare_wallet_dir(wallet_name, home_path)?; #[cfg(feature = "key-value-db")] db_dir.push("wallet.sled"); @@ -147,8 +197,8 @@ pub(crate) fn prepare_wallet_db_dir(wallet_name: &str) -> Result /// Prepare blockchain data directory (for compact filters) #[cfg(feature = "compact_filters")] -pub(crate) fn prepare_bc_dir(wallet_name: &str) -> Result { - let mut bc_dir = prepare_home_wallet_dir(wallet_name)?; +fn prepare_bc_dir(wallet_name: &str, home_path: &Path) -> Result { + let mut bc_dir = prepare_wallet_dir(wallet_name, home_path)?; bc_dir.push("compact_filters"); @@ -163,10 +213,47 @@ pub(crate) 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 `/bitcoind/regtest/wallets`. +#[cfg(feature = "regtest-node")] +pub(crate) fn prepare_bitcoind_datadir(home_path: &Path) -> Result { + let mut dir = home_path.to_owned(); + + dir.push("bitcoind"); + + if !dir.exists() { + log::info!("Creating node directory {}", dir.as_path().display()); + std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + } + + Ok(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 `/electrsd/regtest/wallets`. +#[cfg(feature = "regtest-electrum")] +pub(crate) fn prepare_electrum_datadir(home_path: &Path) -> Result { + let mut dir = home_path.to_owned(); + + dir.push("electrsd"); + + if !dir.exists() { + log::info!("Creating node directory {}", dir.as_path().display()); + std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + } + + Ok(dir) +} + /// Open the wallet database -pub(crate) fn open_database(wallet_opts: &WalletOpts) -> Result { +pub(crate) fn open_database( + wallet_opts: &WalletOpts, + home_path: &Path, +) -> Result { let wallet_name = wallet_opts.wallet.as_ref().expect("wallet name"); - let database_path = prepare_wallet_db_dir(wallet_name)?; + let database_path = prepare_wallet_db_dir(wallet_name, home_path)?; #[cfg(feature = "key-value-db")] let config = AnyDatabaseConfig::Sled(SledDbConfiguration { @@ -191,28 +278,104 @@ pub(crate) fn open_database(wallet_opts: &WalletOpts) -> Result Result { + #[cfg(feature = "regtest-node")] + let bitcoind = { + // Configure node directory according to cli options + // nodes always have a persistent directory + let datadir = prepare_bitcoind_datadir(_datadir)?; + let mut bitcoind_conf = electrsd::bitcoind::Conf::default(); + bitcoind_conf.staticdir = Some(datadir); + let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path() + .expect("We should always have downloaded path"); + electrsd::bitcoind::BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf) + .map_err(|e| Error::Generic(e.to_string()))? + }; + + #[cfg(feature = "regtest-bitcoin")] + let backend = { + Nodes::Bitcoin { + bitcoind: Box::new(bitcoind), + } + }; + + #[cfg(feature = "regtest-electrum")] + let backend = { + // Configure node directory according to cli options + // nodes always have a persistent directory + let datadir = prepare_electrum_datadir(_datadir)?; + let mut elect_conf = electrsd::Conf::default(); + elect_conf.staticdir = Some(datadir); + let elect_exe = + electrsd::downloaded_exe_path().expect("We should always have downloaded path"); + let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf) + .map_err(|e| Error::Generic(e.to_string()))?; + Nodes::Electrum { + bitcoind: Box::new(bitcoind), + electrsd: Box::new(electrsd), + } + }; + + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + let backend = { + // Configure node directory according to cli options + // nodes always have a persistent directory + let mut elect_conf = { + match _datadir { + None => { + let datadir = utils::prepare_electrum_datadir().unwrap(); + let mut conf = electrsd::Conf::default(); + conf.staticdir = Some(_datadir); + conf + } + Some(path) => { + let mut conf = electrsd::Conf::default(); + conf.staticdir = Some(path.into()); + conf + } + } + }; + elect_conf.http_enabled = true; + let elect_exe = + electrsd::downloaded_exe_path().expect("Electrsd downloaded binaries not found"); + let electrsd = electrsd::ElectrsD::with_conf(elect_exe, &bitcoind, &elect_conf).unwrap(); + Nodes::Esplora { + bitcoind: Box::new(bitcoind), + esplorad: Box::new(electrsd), + } + }; + + #[cfg(not(feature = "regtest-node"))] + let backend = Nodes::None; + + Ok(backend) +} + #[cfg(any( feature = "electrum", feature = "esplora", feature = "compact_filters", feature = "rpc" ))] -/// Create a new blockchain for a given [Backend] if available +/// Create a new blockchain for a given [Nodes] if available /// Or else create one from the wallet configuration options pub(crate) fn new_blockchain( _network: Network, wallet_opts: &WalletOpts, _backend: &Nodes, + _home_dir: &Path, ) -> Result { #[cfg(feature = "electrum")] let config = { let url = match _backend { - Nodes::Electrum { electrum_url } => electrum_url.to_owned(), - _ => wallet_opts.electrum_opts.server.clone(), + #[cfg(feature = "regtest-electrum")] + Nodes::Electrum { electrsd, .. } => &electrsd.electrum_url, + _ => &wallet_opts.electrum_opts.server, }; AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig { - url, + url: url.to_owned(), socks5: wallet_opts.proxy_opts.proxy.clone(), retry: wallet_opts.proxy_opts.retries, timeout: wallet_opts.electrum_opts.timeout, @@ -221,13 +384,21 @@ pub(crate) 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 { + #[cfg(any(feature = "regtest-esplora-ureq", feature = "regtest-esplora-reqwest"))] + Nodes::Esplora { esplorad } => esplorad.esplora_url.expect("Esplora url expected"), + _ => wallet_opts.esplora_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 = { @@ -246,7 +417,7 @@ pub(crate) fn new_blockchain( AnyBlockchainConfig::CompactFilters(CompactFiltersBlockchainConfig { peers, network: _network, - storage_dir: prepare_bc_dir(wallet_name)? + storage_dir: prepare_bc_dir(wallet_name, _home_dir)? .into_os_string() .into_string() .map_err(|_| Error::Generic("Internal OS_String conversion error".to_string()))?, @@ -257,10 +428,11 @@ pub(crate) fn new_blockchain( #[cfg(feature = "rpc")] let config: AnyBlockchainConfig = { let (url, auth) = match _backend { - Nodes::Bitcoin { rpc_url, rpc_auth } => ( - rpc_url, + #[cfg(feature = "regtest-node")] + Nodes::Bitcoin { bitcoind } => ( + bitcoind.params.rpc_socket.to_string(), Auth::Cookie { - file: rpc_auth.into(), + file: bitcoind.params.cookie_file.clone(), }, ), _ => { @@ -274,18 +446,15 @@ pub(crate) 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 rpc_url = "http://".to_string() + url; + let wallet_name = wallet_opts + .wallet + .to_owned() + .expect("Wallet name should be available this level"); + + let rpc_url = "http://".to_string() + &url; let rpc_config = RpcConfig { url: rpc_url, diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..01d7683 --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,253 @@ +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! bdk-cli Integration Test Framework +//! +//! This modules performs the necessary integration test for bdk-cli +//! The tests can be run using `cargo test` + +#[cfg(feature = "regtest-node")] +mod test { + use electrsd::bitcoind::tempfile::TempDir; + use serde_json::{json, Value}; + use std::convert::From; + use std::path::PathBuf; + use std::process::Command; + + /// Testing errors for integration tests + #[derive(Debug)] + enum IntTestError { + // IO error + IO(std::io::Error), + // Command execution error + CmdExec(String), + // Json Data error + JsonData(String), + } + + impl From for IntTestError { + fn from(e: std::io::Error) -> Self { + IntTestError::IO(e) + } + } + + // Helper function + // Runs a system command with given args + fn run_cmd_with_args(cmd: &str, args: &[&str]) -> Result { + let output = Command::new(cmd).args(args).output().unwrap(); + let mut value = output.stdout; + let error = output.stderr; + if value.len() == 0 { + return Err(IntTestError::CmdExec(String::from_utf8(error).unwrap())); + } + value.pop(); // remove `\n` at end + let output_string = std::str::from_utf8(&value).unwrap(); + let json_value: serde_json::Value = match serde_json::from_str(output_string) { + Ok(value) => value, + Err(_) => json!(output_string), // bitcoin-cli will sometime return raw string + }; + Ok(json_value) + } + + // Helper Function + // Transforms a json value to string + fn value_to_string(value: &Value) -> Result { + match value { + Value::Bool(bool) => match bool { + true => Ok("true".to_string()), + false => Ok("false".to_string()), + }, + Value::Number(n) => Ok(n.to_string()), + Value::String(s) => Ok(s.to_string()), + _ => Err(IntTestError::JsonData( + "Value parsing not implemented for this type".to_string(), + )), + } + } + + // Helper Function + // Extracts value from a given json object and key + fn get_value(json: &Value, key: &str) -> Result { + let map = json + .as_object() + .ok_or(IntTestError::JsonData("Json is not an object".to_string()))?; + let value = map + .get(key) + .ok_or(IntTestError::JsonData("Invalid key".to_string()))? + .to_owned(); + let string_value = value_to_string(&value)?; + Ok(string_value) + } + + /// The bdk-cli command struct + /// Use it to perform all bdk-cli operations + #[derive(Debug)] + struct BdkCli { + target: String, + network: String, + verbosity: bool, + recv_desc: Option, + chang_desc: Option, + node_datadir: Option, + } + + impl BdkCli { + /// Construct a new [`BdkCli`] struct + fn new( + network: &str, + node_datadir: Option, + verbosity: bool, + features: &[&str], + ) -> Result { + // Build bdk-cli with given features + let mut feat = "--features=".to_string(); + for item in features { + feat.push_str(item); + feat.push_str(","); + } + feat.pop(); // remove the last comma + let _build = Command::new("cargo").args(&["build", &feat]).output()?; + + let mut bdk_cli = Self { + target: "./target/debug/bdk-cli".to_string(), + network: network.to_string(), + verbosity, + recv_desc: None, + chang_desc: None, + node_datadir, + }; + + println!("BDK-CLI Config : {:#?}", bdk_cli); + let bdk_master_key = bdk_cli.key_exec(&["generate"])?; + let bdk_xprv = get_value(&bdk_master_key, "xprv")?; + + let bdk_recv_desc = + bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/0", "--xprv", &bdk_xprv])?; + let bdk_recv_desc = get_value(&bdk_recv_desc, "xprv")?; + let bdk_recv_desc = format!("wpkh({})", bdk_recv_desc); + + let bdk_chng_desc = + bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/1", "--xprv", &bdk_xprv])?; + let bdk_chng_desc = get_value(&bdk_chng_desc, "xprv")?; + let bdk_chng_desc = format!("wpkh({})", bdk_chng_desc); + + bdk_cli.recv_desc = Some(bdk_recv_desc); + bdk_cli.chang_desc = Some(bdk_chng_desc); + + Ok(bdk_cli) + } + + /// Execute bdk-cli wallet commands with given args + fn wallet_exec(&self, args: &[&str]) -> Result { + // Check if data directory is specified + let mut wallet_args = if let Some(datadir) = &self.node_datadir { + let datadir = datadir.as_os_str().to_str().unwrap(); + ["--network", &self.network, "--datadir", datadir, "wallet"].to_vec() + } else { + ["--network", &self.network, "wallet"].to_vec() + }; + + if self.verbosity { + wallet_args.push("-v"); + } + + wallet_args.push("-d"); + wallet_args.push(self.recv_desc.as_ref().unwrap()); + wallet_args.push("-c"); + wallet_args.push(&self.chang_desc.as_ref().unwrap()); + + for arg in args { + wallet_args.push(arg); + } + run_cmd_with_args(&self.target, &wallet_args) + } + + /// Execute bdk-cli key commands with given args + fn key_exec(&self, args: &[&str]) -> Result { + let mut key_args = ["key"].to_vec(); + for arg in args { + key_args.push(arg); + } + run_cmd_with_args(&self.target, &key_args) + } + + /// Execute bdk-cli node command + fn node_exec(&self, args: &[&str]) -> Result { + // Check if data directory is specified + let mut node_args = if let Some(datadir) = &self.node_datadir { + let datadir = datadir.as_os_str().to_str().unwrap(); + ["--network", &self.network, "--datadir", datadir, "node"].to_vec() + } else { + ["--network", &self.network, "node"].to_vec() + }; + + for arg in args { + node_args.push(arg); + } + run_cmd_with_args(&self.target, &node_args) + } + } + + // Run A Basic wallet operation test, with given feature + #[cfg(test)] + fn basic_wallet_ops(feature: &str) { + // Create a temporary directory for testing env + let mut test_dir = std::env::current_dir().unwrap(); + test_dir.push("bdk-testing"); + + let test_temp_dir = TempDir::new().unwrap(); + let test_dir = test_temp_dir.into_path().to_path_buf(); + + // Create bdk-cli instance + let bdk_cli = BdkCli::new("regtest", Some(test_dir), false, &[feature]).unwrap(); + + // Generate 101 blocks + bdk_cli.node_exec(&["generate", "101"]).unwrap(); + + // Get a bdk address + let bdk_addr_json = bdk_cli.wallet_exec(&["get_new_address"]).unwrap(); + let bdk_addr = get_value(&bdk_addr_json, "address").unwrap(); + + // Send coins from core to bdk + bdk_cli + .node_exec(&["sendtoaddress", &bdk_addr, "1000000000"]) + .unwrap(); + + bdk_cli.node_exec(&["generate", "1"]).unwrap(); + + // Sync the bdk wallet + bdk_cli.wallet_exec(&["sync"]).unwrap(); + + // Get the balance + let balance_json = bdk_cli.wallet_exec(&["get_balance"]).unwrap(); + let confirmed_balance = balance_json + .as_object() + .unwrap() + .get("satoshi") + .unwrap() + .as_object() + .unwrap() + .get("confirmed") + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(confirmed_balance, 1000000000u64); + } + + #[test] + #[cfg(feature = "regtest-bitcoin")] + fn test_basic_wallet_op_bitcoind() { + basic_wallet_ops("regtest-bitcoin") + } + + #[test] + #[cfg(feature = "regtest-electrum")] + fn test_basic_wallet_op_electrum() { + basic_wallet_ops("regtest-electrum") + } +}