From 09138c9d4547e10ff9e75e2fe75473918bb6f97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= <32176319+danielgranhao@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:46:16 +0100 Subject: [PATCH] Wasm: wallet cache persistence (#868) * Expose wallet cache persister * Implement IndexedDB wallet cache persister * Refactor wallet persister interfaces * Implement Node Fs persister * Encrypt wallet updates * Remove unnecessary tokio_with_wasm features * Improve async persist logs * Fix flutter binding generation * Use dynamic dispatch for wallet_cache_persister * Optimize conditional compilation branching * Address review * Refactor structure --- lib/Cargo.lock | 2 + lib/core/src/lib.rs | 2 +- lib/core/src/sdk.rs | 40 +-- lib/core/src/signer.rs | 4 +- lib/core/src/test_utils/sdk.rs | 6 +- lib/core/src/{wallet.rs => wallet/mod.rs} | 152 +++++++----- lib/core/src/wallet/persister.rs | 71 ++++++ lib/wasm/Cargo.toml | 4 +- lib/wasm/Makefile | 17 +- lib/wasm/src/backup/indexed_db.rs | 71 ------ lib/wasm/src/backup/mod.rs | 136 ---------- lib/wasm/src/backup/node_fs.rs | 77 ------ lib/wasm/src/error.rs | 3 + lib/wasm/src/lib.rs | 55 ++-- lib/wasm/src/platform/browser/db_backup.rs | 80 ++++++ lib/wasm/src/platform/browser/mod.rs | 53 ++++ .../src/platform/browser/wallet_persister.rs | 234 ++++++++++++++++++ lib/wasm/src/platform/db_backup_common.rs | 135 ++++++++++ lib/wasm/src/platform/default.rs | 25 ++ lib/wasm/src/platform/mod.rs | 28 +++ lib/wasm/src/platform/node_js/db_backup.rs | 45 ++++ lib/wasm/src/platform/node_js/fs.rs | 74 ++++++ lib/wasm/src/platform/node_js/mod.rs | 53 ++++ .../src/platform/node_js/wallet_persister.rs | 155 ++++++++++++ .../src/platform/wallet_persister_common.rs | 132 ++++++++++ lib/wasm/src/utils.rs | 13 - 26 files changed, 1253 insertions(+), 414 deletions(-) rename lib/core/src/{wallet.rs => wallet/mod.rs} (87%) create mode 100644 lib/core/src/wallet/persister.rs delete mode 100644 lib/wasm/src/backup/indexed_db.rs delete mode 100644 lib/wasm/src/backup/mod.rs delete mode 100644 lib/wasm/src/backup/node_fs.rs create mode 100644 lib/wasm/src/platform/browser/db_backup.rs create mode 100644 lib/wasm/src/platform/browser/mod.rs create mode 100644 lib/wasm/src/platform/browser/wallet_persister.rs create mode 100644 lib/wasm/src/platform/db_backup_common.rs create mode 100644 lib/wasm/src/platform/default.rs create mode 100644 lib/wasm/src/platform/mod.rs create mode 100644 lib/wasm/src/platform/node_js/db_backup.rs create mode 100644 lib/wasm/src/platform/node_js/fs.rs create mode 100644 lib/wasm/src/platform/node_js/mod.rs create mode 100644 lib/wasm/src/platform/node_js/wallet_persister.rs create mode 100644 lib/wasm/src/platform/wallet_persister_common.rs delete mode 100644 lib/wasm/src/utils.rs diff --git a/lib/Cargo.lock b/lib/Cargo.lock index b1e9c4f..640e246 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -873,6 +873,7 @@ name = "breez-sdk-liquid-wasm" version = "0.7.2-dev1" dependencies = [ "anyhow", + "async-trait", "breez-sdk-liquid", "console_log", "getrandom 0.2.15", @@ -884,6 +885,7 @@ dependencies = [ "sdk-macros", "serde", "tokio", + "tokio_with_wasm", "tsify-next", "uuid", "wasm-bindgen", diff --git a/lib/core/src/lib.rs b/lib/core/src/lib.rs index a547b3e..8b723e3 100644 --- a/lib/core/src/lib.rs +++ b/lib/core/src/lib.rs @@ -183,7 +183,7 @@ pub mod receive_swap; pub(crate) mod recover; pub mod sdk; pub(crate) mod send_swap; -pub(crate) mod signer; +pub mod signer; pub(crate) mod swapper; pub(crate) mod sync; #[cfg(feature = "test-utils")] diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 47e68dd..fa9d70a 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -175,7 +175,7 @@ impl LiquidSdkBuilder { .get_wallet_dir(&self.config.working_dir, &fingerprint_hex) } - pub fn build(&self) -> Result> { + pub async fn build(&self) -> Result> { if let Some(breez_api_key) = &self.config.breez_api_key { LiquidSdk::validate_breez_api_key(breez_api_key)? } @@ -226,12 +226,15 @@ impl LiquidSdkBuilder { let onchain_wallet: Arc = match self.onchain_wallet.clone() { Some(onchain_wallet) => onchain_wallet, - None => Arc::new(LiquidOnchainWallet::new( - self.config.clone(), - cache_dir, - persister.clone(), - self.signer.clone(), - )?), + None => Arc::new( + LiquidOnchainWallet::new( + self.config.clone(), + cache_dir, + persister.clone(), + self.signer.clone(), + ) + .await?, + ), }; let event_manager = Arc::new(EventManager::new()); @@ -435,7 +438,8 @@ impl LiquidSdk { PRODUCTION_BREEZSERVER_URL.into(), Arc::new(signer), )? - .build()?; + .build() + .await?; sdk.start().await?; let init_time = Instant::now().duration_since(start_ts); @@ -4196,7 +4200,8 @@ mod tests { liquid_chain_service.clone(), bitcoin_chain_service.clone(), None, - )?; + ) + .await?; LiquidSdk::track_swap_updates(&sdk); @@ -4303,11 +4308,9 @@ mod tests { let swapper = Arc::new(MockSwapper::default()); let status_stream = Arc::new(MockStatusStream::new()); - let sdk = Arc::new(new_liquid_sdk( - persister.clone(), - swapper.clone(), - status_stream.clone(), - )?); + let sdk = Arc::new( + new_liquid_sdk(persister.clone(), swapper.clone(), status_stream.clone()).await?, + ); LiquidSdk::track_swap_updates(&sdk); @@ -4368,7 +4371,8 @@ mod tests { liquid_chain_service.clone(), bitcoin_chain_service.clone(), None, - )?; + ) + .await?; LiquidSdk::track_swap_updates(&sdk); @@ -4598,7 +4602,8 @@ mod tests { liquid_chain_service.clone(), bitcoin_chain_service.clone(), None, - )?; + ) + .await?; LiquidSdk::track_swap_updates(&sdk); @@ -4659,7 +4664,8 @@ mod tests { liquid_chain_service.clone(), bitcoin_chain_service.clone(), Some(onchain_fee_rate_leeway_sat_per_vbyte), - )?; + ) + .await?; LiquidSdk::track_swap_updates(&sdk); diff --git a/lib/core/src/signer.rs b/lib/core/src/signer.rs index 3c2d9a5..bbf49bb 100644 --- a/lib/core/src/signer.rs +++ b/lib/core/src/signer.rs @@ -76,7 +76,7 @@ impl SdkLwkSigner { Ok(Self { sdk_signer }) } - pub fn xpub(&self) -> Result { + pub(crate) fn xpub(&self) -> Result { let xpub = self.sdk_signer.xpub()?; Ok(Xpub::decode(&xpub)?) } @@ -88,7 +88,7 @@ impl SdkLwkSigner { Ok(f) } - pub fn sign_ecdsa_recoverable(&self, msg: &Message) -> Result, SignError> { + pub(crate) fn sign_ecdsa_recoverable(&self, msg: &Message) -> Result, SignError> { let sig_bytes = self .sdk_signer .sign_ecdsa_recoverable(msg.as_ref().to_vec())?; diff --git a/lib/core/src/test_utils/sdk.rs b/lib/core/src/test_utils/sdk.rs index 2f1fcae..d35943b 100644 --- a/lib/core/src/test_utils/sdk.rs +++ b/lib/core/src/test_utils/sdk.rs @@ -17,7 +17,7 @@ use super::{ wallet::{MockSigner, MockWallet}, }; -pub(crate) fn new_liquid_sdk( +pub(crate) async fn new_liquid_sdk( persister: Arc, swapper: Arc, status_stream: Arc, @@ -33,9 +33,10 @@ pub(crate) fn new_liquid_sdk( bitcoin_chain_service, None, ) + .await } -pub(crate) fn new_liquid_sdk_with_chain_services( +pub(crate) async fn new_liquid_sdk_with_chain_services( persister: Arc, swapper: Arc, status_stream: Arc, @@ -78,4 +79,5 @@ pub(crate) fn new_liquid_sdk_with_chain_services( .swapper(swapper) .sync_service(sync_service) .build() + .await } diff --git a/lib/core/src/wallet.rs b/lib/core/src/wallet/mod.rs similarity index 87% rename from lib/core/src/wallet.rs rename to lib/core/src/wallet/mod.rs index 0211148..8ebdde3 100644 --- a/lib/core/src/wallet.rs +++ b/lib/core/src/wallet/mod.rs @@ -1,3 +1,5 @@ +pub mod persister; + use std::collections::HashMap; use std::io::Write; use std::str::FromStr; @@ -12,9 +14,7 @@ use lwk_wollet::elements::hex::ToHex; use lwk_wollet::elements::pset::PartiallySignedTransaction; use lwk_wollet::elements::{Address, AssetId, OutPoint, Transaction, TxOut, Txid}; use lwk_wollet::secp256k1::Message; -use lwk_wollet::{ - ElementsNetwork, FsPersister, NoPersist, WalletTx, WalletTxOut, Wollet, WolletDescriptor, -}; +use lwk_wollet::{ElementsNetwork, FsPersister, WalletTx, WalletTxOut, Wollet, WolletDescriptor}; use maybe_sync::{MaybeSend, MaybeSync}; use sdk_common::bitcoin::hashes::{sha256, Hash}; use sdk_common::bitcoin::secp256k1::PublicKey; @@ -32,6 +32,9 @@ use crate::{ }; use sdk_common::utils::Arc; +use crate::wallet::persister::{ + FsWalletCachePersister, NoWalletCachePersister, WalletCachePersister, +}; #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] use lwk_wollet::blocking::BlockchainBackend; @@ -182,114 +185,117 @@ pub struct LiquidOnchainWallet { persister: Arc, wallet: Arc>, client: Mutex>, - working_dir: Option, pub(crate) signer: SdkLwkSigner, + wallet_cache_persister: Arc, } impl LiquidOnchainWallet { /// Creates a new LiquidOnchainWallet that caches data on the provided `working_dir`. - pub(crate) fn new( + pub(crate) async fn new( config: Config, working_dir: String, persister: Arc, user_signer: Arc>, ) -> Result { let signer = SdkLwkSigner::new(user_signer.clone())?; - let wollet = Self::create_wallet(&config, Some(&working_dir), &signer)?; - let working_dir_buf = std::path::PathBuf::from_str(&working_dir)?; - if !working_dir_buf.exists() { - std::fs::create_dir_all(&working_dir_buf)?; - } + let wallet_cache_persister: Arc = + Arc::new(FsWalletCachePersister::new( + working_dir.clone(), + FsPersister::new( + &working_dir, + config.network.into(), + &get_descriptor(&signer, config.network)?, + )?, + config.network.into(), + )?); + + let wollet = Self::create_wallet(&config, &signer, wallet_cache_persister.clone()).await?; Ok(Self { config, persister, wallet: Arc::new(Mutex::new(wollet)), client: Mutex::new(None), - working_dir: Some(working_dir), signer, + wallet_cache_persister, }) } /// Creates a new LiquidOnchainWallet that caches data in memory - pub fn new_in_memory( + pub async fn new_in_memory( config: Config, persister: Arc, user_signer: Arc>, ) -> Result { let signer = SdkLwkSigner::new(user_signer.clone())?; - let wollet = Self::create_wallet(&config, None, &signer)?; + + let wallet_cache_persister: Arc = + Arc::new(NoWalletCachePersister {}); + + let wollet = Self::create_wallet(&config, &signer, wallet_cache_persister.clone()).await?; Ok(Self { config, persister, wallet: Arc::new(Mutex::new(wollet)), client: Mutex::new(None), - working_dir: None, signer, + wallet_cache_persister, }) } - fn create_wallet( + /// Creates a new LiquidOnchainWallet with a custom cache persister implementation + pub async fn new_with_cache_persister( + config: Config, + persister: Arc, + user_signer: Arc>, + wallet_cache_persister: Arc, + ) -> Result { + let signer = SdkLwkSigner::new(user_signer.clone())?; + let wollet = Self::create_wallet(&config, &signer, wallet_cache_persister.clone()).await?; + + Ok(Self { + config, + persister, + wallet: Arc::new(Mutex::new(wollet)), + client: Mutex::new(None), + signer, + wallet_cache_persister, + }) + } + + async fn create_wallet( config: &Config, - working_dir: Option<&str>, signer: &SdkLwkSigner, + wallet_cache_persister: Arc, ) -> Result { let elements_network: ElementsNetwork = config.network.into(); - let descriptor = LiquidOnchainWallet::get_descriptor(signer, config.network)?; - let wollet_res = match &working_dir { - Some(working_dir) => Wollet::new( - elements_network, - FsPersister::new(working_dir, elements_network, &descriptor)?, - descriptor.clone(), - ), - None => Wollet::new(elements_network, NoPersist::new(), descriptor.clone()), - }; + let descriptor = get_descriptor(signer, config.network)?; + let wollet_res = Wollet::new( + elements_network, + wallet_cache_persister.get_lwk_persister(), + descriptor.clone(), + ); match wollet_res { Ok(wollet) => Ok(wollet), res @ Err( lwk_wollet::Error::PersistError(_) | lwk_wollet::Error::UpdateHeightTooOld { .. } | lwk_wollet::Error::UpdateOnDifferentStatus { .. }, - ) => match working_dir { - Some(working_dir) => { - warn!( - "Update error initialising wollet, wipping storage and retrying: {res:?}" - ); - let mut path = std::path::PathBuf::from(working_dir); - path.push(elements_network.as_str()); - std::fs::remove_dir_all(&path)?; - warn!("Wiping wallet in path: {:?}", path); - let lwk_persister = - FsPersister::new(working_dir, elements_network, &descriptor)?; - Ok(Wollet::new( - elements_network, - lwk_persister, - descriptor.clone(), - )?) - } - None => res.map_err(Into::into), - }, + ) => { + warn!("Update error initialising wollet, wiping cache and retrying: {res:?}"); + wallet_cache_persister.clear_cache().await?; + Ok(Wollet::new( + elements_network, + wallet_cache_persister.get_lwk_persister(), + descriptor.clone(), + )?) + } Err(e) => Err(e.into()), } } - fn get_descriptor( - signer: &SdkLwkSigner, - network: LiquidNetwork, - ) -> Result { - let is_mainnet = network == LiquidNetwork::Mainnet; - let descriptor_str = singlesig_desc( - signer, - Singlesig::Wpkh, - lwk_common::DescriptorBlindingKey::Slip77, - is_mainnet, - ) - .map_err(|e| anyhow!("Invalid descriptor: {e}"))?; - Ok(descriptor_str.parse()?) - } - async fn get_txout(&self, wallet: &Wollet, outpoint: &OutPoint) -> Result { let wallet_tx = wallet .transaction(&outpoint.txid)? @@ -303,6 +309,21 @@ impl LiquidOnchainWallet { } } +pub fn get_descriptor( + signer: &SdkLwkSigner, + network: LiquidNetwork, +) -> Result { + let is_mainnet = network == LiquidNetwork::Mainnet; + let descriptor_str = singlesig_desc( + signer, + Singlesig::Wpkh, + lwk_common::DescriptorBlindingKey::Slip77, + is_mainnet, + ) + .map_err(|e| anyhow!("Invalid descriptor: {e}"))?; + Ok(descriptor_str.parse()?) +} + #[sdk_macros::async_trait] impl OnchainWallet for LiquidOnchainWallet { /// List all transactions in the wallet @@ -566,8 +587,12 @@ impl OnchainWallet for LiquidOnchainWallet { Ok(()) => Ok(()), Err(lwk_wollet::Error::UpdateHeightTooOld { .. }) => { warn!("Full scan failed with update height too old, wiping storage and retrying"); - let mut new_wallet = - Self::create_wallet(&self.config, self.working_dir.as_deref(), &self.signer)?; + let mut new_wallet = Self::create_wallet( + &self.config, + &self.signer, + self.wallet_cache_persister.clone(), + ) + .await?; client .full_scan_to_index(&mut new_wallet, index_with_buffer) .await?; @@ -637,12 +662,15 @@ mod tests { .to_string(); Arc::new( LiquidOnchainWallet::new(config, working_dir, storage, sdk_signer.clone()) + .await .unwrap(), ) } #[cfg(all(target_family = "wasm", target_os = "unknown"))] Arc::new( - LiquidOnchainWallet::new_in_memory(config, storage, sdk_signer.clone()).unwrap(), + LiquidOnchainWallet::new_in_memory(config, storage, sdk_signer.clone()) + .await + .unwrap(), ) }; diff --git a/lib/core/src/wallet/persister.rs b/lib/core/src/wallet/persister.rs new file mode 100644 index 0000000..c8feee9 --- /dev/null +++ b/lib/core/src/wallet/persister.rs @@ -0,0 +1,71 @@ +use log::warn; +use lwk_wollet::{ElementsNetwork, FsPersister, NoPersist}; +use maybe_sync::{MaybeSend, MaybeSync}; +use std::path::PathBuf; +use std::str::FromStr; + +pub use lwk_wollet; + +pub type LwkPersister = std::sync::Arc; + +#[sdk_macros::async_trait] +pub trait WalletCachePersister: MaybeSend + MaybeSync { + fn get_lwk_persister(&self) -> LwkPersister; + + async fn clear_cache(&self) -> anyhow::Result<()>; +} + +#[derive(Clone)] +pub struct FsWalletCachePersister { + working_dir: String, + persister: std::sync::Arc, + elements_network: ElementsNetwork, +} + +impl FsWalletCachePersister { + pub(crate) fn new( + working_dir: String, + persister: std::sync::Arc, + elements_network: ElementsNetwork, + ) -> anyhow::Result { + let working_dir_buf = PathBuf::from_str(&working_dir)?; + if !working_dir_buf.exists() { + std::fs::create_dir_all(&working_dir_buf)?; + } + + Ok(Self { + working_dir, + persister, + elements_network, + }) + } +} + +#[sdk_macros::async_trait] +impl WalletCachePersister for FsWalletCachePersister { + fn get_lwk_persister(&self) -> LwkPersister { + self.persister.clone() + } + + async fn clear_cache(&self) -> anyhow::Result<()> { + let mut path = std::path::PathBuf::from(&self.working_dir); + path.push(self.elements_network.as_str()); + warn!("Wiping wallet in path: {:?}", path); + std::fs::remove_dir_all(&path)?; + Ok(()) + } +} + +#[derive(Clone)] +pub struct NoWalletCachePersister {} + +#[sdk_macros::async_trait] +impl WalletCachePersister for NoWalletCachePersister { + fn get_lwk_persister(&self) -> LwkPersister { + NoPersist::new() + } + + async fn clear_cache(&self) -> anyhow::Result<()> { + Ok(()) + } +} diff --git a/lib/wasm/Cargo.toml b/lib/wasm/Cargo.toml index 8c5b482..991db40 100644 --- a/lib/wasm/Cargo.toml +++ b/lib/wasm/Cargo.toml @@ -24,6 +24,7 @@ wasm-bindgen = "0.2.100" wasm-bindgen-futures = "0.4.50" web-time = "1.1.0" indexed_db_futures = "0.6.1" +async-trait = "0.1.88" [dev-dependencies] breez-sdk-liquid = { path = "../core", features = ["test-utils"] } @@ -32,7 +33,8 @@ getrandom = { version = "0.2", features = ["js"] } sdk-common = { workspace = true, features = ["test-utils"] } wasm-bindgen-test = "0.3.33" uuid = "1.16.0" +tokio_with_wasm = { version = "0.8.2" } [features] +browser = [] node-js = [] -browser-tests = [] diff --git a/lib/wasm/Makefile b/lib/wasm/Makefile index 1e2d40f..647d4f2 100644 --- a/lib/wasm/Makefile +++ b/lib/wasm/Makefile @@ -8,18 +8,21 @@ init: cargo install wasm-pack rustup target add wasm32-unknown-unknown -clippy: clippy-base clippy-node +clippy: clippy-default clippy-browser clippy-node -clippy-base: +clippy-default: $(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown -- -D warnings +clippy-browser: + $(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown --features browser -- -D warnings + clippy-node: $(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown --features node-js -- -D warnings build: build-bundle build-deno build-node build-web build-bundle: - $(CLANG_PREFIX) wasm-pack build --target bundler --release --out-dir pkg/bundle + $(CLANG_PREFIX) wasm-pack build --target bundler --release --out-dir pkg/bundle -- features browser build-deno: $(CLANG_PREFIX) wasm-pack build --target deno --release --out-dir pkg/deno @@ -28,7 +31,7 @@ build-node: $(CLANG_PREFIX) wasm-pack build --target nodejs --release --out-dir pkg/node --features node-js build-web: - $(CLANG_PREFIX) wasm-pack build --target web --release --out-dir pkg/web + $(CLANG_PREFIX) wasm-pack build --target web --release --out-dir pkg/web -- features browser test: test-firefox test-node @@ -36,10 +39,10 @@ test-node: $(CLANG_PREFIX) wasm-pack test --node --features node-js test-firefox: - $(CLANG_PREFIX) wasm-pack test --headless --firefox --features browser-tests + $(CLANG_PREFIX) wasm-pack test --headless --firefox --features browser test-chrome: - $(CLANG_PREFIX) wasm-pack test --headless --chrome --features browser-tests + $(CLANG_PREFIX) wasm-pack test --headless --chrome --features browser test-safari: - $(CLANG_PREFIX) wasm-pack test --headless --safari --features browser-tests + $(CLANG_PREFIX) wasm-pack test --headless --safari --features browser diff --git a/lib/wasm/src/backup/indexed_db.rs b/lib/wasm/src/backup/indexed_db.rs deleted file mode 100644 index 608daec..0000000 --- a/lib/wasm/src/backup/indexed_db.rs +++ /dev/null @@ -1,71 +0,0 @@ -use anyhow::Result; -use indexed_db_futures::{ - database::Database, query_source::QuerySource, transaction::TransactionMode, Build, -}; -use js_sys::{global, Reflect}; - -const IDB_STORE_NAME: &str = "BREEZ_SDK_LIQUID_DB_BACKUP_STORE"; - -pub(crate) fn is_indexed_db_supported() -> bool { - let global = global(); - Reflect::get(&global, &"indexedDB".into()).is_ok_and(|v| !v.is_undefined()) -} - -pub(crate) async fn backup_to_indexed_db(db_bytes: Vec, db_name: &str) -> Result<()> { - let idb = open_indexed_db(db_name).await?; - let tx = idb - .transaction([IDB_STORE_NAME]) - .with_mode(TransactionMode::Readwrite) - .build() - .map_err(|e| anyhow::anyhow!("Failed to build transaction: {}", e))?; - - let store = tx - .object_store(IDB_STORE_NAME) - .map_err(|e| anyhow::anyhow!("Failed to open object store: {}", e))?; - - store - .put(db_bytes) - .with_key(1) - .await - .map_err(|e| anyhow::anyhow!("Failed to put key in db: {}", e))?; - - tx.commit() - .await - .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {}", e))?; - - Ok(()) -} - -pub(crate) async fn load_indexed_db_backup(db_name: &str) -> Result>> { - let idb = open_indexed_db(db_name).await?; - - let tx = idb - .transaction([IDB_STORE_NAME]) - .with_mode(TransactionMode::Readonly) - .build() - .map_err(|e| anyhow::anyhow!("Failed to build transaction: {}", e))?; - - let store = tx - .object_store(IDB_STORE_NAME) - .map_err(|e| anyhow::anyhow!("Failed to open object store: {}", e))?; - - store - .get(1) - .await - .map_err(|e| anyhow::anyhow!("Failed to get data: {}", e)) -} - -pub(crate) async fn open_indexed_db(name: &str) -> Result { - let db = Database::open(name) - .with_version(1u32) - .with_on_upgrade_needed(|event, db| { - if let (0.0, Some(1.0)) = (event.old_version(), event.new_version()) { - db.create_object_store(IDB_STORE_NAME).build()?; - } - - Ok(()) - }) - .await - .map_err(|e| anyhow::anyhow!("Failed to open IndexedDB: {}", e))?; - Ok(db) -} diff --git a/lib/wasm/src/backup/mod.rs b/lib/wasm/src/backup/mod.rs deleted file mode 100644 index d7638ad..0000000 --- a/lib/wasm/src/backup/mod.rs +++ /dev/null @@ -1,136 +0,0 @@ -mod indexed_db; -mod node_fs; - -use crate::utils::PathExt; -use anyhow::Result; -use breez_sdk_liquid::model::{EventListener, SdkEvent}; -use breez_sdk_liquid::persist::Persister; -use indexed_db::{backup_to_indexed_db, is_indexed_db_supported, load_indexed_db_backup}; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use tokio::sync::mpsc::{Receiver, Sender}; - -pub(crate) struct ForwardingEventListener { - sender: Sender, -} - -impl ForwardingEventListener { - pub fn new(sender: Sender) -> Self { - Self { sender } - } -} - -impl EventListener for ForwardingEventListener { - fn on_event(&self, e: SdkEvent) { - if let Err(e) = self.sender.try_send(e) { - log::error!("Failed to forward event: {:?}", e); - } - } -} - -pub(crate) fn start_backup_task( - persister: Rc, - mut receiver: Receiver, - backup_dir_path: PathBuf, -) { - wasm_bindgen_futures::spawn_local(async move { - while let Some(e) = receiver.recv().await { - let res = match e { - SdkEvent::Synced => backup(&persister, &backup_dir_path).await, - SdkEvent::DataSynced { - did_pull_new_records, - } if did_pull_new_records => backup(&persister, &backup_dir_path).await, - _ => continue, - }; - if let Err(e) = res { - log::error!("Failed to backup to IndexedDB: {:?}", e); - }; - } - }); -} - -async fn backup(persister: &Rc, backup_dir_path: &Path) -> Result<()> { - let start = web_time::Instant::now(); - - let db_bytes = persister.serialize()?; - - if is_indexed_db_supported() { - backup_to_indexed_db(db_bytes, backup_dir_path.to_str_safe()?).await?; - } else { - #[cfg(not(feature = "node-js"))] - return Err(anyhow::anyhow!("No backup mechanism available")); - #[cfg(feature = "node-js")] - node_fs::backup_to_file_system(db_bytes, backup_dir_path)?; - } - - let backup_duration_ms = start.elapsed().as_millis(); - log::info!("Backup completed successfully ({backup_duration_ms} ms)"); - Ok(()) -} - -pub(crate) async fn load_backup(backup_dir_path: &Path) -> Result>> { - let maybe_data = if is_indexed_db_supported() { - load_indexed_db_backup(backup_dir_path.to_str_safe()?).await? - } else { - #[cfg(not(feature = "node-js"))] - return Err(anyhow::anyhow!("No backup restore mechanism available")); - #[cfg(feature = "node-js")] - node_fs::load_file_system_backup(backup_dir_path)? - }; - Ok(maybe_data) -} - -#[cfg(test)] -mod tests { - use crate::backup::backup; - use crate::backup::load_backup; - use std::path::PathBuf; - use std::str::FromStr; - - use breez_sdk_liquid::model::PaymentState; - use breez_sdk_liquid::persist::Persister; - use breez_sdk_liquid::prelude::LiquidNetwork; - use breez_sdk_liquid::test_utils::persist::{ - create_persister, new_receive_swap, new_send_swap, - }; - - #[cfg(feature = "browser-tests")] - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - #[sdk_macros::async_test_wasm] - async fn test_backup_and_restore() -> anyhow::Result<()> { - create_persister!(local); - - local.test_insert_or_update_send_swap(&new_send_swap(Some(PaymentState::Pending), None))?; - local.test_insert_or_update_receive_swap(&new_receive_swap( - Some(PaymentState::Pending), - None, - ))?; - assert_eq!(local.test_list_ongoing_swaps()?.len(), 2); - - let backup_dir_path = PathBuf::from_str(&format!("/tmp/{}", uuid::Uuid::new_v4()))?; - backup(&local, &backup_dir_path).await?; - - let backup_bytes = load_backup(&backup_dir_path).await?; - let remote = - Persister::new_in_memory("remote", LiquidNetwork::Testnet, false, None, backup_bytes)?; - assert_eq!(remote.test_list_ongoing_swaps()?.len(), 2); - - // Try again to verify that a new backup overwrites an old one - local.test_insert_or_update_send_swap(&new_send_swap(Some(PaymentState::Pending), None))?; - local.test_insert_or_update_receive_swap(&new_receive_swap( - Some(PaymentState::Pending), - None, - ))?; - assert_eq!(local.test_list_ongoing_swaps()?.len(), 4); - - backup(&local, &backup_dir_path).await?; - - let backup_bytes = load_backup(&backup_dir_path).await?; - let remote = - Persister::new_in_memory("remote", LiquidNetwork::Testnet, false, None, backup_bytes)?; - assert_eq!(remote.test_list_ongoing_swaps()?.len(), 4); - - Ok(()) - } -} diff --git a/lib/wasm/src/backup/node_fs.rs b/lib/wasm/src/backup/node_fs.rs deleted file mode 100644 index 7a47b28..0000000 --- a/lib/wasm/src/backup/node_fs.rs +++ /dev/null @@ -1,77 +0,0 @@ -#![cfg(feature = "node-js")] - -use crate::utils::PathExt; -use anyhow::Result; -use js_sys::Reflect; -use std::path::{Path, PathBuf}; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; - -#[wasm_bindgen(module = "fs")] -extern "C" { - #[wasm_bindgen(js_name = writeFileSync, catch)] - fn write_file_sync(path: &str, data: &js_sys::Uint8Array) -> Result<(), JsValue>; - - #[wasm_bindgen(js_name = readFileSync, catch)] - fn read_file_sync(path: &str) -> Result; - - #[wasm_bindgen(js_name = existsSync)] - fn exists_sync(path: &str) -> bool; - - #[wasm_bindgen(js_name = mkdirSync, catch)] - fn mkdir_sync(path: &str, options: &JsValue) -> Result<(), JsValue>; -} - -fn get_backup_file_path(backup_dir_path: &Path) -> PathBuf { - backup_dir_path.join("backup.sql") -} - -pub fn ensure_dir_exists(path: &str) -> Result<(), JsValue> { - if !exists_sync(path) { - let options = js_sys::Object::new(); - Reflect::set(&options, &"recursive".into(), &true.into())?; - mkdir_sync(path, &options)?; - } - Ok(()) -} - -pub(crate) fn backup_to_file_system(db_bytes: Vec, backup_dir_path: &Path) -> Result<()> { - let uint8_array = js_sys::Uint8Array::from(db_bytes.as_slice()); - - ensure_dir_exists(backup_dir_path.to_str_safe()?) - .map_err(|e| anyhow::anyhow!("Failed to create backup directory: {:?}", e))?; - - let backup_file_path = get_backup_file_path(backup_dir_path); - write_file_sync(backup_file_path.to_str_safe()?, &uint8_array).map_err(|e| { - anyhow::anyhow!( - "Failed to write backup to file system using fs.writeFileSync: {:?}", - e - ) - })?; - - Ok(()) -} - -pub(crate) fn load_file_system_backup(backup_dir_path: &Path) -> Result>> { - let backup_file_path = get_backup_file_path(backup_dir_path); - let backup_file_path_str = backup_file_path.to_str_safe()?; - if !exists_sync(backup_file_path_str) { - log::debug!("Backup file '{backup_file_path:?}' not found."); - return Ok(None); - } - log::debug!("Backup file '{backup_file_path:?}' found, attempting to read.",); - - let buffer = read_file_sync(backup_file_path_str).map_err(|e| { - anyhow::anyhow!("Failed to read backup file using fs.readFileSync: {:?}", e) - })?; - - if !buffer.is_undefined() && !buffer.is_null() { - let uint8_array = js_sys::Uint8Array::new(&buffer); - let mut data = vec![0; uint8_array.length() as usize]; - uint8_array.copy_to(&mut data); - Ok(Some(data)) - } else { - Err(anyhow::anyhow!( - "readFileSync returned null or undefined for '{backup_file_path:?}'" - )) - } -} diff --git a/lib/wasm/src/error.rs b/lib/wasm/src/error.rs index e6eaf56..b3a57f2 100644 --- a/lib/wasm/src/error.rs +++ b/lib/wasm/src/error.rs @@ -1,5 +1,6 @@ use breez_sdk_liquid::{ error::{PaymentError, SdkError}, + signer::{NewError, SignError}, LnUrlAuthError, LnUrlPayError, LnUrlWithdrawError, }; use std::fmt::Display; @@ -46,6 +47,8 @@ wasm_error_wrapper!( log::ParseLevelError, PaymentError, SdkError, + NewError, + SignError, &str, String ); diff --git a/lib/wasm/src/lib.rs b/lib/wasm/src/lib.rs index b59059a..eb0a8ea 100644 --- a/lib/wasm/src/lib.rs +++ b/lib/wasm/src/lib.rs @@ -1,10 +1,9 @@ -mod backup; mod error; mod event; mod logger; pub mod model; +mod platform; mod signer; -mod utils; use std::path::PathBuf; use std::rc::Rc; @@ -12,12 +11,13 @@ use std::str::FromStr; use crate::event::{EventListener, WasmEventListener}; use crate::model::*; + use anyhow::anyhow; -use breez_sdk_liquid::bitcoin::bip32::{Fingerprint, Xpub}; use breez_sdk_liquid::elements::hex::ToHex; use breez_sdk_liquid::persist::Persister; use breez_sdk_liquid::sdk::{LiquidSdk, LiquidSdkBuilder}; -use breez_sdk_liquid::wallet::LiquidOnchainWallet; +use breez_sdk_liquid::signer::SdkLwkSigner; +use breez_sdk_liquid::wallet::get_descriptor; use breez_sdk_liquid::PRODUCTION_BREEZSERVER_URL; use log::LevelFilter; use logger::{Logger, WasmLogger}; @@ -59,23 +59,21 @@ async fn connect_inner( Rc::clone(&signer), )?; - let fingerprint: Fingerprint = Xpub::decode( - &signer - .xpub() - .map_err(|e| anyhow!("Failed to get xpub: {e}"))?, - ) - .map_err(|e| anyhow!(e.to_string()))? - .identifier()[0..4] - .try_into() - .map_err(|e| anyhow!("Failed to get fingerprint: {e}"))?; + let sdk_lwk_signer = SdkLwkSigner::new(Rc::clone(&signer))?; + let fingerprint = sdk_lwk_signer.fingerprint()?; let fingerprint = fingerprint.to_hex(); let wallet_dir = PathBuf::from_str(&config.get_wallet_dir(&config.working_dir, &fingerprint)?) .map_err(|e| anyhow!(e.to_string()))?; - let maybe_backup_bytes = backup::load_backup(&wallet_dir).await.unwrap_or_else(|e| { - log::error!("Failed to load backup: {:?}", e); - None - }); + + let maybe_db_backup_persister = platform::create_db_backup_persister(&wallet_dir).ok(); + let maybe_backup_bytes = match &maybe_db_backup_persister { + Some(p) => p.load_backup().await.unwrap_or_else(|e| { + log::error!("Failed to load db backup: {:?}", e); + None + }), + None => None, + }; let persister = Rc::new(Persister::new_in_memory( &config.working_dir, @@ -85,23 +83,30 @@ async fn connect_inner( maybe_backup_bytes, )?); - let onchain_wallet = Rc::new(LiquidOnchainWallet::new_in_memory( + let wollet_descriptor = get_descriptor(&sdk_lwk_signer, config.network)?; + let onchain_wallet = platform::create_onchain_wallet( + &wallet_dir, config.clone(), + wollet_descriptor, + &fingerprint, Rc::clone(&persister), - signer, - )?); + Rc::clone(&signer), + ) + .await?; sdk_builder.persister(persister.clone()); sdk_builder.onchain_wallet(onchain_wallet); - let sdk = sdk_builder.build()?; + let sdk = sdk_builder.build().await?; sdk.start().await?; - let (sender, receiver) = tokio::sync::mpsc::channel(20); - let listener = backup::ForwardingEventListener::new(sender); - sdk.add_event_listener(Box::new(listener)).await?; + if let Some(db_backup_persister) = maybe_db_backup_persister { + let (sender, receiver) = tokio::sync::mpsc::channel(20); + let listener = platform::db_backup_common::ForwardingEventListener::new(sender); + sdk.add_event_listener(Box::new(listener)).await?; - backup::start_backup_task(persister, receiver, wallet_dir); + db_backup_persister.start_backup_task(persister, receiver); + } Ok(BindingLiquidSdk { sdk }) } diff --git a/lib/wasm/src/platform/browser/db_backup.rs b/lib/wasm/src/platform/browser/db_backup.rs new file mode 100644 index 0000000..ef2cd3a --- /dev/null +++ b/lib/wasm/src/platform/browser/db_backup.rs @@ -0,0 +1,80 @@ +use crate::platform::db_backup_common::BackupStorage; +use anyhow::Result; +use indexed_db_futures::{ + database::Database, query_source::QuerySource, transaction::TransactionMode, Build, +}; + +const IDB_STORE_NAME: &str = "BREEZ_SDK_LIQUID_DB_BACKUP_STORE"; + +pub(crate) struct IndexedDbBackupStorage { + db_name: String, +} + +impl IndexedDbBackupStorage { + pub fn new(backup_dir_path: &str) -> Self { + let db_name = format!("{}-db-backup", backup_dir_path); + Self { db_name } + } +} + +#[sdk_macros::async_trait] +impl BackupStorage for IndexedDbBackupStorage { + async fn backup(&self, bytes: &[u8]) -> Result<()> { + let idb = open_indexed_db(&self.db_name).await?; + let tx = idb + .transaction([IDB_STORE_NAME]) + .with_mode(TransactionMode::Readwrite) + .build() + .map_err(|e| anyhow::anyhow!("Failed to build transaction: {}", e))?; + + let store = tx + .object_store(IDB_STORE_NAME) + .map_err(|e| anyhow::anyhow!("Failed to open object store: {}", e))?; + + store + .put(bytes) + .with_key(1) + .await + .map_err(|e| anyhow::anyhow!("Failed to put key in db: {}", e))?; + + tx.commit() + .await + .map_err(|e| anyhow::anyhow!("Failed to commit transaction: {}", e))?; + + Ok(()) + } + + async fn load(&self) -> Result>> { + let idb = open_indexed_db(&self.db_name).await?; + + let tx = idb + .transaction([IDB_STORE_NAME]) + .with_mode(TransactionMode::Readonly) + .build() + .map_err(|e| anyhow::anyhow!("Failed to build transaction: {}", e))?; + + let store = tx + .object_store(IDB_STORE_NAME) + .map_err(|e| anyhow::anyhow!("Failed to open object store: {}", e))?; + + store + .get(1) + .await + .map_err(|e| anyhow::anyhow!("Failed to get data: {}", e)) + } +} + +pub(crate) async fn open_indexed_db(name: &str) -> Result { + let db = Database::open(name) + .with_version(1u32) + .with_on_upgrade_needed(|event, db| { + if let (0.0, Some(1.0)) = (event.old_version(), event.new_version()) { + db.create_object_store(IDB_STORE_NAME).build()?; + } + + Ok(()) + }) + .await + .map_err(|e| anyhow::anyhow!("Failed to open IndexedDB: {}", e))?; + Ok(db) +} diff --git a/lib/wasm/src/platform/browser/mod.rs b/lib/wasm/src/platform/browser/mod.rs new file mode 100644 index 0000000..0dbc6bb --- /dev/null +++ b/lib/wasm/src/platform/browser/mod.rs @@ -0,0 +1,53 @@ +mod db_backup; +mod wallet_persister; + +use crate::platform::browser::db_backup::IndexedDbBackupStorage; +use crate::platform::browser::wallet_persister::{ + AsyncWalletCachePersister, IndexedDbWalletStorage, +}; +use crate::platform::db_backup_common::BackupPersister; +use anyhow::Result; +use breez_sdk_liquid::model::{Config, LiquidNetwork, Signer}; +use breez_sdk_liquid::persist::Persister; +use breez_sdk_liquid::wallet::persister::lwk_wollet::WolletDescriptor; +use breez_sdk_liquid::wallet::persister::WalletCachePersister; +use breez_sdk_liquid::wallet::{LiquidOnchainWallet, OnchainWallet}; +use std::path::Path; +use std::rc::Rc; +use std::sync::Arc; + +pub(crate) async fn create_wallet_persister( + wallet_dir: &Path, + descriptor: WolletDescriptor, + _network: LiquidNetwork, + _fingerprint: &str, +) -> Result> { + let wallet_storage = Arc::new(IndexedDbWalletStorage::new(wallet_dir, descriptor)); + let wallet_persister: Rc = + Rc::new(AsyncWalletCachePersister::new(wallet_storage).await?); + Ok(wallet_persister) +} + +pub(crate) async fn create_onchain_wallet( + wallet_dir: &Path, + config: Config, + descriptor: WolletDescriptor, + fingerprint: &str, + persister: Rc, + signer: Rc>, +) -> Result> { + let wallet_persister = + create_wallet_persister(wallet_dir, descriptor, config.network, fingerprint).await?; + let onchain_wallet: Rc = Rc::new( + LiquidOnchainWallet::new_with_cache_persister(config, persister, signer, wallet_persister) + .await?, + ); + Ok(onchain_wallet) +} + +pub(crate) fn create_db_backup_persister(backup_dir_path: &Path) -> Result { + let backup_storage = Rc::new(IndexedDbBackupStorage::new( + &backup_dir_path.to_string_lossy(), + )); + Ok(BackupPersister::new(backup_storage)) +} diff --git a/lib/wasm/src/platform/browser/wallet_persister.rs b/lib/wasm/src/platform/browser/wallet_persister.rs new file mode 100644 index 0000000..ee6d118 --- /dev/null +++ b/lib/wasm/src/platform/browser/wallet_persister.rs @@ -0,0 +1,234 @@ +use crate::platform::wallet_persister_common::maybe_merge_updates; +use anyhow::{anyhow, Context}; +use breez_sdk_liquid::wallet::persister::lwk_wollet::{PersistError, Update, WolletDescriptor}; +use breez_sdk_liquid::wallet::persister::{lwk_wollet, LwkPersister, WalletCachePersister}; +use indexed_db_futures::database::Database; +use indexed_db_futures::query_source::QuerySource; +use indexed_db_futures::transaction::TransactionMode; +use indexed_db_futures::Build; +use log::info; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc::{Receiver, Sender}; + +const IDB_STORE_NAME: &str = "BREEZ_SDK_LIQUID_WALLET_CACHE_STORE"; + +#[sdk_macros::async_trait] +pub(crate) trait AsyncWalletStorage: Send + Sync + Clone + 'static { + // Load all existing updates from the storage backend. + async fn load_updates(&self) -> anyhow::Result>; + + // Persist a single update at a given index. + async fn persist_update(&self, update: Update, index: u32) -> anyhow::Result<()>; + + // Clear all persisted data. + async fn clear(&self) -> anyhow::Result<()>; +} + +#[derive(Clone)] +pub(crate) struct AsyncWalletCachePersister { + lwk_persister: Arc>, +} + +impl AsyncWalletCachePersister { + pub async fn new(storage: Arc) -> anyhow::Result { + Ok(Self { + lwk_persister: Arc::new(AsyncLwkPersister::new(storage).await?), + }) + } +} + +#[sdk_macros::async_trait] +impl WalletCachePersister for AsyncWalletCachePersister { + fn get_lwk_persister(&self) -> LwkPersister { + let persister = std::sync::Arc::clone(&self.lwk_persister); + persister as LwkPersister + } + + async fn clear_cache(&self) -> anyhow::Result<()> { + self.lwk_persister.storage.clear().await?; + self.lwk_persister.updates.lock().unwrap().clear(); + Ok(()) + } +} + +struct AsyncLwkPersister { + updates: Mutex>, + sender: Sender<(Update, /*index*/ u32)>, + storage: Arc, +} + +impl AsyncLwkPersister { + async fn new(storage: Arc) -> anyhow::Result { + let updates = storage.load_updates().await?; + + let (sender, receiver) = tokio::sync::mpsc::channel(20); + + Self::start_persist_task(storage.clone(), receiver); + + Ok(Self { + updates: Mutex::new(updates), + sender, + storage, + }) + } + + fn start_persist_task(storage: Arc, mut receiver: Receiver<(Update, /*index*/ u32)>) { + wasm_bindgen_futures::spawn_local(async move { + // Persist updates and break on any error (giving up on cache persistence for the rest of the session) + // A failed update followed by a successful one may leave the cache in an inconsistent state + while let Some((update, index)) = receiver.recv().await { + info!("Starting to persist wallet cache update at index {}", index); + if let Err(e) = storage.persist_update(update, index).await { + log::error!("Failed to persist wallet cache update: {:?} - giving up on persisting wallet updates...", e); + break; + } + } + }); + } +} + +impl lwk_wollet::Persister for AsyncLwkPersister { + fn get(&self, index: usize) -> std::result::Result, PersistError> { + Ok(self.updates.lock().unwrap().get(index).cloned()) + } + + fn push(&self, update: Update) -> std::result::Result<(), PersistError> { + let mut updates = self.updates.lock().unwrap(); + + let (update, write_index) = maybe_merge_updates(update, updates.last(), updates.len()); + + if let Err(e) = self.sender.try_send((update.clone(), write_index as u32)) { + log::error!("Failed to send update to persister task {e}"); + } + + if write_index < updates.len() { + updates[write_index] = update; + } else { + updates.push(update); + } + + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) struct IndexedDbWalletStorage { + db_name: String, + desc: WolletDescriptor, +} + +impl IndexedDbWalletStorage { + pub fn new(working_dir: &Path, desc: WolletDescriptor) -> Self { + let db_name = format!("{}-wallet-cache", working_dir.to_string_lossy()); + Self { db_name, desc } + } +} + +#[sdk_macros::async_trait] +impl AsyncWalletStorage for IndexedDbWalletStorage { + async fn load_updates(&self) -> anyhow::Result> { + let idb = open_indexed_db(&self.db_name).await?; + + let tx = idb + .transaction([IDB_STORE_NAME]) + .with_mode(TransactionMode::Readonly) + .build() + .map_err(|e| anyhow!("Failed to build transaction: {}", e))?; + + let store = tx + .object_store(IDB_STORE_NAME) + .map_err(|e| anyhow!("Failed to open object store: {}", e))?; + + let updates_count = store + .count() + .await + .map_err(|e| anyhow!("Failed to get next index: {}", e))?; + + let mut updates = Vec::new(); + for i in 0..updates_count { + let update_bytes: Vec = store + .get(i) + .await + .map_err(|e| anyhow!("Failed to get update bytes: {}", e))? + .ok_or(anyhow!("Missing update on index {i}"))?; + updates.push( + Update::deserialize_decrypted(&update_bytes, &self.desc) + .context("Failed to deserialize update")?, + ); + } + + Ok(updates) + } + + async fn persist_update(&self, update: Update, index: u32) -> anyhow::Result<()> { + let update_bytes = update + .serialize_encrypted(&self.desc) + .map_err(|e| anyhow!("Failed to serialize update: {e}"))?; + + let idb = open_indexed_db(&self.db_name) + .await + .map_err(|e| anyhow!("Failed to open IndexedDB: {e}"))?; + + let tx = idb + .transaction([IDB_STORE_NAME]) + .with_mode(TransactionMode::Readwrite) + .build() + .map_err(|e| anyhow!("Failed to build transaction: {e}"))?; + + let store = tx + .object_store(IDB_STORE_NAME) + .map_err(|e| anyhow!("Failed to open object store: {e}"))?; + + store + .put(update_bytes) + .with_key(index) + .await + .map_err(|e| anyhow!("Failed to put update in store: {e}"))?; + + tx.commit() + .await + .map_err(|e| anyhow!("Failed to commit transaction: {e}"))?; + + Ok(()) + } + + async fn clear(&self) -> anyhow::Result<()> { + let idb = open_indexed_db(&self.db_name).await?; + + let tx = idb + .transaction([IDB_STORE_NAME]) + .with_mode(TransactionMode::Readwrite) + .build() + .map_err(|e| anyhow!("Failed to build transaction: {}", e))?; + + let store = tx + .object_store(IDB_STORE_NAME) + .map_err(|e| anyhow!("Failed to open object store: {}", e))?; + + store + .clear() + .map_err(|e| anyhow!("Failed to clear object store: {}", e))?; + + tx.commit().await.map_err(|e| { + lwk_wollet::PersistError::Other(format!("Failed to commit transaction: {}", e)) + })?; + + Ok(()) + } +} + +pub(crate) async fn open_indexed_db(name: &str) -> Result { + let db = Database::open(name) + .with_version(1u32) + .with_on_upgrade_needed(|event, db| { + if let (0.0, Some(1.0)) = (event.old_version(), event.new_version()) { + db.create_object_store(IDB_STORE_NAME).build()?; + } + + Ok(()) + }) + .await + .map_err(|e| lwk_wollet::PersistError::Other(format!("Failed to open IndexedDB: {}", e)))?; + Ok(db) +} diff --git a/lib/wasm/src/platform/db_backup_common.rs b/lib/wasm/src/platform/db_backup_common.rs new file mode 100644 index 0000000..7880bce --- /dev/null +++ b/lib/wasm/src/platform/db_backup_common.rs @@ -0,0 +1,135 @@ +use anyhow::Result; +use breez_sdk_liquid::model::{EventListener, SdkEvent}; +use breez_sdk_liquid::persist::Persister; +use std::rc::Rc; +use tokio::sync::mpsc::{Receiver, Sender}; + +pub(crate) struct ForwardingEventListener { + sender: Sender, +} + +impl ForwardingEventListener { + pub fn new(sender: Sender) -> Self { + Self { sender } + } +} + +impl EventListener for ForwardingEventListener { + fn on_event(&self, e: SdkEvent) { + if let Err(e) = self.sender.try_send(e) { + log::error!("Failed to forward event: {:?}", e); + } + } +} + +#[sdk_macros::async_trait] +pub(crate) trait BackupStorage { + async fn backup(&self, bytes: &[u8]) -> Result<()>; + + async fn load(&self) -> Result>>; +} + +pub(crate) struct BackupPersister { + storage: Rc, +} + +impl BackupPersister { + pub fn new(storage: Rc) -> Self { + Self { storage } + } + + pub(crate) fn start_backup_task( + &self, + persister: Rc, + mut receiver: Receiver, + ) { + let storage = self.storage.clone(); + wasm_bindgen_futures::spawn_local(async move { + while let Some(e) = receiver.recv().await { + let start = web_time::Instant::now(); + + let bytes = match persister.serialize() { + Ok(bytes) => bytes, + Err(e) => { + log::error!("Failed to serialize persister: {:?}", e); + continue; + } + }; + + let res = match e { + SdkEvent::Synced => storage.backup(&bytes).await, + SdkEvent::DataSynced { + did_pull_new_records, + } if did_pull_new_records => storage.backup(&bytes).await, + _ => continue, + }; + if let Err(e) = res { + log::error!("Failed to backup to IndexedDB: {:?}", e); + }; + + let backup_duration_ms = start.elapsed().as_millis(); + log::info!("Backup completed successfully ({backup_duration_ms} ms)"); + } + }); + } + + pub(crate) async fn load_backup(&self) -> Result>> { + self.storage.load().await + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use std::str::FromStr; + + use crate::platform::create_db_backup_persister; + use breez_sdk_liquid::model::PaymentState; + use breez_sdk_liquid::persist::Persister; + use breez_sdk_liquid::prelude::LiquidNetwork; + use breez_sdk_liquid::test_utils::persist::{ + create_persister, new_receive_swap, new_send_swap, + }; + + #[cfg(feature = "browser")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + #[sdk_macros::async_test_wasm] + async fn test_backup_and_restore() -> anyhow::Result<()> { + create_persister!(local); + + let backup_dir_path = PathBuf::from_str(&format!("/tmp/{}", uuid::Uuid::new_v4()))?; + let backup_persister = create_db_backup_persister(&backup_dir_path)?; + + local.test_insert_or_update_send_swap(&new_send_swap(Some(PaymentState::Pending), None))?; + local.test_insert_or_update_receive_swap(&new_receive_swap( + Some(PaymentState::Pending), + None, + ))?; + assert_eq!(local.test_list_ongoing_swaps()?.len(), 2); + + backup_persister.storage.backup(&local.serialize()?).await?; + + let backup_bytes = backup_persister.load_backup().await?; + let remote = + Persister::new_in_memory("remote", LiquidNetwork::Testnet, false, None, backup_bytes)?; + assert_eq!(remote.test_list_ongoing_swaps()?.len(), 2); + + // Try again to verify that a new backup overwrites an old one + local.test_insert_or_update_send_swap(&new_send_swap(Some(PaymentState::Pending), None))?; + local.test_insert_or_update_receive_swap(&new_receive_swap( + Some(PaymentState::Pending), + None, + ))?; + assert_eq!(local.test_list_ongoing_swaps()?.len(), 4); + + backup_persister.storage.backup(&local.serialize()?).await?; + + let backup_bytes = backup_persister.load_backup().await?; + let remote = + Persister::new_in_memory("remote", LiquidNetwork::Testnet, false, None, backup_bytes)?; + assert_eq!(remote.test_list_ongoing_swaps()?.len(), 4); + + Ok(()) + } +} diff --git a/lib/wasm/src/platform/default.rs b/lib/wasm/src/platform/default.rs new file mode 100644 index 0000000..a995766 --- /dev/null +++ b/lib/wasm/src/platform/default.rs @@ -0,0 +1,25 @@ +use crate::platform::db_backup_common::BackupPersister; +use anyhow::{bail, Result}; +use breez_sdk_liquid::model::{Config, Signer}; +use breez_sdk_liquid::persist::Persister; +use breez_sdk_liquid::wallet::persister::lwk_wollet::WolletDescriptor; +use breez_sdk_liquid::wallet::{LiquidOnchainWallet, OnchainWallet}; +use std::path::Path; +use std::rc::Rc; + +pub(crate) async fn create_onchain_wallet( + _wallet_dir: &Path, + config: Config, + _descriptor: WolletDescriptor, + _fingerprint: &str, + persister: Rc, + signer: Rc>, +) -> Result> { + let onchain_wallet: Rc = + Rc::new(LiquidOnchainWallet::new_in_memory(config, persister, signer).await?); + Ok(onchain_wallet) +} + +pub(crate) fn create_db_backup_persister(_backup_dir_path: &Path) -> Result { + bail!("No backup persister available on this platform") +} diff --git a/lib/wasm/src/platform/mod.rs b/lib/wasm/src/platform/mod.rs new file mode 100644 index 0000000..f232e74 --- /dev/null +++ b/lib/wasm/src/platform/mod.rs @@ -0,0 +1,28 @@ +#![allow(unused_imports)] + +#[cfg(feature = "browser")] +mod browser; +#[cfg(feature = "browser")] +pub(crate) use browser::{ + create_db_backup_persister, create_onchain_wallet, create_wallet_persister, +}; + +#[cfg(feature = "node-js")] +mod node_js; +#[cfg(feature = "node-js")] +pub(crate) use node_js::{ + create_db_backup_persister, create_onchain_wallet, create_wallet_persister, +}; + +#[cfg(all(not(feature = "browser"), not(feature = "node-js")))] +mod default; +#[cfg(all(not(feature = "browser"), not(feature = "node-js")))] +pub(crate) use default::{create_db_backup_persister, create_onchain_wallet}; + +#[cfg_attr( + all(not(feature = "browser"), not(feature = "node-js")), + allow(dead_code) +)] +pub(crate) mod db_backup_common; +#[cfg(any(feature = "browser", feature = "node-js"))] +mod wallet_persister_common; diff --git a/lib/wasm/src/platform/node_js/db_backup.rs b/lib/wasm/src/platform/node_js/db_backup.rs new file mode 100644 index 0000000..b41bb23 --- /dev/null +++ b/lib/wasm/src/platform/node_js/db_backup.rs @@ -0,0 +1,45 @@ +use super::fs::{ensure_dir_exists, exists_sync, read_file_vec, write_file_vec}; +use crate::platform::db_backup_common::BackupStorage; +use anyhow::Result; +use std::path::{Path, PathBuf}; + +pub(crate) struct NodeFsBackupStorage { + backup_dir_path: PathBuf, +} + +impl NodeFsBackupStorage { + pub fn new>(backup_dir_path: P) -> Self { + Self { + backup_dir_path: backup_dir_path.as_ref().to_path_buf(), + } + } +} + +#[sdk_macros::async_trait] +impl BackupStorage for NodeFsBackupStorage { + async fn backup(&self, bytes: &[u8]) -> Result<()> { + ensure_dir_exists(&self.backup_dir_path.to_string_lossy()) + .map_err(|e| anyhow::anyhow!("Failed to create backup directory: {:?}", e))?; + + let backup_file_path = get_backup_file_path(&self.backup_dir_path); + + write_file_vec(&backup_file_path.to_string_lossy(), bytes)?; + Ok(()) + } + + async fn load(&self) -> Result>> { + let backup_file_path = get_backup_file_path(&self.backup_dir_path); + let backup_file_path_string = backup_file_path.to_string_lossy(); + if !exists_sync(&backup_file_path_string) { + log::debug!("Backup file '{backup_file_path_string:?}' not found."); + return Ok(None); + } + log::debug!("Backup file '{backup_file_path_string:?}' found, attempting to read.",); + + Ok(Some(read_file_vec(&backup_file_path_string)?)) + } +} + +fn get_backup_file_path(backup_dir_path: &Path) -> PathBuf { + backup_dir_path.join("backup.sql") +} diff --git a/lib/wasm/src/platform/node_js/fs.rs b/lib/wasm/src/platform/node_js/fs.rs new file mode 100644 index 0000000..8b1f93d --- /dev/null +++ b/lib/wasm/src/platform/node_js/fs.rs @@ -0,0 +1,74 @@ +use anyhow::{anyhow, Context}; +use js_sys::Reflect; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; + +#[wasm_bindgen(module = "fs")] +extern "C" { + #[wasm_bindgen(js_name = writeFileSync, catch)] + pub(crate) fn write_file_sync(path: &str, data: &js_sys::Uint8Array) -> Result<(), JsValue>; + + #[wasm_bindgen(js_name = readFileSync, catch)] + pub(crate) fn read_file_sync(path: &str) -> Result; + + #[wasm_bindgen(js_name = existsSync)] + pub(crate) fn exists_sync(path: &str) -> bool; + + #[wasm_bindgen(js_name = mkdirSync, catch)] + pub(crate) fn mkdir_sync(path: &str, options: &JsValue) -> Result<(), JsValue>; + + #[wasm_bindgen(js_name = rmSync, catch)] + pub(crate) fn rm_sync(path: &str, options: &JsValue) -> Result<(), JsValue>; + + #[wasm_bindgen(js_name = readdirSync, catch)] + pub(crate) fn readdir_sync(path: &str) -> Result; +} + +pub(crate) fn ensure_dir_exists(path: &str) -> anyhow::Result<()> { + if !exists_sync(path) { + let options = js_sys::Object::new(); + Reflect::set(&options, &"recursive".into(), &true.into()) + .map_err(js_value_to_err) + .context("Failed to set recursive option")?; + mkdir_sync(path, &options) + .map_err(js_value_to_err) + .context("Failed to call mkdir_sync")?; + } + Ok(()) +} + +pub(crate) fn remove_dir_all_sync(path: &str) -> anyhow::Result<()> { + if exists_sync(path) { + let options = js_sys::Object::new(); + Reflect::set(&options, &"recursive".into(), &true.into()) + .map_err(js_value_to_err) + .context("Failed to set recursive option")?; + Reflect::set(&options, &"force".into(), &true.into()) + .map_err(js_value_to_err) + .context("Failed to set force option")?; // Ignore errors if path doesn't exist + rm_sync(path, &options) + .map_err(js_value_to_err) + .context("Failed to call rm_sync")?; + } + Ok(()) +} + +pub(crate) fn js_value_to_err(err: JsValue) -> anyhow::Error { + anyhow!(err + .as_string() + .unwrap_or_else(|| "Unknown error".to_string())) +} + +pub(crate) fn read_file_vec(path: &str) -> anyhow::Result> { + read_file_sync(path) + .map(|arr| arr.to_vec()) + .map_err(js_value_to_err) + .context("Failed to call read_file_sync") +} + +pub(crate) fn write_file_vec(path: &str, data: &[u8]) -> anyhow::Result<()> { + let js_arr = js_sys::Uint8Array::from(data); + write_file_sync(path, &js_arr) + .map_err(js_value_to_err) + .context("Failed to call write_file_sync") +} diff --git a/lib/wasm/src/platform/node_js/mod.rs b/lib/wasm/src/platform/node_js/mod.rs new file mode 100644 index 0000000..af5262e --- /dev/null +++ b/lib/wasm/src/platform/node_js/mod.rs @@ -0,0 +1,53 @@ +mod db_backup; +mod fs; +mod wallet_persister; + +use crate::platform::db_backup_common::BackupPersister; +use crate::platform::node_js::db_backup::NodeFsBackupStorage; +use crate::platform::node_js::wallet_persister::NodeFsWalletCachePersister; +use anyhow::Result; +use breez_sdk_liquid::model::LiquidNetwork; +use breez_sdk_liquid::model::{Config, Signer}; +use breez_sdk_liquid::persist::Persister; +use breez_sdk_liquid::wallet::persister::lwk_wollet::WolletDescriptor; +use breez_sdk_liquid::wallet::persister::WalletCachePersister; +use breez_sdk_liquid::wallet::{LiquidOnchainWallet, OnchainWallet}; +use std::path::Path; +use std::rc::Rc; + +pub(crate) async fn create_wallet_persister( + wallet_dir: &Path, + descriptor: WolletDescriptor, + network: LiquidNetwork, + fingerprint: &str, +) -> Result> { + let wallet_persister: Rc = Rc::new(NodeFsWalletCachePersister::new( + wallet_dir, + network.into(), + fingerprint, + descriptor, + )?); + Ok(wallet_persister) +} + +pub(crate) async fn create_onchain_wallet( + wallet_dir: &Path, + config: Config, + descriptor: WolletDescriptor, + fingerprint: &str, + persister: Rc, + signer: Rc>, +) -> Result> { + let wallet_persister = + create_wallet_persister(wallet_dir, descriptor, config.network, fingerprint).await?; + let onchain_wallet: Rc = Rc::new( + LiquidOnchainWallet::new_with_cache_persister(config, persister, signer, wallet_persister) + .await?, + ); + Ok(onchain_wallet) +} + +pub(crate) fn create_db_backup_persister(backup_dir_path: &Path) -> Result { + let backup_storage = Rc::new(NodeFsBackupStorage::new(backup_dir_path)); + Ok(BackupPersister::new(backup_storage)) +} diff --git a/lib/wasm/src/platform/node_js/wallet_persister.rs b/lib/wasm/src/platform/node_js/wallet_persister.rs new file mode 100644 index 0000000..412a072 --- /dev/null +++ b/lib/wasm/src/platform/node_js/wallet_persister.rs @@ -0,0 +1,155 @@ +use super::fs::{ + ensure_dir_exists, exists_sync, read_file_vec, readdir_sync, remove_dir_all_sync, + write_file_vec, +}; +use crate::platform::wallet_persister_common::maybe_merge_updates; +use breez_sdk_liquid::wallet::persister::lwk_wollet::{ + ElementsNetwork, PersistError, Update, WolletDescriptor, +}; +use breez_sdk_liquid::wallet::persister::{lwk_wollet, LwkPersister, WalletCachePersister}; +use js_sys::Array; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::Mutex; + +#[derive(Clone)] +pub(crate) struct NodeFsWalletCachePersister { + cache_dir: String, + lwk_persister: Arc, +} + +impl NodeFsWalletCachePersister { + pub fn new>( + path: P, + network: ElementsNetwork, + fingerprint: &str, + desc: WolletDescriptor, + ) -> anyhow::Result { + let mut cache_dir_path = path.as_ref().to_path_buf(); + cache_dir_path.push(network.as_str()); + cache_dir_path.push("enc_cache"); + cache_dir_path.push(fingerprint); + let cache_dir = cache_dir_path.to_string_lossy().to_string(); + + ensure_dir_exists(&cache_dir)?; + + Ok(Self { + cache_dir, + lwk_persister: Arc::new(NodeFsLwkPersister::new(cache_dir_path, desc)), + }) + } +} + +#[sdk_macros::async_trait] +impl WalletCachePersister for NodeFsWalletCachePersister { + fn get_lwk_persister(&self) -> LwkPersister { + let persister = Arc::clone(&self.lwk_persister); + persister as LwkPersister + } + + async fn clear_cache(&self) -> anyhow::Result<()> { + log::debug!("Clearing lwk wallet cache directory: {}", self.cache_dir); + remove_dir_all_sync(&self.cache_dir)?; + log::info!( + "Successfully cleared lwk wallet cache directory: {}", + self.cache_dir + ); + ensure_dir_exists(&self.cache_dir)?; + *self.lwk_persister.next_index.lock().unwrap() = 0; + Ok(()) + } +} + +struct NodeFsLwkPersister { + cache_dir: PathBuf, + next_index: Mutex, + desc: WolletDescriptor, +} + +impl NodeFsLwkPersister { + fn new(cache_dir: PathBuf, desc: WolletDescriptor) -> Self { + let initial_index = { + let entries = + readdir_sync(&cache_dir.to_string_lossy()).unwrap_or_else(|_| Array::new()); + let mut max_index: Option = None; + + for entry in entries.iter() { + if let Some(name) = entry.as_string() { + if let Ok(index) = name.parse::() { + max_index = Some(max_index.map_or(index, |max| max.max(index))); + } + } + } + max_index.map_or(0, |max| max + 1) + }; + + Self { + cache_dir, + next_index: Mutex::new(initial_index), + desc, + } + } + + fn get_update_file_path(&self, index: usize) -> String { + self.cache_dir + .join(index.to_string()) + .to_string_lossy() + .to_string() + } +} + +impl lwk_wollet::Persister for NodeFsLwkPersister { + fn get(&self, index: usize) -> Result, PersistError> { + let file_path = self.get_update_file_path(index); + if !exists_sync(&file_path) { + log::trace!("Update file not found: {}", file_path); + return Ok(None); + } + + log::debug!("Reading update file: {}", file_path); + let bytes = read_file_vec(&file_path).map_err(to_persist_error)?; + + log::debug!("Deserializing update from file: {}", file_path); + let update = Update::deserialize_decrypted(&bytes, &self.desc).map_err(to_persist_error)?; + Ok(Some(update)) + } + + fn push(&self, update: Update) -> Result<(), PersistError> { + let mut next_index_guard = self.next_index.lock().unwrap(); + let next_index = *next_index_guard; + + let prev_update = if next_index == 0 { + None + } else { + self.get(next_index - 1).unwrap_or(None) + }; + let (update, write_index) = maybe_merge_updates(update, prev_update.as_ref(), next_index); + + let file_path = self.get_update_file_path(write_index); + log::debug!("Serializing and writing update to: {}", file_path); + let bytes = update + .serialize_encrypted(&self.desc) + .map_err(to_persist_error)?; + + write_file_vec(&file_path, &bytes).map_err(to_persist_error)?; + + if write_index == *next_index_guard { + *next_index_guard += 1; + log::info!( + "Successfully pushed wallet cache update to index {}", + write_index + ); + } else { + log::info!( + "Successfully overwrote tip-only wallet cache update at index {}", + write_index + ); + } + + Ok(()) + } +} + +fn to_persist_error(e: E) -> PersistError { + PersistError::Other(format!("{:?}", e)) +} diff --git a/lib/wasm/src/platform/wallet_persister_common.rs b/lib/wasm/src/platform/wallet_persister_common.rs new file mode 100644 index 0000000..ac8b35b --- /dev/null +++ b/lib/wasm/src/platform/wallet_persister_common.rs @@ -0,0 +1,132 @@ +use breez_sdk_liquid::wallet::persister::lwk_wollet::Update; + +// If both updates are only tip updates, we can merge them. +// See https://github.com/Blockstream/lwk/blob/0322a63310f8c8414c537adff68dcbbc7ff4662d/lwk_wollet/src/persister.rs#L174 +pub(crate) fn maybe_merge_updates( + mut new_update: Update, + prev_update: Option<&Update>, + mut next_index: usize, +) -> (Update, /*index*/ usize) { + if new_update.only_tip() { + if let Some(prev_update) = prev_update { + if prev_update.only_tip() { + new_update.wollet_status = prev_update.wollet_status; + next_index -= 1; + } + } + } + (new_update, next_index) +} + +#[cfg(any(feature = "browser", feature = "node-js"))] +#[cfg(test)] +mod tests { + use crate::platform::create_wallet_persister; + use breez_sdk_liquid::elements::hashes::Hash; + use breez_sdk_liquid::elements::{BlockHash, BlockHeader, TxMerkleNode, Txid}; + use breez_sdk_liquid::model::{LiquidNetwork, Signer}; + use breez_sdk_liquid::signer::{SdkLwkSigner, SdkSigner}; + use breez_sdk_liquid::wallet::get_descriptor; + use breez_sdk_liquid::wallet::persister::lwk_wollet::WolletDescriptor; + use breez_sdk_liquid::wallet::persister::{lwk_wollet, WalletCachePersister}; + use std::path::PathBuf; + use std::rc::Rc; + use std::sync::Arc; + use std::time::Duration; + use tokio_with_wasm::alias as tokio; + + #[cfg(feature = "browser")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + fn get_wollet_descriptor() -> anyhow::Result { + let signer: Rc> = Rc::new(Box::new(SdkSigner::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", "", false)?)); + let sdk_lwk_signer = SdkLwkSigner::new(signer)?; + Ok(get_descriptor(&sdk_lwk_signer, LiquidNetwork::Testnet)?) + } + + async fn build_persister(working_dir: &str) -> anyhow::Result> { + let desc = get_wollet_descriptor()?; + create_wallet_persister( + &PathBuf::from(working_dir), + desc, + LiquidNetwork::Testnet, + "aaaaaaaa", + ) + .await + } + + #[sdk_macros::async_test_wasm] + async fn test_wallet_cache() -> anyhow::Result<()> { + let working_dir = format!("/tmp/{}", uuid::Uuid::new_v4()); + + let persister = build_persister(&working_dir).await?; + let lwk_persister = persister.get_lwk_persister(); + + assert!(lwk_persister.get(0)?.is_none()); + + lwk_persister.push(get_lwk_update(5, false))?; + + assert_eq!(lwk_persister.get(0)?.unwrap().tip.height, 5); + assert!(lwk_persister.get(1)?.is_none()); + + lwk_persister.push(get_lwk_update(10, true))?; + + assert_eq!(lwk_persister.get(0)?.unwrap().tip.height, 5); + assert_eq!(lwk_persister.get(1)?.unwrap().tip.height, 10); + assert!(lwk_persister.get(2)?.is_none()); + + lwk_persister.push(get_lwk_update(15, true))?; + + assert_eq!(lwk_persister.get(0)?.unwrap().tip.height, 5); + assert_eq!(lwk_persister.get(1)?.unwrap().tip.height, 15); + assert!(lwk_persister.get(2)?.is_none()); + + // Allow persister task to persist updates when persister is async + tokio::time::sleep(Duration::from_secs(2)).await; + + // Reload persister + let persister = build_persister(&working_dir).await?; + let lwk_persister = persister.get_lwk_persister(); + + assert_eq!(lwk_persister.get(0)?.unwrap().tip.height, 5); + assert_eq!(lwk_persister.get(1)?.unwrap().tip.height, 15); + assert!(lwk_persister.get(2)?.is_none()); + + persister.clear_cache().await?; + assert!(lwk_persister.get(0)?.is_none()); + assert!(lwk_persister.get(1)?.is_none()); + assert!(lwk_persister.get(2)?.is_none()); + + lwk_persister.push(get_lwk_update(20, false))?; + assert_eq!(lwk_persister.get(0)?.unwrap().tip.height, 20); + assert!(lwk_persister.get(1)?.is_none()); + + Ok(()) + } + + fn get_lwk_update(height: u32, only_tip: bool) -> lwk_wollet::Update { + let txid_height_new = match only_tip { + true => Vec::new(), + false => { + vec![(Txid::all_zeros(), None)] + } + }; + lwk_wollet::Update { + version: 1, + wollet_status: 0, + new_txs: Default::default(), + txid_height_new, + txid_height_delete: vec![], + timestamps: vec![], + scripts_with_blinding_pubkey: vec![], + tip: BlockHeader { + version: 0, + prev_blockhash: BlockHash::all_zeros(), + merkle_root: TxMerkleNode::all_zeros(), + time: 0, + height, + ext: Default::default(), + }, + } + } +} diff --git a/lib/wasm/src/utils.rs b/lib/wasm/src/utils.rs deleted file mode 100644 index 757a682..0000000 --- a/lib/wasm/src/utils.rs +++ /dev/null @@ -1,13 +0,0 @@ -use anyhow::anyhow; -use std::path::Path; - -pub trait PathExt { - fn to_str_safe(&self) -> anyhow::Result<&str>; -} - -impl PathExt for Path { - fn to_str_safe(&self) -> anyhow::Result<&str> { - self.to_str() - .ok_or_else(|| anyhow!("Invalid UTF-8 sequence in path: {:?}", self)) - } -}