From 08f0fd26ccce8e0b3b419aa23c46a6ad2049b910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Wed, 18 Dec 2024 00:34:48 +0000 Subject: [PATCH 01/20] [skip ci] Define interface --- lib/bindings/src/breez_sdk_liquid.udl | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 89626f05a..2b9129dd3 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -338,6 +338,7 @@ dictionary Config { u64? zero_conf_max_amount_sat; boolean use_default_external_input_parsers = true; sequence? external_input_parsers = null; + u32? onchain_fee_rate_leeway_sat_per_vbyte; }; enum LiquidNetwork { @@ -541,6 +542,20 @@ interface GetPaymentRequest { Lightning(string payment_hash); }; +dictionary FetchPaymentProposedFeesRequest { + string swap_id; +}; + +dictionary FetchPaymentProposedFeesResponse { + string swap_id; + u64 fees_sat; + u64 payer_amount_sat; +}; + +dictionary AcceptPaymentProposedFeesRequest { + FetchPaymentProposedFeesResponse response; +}; + dictionary LnUrlInfo { string? ln_address; string? lnurl_pay_comment; @@ -584,6 +599,7 @@ enum PaymentState { "TimedOut", "Refundable", "RefundPending", + "WaitingUserAction", }; dictionary RefundableSwap { @@ -630,6 +646,7 @@ interface SdkEvent { PaymentRefundPending(Payment details); PaymentSucceeded(Payment details); PaymentWaitingConfirmation(Payment details); + PaymentWaitingFeeAcceptance(Payment details); Synced(); }; @@ -755,6 +772,12 @@ interface BindingLiquidSdk { [Throws=PaymentError] Payment? get_payment(GetPaymentRequest req); + [Throws=SdkError] + FetchPaymentProposedFeesResponse fetch_payment_proposed_fees(FetchPaymentProposedFeesRequest req); + + [Throws=SdkError] + void accept_payment_proposed_fees(AcceptPaymentProposedFeesRequest req); + [Throws=SdkError] sequence list_refundables(); From 351473a41e8ea8d7e17c25f1ebff8af4ad714efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Fri, 20 Dec 2024 13:36:30 +0000 Subject: [PATCH 02/20] Expose fees for review + auto accept --- cli/src/commands.rs | 27 +- .../include/breez_sdk_liquid.h | 8 + lib/bindings/src/breez_sdk_liquid.udl | 6 +- lib/bindings/src/lib.rs | 14 + lib/core/src/chain_swap.rs | 173 +++++++++--- lib/core/src/frb_generated.rs | 46 +++- lib/core/src/model.rs | 45 ++++ lib/core/src/persist/chain.rs | 3 +- lib/core/src/receive_swap.rs | 4 + lib/core/src/sdk.rs | 241 ++++++++++++++++- lib/core/src/send_swap.rs | 4 + lib/core/src/swapper/boltz/mod.rs | 2 +- lib/core/src/swapper/mod.rs | 2 +- lib/core/src/test_utils/chain.rs | 16 +- lib/core/src/test_utils/chain_swap.rs | 87 ++++++ lib/core/src/test_utils/receive_swap.rs | 2 +- lib/core/src/test_utils/sdk.rs | 3 + lib/core/src/test_utils/send_swap.rs | 2 +- lib/core/src/test_utils/swapper.rs | 45 +++- packages/dart/lib/src/frb_generated.dart | 218 +++------------ packages/dart/lib/src/frb_generated.io.dart | 20 +- packages/dart/lib/src/model.dart | 31 ++- packages/dart/lib/src/model.freezed.dart | 82 ++++++ ...utter_breez_liquid_bindings_generated.dart | 10 + .../breezsdkliquid/BreezSDKLiquidMapper.kt | 116 ++++++++ .../breezsdkliquid/BreezSDKLiquidModule.kt | 36 +++ .../ios/BreezSDKLiquidMapper.swift | 252 ++++++++++-------- packages/react-native/ios/RNBreezSDKLiquid.m | 12 + .../react-native/ios/RNBreezSDKLiquid.swift | 22 ++ packages/react-native/src/index.ts | 31 ++- 30 files changed, 1194 insertions(+), 366 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 48ba70156..4bb7bf73c 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::thread; use std::time::Duration; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, bail, Result}; use breez_sdk_liquid::prelude::*; use clap::{arg, Parser}; use qrcode_rs::render::unicode; @@ -131,6 +131,14 @@ pub(crate) enum Command { /// Lightning payment hash payment_hash: String, }, + /// Get proposed fees for WaitingFeeAcceptance Payment + FetchPaymentProposedFees { swap_id: String }, + /// Accept proposed fees for WaitingFeeAcceptance Payment + AcceptPaymentProposedFees { + swap_id: String, + // Fee amount obtained using FetchPaymentProposedFees + fees_sat: u64, + }, /// List refundable chain swaps ListRefundables, /// Prepare a refund transaction for an incomplete swap @@ -519,6 +527,23 @@ pub(crate) async fn handle_command( } } } + Command::FetchPaymentProposedFees { swap_id } => { + let res = sdk + .fetch_payment_proposed_fees(&FetchPaymentProposedFeesRequest { swap_id }) + .await?; + command_result!(res) + } + Command::AcceptPaymentProposedFees { swap_id, fees_sat } => { + let res = sdk + .fetch_payment_proposed_fees(&FetchPaymentProposedFeesRequest { swap_id }) + .await?; + if fees_sat != res.fees_sat { + bail!("Fees changed since they were fetched") + } + sdk.accept_payment_proposed_fees(&AcceptPaymentProposedFeesRequest { response: res }) + .await?; + command_result!("Proposed fees accepted successfully") + } Command::ListRefundables => { let refundables = sdk.list_refundables().await?; command_result!(refundables) 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 b882180ef..5a6eafade 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 @@ -16,6 +16,8 @@ typedef struct _Dart_Handle* Dart_Handle; #define ESTIMATED_BTC_CLAIM_TX_VSIZE 111 +#define ESTIMATED_BTC_LOCKUP_TX_VSIZE 154 + #define STANDARD_FEE_RATE_SAT_PER_VBYTE 0.1 #define LOWBALL_FEE_RATE_SAT_PER_VBYTE 0.01 @@ -541,6 +543,10 @@ typedef struct wire_cst_SdkEvent_PaymentWaitingConfirmation { struct wire_cst_payment *details; } wire_cst_SdkEvent_PaymentWaitingConfirmation; +typedef struct wire_cst_SdkEvent_PaymentWaitingFeeAcceptance { + struct wire_cst_payment *details; +} wire_cst_SdkEvent_PaymentWaitingFeeAcceptance; + typedef union SdkEventKind { struct wire_cst_SdkEvent_PaymentFailed PaymentFailed; struct wire_cst_SdkEvent_PaymentPending PaymentPending; @@ -548,6 +554,7 @@ typedef union SdkEventKind { struct wire_cst_SdkEvent_PaymentRefundPending PaymentRefundPending; struct wire_cst_SdkEvent_PaymentSucceeded PaymentSucceeded; struct wire_cst_SdkEvent_PaymentWaitingConfirmation PaymentWaitingConfirmation; + struct wire_cst_SdkEvent_PaymentWaitingFeeAcceptance PaymentWaitingFeeAcceptance; } SdkEventKind; typedef struct wire_cst_sdk_event { @@ -580,6 +587,7 @@ typedef struct wire_cst_config { struct wire_cst_list_prim_u_8_strict *breez_api_key; struct wire_cst_list_external_input_parser *external_input_parsers; bool use_default_external_input_parsers; + uint32_t *onchain_fee_rate_leeway_sat_per_vbyte; } wire_cst_config; typedef struct wire_cst_connect_request { diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 2b9129dd3..1abb73773 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -338,7 +338,7 @@ dictionary Config { u64? zero_conf_max_amount_sat; boolean use_default_external_input_parsers = true; sequence? external_input_parsers = null; - u32? onchain_fee_rate_leeway_sat_per_vbyte; + u32? onchain_fee_rate_leeway_sat_per_vbyte = null; }; enum LiquidNetwork { @@ -599,7 +599,7 @@ enum PaymentState { "TimedOut", "Refundable", "RefundPending", - "WaitingUserAction", + "WaitingFeeAcceptance", }; dictionary RefundableSwap { @@ -775,7 +775,7 @@ interface BindingLiquidSdk { [Throws=SdkError] FetchPaymentProposedFeesResponse fetch_payment_proposed_fees(FetchPaymentProposedFeesRequest req); - [Throws=SdkError] + [Throws=PaymentError] void accept_payment_proposed_fees(AcceptPaymentProposedFeesRequest req); [Throws=SdkError] diff --git a/lib/bindings/src/lib.rs b/lib/bindings/src/lib.rs index 995fc3b1f..6845f8eea 100644 --- a/lib/bindings/src/lib.rs +++ b/lib/bindings/src/lib.rs @@ -170,6 +170,20 @@ impl BindingLiquidSdk { rt().block_on(self.sdk.get_payment(&req)) } + pub fn fetch_payment_proposed_fees( + &self, + req: FetchPaymentProposedFeesRequest, + ) -> SdkResult { + rt().block_on(self.sdk.fetch_payment_proposed_fees(&req)) + } + + pub fn accept_payment_proposed_fees( + &self, + req: AcceptPaymentProposedFeesRequest, + ) -> Result<(), PaymentError> { + rt().block_on(self.sdk.accept_payment_proposed_fees(&req)) + } + pub fn prepare_lnurl_pay( &self, req: PrepareLnUrlPayRequest, diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index 94ef25a39..d2edd9fd8 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -32,8 +32,9 @@ use crate::{ wallet::OnchainWallet, }; -// Estimates based on https://github.com/BoltzExchange/boltz-backend/blob/ee4c77be1fcb9bb2b45703c542ad67f7efbf218d/lib/rates/FeeProvider.ts#L78 +// Estimates based on https://github.com/BoltzExchange/boltz-backend/blob/ee4c77be1fcb9bb2b45703c542ad67f7efbf218d/lib/rates/FeeProvider.ts#L68 pub const ESTIMATED_BTC_CLAIM_TX_VSIZE: u64 = 111; +pub const ESTIMATED_BTC_LOCKUP_TX_VSIZE: u64 = 154; pub(crate) struct ChainSwapHandler { config: Config, @@ -327,12 +328,12 @@ impl ChainSwapHandler { | ChainSwapStates::TransactionRefunded | ChainSwapStates::SwapExpired => { // Zero-amount Receive Chain Swaps also get to TransactionLockupFailed when user locks up funds - let is_zero_amount = swap.payer_amount_sat == 0; + let is_zero_amount = swap.get_boltz_create_response()?.lockup_details.amount == 0; if matches!(swap_state, ChainSwapStates::TransactionLockupFailed) && is_zero_amount { match self.handle_amountless_update(swap).await { Ok(_) => { - // We successfully accepted the quote, the swap should continue as normal + // Either we accepted the quote, or we will be waiting for user fee acceptance return Ok(()); // Break from TxLockupFailed branch } // In case of error, we continue and mark it as refundable @@ -383,17 +384,43 @@ impl ChainSwapHandler { .map(|quote| quote.to_sat())?; info!("Got quote of {quote} sat for swap {}", &swap.id); - self.validate_and_update_amountless_swap(swap, quote) - .await?; - self.swapper - .accept_zero_amount_chain_swap_quote(&swap.id, quote) + match self.validate_amountless_swap(swap, quote).await? { + ValidateAmountlessSwapResult::ReadyForAccepting { + user_lockup_amount_sat, + receiver_amount_sat, + } => { + debug!("Zero-amount swap validated. Auto-accepting..."); + self.persister.update_zero_amount_swap_values( + &swap.id, + user_lockup_amount_sat, + receiver_amount_sat, + )?; + self.swapper + .accept_zero_amount_chain_swap_quote(&swap.id, quote) + .map_err(Into::into) + } + ValidateAmountlessSwapResult::RequiresUserAction { + user_lockup_amount_sat, + receiver_amount_sat_original_estimate, + } => { + debug!("Zero-amount swap validated. Fees are too high for automatic accepting. Moving to WaitingFeeAcceptance"); + // While the user doesn't accept new fees, let's continue to show the original estimate + self.persister.update_zero_amount_swap_values( + &swap.id, + user_lockup_amount_sat, + receiver_amount_sat_original_estimate, + )?; + self.update_swap_info(&swap.id, WaitingFeeAcceptance, None, None, None, None) + .await + } + } } - async fn validate_and_update_amountless_swap( + async fn validate_amountless_swap( &self, swap: &ChainSwap, quote_server_lockup_amount_sat: u64, - ) -> Result<(), PaymentError> { + ) -> Result { debug!("Validating {swap:?}"); ensure_sdk!( @@ -425,32 +452,45 @@ impl ChainSwapHandler { ); let pair = swap.get_boltz_pair()?; - let swapper_service_feerate = pair.fees.percentage; - let swapper_server_fees_sat = pair.fees.server(); - let service_fees_sat = - ((swapper_service_feerate / 100.0) * user_lockup_amount_sat as f64).ceil() as u64; - let fees_sat = swapper_server_fees_sat + service_fees_sat; - ensure_sdk!( - user_lockup_amount_sat > fees_sat, - PaymentError::generic(&format!("Invalid quote: fees ({fees_sat} sat) are higher than user lockup ({user_lockup_amount_sat} sat)")) - ); - let expected_server_lockup_amount_sat = user_lockup_amount_sat - fees_sat; - debug!("user_lockup_amount_sat = {}, service_fees_sat = {}, server_fees_sat = {}, expected_server_lockup_amount_sat = {}, quote_server_lockup_amount_sat = {}", - user_lockup_amount_sat, service_fees_sat, swapper_server_fees_sat, expected_server_lockup_amount_sat, quote_server_lockup_amount_sat); - ensure_sdk!( - expected_server_lockup_amount_sat <= quote_server_lockup_amount_sat, - PaymentError::generic(&format!("Invalid quote: expected at least {expected_server_lockup_amount_sat} sat, got {quote_server_lockup_amount_sat} sat")) - ); + // Original server lockup quote estimate + let server_fees_estimate_sat = pair.fees.server(); + let service_fees_sat = pair.fees.boltz(user_lockup_amount_sat); + let server_lockup_amount_estimate_sat = + user_lockup_amount_sat - server_fees_estimate_sat - service_fees_sat; + + // Min auto accept server lockup quote + let server_fees_leeway_sat = self + .config + .onchain_fee_rate_leeway_sat_per_vbyte + .unwrap_or(0) as u64 + * ESTIMATED_BTC_LOCKUP_TX_VSIZE; + let min_auto_accept_server_lockup_amount_sat = + server_lockup_amount_estimate_sat.saturating_sub(server_fees_leeway_sat); - let receiver_amount_sat = quote_server_lockup_amount_sat - swap.claim_fees_sat; - self.persister.update_zero_amount_swap_values( - &swap.id, - user_lockup_amount_sat, - receiver_amount_sat, - )?; + debug!( + "user_lockup_amount_sat = {user_lockup_amount_sat}, \ + service_fees_sat = {service_fees_sat}, \ + server_fees_estimate_sat = {server_fees_estimate_sat}, \ + server_fees_leeway_sat = {server_fees_leeway_sat}, \ + min_auto_accept_server_lockup_amount_sat = {min_auto_accept_server_lockup_amount_sat}, \ + quote_server_lockup_amount_sat = {quote_server_lockup_amount_sat}", + ); - Ok(()) + if min_auto_accept_server_lockup_amount_sat > quote_server_lockup_amount_sat { + let receiver_amount_sat_original_estimate = + server_lockup_amount_estimate_sat - swap.claim_fees_sat; + Ok(ValidateAmountlessSwapResult::RequiresUserAction { + user_lockup_amount_sat, + receiver_amount_sat_original_estimate, + }) + } else { + let receiver_amount_sat = quote_server_lockup_amount_sat - swap.claim_fees_sat; + Ok(ValidateAmountlessSwapResult::ReadyForAccepting { + user_lockup_amount_sat, + receiver_amount_sat, + }) + } } async fn on_new_outgoing_status(&self, swap: &ChainSwap, update: &boltz::Update) -> Result<()> { @@ -1077,12 +1117,17 @@ impl ChainSwapHandler { err: "Cannot transition to Created state".to_string(), }), - (Created | Pending, Pending) => Ok(()), + (Created | Pending | WaitingFeeAcceptance, Pending) => Ok(()), (_, Pending) => Err(PaymentError::Generic { err: format!("Cannot transition from {from_state:?} to Pending state"), }), - (Created | Pending | RefundPending, Complete) => Ok(()), + (Created | Pending | WaitingFeeAcceptance, WaitingFeeAcceptance) => Ok(()), + (_, WaitingFeeAcceptance) => Err(PaymentError::Generic { + err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"), + }), + + (Created | Pending | WaitingFeeAcceptance | RefundPending, Complete) => Ok(()), (_, Complete) => Err(PaymentError::Generic { err: format!("Cannot transition from {from_state:?} to Complete state"), }), @@ -1092,12 +1137,15 @@ impl ChainSwapHandler { err: format!("Cannot transition from {from_state:?} to TimedOut state"), }), - (Created | Pending | RefundPending | Failed | Complete, Refundable) => Ok(()), + ( + Created | Pending | WaitingFeeAcceptance | RefundPending | Failed | Complete, + Refundable, + ) => Ok(()), (_, Refundable) => Err(PaymentError::Generic { err: format!("Cannot transition from {from_state:?} to Refundable state"), }), - (Pending | Refundable, RefundPending) => Ok(()), + (Pending | WaitingFeeAcceptance | Refundable, RefundPending) => Ok(()), (_, RefundPending) => Err(PaymentError::Generic { err: format!("Cannot transition from {from_state:?} to RefundPending state"), }), @@ -1299,6 +1347,17 @@ impl ChainSwapHandler { } } +enum ValidateAmountlessSwapResult { + ReadyForAccepting { + user_lockup_amount_sat: u64, + receiver_amount_sat: u64, + }, + RequiresUserAction { + user_lockup_amount_sat: u64, + receiver_amount_sat_original_estimate: u64, + }, +} + #[cfg(test)] mod tests { use anyhow::Result; @@ -1322,15 +1381,47 @@ mod tests { let chain_swap_handler = new_chain_swap_handler(persister.clone())?; // Test valid combinations of states - let all_states = HashSet::from([Created, Pending, Complete, TimedOut, Failed]); + let all_states = HashSet::from([ + Created, + Pending, + WaitingFeeAcceptance, + Complete, + TimedOut, + Failed, + ]); let valid_combinations = HashMap::from([ ( Created, - HashSet::from([Pending, Complete, TimedOut, Refundable, Failed]), + HashSet::from([ + Pending, + WaitingFeeAcceptance, + Complete, + TimedOut, + Refundable, + Failed, + ]), ), ( Pending, - HashSet::from([Pending, Complete, Refundable, RefundPending, Failed]), + HashSet::from([ + Pending, + WaitingFeeAcceptance, + Complete, + Refundable, + RefundPending, + Failed, + ]), + ), + ( + WaitingFeeAcceptance, + HashSet::from([ + Pending, + WaitingFeeAcceptance, + Complete, + Refundable, + RefundPending, + Failed, + ]), ), (TimedOut, HashSet::from([Failed])), (Complete, HashSet::from([Refundable])), @@ -1342,7 +1433,7 @@ mod tests { for (first_state, allowed_states) in valid_combinations.iter() { for allowed_state in allowed_states { let chain_swap = - new_chain_swap(Direction::Incoming, Some(*first_state), false, None); + new_chain_swap(Direction::Incoming, Some(*first_state), false, None, false); persister.insert_or_update_chain_swap(&chain_swap)?; assert!(chain_swap_handler @@ -1369,7 +1460,7 @@ mod tests { for (first_state, disallowed_states) in invalid_combinations.iter() { for disallowed_state in disallowed_states { let chain_swap = - new_chain_swap(Direction::Incoming, Some(*first_state), false, None); + new_chain_swap(Direction::Incoming, Some(*first_state), false, None, false); persister.insert_or_update_chain_swap(&chain_swap)?; assert!(chain_swap_handler diff --git a/lib/core/src/frb_generated.rs b/lib/core/src/frb_generated.rs index ec5cd90a1..0f6799c8a 100644 --- a/lib/core/src/frb_generated.rs +++ b/lib/core/src/frb_generated.rs @@ -2092,6 +2092,7 @@ impl CstDecode for i32 { 4 => crate::model::PaymentState::TimedOut, 5 => crate::model::PaymentState::Refundable, 6 => crate::model::PaymentState::RefundPending, + 7 => crate::model::PaymentState::WaitingFeeAcceptance, _ => unreachable!("Invalid variant for PaymentState: {}", self), } } @@ -2378,6 +2379,7 @@ impl SseDecode for crate::model::Config { let mut var_externalInputParsers = >>::sse_decode(deserializer); let mut var_useDefaultExternalInputParsers = ::sse_decode(deserializer); + let mut var_onchainFeeRateLeewaySatPerVbyte = >::sse_decode(deserializer); return crate::model::Config { liquid_electrum_url: var_liquidElectrumUrl, bitcoin_electrum_url: var_bitcoinElectrumUrl, @@ -2392,6 +2394,7 @@ impl SseDecode for crate::model::Config { breez_api_key: var_breezApiKey, external_input_parsers: var_externalInputParsers, use_default_external_input_parsers: var_useDefaultExternalInputParsers, + onchain_fee_rate_leeway_sat_per_vbyte: var_onchainFeeRateLeewaySatPerVbyte, }; } } @@ -3781,6 +3784,7 @@ impl SseDecode for crate::model::PaymentState { 4 => crate::model::PaymentState::TimedOut, 5 => crate::model::PaymentState::Refundable, 6 => crate::model::PaymentState::RefundPending, + 7 => crate::model::PaymentState::WaitingFeeAcceptance, _ => unreachable!("Invalid variant for PaymentState: {}", inner), }; } @@ -4170,6 +4174,12 @@ impl SseDecode for crate::model::SdkEvent { }; } 6 => { + let mut var_details = ::sse_decode(deserializer); + return crate::model::SdkEvent::PaymentWaitingFeeAcceptance { + details: var_details, + }; + } + 7 => { return crate::model::SdkEvent::Synced; } _ => { @@ -4677,6 +4687,9 @@ impl flutter_rust_bridge::IntoDart for crate::model::Config { self.use_default_external_input_parsers .into_into_dart() .into_dart(), + self.onchain_fee_rate_leeway_sat_per_vbyte + .into_into_dart() + .into_dart(), ] .into_dart() } @@ -5836,6 +5849,7 @@ impl flutter_rust_bridge::IntoDart for crate::model::PaymentState { Self::TimedOut => 4.into_dart(), Self::Refundable => 5.into_dart(), Self::RefundPending => 6.into_dart(), + Self::WaitingFeeAcceptance => 7.into_dart(), _ => unreachable!(), } } @@ -6368,7 +6382,10 @@ impl flutter_rust_bridge::IntoDart for crate::model::SdkEvent { crate::model::SdkEvent::PaymentWaitingConfirmation { details } => { [5.into_dart(), details.into_into_dart().into_dart()].into_dart() } - crate::model::SdkEvent::Synced => [6.into_dart()].into_dart(), + crate::model::SdkEvent::PaymentWaitingFeeAcceptance { details } => { + [6.into_dart(), details.into_into_dart().into_dart()].into_dart() + } + crate::model::SdkEvent::Synced => [7.into_dart()].into_dart(), _ => { unimplemented!(""); } @@ -6787,6 +6804,7 @@ impl SseEncode for crate::model::Config { serializer, ); ::sse_encode(self.use_default_external_input_parsers, serializer); + >::sse_encode(self.onchain_fee_rate_leeway_sat_per_vbyte, serializer); } } @@ -7907,6 +7925,7 @@ impl SseEncode for crate::model::PaymentState { crate::model::PaymentState::TimedOut => 4, crate::model::PaymentState::Refundable => 5, crate::model::PaymentState::RefundPending => 6, + crate::model::PaymentState::WaitingFeeAcceptance => 7, _ => { unimplemented!(""); } @@ -8181,8 +8200,12 @@ impl SseEncode for crate::model::SdkEvent { ::sse_encode(5, serializer); ::sse_encode(details, serializer); } - crate::model::SdkEvent::Synced => { + crate::model::SdkEvent::PaymentWaitingFeeAcceptance { details } => { ::sse_encode(6, serializer); + ::sse_encode(details, serializer); + } + crate::model::SdkEvent::Synced => { + ::sse_encode(7, serializer); } _ => { unimplemented!(""); @@ -8947,6 +8970,9 @@ mod io { use_default_external_input_parsers: self .use_default_external_input_parsers .cst_decode(), + onchain_fee_rate_leeway_sat_per_vbyte: self + .onchain_fee_rate_leeway_sat_per_vbyte + .cst_decode(), } } } @@ -10152,7 +10178,13 @@ mod io { details: ans.details.cst_decode(), } } - 6 => crate::model::SdkEvent::Synced, + 6 => { + let ans = unsafe { self.kind.PaymentWaitingFeeAcceptance }; + crate::model::SdkEvent::PaymentWaitingFeeAcceptance { + details: ans.details.cst_decode(), + } + } + 7 => crate::model::SdkEvent::Synced, _ => unreachable!(), } } @@ -10437,6 +10469,7 @@ mod io { breez_api_key: core::ptr::null_mut(), external_input_parsers: core::ptr::null_mut(), use_default_external_input_parsers: Default::default(), + onchain_fee_rate_leeway_sat_per_vbyte: core::ptr::null_mut(), } } } @@ -12517,6 +12550,7 @@ mod io { breez_api_key: *mut wire_cst_list_prim_u_8_strict, external_input_parsers: *mut wire_cst_list_external_input_parser, use_default_external_input_parsers: bool, + onchain_fee_rate_leeway_sat_per_vbyte: *mut u32, } #[repr(C)] #[derive(Clone, Copy)] @@ -13492,6 +13526,7 @@ mod io { PaymentRefundPending: wire_cst_SdkEvent_PaymentRefundPending, PaymentSucceeded: wire_cst_SdkEvent_PaymentSucceeded, PaymentWaitingConfirmation: wire_cst_SdkEvent_PaymentWaitingConfirmation, + PaymentWaitingFeeAcceptance: wire_cst_SdkEvent_PaymentWaitingFeeAcceptance, nil__: (), } #[repr(C)] @@ -13526,6 +13561,11 @@ mod io { } #[repr(C)] #[derive(Clone, Copy)] + pub struct wire_cst_SdkEvent_PaymentWaitingFeeAcceptance { + details: *mut wire_cst_payment, + } + #[repr(C)] + #[derive(Clone, Copy)] pub struct wire_cst_send_destination { tag: i32, kind: SendDestinationKind, diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index c6deced95..43c5bc6a8 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -65,6 +65,13 @@ pub struct Config { /// ([DEFAULT_EXTERNAL_INPUT_PARSERS](crate::sdk::DEFAULT_EXTERNAL_INPUT_PARSERS)). /// Set this to false in order to prevent their use. pub use_default_external_input_parsers: bool, + /// For payments where the onchain fees can only be estimated on creation, this can be used + /// in order to automatically allow slightly more expensive fees. If the actual fee rate ends up + /// being above the sum of the initial estimate and this leeway, the payment will require + /// user fee acceptance. See [WaitingFeeAcceptance](PaymentState::WaitingFeeAcceptance). + /// + /// Defaults to zero. + pub onchain_fee_rate_leeway_sat_per_vbyte: Option, } impl Config { @@ -83,6 +90,7 @@ impl Config { breez_api_key: Some(breez_api_key), external_input_parsers: None, use_default_external_input_parsers: true, + onchain_fee_rate_leeway_sat_per_vbyte: None, } } @@ -101,6 +109,7 @@ impl Config { breez_api_key, external_input_parsers: None, use_default_external_input_parsers: true, + onchain_fee_rate_leeway_sat_per_vbyte: None, } } @@ -243,6 +252,7 @@ pub enum SdkEvent { PaymentRefundPending { details: Payment }, PaymentSucceeded { details: Payment }, PaymentWaitingConfirmation { details: Payment }, + PaymentWaitingFeeAcceptance { details: Payment }, Synced, } @@ -1121,6 +1131,19 @@ pub enum PaymentState { /// /// When the refund tx is broadcast, `refund_tx_id` is set in the swap. RefundPending = 6, + + /// ## Chain Swaps + /// + /// This is the state when the user needs to accept new fees before the payment can proceed. + /// + /// Use [LiquidSdk::fetch_payment_proposed_fees](crate::sdk::LiquidSdk::fetch_payment_proposed_fees) + /// to find out the current fees and + /// [LiquidSdk::accept_payment_proposed_fees](crate::sdk::LiquidSdk::accept_payment_proposed_fees) + /// to accept them, allowing the payment to proceed. + /// + /// Otherwise, this payment can be immediately refunded using + /// [prepare_refund](crate::sdk::LiquidSdk::prepare_refund)/[refund](crate::sdk::LiquidSdk::refund). + WaitingFeeAcceptance = 7, } impl ToSql for PaymentState { fn to_sql(&self) -> rusqlite::Result> { @@ -1138,6 +1161,7 @@ impl FromSql for PaymentState { 4 => Ok(PaymentState::TimedOut), 5 => Ok(PaymentState::Refundable), 6 => Ok(PaymentState::RefundPending), + 7 => Ok(PaymentState::WaitingFeeAcceptance), _ => Err(FromSqlError::OutOfRange(i)), }, _ => Err(FromSqlError::InvalidType), @@ -1731,6 +1755,27 @@ impl Utxo { } } +/// An argument when calling [crate::sdk::LiquidSdk::fetch_payment_proposed_fees]. +#[derive(Debug, Clone)] +pub struct FetchPaymentProposedFeesRequest { + pub swap_id: String, +} + +/// Returned when calling [crate::sdk::LiquidSdk::fetch_payment_proposed_fees]. +#[derive(Debug, Clone, Serialize)] +pub struct FetchPaymentProposedFeesResponse { + pub swap_id: String, + pub fees_sat: u64, + /// Amount sent by the swap payer + pub payer_amount_sat: u64, +} + +/// An argument when calling [crate::sdk::LiquidSdk::accept_payment_proposed_fees]. +#[derive(Debug, Clone)] +pub struct AcceptPaymentProposedFeesRequest { + pub response: FetchPaymentProposedFeesResponse, +} + #[macro_export] macro_rules! get_invoice_amount { ($invoice:expr) => { diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index 03ecfd19d..24968ec97 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -233,13 +233,14 @@ impl Persister { let where_clause = vec![get_where_clause_state_in(&[ PaymentState::Created, PaymentState::Pending, + PaymentState::WaitingFeeAcceptance, ])]; self.list_chain_swaps_where(&con, where_clause) } pub(crate) fn list_pending_chain_swaps(&self) -> Result> { - self.list_chain_swaps_by_state(vec![PaymentState::Pending, PaymentState::RefundPending]) + self.list_chain_swaps_by_state(vec![PaymentState::Pending, PaymentState::RefundPending, PaymentState::WaitingFeeAcceptance]) } pub(crate) fn list_refundable_chain_swaps(&self) -> Result> { diff --git a/lib/core/src/receive_swap.rs b/lib/core/src/receive_swap.rs index 420126b99..186582149 100644 --- a/lib/core/src/receive_swap.rs +++ b/lib/core/src/receive_swap.rs @@ -405,6 +405,10 @@ impl ReceiveSwapHandler { err: format!("Cannot transition from {from_state:?} to Failed state"), }), (_, Failed) => Ok(()), + + (_, WaitingFeeAcceptance) => Err(PaymentError::Generic { + err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"), + }), } } diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 5399301c4..799869176 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -3,7 +3,7 @@ use std::ops::Not as _; use std::time::Instant; use std::{fs, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, ensure, Result}; use boltz_client::{swaps::boltz::*, util::secrets::Preimage}; use buy::{BuyBitcoinApi, BuyBitcoinService}; use chain::bitcoin::HybridBitcoinChainService; @@ -560,6 +560,25 @@ impl LiquidSdk { } }; } + WaitingFeeAcceptance => { + let swap_id = &payment + .details + .get_swap_id() + .ok_or(anyhow!("Payment WaitingFeeAcceptance must have a swap"))?; + + ensure!( + matches!( + self.persister.fetch_swap_by_id(swap_id)?, + Swap::Chain(ChainSwap { .. }) + ), + "Swap in WaitingFeeAcceptance payment must be chain swap" + ); + + self.notify_event_listeners(SdkEvent::PaymentWaitingFeeAcceptance { + details: payment, + }) + .await?; + } RefundPending => { // The swap state has changed to RefundPending self.notify_event_listeners(SdkEvent::PaymentRefundPending { @@ -1298,6 +1317,12 @@ impl LiquidSdk { "Payment has already failed. Please try with another invoice", )) } + WaitingFeeAcceptance => { + return Err(PaymentError::Generic { + err: "Send swap payment cannot be in state WaitingFeeAcceptance" + .to_string(), + }) + } }, None => { let keypair = utils::generate_keypair(); @@ -2541,6 +2566,74 @@ impl LiquidSdk { Ok(self.persister.get_payment_by_request(req)?) } + /// Fetches an up-to-date fees proposal for a [Payment] that is [WaitingFeeAcceptance]. + /// + /// Use [LiquidSdk::accept_payment_proposed_fees] to accept the proposed fees and proceed + /// with the payment. + pub async fn fetch_payment_proposed_fees( + &self, + req: &FetchPaymentProposedFeesRequest, + ) -> SdkResult { + let chain_swap = + self.persister + .fetch_chain_swap_by_id(&req.swap_id)? + .ok_or(SdkError::Generic { + err: format!("Could not find Swap {}", req.swap_id), + })?; + + let server_lockup_quote = self + .swapper + .get_zero_amount_chain_swap_quote(&req.swap_id)?; + + let payer_amount_sat = chain_swap.payer_amount_sat; + let fees_sat = payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat; + + Ok(FetchPaymentProposedFeesResponse { + swap_id: req.swap_id.clone(), + fees_sat, + payer_amount_sat, + }) + } + + /// Accepts proposed fees for a [Payment] that is [WaitingFeeAcceptance]. + /// + /// Use [LiquidSdk::fetch_payment_proposed_fees] to get an up-to-date fees proposal. + pub async fn accept_payment_proposed_fees( + &self, + req: &AcceptPaymentProposedFeesRequest, + ) -> Result<(), PaymentError> { + let FetchPaymentProposedFeesResponse { + swap_id, + fees_sat, + payer_amount_sat, + } = req.clone().response; + + let chain_swap = + self.persister + .fetch_chain_swap_by_id(&swap_id)? + .ok_or(SdkError::Generic { + err: format!("Could not find Swap {}", swap_id), + })?; + + let server_lockup_quote = self.swapper.get_zero_amount_chain_swap_quote(&swap_id)?; + + ensure_sdk!( + fees_sat == payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat, + PaymentError::InvalidOrExpiredFees + ); + + self.persister.update_zero_amount_swap_values( + &swap_id, + payer_amount_sat, + payer_amount_sat - fees_sat, + )?; + self.swapper + .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())?; + self.chain_swap_handler + .update_swap_info(&swap_id, Pending, None, None, None, None) + .await + } + /// Empties the Liquid Wallet cache for the [Config::network]. pub fn empty_wallet_cache(&self) -> Result<()> { let mut path = PathBuf::from(self.config.working_dir.clone()); @@ -3029,6 +3122,8 @@ mod tests { use lwk_wollet::{elements::Txid, hashes::hex::DisplayHex}; use tokio::sync::Mutex; + use crate::chain_swap::ESTIMATED_BTC_LOCKUP_TX_VSIZE; + use crate::test_utils::swapper::ZeroAmountSwapMockConfig; use crate::{ model::{Direction, PaymentState, Swap}, sdk::LiquidSdk, @@ -3049,6 +3144,7 @@ mod tests { accepts_zero_conf: bool, initial_payment_state: Option, user_lockup_tx_id: Option, + zero_amount: bool, } impl Default for NewSwapArgs { @@ -3058,6 +3154,7 @@ mod tests { initial_payment_state: None, direction: Direction::Outgoing, user_lockup_tx_id: None, + zero_amount: false, } } } @@ -3082,6 +3179,11 @@ mod tests { self.initial_payment_state = Some(payment_state); self } + + pub fn set_zero_amount(mut self, zero_amount: bool) -> Self { + self.zero_amount = zero_amount; + self + } } macro_rules! trigger_swap_update { @@ -3101,6 +3203,7 @@ mod tests { $args.initial_payment_state, $args.accepts_zero_conf, $args.user_lockup_tx_id, + $args.zero_amount, ); $persister.insert_or_update_chain_swap(&swap).unwrap(); Swap::Chain(swap) @@ -3274,6 +3377,7 @@ mod tests { status_stream.clone(), liquid_chain_service.clone(), bitcoin_chain_service.clone(), + None, )?); LiquidSdk::track_swap_updates(&sdk).await; @@ -3463,4 +3567,139 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_zero_amount_chain_swap_zero_leeway() -> Result<()> { + let user_lockup_sat = 50_000; + + let (_tmp_dir, persister) = new_persister()?; + let persister = Arc::new(persister); + let swapper = Arc::new(MockSwapper::new()); + 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_with_chain_services( + persister.clone(), + swapper.clone(), + status_stream.clone(), + liquid_chain_service.clone(), + bitcoin_chain_service.clone(), + None, + )?); + + LiquidSdk::track_swap_updates(&sdk).await; + + // We spawn a new thread since updates can only be sent when called via async runtimes + tokio::spawn(async move { + // Verify that `TransactionLockupFailed` correctly: + // 1. does not affect state when swapper doesn't increase fees + // 2. triggers a change to WaitingFeeAcceptance when there is a fee increase > 0 + for fee_increase in [0, 1] { + swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig { + user_lockup_sat, + onchain_fee_increase_sat: fee_increase, + }); + bitcoin_chain_service + .lock() + .await + .set_script_balance_sat(user_lockup_sat); + let persisted_swap = trigger_swap_update!( + "chain", + NewSwapArgs::default() + .set_direction(Direction::Incoming) + .set_accepts_zero_conf(false) + .set_zero_amount(true), + persister, + status_stream, + ChainSwapStates::TransactionLockupFailed, + None, + None + ); + match fee_increase { + 0 => { + assert_eq!(persisted_swap.state, PaymentState::Created); + } + 1 => { + assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance); + } + _ => panic!("Unexpected fee_increase"), + } + } + }) + .await?; + + Ok(()) + } + + #[tokio::test] + async fn test_zero_amount_chain_swap_with_leeway() -> Result<()> { + let user_lockup_sat = 50_000; + let onchain_fee_rate_leeway_sat_per_vbyte = 5; + + let (_tmp_dir, persister) = new_persister()?; + let persister = Arc::new(persister); + let swapper = Arc::new(MockSwapper::new()); + 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_with_chain_services( + persister.clone(), + swapper.clone(), + status_stream.clone(), + liquid_chain_service.clone(), + bitcoin_chain_service.clone(), + Some(onchain_fee_rate_leeway_sat_per_vbyte), + )?); + + LiquidSdk::track_swap_updates(&sdk).await; + + let max_fee_increase_for_auto_accept_sat = + onchain_fee_rate_leeway_sat_per_vbyte as u64 * ESTIMATED_BTC_LOCKUP_TX_VSIZE; + + // We spawn a new thread since updates can only be sent when called via async runtimes + tokio::spawn(async move { + // Verify that `TransactionLockupFailed` correctly: + // 1. does not affect state when swapper increases fee by up to sat/vbyte leeway * tx size + // 2. triggers a change to WaitingFeeAcceptance when it is any higher + for fee_increase in [ + max_fee_increase_for_auto_accept_sat, + max_fee_increase_for_auto_accept_sat + 1, + ] { + swapper.set_zero_amount_swap_mock_config(ZeroAmountSwapMockConfig { + user_lockup_sat, + onchain_fee_increase_sat: fee_increase, + }); + bitcoin_chain_service + .lock() + .await + .set_script_balance_sat(user_lockup_sat); + let persisted_swap = trigger_swap_update!( + "chain", + NewSwapArgs::default() + .set_direction(Direction::Incoming) + .set_accepts_zero_conf(false) + .set_zero_amount(true), + persister, + status_stream, + ChainSwapStates::TransactionLockupFailed, + None, + None + ); + match fee_increase { + val if val == max_fee_increase_for_auto_accept_sat => { + assert_eq!(persisted_swap.state, PaymentState::Created); + } + val if val == (max_fee_increase_for_auto_accept_sat + 1) => { + assert_eq!(persisted_swap.state, PaymentState::WaitingFeeAcceptance); + } + _ => panic!("Unexpected fee_increase"), + } + } + }) + .await?; + + Ok(()) + } } diff --git a/lib/core/src/send_swap.rs b/lib/core/src/send_swap.rs index 338da576b..248afe298 100644 --- a/lib/core/src/send_swap.rs +++ b/lib/core/src/send_swap.rs @@ -592,6 +592,10 @@ impl SendSwapHandler { err: format!("Cannot transition from {from_state:?} to Failed state"), }), (_, Failed) => Ok(()), + + (_, WaitingFeeAcceptance) => Err(PaymentError::Generic { + err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"), + }), } } diff --git a/lib/core/src/swapper/boltz/mod.rs b/lib/core/src/swapper/boltz/mod.rs index a91463876..5f3583688 100644 --- a/lib/core/src/swapper/boltz/mod.rs +++ b/lib/core/src/swapper/boltz/mod.rs @@ -180,7 +180,7 @@ impl Swapper for BoltzSwapper { Ok((pair_outgoing, pair_incoming)) } - fn get_zero_amount_chain_swap_quote(&self, swap_id: &str) -> Result { + fn get_zero_amount_chain_swap_quote(&self, swap_id: &str) -> Result { self.client .get_quote(swap_id) .map(|r| Amount::from_sat(r.amount)) diff --git a/lib/core/src/swapper/mod.rs b/lib/core/src/swapper/mod.rs index bf67d9a71..dfc78c805 100644 --- a/lib/core/src/swapper/mod.rs +++ b/lib/core/src/swapper/mod.rs @@ -45,7 +45,7 @@ pub trait Swapper: Send + Sync { /// /// If the user locked-up funds in the valid range this will return that amount. In all other /// cases, this will return an error. - fn get_zero_amount_chain_swap_quote(&self, swap_id: &str) -> Result; + fn get_zero_amount_chain_swap_quote(&self, swap_id: &str) -> Result; /// Accept a specific quote for a Zero-Amount Receive Chain Swap fn accept_zero_amount_chain_swap_quote( diff --git a/lib/core/src/test_utils/chain.rs b/lib/core/src/test_utils/chain.rs index 0a0073d58..cb3ee8ec2 100644 --- a/lib/core/src/test_utils/chain.rs +++ b/lib/core/src/test_utils/chain.rs @@ -128,17 +128,26 @@ impl LiquidChainService for MockLiquidChainService { pub(crate) struct MockBitcoinChainService { history: Vec, + script_balance_sat: u64, } impl MockBitcoinChainService { pub(crate) fn new() -> Self { - MockBitcoinChainService { history: vec![] } + MockBitcoinChainService { + history: vec![], + script_balance_sat: 0, + } } pub(crate) fn set_history(&mut self, history: Vec) -> &mut Self { self.history = history; self } + + pub(crate) fn set_script_balance_sat(&mut self, script_balance_sat: u64) -> &mut Self { + self.script_balance_sat = script_balance_sat; + self + } } #[async_trait] @@ -208,7 +217,10 @@ impl BitcoinChainService for MockBitcoinChainService { _script: &boltz_client::bitcoin::Script, _retries: u64, ) -> Result { - unimplemented!() + Ok(GetBalanceRes { + confirmed: self.script_balance_sat, + unconfirmed: 0, + }) } async fn verify_tx( diff --git a/lib/core/src/test_utils/chain_swap.rs b/lib/core/src/test_utils/chain_swap.rs index 82467d10f..496fc30b3 100644 --- a/lib/core/src/test_utils/chain_swap.rs +++ b/lib/core/src/test_utils/chain_swap.rs @@ -49,7 +49,94 @@ pub(crate) fn new_chain_swap( payment_state: Option, accept_zero_conf: bool, user_lockup_tx_id: Option, + zero_amount: bool, ) -> ChainSwap { + if zero_amount { + if direction == Direction::Outgoing { + panic!("Zero amount swaps must be incoming") + } + return ChainSwap { + id: generate_random_string(4), + direction: Direction::Incoming, + claim_address: None, + lockup_address: "tb1p7cftn5u3ndt8ln0m6hruwyhsz8kc5sxt557ua03qcew0z29u5paqh8f7uu" + .to_string(), + timeout_block_height: 2868778, + preimage: "bbce422d96c0386c3a6c1b1fe11fc7be3fdd871c6855db6ab2e319e96ec19c78" + .to_string(), + description: Some("Bitcoin transfer".to_string()), + create_response_json: r#"{ + "claim_details": { + "swapTree": { + "claimLeaf": { + "output": "82012088a914e5ec6c5b814b2d8616c1a0da0acc8b3388cf80d78820e5f32fc89e6947ca08a7855a99ac145f7de599446a0cc0ff4c9aa2694baa1138ac", + "version": 196 + }, + "refundLeaf": { + "output": "20692bbff63e48c1c05c5efeb7080f7c2416d2f9ecb79d217410eabc125f4d2ff0ad0312a716b1", + "version": 196 + } + }, + "lockupAddress": "tlq1pq0gfse32q454tmr30t7yl6lx2sv5sswdzh3j0zygz9v5jwwdq6deaec8ntnjq55yrx300u9ts5ykqnfcpuzrypmtda9yuszq0zpl6j8l9tunvqjyrdm3", + "serverPublicKey": "02692bbff63e48c1c05c5efeb7080f7c2416d2f9ecb79d217410eabc125f4d2ff0", + "timeoutBlockHeight": 1484562, + "amount": 0, + "blindingKey": "ebdd91bb06b2282e879256ff1c1a976016a582fea5418188799b1598281b0a5b" + }, + "lockup_details": { + "swapTree": { + "claimLeaf": { + "output": "82012088a914e5ec6c5b814b2d8616c1a0da0acc8b3388cf80d7882039688adbf0625672ec56e713e65ce809ee84e96525a13a68fe521588bf41628cac", + "version": 192 + }, + "refundLeaf": { + "output": "20edf1db3da18ad19962c8dfd7566048c7dc2e11f3d6580cbfed8f9a1321ffe4c7ad032ac62bb1", + "version": 192 + } + }, + "lockupAddress": "tb1p7cftn5u3ndt8ln0m6hruwyhsz8kc5sxt557ua03qcew0z29u5paqh8f7uu", + "serverPublicKey": "0239688adbf0625672ec56e713e65ce809ee84e96525a13a68fe521588bf41628c", + "timeoutBlockHeight": 2868778, + "amount": 0, + "bip21": "bitcoin:tb1p7cftn5u3ndt8ln0m6hruwyhsz8kc5sxt557ua03qcew0z29u5paqh8f7uu?amount=0.0001836&label=Send%20to%20L-BTC%20address" + } + }"#.to_string(), + claim_private_key: "4b04c3b95570fc48c7f33bc900b801245c2be31b90d41616477574aedc5b9d28" + .to_string(), + refund_private_key: "9e23d322577cfeb2b5490f3f86db58c806004afcb7c88995927bfdfc1c64cd8c" + .to_string(), + payer_amount_sat: 0, + receiver_amount_sat: 0, + claim_fees_sat: 144, + server_lockup_tx_id: None, + user_lockup_tx_id, + claim_tx_id: None, + refund_tx_id: None, + created_at: utils::now(), + state: payment_state.unwrap_or(PaymentState::Created), + accept_zero_conf, + pair_fees_json: r#"{ + "hash": "43087e267db95668b9b7c48efcf44d922484870f1bdb8b926e5d6b76bf4d0709", + "rate": 1, + "limits": { + "maximal": 4294967, + "minimal": 10000, + "maximalZeroConf": 0 + }, + "fees": { + "percentage": 0.1, + "minerFees": { + "server": 100, + "user": { + "claim": 100, + "lockup": 100 + } + } + } + }"# + .to_string(), + }; + } match direction { Direction::Incoming => ChainSwap { id: generate_random_string(4), diff --git a/lib/core/src/test_utils/receive_swap.rs b/lib/core/src/test_utils/receive_swap.rs index 93bdee61a..4245132bf 100644 --- a/lib/core/src/test_utils/receive_swap.rs +++ b/lib/core/src/test_utils/receive_swap.rs @@ -21,7 +21,7 @@ pub(crate) fn new_receive_swap_handler(persister: Arc) -> Result> = Arc::new(Box::new(MockSigner::new()?)); let onchain_wallet = Arc::new(MockWallet::new(signer)?); - let swapper = Arc::new(MockSwapper::new()); + let swapper = Arc::new(MockSwapper::default()); let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); Ok(ReceiveSwapHandler::new( diff --git a/lib/core/src/test_utils/sdk.rs b/lib/core/src/test_utils/sdk.rs index 14d69021d..9dfc16db7 100644 --- a/lib/core/src/test_utils/sdk.rs +++ b/lib/core/src/test_utils/sdk.rs @@ -40,6 +40,7 @@ pub(crate) fn new_liquid_sdk( status_stream, liquid_chain_service, bitcoin_chain_service, + None, ) } @@ -49,6 +50,7 @@ pub(crate) fn new_liquid_sdk_with_chain_services( status_stream: Arc, liquid_chain_service: Arc>, bitcoin_chain_service: Arc>, + onchain_fee_rate_leeway_sat_per_vbyte: Option, ) -> Result { let mut config = Config::testnet(None); config.working_dir = persister @@ -56,6 +58,7 @@ pub(crate) fn new_liquid_sdk_with_chain_services( .to_str() .ok_or(anyhow!("An invalid SDK directory was specified"))? .to_string(); + config.onchain_fee_rate_leeway_sat_per_vbyte = onchain_fee_rate_leeway_sat_per_vbyte; 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/send_swap.rs b/lib/core/src/test_utils/send_swap.rs index 226d33c69..8cc7adcff 100644 --- a/lib/core/src/test_utils/send_swap.rs +++ b/lib/core/src/test_utils/send_swap.rs @@ -20,7 +20,7 @@ pub(crate) fn new_send_swap_handler(persister: Arc) -> Result> = Arc::new(Box::new(MockSigner::new()?)); let onchain_wallet = Arc::new(MockWallet::new(signer)?); - let swapper = Arc::new(MockSwapper::new()); + let swapper = Arc::new(MockSwapper::default()); let chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); Ok(SendSwapHandler::new( diff --git a/lib/core/src/test_utils/swapper.rs b/lib/core/src/test_utils/swapper.rs index 322568dc7..fb829098e 100644 --- a/lib/core/src/test_utils/swapper.rs +++ b/lib/core/src/test_utils/swapper.rs @@ -11,8 +11,10 @@ use boltz_client::{ Amount, PublicKey, }; use sdk_common::invoice::parse_invoice; +use std::sync::Mutex; use crate::{ + ensure_sdk, error::{PaymentError, SdkError}, model::{Direction, SendSwap, Swap, Transaction as SdkTransaction, Utxo}, swapper::Swapper, @@ -23,7 +25,15 @@ use crate::{ use super::status_stream::MockStatusStream; #[derive(Default)] -pub struct MockSwapper {} +pub struct ZeroAmountSwapMockConfig { + pub user_lockup_sat: u64, + pub onchain_fee_increase_sat: u64, +} + +#[derive(Default)] +pub struct MockSwapper { + zero_amount_swap_mock_config: Mutex, +} impl MockSwapper { pub(crate) fn new() -> Self { @@ -60,6 +70,26 @@ impl MockSwapper { bip21: None, } } + + pub(crate) fn set_zero_amount_swap_mock_config(&self, config: ZeroAmountSwapMockConfig) { + *self.zero_amount_swap_mock_config.lock().unwrap() = config; + } + + fn get_zero_amount_swap_server_lockup_sat(&self) -> u64 { + let zero_amount_swap_mock_config = self.zero_amount_swap_mock_config.lock().unwrap(); + + let pair = self + .get_chain_pair(Direction::Incoming) + .expect("mock get_chain_pair failed") + .expect("no chainpair in mock"); + + let fees = pair + .fees + .boltz(zero_amount_swap_mock_config.user_lockup_sat) + + pair.fees.server() + + zero_amount_swap_mock_config.onchain_fee_increase_sat; + zero_amount_swap_mock_config.user_lockup_sat - fees + } } impl Swapper for MockSwapper { @@ -314,15 +344,20 @@ impl Swapper for MockSwapper { unimplemented!() } - fn get_zero_amount_chain_swap_quote(&self, _swap_id: &str) -> Result { - unimplemented!() + fn get_zero_amount_chain_swap_quote(&self, _swap_id: &str) -> Result { + let server_lockup_amount_sat = self.get_zero_amount_swap_server_lockup_sat(); + Ok(Amount::from_sat(server_lockup_amount_sat)) } fn accept_zero_amount_chain_swap_quote( &self, _swap_id: &str, - _server_lockup_sat: u64, + server_lockup_sat: u64, ) -> Result<(), PaymentError> { - unimplemented!() + ensure_sdk!( + server_lockup_sat == self.get_zero_amount_swap_server_lockup_sat(), + PaymentError::InvalidOrExpiredFees + ); + Ok(()) } } diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index bd7749cef..248a59fb7 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -1476,12 +1476,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_ln_url_error_data(raw); } - @protected - LnUrlInfo dco_decode_box_autoadd_ln_url_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return dco_decode_ln_url_info(raw); - } - @protected LnUrlPayErrorData dco_decode_box_autoadd_ln_url_pay_error_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1709,11 +1703,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { network: dco_decode_liquid_network(arr[5]), paymentTimeoutSec: dco_decode_u_64(arr[6]), zeroConfMinFeeRateMsat: dco_decode_u_32(arr[7]), - syncServiceUrl: dco_decode_String(arr[8]), - zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[9]), - breezApiKey: dco_decode_opt_String(arr[10]), - externalInputParsers: dco_decode_opt_list_external_input_parser(arr[11]), - useDefaultExternalInputParsers: dco_decode_bool(arr[12]), + zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[8]), + breezApiKey: dco_decode_opt_String(arr[9]), + externalInputParsers: dco_decode_opt_list_external_input_parser(arr[10]), + useDefaultExternalInputParsers: dco_decode_bool(arr[11]), + onchainFeeRateLeewaySatPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[12]), ); } @@ -1964,12 +1958,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - List dco_decode_list_payment_state(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return (raw as List).map(dco_decode_payment_state).toList(); - } - @protected List dco_decode_list_payment_type(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1980,15 +1968,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ListPaymentsRequest dco_decode_list_payments_request(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); + if (arr.length != 6) throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); return ListPaymentsRequest( filters: dco_decode_opt_list_payment_type(arr[0]), - states: dco_decode_opt_list_payment_state(arr[1]), - fromTimestamp: dco_decode_opt_box_autoadd_i_64(arr[2]), - toTimestamp: dco_decode_opt_box_autoadd_i_64(arr[3]), - offset: dco_decode_opt_box_autoadd_u_32(arr[4]), - limit: dco_decode_opt_box_autoadd_u_32(arr[5]), - details: dco_decode_opt_box_autoadd_list_payment_details(arr[6]), + fromTimestamp: dco_decode_opt_box_autoadd_i_64(arr[1]), + toTimestamp: dco_decode_opt_box_autoadd_i_64(arr[2]), + offset: dco_decode_opt_box_autoadd_u_32(arr[3]), + limit: dco_decode_opt_box_autoadd_u_32(arr[4]), + details: dco_decode_opt_box_autoadd_list_payment_details(arr[5]), ); } @@ -2129,22 +2116,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } - @protected - LnUrlInfo dco_decode_ln_url_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); - return LnUrlInfo( - lnAddress: dco_decode_opt_String(arr[0]), - lnurlPayComment: dco_decode_opt_String(arr[1]), - lnurlPayDomain: dco_decode_opt_String(arr[2]), - lnurlPayMetadata: dco_decode_opt_String(arr[3]), - lnurlPaySuccessAction: dco_decode_opt_box_autoadd_success_action_processed(arr[4]), - lnurlPayUnprocessedSuccessAction: dco_decode_opt_box_autoadd_success_action(arr[5]), - lnurlWithdrawEndpoint: dco_decode_opt_String(arr[6]), - ); - } - @protected LnUrlPayError dco_decode_ln_url_pay_error(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2458,12 +2429,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_list_payment_details(raw); } - @protected - LnUrlInfo? dco_decode_opt_box_autoadd_ln_url_info(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_box_autoadd_ln_url_info(raw); - } - @protected PayAmount? dco_decode_opt_box_autoadd_pay_amount(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2512,12 +2477,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_list_external_input_parser(raw); } - @protected - List? dco_decode_opt_list_payment_state(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - return raw == null ? null : dco_decode_list_payment_state(raw); - } - @protected List? dco_decode_opt_list_payment_type(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2581,9 +2540,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: dco_decode_opt_String(raw[4]), bolt12Offer: dco_decode_opt_String(raw[5]), paymentHash: dco_decode_opt_String(raw[6]), - lnurlInfo: dco_decode_opt_box_autoadd_ln_url_info(raw[7]), - refundTxId: dco_decode_opt_String(raw[8]), - refundTxAmountSat: dco_decode_opt_box_autoadd_u_64(raw[9]), + refundTxId: dco_decode_opt_String(raw[7]), + refundTxAmountSat: dco_decode_opt_box_autoadd_u_64(raw[8]), ); case 1: return PaymentDetails_Liquid( @@ -2732,13 +2690,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { PrepareLnUrlPayResponse dco_decode_prepare_ln_url_pay_response(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 5) throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); return PrepareLnUrlPayResponse( destination: dco_decode_send_destination(arr[0]), feesSat: dco_decode_u_64(arr[1]), - data: dco_decode_ln_url_pay_request_data(arr[2]), - comment: dco_decode_opt_String(arr[3]), - successAction: dco_decode_opt_box_autoadd_success_action(arr[4]), + successAction: dco_decode_opt_box_autoadd_success_action(arr[2]), ); } @@ -3004,6 +2960,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { details: dco_decode_box_autoadd_payment(raw[1]), ); case 6: + return SdkEvent_PaymentWaitingFeeAcceptance( + details: dco_decode_box_autoadd_payment(raw[1]), + ); + case 7: return SdkEvent_Synced(); default: throw Exception("unreachable"); @@ -3443,12 +3403,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_ln_url_error_data(deserializer)); } - @protected - LnUrlInfo sse_decode_box_autoadd_ln_url_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - return (sse_decode_ln_url_info(deserializer)); - } - @protected LnUrlPayErrorData sse_decode_box_autoadd_ln_url_pay_error_data(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3665,11 +3619,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_network = sse_decode_liquid_network(deserializer); var var_paymentTimeoutSec = sse_decode_u_64(deserializer); var var_zeroConfMinFeeRateMsat = sse_decode_u_32(deserializer); - var var_syncServiceUrl = sse_decode_String(deserializer); var var_zeroConfMaxAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); var var_breezApiKey = sse_decode_opt_String(deserializer); var var_externalInputParsers = sse_decode_opt_list_external_input_parser(deserializer); var var_useDefaultExternalInputParsers = sse_decode_bool(deserializer); + var var_onchainFeeRateLeewaySatPerVbyte = sse_decode_opt_box_autoadd_u_32(deserializer); return Config( liquidElectrumUrl: var_liquidElectrumUrl, bitcoinElectrumUrl: var_bitcoinElectrumUrl, @@ -3679,11 +3633,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { network: var_network, paymentTimeoutSec: var_paymentTimeoutSec, zeroConfMinFeeRateMsat: var_zeroConfMinFeeRateMsat, - syncServiceUrl: var_syncServiceUrl, zeroConfMaxAmountSat: var_zeroConfMaxAmountSat, breezApiKey: var_breezApiKey, externalInputParsers: var_externalInputParsers, - useDefaultExternalInputParsers: var_useDefaultExternalInputParsers); + useDefaultExternalInputParsers: var_useDefaultExternalInputParsers, + onchainFeeRateLeewaySatPerVbyte: var_onchainFeeRateLeewaySatPerVbyte); } @protected @@ -3964,18 +3918,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - List sse_decode_list_payment_state(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - var len_ = sse_decode_i_32(deserializer); - var ans_ = []; - for (var idx_ = 0; idx_ < len_; ++idx_) { - ans_.add(sse_decode_payment_state(deserializer)); - } - return ans_; - } - @protected List sse_decode_list_payment_type(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3992,7 +3934,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ListPaymentsRequest sse_decode_list_payments_request(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var var_filters = sse_decode_opt_list_payment_type(deserializer); - var var_states = sse_decode_opt_list_payment_state(deserializer); var var_fromTimestamp = sse_decode_opt_box_autoadd_i_64(deserializer); var var_toTimestamp = sse_decode_opt_box_autoadd_i_64(deserializer); var var_offset = sse_decode_opt_box_autoadd_u_32(deserializer); @@ -4000,7 +3941,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_details = sse_decode_opt_box_autoadd_list_payment_details(deserializer); return ListPaymentsRequest( filters: var_filters, - states: var_states, fromTimestamp: var_fromTimestamp, toTimestamp: var_toTimestamp, offset: var_offset, @@ -4175,26 +4115,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return LnUrlErrorData(reason: var_reason); } - @protected - LnUrlInfo sse_decode_ln_url_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - var var_lnAddress = sse_decode_opt_String(deserializer); - var var_lnurlPayComment = sse_decode_opt_String(deserializer); - var var_lnurlPayDomain = sse_decode_opt_String(deserializer); - var var_lnurlPayMetadata = sse_decode_opt_String(deserializer); - var var_lnurlPaySuccessAction = sse_decode_opt_box_autoadd_success_action_processed(deserializer); - var var_lnurlPayUnprocessedSuccessAction = sse_decode_opt_box_autoadd_success_action(deserializer); - var var_lnurlWithdrawEndpoint = sse_decode_opt_String(deserializer); - return LnUrlInfo( - lnAddress: var_lnAddress, - lnurlPayComment: var_lnurlPayComment, - lnurlPayDomain: var_lnurlPayDomain, - lnurlPayMetadata: var_lnurlPayMetadata, - lnurlPaySuccessAction: var_lnurlPaySuccessAction, - lnurlPayUnprocessedSuccessAction: var_lnurlPayUnprocessedSuccessAction, - lnurlWithdrawEndpoint: var_lnurlWithdrawEndpoint); - } - @protected LnUrlPayError sse_decode_ln_url_pay_error(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4502,17 +4422,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - LnUrlInfo? sse_decode_opt_box_autoadd_ln_url_info(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_box_autoadd_ln_url_info(deserializer)); - } else { - return null; - } - } - @protected PayAmount? sse_decode_opt_box_autoadd_pay_amount(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4601,17 +4510,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - List? sse_decode_opt_list_payment_state(SseDeserializer deserializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - if (sse_decode_bool(deserializer)) { - return (sse_decode_list_payment_state(deserializer)); - } else { - return null; - } - } - @protected List? sse_decode_opt_list_payment_type(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4686,7 +4584,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_bolt11 = sse_decode_opt_String(deserializer); var var_bolt12Offer = sse_decode_opt_String(deserializer); var var_paymentHash = sse_decode_opt_String(deserializer); - var var_lnurlInfo = sse_decode_opt_box_autoadd_ln_url_info(deserializer); var var_refundTxId = sse_decode_opt_String(deserializer); var var_refundTxAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); return PaymentDetails_Lightning( @@ -4696,7 +4593,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: var_bolt11, bolt12Offer: var_bolt12Offer, paymentHash: var_paymentHash, - lnurlInfo: var_lnurlInfo, refundTxId: var_refundTxId, refundTxAmountSat: var_refundTxAmountSat); case 1: @@ -4839,15 +4735,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs var var_destination = sse_decode_send_destination(deserializer); var var_feesSat = sse_decode_u_64(deserializer); - var var_data = sse_decode_ln_url_pay_request_data(deserializer); - var var_comment = sse_decode_opt_String(deserializer); var var_successAction = sse_decode_opt_box_autoadd_success_action(deserializer); return PrepareLnUrlPayResponse( - destination: var_destination, - feesSat: var_feesSat, - data: var_data, - comment: var_comment, - successAction: var_successAction); + destination: var_destination, feesSat: var_feesSat, successAction: var_successAction); } @protected @@ -5084,6 +4974,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_details = sse_decode_box_autoadd_payment(deserializer); return SdkEvent_PaymentWaitingConfirmation(details: var_details); case 6: + var var_details = sse_decode_box_autoadd_payment(deserializer); + return SdkEvent_PaymentWaitingFeeAcceptance(details: var_details); + case 7: return SdkEvent_Synced(); default: throw UnimplementedError(''); @@ -5596,12 +5489,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_ln_url_error_data(self, serializer); } - @protected - void sse_encode_box_autoadd_ln_url_info(LnUrlInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_ln_url_info(self, serializer); - } - @protected void sse_encode_box_autoadd_ln_url_pay_error_data(LnUrlPayErrorData self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5821,11 +5708,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_liquid_network(self.network, serializer); sse_encode_u_64(self.paymentTimeoutSec, serializer); sse_encode_u_32(self.zeroConfMinFeeRateMsat, serializer); - sse_encode_String(self.syncServiceUrl, serializer); sse_encode_opt_box_autoadd_u_64(self.zeroConfMaxAmountSat, serializer); sse_encode_opt_String(self.breezApiKey, serializer); sse_encode_opt_list_external_input_parser(self.externalInputParsers, serializer); sse_encode_bool(self.useDefaultExternalInputParsers, serializer); + sse_encode_opt_box_autoadd_u_32(self.onchainFeeRateLeewaySatPerVbyte, serializer); } @protected @@ -6052,15 +5939,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - void sse_encode_list_payment_state(List self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_i_32(self.length, serializer); - for (final item in self) { - sse_encode_payment_state(item, serializer); - } - } - @protected void sse_encode_list_payment_type(List self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6074,7 +5952,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { void sse_encode_list_payments_request(ListPaymentsRequest self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_opt_list_payment_type(self.filters, serializer); - sse_encode_opt_list_payment_state(self.states, serializer); sse_encode_opt_box_autoadd_i_64(self.fromTimestamp, serializer); sse_encode_opt_box_autoadd_i_64(self.toTimestamp, serializer); sse_encode_opt_box_autoadd_u_32(self.offset, serializer); @@ -6208,18 +6085,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.reason, serializer); } - @protected - void sse_encode_ln_url_info(LnUrlInfo self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_opt_String(self.lnAddress, serializer); - sse_encode_opt_String(self.lnurlPayComment, serializer); - sse_encode_opt_String(self.lnurlPayDomain, serializer); - sse_encode_opt_String(self.lnurlPayMetadata, serializer); - sse_encode_opt_box_autoadd_success_action_processed(self.lnurlPaySuccessAction, serializer); - sse_encode_opt_box_autoadd_success_action(self.lnurlPayUnprocessedSuccessAction, serializer); - sse_encode_opt_String(self.lnurlWithdrawEndpoint, serializer); - } - @protected void sse_encode_ln_url_pay_error(LnUrlPayError self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6487,16 +6352,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - void sse_encode_opt_box_autoadd_ln_url_info(LnUrlInfo? self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_box_autoadd_ln_url_info(self, serializer); - } - } - @protected void sse_encode_opt_box_autoadd_pay_amount(PayAmount? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6578,16 +6433,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - void sse_encode_opt_list_payment_state(List? self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - - sse_encode_bool(self != null, serializer); - if (self != null) { - sse_encode_list_payment_state(self, serializer); - } - } - @protected void sse_encode_opt_list_payment_type(List? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6645,7 +6490,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: final bolt11, bolt12Offer: final bolt12Offer, paymentHash: final paymentHash, - lnurlInfo: final lnurlInfo, refundTxId: final refundTxId, refundTxAmountSat: final refundTxAmountSat ): @@ -6656,7 +6500,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(bolt11, serializer); sse_encode_opt_String(bolt12Offer, serializer); sse_encode_opt_String(paymentHash, serializer); - sse_encode_opt_box_autoadd_ln_url_info(lnurlInfo, serializer); sse_encode_opt_String(refundTxId, serializer); sse_encode_opt_box_autoadd_u_64(refundTxAmountSat, serializer); case PaymentDetails_Liquid(destination: final destination, description: final description): @@ -6788,8 +6631,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_send_destination(self.destination, serializer); sse_encode_u_64(self.feesSat, serializer); - sse_encode_ln_url_pay_request_data(self.data, serializer); - sse_encode_opt_String(self.comment, serializer); sse_encode_opt_box_autoadd_success_action(self.successAction, serializer); } @@ -6974,8 +6815,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { case SdkEvent_PaymentWaitingConfirmation(details: final details): sse_encode_i_32(5, serializer); sse_encode_box_autoadd_payment(details, serializer); - case SdkEvent_Synced(): + case SdkEvent_PaymentWaitingFeeAcceptance(details: final details): sse_encode_i_32(6, serializer); + sse_encode_box_autoadd_payment(details, serializer); + case SdkEvent_Synced(): + sse_encode_i_32(7, serializer); default: throw UnimplementedError(''); } diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index 4a08438fe..ddaf430a5 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -2280,6 +2280,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.breez_api_key = cst_encode_opt_String(apiObj.breezApiKey); wireObj.external_input_parsers = cst_encode_opt_list_external_input_parser(apiObj.externalInputParsers); wireObj.use_default_external_input_parsers = cst_encode_bool(apiObj.useDefaultExternalInputParsers); + wireObj.onchain_fee_rate_leeway_sat_per_vbyte = + cst_encode_opt_box_autoadd_u_32(apiObj.onchainFeeRateLeewaySatPerVbyte); } @protected @@ -3217,8 +3219,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.kind.PaymentWaitingConfirmation.details = pre_details; return; } - if (apiObj is SdkEvent_Synced) { + if (apiObj is SdkEvent_PaymentWaitingFeeAcceptance) { + var pre_details = cst_encode_box_autoadd_payment(apiObj.details); wireObj.tag = 6; + wireObj.kind.PaymentWaitingFeeAcceptance.details = pre_details; + return; + } + if (apiObj is SdkEvent_Synced) { + wireObj.tag = 7; return; } } @@ -6222,6 +6230,10 @@ final class wire_cst_SdkEvent_PaymentWaitingConfirmation extends ffi.Struct { external ffi.Pointer details; } +final class wire_cst_SdkEvent_PaymentWaitingFeeAcceptance extends ffi.Struct { + external ffi.Pointer details; +} + final class SdkEventKind extends ffi.Union { external wire_cst_SdkEvent_PaymentFailed PaymentFailed; @@ -6234,6 +6246,8 @@ final class SdkEventKind extends ffi.Union { external wire_cst_SdkEvent_PaymentSucceeded PaymentSucceeded; external wire_cst_SdkEvent_PaymentWaitingConfirmation PaymentWaitingConfirmation; + + external wire_cst_SdkEvent_PaymentWaitingFeeAcceptance PaymentWaitingFeeAcceptance; } final class wire_cst_sdk_event extends ffi.Struct { @@ -6288,6 +6302,8 @@ final class wire_cst_config extends ffi.Struct { @ffi.Bool() external bool use_default_external_input_parsers; + + external ffi.Pointer onchain_fee_rate_leeway_sat_per_vbyte; } final class wire_cst_connect_request extends ffi.Struct { @@ -6906,6 +6922,8 @@ final class wire_cst_sign_message_response extends ffi.Struct { const int ESTIMATED_BTC_CLAIM_TX_VSIZE = 111; +const int ESTIMATED_BTC_LOCKUP_TX_VSIZE = 154; + const double STANDARD_FEE_RATE_SAT_PER_VBYTE = 0.1; const double LOWBALL_FEE_RATE_SAT_PER_VBYTE = 0.01; diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index c360b9ee2..ac2698089 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -154,6 +154,14 @@ class Config { /// Set this to false in order to prevent their use. final bool useDefaultExternalInputParsers; + /// For payments where the onchain fees can only be estimated on creation, this can be used + /// in order to automatically allow slightly more expensive fees. If the actual fee rate ends up + /// being above the sum of the initial estimate and this leeway, the payment will require + /// user fee acceptance. See [WaitingFeeAcceptance](PaymentState::WaitingFeeAcceptance). + /// + /// Defaults to zero. + final int? onchainFeeRateLeewaySatPerVbyte; + const Config({ required this.liquidElectrumUrl, required this.bitcoinElectrumUrl, @@ -168,6 +176,7 @@ class Config { this.breezApiKey, this.externalInputParsers, required this.useDefaultExternalInputParsers, + this.onchainFeeRateLeewaySatPerVbyte, }); @override @@ -184,7 +193,8 @@ class Config { zeroConfMaxAmountSat.hashCode ^ breezApiKey.hashCode ^ externalInputParsers.hashCode ^ - useDefaultExternalInputParsers.hashCode; + useDefaultExternalInputParsers.hashCode ^ + onchainFeeRateLeewaySatPerVbyte.hashCode; @override bool operator ==(Object other) => @@ -203,7 +213,8 @@ class Config { zeroConfMaxAmountSat == other.zeroConfMaxAmountSat && breezApiKey == other.breezApiKey && externalInputParsers == other.externalInputParsers && - useDefaultExternalInputParsers == other.useDefaultExternalInputParsers; + useDefaultExternalInputParsers == other.useDefaultExternalInputParsers && + onchainFeeRateLeewaySatPerVbyte == other.onchainFeeRateLeewaySatPerVbyte; } /// An argument when calling [crate::sdk::LiquidSdk::connect]. @@ -817,6 +828,19 @@ enum PaymentState { /// /// When the refund tx is broadcast, `refund_tx_id` is set in the swap. refundPending, + + /// ## Chain Swaps + /// + /// This is the state when the user needs to accept new fees before the payment can proceed. + /// + /// Use [LiquidSdk::fetch_payment_proposed_fees](crate::sdk::LiquidSdk::fetch_payment_proposed_fees) + /// to find out the current fees and + /// [LiquidSdk::accept_payment_proposed_fees](crate::sdk::LiquidSdk::accept_payment_proposed_fees) + /// to accept them, allowing the payment to proceed. + /// + /// Otherwise, this payment can be immediately refunded using + /// [prepare_refund](crate::sdk::LiquidSdk::prepare_refund)/[refund](crate::sdk::LiquidSdk::refund). + waitingFeeAcceptance, ; } @@ -1385,6 +1409,9 @@ sealed class SdkEvent with _$SdkEvent { const factory SdkEvent.paymentWaitingConfirmation({ required Payment details, }) = SdkEvent_PaymentWaitingConfirmation; + const factory SdkEvent.paymentWaitingFeeAcceptance({ + required Payment details, + }) = SdkEvent_PaymentWaitingFeeAcceptance; const factory SdkEvent.synced() = SdkEvent_Synced; } diff --git a/packages/dart/lib/src/model.freezed.dart b/packages/dart/lib/src/model.freezed.dart index a712d5559..8b3d20ab0 100644 --- a/packages/dart/lib/src/model.freezed.dart +++ b/packages/dart/lib/src/model.freezed.dart @@ -1724,6 +1724,88 @@ abstract class SdkEvent_PaymentWaitingConfirmation extends SdkEvent { get copyWith => throw _privateConstructorUsedError; } +/// @nodoc +abstract class _$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWith<$Res> { + factory _$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWith( + _$SdkEvent_PaymentWaitingFeeAcceptanceImpl value, + $Res Function(_$SdkEvent_PaymentWaitingFeeAcceptanceImpl) then) = + __$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWithImpl<$Res>; + @useResult + $Res call({Payment details}); +} + +/// @nodoc +class __$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWithImpl<$Res> + extends _$SdkEventCopyWithImpl<$Res, _$SdkEvent_PaymentWaitingFeeAcceptanceImpl> + implements _$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWith<$Res> { + __$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWithImpl(_$SdkEvent_PaymentWaitingFeeAcceptanceImpl _value, + $Res Function(_$SdkEvent_PaymentWaitingFeeAcceptanceImpl) _then) + : super(_value, _then); + + /// Create a copy of SdkEvent + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? details = null, + }) { + return _then(_$SdkEvent_PaymentWaitingFeeAcceptanceImpl( + details: null == details + ? _value.details + : details // ignore: cast_nullable_to_non_nullable + as Payment, + )); + } +} + +/// @nodoc + +class _$SdkEvent_PaymentWaitingFeeAcceptanceImpl extends SdkEvent_PaymentWaitingFeeAcceptance { + const _$SdkEvent_PaymentWaitingFeeAcceptanceImpl({required this.details}) : super._(); + + @override + final Payment details; + + @override + String toString() { + return 'SdkEvent.paymentWaitingFeeAcceptance(details: $details)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SdkEvent_PaymentWaitingFeeAcceptanceImpl && + (identical(other.details, details) || other.details == details)); + } + + @override + int get hashCode => Object.hash(runtimeType, details); + + /// Create a copy of SdkEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWith<_$SdkEvent_PaymentWaitingFeeAcceptanceImpl> + get copyWith => __$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWithImpl< + _$SdkEvent_PaymentWaitingFeeAcceptanceImpl>(this, _$identity); +} + +abstract class SdkEvent_PaymentWaitingFeeAcceptance extends SdkEvent { + const factory SdkEvent_PaymentWaitingFeeAcceptance({required final Payment details}) = + _$SdkEvent_PaymentWaitingFeeAcceptanceImpl; + const SdkEvent_PaymentWaitingFeeAcceptance._() : super._(); + + Payment get details; + + /// Create a copy of SdkEvent + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SdkEvent_PaymentWaitingFeeAcceptanceImplCopyWith<_$SdkEvent_PaymentWaitingFeeAcceptanceImpl> + get copyWith => throw _privateConstructorUsedError; +} + /// @nodoc abstract class _$$SdkEvent_SyncedImplCopyWith<$Res> { factory _$$SdkEvent_SyncedImplCopyWith( diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 42a12c1da..cde124523 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -4642,6 +4642,10 @@ final class wire_cst_SdkEvent_PaymentWaitingConfirmation extends ffi.Struct { external ffi.Pointer details; } +final class wire_cst_SdkEvent_PaymentWaitingFeeAcceptance extends ffi.Struct { + external ffi.Pointer details; +} + final class SdkEventKind extends ffi.Union { external wire_cst_SdkEvent_PaymentFailed PaymentFailed; @@ -4654,6 +4658,8 @@ final class SdkEventKind extends ffi.Union { external wire_cst_SdkEvent_PaymentSucceeded PaymentSucceeded; external wire_cst_SdkEvent_PaymentWaitingConfirmation PaymentWaitingConfirmation; + + external wire_cst_SdkEvent_PaymentWaitingFeeAcceptance PaymentWaitingFeeAcceptance; } final class wire_cst_sdk_event extends ffi.Struct { @@ -4708,6 +4714,8 @@ final class wire_cst_config extends ffi.Struct { @ffi.Bool() external bool use_default_external_input_parsers; + + external ffi.Pointer onchain_fee_rate_leeway_sat_per_vbyte; } final class wire_cst_connect_request extends ffi.Struct { @@ -5367,6 +5375,8 @@ typedef DartUniFfiRustFutureContinuationFunction = void Function(ffi.Pointer { + val list = ArrayList() + for (value in arr.toList()) { + when (value) { + is ReadableMap -> list.add(asAcceptPaymentProposedFeesRequest(value)!!) + else -> throw SdkException.Generic(errUnexpectedType(value)) + } + } + return list +} + fun asAesSuccessActionData(aesSuccessActionData: ReadableMap): AesSuccessActionData? { if (!validateMandatoryFields( aesSuccessActionData, @@ -284,6 +314,16 @@ fun asConfig(config: ReadableMap): Config? { } else { null } + val onchainFeeRateLeewaySatPerVbyte = + if (hasNonNullKey( + config, + "onchainFeeRateLeewaySatPerVbyte", + ) + ) { + config.getInt("onchainFeeRateLeewaySatPerVbyte").toUInt() + } else { + null + } return Config( liquidElectrumUrl, bitcoinElectrumUrl, @@ -298,6 +338,7 @@ fun asConfig(config: ReadableMap): Config? { zeroConfMaxAmountSat, useDefaultExternalInputParsers, externalInputParsers, + onchainFeeRateLeewaySatPerVbyte, ) } @@ -316,6 +357,7 @@ fun readableMapOf(config: Config): ReadableMap = "zeroConfMaxAmountSat" to config.zeroConfMaxAmountSat, "useDefaultExternalInputParsers" to config.useDefaultExternalInputParsers, "externalInputParsers" to config.externalInputParsers?.let { readableArrayOf(it) }, + "onchainFeeRateLeewaySatPerVbyte" to config.onchainFeeRateLeewaySatPerVbyte, ) fun asConfigList(arr: ReadableArray): List { @@ -473,6 +515,72 @@ fun asExternalInputParserList(arr: ReadableArray): List { return list } +fun asFetchPaymentProposedFeesRequest(fetchPaymentProposedFeesRequest: ReadableMap): FetchPaymentProposedFeesRequest? { + if (!validateMandatoryFields( + fetchPaymentProposedFeesRequest, + arrayOf( + "swapId", + ), + ) + ) { + return null + } + val swapId = fetchPaymentProposedFeesRequest.getString("swapId")!! + return FetchPaymentProposedFeesRequest(swapId) +} + +fun readableMapOf(fetchPaymentProposedFeesRequest: FetchPaymentProposedFeesRequest): ReadableMap = + readableMapOf( + "swapId" to fetchPaymentProposedFeesRequest.swapId, + ) + +fun asFetchPaymentProposedFeesRequestList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toList()) { + when (value) { + is ReadableMap -> list.add(asFetchPaymentProposedFeesRequest(value)!!) + else -> throw SdkException.Generic(errUnexpectedType(value)) + } + } + return list +} + +fun asFetchPaymentProposedFeesResponse(fetchPaymentProposedFeesResponse: ReadableMap): FetchPaymentProposedFeesResponse? { + if (!validateMandatoryFields( + fetchPaymentProposedFeesResponse, + arrayOf( + "swapId", + "feesSat", + "payerAmountSat", + ), + ) + ) { + return null + } + val swapId = fetchPaymentProposedFeesResponse.getString("swapId")!! + val feesSat = fetchPaymentProposedFeesResponse.getDouble("feesSat").toULong() + val payerAmountSat = fetchPaymentProposedFeesResponse.getDouble("payerAmountSat").toULong() + return FetchPaymentProposedFeesResponse(swapId, feesSat, payerAmountSat) +} + +fun readableMapOf(fetchPaymentProposedFeesResponse: FetchPaymentProposedFeesResponse): ReadableMap = + readableMapOf( + "swapId" to fetchPaymentProposedFeesResponse.swapId, + "feesSat" to fetchPaymentProposedFeesResponse.feesSat, + "payerAmountSat" to fetchPaymentProposedFeesResponse.payerAmountSat, + ) + +fun asFetchPaymentProposedFeesResponseList(arr: ReadableArray): List { + val list = ArrayList() + for (value in arr.toList()) { + when (value) { + is ReadableMap -> list.add(asFetchPaymentProposedFeesResponse(value)!!) + else -> throw SdkException.Generic(errUnexpectedType(value)) + } + } + return list +} + fun asFiatCurrency(fiatCurrency: ReadableMap): FiatCurrency? { if (!validateMandatoryFields( fiatCurrency, @@ -3230,6 +3338,10 @@ fun asSdkEvent(sdkEvent: ReadableMap): SdkEvent? { val details = sdkEvent.getMap("details")?.let { asPayment(it) }!! return SdkEvent.PaymentWaitingConfirmation(details) } + if (type == "paymentWaitingFeeAcceptance") { + val details = sdkEvent.getMap("details")?.let { asPayment(it) }!! + return SdkEvent.PaymentWaitingFeeAcceptance(details) + } if (type == "synced") { return SdkEvent.Synced } @@ -3263,6 +3375,10 @@ fun readableMapOf(sdkEvent: SdkEvent): ReadableMap? { pushToMap(map, "type", "paymentWaitingConfirmation") pushToMap(map, "details", readableMapOf(sdkEvent.details)) } + is SdkEvent.PaymentWaitingFeeAcceptance -> { + pushToMap(map, "type", "paymentWaitingFeeAcceptance") + pushToMap(map, "details", readableMapOf(sdkEvent.details)) + } is SdkEvent.Synced -> { pushToMap(map, "type", "synced") } 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 43f31b8c7..df50dea84 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 @@ -420,6 +420,42 @@ class BreezSDKLiquidModule( } } + @ReactMethod + fun fetchPaymentProposedFees( + req: ReadableMap, + promise: Promise, + ) { + executor.execute { + try { + val fetchPaymentProposedFeesRequest = + asFetchPaymentProposedFeesRequest(req) + ?: run { throw SdkException.Generic(errMissingMandatoryField("req", "FetchPaymentProposedFeesRequest")) } + val res = getBindingLiquidSdk().fetchPaymentProposedFees(fetchPaymentProposedFeesRequest) + promise.resolve(readableMapOf(res)) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + + @ReactMethod + fun acceptPaymentProposedFees( + req: ReadableMap, + promise: Promise, + ) { + executor.execute { + try { + val acceptPaymentProposedFeesRequest = + asAcceptPaymentProposedFeesRequest(req) + ?: run { throw SdkException.Generic(errMissingMandatoryField("req", "AcceptPaymentProposedFeesRequest")) } + getBindingLiquidSdk().acceptPaymentProposedFees(acceptPaymentProposedFeesRequest) + promise.resolve(readableMapOf("status" to "ok")) + } catch (e: Exception) { + promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e) + } + } + } + @ReactMethod fun listRefundables(promise: Promise) { executor.execute { diff --git a/packages/react-native/ios/BreezSDKLiquidMapper.swift b/packages/react-native/ios/BreezSDKLiquidMapper.swift index 6e85f25f1..64d718167 100644 --- a/packages/react-native/ios/BreezSDKLiquidMapper.swift +++ b/packages/react-native/ios/BreezSDKLiquidMapper.swift @@ -2,6 +2,38 @@ import BreezSDKLiquid import Foundation enum BreezSDKLiquidMapper { + static func asAcceptPaymentProposedFeesRequest(acceptPaymentProposedFeesRequest: [String: Any?]) throws -> AcceptPaymentProposedFeesRequest { + guard let responseTmp = acceptPaymentProposedFeesRequest["response"] as? [String: Any?] else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "response", typeName: "AcceptPaymentProposedFeesRequest")) + } + let response = try asFetchPaymentProposedFeesResponse(fetchPaymentProposedFeesResponse: responseTmp) + + return AcceptPaymentProposedFeesRequest(response: response) + } + + static func dictionaryOf(acceptPaymentProposedFeesRequest: AcceptPaymentProposedFeesRequest) -> [String: Any?] { + return [ + "response": dictionaryOf(fetchPaymentProposedFeesResponse: acceptPaymentProposedFeesRequest.response), + ] + } + + static func asAcceptPaymentProposedFeesRequestList(arr: [Any]) throws -> [AcceptPaymentProposedFeesRequest] { + var list = [AcceptPaymentProposedFeesRequest]() + for value in arr { + if let val = value as? [String: Any?] { + var acceptPaymentProposedFeesRequest = try asAcceptPaymentProposedFeesRequest(acceptPaymentProposedFeesRequest: val) + list.append(acceptPaymentProposedFeesRequest) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "AcceptPaymentProposedFeesRequest")) + } + } + return list + } + + static func arrayOf(acceptPaymentProposedFeesRequestList: [AcceptPaymentProposedFeesRequest]) -> [Any] { + return acceptPaymentProposedFeesRequestList.map { v -> [String: Any?] in return dictionaryOf(acceptPaymentProposedFeesRequest: v) } + } + static func asAesSuccessActionData(aesSuccessActionData: [String: Any?]) throws -> AesSuccessActionData { guard let description = aesSuccessActionData["description"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "description", typeName: "AesSuccessActionData")) @@ -307,9 +339,6 @@ enum BreezSDKLiquidMapper { guard let zeroConfMinFeeRateMsat = config["zeroConfMinFeeRateMsat"] as? UInt32 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "zeroConfMinFeeRateMsat", typeName: "Config")) } - guard let syncServiceUrl = config["syncServiceUrl"] as? String else { - throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "syncServiceUrl", typeName: "Config")) - } var breezApiKey: String? if hasNonNilKey(data: config, key: "breezApiKey") { guard let breezApiKeyTmp = config["breezApiKey"] as? String else { @@ -339,7 +368,15 @@ enum BreezSDKLiquidMapper { externalInputParsers = try asExternalInputParserList(arr: externalInputParsersTmp) } - return Config(liquidElectrumUrl: liquidElectrumUrl, bitcoinElectrumUrl: bitcoinElectrumUrl, mempoolspaceUrl: mempoolspaceUrl, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, zeroConfMinFeeRateMsat: zeroConfMinFeeRateMsat, syncServiceUrl: syncServiceUrl, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers) + var onchainFeeRateLeewaySatPerVbyte: UInt32? + if hasNonNilKey(data: config, key: "onchainFeeRateLeewaySatPerVbyte") { + guard let onchainFeeRateLeewaySatPerVbyteTmp = config["onchainFeeRateLeewaySatPerVbyte"] as? UInt32 else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "onchainFeeRateLeewaySatPerVbyte")) + } + onchainFeeRateLeewaySatPerVbyte = onchainFeeRateLeewaySatPerVbyteTmp + } + + return Config(liquidElectrumUrl: liquidElectrumUrl, bitcoinElectrumUrl: bitcoinElectrumUrl, mempoolspaceUrl: mempoolspaceUrl, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, zeroConfMinFeeRateMsat: zeroConfMinFeeRateMsat, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers, onchainFeeRateLeewaySatPerVbyte: onchainFeeRateLeewaySatPerVbyte) } static func dictionaryOf(config: Config) -> [String: Any?] { @@ -351,12 +388,12 @@ enum BreezSDKLiquidMapper { "network": valueOf(liquidNetwork: config.network), "paymentTimeoutSec": config.paymentTimeoutSec, "zeroConfMinFeeRateMsat": config.zeroConfMinFeeRateMsat, - "syncServiceUrl": 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!), + "onchainFeeRateLeewaySatPerVbyte": config.onchainFeeRateLeewaySatPerVbyte == nil ? nil : config.onchainFeeRateLeewaySatPerVbyte, ] } @@ -551,6 +588,76 @@ enum BreezSDKLiquidMapper { return externalInputParserList.map { v -> [String: Any?] in return dictionaryOf(externalInputParser: v) } } + static func asFetchPaymentProposedFeesRequest(fetchPaymentProposedFeesRequest: [String: Any?]) throws -> FetchPaymentProposedFeesRequest { + guard let swapId = fetchPaymentProposedFeesRequest["swapId"] as? String else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "swapId", typeName: "FetchPaymentProposedFeesRequest")) + } + + return FetchPaymentProposedFeesRequest(swapId: swapId) + } + + static func dictionaryOf(fetchPaymentProposedFeesRequest: FetchPaymentProposedFeesRequest) -> [String: Any?] { + return [ + "swapId": fetchPaymentProposedFeesRequest.swapId, + ] + } + + static func asFetchPaymentProposedFeesRequestList(arr: [Any]) throws -> [FetchPaymentProposedFeesRequest] { + var list = [FetchPaymentProposedFeesRequest]() + for value in arr { + if let val = value as? [String: Any?] { + var fetchPaymentProposedFeesRequest = try asFetchPaymentProposedFeesRequest(fetchPaymentProposedFeesRequest: val) + list.append(fetchPaymentProposedFeesRequest) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "FetchPaymentProposedFeesRequest")) + } + } + return list + } + + static func arrayOf(fetchPaymentProposedFeesRequestList: [FetchPaymentProposedFeesRequest]) -> [Any] { + return fetchPaymentProposedFeesRequestList.map { v -> [String: Any?] in return dictionaryOf(fetchPaymentProposedFeesRequest: v) } + } + + static func asFetchPaymentProposedFeesResponse(fetchPaymentProposedFeesResponse: [String: Any?]) throws -> FetchPaymentProposedFeesResponse { + guard let swapId = fetchPaymentProposedFeesResponse["swapId"] as? String else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "swapId", typeName: "FetchPaymentProposedFeesResponse")) + } + guard let feesSat = fetchPaymentProposedFeesResponse["feesSat"] as? UInt64 else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "feesSat", typeName: "FetchPaymentProposedFeesResponse")) + } + guard let payerAmountSat = fetchPaymentProposedFeesResponse["payerAmountSat"] as? UInt64 else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "payerAmountSat", typeName: "FetchPaymentProposedFeesResponse")) + } + + return FetchPaymentProposedFeesResponse(swapId: swapId, feesSat: feesSat, payerAmountSat: payerAmountSat) + } + + static func dictionaryOf(fetchPaymentProposedFeesResponse: FetchPaymentProposedFeesResponse) -> [String: Any?] { + return [ + "swapId": fetchPaymentProposedFeesResponse.swapId, + "feesSat": fetchPaymentProposedFeesResponse.feesSat, + "payerAmountSat": fetchPaymentProposedFeesResponse.payerAmountSat, + ] + } + + static func asFetchPaymentProposedFeesResponseList(arr: [Any]) throws -> [FetchPaymentProposedFeesResponse] { + var list = [FetchPaymentProposedFeesResponse]() + for value in arr { + if let val = value as? [String: Any?] { + var fetchPaymentProposedFeesResponse = try asFetchPaymentProposedFeesResponse(fetchPaymentProposedFeesResponse: val) + list.append(fetchPaymentProposedFeesResponse) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "FetchPaymentProposedFeesResponse")) + } + } + return list + } + + static func arrayOf(fetchPaymentProposedFeesResponseList: [FetchPaymentProposedFeesResponse]) -> [Any] { + return fetchPaymentProposedFeesResponseList.map { v -> [String: Any?] in return dictionaryOf(fetchPaymentProposedFeesResponse: v) } + } + static func asFiatCurrency(fiatCurrency: [String: Any?]) throws -> FiatCurrency { guard let id = fiatCurrency["id"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "id", typeName: "FiatCurrency")) @@ -956,11 +1063,6 @@ enum BreezSDKLiquidMapper { filters = try asPaymentTypeList(arr: filtersTmp) } - var states: [PaymentState]? - if let statesTmp = listPaymentsRequest["states"] as? [String] { - states = try asPaymentStateList(arr: statesTmp) - } - var fromTimestamp: Int64? if hasNonNilKey(data: listPaymentsRequest, key: "fromTimestamp") { guard let fromTimestampTmp = listPaymentsRequest["fromTimestamp"] as? Int64 else { @@ -994,13 +1096,12 @@ enum BreezSDKLiquidMapper { details = try asListPaymentDetails(listPaymentDetails: detailsTmp) } - return ListPaymentsRequest(filters: filters, states: states, fromTimestamp: fromTimestamp, toTimestamp: toTimestamp, offset: offset, limit: limit, details: details) + return ListPaymentsRequest(filters: filters, fromTimestamp: fromTimestamp, toTimestamp: toTimestamp, offset: offset, limit: limit, details: details) } static func dictionaryOf(listPaymentsRequest: ListPaymentsRequest) -> [String: Any?] { return [ "filters": listPaymentsRequest.filters == nil ? nil : arrayOf(paymentTypeList: listPaymentsRequest.filters!), - "states": listPaymentsRequest.states == nil ? nil : arrayOf(paymentStateList: listPaymentsRequest.states!), "fromTimestamp": listPaymentsRequest.fromTimestamp == nil ? nil : listPaymentsRequest.fromTimestamp, "toTimestamp": listPaymentsRequest.toTimestamp == nil ? nil : listPaymentsRequest.toTimestamp, "offset": listPaymentsRequest.offset == nil ? nil : listPaymentsRequest.offset, @@ -1135,85 +1236,6 @@ enum BreezSDKLiquidMapper { return lnUrlErrorDataList.map { v -> [String: Any?] in return dictionaryOf(lnUrlErrorData: v) } } - static func asLnUrlInfo(lnUrlInfo: [String: Any?]) throws -> LnUrlInfo { - var lnAddress: String? - if hasNonNilKey(data: lnUrlInfo, key: "lnAddress") { - guard let lnAddressTmp = lnUrlInfo["lnAddress"] as? String else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnAddress")) - } - lnAddress = lnAddressTmp - } - var lnurlPayComment: String? - if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayComment") { - guard let lnurlPayCommentTmp = lnUrlInfo["lnurlPayComment"] as? String else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayComment")) - } - lnurlPayComment = lnurlPayCommentTmp - } - var lnurlPayDomain: String? - if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayDomain") { - guard let lnurlPayDomainTmp = lnUrlInfo["lnurlPayDomain"] as? String else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayDomain")) - } - lnurlPayDomain = lnurlPayDomainTmp - } - var lnurlPayMetadata: String? - if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayMetadata") { - guard let lnurlPayMetadataTmp = lnUrlInfo["lnurlPayMetadata"] as? String else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayMetadata")) - } - lnurlPayMetadata = lnurlPayMetadataTmp - } - var lnurlPaySuccessAction: SuccessActionProcessed? - if let lnurlPaySuccessActionTmp = lnUrlInfo["lnurlPaySuccessAction"] as? [String: Any?] { - lnurlPaySuccessAction = try asSuccessActionProcessed(successActionProcessed: lnurlPaySuccessActionTmp) - } - - var lnurlPayUnprocessedSuccessAction: SuccessAction? - if let lnurlPayUnprocessedSuccessActionTmp = lnUrlInfo["lnurlPayUnprocessedSuccessAction"] as? [String: Any?] { - lnurlPayUnprocessedSuccessAction = try asSuccessAction(successAction: lnurlPayUnprocessedSuccessActionTmp) - } - - var lnurlWithdrawEndpoint: String? - if hasNonNilKey(data: lnUrlInfo, key: "lnurlWithdrawEndpoint") { - guard let lnurlWithdrawEndpointTmp = lnUrlInfo["lnurlWithdrawEndpoint"] as? String else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlWithdrawEndpoint")) - } - lnurlWithdrawEndpoint = lnurlWithdrawEndpointTmp - } - - return LnUrlInfo(lnAddress: lnAddress, lnurlPayComment: lnurlPayComment, lnurlPayDomain: lnurlPayDomain, lnurlPayMetadata: lnurlPayMetadata, lnurlPaySuccessAction: lnurlPaySuccessAction, lnurlPayUnprocessedSuccessAction: lnurlPayUnprocessedSuccessAction, lnurlWithdrawEndpoint: lnurlWithdrawEndpoint) - } - - static func dictionaryOf(lnUrlInfo: LnUrlInfo) -> [String: Any?] { - return [ - "lnAddress": lnUrlInfo.lnAddress == nil ? nil : lnUrlInfo.lnAddress, - "lnurlPayComment": lnUrlInfo.lnurlPayComment == nil ? nil : lnUrlInfo.lnurlPayComment, - "lnurlPayDomain": lnUrlInfo.lnurlPayDomain == nil ? nil : lnUrlInfo.lnurlPayDomain, - "lnurlPayMetadata": lnUrlInfo.lnurlPayMetadata == nil ? nil : lnUrlInfo.lnurlPayMetadata, - "lnurlPaySuccessAction": lnUrlInfo.lnurlPaySuccessAction == nil ? nil : dictionaryOf(successActionProcessed: lnUrlInfo.lnurlPaySuccessAction!), - "lnurlPayUnprocessedSuccessAction": lnUrlInfo.lnurlPayUnprocessedSuccessAction == nil ? nil : dictionaryOf(successAction: lnUrlInfo.lnurlPayUnprocessedSuccessAction!), - "lnurlWithdrawEndpoint": lnUrlInfo.lnurlWithdrawEndpoint == nil ? nil : lnUrlInfo.lnurlWithdrawEndpoint, - ] - } - - static func asLnUrlInfoList(arr: [Any]) throws -> [LnUrlInfo] { - var list = [LnUrlInfo]() - for value in arr { - if let val = value as? [String: Any?] { - var lnUrlInfo = try asLnUrlInfo(lnUrlInfo: val) - list.append(lnUrlInfo) - } else { - throw SdkError.Generic(message: errUnexpectedType(typeName: "LnUrlInfo")) - } - } - return list - } - - static func arrayOf(lnUrlInfoList: [LnUrlInfo]) -> [Any] { - return lnUrlInfoList.map { v -> [String: Any?] in return dictionaryOf(lnUrlInfo: v) } - } - static func asLnUrlPayErrorData(lnUrlPayErrorData: [String: Any?]) throws -> LnUrlPayErrorData { guard let paymentHash = lnUrlPayErrorData["paymentHash"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentHash", typeName: "LnUrlPayErrorData")) @@ -1962,32 +1984,18 @@ enum BreezSDKLiquidMapper { guard let feesSat = prepareLnUrlPayResponse["feesSat"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "feesSat", typeName: "PrepareLnUrlPayResponse")) } - guard let dataTmp = prepareLnUrlPayResponse["data"] as? [String: Any?] else { - throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "data", typeName: "PrepareLnUrlPayResponse")) - } - let data = try asLnUrlPayRequestData(lnUrlPayRequestData: dataTmp) - - var comment: String? - if hasNonNilKey(data: prepareLnUrlPayResponse, key: "comment") { - guard let commentTmp = prepareLnUrlPayResponse["comment"] as? String else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "comment")) - } - comment = commentTmp - } var successAction: SuccessAction? if let successActionTmp = prepareLnUrlPayResponse["successAction"] as? [String: Any?] { successAction = try asSuccessAction(successAction: successActionTmp) } - return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, data: data, comment: comment, successAction: successAction) + return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, successAction: successAction) } static func dictionaryOf(prepareLnUrlPayResponse: PrepareLnUrlPayResponse) -> [String: Any?] { return [ "destination": dictionaryOf(sendDestination: prepareLnUrlPayResponse.destination), "feesSat": prepareLnUrlPayResponse.feesSat, - "data": dictionaryOf(lnUrlPayRequestData: prepareLnUrlPayResponse.data), - "comment": prepareLnUrlPayResponse.comment == nil ? nil : prepareLnUrlPayResponse.comment, "successAction": prepareLnUrlPayResponse.successAction == nil ? nil : dictionaryOf(successAction: prepareLnUrlPayResponse.successAction!), ] } @@ -3764,16 +3772,11 @@ enum BreezSDKLiquidMapper { let _paymentHash = paymentDetails["paymentHash"] as? String - var _lnurlInfo: LnUrlInfo? - if let lnurlInfoTmp = paymentDetails["lnurlInfo"] as? [String: Any?] { - _lnurlInfo = try asLnUrlInfo(lnUrlInfo: lnurlInfoTmp) - } - let _refundTxId = paymentDetails["refundTxId"] as? String let _refundTxAmountSat = paymentDetails["refundTxAmountSat"] as? UInt64 - return PaymentDetails.lightning(swapId: _swapId, description: _description, preimage: _preimage, bolt11: _bolt11, bolt12Offer: _bolt12Offer, paymentHash: _paymentHash, lnurlInfo: _lnurlInfo, refundTxId: _refundTxId, refundTxAmountSat: _refundTxAmountSat) + return PaymentDetails.lightning(swapId: _swapId, description: _description, preimage: _preimage, bolt11: _bolt11, bolt12Offer: _bolt12Offer, paymentHash: _paymentHash, refundTxId: _refundTxId, refundTxAmountSat: _refundTxAmountSat) } if type == "liquid" { guard let _destination = paymentDetails["destination"] as? String else { @@ -3804,7 +3807,7 @@ enum BreezSDKLiquidMapper { static func dictionaryOf(paymentDetails: PaymentDetails) -> [String: Any?] { switch paymentDetails { case let .lightning( - swapId, description, preimage, bolt11, bolt12Offer, paymentHash, lnurlInfo, refundTxId, refundTxAmountSat + swapId, description, preimage, bolt11, bolt12Offer, paymentHash, refundTxId, refundTxAmountSat ): return [ "type": "lightning", @@ -3814,7 +3817,6 @@ enum BreezSDKLiquidMapper { "bolt11": bolt11 == nil ? nil : bolt11, "bolt12Offer": bolt12Offer == nil ? nil : bolt12Offer, "paymentHash": paymentHash == nil ? nil : paymentHash, - "lnurlInfo": lnurlInfo == nil ? nil : dictionaryOf(lnUrlInfo: lnurlInfo!), "refundTxId": refundTxId == nil ? nil : refundTxId, "refundTxAmountSat": refundTxAmountSat == nil ? nil : refundTxAmountSat, ] @@ -3926,6 +3928,9 @@ enum BreezSDKLiquidMapper { case "refundPending": return PaymentState.refundPending + case "waitingFeeAcceptance": + return PaymentState.waitingFeeAcceptance + default: throw SdkError.Generic(message: "Invalid variant \(paymentState) for enum PaymentState") } } @@ -3952,6 +3957,9 @@ enum BreezSDKLiquidMapper { case .refundPending: return "refundPending" + + case .waitingFeeAcceptance: + return "waitingFeeAcceptance" } } @@ -4061,6 +4069,14 @@ enum BreezSDKLiquidMapper { return SdkEvent.paymentWaitingConfirmation(details: _details) } + if type == "paymentWaitingFeeAcceptance" { + guard let detailsTmp = sdkEvent["details"] as? [String: Any?] else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "details", typeName: "SdkEvent")) + } + let _details = try asPayment(payment: detailsTmp) + + return SdkEvent.paymentWaitingFeeAcceptance(details: _details) + } if type == "synced" { return SdkEvent.synced } @@ -4118,6 +4134,14 @@ enum BreezSDKLiquidMapper { "details": dictionaryOf(payment: details), ] + case let .paymentWaitingFeeAcceptance( + details + ): + return [ + "type": "paymentWaitingFeeAcceptance", + "details": dictionaryOf(payment: details), + ] + case .synced: return [ "type": "synced", diff --git a/packages/react-native/ios/RNBreezSDKLiquid.m b/packages/react-native/ios/RNBreezSDKLiquid.m index b9640d45b..7eed3b1e3 100644 --- a/packages/react-native/ios/RNBreezSDKLiquid.m +++ b/packages/react-native/ios/RNBreezSDKLiquid.m @@ -131,6 +131,18 @@ @interface RCT_EXTERN_MODULE(RNBreezSDKLiquid, RCTEventEmitter) reject: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + fetchPaymentProposedFees: (NSDictionary*)req + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD( + acceptPaymentProposedFees: (NSDictionary*)req + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( listRefundables: (RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject diff --git a/packages/react-native/ios/RNBreezSDKLiquid.swift b/packages/react-native/ios/RNBreezSDKLiquid.swift index 7dc002004..3775f5c3a 100644 --- a/packages/react-native/ios/RNBreezSDKLiquid.swift +++ b/packages/react-native/ios/RNBreezSDKLiquid.swift @@ -321,6 +321,28 @@ class RNBreezSDKLiquid: RCTEventEmitter { } } + @objc(fetchPaymentProposedFees:resolve:reject:) + func fetchPaymentProposedFees(_ req: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + let fetchPaymentProposedFeesRequest = try BreezSDKLiquidMapper.asFetchPaymentProposedFeesRequest(fetchPaymentProposedFeesRequest: req) + var res = try getBindingLiquidSdk().fetchPaymentProposedFees(req: fetchPaymentProposedFeesRequest) + resolve(BreezSDKLiquidMapper.dictionaryOf(fetchPaymentProposedFeesResponse: res)) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + + @objc(acceptPaymentProposedFees:resolve:reject:) + func acceptPaymentProposedFees(_ req: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + do { + let acceptPaymentProposedFeesRequest = try BreezSDKLiquidMapper.asAcceptPaymentProposedFeesRequest(acceptPaymentProposedFeesRequest: req) + try getBindingLiquidSdk().acceptPaymentProposedFees(req: acceptPaymentProposedFeesRequest) + resolve(["status": "ok"]) + } catch let err { + rejectErr(err: err, reject: reject) + } + } + @objc(listRefundables:reject:) func listRefundables(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { do { diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index c7b48b8c5..e02b9e236 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -19,6 +19,10 @@ const BreezSDKLiquid = NativeModules.RNBreezSDKLiquid const BreezSDKLiquidEmitter = new NativeEventEmitter(BreezSDKLiquid) +export interface AcceptPaymentProposedFeesRequest { + response: FetchPaymentProposedFeesResponse +} + export interface AesSuccessActionData { description: string ciphertext: string @@ -71,6 +75,7 @@ export interface Config { zeroConfMaxAmountSat?: number useDefaultExternalInputParsers: boolean externalInputParsers?: ExternalInputParser[] + onchainFeeRateLeewaySatPerVbyte?: number } export interface ConnectRequest { @@ -98,6 +103,16 @@ export interface ExternalInputParser { parserUrl: string } +export interface FetchPaymentProposedFeesRequest { + swapId: string +} + +export interface FetchPaymentProposedFeesResponse { + swapId: string + feesSat: number + payerAmountSat: number +} + export interface FiatCurrency { id: string info: CurrencyInfo @@ -648,7 +663,8 @@ export enum PaymentState { FAILED = "failed", TIMED_OUT = "timedOut", REFUNDABLE = "refundable", - REFUND_PENDING = "refundPending" + REFUND_PENDING = "refundPending", + WAITING_FEE_ACCEPTANCE = "waitingFeeAcceptance" } export enum PaymentType { @@ -663,6 +679,7 @@ export enum SdkEventVariant { PAYMENT_REFUND_PENDING = "paymentRefundPending", PAYMENT_SUCCEEDED = "paymentSucceeded", PAYMENT_WAITING_CONFIRMATION = "paymentWaitingConfirmation", + PAYMENT_WAITING_FEE_ACCEPTANCE = "paymentWaitingFeeAcceptance", SYNCED = "synced" } @@ -684,6 +701,9 @@ export type SdkEvent = { } | { type: SdkEventVariant.PAYMENT_WAITING_CONFIRMATION, details: Payment +} | { + type: SdkEventVariant.PAYMENT_WAITING_FEE_ACCEPTANCE, + details: Payment } | { type: SdkEventVariant.SYNCED } @@ -861,6 +881,15 @@ export const getPayment = async (req: GetPaymentRequest): Promise => { + const response = await BreezSDKLiquid.fetchPaymentProposedFees(req) + return response +} + +export const acceptPaymentProposedFees = async (req: AcceptPaymentProposedFeesRequest): Promise => { + await BreezSDKLiquid.acceptPaymentProposedFees(req) +} + export const listRefundables = async (): Promise => { const response = await BreezSDKLiquid.listRefundables() return response From 0394a66d4c43f38fb86a6c294c058a00883f9f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Tue, 24 Dec 2024 12:41:50 +0000 Subject: [PATCH 03/20] Fix after rebase --- lib/core/src/chain_swap.rs | 30 ++- lib/core/src/model.rs | 10 + lib/core/src/persist/chain.rs | 6 +- lib/core/src/sdk.rs | 34 ++- lib/core/src/sync/mod.rs | 1 + packages/dart/lib/src/frb_generated.dart | 204 ++++++++++++++++-- .../ios/BreezSDKLiquidMapper.swift | 119 +++++++++- 7 files changed, 362 insertions(+), 42 deletions(-) diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index d2edd9fd8..03a612324 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -101,9 +101,12 @@ impl ChainSwapHandler { .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?; match swap_state { - // If the swap is not local (pulled from real-time sync) we do not claim twice + // If the swap is not local (pulled from real-time sync) we do not: + // - claim twice + // - accept fees twice ChainSwapStates::TransactionServerMempool - | ChainSwapStates::TransactionServerConfirmed => { + | ChainSwapStates::TransactionServerConfirmed + | ChainSwapStates::TransactionLockupFailed => { log::debug!("Received {swap_state:?} for non-local Chain swap {id} from status stream, skipping update."); return Ok(()); } @@ -378,11 +381,13 @@ impl ChainSwapHandler { } async fn handle_amountless_update(&self, swap: &ChainSwap) -> Result<(), PaymentError> { + let id = swap.id.clone(); + let quote = self .swapper - .get_zero_amount_chain_swap_quote(&swap.id) + .get_zero_amount_chain_swap_quote(&id) .map(|quote| quote.to_sat())?; - info!("Got quote of {quote} sat for swap {}", &swap.id); + info!("Got quote of {quote} sat for swap {}", &id); match self.validate_amountless_swap(swap, quote).await? { ValidateAmountlessSwapResult::ReadyForAccepting { @@ -391,12 +396,12 @@ impl ChainSwapHandler { } => { debug!("Zero-amount swap validated. Auto-accepting..."); self.persister.update_zero_amount_swap_values( - &swap.id, + &id, user_lockup_amount_sat, receiver_amount_sat, )?; self.swapper - .accept_zero_amount_chain_swap_quote(&swap.id, quote) + .accept_zero_amount_chain_swap_quote(&id, quote) .map_err(Into::into) } ValidateAmountlessSwapResult::RequiresUserAction { @@ -406,12 +411,15 @@ impl ChainSwapHandler { debug!("Zero-amount swap validated. Fees are too high for automatic accepting. Moving to WaitingFeeAcceptance"); // While the user doesn't accept new fees, let's continue to show the original estimate self.persister.update_zero_amount_swap_values( - &swap.id, + &id, user_lockup_amount_sat, receiver_amount_sat_original_estimate, )?; - self.update_swap_info(&swap.id, WaitingFeeAcceptance, None, None, None, None) - .await + self.update_swap_info(&ChainSwapUpdate { + swap_id: id, + to_state: WaitingFeeAcceptance, + ..Default::default() + }) } } } @@ -942,9 +950,9 @@ impl ChainSwapHandler { let id = &swap.id; ensure_sdk!( - swap.state == Refundable, + swap.state.is_refundable(), PaymentError::Generic { - err: format!("Chain Swap {id} was not marked as `Refundable`") + err: format!("Chain Swap {id} was not in refundable state") } ); diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 43c5bc6a8..2e74efa2b 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -1,3 +1,4 @@ +use std::cmp::PartialEq; use std::path::PathBuf; use anyhow::{anyhow, Result}; @@ -1169,6 +1170,15 @@ impl FromSql for PaymentState { } } +impl PaymentState { + pub(crate) fn is_refundable(&self) -> bool { + matches!( + self, + PaymentState::Refundable | PaymentState::WaitingFeeAcceptance + ) + } +} + #[derive(Debug, Copy, Clone, Eq, EnumString, Display, Hash, PartialEq, Serialize)] #[strum(serialize_all = "lowercase")] pub enum PaymentType { diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index 24968ec97..77c6e0451 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -240,7 +240,11 @@ impl Persister { } pub(crate) fn list_pending_chain_swaps(&self) -> Result> { - self.list_chain_swaps_by_state(vec![PaymentState::Pending, PaymentState::RefundPending, PaymentState::WaitingFeeAcceptance]) + self.list_chain_swaps_by_state(vec![ + PaymentState::Pending, + PaymentState::RefundPending, + PaymentState::WaitingFeeAcceptance, + ]) } pub(crate) fn list_refundable_chain_swaps(&self) -> Result> { diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 799869176..aa6e607a4 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -2512,7 +2512,11 @@ impl LiquidSdk { let mut pending_send_sat = 0; let mut pending_receive_sat = 0; let payments = self.persister.get_payments(&ListPaymentsRequest { - states: Some(vec![PaymentState::Pending, PaymentState::RefundPending]), + states: Some(vec![ + PaymentState::Pending, + PaymentState::RefundPending, + PaymentState::WaitingFeeAcceptance, + ]), ..Default::default() })?; @@ -2581,6 +2585,13 @@ impl LiquidSdk { err: format!("Could not find Swap {}", req.swap_id), })?; + ensure_sdk!( + chain_swap.state == WaitingFeeAcceptance, + SdkError::Generic { + err: "Payment is not WaitingFeeAcceptance".to_string() + } + ); + let server_lockup_quote = self .swapper .get_zero_amount_chain_swap_quote(&req.swap_id)?; @@ -2615,6 +2626,13 @@ impl LiquidSdk { err: format!("Could not find Swap {}", swap_id), })?; + ensure_sdk!( + chain_swap.state == WaitingFeeAcceptance, + PaymentError::Generic { + err: "Payment is not WaitingFeeAcceptance".to_string() + } + ); + let server_lockup_quote = self.swapper.get_zero_amount_chain_swap_quote(&swap_id)?; ensure_sdk!( @@ -2629,9 +2647,11 @@ impl LiquidSdk { )?; self.swapper .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())?; - self.chain_swap_handler - .update_swap_info(&swap_id, Pending, None, None, None, None) - .await + self.chain_swap_handler.update_swap_info(&ChainSwapUpdate { + swap_id, + to_state: Pending, + ..Default::default() + }) } /// Empties the Liquid Wallet cache for the [Config::network]. @@ -3572,8 +3592,7 @@ mod tests { async fn test_zero_amount_chain_swap_zero_leeway() -> Result<()> { let user_lockup_sat = 50_000; - let (_tmp_dir, persister) = new_persister()?; - let persister = Arc::new(persister); + create_persister!(persister); let swapper = Arc::new(MockSwapper::new()); let status_stream = Arc::new(MockStatusStream::new()); let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); @@ -3637,8 +3656,7 @@ mod tests { let user_lockup_sat = 50_000; let onchain_fee_rate_leeway_sat_per_vbyte = 5; - let (_tmp_dir, persister) = new_persister()?; - let persister = Arc::new(persister); + create_persister!(persister); let swapper = Arc::new(MockSwapper::new()); let status_stream = Arc::new(MockStatusStream::new()); let liquid_chain_service = Arc::new(Mutex::new(MockLiquidChainService::new())); diff --git a/lib/core/src/sync/mod.rs b/lib/core/src/sync/mod.rs index e32511a4d..c01889e31 100644 --- a/lib/core/src/sync/mod.rs +++ b/lib/core/src/sync/mod.rs @@ -570,6 +570,7 @@ mod tests { None, true, None, + false, ))?; sync_service.push().await?; diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 248a59fb7..798674ed6 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -1476,6 +1476,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_ln_url_error_data(raw); } + @protected + LnUrlInfo dco_decode_box_autoadd_ln_url_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_ln_url_info(raw); + } + @protected LnUrlPayErrorData dco_decode_box_autoadd_ln_url_pay_error_data(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1693,7 +1699,7 @@ 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 != 13) throw Exception('unexpected arr length: expect 13 but see ${arr.length}'); + if (arr.length != 14) throw Exception('unexpected arr length: expect 14 but see ${arr.length}'); return Config( liquidElectrumUrl: dco_decode_String(arr[0]), bitcoinElectrumUrl: dco_decode_String(arr[1]), @@ -1703,11 +1709,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { network: dco_decode_liquid_network(arr[5]), paymentTimeoutSec: dco_decode_u_64(arr[6]), zeroConfMinFeeRateMsat: dco_decode_u_32(arr[7]), - zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[8]), - breezApiKey: dco_decode_opt_String(arr[9]), - externalInputParsers: dco_decode_opt_list_external_input_parser(arr[10]), - useDefaultExternalInputParsers: dco_decode_bool(arr[11]), - onchainFeeRateLeewaySatPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[12]), + syncServiceUrl: dco_decode_String(arr[8]), + zeroConfMaxAmountSat: dco_decode_opt_box_autoadd_u_64(arr[9]), + breezApiKey: dco_decode_opt_String(arr[10]), + externalInputParsers: dco_decode_opt_list_external_input_parser(arr[11]), + useDefaultExternalInputParsers: dco_decode_bool(arr[12]), + onchainFeeRateLeewaySatPerVbyte: dco_decode_opt_box_autoadd_u_32(arr[13]), ); } @@ -1958,6 +1965,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + List dco_decode_list_payment_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_payment_state).toList(); + } + @protected List dco_decode_list_payment_type(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1968,14 +1981,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ListPaymentsRequest dco_decode_list_payments_request(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 6) throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); return ListPaymentsRequest( filters: dco_decode_opt_list_payment_type(arr[0]), - fromTimestamp: dco_decode_opt_box_autoadd_i_64(arr[1]), - toTimestamp: dco_decode_opt_box_autoadd_i_64(arr[2]), - offset: dco_decode_opt_box_autoadd_u_32(arr[3]), - limit: dco_decode_opt_box_autoadd_u_32(arr[4]), - details: dco_decode_opt_box_autoadd_list_payment_details(arr[5]), + states: dco_decode_opt_list_payment_state(arr[1]), + fromTimestamp: dco_decode_opt_box_autoadd_i_64(arr[2]), + toTimestamp: dco_decode_opt_box_autoadd_i_64(arr[3]), + offset: dco_decode_opt_box_autoadd_u_32(arr[4]), + limit: dco_decode_opt_box_autoadd_u_32(arr[5]), + details: dco_decode_opt_box_autoadd_list_payment_details(arr[6]), ); } @@ -2116,6 +2130,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + LnUrlInfo dco_decode_ln_url_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 7) throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); + return LnUrlInfo( + lnAddress: dco_decode_opt_String(arr[0]), + lnurlPayComment: dco_decode_opt_String(arr[1]), + lnurlPayDomain: dco_decode_opt_String(arr[2]), + lnurlPayMetadata: dco_decode_opt_String(arr[3]), + lnurlPaySuccessAction: dco_decode_opt_box_autoadd_success_action_processed(arr[4]), + lnurlPayUnprocessedSuccessAction: dco_decode_opt_box_autoadd_success_action(arr[5]), + lnurlWithdrawEndpoint: dco_decode_opt_String(arr[6]), + ); + } + @protected LnUrlPayError dco_decode_ln_url_pay_error(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2429,6 +2459,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_list_payment_details(raw); } + @protected + LnUrlInfo? dco_decode_opt_box_autoadd_ln_url_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_ln_url_info(raw); + } + @protected PayAmount? dco_decode_opt_box_autoadd_pay_amount(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2477,6 +2513,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_list_external_input_parser(raw); } + @protected + List? dco_decode_opt_list_payment_state(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_list_payment_state(raw); + } + @protected List? dco_decode_opt_list_payment_type(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2540,8 +2582,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: dco_decode_opt_String(raw[4]), bolt12Offer: dco_decode_opt_String(raw[5]), paymentHash: dco_decode_opt_String(raw[6]), - refundTxId: dco_decode_opt_String(raw[7]), - refundTxAmountSat: dco_decode_opt_box_autoadd_u_64(raw[8]), + lnurlInfo: dco_decode_opt_box_autoadd_ln_url_info(raw[7]), + refundTxId: dco_decode_opt_String(raw[8]), + refundTxAmountSat: dco_decode_opt_box_autoadd_u_64(raw[9]), ); case 1: return PaymentDetails_Liquid( @@ -2690,11 +2733,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { PrepareLnUrlPayResponse dco_decode_prepare_ln_url_pay_response(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + if (arr.length != 5) throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); return PrepareLnUrlPayResponse( destination: dco_decode_send_destination(arr[0]), feesSat: dco_decode_u_64(arr[1]), - successAction: dco_decode_opt_box_autoadd_success_action(arr[2]), + data: dco_decode_ln_url_pay_request_data(arr[2]), + comment: dco_decode_opt_String(arr[3]), + successAction: dco_decode_opt_box_autoadd_success_action(arr[4]), ); } @@ -3403,6 +3448,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_ln_url_error_data(deserializer)); } + @protected + LnUrlInfo sse_decode_box_autoadd_ln_url_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_ln_url_info(deserializer)); + } + @protected LnUrlPayErrorData sse_decode_box_autoadd_ln_url_pay_error_data(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3619,6 +3670,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_network = sse_decode_liquid_network(deserializer); var var_paymentTimeoutSec = sse_decode_u_64(deserializer); var var_zeroConfMinFeeRateMsat = sse_decode_u_32(deserializer); + var var_syncServiceUrl = sse_decode_String(deserializer); var var_zeroConfMaxAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); var var_breezApiKey = sse_decode_opt_String(deserializer); var var_externalInputParsers = sse_decode_opt_list_external_input_parser(deserializer); @@ -3633,6 +3685,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { network: var_network, paymentTimeoutSec: var_paymentTimeoutSec, zeroConfMinFeeRateMsat: var_zeroConfMinFeeRateMsat, + syncServiceUrl: var_syncServiceUrl, zeroConfMaxAmountSat: var_zeroConfMaxAmountSat, breezApiKey: var_breezApiKey, externalInputParsers: var_externalInputParsers, @@ -3918,6 +3971,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + List sse_decode_list_payment_state(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_payment_state(deserializer)); + } + return ans_; + } + @protected List sse_decode_list_payment_type(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -3934,6 +3999,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ListPaymentsRequest sse_decode_list_payments_request(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs var var_filters = sse_decode_opt_list_payment_type(deserializer); + var var_states = sse_decode_opt_list_payment_state(deserializer); var var_fromTimestamp = sse_decode_opt_box_autoadd_i_64(deserializer); var var_toTimestamp = sse_decode_opt_box_autoadd_i_64(deserializer); var var_offset = sse_decode_opt_box_autoadd_u_32(deserializer); @@ -3941,6 +4007,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_details = sse_decode_opt_box_autoadd_list_payment_details(deserializer); return ListPaymentsRequest( filters: var_filters, + states: var_states, fromTimestamp: var_fromTimestamp, toTimestamp: var_toTimestamp, offset: var_offset, @@ -4115,6 +4182,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return LnUrlErrorData(reason: var_reason); } + @protected + LnUrlInfo sse_decode_ln_url_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_lnAddress = sse_decode_opt_String(deserializer); + var var_lnurlPayComment = sse_decode_opt_String(deserializer); + var var_lnurlPayDomain = sse_decode_opt_String(deserializer); + var var_lnurlPayMetadata = sse_decode_opt_String(deserializer); + var var_lnurlPaySuccessAction = sse_decode_opt_box_autoadd_success_action_processed(deserializer); + var var_lnurlPayUnprocessedSuccessAction = sse_decode_opt_box_autoadd_success_action(deserializer); + var var_lnurlWithdrawEndpoint = sse_decode_opt_String(deserializer); + return LnUrlInfo( + lnAddress: var_lnAddress, + lnurlPayComment: var_lnurlPayComment, + lnurlPayDomain: var_lnurlPayDomain, + lnurlPayMetadata: var_lnurlPayMetadata, + lnurlPaySuccessAction: var_lnurlPaySuccessAction, + lnurlPayUnprocessedSuccessAction: var_lnurlPayUnprocessedSuccessAction, + lnurlWithdrawEndpoint: var_lnurlWithdrawEndpoint); + } + @protected LnUrlPayError sse_decode_ln_url_pay_error(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4422,6 +4509,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + LnUrlInfo? sse_decode_opt_box_autoadd_ln_url_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_ln_url_info(deserializer)); + } else { + return null; + } + } + @protected PayAmount? sse_decode_opt_box_autoadd_pay_amount(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4510,6 +4608,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + List? sse_decode_opt_list_payment_state(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_list_payment_state(deserializer)); + } else { + return null; + } + } + @protected List? sse_decode_opt_list_payment_type(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4584,6 +4693,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_bolt11 = sse_decode_opt_String(deserializer); var var_bolt12Offer = sse_decode_opt_String(deserializer); var var_paymentHash = sse_decode_opt_String(deserializer); + var var_lnurlInfo = sse_decode_opt_box_autoadd_ln_url_info(deserializer); var var_refundTxId = sse_decode_opt_String(deserializer); var var_refundTxAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); return PaymentDetails_Lightning( @@ -4593,6 +4703,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: var_bolt11, bolt12Offer: var_bolt12Offer, paymentHash: var_paymentHash, + lnurlInfo: var_lnurlInfo, refundTxId: var_refundTxId, refundTxAmountSat: var_refundTxAmountSat); case 1: @@ -4735,9 +4846,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs var var_destination = sse_decode_send_destination(deserializer); var var_feesSat = sse_decode_u_64(deserializer); + var var_data = sse_decode_ln_url_pay_request_data(deserializer); + var var_comment = sse_decode_opt_String(deserializer); var var_successAction = sse_decode_opt_box_autoadd_success_action(deserializer); return PrepareLnUrlPayResponse( - destination: var_destination, feesSat: var_feesSat, successAction: var_successAction); + destination: var_destination, + feesSat: var_feesSat, + data: var_data, + comment: var_comment, + successAction: var_successAction); } @protected @@ -5489,6 +5606,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_ln_url_error_data(self, serializer); } + @protected + void sse_encode_box_autoadd_ln_url_info(LnUrlInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_ln_url_info(self, serializer); + } + @protected void sse_encode_box_autoadd_ln_url_pay_error_data(LnUrlPayErrorData self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5708,6 +5831,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_liquid_network(self.network, serializer); sse_encode_u_64(self.paymentTimeoutSec, serializer); sse_encode_u_32(self.zeroConfMinFeeRateMsat, serializer); + sse_encode_String(self.syncServiceUrl, serializer); sse_encode_opt_box_autoadd_u_64(self.zeroConfMaxAmountSat, serializer); sse_encode_opt_String(self.breezApiKey, serializer); sse_encode_opt_list_external_input_parser(self.externalInputParsers, serializer); @@ -5939,6 +6063,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_payment_state(List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_payment_state(item, serializer); + } + } + @protected void sse_encode_list_payment_type(List self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5952,6 +6085,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { void sse_encode_list_payments_request(ListPaymentsRequest self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_opt_list_payment_type(self.filters, serializer); + sse_encode_opt_list_payment_state(self.states, serializer); sse_encode_opt_box_autoadd_i_64(self.fromTimestamp, serializer); sse_encode_opt_box_autoadd_i_64(self.toTimestamp, serializer); sse_encode_opt_box_autoadd_u_32(self.offset, serializer); @@ -6085,6 +6219,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(self.reason, serializer); } + @protected + void sse_encode_ln_url_info(LnUrlInfo self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_String(self.lnAddress, serializer); + sse_encode_opt_String(self.lnurlPayComment, serializer); + sse_encode_opt_String(self.lnurlPayDomain, serializer); + sse_encode_opt_String(self.lnurlPayMetadata, serializer); + sse_encode_opt_box_autoadd_success_action_processed(self.lnurlPaySuccessAction, serializer); + sse_encode_opt_box_autoadd_success_action(self.lnurlPayUnprocessedSuccessAction, serializer); + sse_encode_opt_String(self.lnurlWithdrawEndpoint, serializer); + } + @protected void sse_encode_ln_url_pay_error(LnUrlPayError self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6352,6 +6498,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_ln_url_info(LnUrlInfo? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_ln_url_info(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_pay_amount(PayAmount? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6433,6 +6589,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_list_payment_state(List? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_list_payment_state(self, serializer); + } + } + @protected void sse_encode_opt_list_payment_type(List? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6490,6 +6656,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { bolt11: final bolt11, bolt12Offer: final bolt12Offer, paymentHash: final paymentHash, + lnurlInfo: final lnurlInfo, refundTxId: final refundTxId, refundTxAmountSat: final refundTxAmountSat ): @@ -6500,6 +6667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_opt_String(bolt11, serializer); sse_encode_opt_String(bolt12Offer, serializer); sse_encode_opt_String(paymentHash, serializer); + sse_encode_opt_box_autoadd_ln_url_info(lnurlInfo, serializer); sse_encode_opt_String(refundTxId, serializer); sse_encode_opt_box_autoadd_u_64(refundTxAmountSat, serializer); case PaymentDetails_Liquid(destination: final destination, description: final description): @@ -6631,6 +6799,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_send_destination(self.destination, serializer); sse_encode_u_64(self.feesSat, serializer); + sse_encode_ln_url_pay_request_data(self.data, serializer); + sse_encode_opt_String(self.comment, serializer); sse_encode_opt_box_autoadd_success_action(self.successAction, serializer); } diff --git a/packages/react-native/ios/BreezSDKLiquidMapper.swift b/packages/react-native/ios/BreezSDKLiquidMapper.swift index 64d718167..6c3446933 100644 --- a/packages/react-native/ios/BreezSDKLiquidMapper.swift +++ b/packages/react-native/ios/BreezSDKLiquidMapper.swift @@ -339,6 +339,9 @@ enum BreezSDKLiquidMapper { guard let zeroConfMinFeeRateMsat = config["zeroConfMinFeeRateMsat"] as? UInt32 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "zeroConfMinFeeRateMsat", typeName: "Config")) } + guard let syncServiceUrl = config["syncServiceUrl"] as? String else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "syncServiceUrl", typeName: "Config")) + } var breezApiKey: String? if hasNonNilKey(data: config, key: "breezApiKey") { guard let breezApiKeyTmp = config["breezApiKey"] as? String else { @@ -376,7 +379,7 @@ enum BreezSDKLiquidMapper { onchainFeeRateLeewaySatPerVbyte = onchainFeeRateLeewaySatPerVbyteTmp } - return Config(liquidElectrumUrl: liquidElectrumUrl, bitcoinElectrumUrl: bitcoinElectrumUrl, mempoolspaceUrl: mempoolspaceUrl, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, zeroConfMinFeeRateMsat: zeroConfMinFeeRateMsat, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers, onchainFeeRateLeewaySatPerVbyte: onchainFeeRateLeewaySatPerVbyte) + return Config(liquidElectrumUrl: liquidElectrumUrl, bitcoinElectrumUrl: bitcoinElectrumUrl, mempoolspaceUrl: mempoolspaceUrl, workingDir: workingDir, network: network, paymentTimeoutSec: paymentTimeoutSec, zeroConfMinFeeRateMsat: zeroConfMinFeeRateMsat, syncServiceUrl: syncServiceUrl, breezApiKey: breezApiKey, cacheDir: cacheDir, zeroConfMaxAmountSat: zeroConfMaxAmountSat, useDefaultExternalInputParsers: useDefaultExternalInputParsers, externalInputParsers: externalInputParsers, onchainFeeRateLeewaySatPerVbyte: onchainFeeRateLeewaySatPerVbyte) } static func dictionaryOf(config: Config) -> [String: Any?] { @@ -388,6 +391,7 @@ enum BreezSDKLiquidMapper { "network": valueOf(liquidNetwork: config.network), "paymentTimeoutSec": config.paymentTimeoutSec, "zeroConfMinFeeRateMsat": config.zeroConfMinFeeRateMsat, + "syncServiceUrl": config.syncServiceUrl, "breezApiKey": config.breezApiKey == nil ? nil : config.breezApiKey, "cacheDir": config.cacheDir == nil ? nil : config.cacheDir, "zeroConfMaxAmountSat": config.zeroConfMaxAmountSat == nil ? nil : config.zeroConfMaxAmountSat, @@ -1063,6 +1067,11 @@ enum BreezSDKLiquidMapper { filters = try asPaymentTypeList(arr: filtersTmp) } + var states: [PaymentState]? + if let statesTmp = listPaymentsRequest["states"] as? [String] { + states = try asPaymentStateList(arr: statesTmp) + } + var fromTimestamp: Int64? if hasNonNilKey(data: listPaymentsRequest, key: "fromTimestamp") { guard let fromTimestampTmp = listPaymentsRequest["fromTimestamp"] as? Int64 else { @@ -1096,12 +1105,13 @@ enum BreezSDKLiquidMapper { details = try asListPaymentDetails(listPaymentDetails: detailsTmp) } - return ListPaymentsRequest(filters: filters, fromTimestamp: fromTimestamp, toTimestamp: toTimestamp, offset: offset, limit: limit, details: details) + return ListPaymentsRequest(filters: filters, states: states, fromTimestamp: fromTimestamp, toTimestamp: toTimestamp, offset: offset, limit: limit, details: details) } static func dictionaryOf(listPaymentsRequest: ListPaymentsRequest) -> [String: Any?] { return [ "filters": listPaymentsRequest.filters == nil ? nil : arrayOf(paymentTypeList: listPaymentsRequest.filters!), + "states": listPaymentsRequest.states == nil ? nil : arrayOf(paymentStateList: listPaymentsRequest.states!), "fromTimestamp": listPaymentsRequest.fromTimestamp == nil ? nil : listPaymentsRequest.fromTimestamp, "toTimestamp": listPaymentsRequest.toTimestamp == nil ? nil : listPaymentsRequest.toTimestamp, "offset": listPaymentsRequest.offset == nil ? nil : listPaymentsRequest.offset, @@ -1236,6 +1246,85 @@ enum BreezSDKLiquidMapper { return lnUrlErrorDataList.map { v -> [String: Any?] in return dictionaryOf(lnUrlErrorData: v) } } + static func asLnUrlInfo(lnUrlInfo: [String: Any?]) throws -> LnUrlInfo { + var lnAddress: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnAddress") { + guard let lnAddressTmp = lnUrlInfo["lnAddress"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnAddress")) + } + lnAddress = lnAddressTmp + } + var lnurlPayComment: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayComment") { + guard let lnurlPayCommentTmp = lnUrlInfo["lnurlPayComment"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayComment")) + } + lnurlPayComment = lnurlPayCommentTmp + } + var lnurlPayDomain: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayDomain") { + guard let lnurlPayDomainTmp = lnUrlInfo["lnurlPayDomain"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayDomain")) + } + lnurlPayDomain = lnurlPayDomainTmp + } + var lnurlPayMetadata: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlPayMetadata") { + guard let lnurlPayMetadataTmp = lnUrlInfo["lnurlPayMetadata"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlPayMetadata")) + } + lnurlPayMetadata = lnurlPayMetadataTmp + } + var lnurlPaySuccessAction: SuccessActionProcessed? + if let lnurlPaySuccessActionTmp = lnUrlInfo["lnurlPaySuccessAction"] as? [String: Any?] { + lnurlPaySuccessAction = try asSuccessActionProcessed(successActionProcessed: lnurlPaySuccessActionTmp) + } + + var lnurlPayUnprocessedSuccessAction: SuccessAction? + if let lnurlPayUnprocessedSuccessActionTmp = lnUrlInfo["lnurlPayUnprocessedSuccessAction"] as? [String: Any?] { + lnurlPayUnprocessedSuccessAction = try asSuccessAction(successAction: lnurlPayUnprocessedSuccessActionTmp) + } + + var lnurlWithdrawEndpoint: String? + if hasNonNilKey(data: lnUrlInfo, key: "lnurlWithdrawEndpoint") { + guard let lnurlWithdrawEndpointTmp = lnUrlInfo["lnurlWithdrawEndpoint"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "lnurlWithdrawEndpoint")) + } + lnurlWithdrawEndpoint = lnurlWithdrawEndpointTmp + } + + return LnUrlInfo(lnAddress: lnAddress, lnurlPayComment: lnurlPayComment, lnurlPayDomain: lnurlPayDomain, lnurlPayMetadata: lnurlPayMetadata, lnurlPaySuccessAction: lnurlPaySuccessAction, lnurlPayUnprocessedSuccessAction: lnurlPayUnprocessedSuccessAction, lnurlWithdrawEndpoint: lnurlWithdrawEndpoint) + } + + static func dictionaryOf(lnUrlInfo: LnUrlInfo) -> [String: Any?] { + return [ + "lnAddress": lnUrlInfo.lnAddress == nil ? nil : lnUrlInfo.lnAddress, + "lnurlPayComment": lnUrlInfo.lnurlPayComment == nil ? nil : lnUrlInfo.lnurlPayComment, + "lnurlPayDomain": lnUrlInfo.lnurlPayDomain == nil ? nil : lnUrlInfo.lnurlPayDomain, + "lnurlPayMetadata": lnUrlInfo.lnurlPayMetadata == nil ? nil : lnUrlInfo.lnurlPayMetadata, + "lnurlPaySuccessAction": lnUrlInfo.lnurlPaySuccessAction == nil ? nil : dictionaryOf(successActionProcessed: lnUrlInfo.lnurlPaySuccessAction!), + "lnurlPayUnprocessedSuccessAction": lnUrlInfo.lnurlPayUnprocessedSuccessAction == nil ? nil : dictionaryOf(successAction: lnUrlInfo.lnurlPayUnprocessedSuccessAction!), + "lnurlWithdrawEndpoint": lnUrlInfo.lnurlWithdrawEndpoint == nil ? nil : lnUrlInfo.lnurlWithdrawEndpoint, + ] + } + + static func asLnUrlInfoList(arr: [Any]) throws -> [LnUrlInfo] { + var list = [LnUrlInfo]() + for value in arr { + if let val = value as? [String: Any?] { + var lnUrlInfo = try asLnUrlInfo(lnUrlInfo: val) + list.append(lnUrlInfo) + } else { + throw SdkError.Generic(message: errUnexpectedType(typeName: "LnUrlInfo")) + } + } + return list + } + + static func arrayOf(lnUrlInfoList: [LnUrlInfo]) -> [Any] { + return lnUrlInfoList.map { v -> [String: Any?] in return dictionaryOf(lnUrlInfo: v) } + } + static func asLnUrlPayErrorData(lnUrlPayErrorData: [String: Any?]) throws -> LnUrlPayErrorData { guard let paymentHash = lnUrlPayErrorData["paymentHash"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentHash", typeName: "LnUrlPayErrorData")) @@ -1984,18 +2073,32 @@ enum BreezSDKLiquidMapper { guard let feesSat = prepareLnUrlPayResponse["feesSat"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "feesSat", typeName: "PrepareLnUrlPayResponse")) } + guard let dataTmp = prepareLnUrlPayResponse["data"] as? [String: Any?] else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "data", typeName: "PrepareLnUrlPayResponse")) + } + let data = try asLnUrlPayRequestData(lnUrlPayRequestData: dataTmp) + + var comment: String? + if hasNonNilKey(data: prepareLnUrlPayResponse, key: "comment") { + guard let commentTmp = prepareLnUrlPayResponse["comment"] as? String else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "comment")) + } + comment = commentTmp + } var successAction: SuccessAction? if let successActionTmp = prepareLnUrlPayResponse["successAction"] as? [String: Any?] { successAction = try asSuccessAction(successAction: successActionTmp) } - return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, successAction: successAction) + return PrepareLnUrlPayResponse(destination: destination, feesSat: feesSat, data: data, comment: comment, successAction: successAction) } static func dictionaryOf(prepareLnUrlPayResponse: PrepareLnUrlPayResponse) -> [String: Any?] { return [ "destination": dictionaryOf(sendDestination: prepareLnUrlPayResponse.destination), "feesSat": prepareLnUrlPayResponse.feesSat, + "data": dictionaryOf(lnUrlPayRequestData: prepareLnUrlPayResponse.data), + "comment": prepareLnUrlPayResponse.comment == nil ? nil : prepareLnUrlPayResponse.comment, "successAction": prepareLnUrlPayResponse.successAction == nil ? nil : dictionaryOf(successAction: prepareLnUrlPayResponse.successAction!), ] } @@ -3772,11 +3875,16 @@ enum BreezSDKLiquidMapper { let _paymentHash = paymentDetails["paymentHash"] as? String + var _lnurlInfo: LnUrlInfo? + if let lnurlInfoTmp = paymentDetails["lnurlInfo"] as? [String: Any?] { + _lnurlInfo = try asLnUrlInfo(lnUrlInfo: lnurlInfoTmp) + } + let _refundTxId = paymentDetails["refundTxId"] as? String let _refundTxAmountSat = paymentDetails["refundTxAmountSat"] as? UInt64 - return PaymentDetails.lightning(swapId: _swapId, description: _description, preimage: _preimage, bolt11: _bolt11, bolt12Offer: _bolt12Offer, paymentHash: _paymentHash, refundTxId: _refundTxId, refundTxAmountSat: _refundTxAmountSat) + return PaymentDetails.lightning(swapId: _swapId, description: _description, preimage: _preimage, bolt11: _bolt11, bolt12Offer: _bolt12Offer, paymentHash: _paymentHash, lnurlInfo: _lnurlInfo, refundTxId: _refundTxId, refundTxAmountSat: _refundTxAmountSat) } if type == "liquid" { guard let _destination = paymentDetails["destination"] as? String else { @@ -3807,7 +3915,7 @@ enum BreezSDKLiquidMapper { static func dictionaryOf(paymentDetails: PaymentDetails) -> [String: Any?] { switch paymentDetails { case let .lightning( - swapId, description, preimage, bolt11, bolt12Offer, paymentHash, refundTxId, refundTxAmountSat + swapId, description, preimage, bolt11, bolt12Offer, paymentHash, lnurlInfo, refundTxId, refundTxAmountSat ): return [ "type": "lightning", @@ -3817,6 +3925,7 @@ enum BreezSDKLiquidMapper { "bolt11": bolt11 == nil ? nil : bolt11, "bolt12Offer": bolt12Offer == nil ? nil : bolt12Offer, "paymentHash": paymentHash == nil ? nil : paymentHash, + "lnurlInfo": lnurlInfo == nil ? nil : dictionaryOf(lnUrlInfo: lnurlInfo!), "refundTxId": refundTxId == nil ? nil : refundTxId, "refundTxAmountSat": refundTxAmountSat == nil ? nil : refundTxAmountSat, ] From d2be2ea49aae1b954ff0ab4c3078563e230823a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Thu, 26 Dec 2024 10:52:08 +0000 Subject: [PATCH 04/20] Prevent recovering state from WaitingFeeAcceptance to Pending --- lib/core/src/recover/recoverer.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs index 9efc3c681..7eca6437c 100644 --- a/lib/core/src/recover/recoverer.rs +++ b/lib/core/src/recover/recoverer.rs @@ -12,6 +12,8 @@ use lwk_wollet::hashes::{sha256, Hash as _}; use lwk_wollet::WalletTx; use tokio::sync::Mutex; +use super::model::*; +use crate::model::PaymentState; use crate::prelude::{Direction, Swap}; use crate::wallet::OnchainWallet; use crate::{ @@ -19,8 +21,6 @@ use crate::{ recover::model::{BtcScript, HistoryTxId, LBtcScript}, }; -use super::model::*; - pub(crate) struct Recoverer { master_blinding_key: MasterBlindingKey, onchain_wallet: Arc, @@ -215,10 +215,15 @@ impl Recoverer { }; let is_expired = bitcoin_height >= chain_swap.timeout_block_height; let min_lockup_amount_sat = chain_swap.payer_amount_sat; + let is_waiting_fee_acceptance = + chain_swap.state == PaymentState::WaitingFeeAcceptance; if let Some(new_state) = recovered_data.derive_partial_state(min_lockup_amount_sat, is_expired) { - chain_swap.state = new_state; + // When local state is WaitingFeeAcceptance do not change to Pending + if !(new_state == PaymentState::Pending && is_waiting_fee_acceptance) { + chain_swap.state = new_state; + } } chain_swap.server_lockup_tx_id = recovered_data .lbtc_server_lockup_tx_id From a1e5576286e2408ed1ca43d36a11aa61f4db9026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Mon, 30 Dec 2024 13:56:01 +0000 Subject: [PATCH 05/20] Store actual payer and accepted receiver amounts --- .../include/breez_sdk_liquidFFI.h | 10 +++ lib/core/src/chain_swap.rs | 57 ++++++++--------- lib/core/src/model.rs | 6 ++ lib/core/src/persist/chain.rs | 58 +++++++++++------ lib/core/src/persist/migrations.rs | 2 + lib/core/src/persist/mod.rs | 28 ++++++-- lib/core/src/recover/model.rs | 6 +- lib/core/src/recover/recoverer.rs | 23 ++++--- lib/core/src/sdk.rs | 24 ++++--- lib/core/src/sync/model/data.rs | 4 ++ lib/core/src/sync/model/mod.rs | 2 +- lib/core/src/test_utils/chain_swap.rs | 6 ++ lib/core/src/test_utils/sync.rs | 1 + ...utter_breez_liquid_bindings_generated.dart | 64 +++++++++++++++++++ 14 files changed, 217 insertions(+), 74 deletions(-) diff --git a/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h b/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h index 0f02f4e88..61bb48c5c 100644 --- a/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h +++ b/lib/bindings/langs/flutter/breez_sdk_liquidFFI/include/breez_sdk_liquidFFI.h @@ -65,6 +65,8 @@ typedef void (*UniFfiRustFutureContinuation)(void * _Nonnull, int8_t); // Scaffolding functions void uniffi_breez_sdk_liquid_bindings_fn_free_bindingliquidsdk(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status ); +void uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_accept_payment_proposed_fees(void*_Nonnull ptr, RustBuffer req, RustCallStatus *_Nonnull out_status +); RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_add_event_listener(void*_Nonnull ptr, uint64_t listener, RustCallStatus *_Nonnull out_status ); void uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_backup(void*_Nonnull ptr, RustBuffer req, RustCallStatus *_Nonnull out_status @@ -81,6 +83,8 @@ RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_lig ); RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_onchain_limits(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status ); +RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_payment_proposed_fees(void*_Nonnull ptr, RustBuffer req, RustCallStatus *_Nonnull out_status +); RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_get_info(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status ); RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_get_payment(void*_Nonnull ptr, RustBuffer req, RustCallStatus *_Nonnull out_status @@ -279,6 +283,9 @@ uint16_t uniffi_breez_sdk_liquid_bindings_checksum_func_parse_invoice(void ); uint16_t uniffi_breez_sdk_liquid_bindings_checksum_func_set_logger(void +); +uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_accept_payment_proposed_fees(void + ); uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_add_event_listener(void @@ -303,6 +310,9 @@ uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch ); uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_onchain_limits(void +); +uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_payment_proposed_fees(void + ); uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_get_info(void diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index 03a612324..bcc21fcda 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -101,12 +101,9 @@ impl ChainSwapHandler { .map_err(|_| anyhow!("Invalid ChainSwapState for Chain Swap {id}: {status}"))?; match swap_state { - // If the swap is not local (pulled from real-time sync) we do not: - // - claim twice - // - accept fees twice + // If the swap is not local (pulled from real-time sync) we do not claim twice ChainSwapStates::TransactionServerMempool - | ChainSwapStates::TransactionServerConfirmed - | ChainSwapStates::TransactionLockupFailed => { + | ChainSwapStates::TransactionServerConfirmed => { log::debug!("Received {swap_state:?} for non-local Chain swap {id} from status stream, skipping update."); return Ok(()); } @@ -331,7 +328,7 @@ impl ChainSwapHandler { | ChainSwapStates::TransactionRefunded | ChainSwapStates::SwapExpired => { // Zero-amount Receive Chain Swaps also get to TransactionLockupFailed when user locks up funds - let is_zero_amount = swap.get_boltz_create_response()?.lockup_details.amount == 0; + let is_zero_amount = swap.payer_amount_sat == 0; if matches!(swap_state, ChainSwapStates::TransactionLockupFailed) && is_zero_amount { match self.handle_amountless_update(swap).await { @@ -395,26 +392,19 @@ impl ChainSwapHandler { receiver_amount_sat, } => { debug!("Zero-amount swap validated. Auto-accepting..."); - self.persister.update_zero_amount_swap_values( - &id, - user_lockup_amount_sat, - receiver_amount_sat, - )?; + self.persister + .update_actual_payer_amount(&id, user_lockup_amount_sat)?; self.swapper - .accept_zero_amount_chain_swap_quote(&id, quote) - .map_err(Into::into) + .accept_zero_amount_chain_swap_quote(&id, quote)?; + self.persister + .update_accepted_receiver_amount(&id, receiver_amount_sat) } ValidateAmountlessSwapResult::RequiresUserAction { user_lockup_amount_sat, - receiver_amount_sat_original_estimate, } => { debug!("Zero-amount swap validated. Fees are too high for automatic accepting. Moving to WaitingFeeAcceptance"); - // While the user doesn't accept new fees, let's continue to show the original estimate - self.persister.update_zero_amount_swap_values( - &id, - user_lockup_amount_sat, - receiver_amount_sat_original_estimate, - )?; + self.persister + .update_actual_payer_amount(&id, user_lockup_amount_sat)?; self.update_swap_info(&ChainSwapUpdate { swap_id: id, to_state: WaitingFeeAcceptance, @@ -486,11 +476,8 @@ impl ChainSwapHandler { ); if min_auto_accept_server_lockup_amount_sat > quote_server_lockup_amount_sat { - let receiver_amount_sat_original_estimate = - server_lockup_amount_estimate_sat - swap.claim_fees_sat; Ok(ValidateAmountlessSwapResult::RequiresUserAction { user_lockup_amount_sat, - receiver_amount_sat_original_estimate, }) } else { let receiver_amount_sat = quote_server_lockup_amount_sat - swap.claim_fees_sat; @@ -1233,12 +1220,25 @@ impl ChainSwapHandler { .unblind(&secp, liquid_swap_script.blinding_key.secret_key())? .value; } - if value < claim_details.amount { - return Err(anyhow!( - "Transaction value {value} sats is less than {} sats", - claim_details.amount - )); + match chain_swap.accepted_receiver_amount_sat { + None => { + if value < claim_details.amount { + return Err(anyhow!( + "Transaction value {value} sats is less than {} sats", + claim_details.amount + )); + } + } + Some(accepted_receiver_amount_sat) => { + if value < accepted_receiver_amount_sat - chain_swap.claim_fees_sat { + return Err(anyhow!( + "Transaction value {value} sats is less than accepted {} sats", + claim_details.amount + )); + } + } } + Ok(()) } @@ -1362,7 +1362,6 @@ enum ValidateAmountlessSwapResult { }, RequiresUserAction { user_lockup_amount_sat: u64, - receiver_amount_sat_original_estimate: u64, }, } diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 2e74efa2b..df4abb667 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -729,8 +729,14 @@ pub(crate) struct ChainSwap { pub(crate) timeout_block_height: u32, pub(crate) preimage: String, pub(crate) description: Option, + /// Payer amount defined at swap creation pub(crate) payer_amount_sat: u64, + /// The actual payer amount in case it differs from `payer_amount_sat` (over/underpayment) + pub(crate) actual_payer_amount_sat: Option, + /// Receiver amount defined at swap creation pub(crate) receiver_amount_sat: u64, + /// The final receiver amount, in case of an over/underpayment that has been accepted + pub(crate) accepted_receiver_amount_sat: Option, pub(crate) claim_fees_sat: u64, /// The [ChainPair] chosen on swap creation pub(crate) pair_fees_json: String, diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index 77c6e0451..3d1c0a2c1 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -147,7 +147,9 @@ impl Persister { refund_tx_id, created_at, state, - pair_fees_json + pair_fees_json, + actual_payer_amount_sat, + accepted_receiver_amount_sat FROM chain_swaps {where_clause_str} ORDER BY created_at @@ -197,6 +199,8 @@ impl Persister { created_at: row.get(18)?, state: row.get(19)?, pair_fees_json: row.get(20)?, + actual_payer_amount_sat: row.get(21)?, + accepted_receiver_amount_sat: row.get(22)?, }) } @@ -286,39 +290,55 @@ impl Persister { Ok(()) } - /// Used for Zero-amount Receive Chain swaps, when we fetched the quote and we know how much - /// the sender locked up - pub(crate) fn update_zero_amount_swap_values( + /// Used for receive chain swaps, when the sender over/underpays + pub(crate) fn update_actual_payer_amount( &self, swap_id: &str, - payer_amount_sat: u64, - receiver_amount_sat: u64, + actual_payer_amount_sat: u64, ) -> Result<(), PaymentError> { - log::info!("Updating chain swap {swap_id}: payer_amount_sat = {payer_amount_sat}, receiver_amount_sat = {receiver_amount_sat}"); + log::info!( + "Updating chain swap {swap_id}: actual_payer_amount_sat = {actual_payer_amount_sat}" + ); + let con: Connection = self.get_connection()?; + con.execute( + "UPDATE chain_swaps + SET actual_payer_amount_sat = :actual_payer_amount_sat + WHERE id = :id", + named_params! { + ":id": swap_id, + ":actual_payer_amount_sat": actual_payer_amount_sat, + }, + )?; + + Ok(()) + } + + /// Used for receive chain swaps, when fees are accepted and thus the agreed received amount is known + pub(crate) fn update_accepted_receiver_amount( + &self, + swap_id: &str, + accepted_receiver_amount_sat: u64, + ) -> Result<(), PaymentError> { + log::info!( + "Updating chain swap {swap_id}: accepted_receiver_amount_sat = {accepted_receiver_amount_sat}" + ); let mut con: Connection = self.get_connection()?; let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; tx.execute( - "UPDATE chain_swaps - SET - payer_amount_sat = :payer_amount_sat, - receiver_amount_sat = :receiver_amount_sat - WHERE - id = :id", + "UPDATE chain_swaps + SET accepted_receiver_amount_sat = :accepted_receiver_amount_sat + WHERE id = :id", named_params! { ":id": swap_id, - ":payer_amount_sat": payer_amount_sat, - ":receiver_amount_sat": receiver_amount_sat, + ":accepted_receiver_amount_sat": accepted_receiver_amount_sat, }, )?; self.commit_outgoing( &tx, swap_id, RecordType::Chain, - Some(vec![ - "payer_amount_sat".to_string(), - "receiver_amount_sat".to_string(), - ]), + Some(vec!["accepted_receiver_amount_sat".to_string()]), )?; tx.commit()?; self.sync_trigger diff --git a/lib/core/src/persist/migrations.rs b/lib/core/src/persist/migrations.rs index 965b983d5..87a772d29 100644 --- a/lib/core/src/persist/migrations.rs +++ b/lib/core/src/persist/migrations.rs @@ -215,5 +215,7 @@ pub(crate) fn current_migrations() -> Vec<&'static str> { "ALTER TABLE receive_swaps DROP COLUMN mrh_script_pubkey;", "ALTER TABLE payment_details ADD COLUMN lnurl_info_json TEXT;", "ALTER TABLE payment_tx_data ADD COLUMN unblinding_data TEXT;", + "ALTER TABLE chain_swaps ADD COLUMN actual_payer_amount_sat INTEGER;", + "ALTER TABLE chain_swaps ADD COLUMN accepted_receiver_amount_sat INTEGER;", ] } diff --git a/lib/core/src/persist/mod.rs b/lib/core/src/persist/mod.rs index c9998dbd0..85ddc628f 100644 --- a/lib/core/src/persist/mod.rs +++ b/lib/core/src/persist/mod.rs @@ -400,6 +400,8 @@ impl Persister { cs.claim_address, cs.state, cs.pair_fees_json, + cs.actual_payer_amount_sat, + cs.accepted_receiver_amount_sat, rtx.amount_sat, pd.destination, pd.description, @@ -497,12 +499,14 @@ impl Persister { let maybe_chain_swap_pair_fees_json: Option = row.get(39)?; let maybe_chain_swap_pair_fees: Option = maybe_chain_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok()); + let maybe_chain_swap_actual_payer_amount_sat: Option = row.get(40)?; + let maybe_chain_swap_accepted_receiver_amount_sat: Option = row.get(41)?; - let maybe_swap_refund_tx_amount_sat: Option = row.get(40)?; + let maybe_swap_refund_tx_amount_sat: Option = row.get(42)?; - let maybe_payment_details_destination: Option = row.get(41)?; - let maybe_payment_details_description: Option = row.get(42)?; - let maybe_payment_details_lnurl_info_json: Option = row.get(43)?; + let maybe_payment_details_destination: Option = row.get(43)?; + let maybe_payment_details_description: Option = row.get(44)?; + let maybe_payment_details_lnurl_info_json: Option = row.get(45)?; let maybe_payment_details_lnurl_info: Option = maybe_payment_details_lnurl_info_json.and_then(|info| serde_json::from_str(&info).ok()); @@ -569,7 +573,18 @@ impl Persister { } None => match maybe_chain_swap_id { Some(chain_swap_id) => { - let payer_amount_sat = maybe_chain_swap_payer_amount_sat.unwrap_or(0); + let payer_amount_sat = match maybe_chain_swap_actual_payer_amount_sat { + Some(actual_payer_amount_sat) => actual_payer_amount_sat, + None => maybe_chain_swap_payer_amount_sat.unwrap_or(0), + }; + let receiver_amount_sat = + match maybe_chain_swap_accepted_receiver_amount_sat { + Some(accepted_receiver_amount_sat) => accepted_receiver_amount_sat, + None => match maybe_chain_swap_actual_payer_amount_sat { + Some(_) => payer_amount_sat, // For over/underpaid chain swaps WaitingFeeAcceptance, show zero fees + None => maybe_chain_swap_receiver_amount_sat.unwrap_or(0), + }, + }; let swapper_fees_sat = maybe_chain_swap_pair_fees .map(|pair| pair.fees.percentage) .map(|fr| ((fr / 100.0) * payer_amount_sat as f64).ceil() as u64) @@ -587,8 +602,7 @@ impl Persister { description: maybe_chain_swap_description .unwrap_or("Bitcoin transfer".to_string()), payer_amount_sat, - receiver_amount_sat: maybe_chain_swap_receiver_amount_sat - .unwrap_or(0), + receiver_amount_sat, swapper_fees_sat, refund_tx_id: maybe_chain_swap_refund_tx_id, refund_tx_amount_sat: maybe_swap_refund_tx_amount_sat, diff --git a/lib/core/src/recover/model.rs b/lib/core/src/recover/model.rs index bec768e4a..1747abdb3 100644 --- a/lib/core/src/recover/model.rs +++ b/lib/core/src/recover/model.rs @@ -199,6 +199,7 @@ impl RecoveredOnchainDataChainReceive { &self, min_lockup_amount_sat: u64, is_expired: bool, + is_waiting_fee_acceptance: bool, ) -> Option { let is_refundable = self.btc_user_lockup_amount_sat > 0 && (is_expired || self.btc_user_lockup_amount_sat < min_lockup_amount_sat); @@ -232,7 +233,10 @@ impl RecoveredOnchainDataChainReceive { } (None, None) => match is_refundable { true => Some(PaymentState::Refundable), - false => Some(PaymentState::Pending), + false => match is_waiting_fee_acceptance { + true => Some(PaymentState::WaitingFeeAcceptance), + false => Some(PaymentState::Pending), + }, }, }, None => match is_expired { diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs index 7eca6437c..48d3c1a0a 100644 --- a/lib/core/src/recover/recoverer.rs +++ b/lib/core/src/recover/recoverer.rs @@ -13,7 +13,6 @@ use lwk_wollet::WalletTx; use tokio::sync::Mutex; use super::model::*; -use crate::model::PaymentState; use crate::prelude::{Direction, Swap}; use crate::wallet::OnchainWallet; use crate::{ @@ -213,17 +212,23 @@ impl Recoverer { log::warn!("Could not apply recovered data for incoming Chain swap {swap_id}: recovery data not found"); continue; }; + if chain_swap.receiver_amount_sat + != recovered_data.btc_user_lockup_amount_sat + { + chain_swap.actual_payer_amount_sat = + Some(recovered_data.btc_user_lockup_amount_sat) + } let is_expired = bitcoin_height >= chain_swap.timeout_block_height; let min_lockup_amount_sat = chain_swap.payer_amount_sat; let is_waiting_fee_acceptance = - chain_swap.state == PaymentState::WaitingFeeAcceptance; - if let Some(new_state) = - recovered_data.derive_partial_state(min_lockup_amount_sat, is_expired) - { - // When local state is WaitingFeeAcceptance do not change to Pending - if !(new_state == PaymentState::Pending && is_waiting_fee_acceptance) { - chain_swap.state = new_state; - } + chain_swap.actual_payer_amount_sat.is_some() + && chain_swap.accepted_receiver_amount_sat.is_none(); + if let Some(new_state) = recovered_data.derive_partial_state( + min_lockup_amount_sat, + is_expired, + is_waiting_fee_acceptance, + ) { + chain_swap.state = new_state; } chain_swap.server_lockup_tx_id = recovered_data .lbtc_server_lockup_tx_id diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index aa6e607a4..a720bf3ac 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -1644,7 +1644,9 @@ impl LiquidSdk { preimage: preimage_str, description: Some("Bitcoin transfer".to_string()), payer_amount_sat, + actual_payer_amount_sat: None, receiver_amount_sat, + accepted_receiver_amount_sat: None, claim_fees_sat, pair_fees_json: serde_json::to_string(&pair).map_err(|e| { PaymentError::generic(&format!("Failed to serialize outgoing ChainPair: {e:?}")) @@ -2086,7 +2088,9 @@ impl LiquidSdk { preimage: preimage_str, description: Some("Bitcoin transfer".to_string()), payer_amount_sat: user_lockup_amount_sat.unwrap_or(0), + actual_payer_amount_sat: None, receiver_amount_sat, + accepted_receiver_amount_sat: None, claim_fees_sat, pair_fees_json: serde_json::to_string(&pair).map_err(|e| { PaymentError::generic(&format!("Failed to serialize incoming ChainPair: {e:?}")) @@ -2596,13 +2600,20 @@ impl LiquidSdk { .swapper .get_zero_amount_chain_swap_quote(&req.swap_id)?; - let payer_amount_sat = chain_swap.payer_amount_sat; - let fees_sat = payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat; + let actual_payer_amount_sat = + chain_swap + .actual_payer_amount_sat + .ok_or(SdkError::Generic { + err: "No actual payer amount found when state is WaitingFeeAcceptance" + .to_string(), + })?; + let fees_sat = + actual_payer_amount_sat - server_lockup_quote.to_sat() + chain_swap.claim_fees_sat; Ok(FetchPaymentProposedFeesResponse { swap_id: req.swap_id.clone(), fees_sat, - payer_amount_sat, + payer_amount_sat: actual_payer_amount_sat, }) } @@ -2640,13 +2651,10 @@ impl LiquidSdk { PaymentError::InvalidOrExpiredFees ); - self.persister.update_zero_amount_swap_values( - &swap_id, - payer_amount_sat, - payer_amount_sat - fees_sat, - )?; self.swapper .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())?; + self.persister + .update_accepted_receiver_amount(&swap_id, payer_amount_sat - fees_sat)?; self.chain_swap_handler.update_swap_info(&ChainSwapUpdate { swap_id, to_state: Pending, diff --git a/lib/core/src/sync/model/data.rs b/lib/core/src/sync/model/data.rs index c5deb62d7..bcad8478b 100644 --- a/lib/core/src/sync/model/data.rs +++ b/lib/core/src/sync/model/data.rs @@ -21,6 +21,7 @@ pub(crate) struct ChainSyncData { pub(crate) timeout_block_height: u32, pub(crate) payer_amount_sat: u64, pub(crate) receiver_amount_sat: u64, + pub(crate) accepted_receiver_amount_sat: Option, pub(crate) accept_zero_conf: bool, pub(crate) created_at: u32, pub(crate) description: Option, @@ -68,6 +69,7 @@ impl From for ChainSyncData { timeout_block_height: value.timeout_block_height, payer_amount_sat: value.payer_amount_sat, receiver_amount_sat: value.receiver_amount_sat, + accepted_receiver_amount_sat: value.accepted_receiver_amount_sat, accept_zero_conf: value.accept_zero_conf, created_at: value.created_at, description: value.description, @@ -85,7 +87,9 @@ impl From for ChainSwap { preimage: val.preimage, description: val.description, payer_amount_sat: val.payer_amount_sat, + actual_payer_amount_sat: None, receiver_amount_sat: val.receiver_amount_sat, + accepted_receiver_amount_sat: val.accepted_receiver_amount_sat, claim_fees_sat: val.claim_fees_sat, accept_zero_conf: val.accept_zero_conf, pair_fees_json: val.pair_fees_json, diff --git a/lib/core/src/sync/model/mod.rs b/lib/core/src/sync/model/mod.rs index 7038cc4e5..84e0721de 100644 --- a/lib/core/src/sync/model/mod.rs +++ b/lib/core/src/sync/model/mod.rs @@ -18,7 +18,7 @@ pub(crate) mod sync; const MESSAGE_PREFIX: &[u8; 13] = b"realtimesync:"; lazy_static! { - static ref CURRENT_SCHEMA_VERSION: Version = Version::parse("0.0.1").unwrap(); + static ref CURRENT_SCHEMA_VERSION: Version = Version::parse("0.0.2").unwrap(); } #[derive(Copy, Clone)] diff --git a/lib/core/src/test_utils/chain_swap.rs b/lib/core/src/test_utils/chain_swap.rs index 496fc30b3..affdaa04e 100644 --- a/lib/core/src/test_utils/chain_swap.rs +++ b/lib/core/src/test_utils/chain_swap.rs @@ -106,7 +106,9 @@ pub(crate) fn new_chain_swap( refund_private_key: "9e23d322577cfeb2b5490f3f86db58c806004afcb7c88995927bfdfc1c64cd8c" .to_string(), payer_amount_sat: 0, + actual_payer_amount_sat: None, receiver_amount_sat: 0, + accepted_receiver_amount_sat: None, claim_fees_sat: 144, server_lockup_tx_id: None, user_lockup_tx_id, @@ -191,7 +193,9 @@ pub(crate) fn new_chain_swap( claim_private_key: "4b04c3b95570fc48c7f33bc900b801245c2be31b90d41616477574aedc5b9d28".to_string(), refund_private_key: "9e23d322577cfeb2b5490f3f86db58c806004afcb7c88995927bfdfc1c64cd8c".to_string(), payer_amount_sat: 18360, + actual_payer_amount_sat: None, receiver_amount_sat: 17592, + accepted_receiver_amount_sat: None, claim_fees_sat: 144, server_lockup_tx_id: None, user_lockup_tx_id, @@ -273,7 +277,9 @@ pub(crate) fn new_chain_swap( claim_private_key: "7d3cbecfb76cb8eccc2c2131f3e744311d3655377fe8723d23acb55b041b2b16".to_string(), refund_private_key: "2644c60cc6cd454ea809f0e32fc2871ab7c26603e3009e1fd313ae886c137eaa".to_string(), payer_amount_sat: 25490, + actual_payer_amount_sat: None, receiver_amount_sat: 20000, + accepted_receiver_amount_sat: None, claim_fees_sat: 2109, server_lockup_tx_id: None, user_lockup_tx_id, diff --git a/lib/core/src/test_utils/sync.rs b/lib/core/src/test_utils/sync.rs index a4ab84e3a..9a7e86267 100644 --- a/lib/core/src/test_utils/sync.rs +++ b/lib/core/src/test_utils/sync.rs @@ -161,6 +161,7 @@ pub(crate) fn new_chain_sync_data(accept_zero_conf: Option) -> ChainSyncDa timeout_block_height: 0, payer_amount_sat: 0, receiver_amount_sat: 0, + accepted_receiver_amount_sat: None, accept_zero_conf: accept_zero_conf.unwrap_or(true), created_at: 0, description: None, diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index cde124523..fd0cfb0f0 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -1644,6 +1644,27 @@ class FlutterBreezLiquidBindings { _uniffi_breez_sdk_liquid_bindings_fn_free_bindingliquidsdkPtr .asFunction, ffi.Pointer)>(); + void uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_accept_payment_proposed_fees( + ffi.Pointer ptr, + RustBuffer req, + ffi.Pointer out_status, + ) { + return _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_accept_payment_proposed_fees( + ptr, + req, + out_status, + ); + } + + late final _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_accept_payment_proposed_feesPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, RustBuffer, ffi.Pointer)>>( + 'uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_accept_payment_proposed_fees'); + late final _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_accept_payment_proposed_fees = + _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_accept_payment_proposed_feesPtr + .asFunction, RustBuffer, ffi.Pointer)>(); + RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_add_event_listener( ffi.Pointer ptr, int listener, @@ -1792,6 +1813,27 @@ class FlutterBreezLiquidBindings { _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_onchain_limitsPtr .asFunction, ffi.Pointer)>(); + RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_payment_proposed_fees( + ffi.Pointer ptr, + RustBuffer req, + ffi.Pointer out_status, + ) { + return _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_payment_proposed_fees( + ptr, + req, + out_status, + ); + } + + late final _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_payment_proposed_feesPtr = + _lookup< + ffi.NativeFunction< + RustBuffer Function(ffi.Pointer, RustBuffer, ffi.Pointer)>>( + 'uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_payment_proposed_fees'); + late final _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_payment_proposed_fees = + _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_fetch_payment_proposed_feesPtr + .asFunction, RustBuffer, ffi.Pointer)>(); + RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_get_info( ffi.Pointer ptr, ffi.Pointer out_status, @@ -3421,6 +3463,17 @@ class FlutterBreezLiquidBindings { late final _uniffi_breez_sdk_liquid_bindings_checksum_func_set_logger = _uniffi_breez_sdk_liquid_bindings_checksum_func_set_loggerPtr.asFunction(); + int uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_accept_payment_proposed_fees() { + return _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_accept_payment_proposed_fees(); + } + + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_accept_payment_proposed_feesPtr = + _lookup>( + 'uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_accept_payment_proposed_fees'); + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_accept_payment_proposed_fees = + _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_accept_payment_proposed_feesPtr + .asFunction(); + int uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_add_event_listener() { return _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_add_event_listener(); } @@ -3509,6 +3562,17 @@ class FlutterBreezLiquidBindings { _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_onchain_limitsPtr .asFunction(); + int uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_payment_proposed_fees() { + return _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_payment_proposed_fees(); + } + + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_payment_proposed_feesPtr = + _lookup>( + 'uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_payment_proposed_fees'); + late final _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_payment_proposed_fees = + _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_fetch_payment_proposed_feesPtr + .asFunction(); + int uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_get_info() { return _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_get_info(); } From 74d94668049d2c572146708e7c8fbc5d5a890ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Mon, 30 Dec 2024 16:00:26 +0000 Subject: [PATCH 06/20] Fix: use lockup amount instead of lockup address balance for setting actual_payer_amount --- lib/core/src/recover/model.rs | 8 +++++--- lib/core/src/recover/recoverer.rs | 13 ++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/core/src/recover/model.rs b/lib/core/src/recover/model.rs index 1747abdb3..32f77f495 100644 --- a/lib/core/src/recover/model.rs +++ b/lib/core/src/recover/model.rs @@ -188,7 +188,9 @@ pub(crate) struct RecoveredOnchainDataChainReceive { pub(crate) lbtc_claim_address: Option, /// BTC tx initiated by the payer (the "user" as per Boltz), sending funds to the swap funding address. pub(crate) btc_user_lockup_tx_id: Option, - /// BTC total funds available at the swap funding address. + /// BTC total funds currently available at the swap funding address. + pub(crate) btc_user_lockup_address_balance_sat: u64, + /// BTC sent to lockup address as part of lockup tx. pub(crate) btc_user_lockup_amount_sat: u64, /// BTC tx initiated by the SDK to a user-chosen address, in case the initial funds have to be refunded. pub(crate) btc_refund_tx_id: Option, @@ -201,8 +203,8 @@ impl RecoveredOnchainDataChainReceive { is_expired: bool, is_waiting_fee_acceptance: bool, ) -> Option { - let is_refundable = self.btc_user_lockup_amount_sat > 0 - && (is_expired || self.btc_user_lockup_amount_sat < min_lockup_amount_sat); + let is_refundable = self.btc_user_lockup_address_balance_sat > 0 + && (is_expired || self.btc_user_lockup_amount_sat < min_lockup_amount_sat); // TODO: this does not support accepting over/underpayments match &self.btc_user_lockup_tx_id { Some(_) => match (&self.lbtc_claim_tx_id, &self.btc_refund_tx_id) { (Some(lbtc_claim_tx_id), None) => match lbtc_claim_tx_id.confirmed() { diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs index 48d3c1a0a..7ddc3459c 100644 --- a/lib/core/src/recover/recoverer.rs +++ b/lib/core/src/recover/recoverer.rs @@ -664,7 +664,7 @@ impl Recoverer { }; // Get the current confirmed amount available for the lockup script - let btc_user_lockup_amount_sat = history + let btc_user_lockup_address_balance_sat = history .btc_lockup_script_balance .map(|balance| balance.confirmed) .unwrap_or_default(); @@ -696,6 +696,16 @@ impl Recoverer { .find(|h| h.txid.as_raw_hash() == tx.txid().as_raw_hash()) }) .cloned(); + let btc_user_lockup_amount_sat = btc_lockup_incoming_txs + .first() + .and_then(|tx| { + tx.output + .iter() + .find(|out| out.script_pubkey == btc_lockup_script) + .map(|out| out.value) + }) + .unwrap_or_default() + .to_sat(); let btc_outgoing_tx_ids: Vec = btc_lockup_outgoing_txs .iter() .filter_map(|tx| { @@ -733,6 +743,7 @@ impl Recoverer { lbtc_claim_tx_id, lbtc_claim_address, btc_user_lockup_tx_id, + btc_user_lockup_address_balance_sat, btc_user_lockup_amount_sat, btc_refund_tx_id, }, From d09a9ab6d8234cc212d0f0ed889f5c2e3a135937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Mon, 30 Dec 2024 17:13:43 +0000 Subject: [PATCH 07/20] Fix missing update for new chain swap fields --- lib/core/src/persist/chain.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index 3d1c0a2c1..f7ee7ac50 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -70,7 +70,9 @@ impl Persister { claim_tx_id = :claim_tx_id, refund_tx_id = :refund_tx_id, pair_fees_json = :pair_fees_json, - state = :state + state = :state, + actual_payer_amount_sat = :actual_payer_amount_sat, + accepted_receiver_amount_sat = :accepted_receiver_amount_sat WHERE id = :id", named_params! { @@ -84,6 +86,8 @@ impl Persister { ":refund_tx_id": &chain_swap.refund_tx_id, ":pair_fees_json": &chain_swap.pair_fees_json, ":state": &chain_swap.state, + ":actual_payer_amount_sat": &chain_swap.actual_payer_amount_sat, + ":accepted_receiver_amount_sat": &chain_swap.accepted_receiver_amount_sat, }, )?; From 5198de8b4b4b0c123510a3a03b8395e00f63088e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Mon, 30 Dec 2024 18:40:25 +0000 Subject: [PATCH 08/20] Prevent accepted_receiver_amount_sat overwritting with null --- lib/core/src/persist/chain.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index f7ee7ac50..d5efe1ab5 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -72,7 +72,11 @@ impl Persister { pair_fees_json = :pair_fees_json, state = :state, actual_payer_amount_sat = :actual_payer_amount_sat, - accepted_receiver_amount_sat = :accepted_receiver_amount_sat + accepted_receiver_amount_sat = + CASE + WHEN accepted_receiver_amount_sat IS NULL THEN :accepted_receiver_amount_sat + ELSE accepted_receiver_amount_sat + END WHERE id = :id", named_params! { From cd433446c21e9cfbc800d3618c15e40ac070ed84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Tue, 31 Dec 2024 13:38:52 +0000 Subject: [PATCH 09/20] Fix recoverer over/underpayment condition --- lib/core/src/recover/recoverer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs index 7ddc3459c..2f1b549a3 100644 --- a/lib/core/src/recover/recoverer.rs +++ b/lib/core/src/recover/recoverer.rs @@ -212,8 +212,7 @@ impl Recoverer { log::warn!("Could not apply recovered data for incoming Chain swap {swap_id}: recovery data not found"); continue; }; - if chain_swap.receiver_amount_sat - != recovered_data.btc_user_lockup_amount_sat + if chain_swap.payer_amount_sat != recovered_data.btc_user_lockup_amount_sat { chain_swap.actual_payer_amount_sat = Some(recovered_data.btc_user_lockup_amount_sat) From b120e707cd04ca70d6b2c103e8be54126c425272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Tue, 31 Dec 2024 15:21:42 +0000 Subject: [PATCH 10/20] Fix server lockup tx verification --- lib/core/src/chain_swap.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index bcc21fcda..5215cb220 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -1230,10 +1230,12 @@ impl ChainSwapHandler { } } Some(accepted_receiver_amount_sat) => { - if value < accepted_receiver_amount_sat - chain_swap.claim_fees_sat { + let expected_server_lockup_amount_sat = + accepted_receiver_amount_sat + chain_swap.claim_fees_sat; + if value < expected_server_lockup_amount_sat { return Err(anyhow!( "Transaction value {value} sats is less than accepted {} sats", - claim_details.amount + expected_server_lockup_amount_sat )); } } From 0a371e087b704b5d9a70acd346c640778da94e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Mon, 30 Dec 2024 11:17:06 +0000 Subject: [PATCH 11/20] Show notification if payment requires fee acceptance --- .../Constants.kt | 8 +++ .../job/SwapUpdated.kt | 54 +++++++++++++++++-- .../Sources/BreezSDKLiquid/Constants.swift | 4 ++ .../BreezSDKLiquid/Task/SwapUpdated.swift | 21 ++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt index 1f4e92421..b125dc3a4 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/Constants.kt @@ -57,6 +57,10 @@ object Constants { "payment_sent_notification_text" const val PAYMENT_SENT_NOTIFICATION_TITLE = "payment_sent_notification_title" + const val PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = + "payment_waiting_fee_acceptance_notification_title" + const val PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT = + "payment_waiting_fee_acceptance_text" const val SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "swap_confirmed_notification_failure_text" const val SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = @@ -102,6 +106,10 @@ object Constants { "Sent %d sats" const val DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE = "Payment Sent" + const val DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = + "Payment requires fee acceptance" + const val DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT = + "Tap to review updated fees" const val DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "Tap to complete payment" const val DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt index f96300be4..e12ffd2a2 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt @@ -10,6 +10,8 @@ import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_RECEIVED_NOTIFICA import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_SENT_NOTIFICATION_TEXT import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE +import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT +import breez_sdk_liquid_notification.Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT import breez_sdk_liquid_notification.Constants.DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_SWAP_UPDATED @@ -17,6 +19,8 @@ import breez_sdk_liquid_notification.Constants.PAYMENT_RECEIVED_NOTIFICATION_TEX import breez_sdk_liquid_notification.Constants.PAYMENT_RECEIVED_NOTIFICATION_TITLE import breez_sdk_liquid_notification.Constants.PAYMENT_SENT_NOTIFICATION_TEXT import breez_sdk_liquid_notification.Constants.PAYMENT_SENT_NOTIFICATION_TITLE +import breez_sdk_liquid_notification.Constants.PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT +import breez_sdk_liquid_notification.Constants.PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE import breez_sdk_liquid_notification.Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT import breez_sdk_liquid_notification.Constants.SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE import breez_sdk_liquid_notification.NotificationHelper.Companion.notifyChannel @@ -57,8 +61,9 @@ class SwapUpdatedJob( override fun onEvent(e: SdkEvent) { when (e) { - is SdkEvent.PaymentWaitingConfirmation -> handlePaymentEvent(e.details) - is SdkEvent.PaymentSucceeded -> handlePaymentEvent(e.details) + is SdkEvent.PaymentWaitingConfirmation -> handlePaymentSuccess(e.details) + is SdkEvent.PaymentSucceeded -> handlePaymentSuccess(e.details) + is SdkEvent.PaymentWaitingFeeAcceptance -> handlePaymentWaitingFeeAcceptance(e.details) else -> { logger.log(TAG, "Received event: ${e}", "TRACE") @@ -76,12 +81,16 @@ class SwapUpdatedJob( .fold(StringBuilder()) { sb, it -> sb.append("%02x".format(it)) } .toString() - private fun handlePaymentEvent(payment: Payment) { - val swapId = when (val details = payment.details) { + private fun getSwapId(details: PaymentDetails?): String? { + return when (details) { is PaymentDetails.Bitcoin -> details.swapId is PaymentDetails.Lightning -> details.swapId else -> null } + } + + private fun handlePaymentSuccess(payment: Payment) { + val swapId = getSwapId(payment.details) swapId?.let { if (this.swapIdHash == hashId(it)) { @@ -95,6 +104,21 @@ class SwapUpdatedJob( } } + private fun handlePaymentWaitingFeeAcceptance(payment: Payment) { + val swapId = getSwapId(payment.details) + + swapId?.let { + if (this.swapIdHash == hashId(it)) { + logger.log( + TAG, + "Payment waiting fee acceptance: ${this.swapIdHash}", + "TRACE" + ) + notifyPaymentWaitingFeeAcceptance(payment) + } + } + } + private fun notifySuccess(payment: Payment) { if (!this.notified) { logger.log(TAG, "Payment ${payment.txId} processing successful", "INFO") @@ -121,6 +145,28 @@ class SwapUpdatedJob( } } + private fun notifyPaymentWaitingFeeAcceptance(payment: Payment) { + if (!this.notified) { + logger.log(TAG, "Payment ${payment.txId} requires fee acceptance", "INFO") + notifyChannel( + context, + NOTIFICATION_CHANNEL_SWAP_UPDATED, + getString( + context, + PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE, + DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE + ), + getString( + context, + PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT, + DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT + ) + ) + this.notified = true + fgService.onFinished(this) + } + } + private fun notifyFailure() { this.swapIdHash?.let { swapIdHash -> logger.log(TAG, "Swap $swapIdHash processing failed", "INFO") diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift index aaef8dc11..5e26b545a 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Constants.swift @@ -20,6 +20,8 @@ struct Constants { static let LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "lnurl_pay_notification_failure_title" static let PAYMENT_RECEIVED_NOTIFICATION_TITLE = "payment_received_notification_title" static let PAYMENT_SENT_NOTIFICATION_TITLE = "payment_sent_notification_title" + static let PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = "payment_waiting_fee_acceptance_notification_title" + static let PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT = "payment_waiting_fee_acceptance_text" static let SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "swap_confirmed_notification_failure_text" static let SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "swap_confirmed_notification_failure_title" @@ -30,6 +32,8 @@ struct Constants { static let DEFAULT_LNURL_PAY_NOTIFICATION_FAILURE_TITLE = "Receive Payment Failed" static let DEFAULT_PAYMENT_RECEIVED_NOTIFICATION_TITLE = "Received %d sats" static let DEFAULT_PAYMENT_SENT_NOTIFICATION_TITLE = "Sent %d sats" + static let DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE = "Payment requires fee acceptance" + static let DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT = "Tap to review updated fees" static let DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TEXT = "Tap to complete payment" static let DEFAULT_SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "Payment Pending" } diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift index fd70f715b..d7b0d0867 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift @@ -43,6 +43,13 @@ class SwapUpdatedTask : TaskProtocol { self.notifySuccess(payment: payment) } break + case .paymentWaitingFeeAcceptance(details: let payment): + let swapId = self.getSwapId(details: payment.details) + if swapIdHash == swapId?.sha256() { + self.logger.log(tag: TAG, line: "Received payment event: \(swapIdHash) \(payment.status)", level: "INFO") + self.notifyPaymentWaitingFeeAcceptance(payment: payment) + } + break default: break } @@ -81,4 +88,18 @@ class SwapUpdatedTask : TaskProtocol { self.displayPushNotification(title: String(format: notificationTitle, payment.amountSat), logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED) } } + + func notifyPaymentWaitingFeeAcceptance(payment: Payment) { + if !self.notified { + self.logger.log(tag: TAG, line: "Payment \(payment.txId ?? "") requires fee acceptance", level: "INFO") + let notificationTitle = ResourceHelper.shared.getString( + key: Constants.PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE, + fallback: Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE) + let notificationBody = ResourceHelper.shared.getString( + key: Constants.PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT, + fallback: Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TEXT) + self.notified = true + self.displayPushNotification(title: notificationTitle, body: notificationBody, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_SWAP_UPDATED) + } + } } From 39fab35cfa14e14197a0bcf564606f596da2b091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Mon, 30 Dec 2024 14:55:40 +0000 Subject: [PATCH 12/20] Fix logs using non-existent txid --- .../kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt | 2 +- .../langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt index e12ffd2a2..532a298c6 100644 --- a/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt +++ b/lib/bindings/langs/android/lib/src/main/kotlin/breez_sdk_liquid_notification/job/SwapUpdated.kt @@ -147,7 +147,7 @@ class SwapUpdatedJob( private fun notifyPaymentWaitingFeeAcceptance(payment: Payment) { if (!this.notified) { - logger.log(TAG, "Payment ${payment.txId} requires fee acceptance", "INFO") + logger.log(TAG, "Payment with swap ID ${getSwapId(payment.details)} requires fee acceptance", "INFO") notifyChannel( context, NOTIFICATION_CHANNEL_SWAP_UPDATED, diff --git a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift index d7b0d0867..26a74d2e2 100644 --- a/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift +++ b/lib/bindings/langs/swift/Sources/BreezSDKLiquid/Task/SwapUpdated.swift @@ -91,7 +91,7 @@ class SwapUpdatedTask : TaskProtocol { func notifyPaymentWaitingFeeAcceptance(payment: Payment) { if !self.notified { - self.logger.log(tag: TAG, line: "Payment \(payment.txId ?? "") requires fee acceptance", level: "INFO") + self.logger.log(tag: TAG, line: "Payment \(self.getSwapId(details: payment.details) ?? "") requires fee acceptance", level: "INFO") let notificationTitle = ResourceHelper.shared.getString( key: Constants.PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE, fallback: Constants.DEFAULT_PAYMENT_WAITING_FEE_ACCEPTANCE_TITLE) From 535d2ec063aafe96b231b9558b2e21e152a6dbc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Tue, 31 Dec 2024 18:08:42 +0000 Subject: [PATCH 13/20] Always fill in the `actual_payer_amount_sat` --- lib/core/src/model.rs | 14 +++++++++++++- lib/core/src/persist/mod.rs | 9 ++++++--- lib/core/src/recover/recoverer.rs | 12 +++--------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index df4abb667..4737132ff 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -731,7 +731,8 @@ pub(crate) struct ChainSwap { pub(crate) description: Option, /// Payer amount defined at swap creation pub(crate) payer_amount_sat: u64, - /// The actual payer amount in case it differs from `payer_amount_sat` (over/underpayment) + /// The actual payer amount as seen on the user lockup tx. Might differ from `payer_amount_sat` + /// in the case of an over/underpayment pub(crate) actual_payer_amount_sat: Option, /// Receiver amount defined at swap creation pub(crate) receiver_amount_sat: u64, @@ -861,6 +862,17 @@ impl ChainSwap { Ok(create_response_json) } + + pub(crate) fn is_amount_mismatch(&self) -> bool { + match self.actual_payer_amount_sat { + Some(actual_amount) => actual_amount != self.payer_amount_sat, + None => false, + } + } + + pub(crate) fn is_waiting_fee_acceptance(&self) -> bool { + self.is_amount_mismatch() && self.accepted_receiver_amount_sat.is_none() + } } #[derive(Clone, Debug, Default)] diff --git a/lib/core/src/persist/mod.rs b/lib/core/src/persist/mod.rs index 85ddc628f..15cee000a 100644 --- a/lib/core/src/persist/mod.rs +++ b/lib/core/src/persist/mod.rs @@ -580,9 +580,12 @@ impl Persister { let receiver_amount_sat = match maybe_chain_swap_accepted_receiver_amount_sat { Some(accepted_receiver_amount_sat) => accepted_receiver_amount_sat, - None => match maybe_chain_swap_actual_payer_amount_sat { - Some(_) => payer_amount_sat, // For over/underpaid chain swaps WaitingFeeAcceptance, show zero fees - None => maybe_chain_swap_receiver_amount_sat.unwrap_or(0), + None => match ( + maybe_chain_swap_actual_payer_amount_sat, + maybe_chain_swap_payer_amount_sat, + ) { + (Some(actual), Some(expected)) if actual != expected => actual, // For over/underpaid chain swaps WaitingFeeAcceptance, show zero fees + _ => maybe_chain_swap_receiver_amount_sat.unwrap_or(0), }, }; let swapper_fees_sat = maybe_chain_swap_pair_fees diff --git a/lib/core/src/recover/recoverer.rs b/lib/core/src/recover/recoverer.rs index 2f1b549a3..bde28e1c4 100644 --- a/lib/core/src/recover/recoverer.rs +++ b/lib/core/src/recover/recoverer.rs @@ -212,20 +212,14 @@ impl Recoverer { log::warn!("Could not apply recovered data for incoming Chain swap {swap_id}: recovery data not found"); continue; }; - if chain_swap.payer_amount_sat != recovered_data.btc_user_lockup_amount_sat - { - chain_swap.actual_payer_amount_sat = - Some(recovered_data.btc_user_lockup_amount_sat) - } + chain_swap.actual_payer_amount_sat = + Some(recovered_data.btc_user_lockup_amount_sat); let is_expired = bitcoin_height >= chain_swap.timeout_block_height; let min_lockup_amount_sat = chain_swap.payer_amount_sat; - let is_waiting_fee_acceptance = - chain_swap.actual_payer_amount_sat.is_some() - && chain_swap.accepted_receiver_amount_sat.is_none(); if let Some(new_state) = recovered_data.derive_partial_state( min_lockup_amount_sat, is_expired, - is_waiting_fee_acceptance, + chain_swap.is_waiting_fee_acceptance(), ) { chain_swap.state = new_state; } From ed8cc4f24fcf038f6d884c8541b8c177bbdbcdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Wed, 1 Jan 2025 14:37:57 +0000 Subject: [PATCH 14/20] Address review comments --- cli/src/commands.rs | 40 ++++++++++++++++------------------- lib/core/src/persist/chain.rs | 6 +----- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 4bb7bf73c..c1557dab2 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::thread; use std::time::Duration; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, Result}; use breez_sdk_liquid::prelude::*; use clap::{arg, Parser}; use qrcode_rs::render::unicode; @@ -131,14 +131,8 @@ pub(crate) enum Command { /// Lightning payment hash payment_hash: String, }, - /// Get proposed fees for WaitingFeeAcceptance Payment - FetchPaymentProposedFees { swap_id: String }, - /// Accept proposed fees for WaitingFeeAcceptance Payment - AcceptPaymentProposedFees { - swap_id: String, - // Fee amount obtained using FetchPaymentProposedFees - fees_sat: u64, - }, + /// Get and potentially accept proposed fees for WaitingFeeAcceptance Payment + ReviewPaymentProposedFees { swap_id: String }, /// List refundable chain swaps ListRefundables, /// Prepare a refund transaction for an incomplete swap @@ -527,21 +521,23 @@ pub(crate) async fn handle_command( } } } - Command::FetchPaymentProposedFees { swap_id } => { - let res = sdk - .fetch_payment_proposed_fees(&FetchPaymentProposedFeesRequest { swap_id }) - .await?; - command_result!(res) - } - Command::AcceptPaymentProposedFees { swap_id, fees_sat } => { - let res = sdk + Command::ReviewPaymentProposedFees { swap_id } => { + let fetch_response = sdk .fetch_payment_proposed_fees(&FetchPaymentProposedFeesRequest { swap_id }) .await?; - if fees_sat != res.fees_sat { - bail!("Fees changed since they were fetched") - } - sdk.accept_payment_proposed_fees(&AcceptPaymentProposedFeesRequest { response: res }) - .await?; + + let confirmation_msg = format!( + "Payer amount: {} sat. Fees: {} sat. Are the fees acceptable? (y/N) ", + fetch_response.payer_amount_sat, fetch_response.fees_sat + ); + + wait_confirmation!(confirmation_msg, "Payment proposed fees review halted"); + + sdk.accept_payment_proposed_fees(&AcceptPaymentProposedFeesRequest { + response: fetch_response, + }) + .await?; + command_result!("Proposed fees accepted successfully") } Command::ListRefundables => { diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index d5efe1ab5..929285eab 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -72,11 +72,7 @@ impl Persister { pair_fees_json = :pair_fees_json, state = :state, actual_payer_amount_sat = :actual_payer_amount_sat, - accepted_receiver_amount_sat = - CASE - WHEN accepted_receiver_amount_sat IS NULL THEN :accepted_receiver_amount_sat - ELSE accepted_receiver_amount_sat - END + accepted_receiver_amount_sat = COALESCE(accepted_receiver_amount_sat, :accepted_receiver_amount_sat) WHERE id = :id", named_params! { From 9383be49bc69648610905eb7fb024212c9fc8e53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Wed, 1 Jan 2025 23:43:35 +0000 Subject: [PATCH 15/20] Bump schema minor version --- lib/core/src/sync/model/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/src/sync/model/mod.rs b/lib/core/src/sync/model/mod.rs index 84e0721de..4d2c009b2 100644 --- a/lib/core/src/sync/model/mod.rs +++ b/lib/core/src/sync/model/mod.rs @@ -18,7 +18,7 @@ pub(crate) mod sync; const MESSAGE_PREFIX: &[u8; 13] = b"realtimesync:"; lazy_static! { - static ref CURRENT_SCHEMA_VERSION: Version = Version::parse("0.0.2").unwrap(); + static ref CURRENT_SCHEMA_VERSION: Version = Version::parse("0.1.0").unwrap(); } #[derive(Copy, Clone)] From dd6038be6072128c43df74a6e11c7dab9f082a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Wed, 1 Jan 2025 23:49:13 +0000 Subject: [PATCH 16/20] Implement `accepted_receiver_amount_sat` merge --- lib/core/src/sync/model/data.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/core/src/sync/model/data.rs b/lib/core/src/sync/model/data.rs index bcad8478b..baebda9a7 100644 --- a/lib/core/src/sync/model/data.rs +++ b/lib/core/src/sync/model/data.rs @@ -32,6 +32,9 @@ impl ChainSyncData { for field in updated_fields { match field.as_str() { "accept_zero_conf" => self.accept_zero_conf = other.accept_zero_conf, + "accepted_receiver_amount_sat" => { + self.accepted_receiver_amount_sat = other.accepted_receiver_amount_sat + } _ => continue, } } From c4f707f87e3230566e6d919b0f6ac080adf684ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Thu, 2 Jan 2025 00:22:54 +0000 Subject: [PATCH 17/20] Derive chain swap fees using claim tx amount when available --- lib/core/src/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 4737132ff..e6c0b464b 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -1538,7 +1538,7 @@ impl Payment { .unwrap_or(utils::now()), amount_sat: tx.amount_sat, fees_sat: match swap.as_ref() { - Some(s) => s.payer_amount_sat - s.receiver_amount_sat, + Some(s) => s.payer_amount_sat - tx.amount_sat, None => match tx.payment_type { PaymentType::Receive => 0, PaymentType::Send => tx.fees_sat, From a020ed5ddb53fdb6b8624f6eb70ee9e6e76e2ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Thu, 2 Jan 2025 01:28:30 +0000 Subject: [PATCH 18/20] Optimistically persist accepted receiver amount --- lib/core/src/chain_swap.rs | 18 +++++++++++++++--- lib/core/src/persist/chain.rs | 6 ++++-- lib/core/src/sdk.rs | 12 +++++++++--- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index 5215cb220..d0b8a4829 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -380,6 +380,14 @@ impl ChainSwapHandler { async fn handle_amountless_update(&self, swap: &ChainSwap) -> Result<(), PaymentError> { let id = swap.id.clone(); + // Since we optimistically persist the accepted receiver amount, if accepting a quote with + // the swapper fails, we might still think it's accepted, so now we should get rid of the + // old invalid accepted amount. + if swap.accepted_receiver_amount_sat.is_some() { + info!("Handling amountless update for swap {id} with existing accepted receiver amount. Erasing the accepted amount now..."); + self.persister.update_accepted_receiver_amount(&id, None)?; + } + let quote = self .swapper .get_zero_amount_chain_swap_quote(&id) @@ -394,10 +402,14 @@ impl ChainSwapHandler { debug!("Zero-amount swap validated. Auto-accepting..."); self.persister .update_actual_payer_amount(&id, user_lockup_amount_sat)?; - self.swapper - .accept_zero_amount_chain_swap_quote(&id, quote)?; self.persister - .update_accepted_receiver_amount(&id, receiver_amount_sat) + .update_accepted_receiver_amount(&id, Some(receiver_amount_sat))?; + self.swapper + .accept_zero_amount_chain_swap_quote(&id, quote) + .inspect_err(|e| { + error!("Failed to accept zero-amount swap {id} quote: {e} - trying to erase the accepted receiver amount..."); + let _ = self.persister.update_accepted_receiver_amount(&id, None); + }) } ValidateAmountlessSwapResult::RequiresUserAction { user_lockup_amount_sat, diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index 929285eab..529c9fe8a 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -318,13 +318,15 @@ impl Persister { } /// Used for receive chain swaps, when fees are accepted and thus the agreed received amount is known + /// + /// Can also be used to erase a previously persisted accepted amount in case of failure to accept. pub(crate) fn update_accepted_receiver_amount( &self, swap_id: &str, - accepted_receiver_amount_sat: u64, + accepted_receiver_amount_sat: Option, ) -> Result<(), PaymentError> { log::info!( - "Updating chain swap {swap_id}: accepted_receiver_amount_sat = {accepted_receiver_amount_sat}" + "Updating chain swap {swap_id}: accepted_receiver_amount_sat = {accepted_receiver_amount_sat:?}" ); let mut con: Connection = self.get_connection()?; let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?; diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index a720bf3ac..2744ad9d0 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -2651,10 +2651,16 @@ impl LiquidSdk { PaymentError::InvalidOrExpiredFees ); - self.swapper - .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat())?; self.persister - .update_accepted_receiver_amount(&swap_id, payer_amount_sat - fees_sat)?; + .update_accepted_receiver_amount(&swap_id, Some(payer_amount_sat - fees_sat))?; + self.swapper + .accept_zero_amount_chain_swap_quote(&swap_id, server_lockup_quote.to_sat()) + .inspect_err(|e| { + error!("Failed to accept zero-amount swap {swap_id} quote: {e} - trying to erase the accepted receiver amount..."); + let _ = self + .persister + .update_accepted_receiver_amount(&swap_id, None); + })?; self.chain_swap_handler.update_swap_info(&ChainSwapUpdate { swap_id, to_state: Pending, From b18ad6d097db46630e1e48d82944afeed8046b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Fri, 3 Jan 2025 09:37:02 +0000 Subject: [PATCH 19/20] Fix `update_fields` --- lib/core/src/sync/model/data.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/core/src/sync/model/data.rs b/lib/core/src/sync/model/data.rs index baebda9a7..e79e677db 100644 --- a/lib/core/src/sync/model/data.rs +++ b/lib/core/src/sync/model/data.rs @@ -50,6 +50,9 @@ impl ChainSyncData { if update.accept_zero_conf != swap.accept_zero_conf { updated_fields.push("accept_zero_conf".to_string()); } + if update.accepted_receiver_amount_sat != swap.accepted_receiver_amount_sat { + updated_fields.push("accepted_receiver_amount_sat".to_string()); + } Some(updated_fields) } None => None, From fa70c0b6dfc35cedaccbee745b9fd7100f8997a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Granh=C3=A3o?= Date: Fri, 3 Jan 2025 09:53:12 +0000 Subject: [PATCH 20/20] Add receiver amount to `FetchPaymentProposedFeesResponse` --- cli/src/commands.rs | 4 ++-- lib/bindings/src/breez_sdk_liquid.udl | 1 + lib/core/src/model.rs | 2 ++ lib/core/src/sdk.rs | 2 ++ .../main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt | 5 ++++- packages/react-native/ios/BreezSDKLiquidMapper.swift | 6 +++++- packages/react-native/src/index.ts | 1 + 7 files changed, 17 insertions(+), 4 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index c1557dab2..95eb9032e 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -527,8 +527,8 @@ pub(crate) async fn handle_command( .await?; let confirmation_msg = format!( - "Payer amount: {} sat. Fees: {} sat. Are the fees acceptable? (y/N) ", - fetch_response.payer_amount_sat, fetch_response.fees_sat + "Payer amount: {} sat. Fees: {} sat. Resulting received amount: {} sat. Are the fees acceptable? (y/N) ", + fetch_response.payer_amount_sat, fetch_response.fees_sat, fetch_response.receiver_amount_sat ); wait_confirmation!(confirmation_msg, "Payment proposed fees review halted"); diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 1abb73773..6a63085eb 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -550,6 +550,7 @@ dictionary FetchPaymentProposedFeesResponse { string swap_id; u64 fees_sat; u64 payer_amount_sat; + u64 receiver_amount_sat; }; dictionary AcceptPaymentProposedFeesRequest { diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index e6c0b464b..1d0f55b84 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -1796,6 +1796,8 @@ pub struct FetchPaymentProposedFeesResponse { pub fees_sat: u64, /// Amount sent by the swap payer pub payer_amount_sat: u64, + /// Amount that will be received if these fees are accepted + pub receiver_amount_sat: u64, } /// An argument when calling [crate::sdk::LiquidSdk::accept_payment_proposed_fees]. diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 2744ad9d0..283eae3c7 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -2614,6 +2614,7 @@ impl LiquidSdk { swap_id: req.swap_id.clone(), fees_sat, payer_amount_sat: actual_payer_amount_sat, + receiver_amount_sat: actual_payer_amount_sat - fees_sat, }) } @@ -2628,6 +2629,7 @@ impl LiquidSdk { swap_id, fees_sat, payer_amount_sat, + .. } = req.clone().response; let chain_swap = 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 a56d5c8ee..2b608ca7a 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 @@ -552,6 +552,7 @@ fun asFetchPaymentProposedFeesResponse(fetchPaymentProposedFeesResponse: Readabl "swapId", "feesSat", "payerAmountSat", + "receiverAmountSat", ), ) ) { @@ -560,7 +561,8 @@ fun asFetchPaymentProposedFeesResponse(fetchPaymentProposedFeesResponse: Readabl val swapId = fetchPaymentProposedFeesResponse.getString("swapId")!! val feesSat = fetchPaymentProposedFeesResponse.getDouble("feesSat").toULong() val payerAmountSat = fetchPaymentProposedFeesResponse.getDouble("payerAmountSat").toULong() - return FetchPaymentProposedFeesResponse(swapId, feesSat, payerAmountSat) + val receiverAmountSat = fetchPaymentProposedFeesResponse.getDouble("receiverAmountSat").toULong() + return FetchPaymentProposedFeesResponse(swapId, feesSat, payerAmountSat, receiverAmountSat) } fun readableMapOf(fetchPaymentProposedFeesResponse: FetchPaymentProposedFeesResponse): ReadableMap = @@ -568,6 +570,7 @@ fun readableMapOf(fetchPaymentProposedFeesResponse: FetchPaymentProposedFeesResp "swapId" to fetchPaymentProposedFeesResponse.swapId, "feesSat" to fetchPaymentProposedFeesResponse.feesSat, "payerAmountSat" to fetchPaymentProposedFeesResponse.payerAmountSat, + "receiverAmountSat" to fetchPaymentProposedFeesResponse.receiverAmountSat, ) fun asFetchPaymentProposedFeesResponseList(arr: ReadableArray): List { diff --git a/packages/react-native/ios/BreezSDKLiquidMapper.swift b/packages/react-native/ios/BreezSDKLiquidMapper.swift index 6c3446933..2523dd35b 100644 --- a/packages/react-native/ios/BreezSDKLiquidMapper.swift +++ b/packages/react-native/ios/BreezSDKLiquidMapper.swift @@ -633,8 +633,11 @@ enum BreezSDKLiquidMapper { guard let payerAmountSat = fetchPaymentProposedFeesResponse["payerAmountSat"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "payerAmountSat", typeName: "FetchPaymentProposedFeesResponse")) } + guard let receiverAmountSat = fetchPaymentProposedFeesResponse["receiverAmountSat"] as? UInt64 else { + throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "receiverAmountSat", typeName: "FetchPaymentProposedFeesResponse")) + } - return FetchPaymentProposedFeesResponse(swapId: swapId, feesSat: feesSat, payerAmountSat: payerAmountSat) + return FetchPaymentProposedFeesResponse(swapId: swapId, feesSat: feesSat, payerAmountSat: payerAmountSat, receiverAmountSat: receiverAmountSat) } static func dictionaryOf(fetchPaymentProposedFeesResponse: FetchPaymentProposedFeesResponse) -> [String: Any?] { @@ -642,6 +645,7 @@ enum BreezSDKLiquidMapper { "swapId": fetchPaymentProposedFeesResponse.swapId, "feesSat": fetchPaymentProposedFeesResponse.feesSat, "payerAmountSat": fetchPaymentProposedFeesResponse.payerAmountSat, + "receiverAmountSat": fetchPaymentProposedFeesResponse.receiverAmountSat, ] } diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index e02b9e236..f17447e9f 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -111,6 +111,7 @@ export interface FetchPaymentProposedFeesResponse { swapId: string feesSat: number payerAmountSat: number + receiverAmountSat: number } export interface FiatCurrency {