use std::{ collections::{hash_map::DefaultHasher, HashSet}, hash::{Hash, Hasher}, path::PathBuf, sync::{Arc, Mutex}, }; use anyhow::{Context, Result}; use bdk::{ bitcoin::{bip32::ExtendedPubKey, Network, Txid}, blockchain::{ElectrumBlockchain, GetHeight}, sled, template::{Bip44Public, Bip49Public, Bip84Public, Bip86Public}, KeychainKind, SyncOptions, TransactionDetails, Wallet, }; use log::{debug, error, warn}; use serde::Deserialize; use crate::blockchain::{get_blockchain, ElectrumConfig}; #[derive(Deserialize, Debug, Clone, Copy)] #[serde(rename_all = "snake_case")] pub enum AddressKind { Legacy, NestedSegwit, Segwit, Taproot, } #[derive(Deserialize, Debug)] pub struct XpubSpec { name: String, xpub: String, kind: Option, } impl XpubSpec { pub fn kind(&self) -> AddressKind { self.kind.unwrap_or(AddressKind::Segwit) } pub fn xpub(&self) -> &str { &self.xpub } pub fn name(&self) -> &str { &self.name } } #[derive(Deserialize, Debug, Hash)] pub struct DescriptorsSpec { name: String, primary: String, change: Option, } impl DescriptorsSpec { pub fn get_hash(&self) -> String { let mut s = DefaultHasher::new(); self.hash(&mut s); s.finish().to_string() } pub fn primary(&self) -> &str { &self.primary } pub fn change(&self) -> Option<&String> { self.change.as_ref() } pub fn name(&self) -> &str { &self.name } } #[derive(Deserialize, Debug)] #[serde(untagged)] pub enum WalletConfig { Xpub(XpubSpec), Descriptors(DescriptorsSpec), } impl WalletConfig { pub fn name(&self) -> &str { match self { WalletConfig::Xpub(xpub_spec) => xpub_spec.name(), WalletConfig::Descriptors(descriptors_spec) => descriptors_spec.name(), } } } fn get_cache_dir(db_name: &str) -> PathBuf { dirs::cache_dir() .unwrap_or(PathBuf::from("cache")) .join(env!("CARGO_PKG_NAME")) .join(db_name) } fn get_xpub_wallet(xpub_spec: &XpubSpec, network: Network) -> Result> { let xpub: ExtendedPubKey = xpub_spec.xpub().parse().unwrap(); let fingerprint = xpub.fingerprint(); let sled = sled::open(get_cache_dir(&fingerprint.to_string()))?.open_tree("wallet")?; match xpub_spec.kind() { AddressKind::Legacy => Wallet::new( Bip44Public(xpub, fingerprint, KeychainKind::External), Some(Bip44Public(xpub, fingerprint, KeychainKind::Internal)), network, sled, ), AddressKind::NestedSegwit => Wallet::new( Bip49Public(xpub, fingerprint, KeychainKind::External), Some(Bip49Public(xpub, fingerprint, KeychainKind::Internal)), network, sled, ), AddressKind::Segwit => Wallet::new( Bip84Public(xpub, fingerprint, KeychainKind::External), Some(Bip84Public(xpub, fingerprint, KeychainKind::Internal)), network, sled, ), AddressKind::Taproot => Wallet::new( Bip86Public(xpub, fingerprint, KeychainKind::External), Some(Bip86Public(xpub, fingerprint, KeychainKind::Internal)), network, sled, ), } .with_context(|| format!("invalid xpub wallet '{}'", xpub)) } fn get_descriptors_wallet( descriptors_spec: &DescriptorsSpec, network: Network, ) -> Result> { let sled = sled::open(get_cache_dir(&descriptors_spec.get_hash()))?.open_tree("wallet")?; Wallet::new( descriptors_spec.primary(), descriptors_spec.change().map(String::as_ref), network, sled, ) .with_context(|| format!("invalid descriptor wallet '{:?}'", descriptors_spec)) } fn get_wallet(wallet_config: &WalletConfig, network: Network) -> Result> { match &wallet_config { WalletConfig::Xpub(xpub_spec) => get_xpub_wallet(xpub_spec, network), WalletConfig::Descriptors(descriptors_spec) => { get_descriptors_wallet(descriptors_spec, network) } } } pub struct WalletInfo { name: String, wallet: Wallet, old_txs: HashSet, blockchain: ElectrumBlockchain, } pub type SafeWalletInfo = Arc>; impl WalletInfo { pub fn name(&self) -> &str { &self.name } pub fn get_height(&self) -> Result { self.blockchain.get_height() } pub fn get_network(&self) -> Network { self.wallet.network() } pub fn total_balance(&self) -> Result { self.wallet.get_balance().map(|b| b.get_total()) } pub fn get_new_txs(&mut self) -> Vec { debug!("[{}] syncing wallet", self.name); if let Err(e) = self.wallet.sync(&self.blockchain, SyncOptions::default()) { warn!("[{}] cannot sync wallet: {:#}", self.name, e); return Default::default(); } let tx_list = match self.wallet.list_transactions(false) { Ok(txs) => txs, Err(e) => { warn!("[{}] cannot retrieve transactions: {:#}", self.name, e); Default::default() } }; let new_txs: Vec = tx_list .iter() .filter(|&tx| !self.old_txs.contains(&tx.txid)) .cloned() .collect(); new_txs.iter().for_each(|tx| { self.old_txs.insert(tx.txid); }); new_txs } } pub fn get_wallets( wallet_configs: &[WalletConfig], electrum_cfg: &ElectrumConfig, ) -> Vec { let mut result: Vec = vec![]; for wallet_config in wallet_configs.iter() { let name = wallet_config.name(); match get_wallet(wallet_config, electrum_cfg.network()) { Ok(w) => { result.push(Arc::new(Mutex::new(WalletInfo { name: name.to_string(), wallet: w, old_txs: Default::default(), blockchain: get_blockchain(electrum_cfg).unwrap(), }))); } Err(e) => { error!("[{}] cannot setup wallet: {:#}", name, e); } } } result }