diff --git a/cli/Cargo.lock b/cli/Cargo.lock index c4176ea..27d4705 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -500,7 +500,7 @@ dependencies = [ [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/dangeross/boltz-rust?branch=savage-breez-latest#108d745dbdb2f8fb957d50dc92b5821562fb917d" +source = "git+https://github.com/dangeross/boltz-rust?branch=savage-breez-20240807#7e3ebb1fc43b8b9bda041d76a07fb85bd7251085" dependencies = [ "bip39", "bitcoin 0.31.2", diff --git a/cli/src/commands.rs b/cli/src/commands.rs index dfd1b8f..e3cc2df 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -158,6 +158,10 @@ pub(crate) enum Command { /// LNURL-auth endpoint lnurl: String, }, + /// Register a webhook URL + RegisterWebhook { url: String }, + /// Unregister the webhook URL + UnregisterWebhook, /// List fiat currencies ListFiat {}, /// Fetch available fiat rates @@ -526,6 +530,14 @@ pub(crate) async fn handle_command( command_result!(res) } + Command::RegisterWebhook { url } => { + sdk.register_webhook(url).await?; + command_result!("Url registered successfully") + } + Command::UnregisterWebhook => { + sdk.unregister_webhook().await?; + command_result!("Url unregistered successfully") + } Command::FetchFiatRates {} => { let res = sdk.fetch_fiat_rates().await?; command_result!(res) diff --git a/lib/Cargo.lock b/lib/Cargo.lock index 5996575..d53ca99 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -620,7 +620,7 @@ dependencies = [ [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/dangeross/boltz-rust?branch=savage-breez-latest#108d745dbdb2f8fb957d50dc92b5821562fb917d" +source = "git+https://github.com/dangeross/boltz-rust?branch=savage-breez-20240807#7e3ebb1fc43b8b9bda041d76a07fb85bd7251085" dependencies = [ "bip39", "bitcoin 0.31.2", 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 cf3cccb..8a3f99e 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 @@ -948,6 +948,10 @@ void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_refund(int64_t uintptr_t that, struct wire_cst_refund_request *req); +void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook(int64_t port_, + uintptr_t that, + struct wire_cst_list_prim_u_8_strict *webhook_url); + void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps(int64_t port_, uintptr_t that); @@ -961,6 +965,9 @@ void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_send_payment(in void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sync(int64_t port_, uintptr_t that); +void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook(int64_t port_, + uintptr_t that); + void frbgen_breez_liquid_wire__crate__bindings__binding_event_listener_on_event(int64_t port_, struct wire_cst_binding_event_listener *that, struct wire_cst_sdk_event *e); @@ -1159,10 +1166,12 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_receive_payment); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_recommended_fees); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_refund); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_restore); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_send_payment); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sync); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__binding_event_listener_on_event); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__breez_log_stream); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__connect); diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 50b1a2c..8b58507 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -630,6 +630,12 @@ interface BindingLiquidSdk { [Throws=LnUrlAuthError] LnUrlCallbackStatus lnurl_auth(LnUrlAuthRequestData req_data); + [Throws=SdkError] + void register_webhook(string webhook_url); + + [Throws=SdkError] + void unregister_webhook(); + [Throws=SdkError] sequence fetch_fiat_rates(); diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index 47b6104..1caef4c 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -166,6 +166,14 @@ impl BindingLiquidSdk { rt().block_on(self.sdk.lnurl_auth(req_data)) } + pub fn register_webhook(&self, webhook_url: String) -> Result<(), SdkError> { + rt().block_on(self.sdk.register_webhook(webhook_url)) + } + + pub fn unregister_webhook(&self) -> Result<(), SdkError> { + rt().block_on(self.sdk.unregister_webhook()) + } + pub fn fetch_fiat_rates(&self) -> Result, SdkError> { rt().block_on(self.sdk.fetch_fiat_rates()) } diff --git a/lib/core/Cargo.toml b/lib/core/Cargo.toml index 0305e6c..79f2910 100644 --- a/lib/core/Cargo.toml +++ b/lib/core/Cargo.toml @@ -14,7 +14,7 @@ frb = ["dep:flutter_rust_bridge"] [dependencies] anyhow = { workspace = true } bip39 = "2.0.0" -boltz-client = { git = "https://github.com/dangeross/boltz-rust", branch = "savage-breez-latest" } +boltz-client = { git = "https://github.com/dangeross/boltz-rust", branch = "savage-breez-20240807" } chrono = "0.4" env_logger = "0.11" flutter_rust_bridge = { version = "=2.2.0", features = [ diff --git a/lib/core/src/bindings.rs b/lib/core/src/bindings.rs index 76eabdd..de840c2 100644 --- a/lib/core/src/bindings.rs +++ b/lib/core/src/bindings.rs @@ -191,6 +191,14 @@ impl BindingLiquidSdk { .map_err(Into::into) } + pub async fn register_webhook(&self, webhook_url: String) -> Result<(), SdkError> { + self.sdk.register_webhook(webhook_url).await + } + + pub async fn unregister_webhook(&self) -> Result<(), SdkError> { + self.sdk.unregister_webhook().await + } + pub async fn fetch_fiat_rates(&self) -> Result, SdkError> { self.sdk.fetch_fiat_rates().await } diff --git a/lib/core/src/chain/bitcoin.rs b/lib/core/src/chain/bitcoin.rs index 24c1e97..dc35a6e 100644 --- a/lib/core/src/chain/bitcoin.rs +++ b/lib/core/src/chain/bitcoin.rs @@ -33,6 +33,13 @@ pub trait BitcoinChainService: Send + Sync { /// Get the transactions involved for a script fn get_script_history(&self, script: &Script) -> Result>; + /// Get the transactions involved for a script + async fn get_script_history_with_retry( + &self, + script: &Script, + retries: u64, + ) -> Result>; + /// Return the confirmed and unconfirmed balances of a script hash fn script_get_balance(&self, script: &Script) -> Result; @@ -72,35 +79,6 @@ impl HybridBitcoinChainService { config, }) } - - async fn get_script_history_with_retry( - &self, - script: &Script, - retries: u64, - ) -> Result> { - let script_hash = sha256::Hash::hash(script.as_bytes()) - .to_byte_array() - .to_hex(); - info!("Fetching script history for {}", script_hash); - let mut script_history = vec![]; - - let mut retry = 0; - while retry <= retries { - script_history = self.get_script_history(script)?; - match script_history.is_empty() { - true => { - retry += 1; - info!( - "Script history for {} got zero transactions, retrying in {} seconds...", - script_hash, retry - ); - thread::sleep(Duration::from_secs(retry)); - } - false => break, - } - } - Ok(script_history) - } } #[async_trait] @@ -154,6 +132,35 @@ impl BitcoinChainService for HybridBitcoinChainService { .collect()) } + async fn get_script_history_with_retry( + &self, + script: &Script, + retries: u64, + ) -> Result> { + let script_hash = sha256::Hash::hash(script.as_bytes()) + .to_byte_array() + .to_hex(); + info!("Fetching script history for {}", script_hash); + let mut script_history = vec![]; + + let mut retry = 0; + while retry <= retries { + script_history = self.get_script_history(script)?; + match script_history.is_empty() { + true => { + retry += 1; + info!( + "Script history for {} got zero transactions, retrying in {} seconds...", + script_hash, retry + ); + thread::sleep(Duration::from_secs(retry)); + } + false => break, + } + } + Ok(script_history) + } + fn script_get_balance(&self, script: &Script) -> Result { Ok(self.client.script_get_balance(script)?) } diff --git a/lib/core/src/chain/liquid.rs b/lib/core/src/chain/liquid.rs index 951aeef..8da0305 100644 --- a/lib/core/src/chain/liquid.rs +++ b/lib/core/src/chain/liquid.rs @@ -34,6 +34,13 @@ pub trait LiquidChainService: Send + Sync { /// Get the transactions involved in a list of scripts including lowball async fn get_script_history(&self, scripts: &Script) -> Result>; + /// Get the transactions involved in a list of scripts including lowball + async fn get_script_history_with_retry( + &self, + script: &Script, + retries: u64, + ) -> Result>; + /// Verify that a transaction appears in the address script history async fn verify_tx( &self, @@ -70,33 +77,6 @@ impl HybridLiquidChainService { network: config.network, }) } - - async fn get_script_history_with_retry( - &self, - script: &Script, - retries: u64, - ) -> Result> { - let script_hash = sha256::Hash::hash(script.as_bytes()) - .to_byte_array() - .to_hex(); - info!("Fetching script history for {}", script_hash); - let mut script_history = vec![]; - - let mut retry = 0; - while retry <= retries { - script_history = self.get_script_history(script).await?; - match script_history.is_empty() { - true => { - retry += 1; - info!("Script history for {script_hash} is empty, retrying in 1 second... ({retry} of {retries})"); - // Waiting 1s between retries, so we detect the new tx as soon as possible - thread::sleep(Duration::from_secs(1)); - } - false => break, - } - } - Ok(script_history) - } } #[async_trait] @@ -151,6 +131,33 @@ impl LiquidChainService for HybridLiquidChainService { } } + async fn get_script_history_with_retry( + &self, + script: &Script, + retries: u64, + ) -> Result> { + let script_hash = sha256::Hash::hash(script.as_bytes()) + .to_byte_array() + .to_hex(); + info!("Fetching script history for {}", script_hash); + let mut script_history = vec![]; + + let mut retry = 0; + while retry <= retries { + script_history = self.get_script_history(script).await?; + match script_history.is_empty() { + true => { + retry += 1; + info!("Script history for {script_hash} is empty, retrying in 1 second... ({retry} of {retries})"); + // Waiting 1s between retries, so we detect the new tx as soon as possible + thread::sleep(Duration::from_secs(1)); + } + false => break, + } + } + Ok(script_history) + } + async fn verify_tx( &self, address: &Address, diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index 27d885f..54de2fb 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -6,7 +6,9 @@ use boltz_client::swaps::boltz::{self, SwapUpdateTxDetails}; use boltz_client::swaps::{boltz::ChainSwapStates, boltz::CreateChainResponse}; use boltz_client::{Address, Secp256k1, ToHex}; use log::{debug, error, info, warn}; -use lwk_wollet::elements::Transaction; +use lwk_wollet::elements::hex::FromHex; +use lwk_wollet::elements::{Script, Transaction}; +use lwk_wollet::History; use tokio::sync::{broadcast, watch, Mutex}; use tokio::time::MissedTickBehavior; @@ -260,13 +262,16 @@ impl ChainSwapStateHandler { return Err(anyhow!("Unexpected payload from Boltz status stream")); }; - if let Err(e) = self.verify_lockup_tx(swap, &transaction, false).await { + if let Err(e) = self + .verify_server_lockup_tx(swap, &transaction, false) + .await + { warn!("Server lockup mempool transaction for incoming Chain Swap {} could not be verified. txid: {}, err: {}", swap.id, transaction.id, e); return Err(anyhow!( - "Could not verify transaction {}: {e}", + "Could not verify server lockup transaction {}: {e}", transaction.id )); } @@ -298,13 +303,19 @@ impl ChainSwapStateHandler { return Err(anyhow!("Unexpected payload from Boltz status stream")); }; - if let Err(e) = self.verify_lockup_tx(swap, &transaction, true).await { + if let Err(e) = self.verify_user_lockup_tx(swap).await { + warn!("User lockup transaction for incoming Chain Swap {} could not be verified. err: {}", swap.id, e); + return Err(anyhow!("Could not verify user lockup transaction: {e}",)); + } + + if let Err(e) = self.verify_server_lockup_tx(swap, &transaction, true).await + { warn!("Server lockup transaction for incoming Chain Swap {} could not be verified. txid: {}, err: {}", swap.id, transaction.id, e); return Err(anyhow!( - "Could not verify transaction {}: {e}", + "Could not verify server lockup transaction {}: {e}", transaction.id )); } @@ -340,13 +351,13 @@ impl ChainSwapStateHandler { match swap.refund_tx_id.clone() { None => { warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}"); - match swap.user_lockup_tx_id { - Some(_) => { + match self.verify_user_lockup_tx(swap).await { + Ok(_) => { info!("Chain Swap {id} user lockup tx was broadcast. Setting the swap to refundable."); self.update_swap_info(id, Refundable, None, None, None, None) .await?; } - None => { + Err(_) => { info!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed."); self.update_swap_info(id, Failed, None, None, None, None) .await?; @@ -434,13 +445,16 @@ impl ChainSwapStateHandler { return Err(anyhow!("Unexpected payload from Boltz status stream")); }; - if let Err(e) = self.verify_lockup_tx(swap, &transaction, false).await { + if let Err(e) = self + .verify_server_lockup_tx(swap, &transaction, false) + .await + { warn!("Server lockup mempool transaction for outgoing Chain Swap {} could not be verified. txid: {}, err: {}", swap.id, transaction.id, e); return Err(anyhow!( - "Could not verify transaction {}: {e}", + "Could not verify server lockup transaction {}: {e}", transaction.id )); } @@ -472,13 +486,19 @@ impl ChainSwapStateHandler { return Err(anyhow!("Unexpected payload from Boltz status stream")); }; - if let Err(e) = self.verify_lockup_tx(swap, &transaction, true).await { + if let Err(e) = self.verify_user_lockup_tx(swap).await { + warn!("User lockup transaction for outgoing Chain Swap {} could not be verified. err: {}", swap.id, e); + return Err(anyhow!("Could not verify user lockup transaction: {e}",)); + } + + if let Err(e) = self.verify_server_lockup_tx(swap, &transaction, true).await + { warn!("Server lockup transaction for outgoing Chain Swap {} could not be verified. txid: {}, err: {}", swap.id, transaction.id, e); return Err(anyhow!( - "Could not verify transaction {}: {e}", + "Could not verify server lockup transaction {}: {e}", transaction.id )); } @@ -514,8 +534,8 @@ impl ChainSwapStateHandler { match swap.refund_tx_id.clone() { None => { warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}"); - match swap.user_lockup_tx_id.clone() { - Some(_) => { + match self.verify_user_lockup_tx(swap).await { + Ok(_) => { warn!("Chain Swap {id} user lockup tx has been broadcast. Attempting refund."); let refund_tx_id = self.refund_outgoing_swap(swap).await?; info!("Broadcast refund tx for Chain Swap {id}. Tx id: {refund_tx_id}"); @@ -529,7 +549,7 @@ impl ChainSwapStateHandler { ) .await?; } - None => { + Err(_) => { warn!("Chain Swap {id} user lockup tx was never broadcast. Resolving payment as failed."); self.update_swap_info(id, Failed, None, None, None, None) .await?; @@ -832,7 +852,7 @@ impl ChainSwapStateHandler { } } - async fn verify_lockup_tx( + async fn verify_server_lockup_tx( &self, chain_swap: &ChainSwap, swap_update_tx: &SwapUpdateTxDetails, @@ -840,17 +860,25 @@ impl ChainSwapStateHandler { ) -> Result<()> { match chain_swap.direction { Direction::Incoming => { - self.verify_incoming_lockup_tx(chain_swap, swap_update_tx, verify_confirmation) - .await + self.verify_incoming_server_lockup_tx( + chain_swap, + swap_update_tx, + verify_confirmation, + ) + .await } Direction::Outgoing => { - self.verify_outgoing_lockup_tx(chain_swap, swap_update_tx, verify_confirmation) - .await + self.verify_outgoing_server_lockup_tx( + chain_swap, + swap_update_tx, + verify_confirmation, + ) + .await } } } - async fn verify_incoming_lockup_tx( + async fn verify_incoming_server_lockup_tx( &self, chain_swap: &ChainSwap, swap_update_tx: &SwapUpdateTxDetails, @@ -900,7 +928,7 @@ impl ChainSwapStateHandler { Ok(()) } - async fn verify_outgoing_lockup_tx( + async fn verify_outgoing_server_lockup_tx( &self, chain_swap: &ChainSwap, swap_update_tx: &SwapUpdateTxDetails, @@ -944,6 +972,70 @@ impl ChainSwapStateHandler { } Ok(()) } + + async fn verify_user_lockup_tx(&self, chain_swap: &ChainSwap) -> Result { + let script_history = match chain_swap.direction { + Direction::Incoming => self.fetch_incoming_user_script_history(chain_swap).await, + Direction::Outgoing => self.fetch_outgoing_user_script_history(chain_swap).await, + }?; + + match chain_swap.user_lockup_tx_id.clone() { + Some(user_lockup_tx_id) => { + script_history + .iter() + .find(|h| h.txid.to_hex() == user_lockup_tx_id) + .ok_or(anyhow!("Transaction was not found in script history"))?; + Ok(user_lockup_tx_id) + } + None => { + let txid = script_history + .first() + .ok_or(anyhow!("Script history has no transactions"))? + .txid + .to_hex(); + self.update_swap_info(&chain_swap.id, Pending, None, Some(&txid), None, None) + .await?; + Ok(txid) + } + } + } + + async fn fetch_incoming_user_script_history( + &self, + chain_swap: &ChainSwap, + ) -> Result> { + let swap_script = chain_swap.get_lockup_swap_script()?; + let address = swap_script + .as_bitcoin_script()? + .to_address(self.config.network.as_bitcoin_chain()) + .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))?; + let script_pubkey = address.script_pubkey(); + let script = script_pubkey.as_script(); + self.bitcoin_chain_service + .lock() + .await + .get_script_history_with_retry(script, 5) + .await + } + + async fn fetch_outgoing_user_script_history( + &self, + chain_swap: &ChainSwap, + ) -> Result> { + let swap_script = chain_swap.get_lockup_swap_script()?; + let address = swap_script + .as_liquid_script()? + .to_address(self.config.network.into()) + .map_err(|e| anyhow!("Failed to get swap script address {e:?}"))? + .to_unconfidential(); + let script = Script::from_hex(hex::encode(address.script_pubkey().as_bytes()).as_str()) + .map_err(|e| anyhow!("Failed to get script from address {e:?}"))?; + self.liquid_chain_service + .lock() + .await + .get_script_history_with_retry(&script, 5) + .await + } } #[cfg(test)] diff --git a/lib/core/src/frb_generated.io.rs b/lib/core/src/frb_generated.io.rs index e2c717b..97b4ea9 100644 --- a/lib/core/src/frb_generated.io.rs +++ b/lib/core/src/frb_generated.io.rs @@ -2724,6 +2724,15 @@ pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_re wire__crate__bindings__BindingLiquidSdk_refund_impl(port_, that, req) } +#[no_mangle] +pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook( + port_: i64, + that: usize, + webhook_url: *mut wire_cst_list_prim_u_8_strict, +) { + wire__crate__bindings__BindingLiquidSdk_register_webhook_impl(port_, that, webhook_url) +} + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps( port_: i64, @@ -2757,6 +2766,14 @@ pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sy wire__crate__bindings__BindingLiquidSdk_sync_impl(port_, that) } +#[no_mangle] +pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook( + port_: i64, + that: usize, +) { + wire__crate__bindings__BindingLiquidSdk_unregister_webhook_impl(port_, that) +} + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__binding_event_listener_on_event( port_: i64, diff --git a/lib/core/src/frb_generated.rs b/lib/core/src/frb_generated.rs index f57f907..2396bb4 100644 --- a/lib/core/src/frb_generated.rs +++ b/lib/core/src/frb_generated.rs @@ -39,7 +39,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.2.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1074530283; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 337003852; // Section: executor @@ -1164,6 +1164,55 @@ fn wire__crate__bindings__BindingLiquidSdk_refund_impl( }, ) } +fn wire__crate__bindings__BindingLiquidSdk_register_webhook_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + that: impl CstDecode< + RustOpaqueNom>, + >, + webhook_url: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "BindingLiquidSdk_register_webhook", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_that = that.cst_decode(); + let api_webhook_url = webhook_url.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, crate::error::SdkError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = crate::bindings::BindingLiquidSdk::register_webhook( + &*api_that_guard, + api_webhook_url, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps_impl( port_: flutter_rust_bridge::for_generated::MessagePort, that: impl CstDecode< @@ -1340,6 +1389,51 @@ fn wire__crate__bindings__BindingLiquidSdk_sync_impl( }, ) } +fn wire__crate__bindings__BindingLiquidSdk_unregister_webhook_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + that: impl CstDecode< + RustOpaqueNom>, + >, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "BindingLiquidSdk_unregister_webhook", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_that = that.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, crate::error::SdkError>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = + crate::bindings::BindingLiquidSdk::unregister_webhook(&*api_that_guard) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bindings__binding_event_listener_on_event_impl( port_: flutter_rust_bridge::for_generated::MessagePort, that: impl CstDecode, @@ -9534,6 +9628,15 @@ mod io { wire__crate__bindings__BindingLiquidSdk_refund_impl(port_, that, req) } + #[no_mangle] + pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook( + port_: i64, + that: usize, + webhook_url: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__bindings__BindingLiquidSdk_register_webhook_impl(port_, that, webhook_url) + } + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps( port_: i64, @@ -9567,6 +9670,14 @@ mod io { wire__crate__bindings__BindingLiquidSdk_sync_impl(port_, that) } + #[no_mangle] + pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook( + port_: i64, + that: usize, + ) { + wire__crate__bindings__BindingLiquidSdk_unregister_webhook_impl(port_, that) + } + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__binding_event_listener_on_event( port_: i64, diff --git a/lib/core/src/persist/cache.rs b/lib/core/src/persist/cache.rs index 38fdd99..106495c 100644 --- a/lib/core/src/persist/cache.rs +++ b/lib/core/src/persist/cache.rs @@ -5,6 +5,7 @@ use super::Persister; const KEY_SWAPPER_PROXY_URL: &str = "swapper_proxy_url"; const KEY_IS_FIRST_SYNC_COMPLETE: &str = "is_first_sync_complete"; +const KEY_WEBHOOK_URL: &str = "webhook_url"; impl Persister { pub fn get_cached_item(&self, key: &str) -> Result> { @@ -52,6 +53,18 @@ impl Persister { self.get_cached_item(KEY_IS_FIRST_SYNC_COMPLETE) .map(|maybe_str| maybe_str.and_then(|val_str| bool::from_str(&val_str).ok())) } + + pub fn set_webhook_url(&self, webhook_url: String) -> Result<()> { + self.update_cached_item(KEY_WEBHOOK_URL, webhook_url) + } + + pub fn remove_webhook_url(&self) -> Result<()> { + self.delete_cached_item(KEY_WEBHOOK_URL) + } + + pub fn get_webhook_url(&self) -> Result> { + self.get_cached_item(KEY_WEBHOOK_URL) + } } #[cfg(test)] diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index 4386369..adbd580 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use anyhow::Result; use boltz_client::swaps::boltz::{ChainSwapDetails, CreateChainResponse}; use rusqlite::{named_params, params, Connection, Row}; +use sdk_common::bitcoin::hashes::{hex::ToHex, sha256, Hash}; use serde::{Deserialize, Serialize}; use crate::ensure_sdk; @@ -20,6 +21,7 @@ impl Persister { " INSERT INTO chain_swaps ( id, + id_hash, direction, claim_address, lockup_address, @@ -35,10 +37,12 @@ impl Persister { created_at, state ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", )?; + let id_hash = sha256::Hash::hash(chain_swap.id.as_bytes()).to_hex(); _ = stmt.execute(( &chain_swap.id, + &id_hash, &chain_swap.direction, &chain_swap.claim_address, &chain_swap.lockup_address, @@ -117,7 +121,7 @@ impl Persister { pub(crate) fn fetch_chain_swap_by_id(&self, id: &str) -> Result> { let con: Connection = self.get_connection()?; - let query = Self::list_chain_swaps_query(vec!["id = ?1".to_string()]); + let query = Self::list_chain_swaps_query(vec!["id = ?1 or id_hash = ?1".to_string()]); let res = con.query_row(&query, [id], Self::sql_row_to_chain_swap); Ok(res.ok()) diff --git a/lib/core/src/persist/migrations.rs b/lib/core/src/persist/migrations.rs index 34b2169..0f21200 100644 --- a/lib/core/src/persist/migrations.rs +++ b/lib/core/src/persist/migrations.rs @@ -70,5 +70,10 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { destination TEXT NOT NULL, description TEXT NOT NULL );", + " + ALTER TABLE receive_swaps ADD COLUMN id_hash TEXT; + ALTER TABLE send_swaps ADD COLUMN id_hash TEXT; + ALTER TABLE chain_swaps ADD COLUMN id_hash TEXT; + ", ] } diff --git a/lib/core/src/persist/receive.rs b/lib/core/src/persist/receive.rs index b5085ab..39ab473 100644 --- a/lib/core/src/persist/receive.rs +++ b/lib/core/src/persist/receive.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use anyhow::Result; use boltz_client::swaps::boltz::CreateReverseResponse; use rusqlite::{named_params, params, Connection, Row}; +use sdk_common::bitcoin::hashes::{hex::ToHex, sha256, Hash}; use serde::{Deserialize, Serialize}; use crate::ensure_sdk; @@ -18,6 +19,7 @@ impl Persister { " INSERT INTO receive_swaps ( id, + id_hash, preimage, create_response_json, claim_private_key, @@ -30,10 +32,12 @@ impl Persister { claim_tx_id, state ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", )?; + let id_hash = sha256::Hash::hash(receive_swap.id.as_bytes()).to_hex(); _ = stmt.execute(( &receive_swap.id, + id_hash, &receive_swap.preimage, &receive_swap.create_response_json, &receive_swap.claim_private_key, @@ -81,7 +85,7 @@ impl Persister { pub(crate) fn fetch_receive_swap_by_id(&self, id: &str) -> Result> { let con: Connection = self.get_connection()?; - let query = Self::list_receive_swaps_query(vec!["id = ?1".to_string()]); + let query = Self::list_receive_swaps_query(vec!["id = ?1 or id_hash = ?1".to_string()]); let res = con.query_row(&query, [id], Self::sql_row_to_receive_swap); Ok(res.ok()) diff --git a/lib/core/src/persist/send.rs b/lib/core/src/persist/send.rs index 2dc7d55..14bb99d 100644 --- a/lib/core/src/persist/send.rs +++ b/lib/core/src/persist/send.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use anyhow::Result; use boltz_client::swaps::boltz::CreateSubmarineResponse; use rusqlite::{named_params, params, Connection, Row}; +use sdk_common::bitcoin::hashes::{hex::ToHex, sha256, Hash}; use serde::{Deserialize, Serialize}; use crate::ensure_sdk; @@ -18,6 +19,7 @@ impl Persister { " INSERT INTO send_swaps ( id, + id_hash, invoice, description, payer_amount_sat, @@ -29,10 +31,12 @@ impl Persister { created_at, state ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", )?; + let id_hash = sha256::Hash::hash(send_swap.id.as_bytes()).to_hex(); _ = stmt.execute(( &send_swap.id, + &id_hash, &send_swap.invoice, &send_swap.description, &send_swap.payer_amount_sat, @@ -102,7 +106,7 @@ impl Persister { pub(crate) fn fetch_send_swap_by_id(&self, id: &str) -> Result> { let con: Connection = self.get_connection()?; - let query = Self::list_send_swaps_query(vec!["id = ?1".to_string()]); + let query = Self::list_send_swaps_query(vec!["id = ?1 or id_hash = ?1".to_string()]); let res = con.query_row(&query, [id], Self::sql_row_to_send_swap); Ok(res.ok()) diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 0633ca8..f2a9cf2 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1000,6 +1000,16 @@ impl LiquidSdk { compressed: true, inner: keypair.public_key(), }; + let webhook = self.persister.get_webhook_url()?.map(|url| Webhook { + url, + hash_swap_id: Some(true), + status: Some(vec![ + SubSwapStates::InvoiceFailedToPay, + SubSwapStates::SwapExpired, + SubSwapStates::TransactionClaimPending, + SubSwapStates::TransactionLockupFailed, + ]), + }); let create_response = self.swapper.create_send_swap(CreateSubmarineRequest { from: "L-BTC".to_string(), to: "BTC".to_string(), @@ -1007,6 +1017,7 @@ impl LiquidSdk { refund_public_key, pair_hash: Some(lbtc_pair.hash), referral_id: None, + webhook, })?; let swap_id = &create_response.id; @@ -1202,6 +1213,15 @@ impl LiquidSdk { compressed: true, inner: refund_keypair.public_key(), }; + let webhook = self.persister.get_webhook_url()?.map(|url| Webhook { + url, + hash_swap_id: Some(true), + status: Some(vec![ + ChainSwapStates::TransactionFailed, + ChainSwapStates::TransactionLockupFailed, + ChainSwapStates::TransactionServerConfirmed, + ]), + }); let create_response = self.swapper.create_chain_swap(CreateChainRequest { from: "L-BTC".to_string(), to: "BTC".to_string(), @@ -1212,6 +1232,7 @@ impl LiquidSdk { server_lock_amount: Some(server_lockup_amount_sat as u32), // TODO update our model pair_hash: Some(pair.hash), referral_id: None, + webhook, })?; let swap_id = &create_response.id; @@ -1467,6 +1488,18 @@ impl LiquidSdk { let mrh_addr_hash = sha256::Hash::hash(mrh_addr_str.as_bytes()); let mrh_addr_hash_sig = keypair.sign_schnorr(mrh_addr_hash.into()); + let receiver_amount_sat = payer_amount_sat - fees_sat; + let webhook_claim_status = + match receiver_amount_sat > self.config.zero_conf_max_amount_sat() { + true => RevSwapStates::TransactionConfirmed, + false => RevSwapStates::TransactionMempool, + }; + let webhook = self.persister.get_webhook_url()?.map(|url| Webhook { + url, + hash_swap_id: Some(true), + status: Some(vec![webhook_claim_status]), + }); + let v2_req = CreateReverseRequest { invoice_amount: payer_amount_sat as u32, // TODO update our model from: "BTC".to_string(), @@ -1477,6 +1510,7 @@ impl LiquidSdk { address: Some(mrh_addr_str.clone()), address_signature: Some(mrh_addr_hash_sig.to_hex()), referral_id: None, + webhook, }; let create_response = self.swapper.create_receive_swap(v2_req)?; @@ -1530,7 +1564,7 @@ impl LiquidSdk { invoice: invoice.to_string(), description, payer_amount_sat, - receiver_amount_sat: payer_amount_sat - fees_sat, + receiver_amount_sat, claim_fees_sat: reverse_pair.fees.claim_estimate(), claim_tx_id: None, created_at: utils::now(), @@ -1571,6 +1605,15 @@ impl LiquidSdk { compressed: true, inner: refund_keypair.public_key(), }; + let webhook = self.persister.get_webhook_url()?.map(|url| Webhook { + url, + hash_swap_id: Some(true), + status: Some(vec![ + ChainSwapStates::TransactionFailed, + ChainSwapStates::TransactionLockupFailed, + ChainSwapStates::TransactionServerConfirmed, + ]), + }); let create_response = self.swapper.create_chain_swap(CreateChainRequest { from: "BTC".to_string(), to: "L-BTC".to_string(), @@ -1581,6 +1624,7 @@ impl LiquidSdk { server_lock_amount: None, pair_hash: Some(pair.hash), referral_id: None, + webhook, })?; let swap_id = create_response.id.clone(); @@ -2088,6 +2132,31 @@ impl LiquidSdk { Ok(perform_lnurl_auth(linking_keys, req_data).await?) } + /// Register for webhook callbacks at the given `webhook_url`. Each created swap after registering the + /// webhook will include the `webhook_url`. + /// + /// This method should be called every time the application is started and when the `webhook_url` changes. + /// For example, if the `webhook_url` contains a push notification token and the token changes after + /// the application was started, then this method should be called to register for callbacks at + /// the new correct `webhook_url`. To unregister a webhook call [LiquidSdk::unregister_webhook]. + pub async fn register_webhook(&self, webhook_url: String) -> SdkResult<()> { + info!("Registering for webhook notifications"); + self.persister.set_webhook_url(webhook_url)?; + Ok(()) + } + + /// Unregister webhook callbacks. Each swap already created will continue to use the registered + /// `webhook_url` until complete. + /// + /// This can be called when callbacks are no longer needed or the `webhook_url` + /// has changed such that it needs unregistering. For example, the token is valid but the locale changes. + /// To register a webhook call [LiquidSdk::register_webhook]. + pub async fn unregister_webhook(&self) -> SdkResult<()> { + info!("Unregistering for webhook notifications"); + self.persister.remove_webhook_url()?; + Ok(()) + } + /// Fetch live rates of fiat currencies, sorted by name. pub async fn fetch_fiat_rates(&self) -> Result, SdkError> { self.fiat_api.fetch_fiat_rates().await.map_err(Into::into) @@ -2188,22 +2257,24 @@ impl ReconnectHandler for SwapperReconnectHandler { #[cfg(test)] mod tests { - use std::sync::Arc; + use std::{str::FromStr, sync::Arc}; use anyhow::{anyhow, Result}; use boltz_client::{ boltz::{self, SwapUpdateTxDetails}, swaps::boltz::{ChainSwapStates, RevSwapStates, SubSwapStates}, }; - use lwk_wollet::hashes::hex::DisplayHex; + use lwk_wollet::{elements::Txid, hashes::hex::DisplayHex}; + use tokio::sync::Mutex; use crate::{ model::{Direction, PaymentState, Swap}, sdk::LiquidSdk, test_utils::{ + chain::{MockBitcoinChainService, MockHistory, MockLiquidChainService}, chain_swap::{new_chain_swap, TEST_BITCOIN_TX}, persist::{new_persister, new_receive_swap, new_send_swap}, - sdk::new_liquid_sdk, + sdk::{new_liquid_sdk, new_liquid_sdk_with_chain_services}, status_stream::MockStatusStream, swapper::MockSwapper, wallet::TEST_LIQUID_TX, @@ -2449,11 +2520,15 @@ mod tests { let persister = Arc::new(persister); let swapper = Arc::new(MockSwapper::default()); let status_stream = Arc::new(MockStatusStream::new()); + let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); + let bitcoin_chain_service = Arc::new(Mutex::new(MockBitcoinChainService::new())); - let sdk = Arc::new(new_liquid_sdk( + let sdk = Arc::new(new_liquid_sdk_with_chain_services( persister.clone(), swapper.clone(), status_stream.clone(), + liquid_chain_service.clone(), + bitcoin_chain_service.clone(), )?); LiquidSdk::track_swap_updates(&sdk).await; @@ -2482,10 +2557,53 @@ mod tests { assert_eq!(persisted_swap.state, PaymentState::Failed); } + let (mock_tx_hex, mock_tx_id) = match direction { + Direction::Incoming => { + let tx = TEST_LIQUID_TX.clone(); + ( + lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(), + tx.txid().to_string(), + ) + } + Direction::Outgoing => { + let tx = TEST_BITCOIN_TX.clone(); + ( + sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(), + tx.txid().to_string(), + ) + } + }; + // Verify that `TransactionLockupFailed` correctly sets the state as // `RefundPending`/`Refundable` or as `Failed` depending on whether or not // `user_lockup_tx_id` is present - for user_lockup_tx_id in &[None, Some("user-lockup-tx-id".to_string())] { + for user_lockup_tx_id in &[None, Some(mock_tx_id.clone())] { + if let Some(user_lockup_tx_id) = user_lockup_tx_id { + match direction { + Direction::Incoming => { + bitcoin_chain_service + .lock() + .await + .set_history(vec![MockHistory { + txid: Txid::from_str(&user_lockup_tx_id).unwrap(), + height: 0, + block_hash: None, + block_timestamp: None, + }]); + } + Direction::Outgoing => { + liquid_chain_service + .lock() + .await + .set_history(vec![MockHistory { + txid: Txid::from_str(&user_lockup_tx_id).unwrap(), + height: 0, + block_hash: None, + block_timestamp: None, + }]); + } + } + } let persisted_swap = trigger_swap_update!( "chain", NewSwapArgs::default() @@ -2509,23 +2627,6 @@ mod tests { assert_eq!(persisted_swap.state, expected_state); } - let (mock_tx_hex, mock_tx_id) = match direction { - Direction::Incoming => { - let tx = TEST_LIQUID_TX.clone(); - ( - lwk_wollet::elements::encode::serialize(&tx).to_lower_hex_string(), - tx.txid().to_string(), - ) - } - Direction::Outgoing => { - let tx = TEST_BITCOIN_TX.clone(); - ( - sdk_common::bitcoin::consensus::serialize(&tx).to_lower_hex_string(), - tx.txid().to_string(), - ) - } - }; - // Verify that `TransactionMempool` and `TransactionConfirmed` correctly set // `user_lockup_tx_id` and `accept_zero_conf` for status in [ diff --git a/lib/core/src/test_utils/chain.rs b/lib/core/src/test_utils/chain.rs index c1ca456..6bcac2c 100644 --- a/lib/core/src/test_utils/chain.rs +++ b/lib/core/src/test_utils/chain.rs @@ -2,6 +2,7 @@ use anyhow::Result; use async_trait::async_trait; +use lwk_wollet::elements::{BlockHash, Txid}; use lwk_wollet::{bitcoin::consensus::deserialize, elements::hex::FromHex}; use crate::{ @@ -10,13 +11,39 @@ use crate::{ utils, }; +#[derive(Clone)] +pub(crate) struct MockHistory { + pub txid: Txid, + pub height: i32, + pub block_hash: Option, + pub block_timestamp: Option, +} + +impl From for lwk_wollet::History { + fn from(h: MockHistory) -> Self { + lwk_wollet::History { + txid: h.txid, + height: h.height, + block_hash: h.block_hash, + block_timestamp: h.block_timestamp, + } + } +} + #[derive(Default)] -pub(crate) struct MockLiquidChainService {} +pub(crate) struct MockLiquidChainService { + history: Vec, +} impl MockLiquidChainService { pub(crate) fn new() -> Self { MockLiquidChainService::default() } + + pub(crate) fn set_history(&mut self, history: Vec) -> &mut Self { + self.history = history; + self + } } #[async_trait] @@ -44,7 +71,15 @@ impl LiquidChainService for MockLiquidChainService { &self, _scripts: &lwk_wollet::elements::Script, ) -> Result> { - unimplemented!() + Ok(self.history.clone().into_iter().map(Into::into).collect()) + } + + async fn get_script_history_with_retry( + &self, + _script: &lwk_wollet::elements::Script, + _retries: u64, + ) -> Result> { + Ok(self.history.clone().into_iter().map(Into::into).collect()) } async fn verify_tx( @@ -58,11 +93,18 @@ impl LiquidChainService for MockLiquidChainService { } } -pub(crate) struct MockBitcoinChainService {} +pub(crate) struct MockBitcoinChainService { + history: Vec, +} impl MockBitcoinChainService { pub(crate) fn new() -> Self { - MockBitcoinChainService {} + MockBitcoinChainService { history: vec![] } + } + + pub(crate) fn set_history(&mut self, history: Vec) -> &mut Self { + self.history = history; + self } } @@ -90,7 +132,15 @@ impl BitcoinChainService for MockBitcoinChainService { &self, _script: &boltz_client::bitcoin::Script, ) -> Result> { - unimplemented!() + Ok(self.history.clone().into_iter().map(Into::into).collect()) + } + + async fn get_script_history_with_retry( + &self, + _script: &boltz_client::bitcoin::Script, + _retries: u64, + ) -> Result> { + Ok(self.history.clone().into_iter().map(Into::into).collect()) } fn script_get_balance( diff --git a/lib/core/src/test_utils/sdk.rs b/lib/core/src/test_utils/sdk.rs index 9cb5e2d..ad5d06b 100644 --- a/lib/core/src/test_utils/sdk.rs +++ b/lib/core/src/test_utils/sdk.rs @@ -23,6 +23,25 @@ pub(crate) fn new_liquid_sdk( persister: Arc, swapper: Arc, status_stream: Arc, +) -> Result { + let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); + let bitcoin_chain_service = Arc::new(Mutex::new(MockBitcoinChainService::new())); + + new_liquid_sdk_with_chain_services( + persister, + swapper, + status_stream, + liquid_chain_service, + bitcoin_chain_service, + ) +} + +pub(crate) fn new_liquid_sdk_with_chain_services( + persister: Arc, + swapper: Arc, + status_stream: Arc, + liquid_chain_service: Arc>, + bitcoin_chain_service: Arc>, ) -> Result { let mut config = Config::testnet(); config.working_dir = persister @@ -33,9 +52,6 @@ pub(crate) fn new_liquid_sdk( let onchain_wallet = Arc::new(MockWallet::new()); - let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); - let bitcoin_chain_service = Arc::new(Mutex::new(MockBitcoinChainService::new())); - let send_swap_state_handler = SendSwapStateHandler::new( config.clone(), onchain_wallet.clone(), diff --git a/packages/dart/lib/src/bindings.dart b/packages/dart/lib/src/bindings.dart index b5d6464..686e3b1 100644 --- a/packages/dart/lib/src/bindings.dart +++ b/packages/dart/lib/src/bindings.dart @@ -79,6 +79,8 @@ abstract class BindingLiquidSdk implements RustOpaqueInterface { Future refund({required RefundRequest req}); + Future registerWebhook({required String webhookUrl}); + Future rescanOnchainSwaps(); void restore({required RestoreRequest req}); @@ -86,6 +88,8 @@ abstract class BindingLiquidSdk implements RustOpaqueInterface { Future sendPayment({required SendPaymentRequest req}); Future sync(); + + Future unregisterWebhook(); } class AesSuccessActionDataDecrypted { diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 42e9700..22b1700 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -55,7 +55,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.2.0'; @override - int get rustContentHash => -1074530283; + int get rustContentHash => 337003852; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( stem: 'breez_sdk_liquid', @@ -129,6 +129,9 @@ abstract class RustLibApi extends BaseApi { Future crateBindingsBindingLiquidSdkRefund( {required BindingLiquidSdk that, required RefundRequest req}); + Future crateBindingsBindingLiquidSdkRegisterWebhook( + {required BindingLiquidSdk that, required String webhookUrl}); + Future crateBindingsBindingLiquidSdkRescanOnchainSwaps({required BindingLiquidSdk that}); void crateBindingsBindingLiquidSdkRestore({required BindingLiquidSdk that, required RestoreRequest req}); @@ -138,6 +141,8 @@ abstract class RustLibApi extends BaseApi { Future crateBindingsBindingLiquidSdkSync({required BindingLiquidSdk that}); + Future crateBindingsBindingLiquidSdkUnregisterWebhook({required BindingLiquidSdk that}); + Future crateBindingsBindingEventListenerOnEvent( {required BindingEventListener that, required SdkEvent e}); @@ -776,6 +781,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that", "req"], ); + @override + Future crateBindingsBindingLiquidSdkRegisterWebhook( + {required BindingLiquidSdk that, required String webhookUrl}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + var arg0 = + cst_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( + that); + var arg1 = cst_encode_String(webhookUrl); + return wire.wire__crate__bindings__BindingLiquidSdk_register_webhook(port_, arg0, arg1); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_sdk_error, + ), + constMeta: kCrateBindingsBindingLiquidSdkRegisterWebhookConstMeta, + argValues: [that, webhookUrl], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateBindingsBindingLiquidSdkRegisterWebhookConstMeta => const TaskConstMeta( + debugName: "BindingLiquidSdk_register_webhook", + argNames: ["that", "webhookUrl"], + ); + @override Future crateBindingsBindingLiquidSdkRescanOnchainSwaps({required BindingLiquidSdk that}) { return handler.executeNormal(NormalTask( @@ -875,6 +906,30 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["that"], ); + @override + Future crateBindingsBindingLiquidSdkUnregisterWebhook({required BindingLiquidSdk that}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + var arg0 = + cst_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk( + that); + return wire.wire__crate__bindings__BindingLiquidSdk_unregister_webhook(port_, arg0); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_sdk_error, + ), + constMeta: kCrateBindingsBindingLiquidSdkUnregisterWebhookConstMeta, + argValues: [that], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateBindingsBindingLiquidSdkUnregisterWebhookConstMeta => const TaskConstMeta( + debugName: "BindingLiquidSdk_unregister_webhook", + argNames: ["that"], + ); + @override Future crateBindingsBindingEventListenerOnEvent( {required BindingEventListener that, required SdkEvent e}) { @@ -5811,6 +5866,9 @@ class BindingLiquidSdkImpl extends RustOpaque implements BindingLiquidSdk { Future refund({required RefundRequest req}) => RustLib.instance.api.crateBindingsBindingLiquidSdkRefund(that: this, req: req); + Future registerWebhook({required String webhookUrl}) => + RustLib.instance.api.crateBindingsBindingLiquidSdkRegisterWebhook(that: this, webhookUrl: webhookUrl); + Future rescanOnchainSwaps() => RustLib.instance.api.crateBindingsBindingLiquidSdkRescanOnchainSwaps( that: this, ); @@ -5824,4 +5882,8 @@ class BindingLiquidSdkImpl extends RustOpaque implements BindingLiquidSdk { Future sync() => RustLib.instance.api.crateBindingsBindingLiquidSdkSync( that: this, ); + + Future unregisterWebhook() => RustLib.instance.api.crateBindingsBindingLiquidSdkUnregisterWebhook( + that: this, + ); } diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index cdcff6d..dbf9f77 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -3642,6 +3642,26 @@ class RustLibWire implements BaseWire { _wire__crate__bindings__BindingLiquidSdk_refundPtr .asFunction)>(); + void wire__crate__bindings__BindingLiquidSdk_register_webhook( + int port_, + int that, + ffi.Pointer webhook_url, + ) { + return _wire__crate__bindings__BindingLiquidSdk_register_webhook( + port_, + that, + webhook_url, + ); + } + + late final _wire__crate__bindings__BindingLiquidSdk_register_webhookPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.UintPtr, ffi.Pointer)>>( + 'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook'); + late final _wire__crate__bindings__BindingLiquidSdk_register_webhook = + _wire__crate__bindings__BindingLiquidSdk_register_webhookPtr + .asFunction)>(); + void wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps( int port_, int that, @@ -3712,6 +3732,22 @@ class RustLibWire implements BaseWire { late final _wire__crate__bindings__BindingLiquidSdk_sync = _wire__crate__bindings__BindingLiquidSdk_syncPtr.asFunction(); + void wire__crate__bindings__BindingLiquidSdk_unregister_webhook( + int port_, + int that, + ) { + return _wire__crate__bindings__BindingLiquidSdk_unregister_webhook( + port_, + that, + ); + } + + late final _wire__crate__bindings__BindingLiquidSdk_unregister_webhookPtr = + _lookup>( + 'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook'); + late final _wire__crate__bindings__BindingLiquidSdk_unregister_webhook = + _wire__crate__bindings__BindingLiquidSdk_unregister_webhookPtr.asFunction(); + void wire__crate__bindings__binding_event_listener_on_event( int port_, ffi.Pointer that, diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 757c662..293dfdc 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -487,6 +487,26 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_refundPtr .asFunction)>(); + void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook( + int port_, + int that, + ffi.Pointer webhook_url, + ) { + return _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook( + port_, + that, + webhook_url, + ); + } + + late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhookPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Int64, ffi.UintPtr, ffi.Pointer)>>( + 'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook'); + late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhook = + _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_register_webhookPtr + .asFunction)>(); + void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_rescan_onchain_swaps( int port_, int that, @@ -559,6 +579,23 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_syncPtr .asFunction(); + void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook( + int port_, + int that, + ) { + return _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook( + port_, + that, + ); + } + + late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhookPtr = + _lookup>( + 'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook'); + late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhook = + _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_unregister_webhookPtr + .asFunction(); + void frbgen_breez_liquid_wire__crate__bindings__binding_event_listener_on_event( int port_, ffi.Pointer that, diff --git a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidModule.kt b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidModule.kt index ee1a65b..02e89a2 100644 --- a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidModule.kt +++ b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidModule.kt @@ -550,6 +550,33 @@ class BreezSDKLiquidModule( } } + @ReactMethod + fun registerWebhook( + webhookUrl: String, + promise: Promise, + ) { + executor.execute { + try { + getBindingLiquidSdk().registerWebhook(webhookUrl) + promise.resolve(readableMapOf("status" to "ok")) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + + @ReactMethod + fun unregisterWebhook(promise: Promise) { + executor.execute { + try { + getBindingLiquidSdk().unregisterWebhook() + promise.resolve(readableMapOf("status" to "ok")) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + @ReactMethod fun fetchFiatRates(promise: Promise) { executor.execute { diff --git a/packages/react-native/ios/RNBreezSDKLiquid.m b/packages/react-native/ios/RNBreezSDKLiquid.m index 470a64f..399df60 100644 --- a/packages/react-native/ios/RNBreezSDKLiquid.m +++ b/packages/react-native/ios/RNBreezSDKLiquid.m @@ -179,6 +179,17 @@ RCT_EXTERN_METHOD( reject: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + registerWebhook: (NSString*)webhookUrl + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD( + unregisterWebhook: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( fetchFiatRates: (RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject diff --git a/packages/react-native/ios/RNBreezSDKLiquid.swift b/packages/react-native/ios/RNBreezSDKLiquid.swift index 81161c9..7880f57 100644 --- a/packages/react-native/ios/RNBreezSDKLiquid.swift +++ b/packages/react-native/ios/RNBreezSDKLiquid.swift @@ -411,6 +411,26 @@ class RNBreezSDKLiquid: RCTEventEmitter { } } + @objc(registerWebhook:resolve:reject:) + func registerWebhook(_ webhookUrl: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + try getBindingLiquidSdk().registerWebhook(webhookUrl: webhookUrl) + resolve(["status": "ok"]) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + + @objc(unregisterWebhook:reject:) + func unregisterWebhook(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + try getBindingLiquidSdk().unregisterWebhook() + resolve(["status": "ok"]) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + @objc(fetchFiatRates:reject:) func fetchFiatRates(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { do { diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 897a363..52bb7cf 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -740,6 +740,14 @@ export const lnurlAuth = async (reqData: LnUrlAuthRequestData): Promise => { + await BreezSDKLiquid.registerWebhook(webhookUrl) +} + +export const unregisterWebhook = async (): Promise => { + await BreezSDKLiquid.unregisterWebhook() +} + export const fetchFiatRates = async (): Promise => { const response = await BreezSDKLiquid.fetchFiatRates() return response