Skip to content

Commit

Permalink
Expose fees for review + auto accept
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgranhao committed Dec 20, 2024
1 parent 35c2b94 commit ab9ee4b
Show file tree
Hide file tree
Showing 29 changed files with 1,144 additions and 64 deletions.
27 changes: 26 additions & 1 deletion cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::sync::Arc;
use std::thread;
use std::time::Duration;

use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use breez_sdk_liquid::prelude::*;
use clap::{arg, Parser};
use qrcode_rs::render::unicode;
Expand Down Expand Up @@ -131,6 +131,14 @@ pub(crate) enum Command {
/// Lightning payment hash
payment_hash: String,
},
/// Get proposed fees for WaitingFeeAcceptance Payment
FetchPaymentProposedFees { swap_id: String },
/// Accept proposed fees for WaitingFeeAcceptance Payment
AcceptPaymentProposedFees {
swap_id: String,
// Fee amount obtained using FetchPaymentProposedFees
fees_sat: u64,
},
/// List refundable chain swaps
ListRefundables,
/// Prepare a refund transaction for an incomplete swap
Expand Down Expand Up @@ -519,6 +527,23 @@ pub(crate) async fn handle_command(
}
}
}
Command::FetchPaymentProposedFees { swap_id } => {
let res = sdk
.fetch_payment_proposed_fees(&FetchPaymentProposedFeesRequest { swap_id })
.await?;
command_result!(res)
}
Command::AcceptPaymentProposedFees { swap_id, fees_sat } => {
let res = sdk
.fetch_payment_proposed_fees(&FetchPaymentProposedFeesRequest { swap_id })
.await?;
if fees_sat != res.fees_sat {
bail!("Fees changed since they were fetched")
}
sdk.accept_payment_proposed_fees(&AcceptPaymentProposedFeesRequest { response: res })
.await?;
command_result!("Proposed fees accepted successfully")
}
Command::ListRefundables => {
let refundables = sdk.list_refundables().await?;
command_result!(refundables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ typedef struct _Dart_Handle* Dart_Handle;

#define ESTIMATED_BTC_CLAIM_TX_VSIZE 111

#define ESTIMATED_BTC_LOCKUP_TX_VSIZE 154

#define STANDARD_FEE_RATE_SAT_PER_VBYTE 0.1

#define LOWBALL_FEE_RATE_SAT_PER_VBYTE 0.01
Expand Down Expand Up @@ -481,13 +483,18 @@ typedef struct wire_cst_SdkEvent_PaymentWaitingConfirmation {
struct wire_cst_payment *details;
} wire_cst_SdkEvent_PaymentWaitingConfirmation;

typedef struct wire_cst_SdkEvent_PaymentWaitingFeeAcceptance {
struct wire_cst_payment *details;
} wire_cst_SdkEvent_PaymentWaitingFeeAcceptance;

typedef union SdkEventKind {
struct wire_cst_SdkEvent_PaymentFailed PaymentFailed;
struct wire_cst_SdkEvent_PaymentPending PaymentPending;
struct wire_cst_SdkEvent_PaymentRefunded PaymentRefunded;
struct wire_cst_SdkEvent_PaymentRefundPending PaymentRefundPending;
struct wire_cst_SdkEvent_PaymentSucceeded PaymentSucceeded;
struct wire_cst_SdkEvent_PaymentWaitingConfirmation PaymentWaitingConfirmation;
struct wire_cst_SdkEvent_PaymentWaitingFeeAcceptance PaymentWaitingFeeAcceptance;
} SdkEventKind;

typedef struct wire_cst_sdk_event {
Expand Down Expand Up @@ -519,6 +526,7 @@ typedef struct wire_cst_config {
struct wire_cst_list_prim_u_8_strict *breez_api_key;
struct wire_cst_list_external_input_parser *external_input_parsers;
bool use_default_external_input_parsers;
uint32_t *onchain_fee_rate_leeway_sat_per_vbyte;
} wire_cst_config;

typedef struct wire_cst_connect_request {
Expand Down
4 changes: 2 additions & 2 deletions lib/bindings/src/breez_sdk_liquid.udl
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ enum PaymentState {
"TimedOut",
"Refundable",
"RefundPending",
"WaitingUserAction",
"WaitingFeeAcceptance",
};

dictionary RefundableSwap {
Expand Down Expand Up @@ -755,7 +755,7 @@ interface BindingLiquidSdk {
[Throws=SdkError]
FetchPaymentProposedFeesResponse fetch_payment_proposed_fees(FetchPaymentProposedFeesRequest req);

[Throws=SdkError]
[Throws=PaymentError]
void accept_payment_proposed_fees(AcceptPaymentProposedFeesRequest req);

[Throws=SdkError]
Expand Down
14 changes: 14 additions & 0 deletions lib/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,20 @@ impl BindingLiquidSdk {
rt().block_on(self.sdk.get_payment(&req))
}

pub fn fetch_payment_proposed_fees(
&self,
req: FetchPaymentProposedFeesRequest,
) -> SdkResult<FetchPaymentProposedFeesResponse> {
rt().block_on(self.sdk.fetch_payment_proposed_fees(&req))
}

pub fn accept_payment_proposed_fees(
&self,
req: AcceptPaymentProposedFeesRequest,
) -> Result<(), PaymentError> {
rt().block_on(self.sdk.accept_payment_proposed_fees(&req))
}

pub fn prepare_lnurl_pay(
&self,
req: PrepareLnUrlPayRequest,
Expand Down
135 changes: 100 additions & 35 deletions lib/core/src/chain_swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ use crate::{
wallet::OnchainWallet,
};

// Estimates based on https://github.com/BoltzExchange/boltz-backend/blob/ee4c77be1fcb9bb2b45703c542ad67f7efbf218d/lib/rates/FeeProvider.ts#L78
// Estimates based on https://github.com/BoltzExchange/boltz-backend/blob/ee4c77be1fcb9bb2b45703c542ad67f7efbf218d/lib/rates/FeeProvider.ts#L68
pub const ESTIMATED_BTC_CLAIM_TX_VSIZE: u64 = 111;
pub const ESTIMATED_BTC_LOCKUP_TX_VSIZE: u64 = 154;

pub(crate) struct ChainSwapHandler {
config: Config,
Expand Down Expand Up @@ -487,17 +488,32 @@ impl ChainSwapHandler {
.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)
match self.validate_amountless_swap(swap, quote).await? {
ValidateAmountlessSwapResult::ReadyForAccepting {
user_lockup_amount_sat,
receiver_amount_sat,
} => {
self.persister.update_zero_amount_swap_values(
&swap.id,
user_lockup_amount_sat,
receiver_amount_sat,
)?;
self.swapper
.accept_zero_amount_chain_swap_quote(&swap.id, quote)
.map_err(Into::into)
}
ValidateAmountlessSwapResult::RequiresUserAction => {
self.update_swap_info(&swap.id, WaitingFeeAcceptance, None, None, None, None)
.await
}
}
}

async fn validate_and_update_amountless_swap(
async fn validate_amountless_swap(
&self,
swap: &ChainSwap,
quote_server_lockup_amount_sat: u64,
) -> Result<(), PaymentError> {
) -> Result<ValidateAmountlessSwapResult, PaymentError> {
debug!("Validating {swap:?}");

ensure_sdk!(
Expand Down Expand Up @@ -529,32 +545,40 @@ impl ChainSwapHandler {
);

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 server_fees_estimate_sat = pair.fees.server();
let server_fees_leeway_sat = self
.config
.onchain_fee_rate_leeway_sat_per_vbyte
.unwrap_or(0) as u64
* ESTIMATED_BTC_LOCKUP_TX_VSIZE;
let max_auto_accept_server_fees_sat = server_fees_estimate_sat + server_fees_leeway_sat;

let service_fees_sat = pair.fees.boltz(user_lockup_amount_sat);

let max_auto_accept_fees_sat = max_auto_accept_server_fees_sat + service_fees_sat;

let min_auto_accept_server_lockup_amount_sat =
user_lockup_amount_sat.saturating_sub(max_auto_accept_fees_sat);
debug!(
"user_lockup_amount_sat = {user_lockup_amount_sat}, \
service_fees_sat = {service_fees_sat}, \
server_fees_estimate_sat = {server_fees_estimate_sat}, \
server_fees_leeway_sat = {server_fees_leeway_sat}, \
min_auto_accept_server_lockup_amount_sat = {min_auto_accept_server_lockup_amount_sat}, \
quote_server_lockup_amount_sat = {quote_server_lockup_amount_sat}",
);

if min_auto_accept_server_lockup_amount_sat > quote_server_lockup_amount_sat {
return Ok(ValidateAmountlessSwapResult::RequiresUserAction);
}

let receiver_amount_sat = quote_server_lockup_amount_sat - swap.claim_fees_sat;
self.persister.update_zero_amount_swap_values(
&swap.id,

Ok(ValidateAmountlessSwapResult::ReadyForAccepting {
user_lockup_amount_sat,
receiver_amount_sat,
)?;

Ok(())
})
}

async fn on_new_outgoing_status(&self, swap: &ChainSwap, update: &boltz::Update) -> Result<()> {
Expand Down Expand Up @@ -1227,12 +1251,17 @@ impl ChainSwapHandler {
err: "Cannot transition to Created state".to_string(),
}),

(Created | Pending, Pending) => Ok(()),
(Created | Pending | WaitingFeeAcceptance, Pending) => Ok(()),
(_, Pending) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Pending state"),
}),

(Created | Pending | RefundPending, Complete) => Ok(()),
(Created | Pending, WaitingFeeAcceptance) => Ok(()),
(_, WaitingFeeAcceptance) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to WaitingFeeAcceptance state"),
}),

(Created | Pending | WaitingFeeAcceptance | RefundPending, Complete) => Ok(()),
(_, Complete) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Complete state"),
}),
Expand All @@ -1242,12 +1271,15 @@ impl ChainSwapHandler {
err: format!("Cannot transition from {from_state:?} to TimedOut state"),
}),

(Created | Pending | RefundPending | Failed | Complete, Refundable) => Ok(()),
(
Created | Pending | WaitingFeeAcceptance | RefundPending | Failed | Complete,
Refundable,
) => Ok(()),
(_, Refundable) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to Refundable state"),
}),

