From c4fe733abb206a22f4f677b5422916e8f7ba69c6 Mon Sep 17 00:00:00 2001 From: Dario Anongba Varela Date: Wed, 30 Jul 2025 15:25:49 +0200 Subject: [PATCH 1/4] retry mechanism done --- Cargo.lock | 13 ------ Cargo.toml | 1 - src/bitcoin/esplora_client.rs | 56 ++++++++++++++++++++------ src/utils/future.rs | 24 ----------- src/utils/mod.rs | 2 - tests/browser.rs | 2 +- tests/node/integration/esplora.test.ts | 2 +- 7 files changed, 45 insertions(+), 55 deletions(-) delete mode 100644 src/utils/future.rs diff --git a/Cargo.lock b/Cargo.lock index af5a246..a19fd36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,7 +237,6 @@ dependencies = [ "bitcoin", "console_error_panic_hook", "getrandom 0.2.16", - "gloo-timers", "serde", "serde-wasm-bindgen", "wasm-bindgen", @@ -496,18 +495,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "hashbrown" version = "0.14.5" diff --git a/Cargo.toml b/Cargo.toml index b2877cb..11e4914 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,6 @@ web-sys = { version = "0.3.77", default-features = false } # Compatibility to compile to WASM getrandom = { version = "0.2.16", features = ["js"] } -gloo-timers = { version = "0.3.0", features = ["futures"] } # Bitcoin dependencies bdk_wallet = { version = "2.0.0" } diff --git a/src/bitcoin/esplora_client.rs b/src/bitcoin/esplora_client.rs index a4c474a..efc8587 100644 --- a/src/bitcoin/esplora_client.rs +++ b/src/bitcoin/esplora_client.rs @@ -6,29 +6,38 @@ use bdk_wallet::{ chain::spk_client::{FullScanRequest as BdkFullScanRequest, SyncRequest as BdkSyncRequest}, KeychainKind, }; -use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{ + prelude::{wasm_bindgen, Closure}, + JsCast, JsValue, +}; +use wasm_bindgen_futures::JsFuture; +use web_sys::js_sys::{Function, Promise}; use crate::{ result::JsResult, types::{FeeEstimates, FullScanRequest, SyncRequest, Transaction, Txid, Update}, }; -use std::time::Duration; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; use bdk_esplora::esplora_client::Sleeper; -use gloo_timers::future::{sleep, TimeoutFuture}; - -use crate::utils::SendSyncWrapper; #[wasm_bindgen] pub struct EsploraClient { - client: AsyncClient, + client: AsyncClient, } #[wasm_bindgen] impl EsploraClient { #[wasm_bindgen(constructor)] - pub fn new(url: &str) -> JsResult { - let client = Builder::new(url).build_async_with_sleeper::()?; + pub fn new(url: &str, max_retries: usize) -> JsResult { + let client = Builder::new(url) + .max_retries(max_retries) + .build_async_with_sleeper::()?; Ok(EsploraClient { client }) } @@ -65,13 +74,34 @@ impl EsploraClient { } } -#[derive(Clone)] -struct WebSleeper; +struct WasmSleep(JsFuture); + +impl Future for WasmSleep { + type Output = (); + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + // delegate to the inner JsFuture + Pin::new(&mut self.get_mut().0).poll(cx).map(|_| ()) + } +} + +// SAFETY: Wasm is single-threaded; the value is never accessed concurrently. +unsafe impl Send for WasmSleep {} + +#[derive(Clone, Copy)] +struct WasmSleeper; -impl Sleeper for WebSleeper { - type Sleep = SendSyncWrapper; +impl Sleeper for WasmSleeper { + type Sleep = WasmSleep; fn sleep(dur: Duration) -> Self::Sleep { - SendSyncWrapper(sleep(dur)) + let ms = dur.as_millis() as i32; + let promise = Promise::new(&mut |resolve, _reject| { + let cb = Closure::once_into_js(move || resolve.call0(&JsValue::NULL).unwrap()); + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0(cb.unchecked_ref::(), ms) + .unwrap(); + }); + WasmSleep(JsFuture::from(promise)) } } diff --git a/src/utils/future.rs b/src/utils/future.rs deleted file mode 100644 index 61d8e2d..0000000 --- a/src/utils/future.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; - -// Wrap a future that is not `Send` or `Sync` and make it `Send` and `Sync` -pub struct SendSyncWrapper(pub F); - -unsafe impl Send for SendSyncWrapper {} -unsafe impl Sync for SendSyncWrapper {} - -impl Future for SendSyncWrapper -where - F: Future, -{ - type Output = F::Output; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // SAFETY: Since we're in a single-threaded WASM environment, this is safe. - unsafe { - let this = self.get_unchecked_mut(); - Pin::new_unchecked(&mut this.0).poll(cx) - } - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index da66655..96ba485 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,12 +1,10 @@ mod descriptor; -mod future; #[cfg(feature = "debug")] mod panic_hook; pub mod result; pub use descriptor::*; -pub use future::SendSyncWrapper; #[cfg(feature = "debug")] pub use panic_hook::set_panic_hook; diff --git a/tests/browser.rs b/tests/browser.rs index 2dcb50a..241f4c3 100644 --- a/tests/browser.rs +++ b/tests/browser.rs @@ -23,7 +23,7 @@ async fn test_browser() { "wpkh(tprv8ZgxMBicQKsPe2qpAuh1K1Hig72LCoP4JgNxZM2ZRWHZYnpuw5oHoGBsQm7Qb8mLgPpRJVn3hceWgGQRNbPD6x1pp2Qme2YFRAPeYh7vmvE/84'/1'/0'/0/*)#a6kgzlgq".into(), "wpkh(tprv8ZgxMBicQKsPe2qpAuh1K1Hig72LCoP4JgNxZM2ZRWHZYnpuw5oHoGBsQm7Qb8mLgPpRJVn3hceWgGQRNbPD6x1pp2Qme2YFRAPeYh7vmvE/84'/1'/0'/1/*)#vwnfl2cc".into(), ).expect("wallet"); - let blockchain_client = EsploraClient::new("https://mutinynet.com/api").expect("esplora_client"); + let blockchain_client = EsploraClient::new("https://mutinynet.com/api", 6).expect("esplora_client"); let block_height = wallet.latest_checkpoint().height(); assert_eq!(block_height, 0); diff --git a/tests/node/integration/esplora.test.ts b/tests/node/integration/esplora.test.ts index 9e2de11..610c064 100644 --- a/tests/node/integration/esplora.test.ts +++ b/tests/node/integration/esplora.test.ts @@ -28,7 +28,7 @@ describe("Esplora client", () => { let feeRate: FeeRate; let wallet: Wallet; - const esploraClient = new EsploraClient(esploraUrl); + const esploraClient = new EsploraClient(esploraUrl, 0); it("creates a new wallet", () => { wallet = Wallet.create(network, externalDescriptor, internalDescriptor); From 7b9ce26e036b5dfb898666ba1b50a2a0547ffe6f Mon Sep 17 00:00:00 2001 From: Dario Anongba Varela Date: Wed, 30 Jul 2025 15:39:06 +0200 Subject: [PATCH 2/4] add window feature --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 11e4914..e12a7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,9 @@ wasm-bindgen-futures = { version = "0.4.50", optional = true } anyhow = { version = "1.0.98", default-features = false } serde = { version = "1.0.219", default-features = false, features = ["derive"] } serde-wasm-bindgen = "0.6.5" -web-sys = { version = "0.3.77", default-features = false } +web-sys = { version = "0.3.77", default-features = false, features = [ + "Window", +] } # Compatibility to compile to WASM getrandom = { version = "0.2.16", features = ["js"] } From 6c2bd73c1e0f033c5d15ea90c11b89ec0d19428e Mon Sep 17 00:00:00 2001 From: Dario Anongba Varela Date: Wed, 30 Jul 2025 15:47:39 +0200 Subject: [PATCH 3/4] do not use window --- Cargo.toml | 4 +--- src/bitcoin/esplora_client.rs | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e12a7fc..11e4914 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,7 @@ wasm-bindgen-futures = { version = "0.4.50", optional = true } anyhow = { version = "1.0.98", default-features = false } serde = { version = "1.0.219", default-features = false, features = ["derive"] } serde-wasm-bindgen = "0.6.5" -web-sys = { version = "0.3.77", default-features = false, features = [ - "Window", -] } +web-sys = { version = "0.3.77", default-features = false } # Compatibility to compile to WASM getrandom = { version = "0.2.16", features = ["js"] } diff --git a/src/bitcoin/esplora_client.rs b/src/bitcoin/esplora_client.rs index efc8587..f023f05 100644 --- a/src/bitcoin/esplora_client.rs +++ b/src/bitcoin/esplora_client.rs @@ -8,10 +8,10 @@ use bdk_wallet::{ }; use wasm_bindgen::{ prelude::{wasm_bindgen, Closure}, - JsCast, JsValue, + JsCast, JsError, JsValue, }; use wasm_bindgen_futures::JsFuture; -use web_sys::js_sys::{Function, Promise}; +use web_sys::js_sys::{global, Function, Promise, Reflect}; use crate::{ result::JsResult, @@ -94,14 +94,19 @@ impl Sleeper for WasmSleeper { type Sleep = WasmSleep; fn sleep(dur: Duration) -> Self::Sleep { - let ms = dur.as_millis() as i32; + let ms = dur.as_millis(); let promise = Promise::new(&mut |resolve, _reject| { - let cb = Closure::once_into_js(move || resolve.call0(&JsValue::NULL).unwrap()); - web_sys::window() - .unwrap() - .set_timeout_with_callback_and_timeout_and_arguments_0(cb.unchecked_ref::(), ms) - .unwrap(); + let cb = Closure::once_into_js(move || resolve.call0(&JsValue::NULL).unwrap()).unchecked_into::(); + + // globalThis.setTimeout(cb, ms); + let g = global(); + let set_timeout = Reflect::get(&g, &JsValue::from_str("setTimeout")) + .unwrap_or_else(|_| JsError::new("setTimeout not found").into()) + .unchecked_into::(); + + set_timeout.call2(&g, &cb, &JsValue::from_f64(ms as f64)).unwrap(); }); + WasmSleep(JsFuture::from(promise)) } } From 77de328c5bc6dee06ab5adaa3c688454ff1bfb32 Mon Sep 17 00:00:00 2001 From: Dario Anongba Varela Date: Wed, 30 Jul 2025 15:58:58 +0200 Subject: [PATCH 4/4] keep window but make sure to call it WebSleeper --- Cargo.toml | 4 +++- src/bitcoin/esplora_client.rs | 39 +++++++++++++++-------------------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11e4914..e12a7fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,9 @@ wasm-bindgen-futures = { version = "0.4.50", optional = true } anyhow = { version = "1.0.98", default-features = false } serde = { version = "1.0.219", default-features = false, features = ["derive"] } serde-wasm-bindgen = "0.6.5" -web-sys = { version = "0.3.77", default-features = false } +web-sys = { version = "0.3.77", default-features = false, features = [ + "Window", +] } # Compatibility to compile to WASM getrandom = { version = "0.2.16", features = ["js"] } diff --git a/src/bitcoin/esplora_client.rs b/src/bitcoin/esplora_client.rs index f023f05..149f34b 100644 --- a/src/bitcoin/esplora_client.rs +++ b/src/bitcoin/esplora_client.rs @@ -8,10 +8,10 @@ use bdk_wallet::{ }; use wasm_bindgen::{ prelude::{wasm_bindgen, Closure}, - JsCast, JsError, JsValue, + JsCast, JsValue, }; use wasm_bindgen_futures::JsFuture; -use web_sys::js_sys::{global, Function, Promise, Reflect}; +use web_sys::js_sys::{Function, Promise}; use crate::{ result::JsResult, @@ -28,7 +28,7 @@ use bdk_esplora::esplora_client::Sleeper; #[wasm_bindgen] pub struct EsploraClient { - client: AsyncClient, + client: AsyncClient, } #[wasm_bindgen] @@ -37,7 +37,7 @@ impl EsploraClient { pub fn new(url: &str, max_retries: usize) -> JsResult { let client = Builder::new(url) .max_retries(max_retries) - .build_async_with_sleeper::()?; + .build_async_with_sleeper::()?; Ok(EsploraClient { client }) } @@ -74,9 +74,9 @@ impl EsploraClient { } } -struct WasmSleep(JsFuture); +struct WebSleep(JsFuture); -impl Future for WasmSleep { +impl Future for WebSleep { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { // delegate to the inner JsFuture @@ -85,28 +85,23 @@ impl Future for WasmSleep { } // SAFETY: Wasm is single-threaded; the value is never accessed concurrently. -unsafe impl Send for WasmSleep {} +unsafe impl Send for WebSleep {} #[derive(Clone, Copy)] -struct WasmSleeper; +struct WebSleeper; -impl Sleeper for WasmSleeper { - type Sleep = WasmSleep; +impl Sleeper for WebSleeper { + type Sleep = WebSleep; fn sleep(dur: Duration) -> Self::Sleep { - let ms = dur.as_millis(); + let ms = dur.as_millis() as i32; let promise = Promise::new(&mut |resolve, _reject| { - let cb = Closure::once_into_js(move || resolve.call0(&JsValue::NULL).unwrap()).unchecked_into::(); - - // globalThis.setTimeout(cb, ms); - let g = global(); - let set_timeout = Reflect::get(&g, &JsValue::from_str("setTimeout")) - .unwrap_or_else(|_| JsError::new("setTimeout not found").into()) - .unchecked_into::(); - - set_timeout.call2(&g, &cb, &JsValue::from_f64(ms as f64)).unwrap(); + let cb = Closure::once_into_js(move || resolve.call0(&JsValue::NULL).unwrap()); + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0(cb.unchecked_ref::(), ms) + .unwrap(); }); - - WasmSleep(JsFuture::from(promise)) + WebSleep(JsFuture::from(promise)) } }