diff --git a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h index 605cf09..0760477 100644 --- a/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h +++ b/lib/bindings/langs/flutter/breez_liquid_sdk/include/breez_liquid_sdk.h @@ -14,6 +14,8 @@ void store_dart_post_cobject(DartPostCObjectFnType ptr); // EXTRA END typedef struct _Dart_Handle* Dart_Handle; +#define STANDARD_FEE_RATE_SAT_PER_VBYTE 0.1 + #define LOWBALL_FEE_RATE_SAT_PER_VBYTE 0.01 /** diff --git a/lib/core/src/model.rs b/lib/core/src/model.rs index 20d4b7e..70a04da 100644 --- a/lib/core/src/model.rs +++ b/lib/core/src/model.rs @@ -17,6 +17,7 @@ use crate::receive_swap::{ }; use crate::utils; +pub const STANDARD_FEE_RATE_SAT_PER_VBYTE: f32 = 0.1; pub const LOWBALL_FEE_RATE_SAT_PER_VBYTE: f32 = 0.01; /// Configuration for the Liquid SDK @@ -68,7 +69,7 @@ impl Config { .unwrap_or(DEFAULT_ZERO_CONF_MAX_SAT) } - pub(crate) fn lowball_fee_rate(&self) -> Option { + pub(crate) fn lowball_fee_rate_msat_per_vbyte(&self) -> Option { match self.network { LiquidNetwork::Mainnet => Some(LOWBALL_FEE_RATE_SAT_PER_VBYTE * 1000.0), LiquidNetwork::Testnet => None, diff --git a/lib/core/src/sdk.rs b/lib/core/src/sdk.rs index 0045957..590d094 100644 --- a/lib/core/src/sdk.rs +++ b/lib/core/src/sdk.rs @@ -625,8 +625,12 @@ impl LiquidSdk { LiquidNetwork::Testnet => "tlq1pq0wqu32e2xacxeyps22x8gjre4qk3u6r70pj4r62hzczxeyz8x3yxucrpn79zy28plc4x37aaf33kwt6dz2nn6gtkya6h02mwpzy4eh69zzexq7cf5y5" }; - self.estimate_onchain_tx_fee(amount_sat, temp_p2tr_addr, self.config.lowball_fee_rate()) - .await + self.estimate_onchain_tx_fee( + amount_sat, + temp_p2tr_addr, + self.config.lowball_fee_rate_msat_per_vbyte(), + ) + .await } pub async fn prepare_send_payment( @@ -649,7 +653,7 @@ impl LiquidSdk { self.estimate_onchain_tx_fee( receiver_amount_sat, &lbtc_address, - self.config.lowball_fee_rate(), + self.config.lowball_fee_rate_msat_per_vbyte(), ) .await? } @@ -698,11 +702,9 @@ impl LiquidSdk { } async fn refund_send(&self, swap: &SendSwap) -> Result { - let amount_sat = get_invoice_amount!(swap.invoice); let output_address = self.onchain_wallet.next_unused_address().await?.to_string(); - let cooperative_refund_tx_fees_sat = self - .estimate_onchain_tx_fee(amount_sat, &output_address, self.config.lowball_fee_rate()) - .await?; + let cooperative_refund_tx_fees_sat = + utils::estimate_refund_fees(swap, &self.config, &output_address, true)?; let refund_res = self.swapper.refund_send_swap_cooperative( swap, &output_address, @@ -711,9 +713,8 @@ impl LiquidSdk { match refund_res { Ok(res) => Ok(res), Err(e) => { - let non_cooperative_refund_tx_fees_sat = self - .estimate_onchain_tx_fee(swap.receiver_amount_sat, &output_address, None) - .await?; + let non_cooperative_refund_tx_fees_sat = + utils::estimate_refund_fees(swap, &self.config, &output_address, false)?; warn!("Cooperative refund failed: {:?}", e); self.refund_send_non_cooperative(swap, non_cooperative_refund_tx_fees_sat) .await diff --git a/lib/core/src/send_swap.rs b/lib/core/src/send_swap.rs index c8e8980..7043b68 100644 --- a/lib/core/src/send_swap.rs +++ b/lib/core/src/send_swap.rs @@ -1,3 +1,4 @@ +use std::time::Duration; use std::{str::FromStr, sync::Arc}; use anyhow::{anyhow, Result}; @@ -18,12 +19,16 @@ use crate::model::PaymentState::{ use crate::model::{Config, SendSwap}; use crate::swapper::Swapper; use crate::wallet::OnchainWallet; -use crate::{ensure_sdk, get_invoice_amount}; +use crate::{ensure_sdk, utils}; use crate::{ error::PaymentError, model::{PaymentState, PaymentTxData, PaymentType}, persist::Persister, }; + +pub(crate) const MAX_REFUND_ATTEMPTS: u8 = 6; +pub(crate) const REFUND_REATTEMPT_DELAY_SECS: u64 = 10; + #[derive(Clone)] pub(crate) struct SendSwapStateHandler { config: Config, @@ -150,25 +155,45 @@ impl SendSwapStateHandler { | SubSwapStates::SwapExpired, ) => { match swap.lockup_tx_id { - Some(_) => match swap.refund_tx_id { - Some(refund_tx_id) => warn!( + Some(_) => { + match swap.refund_tx_id { + Some(refund_tx_id) => warn!( "Refund tx for Send Swap {id} was already broadcast: txid {refund_tx_id}" ), - None => { - warn!("Send Swap {id} is in an unrecoverable state: {swap_state:?}, and lockup tx has been broadcast. Attempting refund."); + None => { + warn!("Send Swap {id} is in an unrecoverable state: {swap_state:?}, and lockup tx has been broadcast. Attempting refund."); - let refund_tx_id = self.refund(&swap).await?; - info!("Broadcast refund tx for Send Swap {id}. Tx id: {refund_tx_id}"); - self.update_swap_info( - id, - RefundPending, - None, - None, - Some(&refund_tx_id), - ) - .await?; + let mut refund_attempts = 0; + while refund_attempts < MAX_REFUND_ATTEMPTS { + let refund_tx_id = match self.refund(&swap).await { + Ok(refund_tx_id) => refund_tx_id, + Err(e) => { + warn!("Could not refund yet: {e:?}. Re-attempting in {REFUND_REATTEMPT_DELAY_SECS} seconds."); + refund_attempts += 1; + std::thread::sleep(Duration::from_secs( + REFUND_REATTEMPT_DELAY_SECS, + )); + continue; + } + }; + info!("Broadcast refund tx for Send Swap {id}. Tx id: {refund_tx_id}"); + self.update_swap_info( + id, + RefundPending, + None, + None, + Some(&refund_tx_id), + ) + .await?; + break; + } + + if refund_attempts == MAX_REFUND_ATTEMPTS { + warn!("Failed to issue refunds: max attempts reached.") + } + } } - }, + } // Do not attempt broadcasting a refund if lockup tx was never sent and swap is // unrecoverable. We resolve the payment as failed. None => { @@ -204,7 +229,7 @@ impl SendSwapStateHandler { let lockup_tx = self .onchain_wallet .build_tx( - self.config.lowball_fee_rate(), + self.config.lowball_fee_rate_msat_per_vbyte(), &create_response.address, create_response.expected_amount, ) @@ -359,25 +384,24 @@ impl SendSwapStateHandler { } async fn refund(&self, swap: &SendSwap) -> Result { - let amount_sat = get_invoice_amount!(swap.invoice); let output_address = self.onchain_wallet.next_unused_address().await?.to_string(); - let fee = self - .onchain_wallet - .build_tx(None, &output_address, amount_sat) - .await? - .all_fees() - .values() - .sum(); + let cooperative_refund_tx_fees_sat = + utils::estimate_refund_fees(swap, &self.config, &output_address, true)?; + let refund_res = self.swapper.refund_send_swap_cooperative( + swap, + &output_address, + cooperative_refund_tx_fees_sat, + ); - let refund_res = self - .swapper - .refund_send_swap_cooperative(swap, &output_address, fee); match refund_res { Ok(res) => Ok(res), Err(e) => { warn!("Cooperative refund failed: {:?}", e); - self.refund_non_cooperative(swap, fee).await + let non_cooperative_refund_tx_fees_sat = + utils::estimate_refund_fees(swap, &self.config, &output_address, false)?; + self.refund_non_cooperative(swap, non_cooperative_refund_tx_fees_sat) + .await } } } diff --git a/lib/core/src/swapper/mod.rs b/lib/core/src/swapper/mod.rs index 12683fd..72bf944 100644 --- a/lib/core/src/swapper/mod.rs +++ b/lib/core/src/swapper/mod.rs @@ -388,13 +388,9 @@ impl BoltzSwapper { Amount::from_sat(broadcast_fees_sat), is_cooperative, )?; - let is_lowball = match self.config.network { - LiquidNetwork::Mainnet => None, - LiquidNetwork::Testnet => { - Some((&self.client, boltz_client::network::Chain::LiquidTestnet)) - } - }; - refund_tx.broadcast(&signed_tx, &self.liquid_electrum_config, is_lowball)? + // We attempt lowball broadcast when constructing the tx cooperatively + let lowball = Some((&self.client, self.config.network.into())); + refund_tx.broadcast(&signed_tx, &self.liquid_electrum_config, lowball)? } }; info!( @@ -462,13 +458,8 @@ impl BoltzSwapper { Amount::from_sat(broadcast_fees_sat), None, )?; - let is_lowball = match self.config.network { - LiquidNetwork::Mainnet => None, - LiquidNetwork::Testnet => { - Some((&self.client, boltz_client::network::Chain::LiquidTestnet)) - } - }; - refund_tx.broadcast(&signed_tx, &self.liquid_electrum_config, is_lowball)? + // We cannot broadcast lowball when constructing a non-cooperative tx + refund_tx.broadcast(&signed_tx, &self.liquid_electrum_config, None)? } }; info!( diff --git a/lib/core/src/utils.rs b/lib/core/src/utils.rs index a1aa1bd..736baf1 100644 --- a/lib/core/src/utils.rs +++ b/lib/core/src/utils.rs @@ -2,7 +2,16 @@ use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; use crate::error::{LiquidSdkResult, PaymentError}; +use crate::prelude::{ + Config, LiquidNetwork, SendSwap, LOWBALL_FEE_RATE_SAT_PER_VBYTE, + STANDARD_FEE_RATE_SAT_PER_VBYTE, +}; use anyhow::{anyhow, Result}; +use boltz_client::boltzv2::{ + BoltzApiClientV2, Cooperative, BOLTZ_MAINNET_URL_V2, BOLTZ_TESTNET_URL_V2, +}; +use boltz_client::network::electrum::ElectrumConfig; +use boltz_client::Amount; use lwk_wollet::elements::encode::deserialize; use lwk_wollet::elements::hex::FromHex; use lwk_wollet::elements::{ @@ -49,3 +58,57 @@ pub(crate) fn deserialize_tx_hex(tx_hex: &str) -> Result { |err| anyhow!("Could not deserialize transaction: {err:?}"), )?)?) } + +pub(crate) fn estimate_refund_fees( + swap: &SendSwap, + config: &Config, + output_address: &str, + is_cooperative: bool, +) -> Result { + let swap_script = swap.get_swap_script()?; + let electrum_config = ElectrumConfig::new( + config.network.into(), + &config.liquid_electrum_url, + true, + true, + 100, + ); + let swap_tx = boltz_client::LBtcSwapTxV2::new_refund( + swap_script, + &output_address.to_string(), + &electrum_config, + config.liquid_electrum_url.clone(), + swap.id.clone(), + )?; + let dummy_fees = Amount::from_sat(100); + + let boltz_api = &BoltzApiClientV2::new(match config.network { + LiquidNetwork::Mainnet => BOLTZ_MAINNET_URL_V2, + LiquidNetwork::Testnet => BOLTZ_TESTNET_URL_V2, + }); + + let (fee_rate, cooperative) = match (config.network, is_cooperative) { + (LiquidNetwork::Mainnet, true) => ( + LOWBALL_FEE_RATE_SAT_PER_VBYTE, + Some(Cooperative { + boltz_api, + swap_id: swap.id.clone(), + pub_nonce: None, + partial_sig: None, + }), + ), + (LiquidNetwork::Testnet, true) => ( + STANDARD_FEE_RATE_SAT_PER_VBYTE, + Some(Cooperative { + boltz_api, + swap_id: swap.id.clone(), + pub_nonce: None, + partial_sig: None, + }), + ), + (_, false) => (STANDARD_FEE_RATE_SAT_PER_VBYTE, None), + }; + let dummy_tx = swap_tx.sign_refund(&swap.get_refund_keypair()?, dummy_fees, cooperative)?; + + Ok((dummy_tx.vsize() as f32 * fee_rate).ceil() as u64) +} diff --git a/packages/dart/lib/src/frb_generated.io.dart b/packages/dart/lib/src/frb_generated.io.dart index c35e456..7c702c2 100644 --- a/packages/dart/lib/src/frb_generated.io.dart +++ b/packages/dart/lib/src/frb_generated.io.dart @@ -4944,6 +4944,8 @@ final class wire_cst_send_payment_response extends ffi.Struct { external wire_cst_payment payment; } +const double STANDARD_FEE_RATE_SAT_PER_VBYTE = 0.1; + const double LOWBALL_FEE_RATE_SAT_PER_VBYTE = 0.01; const double DEFAULT_ZERO_CONF_MIN_FEE_RATE_TESTNET = 0.1; diff --git a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart index 89cd0e6..eb7d13a 100644 --- a/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart +++ b/packages/flutter/lib/flutter_breez_liquid_bindings_generated.dart @@ -2215,6 +2215,8 @@ final class wire_cst_send_payment_response extends ffi.Struct { /// EXTRA BEGIN typedef WireSyncRust2DartDco = ffi.Pointer; +const double STANDARD_FEE_RATE_SAT_PER_VBYTE = 0.1; + const double LOWBALL_FEE_RATE_SAT_PER_VBYTE = 0.01; const double DEFAULT_ZERO_CONF_MIN_FEE_RATE_TESTNET = 0.1;