Skip to content

Commit

Permalink
Add zero-amount Receive Chain Swap (#538)
Browse files Browse the repository at this point in the history
Add support for Zero-Amount Receive Chain Swaps
  • Loading branch information
ok300 authored Dec 9, 2024
1 parent 790dfa9 commit cfc883a
Show file tree
Hide file tree
Showing 20 changed files with 570 additions and 59 deletions.
4 changes: 2 additions & 2 deletions cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 15 additions & 7 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions lib/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion lib/bindings/src/breez_sdk_liquid.udl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
92 changes: 91 additions & 1 deletion lib/core/src/chain_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:?}");
Expand All @@ -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(())
Expand All @@ -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;
Expand Down
53 changes: 53 additions & 0 deletions lib/core/src/frb_generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3291,6 +3291,17 @@ impl SseDecode for Option<bool> {
}
}

impl SseDecode for Option<f64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
if (<bool>::sse_decode(deserializer)) {
return Some(<f64>::sse_decode(deserializer));
} else {
return None;
}
}
}

impl SseDecode for Option<i64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
Expand Down Expand Up @@ -3747,10 +3758,16 @@ impl SseDecode for crate::model::PrepareReceiveResponse {
let mut var_paymentMethod = <crate::model::PaymentMethod>::sse_decode(deserializer);
let mut var_payerAmountSat = <Option<u64>>::sse_decode(deserializer);
let mut var_feesSat = <u64>::sse_decode(deserializer);
let mut var_minPayerAmountSat = <Option<u64>>::sse_decode(deserializer);
let mut var_maxPayerAmountSat = <Option<u64>>::sse_decode(deserializer);
let mut var_swapperFeerate = <Option<f64>>::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,
};
}
}
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -7282,6 +7302,16 @@ impl SseEncode for Option<bool> {
}
}

impl SseEncode for Option<f64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<bool>::sse_encode(self.is_some(), serializer);
if let Some(value) = self {
<f64>::sse_encode(value, serializer);
}
}
}

impl SseEncode for Option<i64> {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
Expand Down Expand Up @@ -7680,6 +7710,9 @@ impl SseEncode for crate::model::PrepareReceiveResponse {
<crate::model::PaymentMethod>::sse_encode(self.payment_method, serializer);
<Option<u64>>::sse_encode(self.payer_amount_sat, serializer);
<u64>::sse_encode(self.fees_sat, serializer);
<Option<u64>>::sse_encode(self.min_payer_amount_sat, serializer);
<Option<u64>>::sse_encode(self.max_payer_amount_sat, serializer);
<Option<f64>>::sse_encode(self.swapper_feerate, serializer);
}
}

Expand Down Expand Up @@ -8290,6 +8323,12 @@ mod io {
CstDecode::<crate::model::ConnectRequest>::cst_decode(*wrap).into()
}
}
impl CstDecode<f64> 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<crate::model::GetPaymentRequest> 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 {
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)]
Expand Down
32 changes: 32 additions & 0 deletions lib/core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::path::PathBuf;

use anyhow::{anyhow, Result};

use boltz_client::boltz::ChainPair;
use boltz_client::{
bitcoin::ScriptBuf,
network::Chain,
Expand Down Expand Up @@ -302,7 +303,31 @@ pub struct PrepareReceiveRequest {
pub struct PrepareReceiveResponse {
pub payment_method: PaymentMethod,
pub payer_amount_sat: Option<u64>,

/// 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<u64>,

/// 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<u64>,

/// 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<f64>,
}

/// An argument when calling [crate::sdk::LiquidSdk::receive_payment].
Expand Down Expand Up @@ -671,6 +696,13 @@ impl ChainSwap {
})
}

pub(crate) fn get_boltz_pair(&self) -> Result<ChainPair> {
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<SwapScriptV2> {
let chain_swap_details = self.get_boltz_create_response()?.claim_details;
let our_pubkey = self.get_claim_keypair()?.public_key();
Expand Down
Loading

0 comments on commit cfc883a

Please sign in to comment.