Skip to content
Merged
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
37 changes: 35 additions & 2 deletions graduated-rebalancer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ pub trait TrustedWallet: Send + Sync {
&self, method: PaymentMethod, amount: Amount,
) -> Pin<Box<dyn Future<Output = Result<[u8; 32], Self::Error>> + Send + '_>>;

/// Estimate the fee for making a payment using the trusted wallet
fn estimate_fee(
&self, method: PaymentMethod, amount: Amount,
) -> Pin<Box<dyn Future<Output = Result<Amount, Self::Error>> + Send + '_>>;

/// Wait for a payment success notification
fn await_payment_success(
&self, payment_hash: [u8; 32],
Expand Down Expand Up @@ -272,10 +277,38 @@ where

/// Perform a rebalance from trusted to lightning wallet
async fn do_trusted_rebalance_locked(&self, params: TriggerParams) {
let transfer_amt = params.amount;
let mut transfer_amt = params.amount;
log_info!(self.logger, "Initiating rebalance");

if let Ok(inv) = self.ln_wallet.get_bolt11_invoice(Some(transfer_amt)).await {
if let Ok(mut inv) = self.ln_wallet.get_bolt11_invoice(Some(transfer_amt)).await {
if let Ok(fee) = self
.trusted
.estimate_fee(PaymentMethod::LightningBolt11(inv.clone()), transfer_amt)
.await
{
if fee >= transfer_amt {
log_error!(
self.logger,
"Rebalance trusted transaction fee {fee:?} exceeds amount {transfer_amt:?}",
);
return;
}

if transfer_amt.saturating_add(fee) > params.amount {
transfer_amt = params.amount.saturating_sub(fee);
match self.ln_wallet.get_bolt11_invoice(Some(transfer_amt)).await {
Ok(reduced_inv) => inv = reduced_inv,
Err(e) => {
log_error!(
self.logger,
"Failed to create fee-adjusted rebalance invoice: {e:?}",
);
return;
},
}
}
}

log_debug!(
self.logger,
"Attempting to pay invoice {inv} to rebalance for {transfer_amt:?}",
Expand Down
59 changes: 57 additions & 2 deletions orange-sdk/src/trusted_wallet/cashu/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ impl TrustedWalletInterface for Cashu {
})?;

// The fee is in the quote
convert_amount(quote.fee_reserve, &self.unit)
let quote_fee = convert_amount(quote.fee_reserve, &self.unit)?;
let input_fee =
self.estimate_input_fee(quote.amount + quote.fee_reserve).await?;
Ok(quote_fee.saturating_add(input_fee))
},
PaymentMethod::LightningBolt12(offer) => {
let quote = self
Expand All @@ -230,7 +233,10 @@ impl TrustedWalletInterface for Cashu {
})?;

// The fee is in the quote
convert_amount(quote.fee_reserve, &self.unit)
let quote_fee = convert_amount(quote.fee_reserve, &self.unit)?;
let input_fee =
self.estimate_input_fee(quote.amount + quote.fee_reserve).await?;
Ok(quote_fee.saturating_add(input_fee))
},
PaymentMethod::OnChain(_) => Err(TrustedError::UnsupportedOperation(
"Cashu mint does not support onchain".to_owned(),
Expand Down Expand Up @@ -850,6 +856,55 @@ impl Cashu {
Ok(())
}

async fn estimate_input_fee(&self, input_amount: CdkAmount) -> Result<Amount, TrustedError> {
let proofs = self.cashu_wallet.get_unspent_proofs().await.map_err(|e| {
TrustedError::WalletOperationFailed(format!("Failed to get unspent proofs: {e}"))
})?;

let mut counts_by_keyset = HashMap::new();
for proof in proofs {
*counts_by_keyset.entry(proof.keyset_id).or_insert(0_u64) += 1;
}

let mut fee = Amount::ZERO;
for (keyset_id, proof_count) in counts_by_keyset {
let keyset_fee =
self.cashu_wallet.calculate_fee(proof_count, keyset_id).await.map_err(|e| {
TrustedError::WalletOperationFailed(format!(
"Failed to calculate input fee: {e}"
))
})?;
fee = fee.saturating_add(convert_amount(keyset_fee, &self.unit)?);
}

let active_keyset = self.cashu_wallet.get_active_keyset().await.map_err(|e| {
TrustedError::WalletOperationFailed(format!("Failed to get active keyset: {e}"))
})?;
let fee_and_amounts =
self.cashu_wallet.get_keyset_fees_and_amounts_by_id(active_keyset.id).await.map_err(
|e| {
TrustedError::WalletOperationFailed(format!(
"Failed to get keyset fee amounts: {e}"
))
},
)?;
let output_count = input_amount.split(&fee_and_amounts).map_err(|e| {
TrustedError::WalletOperationFailed(format!(
"Failed to calculate melt output count: {e}"
))
})?;
let output_fee = self
.cashu_wallet
.calculate_fee(output_count.len() as u64, active_keyset.id)
.await
.map_err(|e| {
TrustedError::WalletOperationFailed(format!("Failed to calculate output fee: {e}"))
})?;
fee = fee.saturating_add(convert_amount(output_fee, &self.unit)?);

Ok(fee)
}

pub(crate) async fn await_payment_success(&self) {
let mut flag = self.payment_success_flag.clone();
flag.mark_unchanged();
Expand Down
6 changes: 6 additions & 0 deletions orange-sdk/src/trusted_wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ impl<T: ?Sized + TrustedWalletInterface> graduated_rebalancer::TrustedWallet for
Box::pin(async move { self.0.pay(method, amount).await })
}

fn estimate_fee(
&self, method: PaymentMethod, amount: Amount,
) -> Pin<Box<dyn Future<Output = Result<Amount, Self::Error>> + Send + '_>> {
Box::pin(async move { self.0.estimate_fee(method, amount).await })
}

fn await_payment_success(
&self, payment_hash: [u8; 32],
) -> Pin<Box<dyn Future<Output = Option<ReceivedLightningPayment>> + Send + '_>> {
Expand Down
28 changes: 19 additions & 9 deletions orange-sdk/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,21 +310,24 @@ async fn test_sweep_to_ln() {

let expect_amt = intermediate_amt.saturating_add(recv_amt);

let event = wait_next_event(&wallet).await;
match event {
let received_rebalance_amount = match wait_next_event(&wallet).await {
Event::PaymentReceived { payment_id, amount_msat, lsp_fee_msats, .. } => {
assert!(matches!(payment_id, orange_sdk::PaymentId::SelfCustodial(_)));
assert!(lsp_fee_msats.is_some());
assert_eq!(amount_msat, expect_amt.milli_sats() - lsp_fee_msats.unwrap());
let lsp_fee_msats = lsp_fee_msats.expect("rebalance receive should pay LSP fee");
assert!(
amount_msat + lsp_fee_msats <= expect_amt.milli_sats(),
"rebalance receive should not exceed trusted balance after fees"
);
amount_msat + lsp_fee_msats
},
e => panic!("Expected RebalanceSuccessful event, got {e:?}"),
}
};

let event = wait_next_event(&wallet).await;
match event {
Event::RebalanceSuccessful { amount_msat, fee_msat, .. } => {
assert!(fee_msat > 0);
assert_eq!(amount_msat, expect_amt.milli_sats());
assert_eq!(amount_msat, received_rebalance_amount);
},
e => panic!("Expected RebalanceSuccessful event, got {e:?}"),
}
Expand Down Expand Up @@ -786,9 +789,12 @@ async fn test_receive_to_onchain_with_channel() {

// check we received on-chain, should be pending
// wait for payment success
test_utils::wait_for_condition("pending balance to update", || async {
// onchain balance is always listed as pending until we splice it into the channel.
test_utils::wait_for_condition("onchain receive to appear", || async {
wallet.get_balance().await.unwrap().pending_balance == recv_amt
|| wallet.list_transactions().await.unwrap().iter().any(|tx| {
tx.payment_type == PaymentType::IncomingOnChain { txid: Some(sent_txid) }
&& tx.amount == Some(recv_amt)
})
})
.await;

Expand Down Expand Up @@ -886,8 +892,12 @@ async fn test_concurrent_splice_in_and_out_preserve_pending_events() {
generate_blocks(&bitcoind, &electrsd, 6).await;
wallet.sync_ln_wallet().unwrap();

test_utils::wait_for_condition("pending balance to update", || async {
test_utils::wait_for_condition("onchain receive to appear", || async {
wallet.get_balance().await.unwrap().pending_balance == recv_amt
|| wallet.list_transactions().await.unwrap().iter().any(|tx| {
tx.payment_type == PaymentType::IncomingOnChain { txid: Some(sent_txid) }
&& tx.amount == Some(recv_amt)
})
})
.await;

Expand Down
8 changes: 7 additions & 1 deletion orange-sdk/tests/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use bitcoin_payment_instructions::amount::Amount;
#[cfg(feature = "_cashu-tests")]
use cdk::mint::{MintBuilder, MintMeltLimits};
use cdk::mint::{MintBuilder, MintMeltLimits, UnitConfig};
#[cfg(feature = "_cashu-tests")]
use cdk::types::FeeReserve;
#[cfg(feature = "_cashu-tests")]
Expand Down Expand Up @@ -527,6 +527,12 @@ async fn build_test_nodes() -> TestParams {
let mut mint_seed: [u8; 64] = [0; 64];
rand::thread_rng().fill_bytes(&mut mint_seed);
let mut builder = MintBuilder::new(mem_db.clone());
builder
.configure_unit(
orange_sdk::CurrencyUnit::Sat,
UnitConfig { input_fee_ppk: 1, ..Default::default() },
)
.unwrap();

builder
.add_payment_processor(
Expand Down