Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zero-amount Receive Chain Swap #538

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 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 @@ -263,13 +263,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.zero_amount_min_payer_amount_sat.unwrap();
let max = prepare_response.zero_amount_max_payer_amount_sat.unwrap();
let service_feerate = prepare_response.zero_amount_service_feerate.unwrap();
format!(
"Fees: {fees} sat + {service_feerate}% of the sent amount. \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the service feerate is going to be a 0.1 type value

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
2 changes: 1 addition & 1 deletion 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 @@ -317,6 +317,9 @@ typedef struct wire_cst_prepare_receive_response {
int32_t payment_method;
uint64_t *payer_amount_sat;
uint64_t fees_sat;
uint64_t *zero_amount_min_payer_amount_sat;
uint64_t *zero_amount_max_payer_amount_sat;
double *zero_amount_service_feerate;
} wire_cst_prepare_receive_response;

typedef struct wire_cst_receive_payment_request {
Expand Down Expand Up @@ -1134,6 +1137,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 @@ -1237,6 +1242,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 @@ -407,9 +407,12 @@ dictionary PrepareReceiveRequest {
};

dictionary PrepareReceiveResponse {
u64? payer_amount_sat;
PaymentMethod payment_method;
u64 fees_sat;
u64? payer_amount_sat;
u64? zero_amount_min_payer_amount_sat;
u64? zero_amount_max_payer_amount_sat;
f64? zero_amount_service_feerate;
Comment on lines +412 to +415
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prefix zero_amount is a bit confusing IMO.
Why do we need this prefix? Why not showing these values on all cases?
min/max and service fees seem reasonable to be shown also on cases where amount is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only weird case is when PrepareReceiveResponse.payment_method is LiquidAddress. Then min/max don't mean much.

Should I then set them to None in that case?

};

dictionary ReceivePaymentRequest {
Expand Down
3 changes: 2 additions & 1 deletion lib/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ frb = ["dep:flutter_rust_bridge"]
[dependencies]
anyhow = { workspace = true }
bip39 = "2.0.0"
boltz-client = { git = "https://github.com/SatoshiPortal/boltz-rust", branch = "trunk" }
#boltz-client = { git = "https://github.com/SatoshiPortal/boltz-rust", branch = "trunk" }
boltz-client = { git = "https://github.com/ok300/boltz-rust", branch = "ok300-support-quote" }
chrono = "0.4"
env_logger = "0.11"
flutter_rust_bridge = { version = "=2.4.0", features = [
Expand Down
25 changes: 24 additions & 1 deletion lib/core/src/chain_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,29 @@ 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
{
if let Err(e) = self
.swapper
.get_zero_amount_chain_swap_quote(&swap.id)
.map(|quote| quote.to_sat())
.and_then(|quote| {
info!("Got quote of {quote} sat for swap {}", &swap.id);

self.persister.update_swap_payer_amount(&swap.id, quote)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the quote here is the server lockup amount. If so, I think there needs to be some recalculations done. The receiver amount (quote - claim fees), fees (server fees + boltz fees + claim fees) and payer amount (receiver amount + claim fees + server fees + boltz fees) need calculating.

self.swapper
ok300 marked this conversation as resolved.
Show resolved Hide resolved
.accept_zero_amount_chain_swap_quote(&swap.id, quote)
})
{
warn!("Failed to accept the quote for swap {}: {e:?}", &swap.id);
}

// We successfully accepted the quote, the swap should continue as normal
return Ok(()); // Break from TxLockupFailed branch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems the error case arrives here too, we should allow it to be set to Refundable

}

match swap.refund_tx_id.clone() {
None => {
warn!("Chain Swap {id} is in an unrecoverable state: {swap_state:?}");
Expand All @@ -384,7 +407,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 Down
63 changes: 63 additions & 0 deletions lib/core/src/frb_generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3158,6 +3158,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 @@ -3599,10 +3610,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_zeroAmountMinPayerAmountSat = <Option<u64>>::sse_decode(deserializer);
let mut var_zeroAmountMaxPayerAmountSat = <Option<u64>>::sse_decode(deserializer);
let mut var_zeroAmountServiceFeerate = <Option<f64>>::sse_decode(deserializer);
return crate::model::PrepareReceiveResponse {
payment_method: var_paymentMethod,
payer_amount_sat: var_payerAmountSat,
fees_sat: var_feesSat,
zero_amount_min_payer_amount_sat: var_zeroAmountMinPayerAmountSat,
zero_amount_max_payer_amount_sat: var_zeroAmountMaxPayerAmountSat,
zero_amount_service_feerate: var_zeroAmountServiceFeerate,
};
}
}
Expand Down Expand Up @@ -5572,6 +5589,15 @@ 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.zero_amount_min_payer_amount_sat
.into_into_dart()
.into_dart(),
self.zero_amount_max_payer_amount_sat
.into_into_dart()
.into_dart(),
self.zero_amount_service_feerate
.into_into_dart()
.into_dart(),
]
.into_dart()
}
Expand Down Expand Up @@ -6959,6 +6985,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 @@ -7344,6 +7380,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.zero_amount_min_payer_amount_sat, serializer);
<Option<u64>>::sse_encode(self.zero_amount_max_payer_amount_sat, serializer);
<Option<f64>>::sse_encode(self.zero_amount_service_feerate, serializer);
}
}

