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,