mirror of
https://github.com/aljazceru/breez-sdk-liquid.git
synced 2025-12-24 09:24:25 +01:00
fix: refund InsufficientFunds error when calculating broadcast fee (#360)
Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<f32> {
|
||||
pub(crate) fn lowball_fee_rate_msat_per_vbyte(&self) -> Option<f32> {
|
||||
match self.network {
|
||||
LiquidNetwork::Mainnet => Some(LOWBALL_FEE_RATE_SAT_PER_VBYTE * 1000.0),
|
||||
LiquidNetwork::Testnet => None,
|
||||
|
||||
@@ -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<String, PaymentError> {
|
||||
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
|
||||
|
||||
@@ -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<String, PaymentError> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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<Transaction> {
|
||||
|err| anyhow!("Could not deserialize transaction: {err:?}"),
|
||||
)?)?)
|
||||
}
|
||||
|
||||
pub(crate) fn estimate_refund_fees(
|
||||
swap: &SendSwap,
|
||||
config: &Config,
|
||||
output_address: &str,
|
||||
is_cooperative: bool,
|
||||
) -> Result<u64, PaymentError> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2215,6 +2215,8 @@ final class wire_cst_send_payment_response extends ffi.Struct {
|
||||
/// EXTRA BEGIN
|
||||
typedef WireSyncRust2DartDco = ffi.Pointer<DartCObject>;
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user