Browse Source

Add Database transaction to the Wallet trait

Cesar Rodas 11 tháng trước cách đây
mục cha
commit
eb0f5d478e
33 tập tin đã thay đổi với 1283 bổ sung và 702 xóa
  1. 8 2
      crates/cdk-cli/src/sub_commands/check_pending.rs
  2. 1 1
      crates/cdk-cli/src/sub_commands/list_mint_proofs.rs
  3. 1 14
      crates/cdk-common/src/database/mint/mod.rs
  4. 20 7
      crates/cdk-common/src/database/mod.rs
  5. 95 45
      crates/cdk-common/src/database/wallet.rs
  6. 4 4
      crates/cdk-ffi/src/wallet.rs
  7. 8 2
      crates/cdk-integration-tests/tests/bolt12.rs
  8. 9 9
      crates/cdk-integration-tests/tests/fake_auth.rs
  9. 2 2
      crates/cdk-integration-tests/tests/fake_wallet.rs
  10. 1 1
      crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs
  11. 8 2
      crates/cdk-integration-tests/tests/integration_tests_pure.rs
  12. 5 1
      crates/cdk-integration-tests/tests/test_fees.rs
  13. 2 2
      crates/cdk-sql-common/src/mint/mod.rs
  14. 622 286
      crates/cdk-sql-common/src/wallet/mod.rs
  15. 1 1
      crates/cdk/examples/auth_wallet.rs
  16. 1 1
      crates/cdk/examples/proof-selection.rs
  17. 67 27
      crates/cdk/src/wallet/auth/auth_wallet.rs
  18. 1 1
      crates/cdk/src/wallet/auth/mod.rs
  19. 1 1
      crates/cdk/src/wallet/balance.rs
  20. 42 31
      crates/cdk/src/wallet/issue/issue_bolt11.rs
  21. 40 32
      crates/cdk/src/wallet/issue/issue_bolt12.rs
  22. 41 15
      crates/cdk/src/wallet/keysets.rs
  23. 52 55
      crates/cdk/src/wallet/melt/melt_bolt11.rs
  24. 10 4
      crates/cdk/src/wallet/melt/melt_bolt12.rs
  25. 22 20
      crates/cdk/src/wallet/melt/mod.rs
  26. 0 0
      crates/cdk/src/wallet/mint_connector/transport/mod.rs
  27. 32 17
      crates/cdk/src/wallet/mod.rs
  28. 11 2
      crates/cdk/src/wallet/multi_mint_wallet.rs
  29. 66 31
      crates/cdk/src/wallet/proofs.rs
  30. 36 33
      crates/cdk/src/wallet/receive.rs
  31. 35 28
      crates/cdk/src/wallet/send.rs
  32. 31 23
      crates/cdk/src/wallet/swap.rs
  33. 8 2
      crates/cdk/src/wallet/transactions.rs

+ 8 - 2
crates/cdk-cli/src/sub_commands/check_pending.rs

