diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index cbed2ba6..9bc6971a 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::str::FromStr; +use cdk_common::amount::SplitTarget; use cdk_common::wallet::{Transaction, TransactionDirection}; use lightning_invoice::Bolt11Invoice; use tracing::instrument; @@ -313,7 +314,7 @@ impl Wallet { .map(|k| k.id) .collect(); let keyset_fees = self.get_keyset_fees().await?; - let input_proofs = Wallet::select_proofs( + let (mut input_proofs, mut exchange) = Wallet::select_exact_proofs( inputs_needed_amount, available_proofs, &active_keyset_ids, @@ -321,6 +322,24 @@ impl Wallet { true, )?; + if let Some((proof, exact_amount)) = exchange.take() { + let new_proofs = self + .swap( + Some(exact_amount), + SplitTarget::None, + vec![proof.clone()], + None, + false, + ) + .await? + .ok_or_else(|| { + tracing::error!("Received empty proofs"); + Error::Internal + })?; + + input_proofs.extend_from_slice(&new_proofs); + } + self.melt_proofs(quote_id, input_proofs).await } } diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index dc0d0eb4..2fe0c9e6 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -176,6 +176,63 @@ impl Wallet { Ok(balance) } + /// Select exact proofs + /// + /// This function is similar to `select_proofs` but it the selected proofs will not exceed the + /// requested Amount, it will include a Proof and the exacto amount needed form that Proof to + /// perform a swap. + /// + /// The intent is to perform a swap with info, or include the Proof as part of the return if the + /// swap is not needed or if the swap failed. + pub fn select_exact_proofs( + amount: Amount, + proofs: Proofs, + active_keyset_ids: &Vec, + keyset_fees: &HashMap, + include_fees: bool, + ) -> Result<(Proofs, Option<(Proof, Amount)>), Error> { + let mut input_proofs = + Self::select_proofs(amount, proofs, active_keyset_ids, keyset_fees, include_fees)?; + let mut exchange = None; + + // How much amounts do we have selected in our proof sets? + let total_for_proofs = input_proofs.total_amount().unwrap_or_default(); + + if total_for_proofs > amount { + // If the selected proofs' total amount is more than the needed amount with fees, + // consider swapping if it makes sense to avoid locking large tokens. Instead, make the + // exact amount of tokens for the melting, even if that means paying more fees. + // + // If the fees would make it more expensive than it is already, it makes no sense, so + // skip it. + // + // The first step is to sort the proofs, select the one with the biggest amount, and + // perform a swap requesting the exact amount (covering the swap fees). + input_proofs.sort_by(|a, b| a.amount.cmp(&b.amount)); + + if let Some(proof_to_exchange) = input_proofs.pop() { + let fee_ppk = keyset_fees + .get(&proof_to_exchange.keyset_id) + .cloned() + .unwrap_or_default() + .into(); + + if let Some(exact_amount_to_melt) = total_for_proofs + .checked_sub(proof_to_exchange.amount) + .and_then(|a| a.checked_add(fee_ppk)) + .and_then(|b| amount.checked_sub(b)) + { + exchange = Some((proof_to_exchange, exact_amount_to_melt)); + } else { + // failed for some reason + input_proofs.push(proof_to_exchange); + } + } + } + + Ok((input_proofs, exchange)) + } + /// Select proofs #[instrument(skip_all)] pub fn select_proofs( @@ -512,6 +569,20 @@ mod tests { .for_each(|proof| assert_eq!(proof.amount, Amount::ONE)); } + #[test] + fn test_select_proof_change() { + let proofs = vec![proof(64), proof(4), proof(32)]; + let (selected_proofs, exchange) = + Wallet::select_exact_proofs(97.into(), proofs, &vec![id()], &HashMap::new(), false) + .unwrap(); + assert!(exchange.is_some()); + let (proof_to_exchange, amount) = exchange.unwrap(); + + assert_eq!(selected_proofs.len(), 2); + assert_eq!(proof_to_exchange.amount, 64.into()); + assert_eq!(amount, 61.into()); + } + #[test] fn test_select_proofs_huge_proofs() { let proofs = (0..32)