From: 志宇 Date: Fri, 23 Aug 2024 18:12:46 +0000 (+0000) Subject: feat!: move `spk_client`s to `bdk_core` X-Git-Tag: v1.0.0-beta.2~2^2~4 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/struct.CodeLengthError.html?a=commitdiff_plain;h=ab0315d14fa741a691ee0deef4567ea66cb44a60;p=bdk feat!: move `spk_client`s to `bdk_core` Also introduced extension trait for builder methods that take in a `KeychainTxOutIndex`. `Indexed` and `KeychainIndexed` are also moved to `bdk_core`. --- diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index ce9707c7..c4320809 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -4,6 +4,7 @@ use crate::{ collections::*, miniscript::{Descriptor, DescriptorPublicKey}, + spk_client::{FullScanRequestBuilder, SyncRequestBuilder}, spk_iter::BIP32_MAX_INDEX, spk_txout::SpkTxOutIndex, DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator, @@ -875,3 +876,43 @@ impl Merge for ChangeSet { self.last_revealed.is_empty() } } + +/// Trait to extend [`SyncRequestBuilder`]. +pub trait SyncRequestBuilderExt { + /// Add [`Script`](bitcoin::Script)s that are revealed by the `indexer` of the given `spk_range` + /// that will be synced against. + fn revealed_spks_from_indexer(self, indexer: &KeychainTxOutIndex, spk_range: R) -> Self + where + R: core::ops::RangeBounds; + + /// Add [`Script`](bitcoin::Script)s that are revealed by the `indexer` but currently unused. + fn unused_spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self; +} + +impl SyncRequestBuilderExt for SyncRequestBuilder<(K, u32)> { + fn revealed_spks_from_indexer(self, indexer: &KeychainTxOutIndex, spk_range: R) -> Self + where + R: core::ops::RangeBounds, + { + self.spks_with_indexes(indexer.revealed_spks(spk_range)) + } + + fn unused_spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self { + self.spks_with_indexes(indexer.unused_spks()) + } +} + +/// Trait to extend [`FullScanRequestBuilder`]. +pub trait FullScanRequestBuilderExt { + /// Add spk iterators for each keychain tracked in `indexer`. + fn spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self; +} + +impl FullScanRequestBuilderExt for FullScanRequestBuilder { + fn spks_from_indexer(mut self, indexer: &KeychainTxOutIndex) -> Self { + for (keychain, spks) in indexer.all_unbounded_spk_iters() { + self = self.spks_for_keychain(keychain, spks); + } + self + } +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index c1c55596..9667bb54 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -61,7 +61,6 @@ pub use indexer::keychain_txout; pub use spk_iter::*; #[cfg(feature = "rusqlite")] pub mod rusqlite_impl; -pub mod spk_client; pub extern crate bdk_core; pub use bdk_core::*; @@ -81,11 +80,6 @@ extern crate std; /// How many confirmations are needed f or a coinbase output to be spent. pub const COINBASE_MATURITY: u32 = 100; -/// A tuple of keychain index and `T` representing the indexed value. -pub type Indexed = (u32, T); -/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them. -pub type KeychainIndexed = ((K, u32), T); - /// A wrapper that we use to impl remote traits for types in our crate or dependency crates. pub struct Impl(pub T); diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs deleted file mode 100644 index e31b431d..00000000 --- a/crates/chain/src/spk_client.rs +++ /dev/null @@ -1,586 +0,0 @@ -//! Helper types for spk-based blockchain clients. -use crate::{ - alloc::{boxed::Box, collections::VecDeque, vec::Vec}, - collections::BTreeMap, - local_chain::CheckPoint, - ConfirmationBlockTime, Indexed, -}; -use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; - -type InspectSync = dyn FnMut(SyncItem, SyncProgress) + Send + 'static; - -type InspectFullScan = dyn FnMut(K, u32, &Script) + Send + 'static; - -/// An item reported to the [`inspect`](SyncRequestBuilder::inspect) closure of [`SyncRequest`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum SyncItem<'i, I> { - /// Script pubkey sync item. - Spk(I, &'i Script), - /// Txid sync item. - Txid(Txid), - /// Outpoint sync item. - OutPoint(OutPoint), -} - -impl<'i, I: core::fmt::Debug + core::any::Any> core::fmt::Display for SyncItem<'i, I> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - SyncItem::Spk(i, spk) => { - if (i as &dyn core::any::Any).is::<()>() { - write!(f, "script '{}'", spk) - } else { - write!(f, "script {:?} '{}'", i, spk) - } - } - SyncItem::Txid(txid) => write!(f, "txid '{}'", txid), - SyncItem::OutPoint(op) => write!(f, "outpoint '{}'", op), - } - } -} - -/// The progress of [`SyncRequest`]. -#[derive(Debug, Clone)] -pub struct SyncProgress { - /// Script pubkeys consumed by the request. - pub spks_consumed: usize, - /// Script pubkeys remaining in the request. - pub spks_remaining: usize, - /// Txids consumed by the request. - pub txids_consumed: usize, - /// Txids remaining in the request. - pub txids_remaining: usize, - /// Outpoints consumed by the request. - pub outpoints_consumed: usize, - /// Outpoints remaining in the request. - pub outpoints_remaining: usize, -} - -impl SyncProgress { - /// Total items, consumed and remaining, of the request. - pub fn total(&self) -> usize { - self.total_spks() + self.total_txids() + self.total_outpoints() - } - - /// Total script pubkeys, consumed and remaining, of the request. - pub fn total_spks(&self) -> usize { - self.spks_consumed + self.spks_remaining - } - - /// Total txids, consumed and remaining, of the request. - pub fn total_txids(&self) -> usize { - self.txids_consumed + self.txids_remaining - } - - /// Total outpoints, consumed and remaining, of the request. - pub fn total_outpoints(&self) -> usize { - self.outpoints_consumed + self.outpoints_remaining - } - - /// Total consumed items of the request. - pub fn consumed(&self) -> usize { - self.spks_consumed + self.txids_consumed + self.outpoints_consumed - } - - /// Total remaining items of the request. - pub fn remaining(&self) -> usize { - self.spks_remaining + self.txids_remaining + self.outpoints_remaining - } -} - -/// Builds a [`SyncRequest`]. -#[must_use] -pub struct SyncRequestBuilder { - inner: SyncRequest, -} - -impl Default for SyncRequestBuilder { - fn default() -> Self { - Self { - inner: Default::default(), - } - } -} - -#[cfg(feature = "miniscript")] -impl SyncRequestBuilder<(K, u32)> { - /// Add [`Script`]s that are revealed by the `indexer` of the given `spk_range` that will be - /// synced against. - pub fn revealed_spks_from_indexer( - self, - indexer: &crate::indexer::keychain_txout::KeychainTxOutIndex, - spk_range: impl core::ops::RangeBounds, - ) -> Self { - self.spks_with_indexes(indexer.revealed_spks(spk_range)) - } - - /// Add [`Script`]s that are revealed by the `indexer` but currently unused. - pub fn unused_spks_from_indexer( - self, - indexer: &crate::indexer::keychain_txout::KeychainTxOutIndex, - ) -> Self { - self.spks_with_indexes(indexer.unused_spks()) - } -} - -impl SyncRequestBuilder<()> { - /// Add [`Script`]s that will be synced against. - pub fn spks(self, spks: impl IntoIterator) -> Self { - self.spks_with_indexes(spks.into_iter().map(|spk| ((), spk))) - } -} - -impl SyncRequestBuilder { - /// Set the initial chain tip for the sync request. - /// - /// This is used to update [`LocalChain`](crate::local_chain::LocalChain). - pub fn chain_tip(mut self, cp: CheckPoint) -> Self { - self.inner.chain_tip = Some(cp); - self - } - - /// Add [`Script`]s coupled with associated indexes that will be synced against. - /// - /// # Example - /// - /// Sync revealed script pubkeys obtained from a - /// [`KeychainTxOutIndex`](crate::keychain_txout::KeychainTxOutIndex). - /// - /// ```rust - /// # use bdk_chain::spk_client::SyncRequest; - /// # use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex; - /// # use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey}; - /// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only(); - /// # let (descriptor_a,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap(); - /// # let (descriptor_b,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap(); - /// let mut indexer = KeychainTxOutIndex::<&'static str>::default(); - /// indexer.insert_descriptor("descriptor_a", descriptor_a)?; - /// indexer.insert_descriptor("descriptor_b", descriptor_b)?; - /// - /// /* Assume that the caller does more mutations to the `indexer` here... */ - /// - /// // Reveal spks for "descriptor_a", then build a sync request. Each spk will be indexed with - /// // `u32`, which represents the derivation index of the associated spk from "descriptor_a". - /// let (newly_revealed_spks, _changeset) = indexer - /// .reveal_to_target("descriptor_a", 21) - /// .expect("keychain must exist"); - /// let _request = SyncRequest::builder() - /// .spks_with_indexes(newly_revealed_spks) - /// .build(); - /// - /// // Sync all revealed spks in the indexer. This time, spks may be derived from different - /// // keychains. Each spk will be indexed with `(&'static str, u32)` where `&'static str` is - /// // the keychain identifier and `u32` is the derivation index. - /// let all_revealed_spks = indexer.revealed_spks(..); - /// let _request = SyncRequest::builder() - /// .spks_with_indexes(all_revealed_spks) - /// .build(); - /// # Ok::<_, bdk_chain::keychain_txout::InsertDescriptorError<_>>(()) - /// ``` - pub fn spks_with_indexes(mut self, spks: impl IntoIterator) -> Self { - self.inner.spks.extend(spks); - self - } - - /// Add [`Txid`]s that will be synced against. - pub fn txids(mut self, txids: impl IntoIterator) -> Self { - self.inner.txids.extend(txids); - self - } - - /// Add [`OutPoint`]s that will be synced against. - pub fn outpoints(mut self, outpoints: impl IntoIterator) -> Self { - self.inner.outpoints.extend(outpoints); - self - } - - /// Set the closure that will inspect every sync item visited. - pub fn inspect(mut self, inspect: F) -> Self - where - F: FnMut(SyncItem, SyncProgress) + Send + 'static, - { - self.inner.inspect = Box::new(inspect); - self - } - - /// Build the [`SyncRequest`]. - pub fn build(self) -> SyncRequest { - self.inner - } -} - -/// Data required to perform a spk-based blockchain client sync. -/// -/// A client sync fetches relevant chain data for a known list of scripts, transaction ids and -/// outpoints. The sync process also updates the chain from the given -/// [`chain_tip`](SyncRequestBuilder::chain_tip) (if provided). -/// -/// ```rust -/// # use bdk_chain::{bitcoin::{hashes::Hash, ScriptBuf}, local_chain::LocalChain}; -/// # let (local_chain, _) = LocalChain::from_genesis_hash(Hash::all_zeros()); -/// # let scripts = [ScriptBuf::default(), ScriptBuf::default()]; -/// # use bdk_chain::spk_client::SyncRequest; -/// // Construct a sync request. -/// let sync_request = SyncRequest::builder() -/// // Provide chain tip of the local wallet. -/// .chain_tip(local_chain.tip()) -/// // Provide list of scripts to scan for transactions against. -/// .spks(scripts) -/// // This is called for every synced item. -/// .inspect(|item, progress| println!("{} (remaining: {})", item, progress.remaining())) -/// // Finish constructing the sync request. -/// .build(); -/// ``` -#[must_use] -pub struct SyncRequest { - chain_tip: Option, - spks: VecDeque<(I, ScriptBuf)>, - spks_consumed: usize, - txids: VecDeque, - txids_consumed: usize, - outpoints: VecDeque, - outpoints_consumed: usize, - inspect: Box>, -} - -impl Default for SyncRequest { - fn default() -> Self { - Self { - chain_tip: None, - spks: VecDeque::new(), - spks_consumed: 0, - txids: VecDeque::new(), - txids_consumed: 0, - outpoints: VecDeque::new(), - outpoints_consumed: 0, - inspect: Box::new(|_, _| {}), - } - } -} - -impl From> for SyncRequest { - fn from(builder: SyncRequestBuilder) -> Self { - builder.inner - } -} - -impl SyncRequest { - /// Start building a [`SyncRequest`]. - pub fn builder() -> SyncRequestBuilder { - SyncRequestBuilder { - inner: Default::default(), - } - } - - /// Get the [`SyncProgress`] of this request. - pub fn progress(&self) -> SyncProgress { - SyncProgress { - spks_consumed: self.spks_consumed, - spks_remaining: self.spks.len(), - txids_consumed: self.txids_consumed, - txids_remaining: self.txids.len(), - outpoints_consumed: self.outpoints_consumed, - outpoints_remaining: self.outpoints.len(), - } - } - - /// Get the chain tip [`CheckPoint`] of this request (if any). - pub fn chain_tip(&self) -> Option { - self.chain_tip.clone() - } - - /// Advances the sync request and returns the next [`ScriptBuf`]. - /// - /// Returns [`None`] when there are no more scripts remaining in the request. - pub fn next_spk(&mut self) -> Option { - let (i, spk) = self.spks.pop_front()?; - self.spks_consumed += 1; - self._call_inspect(SyncItem::Spk(i, spk.as_script())); - Some(spk) - } - - /// Advances the sync request and returns the next [`Txid`]. - /// - /// Returns [`None`] when there are no more txids remaining in the request. - pub fn next_txid(&mut self) -> Option { - let txid = self.txids.pop_front()?; - self.txids_consumed += 1; - self._call_inspect(SyncItem::Txid(txid)); - Some(txid) - } - - /// Advances the sync request and returns the next [`OutPoint`]. - /// - /// Returns [`None`] when there are no more outpoints in the request. - pub fn next_outpoint(&mut self) -> Option { - let outpoint = self.outpoints.pop_front()?; - self.outpoints_consumed += 1; - self._call_inspect(SyncItem::OutPoint(outpoint)); - Some(outpoint) - } - - /// Iterate over [`ScriptBuf`]s contained in this request. - pub fn iter_spks(&mut self) -> impl ExactSizeIterator + '_ { - SyncIter::::new(self) - } - - /// Iterate over [`Txid`]s contained in this request. - pub fn iter_txids(&mut self) -> impl ExactSizeIterator + '_ { - SyncIter::::new(self) - } - - /// Iterate over [`OutPoint`]s contained in this request. - pub fn iter_outpoints(&mut self) -> impl ExactSizeIterator + '_ { - SyncIter::::new(self) - } - - fn _call_inspect(&mut self, item: SyncItem) { - let progress = self.progress(); - (*self.inspect)(item, progress); - } -} - -/// Data returned from a spk-based blockchain client sync. -/// -/// See also [`SyncRequest`]. -#[must_use] -#[derive(Debug)] -pub struct SyncResult { - /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). - pub graph_update: crate::tx_graph::Update, - /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). - pub chain_update: Option, -} - -impl Default for SyncResult { - fn default() -> Self { - Self { - graph_update: Default::default(), - chain_update: Default::default(), - } - } -} - -/// Builds a [`FullScanRequest`]. -#[must_use] -pub struct FullScanRequestBuilder { - inner: FullScanRequest, -} - -impl Default for FullScanRequestBuilder { - fn default() -> Self { - Self { - inner: Default::default(), - } - } -} - -#[cfg(feature = "miniscript")] -impl FullScanRequestBuilder { - /// Add spk iterators for each keychain tracked in `indexer`. - pub fn spks_from_indexer( - mut self, - indexer: &crate::indexer::keychain_txout::KeychainTxOutIndex, - ) -> Self { - for (keychain, spks) in indexer.all_unbounded_spk_iters() { - self = self.spks_for_keychain(keychain, spks); - } - self - } -} - -impl FullScanRequestBuilder { - /// Set the initial chain tip for the full scan request. - /// - /// This is used to update [`LocalChain`](crate::local_chain::LocalChain). - pub fn chain_tip(mut self, tip: CheckPoint) -> Self { - self.inner.chain_tip = Some(tip); - self - } - - /// Set the spk iterator for a given `keychain`. - pub fn spks_for_keychain( - mut self, - keychain: K, - spks: impl IntoIterator> + Send + 'static>, - ) -> Self { - self.inner - .spks_by_keychain - .insert(keychain, Box::new(spks.into_iter())); - self - } - - /// Set the closure that will inspect every sync item visited. - pub fn inspect(mut self, inspect: F) -> Self - where - F: FnMut(K, u32, &Script) + Send + 'static, - { - self.inner.inspect = Box::new(inspect); - self - } - - /// Build the [`FullScanRequest`]. - pub fn build(self) -> FullScanRequest { - self.inner - } -} - -/// Data required to perform a spk-based blockchain client full scan. -/// -/// A client full scan iterates through all the scripts for the given keychains, fetching relevant -/// data until some stop gap number of scripts is found that have no data. This operation is -/// generally only used when importing or restoring previously used keychains in which the list of -/// used scripts is not known. The full scan process also updates the chain from the given -/// [`chain_tip`](FullScanRequestBuilder::chain_tip) (if provided). -#[must_use] -pub struct FullScanRequest { - chain_tip: Option, - spks_by_keychain: BTreeMap> + Send>>, - inspect: Box>, -} - -impl From> for FullScanRequest { - fn from(builder: FullScanRequestBuilder) -> Self { - builder.inner - } -} - -impl Default for FullScanRequest { - fn default() -> Self { - Self { - chain_tip: None, - spks_by_keychain: Default::default(), - inspect: Box::new(|_, _, _| {}), - } - } -} - -impl FullScanRequest { - /// Start building a [`FullScanRequest`]. - pub fn builder() -> FullScanRequestBuilder { - FullScanRequestBuilder { - inner: Self::default(), - } - } - - /// Get the chain tip [`CheckPoint`] of this request (if any). - pub fn chain_tip(&self) -> Option { - self.chain_tip.clone() - } - - /// List all keychains contained in this request. - pub fn keychains(&self) -> Vec { - self.spks_by_keychain.keys().cloned().collect() - } - - /// Advances the full scan request and returns the next indexed [`ScriptBuf`] of the given - /// `keychain`. - pub fn next_spk(&mut self, keychain: K) -> Option> { - self.iter_spks(keychain).next() - } - - /// Iterate over indexed [`ScriptBuf`]s contained in this request of the given `keychain`. - pub fn iter_spks(&mut self, keychain: K) -> impl Iterator> + '_ { - let spks = self.spks_by_keychain.get_mut(&keychain); - let inspect = &mut self.inspect; - KeychainSpkIter { - keychain, - spks, - inspect, - } - } -} - -/// Data returned from a spk-based blockchain client full scan. -/// -/// See also [`FullScanRequest`]. -#[must_use] -#[derive(Debug)] -pub struct FullScanResult { - /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). - pub graph_update: crate::tx_graph::Update, - /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). - pub chain_update: Option, - /// Last active indices for the corresponding keychains (`K`). - pub last_active_indices: BTreeMap, -} - -impl Default for FullScanResult { - fn default() -> Self { - Self { - graph_update: Default::default(), - chain_update: Default::default(), - last_active_indices: Default::default(), - } - } -} - -struct KeychainSpkIter<'r, K> { - keychain: K, - spks: Option<&'r mut Box> + Send>>, - inspect: &'r mut Box>, -} - -impl<'r, K: Ord + Clone> Iterator for KeychainSpkIter<'r, K> { - type Item = Indexed; - - fn next(&mut self) -> Option { - let (i, spk) = self.spks.as_mut()?.next()?; - (*self.inspect)(self.keychain.clone(), i, &spk); - Some((i, spk)) - } -} - -struct SyncIter<'r, I, Item> { - request: &'r mut SyncRequest, - marker: core::marker::PhantomData, -} - -impl<'r, I, Item> SyncIter<'r, I, Item> { - fn new(request: &'r mut SyncRequest) -> Self { - Self { - request, - marker: core::marker::PhantomData, - } - } -} - -impl<'r, I, Item> ExactSizeIterator for SyncIter<'r, I, Item> where SyncIter<'r, I, Item>: Iterator {} - -impl<'r, I> Iterator for SyncIter<'r, I, ScriptBuf> { - type Item = ScriptBuf; - - fn next(&mut self) -> Option { - self.request.next_spk() - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = self.request.spks.len(); - (remaining, Some(remaining)) - } -} - -impl<'r, I> Iterator for SyncIter<'r, I, Txid> { - type Item = Txid; - - fn next(&mut self) -> Option { - self.request.next_txid() - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = self.request.txids.len(); - (remaining, Some(remaining)) - } -} - -impl<'r, I> Iterator for SyncIter<'r, I, OutPoint> { - type Item = OutPoint; - - fn next(&mut self) -> Option { - self.request.next_outpoint() - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = self.request.outpoints.len(); - (remaining, Some(remaining)) - } -} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d74cf906..80741b07 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -19,3 +19,6 @@ hashbrown = { version = "0.9.1", optional = true } default = ["std"] std = ["bitcoin/std"] serde = ["dep:serde", "bitcoin/serde", "hashbrown?/serde"] + +[dev-dependencies] +bdk_chain = { version = "0.17.0", path = "../chain" } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index bed9da4a..038bee62 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -54,12 +54,19 @@ pub mod collections { pub use hashbrown::hash_map; } +/// A tuple of keychain index and `T` representing the indexed value. +pub type Indexed = (u32, T); +/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them. +pub type KeychainIndexed = ((K, u32), T); + mod chain_data; pub use chain_data::*; mod checkpoint; pub use checkpoint::*; +pub mod spk_client; + /// Core structures for [`TxGraph`]. /// /// [`TxGraph`]: https://docs.rs/bdk_chain/latest/bdk_chain/tx_graph/struct.TxGraph.html diff --git a/crates/core/src/spk_client.rs b/crates/core/src/spk_client.rs new file mode 100644 index 00000000..e54849fb --- /dev/null +++ b/crates/core/src/spk_client.rs @@ -0,0 +1,553 @@ +//! Helper types for spk-based blockchain clients. +use crate::{ + alloc::{boxed::Box, collections::VecDeque, vec::Vec}, + collections::BTreeMap, + CheckPoint, ConfirmationBlockTime, Indexed, +}; +use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; + +type InspectSync = dyn FnMut(SyncItem, SyncProgress) + Send + 'static; + +type InspectFullScan = dyn FnMut(K, u32, &Script) + Send + 'static; + +/// An item reported to the [`inspect`](SyncRequestBuilder::inspect) closure of [`SyncRequest`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SyncItem<'i, I> { + /// Script pubkey sync item. + Spk(I, &'i Script), + /// Txid sync item. + Txid(Txid), + /// Outpoint sync item. + OutPoint(OutPoint), +} + +impl<'i, I: core::fmt::Debug + core::any::Any> core::fmt::Display for SyncItem<'i, I> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SyncItem::Spk(i, spk) => { + if (i as &dyn core::any::Any).is::<()>() { + write!(f, "script '{}'", spk) + } else { + write!(f, "script {:?} '{}'", i, spk) + } + } + SyncItem::Txid(txid) => write!(f, "txid '{}'", txid), + SyncItem::OutPoint(op) => write!(f, "outpoint '{}'", op), + } + } +} + +/// The progress of [`SyncRequest`]. +#[derive(Debug, Clone)] +pub struct SyncProgress { + /// Script pubkeys consumed by the request. + pub spks_consumed: usize, + /// Script pubkeys remaining in the request. + pub spks_remaining: usize, + /// Txids consumed by the request. + pub txids_consumed: usize, + /// Txids remaining in the request. + pub txids_remaining: usize, + /// Outpoints consumed by the request. + pub outpoints_consumed: usize, + /// Outpoints remaining in the request. + pub outpoints_remaining: usize, +} + +impl SyncProgress { + /// Total items, consumed and remaining, of the request. + pub fn total(&self) -> usize { + self.total_spks() + self.total_txids() + self.total_outpoints() + } + + /// Total script pubkeys, consumed and remaining, of the request. + pub fn total_spks(&self) -> usize { + self.spks_consumed + self.spks_remaining + } + + /// Total txids, consumed and remaining, of the request. + pub fn total_txids(&self) -> usize { + self.txids_consumed + self.txids_remaining + } + + /// Total outpoints, consumed and remaining, of the request. + pub fn total_outpoints(&self) -> usize { + self.outpoints_consumed + self.outpoints_remaining + } + + /// Total consumed items of the request. + pub fn consumed(&self) -> usize { + self.spks_consumed + self.txids_consumed + self.outpoints_consumed + } + + /// Total remaining items of the request. + pub fn remaining(&self) -> usize { + self.spks_remaining + self.txids_remaining + self.outpoints_remaining + } +} + +/// Builds a [`SyncRequest`]. +#[must_use] +pub struct SyncRequestBuilder { + inner: SyncRequest, +} + +impl Default for SyncRequestBuilder { + fn default() -> Self { + Self { + inner: Default::default(), + } + } +} + +impl SyncRequestBuilder<()> { + /// Add [`Script`]s that will be synced against. + pub fn spks(self, spks: impl IntoIterator) -> Self { + self.spks_with_indexes(spks.into_iter().map(|spk| ((), spk))) + } +} + +impl SyncRequestBuilder { + /// Set the initial chain tip for the sync request. + /// + /// This is used to update [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). + pub fn chain_tip(mut self, cp: CheckPoint) -> Self { + self.inner.chain_tip = Some(cp); + self + } + + /// Add [`Script`]s coupled with associated indexes that will be synced against. + /// + /// # Example + /// + /// Sync revealed script pubkeys obtained from a + /// [`KeychainTxOutIndex`](../../bdk_chain/indexer/keychain_txout/struct.KeychainTxOutIndex.html). + /// + /// ```rust + /// # use bdk_chain::spk_client::SyncRequest; + /// # use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex; + /// # use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey}; + /// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only(); + /// # let (descriptor_a,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap(); + /// # let (descriptor_b,_) = Descriptor::::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap(); + /// let mut indexer = KeychainTxOutIndex::<&'static str>::default(); + /// indexer.insert_descriptor("descriptor_a", descriptor_a)?; + /// indexer.insert_descriptor("descriptor_b", descriptor_b)?; + /// + /// /* Assume that the caller does more mutations to the `indexer` here... */ + /// + /// // Reveal spks for "descriptor_a", then build a sync request. Each spk will be indexed with + /// // `u32`, which represents the derivation index of the associated spk from "descriptor_a". + /// let (newly_revealed_spks, _changeset) = indexer + /// .reveal_to_target("descriptor_a", 21) + /// .expect("keychain must exist"); + /// let _request = SyncRequest::builder() + /// .spks_with_indexes(newly_revealed_spks) + /// .build(); + /// + /// // Sync all revealed spks in the indexer. This time, spks may be derived from different + /// // keychains. Each spk will be indexed with `(&'static str, u32)` where `&'static str` is + /// // the keychain identifier and `u32` is the derivation index. + /// let all_revealed_spks = indexer.revealed_spks(..); + /// let _request = SyncRequest::builder() + /// .spks_with_indexes(all_revealed_spks) + /// .build(); + /// # Ok::<_, bdk_chain::keychain_txout::InsertDescriptorError<_>>(()) + /// ``` + pub fn spks_with_indexes(mut self, spks: impl IntoIterator) -> Self { + self.inner.spks.extend(spks); + self + } + + /// Add [`Txid`]s that will be synced against. + pub fn txids(mut self, txids: impl IntoIterator) -> Self { + self.inner.txids.extend(txids); + self + } + + /// Add [`OutPoint`]s that will be synced against. + pub fn outpoints(mut self, outpoints: impl IntoIterator) -> Self { + self.inner.outpoints.extend(outpoints); + self + } + + /// Set the closure that will inspect every sync item visited. + pub fn inspect(mut self, inspect: F) -> Self + where + F: FnMut(SyncItem, SyncProgress) + Send + 'static, + { + self.inner.inspect = Box::new(inspect); + self + } + + /// Build the [`SyncRequest`]. + pub fn build(self) -> SyncRequest { + self.inner + } +} + +/// Data required to perform a spk-based blockchain client sync. +/// +/// A client sync fetches relevant chain data for a known list of scripts, transaction ids and +/// outpoints. The sync process also updates the chain from the given +/// [`chain_tip`](SyncRequestBuilder::chain_tip) (if provided). +/// +/// ```rust +/// # use bdk_chain::{bitcoin::{hashes::Hash, ScriptBuf}, local_chain::LocalChain}; +/// # use bdk_chain::spk_client::SyncRequest; +/// # let (local_chain, _) = LocalChain::from_genesis_hash(Hash::all_zeros()); +/// # let scripts = [ScriptBuf::default(), ScriptBuf::default()]; +/// // Construct a sync request. +/// let sync_request = SyncRequest::builder() +/// // Provide chain tip of the local wallet. +/// .chain_tip(local_chain.tip()) +/// // Provide list of scripts to scan for transactions against. +/// .spks(scripts) +/// // This is called for every synced item. +/// .inspect(|item, progress| println!("{} (remaining: {})", item, progress.remaining())) +/// // Finish constructing the sync request. +/// .build(); +/// ``` +#[must_use] +pub struct SyncRequest { + chain_tip: Option, + spks: VecDeque<(I, ScriptBuf)>, + spks_consumed: usize, + txids: VecDeque, + txids_consumed: usize, + outpoints: VecDeque, + outpoints_consumed: usize, + inspect: Box>, +} + +impl Default for SyncRequest { + fn default() -> Self { + Self { + chain_tip: None, + spks: VecDeque::new(), + spks_consumed: 0, + txids: VecDeque::new(), + txids_consumed: 0, + outpoints: VecDeque::new(), + outpoints_consumed: 0, + inspect: Box::new(|_, _| {}), + } + } +} + +impl From> for SyncRequest { + fn from(builder: SyncRequestBuilder) -> Self { + builder.inner + } +} + +impl SyncRequest { + /// Start building a [`SyncRequest`]. + pub fn builder() -> SyncRequestBuilder { + SyncRequestBuilder { + inner: Default::default(), + } + } + + /// Get the [`SyncProgress`] of this request. + pub fn progress(&self) -> SyncProgress { + SyncProgress { + spks_consumed: self.spks_consumed, + spks_remaining: self.spks.len(), + txids_consumed: self.txids_consumed, + txids_remaining: self.txids.len(), + outpoints_consumed: self.outpoints_consumed, + outpoints_remaining: self.outpoints.len(), + } + } + + /// Get the chain tip [`CheckPoint`] of this request (if any). + pub fn chain_tip(&self) -> Option { + self.chain_tip.clone() + } + + /// Advances the sync request and returns the next [`ScriptBuf`]. + /// + /// Returns [`None`] when there are no more scripts remaining in the request. + pub fn next_spk(&mut self) -> Option { + let (i, spk) = self.spks.pop_front()?; + self.spks_consumed += 1; + self._call_inspect(SyncItem::Spk(i, spk.as_script())); + Some(spk) + } + + /// Advances the sync request and returns the next [`Txid`]. + /// + /// Returns [`None`] when there are no more txids remaining in the request. + pub fn next_txid(&mut self) -> Option { + let txid = self.txids.pop_front()?; + self.txids_consumed += 1; + self._call_inspect(SyncItem::Txid(txid)); + Some(txid) + } + + /// Advances the sync request and returns the next [`OutPoint`]. + /// + /// Returns [`None`] when there are no more outpoints in the request. + pub fn next_outpoint(&mut self) -> Option { + let outpoint = self.outpoints.pop_front()?; + self.outpoints_consumed += 1; + self._call_inspect(SyncItem::OutPoint(outpoint)); + Some(outpoint) + } + + /// Iterate over [`ScriptBuf`]s contained in this request. + pub fn iter_spks(&mut self) -> impl ExactSizeIterator + '_ { + SyncIter::::new(self) + } + + /// Iterate over [`Txid`]s contained in this request. + pub fn iter_txids(&mut self) -> impl ExactSizeIterator + '_ { + SyncIter::::new(self) + } + + /// Iterate over [`OutPoint`]s contained in this request. + pub fn iter_outpoints(&mut self) -> impl ExactSizeIterator + '_ { + SyncIter::::new(self) + } + + fn _call_inspect(&mut self, item: SyncItem) { + let progress = self.progress(); + (*self.inspect)(item, progress); + } +} + +/// Data returned from a spk-based blockchain client sync. +/// +/// See also [`SyncRequest`]. +#[must_use] +#[derive(Debug)] +pub struct SyncResult { + /// The update to apply to the receiving + /// [`TxGraph`](../../bdk_chain/tx_graph/struct.TxGraph.html). + pub graph_update: crate::tx_graph::Update, + /// The update to apply to the receiving + /// [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). + pub chain_update: Option, +} + +impl Default for SyncResult { + fn default() -> Self { + Self { + graph_update: Default::default(), + chain_update: Default::default(), + } + } +} + +/// Builds a [`FullScanRequest`]. +#[must_use] +pub struct FullScanRequestBuilder { + inner: FullScanRequest, +} + +impl Default for FullScanRequestBuilder { + fn default() -> Self { + Self { + inner: Default::default(), + } + } +} + +impl FullScanRequestBuilder { + /// Set the initial chain tip for the full scan request. + /// + /// This is used to update [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). + pub fn chain_tip(mut self, tip: CheckPoint) -> Self { + self.inner.chain_tip = Some(tip); + self + } + + /// Set the spk iterator for a given `keychain`. + pub fn spks_for_keychain( + mut self, + keychain: K, + spks: impl IntoIterator> + Send + 'static>, + ) -> Self { + self.inner + .spks_by_keychain + .insert(keychain, Box::new(spks.into_iter())); + self + } + + /// Set the closure that will inspect every sync item visited. + pub fn inspect(mut self, inspect: F) -> Self + where + F: FnMut(K, u32, &Script) + Send + 'static, + { + self.inner.inspect = Box::new(inspect); + self + } + + /// Build the [`FullScanRequest`]. + pub fn build(self) -> FullScanRequest { + self.inner + } +} + +/// Data required to perform a spk-based blockchain client full scan. +/// +/// A client full scan iterates through all the scripts for the given keychains, fetching relevant +/// data until some stop gap number of scripts is found that have no data. This operation is +/// generally only used when importing or restoring previously used keychains in which the list of +/// used scripts is not known. The full scan process also updates the chain from the given +/// [`chain_tip`](FullScanRequestBuilder::chain_tip) (if provided). +#[must_use] +pub struct FullScanRequest { + chain_tip: Option, + spks_by_keychain: BTreeMap> + Send>>, + inspect: Box>, +} + +impl From> for FullScanRequest { + fn from(builder: FullScanRequestBuilder) -> Self { + builder.inner + } +} + +impl Default for FullScanRequest { + fn default() -> Self { + Self { + chain_tip: None, + spks_by_keychain: Default::default(), + inspect: Box::new(|_, _, _| {}), + } + } +} + +impl FullScanRequest { + /// Start building a [`FullScanRequest`]. + pub fn builder() -> FullScanRequestBuilder { + FullScanRequestBuilder { + inner: Self::default(), + } + } + + /// Get the chain tip [`CheckPoint`] of this request (if any). + pub fn chain_tip(&self) -> Option { + self.chain_tip.clone() + } + + /// List all keychains contained in this request. + pub fn keychains(&self) -> Vec { + self.spks_by_keychain.keys().cloned().collect() + } + + /// Advances the full scan request and returns the next indexed [`ScriptBuf`] of the given + /// `keychain`. + pub fn next_spk(&mut self, keychain: K) -> Option> { + self.iter_spks(keychain).next() + } + + /// Iterate over indexed [`ScriptBuf`]s contained in this request of the given `keychain`. + pub fn iter_spks(&mut self, keychain: K) -> impl Iterator> + '_ { + let spks = self.spks_by_keychain.get_mut(&keychain); + let inspect = &mut self.inspect; + KeychainSpkIter { + keychain, + spks, + inspect, + } + } +} + +/// Data returned from a spk-based blockchain client full scan. +/// +/// See also [`FullScanRequest`]. +#[must_use] +#[derive(Debug)] +pub struct FullScanResult { + /// The update to apply to the receiving + /// [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). + pub graph_update: crate::tx_graph::Update, + /// The update to apply to the receiving [`TxGraph`](../../bdk_chain/tx_graph/struct.TxGraph.html). + pub chain_update: Option, + /// Last active indices for the corresponding keychains (`K`). + pub last_active_indices: BTreeMap, +} + +impl Default for FullScanResult { + fn default() -> Self { + Self { + graph_update: Default::default(), + chain_update: Default::default(), + last_active_indices: Default::default(), + } + } +} + +struct KeychainSpkIter<'r, K> { + keychain: K, + spks: Option<&'r mut Box> + Send>>, + inspect: &'r mut Box>, +} + +impl<'r, K: Ord + Clone> Iterator for KeychainSpkIter<'r, K> { + type Item = Indexed; + + fn next(&mut self) -> Option { + let (i, spk) = self.spks.as_mut()?.next()?; + (*self.inspect)(self.keychain.clone(), i, &spk); + Some((i, spk)) + } +} + +struct SyncIter<'r, I, Item> { + request: &'r mut SyncRequest, + marker: core::marker::PhantomData, +} + +impl<'r, I, Item> SyncIter<'r, I, Item> { + fn new(request: &'r mut SyncRequest) -> Self { + Self { + request, + marker: core::marker::PhantomData, + } + } +} + +impl<'r, I, Item> ExactSizeIterator for SyncIter<'r, I, Item> where SyncIter<'r, I, Item>: Iterator {} + +impl<'r, I> Iterator for SyncIter<'r, I, ScriptBuf> { + type Item = ScriptBuf; + + fn next(&mut self) -> Option { + self.request.next_spk() + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.request.spks.len(); + (remaining, Some(remaining)) + } +} + +impl<'r, I> Iterator for SyncIter<'r, I, Txid> { + type Item = Txid; + + fn next(&mut self) -> Option { + self.request.next_txid() + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.request.txids.len(); + (remaining, Some(remaining)) + } +} + +impl<'r, I> Iterator for SyncIter<'r, I, OutPoint> { + type Item = OutPoint; + + fn next(&mut self) -> Option { + self.request.next_outpoint() + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.request.outpoints.len(); + (remaining, Some(remaining)) + } +} diff --git a/crates/testenv/src/lib.rs b/crates/testenv/src/lib.rs index 747acc44..6d169bdc 100644 --- a/crates/testenv/src/lib.rs +++ b/crates/testenv/src/lib.rs @@ -4,7 +4,8 @@ use bdk_chain::{ secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid, }, - BlockId, local_chain::CheckPoint, + local_chain::CheckPoint, + BlockId, }; use bitcoincore_rpc::{ bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules}, diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 638bb575..72c42ddf 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2472,6 +2472,7 @@ impl Wallet { /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to /// start a blockchain sync with a spk based blockchain client. pub fn start_sync_with_revealed_spks(&self) -> SyncRequestBuilder<(KeychainKind, u32)> { + use bdk_chain::keychain_txout::SyncRequestBuilderExt; SyncRequest::builder() .chain_tip(self.chain.tip()) .revealed_spks_from_indexer(&self.indexed_graph.index, ..) @@ -2486,6 +2487,7 @@ impl Wallet { /// This operation is generally only used when importing or restoring a previously used wallet /// in which the list of used scripts is not known. pub fn start_full_scan(&self) -> FullScanRequestBuilder { + use bdk_chain::keychain_txout::FullScanRequestBuilderExt; FullScanRequest::builder() .chain_tip(self.chain.tip()) .spks_from_indexer(&self.indexed_graph.index) diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index d4692e35..7a400587 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -6,6 +6,7 @@ use std::{ use bdk_chain::{ bitcoin::Network, + keychain_txout::FullScanRequestBuilderExt, spk_client::{FullScanRequest, SyncRequest}, Merge, };