Преглед изворни кода

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

Perform a swap before melting by default.
C пре 2 недеља
родитељ
комит
83a919ccd6
2 измењених фајлова са 91 додато и 1 уклоњено
  1. 20 1
      crates/cdk/src/wallet/melt.rs
  2. 71 0
      crates/cdk/src/wallet/proofs.rs

+ 20 - 1
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
     }
 }

+ 71 - 0
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<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(
@@ -513,6 +570,20 @@ mod tests {
     }
 
     #[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)
             .flat_map(|i| (0..5).map(|_| proof(1 << i)).collect::<Vec<_>>())