Skip to content

Commit

Permalink
Allow immediate refund of failed incoming chain swap
Browse files Browse the repository at this point in the history
  • Loading branch information
dangeross committed Feb 7, 2025
1 parent a1d7ee8 commit 0cd68d5
Show file tree
Hide file tree
Showing 19 changed files with 145 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,8 @@ typedef struct wire_cst_refundable_swap {
struct wire_cst_list_prim_u_8_strict *swap_address;
uint32_t timestamp;
uint64_t amount_sat;
uint64_t confirmed_amount_sat;
int64_t unconfirmed_amount_sat;
struct wire_cst_list_prim_u_8_strict *last_refund_tx_id;
} wire_cst_refundable_swap;

Expand Down
2 changes: 2 additions & 0 deletions lib/bindings/src/breez_sdk_liquid.udl
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,8 @@ dictionary RefundableSwap {
string swap_address;
u32 timestamp;
u64 amount_sat;
u64 confirmed_amount_sat;
i64 unconfirmed_amount_sat;
string? last_refund_tx_id;
};

Expand Down
19 changes: 2 additions & 17 deletions lib/core/src/chain_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -977,27 +977,12 @@ impl ChainSwapHandler {

info!("Initiating refund for incoming Chain Swap {id}, is_cooperative: {is_cooperative}",);

let SwapScriptV2::Bitcoin(swap_script) = swap.get_lockup_swap_script()? else {
return Err(PaymentError::Generic {
err: "Unexpected swap script type found".to_string(),
});
};

let script_pk = swap_script
.to_address(self.config.network.as_bitcoin_chain())
.map_err(|e| anyhow!("Could not retrieve address from swap script: {e:?}"))?
.script_pubkey();
let utxos = self
.bitcoin_chain_service
.get_script_utxos(&script_pk)
.await?;

let SdkTransaction::Bitcoin(refund_tx) = self
.swapper
.create_refund_tx(
Swap::Chain(swap.clone()),
refund_address,
utxos,
None,
Some(broadcast_fee_rate_sat_per_vb as f64),
is_cooperative,
)
Expand Down Expand Up @@ -1069,7 +1054,7 @@ impl ChainSwapHandler {
.create_refund_tx(
Swap::Chain(swap.clone()),
&refund_address,
utxos,
Some(utxos),
None,
is_cooperative,
)
Expand Down
14 changes: 14 additions & 0 deletions lib/core/src/frb_generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4380,11 +4380,15 @@ impl SseDecode for crate::model::RefundableSwap {
let mut var_swapAddress = <String>::sse_decode(deserializer);
let mut var_timestamp = <u32>::sse_decode(deserializer);
let mut var_amountSat = <u64>::sse_decode(deserializer);
let mut var_confirmedAmountSat = <u64>::sse_decode(deserializer);
let mut var_unconfirmedAmountSat = <i64>::sse_decode(deserializer);
let mut var_lastRefundTxId = <Option<String>>::sse_decode(deserializer);
return crate::model::RefundableSwap {
swap_address: var_swapAddress,
timestamp: var_timestamp,
amount_sat: var_amountSat,
confirmed_amount_sat: var_confirmedAmountSat,
unconfirmed_amount_sat: var_unconfirmedAmountSat,
last_refund_tx_id: var_lastRefundTxId,
};
}
Expand Down Expand Up @@ -6819,6 +6823,8 @@ impl flutter_rust_bridge::IntoDart for crate::model::RefundableSwap {
self.swap_address.into_into_dart().into_dart(),
self.timestamp.into_into_dart().into_dart(),
self.amount_sat.into_into_dart().into_dart(),
self.confirmed_amount_sat.into_into_dart().into_dart(),
self.unconfirmed_amount_sat.into_into_dart().into_dart(),
self.last_refund_tx_id.into_into_dart().into_dart(),
]
.into_dart()
Expand Down Expand Up @@ -8872,6 +8878,8 @@ impl SseEncode for crate::model::RefundableSwap {
<String>::sse_encode(self.swap_address, serializer);
<u32>::sse_encode(self.timestamp, serializer);
<u64>::sse_encode(self.amount_sat, serializer);
<u64>::sse_encode(self.confirmed_amount_sat, serializer);
<i64>::sse_encode(self.unconfirmed_amount_sat, serializer);
<Option<String>>::sse_encode(self.last_refund_tx_id, serializer);
}
}
Expand Down Expand Up @@ -11039,6 +11047,8 @@ mod io {
swap_address: self.swap_address.cst_decode(),
timestamp: self.timestamp.cst_decode(),
amount_sat: self.amount_sat.cst_decode(),
confirmed_amount_sat: self.confirmed_amount_sat.cst_decode(),
unconfirmed_amount_sat: self.unconfirmed_amount_sat.cst_decode(),
last_refund_tx_id: self.last_refund_tx_id.cst_decode(),
}
}
Expand Down Expand Up @@ -12411,6 +12421,8 @@ mod io {
swap_address: core::ptr::null_mut(),
timestamp: Default::default(),
amount_sat: Default::default(),
confirmed_amount_sat: Default::default(),
unconfirmed_amount_sat: Default::default(),
last_refund_tx_id: core::ptr::null_mut(),
}
}
Expand Down Expand Up @@ -14763,6 +14775,8 @@ mod io {
swap_address: *mut wire_cst_list_prim_u_8_strict,
timestamp: u32,
amount_sat: u64,
confirmed_amount_sat: u64,
unconfirmed_amount_sat: i64,
last_refund_tx_id: *mut wire_cst_list_prim_u_8_strict,
}
#[repr(C)]
Expand Down
27 changes: 13 additions & 14 deletions lib/core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,11 +969,18 @@ impl ChainSwap {
Ok(script_pubkey)
}

