Merge pull request #793 from crodas/feature/wallet-swap-before-melt

Perform a swap before melting by default.
This commit is contained in:
C
2025-06-07 11:21:19 -03:00
committed by GitHub
parent 9a62777883
commit 83a919ccd6
2 changed files with 91 additions and 1 deletions

View File

@@ -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
}
}

View File

@@ -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<Id>,
keyset_fees: &HashMap<Id, u64>,
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)