diff --git a/crates/bdk/src/types.rs b/crates/bdk/src/types.rs index 870734d6b..b4b6cd784 100644 --- a/crates/bdk/src/types.rs +++ b/crates/bdk/src/types.rs @@ -166,8 +166,6 @@ pub struct LocalUtxo { pub outpoint: OutPoint, /// Transaction output pub txout: TxOut, - /// Type of keychain - pub keychain: KeychainKind, /// Whether this UTXO is spent or not pub is_spent: bool, /// The derivation index for the script pubkey in the wallet @@ -268,6 +266,56 @@ impl Ord for TransactionDetails { } } +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Balance { + /// All coinbase outputs not yet matured + pub immature: u64, + /// Unconfirmed UTXOs generated by a wallet tx + pub trusted_pending: u64, + /// Unconfirmed UTXOs received from an external wallet + pub untrusted_pending: u64, + /// Confirmed and immediately spendable balance + pub confirmed: u64, +} + +impl Balance { + /// Get sum of trusted_pending and confirmed coins. + /// + /// This is the balance you can spend right now that shouldn't get cancelled via another party + /// double spending it. + pub fn trusted_spendable(&self) -> u64 { + self.confirmed + self.trusted_pending + } + + /// Get the whole balance visible to the wallet. + pub fn total(&self) -> u64 { + self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature + } +} + +impl core::fmt::Display for Balance { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}", + self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed + ) + } +} + +impl core::ops::Add for Balance { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self { + immature: self.immature + other.immature, + trusted_pending: self.trusted_pending + other.trusted_pending, + untrusted_pending: self.untrusted_pending + other.untrusted_pending, + confirmed: self.confirmed + other.confirmed, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index 373dbdc38..748e38fc2 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -712,7 +712,6 @@ mod test { value, script_pubkey: Script::new(), }, - keychain: KeychainKind::External, is_spent: false, derivation_index: 42, confirmation_time, @@ -771,7 +770,6 @@ mod test { value: rng.gen_range(0..200000000), script_pubkey: Script::new(), }, - keychain: KeychainKind::External, is_spent: false, derivation_index: rng.next_u32(), confirmation_time: if rng.gen_bool(0.5) { @@ -800,7 +798,6 @@ mod test { value: utxos_value, script_pubkey: Script::new(), }, - keychain: KeychainKind::External, is_spent: false, derivation_index: 42, confirmation_time: ConfirmationTime::Unconfirmed, diff --git a/crates/bdk/src/wallet/export.rs b/crates/bdk/src/wallet/export.rs index 905638449..b9af618a9 100644 --- a/crates/bdk/src/wallet/export.rs +++ b/crates/bdk/src/wallet/export.rs @@ -118,6 +118,7 @@ impl FullyNodedExport { ) -> Result { let descriptor = wallet .get_descriptor_for_keychain(KeychainKind::External) + .expect("external descriptor is always expected for default wallet") .to_string_with_secret( &wallet .get_signers(KeychainKind::External) @@ -147,6 +148,7 @@ impl FullyNodedExport { true => { let descriptor = wallet .get_descriptor_for_keychain(KeychainKind::Internal) + .expect("we ensured that public descriptor exists at this point") .to_string_with_secret( &wallet .get_signers(KeychainKind::Internal) diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 67032cd3c..8e0c74d43 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -12,18 +12,22 @@ //! Wallet //! //! This module defines the [`Wallet`] structure. -use crate::collections::{BTreeMap, HashMap, HashSet}; +use crate::{ + collections::{BTreeMap, HashMap, HashSet}, + descriptor::policy::Condition, + types::Balance, +}; use alloc::{ boxed::Box, string::{String, ToString}, sync::Arc, vec::Vec, }; -pub use bdk_chain::keychain::Balance; use bdk_chain::{ chain_graph, keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker}, - sparse_chain, BlockId, ConfirmationTime, + sparse_chain::{self, ChainPosition}, + BlockId, ConfirmationTime, TxHeight, }; use bitcoin::consensus::encode::serialize; use bitcoin::secp256k1::Secp256k1; @@ -68,6 +72,8 @@ use crate::signer::SignerError; use crate::types::*; use crate::wallet::coin_selection::Excess::{Change, NoChange}; +use self::tx_builder::is_satisfied_by; + const COINBASE_MATURITY: u32 = 100; /// A Bitcoin wallet @@ -80,22 +86,21 @@ const COINBASE_MATURITY: u32 = 100; /// /// [`signer`]: crate::signer #[derive(Debug)] -pub struct Wallet { - signers: Arc, - change_signers: Arc, - keychain_tracker: KeychainTracker, - persist: persist::Persist, +pub struct Wallet { + signers: BTreeMap>, + keychain_tracker: KeychainTracker, + persist: persist::Persist, network: Network, secp: SecpCtx, } /// The update to a [`Wallet`] used in [`Wallet::apply_update`]. This is usually returned from blockchain data sources. /// The type parameter `T` indicates the kind of transaction contained in the update. It's usually a [`bitcoin::Transaction`]. -pub type Update = KeychainScan; +pub type Update = KeychainScan; /// Error indicating that something was wrong with an [`Update`]. pub type UpdateError = chain_graph::UpdateError; /// The changeset produced internally by applying an update -pub(crate) type ChangeSet = KeychainChangeSet; +pub(crate) type ChangeSet = KeychainChangeSet; /// The address index selection strategy to use to derived an address from the wallet's external /// descriptor. See [`Wallet::get_address`]. If you're unsure which one to use use `WalletIndex::New`. @@ -122,16 +127,16 @@ pub enum AddressIndex { /// A derived address and the index it was found at. /// For convenience this automatically derefs to `Address` #[derive(Debug, PartialEq, Eq)] -pub struct AddressInfo { +pub struct AddressInfo { /// Child index of this address pub index: u32, /// Address pub address: Address, /// Type of keychain - pub keychain: KeychainKind, + pub keychain: K, } -impl Deref for AddressInfo { +impl Deref for AddressInfo { type Target = Address; fn deref(&self) -> &Self::Target { @@ -139,7 +144,7 @@ impl Deref for AddressInfo { } } -impl fmt::Display for AddressInfo { +impl fmt::Display for AddressInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.address) } @@ -200,12 +205,24 @@ impl Wallet { let secp = Secp256k1::new(); let mut keychain_tracker = KeychainTracker::default(); + let mut signers = BTreeMap::new(); let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network) .map_err(NewError::Descriptor)?; keychain_tracker .txout_index .add_keychain(KeychainKind::External, descriptor.clone()); - let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); + + // set_lookahead only works for derivable descriptors + if descriptor.has_wildcard() { + keychain_tracker + .txout_index + .set_lookahead(&KeychainKind::External, 20); + } + + signers.insert( + KeychainKind::External, + Arc::new(SignersContainer::build(keymap, &descriptor, &secp)), + ); let change_signers = match change_descriptor { Some(desc) => { let (change_descriptor, change_keymap) = @@ -220,13 +237,22 @@ impl Wallet { keychain_tracker .txout_index - .add_keychain(KeychainKind::Internal, change_descriptor); + .add_keychain(KeychainKind::Internal, change_descriptor.clone()); + + // set_lookahead only works for derivable descriptors + if change_descriptor.has_wildcard() { + keychain_tracker + .txout_index + .set_lookahead(&KeychainKind::Internal, 20); + } change_signers } None => Arc::new(SignersContainer::new()), }; + signers.insert(KeychainKind::Internal, change_signers); + db.load_into_keychain_tracker(&mut keychain_tracker) .map_err(NewError::Persist)?; @@ -234,7 +260,6 @@ impl Wallet { Ok(Wallet { signers, - change_signers, network, persist, secp, @@ -242,24 +267,14 @@ impl Wallet { }) } - /// Get the Bitcoin network the wallet is using. - pub fn network(&self) -> Network { - self.network - } - - /// Iterator over all keychains in this wallet - pub fn keychanins(&self) -> &BTreeMap { - self.keychain_tracker.txout_index.keychains() - } - /// Return a derived address using the external descriptor, see [`AddressIndex`] for /// available address index selection strategies. If none of the keys in the descriptor are derivable /// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. - pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo + pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo where D: persist::PersistBackend, { - self._get_address(address_index, KeychainKind::External) + self.get_address_by_keychain(address_index, KeychainKind::External) } /// Return a derived address using the internal (change) descriptor. @@ -269,416 +284,627 @@ impl Wallet { /// see [`AddressIndex`] for available address index selection strategies. If none of the keys /// in the descriptor are derivable (i.e. does not end with /*) then the same address will always /// be returned for any [`AddressIndex`]. - pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo - where - D: persist::PersistBackend, - { - self._get_address(address_index, KeychainKind::Internal) - } - - fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo + pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo where D: persist::PersistBackend, { - let keychain = self.map_keychain(keychain); - let txout_index = &mut self.keychain_tracker.txout_index; - let (index, spk) = match address_index { - AddressIndex::New => { - let ((index, spk), changeset) = txout_index.reveal_next_spk(&keychain); - let spk = spk.clone(); - - self.persist.stage(changeset.into()); - self.persist.commit().expect("TODO"); - (index, spk) - } - AddressIndex::LastUnused => { - let index = txout_index.last_revealed_index(&keychain); - match index { - Some(index) if !txout_index.is_used(&(keychain, index)) => ( - index, - txout_index - .spk_at_index(&(keychain, index)) - .expect("must exist") - .clone(), - ), - _ => return self._get_address(AddressIndex::New, keychain), - } - } - AddressIndex::Peek(index) => txout_index - .spks_of_keychain(&keychain) - .take(index as usize + 1) - .last() - .unwrap(), - }; - AddressInfo { - index, - address: Address::from_script(&spk, self.network) - .expect("descriptor must have address form"), - keychain, - } + self.get_address_by_keychain(address_index, self.map_keychain(KeychainKind::Internal)) } - /// Return whether or not a `script` is part of this wallet (either internal or external) - pub fn is_mine(&self, script: &Script) -> bool { - self.keychain_tracker - .txout_index - .index_of_spk(script) - .is_some() + /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature + /// values. + pub fn get_balance(&self) -> Balance { + self.get_balance_for_keychains( + [KeychainKind::External, KeychainKind::Internal], + |keychain| match keychain { + KeychainKind::External => false, + KeychainKind::Internal => true, + }, + ) } - /// Finds how the wallet derived the script pubkey `spk`. + /// Start building a transaction. /// - /// Will only return `Some(_)` if the wallet has given out the spk. - pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> { - self.keychain_tracker.txout_index.index_of_spk(spk).copied() - } - - /// Return the list of unspent outputs of this wallet - pub fn list_unspent(&self) -> Vec { - self.keychain_tracker - .full_utxos() - .map(|(&(keychain, derivation_index), utxo)| LocalUtxo { - outpoint: utxo.outpoint, - txout: utxo.txout, - keychain, - is_spent: false, - derivation_index, - confirmation_time: utxo.chain_position, - }) - .collect() - } - - /// Get all the checkpoints the wallet is currently storing indexed by height. - pub fn checkpoints(&self) -> &BTreeMap { - self.keychain_tracker.chain().checkpoints() - } - - /// Returns the latest checkpoint. - pub fn latest_checkpoint(&self) -> Option { - self.keychain_tracker.chain().latest_checkpoint() - } - - /// Returns a iterators of all the script pubkeys for the `Internal` and External` variants in `KeychainKind`. + /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. /// - /// This is inteded to be used when doing a full scan of your addresses (e.g. after restoring - /// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g. - /// electrum server) which will go through each address until it reaches a *stop grap*. + /// ## Example /// - /// Note carefully that iterators go over **all** script pubkeys on the keychains (not what - /// script pubkeys the wallet is storing internally). - pub fn spks_of_all_keychains( - &self, - ) -> BTreeMap + Clone> { - self.keychain_tracker.txout_index.spks_of_all_keychains() - } - - /// Gets an iterator over all the script pubkeys in a single keychain. + /// ``` + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk::*; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + /// let (psbt, details) = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), 50_000); + /// builder.finish()? + /// }; /// - /// See [`spks_of_all_keychains`] for more documentation + /// // sign and broadcast ... + /// # Ok::<(), bdk::Error>(()) + /// ``` /// - /// [`spks_of_all_keychains`]: Self::spks_of_all_keychains - pub fn spks_of_keychain( - &self, - keychain: KeychainKind, - ) -> impl Iterator + Clone { - self.keychain_tracker - .txout_index - .spks_of_keychain(&keychain) + /// [`TxBuilder`]: crate::TxBuilder + pub fn build_tx(&mut self) -> TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, CreateTx> { + TxBuilder { + wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), + params: TxParams::default(), + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, + } } - /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the - /// wallet's database. - pub fn get_utxo(&self, op: OutPoint) -> Option { - self.keychain_tracker - .full_utxos() - .find_map(|(&(keychain, derivation_index), txo)| { - if op == txo.outpoint { - Some(LocalUtxo { - outpoint: txo.outpoint, - txout: txo.txout, - keychain, - is_spent: txo.spent_by.is_none(), - derivation_index, - confirmation_time: txo.chain_position, - }) - } else { - None - } - }) + pub(crate) fn create_tx( + &mut self, + coin_selection: Cs, + params: TxParams, + ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> + where + D: persist::PersistBackend, + { + // change_keychain = KeychainKind::External if wallet doesn't have a change descriptor. + let change_keychain = self.map_keychain(KeychainKind::Internal); + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed + && change_keychain == KeychainKind::External + { + return Err(Error::Generic( + "The `change_policy` can be set only if the wallet has a change_descriptor".into(), + )); + } + + self.create_tx_multi( + [KeychainKind::External].to_vec(), + [change_keychain].to_vec(), + coin_selection, + params, + ) } - /// Return a single transactions made and received by the wallet + /// Bump the fee of a transaction previously created with this wallet. /// - /// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if - /// `include_raw` is `true`. - pub fn get_tx(&self, txid: Txid, include_raw: bool) -> Option { - let (&confirmation_time, tx) = self.keychain_tracker.chain_graph().get_tx_in_chain(txid)?; + /// Returns an error if the transaction is already confirmed or doesn't explicitly signal + /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] + /// pre-populated with the inputs and outputs of the original transaction. + /// + /// ## Example + /// + /// ```no_run + /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk::*; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); + /// let (mut psbt, _) = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), 50_000) + /// .enable_rbf(); + /// builder.finish()? + /// }; + /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// let tx = psbt.extract_tx(); + /// // broadcast tx but it's taking too long to confirm so we want to bump the fee + /// let (mut psbt, _) = { + /// let mut builder = wallet.build_fee_bump(tx.txid())?; + /// builder + /// .fee_rate(FeeRate::from_sat_per_vb(5.0)); + /// builder.finish()? + /// }; + /// + /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; + /// let fee_bumped_tx = psbt.extract_tx(); + /// // broadcast fee_bumped_tx to replace original + /// # Ok::<(), bdk::Error>(()) + /// ``` + // TODO: support for merging multiple transactions while bumping the fees + pub fn build_fee_bump( + &mut self, + txid: Txid, + ) -> Result, Error> { let graph = self.keychain_tracker.graph(); let txout_index = &self.keychain_tracker.txout_index; + let tx_and_height = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); + let mut tx = match tx_and_height { + None => return Err(Error::TransactionNotFound), + Some((ConfirmationTime::Confirmed { .. }, _tx)) => { + return Err(Error::TransactionConfirmed) + } + Some((_, tx)) => tx.clone(), + }; - let received = tx - .output - .iter() - .map(|txout| { - if txout_index.index_of_spk(&txout.script_pubkey).is_some() { - txout.value - } else { - 0 - } - }) - .sum(); - - let sent = tx + if !tx .input .iter() - .map(|txin| { - if let Some((_, txout)) = txout_index.txout(txin.previous_output) { - txout.value - } else { - 0 - } - }) - .sum(); + .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) + { + return Err(Error::IrreplaceableTransaction); + } - let inputs = tx - .input - .iter() - .map(|txin| { - graph - .get_txout(txin.previous_output) - .map(|txout| txout.value) - }) - .sum::>(); - let outputs = tx.output.iter().map(|txout| txout.value).sum(); - let fee = inputs.map(|inputs| inputs.saturating_sub(outputs)); + let fee = graph.calculate_fee(&tx).ok_or(Error::FeeRateUnavailable)?; + if fee < 0 { + // It's available but it's wrong so let's say it's unavailable + return Err(Error::FeeRateUnavailable)?; + } + let fee = fee as u64; + let feerate = FeeRate::from_wu(fee, tx.weight()); - Some(TransactionDetails { - transaction: if include_raw { Some(tx.clone()) } else { None }, - txid, - received, - sent, - fee, - confirmation_time, + // remove the inputs from the tx and process them + let original_txin = tx.input.drain(..).collect::>(); + let original_utxos = original_txin + .iter() + .map(|txin| -> Result<_, Error> { + let (&confirmation_time, prev_tx) = self + .keychain_tracker + .chain_graph() + .get_tx_in_chain(txin.previous_output.txid) + .ok_or(Error::UnknownUtxo)?; + let txout = &prev_tx.output[txin.previous_output.vout as usize]; + + let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) { + Some((keychain, derivation_index)) => { + //TODO: handle the case where get_descriptor_for_keychain returns None + let satisfaction_weight = self + .get_descriptor_for_keychain(*keychain) + .unwrap() + .max_satisfaction_weight() + .unwrap(); + WeightedUtxo { + utxo: Utxo::Local(LocalUtxo { + outpoint: txin.previous_output, + txout: txout.clone(), + //keychain, + is_spent: true, + derivation_index: *derivation_index, + confirmation_time, + }), + satisfaction_weight, + } + } + None => { + let satisfaction_weight = + serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(); + WeightedUtxo { + satisfaction_weight, + utxo: Utxo::Foreign { + outpoint: txin.previous_output, + psbt_input: Box::new(psbt::Input { + witness_utxo: Some(txout.clone()), + non_witness_utxo: Some(prev_tx.clone()), + ..Default::default() + }), + }, + } + } + }; + + Ok(weighted_utxo) + }) + .collect::, _>>()?; + + if tx.output.len() > 1 { + let mut change_index = None; + for (index, txout) in tx.output.iter().enumerate() { + let change_type = self.map_keychain(KeychainKind::Internal); + match txout_index.index_of_spk(&txout.script_pubkey) { + Some(&(keychain, _)) if keychain == change_type => change_index = Some(index), + _ => {} + } + } + + if let Some(change_index) = change_index { + tx.output.remove(change_index); + } + } + + let params = TxParams { + // TODO: figure out what rbf option should be? + version: Some(tx_builder::Version(tx.version)), + recipients: tx + .output + .into_iter() + .map(|txout| (txout.script_pubkey, txout.value)) + .collect(), + utxos: original_utxos, + bumping_fee: Some(tx_builder::PreviousFee { + absolute: fee, + rate: feerate.as_sat_per_vb(), + }), + ..Default::default() + }; + + Ok(TxBuilder { + wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), + params, + coin_selection: DefaultCoinSelectionAlgorithm::default(), + phantom: core::marker::PhantomData, }) } - /// Add a new checkpoint to the wallet's internal view of the chain. - /// This stages but does not [`commit`] the change. + fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { + if keychain == KeychainKind::Internal + && self.public_descriptor(KeychainKind::Internal).is_none() + { + KeychainKind::External + } else { + keychain + } + } +} + +impl Wallet +where + K: core::fmt::Debug + Clone + Ord, +{ + /// Creates a Wallet with multiple descriptors. /// - /// Returns whether anything changed with the insertion (e.g. `false` if checkpoint was already - /// there). + /// It takes in an iterator of (K, E), where K is the `Keychain` marker + /// of the descriptor. This can be a string, ex: "keyhcain_1". /// - /// [`commit`]: Self::commit - pub fn insert_checkpoint( + /// Pass in the descriptor marker in other wallet APIs to query + /// for keychain specific information. + pub fn new_multi_descriptor( + mut db: D, + network: Network, + descriptors: impl IntoIterator, + ) -> Result> + where + D: persist::PersistBackend, + { + let secp = Secp256k1::new(); + let mut keychain_tracker = KeychainTracker::default(); + let mut signers = BTreeMap::new(); + + for (keychain, descriptor) in descriptors { + let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network) + .map_err(NewError::Descriptor)?; + keychain_tracker + .txout_index + .add_keychain(keychain.clone(), descriptor.clone()); + signers.insert( + keychain, + Arc::new(SignersContainer::build(keymap, &descriptor, &secp)), + ); + } + + // Is 20 a good Number? + keychain_tracker.txout_index.set_lookahead_for_all(20); + + db.load_into_keychain_tracker(&mut keychain_tracker) + .map_err(NewError::Persist)?; + + let persist = persist::Persist::new(db); + + Ok(Wallet { + signers, + network, + persist, + secp, + keychain_tracker, + }) + } + + /// Add a new descriptor with a keychain marker. + pub fn add_descriptor( &mut self, - block_id: BlockId, - ) -> Result { - let changeset = self.keychain_tracker.insert_checkpoint(block_id)?; - let changed = changeset.is_empty(); - self.persist.stage(changeset); - Ok(changed) + keychain: K, + descriptor: E, + ) -> Result<(), NewError> + where + D: persist::PersistBackend, + { + let (descriptor, keymap) = + into_wallet_descriptor_checked(descriptor, &self.secp, self.network) + .map_err(NewError::Descriptor)?; + self.keychain_tracker + .txout_index + .add_keychain(keychain.clone(), descriptor.clone()); + self.signers.insert( + keychain, + Arc::new(SignersContainer::build(keymap, &descriptor, &self.secp)), + ); + + Ok(()) } - /// Add a transaction to the wallet's internal view of the chain. - /// This stages but does not [`commit`] the change. - /// - /// There are a number reasons `tx` could be rejected with an `Err(_)`. The most important one - /// is that the transaction is at a height that is greater than [`latest_checkpoint`]. Therefore - /// you should use [`insert_checkpoint`] to insert new checkpoints before manually inserting new - /// transactions. - /// - /// Returns whether anything changed with the transaction insertion (e.g. `false` if the - /// transaction was already inserted at the same position). - /// - /// [`commit`]: Self::commit - /// [`latest_checkpoint`]: Self::latest_checkpoint - /// [`insert_checkpoint`]: Self::insert_checkpoint - pub fn insert_tx( + /// Get new/last_unused/peek an address from a given keychain. + pub fn get_address_by_keychain( &mut self, - tx: Transaction, - position: ConfirmationTime, - ) -> Result> { - let changeset = self.keychain_tracker.insert_tx(tx, position)?; - let changed = changeset.is_empty(); - self.persist.stage(changeset); - Ok(changed) + address_index: AddressIndex, + keychain: K, + ) -> AddressInfo + where + D: persist::PersistBackend, + { + //let keychain = self.map_keychain(keychain); + //TODO: check if keychain exists before carrying on + let txout_index = &mut self.keychain_tracker.txout_index; + let (index, spk) = match address_index { + AddressIndex::New => { + let ((index, spk), changeset) = txout_index.reveal_next_spk(&keychain); + let spk = spk.clone(); + + self.persist.stage(changeset.into()); + self.persist.commit().expect("TODO"); + (index, spk) + } + AddressIndex::LastUnused => { + let index = txout_index.last_revealed_index(&keychain); + match index { + Some(index) if !txout_index.is_used(&(keychain.clone(), index)) => ( + index, + txout_index + .spk_at_index(&(keychain.clone(), index)) + .expect("must exist") + .clone(), + ), + _ => return self.get_address_by_keychain(AddressIndex::New, keychain), + } + } + AddressIndex::Peek(index) => txout_index + .spks_of_keychain(&keychain) + .take(index as usize + 1) + .last() + .unwrap(), + }; + AddressInfo { + index, + address: Address::from_script(&spk, self.network) + .expect("descriptor must have address form"), + keychain: keychain.clone(), + } } - #[deprecated(note = "use Wallet::transactions instead")] - /// Deprecated. use `Wallet::transactions` instead. - pub fn list_transactions(&self, include_raw: bool) -> Vec { + /// Get the available utxos for a given keychain. + pub fn list_utxos_by_keychain(&self, keychain: K) -> Vec { self.keychain_tracker - .chain() - .txids() - .map(|&(_, txid)| self.get_tx(txid, include_raw).expect("must exist")) + .full_utxos() + .filter(|((k, _), _)| *k == keychain) + .map(|((_, derivation_index), utxo)| LocalUtxo { + outpoint: utxo.outpoint, + txout: utxo.txout, + is_spent: false, + derivation_index: *derivation_index, + confirmation_time: utxo.chain_position, + }) .collect() } - /// Iterate over the transactions in the wallet in order of ascending confirmation time with - /// unconfirmed transactions last. - pub fn transactions( + /// List all the transactions known by the wallet for a given keychain. + pub fn list_transactions_by_keychain( &self, - ) -> impl DoubleEndedIterator + '_ { + keychain: &K, + include_raw: bool, + ) -> Vec { self.keychain_tracker - .chain_graph() - .transactions_in_chain() - .map(|(pos, tx)| (*pos, tx)) - } - - /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature - /// values. - pub fn get_balance(&self) -> Balance { - self.keychain_tracker.balance(|keychain| match keychain { - KeychainKind::External => false, - KeychainKind::Internal => true, - }) + .full_txouts() + .filter_map(|((k, _), utxo)| { + if k == keychain { + let tx = self + .get_tx(utxo.outpoint.txid, include_raw) + .expect("This should exist"); + Some(tx) + } else { + None + } + }) + .collect::>() } - /// Add an external signer - /// - /// See [the `signer` module](signer) for an example. - pub fn add_signer( - &mut self, - keychain: KeychainKind, - ordering: SignerOrdering, - signer: Arc, - ) { - let signers = match keychain { - KeychainKind::External => Arc::make_mut(&mut self.signers), - KeychainKind::Internal => Arc::make_mut(&mut self.change_signers), - }; - - signers.add_external(signer.id(&self.secp), ordering, signer); - } + /// Returns the [`Balance`] of the given keychain. + /// If `should_trust` is `true`, unconfirmed balance will be reported as `trusted_pending`. + pub fn get_balance_by_keychain(&self, keychain: &K, should_trust: bool) -> Balance { + let mut immature = 0; + let mut trusted_pending = 0; + let mut untrusted_pending = 0; + let mut confirmed = 0; + let last_sync_height = self + .keychain_tracker + .chain() + .latest_checkpoint() + .map(|latest| latest.height); + for ((_, _), utxo) in self + .keychain_tracker + .full_utxos() + .filter(|((k, _), _)| k == keychain) + { + let chain_position = &utxo.chain_position; + + match chain_position.height() { + TxHeight::Confirmed(_) => { + if utxo.is_on_coinbase { + if utxo.is_mature( + last_sync_height + .expect("since it's confirmed we must have a checkpoint"), + ) { + confirmed += utxo.txout.value; + } else { + immature += utxo.txout.value; + } + } else { + confirmed += utxo.txout.value; + } + } + TxHeight::Unconfirmed => { + if should_trust { + trusted_pending += utxo.txout.value; + } else { + untrusted_pending += utxo.txout.value; + } + } + } + } - /// Get the signers - /// - /// ## Example - /// - /// ``` - /// # use bdk::{Wallet, KeychainKind}; - /// # use bdk::bitcoin::Network; - /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?; - /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { - /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* - /// println!("secret_key: {}", secret_key); - /// } - /// - /// Ok::<(), Box>(()) - /// ``` - pub fn get_signers(&self, keychain: KeychainKind) -> Arc { - match keychain { - KeychainKind::External => Arc::clone(&self.signers), - KeychainKind::Internal => Arc::clone(&self.change_signers), + Balance { + immature, + trusted_pending, + untrusted_pending, + confirmed, } } - /// Start building a transaction. - /// - /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction. + /// Returns the [`Balance`] of a given list of keychains. /// - /// ## Example + /// The `should_trust` predicated can be used if user wishes to categorize the unconfirmed balance among "trusted" and + /// "untrusted" sources. This is typically useful when a wallet is receiving into *external* keychains and which receives funds + /// from external wallets, not controlled by the user. /// - /// ``` - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk::*; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); - /// let (psbt, details) = { - /// let mut builder = wallet.build_tx(); - /// builder - /// .add_recipient(to_address.script_pubkey(), 50_000); - /// builder.finish()? - /// }; - /// - /// // sign and broadcast ... - /// # Ok::<(), bdk::Error>(()) - /// ``` - /// - /// [`TxBuilder`]: crate::TxBuilder - pub fn build_tx(&mut self) -> TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, CreateTx> { - TxBuilder { - wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), - params: TxParams::default(), - coin_selection: DefaultCoinSelectionAlgorithm::default(), - phantom: core::marker::PhantomData, - } + /// When this categorization is not needed, passing in `should_trust : true` will default to all keychain balances being + /// "trusted". + pub fn get_balance_for_keychains( + &self, + keychains: impl IntoIterator, + should_trust: impl Fn(&K) -> bool, + ) -> Balance { + keychains.into_iter().fold(Balance::default(), |acc, k| { + acc + self.get_balance_by_keychain(&k, should_trust(&k)) + }) } - pub(crate) fn create_tx( + /// create transaction for a multi descriptor wallet. + /// + /// `utxo_keychains`: The keychains from which utxos are to be selected. + /// `change_keychain`: The keychains to be used for change address. + /// `coin_selection`: Coinselection algorithm to use. + /// `params`: [`TxParams`] dictating various creation parameter. + // TODO: Currently the change address is being selected randomly from the + // given list. This can be improved to have the advanced feature of matching the recipient's + // address type. + pub(crate) fn create_tx_multi( &mut self, + spend_keychains: impl IntoIterator, + change_keychains: impl IntoIterator, coin_selection: Cs, params: TxParams, ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> where - D: persist::PersistBackend, + D: persist::PersistBackend, { - let external_descriptor = self - .keychain_tracker - .txout_index - .keychains() - .get(&KeychainKind::External) - .expect("must exist"); - let internal_descriptor = self - .keychain_tracker - .txout_index - .keychains() - .get(&KeychainKind::Internal); + // Collect the keychains to simplify lifetime and ownership requirements. + let spend_keychains = spend_keychains.into_iter().collect::>(); + let change_keychains = change_keychains.into_iter().collect::>(); - let external_policy = external_descriptor - .extract_policy(&self.signers, BuildSatisfaction::None, &self.secp)? - .unwrap(); - let internal_policy = internal_descriptor - .as_ref() - .map(|desc| { - Ok::<_, Error>( - desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? - .unwrap(), + let external_descriptors = spend_keychains + .iter() + .map(|k| { + ( + self.keychain_tracker + .txout_index + .keychains() + .get(k) + .expect("must exist"), + k, ) }) - .transpose()?; + .collect::>(); + + // This is required to make the `internal_descriptor` empty to handle single descriptor + // default wallet situation. + let internal_descriptors = if change_keychains != spend_keychains { + change_keychains + .iter() + .map(|k| { + ( + self.keychain_tracker + .txout_index + .keychains() + .get(k) + .expect("change descriptors also must exist"), + k, + ) + }) + .collect::>() + } else { + vec![] + }; + + // TODO: Don't know if the right thing here is to unwrap. This will work with the statusquo descriptor implementation. + let external_policies = external_descriptors + .iter() + .map(|(desc, k)| { + Ok(( + desc.extract_policy( + self.signers.get(k).unwrap(), + BuildSatisfaction::None, + &self.secp, + )? + .unwrap(), + k, + )) + }) + .collect::, Error>>()?; + // TODO: Don't know if the right thing here is to unwrap. This will work with the statusquo descriptor implementation. + let internal_policies = internal_descriptors + .iter() + .map(|(desc, k)| { + Ok(( + desc.extract_policy( + self.signers.get(k).unwrap(), + BuildSatisfaction::None, + &self.secp, + )? + .unwrap(), + k, + )) + }) + .collect::, Error>>()?; // The policy allows spending external outputs, but it requires a policy path that hasn't been // provided if params.change_policy != tx_builder::ChangeSpendPolicy::OnlyChange - && external_policy.requires_path() + && external_policies + .iter() + .any(|(policy, _)| policy.requires_path()) && params.external_policy_path.is_none() { return Err(Error::SpendingPolicyRequired(KeychainKind::External)); }; + // Same for the internal_policy path, if present - if let Some(internal_policy) = &internal_policy { - if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden - && internal_policy.requires_path() - && params.internal_policy_path.is_none() - { - return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); - }; - } + if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeForbidden + && internal_policies + .iter() + .any(|(policy, _)| policy.requires_path()) + && params.internal_policy_path.is_none() + { + return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); + }; - let external_requirements = external_policy.get_condition( - params - .external_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?; - let internal_requirements = internal_policy - .map(|policy| { - Ok::<_, Error>( - policy.get_condition( - params - .internal_policy_path - .as_ref() - .unwrap_or(&BTreeMap::new()), - )?, - ) + let external_requirements = external_policies + .iter() + .map(|(policy, _)| { + Ok(policy.get_condition( + params + .external_policy_path + .as_ref() + .unwrap_or(&BTreeMap::new()), + )?) }) - .transpose()?; + .collect::, Error>>()?; - let requirements = - external_requirements.merge(&internal_requirements.unwrap_or_default())?; + let internal_requirements = internal_policies + .iter() + .map(|(policy, _)| { + Ok(policy.get_condition( + params + .internal_policy_path + .as_ref() + .unwrap_or(&BTreeMap::new()), + )?) + }) + .collect::, Error>>()?; + + // TODO: Don't unwrap, throw result + let requirements = external_requirements + .iter() + .chain(internal_requirements.iter()) + .fold(Condition::default(), |accu, condition| { + accu.merge(condition).unwrap() + }); debug!("Policy requirements: {:?}", requirements); let version = match params.version { @@ -852,7 +1078,7 @@ impl Wallet { fee_amount += fee_rate.fee_wu(2); if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed - && internal_descriptor.is_none() + && internal_descriptors.is_empty() { return Err(Error::Generic( "The `change_policy` can be set only if the wallet has a change_descriptor".into(), @@ -867,13 +1093,17 @@ impl Wallet { params.manually_selected_only, params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee current_height.map(LockTime::to_consensus_u32), + &spend_keychains, + &change_keychains, ); // get drain script let drain_script = match params.drain_to { Some(ref drain_recipient) => drain_recipient.clone(), None => { - let change_keychain = self.map_keychain(KeychainKind::Internal); + // FIXME: This will randomly pop one of the provided change keychain. + // Ideally it should be a keychain who's address type matches with recipient address type. + let change_keychain = change_keychains.clone().pop().unwrap(); let ((index, spk), changeset) = self .keychain_tracker .txout_index @@ -964,176 +1194,200 @@ impl Wallet { let sent = coin_selection.local_selected_amount(); let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; - let transaction_details = TransactionDetails { - transaction: None, + let transaction_details = TransactionDetails { + transaction: None, + txid, + confirmation_time: ConfirmationTime::Unconfirmed, + received, + sent, + fee: Some(fee_amount), + }; + + Ok((psbt, transaction_details)) + } + + /// Get the Bitcoin network the wallet is using. + pub fn network(&self) -> Network { + self.network + } + + /// Finds how the wallet derived the script pubkey `spk`. + /// + /// Will only return `Some(_)` if the wallet has given out the spk. + pub fn derivation_of_spk(&self, spk: &Script) -> Option<(K, u32)> { + self.keychain_tracker.txout_index.index_of_spk(spk).cloned() + } + + /// Get all the checkpoints the wallet is currently storing indexed by height. + pub fn checkpoints(&self) -> &BTreeMap { + self.keychain_tracker.chain().checkpoints() + } + + /// Returns the latest checkpoint. + pub fn latest_checkpoint(&self) -> Option { + self.keychain_tracker.chain().latest_checkpoint() + } + + /// Return a single transactions made and received by the wallet + + /// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if + /// `include_raw` is `true`. + pub fn get_tx(&self, txid: Txid, include_raw: bool) -> Option { + let (&confirmation_time, tx) = self.keychain_tracker.chain_graph().get_tx_in_chain(txid)?; + let graph = self.keychain_tracker.graph(); + let txout_index = &self.keychain_tracker.txout_index; + + let received = tx + .output + .iter() + .map(|txout| { + if txout_index.index_of_spk(&txout.script_pubkey).is_some() { + txout.value + } else { + 0 + } + }) + .sum(); + + let sent = tx + .input + .iter() + .map(|txin| { + if let Some((_, txout)) = txout_index.txout(txin.previous_output) { + txout.value + } else { + 0 + } + }) + .sum(); + + let inputs = tx + .input + .iter() + .map(|txin| { + graph + .get_txout(txin.previous_output) + .map(|txout| txout.value) + }) + .sum::>(); + let outputs = tx.output.iter().map(|txout| txout.value).sum(); + let fee = inputs.map(|inputs| inputs.saturating_sub(outputs)); + + Some(TransactionDetails { + transaction: if include_raw { Some(tx.clone()) } else { None }, txid, - confirmation_time: ConfirmationTime::Unconfirmed, received, sent, - fee: Some(fee_amount), - }; - - Ok((psbt, transaction_details)) + fee, + confirmation_time, + }) } - /// Bump the fee of a transaction previously created with this wallet. + /// Add a new checkpoint to the wallet's internal view of the chain. + /// This stages but does not [`commit`] the change. /// - /// Returns an error if the transaction is already confirmed or doesn't explicitly signal - /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] - /// pre-populated with the inputs and outputs of the original transaction. + /// Returns whether anything changed with the insertion (e.g. `false` if checkpoint was already + /// there). /// - /// ## Example + /// [`commit`]: Self::commit + pub fn insert_checkpoint( + &mut self, + block_id: BlockId, + ) -> Result { + let changeset = self.keychain_tracker.insert_checkpoint(block_id)?; + let changed = changeset.is_empty(); + self.persist.stage(changeset); + Ok(changed) + } + + /// Add a transaction to the wallet's internal view of the chain. + /// This stages but does not [`commit`] the change. /// - /// ```no_run - /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk::*; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap(); - /// let (mut psbt, _) = { - /// let mut builder = wallet.build_tx(); - /// builder - /// .add_recipient(to_address.script_pubkey(), 50_000) - /// .enable_rbf(); - /// builder.finish()? - /// }; - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; - /// let tx = psbt.extract_tx(); - /// // broadcast tx but it's taking too long to confirm so we want to bump the fee - /// let (mut psbt, _) = { - /// let mut builder = wallet.build_fee_bump(tx.txid())?; - /// builder - /// .fee_rate(FeeRate::from_sat_per_vb(5.0)); - /// builder.finish()? - /// }; + /// There are a number reasons `tx` could be rejected with an `Err(_)`. The most important one + /// is that the transaction is at a height that is greater than [`latest_checkpoint`]. Therefore + /// you should use [`insert_checkpoint`] to insert new checkpoints before manually inserting new + /// transactions. /// - /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; - /// let fee_bumped_tx = psbt.extract_tx(); - /// // broadcast fee_bumped_tx to replace original - /// # Ok::<(), bdk::Error>(()) - /// ``` - // TODO: support for merging multiple transactions while bumping the fees - pub fn build_fee_bump( + /// Returns whether anything changed with the transaction insertion (e.g. `false` if the + /// transaction was already inserted at the same position). + /// + /// [`commit`]: Self::commit + /// [`latest_checkpoint`]: Self::latest_checkpoint + /// [`insert_checkpoint`]: Self::insert_checkpoint + pub fn insert_tx( &mut self, - txid: Txid, - ) -> Result, Error> { - let graph = self.keychain_tracker.graph(); - let txout_index = &self.keychain_tracker.txout_index; - let tx_and_height = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); - let mut tx = match tx_and_height { - None => return Err(Error::TransactionNotFound), - Some((ConfirmationTime::Confirmed { .. }, _tx)) => { - return Err(Error::TransactionConfirmed) - } - Some((_, tx)) => tx.clone(), - }; + tx: Transaction, + position: ConfirmationTime, + ) -> Result> { + let changeset = self.keychain_tracker.insert_tx(tx, position)?; + let changed = changeset.is_empty(); + self.persist.stage(changeset); + Ok(changed) + } - if !tx - .input - .iter() - .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) - { - return Err(Error::IrreplaceableTransaction); - } + #[deprecated(note = "use Wallet::transactions instead")] + /// Deprecated. use `Wallet::transactions` instead. + pub fn list_transactions(&self, include_raw: bool) -> Vec { + self.keychain_tracker + .chain() + .txids() + .map(|&(_, txid)| self.get_tx(txid, include_raw).expect("must exist")) + .collect() + } - let fee = graph.calculate_fee(&tx).ok_or(Error::FeeRateUnavailable)?; - if fee < 0 { - // It's available but it's wrong so let's say it's unavailable - return Err(Error::FeeRateUnavailable)?; + /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. + /// + /// This frees up the change address used when creating the tx for use in future transactions. + // TODO: Make this free up reserved utxos when that's implemented + pub fn cancel_tx(&mut self, tx: &Transaction) { + let mut to_unamrk = Vec::new(); + for txout in &tx.output { + if let Some((keychain, index)) = self + .keychain_tracker + .txout_index + .index_of_spk(&txout.script_pubkey) + { + // NOTE: unmark_used will **not** make something unused if it has actually been used + // by a tx in the tracker. It only removes the superficial marking. + to_unamrk.push((keychain.clone(), *index)); + } } - let fee = fee as u64; - let feerate = FeeRate::from_wu(fee, tx.weight()); - - // remove the inputs from the tx and process them - let original_txin = tx.input.drain(..).collect::>(); - let original_utxos = original_txin - .iter() - .map(|txin| -> Result<_, Error> { - let (&confirmation_time, prev_tx) = self - .keychain_tracker - .chain_graph() - .get_tx_in_chain(txin.previous_output.txid) - .ok_or(Error::UnknownUtxo)?; - let txout = &prev_tx.output[txin.previous_output.vout as usize]; - - let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) { - Some(&(keychain, derivation_index)) => { - let satisfaction_weight = self - .get_descriptor_for_keychain(keychain) - .max_satisfaction_weight() - .unwrap(); - WeightedUtxo { - utxo: Utxo::Local(LocalUtxo { - outpoint: txin.previous_output, - txout: txout.clone(), - keychain, - is_spent: true, - derivation_index, - confirmation_time, - }), - satisfaction_weight, - } - } - None => { - let satisfaction_weight = - serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(); - WeightedUtxo { - satisfaction_weight, - utxo: Utxo::Foreign { - outpoint: txin.previous_output, - psbt_input: Box::new(psbt::Input { - witness_utxo: Some(txout.clone()), - non_witness_utxo: Some(prev_tx.clone()), - ..Default::default() - }), - }, - } - } - }; + let txout_index = &mut self.keychain_tracker.txout_index; + for (k, i) in to_unamrk { + txout_index.unmark_used(&k, i); + } + } - Ok(weighted_utxo) + /// Return the list of unspent outputs of this wallet + pub fn list_unspent(&self) -> Vec<(LocalUtxo, K)> { + self.keychain_tracker + .full_utxos() + .map(|((keychain, derivation_index), utxo)| { + ( + LocalUtxo { + outpoint: utxo.outpoint, + txout: utxo.txout, + is_spent: false, + derivation_index: *derivation_index, + confirmation_time: utxo.chain_position, + }, + keychain.clone(), + ) }) - .collect::, _>>()?; - - if tx.output.len() > 1 { - let mut change_index = None; - for (index, txout) in tx.output.iter().enumerate() { - let change_type = self.map_keychain(KeychainKind::Internal); - match txout_index.index_of_spk(&txout.script_pubkey) { - Some(&(keychain, _)) if keychain == change_type => change_index = Some(index), - _ => {} - } - } - - if let Some(change_index) = change_index { - tx.output.remove(change_index); - } - } + .collect() + } - let params = TxParams { - // TODO: figure out what rbf option should be? - version: Some(tx_builder::Version(tx.version)), - recipients: tx - .output - .into_iter() - .map(|txout| (txout.script_pubkey, txout.value)) - .collect(), - utxos: original_utxos, - bumping_fee: Some(tx_builder::PreviousFee { - absolute: fee, - rate: feerate.as_sat_per_vb(), - }), - ..Default::default() - }; + /// Iterator over all keychains in this wallet + pub fn keychains(&self) -> &BTreeMap { + self.keychain_tracker.txout_index.keychains() + } - Ok(TxBuilder { - wallet: alloc::rc::Rc::new(core::cell::RefCell::new(self)), - params, - coin_selection: DefaultCoinSelectionAlgorithm::default(), - phantom: core::marker::PhantomData, - }) + /// Return whether or not a `script` is part of this wallet (either internal or external) + pub fn is_mine(&self, script: &Script) -> bool { + self.keychain_tracker + .txout_index + .index_of_spk(script) + .is_some() } /// Sign a transaction with all the wallet's signers, in the order specified by every signer's @@ -1196,12 +1450,7 @@ impl Wallet { return Err(Error::Signer(signer::SignerError::NonStandardSighash)); } - for signer in self - .signers - .signers() - .iter() - .chain(self.change_signers.signers().iter()) - { + for signer in self.signers.iter().flat_map(|(_, signer)| signer.signers()) { signer.sign_transaction(psbt, &sign_options, &self.secp)?; } @@ -1213,27 +1462,6 @@ impl Wallet { } } - /// Return the spending policies for the wallet's descriptor - pub fn policies(&self, keychain: KeychainKind) -> Result, Error> { - let signers = match keychain { - KeychainKind::External => &self.signers, - KeychainKind::Internal => &self.change_signers, - }; - - match self.public_descriptor(keychain) { - Some(desc) => Ok(desc.extract_policy(signers, BuildSatisfaction::None, &self.secp)?), - None => Ok(None), - } - } - - /// Return the "public" version of the wallet's descriptor, meaning a new descriptor that has - /// the same structure but with every secret key removed - /// - /// This can be used to build a watch-only version of a wallet - pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> { - self.keychain_tracker.txout_index.keychains().get(&keychain) - } - /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer) @@ -1333,74 +1561,18 @@ impl Wallet { Ok(finished) } - /// Return the secp256k1 context used for all signing operations - pub fn secp_ctx(&self) -> &SecpCtx { - &self.secp - } - - /// Returns the descriptor used to create addresses for a particular `keychain`. - pub fn get_descriptor_for_keychain(&self, keychain: KeychainKind) -> &ExtendedDescriptor { - self.public_descriptor(self.map_keychain(keychain)) - .expect("we mapped it to external if it doesn't exist") - } - - /// The derivation index of this wallet. It will return `None` if it has not derived any addresses. - /// Otherwise, it will return the index of the highest address it has derived. - pub fn derivation_index(&self, keychain: KeychainKind) -> Option { - self.keychain_tracker - .txout_index - .last_revealed_index(&keychain) - } - - /// The index of the next address that you would get if you were to ask the wallet for a new address - pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { - self.keychain_tracker.txout_index.next_index(&keychain).0 - } - - /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. - /// - /// This frees up the change address used when creating the tx for use in future transactions. - // TODO: Make this free up reserved utxos when that's implemented - pub fn cancel_tx(&mut self, tx: &Transaction) { - let txout_index = &mut self.keychain_tracker.txout_index; - for txout in &tx.output { - if let Some(&(keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) { - // NOTE: unmark_used will **not** make something unused if it has actually been used - // by a tx in the tracker. It only removes the superficial marking. - txout_index.unmark_used(&keychain, index); - } - } - } - - fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { - if keychain == KeychainKind::Internal - && self.public_descriptor(KeychainKind::Internal).is_none() - { - KeychainKind::External - } else { - keychain - } - } - - fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { - let &(keychain, child) = self - .keychain_tracker - .txout_index - .index_of_spk(&txout.script_pubkey)?; - let descriptor = self.get_descriptor_for_keychain(keychain); - Some(descriptor.at_derivation_index(child)) - } - - fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize)> { + fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize, K)> { self.list_unspent() .into_iter() - .map(|utxo| { - let keychain = utxo.keychain; + .map(|(utxo, keychain)| { ( utxo, - self.get_descriptor_for_keychain(keychain) + //TODO: handle the case where get_descriptor_for_keychain returns None + self.get_descriptor_for_keychain(keychain.clone()) + .unwrap() .max_satisfaction_weight() .unwrap(), + keychain, ) }) .collect() @@ -1418,6 +1590,8 @@ impl Wallet { manual_only: bool, must_only_use_confirmed_tx: bool, current_height: Option, + external_keychains: &[K], + internal_keychains: &[K], ) -> (Vec, Vec) { // must_spend <- manually selected utxos // may_spend <- all other available utxos @@ -1475,9 +1649,13 @@ impl Wallet { .collect::>(); let mut i = 0; - may_spend.retain(|u| { - let retain = change_policy.is_satisfied_by(&u.0) - && !unspendable.contains(&u.0.outpoint) + may_spend.retain(|(u, _, k)| { + let retain = is_satisfied_by( + change_policy, + (u, k.clone()), + external_keychains, + internal_keychains, + ) && !unspendable.contains(&u.outpoint) && satisfies_confirmed[i]; i += 1; retain @@ -1485,7 +1663,7 @@ impl Wallet { let mut may_spend = may_spend .into_iter() - .map(|(local_utxo, satisfaction_weight)| WeightedUtxo { + .map(|(local_utxo, satisfaction_weight, _)| WeightedUtxo { satisfaction_weight, utxo: Utxo::Local(local_utxo), }) @@ -1508,7 +1686,7 @@ impl Wallet { if params.add_global_xpubs { let all_xpubs = self - .keychanins() + .keychains() .iter() .flat_map(|(_, desc)| desc.get_extended_keys()) .collect::>(); @@ -1580,45 +1758,6 @@ impl Wallet { Ok(psbt) } - /// get the corresponding PSBT Input for a LocalUtxo - pub fn get_psbt_input( - &self, - utxo: LocalUtxo, - sighash_type: Option, - only_witness_utxo: bool, - ) -> Result { - // Try to find the prev_script in our db to figure out if this is internal or external, - // and the derivation index - let &(keychain, child) = self - .keychain_tracker - .txout_index - .index_of_spk(&utxo.txout.script_pubkey) - .ok_or(Error::UnknownUtxo)?; - - let mut psbt_input = psbt::Input { - sighash_type, - ..psbt::Input::default() - }; - - let desc = self.get_descriptor_for_keychain(keychain); - let derived_descriptor = desc.at_derivation_index(child); - - psbt_input - .update_with_descriptor_unchecked(&derived_descriptor) - .map_err(MiniscriptPsbtError::Conversion)?; - - let prev_output = utxo.outpoint; - if let Some(prev_tx) = self.keychain_tracker.graph().get_tx(prev_output.txid) { - if desc.is_witness() || desc.is_taproot() { - psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); - } - if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { - psbt_input.non_witness_utxo = Some(prev_tx.clone()); - } - } - Ok(psbt_input) - } - fn update_psbt_with_descriptor( &self, psbt: &mut psbt::PartiallySignedTransaction, @@ -1641,7 +1780,7 @@ impl Wallet { // Try to figure out the keychain and derivation for every input and output for (is_input, index, out) in utxos.into_iter() { - if let Some(&(keychain, child)) = self + if let Some((keychain, child)) = self .keychain_tracker .txout_index .index_of_spk(&out.script_pubkey) @@ -1651,8 +1790,9 @@ impl Wallet { index, keychain, child ); - let desc = self.get_descriptor_for_keychain(keychain); - let desc = desc.at_derivation_index(child); + // TODO: handle the None case. + let desc = self.get_descriptor_for_keychain(keychain.clone()).unwrap(); + let desc = desc.at_derivation_index(*child); if is_input { psbt.update_input_with_descriptor(index, &desc) @@ -1667,16 +1807,83 @@ impl Wallet { Ok(()) } - /// Return the checksum of the public descriptor associated to `keychain` - /// - /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor - pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String { - self.get_descriptor_for_keychain(keychain) - .to_string() - .split_once('#') - .unwrap() - .1 - .to_string() + fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { + let (keychain, child) = self + .keychain_tracker + .txout_index + .index_of_spk(&txout.script_pubkey)?; + //TODO: handle the case where get_descriptor_for_keychain returns None + let descriptor = self.get_descriptor_for_keychain(keychain.clone()).unwrap(); + Some(descriptor.at_derivation_index(*child)) + } + + /// get the corresponding PSBT Input for a LocalUtxo + pub fn get_psbt_input( + &self, + utxo: LocalUtxo, + sighash_type: Option, + only_witness_utxo: bool, + ) -> Result { + // Try to find the prev_script in our db to figure out if this is internal or external, + // and the derivation index + let (keychain, child) = self + .keychain_tracker + .txout_index + .index_of_spk(&utxo.txout.script_pubkey) + .ok_or(Error::UnknownUtxo)?; + + let mut psbt_input = psbt::Input { + sighash_type, + ..psbt::Input::default() + }; + + //TODO: Handle the case where get_descriptor_for_keychain returns None + let desc = self.get_descriptor_for_keychain(keychain.clone()).unwrap(); + let derived_descriptor = desc.at_derivation_index(*child); + + psbt_input + .update_with_descriptor_unchecked(&derived_descriptor) + .map_err(MiniscriptPsbtError::Conversion)?; + + let prev_output = utxo.outpoint; + if let Some(prev_tx) = self.keychain_tracker.graph().get_tx(prev_output.txid) { + if desc.is_witness() || desc.is_taproot() { + psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); + } + if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { + psbt_input.non_witness_utxo = Some(prev_tx.clone()); + } + } + Ok(psbt_input) + } + + /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the + /// wallet's database. + pub fn get_utxo(&self, op: OutPoint) -> Option<(LocalUtxo, K)> { + self.keychain_tracker + .full_utxos() + .find_map(|((k, derivation_index), txo)| { + if op == txo.outpoint { + Some(( + LocalUtxo { + outpoint: txo.outpoint, + txout: txo.txout, + //keychain, + is_spent: txo.spent_by.is_none(), + derivation_index: *derivation_index, + confirmation_time: txo.chain_position, + }, + k.clone(), + )) + } else { + None + } + }) + } + + /// Return the secp256k1 context used for all signing operations + pub fn secp_ctx(&self) -> &SecpCtx { + &self.secp } /// Applies an update to the wallet and stages the changes (but does not [`commit`] them). @@ -1685,9 +1892,9 @@ impl Wallet { /// transactions related to your wallet into it. /// /// [`commit`]: Self::commit - pub fn apply_update(&mut self, update: Update) -> Result<(), UpdateError> + pub fn apply_update(&mut self, update: Update) -> Result<(), UpdateError> where - D: persist::PersistBackend, + D: persist::PersistBackend, { let changeset = self.keychain_tracker.apply_update(update)?; self.persist.stage(changeset); @@ -1699,7 +1906,7 @@ impl Wallet { /// [`staged`]: Self::staged pub fn commit(&mut self) -> Result<(), D::WriteError> where - D: persist::PersistBackend, + D: persist::PersistBackend, { self.persist.commit() } @@ -1707,7 +1914,7 @@ impl Wallet { /// Returns the changes that will be staged with the next call to [`commit`]. /// /// [`commit`]: Self::commit - pub fn staged(&self) -> &ChangeSet { + pub fn staged(&self) -> &ChangeSet { self.persist.staged() } @@ -1720,6 +1927,133 @@ impl Wallet { pub fn as_chain_graph(&self) -> &bdk_chain::chain_graph::ChainGraph { self.keychain_tracker.chain_graph() } + + /// Returns a iterators of all the script pubkeys for the `Internal` and External` variants in `KeychainKind`. + /// + /// This is inteded to be used when doing a full scan of your addresses (e.g. after restoring + /// from seed words). You pass the `BTreeMap` of iterators to a blockchain data source (e.g. + /// electrum server) which will go through each address until it reaches a *stop grap*. + /// + /// Note carefully that iterators go over **all** script pubkeys on the keychains (not what + /// script pubkeys the wallet is storing internally). + pub fn spks_of_all_keychains( + &self, + ) -> BTreeMap + Clone> { + self.keychain_tracker.txout_index.spks_of_all_keychains() + } + + /// Gets an iterator over all the script pubkeys in a single keychain. + /// + /// See [`spks_of_all_keychains`] for more documentation + /// + /// [`spks_of_all_keychains`]: Self::spks_of_all_keychains + pub fn spks_of_keychain(&self, keychain: K) -> impl Iterator + Clone { + self.keychain_tracker + .txout_index + .spks_of_keychain(&keychain) + } + + /// Iterate over the transactions in the wallet in order of ascending confirmation time with + /// unconfirmed transactions last. + pub fn transactions( + &self, + ) -> impl DoubleEndedIterator + '_ { + self.keychain_tracker + .chain_graph() + .transactions_in_chain() + .map(|(pos, tx)| (*pos, tx)) + } + + /// Add an external signer + /// + /// See [the `signer` module](signer) for an example. + pub fn add_signer( + &mut self, + keychain: K, + ordering: SignerOrdering, + signer: Arc, + ) { + if let Some(signers) = self.signers.get_mut(&keychain) { + let signers = Arc::make_mut(signers); + signers.add_external(signer.id(&self.secp), ordering, signer); + } + } + + /// Get the signers + /// + /// ## Example + /// + /// ``` + /// # use bdk::{Wallet, KeychainKind}; + /// # use bdk::bitcoin::Network; + /// let wallet = Wallet::new_no_persist("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet)?; + /// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) { + /// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/* + /// println!("secret_key: {}", secret_key); + /// } + /// + /// Ok::<(), Box>(()) + /// ``` + pub fn get_signers(&self, keychain: K) -> Arc { + let signers = self + .signers + .get(&keychain) + .expect("KeychainKind does not have signers in wallet"); + Arc::clone(signers) + } + + /// Return the spending policies for the wallet's descriptor + pub fn policies(&self, keychain: K) -> Result, Error> { + let signers = match self.signers.get(&keychain) { + Some(signers) => signers, + None => return Ok(None), + }; + + match self.public_descriptor(keychain) { + Some(desc) => Ok(desc.extract_policy(signers, BuildSatisfaction::None, &self.secp)?), + None => Ok(None), + } + } + + /// Return the "public" version of the wallet's descriptor, meaning a new descriptor that has + /// the same structure but with every secret key removed + /// + /// This can be used to build a watch-only version of a wallet + pub fn public_descriptor(&self, keychain: K) -> Option<&ExtendedDescriptor> { + self.keychain_tracker.txout_index.keychains().get(&keychain) + } + + /// Returns the descriptor used to create addresses for a particular `keychain`. + pub fn get_descriptor_for_keychain(&self, keychain: K) -> Option<&ExtendedDescriptor> { + self.public_descriptor(keychain) + } + + /// The derivation index of this wallet. It will return `None` if it has not derived any addresses. + /// Otherwise, it will return the index of the highest address it has derived. + pub fn derivation_index(&self, keychain: K) -> Option { + self.keychain_tracker + .txout_index + .last_revealed_index(&keychain) + } + + /// The index of the next address that you would get if you were to ask the wallet for a new address + pub fn next_derivation_index(&self, keychain: K) -> u32 { + self.keychain_tracker.txout_index.next_index(&keychain).0 + } + + /// Return the checksum of the public descriptor associated to `keychain` + /// + /// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor + pub fn descriptor_checksum(&self, keychain: K) -> String { + // TODO: do not unwrap, properly handle None case. + self.get_descriptor_for_keychain(keychain) + .unwrap() + .to_string() + .split_once('#') + .unwrap() + .1 + .to_string() + } } impl AsRef for Wallet { diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index dbd4811c1..2b1195ce1 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -41,6 +41,7 @@ use crate::collections::HashSet; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; use bdk_chain::ConfirmationTime; use core::cell::RefCell; +use core::fmt::Debug; use core::marker::PhantomData; use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt}; @@ -119,7 +120,7 @@ impl TxBuilderContext for BumpFee {} /// [`coin_selection`]: Self::coin_selection #[derive(Debug)] pub struct TxBuilder<'a, D, Cs, Ctx> { - pub(crate) wallet: Rc>>, + pub(crate) wallet: Rc>>, pub(crate) params: TxParams, pub(crate) coin_selection: Cs, pub(crate) phantom: PhantomData, @@ -282,8 +283,9 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, .map(|outpoint| wallet.get_utxo(*outpoint).ok_or(Error::UnknownUtxo)) .collect::, _>>()?; - for utxo in utxos { - let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain); + for (utxo, keychain) in utxos { + //TODO: handle the case where get_descriptor_for_keychain returns None + let descriptor = wallet.get_descriptor_for_keychain(keychain).unwrap(); let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap(); self.params.utxos.push(WeightedUtxo { satisfaction_weight, @@ -766,13 +768,18 @@ impl Default for ChangeSpendPolicy { } } -impl ChangeSpendPolicy { - pub(crate) fn is_satisfied_by(&self, utxo: &LocalUtxo) -> bool { - match self { - ChangeSpendPolicy::ChangeAllowed => true, - ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal, - ChangeSpendPolicy::ChangeForbidden => utxo.keychain == KeychainKind::External, - } +// This is written as a stand alone function because we don't want +// `ChangeSpendPolicy` to be generic over K. +pub(crate) fn is_satisfied_by( + change_spend_policy: ChangeSpendPolicy, + utxo: (&LocalUtxo, K), + external_keychains: &[K], + change_keychains: &[K], +) -> bool { + match change_spend_policy { + ChangeSpendPolicy::ChangeAllowed => true, + ChangeSpendPolicy::OnlyChange => change_keychains.contains(&utxo.1), + ChangeSpendPolicy::ChangeForbidden => external_keychains.contains(&utxo.1), } } @@ -869,35 +876,39 @@ mod test { assert_eq!(tx.output[2].script_pubkey, From::from(vec![0xAA, 0xEE])); } - fn get_test_utxos() -> Vec { + fn get_test_utxos() -> Vec<(LocalUtxo, KeychainKind)> { use bitcoin::hashes::Hash; vec![ - LocalUtxo { - outpoint: OutPoint { - txid: bitcoin::Txid::from_inner([0; 32]), - vout: 0, - }, - txout: Default::default(), - keychain: KeychainKind::External, - is_spent: false, - confirmation_time: ConfirmationTime::Unconfirmed, - derivation_index: 0, - }, - LocalUtxo { - outpoint: OutPoint { - txid: bitcoin::Txid::from_inner([0; 32]), - vout: 1, + ( + LocalUtxo { + outpoint: OutPoint { + txid: bitcoin::Txid::from_inner([0; 32]), + vout: 0, + }, + txout: Default::default(), + is_spent: false, + confirmation_time: ConfirmationTime::Unconfirmed, + derivation_index: 0, }, - txout: Default::default(), - keychain: KeychainKind::Internal, - is_spent: false, - confirmation_time: ConfirmationTime::Confirmed { - height: 32, - time: 42, + KeychainKind::External, + ), + ( + LocalUtxo { + outpoint: OutPoint { + txid: bitcoin::Txid::from_inner([0; 32]), + vout: 1, + }, + txout: Default::default(), + is_spent: false, + confirmation_time: ConfirmationTime::Confirmed { + height: 32, + time: 42, + }, + derivation_index: 1, }, - derivation_index: 1, - }, + KeychainKind::Internal, + ), ] } @@ -906,7 +917,14 @@ mod test { let change_spend_policy = ChangeSpendPolicy::default(); let filtered = get_test_utxos() .into_iter() - .filter(|u| change_spend_policy.is_satisfied_by(u)) + .filter(|(u, k)| { + is_satisfied_by( + change_spend_policy, + (u, *k), + &[KeychainKind::External], + &[KeychainKind::Internal], + ) + }) .count(); assert_eq!(filtered, 2); @@ -917,11 +935,18 @@ mod test { let change_spend_policy = ChangeSpendPolicy::ChangeForbidden; let filtered = get_test_utxos() .into_iter() - .filter(|u| change_spend_policy.is_satisfied_by(u)) + .filter(|(u, k)| { + is_satisfied_by( + change_spend_policy, + (u, *k), + &[KeychainKind::External], + &[KeychainKind::Internal], + ) + }) .collect::>(); assert_eq!(filtered.len(), 1); - assert_eq!(filtered[0].keychain, KeychainKind::External); + assert_eq!(filtered[0].1, KeychainKind::External); } #[test] @@ -929,11 +954,18 @@ mod test { let change_spend_policy = ChangeSpendPolicy::OnlyChange; let filtered = get_test_utxos() .into_iter() - .filter(|u| change_spend_policy.is_satisfied_by(u)) + .filter(|(u, k)| { + is_satisfied_by( + change_spend_policy, + (u, *k), + &[KeychainKind::External], + &[KeychainKind::Internal], + ) + }) .collect::>(); assert_eq!(filtered.len(), 1); - assert_eq!(filtered[0].keychain, KeychainKind::Internal); + assert_eq!(filtered[0].1, KeychainKind::Internal); } #[test] diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 0ada20d39..2e46bfbfb 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -3,7 +3,8 @@ use bdk::descriptor::calc_checksum; use bdk::signer::{SignOptions, SignerError}; use bdk::wallet::coin_selection::LargestFirstCoinSelection; use bdk::wallet::AddressIndex::*; -use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; +use bdk::wallet::{AddressIndex, AddressInfo, Wallet}; +use bdk::Balance; use bdk::Error; use bdk::FeeRate; use bdk::KeychainKind; @@ -73,7 +74,7 @@ fn test_descriptor_checksum() { assert_eq!(checksum.len(), 8); let raw_descriptor = wallet - .keychanins() + .keychains() .iter() .next() .unwrap() @@ -437,7 +438,7 @@ fn test_create_tx_drain_to_and_utxos() { let utxos: Vec<_> = wallet .list_unspent() .into_iter() - .map(|u| u.outpoint) + .map(|(u, _)| u.outpoint) .collect(); let mut builder = wallet.build_tx(); builder @@ -972,9 +973,10 @@ fn test_add_foreign_utxo() { get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo = wallet2.list_unspent().remove(0); + let (utxo, _) = wallet2.list_unspent().remove(0); let foreign_utxo_satisfaction = wallet2 .get_descriptor_for_keychain(KeychainKind::External) + .unwrap() .max_satisfaction_weight() .unwrap(); @@ -1036,9 +1038,10 @@ fn test_add_foreign_utxo() { #[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] fn test_add_foreign_utxo_invalid_psbt_input() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); - let outpoint = wallet.list_unspent()[0].outpoint; + let outpoint = wallet.list_unspent()[0].0.outpoint; let foreign_utxo_satisfaction = wallet .get_descriptor_for_keychain(KeychainKind::External) + .unwrap() .max_satisfaction_weight() .unwrap(); @@ -1054,12 +1057,13 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { let (wallet2, txid2) = get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); - let utxo2 = wallet2.list_unspent().remove(0); + let (utxo2, _) = wallet2.list_unspent().remove(0); let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap(); let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); let satisfaction_weight = wallet2 .get_descriptor_for_keychain(KeychainKind::External) + .unwrap() .max_satisfaction_weight() .unwrap(); @@ -1098,10 +1102,11 @@ fn test_add_foreign_utxo_only_witness_utxo() { let (wallet2, txid2) = get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo2 = wallet2.list_unspent().remove(0); + let (utxo2, _) = wallet2.list_unspent().remove(0); let satisfaction_weight = wallet2 .get_descriptor_for_keychain(KeychainKind::External) + .unwrap() .max_satisfaction_weight() .unwrap(); @@ -1160,7 +1165,7 @@ fn test_add_foreign_utxo_only_witness_utxo() { fn test_get_psbt_input() { // this should grab a known good utxo and set the input let (wallet, _) = get_funded_wallet(get_test_wpkh()); - for utxo in wallet.list_unspent() { + for (utxo, _) in wallet.list_unspent() { let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()); } @@ -2660,10 +2665,11 @@ fn test_taproot_foreign_utxo() { let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); - let utxo = wallet2.list_unspent().remove(0); + let (utxo, _) = wallet2.list_unspent().remove(0); let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); let foreign_utxo_satisfaction = wallet2 .get_descriptor_for_keychain(KeychainKind::External) + .unwrap() .max_satisfaction_weight() .unwrap();