diff --git a/src/cli/args.rs b/src/cli/args.rs index 5f68710..3fb35e2 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -34,7 +34,7 @@ use crate::cli::{ }; use crate::fs::FsTextStore; use crate::indexers::esplora; -use crate::{AnyIndexer, Wallet}; +use crate::{AnyIndexer, Layer2Empty, Wallet, WalletCache}; /// Command-line arguments #[derive(Parser)] @@ -119,14 +119,14 @@ impl Args { pub fn bp_wallet( &self, conf: &Config, - ) -> Result, ExecError> + ) -> Result>, ExecError> where for<'de> D: From + serde::Serialize + serde::Deserialize<'de>, { eprint!("Loading descriptor"); let sync = self.sync || self.wallet.descriptor_opts.is_some(); - let mut wallet: Wallet = + let mut wallet: Wallet> = if let Some(d) = self.wallet.descriptor_opts.descriptor() { eprintln!(" from command-line argument"); eprint!("Syncing"); diff --git a/src/cli/command.rs b/src/cli/command.rs index 96c33d5..37d4f4c 100644 --- a/src/cli/command.rs +++ b/src/cli/command.rs @@ -36,7 +36,10 @@ use strict_encoding::Ident; use crate::cli::{Args, Config, DescriptorOpts, Exec}; use crate::fs::FsTextStore; -use crate::{coinselect, AnyIndexerError, Indexer, OpType, Wallet, WalletAddr, WalletUtxo}; +use crate::{ + coinselect, AnyIndexerError, Indexer, Layer2Empty, OpType, Wallet, WalletAddr, WalletCache, + WalletUtxo, +}; #[derive(Subcommand, Clone, PartialEq, Eq, Debug, Display)] pub enum Command { @@ -237,14 +240,17 @@ impl Exec for Args { if config.default_wallet == name { "\t[default]\t" } else { "\t\t" } ); let provider = FsTextStore::new(entry.path().clone())?; - let wallet = match Wallet::::load(provider, true) { - Err(err) => { - error!("Error loading wallet descriptor: {err}"); - println!("# broken wallet descriptor"); - continue; - } - Ok(wallet) => wallet, - }; + let wallet = + match Wallet::>::load( + provider, true, + ) { + Err(err) => { + error!("Error loading wallet descriptor: {err}"); + println!("# broken wallet descriptor"); + continue; + } + Ok(wallet) => wallet, + }; println!("\t{}", wallet.descriptor()); } if count == 0 { diff --git a/src/data.rs b/src/data.rs index d2a0dd4..9dbf16a 100644 --- a/src/data.rs +++ b/src/data.rs @@ -32,6 +32,7 @@ use bpstd::{ ScriptPubkey, SeqNo, SigScript, Terminal, TxVer, Txid, Witness, }; use psbt::{Prevout, Utxo}; +use sha2::{Digest, Sha256}; pub type BlockHeight = NonZeroU32; @@ -223,6 +224,18 @@ impl WalletTx { let debit = self.debit_sum().sats_i64(); debit - credit } + + pub fn inputs_sha256(&self) -> Vec { + Sha256::digest( + self.inputs + .iter() + .map(|input| format!("{}:{}", input.outpoint.txid, input.outpoint.vout)) + .collect::>() + .join("|") + .as_bytes(), + ) + .to_vec() + } } #[derive(Clone, Eq, PartialEq, Hash, Debug, From)] diff --git a/src/lib.rs b/src/lib.rs index e73c42a..6fb3355 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,4 +64,4 @@ pub use layer2::{ }; pub use rows::{CoinRow, Counterparty, OpType, TxRow}; pub use util::MayError; -pub use wallet::{Wallet, WalletCache, WalletData, WalletDescr}; +pub use wallet::{Wallet, WalletCache, WalletCacheProvider, WalletData, WalletDescr}; diff --git a/src/rows.rs b/src/rows.rs index 724cf9f..c853561 100644 --- a/src/rows.rs +++ b/src/rows.rs @@ -26,9 +26,7 @@ use std::str::FromStr; use amplify::hex::FromHex; use bpstd::{Address, DerivedAddr, Outpoint, Sats, ScriptPubkey, Txid}; -use crate::{ - BlockHeight, Layer2Cache, Layer2Coin, Layer2Empty, Layer2Tx, Party, TxStatus, WalletCache, -}; +use crate::{BlockHeight, Layer2Coin, Layer2Empty, Layer2Tx, Party, TxStatus}; #[cfg_attr( feature = "serde", @@ -146,82 +144,6 @@ pub struct CoinRow { pub layer2: Vec, } -impl WalletCache { - pub fn coins(&self) -> impl Iterator> + '_ { - self.utxo.iter().map(|outpoint| { - let tx = self.tx.get(&outpoint.txid).expect("cache data inconsistency"); - let out = tx.outputs.get(outpoint.vout_usize()).expect("cache data inconsistency"); - CoinRow { - height: tx.status.map(|info| info.height), - outpoint: *outpoint, - address: out.derived_addr().expect("cache data inconsistency"), - amount: out.value, - layer2: none!(), // TODO: Add support to WalletTx - } - }) - } - - pub fn history(&self) -> impl Iterator> + '_ { - self.tx.values().map(|tx| { - let (credit, debit) = tx.credited_debited(); - let mut row = TxRow { - height: tx.status.map(|info| info.height), - operation: OpType::Credit, - our_inputs: tx - .inputs - .iter() - .enumerate() - .filter_map(|(idx, inp)| inp.derived_addr().map(|_| idx as u32)) - .collect(), - counterparties: none!(), - own: none!(), - txid: tx.txid, - fee: tx.fee, - weight: tx.weight, - size: tx.size, - total: tx.total_moved(), - amount: Sats::ZERO, - balance: Sats::ZERO, - layer2: none!(), // TODO: Add support to WalletTx - }; - // TODO: Add balance calculation - row.own = tx - .inputs - .iter() - .filter_map(|i| i.derived_addr().map(|a| (a, -i.value.sats_i64()))) - .chain( - tx.outputs - .iter() - .filter_map(|o| o.derived_addr().map(|a| (a, o.value.sats_i64()))), - ) - .collect(); - if credit.is_non_zero() { - row.counterparties = tx.credits().fold(Vec::new(), |mut cp, inp| { - let party = Counterparty::from(inp.payer.clone()); - cp.push((party, inp.value.sats_i64())); - cp - }); - row.counterparties.extend(tx.debits().fold(Vec::new(), |mut cp, out| { - let party = Counterparty::from(out.beneficiary.clone()); - cp.push((party, -out.value.sats_i64())); - cp - })); - row.operation = OpType::Credit; - row.amount = credit - debit - tx.fee; - } else if debit.is_non_zero() { - row.counterparties = tx.debits().fold(Vec::new(), |mut cp, out| { - let party = Counterparty::from(out.beneficiary.clone()); - cp.push((party, -out.value.sats_i64())); - cp - }); - row.operation = OpType::Debit; - row.amount = debit; - } - row - }) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/wallet.rs b/src/wallet.rs index ae4afc7..9fd5166 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -20,6 +20,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::borrow::Cow; use std::cmp; use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use std::marker::PhantomData; @@ -35,9 +36,9 @@ use nonasync::persistence::{ use psbt::{Psbt, PsbtConstructor, PsbtMeta, Utxo}; use crate::{ - BlockInfo, CoinRow, Indexer, Layer2, Layer2Cache, Layer2Data, Layer2Descriptor, Layer2Empty, - MayError, MiningInfo, NoLayer2, Party, TxCredit, TxDebit, TxRow, TxStatus, WalletAddr, - WalletTx, WalletUtxo, + BlockInfo, CoinRow, Counterparty, Indexer, Layer2, Layer2Cache, Layer2Data, Layer2Descriptor, + Layer2Empty, MayError, MiningInfo, NoLayer2, OpType, Party, TxCredit, TxDebit, TxRow, TxStatus, + WalletAddr, WalletTx, WalletUtxo, }; #[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Error)] @@ -291,6 +292,203 @@ impl Drop for WalletData { } } +pub trait WalletCacheProvider: Persisting { + fn layer2(&self) -> &L2C; + + fn layer2_mut(&mut self) -> &mut L2C; + + fn addr_by_address(&self, addr: &Address) -> Option<(Keychain, WalletAddr)>; + + fn addrs(&self) -> impl Iterator; + + fn new_tx(&mut self, txid: Txid, tx: WalletTx); + + fn tx(&self, txid: &Txid) -> Option>; + + fn txs(&self) -> impl Iterator)> + '_; + + fn new_utxo(&mut self, outpoint: Outpoint); + + fn utxo(&self, outpoint: &Outpoint) -> Option>; + + fn utxos(&self) -> impl Iterator + '_; + + fn coins(&self) -> impl Iterator> + '_ { + self.utxos().map(|outpoint| { + let tx = self.tx(&outpoint.txid).expect("cache data inconsistency"); + let out = tx.outputs.get(outpoint.vout_usize()).expect("cache data inconsistency"); + CoinRow { + height: tx.status.map(|info| info.height), + outpoint, + address: out.derived_addr().expect("cache data inconsistency"), + amount: out.value, + layer2: none!(), // TODO: Add support to WalletTx + } + }) + } + + fn history(&self) -> impl Iterator> + '_ { + self.txs().map(|(_, tx)| { + let (credit, debit) = tx.credited_debited(); + let mut row = TxRow { + height: tx.status.map(|info| info.height), + operation: OpType::Credit, + our_inputs: tx + .inputs + .iter() + .enumerate() + .filter_map(|(idx, inp)| inp.derived_addr().map(|_| idx as u32)) + .collect(), + counterparties: none!(), + own: none!(), + txid: tx.txid, + fee: tx.fee, + weight: tx.weight, + size: tx.size, + total: tx.total_moved(), + amount: Sats::ZERO, + balance: Sats::ZERO, + layer2: none!(), // TODO: Add support to WalletTx + }; + // TODO: Add balance calculation + row.own = tx + .inputs + .iter() + .filter_map(|i| i.derived_addr().map(|a| (a, -i.value.sats_i64()))) + .chain( + tx.outputs + .iter() + .filter_map(|o| o.derived_addr().map(|a| (a, o.value.sats_i64()))), + ) + .collect(); + if credit.is_non_zero() { + row.counterparties = tx.credits().fold(Vec::new(), |mut cp, inp| { + let party = Counterparty::from(inp.payer.clone()); + cp.push((party, inp.value.sats_i64())); + cp + }); + row.counterparties.extend(tx.debits().fold(Vec::new(), |mut cp, out| { + let party = Counterparty::from(out.beneficiary.clone()); + cp.push((party, -out.value.sats_i64())); + cp + })); + row.operation = OpType::Credit; + row.amount = credit - debit - tx.fee; + } else if debit.is_non_zero() { + row.counterparties = tx.debits().fold(Vec::new(), |mut cp, out| { + let party = Counterparty::from(out.beneficiary.clone()); + cp.push((party, -out.value.sats_i64())); + cp + }); + row.operation = OpType::Debit; + row.amount = debit; + } + row + }) + } + + fn outpoint_by(&self, outpoint: Outpoint) -> Result<(WalletUtxo, ScriptPubkey), NonWalletItem> { + let tx = self.tx(&outpoint.txid).ok_or(NonWalletItem::NonWalletTx(outpoint.txid))?; + let debit = tx + .outputs + .get(outpoint.vout.into_usize()) + .ok_or(NonWalletItem::NoOutput(outpoint.txid, outpoint.vout))?; + let terminal = debit.derived_addr().ok_or(NonWalletItem::NonWalletUtxo(outpoint))?.terminal; + if debit.spent.is_some() { + debug_assert!(!self.is_unspent(outpoint)); + return Err(NonWalletItem::Spent(outpoint)); + } + debug_assert!(self.is_unspent(outpoint)); + let utxo = WalletUtxo { + outpoint, + value: debit.value, + terminal, + status: tx.status, + }; + let spk = + debit.beneficiary.script_pubkey().ok_or(NonWalletItem::NonWalletUtxo(outpoint))?; + Ok((utxo, spk)) + } + + fn is_unspent(&self, outpoint: Outpoint) -> bool { self.utxo(&outpoint).is_some() } + + fn has_outpoint(&self, outpoint: Outpoint) -> bool { + let Some(tx) = self.tx(&outpoint.txid) else { + return false; + }; + let Some(out) = tx.outputs.get(outpoint.vout.to_usize()) else { + return false; + }; + matches!(out.beneficiary, Party::Wallet(_)) + } + + fn register_psbt(&mut self, psbt: &Psbt, meta: &PsbtMeta) { + let unsigned_tx = psbt.to_unsigned_tx(); + let txid = unsigned_tx.txid(); + let wallet_tx = WalletTx { + txid, + status: TxStatus::Mempool, + inputs: psbt + .inputs() + .map(|input| { + let addr = Address::with(&input.prev_txout().script_pubkey, meta.network).ok(); + TxCredit { + outpoint: input.previous_outpoint, + payer: match (self.utxo(&input.previous_outpoint), addr) { + (Some(_), Some(addr)) => { + let (keychain, index) = self + .addr_by_address(&addr) + .map(|(keychain, a)| (keychain, a.terminal.index)) + .expect("address cache inconsistency"); + Party::Wallet(DerivedAddr::new(addr, keychain, index)) + } + (_, Some(addr)) => Party::Counterparty(addr), + _ => Party::Unknown(input.prev_txout().script_pubkey.clone()), + }, + sequence: unsigned_tx.inputs[input.index()].sequence, + coinbase: false, + script_sig: none!(), + witness: none!(), + value: input.value(), + } + }) + .collect(), + outputs: psbt + .outputs() + .map(|output| { + let vout = Vout::from_u32(output.index() as u32); + let addr = Address::with(&output.script, meta.network).ok(); + TxDebit { + outpoint: Outpoint::new(txid, vout), + beneficiary: match (meta.change, addr) { + (Some(change), Some(addr)) if change.vout == vout => { + Party::Wallet(DerivedAddr::new( + addr, + change.terminal.keychain, + change.terminal.index, + )) + } + (_, Some(addr)) => Party::Counterparty(addr), + (_, _) => Party::Unknown(output.script.clone()), + }, + value: output.value(), + spent: None, + } + }) + .collect(), + fee: meta.fee, + size: meta.size, + weight: meta.weight, + version: unsigned_tx.version, + locktime: unsigned_tx.lock_time, + }; + self.new_tx(txid, wallet_tx); + if let Some(change) = meta.change { + self.new_utxo(Outpoint::new(txid, change.vout)); + } + } +} + #[cfg_attr( feature = "serde", derive(serde::Serialize, serde::Deserialize), @@ -317,6 +515,38 @@ pub struct WalletCache { pub layer2: L2, } +impl WalletCacheProvider for WalletCache +where L2C: Layer2Cache +{ + fn layer2(&self) -> &L2C { &self.layer2 } + + fn layer2_mut(&mut self) -> &mut L2C { &mut self.layer2 } + + fn addr_by_address(&self, addr: &Address) -> Option<(Keychain, WalletAddr)> { + self.addrs().find(|(_, a)| a.addr == *addr) + } + + fn addrs(&self) -> impl Iterator { + self.addr.iter().flat_map(|(keychain, addrs)| addrs.iter().map(|addr| (*keychain, *addr))) + } + + fn new_tx(&mut self, txid: Txid, tx: WalletTx) { self.tx.insert(txid, tx); } + + fn tx(&self, txid: &Txid) -> Option> { self.tx.get(txid).map(Cow::Borrowed) } + + fn txs(&self) -> impl Iterator)> + '_ { + self.tx.iter().map(|(txid, wallet_tx)| (*txid, Cow::Borrowed(wallet_tx))) + } + + fn new_utxo(&mut self, outpoint: Outpoint) { self.utxo.insert(outpoint); } + + fn utxo(&self, outpoint: &Outpoint) -> Option> { + self.utxo.get(outpoint).map(Cow::Borrowed) + } + + fn utxos(&self) -> impl Iterator + '_ { self.utxo.iter().copied() } +} + impl WalletCache { pub(crate) fn new_nonsync() -> Self { WalletCache { @@ -483,25 +713,6 @@ impl WalletCache { }) }) } - - pub fn utxos(&self) -> impl Iterator + '_ { - self.utxo.iter().filter_map(|outpoint| { - let tx = self.tx.get(&outpoint.txid).expect("cache data inconsistency"); - let debit = tx.outputs.get(outpoint.vout_usize()).expect("cache data inconsistency"); - let terminal = - debit.derived_addr().expect("UTXO doesn't belong to the wallet").terminal; - if debit.spent.is_some() { - None - } else { - Some(WalletUtxo { - outpoint: *outpoint, - value: debit.value, - terminal, - status: tx.status, - }) - } - }) - } } impl CloneNoPersistence for WalletCache { @@ -543,20 +754,29 @@ impl Drop for WalletCache { } #[derive(Debug)] -pub struct Wallet, L2: Layer2 = NoLayer2> { +pub struct Wallet, Cache: WalletCacheProvider, L2: Layer2 = NoLayer2> +{ descr: WalletDescr, data: WalletData, - cache: WalletCache, + cache: Cache, layer2: L2, } -impl, L2: Layer2> Deref for Wallet { +impl, Cache: WalletCacheProvider, L2: Layer2> Deref + for Wallet +{ type Target = WalletDescr; fn deref(&self) -> &Self::Target { &self.descr } } -impl, L2: Layer2> CloneNoPersistence for Wallet { +impl< + K, + D: Descriptor, + Cache: CloneNoPersistence + WalletCacheProvider, + L2: Layer2, + > CloneNoPersistence for Wallet +{ fn clone_no_persistence(&self) -> Self { Self { descr: self.descr.clone_no_persistence(), @@ -567,7 +787,9 @@ impl, L2: Layer2> CloneNoPersistence for Wallet { } } -impl, L2: Layer2> PsbtConstructor for Wallet { +impl, Cache: Persisting + WalletCacheProvider, L2: Layer2> + PsbtConstructor for Wallet +{ type Key = K; type Descr = D; @@ -597,7 +819,7 @@ impl, L2: Layer2> PsbtConstructor for Wallet { } } -impl> Wallet { +impl> Wallet> { pub fn new_layer1(descr: D, network: Network) -> Self { Wallet { cache: WalletCache::new_nonsync(), @@ -608,7 +830,7 @@ impl> Wallet { } } -impl, L2: Layer2> Wallet { +impl, L2: Layer2> Wallet, L2> { pub fn new_layer2(descr: D, l2_descr: L2::Descr, layer2: L2, network: Network) -> Self { Wallet { cache: WalletCache::new_nonsync(), @@ -618,6 +840,31 @@ impl, L2: Layer2> Wallet { } } + pub fn txos(&self) -> impl Iterator + '_ { self.cache.txos() } +} + +impl, Cache: WalletCacheProvider, L2: Layer2> + Wallet +{ + pub fn bind( + descr: WalletDescr, + data: WalletData, + cache: Cache, + layer2: L2, + ) -> Self { + Self { + descr, + data, + cache, + layer2, + } + } + + #[allow(clippy::type_complexity)] + pub fn unbind(self) -> (WalletDescr, WalletData, Cache) { + (self.descr, self.data, self.cache) + } + pub fn set_name(&mut self, name: String) { self.data.name = name; self.data.mark_dirty(); @@ -631,7 +878,7 @@ impl, L2: Layer2> Wallet { } pub fn data_l2(&self) -> &L2::Data { &self.data.layer2 } - pub fn cache_l2(&self) -> &L2::Cache { &self.cache.layer2 } + pub fn cache_l2(&self) -> &L2::Cache { self.cache.layer2() } pub fn with_data( &mut self, @@ -655,16 +902,11 @@ impl, L2: Layer2> Wallet { &mut self, f: impl FnOnce(&mut L2::Cache) -> Result, ) -> Result { - let res = f(&mut self.cache.layer2)?; + let res = f(self.cache.layer2_mut())?; self.cache.mark_dirty(); Ok(res) } - #[must_use] - pub fn update(&mut self, indexer: &I) -> MayError<(), Vec> { - self.cache.update::(&self.descr, indexer).map(|_| ()) - } - pub fn to_deriver(&self) -> D where D: Clone, @@ -703,7 +945,9 @@ impl, L2: Layer2> Wallet { pub fn balance(&self) -> Sats { self.cache.coins().map(|utxo| utxo.amount).sum::() } #[inline] - pub fn transactions(&self) -> &BTreeMap { &self.cache.tx } + pub fn transactions(&self) -> impl Iterator)> + '_ { + self.cache.txs() + } #[inline] pub fn coins(&self) -> impl Iterator::Coin>> + '_ { @@ -721,7 +965,7 @@ impl, L2: Layer2> Wallet { } pub fn address_balance(&self) -> impl Iterator + '_ { - self.cache.addr.values().flat_map(|set| set.iter()).copied() + self.cache.addrs().map(|(_, addr)| addr) } #[inline] @@ -739,8 +983,24 @@ impl, L2: Layer2> Wallet { self.cache.outpoint_by(outpoint) } - pub fn txos(&self) -> impl Iterator + '_ { self.cache.txos() } - pub fn utxos(&self) -> impl Iterator + '_ { self.cache.utxos() } + pub fn utxos(&self) -> impl Iterator + '_ { + self.cache.utxos().flat_map(|outpoint| { + let tx = self.cache.tx(&outpoint.txid).expect("cache data inconsistency"); + let debit = tx.outputs.get(outpoint.vout_usize()).expect("cache data inconsistency"); + let terminal = + debit.derived_addr().expect("UTXO doesn't belong to the wallet").terminal; + if debit.spent.is_some() { + None + } else { + Some(WalletUtxo { + outpoint, + value: debit.value, + terminal, + status: tx.status, + }) + } + }) + } pub fn coinselect<'a>( &'a self, @@ -762,17 +1022,38 @@ impl, L2: Layer2> Wallet { } } -impl, L2: Layer2> Wallet { - pub fn load

