소스 검색

refactor: replace proof swap with state check in error recovery (#1256)

* refactor: replace proof swap with state check in error recovery

This commit improves the proof recovery mechanism by replacing the swap
operation with a more efficient state check approach when recovering from
failed operations.

Fixes #1255

* refactor: sync all proof states with mint instead of assuming unspent

This commit improves the proof state recovery mechanism to synchronize the
actual state from the mint rather than assuming all failed proofs are unspent.

* Update crates/cdk/src/wallet/reclaim.rs

Co-authored-by: lollerfirst <43107113+lollerfirst@users.noreply.github.com>
C 4 일 전
부모
커밋
52d796e9fe
2개의 변경된 파일50개의 추가작업 그리고 61개의 파일을 삭제
  1. 5 36
      crates/cdk-integration-tests/tests/fake_wallet.rs
  2. 45 25
      crates/cdk/src/wallet/reclaim.rs

+ 5 - 36
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -54,8 +54,6 @@ async fn test_fake_tokens_pending() {
         .expect("payment")
         .expect("no error");
 
-    let old_balance = wallet.total_balance().await.expect("balance");
-
     let fake_description = FakeInvoiceDescription {
         pay_invoice_state: MeltQuoteState::Pending,
         check_payment_state: MeltQuoteState::Pending,
@@ -72,12 +70,7 @@ async fn test_fake_tokens_pending() {
     assert!(melt.is_err());
 
     // melt failed, but there is new code to reclaim unspent proofs
-    assert_eq!(
-        old_balance,
-        wallet.total_balance().await.expect("new balance")
-    );
-
-    assert!(wallet
+    assert!(!wallet
         .localstore
         .get_proofs(None, None, Some(vec![State::Pending]), None)
         .await
@@ -139,7 +132,7 @@ async fn test_fake_melt_payment_fail() {
     assert!(melt.is_err());
 
     let wallet_bal = wallet.total_balance().await.unwrap();
-    assert_eq!(wallet_bal, 100.into());
+    assert_eq!(wallet_bal, 98.into());
 }
 
 /// Tests that when both the pay_invoice and check_invoice both fail,
@@ -165,8 +158,6 @@ async fn test_fake_melt_payment_fail_and_check() {
         .expect("payment")
         .expect("no error");
 
-    let old_balance = wallet.total_balance().await.expect("balance");
-
     let fake_description = FakeInvoiceDescription {
         pay_invoice_state: MeltQuoteState::Unknown,
         check_payment_state: MeltQuoteState::Unknown,
@@ -182,13 +173,7 @@ async fn test_fake_melt_payment_fail_and_check() {
     let melt = wallet.melt(&melt_quote.id).await;
     assert!(melt.is_err());
 
-    // melt failed, but there is new code to reclaim unspent proofs
-    assert_eq!(
-        old_balance,
-        wallet.total_balance().await.expect("new balance")
-    );
-
-    assert!(wallet
+    assert!(!wallet
         .localstore
         .get_proofs(None, None, Some(vec![State::Pending]), None)
         .await
@@ -226,8 +211,6 @@ async fn test_fake_melt_payment_return_fail_status() {
         check_err: false,
     };
 
-    let old_balance = wallet.total_balance().await.expect("balance");
-
     let invoice = create_fake_invoice(7000, serde_json::to_string(&fake_description).unwrap());
 
     let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
@@ -261,14 +244,9 @@ async fn test_fake_melt_payment_return_fail_status() {
     let melt = wallet.melt(&melt_quote.id).await;
     assert!(melt.is_err());
 
-    assert_eq!(
-        old_balance,
-        wallet.total_balance().await.expect("new balance")
-    );
-
     wallet.check_all_pending_proofs().await.unwrap();
 
-    assert!(wallet
+    assert!(!wallet
         .localstore
         .get_proofs(None, None, Some(vec![State::Pending]), None)
         .await
@@ -299,8 +277,6 @@ async fn test_fake_melt_payment_error_unknown() {
         .expect("payment")
         .expect("no error");
 
-    let old_balance = wallet.total_balance().await.expect("balance");
-
     let fake_description = FakeInvoiceDescription {
         pay_invoice_state: MeltQuoteState::Failed,
         check_payment_state: MeltQuoteState::Unknown,
@@ -331,14 +307,7 @@ async fn test_fake_melt_payment_error_unknown() {
     let melt = wallet.melt(&melt_quote.id).await;
     assert!(melt.is_err());
 
-    wallet.check_all_pending_proofs().await.unwrap();
-
-    assert_eq!(
-        old_balance,
-        wallet.total_balance().await.expect("new balance")
-    );
-
-    assert!(wallet
+    assert!(!wallet
         .localstore
         .get_proofs(None, None, Some(vec![State::Pending]), None)
         .await

+ 45 - 25
crates/cdk/src/wallet/reclaim.rs

@@ -1,6 +1,10 @@
+use std::collections::HashMap;
 use std::future::Future;
 
-use crate::nuts::{Proofs, State};
+use cdk_common::{CheckStateRequest, ProofsMethods};
+use tracing::instrument;
+
+use crate::nuts::Proofs;
 use crate::{Error, Wallet};
 
 #[cfg(not(target_arch = "wasm32"))]
@@ -30,9 +34,42 @@ impl<T: ?Sized> MaybeSend for T {}
 const BATCH_PROOF_SIZE: usize = 100;
 
 impl Wallet {
+    /// Synchronizes the states with the mint
+    #[instrument(skip(self, proofs))]
+    pub async fn sync_proofs_state(&self, proofs: Proofs) -> Result<(), Error> {
+        let proof_ys = proofs.ys()?;
+
+        let statuses = self
+            .client
+            .post_check_state(CheckStateRequest { ys: proof_ys })
+            .await?
+            .states;
+
+        for (state, unspent) in proofs
+            .into_iter()
+            .zip(statuses)
+            .map(|(p, s)| (s.state, p))
+            .fold(HashMap::<_, Vec<_>>::new(), |mut acc, (cat, item)| {
+                acc.entry(cat).or_default().push(item);
+                acc
+            })
+        {
+            self.localstore
+                .update_proofs_state(
+                    unspent
+                        .iter()
+                        .map(|x| x.y())
+                        .collect::<Result<Vec<_>, _>>()?,
+                    state,
+                )
+                .await?;
+        }
+
+        Ok(())
+    }
+
     /// 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
+    /// the proofs used in the request are synchronize with the mint and update it locally
     #[inline(always)]
     pub(crate) fn try_proof_operation_or_reclaim<'a, F, R>(
         &'a self,
@@ -69,28 +106,11 @@ impl Wallet {
                             inputs.len()
                         );
                         for proofs in inputs.chunks(BATCH_PROOF_SIZE) {
-                            if let Err(inner_err) = self.reclaim_unspent(proofs.to_owned()).await {
-                                println!(
-                                    "Failed to swap exposed proofs ({}), updating local database instead", inner_err
-                                );
-                                tracing::warn!(
-                                    "Failed to swap exposed proofs ({}), updating local database instead", inner_err
-                                );
-
-                                let _ = self
-                                    .localstore
-                                    .update_proofs_state(
-                                        proofs
-                                            .iter()
-                                            .map(|x| x.y())
-                                            .collect::<Result<Vec<_>, _>>()?,
-                                        State::Unspent,
-                                    )
-                                    .await
-                                    .inspect_err(|err| {
-                                        tracing::error!("Failed err update_proofs_state {}", err)
-                                    });
-                            }
+                            let _ = self.sync_proofs_state(proofs.to_owned()).await.inspect_err(
+                                |err| {
+                                    tracing::warn!("Failed to swap exposed proofs ({})", err);
+                                },
+                            );
                         }
 
                         self.in_error_swap_reverted_proofs