(Pending | Refundable, RefundPending) => Ok(()),
(Pending | WaitingFeeAcceptance | Refundable, RefundPending) => Ok(()),
(_, RefundPending) => Err(PaymentError::Generic {
err: format!("Cannot transition from {from_state:?} to RefundPending state"),
}),
Expand Down Expand Up @@ -1445,6 +1477,14 @@ impl ChainSwapHandler {
}
}

enum ValidateAmountlessSwapResult {
ReadyForAccepting {
user_lockup_amount_sat: u64,
receiver_amount_sat: u64,
},
RequiresUserAction,
}

#[cfg(test)]
mod tests {
use std::{
Expand Down Expand Up @@ -1473,14 +1513,39 @@ mod tests {
let chain_swap_handler = new_chain_swap_handler(storage.clone())?;

// Test valid combinations of states
let all_states = HashSet::from([Created, Pending, Complete, TimedOut, Failed]);
let all_states = HashSet::from([
Created,
Pending,
WaitingFeeAcceptance,
Complete,
TimedOut,
Failed,
]);
let valid_combinations = HashMap::from([
(
Created,
HashSet::from([Pending, Complete, TimedOut, Refundable, Failed]),
HashSet::from([
Pending,
WaitingFeeAcceptance,
Complete,
TimedOut,
Refundable,
Failed,
]),
),
(
Pending,
HashSet::from([
Pending,
WaitingFeeAcceptance,
Complete,
Refundable,
RefundPending,
Failed,
]),
),
(
WaitingFeeAcceptance,
HashSet::from([Pending, Complete, Refundable, RefundPending, Failed]),
),
(TimedOut, HashSet::from([Failed])),
Expand All @@ -1493,7 +1558,7 @@ mod tests {
for (first_state, allowed_states) in valid_combinations.iter() {
for allowed_state in allowed_states {
let chain_swap =
new_chain_swap(Direction::Incoming, Some(*first_state), false, None);
new_chain_swap(Direction::Incoming, Some(*first_state), false, None, false);
storage.insert_chain_swap(&chain_swap)?;

assert!(chain_swap_handler
Expand All @@ -1517,7 +1582,7 @@ mod tests {
for (first_state, disallowed_states) in invalid_combinations.iter() {
for disallowed_state in disallowed_states {
let chain_swap =
new_chain_swap(Direction::Incoming, Some(*first_state), false, None);
new_chain_swap(Direction::Incoming, Some(*first_state), false, None, false);
storage.insert_chain_swap(&chain_swap)?;

assert!(chain_swap_handler
Expand Down
Loading

0 comments on commit ab9ee4b

Please sign in to comment.