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] 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 | 175 +++++++++--- 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, 1195 insertions(+), 367 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 48ba701..4bb7bf7 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 b882180..5a6eafa 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 2b9129d..1abb737 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 995fc3b..6845f8e 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 94ef25a..d2edd9f 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)")) + + // 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); + + 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}", ); - 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")) - ); - - 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, - )?; - - 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 ec5cd90..0f6799c 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 c6deced..43c5bc6 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 03ecfd1..24968ec 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 420126b..1865821 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 5399301..7998691 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 338da57..248afe2 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 a914638..5f35836 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 bf67d9a..dfc78c8 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 0a0073d..cb3ee8e 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 82467d1..496fc30 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 93bdee6..4245132 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 14d6902..9dfc16d 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 226d33c..8cc7adc 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 322568d..fb82909 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 bd7749c..248a59f 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 4a08438..ddaf430 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 c360b9e..ac26980 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 a712d55..8b3d20a 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 42a12c1..cde1245 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 43f31b8..df50dea 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 6e85f25..64d7181 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 b9640d4..7eed3b1 100644 --- a/packages/react-native/ios/RNBreezSDKLiquid.m +++ b/packages/react-native/ios/RNBreezSDKLiquid.m @@ -131,6 +131,18 @@ RCT_EXTERN_METHOD( 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 7dc0020..3775f5c 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 c7b48b8..e02b9e2 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