(provider: P, autosave: bool) -> Result, PersistenceError> - where P: Clone +impl, L2: Layer2> Wallet, L2> { + pub fn set_id(&mut self, id: &impl ToString) { + self.data.id = Some(id.to_string()); + self.cache.id = Some(id.to_string()); + } +} + +impl, L2: Layer2> Wallet, L2> { + #[must_use] + pub fn update(&mut self, indexer: &I) -> MayError<(), Vec> { + indexer.update::(&self.descr, &mut self.cache).map(drop) + } +} + +impl, Cache: WalletCacheProvider + Persisting, L2: Layer2> + Wallet +{ + pub fn load

( + provider: P, + autosave: bool, + ) -> Result, PersistenceError> + where + P: Clone + PersistenceProvider> + PersistenceProvider> - + PersistenceProvider> + + PersistenceProvider + PersistenceProvider - + 'static { + + 'static, + { let descr = WalletDescr::::load(provider.clone(), autosave)?; let data = WalletData::::load(provider.clone(), autosave)?; - let cache = WalletCache::::load(provider.clone(), autosave)?; + let cache = Cache::load(provider.clone(), autosave)?; let layer2 = L2::load(provider, autosave)?; Ok(Wallet { @@ -783,11 +1064,6 @@ impl, L2: Layer2> Wallet { }) } - pub fn set_id(&mut self, id: &impl ToString) { - self.data.id = Some(id.to_string()); - self.cache.id = Some(id.to_string()); - } - pub fn make_persistent

( &mut self, provider: P, @@ -797,7 +1073,7 @@ impl, L2: Layer2> Wallet { P: Clone + PersistenceProvider> + PersistenceProvider> - + PersistenceProvider> + + PersistenceProvider + PersistenceProvider + 'static, {