//!
//! 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};
Ok((addr.script_pubkey(), val))
}
+
#[cfg(any(
feature = "electrum",
feature = "compact_filters",
Ok((user, passwd))
}
+#[cfg(all(feature = "reserves", feature = "electrum"))]
+pub fn get_outpoints_for_address(
+ address: Address,
+ client: &Client,
+ max_confirmation_height: Option<usize>,
+) -> Result<Vec<(OutPoint, TxOut)>, 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, String> {
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<PathBuf, Error> {
- 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<PathBuf>) -> Result<PathBuf, Error> {
+ 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<PathBuf, Error> {
+ let mut dir = home_path.to_owned();
+
dir.push(wallet_name);
if !dir.exists() {
}
/// Prepare wallet database directory
-pub(crate) fn prepare_wallet_db_dir(wallet_name: &str) -> Result<PathBuf, Error> {
- let mut db_dir = prepare_home_wallet_dir(wallet_name)?;
+fn prepare_wallet_db_dir(wallet_name: &str, home_path: &Path) -> Result<PathBuf, Error> {
+ let mut db_dir = prepare_wallet_dir(wallet_name, home_path)?;
#[cfg(feature = "key-value-db")]
db_dir.push("wallet.sled");
/// Prepare blockchain data directory (for compact filters)
#[cfg(feature = "compact_filters")]
-pub(crate) fn prepare_bc_dir(wallet_name: &str) -> Result<PathBuf, Error> {
- let mut bc_dir = prepare_home_wallet_dir(wallet_name)?;
+fn prepare_bc_dir(wallet_name: &str, home_path: &Path) -> Result<PathBuf, Error> {
+ let mut bc_dir = prepare_wallet_dir(wallet_name, home_path)?;
bc_dir.push("compact_filters");
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 `<home_path>/bitcoind/regtest/wallets`.
+#[cfg(feature = "regtest-node")]
+pub(crate) fn prepare_bitcoind_datadir(home_path: &Path) -> Result<PathBuf, Error> {
+ 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 `<home_path>/electrsd/regtest/wallets`.
+#[cfg(feature = "regtest-electrum")]
+pub(crate) fn prepare_electrum_datadir(home_path: &Path) -> Result<PathBuf, Error> {
+ 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<AnyDatabase, Error> {
+pub(crate) fn open_database(
+ wallet_opts: &WalletOpts,
+ home_path: &Path,
+) -> Result<AnyDatabase, Error> {
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 {
Ok(database)
}
+#[allow(dead_code)]
+pub(crate) fn new_backend(_datadir: &Path) -> Result<Nodes, Error> {
+ #[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<AnyBlockchain, Error> {
#[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,
};
#[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 = {
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()))?,
#[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(),
},
),
_ => {
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,