pub(crate) fn to_refundable(&self, refundable_amount_sat: u64) -> RefundableSwap {
pub(crate) fn to_refundable(
&self,
amount_sat: u64,
confirmed_amount_sat: u64,
unconfirmed_amount_sat: i64,
) -> RefundableSwap {
RefundableSwap {
swap_address: self.lockup_address.clone(),
timestamp: self.created_at,
amount_sat: refundable_amount_sat,
amount_sat,
confirmed_amount_sat,
unconfirmed_amount_sat,
last_refund_tx_id: self.refund_tx_id.clone(),
}
}
Expand Down Expand Up @@ -1220,6 +1227,10 @@ pub struct RefundableSwap {
pub timestamp: u32,
/// Amount that is refundable, from all UTXOs
pub amount_sat: u64,
/// Confirmed amount, from all UTXOs
pub confirmed_amount_sat: u64,
/// Unconfirmed amount (negative is an outgoing amount), from all UTXOs
pub unconfirmed_amount_sat: i64,
/// The txid of the last broadcasted refund tx, if any
pub last_refund_tx_id: Option<String>,
}
Expand Down Expand Up @@ -1999,18 +2010,6 @@ pub enum Utxo {
}

impl Utxo {
pub(crate) fn as_bitcoin(
&self,
) -> Option<&(
boltz_client::bitcoin::OutPoint,
boltz_client::bitcoin::TxOut,
)> {
match self {
Utxo::Liquid(_) => None,
Utxo::Bitcoin(utxo) => Some(utxo),
}
}

pub(crate) fn as_liquid(
&self,
) -> Option<
Expand Down
12 changes: 6 additions & 6 deletions lib/core/src/recover/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,19 +214,19 @@ impl RecoveredOnchainDataChainReceive {
self.btc_user_lockup_amount_sat < limits.minimal
|| self.btc_user_lockup_amount_sat > limits.maximal
});
let is_refundable = self.btc_user_lockup_address_balance_sat > 0
&& (is_expired || unexpected_amount || amount_out_of_bounds);
let is_expired_refundable = is_expired && self.btc_user_lockup_address_balance_sat > 0;
let is_refundable = is_expired_refundable || unexpected_amount || amount_out_of_bounds;
match &self.btc_user_lockup_tx_id {
Some(_) => match (&self.lbtc_claim_tx_id, &self.btc_refund_tx_id) {
(Some(lbtc_claim_tx_id), None) => match lbtc_claim_tx_id.confirmed() {
true => match is_refundable {
true => match is_expired_refundable {
true => Some(PaymentState::Refundable),
false => Some(PaymentState::Complete),
},
false => Some(PaymentState::Pending),
},
(None, Some(btc_refund_tx_id)) => match btc_refund_tx_id.confirmed() {
true => match is_refundable {
true => match is_expired_refundable {
true => Some(PaymentState::Refundable),
false => Some(PaymentState::Failed),
},
Expand All @@ -235,7 +235,7 @@ impl RecoveredOnchainDataChainReceive {
(Some(lbtc_claim_tx_id), Some(btc_refund_tx_id)) => {
match lbtc_claim_tx_id.confirmed() {
true => match btc_refund_tx_id.confirmed() {
true => match is_refundable {
true => match is_expired_refundable {
true => Some(PaymentState::Refundable),
false => Some(PaymentState::Complete),
},
Expand All @@ -246,7 +246,7 @@ impl RecoveredOnchainDataChainReceive {
}
(None, None) => match is_refundable {
true => Some(PaymentState::Refundable),
false => match is_waiting_fee_acceptance && !amount_out_of_bounds {
false => match is_waiting_fee_acceptance {
true => Some(PaymentState::WaitingFeeAcceptance),
false => Some(PaymentState::Pending),
},
Expand Down
16 changes: 13 additions & 3 deletions lib/core/src/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2421,10 +2421,20 @@ impl LiquidSdk {
let mut refundables = vec![];
for (chain_swap, script_balance) in chain_swaps.into_iter().zip(scripts_balance) {
let swap_id = &chain_swap.id;
let refundable_confirmed_sat = script_balance.confirmed;
info!("Incoming Chain Swap {swap_id} is refundable with {refundable_confirmed_sat} confirmed sats");
let refundable_amount_sat = script_balance
.confirmed
.saturating_add_signed(script_balance.unconfirmed);
info!("Incoming Chain Swap {swap_id} is refundable with ({} confirmed {} unconfirmed) {} sats",
script_balance.confirmed,
script_balance.unconfirmed,
refundable_amount_sat,
);

let refundable: RefundableSwap = chain_swap.to_refundable(refundable_confirmed_sat);
let refundable: RefundableSwap = chain_swap.to_refundable(
refundable_amount_sat,
script_balance.confirmed,
script_balance.unconfirmed,
);
refundables.push(refundable);
}

Expand Down
2 changes: 1 addition & 1 deletion lib/core/src/send_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ impl SendSwapHandler {
.create_refund_tx(
Swap::Send(swap.clone()),
&refund_address,
utxos,
Some(utxos),
None,
is_cooperative,
)
Expand Down
53 changes: 9 additions & 44 deletions lib/core/src/swapper/boltz/bitcoin.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
use std::str::FromStr;

use boltz_client::{
bitcoin::{address::Address, Transaction},
boltz::SwapTxKind,
fees::Fee,
util::secrets::Preimage,
BtcSwapTx,
};
use boltz_client::{bitcoin::Transaction, fees::Fee, util::secrets::Preimage, BtcSwapTx};

use crate::{
ensure_sdk,
error::{PaymentError, SdkError},
prelude::{ChainSwap, Direction, Swap, Utxo},
prelude::{ChainSwap, Direction, Swap},
};

use super::{BoltzSwapper, ProxyUrlFetcher};
Expand Down Expand Up @@ -53,53 +46,25 @@ impl<P: ProxyUrlFetcher> BoltzSwapper<P> {

pub(crate) async fn new_btc_refund_tx(
&self,
swap: &ChainSwap,
chain_swap: &ChainSwap,
refund_address: &str,
utxos: Vec<Utxo>,
broadcast_fee_rate_sat_per_vb: f64,
is_cooperative: bool,
) -> Result<Transaction, SdkError> {
ensure_sdk!(
swap.direction == Direction::Incoming,
SdkError::generic("Cannot create BTC refund tx for outgoing Chain swaps.")
);

let address = Address::from_str(refund_address)
.map_err(|err| SdkError::generic(format!("Could not parse address: {err:?}")))?;

ensure_sdk!(
address.is_valid_for_network(self.config.network.into()),
SdkError::generic("Address network validation failed")
);

let utxos = utxos
.iter()
.filter_map(|utxo| utxo.as_bitcoin().cloned())
.collect();

let swap_script = swap.get_lockup_swap_script()?.as_bitcoin_script()?;
let refund_tx = BtcSwapTx {
kind: SwapTxKind::Refund,
swap_script,
output_address: address.assume_checked(),
utxos,
};

let refund_keypair = swap.get_refund_keypair()?;
let refund_tx_size = refund_tx.size(&refund_keypair, is_cooperative)?;
let broadcast_fees_sat = (refund_tx_size as f64 * broadcast_fee_rate_sat_per_vb) as u64;

let swap = Swap::Chain(chain_swap.clone());
let refund_tx_wrapper = self.new_btc_refund_wrapper(&swap, refund_address).await?;
let refund_keypair = chain_swap.get_refund_keypair()?;
let cooperative = match is_cooperative {
true => {
self.get_cooperative_details(swap.id.clone(), None, None)
self.get_cooperative_details(chain_swap.id.clone(), None, None)
.await?
}
false => None,
};

let signed_tx = refund_tx.sign_refund(
let signed_tx = refund_tx_wrapper.sign_refund(
&refund_keypair,
Fee::Absolute(broadcast_fees_sat),
Fee::Relative(broadcast_fee_rate_sat_per_vb),
cooperative,
)?;
Ok(signed_tx)
Expand Down
35 changes: 23 additions & 12 deletions lib/core/src/swapper/boltz/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ impl<P: ProxyUrlFetcher> Swapper for BoltzSwapper<P> {
&self,
swap: Swap,
refund_address: &str,
utxos: Vec<Utxo>,
utxos: Option<Vec<Utxo>>,
broadcast_fee_rate_sat_per_vb: Option<f64>,
is_cooperative: bool,
) -> Result<Transaction, PaymentError> {
Expand All @@ -413,31 +413,42 @@ impl<P: ProxyUrlFetcher> Swapper for BoltzSwapper<P> {
Swap::Chain(chain_swap) => match chain_swap.direction {
Direction::Incoming => {
let Some(broadcast_fee_rate_sat_per_vb) = broadcast_fee_rate_sat_per_vb else {
return Err(PaymentError::Generic {
err: format!("No broadcast fee rate provided when refunding incoming Chain Swap {swap_id}")
});
return Err(PaymentError::generic(&format!("No broadcast fee rate provided when refunding incoming Chain Swap {swap_id}")));
};

Transaction::Bitcoin(
self.new_btc_refund_tx(
chain_swap,
refund_address,
utxos,
broadcast_fee_rate_sat_per_vb,
is_cooperative,
)
.await?,
)
}
Direction::Outgoing => Transaction::Liquid(
Direction::Outgoing => {
let Some(utxos) = utxos else {
return Err(PaymentError::generic(&format!(
"No utxos provided for outgoing Chain Swap {swap_id} refund"
)));
};
Transaction::Liquid(
self.new_lbtc_refund_tx(&swap, refund_address, utxos, is_cooperative)
.await?,
)
}
},
Swap::Send(_) => {
let Some(utxos) = utxos else {
return Err(PaymentError::generic(&format!(
"No utxos provided for Send Swap {swap_id} refund"
)));
};
Transaction::Liquid(
self.new_lbtc_refund_tx(&swap, refund_address, utxos, is_cooperative)
.await?,
),
},
Swap::Send(_) => Transaction::Liquid(
self.new_lbtc_refund_tx(&swap, refund_address, utxos, is_cooperative)
.await?,
),
)
}
Swap::Receive(_) => {
return Err(PaymentError::Generic {
err: format!(
Expand Down
2 changes: 1 addition & 1 deletion lib/core/src/swapper/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ pub trait Swapper: Send + Sync {
&self,
swap: Swap,
refund_address: &str,
utxos: Vec<Utxo>,
utxos: Option<Vec<Utxo>>,
broadcast_fee_rate_sat_per_vb: Option<f64>,
is_cooperative: bool,
) -> Result<crate::prelude::Transaction, PaymentError>;
Expand Down
Loading

0 comments on commit 0cd68d5

Please sign in to comment.