diff --git a/cli/src/main.rs b/cli/src/main.rs index 3c15795..8083276 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -19,9 +19,6 @@ pub(crate) struct Args { #[clap(long, action)] pub(crate) no_data_sync: bool, - #[clap(short, long)] - pub(crate) cache_dir: Option, - #[clap(short, long)] pub(crate) log_file: Option, @@ -89,7 +86,6 @@ async fn main() -> Result<()> { .map(|var| var.into_string().expect("Expected valid API key string")); let mut config = LiquidSdk::default_config(network, breez_api_key)?; config.working_dir = data_dir_str; - config.cache_dir = args.cache_dir; if args.no_data_sync { config.sync_service_url = None; } else if data_sync_url.is_some() { diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt index 105f8cb..457e96c 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/ForegroundService.kt @@ -126,10 +126,6 @@ abstract class ForegroundService : // Connect to SDK if source intent has data message with valid payload getConnectRequest()?.let { connectRequest -> - if (connectRequest.config.cacheDir == null) { - connectRequest.config.cacheDir = Path(connectRequest.config.workingDir, "pluginCache").toString() - } - getJobFromIntent(intent)?.also { job -> launchSdkConnection(connectRequest, job) } ?: run { diff --git a/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h b/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h index b3680e2..9e98503 100644 --- a/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h +++ b/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h @@ -704,7 +704,6 @@ typedef struct wire_cst_config { struct wire_cst_blockchain_explorer liquid_explorer; struct wire_cst_blockchain_explorer bitcoin_explorer; struct wire_cst_list_prim_u_8_strict *working_dir; - struct wire_cst_list_prim_u_8_strict *cache_dir; int32_t network; uint64_t payment_timeout_sec; struct wire_cst_list_prim_u_8_strict *sync_service_url; diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/SDKNotificationService.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/SDKNotificationService.swift index 24dd1fe..c512007 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/SDKNotificationService.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/SDKNotificationService.swift @@ -20,22 +20,12 @@ open class SDKNotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - guard var connectRequest = self.getConnectRequest() else { + guard let connectRequest = self.getConnectRequest() else { if let content = bestAttemptContent { contentHandler(content) } return } - - if connectRequest.config.cacheDir == nil { - var workingDir: URL - if #available(iOS 16, *) { - workingDir = URL(filePath: connectRequest.config.workingDir) - } else { - workingDir = URL(fileURLWithPath: connectRequest.config.workingDir) - } - connectRequest.config.cacheDir = workingDir.appendingPathComponent("pluginCache").path - } if let currentTask = self.getTaskFromNotification() { self.currentTask = currentTask diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index a2700de..da18116 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -340,7 +340,6 @@ dictionary Config { u64 payment_timeout_sec; string? sync_service_url; string? breez_api_key; - string? cache_dir; u64? zero_conf_max_amount_sat; boolean use_default_external_input_parsers = true; sequence? external_input_parsers = null; diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index bfb40fa..e5e9a28 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -37,7 +37,7 @@ pub const ESTIMATED_BTC_LOCKUP_TX_VSIZE: u64 = 154; pub(crate) struct ChainSwapHandler { config: Config, onchain_wallet: Arc, - persister: Arc, + persister: std::sync::Arc, swapper: Arc, liquid_chain_service: Arc, bitcoin_chain_service: Arc, @@ -67,7 +67,7 @@ impl ChainSwapHandler { pub(crate) fn new( config: Config, onchain_wallet: Arc, - persister: Arc, + persister: std::sync::Arc, swapper: Arc, liquid_chain_service: Arc, bitcoin_chain_service: Arc, diff --git a/lib/core/src/frb_generated.rs b/lib/core/src/frb_generated.rs index ec8e308..d39e65f 100644 --- a/lib/core/src/frb_generated.rs +++ b/lib/core/src/frb_generated.rs @@ -2628,7 +2628,6 @@ impl SseDecode for crate::model::Config { let mut var_liquidExplorer = ::sse_decode(deserializer); let mut var_bitcoinExplorer = ::sse_decode(deserializer); let mut var_workingDir = ::sse_decode(deserializer); - let mut var_cacheDir = >::sse_decode(deserializer); let mut var_network = ::sse_decode(deserializer); let mut var_paymentTimeoutSec = ::sse_decode(deserializer); let mut var_syncServiceUrl = >::sse_decode(deserializer); @@ -2645,7 +2644,6 @@ impl SseDecode for crate::model::Config { liquid_explorer: var_liquidExplorer, bitcoin_explorer: var_bitcoinExplorer, working_dir: var_workingDir, - cache_dir: var_cacheDir, network: var_network, payment_timeout_sec: var_paymentTimeoutSec, sync_service_url: var_syncServiceUrl, @@ -5320,7 +5318,6 @@ impl flutter_rust_bridge::IntoDart for crate::model::Config { self.liquid_explorer.into_into_dart().into_dart(), self.bitcoin_explorer.into_into_dart().into_dart(), self.working_dir.into_into_dart().into_dart(), - self.cache_dir.into_into_dart().into_dart(), self.network.into_into_dart().into_dart(), self.payment_timeout_sec.into_into_dart().into_dart(), self.sync_service_url.into_into_dart().into_dart(), @@ -7741,7 +7738,6 @@ impl SseEncode for crate::model::Config { ::sse_encode(self.liquid_explorer, serializer); ::sse_encode(self.bitcoin_explorer, serializer); ::sse_encode(self.working_dir, serializer); - >::sse_encode(self.cache_dir, serializer); ::sse_encode(self.network, serializer); ::sse_encode(self.payment_timeout_sec, serializer); >::sse_encode(self.sync_service_url, serializer); @@ -10241,7 +10237,6 @@ mod io { liquid_explorer: self.liquid_explorer.cst_decode(), bitcoin_explorer: self.bitcoin_explorer.cst_decode(), working_dir: self.working_dir.cst_decode(), - cache_dir: self.cache_dir.cst_decode(), network: self.network.cst_decode(), payment_timeout_sec: self.payment_timeout_sec.cst_decode(), sync_service_url: self.sync_service_url.cst_decode(), @@ -11995,7 +11990,6 @@ mod io { liquid_explorer: Default::default(), bitcoin_explorer: Default::default(), working_dir: core::ptr::null_mut(), - cache_dir: core::ptr::null_mut(), network: Default::default(), payment_timeout_sec: Default::default(), sync_service_url: core::ptr::null_mut(), @@ -14321,7 +14315,6 @@ mod io { liquid_explorer: wire_cst_blockchain_explorer, bitcoin_explorer: wire_cst_blockchain_explorer, working_dir: *mut wire_cst_list_prim_u_8_strict, - cache_dir: *mut wire_cst_list_prim_u_8_strict, network: i32, payment_timeout_sec: u64, sync_service_url: *mut wire_cst_list_prim_u_8_strict, diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 8bedb72..7f910b5 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -64,8 +64,6 @@ pub struct Config { /// /// Prefix can be a relative or absolute path to this directory. pub working_dir: String, - /// Directory in which the Liquid wallet cache is stored. Defaults to `working_dir` - pub cache_dir: Option, pub network: LiquidNetwork, /// Send payment timeout. See [LiquidSdk::send_payment](crate::sdk::LiquidSdk::send_payment) pub payment_timeout_sec: u64, @@ -112,7 +110,6 @@ impl Config { url: "bitcoin-mainnet.blockstream.info:50002".to_string(), }, working_dir: ".".to_string(), - cache_dir: None, network: LiquidNetwork::Mainnet, payment_timeout_sec: 15, sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()), @@ -137,7 +134,6 @@ impl Config { use_waterfalls: false, }, working_dir: ".".to_string(), - cache_dir: None, network: LiquidNetwork::Mainnet, payment_timeout_sec: 15, sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()), @@ -161,7 +157,6 @@ impl Config { url: "bitcoin-testnet.blockstream.info:50002".to_string(), }, working_dir: ".".to_string(), - cache_dir: None, network: LiquidNetwork::Testnet, payment_timeout_sec: 15, sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()), @@ -186,7 +181,6 @@ impl Config { use_waterfalls: false, }, working_dir: ".".to_string(), - cache_dir: None, network: LiquidNetwork::Testnet, payment_timeout_sec: 15, sync_service_url: Some(BREEZ_SYNC_SERVICE_URL.to_string()), @@ -210,7 +204,6 @@ impl Config { url: "localhost:19001".to_string(), }, working_dir: ".".to_string(), - cache_dir: None, network: LiquidNetwork::Regtest, payment_timeout_sec: 15, sync_service_url: Some("http://localhost:8088".to_string()), @@ -235,7 +228,6 @@ impl Config { use_waterfalls: false, }, working_dir: ".".to_string(), - cache_dir: None, network: LiquidNetwork::Regtest, payment_timeout_sec: 15, sync_service_url: Some("http://localhost:8089".to_string()), diff --git a/lib/core/src/payjoin/side_swap.rs b/lib/core/src/payjoin/side_swap.rs index 453055b..3867ef7 100644 --- a/lib/core/src/payjoin/side_swap.rs +++ b/lib/core/src/payjoin/side_swap.rs @@ -51,7 +51,7 @@ const SIDESWAP_BASE_USD_FEE_SAT: f64 = 4_000_000.0; pub(crate) struct SideSwapPayjoinService { config: Config, fiat_api: Arc, - persister: Arc, + persister: std::sync::Arc, onchain_wallet: Arc, rest_client: Arc, accepted_assets: OnceCell, @@ -61,7 +61,7 @@ impl SideSwapPayjoinService { pub fn new( config: Config, fiat_api: Arc, - persister: Arc, + persister: std::sync::Arc, onchain_wallet: Arc, rest_client: Arc, ) -> Self { @@ -491,7 +491,7 @@ mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); fn create_sideswap_payjoin_service( - persister: Arc, + persister: std::sync::Arc, ) -> Result<(Arc, Arc, SideSwapPayjoinService)> { let config = Config::testnet_esplora(None); let breez_server = Arc::new(BreezServer::new(STAGING_BREEZSERVER_URL.to_string(), None)?); diff --git a/lib/core/src/persist/migrations.rs b/lib/core/src/persist/migrations.rs index 36212d9..3f9675a 100644 --- a/lib/core/src/persist/migrations.rs +++ b/lib/core/src/persist/migrations.rs @@ -338,5 +338,11 @@ pub(crate) fn current_migrations(network: LiquidNetwork) -> Vec<&'static str> { ) STRICT; ", "ALTER TABLE receive_swaps ADD COLUMN bolt12_offer TEXT;", + " + CREATE TABLE IF NOT EXISTS wallet_updates ( + id INTEGER NOT NULL PRIMARY KEY, + data BLOB NOT NULL + ) STRICT; + ", ] } diff --git a/lib/core/src/persist/mod.rs b/lib/core/src/persist/mod.rs index 6a81d09..912dc56 100644 --- a/lib/core/src/persist/mod.rs +++ b/lib/core/src/persist/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod model; pub(crate) mod receive; pub(crate) mod send; pub(crate) mod sync; +pub(crate) mod wallet_updates; use std::collections::{HashMap, HashSet}; use std::ops::Not; diff --git a/lib/core/src/persist/wallet_updates.rs b/lib/core/src/persist/wallet_updates.rs new file mode 100644 index 0000000..e2bba4b --- /dev/null +++ b/lib/core/src/persist/wallet_updates.rs @@ -0,0 +1,126 @@ +use super::Persister; + +use anyhow::Result; +use rusqlite::{OptionalExtension, TransactionBehavior}; + +impl Persister { + /// Inserts a new wallet update if the provided index matches the next index + pub(crate) fn insert_wallet_update(&self, index: u64, update: &[u8]) -> Result<()> { + let mut conn = self.get_connection()?; + let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + + let next_index = self.get_next_index(&tx)?; + + // Only allow inserting at next_index + if index != next_index { + return Err(anyhow::anyhow!( + "Invalid index for insert: tried {} - must be {}", + index, + next_index + )); + } + + tx.execute( + "INSERT INTO wallet_updates (id, data) VALUES (?, ?)", + (index, update), + )?; + + tx.commit()?; + Ok(()) + } + + pub(crate) fn get_next_wallet_update_index(&self) -> Result { + let conn = self.get_connection()?; + self.get_next_index(&conn) + } + + pub(crate) fn get_wallet_update(&self, index: u64) -> Result>> { + let conn = self.get_connection()?; + let data: Option> = conn + .query_row( + "SELECT data FROM wallet_updates WHERE id = ?", + [index], + |row| row.get(0), + ) + .optional()?; + + Ok(data) + } + + pub(crate) fn clear_wallet_updates(&self) -> Result<()> { + let conn = self.get_connection()?; + conn.execute("DELETE FROM wallet_updates", [])?; + Ok(()) + } + + fn get_next_index(&self, conn: &rusqlite::Connection) -> Result { + let max_index: Option = + conn.query_row("SELECT MAX(id) FROM wallet_updates", [], |row| row.get(0))?; + Ok(max_index.map_or(0, |max| max + 1)) + } +} + +#[cfg(test)] +mod tests { + use crate::test_utils::persist::create_persister; + use anyhow::Result; + + #[cfg(feature = "browser-tests")] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + #[sdk_macros::test_all] + fn test_wallet_updates_basic_operations() -> Result<()> { + create_persister!(storage); + + // Test initial state + assert_eq!(storage.get_next_wallet_update_index()?, 0); + + // Test inserting first update + let update1 = b"test update 1"; + storage.insert_wallet_update(0, update1)?; + assert_eq!(storage.get_next_wallet_update_index()?, 1); + assert_eq!(storage.get_wallet_update(0)?, Some(update1.to_vec())); + + // Test inserting second update + let update2 = b"test update 2"; + storage.insert_wallet_update(1, update2)?; + assert_eq!(storage.get_next_wallet_update_index()?, 2); + assert_eq!(storage.get_wallet_update(1)?, Some(update2.to_vec())); + + // Test clearing updates + storage.clear_wallet_updates()?; + assert_eq!(storage.get_next_wallet_update_index()?, 0); + + // Verify we can insert again + storage.insert_wallet_update(0, update1)?; + + Ok(()) + } + + #[sdk_macros::test_all] + fn test_wallet_updates_invalid_index() -> Result<()> { + create_persister!(storage); + + // Test inserting with invalid index + let update = b"test update"; + assert!(storage.insert_wallet_update(1, update).is_err()); + + // Insert first update + storage.insert_wallet_update(0, update)?; + + // Test inserting with index too far ahead + assert!(storage.insert_wallet_update(2, update).is_err()); + + Ok(()) + } + + #[sdk_macros::test_all] + fn test_wallet_updates_get_nonexistent() -> Result<()> { + create_persister!(storage); + + // Test getting non-existent update + assert_eq!(storage.get_wallet_update(0)?, None); + + Ok(()) + } +} diff --git a/lib/core/src/receive_swap.rs b/lib/core/src/receive_swap.rs index 903fdc7..ea0c87f 100644 --- a/lib/core/src/receive_swap.rs +++ b/lib/core/src/receive_swap.rs @@ -28,7 +28,7 @@ pub const DEFAULT_ZERO_CONF_MAX_SAT: u64 = 1_000_000; pub(crate) struct ReceiveSwapHandler { config: Config, onchain_wallet: Arc, - persister: Arc, + persister: std::sync::Arc, swapper: Arc, subscription_notifier: broadcast::Sender, liquid_chain_service: Arc, @@ -50,7 +50,7 @@ impl ReceiveSwapHandler { pub(crate) fn new( config: Config, onchain_wallet: Arc, - persister: Arc, + persister: std::sync::Arc, swapper: Arc, liquid_chain_service: Arc, ) -> Self { diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs index 1c2d1da..fbc4cb6 100644 --- a/lib/core/src/recover/recoverer.rs +++ b/lib/core/src/recover/recoverer.rs @@ -36,7 +36,7 @@ pub struct Recoverer { onchain_wallet: Arc, liquid_chain_service: Arc, bitcoin_chain_service: Arc, - persister: Arc, + persister: std::sync::Arc, } impl Recoverer { @@ -46,7 +46,7 @@ impl Recoverer { onchain_wallet: Arc, liquid_chain_service: Arc, bitcoin_chain_service: Arc, - persister: Arc, + persister: std::sync::Arc, ) -> Result { Ok(Self { master_blinding_key: MasterBlindingKey::from_hex( diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index ac8ccee..2321180 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -97,7 +97,7 @@ pub struct LiquidSdkBuilder { liquid_chain_service: Option>, onchain_wallet: Option>, payjoin_service: Option>, - persister: Option>, + persister: Option>, recoverer: Option>, rest_client: Option>, status_stream: Option>, @@ -161,7 +161,7 @@ impl LiquidSdkBuilder { self } - pub fn persister(&mut self, persister: Arc) -> &mut Self { + pub fn persister(&mut self, persister: std::sync::Arc) -> &mut Self { self.persister = Some(persister.clone()); self } @@ -198,16 +198,6 @@ impl LiquidSdkBuilder { LiquidSdk::validate_breez_api_key(breez_api_key)? } - let fingerprint_hex: String = - Xpub::decode(self.signer.xpub()?.as_slice())?.identifier()[0..4].to_hex(); - let cache_dir = self.config.get_wallet_dir( - self.config - .cache_dir - .as_ref() - .unwrap_or(&self.config.working_dir), - &fingerprint_hex, - )?; - let persister = match self.persister.clone() { Some(persister) => persister, None => { @@ -216,7 +206,7 @@ impl LiquidSdkBuilder { "Must provide a Wasm-compatible persister on Wasm builds" )); #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] - Arc::new(Persister::new_using_fs( + std::sync::Arc::new(Persister::new_using_fs( &self.get_working_dir()?, self.config.network, self.config.sync_enabled(), @@ -247,7 +237,6 @@ impl LiquidSdkBuilder { None => Arc::new( LiquidOnchainWallet::new( self.config.clone(), - cache_dir, persister.clone(), self.signer.clone(), ) @@ -386,7 +375,7 @@ pub struct LiquidSdk { pub(crate) config: Config, pub(crate) onchain_wallet: Arc, pub(crate) signer: Arc>, - pub(crate) persister: Arc, + pub(crate) persister: std::sync::Arc, pub(crate) rest_client: Arc, pub(crate) event_manager: Arc, pub(crate) status_stream: Arc, diff --git a/lib/core/src/send_swap.rs b/lib/core/src/send_swap.rs index a52f865..24579bc 100644 --- a/lib/core/src/send_swap.rs +++ b/lib/core/src/send_swap.rs @@ -34,7 +34,7 @@ use crate::{ pub(crate) struct SendSwapHandler { config: Config, onchain_wallet: Arc, - persister: Arc, + persister: std::sync::Arc, swapper: Arc, chain_service: Arc, subscription_notifier: broadcast::Sender, @@ -56,7 +56,7 @@ impl SendSwapHandler { pub(crate) fn new( config: Config, onchain_wallet: Arc, - persister: Arc, + persister: std::sync::Arc, swapper: Arc, chain_service: Arc, recoverer: Arc, diff --git a/lib/core/src/swapper/boltz/proxy.rs b/lib/core/src/swapper/boltz/proxy.rs index 86f6561..52be4e9 100644 --- a/lib/core/src/swapper/boltz/proxy.rs +++ b/lib/core/src/swapper/boltz/proxy.rs @@ -3,14 +3,13 @@ use std::sync::OnceLock; use crate::PRODUCTION_BREEZSERVER_URL; use anyhow::Result; use sdk_common::prelude::BreezServer; -use sdk_common::utils::Arc; use url::Url; use crate::{persist::Persister, swapper::ProxyUrlFetcher}; pub(crate) struct BoltzProxyFetcher { url: OnceLock>, - persister: Arc, + persister: std::sync::Arc, } pub(crate) fn split_proxy_url(url: &str) -> (Option, Option) { @@ -30,7 +29,7 @@ pub(crate) fn split_proxy_url(url: &str) -> (Option, Option) { } impl BoltzProxyFetcher { - pub(crate) fn new(persister: Arc) -> Self { + pub(crate) fn new(persister: std::sync::Arc) -> Self { Self { url: OnceLock::new(), persister, diff --git a/lib/core/src/swapper/subscription_handler.rs b/lib/core/src/swapper/subscription_handler.rs index 1ae3b99..bb8aabe 100644 --- a/lib/core/src/swapper/subscription_handler.rs +++ b/lib/core/src/swapper/subscription_handler.rs @@ -14,13 +14,13 @@ pub trait SubscriptionHandler: MaybeSend + MaybeSync { #[derive(Clone)] pub(crate) struct SwapperSubscriptionHandler { - persister: Arc, + persister: std::sync::Arc, status_stream: Arc, } impl SwapperSubscriptionHandler { pub(crate) fn new( - persister: Arc, + persister: std::sync::Arc, status_stream: Arc, ) -> Self { Self { diff --git a/lib/core/src/sync/mod.rs b/lib/core/src/sync/mod.rs index c210cba..575d06e 100644 --- a/lib/core/src/sync/mod.rs +++ b/lib/core/src/sync/mod.rs @@ -45,7 +45,7 @@ pub(crate) struct SyncCompletedData { pub struct SyncService { remote_url: String, client_id: String, - persister: Arc, + persister: std::sync::Arc, recoverer: Arc, signer: Arc>, client: Box, @@ -55,7 +55,7 @@ pub struct SyncService { impl SyncService { pub(crate) fn new( remote_url: String, - persister: Arc, + persister: std::sync::Arc, recoverer: Arc, signer: Arc>, client: Box, @@ -762,7 +762,7 @@ mod tests { } fn get_outgoing_record<'a>( - persister: Arc, + persister: std::sync::Arc, outgoing: &'a HashMap, data_id: &str, record_type: RecordType, diff --git a/lib/core/src/test_utils/chain_swap.rs b/lib/core/src/test_utils/chain_swap.rs index 9b9a5e1..067a2e7 100644 --- a/lib/core/src/test_utils/chain_swap.rs +++ b/lib/core/src/test_utils/chain_swap.rs @@ -26,7 +26,9 @@ lazy_static! { pub(crate) static ref TEST_LIQUID_OUTGOING_USER_LOCKUP_TX: lwk_wollet::elements::Transaction = utils::deserialize_tx_hex("020000000104c057ca341ce44c9e2e580ca66ba9c0b8691b6614d19faa2a6e9070fb26ad43910100000000ffffffffb3c25aefbd60e5594477108e49217c34d5b7045a13c2de425f90a4d08262cfa70000000000ffffffff31497f924303165ac02a11014693efc94b158d4dd006ce5f0b0b7faeef3fdf480000000000ffffffff5c32217e1f90d5dbc1369eb34a153735e0d562d74c18f610e84287787825477f0000000000ffffffff030bd76c792018d962ee22c80b634443220d5954138dc7f2145bd3817381b94c5a5a0804f2788aea2209fc324c38d37a132cfaeb3a2b488629e704f018df00115328e202a5c8970c2ec085227a5773e67038624b00abad99668437a44e45fa826227d11d225120d9544b0c28687bbe2d182c915ae7e2ec43351a17d1cb4e47804d4515440e73560b52f9d6e34d8b54e55c38e3981aefc8c23190b14febb590dc185a5ceebe8c22e609aa638eebc568eabadcd4030e3e4299022a547dcb3520eb3838ae24b29474ac4f0335ad1ae355380a129da6180ae7fde73afa549e8e41c2672cb5b59522453229501600143122fd042d2cde1b119be7bdcdc3d1b2c934751d016d521c38ec1ea15734ae22b7c46064412829c0d0579f0a713d1c04ede979026f01000000000000001c000000000000000002473044022006a61d46702d41fa46119d79875c34ca9dd2be60d7afe2231124db7013e6238502204e0149cafa2c8d6afb75eb6ea5dfa781d6e9f826d4c2c9ae214cbe7be9b77298012102418f21f2efe55a94f0deaa537ce1d4cc8a3dd32e0c8abe2ca9ec18d4dfced1d50000000247304402200e11216fbcb12eed58eb30f3b5013a9635b793f862fc53127fbfd3c744bc0fd5022021430d35bae4676dcc99000ee28a985bd186f5d395f695a1ae28d051f84e18fd0121029080d46117f1e5d3a0e8d193fbaa82ef7b5ed0ca1024e7136f3723ed535ef1c000000002473044022055a8a454ff491ad83e7cf216ec6fe091d5f65d8bd8dc08c0a8f33e81bb5d215502204798b393217ef2810f777c6eeca2819c1fa73a170abda9ff46a7e923da91ee340121030e9409ed7a96b63db1883be5604e438f3a4a4cd5a47372e3ee7fb4829864a3560000000247304402200420104cb02a8b869eb6008415570c7fcaa660aa9e7e93cc0facd432624614ff022071212e977f6e6ad0a916ae58b8c34d5a873f55064fabbf542b9b774d58c9caef012103a5316de2491f5b6300b13b1f9d59504dc36eb9a33242dd9cd501c868f48db66800830400071d1d5522466fad384dad542f2e960fa4e6750afc5da6387d3d84c5afea598a3f607452cb00359bee767a8328f9d62541f538836daffca9b5b741ae9f202b630e7910547485af9fa21a33ce8121958c035e5350f27edc0b9b79d9e9ce2dcc7371dbd5b1b6a6eca0bec7bca426d0514d6377584c73a636a306d5cc3bc9c2dfc1b3fd4e1060330000000000000001a6204b0042064e012f4a701f6920f96450989958d1c51a617701cfe292b9599fbb46905818e0b743ba16dff0c954a4a68040d6e63003cd1cb3beea903078b1fc87aad4d03053e1adb1e196273354c8d6e1389833ad343900b0e35bd0f376c3ea301b0cce5324d76d0c6cde3c8cafdc697e4fbd610ecdbfd952f52c8f6e67276481781968421f0fb865e094f220d11cb8517461450645eddccc4c2ccd48021bf5f526a28cae85d2c1d5e8f20c08cb3bb335af25b60babf2cbde3d55d52f72a9721f508f5414feb2fb4e1681bbe185150213cab5804e1d5187218778e8c1fa371a81034383fc69fbd07cf8882a12f2d40a69b0719521bc0a58d8c65fae966c1f8f001a39765519a3f3025033685c1460007ac5f8c04e45d245ad8218c3a15087779694a23f378792960eca27aeb1bc4b1a638dcd8224cdf6b865569b81b69b14b8752c178c61af2874e60b91d644fd9636feebf581ad1798e3827d5d169d2456e556f97b324c80caa2f6d75919b5729b050cfb847077970c53b20e6e0b70c9d51792a7f625e68d9792f7803f3dfb27e3ad59b28fe825356ffd455bc8ea7f3b5d8034eb52de70de7b12aaf80ae74dd3bc8223c2f33b14ab0de0d0af59b5f33d710c7eba39468ce6df129146885825f92315a088366b2254a9d62a89d5fa8f675d76d03087aa89fd74cc93ac73962fc42257c32c2f08b1ef44895d3a76d9f7a96170428aee8053199183d5dc0f371bb9901b832babcffefda291d11ea95c82af3f3378b61a16bcb6653bec7bfe02c534f9c4f3893e249420e42dec5ae85818f459e02a6eae754f66f601ed160b0cde6d6cf94bf6a786ea9c0e1107001e39717f68a2a3ea6f7ceae597ba5fdcee6b2ac3a38994ad0ec981a1a277c1a0f8b0ffda6fecb1ec2d2916c389aa50b784d62e4771b4361285a72ae1bad3fe7b7d3de9e65f4c4efe6a616204d8a3fc182b7a965c39a040aef8318ab783aa18b17018fc4186f1a5d49e13030cf90f3146ab2cb3a3259543342fe7a2c4824316828da2a65ef838dea9c8d4d8952c2e0d0ddffcbcb2b7db8183926dc3ae710062a982b0b11dc423a98ef0e75aac660e11ed3e926f2476391386cbfef18f4e0efb9b4bc372915251e99580db04f98fe0a6cb3b801f177f42553cac1026e39a4318a6655deb8d6d7a2a6acd1296cf83927a3d2ee8b15ce90d41a3896ea0be685a69aaf20b90c3a49f30ea989801aed514474a8e8f307133bf0cbf513c07f841f494a891d54a88cc57e79f8a29608492b7e36b48caaa682614b7eb7ce7902d8d55988f52b59c1de7c085602d161ce169551ad53ec47c52854e11574741466cb17cac7a4047bc0d173c13e1c204525e7e0184aeb1f6dcbfd84c9bf4eb8a2880cdcee87495a504d45652a3a30851845928dc027957585df5e22caf29ca6110bf538de2b30eaefaaa27a40e299fdd830efa54024729883bc8c69614318bfc2774af43a6bf60d53c32bdb8869ff42a901be1be631c616b26beea430334355617cda9661351bab7936235ece081775914ace312756b8144425cc64244ba7d875fe959dda3af32336666b952205dd6b476ef470ae90a8e82fda57896b7a6a9b3b09c458385ef33f282d8ca337e7c6835f3af9e5461313b9662e63758d6404bb8717f9d69de87260724b0b6ed7021b4ff66f83fc9c6451eff118f8bb5de438e3b599b85a0f65ee0ac8d8ace209521295a956b7b977207f6c210b6897c2632b05db803bc0671b68fbd26aeda97c65d8e33f0d9d6852073eca593dfef539bdc188f7393c2e7b6483c58ef2458729615f7c4402aa810c1690fde4dfe574e57ec5496449866bda42c81d4ebce8eadc9722000a63b9f4f2ffacb8128646143ff3966bd2044aeacfe07186bbaa987da90fbc58bbeff99243c9e8b0b9c39b6a72ea0f52be4647c332d9b93b38219186bd5cc0086a1750a94ebb523600464b6018ef53a590ec6d3119f40fa31fc1644b5da8aaad36a0bca6690f5676df63f9f86f7ae1bef9c47af701a392795b9d16a4900a4191d333694408fb8d31933847f8227c713a8628a486e3a19da851fdd82d785afa201918d44835c2dfbb6feee0683432dc35f1e092069c7c75bb213b2b11eb9bb2a8df641e0f3e15e97e9a331a50048d33e35f9b46f272b46f370db25068955daaa115c48c404d6757816f55415535e22a143e2b5b52b2fbc7e0e853c1a4c970b78cd7c275fdd736b8088951c19787a60ce4542f99d01ee70cbb08b9623cb0aa965acea04f0fe8d3c4c4fa50106f72910e37ea92a35373cdad686276ecddd2cb42e21a8276fef49317546ab98b71c558361502a96daf1198637f2f6033016cf14e29f2f2cf7d14585f8284f9fbc2ff5b00cabd91573090889ccba7de1d3e331f3c14d793335401fe1391c47132ae02cde4e68a5451e91a1e6ef54d0afaa6ce867a8024350636751ef8bc3085e5ee4f15da9e9fb3ff3ca10e4ef85508f4c08c61ddf9348dd50833f4d11922734ca2b5c24cc8f5b2728f9dd071ee98e8aa8470b3b8562e0de0bc8b74607298c675054d6b688db908384a163355b470b433c91bd416f296c531dcbdebc93f27039bc3daa78445e8ddfb922d445b54b061323869b571c0eaa6f3fa3bf647d9c82c16ae07f9a369da43b83ddf0f312137ac51f703263e5084d91cb7ccd01607315cb91aaa15cb6198d95d8206cc561a008d9581827aedc591f4f37681fa59c1c863a3b687b30f16a6fc736e53a9a81fdb5ee6d78eb21245a834912c11c0ca91937a66c1286bcbde12201de3ba077176ddc170dfe74a10f293cf10531587fa07c6cf50c98e700edf6922d728ffca09912e3d04f853b1e5636b55aeec799f3df79c32cb77ded8a31ba35367edecd9d050aaa6913a4102a8c0d6edcb39311cbfe9172bdf62e385e1c8ec7bb70ccac239916961c643477b476897fccc6c3875bc4b5ebc6ef39fe1fe93bcdd5183434a1d4245e86e9755fe1b8411b1623476bc44d1ce93ce87bc6455568a518b589d163d918bdbc24f62f259413085c69807626caf8fe0e9b4d781b074822500d68b33c846422b7137b281c5b340a3fa3d9a509cc791a29713420f7ebeeae68852a5ae8d7885441d9d43b8986a8f84edd2d0c359d2c2e5e1d9989d042db1df48a0b5965262a9c084d05f4f47d048fe8b25f0ac250984ac1d9518b6114b8e569c271ff40373418cb2599a943d65ebb0f62258fdb3bb9efe304b5370be1db4a1d43d3ab1373330b0a03731b0f2c3c74b1764c3d598c81dec0b9960dde0d590d5e00289a6da55a0b9924448b25b091222bbab393f5d15aa6404e05c8534a3099115431dd8ca88b18112ac34a44f023b8cc2a6c545fe5f84dff50134461e367ae73e34c4177c2df9e002cbd9e0a3e8d8001fa0a3cf6b9d2ed835f1b2a65775f272cca4d84588f4f9b699bc967d9f6918434dcba5ca3d85e78e04d9fedfe6ba56e8064626bfbe6ff725592beec16d92cfb71f45d35f6f14a24c174ee5c4d55a72d18c69e407855672a27c5f0686b6f4508d13713f99ab498b292dc10ab6a42d1f50d68b6cf7c2ec1e0838db3666eccb0deba9cd7d1c493aad68fc88aaa7730efdf6ba1c8115f55f723cb05bc4227eca984ec2ac648b6a3bd359d097c2873bead7837f02ed6a25d77b221c1c166c44722aa515c1d4049920b68ef28d849c449a50e92c7eae1555d6e43e1cbfc05d953cdf87d7ed89d359a95fb21e9da1847e96ca92ffe8ea847c7f646a6dfa9d4b391ab8bcfc5e013c95eda7962e0160e346a578270c578e09a3d1787e5dc89b40f7175432cbcd50e06c4d57bed5ea20d812efe3b6a6fe3dc303b563be1943cb42964819373bab43d956fdb317acb440a566b7cda6b101acf53e1cea8f9e8b73e1805abc8c0ff0f5872f696e75f05f006c9913bda0123a3d4ea24e7c6d516e5e54c9512dfb09d8338ba1f5ef3e565ca2642a28e7a1bdfc28cc3ab1803cd03f27b166af791a38645299e5d1e3e6d325d71781e80cf70c90fd170d354f007369fd02f4644e2861d5442a1bfb720df2cbb1bb4f740b6f9bfc57161f8521f6b9c924fa73b23aedd8ccbc2c864cd7c97f27745a0b3e69a5b6a6a549dda72ba8dfd1388856df5f5e85ea4148213865fd0247f22e00d78e00f76ca99def7508c2b1221db118ac9d9913fa23123a8500bc5ac5a59015d88ba5c0f6b34d8b43bb25fb34bd694050473afdb70b8af4e4d87464b4b3684c03b2426331caa605c6aaaef1e7d4616c5479461ba6aacd13142e95d393d0b295160f996e501899654b842138328224b03e3f36ebfa318cbc3be6dc3663b49f3946b3710f430acdc91068c8bdb0fd55f92ca5ef44191a7a924dc1e9b311ef3000201d7796d945ed4ddbb5a7fbbcd63f603a7de64dfea24388037e37a5fa047e32cfbaabc7cc831e92a317b3ded781c77637bffe88e67ceb770c68afae93d187532174dad0c692752740756aeb73483868193bb4067ff6897ada0ed75334e9827eac1fed865df73520624178205ba1527402f92e58a65e8d9404649571549cc5c123db837c12b17360c87e6d0eab58d86c831b726fde172a95154789c38b60475c066670bce0a070e6f1f7b4b3200818075b4a5e90e78215dc35461d2f44e5fa6ddc1e06702614ce26c29c6f2d69cdb45721de105d60e05320d0076a35e378df3b4da76a6166fe1e899d70a853e9214dcd8c22ff87fdb4722f6f5603bd52558b0f1255caed47a8b473816f28702927c06b7f14fc416f6b819e7c805e4abd3bf9a97800a8e13e2617b683478f2bfe39e9872f4616bae103f0e99174fe0e2aa0c7d905dd77fe96278ed03c9e4ceceaa2e25c29e92fe6cd88e26296fbd681e4428adb5127898c5cff93079640da7d667a6a15f8bc86c7f7edacd15bca189e1370256a538e97c9c2b80ff820b4be71fa94d3333d10d913e49bbcb81640c96b02456bcd287ff63f74249dca3c9232b84c28b8cf2bd4fa9a2ed9a12f8d50dbbdf955a7aacd2aa1928ec1c7eccec1726e3a624cec68dbdd44deaf8b84055dd906cf0da36496c9aba37c2ebed0033d73fe256619a1652e56846f46c2324d65e38883379c66363bd233d491f1a8c70c46c7396f7359c35f8244cbc0c6676d53d9f4c1ee40c2aae1cce62e1668a3362ed6e5890085b04003c082362d445d51a60bb617465718b913488a778451bfbba5fb96bda7179ca7d71e610cc5efe532b766a05a988a41160df5a41a670fbf4b5e943a68d6ccb58b7443b32a8f5c3eb523cdb4968670ffaa06b3cfbfef2796b12e4c7950cf36bfe15d01cb43ab5cef529d5ae4b6e93e8cd13f8dd967f75f1039e92f47ac5bb3468afb4500eaa019653d6f59cd2194b4045bab544d93cb2cf8d9130494a346a48f29ade86c43fa02d8a267f80b26a82f90ee8ef4c05b8b2c1c169450e2a551c41da5b6aa32e330b9c65c0c73836a71dcc1ea3a08928aac61ec164394a1b4752e82d774f11fa1e50c047745b549a0888746c734056cb274fe24f0235a4e85df93e7851468bd52c9e1615c9549f6010493f6bc261a5c92690f3cd7f04d86d864a292b46a41df1e78505b9b2bc41c65f69acccd95bb4a0234e934b547660fecc572204fff78f6200cec28006bb6518c2ab0600c7ddb0108c327a2a33857c0eff83d6d9bfcbed78f13a6429f18bd9c09cd436ab989d09491b40db1d4f3f46d94a484e258ae27511468d707c858193e547060b8f60922123835ab59890278441375ead15cbec41dc3de7275b0cea6930efae4043d674c8c589906206f6255b40759bda936e85e013d9202f7b01fd056db4749c0887f8e152e45f8b40e806d9e662ba536853ffe49e5317d031394e5463e48564917bde8d4f9e2b79f0930c00e21da38fb90e830400072a18156bd93c3a781570bff6ff91ed25798c619b0e6179526c32d98699efdcfa352b115ad022a85d13b0a78b8f523fd6e26ca15f1cc510cb50a10043e2ac33152d7226df08ba328773519c683c7c59cc614babe67579886169458c1f1f973bfca3589a26a74211c58fc2e2a5d6b3f548b3f8bb2a4d0cd5d5702b8120cb87df33fd4e1060330000000000000001183f83019ac6d0581ec201c7212ce550b5a4519dca8113806e0f2792971210f6f1248f429294cf09ad67968a5998da95c420a6cca1bb843b1afaf9975a3799751df7394812045fddef2c75338b6f210fe1ddee0ab7605053f6afcfc29e1ddb61a0afcc13759ee48b80c14d449985699fa052037d976d37a6269c0f55c8fb3e68bd9d77c778f19a5711309be6fb89fee839ac04deb4856053a758ded3040135a883015a3960242677b8d9786a88d1b898b316bcc1987c9d72ddfc7dedfeb4ee011f1a6a9c315928c8d415e54cb0580876d79e31a9a62c3110ab849e2e5eeae485d722036e1e2a562e156aeeed3ad17a823507cdfd180046175d71c5b8a399306cb20c26a07af86bfeb98a80733c284f3ead0fe557661b841dd2179233951a06e6342754c00133e93b9b1abcab44edd727bec029bc51c159c3332c5e113ce8b4f9b42020b0663ce4fa22bf2c34315a516f68d48aa61546103669b9f0697f21425cd68be48f225522b6bbfe76e8d6d38665f35f02344ae2546eb3227d8d96cf44da2cd236579155efd792c2d75bc3d9fef24b6b91031e9feddd2b9172d52866a46df27c4c648d6c4a141189fefaa16ef2987f076ff328ea1f49ad602055749e4aba76755ae27dfc8b8bac05833c19cf22b0c859bb39da8f2e9431576c362b1c220dcd6f1f5a3e12d7856be4332dacbc79f70c76abe45a5d578ea1b2e5da847e21eb1a24d4bfb1f51646184e9ad3d69b922d800798a74e044586624922f591a347fd12eb8459322a30327464b8b3777d33c32acd923f78e2760089066289965dc895b25394b0989f6e777ed958e6355118b6c07fe72edc6d60a72c2f6feb22b325e7cefdd7cf149c89ffcaf574d0d974b02a8bafabbf3abdcd7add7a0a1eb486ab6d376816a806c964015f6b69152d197cfb4922dc90b6a1f945de43dcea62484b05bc1992ea5bedad7c21b588ac0030334d48f8521e49d779f8af965f627aa31d63846f69519268dc9d10075a8a1cdcf613fdd6604b7fc988988c7124dc89c9a988a756ed71a5c2e38f427afbb654bdc79119b376d28b3606e89e5a65590ba7a6a3cfde5d86a9705603783a61d4da4b4eb252988a73c7e65a0560d8525288a8dfb4079ddd3ffff81541057edd635bf7b087e8484945eebaabc4f9e317239988882b4f0c39196ec941017a859a702dfd0c6df87be15a08d6c325a0ed5856be386d8bbea88c203e0e7ee929c4b054975d0c318be4885ae604148c385c064588b4b564d247d6d5e8cb4424a7ebc0544a2aa7482c6edf0940621ea26e74d1caf4d5c0df2c5333b9d8aa4b17878c44e7e47f3bb942f8ea7306c2988dd27b47e4f85bf1c2e4fa9844aac375675716113d6606112b03614f192f514e5c9a42d4811cfb5a610f872210cd234102a2f04a1c4561f0d19abc7a874ddc308f23173ef98e8e3e142343157364bd2893910963fbff5bc2cd93c01ba0ea70997b9f2092919d885d1197138bec10230d1a964c701df4f9ffe4df726e6fbcea1a49de7409de76ae3b9f6ec27e2dfe0d5bf4ff28237d3daab71fe4cb0fa913afffe55fdae8cb1246a86ed88ba7dbc3275268d1de94fbd371325ba0b8bccd94632a185ec8b16da9e4d291b1870b0d789eeba69e01b25a5b25dd75d94705eaed68f9d92728988148a9a4ca273f2a5ccca53febf34fafe092f62ffa0698a0c5de8df8c0359e9e47aeaa366bdb9d1036ea4aa47a3e545dabf4712359c41e89cf4f39dfcc7898adce95513832bd26160d3b5472cad29ef87b77f96076fb69a82f1b9f8b39e7e3943dd94b7f78f6a7e7b488c30890c9ba71effcc4167f61eafe687e8531f759f96dc6adeb2157c7607ef48c6bf627dd92cbd0173d75eef5698666f32cb1d3f6d5b0169928efc0034b70eb059cae71278e710e98519e276ba3639aa41cd4bfb5e2cd892639301623394696f8bdfbac99db798cd82959264517e0c28b20704deab62122e8f35fb20ed774f9c05ef23373b77b0cb9ab0eacb654026a607504c5e13ad4adcf26ba12ff07b0642db4dfd9bffc0b7de20e02e1684f62f4649d2be51ec40ac37bfec3a82415128c834b608da0f06e6aba7087a19e244f5024d06a32b1351fd27fafc34ec7a11b94af8dd5dfb6db4788d0e3b5eca889a2d36a0d976fd7c5a25e6b97ff22aa3f0eae12a4663e0c9c9b33f99b75dbd80e16ea5194fd3ad77ee7fbd8008251baa8d75cf9e5a638d381ea5276f72d7cdc781cca1e95515fad0ab83f78c62755e4dcf204a751e1172a0b2f7bb5d95c792566b53f068ed1a318f443535c0633097a51d7fc5debb43b12b5fca482ea8317660ee19b6d04c938aae4612ff1b03900b630b81d605a229985d5785899e1f988717d4fde9cf72db4670582217bc98af826ea686f9b5ff625fa28a8a56e26e931feca25efcb585e3390ad34d34c2e76ca6efbc45f56127014d04d89dd5a1de3281f52548161c511d3338b9d344e738c9b683b0f99ed1152a279375f10f48a852739cd2f7bac8ce543afcfffce3a74452aff6112a1daea5405067556c55ba75b9a224280d6eda44429337801dc6e8f1b01dfb7eea727fa5c32fada0f344921c8bce82724269971c1cf6950ea842db86388217fea5f92550ef09bfe3a3edb803383e330bdee2654723ad89e44676ce611118a155ac412d52f9a9883bcdfc5b68db852f21fbbd51c5f2103820bc8e36418fe724b32a77ccb9f5eddb22b3d6ebd5ba3bec5498e9942b475280768898d25215c9f19502277ae6e7592b3263e360a1a63d1bf94a2e34358dc5a032ae37b0a0ff6b7f1dec514acb86da185a081bc39a4a0eea2edd3159c00c23d357ba845448355fcd85f7a1cb2fa185e6f612ceec79af15143d772a71256843fcc28168f8d1a0af45ef15e60c7b2b95619aaba83e4189397ff08a84b0f37c91cc61f3f7c29beb76b7b7cba682bc6699e78ea99becbc89298b306d1f8fbfb5361990eef08b52cfc56088ccbcc908091b19fe2401fa48d6ce543a263502681ba2637b3df71f62f6b4874d3e10a85abdb0b1578cec6ea5fc80f1e759f8f9ca4dd70f536b8a01b79c4be69eece7d7942b80698de60ddb8afcfde76613e8f6c617e75c5b9db9e80fcb51d5228131673a0124df6e646a470180157c72f9035080b8673ec73f3c1bc0547521ba878fc109bade65f90724c79e0e2e2ec6bb7fe319a4515d5c1a080254790662dbc6aefec8de5e02920151a165931a69d91c95d7bd4390de410d2c11c06c2d00bfa13b15676929385dd7c50488431a055bc6b3397a8de6b05ed3174011f47591aaea3d128582ab67ca009f930d59a8abd808a6eb29a93955513fba0c978ee2b91f48f999e411279e69b6befd820730842632d5f1af28970b3fb3d1072fe57a340e44061d354c8343b3080cb759a5bf5ab18dede45cecb67d0a952bca7d41447de1983d5de06ddc7628402803963e26832c1059bd5eaf7f47d5e33cdb5312c73e0afd06fce20e13c8a6c1184d39372eb5df3b38f24a4f748631170ee8edd3594070202dd4b85a76e6532fb6a34d1cc340bdf9ca0abf934a68230c570a70e15d47f4e5d06c0e4f09ed2fdc7a96fa58c497fba4104a44b058ae51a9210472a3aeb8429d584d6f654c1d6158c92b4413921e55e91193fe211755ed449595c21af177c50c06d7b056dfccc213b566071168f13cb24d868e1793720bb554f150594789fe7ea3b7a5e4d6994b49b0730cf22b5d7482747d41b91e498f604d53b9471b93466facdc9b47e39bc63179f454cf040fd9a008bcfb647e601e2c6df1d1424ac62b42d4f3cf15d3d1297bd9e0139a37bd8a1bfb8f7dcad121f95b8a8536ce896f1487af0b2ebaa7e5dfd36da2b99cdfbc07b5b05bdaa984e3b9d33c88d44ae1eb30c3d43194b1e239bd7dbcd869a92b636cd693de6f1853f9b5087660fcdd58a9e64860ffa76f88913b567da97952d55fba5109614b50aeef7455edfaa11b6779aeaa7586459b4521fc2fa6c169debdfeab3bf552f44491d3e943dbe7dafb34d54a18221362ba615a0352fa0bad114780a7d65305937f7dfabe2469153a02abcf353193be29df7717d8f43111c9c77ea2b875ee4f836ebea09f220fcf3691ada436bb86986489620a8c48a2b502c550bf8a41f3870705ab9980689a550e8051521ad3df55725c4e0dde9efb60430263bd11d862d20ce641e4c779392a5b9d90ec333007241f6f0be33a7ea96444c73d544308c4cae36e347079f2338ea12c75c187407f5cfc3940834660587cc33258577da30a4bb32998a66098235b19a98ec6efc0948b113b78eb8ff0434fcced1bc076ec697fea451cf50e419efeddf992fa731b088c5fa8374f042cf090bafb9d02228bb616e2d0583abc440e0b5aad6769bc53a887612ea19278ee41977ee9aaccb34189b4075d069d853a6149f84b675ea612e5c8fa09c1a11bac15f6ca3f1c09df5a5744909795a7bc39c685219ea9319e33f8fceab7fe59fac9b0b309ae753d0b1bd536420c389d8cc489deac3d749f06d7f8f9a135eac8f30545d5e7abf47b6a0f4c90a4d858e1bcf06be2f025913b0cdb85c0eaa102418728d36363e099b8c954d198075fd303b5dcb642ac9247c4224575f259b8ba530668639af03a456ee5ab506574f862e153514db9884ceba2b11a6a3883fb92baf3e610c1ad51066e979ed5c3ae206315afb9c6ac05bf998421e93bea58de0f59c57e82085453b4d5bb91987373175f9a53ca75e7e88b9f4acc0d6c3f767c08b9fc89df35a2d06f6cc0d85148fd6f95082ebe793716431b1cf38310bcf4285b7799c6976538608d567477790198b9e523a9d4c90d3d022f115b69948a94f78e221aafa51c99368996bdb6680602bbc8295a6008a332ff913a386dcdfed1b24b15c1279a117e6e55863427b8f0c0f459464e6fcffed99889d141aec53c5b4dd6728488247a4c204c6e8b37703a70f81e6ec98393e2defc92f1e9b15593e73b45a087fb6718adc6ce13b0071e8f1e6ee351bb22d3bac9b05d998faf485ad49a62ef6c518c5fbcf23a2857e1cefc7950c3ab96a75591c0a3b3655c207626777b10efe23079af3f5781662b3c7353b6cb28121a0c379df8cf189550c023f9b4159c84ca91d16f2e3cb54055a2eabfd4701cbd22825ea66d379bf157e36caca3c69ea42075f8a2882d438ca217483e1bf02cd968979cea2ad985f3b69c360da701e4f690fa09b4447fb2a70efc3f0d438b8900abfc7dd94be261497128ec4d16b550b781a6a732458e98d5517752ea7c5eb7ef078b045baf5d74f0c8375fb76b022d9e53e292cfb4defeca9e726c828433ea607766f499237d7e0cb46f4bc7678a839b3a91fce842e8624f80a08472760b831dc9f95b2cc7cbbfcc65bb2ae8df7ca947cd12c58ea893aad8a5a9b018033d1191eb15f1cd65df2d71ad65c56402cc3d460f800df4e8146f5131aafc10a0bca676e69d230c51ef2dbda6368c9ad3c585e39858266ed1aaa70fe6fa4d183cb87e4a29e6365f5346cf3e328dca541f54ce08e897d2ca76b2c6c2263872cad0de74254211f1c34d3ea38f4c8711ccfd6335dd07801218a63075f05a76573cd6dc8c69c0b177eb55c3925228b3e871fe6a8ff1c9d07cfd75d05eeeb681aabcd311cb69f3903745c6b92269c8e8594dbe7138390c7745b443f17f4d9cea978f9b6bf877ea0f835a948be8549b853d0c1b233871f46eefcb9e4bd481d6e6a03d4a36f064357899d1592fefca125db7aed85b236b141e66580def942084b1dfc3cd026bb09bd3b9f0c5d857885f8a9b355c5bfeb8bc04e9c97ff16b7b8a7f8fbe66d06ba6c351d396b245de3f10670d0cd69c5fca2e99521ce6a655d62c4f12d780b6367f3db4ef4c94dc5aaba0f270000").unwrap(); } -pub(crate) fn new_chain_swap_handler(persister: Arc) -> Result { +pub(crate) fn new_chain_swap_handler( + persister: std::sync::Arc, +) -> Result { let config = Config::testnet_esplora(None); let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); let onchain_wallet = Arc::new(MockWallet::new(signer)?); diff --git a/lib/core/src/test_utils/persist.rs b/lib/core/src/test_utils/persist.rs index 8af5893..a6e97af 100644 --- a/lib/core/src/test_utils/persist.rs +++ b/lib/core/src/test_utils/persist.rs @@ -138,7 +138,7 @@ macro_rules! create_persister { .collect(); res }; - sdk_common::utils::Arc::new($crate::persist::Persister::new_in_memory( + std::sync::Arc::new($crate::persist::Persister::new_in_memory( &db_id, $crate::model::LiquidNetwork::Testnet, true, @@ -153,7 +153,7 @@ macro_rules! create_persister { .to_str() .ok_or(anyhow::anyhow!("Could not create temporary directory"))? .to_string(); - sdk_common::utils::Arc::new($crate::persist::Persister::new_using_fs( + std::sync::Arc::new($crate::persist::Persister::new_using_fs( &temp_dir_path, $crate::model::LiquidNetwork::Testnet, true, diff --git a/lib/core/src/test_utils/receive_swap.rs b/lib/core/src/test_utils/receive_swap.rs index b82e689..5adba37 100644 --- a/lib/core/src/test_utils/receive_swap.rs +++ b/lib/core/src/test_utils/receive_swap.rs @@ -13,7 +13,9 @@ use super::{ wallet::{MockSigner, MockWallet}, }; -pub(crate) fn new_receive_swap_handler(persister: Arc) -> Result { +pub(crate) fn new_receive_swap_handler( + persister: std::sync::Arc, +) -> Result { let config = Config::testnet_esplora(None); let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); let onchain_wallet = Arc::new(MockWallet::new(signer)?); diff --git a/lib/core/src/test_utils/recover.rs b/lib/core/src/test_utils/recover.rs index 06f9602..1097ede 100644 --- a/lib/core/src/test_utils/recover.rs +++ b/lib/core/src/test_utils/recover.rs @@ -11,7 +11,7 @@ pub(crate) fn new_recoverer( signer: Arc>, swapper: Arc, onchain_wallet: Arc, - persister: Arc, + persister: std::sync::Arc, ) -> Result { let liquid_chain_service = Arc::new(MockLiquidChainService::new()); let bitcoin_chain_service = Arc::new(MockBitcoinChainService::new()); diff --git a/lib/core/src/test_utils/sdk.rs b/lib/core/src/test_utils/sdk.rs index d35943b..b31b122 100644 --- a/lib/core/src/test_utils/sdk.rs +++ b/lib/core/src/test_utils/sdk.rs @@ -18,7 +18,7 @@ use super::{ }; pub(crate) async fn new_liquid_sdk( - persister: Arc, + persister: std::sync::Arc, swapper: Arc, status_stream: Arc, ) -> Result> { @@ -37,7 +37,7 @@ pub(crate) async fn new_liquid_sdk( } pub(crate) async fn new_liquid_sdk_with_chain_services( - persister: Arc, + persister: std::sync::Arc, swapper: Arc, status_stream: Arc, liquid_chain_service: Arc, diff --git a/lib/core/src/test_utils/send_swap.rs b/lib/core/src/test_utils/send_swap.rs index b4a442e..67c70a2 100644 --- a/lib/core/src/test_utils/send_swap.rs +++ b/lib/core/src/test_utils/send_swap.rs @@ -13,7 +13,9 @@ use super::{ wallet::{MockSigner, MockWallet}, }; -pub(crate) fn new_send_swap_handler(persister: Arc) -> Result { +pub(crate) fn new_send_swap_handler( + persister: std::sync::Arc, +) -> Result { let config = Config::testnet_esplora(None); let signer: Arc> = Arc::new(Box::new(MockSigner::new()?)); let onchain_wallet = Arc::new(MockWallet::new(signer.clone())?); diff --git a/lib/core/src/test_utils/sync.rs b/lib/core/src/test_utils/sync.rs index 75c9be2..d7b2964 100644 --- a/lib/core/src/test_utils/sync.rs +++ b/lib/core/src/test_utils/sync.rs @@ -89,7 +89,7 @@ impl SyncerClient for MockSyncerClient { #[allow(clippy::type_complexity)] pub(crate) fn new_sync_service( - persister: Arc, + persister: std::sync::Arc, recoverer: Arc, signer: Arc>, ) -> Result<( diff --git a/lib/core/src/wallet/mod.rs b/lib/core/src/wallet/mod.rs index 4e3637c..1421191 100644 --- a/lib/core/src/wallet/mod.rs +++ b/lib/core/src/wallet/mod.rs @@ -16,6 +16,7 @@ use lwk_wollet::elements::{Address, AssetId, OutPoint, Transaction, TxOut, Txid} use lwk_wollet::secp256k1::Message; use lwk_wollet::{ElementsNetwork, WalletTx, WalletTxOut, Wollet, WolletDescriptor}; use maybe_sync::{MaybeSend, MaybeSync}; +use persister::SqliteWalletCachePersister; use sdk_common::bitcoin::hashes::{sha256, Hash}; use sdk_common::bitcoin::secp256k1::PublicKey; use sdk_common::lightning::util::message_signing::verify; @@ -32,9 +33,7 @@ use crate::{ }; use sdk_common::utils::Arc; -use crate::wallet::persister::{ - FsWalletCachePersister, NoWalletCachePersister, WalletCachePersister, -}; +use crate::wallet::persister::WalletCachePersister; #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] use lwk_wollet::blocking::BlockchainBackend; @@ -182,7 +181,7 @@ impl WalletClient { pub struct LiquidOnchainWallet { config: Config, - persister: Arc, + persister: std::sync::Arc, wallet: Arc>, client: Mutex>, pub(crate) signer: SdkLwkSigner, @@ -193,17 +192,15 @@ impl LiquidOnchainWallet { /// Creates a new LiquidOnchainWallet that caches data on the provided `working_dir`. pub(crate) async fn new( config: Config, - working_dir: String, - persister: Arc, + persister: std::sync::Arc, user_signer: Arc>, ) -> Result { let signer = SdkLwkSigner::new(user_signer.clone())?; let wallet_cache_persister: Arc = - Arc::new(FsWalletCachePersister::new( - working_dir.clone(), + Arc::new(SqliteWalletCachePersister::new( + std::sync::Arc::clone(&persister), get_descriptor(&signer, config.network)?, - config.network.into(), )?); let wollet = Self::create_wallet(&config, &signer, wallet_cache_persister.clone()).await?; @@ -218,49 +215,6 @@ impl LiquidOnchainWallet { }) } - /// Creates a new LiquidOnchainWallet that caches data in memory - pub async fn new_in_memory( - config: Config, - persister: Arc, - user_signer: Arc>, - ) -> Result { - let signer = SdkLwkSigner::new(user_signer.clone())?; - - 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), - signer, - wallet_cache_persister, - }) - } - - /// 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, signer: &SdkLwkSigner, @@ -581,8 +535,14 @@ impl OnchainWallet for LiquidOnchainWallet { .await { Ok(()) => Ok(()), - Err(lwk_wollet::Error::UpdateHeightTooOld { .. }) => { - warn!("Full scan failed with update height too old, wiping storage and retrying"); + Err(e) + if matches!( + e, + lwk_wollet::Error::UpdateHeightTooOld { .. } + | lwk_wollet::Error::PersistError(_) + ) => + { + warn!("Full scan failed due to {e}, reloading wallet and retrying"); let mut new_wallet = Self::create_wallet( &self.config, &self.signer, @@ -646,29 +606,11 @@ mod tests { create_persister!(storage); - let wallet: Arc = { - #[cfg(not(all(target_family = "wasm", target_os = "unknown")))] - { - // Create a temporary directory for working_dir - let working_dir = tempdir::TempDir::new("") - .unwrap() - .path() - .to_str() - .unwrap() - .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()) - .await - .unwrap(), - ) - }; + let wallet: Arc = Arc::new( + LiquidOnchainWallet::new(config, storage, sdk_signer.clone()) + .await + .unwrap(), + ); // Test message let message = "Hello, Liquid!"; diff --git a/lib/core/src/wallet/persister.rs b/lib/core/src/wallet/persister.rs index f7e05a6..4771c8f 100644 --- a/lib/core/src/wallet/persister.rs +++ b/lib/core/src/wallet/persister.rs @@ -1,13 +1,14 @@ use anyhow::Result; -use log::warn; -use lwk_wollet::{ElementsNetwork, FsPersister, NoPersist, WolletDescriptor}; +use log::debug; +use lwk_wollet::{PersistError, Update, WolletDescriptor}; use maybe_sync::{MaybeSend, MaybeSync}; -use std::path::PathBuf; -use std::str::FromStr; +use std::sync::{Arc, Mutex}; pub use lwk_wollet; -pub type LwkPersister = std::sync::Arc; +use crate::persist::Persister; + +pub type LwkPersister = Arc; #[sdk_macros::async_trait] pub trait WalletCachePersister: MaybeSend + MaybeSync { @@ -17,60 +18,84 @@ pub trait WalletCachePersister: MaybeSend + MaybeSync { } #[derive(Clone)] -pub struct FsWalletCachePersister { - working_dir: String, +pub struct SqliteWalletCachePersister { + persister: Arc, descriptor: WolletDescriptor, - elements_network: ElementsNetwork, } -impl FsWalletCachePersister { - pub(crate) fn new( - working_dir: String, - descriptor: WolletDescriptor, - elements_network: ElementsNetwork, - ) -> Result { - let working_dir_buf = PathBuf::from_str(&working_dir)?; - if !working_dir_buf.exists() { - std::fs::create_dir_all(&working_dir_buf)?; - } - +impl SqliteWalletCachePersister { + pub fn new(persister: Arc, descriptor: WolletDescriptor) -> Result { Ok(Self { - working_dir, + persister, descriptor, - elements_network, }) } } #[sdk_macros::async_trait] -impl WalletCachePersister for FsWalletCachePersister { +impl WalletCachePersister for SqliteWalletCachePersister { fn get_lwk_persister(&self) -> Result { - Ok(FsPersister::new( - &self.working_dir, - self.elements_network, - &self.descriptor, - )?) + SqliteLwkPersister::new(Arc::clone(&self.persister), self.descriptor.clone()) } async fn clear_cache(&self) -> 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(()) + self.persister.clear_wallet_updates() } } -#[derive(Clone)] -pub struct NoWalletCachePersister {} +pub(crate) struct SqliteLwkPersister { + persister: Arc, + descriptor: WolletDescriptor, + next: Mutex, +} -#[sdk_macros::async_trait] -impl WalletCachePersister for NoWalletCachePersister { - fn get_lwk_persister(&self) -> Result { - Ok(NoPersist::new()) +impl SqliteLwkPersister { + #[allow(clippy::new_ret_no_self)] + pub(crate) fn new( + persister: Arc, + descriptor: WolletDescriptor, + ) -> Result { + let next = persister.get_next_wallet_update_index()?; + Ok(Arc::new(Self { + persister, + descriptor, + next: Mutex::new(next), + })) + } +} + +impl lwk_wollet::Persister for SqliteLwkPersister { + fn get(&self, index: usize) -> std::result::Result, PersistError> { + let maybe_update_bytes = self + .persister + .get_wallet_update(index as u64) + .map_err(|e| PersistError::Other(e.to_string()))?; + maybe_update_bytes + .map(|update_bytes| { + Update::deserialize_decrypted(&update_bytes, &self.descriptor) + .map_err(|e| PersistError::Other(e.to_string())) + }) + .transpose() } - async fn clear_cache(&self) -> Result<()> { + fn push(&self, update: Update) -> std::result::Result<(), PersistError> { + debug!( + "LwkPersister starting push update with status {}", + update.wollet_status + ); + + let mut next = self.next.lock().unwrap(); + + let ciphertext = update + .serialize_encrypted(&self.descriptor) + .map_err(|e| PersistError::Other(e.to_string()))?; + + self.persister + .insert_wallet_update(*next, &ciphertext) + .map_err(|e| PersistError::Other(e.to_string()))?; + + *next += 1; + Ok(()) } } diff --git a/lib/core/tests/regtest/mod.rs b/lib/core/tests/regtest/mod.rs index 78a9203..fb76dda 100644 --- a/lib/core/tests/regtest/mod.rs +++ b/lib/core/tests/regtest/mod.rs @@ -80,25 +80,16 @@ impl SdkNodeHandle { sdk_common::prelude::PRODUCTION_BREEZSERVER_URL.to_string(), signer.clone(), )?; - let persister = Arc::new(breez_sdk_liquid::persist::Persister::new_in_memory( - &config.working_dir, - config.network, - config.sync_enabled(), - config.asset_metadata.clone(), - None, - )?); - - let onchain_wallet = Arc::new( - breez_sdk_liquid::wallet::LiquidOnchainWallet::new_in_memory( - config, - Arc::clone(&persister), - signer, - ) - .await?, - ); + let persister = + std::sync::Arc::new(breez_sdk_liquid::persist::Persister::new_in_memory( + &config.working_dir, + config.network, + config.sync_enabled(), + config.asset_metadata.clone(), + None, + )?); sdk_builder.persister(persister); - sdk_builder.onchain_wallet(onchain_wallet); let sdk = sdk_builder.build().await?; sdk.start().await?; diff --git a/lib/wasm/src/lib.rs b/lib/wasm/src/lib.rs index ecf641f..3d58e06 100644 --- a/lib/wasm/src/lib.rs +++ b/lib/wasm/src/lib.rs @@ -8,6 +8,7 @@ mod signer; use std::path::PathBuf; use std::rc::Rc; use std::str::FromStr; +use std::sync::Arc; use crate::event::{EventListener, WasmEventListener}; use crate::model::*; @@ -17,7 +18,6 @@ use breez_sdk_liquid::elements::hex::ToHex; use breez_sdk_liquid::persist::Persister; use breez_sdk_liquid::sdk::{LiquidSdk, LiquidSdkBuilder}; 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}; @@ -75,7 +75,7 @@ async fn connect_inner( None => None, }; - let persister = Rc::new(Persister::new_in_memory( + let persister = Arc::new(Persister::new_in_memory( &config.working_dir, config.network, config.sync_enabled(), @@ -83,19 +83,7 @@ async fn connect_inner( maybe_backup_bytes, )?); - 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), - Rc::clone(&signer), - ) - .await?; - sdk_builder.persister(persister.clone()); - sdk_builder.onchain_wallet(onchain_wallet); let sdk = sdk_builder.build().await?; sdk.start().await?; diff --git a/lib/wasm/src/model.rs b/lib/wasm/src/model.rs index 50eb7ed..39995cd 100644 --- a/lib/wasm/src/model.rs +++ b/lib/wasm/src/model.rs @@ -304,7 +304,6 @@ pub struct Config { pub liquid_explorer: BlockchainExplorer, pub bitcoin_explorer: BlockchainExplorer, pub working_dir: String, - pub cache_dir: Option, pub network: LiquidNetwork, pub payment_timeout_sec: u64, pub sync_service_url: Option, diff --git a/lib/wasm/src/platform/browser/mod.rs b/lib/wasm/src/platform/browser/mod.rs index 0dbc6bb..2bc33d7 100644 --- a/lib/wasm/src/platform/browser/mod.rs +++ b/lib/wasm/src/platform/browser/mod.rs @@ -1,10 +1,6 @@ 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}; @@ -16,35 +12,6 @@ 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(), diff --git a/lib/wasm/src/platform/browser/wallet_persister.rs b/lib/wasm/src/platform/browser/wallet_persister.rs deleted file mode 100644 index dc97ea4..0000000 --- a/lib/wasm/src/platform/browser/wallet_persister.rs +++ /dev/null @@ -1,332 +0,0 @@ -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::iter::ArrayMapIter; -use indexed_db_futures::object_store::ObjectStore; -use indexed_db_futures::query_source::QuerySource; -use indexed_db_futures::transaction::TransactionMode; -use indexed_db_futures::Build; -use log::{info, warn}; -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) -> anyhow::Result { - let persister = std::sync::Arc::clone(&self.lwk_persister); - Ok(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?; - info!("Loaded {} updates from storage", updates.len()); - - 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) -> Result, PersistError> { - Ok(self.updates.lock().unwrap().get(index).cloned()) - } - - fn push(&self, update: Update) -> 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_bytes: ArrayMapIter> = store - .get_all() - .await - .map_err(|e| anyhow!("Failed to get all updates: {}", e))?; - - let max_index = get_max_index(&store).await?; - if let Some(max_index) = max_index { - if max_index != updates_bytes.len() as u32 - 1 { - warn!("Wallet cache updates in IndexedDb are not contiguous. The (length - 1) is {}, but the max index is {max_index}. \ - This means it got corrupted (likely by concurrent SDK instances). Clearing the cache.", updates_bytes.len() as u32 - 1); - self.clear().await?; - return Ok(Vec::new()); - } - } - - let mut updates = Vec::new(); - for update_bytes_result in updates_bytes { - let update_bytes = - update_bytes_result.map_err(|e| anyhow!("Failed to get update bytes: {}", e))?; - 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}"))?; - - let max_index = get_max_index(&store).await?; - - if let Some(max_index) = max_index { - if index > max_index + 1 { - return Err(anyhow!( - "Index {index} is greater than the maximum index {max_index} + 1" - )); - } - } else if index != 0 { - return Err(anyhow!("Index {index} is not 0")); - } - - 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| 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| PersistError::Other(format!("Failed to open IndexedDB: {}", e)))?; - Ok(db) -} - -async fn get_max_index(store: &ObjectStore<'_>) -> Result, PersistError> { - let keys: ArrayMapIter = store.get_all_keys().await.map_err(|e| { - PersistError::Other(format!("Failed to get all keys from object store: {}", e)) - })?; - let keys = keys.filter_map(|k| k.ok()).collect::>(); - Ok(keys.into_iter().max()) -} - -#[cfg(test)] -mod tests { - use crate::platform::browser::wallet_persister::{ - open_indexed_db, AsyncWalletStorage, IndexedDbWalletStorage, IDB_STORE_NAME, - }; - use crate::platform::wallet_persister_common::tests::{get_lwk_update, get_wollet_descriptor}; - use anyhow::anyhow; - use indexed_db_futures::transaction::TransactionMode; - use indexed_db_futures::Build; - - wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); - - #[sdk_macros::async_test_wasm] - async fn test_load_updates() -> anyhow::Result<()> { - let desc = get_wollet_descriptor()?; - let storage = IndexedDbWalletStorage::new("test-load-updates".as_ref(), desc); - - // Non-contiguous keys are prevented (first index is 0) - assert!(storage - .persist_update(get_lwk_update(1, false), 1) - .await - .is_err()); - assert!(storage.load_updates().await?.is_empty()); - - storage.persist_update(get_lwk_update(5, false), 0).await?; - storage.persist_update(get_lwk_update(10, false), 1).await?; - - // Overwritting is allowed - storage.persist_update(get_lwk_update(15, false), 1).await?; - - let updates = storage.load_updates().await?; - assert_eq!(updates.len(), 2); - - // Non-contiguous keys are prevented - assert!(storage - .persist_update(get_lwk_update(10, false), 3) - .await - .is_err()); - - let updates = storage.load_updates().await?; - assert_eq!(updates.len(), 2); - - // If for any reason there is a gap, the cache is cleared on load - storage.persist_update(get_lwk_update(15, false), 2).await?; - delete_update_on_index(1, &storage.db_name).await; - let updates = storage.load_updates().await?; - assert_eq!(updates.len(), 0); - - Ok(()) - } - - async fn delete_update_on_index(index: u32, db_name: &str) { - let idb = open_indexed_db(db_name).await.unwrap(); - - let tx = idb - .transaction([IDB_STORE_NAME]) - .with_mode(TransactionMode::Readwrite) - .build() - .unwrap(); - - let store = tx.object_store(IDB_STORE_NAME).unwrap(); - - store.delete(index).await.unwrap(); - - tx.commit().await.unwrap(); - } -} diff --git a/lib/wasm/src/platform/db_backup_common.rs b/lib/wasm/src/platform/db_backup_common.rs index 7880bce..8cde88b 100644 --- a/lib/wasm/src/platform/db_backup_common.rs +++ b/lib/wasm/src/platform/db_backup_common.rs @@ -2,6 +2,7 @@ use anyhow::Result; use breez_sdk_liquid::model::{EventListener, SdkEvent}; use breez_sdk_liquid::persist::Persister; use std::rc::Rc; +use std::sync::Arc; use tokio::sync::mpsc::{Receiver, Sender}; pub(crate) struct ForwardingEventListener { @@ -40,7 +41,7 @@ impl BackupPersister { pub(crate) fn start_backup_task( &self, - persister: Rc, + persister: Arc, mut receiver: Receiver, ) { let storage = self.storage.clone(); diff --git a/lib/wasm/src/platform/default.rs b/lib/wasm/src/platform/default.rs index a995766..14a7a5b 100644 --- a/lib/wasm/src/platform/default.rs +++ b/lib/wasm/src/platform/default.rs @@ -7,19 +7,6 @@ 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 index 006c8eb..7cd179c 100644 --- a/lib/wasm/src/platform/mod.rs +++ b/lib/wasm/src/platform/mod.rs @@ -3,26 +3,20 @@ #[cfg(feature = "browser")] mod browser; #[cfg(feature = "browser")] -pub(crate) use browser::{ - create_db_backup_persister, create_onchain_wallet, create_wallet_persister, -}; +pub(crate) use browser::create_db_backup_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, -}; +pub(crate) use node_js::create_db_backup_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}; +pub(crate) use default::create_db_backup_persister; #[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"))] -pub(crate) mod wallet_persister_common; diff --git a/lib/wasm/src/platform/node_js/fs.rs b/lib/wasm/src/platform/node_js/fs.rs index 8b1f93d..c2c6a5a 100644 --- a/lib/wasm/src/platform/node_js/fs.rs +++ b/lib/wasm/src/platform/node_js/fs.rs @@ -37,22 +37,6 @@ pub(crate) fn ensure_dir_exists(path: &str) -> anyhow::Result<()> { 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() diff --git a/lib/wasm/src/platform/node_js/mod.rs b/lib/wasm/src/platform/node_js/mod.rs index af5262e..c854765 100644 --- a/lib/wasm/src/platform/node_js/mod.rs +++ b/lib/wasm/src/platform/node_js/mod.rs @@ -1,52 +1,17 @@ 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 deleted file mode 100644 index c3261a1..0000000 --- a/lib/wasm/src/platform/node_js/wallet_persister.rs +++ /dev/null @@ -1,155 +0,0 @@ -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) -> anyhow::Result { - let persister = Arc::clone(&self.lwk_persister); - Ok(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 deleted file mode 100644 index 27f1fb9..0000000 --- a/lib/wasm/src/platform/wallet_persister_common.rs +++ /dev/null @@ -1,132 +0,0 @@ -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)] -pub(crate) 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); - - pub(crate) 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(()) - } - - pub(crate) 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/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 4cf1aa2..38bcbcd 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -1970,22 +1970,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { Config dco_decode_config(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 14) throw Exception('unexpected arr length: expect 14 but see ${arr.length}'); + if (arr.length != 13) throw Exception('unexpected arr length: expect 13 but see ${arr.length}'); return Config( liquidExplorer: dco_decode_blockchain_explorer(arr[0]), bitcoinExplorer: dco_decode_blockchain_explorer(arr[1]), workingDir: dco_decode_String(arr[2]), - cacheDir: dco_decode_opt_String(arr[3]), - network: dco_decode_liquid_network(arr[4]), - paymentTimeoutSec: dco_decode_u_64(arr[5]), - syncServiceUrl: dco_decode_opt_String(arr[6]), - zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[7]), - breezApiKey: dco_decode_opt_String(arr[8]), - externalInputParsers: dco_decode_opt_list_external_input_parser(arr[9]), - useDefaultExternalInputParsers: dco_decode_bool(arr[10]), - onchainFeeRateLeewaySatPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[11]), - assetMetadata: dco_decode_opt_list_asset_metadata(arr[12]), - sideswapApiKey: dco_decode_opt_String(arr[13]), + network: dco_decode_liquid_network(arr[3]), + paymentTimeoutSec: dco_decode_u_64(arr[4]), + syncServiceUrl: dco_decode_opt_String(arr[5]), + zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[6]), + breezApiKey: dco_decode_opt_String(arr[7]), + externalInputParsers: dco_decode_opt_list_external_input_parser(arr[8]), + useDefaultExternalInputParsers: dco_decode_bool(arr[9]), + onchainFeeRateLeewaySatPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[10]), + assetMetadata: dco_decode_opt_list_asset_metadata(arr[11]), + sideswapApiKey: dco_decode_opt_String(arr[12]), ); } @@ -4045,7 +4044,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_liquidExplorer = sse_decode_blockchain_explorer(deserializer); var var_bitcoinExplorer = sse_decode_blockchain_explorer(deserializer); var var_workingDir = sse_decode_String(deserializer); - var var_cacheDir = sse_decode_opt_String(deserializer); var var_network = sse_decode_liquid_network(deserializer); var var_paymentTimeoutSec = sse_decode_u_64(deserializer); var var_syncServiceUrl = sse_decode_opt_String(deserializer); @@ -4060,7 +4058,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { liquidExplorer: var_liquidExplorer, bitcoinExplorer: var_bitcoinExplorer, workingDir: var_workingDir, - cacheDir: var_cacheDir, network: var_network, paymentTimeoutSec: var_paymentTimeoutSec, syncServiceUrl: var_syncServiceUrl, @@ -6587,7 +6584,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_blockchain_explorer(self.liquidExplorer, serializer); sse_encode_blockchain_explorer(self.bitcoinExplorer, serializer); sse_encode_String(self.workingDir, serializer); - sse_encode_opt_String(self.cacheDir, serializer); sse_encode_liquid_network(self.network, serializer); sse_encode_u_64(self.paymentTimeoutSec, serializer); sse_encode_opt_String(self.syncServiceUrl, serializer); diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index 66f6fd0..fea9f54 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -2752,7 +2752,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { cst_api_fill_to_wire_blockchain_explorer(apiObj.liquidExplorer, wireObj.liquid_explorer); cst_api_fill_to_wire_blockchain_explorer(apiObj.bitcoinExplorer, wireObj.bitcoin_explorer); wireObj.working_dir = cst_encode_String(apiObj.workingDir); - wireObj.cache_dir = cst_encode_opt_String(apiObj.cacheDir); wireObj.network = cst_encode_liquid_network(apiObj.network); wireObj.payment_timeout_sec = cst_encode_u_64(apiObj.paymentTimeoutSec); wireObj.sync_service_url = cst_encode_opt_String(apiObj.syncServiceUrl); @@ -7287,8 +7286,6 @@ final class wire_cst_config extends ffi.Struct { external ffi.Pointer working_dir; - external ffi.Pointer cache_dir; - @ffi.Int32() external int network; diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index 1f4fc4a..5e1059b 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -259,9 +259,6 @@ class Config { /// /// Prefix can be a relative or absolute path to this directory. final String workingDir; - - /// Directory in which the Liquid wallet cache is stored. Defaults to `working_dir` - final String? cacheDir; final LiquidNetwork network; /// Send payment timeout. See [LiquidSdk::send_payment](crate::sdk::LiquidSdk::send_payment) @@ -309,7 +306,6 @@ class Config { required this.liquidExplorer, required this.bitcoinExplorer, required this.workingDir, - this.cacheDir, required this.network, required this.paymentTimeoutSec, this.syncServiceUrl, @@ -327,7 +323,6 @@ class Config { liquidExplorer.hashCode ^ bitcoinExplorer.hashCode ^ workingDir.hashCode ^ - cacheDir.hashCode ^ network.hashCode ^ paymentTimeoutSec.hashCode ^ syncServiceUrl.hashCode ^ @@ -347,7 +342,6 @@ class Config { liquidExplorer == other.liquidExplorer && bitcoinExplorer == other.bitcoinExplorer && workingDir == other.workingDir && - cacheDir == other.cacheDir && network == other.network && paymentTimeoutSec == other.paymentTimeoutSec && syncServiceUrl == other.syncServiceUrl && diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index e2003bb..ba60d2c 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -5134,8 +5134,6 @@ final class wire_cst_config extends ffi.Struct { external ffi.Pointer working_dir; - external ffi.Pointer cache_dir; - @ffi.Int32() external int network; diff --git a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt index 016750f..122f10f 100644 --- a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt +++ b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt @@ -440,7 +440,6 @@ fun asConfig(config: ReadableMap): Config? { val paymentTimeoutSec = config.getDouble("paymentTimeoutSec").toULong() val syncServiceUrl = if (hasNonNullKey(config, "syncServiceUrl")) config.getString("syncServiceUrl") else null val breezApiKey = if (hasNonNullKey(config, "breezApiKey")) config.getString("breezApiKey") else null - val cacheDir = if (hasNonNullKey(config, "cacheDir")) config.getString("cacheDir") else null val zeroConfMaxAmountSat = if (hasNonNullKey( config, @@ -489,7 +488,6 @@ fun asConfig(config: ReadableMap): Config? { paymentTimeoutSec, syncServiceUrl, breezApiKey, - cacheDir, zeroConfMaxAmountSat, useDefaultExternalInputParsers, externalInputParsers, @@ -508,7 +506,6 @@ fun readableMapOf(config: Config): ReadableMap = "paymentTimeoutSec" to config.paymentTimeoutSec, "syncServiceUrl" to config.syncServiceUrl, "breezApiKey" to config.breezApiKey, - "cacheDir" to config.cacheDir, "zeroConfMaxAmountSat" to config.zeroConfMaxAmountSat, "useDefaultExternalInputParsers" to config.useDefaultExternalInputParsers, "externalInputParsers" to config.externalInputParsers?.let { readableArrayOf(it) }, diff --git a/packages/react-native/ios/BreezSDKLiquidMapper.swift b/packages/react-native/ios/BreezSDKLiquidMapper.swift index 8797671..29b397c 100644 --- a/packages/react-native/ios/BreezSDKLiquidMapper.swift +++ b/packages/react-native/ios/BreezSDKLiquidMapper.swift @@ -543,13 +543,6 @@ enum BreezSDKLiquidMapper { } breezApiKey = breezApiKeyTmp } - var cacheDir: String? - if hasNonNilKey(data: config, key: "cacheDir") { - guard let cacheDirTmp = config["cacheDir"] as? String else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "cacheDir")) - } - cacheDir = cacheDirTmp - } var zeroConfMaxAmountSat: UInt64? if hasNonNilKey(data: config, key: "zeroConfMaxAmountSat") { guard let zeroConfMaxAmountSatTmp = config["zeroConfMaxAmountSat"] as? UInt64 else { @@ -585,7 +578,7 @@ enum BreezSDKLiquidMapper { sideswapApiKey = sideswapApiKeyTmp } - return Config(liquidExplorer: liquidExplorer, bitcoinExplorer: bitcoinExplorer, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, syncServiceUrl: syncServiceUrl, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers, onchainFeeRateLeewaySatPerVbyte: onchainFeeRateLeewaySatPerVbyte, assetMetadata: assetMetadata, sideswapApiKey: sideswapApiKey) + return Config(liquidExplorer: liquidExplorer, bitcoinExplorer: bitcoinExplorer, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, syncServiceUrl: syncServiceUrl, breezApiKey: breezApiKey, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers, onchainFeeRateLeewaySatPerVbyte: onchainFeeRateLeewaySatPerVbyte, assetMetadata: assetMetadata, sideswapApiKey: sideswapApiKey) } static func dictionaryOf(config: Config) -> [String: Any?] { @@ -597,7 +590,6 @@ enum BreezSDKLiquidMapper { "paymentTimeoutSec": config.paymentTimeoutSec, "syncServiceUrl": config.syncServiceUrl == nil ? nil : config.syncServiceUrl, "breezApiKey": config.breezApiKey == nil ? nil : config.breezApiKey, - "cacheDir": config.cacheDir == nil ? nil : config.cacheDir, "zeroConfMaxAmountSat": config.zeroConfMaxAmountSat == nil ? nil : config.zeroConfMaxAmountSat, "useDefaultExternalInputParsers": config.useDefaultExternalInputParsers, "externalInputParsers": config.externalInputParsers == nil ? nil : arrayOf(externalInputParserList: config.externalInputParsers!), diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index b3cb609..f486e10 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -97,7 +97,6 @@ export interface Config { paymentTimeoutSec: number syncServiceUrl?: string breezApiKey?: string - cacheDir?: string zeroConfMaxAmountSat?: number useDefaultExternalInputParsers: boolean externalInputParsers?: ExternalInputParser[]