@@ -9,8 +9,12 @@ pub async fn check_pending(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
         let mint_url = wallet.mint_url.clone();
         println!("{i}: {mint_url}");
 
+        let mut tx = wallet.localstore.begin_db_transaction().await?;
+
         // Get all pending proofs
-        let pending_proofs = wallet.get_pending_proofs().await?;
+        //
+
+        let pending_proofs = wallet.get_pending_proofs(Some(&mut tx)).await?;
         if pending_proofs.is_empty() {
             println!("No pending proofs found");
             continue;
@@ -24,10 +28,12 @@ pub async fn check_pending(multi_mint_wallet: &MultiMintWallet) -> Result<()> {
         );
 
         // Try to reclaim any proofs that are no longer pending
-        match wallet.reclaim_unspent(pending_proofs).await {
+        match wallet.reclaim_unspent(pending_proofs, &mut tx).await {
             Ok(()) => println!("Successfully reclaimed pending proofs"),
             Err(e) => println!("Error reclaimed pending proofs: {e}"),
         }
+
+        tx.commit().await?;
     }
     Ok(())
 }

+ 1 - 1
crates/cdk-cli/src/sub_commands/list_mint_proofs.rs

@@ -35,7 +35,7 @@ async fn list_proofs(
         }
 
         // Pending proofs
-        let pending_proofs = wallet.get_pending_proofs().await?;
+        let pending_proofs = wallet.get_pending_proofs(None).await?;
         for proof in pending_proofs {
             println!(
                 "| {:8} | {:4} | {:8} | {:64} | {}",

+ 1 - 14
crates/cdk-common/src/database/mint/mod.rs

@@ -6,7 +6,7 @@ use async_trait::async_trait;
 use cashu::quote_id::QuoteId;
 use cashu::Amount;
 
-use super::Error;
+use super::{DbTransactionFinalizer, Error};
 use crate::mint::{self, MintKeySetInfo, MintQuote as MintMintQuote, Operation};
 use crate::nuts::{
     BlindSignature, BlindedMessage, CurrencyUnit, Id, MeltQuoteState, Proof, Proofs, PublicKey,
@@ -394,19 +394,6 @@ pub trait SagaDatabase {
     ) -> Result<Vec<mint::Saga>, Self::Err>;
 }
 
-#[async_trait]
-/// Commit and Rollback
-pub trait DbTransactionFinalizer {
-    /// Mint Signature Database Error
-    type Err: Into<Error> + From<Error>;
-
-    /// Commits all the changes into the database
-    async fn commit(self: Box<Self>) -> Result<(), Self::Err>;
-
-    /// Rollbacks the write transaction
-    async fn rollback(self: Box<Self>) -> Result<(), Self::Err>;
-}
-
 /// Key-Value Store Transaction trait
 #[async_trait]
 pub trait KVStoreTransaction<'a, Error>: DbTransactionFinalizer<Err = Error> {

+ 20 - 7
crates/cdk-common/src/database/mod.rs

@@ -7,18 +7,18 @@ mod wallet;
 
 #[cfg(feature = "mint")]
 pub use mint::{
-    Database as MintDatabase, DbTransactionFinalizer as MintDbWriterFinalizer, DynMintDatabase,
-    KVStore as MintKVStore, KVStoreDatabase as MintKVStoreDatabase,
-    KVStoreTransaction as MintKVStoreTransaction, KeysDatabase as MintKeysDatabase,
-    KeysDatabaseTransaction as MintKeyDatabaseTransaction, ProofsDatabase as MintProofsDatabase,
-    ProofsTransaction as MintProofsTransaction, QuotesDatabase as MintQuotesDatabase,
-    QuotesTransaction as MintQuotesTransaction, SignaturesDatabase as MintSignaturesDatabase,
+    Database as MintDatabase, DynMintDatabase, KVStore as MintKVStore,
+    KVStoreDatabase as MintKVStoreDatabase, KVStoreTransaction as MintKVStoreTransaction,
+    KeysDatabase as MintKeysDatabase, KeysDatabaseTransaction as MintKeyDatabaseTransaction,
+    ProofsDatabase as MintProofsDatabase, ProofsTransaction as MintProofsTransaction,
+    QuotesDatabase as MintQuotesDatabase, QuotesTransaction as MintQuotesTransaction,
+    SignaturesDatabase as MintSignaturesDatabase,
     SignaturesTransaction as MintSignatureTransaction, Transaction as MintTransaction,
 };
 #[cfg(all(feature = "mint", feature = "auth"))]
 pub use mint::{DynMintAuthDatabase, MintAuthDatabase, MintAuthTransaction};
 #[cfg(feature = "wallet")]
-pub use wallet::Database as WalletDatabase;
+pub use wallet::{Database as WalletDatabase, DatabaseTransaction as WalletDatabaseTransaction};
 
 /// Data conversion error
 #[derive(thiserror::Error, Debug)]
@@ -203,3 +203,16 @@ impl From<crate::state::Error> for Error {
         }
     }
 }
+
+#[async_trait::async_trait]
+/// Commit and Rollback
+pub trait DbTransactionFinalizer {
+    /// Mint Signature Database Error
+    type Err: Into<Error> + From<Error>;
+
+    /// Commits all the changes into the database
+    async fn commit(self: Box<Self>) -> Result<(), Self::Err>;
+
+    /// Rollbacks the write transaction
+    async fn rollback(self: Box<Self>) -> Result<(), Self::Err>;
+}

+ 95 - 45
crates/cdk-common/src/database/wallet.rs

@@ -6,7 +6,7 @@ use std::fmt::Debug;
 use async_trait::async_trait;
 use cashu::KeySet;
 
-use super::Error;
+use super::{DbTransactionFinalizer, Error};
 use crate::common::ProofInfo;
 use crate::mint_url::MintUrl;
 use crate::nuts::{
@@ -16,78 +16,135 @@ use crate::wallet::{
     self, MintQuote as WalletMintQuote, Transaction, TransactionDirection, TransactionId,
 };
 
-/// Wallet Database trait
+/// Database transaction
+///
+/// This trait encapsulates all the changes to be done in the wallet
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-pub trait Database: Debug {
-    /// Wallet Database Error
-    type Err: Into<Error> + From<Error>;
-
+pub trait DatabaseTransaction<'a, Error>: DbTransactionFinalizer<Err = Error> {
     /// Add Mint to storage
     async fn add_mint(
-        &self,
+        &mut self,
         mint_url: MintUrl,
         mint_info: Option<MintInfo>,
-    ) -> Result<(), Self::Err>;
+    ) -> Result<(), Error>;
+
     /// Remove Mint from storage
-    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Self::Err>;
-    /// Get mint from storage
-    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err>;
-    /// Get all mints from storage
-    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err>;
+    async fn remove_mint(&mut self, mint_url: MintUrl) -> Result<(), Error>;
+
     /// Update mint url
     async fn update_mint_url(
-        &self,
+        &mut self,
         old_mint_url: MintUrl,
         new_mint_url: MintUrl,
-    ) -> Result<(), Self::Err>;
+    ) -> Result<(), Error>;
 
     /// Add mint keyset to storage
     async fn add_mint_keysets(
-        &self,
+        &mut self,
         mint_url: MintUrl,
         keysets: Vec<KeySetInfo>,
-    ) -> Result<(), Self::Err>;
+    ) -> Result<(), Error>;
+
+    /// Get mint quote from storage. This function locks the returned minted quote for update
+    async fn get_mint_quote(
+        &mut self,
+        quote_id: &str,
+    ) -> Result<Option<WalletMintQuote>, Self::Err>;
+
+    /// Add mint quote to storage
+    async fn add_mint_quote(&mut self, quote: WalletMintQuote) -> Result<(), Error>;
+
+    /// Remove mint quote from storage
+    async fn remove_mint_quote(&mut self, quote_id: &str) -> Result<(), Error>;
+
+    /// Get melt quote from storage
+    async fn get_melt_quote(&mut self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Error>;
+
+    /// Add melt quote to storage
+    async fn add_melt_quote(&mut self, quote: wallet::MeltQuote) -> Result<(), Error>;
+
+    /// Remove melt quote from storage
+    async fn remove_melt_quote(&mut self, quote_id: &str) -> Result<(), Error>;
+
+    /// Add [`Keys`] to storage
+    async fn add_keys(&mut self, keyset: KeySet) -> Result<(), Error>;
+
+    /// Remove [`Keys`] from storage
+    async fn remove_keys(&mut self, id: &Id) -> Result<(), Error>;
+
+    /// Get proofs from storage and lock them for update
+    async fn get_proofs(
+        &mut self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<State>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, Error>;
+
+    /// Update the proofs in storage by adding new proofs or removing proofs by
+    /// their Y value.
+    async fn update_proofs(
+        &mut self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), Error>;
+
+    /// Update proofs state in storage
+    async fn update_proofs_state(&mut self, ys: Vec<PublicKey>, state: State) -> Result<(), Error>;
+
+    /// Atomically increment Keyset counter and return new value
+    async fn increment_keyset_counter(&mut self, keyset_id: &Id, count: u32) -> Result<u32, Error>;
+
+    /// Add transaction to storage
+    async fn add_transaction(&mut self, transaction: Transaction) -> Result<(), Error>;
+
+    /// Remove transaction from storage
+    async fn remove_transaction(&mut self, transaction_id: TransactionId) -> Result<(), Error>;
+}
+
+/// Wallet Database trait
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+pub trait Database: Debug {
+    /// Wallet Database Error
+    type Err: Into<Error> + From<Error>;
+
+    /// Beings a KV transaction
+    async fn begin_db_transaction<'a>(
+        &'a self,
+    ) -> Result<Box<dyn DatabaseTransaction<'a, Self::Err> + Send + Sync + 'a>, Error>;
+
+    /// Get mint from storage
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err>;
+
+    /// Get all mints from storage
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err>;
+
     /// Get mint keysets for mint url
     async fn get_mint_keysets(
         &self,
         mint_url: MintUrl,
     ) -> Result<Option<Vec<KeySetInfo>>, Self::Err>;
+
     /// Get mint keyset by id
     async fn get_keyset_by_id(&self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Self::Err>;
 
-    /// Add mint quote to storage
-    async fn add_mint_quote(&self, quote: WalletMintQuote) -> Result<(), Self::Err>;
     /// Get mint quote from storage
     async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<WalletMintQuote>, Self::Err>;
+
     /// Get mint quotes from storage
     async fn get_mint_quotes(&self) -> Result<Vec<WalletMintQuote>, Self::Err>;
-    /// Remove mint quote from storage
-    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
-    /// Add melt quote to storage
-    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err>;
     /// Get melt quote from storage
     async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err>;
+
     /// Get melt quotes from storage
     async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, Self::Err>;
-    /// Remove melt quote from storage
-    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err>;
 
-    /// Add [`Keys`] to storage
-    async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err>;
     /// Get [`Keys`] from storage
     async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
-    /// Remove [`Keys`] from storage
-    async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err>;
 
-    /// Update the proofs in storage by adding new proofs or removing proofs by
-    /// their Y value.
-    async fn update_proofs(
-        &self,
-        added: Vec<ProofInfo>,
-        removed_ys: Vec<PublicKey>,
-    ) -> Result<(), Self::Err>;
     /// Get proofs from storage
     async fn get_proofs(
         &self,
@@ -96,6 +153,7 @@ pub trait Database: Debug {
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Vec<ProofInfo>, Self::Err>;
+
     /// Get balance
     async fn get_balance(
         &self,
@@ -103,19 +161,13 @@ pub trait Database: Debug {
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
     ) -> Result<u64, Self::Err>;
-    /// Update proofs state in storage
-    async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err>;
 
-    /// Atomically increment Keyset counter and return new value
-    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<u32, Self::Err>;
-
-    /// Add transaction to storage
-    async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err>;
     /// Get transaction from storage
     async fn get_transaction(
         &self,
         transaction_id: TransactionId,
     ) -> Result<Option<Transaction>, Self::Err>;
+
     /// List transactions from storage
     async fn list_transactions(
         &self,
@@ -123,6 +175,4 @@ pub trait Database: Debug {
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
     ) -> Result<Vec<Transaction>, Self::Err>;
-    /// Remove transaction from storage
-    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err>;
 }

+ 4 - 4
crates/cdk-ffi/src/wallet.rs

@@ -82,7 +82,7 @@ impl Wallet {
 
     /// Get mint info
     pub async fn get_mint_info(&self) -> Result<Option<MintInfo>, FfiError> {
-        let info = self.inner.fetch_mint_info().await?;
+        let info = self.inner.fetch_mint_info(None).await?;
         Ok(info.map(Into::into))
     }
 
@@ -271,9 +271,9 @@ impl Wallet {
         for state in states {
             let proofs = match state {
                 ProofState::Unspent => self.inner.get_unspent_proofs().await?,
-                ProofState::Pending => self.inner.get_pending_proofs().await?,
+                ProofState::Pending => self.inner.get_pending_proofs(None).await?,
                 ProofState::Reserved => self.inner.get_reserved_proofs().await?,
-                ProofState::PendingSpent => self.inner.get_pending_spent_proofs().await?,
+                ProofState::PendingSpent => self.inner.get_pending_spent_proofs(None).await?,
                 ProofState::Spent => {
                     // CDK doesn't have a method to get spent proofs directly
                     // They are removed from the database when spent
@@ -351,7 +351,7 @@ impl Wallet {
 
     /// Refresh keysets from the mint
     pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, FfiError> {
-        let keysets = self.inner.refresh_keysets().await?;
+        let keysets = self.inner.refresh_keysets(None).await?;
         Ok(keysets.into_iter().map(Into::into).collect())
     }
 

+ 8 - 2
crates/cdk-integration-tests/tests/bolt12.rs

@@ -325,7 +325,11 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> {
     // Create a single-use BOLT12 quote
     let mint_quote = wallet.mint_bolt12_quote(None, None).await?;
 
-    let state = wallet.mint_bolt12_quote_state(&mint_quote.id).await?;
+    let mut tx = wallet.localstore.begin_db_transaction().await?;
+    let state = wallet
+        .mint_bolt12_quote_state(&mint_quote.id, &mut tx)
+        .await?;
+    tx.commit().await?;
 
     assert_eq!(state.amount_paid, Amount::ZERO);
     assert_eq!(state.amount_issued, Amount::ZERO);
@@ -436,10 +440,12 @@ async fn test_attempt_to_mint_unpaid() {
         .await
         .unwrap();
 
+    let mut tx = wallet.localstore.begin_db_transaction().await?;
     let state = wallet
-        .mint_bolt12_quote_state(&mint_quote.id)
+        .mint_bolt12_quote_state(&mint_quote.id, &mut tx)
         .await
         .unwrap();
+    tx.commit().await?;
 
     assert!(state.amount_paid == Amount::ZERO);
 

+ 9 - 9
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -277,7 +277,7 @@ async fn test_mint_blind_auth() {
         .seed(Mnemonic::generate(12).unwrap().to_seed_normalized(""))
         .build()
         .expect("Wallet");
-    let mint_info = wallet.fetch_mint_info().await.unwrap().unwrap();
+    let mint_info = wallet.fetch_mint_info(None).await.unwrap().unwrap();
 
     let (access_token, _) = get_access_token(&mint_info).await;
 
@@ -355,7 +355,7 @@ async fn test_swap_with_auth() {
         .seed(Mnemonic::generate(12).unwrap().to_seed_normalized(""))
         .build()
         .expect("Wallet");
-    let mint_info = wallet.fetch_mint_info().await.unwrap().unwrap();
+    let mint_info = wallet.fetch_mint_info(None).await.unwrap().unwrap();
     let (access_token, _) = get_access_token(&mint_info).await;
 
     wallet.set_cat(access_token).await.unwrap();
@@ -384,7 +384,7 @@ async fn test_swap_with_auth() {
         .expect("Could not swap");
 
     let check_spent = wallet
-        .check_proofs_spent(proofs.clone())
+        .check_proofs_spent(proofs.clone(), None)
         .await
         .expect("Could not check proofs");
 
@@ -410,7 +410,7 @@ async fn test_melt_with_auth() {
         .expect("Wallet");
 
     let mint_info = wallet
-        .fetch_mint_info()
+        .fetch_mint_info(None)
         .await
         .expect("Mint info not found")
         .expect("Mint info not found");
@@ -452,7 +452,7 @@ async fn test_mint_auth_over_max() {
     let wallet = Arc::new(wallet);
 
     let mint_info = wallet
-        .fetch_mint_info()
+        .fetch_mint_info(None)
         .await
         .expect("Mint info not found")
         .expect("Mint info not found");
@@ -490,7 +490,7 @@ async fn test_reuse_auth_proof() {
         .seed(Mnemonic::generate(12).unwrap().to_seed_normalized(""))
         .build()
         .expect("Wallet");
-    let mint_info = wallet.fetch_mint_info().await.unwrap().unwrap();
+    let mint_info = wallet.fetch_mint_info(None).await.unwrap().unwrap();
 
     let (access_token, _) = get_access_token(&mint_info).await;
 
@@ -542,7 +542,7 @@ async fn test_melt_with_invalid_auth() {
         .seed(Mnemonic::generate(12).unwrap().to_seed_normalized(""))
         .build()
         .expect("Wallet");
-    let mint_info = wallet.fetch_mint_info().await.unwrap().unwrap();
+    let mint_info = wallet.fetch_mint_info(None).await.unwrap().unwrap();
 
     let (access_token, _) = get_access_token(&mint_info).await;
 
@@ -607,7 +607,7 @@ async fn test_refresh_access_token() {
         .expect("Wallet");
 
     let mint_info = wallet
-        .fetch_mint_info()
+        .fetch_mint_info(None)
         .await
         .expect("mint info")
         .expect("could not get mint info");
@@ -663,7 +663,7 @@ async fn test_invalid_refresh_token() {
         .expect("Wallet");
 
     let mint_info = wallet
-        .fetch_mint_info()
+        .fetch_mint_info(None)
         .await
         .expect("mint info")
         .expect("could not get mint info");

+ 2 - 2
crates/cdk-integration-tests/tests/fake_wallet.rs

@@ -128,7 +128,7 @@ async fn test_fake_melt_payment_fail() {
 
     // The mint should have unset proofs from pending since payment failed
     let all_proof = wallet.get_unspent_proofs().await.unwrap();
-    let states = wallet.check_proofs_spent(all_proof).await.unwrap();
+    let states = wallet.check_proofs_spent(all_proof, None).await.unwrap();
     for state in states {
         assert!(state.state == State::Unspent);
     }
@@ -749,7 +749,7 @@ async fn test_fake_mint_multiple_unit_swap() {
         None,
     )
     .expect("failed to create usd wallet");
-    wallet_usd.refresh_keysets().await.unwrap();
+    wallet_usd.refresh_keysets(None).await.unwrap();
 
     let mint_quote = wallet_usd.mint_quote(100.into(), None).await.unwrap();
 

+ 1 - 1
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -338,7 +338,7 @@ async fn test_restore() {
 
     let proofs = wallet.get_unspent_proofs().await.unwrap();
 
-    let states = wallet.check_proofs_spent(proofs).await.unwrap();
+    let states = wallet.check_proofs_spent(proofs, None).await.unwrap();
 
     for state in states {
         if state.state != State::Spent {

+ 8 - 2
crates/cdk-integration-tests/tests/integration_tests_pure.rs

@@ -99,7 +99,7 @@ async fn test_swap_to_send() {
         HashSet::<_, RandomState>::from_iter(token_proofs.ys().expect("Failed to get ys")),
         HashSet::from_iter(
             wallet_alice
-                .get_pending_spent_proofs()
+                .get_pending_spent_proofs(None)
                 .await
                 .expect("Failed to get pending spent proofs")
                 .ys()
@@ -765,10 +765,16 @@ async fn test_mint_change_with_fee_melt() {
         .await
         .unwrap();
 
+    let mut tx = wallet_alice
+        .localstore
+        .begin_db_transaction()
+        .await
+        .unwrap();
     let w = wallet_alice
-        .melt_proofs(&melt_quote.id, proofs)
+        .melt_proofs_with_metadata(&melt_quote.id, proofs, HashMap::new(), &mut tx)
         .await
         .unwrap();
+    tx.commit().await.unwrap();
 
     assert_eq!(w.change.unwrap().total_amount().unwrap(), 97.into());
 }

+ 5 - 1
crates/cdk-integration-tests/tests/test_fees.rs

@@ -108,10 +108,14 @@ async fn test_fake_melt_change_in_quote() {
     let proofs_total = proofs.total_amount().unwrap();
 
     let fee = wallet.get_proofs_fee(&proofs).await.unwrap();
+
+    let mut tx = wallet.localstore.begin_db_transaction().await.unwrap();
     let melt = wallet
-        .melt_proofs(&melt_quote.id, proofs.clone())
+        .melt_proofs_with_metadata(&melt_quote.id, proofs, HashMap::new())
         .await
         .unwrap();
+    tx.commit().await.unwrap();
+
     let change = melt.change.unwrap().total_amount().unwrap();
     let idk = proofs.total_amount().unwrap() - Amount::from(invoice_amount) - change;
 

+ 2 - 2
crates/cdk-sql-common/src/mint/mod.rs

@@ -17,7 +17,7 @@ use async_trait::async_trait;
 use bitcoin::bip32::DerivationPath;
 use cdk_common::database::mint::{validate_kvstore_params, SagaDatabase, SagaTransaction};
 use cdk_common::database::{
-    self, ConversionError, Error, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction,
+    self, ConversionError, DbTransactionFinalizer, Error, MintDatabase, MintKeyDatabaseTransaction,
     MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase, MintQuotesTransaction,
     MintSignatureTransaction, MintSignaturesDatabase,
 };
@@ -280,7 +280,7 @@ impl<RM> database::MintTransaction<'_, Error> for SQLTransaction<RM> where RM: D
 {}
 
 #[async_trait]
-impl<RM> MintDbWriterFinalizer for SQLTransaction<RM>
+impl<RM> DbTransactionFinalizer for SQLTransaction<RM>
 where
     RM: DatabasePool + 'static,
 {

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 622 - 286
crates/cdk-sql-common/src/wallet/mod.rs


+ 1 - 1
crates/cdk/examples/auth_wallet.rs

@@ -37,7 +37,7 @@ async fn main() -> Result<(), Error> {
     let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None)?;
 
     let mint_info = wallet
-        .fetch_mint_info()
+        .fetch_mint_info(None)
         .await
         .expect("mint info")
         .expect("could not get mint info");

+ 1 - 1
crates/cdk/examples/proof-selection.rs

@@ -52,7 +52,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
     // Select proofs to send
     let amount = Amount::from(64);
     let active_keyset_ids = wallet
-        .refresh_keysets()
+        .refresh_keysets(None)
         .await?
         .active()
         .map(|keyset| keyset.id)

+ 67 - 27
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -19,6 +19,7 @@ use crate::nuts::{
 };
 use crate::types::ProofInfo;
 use crate::wallet::mint_connector::AuthHttpClient;
+use crate::wallet::Tx;
 use crate::{Amount, Error, OidcClient};
 
 /// JWT Claims structure for decoding tokens
@@ -181,7 +182,9 @@ impl AuthWallet {
 
             keys.verify_id()?;
 
-            self.localstore.add_keys(keys.clone()).await?;
+            let mut tx = self.localstore.begin_db_transaction().await?;
+            tx.add_keys(keys.clone()).await?;
+            tx.commit().await?;
 
             keys.keys
         };
@@ -195,8 +198,11 @@ impl AuthWallet {
     /// goes online to refresh keysets from the mint and updates the local database.
     /// This is the main method for getting auth keysets in operations that can work offline
     /// but will fall back to online if needed.
-    #[instrument(skip(self))]
-    pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
+    #[instrument(skip(self, tx))]
+    pub async fn load_mint_keysets(
+        &self,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<Vec<KeySetInfo>, Error> {
         match self
             .localstore
             .get_mint_keysets(self.mint_url.clone())
@@ -207,7 +213,7 @@ impl AuthWallet {
                     keysets_info.unit(CurrencyUnit::Sat).cloned().collect();
                 if auth_keysets.is_empty() {
                     // If we don't have any auth keysets, fetch them from the mint
-                    let keysets = self.refresh_keysets().await?;
+                    let keysets = self.refresh_keysets(tx).await?;
                     Ok(keysets)
                 } else {
                     Ok(auth_keysets)
@@ -215,7 +221,7 @@ impl AuthWallet {
             }
             None => {
                 // If we don't have any keysets, fetch them from the mint
-                let keysets = self.refresh_keysets().await?;
+                let keysets = self.refresh_keysets(tx).await?;
                 Ok(keysets)
             }
         }
@@ -227,15 +233,24 @@ impl AuthWallet {
     /// It updates the local database with the fetched keysets and ensures we have keys for all keysets.
     /// Returns only the keysets with Auth currency unit. This is used when operations need the most
     /// up-to-date keyset information and are willing to go online.
-    #[instrument(skip(self))]
-    pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
+    #[instrument(skip(self, tx))]
+    pub async fn refresh_keysets(
+        &self,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<Vec<KeySetInfo>, Error> {
         let keysets_response = self.client.get_mint_blind_auth_keysets().await?;
         let keysets = keysets_response.keysets;
 
         // Update local store with keysets
-        self.localstore
-            .add_mint_keysets(self.mint_url.clone(), keysets.clone())
-            .await?;
+        if let Some(tx) = tx {
+            tx.add_mint_keysets(self.mint_url.clone(), keysets.clone())
+                .await?;
+        } else {
+            let mut tx = self.localstore.begin_db_transaction().await?;
+            tx.add_mint_keysets(self.mint_url.clone(), keysets.clone())
+                .await?;
+            tx.commit().await?;
+        }
 
         // Filter for auth keysets
         let auth_keysets = keysets
@@ -260,9 +275,12 @@ impl AuthWallet {
     /// This method always goes online to refresh keysets from the mint and then returns
     /// the first active keyset found. Use this when you need the most up-to-date
     /// keyset information for blind auth operations.
-    #[instrument(skip(self))]
-    pub async fn fetch_active_keyset(&self) -> Result<KeySetInfo, Error> {
-        let auth_keysets = self.refresh_keysets().await?;
+    #[instrument(skip(self, tx))]
+    pub async fn fetch_active_keyset(
+        &self,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<KeySetInfo, Error> {
+        let auth_keysets = self.refresh_keysets(tx).await?;
         let keyset = auth_keysets.first().ok_or(Error::NoActiveKeyset)?;
         Ok(keyset.clone())
     }
@@ -271,11 +289,16 @@ impl AuthWallet {
     ///
     /// Returns auth proofs from the local database that are in the Unspent state.
     /// This is an offline operation that does not contact the mint.
-    #[instrument(skip(self))]
-    pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Error> {
-        Ok(self
-            .localstore
-            .get_proofs(
+    ///
+    /// If a DB Transaction is passed as an argument the selected proofs are locked for update
+    /// within this DBTransaction
+    #[instrument(skip(self, tx))]
+    pub async fn get_unspent_auth_proofs(
+        &self,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<Vec<AuthProof>, Error> {
+        Ok(if let Some(tx) = tx {
+            tx.get_proofs(
                 Some(self.mint_url.clone()),
                 Some(CurrencyUnit::Auth),
                 Some(vec![State::Unspent]),
@@ -284,7 +307,20 @@ impl AuthWallet {
             .await?
             .into_iter()
             .map(|p| p.proof.try_into())
-            .collect::<Result<Vec<AuthProof>, _>>()?)
+            .collect::<Result<Vec<AuthProof>, _>>()?
+        } else {
+            self.localstore
+                .get_proofs(
+                    Some(self.mint_url.clone()),
+                    Some(CurrencyUnit::Auth),
+                    Some(vec![State::Unspent]),
+                    None,
+                )
+                .await?
+                .into_iter()
+                .map(|p| p.proof.try_into())
+                .collect::<Result<Vec<AuthProof>, _>>()?
+        })
     }
 
     /// Check if and what kind of auth is required for a method
@@ -298,18 +334,20 @@ impl AuthWallet {
     /// Get Auth Token
     #[instrument(skip(self))]
     pub async fn get_blind_auth_token(&self) -> Result<Option<BlindAuthToken>, Error> {
-        let unspent = self.get_unspent_auth_proofs().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        let unspent = self.get_unspent_auth_proofs(Some(&mut tx)).await?;
 
         let auth_proof = match unspent.first() {
             Some(proof) => {
-                self.localstore
-                    .update_proofs(vec![], vec![proof.y()?])
-                    .await?;
+                tx.update_proofs(vec![], vec![proof.y()?]).await?;
                 proof
             }
             None => return Ok(None),
         };
 
+        tx.commit().await?;
+
         Ok(Some(BlindAuthToken {
             auth_proof: auth_proof.clone(),
         }))
@@ -394,13 +432,13 @@ impl AuthWallet {
         }
 
         let keysets = self
-            .load_mint_keysets()
+            .load_mint_keysets(None)
             .await?
             .into_iter()
             .map(|x| (x.id, x))
             .collect::<HashMap<_, _>>();
 
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let active_keyset_id = self.fetch_active_keyset(None).await?.id;
         let fee_and_amounts = (
             keysets
                 .get(&active_keyset_id)
@@ -467,7 +505,9 @@ impl AuthWallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
         // Add new proofs to store
-        self.localstore.update_proofs(proof_infos, vec![]).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.update_proofs(proof_infos, vec![]).await?;
+        tx.commit().await?;
 
         Ok(proofs)
     }
@@ -476,7 +516,7 @@ impl AuthWallet {
     #[instrument(skip(self))]
     pub async fn total_blind_auth_balance(&self) -> Result<Amount, Error> {
         Ok(Amount::from(
-            self.get_unspent_auth_proofs().await?.len() as u64
+            self.get_unspent_auth_proofs(None).await?.len() as u64,
         ))
     }
 }

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

@@ -30,7 +30,7 @@ impl Wallet {
             .await
             .as_ref()
             .ok_or(Error::AuthSettingsUndefined)?
-            .get_unspent_auth_proofs()
+            .get_unspent_auth_proofs(None)
             .await
     }
 

+ 1 - 1
crates/cdk/src/wallet/balance.rs

@@ -23,7 +23,7 @@ impl Wallet {
     /// Total pending balance
     #[instrument(skip(self))]
     pub async fn total_pending_balance(&self) -> Result<Amount, Error> {
-        Ok(self.get_pending_proofs().await?.total_amount()?)
+        Ok(self.get_pending_proofs(None).await?.total_amount()?)
     }
 
     /// Total reserved balance

+ 42 - 31
crates/cdk/src/wallet/issue/issue_bolt11.rs

@@ -52,7 +52,9 @@ impl Wallet {
         let mint_url = self.mint_url.clone();
         let unit = self.unit.clone();
 
-        self.refresh_keysets().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        self.refresh_keysets(Some(&mut tx)).await?;
 
         // If we have a description, we check that the mint supports it.
         if description.is_some() {
@@ -94,7 +96,8 @@ impl Wallet {
             Some(secret_key),
         );
 
-        self.localstore.add_mint_quote(quote.clone()).await?;
+        tx.add_mint_quote(quote.clone()).await?;
+        tx.commit().await?;
 
         Ok(quote)
     }
@@ -107,12 +110,15 @@ impl Wallet {
     ) -> Result<MintQuoteBolt11Response<String>, Error> {
         let response = self.client.get_mint_quote_status(quote_id).await?;
 
-        match self.localstore.get_mint_quote(quote_id).await? {
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        match tx.get_mint_quote(quote_id).await? {
             Some(quote) => {
                 let mut quote = quote;
 
                 quote.state = response.state;
-                self.localstore.add_mint_quote(quote).await?;
+                tx.add_mint_quote(quote).await?;
+                tx.commit().await?;
             }
             None => {
                 tracing::info!("Quote mint {} unknown", quote_id);
@@ -137,7 +143,9 @@ impl Wallet {
                     .await?;
                 total_amount += proofs.total_amount()?;
             } else if mint_quote.expiry.le(&unix_time()) {
-                self.localstore.remove_mint_quote(&mint_quote.id).await?;
+                let mut tx = self.localstore.begin_db_transaction().await?;
+                tx.remove_mint_quote(&mint_quote.id).await?;
+                tx.commit().await?;
             }
         }
         Ok(total_amount)
@@ -196,10 +204,11 @@ impl Wallet {
         amount_split_target: SplitTarget,
         spending_conditions: Option<SpendingConditions>,
     ) -> Result<Proofs, Error> {
-        self.refresh_keysets().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        self.refresh_keysets(Some(&mut tx)).await?;
 
-        let quote_info = self
-            .localstore
+        let quote_info = tx
             .get_mint_quote(quote_id)
             .await?
             .ok_or(Error::UnknownQuote)?;
@@ -221,7 +230,7 @@ impl Wallet {
             tracing::warn!("Attempting to mint with expired quote.");
         }
 
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let active_keyset_id = self.fetch_active_keyset(Some(&mut tx)).await?.id;
         let fee_and_amounts = self
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
@@ -247,8 +256,7 @@ impl Wallet {
                 );
 
                 // Atomically get the counter range we need
-                let new_counter = self
-                    .localstore
+                let new_counter = tx
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .await?;
 
@@ -277,12 +285,14 @@ impl Wallet {
 
         let mint_res = self.client.post_mint(request).await?;
 
-        let keys = self.load_keyset_keys(active_keyset_id).await?;
+        let keys = self
+            .load_keyset_keys(active_keyset_id, Some(&mut tx))
+            .await?;
 
         // Verify the signature DLEQ is valid
         {
             for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
-                let keys = self.load_keyset_keys(sig.keyset_id).await?;
+                let keys = self.load_keyset_keys(sig.keyset_id, Some(&mut tx)).await?;
                 let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
                 match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
                     Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
@@ -299,7 +309,7 @@ impl Wallet {
         )?;
 
         // Remove filled quote from store
-        self.localstore.remove_mint_quote(&quote_info.id).await?;
+        tx.remove_mint_quote(&quote_info.id).await?;
 
         let proof_infos = proofs
             .iter()
@@ -314,25 +324,26 @@ impl Wallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
         // Add new proofs to store
-        self.localstore.update_proofs(proof_infos, vec![]).await?;
+        tx.update_proofs(proof_infos, vec![]).await?;
 
         // Add transaction to store
-        self.localstore
-            .add_transaction(Transaction {
-                mint_url: self.mint_url.clone(),
-                direction: TransactionDirection::Incoming,
-                amount: proofs.total_amount()?,
-                fee: Amount::ZERO,
-                unit: self.unit.clone(),
-                ys: proofs.ys()?,
-                timestamp: unix_time,
-                memo: None,
-                metadata: HashMap::new(),
-                quote_id: Some(quote_id.to_string()),
-                payment_request: Some(quote_info.request),
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Incoming,
+            amount: proofs.total_amount()?,
+            fee: Amount::ZERO,
+            unit: self.unit.clone(),
+            ys: proofs.ys()?,
+            timestamp: unix_time,
+            memo: None,
+            metadata: HashMap::new(),
+            quote_id: Some(quote_id.to_string()),
+            payment_request: Some(quote_info.request),
+            payment_proof: None,
+        })
+        .await?;
+
+        tx.commit().await?;
 
         Ok(proofs)
     }

+ 40 - 32
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -15,7 +15,7 @@ use crate::nuts::{
 };
 use crate::types::ProofInfo;
 use crate::util::unix_time;
-use crate::wallet::MintQuote;
+use crate::wallet::{MintQuote, Tx};
 use crate::{Amount, Error, Wallet};
 
 impl Wallet {
@@ -29,7 +29,9 @@ impl Wallet {
         let mint_url = self.mint_url.clone();
         let unit = &self.unit;
 
-        self.refresh_keysets().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        self.refresh_keysets(Some(&mut tx)).await?;
 
         // If we have a description, we check that the mint supports it.
         if description.is_some() {
@@ -71,7 +73,8 @@ impl Wallet {
             Some(secret_key),
         );
 
-        self.localstore.add_mint_quote(quote.clone()).await?;
+        tx.add_mint_quote(quote.clone()).await?;
+        tx.commit().await?;
 
         Ok(quote)
     }
@@ -85,9 +88,11 @@ impl Wallet {
         amount_split_target: SplitTarget,
         spending_conditions: Option<SpendingConditions>,
     ) -> Result<Proofs, Error> {
-        self.refresh_keysets().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        self.refresh_keysets(Some(&mut tx)).await?;
 
-        let quote_info = self.localstore.get_mint_quote(quote_id).await?;
+        let quote_info = tx.get_mint_quote(quote_id).await?;
 
         let quote_info = if let Some(quote) = quote_info {
             if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) {
@@ -99,7 +104,7 @@ impl Wallet {
             return Err(Error::UnknownQuote);
         };
 
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let active_keyset_id = self.fetch_active_keyset(Some(&mut tx)).await?.id;
         let fee_and_amounts = self
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
@@ -109,7 +114,7 @@ impl Wallet {
             None => {
                 // If an amount it not supplied with check the status of the quote
                 // The mint will tell us how much can be minted
-                let state = self.mint_bolt12_quote_state(quote_id).await?;
+                let state = self.mint_bolt12_quote_state(quote_id, &mut tx).await?;
 
                 state.amount_paid - state.amount_issued
             }
@@ -140,8 +145,7 @@ impl Wallet {
                 );
 
                 // Atomically get the counter range we need
-                let new_counter = self
-                    .localstore
+                let new_counter = tx
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .await?;
 
@@ -173,12 +177,14 @@ impl Wallet {
 
         let mint_res = self.client.post_mint(request).await?;
 
-        let keys = self.load_keyset_keys(active_keyset_id).await?;
+        let keys = self
+            .load_keyset_keys(active_keyset_id, Some(&mut tx))
+            .await?;
 
         // Verify the signature DLEQ is valid
         {
             for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) {
-                let keys = self.load_keyset_keys(sig.keyset_id).await?;
+                let keys = self.load_keyset_keys(sig.keyset_id, Some(&mut tx)).await?;
                 let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
                 match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
                     Ok(_) | Err(nut12::Error::MissingDleqProof) => (),
@@ -202,7 +208,7 @@ impl Wallet {
             .ok_or(Error::UnpaidQuote)?;
         quote_info.amount_issued += proofs.total_amount()?;
 
-        self.localstore.add_mint_quote(quote_info.clone()).await?;
+        tx.add_mint_quote(quote_info.clone()).await?;
 
         let proof_infos = proofs
             .iter()
@@ -217,44 +223,46 @@ impl Wallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
         // Add new proofs to store
-        self.localstore.update_proofs(proof_infos, vec![]).await?;
+        tx.update_proofs(proof_infos, vec![]).await?;
 
         // Add transaction to store
-        self.localstore
-            .add_transaction(Transaction {
-                mint_url: self.mint_url.clone(),
-                direction: TransactionDirection::Incoming,
-                amount: proofs.total_amount()?,
-                fee: Amount::ZERO,
-                unit: self.unit.clone(),
-                ys: proofs.ys()?,
-                timestamp: unix_time(),
-                memo: None,
-                metadata: HashMap::new(),
-                quote_id: Some(quote_id.to_string()),
-                payment_request: Some(quote_info.request),
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Incoming,
+            amount: proofs.total_amount()?,
+            fee: Amount::ZERO,
+            unit: self.unit.clone(),
+            ys: proofs.ys()?,
+            timestamp: unix_time(),
+            memo: None,
+            metadata: HashMap::new(),
+            quote_id: Some(quote_id.to_string()),
+            payment_request: Some(quote_info.request),
+            payment_proof: None,
+        })
+        .await?;
+
+        tx.commit().await?;
 
         Ok(proofs)
     }
 
     /// Check mint quote status
-    #[instrument(skip(self, quote_id))]
+    #[instrument(skip(self, quote_id, tx))]
     pub async fn mint_bolt12_quote_state(
         &self,
         quote_id: &str,
+        tx: &mut Tx<'_, '_>,
     ) -> Result<MintQuoteBolt12Response<String>, Error> {
         let response = self.client.get_mint_quote_bolt12_status(quote_id).await?;
 
-        match self.localstore.get_mint_quote(quote_id).await? {
+        match tx.get_mint_quote(quote_id).await? {
             Some(quote) => {
                 let mut quote = quote;
                 quote.amount_issued = response.amount_issued;
                 quote.amount_paid = response.amount_paid;
 
-                self.localstore.add_mint_quote(quote).await?;
+                tx.add_mint_quote(quote).await?;
             }
             None => {
                 tracing::info!("Quote mint {} unknown", quote_id);

+ 41 - 15
crates/cdk/src/wallet/keysets.rs

@@ -7,13 +7,19 @@ use tracing::instrument;
 use crate::nuts::{Id, KeySetInfo, Keys};
 use crate::{Error, Wallet};
 
+use super::Tx;
+
 impl Wallet {
     /// Load keys for mint keyset
     ///
     /// Returns keys from local database if they are already stored.
     /// If keys are not found locally, goes online to query the mint for the keyset and stores the [`Keys`] in local database.
-    #[instrument(skip(self))]
-    pub async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
+    #[instrument(skip(self, tx))]
+    pub async fn load_keyset_keys(
+        &self,
+        keyset_id: Id,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<Keys, Error> {
         let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? {
             keys
         } else {
@@ -27,7 +33,13 @@ impl Wallet {
 
             keys.verify_id()?;
 
-            self.localstore.add_keys(keys.clone()).await?;
+            if let Some(tx) = tx {
+                tx.add_keys(keys.clone()).await?;
+            } else {
+                let mut tx = self.localstore.begin_db_transaction().await?;
+                tx.add_keys(keys.clone()).await?;
+                tx.commit().await?;
+            }
 
             keys.keys
         };
@@ -51,7 +63,7 @@ impl Wallet {
             Some(keysets_info) => Ok(keysets_info),
             None => {
                 // If we don't have any keysets, fetch them from the mint
-                let keysets = self.refresh_keysets().await?;
+                let keysets = self.refresh_keysets(None).await?;
                 Ok(keysets)
             }
         }
@@ -80,26 +92,37 @@ impl Wallet {
     /// It updates the local database with the fetched keysets and ensures we have keys
     /// for all active keysets. This is used when operations need the most up-to-date
     /// keyset information and are willing to go online.
-    #[instrument(skip(self))]
-    pub async fn refresh_keysets(&self) -> Result<KeySetInfos, Error> {
+    #[instrument(skip(self, tx))]
+    pub async fn refresh_keysets(&self, tx: Option<&mut Tx<'_, '_>>) -> Result<KeySetInfos, Error> {
         tracing::debug!("Refreshing keysets and ensuring we have keys");
-        let _ = self.fetch_mint_info().await?;
+        let _ = self.fetch_mint_info(None).await?;
 
         // Fetch all current keysets from mint
         let keysets_response = self.client.get_mint_keysets().await?;
         let all_keysets = keysets_response.keysets;
 
         // Update local storage with keyset info
-        self.localstore
-            .add_mint_keysets(self.mint_url.clone(), all_keysets.clone())
-            .await?;
+        let mut tx = tx;
+        if let Some(tx) = tx.as_mut() {
+            tx.add_mint_keysets(self.mint_url.clone(), all_keysets.clone())
+                .await?;
+        } else {
+            let mut tx = self.localstore.begin_db_transaction().await?;
+            tx.add_mint_keysets(self.mint_url.clone(), all_keysets.clone())
+                .await?;
+            tx.commit().await?;
+        }
 
         // Filter for active keysets matching our unit
         let keysets: KeySetInfos = all_keysets.unit(self.unit.clone()).cloned().collect();
 
         // Ensure we have keys for all active keysets
         for keyset in &keysets {
-            self.load_keyset_keys(keyset.id).await?;
+            if let Some(tx) = tx.as_mut() {
+                self.load_keyset_keys(keyset.id, Some(*tx)).await?;
+            } else {
+                self.load_keyset_keys(keyset.id, None).await?;
+            }
         }
 
         Ok(keysets)
@@ -110,9 +133,12 @@ impl Wallet {
     /// This method always goes online to refresh keysets from the mint and then returns
     /// the active keyset with the minimum input fees. Use this when you need the most
     /// up-to-date keyset information for operations.
-    #[instrument(skip(self))]
-    pub async fn fetch_active_keyset(&self) -> Result<KeySetInfo, Error> {
-        self.refresh_keysets()
+    #[instrument(skip(self, tx))]
+    pub async fn fetch_active_keyset(
+        &self,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<KeySetInfo, Error> {
+        self.refresh_keysets(tx)
             .await?
             .active()
             .min_by_key(|k| k.input_fee_ppk)
@@ -158,7 +184,7 @@ impl Wallet {
                 keyset.id,
                 (
                     keyset.input_fee_ppk,
-                    self.load_keyset_keys(keyset.id)
+                    self.load_keyset_keys(keyset.id, None)
                         .await?
                         .iter()
                         .map(|(amount, _)| amount.to_u64())

+ 52 - 55
crates/cdk/src/wallet/melt/melt_bolt11.rs

@@ -15,7 +15,7 @@ use crate::nuts::{
 };
 use crate::types::{Melted, ProofInfo};
 use crate::util::unix_time;
-use crate::wallet::MeltQuote;
+use crate::wallet::{MeltQuote, Tx};
 use crate::{ensure_cdk, Amount, Error, Wallet};
 
 impl Wallet {
@@ -49,7 +49,7 @@ impl Wallet {
         request: String,
         options: Option<MeltOptions>,
     ) -> Result<MeltQuote, Error> {
-        self.refresh_keysets().await?;
+        self.refresh_keysets(None).await?;
 
         let invoice = Bolt11Invoice::from_str(&request)?;
 
@@ -91,7 +91,9 @@ impl Wallet {
             payment_method: PaymentMethod::Bolt11,
         };
 
-        self.localstore.add_melt_quote(quote.clone()).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.add_melt_quote(quote.clone()).await?;
+        tx.commit().await?;
 
         Ok(quote)
     }
@@ -103,50 +105,54 @@ impl Wallet {
         quote_id: &str,
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         let response = self.client.get_melt_quote_status(quote_id).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
 
-        match self.localstore.get_melt_quote(quote_id).await? {
+        match tx.get_melt_quote(quote_id).await? {
             Some(quote) => {
                 let mut quote = quote;
 
                 if let Err(e) = self
-                    .add_transaction_for_pending_melt(&quote, &response)
+                    .add_transaction_for_pending_melt(&quote, &response, &mut tx)
                     .await
                 {
                     tracing::error!("Failed to add transaction for pending melt: {}", e);
                 }
 
                 quote.state = response.state;
-                self.localstore.add_melt_quote(quote).await?;
+                tx.add_melt_quote(quote).await?;
             }
             None => {
                 tracing::info!("Quote melt {} unknown", quote_id);
             }
         }
 
-        Ok(response)
-    }
+        tx.commit().await?;
 
-    /// Melt specific proofs
-    #[instrument(skip(self, proofs))]
-    pub async fn melt_proofs(&self, quote_id: &str, proofs: Proofs) -> Result<Melted, Error> {
-        self.melt_proofs_with_metadata(quote_id, proofs, HashMap::new())
-            .await
+        Ok(response)
     }
 
     /// Melt specific proofs
-    #[instrument(skip(self, proofs))]
+    #[instrument(skip(self, proofs, tx))]
     pub async fn melt_proofs_with_metadata(
         &self,
         quote_id: &str,
         proofs: Proofs,
         metadata: HashMap<String, String>,
+        tx: &mut Tx<'_, '_>,
     ) -> Result<Melted, Error> {
-        let quote_info = self
-            .localstore
+        let quote_info = tx
             .get_melt_quote(quote_id)
             .await?
             .ok_or(Error::UnknownQuote)?;
 
+        let active_keyset_id = self.fetch_active_keyset(Some(tx)).await?.id;
+
+        let active_keys = self
+            .localstore
+            .get_keys(&active_keyset_id)
+            .await?
+            .ok_or(Error::NoActiveKeyset)?;
+
         ensure_cdk!(
             quote_info.expiry.gt(&unix_time()),
             Error::ExpiredQuote(quote_info.expiry, unix_time())
@@ -158,11 +164,7 @@ impl Wallet {
         }
 
         let ys = proofs.ys()?;
-        self.localstore
-            .update_proofs_state(ys, State::Pending)
-            .await?;
-
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        tx.update_proofs_state(ys, State::Pending).await?;
 
         let change_amount = proofs_total - quote_info.amount;
 
@@ -181,8 +183,7 @@ impl Wallet {
             );
 
             // Atomically get the counter range we need
-            let new_counter = self
-                .localstore
+            let new_counter = tx
                 .increment_keyset_counter(&active_keyset_id, num_secrets)
                 .await?;
 
@@ -211,18 +212,12 @@ impl Wallet {
                 tracing::error!("Could not melt: {}", err);
                 tracing::info!("Checking status of input proofs.");
 
-                self.reclaim_unspent(proofs).await?;
+                self.reclaim_unspent(proofs, tx).await?;
 
                 return Err(err);
             }
         };
 
-        let active_keys = self
-            .localstore
-            .get_keys(&active_keyset_id)
-            .await?
-            .ok_or(Error::NoActiveKeyset)?;
-
         let change_proofs = match melt_response.change {
             Some(change) => {
                 let num_change_proof = change.len();
@@ -280,30 +275,27 @@ impl Wallet {
             None => Vec::new(),
         };
 
-        self.localstore.remove_melt_quote(&quote_info.id).await?;
+        tx.remove_melt_quote(&quote_info.id).await?;
 
         let deleted_ys = proofs.ys()?;
-        self.localstore
-            .update_proofs(change_proof_infos, deleted_ys)
-            .await?;
+        tx.update_proofs(change_proof_infos, deleted_ys).await?;
 
         // Add transaction to store
-        self.localstore
-            .add_transaction(Transaction {
-                mint_url: self.mint_url.clone(),
-                direction: TransactionDirection::Outgoing,
-                amount: melted.amount,
-                fee: melted.fee_paid,
-                unit: self.unit.clone(),
-                ys: proofs.ys()?,
-                timestamp: unix_time(),
-                memo: None,
-                metadata,
-                quote_id: Some(quote_id.to_string()),
-                payment_request: Some(quote_info.request),
-                payment_proof: payment_preimage,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Outgoing,
+            amount: melted.amount,
+            fee: melted.fee_paid,
+            unit: self.unit.clone(),
+            ys: proofs.ys()?,
+            timestamp: unix_time(),
+            memo: None,
+            metadata,
+            quote_id: Some(quote_id.to_string()),
+            payment_request: Some(quote_info.request),
+            payment_proof: payment_preimage,
+        })
+        .await?;
 
         Ok(melted)
     }
@@ -374,8 +366,8 @@ impl Wallet {
         quote_id: &str,
         metadata: HashMap<String, String>,
     ) -> Result<Melted, Error> {
-        let quote_info = self
-            .localstore
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        let quote_info = tx
             .get_melt_quote(quote_id)
             .await?
             .ok_or(Error::UnknownQuote)?;
@@ -390,7 +382,7 @@ impl Wallet {
         let available_proofs = self.get_unspent_proofs().await?;
 
         let active_keyset_ids = self
-            .refresh_keysets()
+            .refresh_keysets(Some(&mut tx))
             .await?
             .into_iter()
             .map(|k| k.id)
@@ -422,7 +414,12 @@ impl Wallet {
             input_proofs.extend_from_slice(&new_proofs);
         }
 
-        self.melt_proofs_with_metadata(quote_id, input_proofs, metadata)
-            .await
+        let melted = self
+            .melt_proofs_with_metadata(quote_id, input_proofs, metadata, &mut tx)
+            .await?;
+
+        tx.commit().await?;
+
+        Ok(melted)
     }
 }

+ 10 - 4
crates/cdk/src/wallet/melt/melt_bolt12.rs

@@ -61,7 +61,9 @@ impl Wallet {
             payment_method: PaymentMethod::Bolt12,
         };
 
-        self.localstore.add_melt_quote(quote.clone()).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.add_melt_quote(quote.clone()).await?;
+        tx.commit().await?;
 
         Ok(quote)
     }
@@ -74,25 +76,29 @@ impl Wallet {
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         let response = self.client.get_melt_bolt12_quote_status(quote_id).await?;
 
-        match self.localstore.get_melt_quote(quote_id).await? {
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        match tx.get_melt_quote(quote_id).await? {
             Some(quote) => {
                 let mut quote = quote;
 
                 if let Err(e) = self
-                    .add_transaction_for_pending_melt(&quote, &response)
+                    .add_transaction_for_pending_melt(&quote, &response, &mut tx)
                     .await
                 {
                     tracing::error!("Failed to add transaction for pending melt: {}", e);
                 }
 
                 quote.state = response.state;
-                self.localstore.add_melt_quote(quote).await?;
+                tx.add_melt_quote(quote).await?;
             }
             None => {
                 tracing::info!("Quote melt {} unknown", quote_id);
             }
         }
 
+        tx.commit().await?;
+
         Ok(response)
     }
 }

+ 22 - 20
crates/cdk/src/wallet/melt/mod.rs

@@ -7,6 +7,8 @@ use tracing::instrument;
 
 use crate::Wallet;
 
+use super::Tx;
+
 #[cfg(all(feature = "bip353", not(target_arch = "wasm32")))]
 mod melt_bip353;
 mod melt_bolt11;
@@ -48,6 +50,7 @@ impl Wallet {
         &self,
         quote: &MeltQuote,
         response: &MeltQuoteBolt11Response<String>,
+        tx: &mut Tx<'_, '_>,
     ) -> Result<(), Error> {
         if quote.state != response.state {
             tracing::info!(
@@ -57,29 +60,28 @@ impl Wallet {
                 response.state
             );
             if response.state == MeltQuoteState::Paid {
-                let pending_proofs = self.get_pending_proofs().await?;
+                let pending_proofs = self.get_pending_proofs(Some(tx)).await?;
                 let proofs_total = pending_proofs.total_amount().unwrap_or_default();
                 let change_total = response.change_amount().unwrap_or_default();
 
-                self.localstore
-                    .add_transaction(Transaction {
-                        mint_url: self.mint_url.clone(),
-                        direction: TransactionDirection::Outgoing,
-                        amount: response.amount,
-                        fee: proofs_total
-                            .checked_sub(response.amount)
-                            .and_then(|amt| amt.checked_sub(change_total))
-                            .unwrap_or_default(),
-                        unit: quote.unit.clone(),
-                        ys: pending_proofs.ys()?,
-                        timestamp: unix_time(),
-                        memo: None,
-                        metadata: HashMap::new(),
-                        quote_id: Some(quote.id.clone()),
-                        payment_request: Some(quote.request.clone()),
-                        payment_proof: response.payment_preimage.clone(),
-                    })
-                    .await?;
+                tx.add_transaction(Transaction {
+                    mint_url: self.mint_url.clone(),
+                    direction: TransactionDirection::Outgoing,
+                    amount: response.amount,
+                    fee: proofs_total
+                        .checked_sub(response.amount)
+                        .and_then(|amt| amt.checked_sub(change_total))
+                        .unwrap_or_default(),
+                    unit: quote.unit.clone(),
+                    ys: pending_proofs.ys()?,
+                    timestamp: unix_time(),
+                    memo: None,
+                    metadata: HashMap::new(),
+                    quote_id: Some(quote.id.clone()),
+                    payment_request: Some(quote.request.clone()),
+                    payment_proof: response.payment_preimage.clone(),
+                })
+                .await?;
             }
         }
         Ok(())

+ 0 - 0
crates/cdk/src/wallet/mint_connector/transport.rs → crates/cdk/src/wallet/mint_connector/transport/mod.rs


+ 32 - 17
crates/cdk/src/wallet/mod.rs

@@ -5,7 +5,7 @@ use std::str::FromStr;
 use std::sync::Arc;
 
 use cdk_common::amount::FeeAndAmounts;
-use cdk_common::database::{self, WalletDatabase};
+use cdk_common::database::{self, WalletDatabase, WalletDatabaseTransaction};
 use cdk_common::subscription::WalletParams;
 use getrandom::getrandom;
 use subscription::{ActiveSubscription, SubscriptionManager};
@@ -71,6 +71,9 @@ pub use types::{MeltQuote, MintQuote, SendKind};
 
 use crate::nuts::nut00::ProofsMethods;
 
+pub(crate) type Tx<'a, 'b> =
+    Box<dyn WalletDatabaseTransaction<'a, database::Error> + Send + Sync + 'b>;
+
 /// CDK Wallet
 ///
 /// The CDK [`Wallet`] is a high level cashu wallet.
@@ -249,9 +252,10 @@ impl Wallet {
     #[instrument(skip(self))]
     pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
         // Update the mint URL in the wallet DB
-        self.localstore
-            .update_mint_url(self.mint_url.clone(), new_mint_url.clone())
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.update_mint_url(self.mint_url.clone(), new_mint_url.clone())
             .await?;
+        tx.commit().await?;
 
         // Update the mint URL in the wallet struct field
         self.mint_url = new_mint_url;
@@ -260,8 +264,11 @@ impl Wallet {
     }
 
     /// Query mint for current mint information
-    #[instrument(skip(self))]
-    pub async fn fetch_mint_info(&self) -> Result<Option<MintInfo>, Error> {
+    #[instrument(skip(self, tx))]
+    pub async fn fetch_mint_info(
+        &self,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<Option<MintInfo>, Error> {
         match self.client.get_mint_info().await {
             Ok(mint_info) => {
                 // If mint provides time make sure it is accurate
@@ -314,9 +321,15 @@ impl Wallet {
                     }
                 }
 
-                self.localstore
-                    .add_mint(self.mint_url.clone(), Some(mint_info.clone()))
-                    .await?;
+                if let Some(tx) = tx {
+                    tx.add_mint(self.mint_url.clone(), Some(mint_info.clone()))
+                        .await?;
+                } else {
+                    let mut tx = self.localstore.begin_db_transaction().await?;
+                    tx.add_mint(self.mint_url.clone(), Some(mint_info.clone()))
+                        .await?;
+                    tx.commit().await?;
+                };
 
                 tracing::trace!("Mint info updated for {}", self.mint_url);
 
@@ -399,7 +412,7 @@ impl Wallet {
             .await?
             .is_none()
         {
-            self.fetch_mint_info().await?;
+            self.fetch_mint_info(None).await?;
         }
 
         let keysets = self.load_mint_keysets().await?;
@@ -407,7 +420,8 @@ impl Wallet {
         let mut restored_value = Amount::ZERO;
 
         for keyset in keysets {
-            let keys = self.load_keyset_keys(keyset.id).await?;
+            let mut tx = self.localstore.begin_db_transaction().await?;
+            let keys = self.load_keyset_keys(keyset.id, Some(&mut tx)).await?;
             let mut empty_batch = 0;
             let mut start_counter = 0;
 
@@ -458,11 +472,12 @@ impl Wallet {
 
                 tracing::debug!("Restored {} proofs", proofs.len());
 
-                self.localstore
-                    .increment_keyset_counter(&keyset.id, proofs.len() as u32)
+                tx.increment_keyset_counter(&keyset.id, proofs.len() as u32)
                     .await?;
 
-                let states = self.check_proofs_spent(proofs.clone()).await?;
+                let states = self
+                    .check_proofs_spent(proofs.clone(), Some(&mut tx))
+                    .await?;
 
                 let unspent_proofs: Vec<Proof> = proofs
                     .iter()
@@ -486,13 +501,13 @@ impl Wallet {
                     })
                     .collect::<Result<Vec<ProofInfo>, _>>()?;
 
-                self.localstore
-                    .update_proofs(unspent_proofs, vec![])
-                    .await?;
+                tx.update_proofs(unspent_proofs, vec![]).await?;
 
                 empty_batch = 0;
                 start_counter += 100;
             }
+
+            tx.commit().await?;
         }
         Ok(restored_value)
     }
@@ -656,7 +671,7 @@ impl Wallet {
             let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
                 Some(keys) => keys.amount_key(proof.amount),
                 None => {
-                    let keys = self.load_keyset_keys(proof.keyset_id).await?;
+                    let keys = self.load_keyset_keys(proof.keyset_id, None).await?;
 
                     let key = keys.amount_key(proof.amount);
                     keys_cache.insert(proof.keyset_id, keys);

+ 11 - 2
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -1173,8 +1173,15 @@ impl MultiMintWallet {
 
         let mut amount_received = Amount::ZERO;
 
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
         match wallet
-            .receive_proofs(proofs, opts.receive_options, token_data.memo().clone())
+            .receive_proofs(
+                &mut tx,
+                proofs,
+                opts.receive_options,
+                token_data.memo().clone(),
+            )
             .await
         {
             Ok(amount) => {
@@ -1190,6 +1197,8 @@ impl MultiMintWallet {
             }
         }
 
+        tx.commit().await?;
+
         drop(wallets);
 
         // If we should transfer to a trusted mint, do so now
@@ -1680,7 +1689,7 @@ impl MultiMintWallet {
             mint_url: mint_url.to_string(),
         })?;
 
-        wallet.fetch_mint_info().await
+        wallet.fetch_mint_info(None).await
     }
 }
 

+ 66 - 31
crates/cdk/src/wallet/proofs.rs

@@ -14,30 +14,36 @@ use crate::nuts::{
 use crate::types::ProofInfo;
 use crate::{ensure_cdk, Amount, Error, Wallet};
 
+use super::Tx;
+
 impl Wallet {
     /// Get unspent proofs for mint
     #[instrument(skip(self))]
     pub async fn get_unspent_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(Some(vec![State::Unspent]), None).await
+        self.get_proofs_with(Some(vec![State::Unspent]), None, None)
+            .await
     }
 
     /// Get pending [`Proofs`]
-    #[instrument(skip(self))]
-    pub async fn get_pending_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(Some(vec![State::Pending]), None).await
+    #[instrument(skip(self, tx))]
+    pub async fn get_pending_proofs(&self, tx: Option<&mut Tx<'_, '_>>) -> Result<Proofs, Error> {
+        self.get_proofs_with(Some(vec![State::Pending]), None, tx)
+            .await
     }
 
     /// Get reserved [`Proofs`]
     #[instrument(skip(self))]
     pub async fn get_reserved_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(Some(vec![State::Reserved]), None)
+        self.get_proofs_with(Some(vec![State::Reserved]), None, None)
             .await
     }
 
     /// Get pending spent [`Proofs`]
-    #[instrument(skip(self))]
-    pub async fn get_pending_spent_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(Some(vec![State::PendingSpent]), None)
+    pub async fn get_pending_spent_proofs(
+        &self,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<Proofs, Error> {
+        self.get_proofs_with(Some(vec![State::PendingSpent]), None, tx)
             .await
     }
 
@@ -46,10 +52,10 @@ impl Wallet {
         &self,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
+        tx: Option<&mut Tx<'_, '_>>,
     ) -> Result<Proofs, Error> {
-        Ok(self
-            .localstore
-            .get_proofs(
+        Ok(if let Some(tx) = tx {
+            tx.get_proofs(
                 Some(self.mint_url.clone()),
                 Some(self.unit.clone()),
                 state,
@@ -58,23 +64,37 @@ impl Wallet {
             .await?
             .into_iter()
             .map(|p| p.proof)
-            .collect())
+            .collect()
+        } else {
+            self.localstore
+                .get_proofs(
+                    Some(self.mint_url.clone()),
+                    Some(self.unit.clone()),
+                    state,
+                    spending_conditions,
+                )
+                .await?
+                .into_iter()
+                .map(|p| p.proof)
+                .collect()
+        })
     }
 
     /// Return proofs to unspent allowing them to be selected and spent
     #[instrument(skip(self))]
     pub async fn unreserve_proofs(&self, ys: Vec<PublicKey>) -> Result<(), Error> {
-        Ok(self
-            .localstore
-            .update_proofs_state(ys, State::Unspent)
-            .await?)
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        tx.update_proofs_state(ys, State::Unspent).await?;
+
+        Ok(tx.commit().await?)
     }
 
     /// Reclaim unspent proofs
     ///
     /// Checks the stats of [`Proofs`] swapping for a new [`Proof`] if unspent
-    #[instrument(skip(self, proofs))]
-    pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> {
+    #[instrument(skip(self, proofs, tx))]
+    pub async fn reclaim_unspent(&self, proofs: Proofs, tx: &mut Tx<'_, '_>) -> Result<(), Error> {
         let proof_ys = proofs.ys()?;
 
         let transaction_id = TransactionId::new(proof_ys.clone());
@@ -94,7 +114,7 @@ impl Wallet {
         self.swap(None, SplitTarget::default(), unspent, None, false)
             .await?;
 
-        match self.localstore.remove_transaction(transaction_id).await {
+        match tx.remove_transaction(transaction_id).await {
             Ok(_) => (),
             Err(e) => {
                 tracing::warn!("Failed to remove transaction: {:?}", e);
@@ -105,8 +125,12 @@ impl Wallet {
     }
 
     /// NUT-07 Check the state of a [`Proof`] with the mint
-    #[instrument(skip(self, proofs))]
-    pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result<Vec<ProofState>, Error> {
+    #[instrument(skip(self, proofs, tx))]
+    pub async fn check_proofs_spent(
+        &self,
+        proofs: Proofs,
+        tx: Option<&mut Tx<'_, '_>>,
+    ) -> Result<Vec<ProofState>, Error> {
         let spendable = self
             .client
             .post_check_state(CheckStateRequest { ys: proofs.ys()? })
@@ -121,7 +145,13 @@ impl Wallet {
             })
             .collect();
 
-        self.localstore.update_proofs(vec![], spent_ys).await?;
+        if let Some(tx) = tx {
+            tx.update_proofs(vec![], spent_ys).await?;
+        } else {
+            let mut tx = self.localstore.begin_db_transaction().await?;
+            tx.update_proofs(vec![], spent_ys).await?;
+            tx.commit().await?;
+        }
 
         Ok(spendable.states)
     }
@@ -131,8 +161,9 @@ impl Wallet {
     pub async fn check_all_pending_proofs(&self) -> Result<Amount, Error> {
         let mut balance = Amount::ZERO;
 
-        let proofs = self
-            .localstore
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        let proofs = tx
             .get_proofs(
                 Some(self.mint_url.clone()),
                 Some(self.unit.clone()),
@@ -146,7 +177,10 @@ impl Wallet {
         }
 
         let states = self
-            .check_proofs_spent(proofs.clone().into_iter().map(|p| p.proof).collect())
+            .check_proofs_spent(
+                proofs.clone().into_iter().map(|p| p.proof).collect(),
+                Some(&mut tx),
+            )
             .await?;
 
         // Both `State::Pending` and `State::Unspent` should be included in the pending
@@ -165,15 +199,16 @@ impl Wallet {
 
         let amount = Amount::try_sum(pending_proofs.iter().map(|p| p.proof.amount))?;
 
-        self.localstore
-            .update_proofs(
-                vec![],
-                non_pending_proofs.into_iter().map(|p| p.y).collect(),
-            )
-            .await?;
+        tx.update_proofs(
+            vec![],
+            non_pending_proofs.into_iter().map(|p| p.y).collect(),
+        )
+        .await?;
 
         balance += amount;
 
+        tx.commit().await?;
+
         Ok(balance)
     }
 

+ 36 - 33
crates/cdk/src/wallet/receive.rs

@@ -17,22 +17,25 @@ use crate::types::ProofInfo;
 use crate::util::hex;
 use crate::{ensure_cdk, Amount, Error, Wallet, SECP256K1};
 
+use super::Tx;
+
 impl Wallet {
     /// Receive proofs
     #[instrument(skip_all)]
     pub async fn receive_proofs(
         &self,
+        tx: &mut Tx<'_, '_>,
         proofs: Proofs,
         opts: ReceiveOptions,
         memo: Option<String>,
     ) -> Result<Amount, Error> {
         let mint_url = &self.mint_url;
 
-        self.refresh_keysets().await?;
+        self.refresh_keysets(Some(tx)).await?;
 
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let active_keyset_id = self.fetch_active_keyset(Some(tx)).await?.id;
 
-        let keys = self.load_keyset_keys(active_keyset_id).await?;
+        let keys = self.load_keyset_keys(active_keyset_id, Some(tx)).await?;
 
         let mut proofs = proofs;
 
@@ -60,7 +63,7 @@ impl Wallet {
         for proof in &mut proofs {
             // Verify that proof DLEQ is valid
             if proof.dleq.is_some() {
-                let keys = self.load_keyset_keys(proof.keyset_id).await?;
+                let keys = self.load_keyset_keys(proof.keyset_id, Some(tx)).await?;
                 let key = keys.amount_key(proof.amount).ok_or(Error::AmountKey)?;
                 proof.verify_dleq(key)?;
             }
@@ -112,12 +115,10 @@ impl Wallet {
             .into_iter()
             .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
-        self.localstore
-            .update_proofs(proofs_info.clone(), vec![])
-            .await?;
+        tx.update_proofs(proofs_info.clone(), vec![]).await?;
 
         let mut pre_swap = self
-            .create_swap(None, opts.amount_split_target, proofs, None, false)
+            .create_swap(tx, None, opts.amount_split_target, proofs, None, false)
             .await?;
 
         if sig_flag.eq(&SigFlag::SigAll) {
@@ -138,8 +139,7 @@ impl Wallet {
             &keys,
         )?;
 
-        self.localstore
-            .increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32)
+        tx.increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32)
             .await?;
 
         let total_amount = recv_proofs.total_amount()?;
@@ -148,30 +148,29 @@ impl Wallet {
             .into_iter()
             .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
-        self.localstore
-            .update_proofs(
-                recv_proof_infos,
-                proofs_info.into_iter().map(|p| p.y).collect(),
-            )
-            .await?;
+
+        tx.update_proofs(
+            recv_proof_infos,
+            proofs_info.into_iter().map(|p| p.y).collect(),
+        )
+        .await?;
 
         // Add transaction to store
-        self.localstore
-            .add_transaction(Transaction {
-                mint_url: self.mint_url.clone(),
-                direction: TransactionDirection::Incoming,
-                amount: total_amount,
-                fee: proofs_amount - total_amount,
-                unit: self.unit.clone(),
-                ys: proofs_ys,
-                timestamp: unix_time(),
-                memo,
-                metadata: opts.metadata,
-                quote_id: None,
-                payment_request: None,
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Incoming,
+            amount: total_amount,
+            fee: proofs_amount - total_amount,
+            unit: self.unit.clone(),
+            ys: proofs_ys,
+            timestamp: unix_time(),
+            memo,
+            metadata: opts.metadata,
+            quote_id: None,
+            payment_request: None,
+            payment_proof: None,
+        })
+        .await?;
 
         Ok(total_amount)
     }
@@ -221,10 +220,14 @@ impl Wallet {
 
         ensure_cdk!(self.mint_url == token.mint_url()?, Error::IncorrectMint);
 
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
         let amount = self
-            .receive_proofs(proofs, opts, token.memo().clone())
+            .receive_proofs(&mut tx, proofs, opts, token.memo().clone())
             .await?;
 
+        tx.commit().await?;
+
         Ok(amount)
     }
 

+ 35 - 28
crates/cdk/src/wallet/send.rs

@@ -33,7 +33,7 @@ impl Wallet {
 
         // If online send check mint for current keysets fees
         if opts.send_kind.is_online() {
-            if let Err(e) = self.refresh_keysets().await {
+            if let Err(e) = self.refresh_keysets(None).await {
                 tracing::error!("Error refreshing keysets: {:?}. Using stored keysets", e);
             }
         }
@@ -46,6 +46,7 @@ impl Wallet {
             .get_proofs_with(
                 Some(vec![State::Unspent]),
                 opts.conditions.clone().map(|c| vec![c]),
+                None,
             )
             .await?;
 
@@ -128,6 +129,7 @@ impl Wallet {
         proofs: Proofs,
         force_swap: bool,
     ) -> Result<PreparedSend, Error> {
+        let wallet = self.clone();
         // Split amount with fee if necessary
         let active_keyset_id = self.get_active_keyset().await?.id;
         let fee_and_amounts = self
@@ -152,10 +154,12 @@ impl Wallet {
         tracing::debug!("Send amounts: {:?}", send_amounts);
         tracing::debug!("Send fee: {:?}", send_fee);
 
+        let mut tx = wallet.localstore.begin_db_transaction().await?;
+
         // Reserve proofs
-        self.localstore
-            .update_proofs_state(proofs.ys()?, State::Reserved)
+        tx.update_proofs_state(proofs.ys()?, State::Reserved)
             .await?;
+        tx.commit().await?;
 
         // Check if proofs are exact send amount (and does not exceed max_proofs)
         let mut exact_proofs = proofs.total_amount()? == amount + send_fee;
@@ -190,7 +194,7 @@ impl Wallet {
 
         // Return prepared send
         Ok(PreparedSend {
-            wallet: self.clone(),
+            wallet,
             amount,
             options: opts,
             proofs_to_swap,
@@ -262,8 +266,10 @@ impl PreparedSend {
         let total_send_fee = self.fee();
         let mut proofs_to_send = self.proofs_to_send;
 
+        let mut tx = self.wallet.localstore.begin_db_transaction().await?;
+
         // Get active keyset ID
-        let active_keyset_id = self.wallet.fetch_active_keyset().await?.id;
+        let active_keyset_id = self.wallet.fetch_active_keyset(Some(&mut tx)).await?.id;
         tracing::debug!("Active keyset ID: {:?}", active_keyset_id);
 
         // Get keyset fees
@@ -311,9 +317,11 @@ impl PreparedSend {
             .get_proofs_with(
                 Some(vec![State::Reserved, State::Unspent]),
                 self.options.conditions.clone().map(|c| vec![c]),
+                None,
             )
             .await?
             .ys()?;
+
         if proofs_to_send
             .ys()?
             .iter()
@@ -328,9 +336,7 @@ impl PreparedSend {
             "Updating proofs state to pending spent: {:?}",
             proofs_to_send.ys()?
         );
-        self.wallet
-            .localstore
-            .update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
+        tx.update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
             .await?;
 
         // Include token memo
@@ -338,23 +344,23 @@ impl PreparedSend {
         let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None });
 
         // Add transaction to store
-        self.wallet
-            .localstore
-            .add_transaction(Transaction {
-                mint_url: self.wallet.mint_url.clone(),
-                direction: TransactionDirection::Outgoing,
-                amount: self.amount,
-                fee: total_send_fee,
-                unit: self.wallet.unit.clone(),
-                ys: proofs_to_send.ys()?,
-                timestamp: unix_time(),
-                memo: memo.clone(),
-                metadata: self.options.metadata,
-                quote_id: None,
-                payment_request: None,
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.wallet.mint_url.clone(),
+            direction: TransactionDirection::Outgoing,
+            amount: self.amount,
+            fee: total_send_fee,
+            unit: self.wallet.unit.clone(),
+            ys: proofs_to_send.ys()?,
+            timestamp: unix_time(),
+            memo: memo.clone(),
+            metadata: self.options.metadata,
+            quote_id: None,
+            payment_request: None,
+            payment_proof: None,
+        })
+        .await?;
+
+        tx.commit().await?;
 
         // Create and return token
         Ok(Token::new(
@@ -368,6 +374,7 @@ impl PreparedSend {
     /// Cancel the prepared send
     pub async fn cancel(self) -> Result<(), Error> {
         tracing::info!("Cancelling prepared send");
+        let mut tx = self.wallet.localstore.begin_db_transaction().await?;
 
         // Double-check proofs state
         let reserved_proofs = self.wallet.get_reserved_proofs().await?.ys()?;
@@ -380,11 +387,11 @@ impl PreparedSend {
             return Err(Error::UnexpectedProofState);
         }
 
-        self.wallet
-            .localstore
-            .update_proofs_state(self.proofs().ys()?, State::Unspent)
+        tx.update_proofs_state(self.proofs().ys()?, State::Unspent)
             .await?;
 
+        tx.commit().await?;
+
         Ok(())
     }
 }

+ 31 - 23
crates/cdk/src/wallet/swap.rs

@@ -10,6 +10,8 @@ use crate::nuts::{
 use crate::types::ProofInfo;
 use crate::{ensure_cdk, Amount, Error, Wallet};
 
+use super::Tx;
+
 impl Wallet {
     /// Swap
     #[instrument(skip(self, input_proofs))]
@@ -21,7 +23,8 @@ impl Wallet {
         spending_conditions: Option<SpendingConditions>,
         include_fees: bool,
     ) -> Result<Option<Proofs>, Error> {
-        self.refresh_keysets().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        self.refresh_keysets(Some(&mut tx)).await?;
 
         tracing::info!("Swapping");
         let mint_url = &self.mint_url;
@@ -29,6 +32,7 @@ impl Wallet {
 
         let pre_swap = self
             .create_swap(
+                &mut tx,
                 amount,
                 amount_split_target.clone(),
                 input_proofs.clone(),
@@ -134,9 +138,10 @@ impl Wallet {
             .map(|proof| proof.y())
             .collect::<Result<Vec<PublicKey>, _>>()?;
 
-        self.localstore
-            .update_proofs(added_proofs, deleted_ys)
-            .await?;
+        tx.update_proofs(added_proofs, deleted_ys).await?;
+
+        tx.commit().await?;
+
         Ok(send_proofs)
     }
 
@@ -148,8 +153,8 @@ impl Wallet {
         conditions: Option<SpendingConditions>,
         include_fees: bool,
     ) -> Result<Proofs, Error> {
-        let available_proofs = self
-            .localstore
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        let available_proofs = tx
             .get_proofs(
                 Some(self.mint_url.clone()),
                 Some(self.unit.clone()),
@@ -170,7 +175,7 @@ impl Wallet {
         ensure_cdk!(proofs_sum >= amount, Error::InsufficientFunds);
 
         let active_keyset_ids = self
-            .refresh_keysets()
+            .refresh_keysets(Some(&mut tx))
             .await?
             .active()
             .map(|k| k.id)
@@ -185,21 +190,27 @@ impl Wallet {
             true,
         )?;
 
-        self.swap(
-            Some(amount),
-            SplitTarget::default(),
-            proofs,
-            conditions,
-            include_fees,
-        )
-        .await?
-        .ok_or(Error::InsufficientFunds)
+        let to_return = self
+            .swap(
+                Some(amount),
+                SplitTarget::default(),
+                proofs,
+                conditions,
+                include_fees,
+            )
+            .await?
+            .ok_or(Error::InsufficientFunds)?;
+
+        tx.commit().await?;
+
+        Ok(to_return)
     }
 
     /// Create Swap Payload
-    #[instrument(skip(self, proofs))]
+    #[instrument(skip(self, tx, proofs))]
     pub async fn create_swap(
         &self,
+        tx: &mut Tx<'_, '_>,
         amount: Option<Amount>,
         amount_split_target: SplitTarget,
         proofs: Proofs,
@@ -207,15 +218,13 @@ impl Wallet {
         include_fees: bool,
     ) -> Result<PreSwap, Error> {
         tracing::info!("Creating swap");
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let active_keyset_id = self.fetch_active_keyset(Some(tx)).await?.id;
 
         // Desired amount is either amount passed or value of all proof
         let proofs_total = proofs.total_amount()?;
 
         let ys: Vec<PublicKey> = proofs.ys()?;
-        self.localstore
-            .update_proofs_state(ys, State::Reserved)
-            .await?;
+        tx.update_proofs_state(ys, State::Reserved).await?;
 
         let fee = self.get_proofs_fee(&proofs).await?;
 
@@ -297,8 +306,7 @@ impl Wallet {
                 total_secrets_needed
             );
 
-            let new_counter = self
-                .localstore
+            let new_counter = tx
                 .increment_keyset_counter(&active_keyset_id, total_secrets_needed)
                 .await?;
 

+ 8 - 2
crates/cdk/src/wallet/transactions.rs

@@ -41,8 +41,10 @@ impl Wallet {
             return Err(Error::InvalidTransactionDirection);
         }
 
+        let mut db_tx = self.localstore.begin_db_transaction().await?;
+
         let pending_spent_proofs = self
-            .get_pending_spent_proofs()
+            .get_pending_spent_proofs(Some(&mut db_tx))
             .await?
             .into_iter()
             .filter(|p| match p.y() {
@@ -51,7 +53,11 @@ impl Wallet {
             })
             .collect::<Vec<_>>();
 
-        self.reclaim_unspent(pending_spent_proofs).await?;
+        self.reclaim_unspent(pending_spent_proofs, &mut db_tx)
+            .await?;
+
+        db_tx.commit().await?;
+
         Ok(())
     }
 }

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác