Kaynağa Gözat

remove wallet database transaction support (#1472)

* feat: remove wallet database transaction support

Remove DatabaseTransaction trait to support databases without native
transaction support (IndexedDB, FFI bindings, LDK's KVStore).

The saga pattern will handle failure recovery instead of relying on
database transactions.
   - Remove DatabaseTransaction trait and begin_db_transaction method
   - Move transaction methods to direct Database trait methods

feat: wallet kv tests

* fix: rebase with custom fns
tsk 2 hafta önce
ebeveyn
işleme
855798f378
32 değiştirilmiş dosya ile 3325 ekleme ve 2860 silme
  1. 1 4
      crates/cdk-common/src/database/mod.rs
  2. 79 102
      crates/cdk-common/src/database/wallet/mod.rs
  3. 225 165
      crates/cdk-common/src/database/wallet/test/mod.rs
  4. 139 865
      crates/cdk-ffi/src/database.rs
  5. 109 8
      crates/cdk-ffi/src/postgres.rs
  6. 112 11
      crates/cdk-ffi/src/sqlite.rs
  7. 23 369
      crates/cdk-ffi/tests/test_transactions.py
  8. 5 3
      crates/cdk-integration-tests/tests/fake_auth.rs
  9. 5 7
      crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs
  10. 366 566
      crates/cdk-redb/src/wallet/mod.rs
  11. 78 1
      crates/cdk-sql-common/src/keyvalue.rs
  12. 653 381
      crates/cdk-sql-common/src/wallet/mod.rs
  13. 10 29
      crates/cdk-sqlite/src/wallet/mod.rs
  14. 6 8
      crates/cdk/src/wallet/auth/auth_wallet.rs
  15. 27 38
      crates/cdk/src/wallet/issue/bolt11.rs
  16. 33 44
      crates/cdk/src/wallet/issue/bolt12.rs
  17. 23 24
      crates/cdk/src/wallet/issue/custom.rs
  18. 367 0
      crates/cdk/src/wallet/issue/issue_bolt11.rs
  19. 274 0
      crates/cdk/src/wallet/issue/issue_bolt12.rs
  20. 30 38
      crates/cdk/src/wallet/melt/bolt11.rs
  21. 4 10
      crates/cdk/src/wallet/melt/bolt12.rs
  22. 1 3
      crates/cdk/src/wallet/melt/custom.rs
  23. 519 0
      crates/cdk/src/wallet/melt/melt_bolt11.rs
  24. 98 0
      crates/cdk/src/wallet/melt/melt_bolt12.rs
  25. 21 22
      crates/cdk/src/wallet/melt/mod.rs
  26. 7 16
      crates/cdk/src/wallet/mint_metadata_cache.rs
  27. 13 16
      crates/cdk/src/wallet/mod.rs
  28. 21 40
      crates/cdk/src/wallet/proofs.rs
  29. 30 31
      crates/cdk/src/wallet/receive.rs
  30. 9 12
      crates/cdk/src/wallet/reclaim.rs
  31. 27 34
      crates/cdk/src/wallet/send.rs
  32. 10 13
      crates/cdk/src/wallet/swap.rs

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

@@ -30,10 +30,7 @@ pub use mint::{
 #[cfg(all(feature = "mint", feature = "auth"))]
 pub use mint::{DynMintAuthDatabase, MintAuthDatabase, MintAuthTransaction};
 #[cfg(feature = "wallet")]
-pub use wallet::{
-    Database as WalletDatabase, DatabaseTransaction as WalletDatabaseTransaction,
-    DynWalletDatabaseTransaction,
-};
+pub use wallet::Database as WalletDatabase;
 
 /// A wrapper indicating that a resource has been acquired with a database lock.
 ///

+ 79 - 102
crates/cdk-common/src/database/wallet/mod.rs

@@ -6,9 +6,9 @@ use std::fmt::Debug;
 use async_trait::async_trait;
 use cashu::KeySet;
 
-use super::{DbTransactionFinalizer, Error};
+use super::Error;
 use crate::common::ProofInfo;
-use crate::database::{KVStoreDatabase, KVStoreTransaction};
+use crate::database::KVStoreDatabase;
 use crate::mint_url::MintUrl;
 use crate::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
@@ -20,101 +20,6 @@ use crate::wallet::{
 #[cfg(feature = "test")]
 pub mod test;
 
-/// Easy to use Dynamic Database type alias
-pub type DynWalletDatabaseTransaction = Box<dyn DatabaseTransaction<super::Error> + Sync + Send>;
-
-/// 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 DatabaseTransaction<Error>:
-    KVStoreTransaction<Error> + DbTransactionFinalizer<Err = Error>
-{
-    /// Add Mint to storage
-    async fn add_mint(
-        &mut self,
-        mint_url: MintUrl,
-        mint_info: Option<MintInfo>,
-    ) -> Result<(), Error>;
-
-    /// Remove Mint from storage
-    async fn remove_mint(&mut self, mint_url: MintUrl) -> Result<(), Error>;
-
-    /// Update mint url
-    async fn update_mint_url(
-        &mut self,
-        old_mint_url: MintUrl,
-        new_mint_url: MintUrl,
-    ) -> Result<(), Error>;
-
-    /// Get mint keyset by id
-    async fn get_keyset_by_id(&mut self, keyset_id: &Id) -> Result<Option<KeySetInfo>, Error>;
-
-    /// Get [`Keys`] from storage
-    async fn get_keys(&mut self, id: &Id) -> Result<Option<Keys>, Error>;
-
-    /// Add mint keyset to storage
-    async fn add_mint_keysets(
-        &mut self,
-        mint_url: MintUrl,
-        keysets: Vec<KeySetInfo>,
-    ) -> 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>, Error>;
-
-    /// 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)]
@@ -122,11 +27,6 @@ pub trait Database<Err>: KVStoreDatabase<Err = Err> + Debug
 where
     Err: Into<Error> + From<Error>,
 {
-    /// Begins a DB transaction
-    async fn begin_db_transaction(
-        &self,
-    ) -> Result<Box<dyn DatabaseTransaction<Err> + Send + Sync>, Err>;
-
     /// Get mint from storage
     async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Err>;
 
@@ -191,4 +91,81 @@ where
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
     ) -> Result<Vec<Transaction>, Err>;
+
+    /// Update the proofs in storage by adding new proofs or removing proofs by
+    /// their Y value (without transaction)
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), Err>;
+
+    /// Update proofs state in storage (without transaction)
+    async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Err>;
+
+    /// Add transaction to storage (without transaction)
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), Err>;
+
+    /// Update mint url (without transaction)
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), Err>;
+
+    /// Atomically increment Keyset counter and return new value (without transaction)
+    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<u32, Err>;
+
+    /// Add Mint to storage
+    async fn add_mint(&self, mint_url: MintUrl, mint_info: Option<MintInfo>) -> Result<(), Err>;
+
+    /// Remove Mint from storage
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), Err>;
+
+    /// Add mint keyset to storage
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), Err>;
+
+    /// Add mint quote to storage
+    async fn add_mint_quote(&self, quote: WalletMintQuote) -> Result<(), Err>;
+
+    /// Remove mint quote from storage
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Err>;
+
+    /// Add melt quote to storage
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Err>;
+
+    /// Remove melt quote from storage
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Err>;
+
+    /// Add [`Keys`] to storage
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), Err>;
+
+    /// Remove [`Keys`] from storage
+    async fn remove_keys(&self, id: &Id) -> Result<(), Err>;
+
+    /// Remove transaction from storage
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Err>;
+
+    // KV Store write methods (non-transactional)
+
+    /// Write a value to the key-value store
+    async fn kv_write(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+        value: &[u8],
+    ) -> Result<(), Err>;
+
+    /// Remove a value from the key-value store
+    async fn kv_remove(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<(), Err>;
 }

+ 225 - 165
crates/cdk-common/src/database/wallet/test/mod.rs

@@ -164,11 +164,9 @@ where
     let mint_info = MintInfo::default();
 
     // Add mint
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint(mint_url.clone(), Some(mint_info.clone()))
+    db.add_mint(mint_url.clone(), Some(mint_info.clone()))
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Get mint
     let retrieved = db.get_mint(mint_url.clone()).await.unwrap();
@@ -186,9 +184,7 @@ where
 {
     let mint_url = test_mint_url();
 
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint(mint_url.clone(), None).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_mint(mint_url.clone(), None).await.unwrap();
 
     // Verify mint exists in the database
     let mints = db.get_mints().await.unwrap();
@@ -203,14 +199,10 @@ where
     let mint_url = test_mint_url();
 
     // Add mint
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint(mint_url.clone(), None).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_mint(mint_url.clone(), None).await.unwrap();
 
     // Remove mint
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.remove_mint(mint_url.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.remove_mint(mint_url.clone()).await.unwrap();
 
     let result = db.get_mint(mint_url).await.unwrap();
     assert!(result.is_none());
@@ -225,16 +217,12 @@ where
     let new_url = test_mint_url_2();
 
     // Add mint with old URL
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint(old_url.clone(), None).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_mint(old_url.clone(), None).await.unwrap();
 
     // Update URL
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_mint_url(old_url.clone(), new_url.clone())
+    db.update_mint_url(old_url.clone(), new_url.clone())
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 }
 
 // =============================================================================
@@ -251,12 +239,10 @@ where
     let keyset_info = test_keyset_info(keyset_id, &mint_url);
 
     // Add mint first
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint(mint_url.clone(), None).await.unwrap();
-    tx.add_mint_keysets(mint_url.clone(), vec![keyset_info.clone()])
+    db.add_mint(mint_url.clone(), None).await.unwrap();
+    db.add_mint_keysets(mint_url.clone(), vec![keyset_info.clone()])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Get keyset by ID
     let retrieved = db.get_keyset_by_id(&keyset_id).await.unwrap();
@@ -279,18 +265,14 @@ where
     let keyset_info = test_keyset_info(keyset_id, &mint_url);
 
     // Add keyset
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint(mint_url.clone(), None).await.unwrap();
-    tx.add_mint_keysets(mint_url.clone(), vec![keyset_info])
+    db.add_mint(mint_url.clone(), None).await.unwrap();
+    db.add_mint_keysets(mint_url.clone(), vec![keyset_info])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Get in transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let retrieved = tx.get_keyset_by_id(&keyset_id).await.unwrap();
+    let retrieved = db.get_keyset_by_id(&keyset_id).await.unwrap();
     assert!(retrieved.is_some());
-    tx.rollback().await.unwrap();
 }
 
 /// Test adding and retrieving keys
@@ -308,9 +290,7 @@ where
     };
 
     // Add keys
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_keys(keyset).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_keys(keyset).await.unwrap();
 
     // Get keys
     let retrieved = db.get_keys(&keyset_id).await.unwrap();
@@ -334,17 +314,13 @@ where
     };
 
     // Add keys
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_keys(keyset).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_keys(keyset).await.unwrap();
 
     // Get in transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let retrieved = tx.get_keys(&keyset_id).await.unwrap();
+    let retrieved = db.get_keys(&keyset_id).await.unwrap();
     assert!(retrieved.is_some());
     let retrieved_keys = retrieved.unwrap();
     assert_eq!(retrieved_keys.len(), keys.len());
-    tx.rollback().await.unwrap();
 }
 
 /// Test removing keys
@@ -362,18 +338,14 @@ where
     };
 
     // Add keys
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_keys(keyset).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_keys(keyset).await.unwrap();
 
     // Verify keys were added
     let retrieved = db.get_keys(&keyset_id).await.unwrap();
     assert!(retrieved.is_some());
 
     // Remove keys
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.remove_keys(&keyset_id).await.unwrap();
-    tx.commit().await.unwrap();
+    db.remove_keys(&keyset_id).await.unwrap();
 
     let retrieved = db.get_keys(&keyset_id).await.unwrap();
     assert!(retrieved.is_none());
@@ -392,9 +364,7 @@ where
     let quote = test_mint_quote(mint_url);
 
     // Add quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint_quote(quote.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_mint_quote(quote.clone()).await.unwrap();
 
     // Get quote
     let retrieved = db.get_mint_quote(&quote.id).await.unwrap();
@@ -415,15 +385,11 @@ where
     let quote = test_mint_quote(mint_url);
 
     // Add quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint_quote(quote.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_mint_quote(quote.clone()).await.unwrap();
 
     // Get in transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let retrieved = tx.get_mint_quote(&quote.id).await.unwrap();
+    let retrieved = db.get_mint_quote(&quote.id).await.unwrap();
     assert!(retrieved.is_some());
-    tx.rollback().await.unwrap();
 }
 
 /// Test removing mint quote
@@ -435,14 +401,10 @@ where
     let quote = test_mint_quote(mint_url);
 
     // Add quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint_quote(quote.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_mint_quote(quote.clone()).await.unwrap();
 
     // Remove quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.remove_mint_quote(&quote.id).await.unwrap();
-    tx.commit().await.unwrap();
+    db.remove_mint_quote(&quote.id).await.unwrap();
 
     let retrieved = db.get_mint_quote(&quote.id).await.unwrap();
     assert!(retrieved.is_none());
@@ -460,9 +422,7 @@ where
     let quote = test_melt_quote();
 
     // Add quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_melt_quote(quote.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_melt_quote(quote.clone()).await.unwrap();
 
     // Get quote
     let retrieved = db.get_melt_quote(&quote.id).await.unwrap();
@@ -482,15 +442,11 @@ where
     let quote = test_melt_quote();
 
     // Add quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_melt_quote(quote.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_melt_quote(quote.clone()).await.unwrap();
 
     // Get in transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let retrieved = tx.get_melt_quote(&quote.id).await.unwrap();
+    let retrieved = db.get_melt_quote(&quote.id).await.unwrap();
     assert!(retrieved.is_some());
-    tx.rollback().await.unwrap();
 }
 
 /// Test removing melt quote
@@ -501,14 +457,10 @@ where
     let quote = test_melt_quote();
 
     // Add quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_melt_quote(quote.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_melt_quote(quote.clone()).await.unwrap();
 
     // Remove quote
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.remove_melt_quote(&quote.id).await.unwrap();
-    tx.commit().await.unwrap();
+    db.remove_melt_quote(&quote.id).await.unwrap();
 
     let retrieved = db.get_melt_quote(&quote.id).await.unwrap();
     assert!(retrieved.is_none());
@@ -528,11 +480,9 @@ where
     let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
 
     // Add proof
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info.clone()], vec![])
+    db.update_proofs(vec![proof_info.clone()], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Get proofs
     let proofs = db.get_proofs(None, None, None, None).await.unwrap();
@@ -561,17 +511,13 @@ where
     let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
 
     // Add proof
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info.clone()], vec![])
+    db.update_proofs(vec![proof_info.clone()], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Get proofs in transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let proofs = tx.get_proofs(None, None, None, None).await.unwrap();
+    let proofs = db.get_proofs(None, None, None, None).await.unwrap();
     assert!(!proofs.is_empty());
-    tx.rollback().await.unwrap();
 }
 
 /// Test updating proofs (add and remove)
@@ -585,18 +531,14 @@ where
     let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone());
 
     // Add first proof
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info_1.clone()], vec![])
+    db.update_proofs(vec![proof_info_1.clone()], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Add second, remove first
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info_2.clone()], vec![proof_info_1.y])
+    db.update_proofs(vec![proof_info_2.clone()], vec![proof_info_1.y])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Verify
     let proofs = db.get_proofs(None, None, None, None).await.unwrap();
@@ -614,18 +556,14 @@ where
     let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
 
     // Add proof
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info.clone()], vec![])
+    db.update_proofs(vec![proof_info.clone()], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Update state
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs_state(vec![proof_info.y], State::Pending)
+    db.update_proofs_state(vec![proof_info.y], State::Pending)
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Verify
     let proofs = db
@@ -645,11 +583,9 @@ where
     let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
 
     // Add proof
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info.clone()], vec![])
+    db.update_proofs(vec![proof_info.clone()], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Filter by unit
     let proofs = db
@@ -676,11 +612,9 @@ where
     let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
 
     // Add proof
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info.clone()], vec![])
+    db.update_proofs(vec![proof_info.clone()], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Filter by state
     let proofs = db
@@ -712,11 +646,9 @@ where
     let proof_info_2 = test_proof_info(keyset_id, 200, mint_url.clone());
 
     // Add proofs
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info_1, proof_info_2], vec![])
+    db.update_proofs(vec![proof_info_1, proof_info_2], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Get total balance
     let balance = db.get_balance(None, None, None).await.unwrap();
@@ -737,11 +669,9 @@ where
     let proof_info = test_proof_info(keyset_id, 100, mint_url.clone());
 
     // Add proof
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info.clone()], vec![])
+    db.update_proofs(vec![proof_info.clone()], vec![])
         .await
         .unwrap();
-    tx.commit().await.unwrap();
 
     // Get balance by state
     let balance = db
@@ -770,16 +700,12 @@ where
     let keyset_id = test_keyset_id();
 
     // Increment counter
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let counter1 = tx.increment_keyset_counter(&keyset_id, 5).await.unwrap();
-    tx.commit().await.unwrap();
+    let counter1 = db.increment_keyset_counter(&keyset_id, 5).await.unwrap();
 
     assert_eq!(counter1, 5);
 
     // Increment again
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let counter2 = tx.increment_keyset_counter(&keyset_id, 10).await.unwrap();
-    tx.commit().await.unwrap();
+    let counter2 = db.increment_keyset_counter(&keyset_id, 10).await.unwrap();
 
     assert_eq!(counter2, 15);
 }
@@ -793,22 +719,16 @@ where
     let keyset_id_2 = test_keyset_id_2();
 
     // Increment first keyset
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.increment_keyset_counter(&keyset_id_1, 5).await.unwrap();
-    tx.commit().await.unwrap();
+    db.increment_keyset_counter(&keyset_id_1, 5).await.unwrap();
 
     // Increment second keyset
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let counter2 = tx.increment_keyset_counter(&keyset_id_2, 10).await.unwrap();
-    tx.commit().await.unwrap();
+    let counter2 = db.increment_keyset_counter(&keyset_id_2, 10).await.unwrap();
 
     // Second keyset should start from 0
     assert_eq!(counter2, 10);
 
     // First keyset should still be at 5
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    let counter1 = tx.increment_keyset_counter(&keyset_id_1, 0).await.unwrap();
-    tx.rollback().await.unwrap();
+    let counter1 = db.increment_keyset_counter(&keyset_id_1, 0).await.unwrap();
 
     assert_eq!(counter1, 5);
 }
@@ -827,9 +747,7 @@ where
     let tx_id = transaction.id();
 
     // Add transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_transaction(transaction.clone()).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_transaction(transaction.clone()).await.unwrap();
 
     // Get transaction
     let retrieved = db.get_transaction(tx_id).await.unwrap();
@@ -847,10 +765,8 @@ where
     let tx_outgoing = test_transaction(mint_url.clone(), TransactionDirection::Outgoing);
 
     // Add transactions
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_transaction(tx_incoming).await.unwrap();
-    tx.add_transaction(tx_outgoing).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_transaction(tx_incoming).await.unwrap();
+    db.add_transaction(tx_outgoing).await.unwrap();
 
     // List all
     let transactions = db.list_transactions(None, None, None).await.unwrap();
@@ -881,10 +797,8 @@ where
     let tx_2 = test_transaction(mint_url_2.clone(), TransactionDirection::Incoming);
 
     // Add transactions
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_transaction(tx_1).await.unwrap();
-    tx.add_transaction(tx_2).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_transaction(tx_1).await.unwrap();
+    db.add_transaction(tx_2).await.unwrap();
 
     // Filter by mint
     let transactions = db
@@ -904,57 +818,200 @@ where
     let tx_id = transaction.id();
 
     // Add transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_transaction(transaction).await.unwrap();
