Skip to content

Commit f532b30

Browse files
authored
Merge pull request #82 from lightningdevkit/cashu-cdk-test-fees
2 parents dacd08a + 093e5cc commit f532b30

5 files changed

Lines changed: 124 additions & 14 deletions

File tree

graduated-rebalancer/src/lib.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ pub trait TrustedWallet: Send + Sync {
7878
&self, method: PaymentMethod, amount: Amount,
7979
) -> Pin<Box<dyn Future<Output = Result<[u8; 32], Self::Error>> + Send + '_>>;
8080

81+
/// Estimate the fee for making a payment using the trusted wallet
82+
fn estimate_fee(
83+
&self, method: PaymentMethod, amount: Amount,
84+
) -> Pin<Box<dyn Future<Output = Result<Amount, Self::Error>> + Send + '_>>;
85+
8186
/// Wait for a payment success notification
8287
fn await_payment_success(
8388
&self, payment_hash: [u8; 32],
@@ -272,10 +277,38 @@ where
272277

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

278-
if let Ok(inv) = self.ln_wallet.get_bolt11_invoice(Some(transfer_amt)).await {
283+
if let Ok(mut inv) = self.ln_wallet.get_bolt11_invoice(Some(transfer_amt)).await {
284+
if let Ok(fee) = self
285+
.trusted
286+
.estimate_fee(PaymentMethod::LightningBolt11(inv.clone()), transfer_amt)
287+
.await
288+
{
289+
if fee >= transfer_amt {
290+
log_error!(
291+
self.logger,
292+
"Rebalance trusted transaction fee {fee:?} exceeds amount {transfer_amt:?}",
293+
);
294+
return;
295+
}
296+
297+
if transfer_amt.saturating_add(fee) > params.amount {
298+
transfer_amt = params.amount.saturating_sub(fee);
299+
match self.ln_wallet.get_bolt11_invoice(Some(transfer_amt)).await {
300+
Ok(reduced_inv) => inv = reduced_inv,
301+
Err(e) => {
302+
log_error!(
303+
self.logger,
304+
"Failed to create fee-adjusted rebalance invoice: {e:?}",
305+
);
306+
return;
307+
},
308+
}
309+
}
310+
}
311+
279312
log_debug!(
280313
self.logger,
281314
"Attempting to pay invoice {inv} to rebalance for {transfer_amt:?}",

orange-sdk/src/trusted_wallet/cashu/mod.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ impl TrustedWalletInterface for Cashu {
217217
})?;
218218

219219
// The fee is in the quote
220-
convert_amount(quote.fee_reserve, &self.unit)
220+
let quote_fee = convert_amount(quote.fee_reserve, &self.unit)?;
221+
let input_fee =
222+
self.estimate_input_fee(quote.amount + quote.fee_reserve).await?;
223+
Ok(quote_fee.saturating_add(input_fee))
221224
},
222225
PaymentMethod::LightningBolt12(offer) => {
223226
let quote = self
@@ -231,7 +234,10 @@ impl TrustedWalletInterface for Cashu {
231234
})?;
232235

233236
// The fee is in the quote
234-
convert_amount(quote.fee_reserve, &self.unit)
237+
let quote_fee = convert_amount(quote.fee_reserve, &self.unit)?;
238+
let input_fee =
239+
self.estimate_input_fee(quote.amount + quote.fee_reserve).await?;
240+
Ok(quote_fee.saturating_add(input_fee))
235241
},
236242
PaymentMethod::OnChain(_) => Err(TrustedError::UnsupportedOperation(
237243
"Cashu mint does not support onchain".to_owned(),
@@ -912,6 +918,55 @@ impl Cashu {
912918
Ok(())
913919
}
914920

921+
async fn estimate_input_fee(&self, input_amount: CdkAmount) -> Result<Amount, TrustedError> {
922+
let proofs = self.cashu_wallet.get_unspent_proofs().await.map_err(|e| {
923+
TrustedError::WalletOperationFailed(format!("Failed to get unspent proofs: {e}"))
924+
})?;
925+
926+
let mut counts_by_keyset = HashMap::new();
927+
for proof in proofs {
928+
*counts_by_keyset.entry(proof.keyset_id).or_insert(0_u64) += 1;
929+
}
930+
931+
let mut fee = Amount::ZERO;
932+
for (keyset_id, proof_count) in counts_by_keyset {
933+
let keyset_fee =
934+
self.cashu_wallet.calculate_fee(proof_count, keyset_id).await.map_err(|e| {
935+
TrustedError::WalletOperationFailed(format!(
936+
"Failed to calculate input fee: {e}"
937+
))
938+
})?;
939+
fee = fee.saturating_add(convert_amount(keyset_fee, &self.unit)?);
940+
}
941+
942+
let active_keyset = self.cashu_wallet.get_active_keyset().await.map_err(|e| {
943+
TrustedError::WalletOperationFailed(format!("Failed to get active keyset: {e}"))
944+
})?;
945+
let fee_and_amounts =
946+
self.cashu_wallet.get_keyset_fees_and_amounts_by_id(active_keyset.id).await.map_err(
947+
|e| {
948+
TrustedError::WalletOperationFailed(format!(
949+
"Failed to get keyset fee amounts: {e}"
950+
))
951+
},
952+
)?;
953+
let output_count = input_amount.split(&fee_and_amounts).map_err(|e| {
954+
TrustedError::WalletOperationFailed(format!(
955+
"Failed to calculate melt output count: {e}"
956+
))
957+
})?;
958+
let output_fee = self
959+
.cashu_wallet
960+
.calculate_fee(output_count.len() as u64, active_keyset.id)
961+
.await
962+
.map_err(|e| {
963+
TrustedError::WalletOperationFailed(format!("Failed to calculate output fee: {e}"))
964+
})?;
965+
fee = fee.saturating_add(convert_amount(output_fee, &self.unit)?);
966+
967+
Ok(fee)
968+
}
969+
915970
pub(crate) async fn await_payment_success(&self) {
916971
let mut flag = self.payment_success_flag.clone();
917972
flag.mark_unchanged();

orange-sdk/src/trusted_wallet/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@ impl<T: ?Sized + TrustedWalletInterface> graduated_rebalancer::TrustedWallet for
159159
Box::pin(async move { self.0.pay(method, amount).await })
160160
}
161161

162+
fn estimate_fee(
163+
&self, method: PaymentMethod, amount: Amount,
164+
) -> Pin<Box<dyn Future<Output = Result<Amount, Self::Error>> + Send + '_>> {
165+
Box::pin(async move { self.0.estimate_fee(method, amount).await })
166+
}
167+
162168
fn await_payment_success(
163169
&self, payment_hash: [u8; 32],
164170
) -> Pin<Box<dyn Future<Output = Option<ReceivedLightningPayment>> + Send + '_>> {

orange-sdk/tests/integration_tests.rs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -451,21 +451,24 @@ async fn test_sweep_to_ln() {
451451

452452
let expect_amt = intermediate_amt.saturating_add(recv_amt);
453453

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

464467
let event = wait_next_event(&wallet).await;
465468
match event {
466469
Event::RebalanceSuccessful { amount_msat, fee_msat, .. } => {
467470
assert!(fee_msat > 0);
468-
assert_eq!(amount_msat, expect_amt.milli_sats());
471+
assert_eq!(amount_msat, received_rebalance_amount);
469472
},
470473
e => panic!("Expected RebalanceSuccessful event, got {e:?}"),
471474
}
@@ -927,9 +930,12 @@ async fn test_receive_to_onchain_with_channel() {
927930

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

@@ -1027,8 +1033,12 @@ async fn test_concurrent_splice_in_and_out_preserve_pending_events() {
10271033
generate_blocks(&bitcoind, &electrsd, 6).await;
10281034
wallet.sync_ln_wallet().unwrap();
10291035

1030-
test_utils::wait_for_condition("pending balance to update", || async {
1036+
test_utils::wait_for_condition("onchain receive to appear", || async {
10311037
wallet.get_balance().await.unwrap().pending_balance == recv_amt
1038+
|| wallet.list_transactions().await.unwrap().iter().any(|tx| {
1039+
tx.payment_type == PaymentType::IncomingOnChain { txid: Some(sent_txid) }
1040+
&& tx.amount == Some(recv_amt)
1041+
})
10321042
})
10331043
.await;
10341044

orange-sdk/tests/test_utils.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use bitcoin_payment_instructions::amount::Amount;
44
#[cfg(feature = "_cashu-tests")]
5-
use cdk::mint::{MintBuilder, MintMeltLimits};
5+
use cdk::mint::{MintBuilder, MintMeltLimits, UnitConfig};
66
#[cfg(feature = "_cashu-tests")]
77
use cdk::types::FeeReserve;
88
#[cfg(feature = "_cashu-tests")]
@@ -527,6 +527,12 @@ async fn build_test_nodes() -> TestParams {
527527
let mut mint_seed: [u8; 64] = [0; 64];
528528
rand::thread_rng().fill_bytes(&mut mint_seed);
529529
let mut builder = MintBuilder::new(mem_db.clone());
530+
builder
531+
.configure_unit(
532+
orange_sdk::CurrencyUnit::Sat,
533+
UnitConfig { input_fee_ppk: 1, ..Default::default() },
534+
)
535+
.unwrap();
530536

531537
builder
532538
.add_payment_processor(

0 commit comments

Comments
 (0)