From b4c68c777ac8b6d66d80c9d6ceeee8024a6ca7bb Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 20 Jun 2025 10:50:47 +0200 Subject: [PATCH 01/15] Register spent transaction with txid or blockid raw bytes not hex string --- src/client/structs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/structs.rs b/src/client/structs.rs index dccc587..d3718cb 100644 --- a/src/client/structs.rs +++ b/src/client/structs.rs @@ -12,8 +12,8 @@ use bitcoin::{ use serde::{Deserialize, Serialize}; use silentpayments::{receiving::Label, SilentPaymentAddress}; -type SpendingTxId = String; -type MinedInBlock = String; +type SpendingTxId = [u8; 32]; +type MinedInBlock = [u8; 32]; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum OutputSpendStatus { From b5fa5364cea07f12aca05aec83a0d9baf75814e2 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 21 Jun 2025 13:40:35 +0200 Subject: [PATCH 02/15] Add wasm dependencies + separate targets --- .cargo/config.toml | 12 ++++++++++++ Cargo.toml | 17 ++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..d9025ec --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,12 @@ +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "target-feature=+crt-static", + "-C", "link-arg=--import-memory", + "-C", "link-arg=--initial-memory=2097152", + "-C", "link-arg=--max-memory=2097152", + "-C", "link-arg=--stack-first", + "-C", "link-arg=--export-table", +] + +[build] +target = "wasm32-unknown-unknown" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d8e963a..57fd02d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ anyhow = "1.0" serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" bitcoin = { version = "0.31.1", features = ["serde", "rand", "base64"] } -rayon = "1.10.0" futures = "0.3" log = "0.4" async-trait = "0.1" @@ -21,5 +20,21 @@ reqwest = { version = "0.12.4", features = ["rustls-tls", "gzip", "json"], defau hex = { version = "0.4.3", features = ["serde"], optional = true } bdk_coin_select = "0.4.0" +# WASM-incompatible dependencies - only include when not targeting WASM +rayon = { version = "1.10.0", optional = true } + +# WASM-specific dependencies +wasm-bindgen = { version = "0.2", optional = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +rayon = "1.10.0" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + [features] blindbit-backend = ["reqwest", "hex"] +default = [] + +# WASM-specific features +wasm = ["wasm-bindgen"] From 3be073c6c5ec7b64205c5954608f7e931b070169 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 21 Jun 2025 13:41:43 +0200 Subject: [PATCH 03/15] wasm compatibility refactoring --- src/backend/backend.rs | 18 +++++++ src/backend/blindbit/backend/backend.rs | 72 ++++++++++++++++++++++++- src/backend/mod.rs | 4 ++ src/client/client.rs | 58 +++++++++++++------- 4 files changed, 133 insertions(+), 19 deletions(-) diff --git a/src/backend/backend.rs b/src/backend/backend.rs index d1892f0..55dc15c 100644 --- a/src/backend/backend.rs +++ b/src/backend/backend.rs @@ -22,3 +22,21 @@ pub trait ChainBackend { async fn block_height(&self) -> Result; } + +// WASM-specific version without Send bounds +#[cfg(target_arch = "wasm32")] +#[async_trait(?Send)] +pub trait ChainBackendWasm { + fn get_block_data_for_range( + &self, + range: RangeInclusive, + dust_limit: Amount, + with_cutthrough: bool, + ) -> Pin>>>; + + async fn spent_index(&self, block_height: Height) -> Result; + + async fn utxos(&self, block_height: Height) -> Result>; + + async fn block_height(&self) -> Result; +} diff --git a/src/backend/blindbit/backend/backend.rs b/src/backend/blindbit/backend/backend.rs index ba47e70..ed3b77d 100644 --- a/src/backend/blindbit/backend/backend.rs +++ b/src/backend/blindbit/backend/backend.rs @@ -1,4 +1,7 @@ -use std::{ops::RangeInclusive, pin::Pin, sync::Arc}; +use std::{ops::RangeInclusive, pin::Pin}; + +#[cfg(not(target_arch = "wasm32"))] +use std::sync::Arc; use async_trait::async_trait; use bitcoin::{absolute::Height, Amount}; @@ -8,6 +11,9 @@ use anyhow::Result; use crate::{backend::blindbit::BlindbitClient, BlockData, ChainBackend, SpentIndexData, UtxoData}; +#[cfg(target_arch = "wasm32")] +use crate::backend::ChainBackendWasm; + const CONCURRENT_FILTER_REQUESTS: usize = 200; #[derive(Debug)] @@ -23,6 +29,7 @@ impl BlindbitBackend { } } +#[cfg(not(target_arch = "wasm32"))] #[async_trait] impl ChainBackend for BlindbitBackend { /// High-level function to get block data for a range of blocks. @@ -82,3 +89,66 @@ impl ChainBackend for BlindbitBackend { self.client.block_height().await } } + +#[cfg(target_arch = "wasm32")] +#[async_trait(?Send)] +impl ChainBackendWasm for BlindbitBackend { + /// High-level function to get block data for a range of blocks. + /// Block data includes all the information needed to determine if a block is relevant for scanning, + /// but does not include utxos, or spent index. + /// These need to be fetched separately afterwards, if it is determined this block is relevant. + fn get_block_data_for_range( + &self, + range: RangeInclusive, + dust_limit: Amount, + with_cutthrough: bool, + ) -> Pin>>> { + // For WASM, we need to avoid Arc and use a different approach + // Since WASM doesn't support threading well, we'll use a simpler approach + let client = self.client.clone(); + + let res = stream::iter(range) + .map(move |n| { + let client = client.clone(); + + async move { + let blkheight = Height::from_consensus(n)?; + let tweaks = match with_cutthrough { + true => client.tweaks(blkheight, dust_limit).await?, + false => client.tweak_index(blkheight, dust_limit).await?, + }; + let new_utxo_filter = client.filter_new_utxos(blkheight).await?; + let spent_filter = client.filter_spent(blkheight).await?; + let blkhash = new_utxo_filter.block_hash; + Ok(BlockData { + blkheight, + blkhash, + tweaks, + new_utxo_filter: new_utxo_filter.into(), + spent_filter: spent_filter.into(), + }) + } + }) + .buffered(1); // Use buffered(1) for WASM to avoid concurrency issues + + Box::pin(res) + } + + async fn spent_index(&self, block_height: Height) -> Result { + self.client.spent_index(block_height).await.map(Into::into) + } + + async fn utxos(&self, block_height: Height) -> Result> { + Ok(self + .client + .utxos(block_height) + .await? + .into_iter() + .map(Into::into) + .collect()) + } + + async fn block_height(&self) -> Result { + self.client.block_height().await + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index d81e60c..89c767e 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -4,6 +4,10 @@ mod blindbit; mod structs; pub use backend::ChainBackend; + +#[cfg(target_arch = "wasm32")] +pub use backend::ChainBackendWasm; + pub use structs::*; #[cfg(feature = "blindbit-backend")] diff --git a/src/client/client.rs b/src/client/client.rs index 0a4c1d0..e80b793 100644 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -108,30 +108,52 @@ impl SpClient { &self, tweak_data_vec: Vec, ) -> Result> { - use rayon::prelude::*; let b_scan = &self.get_scan_key(); - let shared_secrets: Vec = tweak_data_vec - .into_par_iter() - .map(|tweak| sp_utils::receiving::calculate_ecdh_shared_secret(&tweak, b_scan)) - .collect(); - - let items: Result> = shared_secrets - .into_par_iter() - .map(|secret| { - let spks = self.sp_receiver.get_spks_from_shared_secret(&secret)?; + #[cfg(not(target_arch = "wasm32"))] + { + use rayon::prelude::*; + + let shared_secrets: Vec = tweak_data_vec + .into_par_iter() + .map(|tweak| sp_utils::receiving::calculate_ecdh_shared_secret(&tweak, b_scan)) + .collect(); + + let items: Result> = shared_secrets + .into_par_iter() + .map(|secret| { + let spks = self.sp_receiver.get_spks_from_shared_secret(&secret)?; + + Ok((secret, spks.into_values())) + }) + .collect(); + + let mut res = HashMap::new(); + for (secret, spks) in items? { + for spk in spks { + res.insert(spk, secret); + } + } + Ok(res) + } - Ok((secret, spks.into_values())) - }) - .collect(); + #[cfg(target_arch = "wasm32")] + { + // Sequential fallback for WASM + let shared_secrets: Vec = tweak_data_vec + .into_iter() + .map(|tweak| sp_utils::receiving::calculate_ecdh_shared_secret(&tweak, b_scan)) + .collect(); - let mut res = HashMap::new(); - for (secret, spks) in items? { - for spk in spks { - res.insert(spk, secret); + let mut res = HashMap::new(); + for secret in shared_secrets { + let spks = self.sp_receiver.get_spks_from_shared_secret(&secret)?; + for spk in spks.into_values() { + res.insert(spk, secret); + } } + Ok(res) } - Ok(res) } pub fn get_client_fingerprint(&self) -> Result<[u8; 8]> { From 8566f713ad0d2532c65491532dc975d56d9364db Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 21 Jun 2025 13:42:03 +0200 Subject: [PATCH 04/15] update examples --- examples/Cargo.toml | 15 +++++++ examples/lib.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 examples/Cargo.toml create mode 100644 examples/lib.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml new file mode 100644 index 0000000..b2e2db6 --- /dev/null +++ b/examples/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "wasm_example" +version = "0.1.0" +edition = "2021" + +[dependencies] +sp_client = { path = ".." } +bitcoin = { version = "0.31.1", features = ["serde", "rand", "base64"] } +hex = "0.4.3" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = "0.2" + +[lib] +crate-type = ["cdylib"] \ No newline at end of file diff --git a/examples/lib.rs b/examples/lib.rs new file mode 100644 index 0000000..4f9cbe4 --- /dev/null +++ b/examples/lib.rs @@ -0,0 +1,105 @@ +//! Example of using sp_client in a WASM environment +//! +//! This example shows how to create a basic silent payment client +//! and generate receiving addresses in a WASM context. + +use sp_client::{SpClient, SpendKey}; +use bitcoin::{Network, secp256k1::SecretKey}; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +/// Create a new silent payment client for WASM +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub fn create_client(scan_key_hex: &str, network: &str) -> Result { + // Parse the scan key + let scan_key_bytes = hex::decode(scan_key_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid scan key: {}", e)))?; + let scan_sk = SecretKey::from_slice(&scan_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + + // Parse network + let network = match network { + "bitcoin" => Network::Bitcoin, + "testnet" => Network::Testnet, + "regtest" => Network::Regtest, + "signet" => Network::Signet, + _ => return Err(JsValue::from_str("Invalid network")), + }; + + // Create spend key (using scan key as spend key for this example) + let spend_key = SpendKey::Secret(scan_sk); + + // Create client + let client = SpClient::new(scan_sk, spend_key, network) + .map_err(|e| JsValue::from_str(&format!("Failed to create client: {}", e)))?; + + // Get receiving address + let address = client.get_receiving_address(); + + Ok(address.to_string()) +} + +/// Get client fingerprint for WASM +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +pub fn get_fingerprint(scan_key_hex: &str, network: &str) -> Result { + // Parse the scan key + let scan_key_bytes = hex::decode(scan_key_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid scan key: {}", e)))?; + let scan_sk = SecretKey::from_slice(&scan_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Invalid secret key: {}", e)))?; + + // Parse network + let network = match network { + "bitcoin" => Network::Bitcoin, + "testnet" => Network::Testnet, + "regtest" => Network::Regtest, + "signet" => Network::Signet, + _ => return Err(JsValue::from_str("Invalid network")), + }; + + // Create spend key (using scan key as spend key for this example) + let spend_key = SpendKey::Secret(scan_sk); + + // Create client + let client = SpClient::new(scan_sk, spend_key, network) + .map_err(|e| JsValue::from_str(&format!("Failed to create client: {}", e)))?; + + // Get fingerprint + let fingerprint = client.get_client_fingerprint() + .map_err(|e| JsValue::from_str(&format!("Failed to get fingerprint: {}", e)))?; + + Ok(hex::encode(fingerprint)) +} + +// Non-WASM example for testing +#[cfg(not(target_arch = "wasm32"))] +pub fn create_client_example() -> Result> { + // Create a test scan key + let scan_sk = SecretKey::from_slice(&[0x01; 32])?; + let spend_key = SpendKey::Secret(scan_sk); + + // Create client for testnet + let client = SpClient::new(scan_sk, spend_key, Network::Testnet)?; + + // Get receiving address + let address = client.get_receiving_address(); + + Ok(address.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn test_create_client() { + let result = create_client_example(); + assert!(result.is_ok()); + let address = result.unwrap(); + assert!(address.starts_with("sp")); + } +} \ No newline at end of file From 71862a092878931065e230c7533e6e2f7c3d6d6b Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 21 Jun 2025 13:42:23 +0200 Subject: [PATCH 05/15] Update README --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 81c3bc9..bc06c45 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,42 @@ Whereas rust-silentpayments concerns itself with cryptography (it is essentially sp-client is concerned with high-level wallet stuff, such as parsing incoming transactions, managing owned outputs, and signing transactions. This library is used as a backend for the silent payment wallet [Dana wallet](https://github.com/cygnet3/danawallet). + +## WASM Support + +This library supports WebAssembly (WASM) targets for use in web applications. To build for WASM: + +### Prerequisites + +1. Install the WASM target: + ```bash + rustup target add wasm32-unknown-unknown + ``` + +2. Install wasm-pack (optional, for easier WASM builds): + ```bash + cargo install wasm-pack + ``` + +### Building for WASM + +#### Using Cargo directly: +```bash +cargo build --target wasm32-unknown-unknown +``` + +#### Using wasm-pack: +```bash +wasm-pack build --target web +``` + +### Features + +When building for WASM: +- The `rayon` dependency is automatically disabled and parallel processing falls back to sequential processing +- The `blindbit-backend` feature is available but requires appropriate HTTP client configuration for WASM +- All core functionality remains available + +### Usage in Web Applications + +The library can be used in web applications through standard WASM interop. Note that some features like the `blindbit-backend` may require additional configuration for HTTP requests in the browser environment. From ba560596f2da8f0e9829db0e59a7bd5841f41e22 Mon Sep 17 00:00:00 2001 From: Sosthene Date: Sat, 21 Jun 2025 17:11:10 +0200 Subject: [PATCH 06/15] Reexport futures --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index c055981..2e27d2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ mod updater; pub use bdk_coin_select::FeeRate; pub use bitcoin; pub use silentpayments; +pub use futures; pub use backend::*; pub use client::*; From 260024f7065c969e2224364d69f785af6a99611c Mon Sep 17 00:00:00 2001 From: Sosthene Date: Tue, 24 Jun 2025 15:18:21 +0200 Subject: [PATCH 07/15] Rewrite the backend to be compatible with wasm * Abstraction of SpScanner into a trait * Gating of the code with wasm32 target when necessary --- src/backend/backend.rs | 1 + src/backend/blindbit/backend/backend.rs | 5 +- src/backend/mod.rs | 5 +- src/scanner/mod.rs | 345 ++++++++++++++++++++- src/scanner/scanner.rs | 380 ------------------------ 5 files changed, 351 insertions(+), 385 deletions(-) delete mode 100644 src/scanner/scanner.rs diff --git a/src/backend/backend.rs b/src/backend/backend.rs index 55dc15c..26a86d2 100644 --- a/src/backend/backend.rs +++ b/src/backend/backend.rs @@ -7,6 +7,7 @@ use futures::Stream; use super::structs::{BlockData, SpentIndexData, UtxoData}; +#[cfg(not(target_arch = "wasm32"))] #[async_trait] pub trait ChainBackend { fn get_block_data_for_range( diff --git a/src/backend/blindbit/backend/backend.rs b/src/backend/blindbit/backend/backend.rs index ed3b77d..866f374 100644 --- a/src/backend/blindbit/backend/backend.rs +++ b/src/backend/blindbit/backend/backend.rs @@ -9,7 +9,10 @@ use futures::{stream, Stream, StreamExt}; use anyhow::Result; -use crate::{backend::blindbit::BlindbitClient, BlockData, ChainBackend, SpentIndexData, UtxoData}; +use crate::{backend::blindbit::BlindbitClient, BlockData, SpentIndexData, UtxoData}; + +#[cfg(not(target_arch = "wasm32"))] +use crate::backend::ChainBackend; #[cfg(target_arch = "wasm32")] use crate::backend::ChainBackendWasm; diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 89c767e..1dd45aa 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -3,11 +3,12 @@ mod backend; mod blindbit; mod structs; -pub use backend::ChainBackend; - #[cfg(target_arch = "wasm32")] pub use backend::ChainBackendWasm; +#[cfg(not(target_arch = "wasm32"))] +pub use backend::ChainBackend; + pub use structs::*; #[cfg(feature = "blindbit-backend")] diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 2d6bea2..e35437c 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -1,3 +1,344 @@ -mod scanner; +use std::collections::{HashMap, HashSet}; -pub use scanner::SpScanner; +use anyhow::{Error, Result}; +use bitcoin::{ + absolute::Height, bip158::BlockFilter, hashes::{sha256, Hash}, + Amount, BlockHash, OutPoint, Txid, XOnlyPublicKey +}; +use futures::Stream; +use silentpayments::receiving::Label; + +use crate::{ + backend::{BlockData, FilterData, UtxoData}, + client::{OwnedOutput, SpClient}, + updater::Updater, +}; + +#[cfg(not(target_arch = "wasm32"))] +use crate::backend::ChainBackend; + +#[cfg(target_arch = "wasm32")] +use crate::backend::ChainBackendWasm; + +/// Trait for scanning silent payment blocks +/// +/// This trait abstracts the core scanning functionality, allowing consumers +/// to implement it with their own constraints and requirements. +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +pub trait SpScanner { + /// Scan a range of blocks for silent payment outputs and inputs + /// + /// # Arguments + /// * `start` - Starting block height (inclusive) + /// * `end` - Ending block height (inclusive) + /// * `dust_limit` - Minimum amount to consider (dust outputs are ignored) + /// * `with_cutthrough` - Whether to use cutthrough optimization + async fn scan_blocks( + &mut self, + start: Height, + end: Height, + dust_limit: Amount, + with_cutthrough: bool, + ) -> Result<()>; + + /// Process a single block's data + /// + /// # Arguments + /// * `blockdata` - Block data containing tweaks and filters + /// + /// # Returns + /// * `(found_outputs, found_inputs)` - Tuple of found outputs and spent inputs + async fn process_block( + &mut self, + blockdata: BlockData, + ) -> Result<(HashMap, HashSet)>; + + /// Process block outputs to find owned silent payment outputs + /// + /// # Arguments + /// * `blkheight` - Block height + /// * `tweaks` - List of tweak public keys + /// * `new_utxo_filter` - Filter data for new UTXOs + /// + /// # Returns + /// * Map of outpoints to owned outputs + async fn process_block_outputs( + &self, + blkheight: Height, + tweaks: Vec, + new_utxo_filter: FilterData, + ) -> Result>; + + /// Process block inputs to find spent outputs + /// + /// # Arguments + /// * `blkheight` - Block height + /// * `spent_filter` - Filter data for spent outputs + /// + /// # Returns + /// * Set of spent outpoints + async fn process_block_inputs( + &self, + blkheight: Height, + spent_filter: FilterData, + ) -> Result>; + + /// Get the block data stream for a range of blocks + /// + /// # Arguments + /// * `range` - Range of block heights + /// * `dust_limit` - Minimum amount to consider + /// * `with_cutthrough` - Whether to use cutthrough optimization + /// + /// # Returns + /// * Stream of block data results + fn get_block_data_stream( + &self, + range: std::ops::RangeInclusive, + dust_limit: Amount, + with_cutthrough: bool, + ) -> std::pin::Pin> + Send>>; + + /// Check if scanning should be interrupted + /// + /// # Returns + /// * `true` if scanning should stop, `false` otherwise + fn should_interrupt(&self) -> bool; + + /// Save current state to persistent storage + fn save_state(&mut self) -> Result<()>; + + /// Record found outputs for a block + /// + /// # Arguments + /// * `height` - Block height + /// * `block_hash` - Block hash + /// * `outputs` - Found outputs + fn record_outputs( + &mut self, + height: Height, + block_hash: BlockHash, + outputs: HashMap, + ) -> Result<()>; + + /// Record spent inputs for a block + /// + /// # Arguments + /// * `height` - Block height + /// * `block_hash` - Block hash + /// * `inputs` - Spent inputs + fn record_inputs( + &mut self, + height: Height, + block_hash: BlockHash, + inputs: HashSet, + ) -> Result<()>; + + /// Record scan progress + /// + /// # Arguments + /// * `start` - Start height + /// * `current` - Current height + /// * `end` - End height + fn record_progress(&mut self, start: Height, current: Height, end: Height) -> Result<()>; + + /// Get the silent payment client + fn client(&self) -> &SpClient; + + /// Get the chain backend + #[cfg(not(target_arch = "wasm32"))] + fn backend(&self) -> &dyn ChainBackend; + + /// Get the chain backend (WASM version) + #[cfg(target_arch = "wasm32")] + fn backend(&self) -> &dyn ChainBackendWasm; + + /// Get the updater + fn updater(&mut self) -> &mut dyn Updater; + + // Helper methods with default implementations + + /// Process multiple blocks from a stream + /// + /// This is a default implementation that can be overridden if needed + async fn process_blocks( + &mut self, + start: Height, + end: Height, + block_data_stream: impl Stream> + Unpin + Send, + ) -> Result<()> { + use futures::StreamExt; + use std::time::{Duration, Instant}; + + let mut update_time = Instant::now(); + let mut stream = block_data_stream; + + while let Some(blockdata) = stream.next().await { + let blockdata = blockdata?; + let blkheight = blockdata.blkheight; + let blkhash = blockdata.blkhash; + + // stop scanning and return if interrupted + if self.should_interrupt() { + self.save_state()?; + return Ok(()); + } + + let mut save_to_storage = false; + + // always save on last block or after 30 seconds since last save + if blkheight == end || update_time.elapsed() > Duration::from_secs(30) { + save_to_storage = true; + } + + let (found_outputs, found_inputs) = self.process_block(blockdata).await?; + + if !found_outputs.is_empty() { + save_to_storage = true; + self.record_outputs(blkheight, blkhash, found_outputs)?; + } + + if !found_inputs.is_empty() { + save_to_storage = true; + self.record_inputs(blkheight, blkhash, found_inputs)?; + } + + // tell the updater we scanned this block + self.record_progress(start, blkheight, end)?; + + if save_to_storage { + self.save_state()?; + update_time = Instant::now(); + } + } + + Ok(()) + } + + /// Scan UTXOs for a given block and secrets map + /// + /// This is a default implementation that can be overridden if needed + async fn scan_utxos( + &self, + blkheight: Height, + secrets_map: HashMap<[u8; 34], bitcoin::secp256k1::PublicKey>, + ) -> Result, UtxoData, bitcoin::secp256k1::Scalar)>> { + let utxos = self.backend().utxos(blkheight).await?; + + let mut res: Vec<(Option