Browse Source

fix: add proof recovery mechanism for failed wallet operations

This commit introduces a new `try_proof_operation` helper that wraps wallet
operations (swap, melt) with automatic proof recovery in case of network or
mint failures. When an operation fails, the wallet now attempts to recover by
marking proofs as unspent and swapping them to prevent loss of funds.

Fixes #1180
Cesar Rodas 3 weeks ago
parent
commit
62db3d0b9b

+ 11 - 2
crates/cdk/src/wallet/melt/melt_bolt11.rs

@@ -198,8 +198,17 @@ impl Wallet {
         );
 
         let melt_response = match quote_info.payment_method {
-            cdk_common::PaymentMethod::Bolt11 => self.client.post_melt(request).await,
-            cdk_common::PaymentMethod::Bolt12 => self.client.post_melt_bolt12(request).await,
+            cdk_common::PaymentMethod::Bolt11 => {
+                self.try_proof_operation(request.inputs().clone(), self.client.post_melt(request))
+                    .await
+            }
+            cdk_common::PaymentMethod::Bolt12 => {
+                self.try_proof_operation(
+                    request.inputs().clone(),
+                    self.client.post_melt_bolt12(request),
+                )
+                .await
+            }
             cdk_common::PaymentMethod::Custom(_) => {
                 return Err(Error::UnsupportedPaymentMethod);
             }

+ 1 - 0
crates/cdk/src/wallet/mod.rs

@@ -51,6 +51,7 @@ mod streams;
 pub mod subscription;
 mod swap;
 mod transactions;
+mod try_proof_operation;
 pub mod util;
 
 #[cfg(feature = "auth")]

+ 6 - 1
crates/cdk/src/wallet/swap.rs

@@ -37,7 +37,12 @@ impl Wallet {
             )
             .await?;
 
-        let swap_response = self.client.post_swap(pre_swap.swap_request).await?;
+        let swap_response = self
+            .try_proof_operation(
+                pre_swap.swap_request.inputs().clone(),
+                self.client.post_swap(pre_swap.swap_request),
+            )
+            .await?;
 
         let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id;
         let fee_and_amounts = self

+ 63 - 0
crates/cdk/src/wallet/try_proof_operation.rs

@@ -0,0 +1,63 @@
+use std::future::Future;
+
+use futures::future::BoxFuture;
+
+use crate::amount::SplitTarget;
+use crate::nuts::{Proofs, State};
+use crate::{Error, Wallet};
+
+/// Size of proofs to send to avoid hitting the mint limit.
+const BATCH_PROOF_SIZE: usize = 100;
+
+impl Wallet {
+    /// Perform an async task, which is assumed to be a foreign mint call that can fail. If fails,
+    /// the proofs used in the request are set as unspent, then they are swapped, as they are
+    /// believed to be already shown to the mint
+    #[inline(always)]
+    pub(crate) fn try_proof_operation<'a, F, R>(
+        &'a self,
+        inputs: Proofs,
+        f: F,
+    ) -> BoxFuture<'a, F::Output>
+    where
+        F: Future<Output = Result<R, Error>> + Send + 'a,
+        R: Send + Sync,
+    {
+        Box::pin(async move {
+            match f.await {
+                Ok(r) => Ok(r),
+                Err(err) => {
+                    tracing::error!(
+                        "Http operation failed, revering  {} proofs states to UNSPENT",
+                        inputs.len()
+                    );
+
+                    // Although the proofs has been leaked already, we cannot swap them internally to
+                    // recover them, at least we flag it as unspent.
+                    self.localstore
+                        .update_proofs_state(
+                            inputs
+                                .iter()
+                                .map(|x| x.y())
+                                .collect::<Result<Vec<_>, _>>()?,
+                            State::Unspent,
+                        )
+                        .await?;
+
+                    tracing::error!(
+                        "Attempting to swap exposed {} proofs to new proofs",
+                        inputs.len()
+                    );
+
+                    for proofs in inputs.chunks(BATCH_PROOF_SIZE) {
+                        let _ = self
+                            .swap(None, SplitTarget::None, proofs.to_owned(), None, true)
+                            .await?;
+                    }
+
+                    Err(err)
+                }
+            }
+        })
+    }
+}