Expand Down Expand Up @@ -7918,6 +7957,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 @@ -9114,6 +9159,13 @@ 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(),
zero_amount_min_payer_amount_sat: self
.zero_amount_min_payer_amount_sat
.cst_decode(),
zero_amount_max_payer_amount_sat: self
.zero_amount_max_payer_amount_sat
.cst_decode(),
zero_amount_service_feerate: self.zero_amount_service_feerate.cst_decode(),
}
}
}
Expand Down Expand Up @@ -10209,6 +10261,9 @@ mod io {
payment_method: Default::default(),
payer_amount_sat: core::ptr::null_mut(),
fees_sat: Default::default(),
zero_amount_min_payer_amount_sat: core::ptr::null_mut(),
zero_amount_max_payer_amount_sat: core::ptr::null_mut(),
zero_amount_service_feerate: core::ptr::null_mut(),
}
}
}
Expand Down Expand Up @@ -10983,6 +11038,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 @@ -12239,6 +12299,9 @@ mod io {
payment_method: i32,
payer_amount_sat: *mut u64,
fees_sat: u64,
zero_amount_min_payer_amount_sat: *mut u64,
zero_amount_max_payer_amount_sat: *mut u64,
zero_amount_service_feerate: *mut f64,
}
#[repr(C)]
#[derive(Clone, Copy)]
Expand Down
15 changes: 15 additions & 0 deletions lib/core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,21 @@ pub struct PrepareReceiveResponse {
pub payment_method: PaymentMethod,
pub payer_amount_sat: Option<u64>,
pub fees_sat: u64,

/// The minimum amount the payer can send for this swap to succeed.
///
/// Only applicable to Zero-Amount Receive Chain Swaps.
pub zero_amount_min_payer_amount_sat: Option<u64>,

/// The maximum amount the payer can send for this swap to succeed.
///
/// Only applicable to Zero-Amount Receive Chain Swaps.
pub zero_amount_max_payer_amount_sat: Option<u64>,

/// The percentage of the sent amount that will count towards the service fee.
///
/// Only applicable to Zero-Amount Receive Chain Swaps.
pub zero_amount_service_feerate: Option<f64>,
}

/// An argument when calling [crate::sdk::LiquidSdk::receive_payment].
Expand Down
37 changes: 37 additions & 0 deletions lib/core/src/persist/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,43 @@ impl Persister {
Ok(())
}

/// Used for Zero-amount Receive Chain swaps, when we fetched the quote and we know how much
/// the sender locked up
pub(crate) fn update_swap_payer_amount(
&self,
swap_id: &str,
payer_amount_sat: u64,
) -> Result<(), PaymentError> {
let swap = self
.fetch_chain_swap_by_id(swap_id)?
.ok_or_else(|| PaymentError::Generic {
err: format!("Cannot update non-existent chain swap with ID: {swap_id}"),
})?;
ensure_sdk!(
matches!(swap.direction, Direction::Incoming),
PaymentError::Generic {
err: format!(
"Can only update payer_amount_sat for incoming chain swaps. Swap ID: {swap_id}"
)
}
);
Comment on lines +273 to +285
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a part of the SQL where clause below


log::info!("Updating chain swap {swap_id}: payer_amount_sat = {payer_amount_sat}");
let con: Connection = self.get_connection()?;
con.execute(
"UPDATE chain_swaps
SET
payer_amount_sat = :payer_amount_sat
WHERE
id = :id",
named_params! {
":id": swap_id,
":payer_amount_sat": payer_amount_sat,
},
)?;
Ok(())
}

// Only set the Chain Swap claim_tx_id if not set, otherwise return an error
pub(crate) fn set_chain_swap_claim_tx_id(
&self,
Expand Down
Loading
Loading