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

Merged
merged 25 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
89e20d4
Add zero-amount Receive Chain Swap
ok300 Oct 25, 2024
bedc05d
prep-receive-payment: Add min/max fields for zero-amount swaps
ok300 Oct 29, 2024
92c403b
prep-receive-payment: Add service feerate for zero-amount swaps
ok300 Oct 29, 2024
445418c
Merge branch 'main' into ok300-amountless-chain-swaps
ok300 Nov 4, 2024
c1a556b
Get and accept quote to advance Zero-Amount Chain Swaps
ok300 Nov 4, 2024
333c5c5
Fix tests
ok300 Nov 4, 2024
05483a5
Re-generate flutter bindings
ok300 Nov 5, 2024
322972f
Re-generate RN bindings
ok300 Nov 5, 2024
35cb343
Update swap payer_amount when we know the quote
ok300 Nov 10, 2024
f201d6c
Merge branch 'main' into ok300-amountless-chain-swaps
ok300 Nov 26, 2024
d3f4488
Rename receive_onchain arg
ok300 Nov 26, 2024
1e18406
Rename PrepareReceiveResponse fields
ok300 Nov 27, 2024
ab921c4
Fix cargo clippy
ok300 Nov 27, 2024
5a44caf
Improve error handling when accepting quote
ok300 Nov 27, 2024
c597ced
Update zero-amount payer/receiver values in DB
ok300 Nov 27, 2024
98864c7
Set min/max/feerate in prepare response for both LN and Chain swaps
ok300 Nov 27, 2024
a8efee5
Merge branch 'main' into ok300-amountless-chain-swaps
ok300 Dec 6, 2024
2bfd81c
Validate fees amountless swaps before accepting quote
ok300 Dec 6, 2024
842df7a
Fix CLI fee arg handling
ok300 Dec 6, 2024
183c828
Calculate, persist payer/receiver amounts when quote is accepted
ok300 Dec 6, 2024
618879d
Update log comment
ok300 Dec 6, 2024
e8ac979
Bump boltz-rust to latest trunk
ok300 Dec 9, 2024
addde46
PrepareReceiveResponse: rename service_feerate to swapper_feerate
ok300 Dec 9, 2024
4576ad9
Tweak amountless swap quote validation
ok300 Dec 9, 2024
542dec7
Add docs for swapper_feerate handling in total fee
ok300 Dec 9, 2024
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
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!(
Copy link
Member

Choose a reason for hiding this comment

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

I guess we will need to document the fact that in amountless swap applications need to add the service fee on top of the fees_sat.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done: 542dec7

"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
23 changes: 23 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 @@ -303,6 +304,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.
///
/// 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 +687,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
Loading