浏览代码

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 周之前
父节点
当前提交
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 {
         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(_) => {
             cdk_common::PaymentMethod::Custom(_) => {
                 return Err(Error::UnsupportedPaymentMethod);
                 return Err(Error::UnsupportedPaymentMethod);
             }
             }

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

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

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

@@ -37,7 +37,12 @@ impl Wallet {
             )
             )
             .await?;
             .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 active_keyset_id = pre_swap.pre_mint_secrets.keyset_id;
         let fee_and_amounts = self
         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)
+                }
+            }
+        })
+    }
+}