Kaynağa Gözat

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.
Cesar Rodas 2 ay önce
ebeveyn
işleme
4ca4f474ac

+ 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

+ 34 - 44
crates/cdk/src/wallet/reclaim.rs

@@ -1,10 +1,11 @@
+use std::collections::HashMap;
 use std::future::Future;
 
 use cdk_common::wallet::TransactionId;
 use cdk_common::{CheckStateRequest, ProofsMethods};
 use tracing::instrument;
 
-use crate::nuts::{Proofs, State};
+use crate::nuts::Proofs;
 use crate::{Error, Wallet};
 
 #[cfg(not(target_arch = "wasm32"))]
@@ -34,37 +35,38 @@ impl<T: ?Sized> MaybeSend for T {}
 const BATCH_PROOF_SIZE: usize = 100;
 
 impl Wallet {
-    /// Reclaim unspent proofs from the local database
-    ///
-    /// Checks the stats of [`Proofs`] updating in their status in the wallet's database if unspent
-    ///
+    /// Synchronizes the states with the mint
     #[instrument(skip(self, proofs))]
-    pub async fn update_proofs_state(&self, proofs: Proofs) -> Result<(), Error> {
+    pub async fn sync_proofs_state(&self, proofs: Proofs) -> Result<(), Error> {
         let proof_ys = proofs.ys()?;
 
         let transaction_id = TransactionId::new(proof_ys.clone());
 
-        let spendable = self
+        let statuses = self
             .client
             .post_check_state(CheckStateRequest { ys: proof_ys })
             .await?
             .states;
 
-        let unspent: Proofs = proofs
+        for (state, unspent) in proofs
             .into_iter()
-            .zip(spendable)
-            .filter_map(|(p, s)| (s.state == State::Unspent).then_some(p))
-            .collect();
-
-        self.localstore
-            .update_proofs_state(
-                unspent
-                    .iter()
-                    .map(|x| x.y())
-                    .collect::<Result<Vec<_>, _>>()?,
-                State::Unspent,
-            )
-            .await?;
+            .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?;
+        }
 
         match self.localstore.remove_transaction(transaction_id).await {
             Ok(_) => (),
@@ -77,8 +79,7 @@ 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
+    /// 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,
@@ -93,6 +94,11 @@ impl Wallet {
             match f.await {
                 Ok(r) => Ok(r),
                 Err(err) => {
+                    println!(
+                        "Http operation failed with \"{}\", revering  {} proofs states to UNSPENT",
+                        err,
+                        inputs.len()
+                    );
                     tracing::error!(
                         "Http operation failed with \"{}\", revering  {} proofs states to UNSPENT",
                         err,
@@ -115,27 +121,11 @@ impl Wallet {
                             inputs.len()
                         );
                         for proofs in inputs.chunks(BATCH_PROOF_SIZE) {
-                            if let Err(inner_err) =
-                                self.update_proofs_state(proofs.to_owned()).await
-                            {
-                                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