diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 9b2448d..27600a2 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -609,7 +609,7 @@ dependencies = [ [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/SatoshiPortal/boltz-rust?branch=trunk#19e01071f5722c21f80ae9b34edef5c3972e9e0b" +source = "git+https://github.com/SatoshiPortal/boltz-rust?branch=trunk#68ff15b0f80abfa54d8dca4c11c8c2794e4b6921" dependencies = [ "bip39", "bitcoin 0.31.2", diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 78cf742..ba8d6cf 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -271,13 +271,21 @@ pub(crate) async fn handle_command( }) .await?; - wait_confirmation!( - format!( - "Fees: {} sat. Are the fees acceptable? (y/N) ", - prepare_response.fees_sat - ), - "Payment receive halted" - ); + let fees = prepare_response.fees_sat; + let confirmation_msg = match payer_amount_sat { + Some(_) => format!("Fees: {fees} sat. Are the fees acceptable? (y/N)"), + None => { + let min = prepare_response.min_payer_amount_sat; + let max = prepare_response.max_payer_amount_sat; + let service_feerate = prepare_response.service_feerate; + format!( + "Fees: {fees} sat + {service_feerate:?}% of the sent amount. \ + Sender should send between {min:?} sat and {max:?} sat. \ + Are the fees acceptable? (y/N)" + ) + } + }; + wait_confirmation!(confirmation_msg, "Payment receive halted"); let response = sdk .receive_payment(&ReceivePaymentRequest { diff --git a/lib/Cargo.lock b/lib/Cargo.lock index c4aaf03..cca4156 100644 --- a/lib/Cargo.lock +++ b/lib/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -723,7 +723,7 @@ dependencies = [ [[package]] name = "boltz-client" version = "0.1.3" -source = "git+https://github.com/SatoshiPortal/boltz-rust?branch=trunk#19e01071f5722c21f80ae9b34edef5c3972e9e0b" +source = "git+https://github.com/SatoshiPortal/boltz-rust?branch=trunk#68ff15b0f80abfa54d8dca4c11c8c2794e4b6921" dependencies = [ "bip39", "bitcoin 0.31.2", diff --git a/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h b/lib/bindings/langs/flutter/breez_sdk_liquid/include/breez_sdk_liquid.h index 94551b1..c22c6c1 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 @@ -367,6 +367,9 @@ typedef struct wire_cst_prepare_receive_response { int32_t payment_method; uint64_t *payer_amount_sat; uint64_t fees_sat; + uint64_t *min_payer_amount_sat; + uint64_t *max_payer_amount_sat; + double *swapper_feerate; } wire_cst_prepare_receive_response; typedef struct wire_cst_receive_payment_request { @@ -1194,6 +1197,8 @@ struct wire_cst_check_message_request *frbgen_breez_liquid_cst_new_box_autoadd_c struct wire_cst_connect_request *frbgen_breez_liquid_cst_new_box_autoadd_connect_request(void); +double *frbgen_breez_liquid_cst_new_box_autoadd_f_64(double value); + struct wire_cst_get_payment_request *frbgen_breez_liquid_cst_new_box_autoadd_get_payment_request(void); int64_t *frbgen_breez_liquid_cst_new_box_autoadd_i_64(int64_t value); @@ -1306,6 +1311,7 @@ static int64_t dummy_method_to_enforce_bundling(void) { dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_buy_bitcoin_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_check_message_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_connect_request); + dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_f_64); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_get_payment_request); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_i_64); dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_liquid_address_data); diff --git a/lib/bindings/src/breez_sdk_liquid.udl b/lib/bindings/src/breez_sdk_liquid.udl index 7a56320..d1975ce 100644 --- a/lib/bindings/src/breez_sdk_liquid.udl +++ b/lib/bindings/src/breez_sdk_liquid.udl @@ -431,9 +431,12 @@ dictionary PrepareReceiveRequest { }; dictionary PrepareReceiveResponse { - u64? payer_amount_sat; PaymentMethod payment_method; u64 fees_sat; + u64? payer_amount_sat; + u64? min_payer_amount_sat; + u64? max_payer_amount_sat; + f64? swapper_feerate; }; dictionary ReceivePaymentRequest { diff --git a/lib/core/src/chain_swap.rs b/lib/core/src/chain_swap.rs index e9aacd6..0ca54b7 100644 --- a/lib/core/src/chain_swap.rs +++ b/lib/core/src/chain_swap.rs @@ -436,6 +436,20 @@ impl ChainSwapHandler { | ChainSwapStates::TransactionLockupFailed | 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; + 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 + return Ok(()); // Break from TxLockupFailed branch + } + // In case of error, we continue and mark it as refundable + Err(e) => error!("Failed to accept the quote for swap {}: {e:?}", &swap.id), + } + } + match swap.refund_tx_id.clone() { None => { warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}"); @@ -453,7 +467,7 @@ impl ChainSwapHandler { } } Some(refund_tx_id) => warn!( - "Refund tx for Chain Swap {id} was already broadcast: txid {refund_tx_id}" + "Refund for Chain Swap {id} was already broadcast: txid {refund_tx_id}" ), }; Ok(()) @@ -466,6 +480,82 @@ impl ChainSwapHandler { } } + async fn handle_amountless_update(&self, swap: &ChainSwap) -> Result<(), PaymentError> { + let quote = self + .swapper + .get_zero_amount_chain_swap_quote(&swap.id) + .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) + } + + async fn validate_and_update_amountless_swap( + &self, + swap: &ChainSwap, + quote_server_lockup_amount_sat: u64, + ) -> Result<(), PaymentError> { + debug!("Validating {swap:?}"); + + ensure_sdk!( + matches!(swap.direction, Direction::Incoming), + PaymentError::generic(&format!( + "Only an incoming chain swap can be a zero-amount swap. Swap ID: {}", + &swap.id + )) + ); + + let script_pubkey = swap.get_receive_lockup_swap_script_pubkey(self.config.network)?; + let script_balance = self + .bitcoin_chain_service + .lock() + .await + .script_get_balance(script_pubkey.as_script())?; + debug!("Found lockup balance {script_balance:?}"); + let user_lockup_amount_sat = match script_balance.confirmed > 0 { + true => script_balance.confirmed, + false => match script_balance.unconfirmed > 0 { + true => script_balance.unconfirmed.unsigned_abs(), + false => 0, + }, + }; + ensure_sdk!( + user_lockup_amount_sat > 0, + PaymentError::generic("Lockup address has no confirmed or unconfirmed balance") + ); + + let pair = swap.get_boltz_pair()?; + let swapper_service_feerate = pair.fees.percentage; + let swapper_server_fees_sat = pair.fees.server(); + let service_fees_sat = + ((swapper_service_feerate / 100.0) * user_lockup_amount_sat as f64).ceil() as u64; + let fees_sat = swapper_server_fees_sat + service_fees_sat; + ensure_sdk!( + user_lockup_amount_sat > fees_sat, + PaymentError::generic(&format!("Invalid quote: fees ({fees_sat} sat) are higher than user lockup ({user_lockup_amount_sat} sat)")) + ); + + let expected_server_lockup_amount_sat = user_lockup_amount_sat - fees_sat; + debug!("user_lockup_amount_sat = {}, service_fees_sat = {}, server_fees_sat = {}, expected_server_lockup_amount_sat = {}, quote_server_lockup_amount_sat = {}", + user_lockup_amount_sat, service_fees_sat, swapper_server_fees_sat, expected_server_lockup_amount_sat, quote_server_lockup_amount_sat); + ensure_sdk!( + expected_server_lockup_amount_sat <= quote_server_lockup_amount_sat, + PaymentError::generic(&format!("Invalid quote: expected at least {expected_server_lockup_amount_sat} sat, got {quote_server_lockup_amount_sat} sat")) + ); + + 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(()) + } + async fn on_new_outgoing_status(&self, swap: &ChainSwap, update: &boltz::Update) -> Result<()> { let id = &update.id; let status = &update.status; diff --git a/lib/core/src/frb_generated.rs b/lib/core/src/frb_generated.rs index 4a15187..289c917 100644 --- a/lib/core/src/frb_generated.rs +++ b/lib/core/src/frb_generated.rs @@ -3291,6 +3291,17 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -3747,10 +3758,16 @@ impl SseDecode for crate::model::PrepareReceiveResponse { let mut var_paymentMethod = ::sse_decode(deserializer); let mut var_payerAmountSat = >::sse_decode(deserializer); let mut var_feesSat = ::sse_decode(deserializer); + let mut var_minPayerAmountSat = >::sse_decode(deserializer); + let mut var_maxPayerAmountSat = >::sse_decode(deserializer); + let mut var_swapperFeerate = >::sse_decode(deserializer); return crate::model::PrepareReceiveResponse { payment_method: var_paymentMethod, payer_amount_sat: var_payerAmountSat, fees_sat: var_feesSat, + min_payer_amount_sat: var_minPayerAmountSat, + max_payer_amount_sat: var_maxPayerAmountSat, + swapper_feerate: var_swapperFeerate, }; } } @@ -5807,6 +5824,9 @@ impl flutter_rust_bridge::IntoDart for crate::model::PrepareReceiveResponse { self.payment_method.into_into_dart().into_dart(), self.payer_amount_sat.into_into_dart().into_dart(), self.fees_sat.into_into_dart().into_dart(), + self.min_payer_amount_sat.into_into_dart().into_dart(), + self.max_payer_amount_sat.into_into_dart().into_dart(), + self.swapper_feerate.into_into_dart().into_dart(), ] .into_dart() } @@ -7282,6 +7302,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7680,6 +7710,9 @@ impl SseEncode for crate::model::PrepareReceiveResponse { ::sse_encode(self.payment_method, serializer); >::sse_encode(self.payer_amount_sat, serializer); ::sse_encode(self.fees_sat, serializer); + >::sse_encode(self.min_payer_amount_sat, serializer); + >::sse_encode(self.max_payer_amount_sat, serializer); + >::sse_encode(self.swapper_feerate, serializer); } } @@ -8290,6 +8323,12 @@ mod io { CstDecode::::cst_decode(*wrap).into() } } + impl CstDecode for *mut f64 { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> f64 { + unsafe { *flutter_rust_bridge::for_generated::box_from_leak_ptr(self) } + } + } impl CstDecode for *mut wire_cst_get_payment_request { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::model::GetPaymentRequest { @@ -9554,6 +9593,9 @@ mod io { payment_method: self.payment_method.cst_decode(), payer_amount_sat: self.payer_amount_sat.cst_decode(), fees_sat: self.fees_sat.cst_decode(), + min_payer_amount_sat: self.min_payer_amount_sat.cst_decode(), + max_payer_amount_sat: self.max_payer_amount_sat.cst_decode(), + swapper_feerate: self.swapper_feerate.cst_decode(), } } } @@ -10702,6 +10744,9 @@ mod io { payment_method: Default::default(), payer_amount_sat: core::ptr::null_mut(), fees_sat: Default::default(), + min_payer_amount_sat: core::ptr::null_mut(), + max_payer_amount_sat: core::ptr::null_mut(), + swapper_feerate: core::ptr::null_mut(), } } } @@ -11481,6 +11526,11 @@ mod io { ) } + #[no_mangle] + pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_f_64(value: f64) -> *mut f64 { + flutter_rust_bridge::for_generated::new_leak_box_ptr(value) + } + #[no_mangle] pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_get_payment_request( ) -> *mut wire_cst_get_payment_request { @@ -12840,6 +12890,9 @@ mod io { payment_method: i32, payer_amount_sat: *mut u64, fees_sat: u64, + min_payer_amount_sat: *mut u64, + max_payer_amount_sat: *mut u64, + swapper_feerate: *mut f64, } #[repr(C)] #[derive(Clone, Copy)] diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 1cd4bbd..afeb4a1 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use anyhow::{anyhow, Result}; +use boltz_client::boltz::ChainPair; use boltz_client::{ bitcoin::ScriptBuf, network::Chain, @@ -302,7 +303,31 @@ pub struct PrepareReceiveRequest { pub struct PrepareReceiveResponse { pub payment_method: PaymentMethod, pub payer_amount_sat: Option, + + /// Generally represents the total fees that would be paid to send or receive this payment. + /// + /// In case of Zero-Amount Receive Chain swaps, the swapper service fee (`swapper_feerate` times + /// the amount) is paid in addition to `fees_sat`. The swapper service feerate is already known + /// in the beginning, but the exact swapper service fee will only be known when the + /// `payer_amount_sat` is known. + /// + /// In all other types of swaps, the swapper service fee is included in `fees_sat`. pub fees_sat: u64, + + /// The minimum amount the payer can send for this swap to succeed. + /// + /// When the method is [PaymentMethod::LiquidAddress], this is empty. + pub min_payer_amount_sat: Option, + + /// The maximum amount the payer can send for this swap to succeed. + /// + /// When the method is [PaymentMethod::LiquidAddress], this is empty. + pub max_payer_amount_sat: Option, + + /// The percentage of the sent amount that will count towards the service fee. + /// + /// When the method is [PaymentMethod::LiquidAddress], this is empty. + pub swapper_feerate: Option, } /// An argument when calling [crate::sdk::LiquidSdk::receive_payment]. @@ -671,6 +696,13 @@ impl ChainSwap { }) } + pub(crate) fn get_boltz_pair(&self) -> Result { + let pair: ChainPair = serde_json::from_str(&self.pair_fees_json) + .map_err(|e| anyhow!("Failed to deserialize ChainPair: {e:?}"))?; + + Ok(pair) + } + pub(crate) fn get_claim_swap_script(&self) -> SdkResult { let chain_swap_details = self.get_boltz_create_response()?.claim_details; let our_pubkey = self.get_claim_keypair()?.public_key(); diff --git a/lib/core/src/persist/chain.rs b/lib/core/src/persist/chain.rs index de17488..4ec2a5f 100644 --- a/lib/core/src/persist/chain.rs +++ b/lib/core/src/persist/chain.rs @@ -267,6 +267,32 @@ impl Persister { Ok(()) } + /// Used for Zero-amount Receive Chain swaps, when we fetched the quote and we know how much + /// the sender locked up + pub(crate) fn update_zero_amount_swap_values( + &self, + swap_id: &str, + payer_amount_sat: u64, + receiver_amount_sat: u64, + ) -> Result<(), PaymentError> { + log::info!("Updating chain swap {swap_id}: payer_amount_sat = {payer_amount_sat}, receiver_amount_sat = {receiver_amount_sat}"); + let con: Connection = self.get_connection()?; + con.execute( + "UPDATE chain_swaps + SET + payer_amount_sat = :payer_amount_sat, + receiver_amount_sat = :receiver_amount_sat + WHERE + id = :id", + named_params! { + ":id": swap_id, + ":payer_amount_sat": payer_amount_sat, + ":receiver_amount_sat": receiver_amount_sat, + }, + )?; + Ok(()) + } + // Only set the Chain Swap claim_tx_id if not set, otherwise return an error pub(crate) fn set_chain_swap_claim_tx_id( &self, diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 9c75041..d6ad914 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -706,10 +706,12 @@ impl LiquidSdk { fn get_and_validate_chain_pair( &self, direction: Direction, - user_lockup_amount_sat: u64, + user_lockup_amount_sat: Option, ) -> Result { let pair = self.get_chain_pair(direction)?; - self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?; + if let Some(user_lockup_amount_sat) = user_lockup_amount_sat { + self.validate_user_lockup_amount_for_chain_pair(&pair, user_lockup_amount_sat)?; + } Ok(pair) } @@ -1631,6 +1633,9 @@ impl LiquidSdk { ) -> Result { self.ensure_is_started().await?; + let mut min_payer_amount_sat = None; + let mut max_payer_amount_sat = None; + let mut swapper_feerate = None; let fees_sat; match req.payment_method { PaymentMethod::Lightning => { @@ -1651,29 +1656,35 @@ impl LiquidSdk { .within(payer_amount_sat) .map_err(|_| PaymentError::AmountOutOfRange)?; + min_payer_amount_sat = Some(reverse_pair.limits.minimal); + max_payer_amount_sat = Some(reverse_pair.limits.maximal); + swapper_feerate = Some(reverse_pair.fees.percentage); + debug!( "Preparing Lightning Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat" ); } PaymentMethod::BitcoinAddress => { - let Some(payer_amount_sat) = req.payer_amount_sat else { - return Err(PaymentError::AmountMissing { err: "`payer_amount_sat` must be specified when `PaymentMethod::BitcoinAddress` is used.".to_string() }); - }; + let payer_amount_sat = req.payer_amount_sat; let pair = self.get_and_validate_chain_pair(Direction::Incoming, payer_amount_sat)?; let claim_fees_sat = pair.fees.claim_estimate(); let server_fees_sat = pair.fees.server(); - fees_sat = pair.fees.boltz(payer_amount_sat) + claim_fees_sat + server_fees_sat; - debug!( - "Preparing Chain Receive Swap with: payer_amount_sat {payer_amount_sat} sat, fees_sat {fees_sat} sat" - ); + let service_fees_sat = payer_amount_sat + .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat)) + .unwrap_or_default(); + + min_payer_amount_sat = Some(pair.limits.minimal); + max_payer_amount_sat = Some(pair.limits.maximal); + swapper_feerate = Some(pair.fees.percentage); + + fees_sat = service_fees_sat + claim_fees_sat + server_fees_sat; + debug!("Preparing Chain Receive Swap with: payer_amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}"); } PaymentMethod::LiquidAddress => { fees_sat = 0; - debug!( - "Preparing Liquid Receive Swap with: amount_sat {:?} sat, fees_sat {fees_sat} sat", - req.payer_amount_sat - ); + let payer_amount_sat = req.payer_amount_sat; + debug!("Preparing Liquid Receive Swap with: amount_sat {payer_amount_sat:?}, fees_sat {fees_sat}"); } }; @@ -1681,6 +1692,9 @@ impl LiquidSdk { payer_amount_sat: req.payer_amount_sat, fees_sat, payment_method: req.payment_method.clone(), + min_payer_amount_sat, + max_payer_amount_sat, + swapper_feerate, }) } @@ -1708,6 +1722,7 @@ impl LiquidSdk { payment_method, payer_amount_sat: amount_sat, fees_sat, + .. } = &req.prepare_response; match payment_method { @@ -1733,12 +1748,7 @@ impl LiquidSdk { self.create_receive_swap(*amount_sat, *fees_sat, description, description_hash) .await } - PaymentMethod::BitcoinAddress => { - let Some(amount_sat) = amount_sat else { - return Err(PaymentError::AmountMissing { err: "`amount_sat` must be specified when `PaymentMethod::BitcoinAddress` is used.".to_string() }); - }; - self.receive_onchain(*amount_sat, *fees_sat).await - } + PaymentMethod::BitcoinAddress => self.receive_onchain(*amount_sat, *fees_sat).await, PaymentMethod::LiquidAddress => { let address = self.onchain_wallet.next_unused_address().await?.to_string(); @@ -1898,15 +1908,19 @@ impl LiquidSdk { async fn create_receive_chain_swap( &self, - user_lockup_amount_sat: u64, + user_lockup_amount_sat: Option, fees_sat: u64, ) -> Result { let pair = self.get_and_validate_chain_pair(Direction::Incoming, user_lockup_amount_sat)?; let claim_fees_sat = pair.fees.claim_estimate(); let server_fees_sat = pair.fees.server(); + // Service fees are 0 if this is a zero-amount swap + let service_fees_sat = user_lockup_amount_sat + .map(|user_lockup_amount_sat| pair.fees.boltz(user_lockup_amount_sat)) + .unwrap_or_default(); ensure_sdk!( - fees_sat == pair.fees.boltz(user_lockup_amount_sat) + claim_fees_sat + server_fees_sat, + fees_sat == service_fees_sat + claim_fees_sat + server_fees_sat, PaymentError::InvalidOrExpiredFees ); @@ -1938,7 +1952,7 @@ impl LiquidSdk { preimage_hash: preimage.sha256, claim_public_key: Some(claim_public_key), refund_public_key: Some(refund_public_key), - user_lock_amount: Some(user_lockup_amount_sat), + user_lock_amount: user_lockup_amount_sat, server_lock_amount: None, pair_hash: Some(pair.hash.clone()), referral_id: None, @@ -1949,8 +1963,12 @@ impl LiquidSdk { let create_response_json = ChainSwap::from_boltz_struct_to_json(&create_response, &swap_id)?; - let accept_zero_conf = user_lockup_amount_sat <= pair.limits.maximal_zero_conf; - let receiver_amount_sat = user_lockup_amount_sat - fees_sat; + let accept_zero_conf = user_lockup_amount_sat + .map(|user_lockup_amount_sat| user_lockup_amount_sat <= pair.limits.maximal_zero_conf) + .unwrap_or(false); + let receiver_amount_sat = user_lockup_amount_sat + .map(|user_lockup_amount_sat| user_lockup_amount_sat - fees_sat) + .unwrap_or(0); let swap = ChainSwap { id: swap_id.clone(), @@ -1960,7 +1978,7 @@ impl LiquidSdk { timeout_block_height: create_response.lockup_details.timeout_block_height, preimage: preimage_str, description: Some("Bitcoin transfer".to_string()), - payer_amount_sat: user_lockup_amount_sat, + payer_amount_sat: user_lockup_amount_sat.unwrap_or(0), receiver_amount_sat, claim_fees_sat, pair_fees_json: serde_json::to_string(&pair).map_err(|e| { @@ -1983,15 +2001,18 @@ impl LiquidSdk { } /// Receive from a Bitcoin transaction via a chain swap. + /// + /// If no `user_lockup_amount_sat` is specified, this is an amountless swap and `fees_sat` exclude + /// the service fees. async fn receive_onchain( &self, - payer_amount_sat: u64, + user_lockup_amount_sat: Option, fees_sat: u64, ) -> Result { self.ensure_is_started().await?; let swap = self - .create_receive_chain_swap(payer_amount_sat, fees_sat) + .create_receive_chain_swap(user_lockup_amount_sat, fees_sat) .await?; let create_response = swap.get_boltz_create_response()?; let address = create_response.lockup_details.lockup_address; @@ -2170,7 +2191,7 @@ impl LiquidSdk { let swap = self .create_receive_chain_swap( - req.prepare_response.amount_sat, + Some(req.prepare_response.amount_sat), req.prepare_response.fees_sat, ) .await?; diff --git a/lib/core/src/swapper/boltz/mod.rs b/lib/core/src/swapper/boltz/mod.rs index 4698d8c..a914638 100644 --- a/lib/core/src/swapper/boltz/mod.rs +++ b/lib/core/src/swapper/boltz/mod.rs @@ -8,6 +8,7 @@ use boltz_client::{ elements::secp256k1_zkp::{MusigPartialSignature, MusigPubNonce}, network::{electrum::ElectrumConfig, Chain}, util::secrets::Preimage, + Amount, }; use log::info; use url::Url; @@ -179,6 +180,23 @@ impl Swapper for BoltzSwapper { Ok((pair_outgoing, pair_incoming)) } + 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)) + .map_err(Into::into) + } + + fn accept_zero_amount_chain_swap_quote( + &self, + swap_id: &str, + server_lockup_sat: u64, + ) -> Result<(), PaymentError> { + self.client + .accept_quote(swap_id, server_lockup_sat) + .map_err(Into::into) + } + /// Get a submarine pair information fn get_submarine_pairs(&self) -> Result, PaymentError> { Ok(self.client.get_submarine_pairs()?.get_lbtc_to_btc_pair()) diff --git a/lib/core/src/swapper/mod.rs b/lib/core/src/swapper/mod.rs index bc8ab01..bf67d9a 100644 --- a/lib/core/src/swapper/mod.rs +++ b/lib/core/src/swapper/mod.rs @@ -8,6 +8,7 @@ use boltz_client::{ SubmarineClaimTxResponse, SubmarinePair, }, network::Chain, + Amount, }; use tokio::sync::{broadcast, watch}; @@ -40,6 +41,19 @@ pub trait Swapper: Send + Sync { /// Get the current rate, limits and fees for both swap directions fn get_chain_pairs(&self) -> Result<(Option, Option), PaymentError>; + /// Get the quote for a Zero-Amount Receive Chain Swap. + /// + /// 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; + + /// Accept a specific quote for a Zero-Amount Receive Chain Swap + fn accept_zero_amount_chain_swap_quote( + &self, + swap_id: &str, + server_lockup_sat: u64, + ) -> Result<(), PaymentError>; + /// Get a submarine pair information fn get_submarine_pairs(&self) -> Result, PaymentError>; diff --git a/lib/core/src/test_utils/swapper.rs b/lib/core/src/test_utils/swapper.rs index e4ba678..68c3551 100644 --- a/lib/core/src/test_utils/swapper.rs +++ b/lib/core/src/test_utils/swapper.rs @@ -8,7 +8,7 @@ use boltz_client::{ SubmarinePair, SwapTree, }, util::secrets::Preimage, - PublicKey, + Amount, PublicKey, }; use sdk_common::invoice::parse_invoice; @@ -313,4 +313,16 @@ impl Swapper for MockSwapper { fn get_bolt12_invoice(&self, _offer: &str, _amount_sat: u64) -> Result { unimplemented!() } + + fn get_zero_amount_chain_swap_quote(&self, swap_id: &str) -> Result { + unimplemented!() + } + + fn accept_zero_amount_chain_swap_quote( + &self, + _swap_id: &str, + _server_lockup_sat: u64, + ) -> Result<(), PaymentError> { + unimplemented!() + } } diff --git a/packages/dart/lib/src/frb_generated.dart b/packages/dart/lib/src/frb_generated.dart index 8ade849..97887a9 100644 --- a/packages/dart/lib/src/frb_generated.dart +++ b/packages/dart/lib/src/frb_generated.dart @@ -1411,6 +1411,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_connect_request(raw); } + @protected + double dco_decode_box_autoadd_f_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + @protected GetPaymentRequest dco_decode_box_autoadd_get_payment_request(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2379,6 +2385,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_bool(raw); } + @protected + double? dco_decode_opt_box_autoadd_f_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_box_autoadd_f_64(raw); + } + @protected PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2691,11 +2703,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { PrepareReceiveResponse dco_decode_prepare_receive_response(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}'); + if (arr.length != 6) throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); return PrepareReceiveResponse( paymentMethod: dco_decode_payment_method(arr[0]), payerAmountSat: dco_decode_opt_box_autoadd_u_64(arr[1]), feesSat: dco_decode_u_64(arr[2]), + minPayerAmountSat: dco_decode_opt_box_autoadd_u_64(arr[3]), + maxPayerAmountSat: dco_decode_opt_box_autoadd_u_64(arr[4]), + swapperFeerate: dco_decode_opt_box_autoadd_f_64(arr[5]), ); } @@ -3291,6 +3306,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_connect_request(deserializer)); } + @protected + double sse_decode_box_autoadd_f_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_f_64(deserializer)); + } + @protected GetPaymentRequest sse_decode_box_autoadd_get_payment_request(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4303,6 +4324,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_f_64(deserializer)); + } else { + return null; + } + } + @protected PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -4664,8 +4696,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_paymentMethod = sse_decode_payment_method(deserializer); var var_payerAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); var var_feesSat = sse_decode_u_64(deserializer); + var var_minPayerAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); + var var_maxPayerAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer); + var var_swapperFeerate = sse_decode_opt_box_autoadd_f_64(deserializer); return PrepareReceiveResponse( - paymentMethod: var_paymentMethod, payerAmountSat: var_payerAmountSat, feesSat: var_feesSat); + paymentMethod: var_paymentMethod, + payerAmountSat: var_payerAmountSat, + feesSat: var_feesSat, + minPayerAmountSat: var_minPayerAmountSat, + maxPayerAmountSat: var_maxPayerAmountSat, + swapperFeerate: var_swapperFeerate); } @protected @@ -5308,6 +5348,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_connect_request(self, serializer); } + @protected + void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_f_64(self, serializer); + } + @protected void sse_encode_box_autoadd_get_payment_request(GetPaymentRequest self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6175,6 +6221,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_f_64(double? 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_f_64(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_i_64(PlatformInt64? self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6494,6 +6550,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_payment_method(self.paymentMethod, serializer); sse_encode_opt_box_autoadd_u_64(self.payerAmountSat, serializer); sse_encode_u_64(self.feesSat, serializer); + sse_encode_opt_box_autoadd_u_64(self.minPayerAmountSat, serializer); + sse_encode_opt_box_autoadd_u_64(self.maxPayerAmountSat, serializer); + sse_encode_opt_box_autoadd_f_64(self.swapperFeerate, serializer); } @protected diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index 44fd24b..1b5e6b0 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -107,6 +107,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected ConnectRequest dco_decode_box_autoadd_connect_request(dynamic raw); + @protected + double dco_decode_box_autoadd_f_64(dynamic raw); + @protected GetPaymentRequest dco_decode_box_autoadd_get_payment_request(dynamic raw); @@ -398,6 +401,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool? dco_decode_opt_box_autoadd_bool(dynamic raw); + @protected + double? dco_decode_opt_box_autoadd_f_64(dynamic raw); + @protected PlatformInt64? dco_decode_opt_box_autoadd_i_64(dynamic raw); @@ -654,6 +660,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected ConnectRequest sse_decode_box_autoadd_connect_request(SseDeserializer deserializer); + @protected + double sse_decode_box_autoadd_f_64(SseDeserializer deserializer); + @protected GetPaymentRequest sse_decode_box_autoadd_get_payment_request(SseDeserializer deserializer); @@ -945,6 +954,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool? sse_decode_opt_box_autoadd_bool(SseDeserializer deserializer); + @protected + double? sse_decode_opt_box_autoadd_f_64(SseDeserializer deserializer); + @protected PlatformInt64? sse_decode_opt_box_autoadd_i_64(SseDeserializer deserializer); @@ -1243,6 +1255,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return ptr; } + @protected + ffi.Pointer cst_encode_box_autoadd_f_64(double raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return wire.cst_new_box_autoadd_f_64(cst_encode_f_64(raw)); + } + @protected ffi.Pointer cst_encode_box_autoadd_get_payment_request( GetPaymentRequest raw) { @@ -1708,6 +1726,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return raw == null ? ffi.nullptr : cst_encode_box_autoadd_bool(raw); } + @protected + ffi.Pointer cst_encode_opt_box_autoadd_f_64(double? raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return raw == null ? ffi.nullptr : cst_encode_box_autoadd_f_64(raw); + } + @protected ffi.Pointer cst_encode_opt_box_autoadd_i_64(PlatformInt64? raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -2895,6 +2919,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { wireObj.payment_method = cst_encode_payment_method(apiObj.paymentMethod); wireObj.payer_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.payerAmountSat); wireObj.fees_sat = cst_encode_u_64(apiObj.feesSat); + wireObj.min_payer_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.minPayerAmountSat); + wireObj.max_payer_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.maxPayerAmountSat); + wireObj.swapper_feerate = cst_encode_opt_box_autoadd_f_64(apiObj.swapperFeerate); } @protected @@ -3308,6 +3335,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_connect_request(ConnectRequest self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer); + @protected void sse_encode_box_autoadd_get_payment_request(GetPaymentRequest self, SseSerializer serializer); @@ -3607,6 +3637,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_bool(bool? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_f_64(double? self, SseSerializer serializer); + @protected void sse_encode_opt_box_autoadd_i_64(PlatformInt64? self, SseSerializer serializer); @@ -4689,6 +4722,20 @@ class RustLibWire implements BaseWire { late final _cst_new_box_autoadd_connect_request = _cst_new_box_autoadd_connect_requestPtr.asFunction Function()>(); + ffi.Pointer cst_new_box_autoadd_f_64( + double value, + ) { + return _cst_new_box_autoadd_f_64( + value, + ); + } + + late final _cst_new_box_autoadd_f_64Ptr = + _lookup Function(ffi.Double)>>( + 'frbgen_breez_liquid_cst_new_box_autoadd_f_64'); + late final _cst_new_box_autoadd_f_64 = + _cst_new_box_autoadd_f_64Ptr.asFunction Function(double)>(); + ffi.Pointer cst_new_box_autoadd_get_payment_request() { return _cst_new_box_autoadd_get_payment_request(); } @@ -5744,6 +5791,12 @@ final class wire_cst_prepare_receive_response extends ffi.Struct { @ffi.Uint64() external int fees_sat; + + external ffi.Pointer min_payer_amount_sat; + + external ffi.Pointer max_payer_amount_sat; + + external ffi.Pointer swapper_feerate; } final class wire_cst_receive_payment_request extends ffi.Struct { diff --git a/packages/dart/lib/src/model.dart b/packages/dart/lib/src/model.dart index 8e9f972..3f17f44 100644 --- a/packages/dart/lib/src/model.dart +++ b/packages/dart/lib/src/model.dart @@ -936,16 +936,49 @@ class PrepareReceiveRequest { class PrepareReceiveResponse { final PaymentMethod paymentMethod; final BigInt? payerAmountSat; + + /// Generally represents the total fees that would be paid to send or receive this payment. + /// + /// In case of Zero-Amount Receive Chain swaps, the swapper service fee (`swapper_feerate` times + /// the amount) is paid in addition to `fees_sat`. The swapper service feerate is already known + /// in the beginning, but the exact swapper service fee will only be known when the + /// `payer_amount_sat` is known. + /// + /// In all other types of swaps, the swapper service fee is included in `fees_sat`. final BigInt feesSat; + /// The minimum amount the payer can send for this swap to succeed. + /// + /// When the method is [PaymentMethod::LiquidAddress], this is empty. + final BigInt? minPayerAmountSat; + + /// The maximum amount the payer can send for this swap to succeed. + /// + /// When the method is [PaymentMethod::LiquidAddress], this is empty. + final BigInt? maxPayerAmountSat; + + /// The percentage of the sent amount that will count towards the service fee. + /// + /// When the method is [PaymentMethod::LiquidAddress], this is empty. + final double? swapperFeerate; + const PrepareReceiveResponse({ required this.paymentMethod, this.payerAmountSat, required this.feesSat, + this.minPayerAmountSat, + this.maxPayerAmountSat, + this.swapperFeerate, }); @override - int get hashCode => paymentMethod.hashCode ^ payerAmountSat.hashCode ^ feesSat.hashCode; + int get hashCode => + paymentMethod.hashCode ^ + payerAmountSat.hashCode ^ + feesSat.hashCode ^ + minPayerAmountSat.hashCode ^ + maxPayerAmountSat.hashCode ^ + swapperFeerate.hashCode; @override bool operator ==(Object other) => @@ -954,7 +987,10 @@ class PrepareReceiveResponse { runtimeType == other.runtimeType && paymentMethod == other.paymentMethod && payerAmountSat == other.payerAmountSat && - feesSat == other.feesSat; + feesSat == other.feesSat && + minPayerAmountSat == other.minPayerAmountSat && + maxPayerAmountSat == other.maxPayerAmountSat && + swapperFeerate == other.swapperFeerate; } /// An argument when calling [crate::sdk::LiquidSdk::prepare_refund]. diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 171c0bd..7580437 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -939,6 +939,20 @@ class FlutterBreezLiquidBindings { _frbgen_breez_liquid_cst_new_box_autoadd_connect_requestPtr .asFunction Function()>(); + ffi.Pointer frbgen_breez_liquid_cst_new_box_autoadd_f_64( + double value, + ) { + return _frbgen_breez_liquid_cst_new_box_autoadd_f_64( + value, + ); + } + + late final _frbgen_breez_liquid_cst_new_box_autoadd_f_64Ptr = + _lookup Function(ffi.Double)>>( + 'frbgen_breez_liquid_cst_new_box_autoadd_f_64'); + late final _frbgen_breez_liquid_cst_new_box_autoadd_f_64 = + _frbgen_breez_liquid_cst_new_box_autoadd_f_64Ptr.asFunction Function(double)>(); + ffi.Pointer frbgen_breez_liquid_cst_new_box_autoadd_get_payment_request() { return _frbgen_breez_liquid_cst_new_box_autoadd_get_payment_request(); } @@ -4327,6 +4341,12 @@ final class wire_cst_prepare_receive_response extends ffi.Struct { @ffi.Uint64() external int fees_sat; + + external ffi.Pointer min_payer_amount_sat; + + external ffi.Pointer max_payer_amount_sat; + + external ffi.Pointer swapper_feerate; } final class wire_cst_receive_payment_request extends ffi.Struct { diff --git a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt index e692ead..2959f6f 100644 --- a/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt +++ b/packages/react-native/android/src/main/java/com/breezsdkliquid/BreezSDKLiquidMapper.kt @@ -1687,6 +1687,8 @@ fun asPrepareReceiveResponse(prepareReceiveResponse: ReadableMap): PrepareReceiv ) { return null } + val paymentMethod = prepareReceiveResponse.getString("paymentMethod")?.let { asPaymentMethod(it) }!! + val feesSat = prepareReceiveResponse.getDouble("feesSat").toULong() val payerAmountSat = if (hasNonNullKey( prepareReceiveResponse, @@ -1697,16 +1699,47 @@ fun asPrepareReceiveResponse(prepareReceiveResponse: ReadableMap): PrepareReceiv } else { null } - val paymentMethod = prepareReceiveResponse.getString("paymentMethod")?.let { asPaymentMethod(it) }!! - val feesSat = prepareReceiveResponse.getDouble("feesSat").toULong() - return PrepareReceiveResponse(payerAmountSat, paymentMethod, feesSat) + val minPayerAmountSat = + if (hasNonNullKey( + prepareReceiveResponse, + "minPayerAmountSat", + ) + ) { + prepareReceiveResponse.getDouble("minPayerAmountSat").toULong() + } else { + null + } + val maxPayerAmountSat = + if (hasNonNullKey( + prepareReceiveResponse, + "maxPayerAmountSat", + ) + ) { + prepareReceiveResponse.getDouble("maxPayerAmountSat").toULong() + } else { + null + } + val swapperFeerate = + if (hasNonNullKey( + prepareReceiveResponse, + "swapperFeerate", + ) + ) { + prepareReceiveResponse.getDouble("swapperFeerate") + } else { + null + } + return PrepareReceiveResponse(paymentMethod, feesSat, payerAmountSat, minPayerAmountSat, maxPayerAmountSat, swapperFeerate) } fun readableMapOf(prepareReceiveResponse: PrepareReceiveResponse): ReadableMap = readableMapOf( - "payerAmountSat" to prepareReceiveResponse.payerAmountSat, "paymentMethod" to prepareReceiveResponse.paymentMethod.name.lowercase(), "feesSat" to prepareReceiveResponse.feesSat, + "payerAmountSat" to prepareReceiveResponse.payerAmountSat, + "minPayerAmountSat" to prepareReceiveResponse.minPayerAmountSat, + "maxPayerAmountSat" to prepareReceiveResponse.maxPayerAmountSat, + "swapperFeerate" to prepareReceiveResponse.swapperFeerate, ) fun asPrepareReceiveResponseList(arr: ReadableArray): List { diff --git a/packages/react-native/ios/BreezSDKLiquidMapper.swift b/packages/react-native/ios/BreezSDKLiquidMapper.swift index 57ccf64..12b0419 100644 --- a/packages/react-native/ios/BreezSDKLiquidMapper.swift +++ b/packages/react-native/ios/BreezSDKLiquidMapper.swift @@ -1972,13 +1972,6 @@ enum BreezSDKLiquidMapper { } static func asPrepareReceiveResponse(prepareReceiveResponse: [String: Any?]) throws -> PrepareReceiveResponse { - var payerAmountSat: UInt64? - if hasNonNilKey(data: prepareReceiveResponse, key: "payerAmountSat") { - guard let payerAmountSatTmp = prepareReceiveResponse["payerAmountSat"] as? UInt64 else { - throw SdkError.Generic(message: errUnexpectedValue(fieldName: "payerAmountSat")) - } - payerAmountSat = payerAmountSatTmp - } guard let paymentMethodTmp = prepareReceiveResponse["paymentMethod"] as? String else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentMethod", typeName: "PrepareReceiveResponse")) } @@ -1987,15 +1980,46 @@ enum BreezSDKLiquidMapper { guard let feesSat = prepareReceiveResponse["feesSat"] as? UInt64 else { throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "feesSat", typeName: "PrepareReceiveResponse")) } + var payerAmountSat: UInt64? + if hasNonNilKey(data: prepareReceiveResponse, key: "payerAmountSat") { + guard let payerAmountSatTmp = prepareReceiveResponse["payerAmountSat"] as? UInt64 else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "payerAmountSat")) + } + payerAmountSat = payerAmountSatTmp + } + var minPayerAmountSat: UInt64? + if hasNonNilKey(data: prepareReceiveResponse, key: "minPayerAmountSat") { + guard let minPayerAmountSatTmp = prepareReceiveResponse["minPayerAmountSat"] as? UInt64 else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "minPayerAmountSat")) + } + minPayerAmountSat = minPayerAmountSatTmp + } + var maxPayerAmountSat: UInt64? + if hasNonNilKey(data: prepareReceiveResponse, key: "maxPayerAmountSat") { + guard let maxPayerAmountSatTmp = prepareReceiveResponse["maxPayerAmountSat"] as? UInt64 else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "maxPayerAmountSat")) + } + maxPayerAmountSat = maxPayerAmountSatTmp + } + var swapperFeerate: Double? + if hasNonNilKey(data: prepareReceiveResponse, key: "swapperFeerate") { + guard let swapperFeerateTmp = prepareReceiveResponse["swapperFeerate"] as? Double else { + throw SdkError.Generic(message: errUnexpectedValue(fieldName: "swapperFeerate")) + } + swapperFeerate = swapperFeerateTmp + } - return PrepareReceiveResponse(payerAmountSat: payerAmountSat, paymentMethod: paymentMethod, feesSat: feesSat) + return PrepareReceiveResponse(paymentMethod: paymentMethod, feesSat: feesSat, payerAmountSat: payerAmountSat, minPayerAmountSat: minPayerAmountSat, maxPayerAmountSat: maxPayerAmountSat, swapperFeerate: swapperFeerate) } static func dictionaryOf(prepareReceiveResponse: PrepareReceiveResponse) -> [String: Any?] { return [ - "payerAmountSat": prepareReceiveResponse.payerAmountSat == nil ? nil : prepareReceiveResponse.payerAmountSat, "paymentMethod": valueOf(paymentMethod: prepareReceiveResponse.paymentMethod), "feesSat": prepareReceiveResponse.feesSat, + "payerAmountSat": prepareReceiveResponse.payerAmountSat == nil ? nil : prepareReceiveResponse.payerAmountSat, + "minPayerAmountSat": prepareReceiveResponse.minPayerAmountSat == nil ? nil : prepareReceiveResponse.minPayerAmountSat, + "maxPayerAmountSat": prepareReceiveResponse.maxPayerAmountSat == nil ? nil : prepareReceiveResponse.maxPayerAmountSat, + "swapperFeerate": prepareReceiveResponse.swapperFeerate == nil ? nil : prepareReceiveResponse.swapperFeerate, ] } diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts index 908c277..23d8c85 100644 --- a/packages/react-native/src/index.ts +++ b/packages/react-native/src/index.ts @@ -299,9 +299,12 @@ export interface PrepareReceiveRequest { } export interface PrepareReceiveResponse { - payerAmountSat?: number paymentMethod: PaymentMethod feesSat: number + payerAmountSat?: number + minPayerAmountSat?: number + maxPayerAmountSat?: number + swapperFeerate?: number } export interface PrepareRefundRequest {