diff --git a/crates/bdk/src/wallet/export.rs b/crates/bdk/src/wallet/export.rs index 90563844..36b75ea2 100644 --- a/crates/bdk/src/wallet/export.rs +++ b/crates/bdk/src/wallet/export.rs @@ -56,7 +56,6 @@ use core::str::FromStr; use alloc::string::{String, ToString}; -use bdk_chain::sparse_chain::ChainPosition; use serde::{Deserialize, Serialize}; use miniscript::descriptor::{ShInner, WshInner}; @@ -130,8 +129,10 @@ impl FullyNodedExport { wallet .transactions() .next() - .and_then(|(pos, _)| pos.height().into()) - .unwrap_or(0) + .map_or(0, |canonical_tx| match canonical_tx.observed_as { + bdk_chain::ObservedAs::Confirmed(a) => a.confirmation_height, + bdk_chain::ObservedAs::Unconfirmed(_) => 0, + }) } else { 0 }; @@ -246,6 +247,7 @@ mod test { height: 5000, time: 0, }, + None, ) .unwrap(); wallet diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 95197545..c894d6ba 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -21,9 +21,12 @@ use alloc::{ }; pub use bdk_chain::keychain::Balance; use bdk_chain::{ - chain_graph, - keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker}, - sparse_chain, BlockId, ConfirmationTime, + indexed_tx_graph::{IndexedAdditions, IndexedTxGraph}, + keychain::{DerivationAdditions, KeychainTxOutIndex}, + local_chain::{self, LocalChain, UpdateNotConnectedError}, + tx_graph::{CanonicalTx, TxGraph}, + Anchor, Append, BlockId, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut, ObservedAs, + Persist, PersistBackend, }; use bitcoin::consensus::encode::serialize; use bitcoin::secp256k1::Secp256k1; @@ -83,19 +86,83 @@ const COINBASE_MATURITY: u32 = 100; pub struct Wallet { signers: Arc, change_signers: Arc, - keychain_tracker: KeychainTracker, - persist: persist::Persist, + chain: LocalChain, + indexed_graph: IndexedTxGraph>, + persist: Persist, // [TODO] Use a different `ChangeSet` 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; -/// Error indicating that something was wrong with an [`Update`]. -pub type UpdateError = chain_graph::UpdateError; +#[derive(Debug, Default, PartialEq)] +pub struct Update { + keychain: BTreeMap, + graph: TxGraph, + chain: LocalChain, +} + /// The changeset produced internally by applying an update -pub(crate) type ChangeSet = KeychainChangeSet; +#[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(bound( + deserialize = "A: Ord + serde::Deserialize<'de>, K: Ord + serde::Deserialize<'de>", + serialize = "A: Ord + serde::Serialize, K: Ord + serde::Serialize" +))] +// #[cfg_attr(predicate, attr)] +pub struct ChangeSet { + pub chain_changeset: local_chain::ChangeSet, + pub indexed_additions: IndexedAdditions>, +} + +impl Default for ChangeSet { + fn default() -> Self { + Self { + chain_changeset: Default::default(), + indexed_additions: Default::default(), + } + } +} + +impl Append for ChangeSet { + fn append(&mut self, other: Self) { + Append::append(&mut self.chain_changeset, other.chain_changeset); + Append::append(&mut self.indexed_additions, other.indexed_additions); + } + + fn is_empty(&self) -> bool { + self.chain_changeset.is_empty() && self.indexed_additions.is_empty() + } +} + +impl From>> for ChangeSet { + fn from(indexed_additions: IndexedAdditions>) -> Self { + Self { + indexed_additions, + ..Default::default() + } + } +} + +impl From> for ChangeSet { + fn from(index_additions: DerivationAdditions) -> Self { + Self { + indexed_additions: IndexedAdditions { + index_additions, + ..Default::default() + }, + ..Default::default() + } + } +} + +impl From for ChangeSet { + fn from(chain_changeset: local_chain::ChangeSet) -> Self { + Self { + chain_changeset, + ..Default::default() + } + } +} /// 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`. @@ -182,6 +249,14 @@ where } } +#[derive(Debug)] +pub enum InsertTxError { + ConfirmationHeightCannotBeGreaterThanTip { + tip_height: Option, + tx_height: u32, + }, +} + #[cfg(feature = "std")] impl std::error::Error for NewError

{} @@ -195,15 +270,17 @@ impl Wallet { network: Network, ) -> Result> where - D: persist::PersistBackend, + D: PersistBackend, { let secp = Secp256k1::new(); + let mut chain = LocalChain::default(); + let mut indexed_graph = + IndexedTxGraph::>::default(); - let mut keychain_tracker = KeychainTracker::default(); let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network) .map_err(NewError::Descriptor)?; - keychain_tracker - .txout_index + indexed_graph + .index .add_keychain(KeychainKind::External, descriptor.clone()); let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); let change_signers = match change_descriptor { @@ -218,8 +295,8 @@ impl Wallet { &secp, )); - keychain_tracker - .txout_index + indexed_graph + .index .add_keychain(KeychainKind::Internal, change_descriptor); change_signers @@ -227,18 +304,20 @@ impl Wallet { None => Arc::new(SignersContainer::new()), }; - db.load_into_keychain_tracker(&mut keychain_tracker) - .map_err(NewError::Persist)?; + let changeset = db.load_from_persistence().map_err(NewError::Persist)?; + chain.apply_changeset(changeset.chain_changeset); + indexed_graph.apply_additions(changeset.indexed_additions); - let persist = persist::Persist::new(db); + let persist = Persist::new(db); Ok(Wallet { signers, change_signers, network, + chain, + indexed_graph, persist, secp, - keychain_tracker, }) } @@ -249,7 +328,7 @@ impl Wallet { /// Iterator over all keychains in this wallet pub fn keychains(&self) -> &BTreeMap { - self.keychain_tracker.txout_index.keychains() + self.indexed_graph.index.keychains() } /// Return a derived address using the external descriptor, see [`AddressIndex`] for @@ -257,7 +336,7 @@ impl Wallet { /// (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 where - D: persist::PersistBackend, + D: PersistBackend, { self._get_address(address_index, KeychainKind::External) } @@ -271,17 +350,17 @@ impl Wallet { /// be returned for any [`AddressIndex`]. pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo where - D: persist::PersistBackend, + D: PersistBackend, { self._get_address(address_index, KeychainKind::Internal) } fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo where - D: persist::PersistBackend, + D: PersistBackend, { let keychain = self.map_keychain(keychain); - let txout_index = &mut self.keychain_tracker.txout_index; + let txout_index = &mut self.indexed_graph.index; let (index, spk) = match address_index { AddressIndex::New => { let ((index, spk), changeset) = txout_index.reveal_next_spk(&keychain); @@ -320,42 +399,36 @@ impl Wallet { /// 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() + self.indexed_graph.index.index_of_spk(script).is_some() } /// 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<(KeychainKind, u32)> { - self.keychain_tracker.txout_index.index_of_spk(spk).copied() + self.indexed_graph.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() + pub fn list_unspent(&self) -> impl Iterator + '_ { + self.indexed_graph + .graph() + .filter_chain_unspents( + &self.chain, + self.chain.tip().unwrap_or_default(), + self.indexed_graph.index.outpoints().iter().cloned(), + ) + .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) } /// Get all the checkpoints the wallet is currently storing indexed by height. pub fn checkpoints(&self) -> &BTreeMap { - self.keychain_tracker.chain().checkpoints() + self.chain.blocks() } /// Returns the latest checkpoint. pub fn latest_checkpoint(&self) -> Option { - self.keychain_tracker.chain().latest_checkpoint() + self.chain.tip() } /// Returns a iterators of all the script pubkeys for the `Internal` and External` variants in `KeychainKind`. @@ -369,7 +442,7 @@ impl Wallet { pub fn spks_of_all_keychains( &self, ) -> BTreeMap + Clone> { - self.keychain_tracker.txout_index.spks_of_all_keychains() + self.indexed_graph.index.spks_of_all_keychains() } /// Gets an iterator over all the script pubkeys in a single keychain. @@ -381,30 +454,22 @@ impl Wallet { &self, keychain: KeychainKind, ) -> impl Iterator + Clone { - self.keychain_tracker - .txout_index - .spks_of_keychain(&keychain) + self.indexed_graph.index.spks_of_keychain(&keychain) } /// 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 - } - }) + let (&spk_i, _) = self.indexed_graph.index.txout(op)?; + self.indexed_graph + .graph() + .filter_chain_unspents( + &self.chain, + self.chain.tip().unwrap_or_default(), + core::iter::once((spk_i, op)), + ) + .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) + .next() } /// Return a single transactions made and received by the wallet @@ -412,54 +477,22 @@ impl 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 graph = self.indexed_graph.graph(); - let received = tx - .output - .iter() - .map(|txout| { - if txout_index.index_of_spk(&txout.script_pubkey).is_some() { - txout.value - } else { - 0 - } - }) - .sum(); + let canonical_tx = CanonicalTx { + observed_as: graph.get_chain_position( + &self.chain, + self.chain.tip().unwrap_or_default(), + txid, + )?, + node: graph.get_tx_node(txid)?, + }; - 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, - received, - sent, - fee, - confirmation_time, - }) + Some(new_tx_details( + &self.indexed_graph, + canonical_tx, + include_raw, + )) } /// Add a new checkpoint to the wallet's internal view of the chain. @@ -472,10 +505,15 @@ impl Wallet { 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); + ) -> Result + where + D: PersistBackend, + { + let changeset = self.chain.insert_block(block_id)?; + let changed = !changeset.is_empty(); + if changed { + self.persist.stage(changeset.into()); + } Ok(changed) } @@ -497,41 +535,80 @@ impl Wallet { &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); + seen_at: Option, + ) -> Result + where + D: PersistBackend, + { + let tip = self.chain.tip(); + + if let ConfirmationTime::Confirmed { height, .. } = position { + let tip_height = tip.map(|b| b.height); + if Some(height) > tip_height { + return Err(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { + tip_height, + tx_height: height, + }); + } + } + + let anchor = match position { + ConfirmationTime::Confirmed { height, time } => { + let tip_height = tip.map(|b| b.height); + if Some(height) > tip_height { + return Err(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip { + tip_height, + tx_height: height, + }); + } + Some(ConfirmationTimeAnchor { + anchor_block: tip.expect("already checked if tip_height > height"), + confirmation_height: height, + confirmation_time: time, + }) + } + ConfirmationTime::Unconfirmed => None, + }; + + let changeset: ChangeSet = self.indexed_graph.insert_tx(&tx, anchor, seen_at).into(); + let changed = !changeset.is_empty(); + if changed { + self.persist.stage(changeset); + } Ok(changed) } #[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() + pub fn list_transactions( + &self, + include_raw: bool, + ) -> impl Iterator + '_ { + self.indexed_graph + .graph() + .list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default()) + .map(move |canonical_tx| new_tx_details(&self.indexed_graph, canonical_tx, include_raw)) } /// 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)) + ) -> impl Iterator> + '_ { + self.indexed_graph + .graph() + .list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default()) } /// 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, - }) + self.indexed_graph.graph().balance( + &self.chain, + self.chain.tip().unwrap_or_default(), + self.indexed_graph.index.outpoints().iter().cloned(), + |&(k, _), _| k == KeychainKind::Internal, + ) } /// Add an external signer @@ -613,17 +690,17 @@ impl Wallet { params: TxParams, ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> where - D: persist::PersistBackend, + D: PersistBackend, { let external_descriptor = self - .keychain_tracker - .txout_index + .indexed_graph + .index .keychains() .get(&KeychainKind::External) .expect("must exist"); let internal_descriptor = self - .keychain_tracker - .txout_index + .indexed_graph + .index .keychains() .get(&KeychainKind::Internal); @@ -700,9 +777,8 @@ impl Wallet { let current_height = match params.current_height { // If they didn't tell us the current height, we assume it's the latest sync height. None => self - .keychain_tracker - .chain() - .latest_checkpoint() + .chain + .tip() .and_then(|cp| cp.height.into()) .map(|height| LockTime::from_height(height).expect("Invalid height")), h => h, @@ -874,14 +950,10 @@ impl Wallet { Some(ref drain_recipient) => drain_recipient.clone(), None => { let change_keychain = self.map_keychain(KeychainKind::Internal); - let ((index, spk), changeset) = self - .keychain_tracker - .txout_index - .next_unused_spk(&change_keychain); + let ((index, spk), changeset) = + self.indexed_graph.index.next_unused_spk(&change_keychain); let spk = spk.clone(); - self.keychain_tracker - .txout_index - .mark_used(&change_keychain, index); + self.indexed_graph.index.mark_used(&change_keychain, index); self.persist.stage(changeset.into()); self.persist.commit().expect("TODO"); spk @@ -1019,16 +1091,21 @@ impl Wallet { &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 graph = self.indexed_graph.graph(); + let txout_index = &self.indexed_graph.index; + let chain_tip = self.chain.tip().unwrap_or_default(); + + let mut tx = graph + .get_tx(txid) + .ok_or(Error::TransactionNotFound)? + .clone(); + + let pos = graph + .get_chain_position(&self.chain, chain_tip, txid) + .ok_or(Error::TransactionNotFound)?; + if let ObservedAs::Confirmed(_) = pos { + return Err(Error::TransactionConfirmed); + } if !tx .input @@ -1051,13 +1128,17 @@ impl Wallet { 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) + let prev_tx = graph + .get_tx(txin.previous_output.txid) .ok_or(Error::UnknownUtxo)?; let txout = &prev_tx.output[txin.previous_output.vout as usize]; + let confirmation_time: ConfirmationTime = graph + .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid) + .ok_or(Error::UnknownUtxo)? + .cloned() + .into(); + let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) { Some(&(keychain, derivation_index)) => { let satisfaction_weight = self @@ -1231,7 +1312,7 @@ impl Wallet { /// /// 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) + self.indexed_graph.index.keychains().get(&keychain) } /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass @@ -1247,6 +1328,8 @@ impl Wallet { psbt: &mut psbt::PartiallySignedTransaction, sign_options: SignOptions, ) -> Result { + let chain_tip = self.chain.tip().unwrap_or_default(); + let tx = &psbt.unsigned_tx; let mut finished = true; @@ -1259,19 +1342,16 @@ impl Wallet { continue; } let confirmation_height = self - .keychain_tracker - .chain() - .tx_position(input.previous_output.txid) - .map(|conftime| match conftime { - &ConfirmationTime::Confirmed { height, .. } => height, - ConfirmationTime::Unconfirmed => u32::MAX, + .indexed_graph + .graph() + .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) + .map(|observed_as| match observed_as { + ObservedAs::Confirmed(a) => a.confirmation_height, + ObservedAs::Unconfirmed(_) => u32::MAX, }); - let last_sync_height = self - .keychain_tracker - .chain() - .latest_checkpoint() - .map(|block_id| block_id.height); - let current_height = sign_options.assume_height.or(last_sync_height); + let current_height = sign_options + .assume_height + .or(self.chain.tip().map(|b| b.height)); debug!( "Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}", @@ -1288,8 +1368,8 @@ impl Wallet { .get_utxo_for(n) .and_then(|txout| self.get_descriptor_for_txout(&txout)) .or_else(|| { - self.keychain_tracker - .txout_index + self.indexed_graph + .index .keychains() .iter() .find_map(|(_, desc)| { @@ -1347,14 +1427,12 @@ impl Wallet { /// 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) + self.indexed_graph.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 + self.indexed_graph.index.next_index(&keychain).0 } /// Informs the wallet that you no longer intend to broadcast a tx that was built from it. @@ -1362,7 +1440,7 @@ impl Wallet { /// 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; + let txout_index = &mut self.indexed_graph.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 @@ -1384,8 +1462,8 @@ impl Wallet { fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { let &(keychain, child) = self - .keychain_tracker - .txout_index + .indexed_graph + .index .index_of_spk(&txout.script_pubkey)?; let descriptor = self.get_descriptor_for_keychain(keychain); Some(descriptor.at_derivation_index(child)) @@ -1393,7 +1471,6 @@ impl Wallet { fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize)> { self.list_unspent() - .into_iter() .map(|utxo| { let keychain = utxo.keychain; ( @@ -1419,6 +1496,7 @@ impl Wallet { must_only_use_confirmed_tx: bool, current_height: Option, ) -> (Vec, Vec) { + let chain_tip = self.chain.tip().unwrap_or_default(); // must_spend <- manually selected utxos // may_spend <- all other available utxos let mut may_spend = self.get_available_utxos(); @@ -1438,39 +1516,43 @@ impl Wallet { let satisfies_confirmed = may_spend .iter() - .map(|u| { + .map(|u| -> bool { let txid = u.0.outpoint.txid; - let tx = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); - match tx { - // We don't have the tx in the db for some reason, - // so we can't know for sure if it's mature or not. - // We prefer not to spend it. - None => false, - Some((confirmation_time, tx)) => { - // Whether the UTXO is mature and, if needed, confirmed - let mut spendable = true; - if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() { - return false; - } - if tx.is_coin_base() { - debug_assert!( - confirmation_time.is_confirmed(), - "coinbase must always be confirmed" - ); - if let Some(current_height) = current_height { - match confirmation_time { - ConfirmationTime::Confirmed { height, .. } => { - // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 - spendable &= (current_height.saturating_sub(*height)) - >= COINBASE_MATURITY; - } - ConfirmationTime::Unconfirmed => spendable = false, - } + let tx = match self.indexed_graph.graph().get_tx(txid) { + Some(tx) => tx, + None => return false, + }; + let confirmation_time: ConfirmationTime = match self + .indexed_graph + .graph() + .get_chain_position(&self.chain, chain_tip, txid) + { + Some(observed_as) => observed_as.cloned().into(), + None => return false, + }; + + // Whether the UTXO is mature and, if needed, confirmed + let mut spendable = true; + if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() { + return false; + } + if tx.is_coin_base() { + debug_assert!( + confirmation_time.is_confirmed(), + "coinbase must always be confirmed" + ); + if let Some(current_height) = current_height { + match confirmation_time { + ConfirmationTime::Confirmed { height, .. } => { + // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 + spendable &= + (current_height.saturating_sub(height)) >= COINBASE_MATURITY; } + ConfirmationTime::Unconfirmed => spendable = false, } - spendable } } + spendable }) .collect::>(); @@ -1590,8 +1672,8 @@ impl Wallet { // 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 + .indexed_graph + .index .index_of_spk(&utxo.txout.script_pubkey) .ok_or(Error::UnknownUtxo)?; @@ -1608,7 +1690,7 @@ impl Wallet { .map_err(MiniscriptPsbtError::Conversion)?; let prev_output = utxo.outpoint; - if let Some(prev_tx) = self.keychain_tracker.graph().get_tx(prev_output.txid) { + if let Some(prev_tx) = self.indexed_graph.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()); } @@ -1641,10 +1723,8 @@ 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 - .keychain_tracker - .txout_index - .index_of_spk(&out.script_pubkey) + if let Some(&(keychain, child)) = + self.indexed_graph.index.index_of_spk(&out.script_pubkey) { debug!( "Found descriptor for input #{} {:?}/{}", @@ -1685,52 +1765,62 @@ 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 where - D: persist::PersistBackend, + D: PersistBackend, { - let changeset = self.keychain_tracker.apply_update(update)?; - self.persist.stage(changeset); - Ok(()) + let mut changeset: ChangeSet = self.chain.apply_update(update.chain)?.into(); + let (_, derivation_additions) = self + .indexed_graph + .index + .reveal_to_target_multi(&update.keychain); + changeset.append(derivation_additions.into()); + changeset.append(self.indexed_graph.apply_update(update.graph).into()); + + let changed = !changeset.is_empty(); + if changed { + self.persist.stage(changeset); + } + Ok(changed) } /// Commits all curently [`staged`] changed to the persistence backend returning and error when this fails. /// /// [`staged`]: Self::staged - pub fn commit(&mut self) -> Result<(), D::WriteError> + pub fn commit(&mut self) -> Result where - D: persist::PersistBackend, + D: PersistBackend, { - self.persist.commit() + self.persist.commit().map(|c| c.is_some()) } /// 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 + where + D: PersistBackend, + { self.persist.staged() } /// Get a reference to the inner [`TxGraph`](bdk_chain::tx_graph::TxGraph). - pub fn as_graph(&self) -> &bdk_chain::tx_graph::TxGraph { - self.keychain_tracker.graph() + pub fn as_graph(&self) -> &TxGraph { + self.indexed_graph.graph() } - /// Get a reference to the inner [`ChainGraph`](bdk_chain::chain_graph::ChainGraph). - pub fn as_chain_graph(&self) -> &bdk_chain::chain_graph::ChainGraph { - self.keychain_tracker.chain_graph() + pub fn as_index(&self) -> &KeychainTxOutIndex { + &self.indexed_graph.index + } + + pub fn as_chain(&self) -> &LocalChain { + &self.chain } } -impl AsRef for Wallet { - fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { - self.keychain_tracker.graph() - } -} - -impl AsRef> for Wallet { - fn as_ref(&self) -> &bdk_chain::chain_graph::ChainGraph { - self.keychain_tracker.chain_graph() +impl AsRef> for Wallet { + fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { + self.indexed_graph.graph() } } @@ -1765,6 +1855,76 @@ where Ok(wallet_name) } +fn new_local_utxo( + keychain: KeychainKind, + derivation_index: u32, + full_txo: FullTxOut>, +) -> LocalUtxo { + LocalUtxo { + outpoint: full_txo.outpoint, + txout: full_txo.txout, + is_spent: full_txo.spent_by.is_some(), + confirmation_time: full_txo.chain_position.into(), + keychain, + derivation_index, + } +} + +fn new_tx_details( + indexed_graph: &IndexedTxGraph>, + canonical_tx: CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>, + include_raw: bool, +) -> TransactionDetails { + let graph = indexed_graph.graph(); + let index = &indexed_graph.index; + let tx = canonical_tx.node.tx; + + let received = tx + .output + .iter() + .map(|txout| { + if 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)) = 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)); + + TransactionDetails { + transaction: if include_raw { Some(tx.clone()) } else { None }, + txid: canonical_tx.node.txid, + received, + sent, + fee, + confirmation_time: canonical_tx.observed_as.cloned().into(), + } +} + #[macro_export] #[doc(hidden)] /// Macro for getting a wallet for use in a doctest @@ -1796,7 +1956,7 @@ macro_rules! doctest_wallet { let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed { height: 500, time: 50_000 - }); + }, None); wallet }} diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index 6e812e59..5d106260 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -39,7 +39,7 @@ use crate::collections::BTreeMap; use crate::collections::HashSet; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; -use bdk_chain::ConfirmationTime; +use bdk_chain::PersistBackend; use core::cell::RefCell; use core::marker::PhantomData; @@ -47,7 +47,7 @@ use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt}; use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; -use super::persist; +use super::ChangeSet; use crate::{ types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}, TransactionDetails, @@ -529,7 +529,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> where - D: persist::PersistBackend, + D: PersistBackend, { self.wallet .borrow_mut() diff --git a/crates/bdk/tests/common.rs b/crates/bdk/tests/common.rs index de946703..cbf74f24 100644 --- a/crates/bdk/tests/common.rs +++ b/crates/bdk/tests/common.rs @@ -35,6 +35,7 @@ pub fn get_funded_wallet_with_change( height: 1_000, time: 100, }, + None, ) .unwrap(); diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 94c5ad1e..6291df1d 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -44,6 +44,7 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: TxHeight) -> OutPoint }, TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed, }, + None, ) .unwrap(); @@ -811,7 +812,7 @@ fn test_create_tx_add_utxo() { lock_time: PackedLockTime(0), }; wallet - .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) + .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed, None) .unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); @@ -848,7 +849,7 @@ fn test_create_tx_manually_selected_insufficient() { }; wallet - .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) + .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed, None) .unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); @@ -889,7 +890,9 @@ fn test_create_tx_policy_path_no_csv() { script_pubkey: wallet.get_address(New).script_pubkey(), }], }; - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); let root_id = external_policy.id; @@ -972,7 +975,7 @@ 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().next().expect("must take!"); let foreign_utxo_satisfaction = wallet2 .get_descriptor_for_keychain(KeychainKind::External) .max_satisfaction_weight() @@ -1036,7 +1039,7 @@ 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().next().expect("must exist").outpoint; let foreign_utxo_satisfaction = wallet .get_descriptor_for_keychain(KeychainKind::External) .max_satisfaction_weight() @@ -1054,7 +1057,7 @@ 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().next().unwrap(); let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap(); let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); @@ -1098,7 +1101,7 @@ 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().next().unwrap(); let satisfaction_weight = wallet2 .get_descriptor_for_keychain(KeychainKind::External) @@ -1214,7 +1217,9 @@ fn test_bump_fee_irreplaceable_tx() { let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); wallet.build_fee_bump(txid).unwrap().finish().unwrap(); } @@ -1237,6 +1242,7 @@ fn test_bump_fee_confirmed_tx() { height: 42, time: 42_000, }, + None, ) .unwrap(); @@ -1257,7 +1263,9 @@ fn test_bump_fee_low_fee_rate() { let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_rate(FeeRate::from_sat_per_vb(1.0)); @@ -1278,7 +1286,9 @@ fn test_bump_fee_low_abs() { let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(10); @@ -1298,7 +1308,9 @@ fn test_bump_fee_zero_abs() { let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(0); @@ -1316,7 +1328,9 @@ fn test_bump_fee_reduce_change() { let (psbt, original_details) = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf(); @@ -1401,7 +1415,9 @@ fn test_bump_fee_reduce_single_recipient() { let (psbt, original_details) = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder @@ -1432,7 +1448,9 @@ fn test_bump_fee_absolute_reduce_single_recipient() { let (psbt, original_details) = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder @@ -1471,6 +1489,7 @@ fn test_bump_fee_drain_wallet() { height: wallet.latest_checkpoint().unwrap().height, time: 42_000, }, + None, ) .unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); @@ -1488,7 +1507,9 @@ fn test_bump_fee_drain_wallet() { let (psbt, original_details) = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); assert_eq!(original_details.sent, 25_000); // for the new feerate, it should be enough to reduce the output, but since we specify @@ -1523,7 +1544,17 @@ fn test_bump_fee_remove_output_manually_selected_only() { }], }; wallet - .insert_tx(init_tx.clone(), wallet.transactions().last().unwrap().0) + .insert_tx( + init_tx.clone(), + wallet + .transactions() + .last() + .unwrap() + .observed_as + .cloned() + .into(), + None, + ) .unwrap(); let outpoint = OutPoint { txid: init_tx.txid(), @@ -1540,7 +1571,9 @@ fn test_bump_fee_remove_output_manually_selected_only() { let (psbt, original_details) = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); assert_eq!(original_details.sent, 25_000); let mut builder = wallet.build_fee_bump(txid).unwrap(); @@ -1562,9 +1595,14 @@ fn test_bump_fee_add_input() { value: 25_000, }], }; - wallet - .insert_tx(init_tx, wallet.transactions().last().unwrap().0) - .unwrap(); + let pos = wallet + .transactions() + .last() + .unwrap() + .observed_as + .cloned() + .into(); + wallet.insert_tx(init_tx, pos, None).unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); @@ -1574,7 +1612,9 @@ fn test_bump_fee_add_input() { let (psbt, original_details) = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); @@ -1618,7 +1658,9 @@ fn test_bump_fee_absolute_add_input() { let (psbt, original_details) = builder.finish().unwrap(); let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(6_000); @@ -1668,7 +1710,9 @@ fn test_bump_fee_no_change_add_input_and_change() { let tx = psbt.extract_tx(); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); // now bump the fees without using `allow_shrinking`. the wallet should add an // extra input and a change output, and leave the original output untouched @@ -1724,7 +1768,9 @@ fn test_bump_fee_add_input_change_dust() { assert_eq!(tx.input.len(), 1); assert_eq!(tx.output.len(), 2); let txid = tx.txid(); - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); // We set a fee high enough that during rbf we are forced to add @@ -1784,7 +1830,7 @@ fn test_bump_fee_force_add_input() { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) + .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed, None) .unwrap(); // the new fee_rate is low enough that just reducing the change would be fine, but we force // the addition of an extra input with `add_utxo()` @@ -1839,7 +1885,7 @@ fn test_bump_fee_absolute_force_add_input() { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } wallet - .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) + .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed, None) .unwrap(); // the new fee_rate is low enough that just reducing the change would be fine, but we force @@ -1899,7 +1945,9 @@ fn test_bump_fee_unconfirmed_inputs_only() { for txin in &mut tx.input { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_rate(FeeRate::from_sat_per_vb(25.0)); builder.finish().unwrap(); @@ -1928,7 +1976,9 @@ fn test_bump_fee_unconfirmed_input() { for txin in &mut tx.input { txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature } - wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); + wallet + .insert_tx(tx, ConfirmationTime::Unconfirmed, None) + .unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder @@ -2660,7 +2710,7 @@ 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().next().unwrap(); let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); let foreign_utxo_satisfaction = wallet2 .get_descriptor_for_keychain(KeychainKind::External) @@ -3022,6 +3072,7 @@ fn test_spend_coinbase() { height: confirmation_height, time: 30_000, }, + None, ) .unwrap(); diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 3252febc..9baa643e 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -137,6 +137,18 @@ impl ConfirmationTime { } } +impl From> for ConfirmationTime { + fn from(observed_as: ObservedAs) -> Self { + match observed_as { + ObservedAs::Confirmed(a) => Self::Confirmed { + height: a.confirmation_height, + time: a.confirmation_time, + }, + ObservedAs::Unconfirmed(_) => Self::Unconfirmed, + } + } +} + /// A reference to a block in the canonical chain. #[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] #[cfg_attr( diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 5145d593..7eb88264 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -1,104 +1,105 @@ -use std::{io::Write, str::FromStr}; +// use std::{io::Write, str::FromStr}; -use bdk::{ - bitcoin::{Address, Network}, - SignOptions, Wallet, -}; -use bdk_electrum::{ - electrum_client::{self, ElectrumApi}, - ElectrumExt, -}; -use bdk_file_store::KeychainStore; +// use bdk::{ +// bitcoin::{Address, Network}, +// SignOptions, Wallet, +// }; +// use bdk_electrum::{ +// electrum_client::{self, ElectrumApi}, +// ElectrumExt, +// }; +// use bdk_file_store::KeychainStore; -const SEND_AMOUNT: u64 = 5000; -const STOP_GAP: usize = 50; -const BATCH_SIZE: usize = 5; +// const SEND_AMOUNT: u64 = 5000; +// const STOP_GAP: usize = 50; +// const BATCH_SIZE: usize = 5; fn main() -> Result<(), Box> { - println!("Hello, world!"); + todo!("update this example!"); + // println!("Hello, world!"); - let db_path = std::env::temp_dir().join("bdk-electrum-example"); - let db = KeychainStore::new_from_path(db_path)?; - let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; - let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; + // let db_path = std::env::temp_dir().join("bdk-electrum-example"); + // let db = KeychainStore::new_from_path(db_path)?; + // let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; + // let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; - let mut wallet = Wallet::new( - external_descriptor, - Some(internal_descriptor), - db, - Network::Testnet, - )?; + // let mut wallet = Wallet::new( + // external_descriptor, + // Some(internal_descriptor), + // db, + // Network::Testnet, + // )?; - let address = wallet.get_address(bdk::wallet::AddressIndex::New); - println!("Generated Address: {}", address); + // let address = wallet.get_address(bdk::wallet::AddressIndex::New); + // println!("Generated Address: {}", address); - let balance = wallet.get_balance(); - println!("Wallet balance before syncing: {} sats", balance.total()); + // let balance = wallet.get_balance(); + // println!("Wallet balance before syncing: {} sats", balance.total()); - print!("Syncing..."); - // Scanning the chain... - let electrum_url = "ssl://electrum.blockstream.info:60002"; - let client = electrum_client::Client::new(electrum_url)?; - let local_chain = wallet.checkpoints(); - let spks = wallet - .spks_of_all_keychains() - .into_iter() - .map(|(k, spks)| { - let mut first = true; - ( - k, - spks.inspect(move |(spk_i, _)| { - if first { - first = false; - print!("\nScanning keychain [{:?}]:", k); - } - print!(" {}", spk_i); - let _ = std::io::stdout().flush(); - }), - ) - }) - .collect(); - let electrum_update = client - .scan( - local_chain, - spks, - core::iter::empty(), - core::iter::empty(), - STOP_GAP, - BATCH_SIZE, - )? - .into_confirmation_time_update(&client)?; - println!(); - let new_txs = client.batch_transaction_get(electrum_update.missing_full_txs(&wallet))?; - let update = electrum_update.into_keychain_scan(new_txs, &wallet)?; - wallet.apply_update(update)?; - wallet.commit()?; + // print!("Syncing..."); + // // Scanning the chain... + // let electrum_url = "ssl://electrum.blockstream.info:60002"; + // let client = electrum_client::Client::new(electrum_url)?; + // let local_chain = wallet.checkpoints(); + // let spks = wallet + // .spks_of_all_keychains() + // .into_iter() + // .map(|(k, spks)| { + // let mut first = true; + // ( + // k, + // spks.inspect(move |(spk_i, _)| { + // if first { + // first = false; + // print!("\nScanning keychain [{:?}]:", k); + // } + // print!(" {}", spk_i); + // let _ = std::io::stdout().flush(); + // }), + // ) + // }) + // .collect(); + // let electrum_update = client + // .scan( + // local_chain, + // spks, + // core::iter::empty(), + // core::iter::empty(), + // STOP_GAP, + // BATCH_SIZE, + // )? + // .into_confirmation_time_update(&client)?; + // println!(); + // let new_txs = client.batch_transaction_get(electrum_update.missing_full_txs(&wallet))?; + // let update = electrum_update.into_keychain_scan(new_txs, &wallet)?; + // wallet.apply_update(update)?; + // wallet.commit()?; - let balance = wallet.get_balance(); - println!("Wallet balance after syncing: {} sats", balance.total()); + // let balance = wallet.get_balance(); + // println!("Wallet balance after syncing: {} sats", balance.total()); - if balance.total() < SEND_AMOUNT { - println!( - "Please send at least {} sats to the receiving address", - SEND_AMOUNT - ); - std::process::exit(0); - } + // if balance.total() < SEND_AMOUNT { + // println!( + // "Please send at least {} sats to the receiving address", + // SEND_AMOUNT + // ); + // std::process::exit(0); + // } - let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; + // let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; - let mut tx_builder = wallet.build_tx(); - tx_builder - .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) - .enable_rbf(); + // let mut tx_builder = wallet.build_tx(); + // tx_builder + // .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) + // .enable_rbf(); - let (mut psbt, _) = tx_builder.finish()?; - let finalized = wallet.sign(&mut psbt, SignOptions::default())?; - assert!(finalized); + // let (mut psbt, _) = tx_builder.finish()?; + // let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + // assert!(finalized); - let tx = psbt.extract_tx(); - client.transaction_broadcast(&tx)?; - println!("Tx broadcasted! Txid: {}", tx.txid()); + // let tx = psbt.extract_tx(); + // client.transaction_broadcast(&tx)?; + // println!("Tx broadcasted! Txid: {}", tx.txid()); - Ok(()) + // Ok(()) } diff --git a/example-crates/wallet_esplora/src/main.rs b/example-crates/wallet_esplora/src/main.rs index d8eda32a..d9d07c7a 100644 --- a/example-crates/wallet_esplora/src/main.rs +++ b/example-crates/wallet_esplora/src/main.rs @@ -1,96 +1,97 @@ -use bdk::{ - bitcoin::{Address, Network}, - wallet::AddressIndex, - SignOptions, Wallet, -}; -use bdk_esplora::esplora_client; -use bdk_esplora::EsploraExt; -use bdk_file_store::KeychainStore; -use std::{io::Write, str::FromStr}; +// use bdk::{ +// bitcoin::{Address, Network}, +// wallet::AddressIndex, +// SignOptions, Wallet, +// }; +// use bdk_esplora::esplora_client; +// use bdk_esplora::EsploraExt; +// use bdk_file_store::KeychainStore; +// use std::{io::Write, str::FromStr}; -const SEND_AMOUNT: u64 = 5000; -const STOP_GAP: usize = 50; -const PARALLEL_REQUESTS: usize = 5; +// const SEND_AMOUNT: u64 = 5000; +// const STOP_GAP: usize = 50; +// const PARALLEL_REQUESTS: usize = 5; fn main() -> Result<(), Box> { - let db_path = std::env::temp_dir().join("bdk-esplora-example"); - let db = KeychainStore::new_from_path(db_path)?; - let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; - let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; + todo!("update this exampe!"); + // let db_path = std::env::temp_dir().join("bdk-esplora-example"); + // let db = KeychainStore::new_from_path(db_path)?; + // let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; + // let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; - let mut wallet = Wallet::new( - external_descriptor, - Some(internal_descriptor), - db, - Network::Testnet, - )?; + // let mut wallet = Wallet::new( + // external_descriptor, + // Some(internal_descriptor), + // db, + // Network::Testnet, + // )?; - let address = wallet.get_address(AddressIndex::New); - println!("Generated Address: {}", address); + // let address = wallet.get_address(AddressIndex::New); + // println!("Generated Address: {}", address); - let balance = wallet.get_balance(); - println!("Wallet balance before syncing: {} sats", balance.total()); + // let balance = wallet.get_balance(); + // println!("Wallet balance before syncing: {} sats", balance.total()); - print!("Syncing..."); - // Scanning the chain... - let esplora_url = "https://mempool.space/testnet/api"; - let client = esplora_client::Builder::new(esplora_url).build_blocking()?; - let checkpoints = wallet.checkpoints(); - let spks = wallet - .spks_of_all_keychains() - .into_iter() - .map(|(k, spks)| { - let mut first = true; - ( - k, - spks.inspect(move |(spk_i, _)| { - if first { - first = false; - print!("\nScanning keychain [{:?}]:", k); - } - print!(" {}", spk_i); - let _ = std::io::stdout().flush(); - }), - ) - }) - .collect(); - let update = client.scan( - checkpoints, - spks, - core::iter::empty(), - core::iter::empty(), - STOP_GAP, - PARALLEL_REQUESTS, - )?; - println!(); - wallet.apply_update(update)?; - wallet.commit()?; + // print!("Syncing..."); + // // Scanning the chain... + // let esplora_url = "https://mempool.space/testnet/api"; + // let client = esplora_client::Builder::new(esplora_url).build_blocking()?; + // let checkpoints = wallet.checkpoints(); + // let spks = wallet + // .spks_of_all_keychains() + // .into_iter() + // .map(|(k, spks)| { + // let mut first = true; + // ( + // k, + // spks.inspect(move |(spk_i, _)| { + // if first { + // first = false; + // print!("\nScanning keychain [{:?}]:", k); + // } + // print!(" {}", spk_i); + // let _ = std::io::stdout().flush(); + // }), + // ) + // }) + // .collect(); + // let update = client.scan( + // checkpoints, + // spks, + // core::iter::empty(), + // core::iter::empty(), + // STOP_GAP, + // PARALLEL_REQUESTS, + // )?; + // println!(); + // wallet.apply_update(update)?; + // wallet.commit()?; - let balance = wallet.get_balance(); - println!("Wallet balance after syncing: {} sats", balance.total()); + // let balance = wallet.get_balance(); + // println!("Wallet balance after syncing: {} sats", balance.total()); - if balance.total() < SEND_AMOUNT { - println!( - "Please send at least {} sats to the receiving address", - SEND_AMOUNT - ); - std::process::exit(0); - } + // if balance.total() < SEND_AMOUNT { + // println!( + // "Please send at least {} sats to the receiving address", + // SEND_AMOUNT + // ); + // std::process::exit(0); + // } - let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; + // let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; - let mut tx_builder = wallet.build_tx(); - tx_builder - .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) - .enable_rbf(); + // let mut tx_builder = wallet.build_tx(); + // tx_builder + // .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) + // .enable_rbf(); - let (mut psbt, _) = tx_builder.finish()?; - let finalized = wallet.sign(&mut psbt, SignOptions::default())?; - assert!(finalized); + // let (mut psbt, _) = tx_builder.finish()?; + // let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + // assert!(finalized); - let tx = psbt.extract_tx(); - client.broadcast(&tx)?; - println!("Tx broadcasted! Txid: {}", tx.txid()); + // let tx = psbt.extract_tx(); + // client.broadcast(&tx)?; + // println!("Tx broadcasted! Txid: {}", tx.txid()); - Ok(()) + // Ok(()) } diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index b78b09df..66cdf689 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -1,99 +1,100 @@ -use std::{io::Write, str::FromStr}; +// use std::{io::Write, str::FromStr}; -use bdk::{ - bitcoin::{Address, Network}, - wallet::AddressIndex, - SignOptions, Wallet, -}; -use bdk_esplora::{esplora_client, EsploraAsyncExt}; -use bdk_file_store::KeychainStore; +// use bdk::{ +// bitcoin::{Address, Network}, +// wallet::AddressIndex, +// SignOptions, Wallet, +// }; +// use bdk_esplora::{esplora_client, EsploraAsyncExt}; +// use bdk_file_store::KeychainStore; -const SEND_AMOUNT: u64 = 5000; -const STOP_GAP: usize = 50; -const PARALLEL_REQUESTS: usize = 5; +// const SEND_AMOUNT: u64 = 5000; +// const STOP_GAP: usize = 50; +// const PARALLEL_REQUESTS: usize = 5; #[tokio::main] async fn main() -> Result<(), Box> { - let db_path = std::env::temp_dir().join("bdk-esplora-example"); - let db = KeychainStore::new_from_path(db_path)?; - let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; - let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; + todo!("update this example!"); + // let db_path = std::env::temp_dir().join("bdk-esplora-example"); + // let db = KeychainStore::new_from_path(db_path)?; + // let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; + // let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; - let mut wallet = Wallet::new( - external_descriptor, - Some(internal_descriptor), - db, - Network::Testnet, - )?; + // let mut wallet = Wallet::new( + // external_descriptor, + // Some(internal_descriptor), + // db, + // Network::Testnet, + // )?; - let address = wallet.get_address(AddressIndex::New); - println!("Generated Address: {}", address); + // let address = wallet.get_address(AddressIndex::New); + // println!("Generated Address: {}", address); - let balance = wallet.get_balance(); - println!("Wallet balance before syncing: {} sats", balance.total()); + // let balance = wallet.get_balance(); + // println!("Wallet balance before syncing: {} sats", balance.total()); - print!("Syncing..."); - // Scanning the blockchain - let esplora_url = "https://mempool.space/testnet/api"; - let client = esplora_client::Builder::new(esplora_url).build_async()?; - let checkpoints = wallet.checkpoints(); - let spks = wallet - .spks_of_all_keychains() - .into_iter() - .map(|(k, spks)| { - let mut first = true; - ( - k, - spks.inspect(move |(spk_i, _)| { - if first { - first = false; - print!("\nScanning keychain [{:?}]:", k); - } - print!(" {}", spk_i); - let _ = std::io::stdout().flush(); - }), - ) - }) - .collect(); - let update = client - .scan( - checkpoints, - spks, - std::iter::empty(), - std::iter::empty(), - STOP_GAP, - PARALLEL_REQUESTS, - ) - .await?; - println!(); - wallet.apply_update(update)?; - wallet.commit()?; + // print!("Syncing..."); + // // Scanning the blockchain + // let esplora_url = "https://mempool.space/testnet/api"; + // let client = esplora_client::Builder::new(esplora_url).build_async()?; + // let checkpoints = wallet.checkpoints(); + // let spks = wallet + // .spks_of_all_keychains() + // .into_iter() + // .map(|(k, spks)| { + // let mut first = true; + // ( + // k, + // spks.inspect(move |(spk_i, _)| { + // if first { + // first = false; + // print!("\nScanning keychain [{:?}]:", k); + // } + // print!(" {}", spk_i); + // let _ = std::io::stdout().flush(); + // }), + // ) + // }) + // .collect(); + // let update = client + // .scan( + // checkpoints, + // spks, + // std::iter::empty(), + // std::iter::empty(), + // STOP_GAP, + // PARALLEL_REQUESTS, + // ) + // .await?; + // println!(); + // wallet.apply_update(update)?; + // wallet.commit()?; - let balance = wallet.get_balance(); - println!("Wallet balance after syncing: {} sats", balance.total()); + // let balance = wallet.get_balance(); + // println!("Wallet balance after syncing: {} sats", balance.total()); - if balance.total() < SEND_AMOUNT { - println!( - "Please send at least {} sats to the receiving address", - SEND_AMOUNT - ); - std::process::exit(0); - } + // if balance.total() < SEND_AMOUNT { + // println!( + // "Please send at least {} sats to the receiving address", + // SEND_AMOUNT + // ); + // std::process::exit(0); + // } - let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; + // let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; - let mut tx_builder = wallet.build_tx(); - tx_builder - .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) - .enable_rbf(); + // let mut tx_builder = wallet.build_tx(); + // tx_builder + // .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) + // .enable_rbf(); - let (mut psbt, _) = tx_builder.finish()?; - let finalized = wallet.sign(&mut psbt, SignOptions::default())?; - assert!(finalized); + // let (mut psbt, _) = tx_builder.finish()?; + // let finalized = wallet.sign(&mut psbt, SignOptions::default())?; + // assert!(finalized); - let tx = psbt.extract_tx(); - client.broadcast(&tx).await?; - println!("Tx broadcasted! Txid: {}", tx.txid()); + // let tx = psbt.extract_tx(); + // client.broadcast(&tx).await?; + // println!("Tx broadcasted! Txid: {}", tx.txid()); - Ok(()) + // Ok(()) }