-    tx.commit().await.unwrap();
+    db.add_transaction(transaction).await.unwrap();
 
     // Remove transaction
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.remove_transaction(tx_id).await.unwrap();
-    tx.commit().await.unwrap();
+    db.remove_transaction(tx_id).await.unwrap();
 
     let retrieved = db.get_transaction(tx_id).await.unwrap();
     assert!(retrieved.is_none());
 }
 
-// =============================================================================
-// Transaction Rollback Tests
+// KV Store Tests
 // =============================================================================
 
-/// Test transaction rollback
-pub async fn transaction_rollback<DB>(db: DB)
+/// Test KV store write and read operations
+pub async fn kvstore_write_and_read<DB>(db: DB)
 where
     DB: Database<crate::database::Error>,
 {
-    let mint_url = test_mint_url();
+    // Write some test data
+    db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
+        .await
+        .unwrap();
+    db.kv_write("test_namespace", "sub_namespace", "key2", b"value2")
+        .await
+        .unwrap();
+    db.kv_write("test_namespace", "other_sub", "key3", b"value3")
+        .await
+        .unwrap();
 
-    // Add mint but rollback
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.add_mint(mint_url.clone(), None).await.unwrap();
-    tx.rollback().await.unwrap();
+    // Read back the data
+    let value1 = db
+        .kv_read("test_namespace", "sub_namespace", "key1")
+        .await
+        .unwrap();
+    assert_eq!(value1, Some(b"value1".to_vec()));
 
-    // Verify mint was not added
-    let result = db.get_mint(mint_url).await.unwrap();
-    assert!(result.is_none());
+    let value2 = db
+        .kv_read("test_namespace", "sub_namespace", "key2")
+        .await
+        .unwrap();
+    assert_eq!(value2, Some(b"value2".to_vec()));
+
+    let value3 = db
+        .kv_read("test_namespace", "other_sub", "key3")
+        .await
+        .unwrap();
+    assert_eq!(value3, Some(b"value3".to_vec()));
+
+    // Read non-existent key
+    let missing = db
+        .kv_read("test_namespace", "sub_namespace", "missing")
+        .await
+        .unwrap();
+    assert_eq!(missing, None);
 }
 
-/// Test proof rollback
-pub async fn proof_rollback<DB>(db: DB)
+/// Test KV store list operation
+pub async fn kvstore_list<DB>(db: DB)
 where
     DB: Database<crate::database::Error>,
 {
-    let mint_url = test_mint_url();
-    let keyset_id = test_keyset_id();
-    let proof_info = test_proof_info(keyset_id, 100, mint_url);
+    // Write some test data
+    db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
+        .await
+        .unwrap();
+    db.kv_write("test_namespace", "sub_namespace", "key2", b"value2")
+        .await
+        .unwrap();
+    db.kv_write("test_namespace", "other_sub", "key3", b"value3")
+        .await
+        .unwrap();
 
-    // Add proof but rollback
-    let mut tx = db.begin_db_transaction().await.unwrap();
-    tx.update_proofs(vec![proof_info], vec![]).await.unwrap();
-    tx.rollback().await.unwrap();
+    // List keys in namespace
+    let mut keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
+    keys.sort();
+    assert_eq!(keys, vec!["key1", "key2"]);
 
-    // Verify proof was not added
-    let proofs = db.get_proofs(None, None, None, None).await.unwrap();
-    assert!(proofs.is_empty());
+    // List keys in other namespace
+    let other_keys = db.kv_list("test_namespace", "other_sub").await.unwrap();
+    assert_eq!(other_keys, vec!["key3"]);
+
+    // List keys in empty namespace
+    let empty_keys = db.kv_list("test_namespace", "empty_sub").await.unwrap();
+    assert!(empty_keys.is_empty());
+}
+
+/// Test KV store update operation
+pub async fn kvstore_update<DB>(db: DB)
+where
+    DB: Database<crate::database::Error>,
+{
+    // Write initial value
+    db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
+        .await
+        .unwrap();
+
+    // Verify initial value
+    let value = db
+        .kv_read("test_namespace", "sub_namespace", "key1")
+        .await
+        .unwrap();
+    assert_eq!(value, Some(b"value1".to_vec()));
+
+    // Update value
+    db.kv_write("test_namespace", "sub_namespace", "key1", b"updated_value1")
+        .await
+        .unwrap();
+
+    // Verify updated value
+    let value = db
+        .kv_read("test_namespace", "sub_namespace", "key1")
+        .await
+        .unwrap();
+    assert_eq!(value, Some(b"updated_value1".to_vec()));
+}
+
+/// Test KV store remove operation
+pub async fn kvstore_remove<DB>(db: DB)
+where
+    DB: Database<crate::database::Error>,
+{
+    // Write some test data
+    db.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
+        .await
+        .unwrap();
+    db.kv_write("test_namespace", "sub_namespace", "key2", b"value2")
+        .await
+        .unwrap();
+
+    // Verify data exists
+    let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
+    assert_eq!(keys.len(), 2);
+
+    // Remove one key
+    db.kv_remove("test_namespace", "sub_namespace", "key1")
+        .await
+        .unwrap();
+
+    // Verify key is removed
+    let value = db
+        .kv_read("test_namespace", "sub_namespace", "key1")
+        .await
+        .unwrap();
+    assert_eq!(value, None);
+
+    // Verify other key still exists
+    let value = db
+        .kv_read("test_namespace", "sub_namespace", "key2")
+        .await
+        .unwrap();
+    assert_eq!(value, Some(b"value2".to_vec()));
+
+    // Verify list is updated
+    let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
+    assert_eq!(keys, vec!["key2"]);
+}
+
+/// Test KV store namespace isolation
+pub async fn kvstore_namespace_isolation<DB>(db: DB)
+where
+    DB: Database<crate::database::Error>,
+{
+    // Write same key to different namespaces
+    db.kv_write("ns1", "sub", "key", b"value_ns1")
+        .await
+        .unwrap();
+    db.kv_write("ns2", "sub", "key", b"value_ns2")
+        .await
+        .unwrap();
+    db.kv_write("ns1", "sub2", "key", b"value_sub2")
+        .await
+        .unwrap();
+
+    // Verify isolation by primary namespace
+    let value1 = db.kv_read("ns1", "sub", "key").await.unwrap();
+    assert_eq!(value1, Some(b"value_ns1".to_vec()));
+
+    let value2 = db.kv_read("ns2", "sub", "key").await.unwrap();
+    assert_eq!(value2, Some(b"value_ns2".to_vec()));
+
+    // Verify isolation by secondary namespace
+    let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap();
+    assert_eq!(value3, Some(b"value_sub2".to_vec()));
+
+    // Remove from one namespace shouldn't affect others
+    db.kv_remove("ns1", "sub", "key").await.unwrap();
+
+    let value1 = db.kv_read("ns1", "sub", "key").await.unwrap();
+    assert_eq!(value1, None);
+
+    let value2 = db.kv_read("ns2", "sub", "key").await.unwrap();
+    assert_eq!(value2, Some(b"value_ns2".to_vec()));
+
+    let value3 = db.kv_read("ns1", "sub2", "key").await.unwrap();
+    assert_eq!(value3, Some(b"value_sub2".to_vec()));
 }
 
 /// Unit test that is expected to be passed for a correct wallet database implementation
@@ -992,8 +1049,11 @@ macro_rules! wallet_db_test {
             list_transactions,
             filter_transactions_by_mint,
             remove_transaction,
-            transaction_rollback,
-            proof_rollback
+            kvstore_write_and_read,
+            kvstore_list,
+            kvstore_update,
+            kvstore_remove,
+            kvstore_namespace_isolation
         );
     };
     ($make_db_fn:ident, $($name:ident),+ $(,)?) => {

Dosya farkı çok büyük olduğundan ihmal edildi
+ 139 - 865
crates/cdk-ffi/src/database.rs


+ 109 - 8
crates/cdk-ffi/src/postgres.rs

@@ -5,9 +5,9 @@ use std::sync::Arc;
 use cdk_postgres::PgConnectionPool;
 
 use crate::{
-    CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySetInfo, Keys, MeltQuote, MintInfo,
-    MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction,
-    TransactionDirection, TransactionId, WalletDatabase, WalletDatabaseTransactionWrapper,
+    CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySet, KeySetInfo, Keys, MeltQuote,
+    MintInfo, MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions,
+    Transaction, TransactionDirection, TransactionId, WalletDatabase,
 };
 
 #[derive(uniffi::Object)]
@@ -62,11 +62,7 @@ impl WalletPostgresDatabase {
 #[uniffi::export(async_runtime = "tokio")]
 #[async_trait::async_trait]
 impl WalletDatabase for WalletPostgresDatabase {
-    async fn begin_db_transaction(
-        &self,
-    ) -> Result<Arc<WalletDatabaseTransactionWrapper>, FfiError> {
-        self.inner.begin_db_transaction().await
-    }
+    // ========== Read methods ==========
 
     async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError> {
         self.inner.get_proofs_by_ys(ys).await
@@ -174,4 +170,109 @@ impl WalletDatabase for WalletPostgresDatabase {
             .kv_list(primary_namespace, secondary_namespace)
             .await
     }
+
+    async fn kv_write(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+        value: Vec<u8>,
+    ) -> Result<(), FfiError> {
+        self.inner
+            .kv_write(primary_namespace, secondary_namespace, key, value)
+            .await
+    }
+
+    async fn kv_remove(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<(), FfiError> {
+        self.inner
+            .kv_remove(primary_namespace, secondary_namespace, key)
+            .await
+    }
+
+    // ========== Write methods ==========
+
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), FfiError> {
+        self.inner.update_proofs(added, removed_ys).await
+    }
+
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: ProofState,
+    ) -> Result<(), FfiError> {
+        self.inner.update_proofs_state(ys, state).await
+    }
+
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
+        self.inner.add_transaction(transaction).await
+    }
+
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
+        self.inner.remove_transaction(transaction_id).await
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), FfiError> {
+        self.inner.update_mint_url(old_mint_url, new_mint_url).await
+    }
+
+    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
+        self.inner.increment_keyset_counter(keyset_id, count).await
+    }
+
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), FfiError> {
+        self.inner.add_mint(mint_url, mint_info).await
+    }
+
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
+        self.inner.remove_mint(mint_url).await
+    }
+
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), FfiError> {
+        self.inner.add_mint_keysets(mint_url, keysets).await
+    }
+
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
+        self.inner.add_mint_quote(quote).await
+    }
+
+    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner.remove_mint_quote(quote_id).await
+    }
+
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
+        self.inner.add_melt_quote(quote).await
+    }
+
+    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner.remove_melt_quote(quote_id).await
+    }
+
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
+        self.inner.add_keys(keyset).await
+    }
+
+    async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
+        self.inner.remove_keys(id).await
+    }
 }

+ 112 - 11
crates/cdk-ffi/src/sqlite.rs

@@ -5,9 +5,9 @@ use cdk_sqlite::wallet::WalletSqliteDatabase as CdkWalletSqliteDatabase;
 use cdk_sqlite::SqliteConnectionManager;
 
 use crate::{
-    CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySetInfo, Keys, MeltQuote, MintInfo,
-    MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction,
-    TransactionDirection, TransactionId, WalletDatabase,
+    CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySet, KeySetInfo, Keys, MeltQuote,
+    MintInfo, MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions,
+    Transaction, TransactionDirection, TransactionId, WalletDatabase,
 };
 
 /// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabaseFfi trait
@@ -67,10 +67,10 @@ impl WalletSqliteDatabase {
 #[uniffi::export(async_runtime = "tokio")]
 #[async_trait::async_trait]
 impl WalletDatabase for WalletSqliteDatabase {
-    async fn begin_db_transaction(
-        &self,
-    ) -> Result<Arc<crate::database::WalletDatabaseTransactionWrapper>, FfiError> {
-        self.inner.begin_db_transaction().await
+    // ========== Read methods ==========
+
+    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError> {
+        self.inner.get_proofs_by_ys(ys).await
     }
 
     async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
@@ -128,10 +128,6 @@ impl WalletDatabase for WalletSqliteDatabase {
             .await
     }
 
-    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError> {
-        self.inner.get_proofs_by_ys(ys).await
-    }
-
     async fn get_balance(
         &self,
         mint_url: Option<MintUrl>,
@@ -179,4 +175,109 @@ impl WalletDatabase for WalletSqliteDatabase {
             .kv_list(primary_namespace, secondary_namespace)
             .await
     }
+
+    async fn kv_write(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+        value: Vec<u8>,
+    ) -> Result<(), FfiError> {
+        self.inner
+            .kv_write(primary_namespace, secondary_namespace, key, value)
+            .await
+    }
+
+    async fn kv_remove(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<(), FfiError> {
+        self.inner
+            .kv_remove(primary_namespace, secondary_namespace, key)
+            .await
+    }
+
+    // ========== Write methods ==========
+
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), FfiError> {
+        self.inner.update_proofs(added, removed_ys).await
+    }
+
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: ProofState,
+    ) -> Result<(), FfiError> {
+        self.inner.update_proofs_state(ys, state).await
+    }
+
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
+        self.inner.add_transaction(transaction).await
+    }
+
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
+        self.inner.remove_transaction(transaction_id).await
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), FfiError> {
+        self.inner.update_mint_url(old_mint_url, new_mint_url).await
+    }
+
+    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
+        self.inner.increment_keyset_counter(keyset_id, count).await
+    }
+
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), FfiError> {
+        self.inner.add_mint(mint_url, mint_info).await
+    }
+
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
+        self.inner.remove_mint(mint_url).await
+    }
+
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), FfiError> {
+        self.inner.add_mint_keysets(mint_url, keysets).await
+    }
+
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
+        self.inner.add_mint_quote(quote).await
+    }
+
+    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner.remove_mint_quote(quote_id).await
+    }
+
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
+        self.inner.add_melt_quote(quote).await
+    }
+
+    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner.remove_melt_quote(quote_id).await
+    }
+
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
+        self.inner.add_keys(keyset).await
+    }
+
+    async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
+        self.inner.remove_keys(id).await
+    }
 }

+ 23 - 369
crates/cdk-ffi/tests/test_transactions.py

