diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 04ca62c4c6..2065669714 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -1,4 +1,4 @@ -//! 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: //! diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index b41050533e..649cd6891a 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -36,58 +36,45 @@ pub trait EsploraAsyncExt { request_heights: impl IntoIterator + Send> + Send, ) -> Result; - /// 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( + async fn full_scan( &self, keychain_spks: BTreeMap< K, impl IntoIterator + Send> + Send, >, - txids: impl IntoIterator + Send> + Send, - outpoints: impl IntoIterator + Send> + Send, stop_gap: usize, parallel_requests: usize, ) -> Result<(TxGraph, BTreeMap), 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 + Send> + Send, txids: impl IntoIterator + Send> + Send, outpoints: impl IntoIterator + Send> + Send, parallel_requests: usize, - ) -> Result, 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, Error>; } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -199,14 +186,12 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { }) } - async fn scan_txs_with_keychains( + async fn full_scan( &self, keychain_spks: BTreeMap< K, impl IntoIterator + Send> + Send, >, - txids: impl IntoIterator + Send> + Send, - outpoints: impl IntoIterator + Send> + Send, stop_gap: usize, parallel_requests: usize, ) -> Result<(TxGraph, BTreeMap), Error> { @@ -275,6 +260,32 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { } } + Ok((graph, last_active_indexes)) + } + + async fn sync( + &self, + misc_spks: impl IntoIterator + Send> + Send, + txids: impl IntoIterator + Send> + Send, + outpoints: impl IntoIterator + Send> + Send, + parallel_requests: usize, + ) -> Result, 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 @@ -323,7 +334,6 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { } } } - - Ok((graph, last_active_indexes)) + Ok(graph) } } diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index bde24f832b..d088ed6af1 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -34,54 +34,42 @@ pub trait EsploraExt { request_heights: impl IntoIterator, ) -> Result; - /// 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( + fn full_scan( &self, keychain_spks: BTreeMap>, - txids: impl IntoIterator, - outpoints: impl IntoIterator, stop_gap: usize, parallel_requests: usize, ) -> Result<(TxGraph, BTreeMap), 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, txids: impl IntoIterator, outpoints: impl IntoIterator, parallel_requests: usize, - ) -> Result, 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, Error>; } impl EsploraExt for esplora_client::BlockingClient { @@ -190,11 +178,9 @@ impl EsploraExt for esplora_client::BlockingClient { }) } - fn scan_txs_with_keychains( + fn full_scan( &self, keychain_spks: BTreeMap>, - txids: impl IntoIterator, - outpoints: impl IntoIterator, stop_gap: usize, parallel_requests: usize, ) -> Result<(TxGraph, BTreeMap), Error> { @@ -266,6 +252,31 @@ impl EsploraExt for esplora_client::BlockingClient { } } + Ok((graph, last_active_indexes)) + } + + fn sync( + &self, + misc_spks: impl IntoIterator, + txids: impl IntoIterator, + outpoints: impl IntoIterator, + parallel_requests: usize, + ) -> Result, 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 @@ -292,7 +303,7 @@ impl EsploraExt for esplora_client::BlockingClient { } } - 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); @@ -317,7 +328,6 @@ impl EsploraExt for esplora_client::BlockingClient { } } } - - Ok((graph, last_active_indexes)) + Ok(graph) } } diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index e8c6672779..727c8c53b2 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -1,4 +1,21 @@ #![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; diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index 38833f588e..3124bd2d11 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -101,7 +101,7 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { let graph_update = env .client - .scan_txs( + .sync( misc_spks.into_iter(), vec![].into_iter(), vec![].into_iter(), @@ -166,28 +166,10 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> { // 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); @@ -209,24 +191,12 @@ pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> { // 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)); diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index 5a76172325..50b19d1ccd 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -99,7 +99,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { 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(), @@ -164,22 +164,10 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> { // 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); @@ -201,24 +189,12 @@ pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> { // 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)); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index cabd8ea828..101fd58ba2 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -188,13 +188,7 @@ fn main() -> anyhow::Result<()> { // 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"); @@ -312,7 +306,7 @@ fn main() -> anyhow::Result<()> { } 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) } diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index fb8f7b5105..755b39007b 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -54,7 +54,7 @@ async fn main() -> Result<(), anyhow::Error> { }) .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?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 09e7c3ad4a..d0f35bea8e 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -54,7 +54,7 @@ fn main() -> Result<(), anyhow::Error> { .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 {