diff --git a/src/cli/args.rs b/src/cli/args.rs index 5f68710..f4ccf8b 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -41,7 +41,7 @@ use crate::{AnyIndexer, Wallet}; #[derive(Clone, Eq, PartialEq, Debug)] #[command(author, version, about)] pub struct Args { - /// Set verbosity level. + /// Set verbosity level /// /// Can be used multiple times to increase verbosity. #[clap(short, long, global = true, action = clap::ArgAction::Count)] @@ -53,10 +53,14 @@ pub struct Args Args { wallet: self.wallet.clone(), resolver: self.resolver.clone(), sync: self.sync, + prune: self.prune, general: self.general.clone(), command: cmd.clone(), } @@ -154,7 +159,7 @@ impl Args { if sync { let indexer = self.indexer()?; eprint!("Syncing"); - if let Some(errors) = wallet.update(&indexer).into_err() { + if let Some(errors) = wallet.update(&indexer, self.prune).into_err() { eprintln!(" partial, some requests has failed:"); for err in errors { eprintln!("- {err}"); diff --git a/src/cli/command.rs b/src/cli/command.rs index bf9b5f2..9512661 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -323,7 +323,7 @@ impl Exec for Args { if *publish { let indexer = self.indexer()?; eprint!("Publishing transaction via {} ... ", indexer.name()); - indexer.publish(&tx)?; + indexer.broadcast(&tx)?; eprintln!("success"); } } @@ -343,7 +343,7 @@ impl Exec for Args { if *publish { let indexer = self.indexer()?; eprint!("Publishing transaction via {} ... ", indexer.name()); - indexer.publish(&tx)?; + indexer.broadcast(&tx)?; eprintln!("success"); } } diff --git a/src/indexers/any.rs b/src/indexers/any.rs index 950def4..daa0888 100644 --- a/src/indexers/any.rs +++ b/src/indexers/any.rs @@ -72,47 +72,16 @@ pub enum AnyIndexerError { impl Indexer for AnyIndexer { type Error = AnyIndexerError; - fn create, L2: Layer2>( - &self, - descr: &WalletDescr, - ) -> MayError, Vec> { - match self { - #[cfg(feature = "electrum")] - AnyIndexer::Electrum(inner) => { - let result = inner.create::(descr); - MayError { - ok: result.ok, - err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), - } - } - #[cfg(feature = "esplora")] - AnyIndexer::Esplora(inner) => { - let result = inner.create::(descr); - MayError { - ok: result.ok, - err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), - } - } - #[cfg(feature = "mempool")] - AnyIndexer::Mempool(inner) => { - let result = inner.create::(descr); - MayError { - ok: result.ok, - err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), - } - } - } - } - fn update, L2: Layer2>( &self, descr: &WalletDescr, cache: &mut WalletCache, + prune: bool, ) -> MayError> { match self { #[cfg(feature = "electrum")] AnyIndexer::Electrum(inner) => { - let result = inner.update::(descr, cache); + let result = inner.update::(descr, cache, prune); MayError { ok: result.ok, err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), @@ -120,7 +89,7 @@ impl Indexer for AnyIndexer { } #[cfg(feature = "esplora")] AnyIndexer::Esplora(inner) => { - let result = inner.update::(descr, cache); + let result = inner.update::(descr, cache, prune); MayError { ok: result.ok, err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), @@ -128,7 +97,7 @@ impl Indexer for AnyIndexer { } #[cfg(feature = "mempool")] AnyIndexer::Mempool(inner) => { - let result = inner.update::(descr, cache); + let result = inner.update::(descr, cache, prune); MayError { ok: result.ok, err: result.err.map(|v| v.into_iter().map(|e| e.into()).collect()), @@ -137,14 +106,14 @@ impl Indexer for AnyIndexer { } } - fn publish(&self, tx: &Tx) -> Result<(), Self::Error> { + fn broadcast(&self, tx: &Tx) -> Result<(), Self::Error> { match self { #[cfg(feature = "electrum")] - AnyIndexer::Electrum(inner) => inner.publish(tx).map_err(|e| e.into()), + AnyIndexer::Electrum(inner) => inner.broadcast(tx).map_err(|e| e.into()), #[cfg(feature = "esplora")] - AnyIndexer::Esplora(inner) => inner.publish(tx).map_err(|e| e.into()), + AnyIndexer::Esplora(inner) => inner.broadcast(tx).map_err(|e| e.into()), #[cfg(feature = "mempool")] - AnyIndexer::Mempool(inner) => inner.publish(tx).map_err(|e| e.into()), + AnyIndexer::Mempool(inner) => inner.broadcast(tx).map_err(|e| e.into()), } } } diff --git a/src/indexers/electrum.rs b/src/indexers/electrum.rs index b08b3ee..95fcbe3 100644 --- a/src/indexers/electrum.rs +++ b/src/indexers/electrum.rs @@ -20,6 +20,7 @@ // limitations under the License. use std::collections::BTreeMap; +use std::mem; use std::num::NonZeroU32; use std::str::FromStr; @@ -63,21 +64,21 @@ pub enum ElectrumError { impl Indexer for Client { type Error = ElectrumError; - fn create, L2: Layer2>( - &self, - descriptor: &WalletDescr, - ) -> MayError, Vec> { - let mut cache = WalletCache::new_nonsync(); - self.update::(descriptor, &mut cache).map(|_| cache) - } - fn update, L2: Layer2>( &self, descriptor: &WalletDescr, cache: &mut WalletCache, + prune: bool, ) -> MayError> { let mut errors = Vec::::new(); + // First, we scan all addresses. + // Addresses may be re-used, so known transactions doesn't help here. + // We collect these transactions, which contain the most recent information, into a new + // cache. We remove old transaction, since its data are now updated (for instance, if a + // transaction was re-orged, it may have a different height). + + let mut old_cache = mem::take(&mut cache.tx); let mut address_index = BTreeMap::new(); for keychain in descriptor.keychains() { let mut empty_count = 0usize; @@ -104,6 +105,7 @@ impl Indexer for Client { empty_count = 0; + // TODO: Separate as `WalletTx::from_electrum_history` method. let mut process_history_entry = |hr: GetHistoryRes| -> Result { let txid = hr.tx_hash; @@ -202,6 +204,7 @@ impl Indexer for Client { for hr in hres { match process_history_entry(hr) { Ok(tx) => { + old_cache.remove(&tx.txid); cache.tx.insert(tx.txid, tx); } Err(e) => errors.push(e), @@ -282,6 +285,14 @@ impl Indexer for Client { .insert(wallet_addr.expect_transmute()); } + // The remaining transactions are unmined ones. + if !prune { + for (txid, mut tx) in old_cache { + tx.status = TxStatus::Unknown; + cache.tx.insert(txid, tx); + } + } + if errors.is_empty() { MayError::ok(0) } else { @@ -289,7 +300,7 @@ impl Indexer for Client { } } - fn publish(&self, tx: &Tx) -> Result<(), Self::Error> { + fn broadcast(&self, tx: &Tx) -> Result<(), Self::Error> { self.transaction_broadcast(tx)?; Ok(()) } diff --git a/src/indexers/esplora.rs b/src/indexers/esplora.rs index a4a7349..c9495f6 100644 --- a/src/indexers/esplora.rs +++ b/src/indexers/esplora.rs @@ -21,6 +21,7 @@ // limitations under the License. use std::collections::BTreeMap; +use std::mem; use std::num::NonZeroU32; use std::ops::{Deref, DerefMut}; @@ -193,21 +194,21 @@ fn get_scripthash_txs_all( impl Indexer for Client { type Error = Error; - fn create, L2: Layer2>( - &self, - descriptor: &WalletDescr, - ) -> MayError, Vec> { - let mut cache = WalletCache::new_nonsync(); - self.update::(descriptor, &mut cache).map(|_| cache) - } - fn update, L2: Layer2>( &self, descriptor: &WalletDescr, cache: &mut WalletCache, + prune: bool, ) -> MayError> { let mut errors = vec![]; + // First, we scan all addresses. + // Addresses may be re-used, so known transactions doesn't help here. + // We collect these transactions, which contain the most recent information, into a new + // cache. We remove old transaction, since its data are now updated (for instance, if a + // transaction was re-orged, it may have a different height). + + let mut old_cache = mem::take(&mut cache.tx); let mut address_index = BTreeMap::new(); for keychain in descriptor.keychains() { let mut empty_count = 0usize; @@ -232,7 +233,10 @@ impl Indexer for Client { } Ok(txes) => { empty_count = 0; - txids = txes.iter().map(|tx| tx.txid).collect(); + txids.extend(txes.iter().map(|tx| tx.txid)); + for txid in &txids { + old_cache.remove(txid); + } cache .tx .extend(txes.into_iter().map(WalletTx::from).map(|tx| (tx.txid, tx))); @@ -313,6 +317,14 @@ impl Indexer for Client { .insert(wallet_addr.expect_transmute()); } + // The remaining transactions are unmined ones. + if !prune { + for (txid, mut tx) in old_cache { + tx.status = TxStatus::Unknown; + cache.tx.insert(txid, tx); + } + } + if errors.is_empty() { MayError::ok(0) } else { @@ -320,5 +332,5 @@ impl Indexer for Client { } } - fn publish(&self, tx: &Tx) -> Result<(), Self::Error> { self.inner.broadcast(tx) } + fn broadcast(&self, tx: &Tx) -> Result<(), Self::Error> { self.inner.broadcast(tx) } } diff --git a/src/indexers/mod.rs b/src/indexers/mod.rs index a120ec6..866f37c 100644 --- a/src/indexers/mod.rs +++ b/src/indexers/mod.rs @@ -45,13 +45,21 @@ pub trait Indexer { fn create, L2: Layer2>( &self, descr: &WalletDescr, - ) -> MayError, Vec>; + ) -> MayError, Vec> { + let mut cache = WalletCache::new_nonsync(); + self.update::(descr, &mut cache, true).map(|_| cache) + } + /// Update the wallet transaction cache and balances + /// + /// If `prune` argument is set, removes all transactions which are not present in blockchain or + /// mempool (not known to the indexer, i.e. has `TxStatus::Unknown`). fn update, L2: Layer2>( &self, descr: &WalletDescr, cache: &mut WalletCache, + prune: bool, ) -> MayError>; - fn publish(&self, tx: &Tx) -> Result<(), Self::Error>; + fn broadcast(&self, tx: &Tx) -> Result<(), Self::Error>; } diff --git a/src/wallet.rs b/src/wallet.rs index 214e180..22832a4 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -36,7 +36,7 @@ use psbt::{PsbtConstructor, Utxo}; use crate::{ BlockInfo, CoinRow, Indexer, Layer2, Layer2Cache, Layer2Data, Layer2Descriptor, Layer2Empty, - MayError, MiningInfo, NoLayer2, Party, TxRow, WalletAddr, WalletTx, WalletUtxo, + MayError, MiningInfo, NoLayer2, Party, TxRow, TxStatus, WalletAddr, WalletTx, WalletUtxo, }; #[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Error)] @@ -331,12 +331,16 @@ impl WalletCache { &mut self, descriptor: &WalletDescr, indexer: &I, + prune: bool, ) -> MayError> { - let res = indexer.update::(descriptor, self); + let res = indexer.update::(descriptor, self, prune); self.mark_dirty(); res } + /// Prunes transaction cache by removing all transactions with `TxStatus::Unknown` + pub fn prune(&mut self) { self.tx.retain(|_, tx| tx.status != TxStatus::Unknown) } + pub fn addresses_on(&self, keychain: Keychain) -> &BTreeSet { self.addr.get(&keychain).unwrap_or_else(|| { panic!("keychain #{keychain} is not supported by the wallet descriptor") @@ -543,10 +547,13 @@ impl, L2: Layer2> Wallet { res } - pub fn update(&mut self, indexer: &I) -> MayError<(), Vec> { - self.cache.update::(&self.descr, indexer).map(|_| ()) + pub fn update(&mut self, indexer: &I, prune: bool) -> MayError<(), Vec> { + self.cache.update::(&self.descr, indexer, prune).map(|_| ()) } + /// Prunes transaction cache by removing all transactions with `TxStatus::Unknown` + pub fn prune(&mut self) { self.cache.prune() } + pub fn to_deriver(&self) -> D where D: Clone,