@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-Test suite for CDK FFI wallet and transaction operations
+Test suite for CDK FFI wallet database operations
 """
 
 import asyncio
@@ -29,333 +29,7 @@ sys.path.insert(0, str(bindings_path))
 import cdk_ffi
 
 
-# Transaction Tests (using explicit transactions)
-
-async def test_increment_keyset_counter_commit():
-    """Test that increment_keyset_counter works and persists after commit"""
-    print("\n=== Test: Increment Keyset Counter with Commit ===")
-
-    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
-        db_path = tmp.name
-
-    try:
-        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
-        db = cdk_ffi.create_wallet_db(backend)
-
-        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
-        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
-        keyset_info = cdk_ffi.KeySetInfo(
-            id=keyset_id.hex,
-            unit=cdk_ffi.CurrencyUnit.SAT(),
-            active=True,
-            input_fee_ppk=0
-        )
-
-        # Setup
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
-        await tx.add_mint_keysets(mint_url, [keyset_info])
-        await tx.commit()
-
-        # Increment counter in transaction
-        tx = await db.begin_db_transaction()
-        counter1 = await tx.increment_keyset_counter(keyset_id, 1)
-        counter2 = await tx.increment_keyset_counter(keyset_id, 5)
-        await tx.commit()
-
-        assert counter1 == 1, f"Expected counter 1, got {counter1}"
-        assert counter2 == 6, f"Expected counter 6, got {counter2}"
-        print("✓ Counters incremented correctly")
-
-        # Verify persistence
-        tx_read = await db.begin_db_transaction()
-        counter3 = await tx_read.increment_keyset_counter(keyset_id, 0)
-        await tx_read.rollback()
-        assert counter3 == 6, f"Expected persisted counter 6, got {counter3}"
-        print("✓ Counter persisted after commit")
-
-        print("✓ Test passed: Counter increments and commits work")
-
-    finally:
-        if os.path.exists(db_path):
-            os.unlink(db_path)
-
-
-async def test_implicit_rollback_on_drop():
-    """Test that transactions are implicitly rolled back when dropped"""
-    print("\n=== Test: Implicit Rollback on Drop ===")
-
-    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
-        db_path = tmp.name
-
-    try:
-        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
-        db = cdk_ffi.create_wallet_db(backend)
-
-        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
-        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
-
-        # Setup
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
-        keyset_info = cdk_ffi.KeySetInfo(
-            id=keyset_id.hex,
-            unit=cdk_ffi.CurrencyUnit.SAT(),
-            active=True,
-            input_fee_ppk=0
-        )
-        await tx.add_mint_keysets(mint_url, [keyset_info])
-        await tx.commit()
-
-        # Get initial counter
-        tx_read = await db.begin_db_transaction()
-        initial_counter = await tx_read.increment_keyset_counter(keyset_id, 0)
-        await tx_read.rollback()
-        print(f"Initial counter: {initial_counter}")
-
-        # Increment without commit
-        tx_no_commit = await db.begin_db_transaction()
-        incremented = await tx_no_commit.increment_keyset_counter(keyset_id, 10)
-        print(f"Counter incremented to {incremented} (not committed)")
-        del tx_no_commit
-
-        await asyncio.sleep(0.5)
-        print("Transaction dropped (should trigger implicit rollback)")
-
-        # Verify rollback
-        tx_verify = await db.begin_db_transaction()
-        final_counter = await tx_verify.increment_keyset_counter(keyset_id, 0)
-        await tx_verify.rollback()
-
-        assert final_counter == initial_counter, \
-            f"Expected counter to rollback to {initial_counter}, got {final_counter}"
-        print("✓ Implicit rollback works correctly")
-
-        print("✓ Test passed: Implicit rollback on drop works")
-
-    finally:
-        if os.path.exists(db_path):
-            os.unlink(db_path)
-
-
-async def test_explicit_rollback():
-    """Test explicit rollback of transaction changes"""
-    print("\n=== Test: Explicit Rollback ===")
-
-    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
-        db_path = tmp.name
-
-    try:
-        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
-        db = cdk_ffi.create_wallet_db(backend)
-
-        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
-        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
-
-        # Setup
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
-        keyset_info = cdk_ffi.KeySetInfo(
-            id=keyset_id.hex,
-            unit=cdk_ffi.CurrencyUnit.SAT(),
-            active=True,
-            input_fee_ppk=0
-        )
-        await tx.add_mint_keysets(mint_url, [keyset_info])
-        counter_initial = await tx.increment_keyset_counter(keyset_id, 5)
-        await tx.commit()
-        print(f"Initial counter: {counter_initial}")
-
-        # Increment and rollback
-        tx_rollback = await db.begin_db_transaction()
-        counter_incremented = await tx_rollback.increment_keyset_counter(keyset_id, 100)
-        print(f"Counter incremented to {counter_incremented} in transaction")
-        await tx_rollback.rollback()
-        print("Explicitly rolled back transaction")
-
-        # Verify rollback
-        tx_verify = await db.begin_db_transaction()
-        counter_after = await tx_verify.increment_keyset_counter(keyset_id, 0)
-        await tx_verify.rollback()
-
-        assert counter_after == counter_initial, \
-            f"Expected counter {counter_initial}, got {counter_after}"
-        print("✓ Explicit rollback works correctly")
-
-        print("✓ Test passed: Explicit rollback works")
-
-    finally:
-        if os.path.exists(db_path):
-            os.unlink(db_path)
-
-
-async def test_transaction_reads():
-    """Test reading data within transactions"""
-    print("\n=== Test: Transaction Reads ===")
-
-    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
-        db_path = tmp.name
-
-    try:
-        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
-        db = cdk_ffi.create_wallet_db(backend)
-
-        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
-        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
-
-        # Add keyset in transaction and read within same transaction
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
-        keyset_info = cdk_ffi.KeySetInfo(
-            id=keyset_id.hex,
-            unit=cdk_ffi.CurrencyUnit.SAT(),
-            active=True,
-            input_fee_ppk=0
-        )
-        await tx.add_mint_keysets(mint_url, [keyset_info])
-
-        keyset_read = await tx.get_keyset_by_id(keyset_id)
-        assert keyset_read is not None, "Should read within transaction"
-        assert keyset_read.id == keyset_id.hex, "Keyset ID should match"
-        print("✓ Read keyset within transaction")
-
-        await tx.commit()
-
-        # Read from new transaction
-        tx_new = await db.begin_db_transaction()
-        keyset_read2 = await tx_new.get_keyset_by_id(keyset_id)
-        assert keyset_read2 is not None, "Should read committed keyset"
-        await tx_new.rollback()
-        print("✓ Read keyset in new transaction")
-
-        print("✓ Test passed: Transaction reads work")
-
-    finally:
-        if os.path.exists(db_path):
-            os.unlink(db_path)
-
-
-async def test_multiple_increments_same_transaction():
-    """Test multiple increments in same transaction"""
-    print("\n=== Test: Multiple Increments in Same Transaction ===")
-
-    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
-        db_path = tmp.name
-
-    try:
-        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
-        db = cdk_ffi.create_wallet_db(backend)
-
-        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
-        mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
-
-        # Setup
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
-        keyset_info = cdk_ffi.KeySetInfo(
-            id=keyset_id.hex,
-            unit=cdk_ffi.CurrencyUnit.SAT(),
-            active=True,
-            input_fee_ppk=0
-        )
-        await tx.add_mint_keysets(mint_url, [keyset_info])
-        await tx.commit()
-
-        # Multiple increments in one transaction
-        tx = await db.begin_db_transaction()
-        counters = []
-        for i in range(1, 6):
-            counter = await tx.increment_keyset_counter(keyset_id, 1)
-            counters.append(counter)
-
-        expected = list(range(1, 6))
-        assert counters == expected, f"Expected {expected}, got {counters}"
-        print(f"✓ Counters incremented: {counters}")
-
-        await tx.commit()
-
-        # Verify final value
-        tx_verify = await db.begin_db_transaction()
-        final = await tx_verify.increment_keyset_counter(keyset_id, 0)
-        await tx_verify.rollback()
-        assert final == 5, f"Expected final counter 5, got {final}"
-        print("✓ Final counter value correct")
-
-        print("✓ Test passed: Multiple increments work")
-
-    finally:
-        if os.path.exists(db_path):
-            os.unlink(db_path)
-
-
-async def test_transaction_atomicity():
-    """Test that transaction rollback reverts all changes"""
-    print("\n=== Test: Transaction Atomicity ===")
-
-    with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
-        db_path = tmp.name
-
-    try:
-        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
-        db = cdk_ffi.create_wallet_db(backend)
-
-        mint_url1 = cdk_ffi.MintUrl(url="https://mint1.example.com")
-        mint_url2 = cdk_ffi.MintUrl(url="https://mint2.example.com")
-        keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
-
-        # Transaction with multiple operations
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url1, None)
-        await tx.add_mint(mint_url2, None)
-        keyset_info = cdk_ffi.KeySetInfo(
-            id=keyset_id.hex,
-            unit=cdk_ffi.CurrencyUnit.SAT(),
-            active=True,
-            input_fee_ppk=0
-        )
-        await tx.add_mint_keysets(mint_url1, [keyset_info])
-        await tx.increment_keyset_counter(keyset_id, 42)
-        print("✓ Performed multiple operations")
-
-        # Rollback
-        await tx.rollback()
-        print("✓ Rolled back transaction")
-
-        # Verify nothing persisted
-        tx_read = await db.begin_db_transaction()
-        keyset_read = await tx_read.get_keyset_by_id(keyset_id)
-        await tx_read.rollback()
-        assert keyset_read is None, "Keyset should not exist after rollback"
-        print("✓ Nothing persisted after rollback")
-
-        # Now commit
-        tx2 = await db.begin_db_transaction()
-        await tx2.add_mint(mint_url1, None)
-        await tx2.add_mint(mint_url2, None)
-        await tx2.add_mint_keysets(mint_url1, [keyset_info])
-        await tx2.increment_keyset_counter(keyset_id, 42)
-        await tx2.commit()
-        print("✓ Committed transaction")
-
-        # Verify persistence
-        tx_verify = await db.begin_db_transaction()
-        keyset_after = await tx_verify.get_keyset_by_id(keyset_id)
-        assert keyset_after is not None, "Keyset should exist after commit"
-        counter_after = await tx_verify.increment_keyset_counter(keyset_id, 0)
-        await tx_verify.rollback()
-        assert counter_after == 42, f"Expected counter 42, got {counter_after}"
-        print("✓ All operations persisted after commit")
-
-        print("✓ Test passed: Transaction atomicity works")
-
-    finally:
-        if os.path.exists(db_path):
-            os.unlink(db_path)
-
-
-
-
-# Wallet Tests (using direct wallet methods without explicit transactions)
+# Wallet Database Tests
 
 async def test_wallet_creation():
     """Test creating a wallet with SQLite backend"""
@@ -394,20 +68,16 @@ async def test_wallet_mint_management():
 
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
 
-        # Add mint (using transaction)
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
-        await tx.commit()
+        # Add mint
+        await db.add_mint(mint_url, None)
         print("✓ Added mint to wallet")
 
-        # Get specific mint (read-only, can use db directly)
+        # Get specific mint
         await db.get_mint(mint_url)
         print("✓ Retrieved mint from database")
 
-        # Remove mint (using transaction)
-        tx = await db.begin_db_transaction()
-        await tx.remove_mint(mint_url)
-        await tx.commit()
+        # Remove mint
+        await db.remove_mint(mint_url)
         print("✓ Removed mint from wallet")
 
         # Verify removal
@@ -436,26 +106,24 @@ async def test_wallet_keyset_management():
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
 
-        # Add mint and keyset (using transaction)
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
+        # Add mint and keyset
+        await db.add_mint(mint_url, None)
         keyset_info = cdk_ffi.KeySetInfo(
             id=keyset_id.hex,
             unit=cdk_ffi.CurrencyUnit.SAT(),
             active=True,
             input_fee_ppk=0
         )
-        await tx.add_mint_keysets(mint_url, [keyset_info])
-        await tx.commit()
+        await db.add_mint_keysets(mint_url, [keyset_info])
         print("✓ Added mint and keyset")
 
-        # Query keyset by ID (read-only)
+        # Query keyset by ID
         keyset = await db.get_keyset_by_id(keyset_id)
         assert keyset is not None, "Keyset should exist"
         assert keyset.id == keyset_id.hex, "Keyset ID should match"
         print(f"✓ Retrieved keyset: {keyset.id}")
 
-        # Query keysets for mint (read-only)
+        # Query keysets for mint
         keysets = await db.get_mint_keysets(mint_url)
         assert keysets is not None and len(keysets) > 0, "Should have keysets for mint"
         print(f"✓ Retrieved {len(keysets)} keyset(s) for mint")
@@ -481,25 +149,21 @@ async def test_wallet_keyset_counter():
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
 
-        # Setup (using transaction)
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
+        # Setup
+        await db.add_mint(mint_url, None)
         keyset_info = cdk_ffi.KeySetInfo(
             id=keyset_id.hex,
             unit=cdk_ffi.CurrencyUnit.SAT(),
             active=True,
             input_fee_ppk=0
         )
-        await tx.add_mint_keysets(mint_url, [keyset_info])
-        await tx.commit()
+        await db.add_mint_keysets(mint_url, [keyset_info])
         print("✓ Setup complete")
 
-        # Increment counter (using transaction)
-        tx = await db.begin_db_transaction()
-        counter1 = await tx.increment_keyset_counter(keyset_id, 1)
-        counter2 = await tx.increment_keyset_counter(keyset_id, 5)
-        counter3 = await tx.increment_keyset_counter(keyset_id, 0)
-        await tx.commit()
+        # Increment counter
+        counter1 = await db.increment_keyset_counter(keyset_id, 1)
+        counter2 = await db.increment_keyset_counter(keyset_id, 5)
+        counter3 = await db.increment_keyset_counter(keyset_id, 0)
 
         print(f"✓ Counter after +1: {counter1}")
         assert counter1 == 1, f"Expected counter 1, got {counter1}"
@@ -528,13 +192,11 @@ async def test_wallet_quotes():
 
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
 
-        # Add mint (using transaction)
-        tx = await db.begin_db_transaction()
-        await tx.add_mint(mint_url, None)
-        await tx.commit()
+        # Add mint
+        await db.add_mint(mint_url, None)
         print("✓ Added mint")
 
-        # Query quotes (read-only)
+        # Query quotes
         mint_quotes = await db.get_mint_quotes()
         assert isinstance(mint_quotes, list), "get_mint_quotes should return a list"
         print(f"✓ Retrieved {len(mint_quotes)} mint quote(s)")
@@ -575,18 +237,10 @@ async def test_wallet_proofs_by_ys():
 
 async def main():
     """Run all tests"""
-    print("Starting CDK FFI Wallet and Transaction Tests")
+    print("Starting CDK FFI Wallet Database Tests")
     print("=" * 50)
 
     tests = [
-        # Transaction tests
-        ("Increment Counter with Commit", test_increment_keyset_counter_commit),
-        ("Implicit Rollback on Drop", test_implicit_rollback_on_drop),
-        ("Explicit Rollback", test_explicit_rollback),
-        ("Transaction Reads", test_transaction_reads),
-        ("Multiple Increments", test_multiple_increments_same_transaction),
-        ("Transaction Atomicity", test_transaction_atomicity),
-        # Wallet tests (read methods + write via transactions)
         ("Wallet Creation", test_wallet_creation),
         ("Wallet Mint Management", test_wallet_mint_management),
         ("Wallet Keyset Management", test_wallet_keyset_management),

+ 5 - 3
crates/cdk-integration-tests/tests/fake_auth.rs

@@ -515,9 +515,11 @@ async fn test_reuse_auth_proof() {
         assert!(quote.amount == Some(10.into()));
     }
 
-    let mut tx = wallet.localstore.begin_db_transaction().await.unwrap();
-    tx.update_proofs(proofs, vec![]).await.unwrap();
-    tx.commit().await.unwrap();
+    wallet
+        .localstore
+        .update_proofs(proofs, vec![])
+        .await
+        .unwrap();
 
     {
         let quote_res = wallet.mint_quote(10.into(), None).await;

+ 5 - 7
crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs

@@ -512,13 +512,11 @@ async fn test_restore_with_counter_gap() {
     // This simulates failed operations or multi-device usage where counter values
     // were consumed but no signatures were obtained
     let gap_size = 50u32;
-    {
-        let mut tx = wallet.localstore.begin_db_transaction().await.unwrap();
-        tx.increment_keyset_counter(&keyset_id, gap_size)
-            .await
-            .unwrap();
-        tx.commit().await.unwrap();
-    }
+    wallet
+        .localstore
+        .increment_keyset_counter(&keyset_id, gap_size)
+        .await
+        .unwrap();
 
     // Mint second batch of proofs (uses counters after the gap)
     let mint_quote2 = wallet.mint_quote(100.into(), None).await.unwrap();

+ 366 - 566
crates/cdk-redb/src/wallet/mod.rs

@@ -8,10 +8,7 @@ use std::sync::Arc;
 
 use async_trait::async_trait;
 use cdk_common::common::ProofInfo;
-use cdk_common::database::{
-    validate_kvstore_params, DbTransactionFinalizer, KVStore, KVStoreDatabase, KVStoreTransaction,
-    WalletDatabase, WalletDatabaseTransaction,
-};
+use cdk_common::database::{validate_kvstore_params, KVStoreDatabase, WalletDatabase};
 use cdk_common::mint_url::MintUrl;
 use cdk_common::nut00::KnownMethod;
 use cdk_common::util::unix_time;
@@ -23,79 +20,6 @@ use cdk_common::{
 use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
 use tracing::instrument;
 
-/// Enum to abstract over read-only and read-write table access for KV store operations
-enum KvTable<'txn> {
-    ReadOnly(redb::ReadOnlyTable<(&'static str, &'static str, &'static str), &'static [u8]>),
-    ReadWrite(redb::Table<'txn, (&'static str, &'static str, &'static str), &'static [u8]>),
-}
-
-impl KvTable<'_> {
-    /// Read a value from the KV store table
-    #[inline(always)]
-    fn kv_read(
-        &self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-        key: &str,
-    ) -> Result<Option<Vec<u8>>, Error> {
-        let result = match self {
-            KvTable::ReadOnly(table) => table
-                .get((primary_namespace, secondary_namespace, key))
-                .map_err(Error::from)?
-                .map(|v| v.value().to_vec()),
-            KvTable::ReadWrite(table) => table
-                .get((primary_namespace, secondary_namespace, key))
-                .map_err(Error::from)?
-                .map(|v| v.value().to_vec()),
-        };
-
-        Ok(result)
-    }
-
-    /// List all keys in a namespace from the KV store table
-    #[inline(always)]
-    fn kv_list(
-        &self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-    ) -> Result<Vec<String>, Error> {
-        let mut keys = Vec::new();
-
-        // Use range iterator for efficient lookup by namespace prefix
-        let start = (primary_namespace, secondary_namespace, "");
-
-        match self {
-            KvTable::ReadOnly(table) => {
-                for result in table.range(start..).map_err(Error::from)? {
-                    let (key_tuple, _) = result.map_err(Error::from)?;
-                    let (primary_from_db, secondary_from_db, k) = key_tuple.value();
-                    if primary_from_db != primary_namespace
-                        || secondary_from_db != secondary_namespace
-                    {
-                        break;
-                    }
-                    keys.push(k.to_string());
-                }
-            }
-            KvTable::ReadWrite(table) => {
-                for result in table.range(start..).map_err(Error::from)? {
-                    let (key_tuple, _) = result.map_err(Error::from)?;
-                    let (primary_from_db, secondary_from_db, k) = key_tuple.value();
-                    if primary_from_db != primary_namespace
-                        || secondary_from_db != secondary_namespace
-                    {
-                        break;
-                    }
-                    keys.push(k.to_string());
-                }
-            }
-        }
-
-        // Keys are already sorted by the B-tree structure
-        Ok(keys)
-    }
-}
-
 use super::error::Error;
 use crate::migrations::migrate_00_to_01;
 use crate::wallet::migrations::{migrate_01_to_02, migrate_02_to_03, migrate_03_to_04};
@@ -133,30 +57,6 @@ pub struct WalletRedbDatabase {
     db: Arc<Database>,
 }
 
-/// Redb Wallet Transaction
-#[allow(missing_debug_implementations)]
-pub struct RedbWalletTransaction {
-    write_txn: Option<redb::WriteTransaction>,
-}
-
-impl RedbWalletTransaction {
-    /// Create a new transaction
-    fn new(write_txn: redb::WriteTransaction) -> Self {
-        Self {
-            write_txn: Some(write_txn),
-        }
-    }
-
-    /// Get a mutable reference to the write transaction
-    fn txn(&mut self) -> Result<&mut redb::WriteTransaction, Error> {
-        self.write_txn.as_mut().ok_or_else(|| {
-            Error::CDKDatabase(database::Error::Internal(
-                "Transaction already consumed".to_owned(),
-            ))
-        })
-    }
-}
-
 impl WalletRedbDatabase {
     /// Create new [`WalletRedbDatabase`]
     pub fn new(path: &Path) -> Result<Self, Error> {
@@ -597,189 +497,156 @@ impl WalletDatabase<database::Error> for WalletRedbDatabase {
         Ok(transactions)
     }
 
-    async fn begin_db_transaction(
+    #[instrument(skip(self, added, removed_ys))]
+    async fn update_proofs(
         &self,
-    ) -> Result<Box<dyn WalletDatabaseTransaction<database::Error> + Send + Sync>, database::Error>
-    {
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), database::Error> {
         let write_txn = self.db.begin_write().map_err(Error::from)?;
-        Ok(Box::new(RedbWalletTransaction::new(write_txn)))
-    }
-}
-
-#[async_trait]
-impl KVStoreDatabase for WalletRedbDatabase {
-    type Err = database::Error;
-
-    #[instrument(skip_all)]
-    async fn kv_read(
-        &self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-        key: &str,
-    ) -> Result<Option<Vec<u8>>, Self::Err> {
-        // Validate parameters according to KV store requirements
-        validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
-
-        let read_txn = self.db.begin_read().map_err(Error::from)?;
-        let table = KvTable::ReadOnly(read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
-
-        Ok(table.kv_read(primary_namespace, secondary_namespace, key)?)
-    }
-
-    #[instrument(skip_all)]
-    async fn kv_list(
-        &self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-    ) -> Result<Vec<String>, Self::Err> {
-        validate_kvstore_params(primary_namespace, secondary_namespace, None)?;
+        {
+            let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
 
-        let read_txn = self.db.begin_read().map_err(Error::from)?;
-        let table = KvTable::ReadOnly(read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
+            for proof_info in added.iter() {
+                table
+                    .insert(
+                        proof_info.y.to_bytes().as_slice(),
+                        serde_json::to_string(&proof_info)
+                            .map_err(Error::from)?
+                            .as_str(),
+                    )
+                    .map_err(Error::from)?;
+            }
 
-        Ok(table.kv_list(primary_namespace, secondary_namespace)?)
+            for y in removed_ys.iter() {
+                table.remove(y.to_bytes().as_slice()).map_err(Error::from)?;
+            }
+        }
+        write_txn.commit().map_err(Error::from)?;
+        Ok(())
     }
-}
 
-#[async_trait]
-impl KVStore for WalletRedbDatabase {
-    async fn begin_transaction(
+    async fn update_proofs_state(
         &self,
-    ) -> Result<Box<dyn KVStoreTransaction<Self::Err> + Send + Sync>, database::Error> {
+        ys: Vec<PublicKey>,
+        state: State,
+    ) -> Result<(), database::Error> {
         let write_txn = self.db.begin_write().map_err(Error::from)?;
-        Ok(Box::new(RedbWalletTransaction::new(write_txn)))
-    }
-}
-
-#[async_trait]
-impl WalletDatabaseTransaction<database::Error> for RedbWalletTransaction {
-    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
-    async fn get_keyset_by_id(
-        &mut self,
-        keyset_id: &Id,
-    ) -> Result<Option<KeySetInfo>, database::Error> {
-        let txn = self.txn().map_err(Into::<database::Error>::into)?;
-        let table = txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
-
-        let result = match table
-            .get(keyset_id.to_bytes().as_slice())
-            .map_err(Error::from)?
         {
-            Some(keyset) => {
-                let keyset: KeySetInfo =
-                    serde_json::from_str(keyset.value()).map_err(Error::from)?;
+            let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
 
-                Ok(Some(keyset))
-            }
-            None => Ok(None),
-        };
+            for y in ys {
+                let y_slice = y.to_bytes();
+                let proof = table
+                    .get(y_slice.as_slice())
+                    .map_err(Error::from)?
+                    .ok_or(Error::UnknownY)?;
 
-        result
-    }
+                let mut proof_info =
+                    serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
+                drop(proof);
 
-    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
-    async fn get_keys(&mut self, keyset_id: &Id) -> Result<Option<Keys>, database::Error> {
-        let txn = self.txn().map_err(Into::<database::Error>::into)?;
-        let table = txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
+                proof_info.state = state;
 
-        if let Some(mint_info) = table
-            .get(keyset_id.to_string().as_str())
-            .map_err(Error::from)?
-        {
-            return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
+                table
+                    .insert(
+                        y_slice.as_slice(),
+                        serde_json::to_string(&proof_info)
+                            .map_err(Error::from)?
+                            .as_str(),
+                    )
+                    .map_err(Error::from)?;
+            }
         }
-
-        Ok(None)
-    }
-
-    #[instrument(skip(self))]
-    async fn add_mint(
-        &mut self,
-        mint_url: MintUrl,
-        mint_info: Option<MintInfo>,
-    ) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(MINTS_TABLE).map_err(Error::from)?;
-        table
-            .insert(
-                mint_url.to_string().as_str(),
-                serde_json::to_string(&mint_info)
-                    .map_err(Error::from)?
-                    .as_str(),
-            )
-            .map_err(Error::from)?;
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
     #[instrument(skip(self))]
-    async fn remove_mint(&mut self, mint_url: MintUrl) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(MINTS_TABLE).map_err(Error::from)?;
-        table
-            .remove(mint_url.to_string().as_str())
-            .map_err(Error::from)?;
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), database::Error> {
+        let id = transaction.id();
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(TRANSACTIONS_TABLE)
+                .map_err(Error::from)?;
+            table
+                .insert(
+                    id.as_slice(),
+                    serde_json::to_string(&transaction)
+                        .map_err(Error::from)?
+                        .as_str(),
+                )
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
     #[instrument(skip(self))]
     async fn update_mint_url(
-        &mut self,
+        &self,
         old_mint_url: MintUrl,
         new_mint_url: MintUrl,
     ) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+
         // Update proofs table
         {
-            let proofs = self
-                .get_proofs(Some(old_mint_url.clone()), None, None, None)
-                .await
-                .map_err(Error::from)?;
-
-            // Proofs with new url
-            let updated_proofs: Vec<ProofInfo> = proofs
-                .clone()
-                .into_iter()
-                .map(|mut p| {
-                    p.mint_url = new_mint_url.clone();
-                    p
+            let read_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
+            let proofs: Vec<ProofInfo> = read_table
+                .iter()
+                .map_err(Error::from)?
+                .flatten()
+                .filter_map(|(_k, v)| {
+                    let proof_info = serde_json::from_str::<ProofInfo>(v.value()).ok()?;
+                    if proof_info.mint_url == old_mint_url {
+                        Some(proof_info)
+                    } else {
+                        None
+                    }
                 })
                 .collect();
-
-            if !updated_proofs.is_empty() {
-                self.update_proofs(updated_proofs, vec![]).await?;
+            drop(read_table);
+
+            if !proofs.is_empty() {
+                let mut write_table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
+                for mut proof_info in proofs {
+                    proof_info.mint_url = new_mint_url.clone();
+                    write_table
+                        .insert(
+                            proof_info.y.to_bytes().as_slice(),
+                            serde_json::to_string(&proof_info)
+                                .map_err(Error::from)?
+                                .as_str(),
+                        )
+                        .map_err(Error::from)?;
+                }
             }
         }
 
         // Update mint quotes
         {
-            let read_txn = self.txn()?;
-            let mut table = read_txn
+            let mut table = write_txn
                 .open_table(MINT_QUOTES_TABLE)
                 .map_err(Error::from)?;
 
             let unix_time = unix_time();
 
-            let quotes = table
+            let quotes: Vec<MintQuote> = table
                 .iter()
                 .map_err(Error::from)?
                 .flatten()
                 .filter_map(|(_, quote)| {
-                    let mut q: MintQuote = serde_json::from_str(quote.value())
-                        .inspect_err(|err| {
-                            tracing::warn!(
-                                "Failed to deserialize {}  with error {}",
-                                quote.value(),
-                                err
-                            )
-                        })
-                        .ok()?;
-                    if q.expiry < unix_time {
+                    let mut q: MintQuote = serde_json::from_str(quote.value()).ok()?;
+                    if q.mint_url == old_mint_url && q.expiry >= unix_time {
                         q.mint_url = new_mint_url.clone();
                         Some(q)
                     } else {
                         None
                     }
                 })
-                .collect::<Vec<_>>();
+                .collect();
 
             for quote in quotes {
                 table
@@ -791,457 +658,390 @@ impl WalletDatabaseTransaction<database::Error> for RedbWalletTransaction {
             }
         }
 
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
-    #[instrument(skip(self))]
-    async fn add_mint_keysets(
-        &mut self,
-        mint_url: MintUrl,
-        keysets: Vec<KeySetInfo>,
-    ) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn
-            .open_multimap_table(MINT_KEYSETS_TABLE)
-            .map_err(Error::from)?;
-        let mut keysets_table = txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
-        let mut u32_table = txn.open_table(KEYSET_U32_MAPPING).map_err(Error::from)?;
-
-        let mut existing_u32 = false;
-
-        for keyset in keysets {
-            // Check if keyset already exists
-            let existing_keyset = {
-                let existing_keyset = keysets_table
-                    .get(keyset.id.to_bytes().as_slice())
-                    .map_err(Error::from)?;
+    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
+    async fn increment_keyset_counter(
+        &self,
+        keyset_id: &Id,
+        count: u32,
+    ) -> Result<u32, database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        let new_counter = {
+            let mut table = write_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
+            let current_counter = table
+                .get(keyset_id.to_string().as_str())
+                .map_err(Error::from)?
+                .map(|x| x.value())
+                .unwrap_or_default();
 
-                existing_keyset.map(|r| r.value().to_string())
-            };
+            let new_counter = current_counter + count;
 
-            let existing = u32_table
-                .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
+            table
+                .insert(keyset_id.to_string().as_str(), new_counter)
                 .map_err(Error::from)?;
 
-            match existing {
-                None => existing_u32 = false,
-                Some(id) => {
-                    let id = Id::from_str(id.value())?;
-
-                    if id == keyset.id {
-                        existing_u32 = false;
-                    } else {
-                        existing_u32 = true;
-                        break;
-                    }
-                }
-            }
-
-            let keyset = if let Some(existing_keyset) = existing_keyset {
-                let mut existing_keyset: KeySetInfo = serde_json::from_str(&existing_keyset)?;
-
-                existing_keyset.active = keyset.active;
-                existing_keyset.input_fee_ppk = keyset.input_fee_ppk;
-
-                existing_keyset
-            } else {
-                table
-                    .insert(
-                        mint_url.to_string().as_str(),
-                        keyset.id.to_bytes().as_slice(),
-                    )
-                    .map_err(Error::from)?;
-
-                keyset
-            };
+            new_counter
+        };
+        write_txn.commit().map_err(Error::from)?;
+        Ok(new_counter)
+    }
 
-            keysets_table
+    #[instrument(skip(self))]
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
+            table
                 .insert(
-                    keyset.id.to_bytes().as_slice(),
-                    serde_json::to_string(&keyset)
+                    mint_url.to_string().as_str(),
+                    serde_json::to_string(&mint_info)
                         .map_err(Error::from)?
                         .as_str(),
                 )
                 .map_err(Error::from)?;
         }
-
-        if existing_u32 {
-            tracing::warn!("Keyset already exists for keyset id");
-            return Err(database::Error::Duplicate);
-        }
-
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
-    #[instrument(skip_all)]
-    async fn get_mint_quote(
-        &mut self,
-        quote_id: &str,
-    ) -> Result<Option<MintQuote>, database::Error> {
-        let txn = self.txn()?;
-        let table = txn.open_table(MINT_QUOTES_TABLE).map_err(Error::from)?;
-
-        if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? {
-            return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
+    #[instrument(skip(self))]
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
+            table
+                .remove(mint_url.to_string().as_str())
+                .map_err(Error::from)?;
         }
-
-        Ok(None)
-    }
-
-    #[instrument(skip_all)]
-    async fn add_mint_quote(&mut self, quote: MintQuote) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(MINT_QUOTES_TABLE).map_err(Error::from)?;
-        table
-            .insert(
-                quote.id.as_str(),
-                serde_json::to_string(&quote).map_err(Error::from)?.as_str(),
-            )
-            .map_err(Error::from)?;
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
-    #[instrument(skip_all)]
-    async fn remove_mint_quote(&mut self, quote_id: &str) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(MINT_QUOTES_TABLE).map_err(Error::from)?;
-        table.remove(quote_id).map_err(Error::from)?;
-        Ok(())
-    }
-
-    #[instrument(skip_all)]
-    async fn get_melt_quote(
-        &mut self,
-        quote_id: &str,
-    ) -> Result<Option<wallet::MeltQuote>, database::Error> {
-        let txn = self.txn()?;
-        let table = txn.open_table(MELT_QUOTES_TABLE).map_err(Error::from)?;
+    #[instrument(skip(self))]
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_multimap_table(MINT_KEYSETS_TABLE)
+                .map_err(Error::from)?;
+            let mut keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
+            let mut u32_table = write_txn
+                .open_table(KEYSET_U32_MAPPING)
+                .map_err(Error::from)?;
 
-        if let Some(mint_info) = table.get(quote_id).map_err(Error::from)? {
-            return Ok(serde_json::from_str(mint_info.value()).map_err(Error::from)?);
-        }
+            let mut existing_u32 = false;
 
-        Ok(None)
-    }
+            for keyset in keysets {
+                // Check if keyset already exists
+                let existing_keyset = {
+                    let existing_keyset = keysets_table
+                        .get(keyset.id.to_bytes().as_slice())
+                        .map_err(Error::from)?;
 
-    #[instrument(skip_all)]
-    async fn add_melt_quote(&mut self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(MELT_QUOTES_TABLE).map_err(Error::from)?;
-        table
-            .insert(
-                quote.id.as_str(),
-                serde_json::to_string(&quote).map_err(Error::from)?.as_str(),
-            )
-            .map_err(Error::from)?;
-        Ok(())
-    }
+                    existing_keyset.map(|r| r.value().to_string())
+                };
 
-    #[instrument(skip_all)]
-    async fn remove_melt_quote(&mut self, quote_id: &str) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(MELT_QUOTES_TABLE).map_err(Error::from)?;
-        table.remove(quote_id).map_err(Error::from)?;
-        Ok(())
-    }
+                let existing = u32_table
+                    .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
+                    .map_err(Error::from)?;
 
-    #[instrument(skip_all)]
-    async fn add_keys(&mut self, keyset: KeySet) -> Result<(), database::Error> {
-        let txn = self.txn()?;
+                match existing {
+                    None => existing_u32 = false,
+                    Some(id) => {
+                        let id = Id::from_str(id.value())?;
 
-        keyset.verify_id()?;
+                        if id == keyset.id {
+                            existing_u32 = false;
+                        } else {
+                            existing_u32 = true;
+                            break;
+                        }
+                    }
+                }
 
-        let mut table = txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
+                let keyset = if let Some(existing_keyset) = existing_keyset {
+                    let mut existing_keyset: KeySetInfo = serde_json::from_str(&existing_keyset)?;
 
-        let existing_keys = table
-            .insert(
-                keyset.id.to_string().as_str(),
-                serde_json::to_string(&keyset.keys)
-                    .map_err(Error::from)?
-                    .as_str(),
-            )
-            .map_err(Error::from)?
-            .is_some();
+                    existing_keyset.active = keyset.active;
+                    existing_keyset.input_fee_ppk = keyset.input_fee_ppk;
 
-        let mut table = txn.open_table(KEYSET_U32_MAPPING).map_err(Error::from)?;
+                    existing_keyset
+                } else {
+                    table
+                        .insert(
+                            mint_url.to_string().as_str(),
+                            keyset.id.to_bytes().as_slice(),
+                        )
+                        .map_err(Error::from)?;
 
-        let existing = table
-            .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
-            .map_err(Error::from)?;
+                    keyset
+                };
 
-        let existing_u32 = match existing {
-            None => false,
-            Some(id) => {
-                let id = Id::from_str(id.value())?;
-                id != keyset.id
+                keysets_table
+                    .insert(
+                        keyset.id.to_bytes().as_slice(),
+                        serde_json::to_string(&keyset)
+                            .map_err(Error::from)?
+                            .as_str(),
+                    )
+                    .map_err(Error::from)?;
             }
-        };
 
-        if existing_keys || existing_u32 {
-            tracing::warn!("Keys already exist for keyset id");
-            return Err(database::Error::Duplicate);
+            if existing_u32 {
+                tracing::warn!("Keyset already exists for keyset id");
+                return Err(database::Error::Duplicate);
+            }
         }
-
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
-    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
-    async fn remove_keys(&mut self, keyset_id: &Id) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
-
-        table
-            .remove(keyset_id.to_string().as_str())
-            .map_err(Error::from)?;
-
+    #[instrument(skip_all)]
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(MINT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+            table
+                .insert(
+                    quote.id.as_str(),
+                    serde_json::to_string(&quote).map_err(Error::from)?.as_str(),
+                )
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
     #[instrument(skip_all)]
-    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>, database::Error> {
-        let txn = self.txn()?;
-        let table = txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
-
-        let proofs: Vec<ProofInfo> = table
-            .iter()
-            .map_err(Error::from)?
-            .flatten()
-            .filter_map(|(_k, v)| {
-                let mut proof = None;
-
-                if let Ok(proof_info) = serde_json::from_str::<ProofInfo>(v.value()) {
-                    if proof_info.matches_conditions(&mint_url, &unit, &state, &spending_conditions)
-                    {
-                        proof = Some(proof_info)
-                    }
-                }
-
-                proof
-            })
-            .collect();
-
-        Ok(proofs)
+    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(MINT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+            table.remove(quote_id).map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
+        Ok(())
     }
 
-    #[instrument(skip(self, added, deleted_ys))]
-    async fn update_proofs(
-        &mut self,
-        added: Vec<ProofInfo>,
-        deleted_ys: Vec<PublicKey>,
-    ) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
-
-        for proof_info in added.iter() {
+    #[instrument(skip_all)]
+    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(MELT_QUOTES_TABLE)
+                .map_err(Error::from)?;
             table
                 .insert(
-                    proof_info.y.to_bytes().as_slice(),
-                    serde_json::to_string(&proof_info)
-                        .map_err(Error::from)?
-                        .as_str(),
+                    quote.id.as_str(),
+                    serde_json::to_string(&quote).map_err(Error::from)?.as_str(),
                 )
                 .map_err(Error::from)?;
         }
+        write_txn.commit().map_err(Error::from)?;
+        Ok(())
+    }
 
-        for y in deleted_ys.iter() {
-            table.remove(y.to_bytes().as_slice()).map_err(Error::from)?;
+    #[instrument(skip_all)]
+    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(MELT_QUOTES_TABLE)
+                .map_err(Error::from)?;
+            table.remove(quote_id).map_err(Error::from)?;
         }
-
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
-    async fn update_proofs_state(
-        &mut self,
-        ys: Vec<PublicKey>,
-        state: State,
-    ) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
-
-        for y in ys {
-            let y_slice = y.to_bytes();
-            let proof = table
-                .get(y_slice.as_slice())
-                .map_err(Error::from)?
-                .ok_or(Error::UnknownY)?;
+    #[instrument(skip_all)]
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
 
-            let mut proof_info =
-                serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
-            drop(proof);
+        keyset.verify_id()?;
 
-            proof_info.state = state;
+        {
+            let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
 
-            table
+            let existing_keys = table
                 .insert(
-                    y_slice.as_slice(),
-                    serde_json::to_string(&proof_info)
+                    keyset.id.to_string().as_str(),
+                    serde_json::to_string(&keyset.keys)
                         .map_err(Error::from)?
                         .as_str(),
                 )
+                .map_err(Error::from)?
+                .is_some();
+
+            let mut table = write_txn
+                .open_table(KEYSET_U32_MAPPING)
+                .map_err(Error::from)?;
+
+            let existing = table
+                .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
                 .map_err(Error::from)?;
-        }
 
+            let existing_u32 = match existing {
+                None => false,
+                Some(id) => {
+                    let id = Id::from_str(id.value())?;
+                    id != keyset.id
+                }
+            };
+
+            if existing_keys || existing_u32 {
+                tracing::warn!("Keys already exist for keyset id");
+                return Err(database::Error::Duplicate);
+            }
+        }
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
     #[instrument(skip(self), fields(keyset_id = %keyset_id))]
-    async fn increment_keyset_counter(
-        &mut self,
-        keyset_id: &Id,
-        count: u32,
-    ) -> Result<u32, database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
-        let current_counter = table
-            .get(keyset_id.to_string().as_str())
-            .map_err(Error::from)?
-            .map(|x| x.value())
-            .unwrap_or_default();
-
-        let new_counter = current_counter + count;
-
-        table
-            .insert(keyset_id.to_string().as_str(), new_counter)
-            .map_err(Error::from)?;
-
-        Ok(new_counter)
-    }
+    async fn remove_keys(&self, keyset_id: &Id) -> Result<(), database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
 
-    #[instrument(skip(self))]
-    async fn add_transaction(&mut self, transaction: Transaction) -> Result<(), database::Error> {
-        let id = transaction.id();
-        let txn = self.txn()?;
-        let mut table = txn.open_table(TRANSACTIONS_TABLE).map_err(Error::from)?;
-        table
-            .insert(
-                id.as_slice(),
-                serde_json::to_string(&transaction)
-                    .map_err(Error::from)?
-                    .as_str(),
-            )
-            .map_err(Error::from)?;
+            table
+                .remove(keyset_id.to_string().as_str())
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
 
     #[instrument(skip(self))]
     async fn remove_transaction(
-        &mut self,
+        &self,
         transaction_id: TransactionId,
     ) -> Result<(), database::Error> {
-        let txn = self.txn()?;
-        let mut table = txn.open_table(TRANSACTIONS_TABLE).map_err(Error::from)?;
-        table
-            .remove(transaction_id.as_slice())
-            .map_err(Error::from)?;
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn
+                .open_table(TRANSACTIONS_TABLE)
+                .map_err(Error::from)?;
+            table
+                .remove(transaction_id.as_slice())
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
         Ok(())
     }
-}
 
-#[async_trait]
-impl KVStoreTransaction<database::Error> for RedbWalletTransaction {
-    #[instrument(skip_all)]
-    async fn kv_read(
-        &mut self,
+    // KV Store write methods (non-transactional)
+
+    #[instrument(skip(self, value))]
+    async fn kv_write(
+        &self,
         primary_namespace: &str,
         secondary_namespace: &str,
         key: &str,
-    ) -> Result<Option<Vec<u8>>, database::Error> {
+        value: &[u8],
+    ) -> Result<(), database::Error> {
         // Validate parameters according to KV store requirements
         validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
 
-        let txn = self.txn()?;
-        let table = KvTable::ReadWrite(txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
+            table
+                .insert((primary_namespace, secondary_namespace, key), value)
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
 
-        Ok(table.kv_read(primary_namespace, secondary_namespace, key)?)
+        Ok(())
     }
 
-    #[instrument(skip_all)]
-    async fn kv_write(
-        &mut self,
+    #[instrument(skip(self))]
+    async fn kv_remove(
+        &self,
         primary_namespace: &str,
         secondary_namespace: &str,
         key: &str,
-        value: &[u8],
     ) -> Result<(), database::Error> {
         // Validate parameters according to KV store requirements
         validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
 
-        let txn = self.txn()?;
-        let mut table = txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
-
-        table
-            .insert((primary_namespace, secondary_namespace, key), value)
-            .map_err(Error::from)?;
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        {
+            let mut table = write_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
+            table
+                .remove((primary_namespace, secondary_namespace, key))
+                .map_err(Error::from)?;
+        }
+        write_txn.commit().map_err(Error::from)?;
 
         Ok(())
     }
+}
+
+#[async_trait]
+impl KVStoreDatabase for WalletRedbDatabase {
+    type Err = database::Error;
 
     #[instrument(skip_all)]
-    async fn kv_remove(
-        &mut self,
+    async fn kv_read(
+        &self,
         primary_namespace: &str,
         secondary_namespace: &str,
         key: &str,
-    ) -> Result<(), database::Error> {
+    ) -> Result<Option<Vec<u8>>, Self::Err> {
         // Validate parameters according to KV store requirements
         validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
 
-        let txn = self.txn()?;
-        let mut table = txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
+        let table = read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
 
-        table
-            .remove((primary_namespace, secondary_namespace, key))
-            .map_err(Error::from)?;
+        let result = table
+            .get((primary_namespace, secondary_namespace, key))
+            .map_err(Error::from)?
+            .map(|v| v.value().to_vec());
 
-        Ok(())
+        Ok(result)
     }
 
     #[instrument(skip_all)]
     async fn kv_list(
-        &mut self,
+        &self,
         primary_namespace: &str,
         secondary_namespace: &str,
-    ) -> Result<Vec<String>, database::Error> {
-        // Validate namespace parameters according to KV store requirements
+    ) -> Result<Vec<String>, Self::Err> {
         validate_kvstore_params(primary_namespace, secondary_namespace, None)?;
 
-        let txn = self.txn()?;
-        let table = KvTable::ReadWrite(txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
-
-        Ok(table.kv_list(primary_namespace, secondary_namespace)?)
-    }
-}
-
-#[async_trait]
-impl DbTransactionFinalizer for RedbWalletTransaction {
-    type Err = database::Error;
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
+        let table = read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
 
-    async fn commit(mut self: Box<Self>) -> Result<(), database::Error> {
-        if let Some(txn) = self.write_txn.take() {
-            txn.commit().map_err(Error::from)?;
-        }
-        Ok(())
-    }
+        let mut keys = Vec::new();
+        let start = (primary_namespace, secondary_namespace, "");
 
-    async fn rollback(mut self: Box<Self>) -> Result<(), database::Error> {
-        if let Some(txn) = self.write_txn.take() {
-            txn.abort().map_err(Error::from)?;
+        for result in table.range(start..).map_err(Error::from)? {
+            let (key_tuple, _) = result.map_err(Error::from)?;
+            let (primary_from_db, secondary_from_db, k) = key_tuple.value();
+            if primary_from_db != primary_namespace || secondary_from_db != secondary_namespace {
+                break;
+            }
+            keys.push(k.to_string());
         }
-        Ok(())
-    }
-}
 
-impl Drop for RedbWalletTransaction {
-    fn drop(&mut self) {
-        if let Some(txn) = self.write_txn.take() {
-            let _ = txn.abort();
-        }
+        Ok(keys)
     }
 }
 

+ 78 - 1
crates/cdk-sql-common/src/keyvalue.rs

@@ -9,11 +9,15 @@ use cdk_common::database::{validate_kvstore_params, Error};
 use cdk_common::util::unix_time;
 
 use crate::column_as_string;
+#[cfg(feature = "mint")]
 use crate::database::ConnectionWithTransaction;
-use crate::pool::{DatabasePool, Pool, PooledResource};
+#[cfg(feature = "mint")]
+use crate::pool::PooledResource;
+use crate::pool::{DatabasePool, Pool};
 use crate::stmt::{query, Column};
 
 /// Generic implementation of KVStoreTransaction for SQL databases
+#[cfg(feature = "mint")]
 pub(crate) async fn kv_read_in_transaction<RM>(
     conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
     primary_namespace: &str,
@@ -46,6 +50,7 @@ where
 }
 
 /// Generic implementation of kv_write for transactions
+#[cfg(feature = "mint")]
 pub(crate) async fn kv_write_in_transaction<RM>(
     conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
     primary_namespace: &str,
@@ -85,6 +90,7 @@ where
 }
 
 /// Generic implementation of kv_remove for transactions
+#[cfg(feature = "mint")]
 pub(crate) async fn kv_remove_in_transaction<RM>(
     conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
     primary_namespace: &str,
@@ -114,6 +120,7 @@ where
 }
 
 /// Generic implementation of kv_list for transactions
+#[cfg(feature = "mint")]
 pub(crate) async fn kv_list_in_transaction<RM>(
     conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
     primary_namespace: &str,
@@ -206,3 +213,73 @@ where
     .map(|row| Ok(column_as_string!(&row[0])))
     .collect::<Result<Vec<_>, Error>>()
 }
+
+/// Generic implementation of kv_write for database (non-transactional, standalone)
+#[cfg(feature = "wallet")]
+pub(crate) async fn kv_write_standalone<C>(
+    conn: &C,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+    key: &str,
+    value: &[u8],
+) -> Result<(), Error>
+where
+    C: crate::database::DatabaseExecutor,
+{
+    // Validate parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+
+    let current_time = unix_time();
+
+    query(
+        r#"
+        INSERT INTO kv_store
+        (primary_namespace, secondary_namespace, key, value, created_time, updated_time)
+        VALUES (:primary_namespace, :secondary_namespace, :key, :value, :created_time, :updated_time)
+        ON CONFLICT(primary_namespace, secondary_namespace, key)
+        DO UPDATE SET
+            value = excluded.value,
+            updated_time = excluded.updated_time
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .bind("key", key.to_owned())
+    .bind("value", value.to_vec())
+    .bind("created_time", current_time as i64)
+    .bind("updated_time", current_time as i64)
+    .execute(conn)
+    .await?;
+
+    Ok(())
+}
+
+/// Generic implementation of kv_remove for database (non-transactional, standalone)
+#[cfg(feature = "wallet")]
+pub(crate) async fn kv_remove_standalone<C>(
+    conn: &C,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+    key: &str,
+) -> Result<(), Error>
+where
+    C: crate::database::DatabaseExecutor,
+{
+    // Validate parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+    query(
+        r#"
+        DELETE FROM kv_store
+        WHERE primary_namespace = :primary_namespace
+        AND secondary_namespace = :secondary_namespace
+        AND key = :key
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .bind("key", key.to_owned())
+    .execute(conn)
+    .await?;
+
+    Ok(())
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 653 - 381
crates/cdk-sql-common/src/wallet/mod.rs


+ 10 - 29
crates/cdk-sqlite/src/wallet/mod.rs

@@ -46,14 +46,10 @@ mod tests {
         let mint_info = MintInfo::new().description("test");
         let mint_url = MintUrl::from_str("https://mint.xyz").unwrap();
 
-        let mut tx = db.begin_db_transaction().await.expect("tx");
-
-        tx.add_mint(mint_url.clone(), Some(mint_info.clone()))
+        db.add_mint(mint_url.clone(), Some(mint_info.clone()))
             .await
             .unwrap();
 
-        tx.commit().await.expect("commit");
-
         let res = db.get_mint(mint_url).await.unwrap();
         assert_eq!(mint_info, res.clone().unwrap());
         assert_eq!("test", &res.unwrap().description.unwrap());
@@ -108,15 +104,11 @@ mod tests {
         let proof_info =
             ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
 
-        let mut tx = db.begin_db_transaction().await.expect("tx");
-
         // Store the proof in the database
-        tx.update_proofs(vec![proof_info.clone()], vec![])
+        db.update_proofs(vec![proof_info.clone()], vec![])
             .await
             .unwrap();
 
-        tx.commit().await.expect("commit");
-
         // Retrieve the proof from the database
         let retrieved_proofs = db
             .get_proofs(
@@ -172,8 +164,6 @@ mod tests {
             PaymentMethod::Custom("custom".to_string()),
         ];
 
-        let mut tx = db.begin_db_transaction().await.expect("begin");
-
         for (i, payment_method) in payment_methods.iter().enumerate() {
             let quote = MintQuote {
                 id: format!("test_quote_{}", i),
@@ -190,15 +180,14 @@ mod tests {
             };
 
             // Store the quote
-            tx.add_mint_quote(quote.clone()).await.unwrap();
+            db.add_mint_quote(quote.clone()).await.unwrap();
 
             // Retrieve and verify
-            let retrieved = tx.get_mint_quote(&quote.id).await.unwrap().unwrap();
+            let retrieved = db.get_mint_quote(&quote.id).await.unwrap().unwrap();
             assert_eq!(retrieved.payment_method, *payment_method);
             assert_eq!(retrieved.amount_issued, Amount::from(0));
             assert_eq!(retrieved.amount_paid, Amount::from(0));
         }
-        tx.commit().await.expect("commit");
     }
 
     #[tokio::test]
@@ -247,9 +236,7 @@ mod tests {
         }
 
         // Store all proofs in the database
-        let mut tx = db.begin_db_transaction().await.unwrap();
-        tx.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
-        tx.commit().await.unwrap();
+        db.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
 
         // Test 1: Retrieve all proofs by their Y values
         let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap();
@@ -375,17 +362,11 @@ mod tests {
             amount_paid: Amount::from(0),
         };
 
-        {
-            let mut tx = db.begin_db_transaction().await.unwrap();
-
-            // Add all quotes to the database
-            tx.add_mint_quote(quote1).await.unwrap();
-            tx.add_mint_quote(quote2.clone()).await.unwrap();
-            tx.add_mint_quote(quote3.clone()).await.unwrap();
-            tx.add_mint_quote(quote4.clone()).await.unwrap();
-
-            tx.commit().await.unwrap();
-        }
+        // Add all quotes to the database
+        db.add_mint_quote(quote1).await.unwrap();
+        db.add_mint_quote(quote2.clone()).await.unwrap();
+        db.add_mint_quote(quote3.clone()).await.unwrap();
+        db.add_mint_quote(quote4.clone()).await.unwrap();
 
         // Get unissued mint quotes
         let unissued_quotes = db.get_unissued_mint_quotes().await.unwrap();

+ 6 - 8
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -286,9 +286,8 @@ impl AuthWallet {
     /// Get Auth Token
     #[instrument(skip(self))]
     pub async fn get_blind_auth_token(&self) -> Result<Option<BlindAuthToken>, Error> {
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
-        let auth_proof = match tx
+        let auth_proof = match self
+            .localstore
             .get_proofs(
                 Some(self.mint_url.clone()),
                 Some(CurrencyUnit::Auth),
@@ -299,8 +298,9 @@ impl AuthWallet {
             .pop()
         {
             Some(proof) => {
-                tx.update_proofs(vec![], vec![proof.proof.y()?]).await?;
-                tx.commit().await?;
+                self.localstore
+                    .update_proofs(vec![], vec![proof.proof.y()?])
+                    .await?;
                 proof.proof.try_into()?
             }
             None => return Ok(None),
@@ -458,9 +458,7 @@ impl AuthWallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
         // Add new proofs to store
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.update_proofs(proof_infos, vec![]).await?;
-        tx.commit().await?;
+        self.localstore.update_proofs(proof_infos, vec![]).await?;
 
         Ok(proofs)
     }

+ 27 - 38
crates/cdk/src/wallet/issue/bolt11.rs

@@ -94,9 +94,7 @@ impl Wallet {
             Some(secret_key),
         );
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.add_mint_quote(quote.clone()).await?;
-        tx.commit().await?;
+        self.localstore.add_mint_quote(quote.clone()).await?;
 
         Ok(quote)
     }
@@ -109,22 +107,18 @@ impl Wallet {
     ) -> Result<MintQuoteBolt11Response<String>, Error> {
         let response = self.client.get_mint_quote_status(quote_id).await?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
-        match tx.get_mint_quote(quote_id).await? {
+        match self.localstore.get_mint_quote(quote_id).await? {
             Some(quote) => {
                 let mut quote = quote;
 
                 quote.state = response.state;
-                tx.add_mint_quote(quote).await?;
+                self.localstore.add_mint_quote(quote).await?;
             }
             None => {
                 tracing::info!("Quote mint {} unknown", quote_id);
             }
         }
 
-        tx.commit().await?;
-
         Ok(response)
     }
 
@@ -233,8 +227,8 @@ impl Wallet {
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        let quote_info = tx
+        let quote_info = self
+            .localstore
             .get_mint_quote(quote_id)
             .await?
             .ok_or(Error::UnknownQuote)?;
@@ -258,7 +252,7 @@ impl Wallet {
 
         let split_target = match amount_split_target {
             SplitTarget::None => {
-                self.determine_split_target_values(&mut tx, amount_mintable, &fee_and_amounts)
+                self.determine_split_target_values(amount_mintable, &fee_and_amounts)
                     .await?
             }
             s => s,
@@ -284,7 +278,8 @@ impl Wallet {
                 );
 
                 // Atomically get the counter range we need
-                let new_counter = tx
+                let new_counter = self
+                    .localstore
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .await?;
 
@@ -311,8 +306,6 @@ impl Wallet {
             request.sign(secret_key.clone())?;
         }
 
-        tx.commit().await?;
-
         let mint_res = self.client.post_mint(request).await?;
 
         let keys = self.load_keyset_keys(active_keyset_id).await?;
@@ -336,11 +329,8 @@ impl Wallet {
             &keys,
         )?;
 
-        // Start new transaction for post-mint operations
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
         // Remove filled quote from store
-        tx.remove_mint_quote(&quote_info.id).await?;
+        self.localstore.remove_mint_quote(&quote_info.id).await?;
 
         let proof_infos = proofs
             .iter()
@@ -355,27 +345,26 @@ impl Wallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
         // Add new proofs to store
-        tx.update_proofs(proof_infos, vec![]).await?;
+        self.localstore.update_proofs(proof_infos, vec![]).await?;
 
         // Add transaction to store
-        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,
-            payment_method: Some(quote_info.payment_method),
-        })
-        .await?;
-
-        tx.commit().await?;
+        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,
+                payment_method: Some(quote_info.payment_method),
+            })
+            .await?;
 
         Ok(proofs)
     }

+ 33 - 44
crates/cdk/src/wallet/issue/bolt12.rs

@@ -71,9 +71,7 @@ impl Wallet {
             Some(secret_key),
         );
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.add_mint_quote(quote.clone()).await?;
-        tx.commit().await?;
+        self.localstore.add_mint_quote(quote.clone()).await?;
 
         Ok(quote)
     }
@@ -92,8 +90,7 @@ impl Wallet {
             .get_keyset_fees_and_amounts_by_id(active_keyset_id)
             .await?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        let quote_info = tx.get_mint_quote(quote_id).await?;
+        let quote_info = self.localstore.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) {
@@ -105,21 +102,20 @@ impl Wallet {
             return Err(Error::UnknownQuote);
         };
 
-        let (mut tx, quote_info, amount) = match amount {
-            Some(amount) => (tx, quote_info, amount),
+        let (quote_info, amount) = match amount {
+            Some(amount) => (quote_info, amount),
             None => {
                 // If an amount it not supplied with check the status of the quote
                 // The mint will tell us how much can be minted
-                tx.commit().await?;
                 let state = self.mint_bolt12_quote_state(quote_id).await?;
 
-                let mut tx = self.localstore.begin_db_transaction().await?;
-                let quote_info = tx
+                let quote_info = self
+                    .localstore
                     .get_mint_quote(quote_id)
                     .await?
                     .ok_or(Error::UnknownQuote)?;
 
-                (tx, quote_info, state.amount_paid - state.amount_issued)
+                (quote_info, state.amount_paid - state.amount_issued)
             }
         };
 
@@ -130,7 +126,7 @@ impl Wallet {
 
         let split_target = match amount_split_target {
             SplitTarget::None => {
-                self.determine_split_target_values(&mut tx, amount, &fee_and_amounts)
+                self.determine_split_target_values(amount, &fee_and_amounts)
                     .await?
             }
             s => s,
@@ -155,7 +151,8 @@ impl Wallet {
                 );
 
                 // Atomically get the counter range we need
-                let new_counter = tx
+                let new_counter = self
+                    .localstore
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .await?;
 
@@ -185,12 +182,8 @@ impl Wallet {
             return Err(Error::SignatureMissingOrInvalid);
         }
 
-        tx.commit().await?;
-
         let mint_res = self.client.post_mint(request).await?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
         let keys = self.load_keyset_keys(active_keyset_id).await?;
 
         // Verify the signature DLEQ is valid
@@ -213,13 +206,14 @@ impl Wallet {
         )?;
 
         // Update quote with issued amount
-        let mut quote_info = tx
+        let mut quote_info = self
+            .localstore
             .get_mint_quote(quote_id)
             .await?
             .ok_or(Error::UnpaidQuote)?;
         quote_info.amount_issued += proofs.total_amount()?;
 
-        tx.add_mint_quote(quote_info.clone()).await?;
+        self.localstore.add_mint_quote(quote_info.clone()).await?;
 
         let proof_infos = proofs
             .iter()
@@ -234,27 +228,26 @@ impl Wallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
         // Add new proofs to store
-        tx.update_proofs(proof_infos, vec![]).await?;
+        self.localstore.update_proofs(proof_infos, vec![]).await?;
 
         // Add transaction to store
-        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,
-            payment_method: Some(quote_info.payment_method),
-        })
-        .await?;
-
-        tx.commit().await?;
+        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,
+                payment_method: Some(quote_info.payment_method),
+            })
+            .await?;
 
         Ok(proofs)
     }
@@ -267,23 +260,19 @@ impl Wallet {
     ) -> Result<MintQuoteBolt12Response<String>, Error> {
         let response = self.client.get_mint_quote_bolt12_status(quote_id).await?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
-        match tx.get_mint_quote(quote_id).await? {
+        match self.localstore.get_mint_quote(quote_id).await? {
             Some(quote) => {
                 let mut quote = quote;
                 quote.amount_issued = response.amount_issued;
                 quote.amount_paid = response.amount_paid;
 
-                tx.add_mint_quote(quote).await?;
+                self.localstore.add_mint_quote(quote).await?;
             }
             None => {
                 tracing::info!("Quote mint {} unknown", quote_id);
             }
         }
 
-        tx.commit().await?;
-
         Ok(response)
     }
 }

+ 23 - 24
crates/cdk/src/wallet/issue/custom.rs

@@ -77,10 +77,8 @@ impl Wallet {
             quote_res.expiry.unwrap_or(0),
             Some(secret_key),
         );
-        let mut tx = self.localstore.begin_db_transaction().await?;
+        self.localstore.add_mint_quote(quote.clone()).await?;
 
-        tx.add_mint_quote(quote.clone()).await?;
-        tx.commit().await?;
         Ok(quote)
     }
 
@@ -94,7 +92,6 @@ impl Wallet {
         spending_conditions: Option<SpendingConditions>,
     ) -> Result<Proofs, Error> {
         self.refresh_keysets().await?;
-        let mut tx = self.localstore.begin_db_transaction().await?;
 
         let quote_info = self
             .localstore
@@ -146,7 +143,8 @@ impl Wallet {
                 );
 
                 // Atomically get the counter range we need
-                let new_counter = tx
+                let new_counter = self
+                    .localstore
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .await?;
 
@@ -197,7 +195,7 @@ impl Wallet {
         )?;
 
         // Remove filled quote from store
-        tx.remove_mint_quote(&quote_info.id).await?;
+        self.localstore.remove_mint_quote(&quote_info.id).await?;
 
         let proof_infos = proofs
             .iter()
@@ -212,26 +210,27 @@ impl Wallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
         // Add new proofs to store
-        tx.update_proofs(proof_infos, vec![]).await?;
+        self.localstore.update_proofs(proof_infos, vec![]).await?;
 
         // Add transaction to store
-        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,
-            payment_method: Some(quote_info.payment_method),
-        })
-        .await?;
-        tx.commit().await?;
+        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,
+                payment_method: Some(quote_info.payment_method),
+            })
+            .await?;
+
         Ok(proofs)
     }
 }

+ 367 - 0
crates/cdk/src/wallet/issue/issue_bolt11.rs

@@ -0,0 +1,367 @@
+use std::collections::HashMap;
+
+use cdk_common::nut04::MintMethodOptions;
+use cdk_common::wallet::{MintQuote, Transaction, TransactionDirection};
+use cdk_common::PaymentMethod;
+use tracing::instrument;
+
+use crate::amount::SplitTarget;
+use crate::dhke::construct_proofs;
+use crate::nuts::nut00::ProofsMethods;
+use crate::nuts::{
+    nut12, MintQuoteBolt11Request, MintQuoteBolt11Response, MintRequest, PreMintSecrets, Proofs,
+    SecretKey, SpendingConditions, State,
+};
+use crate::types::ProofInfo;
+use crate::util::unix_time;
+use crate::wallet::MintQuoteState;
+use crate::{Amount, Error, Wallet};
+
+impl Wallet {
+    /// Mint Quote
+    /// # Synopsis
+    /// ```rust,no_run
+    /// use std::sync::Arc;
+    ///
+    /// use cdk::amount::Amount;
+    /// use cdk::nuts::CurrencyUnit;
+    /// use cdk::wallet::Wallet;
+    /// use cdk_sqlite::wallet::memory;
+    /// use rand::random;
+    ///
+    /// #[tokio::main]
+    /// async fn main() -> anyhow::Result<()> {
+    ///     let seed = random::<[u8; 64]>();
+    ///     let mint_url = "https://fake.thesimplekid.dev";
+    ///     let unit = CurrencyUnit::Sat;
+    ///
+    ///     let localstore = memory::empty().await?;
+    ///     let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None)?;
+    ///     let amount = Amount::from(100);
+    ///
+    ///     let quote = wallet.mint_quote(amount, None).await?;
+    ///     Ok(())
+    /// }
+    /// ```
+    #[instrument(skip(self))]
+    pub async fn mint_quote(
+        &self,
+        amount: Amount,
+        description: Option<String>,
+    ) -> Result<MintQuote, Error> {
+        let mint_info = self.load_mint_info().await?;
+
+        let mint_url = self.mint_url.clone();
+        let unit = self.unit.clone();
+
+        // If we have a description, we check that the mint supports it.
+        if description.is_some() {
+            let settings = mint_info
+                .nuts
+                .nut04
+                .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11)
+                .ok_or(Error::UnsupportedUnit)?;
+
+            match settings.options {
+                Some(MintMethodOptions::Bolt11 { description }) if description => (),
+                _ => return Err(Error::InvoiceDescriptionUnsupported),
+            }
+        }
+
+        let secret_key = SecretKey::generate();
+
+        let request = MintQuoteBolt11Request {
+            amount,
+            unit: unit.clone(),
+            description,
+            pubkey: Some(secret_key.public_key()),
+        };
+
+        let quote_res = self.client.post_mint_quote(request).await?;
+
+        let quote = MintQuote::new(
+            quote_res.quote,
+            mint_url,
+            PaymentMethod::Bolt11,
+            Some(amount),
+            unit,
+            quote_res.request,
+            quote_res.expiry.unwrap_or(0),
+            Some(secret_key),
+        );
+
+        self.localstore.add_mint_quote(quote.clone()).await?;
+
+        Ok(quote)
+    }
+
+    /// Check mint quote status
+    #[instrument(skip(self, quote_id))]
+    pub async fn mint_quote_state(
+        &self,
+        quote_id: &str,
+    ) -> Result<MintQuoteBolt11Response<String>, Error> {
+        let response = self.client.get_mint_quote_status(quote_id).await?;
+
+        match self.localstore.get_mint_quote(quote_id).await? {
+            Some(quote) => {
+                let mut quote = quote;
+
+                quote.state = response.state;
+                self.localstore.add_mint_quote(quote).await?;
+            }
+            None => {
+                tracing::info!("Quote mint {} unknown", quote_id);
+            }
+        }
+
+        Ok(response)
+    }
+
+    /// Check status of pending mint quotes
+    #[instrument(skip(self))]
+    pub async fn check_all_mint_quotes(&self) -> Result<Amount, Error> {
+        let mint_quotes = self.localstore.get_unissued_mint_quotes().await?;
+        let mut total_amount = Amount::ZERO;
+
+        for mint_quote in mint_quotes {
+            match mint_quote.payment_method {
+                PaymentMethod::Bolt11 => {
+                    let mint_quote_response = self.mint_quote_state(&mint_quote.id).await?;
+
+                    if mint_quote_response.state == MintQuoteState::Paid {
+                        let proofs = self
+                            .mint(&mint_quote.id, SplitTarget::default(), None)
+                            .await?;
+                        total_amount += proofs.total_amount()?;
+                    }
+                }
+                PaymentMethod::Bolt12 => {
+                    let mint_quote_response = self.mint_bolt12_quote_state(&mint_quote.id).await?;
+                    if mint_quote_response.amount_paid > mint_quote_response.amount_issued {
+                        let proofs = self
+                            .mint_bolt12(&mint_quote.id, None, SplitTarget::default(), None)
+                            .await?;
+                        total_amount += proofs.total_amount()?;
+                    }
+                }
+                PaymentMethod::Custom(_) => {
+                    tracing::warn!("We cannot check unknown types");
+                }
+            }
+        }
+        Ok(total_amount)
+    }
+
+    /// Get active mint quotes
+    /// Returns mint quotes that are not expired and not yet issued.
+    #[instrument(skip(self))]
+    pub async fn get_active_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let mut mint_quotes = self.localstore.get_mint_quotes().await?;
+        let unix_time = unix_time();
+        mint_quotes.retain(|quote| {
+            quote.mint_url == self.mint_url
+                && quote.state != MintQuoteState::Issued
+                && quote.expiry > unix_time
+        });
+        Ok(mint_quotes)
+    }
+
+    /// Get unissued mint quotes
+    /// Returns bolt11 quotes where nothing has been issued yet (amount_issued = 0) and all bolt12 quotes.
+    /// Includes unpaid bolt11 quotes to allow checking with the mint if they've been paid (wallet state may be outdated).
+    /// Filters out quotes from other mints. Does not filter by expiry time to allow
+    /// checking with the mint if expired quotes can still be minted.
+    #[instrument(skip(self))]
+    pub async fn get_unissued_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
+        let mut pending_quotes = self.localstore.get_unissued_mint_quotes().await?;
+        pending_quotes.retain(|quote| quote.mint_url == self.mint_url);
+        Ok(pending_quotes)
+    }
+
+    /// Mint
+    /// # Synopsis
+    /// ```rust,no_run
+    /// use std::sync::Arc;
+    ///
+    /// use anyhow::Result;
+    /// use cdk::amount::{Amount, SplitTarget};
+    /// use cdk::nuts::nut00::ProofsMethods;
+    /// use cdk::nuts::CurrencyUnit;
+    /// use cdk::wallet::Wallet;
+    /// use cdk_sqlite::wallet::memory;
+    /// use rand::random;
+    ///
+    /// #[tokio::main]
+    /// async fn main() -> Result<()> {
+    ///     let seed = random::<[u8; 64]>();
+    ///     let mint_url = "https://fake.thesimplekid.dev";
+    ///     let unit = CurrencyUnit::Sat;
+    ///
+    ///     let localstore = memory::empty().await?;
+    ///     let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap();
+    ///     let amount = Amount::from(100);
+    ///
+    ///     let quote = wallet.mint_quote(amount, None).await?;
+    ///     let quote_id = quote.id;
+    ///     // To be called after quote request is paid
+    ///     let minted_proofs = wallet.mint(&quote_id, SplitTarget::default(), None).await?;
+    ///     let minted_amount = minted_proofs.total_amount()?;
+    ///
+    ///     Ok(())
+    /// }
+    /// ```
+    #[instrument(skip(self))]
+    pub async fn mint(
+        &self,
+        quote_id: &str,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, Error> {
+        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let fee_and_amounts = self
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
+
+        let quote_info = self
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        if quote_info.payment_method != PaymentMethod::Bolt11 {
+            return Err(Error::UnsupportedPaymentMethod);
+        }
+
+        let amount_mintable = quote_info.amount_mintable();
+
+        if amount_mintable == Amount::ZERO {
+            tracing::debug!("Amount mintable 0.");
+            return Err(Error::AmountUndefined);
+        }
+
+        let unix_time = unix_time();
+
+        if quote_info.expiry > unix_time {
+            tracing::warn!("Attempting to mint with expired quote.");
+        }
+
+        let split_target = match amount_split_target {
+            SplitTarget::None => {
+                self.determine_split_target_values(amount_mintable, &fee_and_amounts)
+                    .await?
+            }
+            s => s,
+        };
+
+        let premint_secrets = match &spending_conditions {
+            Some(spending_conditions) => PreMintSecrets::with_conditions(
+                active_keyset_id,
+                amount_mintable,
+                &split_target,
+                spending_conditions,
+                &fee_and_amounts,
+            )?,
+            None => {
+                let amount_split =
+                    amount_mintable.split_targeted(&split_target, &fee_and_amounts)?;
+                let num_secrets = amount_split.len() as u32;
+
+                tracing::debug!(
+                    "Incrementing keyset {} counter by {}",
+                    active_keyset_id,
+                    num_secrets
+                );
+
+                // Atomically get the counter range we need
+                let new_counter = self
+                    .localstore
+                    .increment_keyset_counter(&active_keyset_id, num_secrets)
+                    .await?;
+
+                let count = new_counter - num_secrets;
+
+                PreMintSecrets::from_seed(
+                    active_keyset_id,
+                    count,
+                    &self.seed,
+                    amount_mintable,
+                    &split_target,
+                    &fee_and_amounts,
+                )?
+            }
+        };
+
+        let mut request = MintRequest {
+            quote: quote_id.to_string(),
+            outputs: premint_secrets.blinded_messages(),
+            signature: None,
+        };
+
+        if let Some(secret_key) = &quote_info.secret_key {
+            request.sign(secret_key.clone())?;
+        }
+
+        let mint_res = self.client.post_mint(request).await?;
+
+        let keys = self.load_keyset_keys(active_keyset_id).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 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) => (),
+                    Err(_) => return Err(Error::CouldNotVerifyDleq),
+                }
+            }
+        }
+
+        let proofs = construct_proofs(
+            mint_res.signatures,
+            premint_secrets.rs(),
+            premint_secrets.secrets(),
+            &keys,
+        )?;
+
+        // Remove filled quote from store
+        self.localstore.remove_mint_quote(&quote_info.id).await?;
+
+        let proof_infos = proofs
+            .iter()
+            .map(|proof| {
+                ProofInfo::new(
+                    proof.clone(),
+                    self.mint_url.clone(),
+                    State::Unspent,
+                    quote_info.unit.clone(),
+                )
+            })
+            .collect::<Result<Vec<ProofInfo>, _>>()?;
+
+        // Add new proofs to store
+        self.localstore.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,
+                payment_method: Some(quote_info.payment_method),
+            })
+            .await?;
+
+        Ok(proofs)
+    }
+}

+ 274 - 0
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -0,0 +1,274 @@
+use std::collections::HashMap;
+
+use cdk_common::nut04::MintMethodOptions;
+use cdk_common::nut25::MintQuoteBolt12Request;
+use cdk_common::wallet::{Transaction, TransactionDirection};
+use cdk_common::{Proofs, SecretKey};
+use tracing::instrument;
+
+use crate::amount::SplitTarget;
+use crate::dhke::construct_proofs;
+use crate::nuts::nut00::ProofsMethods;
+use crate::nuts::{
+    nut12, MintQuoteBolt12Response, MintRequest, PaymentMethod, PreMintSecrets, SpendingConditions,
+    State,
+};
+use crate::types::ProofInfo;
+use crate::util::unix_time;
+use crate::wallet::MintQuote;
+use crate::{Amount, Error, Wallet};
+
+impl Wallet {
+    /// Mint Bolt12
+    #[instrument(skip(self))]
+    pub async fn mint_bolt12_quote(
+        &self,
+        amount: Option<Amount>,
+        description: Option<String>,
+    ) -> Result<MintQuote, Error> {
+        let mint_info = self.load_mint_info().await?;
+
+        let mint_url = self.mint_url.clone();
+        let unit = &self.unit;
+
+        // If we have a description, we check that the mint supports it.
+        if description.is_some() {
+            let mint_method_settings = mint_info
+                .nuts
+                .nut04
+                .get_settings(unit, &crate::nuts::PaymentMethod::Bolt12)
+                .ok_or(Error::UnsupportedUnit)?;
+
+            match mint_method_settings.options {
+                Some(MintMethodOptions::Bolt11 { description }) if description => (),
+                _ => return Err(Error::InvoiceDescriptionUnsupported),
+            }
+        }
+
+        let secret_key = SecretKey::generate();
+
+        let mint_request = MintQuoteBolt12Request {
+            amount,
+            unit: self.unit.clone(),
+            description,
+            pubkey: secret_key.public_key(),
+        };
+
+        let quote_res = self.client.post_mint_bolt12_quote(mint_request).await?;
+
+        let quote = MintQuote::new(
+            quote_res.quote,
+            mint_url,
+            PaymentMethod::Bolt12,
+            amount,
+            unit.clone(),
+            quote_res.request,
+            quote_res.expiry.unwrap_or(0),
+            Some(secret_key),
+        );
+
+        self.localstore.add_mint_quote(quote.clone()).await?;
+
+        Ok(quote)
+    }
+
+    /// Mint bolt12
+    #[instrument(skip(self))]
+    pub async fn mint_bolt12(
+        &self,
+        quote_id: &str,
+        amount: Option<Amount>,
+        amount_split_target: SplitTarget,
+        spending_conditions: Option<SpendingConditions>,
+    ) -> Result<Proofs, Error> {
+        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let fee_and_amounts = self
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
+
+        let quote_info = self.localstore.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) {
+                tracing::info!("Attempting to mint expired quote.");
+            }
+
+            quote.clone()
+        } else {
+            return Err(Error::UnknownQuote);
+        };
+
+        let (quote_info, amount) = match amount {
+            Some(amount) => (quote_info, amount),
+            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 quote_info = self
+                    .localstore
+                    .get_mint_quote(quote_id)
+                    .await?
+                    .ok_or(Error::UnknownQuote)?;
+
+                (quote_info, state.amount_paid - state.amount_issued)
+            }
+        };
+
+        if amount == Amount::ZERO {
+            tracing::error!("Cannot mint zero amount.");
+            return Err(Error::UnpaidQuote);
+        }
+
+        let split_target = match amount_split_target {
+            SplitTarget::None => {
+                self.determine_split_target_values(amount, &fee_and_amounts)
+                    .await?
+            }
+            s => s,
+        };
+
+        let premint_secrets = match &spending_conditions {
+            Some(spending_conditions) => PreMintSecrets::with_conditions(
+                active_keyset_id,
+                amount,
+                &split_target,
+                spending_conditions,
+                &fee_and_amounts,
+            )?,
+            None => {
+                let amount_split = amount.split_targeted(&split_target, &fee_and_amounts)?;
+                let num_secrets = amount_split.len() as u32;
+
+                tracing::debug!(
+                    "Incrementing keyset {} counter by {}",
+                    active_keyset_id,
+                    num_secrets
+                );
+
+                // Atomically get the counter range we need
+                let new_counter = self
+                    .localstore
+                    .increment_keyset_counter(&active_keyset_id, num_secrets)
+                    .await?;
+
+                let count = new_counter - num_secrets;
+
+                PreMintSecrets::from_seed(
+                    active_keyset_id,
+                    count,
+                    &self.seed,
+                    amount,
+                    &split_target,
+                    &fee_and_amounts,
+                )?
+            }
+        };
+
+        let mut request = MintRequest {
+            quote: quote_id.to_string(),
+            outputs: premint_secrets.blinded_messages(),
+            signature: None,
+        };
+
+        if let Some(secret_key) = quote_info.secret_key.clone() {
+            request.sign(secret_key)?;
+        } else {
+            tracing::error!("Signature is required for bolt12.");
+            return Err(Error::SignatureMissingOrInvalid);
+        }
+
+        let mint_res = self.client.post_mint(request).await?;
+
+        let keys = self.load_keyset_keys(active_keyset_id).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 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) => (),
+                    Err(_) => return Err(Error::CouldNotVerifyDleq),
+                }
+            }
+        }
+
+        let proofs = construct_proofs(
+            mint_res.signatures,
+            premint_secrets.rs(),
+            premint_secrets.secrets(),
+            &keys,
+        )?;
+
+        // Update quote with issued amount
+        let mut quote_info = self
+            .localstore
+            .get_mint_quote(quote_id)
+            .await?
+            .ok_or(Error::UnpaidQuote)?;
+        quote_info.amount_issued += proofs.total_amount()?;
+
+        self.localstore.add_mint_quote(quote_info.clone()).await?;
+
+        let proof_infos = proofs
+            .iter()
+            .map(|proof| {
+                ProofInfo::new(
+                    proof.clone(),
+                    self.mint_url.clone(),
+                    State::Unspent,
+                    quote_info.unit.clone(),
+                )
+            })
+            .collect::<Result<Vec<ProofInfo>, _>>()?;
+
+        // Add new proofs to store
+        self.localstore.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,
+                payment_method: Some(quote_info.payment_method),
+            })
+            .await?;
+
+        Ok(proofs)
+    }
+
+    /// Check mint quote status
+    #[instrument(skip(self, quote_id))]
+    pub async fn mint_bolt12_quote_state(
+        &self,
+        quote_id: &str,
+    ) -> 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? {
+            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?;
+            }
+            None => {
+                tracing::info!("Quote mint {} unknown", quote_id);
+            }
+        }
+
+        Ok(response)
+    }
+}

+ 30 - 38
crates/cdk/src/wallet/melt/bolt11.rs

@@ -93,9 +93,7 @@ impl Wallet {
             payment_method: PaymentMethod::Known(KnownMethod::Bolt11),
         };
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.add_melt_quote(quote.clone()).await?;
-        tx.commit().await?;
+        self.localstore.add_melt_quote(quote.clone()).await?;
 
         Ok(quote)
     }
@@ -108,29 +106,25 @@ impl Wallet {
     ) -> 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 tx.get_melt_quote(quote_id).await? {
+        match self.localstore.get_melt_quote(quote_id).await? {
             Some(quote) => {
                 let mut quote = quote;
 
                 if let Err(e) = self
-                    .add_transaction_for_pending_melt(&mut tx, &quote, &response)
+                    .add_transaction_for_pending_melt(&quote, &response)
                     .await
                 {
                     tracing::error!("Failed to add transaction for pending melt: {}", e);
                 }
 
                 quote.state = response.state;
-                tx.add_melt_quote(quote).await?;
+                self.localstore.add_melt_quote(quote).await?;
             }
             None => {
                 tracing::info!("Quote melt {} unknown", quote_id);
             }
         }
 
-        tx.commit().await?;
-
         Ok(response)
     }
 
@@ -150,8 +144,8 @@ impl Wallet {
         metadata: HashMap<String, String>,
     ) -> Result<Melted, Error> {
         let active_keyset_id = self.fetch_active_keyset().await?.id;
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        let mut quote_info = tx
+        let mut quote_info = self
+            .localstore
             .get_melt_quote(quote_id)
             .await?
             .ok_or(Error::UnknownQuote)?;
@@ -173,7 +167,7 @@ impl Wallet {
             .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
-        tx.update_proofs(proofs_info, vec![]).await?;
+        self.localstore.update_proofs(proofs_info, vec![]).await?;
 
         // Calculate change accounting for input fees
         // The mint deducts input fees from available funds before calculating change
@@ -195,7 +189,8 @@ impl Wallet {
             );
 
             // Atomically get the counter range we need
-            let new_counter = tx
+            let new_counter = self
+                .localstore
                 .increment_keyset_counter(&active_keyset_id, num_secrets)
                 .await?;
 
@@ -210,8 +205,6 @@ impl Wallet {
             Some(premint_secrets.blinded_messages()),
         );
 
-        tx.commit().await?;
-
         let melt_response = match quote_info.payment_method {
             cdk_common::PaymentMethod::Known(cdk_common::nut00::KnownMethod::Bolt11) => {
                 self.try_proof_operation_or_reclaim(
@@ -298,37 +291,36 @@ impl Wallet {
             None => Vec::new(),
         };
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
         quote_info.state = cdk_common::MeltQuoteState::Paid;
 
         let payment_request = quote_info.request.clone();
         let payment_method = quote_info.payment_method.clone();
-        tx.add_melt_quote(quote_info).await?;
+        self.localstore.add_melt_quote(quote_info).await?;
 
         let deleted_ys = proofs.ys()?;
 
-        tx.update_proofs(change_proof_infos, deleted_ys).await?;
+        self.localstore
+            .update_proofs(change_proof_infos, deleted_ys)
+            .await?;
 
         // Add transaction to store
-        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(payment_request),
-            payment_proof: payment_preimage,
-            payment_method: Some(payment_method),
-        })
-        .await?;
-
-        tx.commit().await?;
+        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(payment_request),
+                payment_proof: payment_preimage,
+                payment_method: Some(payment_method),
+            })
+            .await?;
 
         Ok(melted)
     }

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

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

+ 1 - 3
crates/cdk/src/wallet/melt/custom.rs

@@ -42,9 +42,7 @@ impl Wallet {
             payment_preimage: quote_res.payment_preimage,
             payment_method: PaymentMethod::Custom(method.to_string()),
         };
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.add_melt_quote(quote.clone()).await?;
-        tx.commit().await?;
+        self.localstore.add_melt_quote(quote.clone()).await?;
 
         Ok(quote)
     }

+ 519 - 0
crates/cdk/src/wallet/melt/melt_bolt11.rs

@@ -0,0 +1,519 @@
+use std::collections::HashMap;
+use std::str::FromStr;
+
+use cdk_common::amount::SplitTarget;
+use cdk_common::wallet::{Transaction, TransactionDirection};
+use cdk_common::PaymentMethod;
+use lightning_invoice::Bolt11Invoice;
+use tracing::instrument;
+
+use crate::amount::to_unit;
+use crate::dhke::construct_proofs;
+use crate::nuts::nut00::ProofsMethods;
+use crate::nuts::{
+    CurrencyUnit, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltRequest,
+    PreMintSecrets, Proofs, State,
+};
+use crate::types::{Melted, ProofInfo};
+use crate::util::unix_time;
+use crate::wallet::send::split_proofs_for_send;
+use crate::wallet::MeltQuote;
+use crate::{ensure_cdk, Amount, Error, Wallet};
+
+impl Wallet {
+    /// Melt Quote
+    /// # Synopsis
+    /// ```rust,no_run
+    ///  use std::sync::Arc;
+    ///
+    ///  use cdk_sqlite::wallet::memory;
+    ///  use cdk::nuts::CurrencyUnit;
+    ///  use cdk::wallet::Wallet;
+    ///  use rand::random;
+    ///
+    /// #[tokio::main]
+    /// async fn main() -> anyhow::Result<()> {
+    ///     let seed = random::<[u8; 64]>();
+    ///     let mint_url = "https://fake.thesimplekid.dev";
+    ///     let unit = CurrencyUnit::Sat;
+    ///
+    ///     let localstore = memory::empty().await?;
+    ///     let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap();
+    ///     let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string();
+    ///     let quote = wallet.melt_quote(bolt11, None).await?;
+    ///
+    ///     Ok(())
+    /// }
+    /// ```
+    #[instrument(skip(self, request))]
+    pub async fn melt_quote(
+        &self,
+        request: String,
+        options: Option<MeltOptions>,
+    ) -> Result<MeltQuote, Error> {
+        let invoice = Bolt11Invoice::from_str(&request)?;
+
+        let quote_request = MeltQuoteBolt11Request {
+            request: invoice.clone(),
+            unit: self.unit.clone(),
+            options,
+        };
+
+        let quote_res = self.client.post_melt_quote(quote_request).await?;
+
+        if self.unit == CurrencyUnit::Msat || self.unit == CurrencyUnit::Sat {
+            let amount_msat = options
+                .map(|opt| opt.amount_msat().into())
+                .or_else(|| invoice.amount_milli_satoshis())
+                .ok_or(Error::InvoiceAmountUndefined)?;
+
+            let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit)?;
+
+            if quote_res.amount != amount_quote_unit {
+                tracing::warn!(
+                    "Mint returned incorrect quote amount. Expected {}, got {}",
+                    amount_quote_unit,
+                    quote_res.amount
+                );
+                return Err(Error::IncorrectQuoteAmount);
+            }
+        }
+
+        let quote = MeltQuote {
+            id: quote_res.quote,
+            amount: quote_res.amount,
+            request,
+            unit: self.unit.clone(),
+            fee_reserve: quote_res.fee_reserve,
+            state: quote_res.state,
+            expiry: quote_res.expiry,
+            payment_preimage: quote_res.payment_preimage,
+            payment_method: PaymentMethod::Bolt11,
+        };
+
+        self.localstore.add_melt_quote(quote.clone()).await?;
+
+        Ok(quote)
+    }
+
+    /// Melt quote status
+    #[instrument(skip(self, quote_id))]
+    pub async fn melt_quote_status(
+        &self,
+        quote_id: &str,
+    ) -> Result<MeltQuoteBolt11Response<String>, Error> {
+        let response = self.client.get_melt_quote_status(quote_id).await?;
+
+        match self.localstore.get_melt_quote(quote_id).await? {
+            Some(quote) => {
+                let mut quote = quote;
+
+                if let Err(e) = self
+                    .add_transaction_for_pending_melt(&quote, &response)
+                    .await
+                {
+                    tracing::error!("Failed to add transaction for pending melt: {}", e);
+                }
+
+                quote.state = response.state;
+                self.localstore.add_melt_quote(quote).await?;
+            }
+            None => {
+                tracing::info!("Quote melt {} unknown", quote_id);
+            }
+        }
+
+        Ok(response)
+    }
+
+    /// 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
+    }
+
+    /// Melt specific proofs
+    #[instrument(skip(self, proofs))]
+    pub async fn melt_proofs_with_metadata(
+        &self,
+        quote_id: &str,
+        proofs: Proofs,
+        metadata: HashMap<String, String>,
+    ) -> Result<Melted, Error> {
+        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let mut quote_info = self
+            .localstore
+            .get_melt_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        ensure_cdk!(
+            quote_info.expiry.gt(&unix_time()),
+            Error::ExpiredQuote(quote_info.expiry, unix_time())
+        );
+
+        let proofs_total = proofs.total_amount()?;
+        if proofs_total < quote_info.amount + quote_info.fee_reserve {
+            return Err(Error::InsufficientFunds);
+        }
+
+        // Since the proofs may be external (not in our database), add them first
+        let proofs_info = proofs
+            .clone()
+            .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, vec![]).await?;
+
+        // Calculate change accounting for input fees
+        // The mint deducts input fees from available funds before calculating change
+        let input_fee = self.get_proofs_fee(&proofs).await?.total;
+        let change_amount = proofs_total - quote_info.amount - input_fee;
+
+        let premint_secrets = if change_amount <= Amount::ZERO {
+            PreMintSecrets::new(active_keyset_id)
+        } else {
+            // TODO: consolidate this calculation with from_seed_blank into a shared function
+            // Calculate how many secrets will be needed using the same logic as from_seed_blank
+            let num_secrets =
+                ((u64::from(change_amount) as f64).log2().ceil() as u64).max(1) as u32;
+
+            tracing::debug!(
+                "Incrementing keyset {} counter by {}",
+                active_keyset_id,
+                num_secrets
+            );
+
+            // Atomically get the counter range we need
+            let new_counter = self
+                .localstore
+                .increment_keyset_counter(&active_keyset_id, num_secrets)
+                .await?;
+
+            let count = new_counter - num_secrets;
+
+            PreMintSecrets::from_seed_blank(active_keyset_id, count, &self.seed, change_amount)?
+        };
+
+        let request = MeltRequest::new(
+            quote_id.to_string(),
+            proofs.clone(),
+            Some(premint_secrets.blinded_messages()),
+        );
+
+        let melt_response = match quote_info.payment_method {
+            cdk_common::PaymentMethod::Bolt11 => {
+                self.try_proof_operation_or_reclaim(
+                    request.inputs().clone(),
+                    self.client.post_melt(request),
+                )
+                .await?
+            }
+            cdk_common::PaymentMethod::Bolt12 => {
+                self.try_proof_operation_or_reclaim(
+                    request.inputs().clone(),
+                    self.client.post_melt_bolt12(request),
+                )
+                .await?
+            }
+            cdk_common::PaymentMethod::Custom(_) => {
+                return Err(Error::UnsupportedPaymentMethod);
+            }
+        };
+
+        let active_keys = self.load_keyset_keys(active_keyset_id).await?;
+
+        let change_proofs = match melt_response.change {
+            Some(change) => {
+                let num_change_proof = change.len();
+
+                let num_change_proof = match (
+                    premint_secrets.len() < num_change_proof,
+                    premint_secrets.secrets().len() < num_change_proof,
+                ) {
+                    (true, _) | (_, true) => {
+                        tracing::error!("Mismatch in change promises to change");
+                        premint_secrets.len()
+                    }
+                    _ => num_change_proof,
+                };
+
+                Some(construct_proofs(
+                    change,
+                    premint_secrets.rs()[..num_change_proof].to_vec(),
+                    premint_secrets.secrets()[..num_change_proof].to_vec(),
+                    &active_keys,
+                )?)
+            }
+            None => None,
+        };
+
+        let payment_preimage = melt_response.payment_preimage.clone();
+
+        let melted = Melted::from_proofs(
+            melt_response.state,
+            melt_response.payment_preimage,
+            quote_info.amount,
+            proofs.clone(),
+            change_proofs.clone(),
+        )?;
+
+        let change_proof_infos = match change_proofs {
+            Some(change_proofs) => {
+                tracing::debug!(
+                    "Change amount returned from melt: {}",
+                    change_proofs.total_amount()?
+                );
+
+                change_proofs
+                    .into_iter()
+                    .map(|proof| {
+                        ProofInfo::new(
+                            proof,
+                            self.mint_url.clone(),
+                            State::Unspent,
+                            quote_info.unit.clone(),
+                        )
+                    })
+                    .collect::<Result<Vec<ProofInfo>, _>>()?
+            }
+            None => Vec::new(),
+        };
+
+        quote_info.state = cdk_common::MeltQuoteState::Paid;
+
+        let payment_request = quote_info.request.clone();
+        let payment_method = quote_info.payment_method.clone();
+        self.localstore.add_melt_quote(quote_info).await?;
+
+        let deleted_ys = proofs.ys()?;
+
+        self.localstore
+            .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(payment_request),
+                payment_proof: payment_preimage,
+                payment_method: Some(payment_method),
+            })
+            .await?;
+
+        Ok(melted)
+    }
+
+    /// Melt
+    /// # Synopsis
+    /// ```rust, no_run
+    ///  use std::sync::Arc;
+    ///
+    ///  use cdk_sqlite::wallet::memory;
+    ///  use cdk::nuts::CurrencyUnit;
+    ///  use cdk::wallet::Wallet;
+    ///  use rand::random;
+    ///
+    /// #[tokio::main]
+    /// async fn main() -> anyhow::Result<()> {
+    ///  let seed = random::<[u8; 64]>();
+    ///  let mint_url = "https://fake.thesimplekid.dev";
+    ///  let unit = CurrencyUnit::Sat;
+    ///
+    ///  let localstore = memory::empty().await?;
+    ///  let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap();
+    ///  let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string();
+    ///  let quote = wallet.melt_quote(bolt11, None).await?;
+    ///  let quote_id = quote.id;
+    ///
+    ///  let _ = wallet.melt(&quote_id).await?;
+    ///
+    ///  Ok(())
+    /// }
+    #[instrument(skip(self))]
+    pub async fn melt(&self, quote_id: &str) -> Result<Melted, Error> {
+        self.melt_with_metadata(quote_id, HashMap::new()).await
+    }
+
+    /// Melt with additional metadata to be saved locally with the transaction
+    /// # Synopsis
+    /// ```rust, no_run
+    ///  use std::sync::Arc;
+    ///
+    ///  use cdk_sqlite::wallet::memory;
+    ///  use cdk::nuts::CurrencyUnit;
+    ///  use cdk::wallet::Wallet;
+    ///  use rand::random;
+    ///
+    /// #[tokio::main]
+    /// async fn main() -> anyhow::Result<()> {
+    ///  let seed = random::<[u8; 64]>();
+    ///  let mint_url = "https://fake.thesimplekid.dev";
+    ///  let unit = CurrencyUnit::Sat;
+    ///
+    ///  let localstore = memory::empty().await?;
+    ///  let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), seed, None).unwrap();
+    ///  let bolt11 = "lnbc100n1pnvpufspp5djn8hrq49r8cghwye9kqw752qjncwyfnrprhprpqk43mwcy4yfsqdq5g9kxy7fqd9h8vmmfvdjscqzzsxqyz5vqsp5uhpjt36rj75pl7jq2sshaukzfkt7uulj456s4mh7uy7l6vx7lvxs9qxpqysgqedwz08acmqwtk8g4vkwm2w78suwt2qyzz6jkkwcgrjm3r3hs6fskyhvud4fan3keru7emjm8ygqpcrwtlmhfjfmer3afs5hhwamgr4cqtactdq".to_string();
+    ///  let quote = wallet.melt_quote(bolt11, None).await?;
+    ///  let quote_id = quote.id;
+    ///
+    ///  let mut metadata = std::collections::HashMap::new();
+    ///  metadata.insert("my key".to_string(), "my value".to_string());
+    ///
+    ///  let _ = wallet.melt_with_metadata(&quote_id, metadata).await?;
+    ///
+    ///  Ok(())
+    /// }
+    #[instrument(skip(self))]
+    pub async fn melt_with_metadata(
+        &self,
+        quote_id: &str,
+        metadata: HashMap<String, String>,
+    ) -> Result<Melted, Error> {
+        let quote_info = self
+            .localstore
+            .get_melt_quote(quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        ensure_cdk!(
+            quote_info.expiry.gt(&unix_time()),
+            Error::ExpiredQuote(quote_info.expiry, unix_time())
+        );
+
+        let inputs_needed_amount = quote_info.amount + quote_info.fee_reserve;
+
+        let active_keyset_ids = self
+            .get_mint_keysets()
+            .await?
+            .into_iter()
+            .map(|k| k.id)
+            .collect();
+        let keyset_fees_and_amounts = self.get_keyset_fees_and_amounts().await?;
+
+        let available_proofs = self.get_unspent_proofs().await?;
+
+        // Two-step proof selection for melt:
+        // Step 1: Try to select proofs that exactly match inputs_needed_amount.
+        //         If successful, no swap is required and we avoid paying swap fees.
+        // Step 2: If exact match not possible, we need to swap to get optimal denominations.
+        //         In this case, we must select more proofs to cover the additional swap fees.
+        {
+            let input_proofs = Wallet::select_proofs(
+                inputs_needed_amount,
+                available_proofs.clone(),
+                &active_keyset_ids,
+                &keyset_fees_and_amounts,
+                true,
+            )?;
+            let proofs_total = input_proofs.total_amount()?;
+
+            // If exact match, use proofs directly without swap
+            if proofs_total == inputs_needed_amount {
+                return self
+                    .melt_proofs_with_metadata(quote_id, input_proofs, metadata)
+                    .await;
+            }
+        }
+
+        let active_keyset_id = self.get_active_keyset().await?.id;
+        let fee_and_amounts = self
+            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
+            .await?;
+
+        // Calculate optimal denomination split and the fee for those proofs
+        // First estimate based on inputs_needed_amount to get target_fee
+        let initial_split = inputs_needed_amount.split(&fee_and_amounts);
+        let target_fee = self
+            .get_proofs_fee_by_count(
+                vec![(active_keyset_id, initial_split.len() as u64)]
+                    .into_iter()
+                    .collect(),
+            )
+            .await?
+            .total;
+
+        // Since we could not select the correct inputs amount needed for melting,
+        // we select again this time including the amount we will now have to pay as a fee for the swap.
+        let inputs_total_needed = inputs_needed_amount + target_fee;
+
+        // Recalculate target amounts based on the actual total we need (including fee)
+        let target_amounts = inputs_total_needed.split(&fee_and_amounts);
+        let input_proofs = Wallet::select_proofs(
+            inputs_total_needed,
+            available_proofs,
+            &active_keyset_ids,
+            &keyset_fees_and_amounts,
+            true,
+        )?;
+        let proofs_total = input_proofs.total_amount()?;
+
+        // Need to swap to get exact denominations
+        tracing::debug!(
+            "Proofs total {} != inputs needed {}, swapping to get exact amount",
+            proofs_total,
+            inputs_total_needed
+        );
+
+        let keyset_fees: HashMap<cdk_common::Id, u64> = keyset_fees_and_amounts
+            .iter()
+            .map(|(key, values)| (*key, values.fee()))
+            .collect();
+
+        let split_result = split_proofs_for_send(
+            input_proofs,
+            &target_amounts,
+            inputs_total_needed,
+            target_fee,
+            &keyset_fees,
+            false,
+            false,
+        )?;
+
+        let mut final_proofs = split_result.proofs_to_send;
+
+        if !split_result.proofs_to_swap.is_empty() {
+            let swap_amount = inputs_total_needed
+                .checked_sub(final_proofs.total_amount()?)
+                .ok_or(Error::AmountOverflow)?;
+
+            tracing::debug!(
+                "Swapping {} proofs to get {} sats (swap fee: {} sats)",
+                split_result.proofs_to_swap.len(),
+                swap_amount,
+                split_result.swap_fee
+            );
+
+            if let Some(swapped) = self
+                .try_proof_operation_or_reclaim(
+                    split_result.proofs_to_swap.clone(),
+                    self.swap(
+                        Some(swap_amount),
+                        SplitTarget::None,
+                        split_result.proofs_to_swap,
+                        None,
+                        false, // fees already accounted for in inputs_total_needed
+                    ),
+                )
+                .await?
+            {
+                final_proofs.extend(swapped);
+            }
+        }
+
+        self.melt_proofs_with_metadata(quote_id, final_proofs, metadata)
+            .await
+    }
+}

+ 98 - 0
crates/cdk/src/wallet/melt/melt_bolt12.rs

@@ -0,0 +1,98 @@
+//! Melt BOLT12
+//!
+//! Implementation of melt functionality for BOLT12 offers
+
+use std::str::FromStr;
+
+use cdk_common::amount::amount_for_offer;
+use cdk_common::wallet::MeltQuote;
+use cdk_common::PaymentMethod;
+use lightning::offers::offer::Offer;
+use tracing::instrument;
+
+use crate::amount::to_unit;
+use crate::nuts::{CurrencyUnit, MeltOptions, MeltQuoteBolt11Response, MeltQuoteBolt12Request};
+use crate::{Error, Wallet};
+
+impl Wallet {
+    /// Melt Quote for BOLT12 offer
+    #[instrument(skip(self, request))]
+    pub async fn melt_bolt12_quote(
+        &self,
+        request: String,
+        options: Option<MeltOptions>,
+    ) -> Result<MeltQuote, Error> {
+        let quote_request = MeltQuoteBolt12Request {
+            request: request.clone(),
+            unit: self.unit.clone(),
+            options,
+        };
+
+        let quote_res = self.client.post_melt_bolt12_quote(quote_request).await?;
+
+        if self.unit == CurrencyUnit::Sat || self.unit == CurrencyUnit::Msat {
+            let offer = Offer::from_str(&request).map_err(|_| Error::Bolt12parse)?;
+            // Get amount from offer or options
+            let amount_msat = options
+                .map(|opt| opt.amount_msat())
+                .or_else(|| amount_for_offer(&offer, &CurrencyUnit::Msat).ok())
+                .ok_or(Error::AmountUndefined)?;
+            let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit)?;
+
+            if quote_res.amount != amount_quote_unit {
+                tracing::warn!(
+                    "Mint returned incorrect quote amount. Expected {}, got {}",
+                    amount_quote_unit,
+                    quote_res.amount
+                );
+                return Err(Error::IncorrectQuoteAmount);
+            }
+        }
+
+        let quote = MeltQuote {
+            id: quote_res.quote,
+            amount: quote_res.amount,
+            request,
+            unit: self.unit.clone(),
+            fee_reserve: quote_res.fee_reserve,
+            state: quote_res.state,
+            expiry: quote_res.expiry,
+            payment_preimage: quote_res.payment_preimage,
+            payment_method: PaymentMethod::Bolt12,
+        };
+
+        self.localstore.add_melt_quote(quote.clone()).await?;
+
+        Ok(quote)
+    }
+
+    /// BOLT12 melt quote status
+    #[instrument(skip(self, quote_id))]
+    pub async fn melt_bolt12_quote_status(
+        &self,
+        quote_id: &str,
+    ) -> 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? {
+            Some(quote) => {
+                let mut quote = quote;
+
+                if let Err(e) = self
+                    .add_transaction_for_pending_melt(&quote, &response)
+                    .await
+                {
+                    tracing::error!("Failed to add transaction for pending melt: {}", e);
+                }
+
+                quote.state = response.state;
+                self.localstore.add_melt_quote(quote).await?;
+            }
+            None => {
+                tracing::info!("Quote melt {} unknown", quote_id);
+            }
+        }
+
+        Ok(response)
+    }
+}

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

@@ -1,6 +1,5 @@
 use std::collections::HashMap;
 
-use cdk_common::database::DynWalletDatabaseTransaction;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection};
 use cdk_common::{
@@ -54,7 +53,6 @@ impl Wallet {
 
     pub(crate) async fn add_transaction_for_pending_melt(
         &self,
-        tx: &mut DynWalletDatabaseTransaction,
         quote: &MeltQuote,
         response: &MeltQuoteBolt11Response<String>,
     ) -> Result<(), Error> {
@@ -67,30 +65,31 @@ impl Wallet {
             );
             if response.state == MeltQuoteState::Paid {
                 let pending_proofs = self
-                    .get_proofs_with(Some(tx), Some(vec![State::Pending]), None)
+                    .get_proofs_with(Some(vec![State::Pending]), None)
                     .await?;
                 let proofs_total = pending_proofs.total_amount().unwrap_or_default();
                 let change_total = response.change_amount().unwrap_or_default();
 
-                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(),
-                    payment_method: Some(quote.payment_method.clone()),
-                })
-                .await?;
+                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(),
+                        payment_method: Some(quote.payment_method.clone()),
+                    })
+                    .await?;
             }
         }
         Ok(())

+ 7 - 16
crates/cdk/src/wallet/mint_metadata_cache.rs

@@ -462,18 +462,9 @@ impl MintMetadataCache {
             versions.insert(storage_id, metadata.status.version);
         }
 
-        let mut tx = if let Ok(ok) = storage
-            .begin_db_transaction()
-            .await
-            .inspect_err(|err| tracing::warn!("Could not begin database transaction: {err}"))
-        {
-            ok
-        } else {
-            return;
-        };
-
         // Save mint info
-        tx.add_mint(mint_url.clone(), Some(metadata.mint_info.clone()))
+        storage
+            .add_mint(mint_url.clone(), Some(metadata.mint_info.clone()))
             .await
             .inspect_err(|e| tracing::warn!("Failed to save mint info for {}: {}", mint_url, e))
             .ok();
@@ -482,7 +473,8 @@ impl MintMetadataCache {
         let keysets: Vec<_> = metadata.keysets.values().map(|ks| (**ks).clone()).collect();
 
         if !keysets.is_empty() {
-            tx.add_mint_keysets(mint_url.clone(), keysets)
+            storage
+                .add_mint_keysets(mint_url.clone(), keysets)
                 .await
                 .inspect_err(|e| tracing::warn!("Failed to save keysets for {}: {}", mint_url, e))
                 .ok();
@@ -492,7 +484,7 @@ impl MintMetadataCache {
         for (keyset_id, keys) in &metadata.keys {
             if let Some(keyset_info) = metadata.keysets.get(keyset_id) {
                 // Check if keys already exist in database to avoid duplicate insertion
-                if tx.get_keys(keyset_id).await.ok().flatten().is_some() {
+                if storage.get_keys(keyset_id).await.ok().flatten().is_some() {
                     tracing::trace!(
                         "Keys for keyset {} already in database, skipping insert",
                         keyset_id
@@ -507,7 +499,8 @@ impl MintMetadataCache {
                     keys: (**keys).clone(),
                 };
 
-                tx.add_keys(keyset)
+                storage
+                    .add_keys(keyset)
                     .await
                     .inspect_err(|e| {
                         tracing::warn!(
@@ -520,8 +513,6 @@ impl MintMetadataCache {
                     .ok();
             }
         }
-
-        let _ = tx.commit().await.ok();
     }
 
     /// Fetch fresh metadata from mint HTTP API and update cache

+ 13 - 16
crates/cdk/src/wallet/mod.rs

@@ -8,7 +8,7 @@ use std::sync::Arc;
 use std::time::Duration;
 
 use cdk_common::amount::FeeAndAmounts;
-use cdk_common::database::{self, DynWalletDatabaseTransaction, WalletDatabase};
+use cdk_common::database::{self, WalletDatabase};
 use cdk_common::parking_lot::RwLock;
 use cdk_common::subscription::WalletParams;
 use getrandom::getrandom;
@@ -280,10 +280,9 @@ 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
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.update_mint_url(self.mint_url.clone(), new_mint_url.clone())
+        self.localstore
+            .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;
@@ -377,14 +376,13 @@ impl Wallet {
     }
 
     /// Get amounts needed to refill proof state
-    #[instrument(skip(self, tx))]
+    #[instrument(skip(self))]
     pub(crate) async fn amounts_needed_for_state_target(
         &self,
-        tx: &mut DynWalletDatabaseTransaction,
         fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Vec<Amount>, Error> {
         let unspent_proofs = self
-            .get_proofs_with(Some(tx), Some(vec![State::Unspent]), None)
+            .get_proofs_with(Some(vec![State::Unspent]), None)
             .await?;
 
         let amounts_count: HashMap<u64, u64> =
@@ -415,15 +413,14 @@ impl Wallet {
     }
 
     /// Determine [`SplitTarget`] for amount based on state
-    #[instrument(skip(self, tx))]
+    #[instrument(skip(self))]
     async fn determine_split_target_values(
         &self,
-        tx: &mut DynWalletDatabaseTransaction,
         change_amount: Amount,
         fee_and_amounts: &FeeAndAmounts,
     ) -> Result<SplitTarget, Error> {
         let mut amounts_needed_refill = self
-            .amounts_needed_for_state_target(tx, fee_and_amounts)
+            .amounts_needed_for_state_target(fee_and_amounts)
             .await?;
 
         amounts_needed_refill.sort();
@@ -552,9 +549,9 @@ impl Wallet {
                     })
                     .collect::<Result<Vec<ProofInfo>, _>>()?;
 
-                let mut tx = self.localstore.begin_db_transaction().await?;
-                tx.update_proofs(unspent_proofs, vec![]).await?;
-                tx.commit().await?;
+                self.localstore
+                    .update_proofs(unspent_proofs, vec![])
+                    .await?;
 
                 empty_batch = 0;
                 start_counter += 100;
@@ -563,9 +560,9 @@ impl Wallet {
             // Set counter to highest found + 1 to avoid reusing any counter values
             // that already have signatures at the mint
             if let Some(highest) = highest_counter {
-                let mut tx = self.localstore.begin_db_transaction().await?;
-                tx.increment_keyset_counter(&keyset.id, highest + 1).await?;
-                tx.commit().await?;
+                self.localstore
+                    .increment_keyset_counter(&keyset.id, highest + 1)
+                    .await?;
                 tracing::debug!(
                     "Set keyset {} counter to {} after restore",
                     keyset.id,

+ 21 - 40
crates/cdk/src/wallet/proofs.rs

@@ -1,7 +1,6 @@
 use std::collections::{HashMap, HashSet};
 
 use cdk_common::amount::KeysetFeeAndAmounts;
-use cdk_common::database::DynWalletDatabaseTransaction;
 use cdk_common::wallet::TransactionId;
 use cdk_common::Id;
 use tracing::instrument;
@@ -19,40 +18,38 @@ impl Wallet {
     /// Get unspent proofs for mint
     #[instrument(skip(self))]
     pub async fn get_unspent_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(None, Some(vec![State::Unspent]), None)
-            .await
+        self.get_proofs_with(Some(vec![State::Unspent]), None).await
     }
 
     /// Get pending [`Proofs`]
     #[instrument(skip(self))]
     pub async fn get_pending_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(None, Some(vec![State::Pending]), None)
-            .await
+        self.get_proofs_with(Some(vec![State::Pending]), None).await
     }
 
     /// Get reserved [`Proofs`]
     #[instrument(skip(self))]
     pub async fn get_reserved_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(None, Some(vec![State::Reserved]), None)
+        self.get_proofs_with(Some(vec![State::Reserved]), None)
             .await
     }
 
     /// Get pending spent [`Proofs`]
     #[instrument(skip(self))]
     pub async fn get_pending_spent_proofs(&self) -> Result<Proofs, Error> {
-        self.get_proofs_with(None, Some(vec![State::PendingSpent]), None)
+        self.get_proofs_with(Some(vec![State::PendingSpent]), None)
             .await
     }
 
     /// Get this wallet's [Proofs] that match the args
     pub async fn get_proofs_with(
         &self,
-        tx: Option<&mut DynWalletDatabaseTransaction>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Proofs, Error> {
-        Ok(if let Some(tx) = tx {
-            tx.get_proofs(
+        Ok(self
+            .localstore
+            .get_proofs(
                 Some(self.mint_url.clone()),
                 Some(self.unit.clone()),
                 state,
@@ -61,28 +58,16 @@ impl Wallet {
             .await?
             .into_iter()
             .map(|p| p.proof)
-            .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()
-        })
+            .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> {
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.update_proofs_state(ys, State::Unspent).await?;
-        Ok(tx.commit().await?)
+        self.localstore
+            .update_proofs_state(ys, State::Unspent)
+            .await?;
+        Ok(())
     }
 
     /// Reclaim unspent proofs
@@ -108,14 +93,13 @@ impl Wallet {
 
         self.swap(None, SplitTarget::default(), unspent, None, false)
             .await?;
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        let _ = tx
+        let _ = self
+            .localstore
             .remove_transaction(transaction_id)
             .await
             .inspect_err(|err| {
                 tracing::warn!("Failed to remove transaction: {:?}", err);
             });
-        tx.commit().await?;
 
         Ok(())
     }
@@ -137,9 +121,7 @@ impl Wallet {
             })
             .collect();
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.update_proofs(vec![], spent_ys).await?;
-        tx.commit().await?;
+        self.localstore.update_proofs(vec![], spent_ys).await?;
 
         Ok(spendable.states)
     }
@@ -183,13 +165,12 @@ impl Wallet {
 
         let amount = Amount::try_sum(pending_proofs.iter().map(|p| p.proof.amount))?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.update_proofs(
-            vec![],
-            non_pending_proofs.into_iter().map(|p| p.y).collect(),
-        )
-        .await?;
-        tx.commit().await?;
+        self.localstore
+            .update_proofs(
+                vec![],
+                non_pending_proofs.into_iter().map(|p| p.y).collect(),
+            )
+            .await?;
 
         balance += amount;
 

+ 30 - 31
crates/cdk/src/wallet/receive.rs

@@ -120,12 +120,12 @@ impl Wallet {
             .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.update_proofs(proofs_info.clone(), vec![]).await?;
+        self.localstore
+            .update_proofs(proofs_info.clone(), vec![])
+            .await?;
 
         let mut pre_swap = self
             .create_swap(
-                tx,
                 active_keyset_id,
                 &fee_and_amounts,
                 None,
@@ -151,10 +151,9 @@ impl Wallet {
                 tracing::error!("Failed to post swap request: {}", err);
 
                 // Remove the pending proofs we added since the swap failed
-                let mut tx = self.localstore.begin_db_transaction().await?;
-                tx.update_proofs(vec![], proofs_info.into_iter().map(|p| p.y).collect())
+                self.localstore
+                    .update_proofs(vec![], proofs_info.into_iter().map(|p| p.y).collect())
                     .await?;
-                tx.commit().await?;
 
                 return Err(err);
             }
@@ -168,8 +167,8 @@ impl Wallet {
             &keys,
         )?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-        tx.increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32)
+        self.localstore
+            .increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32)
             .await?;
 
         let total_amount = recv_proofs.total_amount()?;
@@ -179,31 +178,31 @@ impl Wallet {
             .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
-        tx.update_proofs(
-            recv_proof_infos,
-            proofs_info.into_iter().map(|p| p.y).collect(),
-        )
-        .await?;
+        self.localstore
+            .update_proofs(
+                recv_proof_infos,
+                proofs_info.into_iter().map(|p| p.y).collect(),
+            )
+            .await?;
 
         // Add transaction to store
-        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,
-            payment_method: None,
-        })
-        .await?;
-
-        tx.commit().await?;
+        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,
+                payment_method: None,
+            })
+            .await?;
 
         Ok(total_amount)
     }

+ 9 - 12
crates/cdk/src/wallet/reclaim.rs

@@ -45,8 +45,6 @@ impl Wallet {
             .await?
             .states;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
         for (state, unspent) in proofs
             .into_iter()
             .zip(statuses)
@@ -56,18 +54,17 @@ impl Wallet {
                 acc
             })
         {
-            tx.update_proofs_state(
-                unspent
-                    .iter()
-                    .map(|x| x.y())
-                    .collect::<Result<Vec<_>, _>>()?,
-                state,
-            )
-            .await?;
+            self.localstore
+                .update_proofs_state(
+                    unspent
+                        .iter()
+                        .map(|x| x.y())
+                        .collect::<Result<Vec<_>, _>>()?,
+                    state,
+                )
+                .await?;
         }
 
-        tx.commit().await?;
-
         Ok(())
     }
 

+ 27 - 34
crates/cdk/src/wallet/send.rs

@@ -46,7 +46,6 @@ impl Wallet {
         // Get available proofs matching conditions
         let mut available_proofs = self
             .get_proofs_with(
-                None,
                 Some(vec![State::Unspent]),
                 opts.conditions.clone().map(|c| vec![c]),
             )
@@ -184,10 +183,9 @@ impl Wallet {
         tracing::debug!("Send amounts: {:?}", send_amounts);
         tracing::debug!("Send fee: {:?}", send_fee);
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
         // Reserve proofs
-        tx.update_proofs_state(proofs.ys()?, State::Reserved)
+        self.localstore
+            .update_proofs_state(proofs.ys()?, State::Reserved)
             .await?;
 
         // Check if proofs are exact send amount (and does not exceed max_proofs)
@@ -218,8 +216,6 @@ impl Wallet {
             is_exact_or_offline,
         )?;
 
-        tx.commit().await?;
-
         // Return prepared send
         Ok(PreparedSend {
             wallet: self.clone(),
@@ -340,13 +336,10 @@ impl PreparedSend {
             return Err(Error::InsufficientFunds);
         }
 
-        let mut tx = self.wallet.localstore.begin_db_transaction().await?;
-
         // Check if proofs are reserved or unspent
         let sendable_proof_ys = self
             .wallet
             .get_proofs_with(
-                Some(&mut tx),
                 Some(vec![State::Reserved, State::Unspent]),
                 self.options.conditions.clone().map(|c| vec![c]),
             )
@@ -367,7 +360,9 @@ impl PreparedSend {
             proofs_to_send.ys()?
         );
 
-        tx.update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
+        self.wallet
+            .localstore
+            .update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
             .await?;
 
         // Include token memo
@@ -375,24 +370,24 @@ impl PreparedSend {
         let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None });
 
         // Add transaction to store
-        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,
-            payment_method: None,
-        })
-        .await?;
-
-        tx.commit().await?;
+        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,
+                payment_method: None,
+            })
+            .await?;
 
         // Create and return token
         Ok(Token::new(
@@ -407,12 +402,10 @@ impl PreparedSend {
     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_proofs_with(Some(&mut tx), Some(vec![State::Reserved]), None)
+            .get_proofs_with(Some(vec![State::Reserved]), None)
             .await?
             .ys()?;
 
@@ -425,11 +418,11 @@ impl PreparedSend {
             return Err(Error::UnexpectedProofState);
         }
 
-        tx.update_proofs_state(self.proofs().ys()?, State::Unspent)
+        self.wallet
+            .localstore
+            .update_proofs_state(self.proofs().ys()?, State::Unspent)
             .await?;
 
-        tx.commit().await?;
-
         Ok(())
     }
 }

+ 10 - 13
crates/cdk/src/wallet/swap.rs

@@ -1,5 +1,4 @@
 use cdk_common::amount::FeeAndAmounts;
-use cdk_common::database::DynWalletDatabaseTransaction;
 use cdk_common::nut02::KeySetInfosMethods;
 use cdk_common::Id;
 use tracing::instrument;
@@ -37,7 +36,6 @@ impl Wallet {
 
         let pre_swap = self
             .create_swap(
-                self.localstore.begin_db_transaction().await?,
                 active_keyset_id,
                 &fee_and_amounts,
                 amount,
@@ -134,10 +132,9 @@ impl Wallet {
             .map(|proof| proof.y())
             .collect::<Result<Vec<PublicKey>, _>>()?;
 
-        let mut tx = self.localstore.begin_db_transaction().await?;
-
-        tx.update_proofs(added_proofs, deleted_ys).await?;
-        tx.commit().await?;
+        self.localstore
+            .update_proofs(added_proofs, deleted_ys)
+            .await?;
 
         Ok(send_proofs)
     }
@@ -199,11 +196,10 @@ impl Wallet {
     }
 
     /// Create Swap Payload
-    #[instrument(skip(self, proofs, tx))]
+    #[instrument(skip(self, proofs))]
     #[allow(clippy::too_many_arguments)]
     pub async fn create_swap(
         &self,
-        mut tx: DynWalletDatabaseTransaction,
         active_keyset_id: Id,
         fee_and_amounts: &FeeAndAmounts,
         amount: Option<Amount>,
@@ -219,7 +215,9 @@ impl Wallet {
         let proofs_total = proofs.total_amount()?;
 
         let ys: Vec<PublicKey> = proofs.ys()?;
-        tx.update_proofs_state(ys, State::Reserved).await?;
+        self.localstore
+            .update_proofs_state(ys, State::Reserved)
+            .await?;
 
         let total_to_subtract = amount
             .unwrap_or(Amount::ZERO)
@@ -257,7 +255,7 @@ impl Wallet {
         // else use state refill
         let change_split_target = match amount_split_target {
             SplitTarget::None => {
-                self.determine_split_target_values(&mut tx, change_amount, fee_and_amounts)
+                self.determine_split_target_values(change_amount, fee_and_amounts)
                     .await?
             }
             s => s,
@@ -294,7 +292,8 @@ impl Wallet {
                 total_secrets_needed
             );
 
-            let new_counter = tx
+            let new_counter = self
+                .localstore
                 .increment_keyset_counter(&active_keyset_id, total_secrets_needed)
                 .await?;
 
@@ -363,8 +362,6 @@ impl Wallet {
 
         let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages());
 
-        tx.commit().await?;
-
         Ok(PreSwap {
             pre_mint_secrets: desired_messages,
             swap_request,

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor