From 7c59bd34c813c48b7a0dae150e89eeba5a3d2245 Mon Sep 17 00:00:00 2001 From: Alberto De Luigi <43697715+albertodeluigi@users.noreply.github.com> Date: Thu, 8 Oct 2026 03:19:41 +0200 Subject: [PATCH] esplora: fetch address histories concurrently get_scripts_history_esplora walked each batch one address at a time, so the concurrency the caller sets reached only transactions and headers. A wallet's scan is mostly address histories: every used address plus the 20-address gap on each chain. For a fresh testnet web wallet that was 44 requests in strict sequence, about 7 s behind a 120 ms round trip, with concurrency set to 8 or to 1 alike. The batch now goes through the same stream as get_transactions, up to `concurrency` requests in flight. `buffered` keeps the results in address order, which get_history needs to find the last used index. With the default concurrency of 1 nothing changes. --- lwk_wollet/src/clients/asyncr/esplora.rs | 46 ++++++++++++++---------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/lwk_wollet/src/clients/asyncr/esplora.rs b/lwk_wollet/src/clients/asyncr/esplora.rs index 4f861719c..10c0dfc8a 100644 --- a/lwk_wollet/src/clients/asyncr/esplora.rs +++ b/lwk_wollet/src/clients/asyncr/esplora.rs @@ -211,25 +211,35 @@ impl EsploraClient { &self, addresses: &[Address], ) -> Result>, Error> { - let mut result = vec![]; - for address in addresses.iter() { - let url = format!("{}/address/{}/txs", self.base_url, address); - // TODO must handle paging -> https://github.com/blockstream/esplora/blob/master/API.md#addresses - let response = self.get_with_retry(&url).await?; + // SEQUENTIA: up to `concurrency` address histories in flight at once, as get_transactions + // and get_headers already do. Upstream walks the batch one request after another, so a + // first scan (every used address plus the 20-address gap, per chain) costs one round trip + // per address: ~44 sequential requests, about 7 s for a fresh testnet wallet behind a + // 120 ms round trip, whatever concurrency the caller asked for. `buffered` (not + // `buffer_unordered`) keeps the results in address order, which get_history relies on to + // find the last used index. + let stream = iter(addresses.iter()) + .map(|address| async move { + let url = format!("{}/address/{}/txs", self.base_url, address); + // TODO must handle paging -> https://github.com/blockstream/esplora/blob/master/API.md#addresses + let response = self.get_with_retry(&url).await?; - // TODO going through string and then json is not as efficient as it could be but we prioritize debugging for now - let text = response.text().await?; - let json: Vec = match serde_json::from_str(&text) { - Ok(e) => e, - Err(e) => { - log::warn!("error {e:?} in converting following text:\n{text}"); - return Err(e.into()); - } - }; - let history: Vec = json.into_iter().map(Into::into).collect(); - result.push(history) - } - Ok(result) + // TODO going through string and then json is not as efficient as it could be but we prioritize debugging for now + let text = response.text().await?; + let json: Vec = match serde_json::from_str(&text) { + Ok(e) => e, + Err(e) => { + log::warn!("error {e:?} in converting following text:\n{text}"); + return Err(e.into()); + } + }; + let history: Vec = json.into_iter().map(Into::into).collect(); + Ok::, Error>(history) + }) + .buffered(self.concurrency); + + let results: Vec, Error>> = stream.collect().await; + results.into_iter().collect() } async fn get_scripts_history_waterfalls(