-//! This crate is a collection of core structures for [Bitcoin Dev Kit] (alpha release).
+//! This crate is a collection of core structures for [Bitcoin Dev Kit].
//!
//! The goal of this crate is to give wallets the mechanisms needed to:
//!
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
) -> Result<local_chain::Update, Error>;
- /// Scan Esplora for the data specified and return a [`TxGraph`] and a map of last active
- /// indices.
+ /// Full scan the keychain scripts specified with the blockchain (via an Esplora client) and
+ /// returns a [`TxGraph`] and a map of last active indices.
///
/// * `keychain_spks`: keychains that we want to scan transactions for
- /// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
- /// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
- /// want to include in the update
///
- /// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
+ /// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
#[allow(clippy::result_large_err)]
- async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
+ async fn full_scan<K: Ord + Clone + Send>(
&self,
keychain_spks: BTreeMap<
K,
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
>,
- txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
- outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
- /// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
+ /// Sync a set of scripts with the blockchain (via an Esplora client) for the data
+ /// specified and return a [`TxGraph`].
///
- /// [`scan_txs_with_keychains`]: EsploraAsyncExt::scan_txs_with_keychains
+ /// * `misc_spks`: scripts that we want to sync transactions for
+ /// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
+ /// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
+ /// want to include in the update
+ ///
+ /// If the scripts to sync are unknown, such as when restoring or importing a keychain that
+ /// may include scripts that have been used, use [`full_scan`] with the keychain.
+ ///
+ /// [`full_scan`]: EsploraAsyncExt::full_scan
#[allow(clippy::result_large_err)]
- async fn scan_txs(
+ async fn sync(
&self,
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
parallel_requests: usize,
- ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
- self.scan_txs_with_keychains(
- [(
- (),
- misc_spks
- .into_iter()
- .enumerate()
- .map(|(i, spk)| (i as u32, spk)),
- )]
- .into(),
- txids,
- outpoints,
- usize::MAX,
- parallel_requests,
- )
- .await
- .map(|(g, _)| g)
- }
+ ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
})
}
- async fn scan_txs_with_keychains<K: Ord + Clone + Send>(
+ async fn full_scan<K: Ord + Clone + Send>(
&self,
keychain_spks: BTreeMap<
K,
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
>,
- txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
- outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
stop_gap: usize,
parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
}
}
+ Ok((graph, last_active_indexes))
+ }
+
+ async fn sync(
+ &self,
+ misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
+ txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
+ outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
+ parallel_requests: usize,
+ ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
+ let mut graph = self
+ .full_scan(
+ [(
+ (),
+ misc_spks
+ .into_iter()
+ .enumerate()
+ .map(|(i, spk)| (i as u32, spk)),
+ )]
+ .into(),
+ usize::MAX,
+ parallel_requests,
+ )
+ .await
+ .map(|(g, _)| g)?;
+
let mut txids = txids.into_iter();
loop {
let handles = txids
}
}
}
-
- Ok((graph, last_active_indexes))
+ Ok(graph)
}
}
pub trait EsploraExt {
/// Prepare an [`LocalChain`] update with blocks fetched from Esplora.
///
- /// * `prev_tip` is the previous tip of [`LocalChain::tip`].
- /// * `get_heights` is the block heights that we are interested in fetching from Esplora.
+ /// * `local_tip` is the previous tip of [`LocalChain::tip`].
+ /// * `request_heights` is the block heights that we are interested in fetching from Esplora.
///
/// The result of this method can be applied to [`LocalChain::apply_update`].
///
request_heights: impl IntoIterator<Item = u32>,
) -> Result<local_chain::Update, Error>;
- /// Scan Esplora for the data specified and return a [`TxGraph`] and a map of last active
- /// indices.
+ /// Full scan the keychain scripts specified with the blockchain (via an Esplora client) and
+ /// returns a [`TxGraph`] and a map of last active indices.
///
/// * `keychain_spks`: keychains that we want to scan transactions for
- /// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
- /// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
- /// want to include in the update
///
- /// The scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
+ /// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
/// parallel.
#[allow(clippy::result_large_err)]
- fn scan_txs_with_keychains<K: Ord + Clone>(
+ fn full_scan<K: Ord + Clone>(
&self,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
- /// Convenience method to call [`scan_txs_with_keychains`] without requiring a keychain.
+ /// Sync a set of scripts with the blockchain (via an Esplora client) for the data
+ /// specified and return a [`TxGraph`].
///
- /// [`scan_txs_with_keychains`]: EsploraExt::scan_txs_with_keychains
+ /// * `misc_spks`: scripts that we want to sync transactions for
+ /// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
+ /// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
+ /// want to include in the update
+ ///
+ /// If the scripts to sync are unknown, such as when restoring or importing a keychain that
+ /// may include scripts that have been used, use [`full_scan`] with the keychain.
+ ///
+ /// [`full_scan`]: EsploraExt::full_scan
#[allow(clippy::result_large_err)]
- fn scan_txs(
+ fn sync(
&self,
misc_spks: impl IntoIterator<Item = ScriptBuf>,
txids: impl IntoIterator<Item = Txid>,
outpoints: impl IntoIterator<Item = OutPoint>,
parallel_requests: usize,
- ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
- self.scan_txs_with_keychains(
- [(
- (),
- misc_spks
- .into_iter()
- .enumerate()
- .map(|(i, spk)| (i as u32, spk)),
- )]
- .into(),
- txids,
- outpoints,
- usize::MAX,
- parallel_requests,
- )
- .map(|(g, _)| g)
- }
+ ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
}
impl EsploraExt for esplora_client::BlockingClient {
})
}
- fn scan_txs_with_keychains<K: Ord + Clone>(
+ fn full_scan<K: Ord + Clone>(
&self,
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
- txids: impl IntoIterator<Item = Txid>,
- outpoints: impl IntoIterator<Item = OutPoint>,
stop_gap: usize,
parallel_requests: usize,
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
}
}
+ Ok((graph, last_active_indexes))
+ }
+
+ fn sync(
+ &self,
+ misc_spks: impl IntoIterator<Item = ScriptBuf>,
+ txids: impl IntoIterator<Item = Txid>,
+ outpoints: impl IntoIterator<Item = OutPoint>,
+ parallel_requests: usize,
+ ) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
+ let mut graph = self
+ .full_scan(
+ [(
+ (),
+ misc_spks
+ .into_iter()
+ .enumerate()
+ .map(|(i, spk)| (i as u32, spk)),
+ )]
+ .into(),
+ usize::MAX,
+ parallel_requests,
+ )
+ .map(|(g, _)| g)?;
+
let mut txids = txids.into_iter();
loop {
let handles = txids
}
}
- for op in outpoints.into_iter() {
+ for op in outpoints {
if graph.get_tx(op.txid).is_none() {
if let Some(tx) = self.get_tx(&op.txid)? {
let _ = graph.insert_tx(tx);
}
}
}
-
- Ok((graph, last_active_indexes))
+ Ok(graph)
}
}
#![doc = include_str!("../README.md")]
+
+//! This crate is used for updating structures of [`bdk_chain`] with data from an Esplora server.
+//!
+//! The two primary methods are [`EsploraExt::sync`] and [`EsploraExt::full_scan`]. In most cases
+//! [`EsploraExt::sync`] is used to sync the transaction histories of scripts that the application
+//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it
+//! has shown a user. [`EsploraExt::full_scan`] is meant to be used when importing or restoring a
+//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
+//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
+//! sync or full scan the user receives relevant blockchain data and output updates for [`bdk_chain`]
+//! via a new [`TxGraph`] to be appended to any existing [`TxGraph`] data.
+//!
+//! Refer to [`example_esplora`] for a complete example.
+//!
+//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
+//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
+
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
use esplora_client::TxStatus;
let graph_update = env
.client
- .scan_txs(
+ .sync(
misc_spks.into_iter(),
vec![].into_iter(),
vec![].into_iter(),
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
- let (graph_update, active_indices) = env
- .client
- .scan_txs_with_keychains(
- keychains.clone(),
- vec![].into_iter(),
- vec![].into_iter(),
- 2,
- 1,
- )
- .await?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
- let (graph_update, active_indices) = env
- .client
- .scan_txs_with_keychains(
- keychains.clone(),
- vec![].into_iter(),
- vec![].into_iter(),
- 3,
- 1,
- )
- .await?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
- let (graph_update, active_indices) = env
- .client
- .scan_txs_with_keychains(
- keychains.clone(),
- vec![].into_iter(),
- vec![].into_iter(),
- 4,
- 1,
- )
- .await?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(active_indices[&0], 3);
- let (graph_update, active_indices) = env
- .client
- .scan_txs_with_keychains(keychains, vec![].into_iter(), vec![].into_iter(), 5, 1)
- .await?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
sleep(Duration::from_millis(10))
}
- let graph_update = env.client.scan_txs(
+ let graph_update = env.client.sync(
misc_spks.into_iter(),
vec![].into_iter(),
vec![].into_iter(),
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
// will.
- let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
- keychains.clone(),
- vec![].into_iter(),
- vec![].into_iter(),
- 2,
- 1,
- )?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
assert!(graph_update.full_txs().next().is_none());
assert!(active_indices.is_empty());
- let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
- keychains.clone(),
- vec![].into_iter(),
- vec![].into_iter(),
- 3,
- 1,
- )?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
assert_eq!(active_indices[&0], 3);
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
// The last active indice won't be updated in the first case but will in the second one.
- let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
- keychains.clone(),
- vec![].into_iter(),
- vec![].into_iter(),
- 4,
- 1,
- )?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 1);
assert!(txs.contains(&txid_4th_addr));
assert_eq!(active_indices[&0], 3);
- let (graph_update, active_indices) = env.client.scan_txs_with_keychains(
- keychains,
- vec![].into_iter(),
- vec![].into_iter(),
- 5,
- 1,
- )?;
+ let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
assert_eq!(txs.len(), 2);
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
// represents the last active spk derivation indices of keychains
// (`keychain_indices_update`).
let (graph_update, last_active_indices) = client
- .scan_txs_with_keychains(
- keychain_spks,
- core::iter::empty(),
- core::iter::empty(),
- *stop_gap,
- scan_options.parallel_requests,
- )
+ .full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
.context("scanning for transactions")?;
let mut graph = graph.lock().expect("mutex must not be poisoned");
}
let graph_update =
- client.scan_txs(spks, txids, outpoints, scan_options.parallel_requests)?;
+ client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
graph.lock().unwrap().apply_update(graph_update)
}
})
.collect();
let (update_graph, last_active_indices) = client
- .scan_txs_with_keychains(keychain_spks, None, None, STOP_GAP, PARALLEL_REQUESTS)
+ .full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)
.await?;
let missing_heights = update_graph.missing_heights(wallet.local_chain());
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
.collect();
let (update_graph, last_active_indices) =
- client.scan_txs_with_keychains(keychain_spks, None, None, STOP_GAP, PARALLEL_REQUESTS)?;
+ client.full_scan(keychain_spks, STOP_GAP, PARALLEL_REQUESTS)?;
let missing_heights = update_graph.missing_heights(wallet.local_chain());
let chain_update = client.update_local_chain(prev_tip, missing_heights)?;
let update = Update {