fix: refund InsufficientFunds error when calculating broadcast fee (#360)

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
This commit is contained in:
yse
2024-07-08 17:30:17 +02:00
committed by GitHub
parent bf5828c6a6
commit 50be093230
8 changed files with 140 additions and 54 deletions

View File

@@ -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
/**

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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!(

View File

@@ -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)
}

View File

@@ -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;

View File

@@ -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;