Просмотр исходного кода

Add database transaction support to wallet database (#1204)

Implements explicit transaction management for wallet database operations to
ensure atomicity and proper isolation. This refactoring separates read-only
operations from write operations that require transactional context.

Key changes:
- Split WalletDatabase trait into read-only operations and DatabaseTransaction
  trait for write operations
- Move DbTransactionFinalizer trait from mint module to shared database module
- Add begin_db_transaction method to create explicit transaction scopes
- Implement transaction-scoped locking for concurrent access (SELECT FOR
  UPDATE)
- Update all wallet operations (mint, melt, swap, send, receive, reclaim) to
  use transactions
- Add FFI bindings for transaction support with WalletDatabaseTransaction trait
- Fix tokio runtime initialization for non-async contexts in cdk-common task
  spawning
- Configure tokio as platform-specific dependency (full features for native,
  workspace for wasm32)
C 1 месяц назад
Родитель
Сommit
3133d27d5d
33 измененных файлов с 3614 добавлено и 2271 удалено
  1. 6 0
      .gitignore
  2. 5 1
      crates/cdk-common/Cargo.toml
  3. 1 14
      crates/cdk-common/src/database/mint/mod.rs
  4. 23 7
      crates/cdk-common/src/database/mod.rs
  5. 102 45
      crates/cdk-common/src/database/wallet.rs
  6. 18 1
      crates/cdk-common/src/task.rs
  7. 2 0
      crates/cdk-ffi/Cargo.toml
  8. 1165 211
      crates/cdk-ffi/src/database.rs
  9. 1 1
      crates/cdk-ffi/src/multi_mint_wallet.rs
  10. 54 334
      crates/cdk-ffi/src/postgres.rs
  11. 26 302
      crates/cdk-ffi/src/sqlite.rs
  12. 1 1
      crates/cdk-ffi/src/wallet.rs
  13. 35 12
      crates/cdk-ffi/tests/README.md
  14. 373 32
      crates/cdk-ffi/tests/test_transactions.py
  15. 3 5
      crates/cdk-integration-tests/tests/fake_auth.rs
  16. 552 471
      crates/cdk-redb/src/wallet/mod.rs
  17. 2 2
      crates/cdk-sql-common/src/mint/mod.rs
  18. 832 548
      crates/cdk-sql-common/src/wallet/mod.rs
  19. 2 0
      crates/cdk-sqlite/src/lib.rs
  20. 18 5
      crates/cdk-sqlite/src/wallet/mod.rs
  21. 18 19
      crates/cdk/src/wallet/auth/auth_wallet.rs
  22. 44 32
      crates/cdk/src/wallet/issue/issue_bolt11.rs
  23. 52 35
      crates/cdk/src/wallet/issue/issue_bolt12.rs
  24. 39 31
      crates/cdk/src/wallet/melt/melt_bolt11.rs
  25. 10 4
      crates/cdk/src/wallet/melt/melt_bolt12.rs
  26. 24 21
      crates/cdk/src/wallet/melt/mod.rs
  27. 16 7
      crates/cdk/src/wallet/mint_metadata_cache.rs
  28. 19 13
      crates/cdk/src/wallet/mod.rs
  29. 45 26
      crates/cdk/src/wallet/proofs.rs
  30. 41 28
      crates/cdk/src/wallet/receive.rs
  31. 12 9
      crates/cdk/src/wallet/reclaim.rs
  32. 39 26
      crates/cdk/src/wallet/send.rs
  33. 34 28
      crates/cdk/src/wallet/swap.rs

+ 6 - 0
.gitignore

@@ -18,3 +18,9 @@ Cargo.lock
 mutants.out/
 mutants.out/
 mutants-*.log
 mutants-*.log
 .mutants.lock
 .mutants.lock
+
+
+# Python ffi
+__pycache__
+libcdk_ffi*
+cdk_ffi.py

+ 5 - 1
crates/cdk-common/Cargo.toml

@@ -40,12 +40,16 @@ anyhow.workspace = true
 serde_json.workspace = true
 serde_json.workspace = true
 serde_with.workspace = true
 serde_with.workspace = true
 web-time.workspace = true
 web-time.workspace = true
-tokio.workspace = true
 parking_lot = "0.12.5"
 parking_lot = "0.12.5"
 
 
+
+[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
+tokio = { version = "1", default-features = false, features = ["rt", "rt-multi-thread", "macros", "test-util", "sync"] }
+
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 uuid = { workspace = true, features = ["js"], optional = true }
 uuid = { workspace = true, features = ["js"], optional = true }
 getrandom = { version = "0.2", features = ["js"] }
 getrandom = { version = "0.2", features = ["js"] }
+tokio.workspace = true
 wasm-bindgen = "0.2"
 wasm-bindgen = "0.2"
 wasm-bindgen-futures = "0.4"
 wasm-bindgen-futures = "0.4"
 
 

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

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

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

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

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

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

+ 18 - 1
crates/cdk-common/src/task.rs

@@ -1,9 +1,13 @@
 //! Thin wrapper for spawn and spawn_local for native and wasm.
 //! Thin wrapper for spawn and spawn_local for native and wasm.
 
 
 use std::future::Future;
 use std::future::Future;
+use std::sync::OnceLock;
 
 
 use tokio::task::JoinHandle;
 use tokio::task::JoinHandle;
 
 
+#[cfg(not(target_arch = "wasm32"))]
+static GLOBAL_RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
+
 /// Spawns a new asynchronous task returning nothing
 /// Spawns a new asynchronous task returning nothing
 #[cfg(not(target_arch = "wasm32"))]
 #[cfg(not(target_arch = "wasm32"))]
 pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
 pub fn spawn<F>(future: F) -> JoinHandle<F::Output>
@@ -11,7 +15,20 @@ where
     F: Future + Send + 'static,
     F: Future + Send + 'static,
     F::Output: Send + 'static,
     F::Output: Send + 'static,
 {
 {
-    tokio::spawn(future)
+    if let Ok(handle) = tokio::runtime::Handle::try_current() {
+        handle.spawn(future)
+    } else {
+        // No runtime on this thread (FFI/regular sync context):
+        // use (or lazily create) a global runtime and spawn on it.
+        GLOBAL_RUNTIME
+            .get_or_init(|| {
+                tokio::runtime::Builder::new_multi_thread()
+                    .enable_all()
+                    .build()
+                    .expect("failed to build global Tokio runtime")
+            })
+            .spawn(future)
+    }
 }
 }
 
 
 /// Spawns a new asynchronous task returning nothing
 /// Spawns a new asynchronous task returning nothing

+ 2 - 0
crates/cdk-ffi/Cargo.toml

@@ -21,6 +21,7 @@ cdk-postgres = { workspace = true, optional = true }
 futures = { workspace = true }
 futures = { workspace = true }
 once_cell = { workspace = true }
 once_cell = { workspace = true }
 rand = { workspace = true }
 rand = { workspace = true }
+cdk-sql-common = { workspace = true }
 serde = { workspace = true, features = ["derive", "rc"] }
 serde = { workspace = true, features = ["derive", "rc"] }
 serde_json = { workspace = true }
 serde_json = { workspace = true }
 thiserror = { workspace = true }
 thiserror = { workspace = true }
@@ -28,6 +29,7 @@ tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
 uniffi = { version = "0.29", features = ["cli", "tokio"] }
 uniffi = { version = "0.29", features = ["cli", "tokio"] }
 url = { workspace = true }
 url = { workspace = true }
 uuid = { workspace = true, features = ["v4"] }
 uuid = { workspace = true, features = ["v4"] }
+cdk-common.workspace = true
 
 
 
 
 [features]
 [features]

+ 1165 - 211
crates/cdk-ffi/src/database.rs

@@ -1,9 +1,17 @@
 //! FFI Database bindings
 //! FFI Database bindings
 
 
 use std::collections::HashMap;
 use std::collections::HashMap;
+use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
-use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
+use cdk_common::database::{
+    DbTransactionFinalizer, DynWalletDatabaseTransaction, WalletDatabase as CdkWalletDatabase,
+    WalletDatabaseTransaction as CdkWalletDatabaseTransaction,
+};
+use cdk_common::task::spawn;
+use cdk_sql_common::pool::DatabasePool;
+use cdk_sql_common::SQLWalletDatabase;
+use tokio::sync::Mutex;
 
 
 use crate::error::FfiError;
 use crate::error::FfiError;
 #[cfg(feature = "postgres")]
 #[cfg(feature = "postgres")]
@@ -11,11 +19,91 @@ use crate::postgres::WalletPostgresDatabase;
 use crate::sqlite::WalletSqliteDatabase;
 use crate::sqlite::WalletSqliteDatabase;
 use crate::types::*;
 use crate::types::*;
 
 
-/// FFI-compatible trait for wallet database operations
-/// This trait mirrors the CDK WalletDatabase trait but uses FFI-compatible types
-#[uniffi::export(with_foreign)]
+/// FFI-compatible wallet database trait (read-only operations + begin_db_transaction)
+/// This trait mirrors the CDK WalletDatabase trait structure
+#[uniffi::export]
 #[async_trait::async_trait]
 #[async_trait::async_trait]
 pub trait WalletDatabase: Send + Sync {
 pub trait WalletDatabase: Send + Sync {
+    /// Begin a database transaction
+    async fn begin_db_transaction(&self)
+        -> Result<Arc<WalletDatabaseTransactionWrapper>, FfiError>;
+
+    /// Get mint from storage
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError>;
+
+    /// Get all mints from storage
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError>;
+
+    /// Get mint keysets for mint url
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, FfiError>;
+
+    /// Get mint keyset by id
+    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError>;
+
+    /// Get mint quote from storage
+    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError>;
+
+    /// Get mint quotes from storage
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError>;
+
+    /// Get melt quote from storage
+    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError>;
+
+    /// Get melt quotes from storage
+    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError>;
+
+    /// Get Keys from storage
+    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError>;
+
+    /// Get proofs from storage
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, FfiError>;
+
+    /// Get proofs by Y values
+    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError>;
+
+    /// Get balance efficiently using SQL aggregation
+    async fn get_balance(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+    ) -> Result<u64, FfiError>;
+
+    /// Get transaction from storage
+    async fn get_transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, FfiError>;
+
+    /// List transactions from storage
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, FfiError>;
+}
+
+/// FFI-compatible transaction trait for wallet database write operations
+/// This trait mirrors the CDK WalletDatabaseTransaction trait but uses FFI-compatible types
+#[uniffi::export(with_foreign)]
+#[async_trait::async_trait]
+pub trait WalletDatabaseTransaction: Send + Sync {
+    /// Commit the transaction
+    async fn commit(self: Arc<Self>) -> Result<(), FfiError>;
+
+    /// Rollback the transaction
+    async fn rollback(self: Arc<Self>) -> Result<(), FfiError>;
+
     // Mint Management
     // Mint Management
     /// Add Mint to storage
     /// Add Mint to storage
     async fn add_mint(
     async fn add_mint(
@@ -27,12 +115,6 @@ pub trait WalletDatabase: Send + Sync {
     /// Remove Mint from storage
     /// Remove Mint from storage
     async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError>;
     async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError>;
 
 
-    /// Get mint from storage
-    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError>;
-
-    /// Get all mints from storage
-    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError>;
-
     /// Update mint url
     /// Update mint url
     async fn update_mint_url(
     async fn update_mint_url(
         &self,
         &self,
@@ -48,37 +130,28 @@ pub trait WalletDatabase: Send + Sync {
         keysets: Vec<KeySetInfo>,
         keysets: Vec<KeySetInfo>,
     ) -> Result<(), FfiError>;
     ) -> Result<(), FfiError>;
 
 
-    /// Get mint keysets for mint url
-    async fn get_mint_keysets(
-        &self,
-        mint_url: MintUrl,
-    ) -> Result<Option<Vec<KeySetInfo>>, FfiError>;
-
-    /// Get mint keyset by id
+    /// Get mint keyset by id (transaction-scoped)
     async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError>;
     async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError>;
 
 
-    // Mint Quote Management
-    /// Add mint quote to storage
-    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError>;
+    /// Get Keys from storage (transaction-scoped)
+    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError>;
 
 
-    /// Get mint quote from storage
+    // Mint Quote Management
+    /// Get mint quote from storage (transaction-scoped, with locking)
     async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError>;
     async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError>;
 
 
-    /// Get mint quotes from storage
-    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError>;
+    /// Add mint quote to storage
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError>;
 
 
     /// Remove mint quote from storage
     /// Remove mint quote from storage
     async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError>;
     async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError>;
 
 
     // Melt Quote Management
     // Melt Quote Management
-    /// Add melt quote to storage
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError>;
-
-    /// Get melt quote from storage
+    /// Get melt quote from storage (transaction-scoped)
     async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError>;
     async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError>;
 
 
-    /// Get melt quotes from storage
-    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError>;
+    /// Add melt quote to storage
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError>;
 
 
     /// Remove melt quote from storage
     /// Remove melt quote from storage
     async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError>;
     async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError>;
@@ -87,21 +160,11 @@ pub trait WalletDatabase: Send + Sync {
     /// Add Keys to storage
     /// Add Keys to storage
     async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError>;
     async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError>;
 
 
-    /// Get Keys from storage
-    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError>;
-
     /// Remove Keys from storage
     /// Remove Keys from storage
     async fn remove_keys(&self, id: Id) -> Result<(), FfiError>;
     async fn remove_keys(&self, id: Id) -> Result<(), FfiError>;
 
 
     // Proof Management
     // Proof Management
-    /// Update the proofs in storage by adding new proofs or removing proofs by their Y value
-    async fn update_proofs(
-        &self,
-        added: Vec<ProofInfo>,
-        removed_ys: Vec<PublicKey>,
-    ) -> Result<(), FfiError>;
-
-    /// Get proofs from storage
+    /// Get proofs from storage (transaction-scoped, with locking)
     async fn get_proofs(
     async fn get_proofs(
         &self,
         &self,
         mint_url: Option<MintUrl>,
         mint_url: Option<MintUrl>,
@@ -110,16 +173,12 @@ pub trait WalletDatabase: Send + Sync {
         spending_conditions: Option<Vec<SpendingConditions>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Vec<ProofInfo>, FfiError>;
     ) -> Result<Vec<ProofInfo>, FfiError>;
 
 
-    /// Get proofs by Y values
-    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError>;
-
-    /// Get balance efficiently using SQL aggregation
-    async fn get_balance(
+    /// Update the proofs in storage by adding new proofs or removing proofs by their Y value
+    async fn update_proofs(
         &self,
         &self,
-        mint_url: Option<MintUrl>,
-        unit: Option<CurrencyUnit>,
-        state: Option<Vec<ProofState>>,
-    ) -> Result<u64, FfiError>;
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), FfiError>;
 
 
     /// Update proofs state in storage
     /// Update proofs state in storage
     async fn update_proofs_state(
     async fn update_proofs_state(
@@ -136,22 +195,159 @@ pub trait WalletDatabase: Send + Sync {
     /// Add transaction to storage
     /// Add transaction to storage
     async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError>;
     async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError>;
 
 
-    /// Get transaction from storage
-    async fn get_transaction(
+    /// Remove transaction from storage
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError>;
+}
+
+/// Wallet database transaction wrapper
+#[derive(uniffi::Object)]
+pub struct WalletDatabaseTransactionWrapper {
+    inner: Arc<dyn WalletDatabaseTransaction>,
+}
+
+#[uniffi::export(async_runtime = "tokio")]
+impl WalletDatabaseTransactionWrapper {
+    /// Commit the transaction
+    pub async fn commit(&self) -> Result<(), FfiError> {
+        self.inner.clone().commit().await
+    }
+
+    /// Rollback the transaction
+    pub async fn rollback(&self) -> Result<(), FfiError> {
+        self.inner.clone().rollback().await
+    }
+
+    /// Add Mint to storage
+    pub async fn add_mint(
         &self,
         &self,
-        transaction_id: TransactionId,
-    ) -> Result<Option<Transaction>, FfiError>;
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), FfiError> {
+        self.inner.add_mint(mint_url, mint_info).await
+    }
 
 
-    /// List transactions from storage
-    async fn list_transactions(
+    /// Remove Mint from storage
+    pub async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
+        self.inner.remove_mint(mint_url).await
+    }
+
+    /// Update mint url
+    pub 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
+    }
+
+    /// Add mint keyset to storage
+    pub async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), FfiError> {
+        self.inner.add_mint_keysets(mint_url, keysets).await
+    }
+
+    /// Get mint keyset by id (transaction-scoped)
+    pub async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
+        self.inner.get_keyset_by_id(keyset_id).await
+    }
+
+    /// Get Keys from storage (transaction-scoped)
+    pub async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
+        self.inner.get_keys(id).await
+    }
+
+    /// Get mint quote from storage (transaction-scoped, with locking)
+    pub async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
+        self.inner.get_mint_quote(quote_id).await
+    }
+
+    /// Add mint quote to storage
+    pub async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
+        self.inner.add_mint_quote(quote).await
+    }
+
+    /// Remove mint quote from storage
+    pub async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner.remove_mint_quote(quote_id).await
+    }
+
+    /// Get melt quote from storage (transaction-scoped)
+    pub async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
+        self.inner.get_melt_quote(quote_id).await
+    }
+
+    /// Add melt quote to storage
+    pub async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
+        self.inner.add_melt_quote(quote).await
+    }
+
+    /// Remove melt quote from storage
+    pub async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        self.inner.remove_melt_quote(quote_id).await
+    }
+
+    /// Add Keys to storage
+    pub async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
+        self.inner.add_keys(keyset).await
+    }
+
+    /// Remove Keys from storage
+    pub async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
+        self.inner.remove_keys(id).await
+    }
+
+    /// Get proofs from storage (transaction-scoped, with locking)
+    pub async fn get_proofs(
         &self,
         &self,
         mint_url: Option<MintUrl>,
         mint_url: Option<MintUrl>,
-        direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
         unit: Option<CurrencyUnit>,
-    ) -> Result<Vec<Transaction>, FfiError>;
+        state: Option<Vec<ProofState>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, FfiError> {
+        self.inner
+            .get_proofs(mint_url, unit, state, spending_conditions)
+            .await
+    }
+
+    /// Update the proofs in storage by adding new proofs or removing proofs by their Y value
+    pub async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), FfiError> {
+        self.inner.update_proofs(added, removed_ys).await
+    }
+
+    /// Update proofs state in storage
+    pub async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: ProofState,
+    ) -> Result<(), FfiError> {
+        self.inner.update_proofs_state(ys, state).await
+    }
+
+    /// Increment Keyset counter
+    pub async fn increment_keyset_counter(
+        &self,
+        keyset_id: Id,
+        count: u32,
+    ) -> Result<u32, FfiError> {
+        self.inner.increment_keyset_counter(keyset_id, count).await
+    }
+
+    /// Add transaction to storage
+    pub async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
+        self.inner.add_transaction(transaction).await
+    }
 
 
     /// Remove transaction from storage
     /// Remove transaction from storage
-    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError>;
+    pub async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
+        self.inner.remove_transaction(transaction_id).await
+    }
 }
 }
 
 
 /// Internal bridge trait to convert from the FFI trait to the CDK database trait
 /// Internal bridge trait to convert from the FFI trait to the CDK database trait
@@ -177,27 +373,6 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     type Err = cdk::cdk_database::Error;
     type Err = cdk::cdk_database::Error;
 
 
     // Mint Management
     // Mint Management
-    async fn add_mint(
-        &self,
-        mint_url: cdk::mint_url::MintUrl,
-        mint_info: Option<cdk::nuts::MintInfo>,
-    ) -> Result<(), Self::Err> {
-        let ffi_mint_url = mint_url.into();
-        let ffi_mint_info = mint_info.map(Into::into);
-        self.ffi_db
-            .add_mint(ffi_mint_url, ffi_mint_info)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
-    async fn remove_mint(&self, mint_url: cdk::mint_url::MintUrl) -> Result<(), Self::Err> {
-        let ffi_mint_url = mint_url.into();
-        self.ffi_db
-            .remove_mint(ffi_mint_url)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     async fn get_mint(
     async fn get_mint(
         &self,
         &self,
         mint_url: cdk::mint_url::MintUrl,
         mint_url: cdk::mint_url::MintUrl,
@@ -230,38 +405,11 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
         Ok(cdk_result)
         Ok(cdk_result)
     }
     }
 
 
-    async fn update_mint_url(
-        &self,
-        old_mint_url: cdk::mint_url::MintUrl,
-        new_mint_url: cdk::mint_url::MintUrl,
-    ) -> Result<(), Self::Err> {
-        let ffi_old_mint_url = old_mint_url.into();
-        let ffi_new_mint_url = new_mint_url.into();
-        self.ffi_db
-            .update_mint_url(ffi_old_mint_url, ffi_new_mint_url)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     // Keyset Management
     // Keyset Management
-    async fn add_mint_keysets(
+    async fn get_mint_keysets(
         &self,
         &self,
         mint_url: cdk::mint_url::MintUrl,
         mint_url: cdk::mint_url::MintUrl,
-        keysets: Vec<cdk::nuts::KeySetInfo>,
-    ) -> Result<(), Self::Err> {
-        let ffi_mint_url = mint_url.into();
-        let ffi_keysets: Vec<KeySetInfo> = keysets.into_iter().map(Into::into).collect();
-
-        self.ffi_db
-            .add_mint_keysets(ffi_mint_url, ffi_keysets)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
-    async fn get_mint_keysets(
-        &self,
-        mint_url: cdk::mint_url::MintUrl,
-    ) -> Result<Option<Vec<cdk::nuts::KeySetInfo>>, Self::Err> {
+    ) -> Result<Option<Vec<cdk::nuts::KeySetInfo>>, Self::Err> {
         let ffi_mint_url = mint_url.into();
         let ffi_mint_url = mint_url.into();
         let result = self
         let result = self
             .ffi_db
             .ffi_db
@@ -285,14 +433,6 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     }
     }
 
 
     // Mint Quote Management
     // Mint Quote Management
-    async fn add_mint_quote(&self, quote: cdk::wallet::MintQuote) -> Result<(), Self::Err> {
-        let ffi_quote = quote.into();
-        self.ffi_db
-            .add_mint_quote(ffi_quote)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     async fn get_mint_quote(
     async fn get_mint_quote(
         &self,
         &self,
         quote_id: &str,
         quote_id: &str,
@@ -325,22 +465,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
             .collect::<Result<Vec<_>, _>>()?)
             .collect::<Result<Vec<_>, _>>()?)
     }
     }
 
 
-    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
-        self.ffi_db
-            .remove_mint_quote(quote_id.to_string())
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     // Melt Quote Management
     // Melt Quote Management
-    async fn add_melt_quote(&self, quote: cdk::wallet::MeltQuote) -> Result<(), Self::Err> {
-        let ffi_quote = quote.into();
-        self.ffi_db
-            .add_melt_quote(ffi_quote)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     async fn get_melt_quote(
     async fn get_melt_quote(
         &self,
         &self,
         quote_id: &str,
         quote_id: &str,
@@ -373,22 +498,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
             .collect::<Result<Vec<_>, _>>()?)
             .collect::<Result<Vec<_>, _>>()?)
     }
     }
 
 
-    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
-        self.ffi_db
-            .remove_melt_quote(quote_id.to_string())
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     // Keys Management
     // Keys Management
-    async fn add_keys(&self, keyset: cdk::nuts::KeySet) -> Result<(), Self::Err> {
-        let ffi_keyset: KeySet = keyset.into();
-        self.ffi_db
-            .add_keys(ffi_keyset)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     async fn get_keys(&self, id: &cdk::nuts::Id) -> Result<Option<cdk::nuts::Keys>, Self::Err> {
     async fn get_keys(&self, id: &cdk::nuts::Id) -> Result<Option<cdk::nuts::Keys>, Self::Err> {
         let ffi_id: Id = (*id).into();
         let ffi_id: Id = (*id).into();
         let result = self
         let result = self
@@ -407,29 +517,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
             .transpose()
             .transpose()
     }
     }
 
 
-    async fn remove_keys(&self, id: &cdk::nuts::Id) -> Result<(), Self::Err> {
-        let ffi_id = (*id).into();
-        self.ffi_db
-            .remove_keys(ffi_id)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     // Proof Management
     // Proof Management
-    async fn update_proofs(
-        &self,
-        added: Vec<cdk::types::ProofInfo>,
-        removed_ys: Vec<cdk::nuts::PublicKey>,
-    ) -> Result<(), Self::Err> {
-        let ffi_added: Vec<ProofInfo> = added.into_iter().map(Into::into).collect();
-        let ffi_removed_ys: Vec<PublicKey> = removed_ys.into_iter().map(Into::into).collect();
-
-        self.ffi_db
-            .update_proofs(ffi_added, ffi_removed_ys)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     async fn get_proofs(
     async fn get_proofs(
         &self,
         &self,
         mint_url: Option<cdk::mint_url::MintUrl>,
         mint_url: Option<cdk::mint_url::MintUrl>,
@@ -537,45 +625,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
             .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
             .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
     }
     }
 
 
-    async fn update_proofs_state(
-        &self,
-        ys: Vec<cdk::nuts::PublicKey>,
-        state: cdk::nuts::State,
-    ) -> Result<(), Self::Err> {
-        let ffi_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
-        let ffi_state = state.into();
-
-        self.ffi_db
-            .update_proofs_state(ffi_ys, ffi_state)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
-    // Keyset Counter Management
-    async fn increment_keyset_counter(
-        &self,
-        keyset_id: &cdk::nuts::Id,
-        count: u32,
-    ) -> Result<u32, Self::Err> {
-        let ffi_id = (*keyset_id).into();
-        self.ffi_db
-            .increment_keyset_counter(ffi_id, count)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     // Transaction Management
     // Transaction Management
-    async fn add_transaction(
-        &self,
-        transaction: cdk::wallet::types::Transaction,
-    ) -> Result<(), Self::Err> {
-        let ffi_transaction = transaction.into();
-        self.ffi_db
-            .add_transaction(ffi_transaction)
-            .await
-            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
-    }
-
     async fn get_transaction(
     async fn get_transaction(
         &self,
         &self,
         transaction_id: cdk::wallet::types::TransactionId,
         transaction_id: cdk::wallet::types::TransactionId,
@@ -616,20 +666,924 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
             .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
             .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
     }
     }
 
 
-    async fn remove_transaction(
+    async fn begin_db_transaction(
         &self,
         &self,
+    ) -> Result<Box<dyn CdkWalletDatabaseTransaction<Self::Err> + Send + Sync>, Self::Err> {
+        let ffi_tx = self
+            .ffi_db
+            .begin_db_transaction()
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        Ok(Box::new(WalletDatabaseTransactionBridge {
+            ffi_tx,
+            is_finalized: false,
+        }))
+    }
+}
+
+/// Transaction bridge for FFI wallet database
+struct WalletDatabaseTransactionBridge {
+    ffi_tx: Arc<WalletDatabaseTransactionWrapper>,
+    is_finalized: bool,
+}
+
+#[async_trait::async_trait]
+impl CdkWalletDatabaseTransaction<cdk::cdk_database::Error> for WalletDatabaseTransactionBridge {
+    async fn add_mint(
+        &mut self,
+        mint_url: cdk::mint_url::MintUrl,
+        mint_info: Option<cdk::nuts::MintInfo>,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_mint_url = mint_url.into();
+        let ffi_mint_info = mint_info.map(Into::into);
+        self.ffi_tx
+            .add_mint(ffi_mint_url, ffi_mint_info)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_mint(
+        &mut self,
+        mint_url: cdk::mint_url::MintUrl,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_mint_url = mint_url.into();
+        self.ffi_tx
+            .remove_mint(ffi_mint_url)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn update_mint_url(
+        &mut self,
+        old_mint_url: cdk::mint_url::MintUrl,
+        new_mint_url: cdk::mint_url::MintUrl,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_old_mint_url = old_mint_url.into();
+        let ffi_new_mint_url = new_mint_url.into();
+        self.ffi_tx
+            .update_mint_url(ffi_old_mint_url, ffi_new_mint_url)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_mint_keysets(
+        &mut self,
+        mint_url: cdk::mint_url::MintUrl,
+        keysets: Vec<cdk::nuts::KeySetInfo>,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_mint_url = mint_url.into();
+        let ffi_keysets: Vec<KeySetInfo> = keysets.into_iter().map(Into::into).collect();
+        self.ffi_tx
+            .add_mint_keysets(ffi_mint_url, ffi_keysets)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_mint_quote(
+        &mut self,
+        quote: cdk::wallet::MintQuote,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_quote = quote.into();
+        self.ffi_tx
+            .add_mint_quote(ffi_quote)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_mint_quote(&mut self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> {
+        self.ffi_tx
+            .remove_mint_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_melt_quote(
+        &mut self,
+        quote: cdk::wallet::MeltQuote,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_quote = quote.into();
+        self.ffi_tx
+            .add_melt_quote(ffi_quote)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_melt_quote(&mut self, quote_id: &str) -> Result<(), cdk::cdk_database::Error> {
+        self.ffi_tx
+            .remove_melt_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_keys(
+        &mut self,
+        keyset: cdk::nuts::KeySet,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_keyset: KeySet = keyset.into();
+        self.ffi_tx
+            .add_keys(ffi_keyset)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_keys(&mut self, id: &cdk::nuts::Id) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_id = (*id).into();
+        self.ffi_tx
+            .remove_keys(ffi_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn update_proofs(
+        &mut self,
+        added: Vec<cdk::types::ProofInfo>,
+        removed_ys: Vec<cdk::nuts::PublicKey>,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_added: Vec<ProofInfo> = added.into_iter().map(Into::into).collect();
+        let ffi_removed_ys: Vec<PublicKey> = removed_ys.into_iter().map(Into::into).collect();
+        self.ffi_tx
+            .update_proofs(ffi_added, ffi_removed_ys)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn update_proofs_state(
+        &mut self,
+        ys: Vec<cdk::nuts::PublicKey>,
+        state: cdk::nuts::State,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
+        let ffi_state = state.into();
+        self.ffi_tx
+            .update_proofs_state(ffi_ys, ffi_state)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn increment_keyset_counter(
+        &mut self,
+        keyset_id: &cdk::nuts::Id,
+        count: u32,
+    ) -> Result<u32, cdk::cdk_database::Error> {
+        let ffi_id = (*keyset_id).into();
+        self.ffi_tx
+            .increment_keyset_counter(ffi_id, count)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn add_transaction(
+        &mut self,
+        transaction: cdk::wallet::types::Transaction,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        let ffi_transaction = transaction.into();
+        self.ffi_tx
+            .add_transaction(ffi_transaction)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn remove_transaction(
+        &mut self,
         transaction_id: cdk::wallet::types::TransactionId,
         transaction_id: cdk::wallet::types::TransactionId,
-    ) -> Result<(), Self::Err> {
+    ) -> Result<(), cdk::cdk_database::Error> {
         let ffi_id = transaction_id.into();
         let ffi_id = transaction_id.into();
-        self.ffi_db
+        self.ffi_tx
             .remove_transaction(ffi_id)
             .remove_transaction(ffi_id)
             .await
             .await
             .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
             .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
     }
     }
+
+    // Read methods needed during transactions
+    async fn get_keyset_by_id(
+        &mut self,
+        keyset_id: &cdk::nuts::Id,
+    ) -> Result<Option<cdk::nuts::KeySetInfo>, cdk::cdk_database::Error> {
+        let ffi_id = (*keyset_id).into();
+        let result = self
+            .ffi_tx
+            .get_keyset_by_id(ffi_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_keys(
+        &mut self,
+        id: &cdk::nuts::Id,
+    ) -> Result<Option<cdk::nuts::Keys>, cdk::cdk_database::Error> {
+        let ffi_id = (*id).into();
+        let result = self
+            .ffi_tx
+            .get_keys(ffi_id)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        match result {
+            Some(keys) => Ok(Some(keys.try_into().map_err(|e: FfiError| {
+                cdk::cdk_database::Error::Database(e.to_string().into())
+            })?)),
+            None => Ok(None),
+        }
+    }
+
+    async fn get_mint_quote(
+        &mut self,
+        quote_id: &str,
+    ) -> Result<Option<cdk::wallet::MintQuote>, cdk::cdk_database::Error> {
+        let result = self
+            .ffi_tx
+            .get_mint_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .map(|q| {
+                q.try_into()
+                    .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
+            })
+            .transpose()?)
+    }
+
+    async fn get_melt_quote(
+        &mut self,
+        quote_id: &str,
+    ) -> Result<Option<cdk::wallet::MeltQuote>, cdk::cdk_database::Error> {
+        let result = self
+            .ffi_tx
+            .get_melt_quote(quote_id.to_string())
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+        Ok(result
+            .map(|q| {
+                q.try_into()
+                    .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
+            })
+            .transpose()?)
+    }
+
+    async fn get_proofs(
+        &mut self,
+        mint_url: Option<cdk::mint_url::MintUrl>,
+        unit: Option<cdk::nuts::CurrencyUnit>,
+        state: Option<Vec<cdk::nuts::State>>,
+        spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>>,
+    ) -> Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> {
+        let ffi_mint_url = mint_url.map(Into::into);
+        let ffi_unit = unit.map(Into::into);
+        let ffi_state = state.map(|s| s.into_iter().map(Into::into).collect());
+        let ffi_spending_conditions =
+            spending_conditions.map(|sc| sc.into_iter().map(Into::into).collect());
+
+        let result = self
+            .ffi_tx
+            .get_proofs(ffi_mint_url, ffi_unit, ffi_state, ffi_spending_conditions)
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))?;
+
+        // Convert back to CDK ProofInfo
+        let cdk_result: Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> = result
+            .into_iter()
+            .map(|info| {
+                Ok(cdk::types::ProofInfo {
+                    proof: info.proof.try_into().map_err(|e: FfiError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    y: info.y.try_into().map_err(|e: FfiError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    mint_url: info.mint_url.try_into().map_err(|e: FfiError| {
+                        cdk::cdk_database::Error::Database(e.to_string().into())
+                    })?,
+                    state: info.state.into(),
+                    spending_condition: info
+                        .spending_condition
+                        .map(|sc| sc.try_into())
+                        .transpose()
+                        .map_err(|e: FfiError| {
+                            cdk::cdk_database::Error::Database(e.to_string().into())
+                        })?,
+                    unit: info.unit.into(),
+                })
+            })
+            .collect();
+
+        cdk_result
+    }
+}
+
+#[async_trait::async_trait]
+impl DbTransactionFinalizer for WalletDatabaseTransactionBridge {
+    type Err = cdk::cdk_database::Error;
+
+    async fn commit(mut self: Box<Self>) -> Result<(), cdk::cdk_database::Error> {
+        self.is_finalized = true;
+        let tx = self.ffi_tx.clone();
+        tx.commit()
+            .await
+            .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn rollback(mut self: Box<Self>) -> Result<(), cdk::cdk_database::Error> {
+        self.is_finalized = true;
+        let tx = self.ffi_tx.clone();
+        tx.rollback()
+            .await
+            .map_err(|e: FfiError| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+}
+
+impl Drop for WalletDatabaseTransactionBridge {
+    fn drop(&mut self) {
+        if !self.is_finalized {
+            let tx = self.ffi_tx.clone();
+            spawn(async move {
+                let _ = tx.rollback().await;
+            });
+        }
+    }
+}
+
+pub(crate) struct FfiWalletSQLDatabase<RM>
+where
+    RM: DatabasePool + 'static,
+{
+    inner: SQLWalletDatabase<RM>,
+}
+
+impl<RM> FfiWalletSQLDatabase<RM>
+where
+    RM: DatabasePool + 'static,
+{
+    /// Creates a new instance
+    pub fn new(inner: SQLWalletDatabase<RM>) -> Arc<Self> {
+        Arc::new(Self { inner })
+    }
+}
+
+/// Transaction wrapper for FFI
+pub(crate) struct FfiWalletTransaction {
+    tx: Arc<Mutex<Option<DynWalletDatabaseTransaction>>>,
+    is_finalized: AtomicBool,
+}
+
+impl Drop for FfiWalletTransaction {
+    fn drop(&mut self) {
+        if !self.is_finalized.load(std::sync::atomic::Ordering::SeqCst) {
+            let tx = self.tx.clone();
+            spawn(async move {
+                if let Some(s) = tx.lock().await.take() {
+                    let _ = s.rollback().await;
+                }
+            });
+        }
+    }
+}
+
+impl FfiWalletTransaction {
+    pub fn new(tx: DynWalletDatabaseTransaction) -> Arc<Self> {
+        Arc::new(Self {
+            tx: Arc::new(Mutex::new(Some(tx))),
+            is_finalized: false.into(),
+        })
+    }
+}
+
+// Implement WalletDatabaseFfi trait - only read methods + begin_db_transaction
+#[async_trait::async_trait]
+impl<RM> WalletDatabase for FfiWalletSQLDatabase<RM>
+where
+    RM: DatabasePool + 'static,
+{
+    async fn begin_db_transaction(
+        &self,
+    ) -> Result<Arc<WalletDatabaseTransactionWrapper>, FfiError> {
+        let tx = self
+            .inner
+            .begin_db_transaction()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+
+        Ok(Arc::new(WalletDatabaseTransactionWrapper {
+            inner: FfiWalletTransaction::new(tx),
+        }))
+    }
+
+    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError> {
+        let cdk_ys: Vec<cdk::nuts::PublicKey> = ys
+            .into_iter()
+            .map(|y| y.try_into())
+            .collect::<Result<Vec<_>, FfiError>>()?;
+
+        let result = self
+            .inner
+            .get_proofs_by_ys(cdk_ys)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+
+        Ok(result.into_iter().map(Into::into).collect())
+    }
+
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
+        let cdk_mint_url = mint_url.try_into()?;
+        let result = self
+            .inner
+            .get_mint(cdk_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
+        let result = self
+            .inner
+            .get_mints()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result
+            .into_iter()
+            .map(|(k, v)| (k.into(), v.map(Into::into)))
+            .collect())
+    }
+
+    async fn get_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
+        let cdk_mint_url = mint_url.try_into()?;
+        let result = self
+            .inner
+            .get_mint_keysets(cdk_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
+    }
+
+    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
+        let cdk_id = keyset_id.into();
+        let result = self
+            .inner
+            .get_keyset_by_id(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_mint_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|q| q.into()))
+    }
+
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_mint_quotes()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.into_iter().map(|q| q.into()).collect())
+    }
+
+    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_melt_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|q| q.into()))
+    }
+
+    async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
+        let result = self
+            .inner
+            .get_melt_quotes()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.into_iter().map(|q| q.into()).collect())
+    }
+
+    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
+        let cdk_id = id.into();
+        let result = self
+            .inner
+            .get_keys(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, FfiError> {
+        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
+        let cdk_unit = unit.map(Into::into);
+        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
+        let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
+            spending_conditions
+                .map(|sc| {
+                    sc.into_iter()
+                        .map(|c| c.try_into())
+                        .collect::<Result<Vec<_>, FfiError>>()
+                })
+                .transpose()?;
+
+        let result = self
+            .inner
+            .get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+
+        Ok(result.into_iter().map(Into::into).collect())
+    }
+
+    async fn get_balance(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+    ) -> Result<u64, FfiError> {
+        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
+        let cdk_unit = unit.map(Into::into);
+        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
+
+        self.inner
+            .get_balance(cdk_mint_url, cdk_unit, cdk_state)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_transaction(
+        &self,
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, FfiError> {
+        let cdk_id = transaction_id.try_into()?;
+        let result = self
+            .inner
+            .get_transaction(cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, FfiError> {
+        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
+        let cdk_direction = direction.map(Into::into);
+        let cdk_unit = unit.map(Into::into);
+
+        let result = self
+            .inner
+            .list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+
+        Ok(result.into_iter().map(Into::into).collect())
+    }
+}
+
+// Implement WalletDatabaseTransactionFfi trait - all write methods
+#[async_trait::async_trait]
+impl WalletDatabaseTransaction for FfiWalletTransaction {
+    async fn commit(self: Arc<Self>) -> Result<(), FfiError> {
+        self.is_finalized
+            .store(true, std::sync::atomic::Ordering::SeqCst);
+        self.tx
+            .lock()
+            .await
+            .take()
+            .ok_or(FfiError::Database {
+                msg: "Transaction already finalized".to_owned(),
+            })?
+            .commit()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn rollback(self: Arc<Self>) -> Result<(), FfiError> {
+        self.is_finalized
+            .store(true, std::sync::atomic::Ordering::SeqCst);
+        self.tx
+            .lock()
+            .await
+            .take()
+            .ok_or(FfiError::Database {
+                msg: "Transaction already finalized".to_owned(),
+            })?
+            .rollback()
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn add_mint(
+        &self,
+        mint_url: MintUrl,
+        mint_info: Option<MintInfo>,
+    ) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+
+        let cdk_mint_url = mint_url.try_into()?;
+        let cdk_mint_info = mint_info.map(Into::into);
+        tx.add_mint(cdk_mint_url, cdk_mint_info)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_mint_url = mint_url.try_into()?;
+        tx.remove_mint(cdk_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn update_mint_url(
+        &self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_old_mint_url = old_mint_url.try_into()?;
+        let cdk_new_mint_url = new_mint_url.try_into()?;
+        tx.update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn add_mint_keysets(
+        &self,
+        mint_url: MintUrl,
+        keysets: Vec<KeySetInfo>,
+    ) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_mint_url = mint_url.try_into()?;
+        let cdk_keysets: Vec<cdk::nuts::KeySetInfo> = keysets.into_iter().map(Into::into).collect();
+        tx.add_mint_keysets(cdk_mint_url, cdk_keysets)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_id = keyset_id.into();
+        let result = tx
+            .get_keyset_by_id(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_id = id.into();
+        let result = tx
+            .get_keys(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(Into::into))
+    }
+
+    async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let result = tx
+            .get_mint_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|q| q.into()))
+    }
+
+    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_quote = quote.try_into()?;
+        tx.add_mint_quote(cdk_quote)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        tx.remove_mint_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let result = tx
+            .get_melt_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(result.map(|q| q.into()))
+    }
+
+    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_quote = quote.try_into()?;
+        tx.add_melt_quote(cdk_quote)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        tx.remove_melt_quote(&quote_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?;
+        tx.add_keys(cdk_keyset)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_id = id.into();
+        tx.remove_keys(&cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<ProofState>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+
+        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
+        let cdk_unit = unit.map(Into::into);
+        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
+        let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
+            spending_conditions
+                .map(|sc| {
+                    sc.into_iter()
+                        .map(|c| c.try_into())
+                        .collect::<Result<Vec<_>, FfiError>>()
+                })
+                .transpose()?;
+
+        let result = tx
+            .get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+
+        Ok(result.into_iter().map(Into::into).collect())
+    }
+
+    async fn update_proofs(
+        &self,
+        added: Vec<ProofInfo>,
+        removed_ys: Vec<PublicKey>,
+    ) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+
+        let cdk_added: Result<Vec<cdk::types::ProofInfo>, FfiError> = added
+            .into_iter()
+            .map(|info| {
+                Ok::<cdk::types::ProofInfo, FfiError>(cdk::types::ProofInfo {
+                    proof: info.proof.try_into()?,
+                    y: info.y.try_into()?,
+                    mint_url: info.mint_url.try_into()?,
+                    state: info.state.into(),
+                    spending_condition: info
+                        .spending_condition
+                        .map(|sc| sc.try_into())
+                        .transpose()?,
+                    unit: info.unit.into(),
+                })
+            })
+            .collect();
+        let cdk_added = cdk_added?;
+
+        let cdk_removed_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
+            removed_ys.into_iter().map(|pk| pk.try_into()).collect();
+        let cdk_removed_ys = cdk_removed_ys?;
+
+        tx.update_proofs(cdk_added, cdk_removed_ys)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn update_proofs_state(
+        &self,
+        ys: Vec<PublicKey>,
+        state: ProofState,
+    ) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
+            ys.into_iter().map(|pk| pk.try_into()).collect();
+        let cdk_ys = cdk_ys?;
+        let cdk_state = state.into();
+
+        tx.update_proofs_state(cdk_ys, cdk_state)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_id = keyset_id.into();
+        tx.increment_keyset_counter(&cdk_id, count)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+
+        let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
+
+        tx.add_transaction(cdk_transaction)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
+        let mut tx_guard = self.tx.lock().await;
+        let tx = tx_guard.as_mut().ok_or(FfiError::Database {
+            msg: "Transaction already finalized".to_owned(),
+        })?;
+        let cdk_id = transaction_id.try_into()?;
+        tx.remove_transaction(cdk_id)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
 }
 }
 
 
-/// FFI-safe wallet database backend selection
-#[derive(uniffi::Enum)]
+/// FFI-safe database type enum
+#[derive(uniffi::Enum, Clone)]
 pub enum WalletDbBackend {
 pub enum WalletDbBackend {
     Sqlite {
     Sqlite {
         path: String,
         path: String,

+ 1 - 1
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -27,7 +27,7 @@ pub struct MultiMintWallet {
 
 
 #[uniffi::export(async_runtime = "tokio")]
 #[uniffi::export(async_runtime = "tokio")]
 impl MultiMintWallet {
 impl MultiMintWallet {
-    /// Create a new MultiMintWallet from mnemonic using WalletDatabase trait
+    /// Create a new MultiMintWallet from mnemonic using WalletDatabaseFfi trait
     #[uniffi::constructor]
     #[uniffi::constructor]
     pub fn new(
     pub fn new(
         unit: CurrencyUnit,
         unit: CurrencyUnit,

+ 54 - 334
crates/cdk-ffi/src/postgres.rs

@@ -2,19 +2,17 @@ use std::collections::HashMap;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 // Bring the CDK wallet database trait into scope so trait methods resolve on the inner DB
 // Bring the CDK wallet database trait into scope so trait methods resolve on the inner DB
-use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
-#[cfg(feature = "postgres")]
-use cdk_postgres::WalletPgDatabase as CdkWalletPgDatabase;
+use cdk_postgres::PgConnectionPool;
 
 
 use crate::{
 use crate::{
-    CurrencyUnit, FfiError, Id, KeySet, KeySetInfo, Keys, MeltQuote, MintInfo, MintQuote, MintUrl,
-    ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction, TransactionDirection,
-    TransactionId, WalletDatabase,
+    CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySetInfo, Keys, MeltQuote, MintInfo,
+    MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction,
+    TransactionDirection, TransactionId, WalletDatabase, WalletDatabaseTransactionWrapper,
 };
 };
 
 
 #[derive(uniffi::Object)]
 #[derive(uniffi::Object)]
 pub struct WalletPostgresDatabase {
 pub struct WalletPostgresDatabase {
-    inner: Arc<CdkWalletPgDatabase>,
+    inner: Arc<FfiWalletSQLDatabase<PgConnectionPool>>,
 }
 }
 
 
 // Keep a long-lived Tokio runtime for Postgres-created resources so that
 // Keep a long-lived Tokio runtime for Postgres-created resources so that
@@ -35,230 +33,82 @@ fn pg_runtime() -> &'static tokio::runtime::Runtime {
     })
     })
 }
 }
 
 
-// Implement the local WalletDatabase trait (simple trait path required by uniffi)
+#[uniffi::export]
+impl WalletPostgresDatabase {
+    /// Create a new Postgres-backed wallet database
+    /// Requires cdk-ffi to be built with feature "postgres".
+    /// Example URL:
+    ///  "host=localhost user=test password=test dbname=testdb port=5433 schema=wallet sslmode=prefer"
+    #[cfg(feature = "postgres")]
+    #[uniffi::constructor]
+    pub fn new(url: String) -> Result<Arc<Self>, FfiError> {
+        let inner = match tokio::runtime::Handle::try_current() {
+            Ok(handle) => tokio::task::block_in_place(|| {
+                handle.block_on(
+                    async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await },
+                )
+            }),
+            // Important: use a process-long runtime so background connection tasks stay alive.
+            Err(_) => pg_runtime()
+                .block_on(async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await }),
+        }
+        .map_err(|e| FfiError::Database { msg: e.to_string() })?;
+        Ok(Arc::new(WalletPostgresDatabase {
+            inner: FfiWalletSQLDatabase::new(inner),
+        }))
+    }
+}
+
 #[uniffi::export(async_runtime = "tokio")]
 #[uniffi::export(async_runtime = "tokio")]
 #[async_trait::async_trait]
 #[async_trait::async_trait]
 impl WalletDatabase for WalletPostgresDatabase {
 impl WalletDatabase for WalletPostgresDatabase {
-    // Forward all trait methods to inner CDK database via the bridge adapter
-    async fn add_mint(
+    async fn begin_db_transaction(
         &self,
         &self,
-        mint_url: MintUrl,
-        mint_info: Option<MintInfo>,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let cdk_mint_info = mint_info.map(Into::into);
-        println!("adding new mint");
-        self.inner
-            .add_mint(cdk_mint_url, cdk_mint_info)
-            .await
-            .map_err(|e| {
-                println!("ffi error {:?}", e);
-                FfiError::Database { msg: e.to_string() }
-            })
+    ) -> Result<Arc<WalletDatabaseTransactionWrapper>, FfiError> {
+        self.inner.begin_db_transaction().await
     }
     }
-    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        self.inner
-            .remove_mint(cdk_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+
+    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> {
     async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let result = self
-            .inner
-            .get_mint(cdk_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
+        self.inner.get_mint(mint_url).await
     }
     }
+
     async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
     async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
-        let result = self
-            .inner
-            .get_mints()
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result
-            .into_iter()
-            .map(|(k, v)| (k.into(), v.map(Into::into)))
-            .collect())
-    }
-    async fn update_mint_url(
-        &self,
-        old_mint_url: MintUrl,
-        new_mint_url: MintUrl,
-    ) -> Result<(), FfiError> {
-        let cdk_old_mint_url = old_mint_url.try_into()?;
-        let cdk_new_mint_url = new_mint_url.try_into()?;
-        self.inner
-            .update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-    async fn add_mint_keysets(
-        &self,
-        mint_url: MintUrl,
-        keysets: Vec<KeySetInfo>,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let cdk_keysets: Vec<cdk::nuts::KeySetInfo> = keysets.into_iter().map(Into::into).collect();
-        self.inner
-            .add_mint_keysets(cdk_mint_url, cdk_keysets)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_mints().await
     }
     }
+
     async fn get_mint_keysets(
     async fn get_mint_keysets(
         &self,
         &self,
         mint_url: MintUrl,
         mint_url: MintUrl,
     ) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
     ) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let result = self
-            .inner
-            .get_mint_keysets(cdk_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
+        self.inner.get_mint_keysets(mint_url).await
     }
     }
 
 
     async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
     async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
-        let cdk_id = keyset_id.into();
-        let result = self
-            .inner
-            .get_keyset_by_id(&cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
-    }
-
-    // Mint Quote Management
-    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
-        let cdk_quote = quote.try_into()?;
-        self.inner
-            .add_mint_quote(cdk_quote)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_keyset_by_id(keyset_id).await
     }
     }
 
 
     async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
     async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_mint_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(|q| q.into()))
+        self.inner.get_mint_quote(quote_id).await
     }
     }
 
 
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_mint_quotes()
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.into_iter().map(|q| q.into()).collect())
-    }
-
-    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
-        self.inner
-            .remove_mint_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Melt Quote Management
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
-        let cdk_quote = quote.try_into()?;
-        self.inner
-            .add_melt_quote(cdk_quote)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_mint_quotes().await
     }
     }
 
 
     async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
     async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_melt_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(|q| q.into()))
+        self.inner.get_melt_quote(quote_id).await
     }
     }
 
 
     async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
     async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_melt_quotes()
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.into_iter().map(|q| q.into()).collect())
-    }
-
-    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
-        self.inner
-            .remove_melt_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Keys Management
-    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
-        // Convert FFI KeySet to cdk::nuts::KeySet
-        let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?;
-        self.inner
-            .add_keys(cdk_keyset)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_melt_quotes().await
     }
     }
 
 
     async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
     async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
-        let cdk_id = id.into();
-        let result = self
-            .inner
-            .get_keys(&cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
-    }
-
-    async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
-        let cdk_id = id.into();
-        self.inner
-            .remove_keys(&cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Proof Management
-    async fn update_proofs(
-        &self,
-        added: Vec<ProofInfo>,
-        removed_ys: Vec<PublicKey>,
-    ) -> Result<(), FfiError> {
-        // Convert FFI types to CDK types
-        let cdk_added: Result<Vec<cdk::types::ProofInfo>, FfiError> = added
-            .into_iter()
-            .map(|info| {
-                Ok::<cdk::types::ProofInfo, FfiError>(cdk::types::ProofInfo {
-                    proof: info.proof.try_into()?,
-                    y: info.y.try_into()?,
-                    mint_url: info.mint_url.try_into()?,
-                    state: info.state.into(),
-                    spending_condition: info
-                        .spending_condition
-                        .map(|sc| sc.try_into())
-                        .transpose()?,
-                    unit: info.unit.into(),
-                })
-            })
-            .collect();
-        let cdk_added = cdk_added?;
-
-        let cdk_removed_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
-            removed_ys.into_iter().map(|pk| pk.try_into()).collect();
-        let cdk_removed_ys = cdk_removed_ys?;
-
-        self.inner
-            .update_proofs(cdk_added, cdk_removed_ys)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_keys(id).await
     }
     }
 
 
     async fn get_proofs(
     async fn get_proofs(
@@ -268,40 +118,9 @@ impl WalletDatabase for WalletPostgresDatabase {
         state: Option<Vec<ProofState>>,
         state: Option<Vec<ProofState>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Vec<ProofInfo>, FfiError> {
     ) -> Result<Vec<ProofInfo>, FfiError> {
-        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
-        let cdk_unit = unit.map(Into::into);
-        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
-        let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
-            spending_conditions
-                .map(|sc| {
-                    sc.into_iter()
-                        .map(|c| c.try_into())
-                        .collect::<Result<Vec<_>, FfiError>>()
-                })
-                .transpose()?;
-
-        let result = self
-            .inner
-            .get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-
-        Ok(result.into_iter().map(Into::into).collect())
-    }
-
-    async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError> {
-        let cdk_ys: Vec<cdk::nuts::PublicKey> = ys
-            .into_iter()
-            .map(|y| y.try_into())
-            .collect::<Result<Vec<_>, FfiError>>()?;
-
-        let result = self
-            .inner
-            .get_proofs_by_ys(cdk_ys)
+        self.inner
+            .get_proofs(mint_url, unit, state, spending_conditions)
             .await
             .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-
-        Ok(result.into_iter().map(Into::into).collect())
     }
     }
 
 
     async fn get_balance(
     async fn get_balance(
@@ -310,63 +129,14 @@ impl WalletDatabase for WalletPostgresDatabase {
         unit: Option<CurrencyUnit>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<ProofState>>,
         state: Option<Vec<ProofState>>,
     ) -> Result<u64, FfiError> {
     ) -> Result<u64, FfiError> {
-        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
-        let cdk_unit = unit.map(Into::into);
-        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
-
-        self.inner
-            .get_balance(cdk_mint_url, cdk_unit, cdk_state)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    async fn update_proofs_state(
-        &self,
-        ys: Vec<PublicKey>,
-        state: ProofState,
-    ) -> Result<(), FfiError> {
-        let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
-            ys.into_iter().map(|pk| pk.try_into()).collect();
-        let cdk_ys = cdk_ys?;
-        let cdk_state = state.into();
-
-        self.inner
-            .update_proofs_state(cdk_ys, cdk_state)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Keyset Counter Management
-    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
-        let cdk_id = keyset_id.into();
-        self.inner
-            .increment_keyset_counter(&cdk_id, count)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Transaction Management
-    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
-        // Convert FFI Transaction to CDK Transaction using TryFrom
-        let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
-
-        self.inner
-            .add_transaction(cdk_transaction)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_balance(mint_url, unit, state).await
     }
     }
 
 
     async fn get_transaction(
     async fn get_transaction(
         &self,
         &self,
         transaction_id: TransactionId,
         transaction_id: TransactionId,
     ) -> Result<Option<Transaction>, FfiError> {
     ) -> Result<Option<Transaction>, FfiError> {
-        let cdk_id = transaction_id.try_into()?;
-        let result = self
-            .inner
-            .get_transaction(cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
+        self.inner.get_transaction(transaction_id).await
     }
     }
 
 
     async fn list_transactions(
     async fn list_transactions(
@@ -375,58 +145,8 @@ impl WalletDatabase for WalletPostgresDatabase {
         direction: Option<TransactionDirection>,
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
         unit: Option<CurrencyUnit>,
     ) -> Result<Vec<Transaction>, FfiError> {
     ) -> Result<Vec<Transaction>, FfiError> {
-        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
-        let cdk_direction = direction.map(Into::into);
-        let cdk_unit = unit.map(Into::into);
-
-        let result = self
-            .inner
-            .list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-
-        Ok(result.into_iter().map(Into::into).collect())
-    }
-
-    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
-        let cdk_id = transaction_id.try_into()?;
         self.inner
         self.inner
-            .remove_transaction(cdk_id)
+            .list_transactions(mint_url, direction, unit)
             .await
             .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-}
-
-#[uniffi::export]
-impl WalletPostgresDatabase {
-    /// Create a new Postgres-backed wallet database
-    /// Requires cdk-ffi to be built with feature "postgres".
-    /// Example URL:
-    ///  "host=localhost user=test password=test dbname=testdb port=5433 schema=wallet sslmode=prefer"
-    #[cfg(feature = "postgres")]
-    #[uniffi::constructor]
-    pub fn new(url: String) -> Result<Arc<Self>, FfiError> {
-        let inner = match tokio::runtime::Handle::try_current() {
-            Ok(handle) => tokio::task::block_in_place(|| {
-                handle.block_on(
-                    async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await },
-                )
-            }),
-            // Important: use a process-long runtime so background connection tasks stay alive.
-            Err(_) => pg_runtime()
-                .block_on(async move { cdk_postgres::new_wallet_pg_database(url.as_str()).await }),
-        }
-        .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(Arc::new(WalletPostgresDatabase {
-            inner: Arc::new(inner),
-        }))
-    }
-
-    fn clone_as_trait(&self) -> Arc<dyn WalletDatabase> {
-        // Safety: UniFFI objects are reference counted and Send+Sync via Arc
-        let obj: Arc<dyn WalletDatabase> = Arc::new(WalletPostgresDatabase {
-            inner: self.inner.clone(),
-        });
-        obj
     }
     }
 }
 }

+ 26 - 302
crates/cdk-ffi/src/sqlite.rs

@@ -2,22 +2,18 @@ use std::collections::HashMap;
 use std::sync::Arc;
 use std::sync::Arc;
 
 
 use cdk_sqlite::wallet::WalletSqliteDatabase as CdkWalletSqliteDatabase;
 use cdk_sqlite::wallet::WalletSqliteDatabase as CdkWalletSqliteDatabase;
+use cdk_sqlite::SqliteConnectionManager;
 
 
 use crate::{
 use crate::{
-    CurrencyUnit, FfiError, Id, KeySet, KeySetInfo, Keys, MeltQuote, MintInfo, MintQuote, MintUrl,
-    ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction, TransactionDirection,
-    TransactionId, WalletDatabase,
+    CurrencyUnit, FfiError, FfiWalletSQLDatabase, Id, KeySetInfo, Keys, MeltQuote, MintInfo,
+    MintQuote, MintUrl, ProofInfo, ProofState, PublicKey, SpendingConditions, Transaction,
+    TransactionDirection, TransactionId, WalletDatabase,
 };
 };
 
 
-/// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabase trait
+/// FFI-compatible WalletSqliteDatabase implementation that implements the WalletDatabaseFfi trait
 #[derive(uniffi::Object)]
 #[derive(uniffi::Object)]
 pub struct WalletSqliteDatabase {
 pub struct WalletSqliteDatabase {
-    inner: Arc<CdkWalletSqliteDatabase>,
-}
-use cdk::cdk_database::WalletDatabase as CdkWalletDatabase;
-
-impl WalletSqliteDatabase {
-    // No additional methods needed beyond the trait implementation
+    inner: Arc<FfiWalletSQLDatabase<SqliteConnectionManager>>,
 }
 }
 
 
 #[uniffi::export]
 #[uniffi::export]
@@ -41,7 +37,7 @@ impl WalletSqliteDatabase {
         }
         }
         .map_err(|e| FfiError::Database { msg: e.to_string() })?;
         .map_err(|e| FfiError::Database { msg: e.to_string() })?;
         Ok(Arc::new(Self {
         Ok(Arc::new(Self {
-            inner: Arc::new(db),
+            inner: FfiWalletSQLDatabase::new(db),
         }))
         }))
     }
     }
 
 
@@ -63,7 +59,7 @@ impl WalletSqliteDatabase {
         }
         }
         .map_err(|e| FfiError::Database { msg: e.to_string() })?;
         .map_err(|e| FfiError::Database { msg: e.to_string() })?;
         Ok(Arc::new(Self {
         Ok(Arc::new(Self {
-            inner: Arc::new(db),
+            inner: FfiWalletSQLDatabase::new(db),
         }))
         }))
     }
     }
 }
 }
@@ -71,229 +67,49 @@ impl WalletSqliteDatabase {
 #[uniffi::export(async_runtime = "tokio")]
 #[uniffi::export(async_runtime = "tokio")]
 #[async_trait::async_trait]
 #[async_trait::async_trait]
 impl WalletDatabase for WalletSqliteDatabase {
 impl WalletDatabase for WalletSqliteDatabase {
-    // Mint Management
-    async fn add_mint(
+    async fn begin_db_transaction(
         &self,
         &self,
-        mint_url: MintUrl,
-        mint_info: Option<MintInfo>,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let cdk_mint_info = mint_info.map(Into::into);
-        self.inner
-            .add_mint(cdk_mint_url, cdk_mint_info)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    async fn remove_mint(&self, mint_url: MintUrl) -> Result<(), FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        self.inner
-            .remove_mint(cdk_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    ) -> Result<Arc<crate::database::WalletDatabaseTransactionWrapper>, FfiError> {
+        self.inner.begin_db_transaction().await
     }
     }
 
 
     async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
     async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let result = self
-            .inner
-            .get_mint(cdk_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
+        self.inner.get_mint(mint_url).await
     }
     }
 
 
     async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
     async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, FfiError> {
-        let result = self
-            .inner
-            .get_mints()
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result
-            .into_iter()
-            .map(|(k, v)| (k.into(), v.map(Into::into)))
-            .collect())
-    }
-
-    async fn update_mint_url(
-        &self,
-        old_mint_url: MintUrl,
-        new_mint_url: MintUrl,
-    ) -> Result<(), FfiError> {
-        let cdk_old_mint_url = old_mint_url.try_into()?;
-        let cdk_new_mint_url = new_mint_url.try_into()?;
-        self.inner
-            .update_mint_url(cdk_old_mint_url, cdk_new_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Keyset Management
-    async fn add_mint_keysets(
-        &self,
-        mint_url: MintUrl,
-        keysets: Vec<KeySetInfo>,
-    ) -> Result<(), FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let cdk_keysets: Vec<cdk::nuts::KeySetInfo> = keysets.into_iter().map(Into::into).collect();
-        self.inner
-            .add_mint_keysets(cdk_mint_url, cdk_keysets)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_mints().await
     }
     }
 
 
     async fn get_mint_keysets(
     async fn get_mint_keysets(
         &self,
         &self,
         mint_url: MintUrl,
         mint_url: MintUrl,
     ) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
     ) -> Result<Option<Vec<KeySetInfo>>, FfiError> {
-        let cdk_mint_url = mint_url.try_into()?;
-        let result = self
-            .inner
-            .get_mint_keysets(cdk_mint_url)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(|keysets| keysets.into_iter().map(Into::into).collect()))
+        self.inner.get_mint_keysets(mint_url).await
     }
     }
 
 
     async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
     async fn get_keyset_by_id(&self, keyset_id: Id) -> Result<Option<KeySetInfo>, FfiError> {
-        let cdk_id = keyset_id.into();
-        let result = self
-            .inner
-            .get_keyset_by_id(&cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
-    }
-
-    // Mint Quote Management
-    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), FfiError> {
-        let cdk_quote = quote.try_into()?;
-        self.inner
-            .add_mint_quote(cdk_quote)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_keyset_by_id(keyset_id).await
     }
     }
 
 
     async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
     async fn get_mint_quote(&self, quote_id: String) -> Result<Option<MintQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_mint_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(|q| q.into()))
+        self.inner.get_mint_quote(quote_id).await
     }
     }
 
 
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
     async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_mint_quotes()
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.into_iter().map(|q| q.into()).collect())
-    }
-
-    async fn remove_mint_quote(&self, quote_id: String) -> Result<(), FfiError> {
-        self.inner
-            .remove_mint_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Melt Quote Management
-    async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), FfiError> {
-        let cdk_quote = quote.try_into()?;
-        self.inner
-            .add_melt_quote(cdk_quote)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_mint_quotes().await
     }
     }
 
 
     async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
     async fn get_melt_quote(&self, quote_id: String) -> Result<Option<MeltQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_melt_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(|q| q.into()))
+        self.inner.get_melt_quote(quote_id).await
     }
     }
 
 
     async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
     async fn get_melt_quotes(&self) -> Result<Vec<MeltQuote>, FfiError> {
-        let result = self
-            .inner
-            .get_melt_quotes()
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.into_iter().map(|q| q.into()).collect())
-    }
-
-    async fn remove_melt_quote(&self, quote_id: String) -> Result<(), FfiError> {
-        self.inner
-            .remove_melt_quote(&quote_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Keys Management
-    async fn add_keys(&self, keyset: KeySet) -> Result<(), FfiError> {
-        // Convert FFI KeySet to cdk::nuts::KeySet
-        let cdk_keyset: cdk::nuts::KeySet = keyset.try_into()?;
-        self.inner
-            .add_keys(cdk_keyset)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_melt_quotes().await
     }
     }
 
 
     async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
     async fn get_keys(&self, id: Id) -> Result<Option<Keys>, FfiError> {
-        let cdk_id = id.into();
-        let result = self
-            .inner
-            .get_keys(&cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
-    }
-
-    async fn remove_keys(&self, id: Id) -> Result<(), FfiError> {
-        let cdk_id = id.into();
-        self.inner
-            .remove_keys(&cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Proof Management
-    async fn update_proofs(
-        &self,
-        added: Vec<ProofInfo>,
-        removed_ys: Vec<PublicKey>,
-    ) -> Result<(), FfiError> {
-        // Convert FFI types to CDK types
-        let cdk_added: Result<Vec<cdk::types::ProofInfo>, FfiError> = added
-            .into_iter()
-            .map(|info| {
-                Ok::<cdk::types::ProofInfo, FfiError>(cdk::types::ProofInfo {
-                    proof: info.proof.try_into()?,
-                    y: info.y.try_into()?,
-                    mint_url: info.mint_url.try_into()?,
-                    state: info.state.into(),
-                    spending_condition: info
-                        .spending_condition
-                        .map(|sc| sc.try_into())
-                        .transpose()?,
-                    unit: info.unit.into(),
-                })
-            })
-            .collect();
-        let cdk_added = cdk_added?;
-
-        let cdk_removed_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
-            removed_ys.into_iter().map(|pk| pk.try_into()).collect();
-        let cdk_removed_ys = cdk_removed_ys?;
-
-        self.inner
-            .update_proofs(cdk_added, cdk_removed_ys)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_keys(id).await
     }
     }
 
 
     async fn get_proofs(
     async fn get_proofs(
@@ -303,40 +119,13 @@ impl WalletDatabase for WalletSqliteDatabase {
         state: Option<Vec<ProofState>>,
         state: Option<Vec<ProofState>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
     ) -> Result<Vec<ProofInfo>, FfiError> {
     ) -> Result<Vec<ProofInfo>, FfiError> {
-        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
-        let cdk_unit = unit.map(Into::into);
-        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
-        let cdk_spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>> =
-            spending_conditions
-                .map(|sc| {
-                    sc.into_iter()
-                        .map(|c| c.try_into())
-                        .collect::<Result<Vec<_>, FfiError>>()
-                })
-                .transpose()?;
-
-        let result = self
-            .inner
-            .get_proofs(cdk_mint_url, cdk_unit, cdk_state, cdk_spending_conditions)
+        self.inner
+            .get_proofs(mint_url, unit, state, spending_conditions)
             .await
             .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-
-        Ok(result.into_iter().map(Into::into).collect())
     }
     }
 
 
     async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError> {
     async fn get_proofs_by_ys(&self, ys: Vec<PublicKey>) -> Result<Vec<ProofInfo>, FfiError> {
-        let cdk_ys: Vec<cdk::nuts::PublicKey> = ys
-            .into_iter()
-            .map(|y| y.try_into())
-            .collect::<Result<Vec<_>, FfiError>>()?;
-
-        let result = self
-            .inner
-            .get_proofs_by_ys(cdk_ys)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-
-        Ok(result.into_iter().map(Into::into).collect())
+        self.inner.get_proofs_by_ys(ys).await
     }
     }
 
 
     async fn get_balance(
     async fn get_balance(
@@ -345,63 +134,14 @@ impl WalletDatabase for WalletSqliteDatabase {
         unit: Option<CurrencyUnit>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<ProofState>>,
         state: Option<Vec<ProofState>>,
     ) -> Result<u64, FfiError> {
     ) -> Result<u64, FfiError> {
-        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
-        let cdk_unit = unit.map(Into::into);
-        let cdk_state = state.map(|s| s.into_iter().map(Into::into).collect());
-
-        self.inner
-            .get_balance(cdk_mint_url, cdk_unit, cdk_state)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    async fn update_proofs_state(
-        &self,
-        ys: Vec<PublicKey>,
-        state: ProofState,
-    ) -> Result<(), FfiError> {
-        let cdk_ys: Result<Vec<cdk::nuts::PublicKey>, FfiError> =
-            ys.into_iter().map(|pk| pk.try_into()).collect();
-        let cdk_ys = cdk_ys?;
-        let cdk_state = state.into();
-
-        self.inner
-            .update_proofs_state(cdk_ys, cdk_state)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Keyset Counter Management
-    async fn increment_keyset_counter(&self, keyset_id: Id, count: u32) -> Result<u32, FfiError> {
-        let cdk_id = keyset_id.into();
-        self.inner
-            .increment_keyset_counter(&cdk_id, count)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
-    }
-
-    // Transaction Management
-    async fn add_transaction(&self, transaction: Transaction) -> Result<(), FfiError> {
-        // Convert FFI Transaction to CDK Transaction using TryFrom
-        let cdk_transaction: cdk::wallet::types::Transaction = transaction.try_into()?;
-
-        self.inner
-            .add_transaction(cdk_transaction)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
+        self.inner.get_balance(mint_url, unit, state).await
     }
     }
 
 
     async fn get_transaction(
     async fn get_transaction(
         &self,
         &self,
         transaction_id: TransactionId,
         transaction_id: TransactionId,
     ) -> Result<Option<Transaction>, FfiError> {
     ) -> Result<Option<Transaction>, FfiError> {
-        let cdk_id = transaction_id.try_into()?;
-        let result = self
-            .inner
-            .get_transaction(cdk_id)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-        Ok(result.map(Into::into))
+        self.inner.get_transaction(transaction_id).await
     }
     }
 
 
     async fn list_transactions(
     async fn list_transactions(
@@ -410,24 +150,8 @@ impl WalletDatabase for WalletSqliteDatabase {
         direction: Option<TransactionDirection>,
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
         unit: Option<CurrencyUnit>,
     ) -> Result<Vec<Transaction>, FfiError> {
     ) -> Result<Vec<Transaction>, FfiError> {
-        let cdk_mint_url = mint_url.map(|u| u.try_into()).transpose()?;
-        let cdk_direction = direction.map(Into::into);
-        let cdk_unit = unit.map(Into::into);
-
-        let result = self
-            .inner
-            .list_transactions(cdk_mint_url, cdk_direction, cdk_unit)
-            .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })?;
-
-        Ok(result.into_iter().map(Into::into).collect())
-    }
-
-    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
-        let cdk_id = transaction_id.try_into()?;
         self.inner
         self.inner
-            .remove_transaction(cdk_id)
+            .list_transactions(mint_url, direction, unit)
             .await
             .await
-            .map_err(|e| FfiError::Database { msg: e.to_string() })
     }
     }
 }
 }

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

@@ -26,7 +26,7 @@ impl Wallet {
 
 
 #[uniffi::export(async_runtime = "tokio")]
 #[uniffi::export(async_runtime = "tokio")]
 impl Wallet {
 impl Wallet {
-    /// Create a new Wallet from mnemonic using WalletDatabase trait
+    /// Create a new Wallet from mnemonic using WalletDatabaseFfi trait
     #[uniffi::constructor]
     #[uniffi::constructor]
     pub fn new(
     pub fn new(
         mint_url: String,
         mint_url: String,

+ 35 - 12
crates/cdk-ffi/tests/README.md

@@ -1,6 +1,6 @@
 # CDK FFI Python Tests
 # CDK FFI Python Tests
 
 
-This directory contains Python tests for the CDK FFI (Foreign Function Interface) bindings, focusing on wallet database operations.
+This directory contains Python tests for the CDK FFI (Foreign Function Interface) bindings, covering both transaction operations and wallet database operations.
 
 
 ## Running the Tests
 ## Running the Tests
 
 
@@ -34,25 +34,45 @@ python3 crates/cdk-ffi/tests/test_transactions.py
 The test script automatically:
 The test script automatically:
 1. Locates the bindings in `target/bindings/python/`
 1. Locates the bindings in `target/bindings/python/`
 2. Copies the shared library from `target/release/` to the bindings directory
 2. Copies the shared library from `target/release/` to the bindings directory
-3. Runs all wallet tests
+3. Runs all tests
 
 
 **No manual file copying required!**
 **No manual file copying required!**
 
 
 ## Test Suite
 ## Test Suite
 
 
-### Wallet Tests (test_transactions.py)
+### Transaction Tests (explicit transaction management)
 
 
-Comprehensive tests for wallet database operations:
+Tests that use `begin_db_transaction()`, `commit()`, and `rollback()`:
 
 
-1. **Wallet Creation** - Tests creating a wallet with SQLite backend
-2. **Wallet Mint Management** - Tests adding and querying mints
-3. **Wallet Keyset Management** - Tests adding and querying keysets
-4. **Wallet Keyset Counter** - Tests keyset counter increment operations
-5. **Wallet Quote Operations** - Tests querying mint and melt quotes
-6. **Wallet Get Proofs by Y Values** - Tests retrieving proofs by Y values
+1. **Increment Counter with Commit** - Tests `increment_keyset_counter()` and persistence
+2. **Implicit Rollback on Drop** - Verifies automatic rollback when transactions are dropped
+3. **Explicit Rollback** - Tests manual `rollback()` calls
+4. **Transaction Reads** - Tests reading data within active transactions
+5. **Multiple Increments** - Tests sequential counter operations in one transaction
+6. **Transaction Atomicity** - Tests that rollback properly reverts ALL changes
+7. **Get Proofs by Y Values (Transaction)** - Tests retrieving proofs within transactions
+
+### Wallet Tests (direct wallet methods)
+
+Tests that use wallet database methods directly without explicit transactions:
+
+8. **Wallet Creation** - Tests creating a wallet with SQLite backend
+9. **Wallet Mint Management** - Tests adding, querying, and removing mints
+10. **Wallet Keyset Management** - Tests adding and querying keysets
+11. **Wallet Keyset Counter** - Tests keyset counter increment operations
+12. **Wallet Quote Operations** - Tests querying mint and melt quotes
+13. **Wallet Get Proofs by Y Values** - Tests retrieving proofs by Y values
 
 
 ### Key Features Tested
 ### Key Features Tested
 
 
+**Transaction Features:**
+- ✅ **Transaction atomicity** - All-or-nothing commits/rollbacks
+- ✅ **Isolation** - Uncommitted changes not visible outside transaction
+- ✅ **Durability** - Committed changes persist
+- ✅ **Implicit rollback** - Automatic cleanup on transaction drop
+- ✅ **Explicit rollback** - Manual transaction rollback
+
+**Wallet Features:**
 - ✅ **Wallet creation** - SQLite backend initialization
 - ✅ **Wallet creation** - SQLite backend initialization
 - ✅ **Mint management** - Add, query, and retrieve mint URLs
 - ✅ **Mint management** - Add, query, and retrieve mint URLs
 - ✅ **Keyset operations** - Add keysets and query by ID or mint
 - ✅ **Keyset operations** - Add keysets and query by ID or mint
@@ -66,11 +86,11 @@ Comprehensive tests for wallet database operations:
 Expected output for successful run:
 Expected output for successful run:
 
 
 ```
 ```
-Starting CDK FFI Wallet Tests
+Starting CDK FFI Wallet and Transaction Tests
 ==================================================
 ==================================================
 ... (test execution) ...
 ... (test execution) ...
 ==================================================
 ==================================================
-Test Results: 6 passed, 0 failed
+Test Results: 13 passed, 0 failed
 ==================================================
 ==================================================
 ```
 ```
 
 
@@ -103,6 +123,7 @@ When adding new tests:
 2. Add test to the `tests` list in `main()`
 2. Add test to the `tests` list in `main()`
 3. Use temporary databases for isolation
 3. Use temporary databases for isolation
 4. Follow existing patterns for setup/teardown
 4. Follow existing patterns for setup/teardown
+5. Clearly indicate if the test uses transactions or direct wallet methods
 
 
 ## Implementation Notes
 ## Implementation Notes
 
 
@@ -110,3 +131,5 @@ When adding new tests:
 - Each test is fully isolated with its own database
 - Each test is fully isolated with its own database
 - Tests clean up automatically via `finally` blocks
 - Tests clean up automatically via `finally` blocks
 - The script handles path resolution and library loading automatically
 - The script handles path resolution and library loading automatically
+- Transaction tests demonstrate ACID properties
+- Wallet tests demonstrate direct database operations

+ 373 - 32
crates/cdk-ffi/tests/test_transactions.py

@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 #!/usr/bin/env python3
 """
 """
-Test suite for CDK FFI wallet operations
+Test suite for CDK FFI wallet and transaction operations
 """
 """
 
 
 import asyncio
 import asyncio
@@ -29,6 +29,334 @@ sys.path.insert(0, str(bindings_path))
 import cdk_ffi
 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)
+
 async def test_wallet_creation():
 async def test_wallet_creation():
     """Test creating a wallet with SQLite backend"""
     """Test creating a wallet with SQLite backend"""
     print("\n=== Test: Wallet Creation ===")
     print("\n=== Test: Wallet Creation ===")
@@ -41,7 +369,7 @@ async def test_wallet_creation():
         db = cdk_ffi.create_wallet_db(backend)
         db = cdk_ffi.create_wallet_db(backend)
         print("✓ Wallet database created")
         print("✓ Wallet database created")
 
 
-        # Verify database is accessible by querying quotes
+        # Verify database is accessible
         mint_quotes = await db.get_mint_quotes()
         mint_quotes = await db.get_mint_quotes()
         assert isinstance(mint_quotes, list), "get_mint_quotes should return a list"
         assert isinstance(mint_quotes, list), "get_mint_quotes should return a list"
         print("✓ Wallet database accessible")
         print("✓ Wallet database accessible")
@@ -66,16 +394,20 @@ async def test_wallet_mint_management():
 
 
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
 
 
-        # Add mint
-        await db.add_mint(mint_url, None)
+        # Add mint (using transaction)
+        tx = await db.begin_db_transaction()
+        await tx.add_mint(mint_url, None)
+        await tx.commit()
         print("✓ Added mint to wallet")
         print("✓ Added mint to wallet")
 
 
-        # Get specific mint (verifies it was added)
+        # Get specific mint (read-only, can use db directly)
         await db.get_mint(mint_url)
         await db.get_mint(mint_url)
         print("✓ Retrieved mint from database")
         print("✓ Retrieved mint from database")
 
 
-        # Remove mint
-        await db.remove_mint(mint_url)
+        # Remove mint (using transaction)
+        tx = await db.begin_db_transaction()
+        await tx.remove_mint(mint_url)
+        await tx.commit()
         print("✓ Removed mint from wallet")
         print("✓ Removed mint from wallet")
 
 
         # Verify removal
         # Verify removal
@@ -104,27 +436,26 @@ async def test_wallet_keyset_management():
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
         keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
 
 
-        # Add mint first (foreign key requirement)
-        await db.add_mint(mint_url, None)
-        print("✓ Added mint")
-
-        # Add keyset
+        # Add mint and keyset (using transaction)
+        tx = await db.begin_db_transaction()
+        await tx.add_mint(mint_url, None)
         keyset_info = cdk_ffi.KeySetInfo(
         keyset_info = cdk_ffi.KeySetInfo(
             id=keyset_id.hex,
             id=keyset_id.hex,
             unit=cdk_ffi.CurrencyUnit.SAT(),
             unit=cdk_ffi.CurrencyUnit.SAT(),
             active=True,
             active=True,
             input_fee_ppk=0
             input_fee_ppk=0
         )
         )
-        await db.add_mint_keysets(mint_url, [keyset_info])
-        print("✓ Added keyset")
+        await tx.add_mint_keysets(mint_url, [keyset_info])
+        await tx.commit()
+        print("✓ Added mint and keyset")
 
 
-        # Query keyset by ID
+        # Query keyset by ID (read-only)
         keyset = await db.get_keyset_by_id(keyset_id)
         keyset = await db.get_keyset_by_id(keyset_id)
         assert keyset is not None, "Keyset should exist"
         assert keyset is not None, "Keyset should exist"
         assert keyset.id == keyset_id.hex, "Keyset ID should match"
         assert keyset.id == keyset_id.hex, "Keyset ID should match"
         print(f"✓ Retrieved keyset: {keyset.id}")
         print(f"✓ Retrieved keyset: {keyset.id}")
 
 
-        # Query keysets for mint
+        # Query keysets for mint (read-only)
         keysets = await db.get_mint_keysets(mint_url)
         keysets = await db.get_mint_keysets(mint_url)
         assert keysets is not None and len(keysets) > 0, "Should have keysets for mint"
         assert keysets is not None and len(keysets) > 0, "Should have keysets for mint"
         print(f"✓ Retrieved {len(keysets)} keyset(s) for mint")
         print(f"✓ Retrieved {len(keysets)} keyset(s) for mint")
@@ -150,29 +481,30 @@ async def test_wallet_keyset_counter():
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
         keyset_id = cdk_ffi.Id(hex="004146bdf4a9afab")
 
 
-        # Setup mint and keyset
-        await db.add_mint(mint_url, None)
+        # Setup (using transaction)
+        tx = await db.begin_db_transaction()
+        await tx.add_mint(mint_url, None)
         keyset_info = cdk_ffi.KeySetInfo(
         keyset_info = cdk_ffi.KeySetInfo(
             id=keyset_id.hex,
             id=keyset_id.hex,
             unit=cdk_ffi.CurrencyUnit.SAT(),
             unit=cdk_ffi.CurrencyUnit.SAT(),
             active=True,
             active=True,
             input_fee_ppk=0
             input_fee_ppk=0
         )
         )
-        await db.add_mint_keysets(mint_url, [keyset_info])
+        await tx.add_mint_keysets(mint_url, [keyset_info])
+        await tx.commit()
         print("✓ Setup complete")
         print("✓ Setup complete")
 
 
-        # Increment counter
-        counter1 = await db.increment_keyset_counter(keyset_id, 1)
+        # 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()
+
         print(f"✓ Counter after +1: {counter1}")
         print(f"✓ Counter after +1: {counter1}")
         assert counter1 == 1, f"Expected counter 1, got {counter1}"
         assert counter1 == 1, f"Expected counter 1, got {counter1}"
-
-        # Increment again
-        counter2 = await db.increment_keyset_counter(keyset_id, 5)
         print(f"✓ Counter after +5: {counter2}")
         print(f"✓ Counter after +5: {counter2}")
         assert counter2 == 6, f"Expected counter 6, got {counter2}"
         assert counter2 == 6, f"Expected counter 6, got {counter2}"
-
-        # Read current value (increment by 0)
-        counter3 = await db.increment_keyset_counter(keyset_id, 0)
         print(f"✓ Current counter: {counter3}")
         print(f"✓ Current counter: {counter3}")
         assert counter3 == 6, f"Expected counter 6, got {counter3}"
         assert counter3 == 6, f"Expected counter 6, got {counter3}"
 
 
@@ -196,16 +528,17 @@ async def test_wallet_quotes():
 
 
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
         mint_url = cdk_ffi.MintUrl(url="https://testmint.example.com")
 
 
-        # Add mint first
-        await db.add_mint(mint_url, None)
+        # Add mint (using transaction)
+        tx = await db.begin_db_transaction()
+        await tx.add_mint(mint_url, None)
+        await tx.commit()
         print("✓ Added mint")
         print("✓ Added mint")
 
 
-        # Query mint quotes (should be empty initially)
+        # Query quotes (read-only)
         mint_quotes = await db.get_mint_quotes()
         mint_quotes = await db.get_mint_quotes()
         assert isinstance(mint_quotes, list), "get_mint_quotes should return a list"
         assert isinstance(mint_quotes, list), "get_mint_quotes should return a list"
         print(f"✓ Retrieved {len(mint_quotes)} mint quote(s)")
         print(f"✓ Retrieved {len(mint_quotes)} mint quote(s)")
 
 
-        # Query melt quotes (should be empty initially)
         melt_quotes = await db.get_melt_quotes()
         melt_quotes = await db.get_melt_quotes()
         assert isinstance(melt_quotes, list), "get_melt_quotes should return a list"
         assert isinstance(melt_quotes, list), "get_melt_quotes should return a list"
         print(f"✓ Retrieved {len(melt_quotes)} melt quote(s)")
         print(f"✓ Retrieved {len(melt_quotes)} melt quote(s)")
@@ -242,10 +575,18 @@ async def test_wallet_proofs_by_ys():
 
 
 async def main():
 async def main():
     """Run all tests"""
     """Run all tests"""
-    print("Starting CDK FFI Wallet Tests")
+    print("Starting CDK FFI Wallet and Transaction Tests")
     print("=" * 50)
     print("=" * 50)
 
 
     tests = [
     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 Creation", test_wallet_creation),
         ("Wallet Mint Management", test_wallet_mint_management),
         ("Wallet Mint Management", test_wallet_mint_management),
         ("Wallet Keyset Management", test_wallet_keyset_management),
         ("Wallet Keyset Management", test_wallet_keyset_management),

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

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

+ 552 - 471
crates/cdk-redb/src/wallet/mod.rs

@@ -54,6 +54,29 @@ pub struct WalletRedbDatabase {
     db: Arc<Database>,
     db: Arc<Database>,
 }
 }
 
 
+/// Redb Wallet Transaction
+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 {
 impl WalletRedbDatabase {
     /// Create new [`WalletRedbDatabase`]
     /// Create new [`WalletRedbDatabase`]
     pub fn new(path: &Path) -> Result<Self, Error> {
     pub fn new(path: &Path) -> Result<Self, Error> {
@@ -189,45 +212,6 @@ impl WalletDatabase for WalletRedbDatabase {
     type Err = database::Error;
     type Err = database::Error;
 
 
     #[instrument(skip(self))]
     #[instrument(skip(self))]
-    async fn add_mint(
-        &self,
-        mint_url: MintUrl,
-        mint_info: Option<MintInfo>,
-    ) -> Result<(), Self::Err> {
-        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(
-                    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(&self, mint_url: MintUrl) -> Result<(), Self::Err> {
-        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)?;
-        }
-        write_txn.commit().map_err(Error::from)?;
-
-        Ok(())
-    }
-
-    #[instrument(skip(self))]
     async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
     async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err> {
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
         let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
@@ -262,149 +246,6 @@ impl WalletDatabase for WalletRedbDatabase {
     }
     }
 
 
     #[instrument(skip(self))]
     #[instrument(skip(self))]
-    async fn update_mint_url(
-        &self,
-        old_mint_url: MintUrl,
-        new_mint_url: MintUrl,
-    ) -> Result<(), Self::Err> {
-        // 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
-                })
-                .collect();
-
-            if !updated_proofs.is_empty() {
-                self.update_proofs(updated_proofs, vec![]).await?;
-            }
-        }
-
-        // Update mint quotes
-        {
-            let quotes = self.get_mint_quotes().await?;
-
-            let unix_time = unix_time();
-
-            let quotes: Vec<MintQuote> = quotes
-                .into_iter()
-                .filter_map(|mut q| {
-                    if q.expiry < unix_time {
-                        q.mint_url = new_mint_url.clone();
-                        Some(q)
-                    } else {
-                        None
-                    }
-                })
-                .collect();
-
-            for quote in quotes {
-                self.add_mint_quote(quote).await?;
-            }
-        }
-
-        Ok(())
-    }
-
-    #[instrument(skip(self))]
-    async fn add_mint_keysets(
-        &self,
-        mint_url: MintUrl,
-        keysets: Vec<KeySetInfo>,
-    ) -> Result<(), Self::Err> {
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
-
-        let mut existing_u32 = false;
-
-        {
-            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)?;
-
-            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)?;
-
-                    existing_keyset.map(|r| r.value().to_string())
-                };
-
-                let existing = u32_table
-                    .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
-                    .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 {
-                            println!("Breaking here");
-                            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
-                };
-
-                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_u32 {
-            tracing::warn!("Keyset already exists for keyset id");
-            write_txn.abort().map_err(Error::from)?;
-
-            return Err(database::Error::Duplicate);
-        }
-
-        write_txn.commit().map_err(Error::from)?;
-
-        Ok(())
-    }
-
-    #[instrument(skip(self))]
     async fn get_mint_keysets(
     async fn get_mint_keysets(
         &self,
         &self,
         mint_url: MintUrl,
         mint_url: MintUrl,
@@ -462,27 +303,6 @@ impl WalletDatabase for WalletRedbDatabase {
     }
     }
 
 
     #[instrument(skip_all)]
     #[instrument(skip_all)]
-    async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Self::Err> {
-        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_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err> {
     async fn get_mint_quote(&self, quote_id: &str) -> Result<Option<MintQuote>, Self::Err> {
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn
         let table = read_txn
@@ -512,43 +332,6 @@ impl WalletDatabase for WalletRedbDatabase {
     }
     }
 
 
     #[instrument(skip_all)]
     #[instrument(skip_all)]
-    async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
-        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_all)]
-    async fn add_melt_quote(&self, quote: wallet::MeltQuote) -> Result<(), Self::Err> {
-        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(
-                    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_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
     async fn get_melt_quote(&self, quote_id: &str) -> Result<Option<wallet::MeltQuote>, Self::Err> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn
         let table = read_txn
@@ -577,74 +360,6 @@ impl WalletDatabase for WalletRedbDatabase {
             .collect())
             .collect())
     }
     }
 
 
-    #[instrument(skip_all)]
-    async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Self::Err> {
-        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(())
-    }
-
-    #[instrument(skip_all)]
-    async fn add_keys(&self, keyset: KeySet) -> Result<(), Self::Err> {
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
-
-        keyset.verify_id()?;
-
-        let existing_keys;
-        let existing_u32;
-
-        {
-            let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
-
-            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();
-
-            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)?;
-
-            match existing {
-                None => existing_u32 = false,
-                Some(id) => {
-                    let id = Id::from_str(id.value())?;
-
-                    existing_u32 = id != keyset.id;
-                }
-            }
-        }
-
-        if existing_keys || existing_u32 {
-            tracing::warn!("Keys already exist for keyset id");
-            write_txn.abort().map_err(Error::from)?;
-
-            return Err(database::Error::Duplicate);
-        }
-
-        write_txn.commit().map_err(Error::from)?;
-
-        Ok(())
-    }
-
     #[instrument(skip(self), fields(keyset_id = %keyset_id))]
     #[instrument(skip(self), fields(keyset_id = %keyset_id))]
     async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, Self::Err> {
     async fn get_keys(&self, keyset_id: &Id) -> Result<Option<Keys>, Self::Err> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let read_txn = self.db.begin_read().map_err(Error::from)?;
@@ -660,83 +375,35 @@ impl WalletDatabase for WalletRedbDatabase {
         Ok(None)
         Ok(None)
     }
     }
 
 
-    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
-    async fn remove_keys(&self, keyset_id: &Id) -> Result<(), Self::Err> {
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
+    #[instrument(skip_all)]
+    async fn get_proofs(
+        &self,
+        mint_url: Option<MintUrl>,
+        unit: Option<CurrencyUnit>,
+        state: Option<Vec<State>>,
+        spending_conditions: Option<Vec<SpendingConditions>>,
+    ) -> Result<Vec<ProofInfo>, Self::Err> {
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
 
 
-        {
-            let mut table = write_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
+        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
 
 
-            table
-                .remove(keyset_id.to_string().as_str())
-                .map_err(Error::from)?;
-        }
+        let proofs: Vec<ProofInfo> = table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .filter_map(|(_k, v)| {
+                let mut proof = None;
 
 
-        write_txn.commit().map_err(Error::from)?;
+                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)
+                    }
+                }
 
 
-        Ok(())
-    }
-
-    #[instrument(skip(self, added, deleted_ys))]
-    async fn update_proofs(
-        &self,
-        added: Vec<ProofInfo>,
-        deleted_ys: Vec<PublicKey>,
-    ) -> Result<(), Self::Err> {
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
-
-        {
-            let mut table = write_txn.open_table(PROOFS_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)?;
-            }
-
-            for y in deleted_ys.iter() {
-                table.remove(y.to_bytes().as_slice()).map_err(Error::from)?;
-            }
-        }
-        write_txn.commit().map_err(Error::from)?;
-
-        Ok(())
-    }
-
-    #[instrument(skip_all)]
-    async fn get_proofs(
-        &self,
-        mint_url: Option<MintUrl>,
-        unit: Option<CurrencyUnit>,
-        state: Option<Vec<State>>,
-        spending_conditions: Option<Vec<SpendingConditions>>,
-    ) -> Result<Vec<ProofInfo>, Self::Err> {
-        let read_txn = self.db.begin_read().map_err(Error::from)?;
-
-        let table = read_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();
+                proof
+            })
+            .collect();
 
 
         Ok(proofs)
         Ok(proofs)
     }
     }
@@ -775,168 +442,582 @@ impl WalletDatabase for WalletRedbDatabase {
         Ok(proofs.iter().map(|p| u64::from(p.proof.amount)).sum())
         Ok(proofs.iter().map(|p| u64::from(p.proof.amount)).sum())
     }
     }
 
 
-    async fn update_proofs_state(
+    #[instrument(skip(self))]
+    async fn get_transaction(
         &self,
         &self,
-        ys: Vec<PublicKey>,
-        state: State,
-    ) -> Result<(), database::Error> {
+        transaction_id: TransactionId,
+    ) -> Result<Option<Transaction>, Self::Err> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let read_txn = self.db.begin_read().map_err(Error::from)?;
-        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
+        let table = read_txn
+            .open_table(TRANSACTIONS_TABLE)
+            .map_err(Error::from)?;
 
 
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        if let Some(transaction) = table.get(transaction_id.as_slice()).map_err(Error::from)? {
+            return Ok(serde_json::from_str(transaction.value()).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)?;
+        Ok(None)
+    }
 
 
-            let mut proof_info =
-                serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
+    #[instrument(skip(self))]
+    async fn list_transactions(
+        &self,
+        mint_url: Option<MintUrl>,
+        direction: Option<TransactionDirection>,
+        unit: Option<CurrencyUnit>,
+    ) -> Result<Vec<Transaction>, Self::Err> {
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
 
 
-            proof_info.state = state;
+        let table = read_txn
+            .open_table(TRANSACTIONS_TABLE)
+            .map_err(Error::from)?;
 
 
-            {
-                let mut table = write_txn.open_table(PROOFS_TABLE).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)?;
+        let transactions: Vec<Transaction> = table
+            .iter()
+            .map_err(Error::from)?
+            .flatten()
+            .filter_map(|(_k, v)| {
+                let mut transaction = None;
+
+                if let Ok(tx) = serde_json::from_str::<Transaction>(v.value()) {
+                    if tx.matches_conditions(&mint_url, &direction, &unit) {
+                        transaction = Some(tx)
+                    }
+                }
+
+                transaction
+            })
+            .collect();
+
+        Ok(transactions)
+    }
+
+    async fn begin_db_transaction(
+        &self,
+    ) -> Result<
+        Box<dyn cdk_common::database::WalletDatabaseTransaction<Self::Err> + Send + Sync>,
+        Self::Err,
+    > {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        Ok(Box::new(RedbWalletTransaction::new(write_txn)))
+    }
+}
+
+#[async_trait]
+impl cdk_common::database::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)?;
+
+                Ok(Some(keyset))
             }
             }
+            None => Ok(None),
+        };
+
+        result
+    }
+
+    #[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)?;
+
+        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)?);
         }
         }
 
 
-        write_txn.commit().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)?;
         Ok(())
         Ok(())
     }
     }
 
 
-    #[instrument(skip(self), fields(keyset_id = %keyset_id))]
-    async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<u32, Self::Err> {
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
+    #[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)?;
+        Ok(())
+    }
 
 
-        let current_counter;
-        let new_counter;
+    #[instrument(skip(self))]
+    async fn update_mint_url(
+        &mut self,
+        old_mint_url: MintUrl,
+        new_mint_url: MintUrl,
+    ) -> Result<(), database::Error> {
+        // Update proofs table
         {
         {
-            let table = write_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
-            let counter = table
-                .get(keyset_id.to_string().as_str())
+            let proofs = self
+                .get_proofs(Some(old_mint_url.clone()), None, None, None)
+                .await
                 .map_err(Error::from)?;
                 .map_err(Error::from)?;
 
 
-            current_counter = match counter {
-                Some(c) => c.value(),
-                None => 0,
-            };
+            // Proofs with new url
+            let updated_proofs: Vec<ProofInfo> = proofs
+                .clone()
+                .into_iter()
+                .map(|mut p| {
+                    p.mint_url = new_mint_url.clone();
+                    p
+                })
+                .collect();
 
 
-            new_counter = current_counter + count;
+            if !updated_proofs.is_empty() {
+                self.update_proofs(updated_proofs, vec![]).await?;
+            }
         }
         }
 
 
+        // Update mint quotes
         {
         {
-            let mut table = write_txn.open_table(KEYSET_COUNTER).map_err(Error::from)?;
-
-            table
-                .insert(keyset_id.to_string().as_str(), new_counter)
+            let read_txn = self.txn()?;
+            let mut table = read_txn
+                .open_table(MINT_QUOTES_TABLE)
                 .map_err(Error::from)?;
                 .map_err(Error::from)?;
+
+            let unix_time = unix_time();
+
+            let quotes = 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 {
+                        q.mint_url = new_mint_url.clone();
+                        Some(q)
+                    } else {
+                        None
+                    }
+                })
+                .collect::<Vec<_>>();
+
+            for quote in quotes {
+                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(new_counter)
+        Ok(())
     }
     }
 
 
     #[instrument(skip(self))]
     #[instrument(skip(self))]
-    async fn add_transaction(&self, transaction: Transaction) -> Result<(), Self::Err> {
-        let id = transaction.id();
+    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 write_txn = self.db.begin_write().map_err(Error::from)?;
+        let mut existing_u32 = false;
 
 
-        {
-            let mut table = write_txn
-                .open_table(TRANSACTIONS_TABLE)
+        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)?;
+
+                existing_keyset.map(|r| r.value().to_string())
+            };
+
+            let existing = u32_table
+                .insert(u32::from(keyset.id), keyset.id.to_string().as_str())
                 .map_err(Error::from)?;
                 .map_err(Error::from)?;
-            table
+
+            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
+            };
+
+            keysets_table
                 .insert(
                 .insert(
-                    id.as_slice(),
-                    serde_json::to_string(&transaction)
+                    keyset.id.to_bytes().as_slice(),
+                    serde_json::to_string(&keyset)
                         .map_err(Error::from)?
                         .map_err(Error::from)?
                         .as_str(),
                         .as_str(),
                 )
                 )
                 .map_err(Error::from)?;
                 .map_err(Error::from)?;
         }
         }
 
 
-        write_txn.commit().map_err(Error::from)?;
+        if existing_u32 {
+            tracing::warn!("Keyset already exists for keyset id");
+            return Err(database::Error::Duplicate);
+        }
 
 
         Ok(())
         Ok(())
     }
     }
 
 
-    #[instrument(skip(self))]
-    async fn get_transaction(
-        &self,
-        transaction_id: TransactionId,
-    ) -> Result<Option<Transaction>, Self::Err> {
-        let read_txn = self.db.begin_read().map_err(Error::from)?;
-        let table = read_txn
-            .open_table(TRANSACTIONS_TABLE)
+    #[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)?);
+        }
+
+        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)?;
             .map_err(Error::from)?;
+        Ok(())
+    }
 
 
-        if let Some(transaction) = table.get(transaction_id.as_slice()).map_err(Error::from)? {
-            return Ok(serde_json::from_str(transaction.value()).map_err(Error::from)?);
+    #[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)?;
+
+        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)?);
         }
         }
 
 
         Ok(None)
         Ok(None)
     }
     }
 
 
-    #[instrument(skip(self))]
-    async fn list_transactions(
-        &self,
-        mint_url: Option<MintUrl>,
-        direction: Option<TransactionDirection>,
-        unit: Option<CurrencyUnit>,
-    ) -> Result<Vec<Transaction>, Self::Err> {
-        let read_txn = self.db.begin_read().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(())
+    }
 
 
-        let table = read_txn
-            .open_table(TRANSACTIONS_TABLE)
+    #[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(())
+    }
+
+    #[instrument(skip_all)]
+    async fn add_keys(&mut self, keyset: KeySet) -> Result<(), database::Error> {
+        let txn = self.txn()?;
+
+        keyset.verify_id()?;
+
+        let mut table = txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
+
+        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();
+
+        let mut table = 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)?;
             .map_err(Error::from)?;
 
 
-        let transactions: Vec<Transaction> = table
+        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);
+        }
+
+        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)?;
+
+        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()
             .iter()
             .map_err(Error::from)?
             .map_err(Error::from)?
             .flatten()
             .flatten()
             .filter_map(|(_k, v)| {
             .filter_map(|(_k, v)| {
-                let mut transaction = None;
+                let mut proof = None;
 
 
-                if let Ok(tx) = serde_json::from_str::<Transaction>(v.value()) {
-                    if tx.matches_conditions(&mint_url, &direction, &unit) {
-                        transaction = Some(tx)
+                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)
                     }
                     }
                 }
                 }
 
 
-                transaction
+                proof
             })
             })
             .collect();
             .collect();
 
 
-        Ok(transactions)
+        Ok(proofs)
     }
     }
 
 
-    #[instrument(skip(self))]
-    async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), Self::Err> {
-        let write_txn = self.db.begin_write().map_err(Error::from)?;
+    #[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)?;
 
 
-        {
-            let mut table = write_txn
-                .open_table(TRANSACTIONS_TABLE)
+        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)?;
                 .map_err(Error::from)?;
+        }
+
+        for y in deleted_ys.iter() {
+            table.remove(y.to_bytes().as_slice()).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)?;
+
+            let mut proof_info =
+                serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
+            drop(proof);
+
+            proof_info.state = state;
+
             table
             table
-                .remove(transaction_id.as_slice())
+                .insert(
+                    y_slice.as_slice(),
+                    serde_json::to_string(&proof_info)
+                        .map_err(Error::from)?
+                        .as_str(),
+                )
                 .map_err(Error::from)?;
                 .map_err(Error::from)?;
         }
         }
 
 
-        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)
+    }
+
+    #[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)?;
+        Ok(())
+    }
+
+    #[instrument(skip(self))]
+    async fn remove_transaction(
+        &mut 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)?;
+        Ok(())
+    }
+}
+
+#[async_trait]
+impl cdk_common::database::DbTransactionFinalizer for RedbWalletTransaction {
+    type Err = database::Error;
 
 
+    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(())
         Ok(())
     }
     }
+
+    async fn rollback(mut self: Box<Self>) -> Result<(), database::Error> {
+        if let Some(txn) = self.write_txn.take() {
+            txn.abort().map_err(Error::from)?;
+        }
+        Ok(())
+    }
+}
+
+impl Drop for RedbWalletTransaction {
+    fn drop(&mut self) {
+        if let Some(txn) = self.write_txn.take() {
+            let _ = txn.abort();
+        }
+    }
 }
 }

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

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

Разница между файлами не показана из-за своего большого размера
+ 832 - 548
crates/cdk-sql-common/src/wallet/mod.rs


+ 2 - 0
crates/cdk-sqlite/src/lib.rs

@@ -6,6 +6,8 @@
 mod async_sqlite;
 mod async_sqlite;
 mod common;
 mod common;
 
 
+pub use common::SqliteConnectionManager;
+
 #[cfg(feature = "mint")]
 #[cfg(feature = "mint")]
 pub mod mint;
 pub mod mint;
 #[cfg(feature = "wallet")]
 #[cfg(feature = "wallet")]

+ 18 - 5
crates/cdk-sqlite/src/wallet/mod.rs

@@ -36,10 +36,14 @@ mod tests {
         let mint_info = MintInfo::new().description("test");
         let mint_info = MintInfo::new().description("test");
         let mint_url = MintUrl::from_str("https://mint.xyz").unwrap();
         let mint_url = MintUrl::from_str("https://mint.xyz").unwrap();
 
 
-        db.add_mint(mint_url.clone(), Some(mint_info.clone()))
+        let mut tx = db.begin_db_transaction().await.expect("tx");
+
+        tx.add_mint(mint_url.clone(), Some(mint_info.clone()))
             .await
             .await
             .unwrap();
             .unwrap();
 
 
+        tx.commit().await.expect("commit");
+
         let res = db.get_mint(mint_url).await.unwrap();
         let res = db.get_mint(mint_url).await.unwrap();
         assert_eq!(mint_info, res.clone().unwrap());
         assert_eq!(mint_info, res.clone().unwrap());
         assert_eq!("test", &res.unwrap().description.unwrap());
         assert_eq!("test", &res.unwrap().description.unwrap());
@@ -94,11 +98,15 @@ mod tests {
         let proof_info =
         let proof_info =
             ProofInfo::new(proof, mint_url.clone(), State::Unspent, CurrencyUnit::Sat).unwrap();
             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
         // Store the proof in the database
-        db.update_proofs(vec![proof_info.clone()], vec![])
+        tx.update_proofs(vec![proof_info.clone()], vec![])
             .await
             .await
             .unwrap();
             .unwrap();
 
 
+        tx.commit().await.expect("commit");
+
         // Retrieve the proof from the database
         // Retrieve the proof from the database
         let retrieved_proofs = db
         let retrieved_proofs = db
             .get_proofs(
             .get_proofs(
@@ -154,6 +162,8 @@ mod tests {
             PaymentMethod::Custom("custom".to_string()),
             PaymentMethod::Custom("custom".to_string()),
         ];
         ];
 
 
+        let mut tx = db.begin_db_transaction().await.expect("begin");
+
         for (i, payment_method) in payment_methods.iter().enumerate() {
         for (i, payment_method) in payment_methods.iter().enumerate() {
             let quote = MintQuote {
             let quote = MintQuote {
                 id: format!("test_quote_{}", i),
                 id: format!("test_quote_{}", i),
@@ -170,14 +180,15 @@ mod tests {
             };
             };
 
 
             // Store the quote
             // Store the quote
-            db.add_mint_quote(quote.clone()).await.unwrap();
+            tx.add_mint_quote(quote.clone()).await.unwrap();
 
 
             // Retrieve and verify
             // Retrieve and verify
-            let retrieved = db.get_mint_quote(&quote.id).await.unwrap().unwrap();
+            let retrieved = tx.get_mint_quote(&quote.id).await.unwrap().unwrap();
             assert_eq!(retrieved.payment_method, *payment_method);
             assert_eq!(retrieved.payment_method, *payment_method);
             assert_eq!(retrieved.amount_issued, Amount::from(0));
             assert_eq!(retrieved.amount_issued, Amount::from(0));
             assert_eq!(retrieved.amount_paid, Amount::from(0));
             assert_eq!(retrieved.amount_paid, Amount::from(0));
         }
         }
+        tx.commit().await.expect("commit");
     }
     }
 
 
     #[tokio::test]
     #[tokio::test]
@@ -226,7 +237,9 @@ mod tests {
         }
         }
 
 
         // Store all proofs in the database
         // Store all proofs in the database
-        db.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
+        let mut tx = db.begin_db_transaction().await.unwrap();
+        tx.update_proofs(proof_infos.clone(), vec![]).await.unwrap();
+        tx.commit().await.unwrap();
 
 
         // Test 1: Retrieve all proofs by their Y values
         // Test 1: Retrieve all proofs by their Y values
         let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap();
         let retrieved_proofs = db.get_proofs_by_ys(expected_ys.clone()).await.unwrap();

+ 18 - 19
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -286,21 +286,27 @@ impl AuthWallet {
     /// Get Auth Token
     /// Get Auth Token
     #[instrument(skip(self))]
     #[instrument(skip(self))]
     pub async fn get_blind_auth_token(&self) -> Result<Option<BlindAuthToken>, Error> {
     pub async fn get_blind_auth_token(&self) -> Result<Option<BlindAuthToken>, Error> {
-        let unspent = self.get_unspent_auth_proofs().await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
 
 
-        let auth_proof = match unspent.first() {
+        let auth_proof = match tx
+            .get_proofs(
+                Some(self.mint_url.clone()),
+                Some(CurrencyUnit::Auth),
+                Some(vec![State::Unspent]),
+                None,
+            )
+            .await?
+            .pop()
+        {
             Some(proof) => {
             Some(proof) => {
-                self.localstore
-                    .update_proofs(vec![], vec![proof.y()?])
-                    .await?;
-                proof
+                tx.update_proofs(vec![], vec![proof.proof.y()?]).await?;
+                tx.commit().await?;
+                proof.proof.try_into()?
             }
             }
             None => return Ok(None),
             None => return Ok(None),
         };
         };
 
 
-        Ok(Some(BlindAuthToken {
-            auth_proof: auth_proof.clone(),
-        }))
+        Ok(Some(BlindAuthToken { auth_proof }))
     }
     }
 
 
     /// Auth for request
     /// Auth for request
@@ -337,15 +343,6 @@ impl AuthWallet {
     #[instrument(skip(self))]
     #[instrument(skip(self))]
     pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, Error> {
     pub async fn mint_blind_auth(&self, amount: Amount) -> Result<Proofs, Error> {
         tracing::debug!("Minting {} blind auth proofs", amount);
         tracing::debug!("Minting {} blind auth proofs", amount);
-        // Check that mint is in store of mints
-        if self
-            .localstore
-            .get_mint(self.mint_url.clone())
-            .await?
-            .is_none()
-        {
-            self.get_mint_info().await?;
-        }
 
 
         let auth_token = self.auth_client.get_auth_token().await?;
         let auth_token = self.auth_client.get_auth_token().await?;
 
 
@@ -455,7 +452,9 @@ impl AuthWallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
 
         // Add new proofs to store
         // Add new proofs to store
-        self.localstore.update_proofs(proof_infos, vec![]).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.update_proofs(proof_infos, vec![]).await?;
+        tx.commit().await?;
 
 
         Ok(proofs)
         Ok(proofs)
     }
     }

+ 44 - 32
crates/cdk/src/wallet/issue/issue_bolt11.rs

@@ -90,7 +90,9 @@ impl Wallet {
             Some(secret_key),
             Some(secret_key),
         );
         );
 
 
-        self.localstore.add_mint_quote(quote.clone()).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.add_mint_quote(quote.clone()).await?;
+        tx.commit().await?;
 
 
         Ok(quote)
         Ok(quote)
     }
     }
@@ -103,18 +105,22 @@ impl Wallet {
     ) -> Result<MintQuoteBolt11Response<String>, Error> {
     ) -> Result<MintQuoteBolt11Response<String>, Error> {
         let response = self.client.get_mint_quote_status(quote_id).await?;
         let response = self.client.get_mint_quote_status(quote_id).await?;
 
 
-        match self.localstore.get_mint_quote(quote_id).await? {
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        match tx.get_mint_quote(quote_id).await? {
             Some(quote) => {
             Some(quote) => {
                 let mut quote = quote;
                 let mut quote = quote;
 
 
                 quote.state = response.state;
                 quote.state = response.state;
-                self.localstore.add_mint_quote(quote).await?;
+                tx.add_mint_quote(quote).await?;
             }
             }
             None => {
             None => {
                 tracing::info!("Quote mint {} unknown", quote_id);
                 tracing::info!("Quote mint {} unknown", quote_id);
             }
             }
         }
         }
 
 
+        tx.commit().await?;
+
         Ok(response)
         Ok(response)
     }
     }
 
 
@@ -133,7 +139,9 @@ impl Wallet {
                     .await?;
                     .await?;
                 total_amount += proofs.total_amount()?;
                 total_amount += proofs.total_amount()?;
             } else if mint_quote.expiry.le(&unix_time()) {
             } else if mint_quote.expiry.le(&unix_time()) {
-                self.localstore.remove_mint_quote(&mint_quote.id).await?;
+                let mut tx = self.localstore.begin_db_transaction().await?;
+                tx.remove_mint_quote(&mint_quote.id).await?;
+                tx.commit().await?;
             }
             }
         }
         }
         Ok(total_amount)
         Ok(total_amount)
@@ -192,8 +200,13 @@ impl Wallet {
         amount_split_target: SplitTarget,
         amount_split_target: SplitTarget,
         spending_conditions: Option<SpendingConditions>,
         spending_conditions: Option<SpendingConditions>,
     ) -> Result<Proofs, Error> {
     ) -> Result<Proofs, Error> {
-        let quote_info = self
-            .localstore
+        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 mut tx = self.localstore.begin_db_transaction().await?;
+        let quote_info = tx
             .get_mint_quote(quote_id)
             .get_mint_quote(quote_id)
             .await?
             .await?
             .ok_or(Error::UnknownQuote)?;
             .ok_or(Error::UnknownQuote)?;
@@ -215,14 +228,9 @@ impl Wallet {
             tracing::warn!("Attempting to mint with expired quote.");
             tracing::warn!("Attempting to mint with expired quote.");
         }
         }
 
 
-        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 split_target = match amount_split_target {
         let split_target = match amount_split_target {
             SplitTarget::None => {
             SplitTarget::None => {
-                self.determine_split_target_values(amount_mintable, &fee_and_amounts)
+                self.determine_split_target_values(&mut tx, amount_mintable, &fee_and_amounts)
                     .await?
                     .await?
             }
             }
             s => s,
             s => s,
@@ -248,8 +256,7 @@ impl Wallet {
                 );
                 );
 
 
                 // Atomically get the counter range we need
                 // Atomically get the counter range we need
-                let new_counter = self
-                    .localstore
+                let new_counter = tx
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .await?;
                     .await?;
 
 
@@ -276,6 +283,8 @@ impl Wallet {
             request.sign(secret_key)?;
             request.sign(secret_key)?;
         }
         }
 
 
+        tx.commit().await?;
+
         let mint_res = self.client.post_mint(request).await?;
         let mint_res = self.client.post_mint(request).await?;
 
 
         let keys = self.load_keyset_keys(active_keyset_id).await?;
         let keys = self.load_keyset_keys(active_keyset_id).await?;
@@ -299,8 +308,10 @@ impl Wallet {
             &keys,
             &keys,
         )?;
         )?;
 
 
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
         // Remove filled quote from store
         // Remove filled quote from store
-        self.localstore.remove_mint_quote(&quote_info.id).await?;
+        tx.remove_mint_quote(&quote_info.id).await?;
 
 
         let proof_infos = proofs
         let proof_infos = proofs
             .iter()
             .iter()
@@ -315,25 +326,26 @@ impl Wallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
 
         // Add new proofs to store
         // Add new proofs to store
-        self.localstore.update_proofs(proof_infos, vec![]).await?;
+        tx.update_proofs(proof_infos, vec![]).await?;
 
 
         // Add transaction to store
         // Add transaction to store
-        self.localstore
-            .add_transaction(Transaction {
-                mint_url: self.mint_url.clone(),
-                direction: TransactionDirection::Incoming,
-                amount: proofs.total_amount()?,
-                fee: Amount::ZERO,
-                unit: self.unit.clone(),
-                ys: proofs.ys()?,
-                timestamp: unix_time,
-                memo: None,
-                metadata: HashMap::new(),
-                quote_id: Some(quote_id.to_string()),
-                payment_request: Some(quote_info.request),
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Incoming,
+            amount: proofs.total_amount()?,
+            fee: Amount::ZERO,
+            unit: self.unit.clone(),
+            ys: proofs.ys()?,
+            timestamp: unix_time,
+            memo: None,
+            metadata: HashMap::new(),
+            quote_id: Some(quote_id.to_string()),
+            payment_request: Some(quote_info.request),
+            payment_proof: None,
+        })
+        .await?;
+
+        tx.commit().await?;
 
 
         Ok(proofs)
         Ok(proofs)
     }
     }

+ 52 - 35
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -67,7 +67,9 @@ impl Wallet {
             Some(secret_key),
             Some(secret_key),
         );
         );
 
 
-        self.localstore.add_mint_quote(quote.clone()).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.add_mint_quote(quote.clone()).await?;
+        tx.commit().await?;
 
 
         Ok(quote)
         Ok(quote)
     }
     }
@@ -81,7 +83,13 @@ impl Wallet {
         amount_split_target: SplitTarget,
         amount_split_target: SplitTarget,
         spending_conditions: Option<SpendingConditions>,
         spending_conditions: Option<SpendingConditions>,
     ) -> Result<Proofs, Error> {
     ) -> Result<Proofs, Error> {
-        let quote_info = self.localstore.get_mint_quote(quote_id).await?;
+        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 mut tx = self.localstore.begin_db_transaction().await?;
+        let quote_info = tx.get_mint_quote(quote_id).await?;
 
 
         let quote_info = if let Some(quote) = quote_info {
         let quote_info = if let Some(quote) = quote_info {
             if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) {
             if quote.expiry.le(&unix_time()) && quote.expiry.ne(&0) {
@@ -93,19 +101,21 @@ impl Wallet {
             return Err(Error::UnknownQuote);
             return Err(Error::UnknownQuote);
         };
         };
 
 
-        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 amount = match amount {
-            Some(amount) => amount,
+        let (mut tx, quote_info, amount) = match amount {
+            Some(amount) => (tx, quote_info, amount),
             None => {
             None => {
                 // If an amount it not supplied with check the status of the quote
                 // If an amount it not supplied with check the status of the quote
                 // The mint will tell us how much can be minted
                 // The mint will tell us how much can be minted
+                tx.commit().await?;
                 let state = self.mint_bolt12_quote_state(quote_id).await?;
                 let state = self.mint_bolt12_quote_state(quote_id).await?;
 
 
-                state.amount_paid - state.amount_issued
+                let mut tx = self.localstore.begin_db_transaction().await?;
+                let quote_info = tx
+                    .get_mint_quote(quote_id)
+                    .await?
+                    .ok_or(Error::UnknownQuote)?;
+
+                (tx, quote_info, state.amount_paid - state.amount_issued)
             }
             }
         };
         };
 
 
@@ -116,7 +126,7 @@ impl Wallet {
 
 
         let split_target = match amount_split_target {
         let split_target = match amount_split_target {
             SplitTarget::None => {
             SplitTarget::None => {
-                self.determine_split_target_values(amount, &fee_and_amounts)
+                self.determine_split_target_values(&mut tx, amount, &fee_and_amounts)
                     .await?
                     .await?
             }
             }
             s => s,
             s => s,
@@ -141,8 +151,7 @@ impl Wallet {
                 );
                 );
 
 
                 // Atomically get the counter range we need
                 // Atomically get the counter range we need
-                let new_counter = self
-                    .localstore
+                let new_counter = tx
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .increment_keyset_counter(&active_keyset_id, num_secrets)
                     .await?;
                     .await?;
 
 
@@ -172,8 +181,12 @@ impl Wallet {
             return Err(Error::SignatureMissingOrInvalid);
             return Err(Error::SignatureMissingOrInvalid);
         }
         }
 
 
+        tx.commit().await?;
+
         let mint_res = self.client.post_mint(request).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?;
         let keys = self.load_keyset_keys(active_keyset_id).await?;
 
 
         // Verify the signature DLEQ is valid
         // Verify the signature DLEQ is valid
@@ -196,14 +209,13 @@ impl Wallet {
         )?;
         )?;
 
 
         // Remove filled quote from store
         // Remove filled quote from store
-        let mut quote_info = self
-            .localstore
+        let mut quote_info = tx
             .get_mint_quote(quote_id)
             .get_mint_quote(quote_id)
             .await?
             .await?
             .ok_or(Error::UnpaidQuote)?;
             .ok_or(Error::UnpaidQuote)?;
         quote_info.amount_issued += proofs.total_amount()?;
         quote_info.amount_issued += proofs.total_amount()?;
 
 
-        self.localstore.add_mint_quote(quote_info.clone()).await?;
+        tx.add_mint_quote(quote_info.clone()).await?;
 
 
         let proof_infos = proofs
         let proof_infos = proofs
             .iter()
             .iter()
@@ -218,25 +230,26 @@ impl Wallet {
             .collect::<Result<Vec<ProofInfo>, _>>()?;
             .collect::<Result<Vec<ProofInfo>, _>>()?;
 
 
         // Add new proofs to store
         // Add new proofs to store
-        self.localstore.update_proofs(proof_infos, vec![]).await?;
+        tx.update_proofs(proof_infos, vec![]).await?;
 
 
         // Add transaction to store
         // Add transaction to store
-        self.localstore
-            .add_transaction(Transaction {
-                mint_url: self.mint_url.clone(),
-                direction: TransactionDirection::Incoming,
-                amount: proofs.total_amount()?,
-                fee: Amount::ZERO,
-                unit: self.unit.clone(),
-                ys: proofs.ys()?,
-                timestamp: unix_time(),
-                memo: None,
-                metadata: HashMap::new(),
-                quote_id: Some(quote_id.to_string()),
-                payment_request: Some(quote_info.request),
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Incoming,
+            amount: proofs.total_amount()?,
+            fee: Amount::ZERO,
+            unit: self.unit.clone(),
+            ys: proofs.ys()?,
+            timestamp: unix_time(),
+            memo: None,
+            metadata: HashMap::new(),
+            quote_id: Some(quote_id.to_string()),
+            payment_request: Some(quote_info.request),
+            payment_proof: None,
+        })
+        .await?;
+
+        tx.commit().await?;
 
 
         Ok(proofs)
         Ok(proofs)
     }
     }
@@ -249,19 +262,23 @@ impl Wallet {
     ) -> Result<MintQuoteBolt12Response<String>, Error> {
     ) -> Result<MintQuoteBolt12Response<String>, Error> {
         let response = self.client.get_mint_quote_bolt12_status(quote_id).await?;
         let response = self.client.get_mint_quote_bolt12_status(quote_id).await?;
 
 
-        match self.localstore.get_mint_quote(quote_id).await? {
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        match tx.get_mint_quote(quote_id).await? {
             Some(quote) => {
             Some(quote) => {
                 let mut quote = quote;
                 let mut quote = quote;
                 quote.amount_issued = response.amount_issued;
                 quote.amount_issued = response.amount_issued;
                 quote.amount_paid = response.amount_paid;
                 quote.amount_paid = response.amount_paid;
 
 
-                self.localstore.add_mint_quote(quote).await?;
+                tx.add_mint_quote(quote).await?;
             }
             }
             None => {
             None => {
                 tracing::info!("Quote mint {} unknown", quote_id);
                 tracing::info!("Quote mint {} unknown", quote_id);
             }
             }
         }
         }
 
 
+        tx.commit().await?;
+
         Ok(response)
         Ok(response)
     }
     }
 }
 }

+ 39 - 31
crates/cdk/src/wallet/melt/melt_bolt11.rs

@@ -91,7 +91,9 @@ impl Wallet {
             payment_method: PaymentMethod::Bolt11,
             payment_method: PaymentMethod::Bolt11,
         };
         };
 
 
-        self.localstore.add_melt_quote(quote.clone()).await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.add_melt_quote(quote.clone()).await?;
+        tx.commit().await?;
 
 
         Ok(quote)
         Ok(quote)
     }
     }
@@ -104,25 +106,29 @@ impl Wallet {
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
     ) -> Result<MeltQuoteBolt11Response<String>, Error> {
         let response = self.client.get_melt_quote_status(quote_id).await?;
         let response = self.client.get_melt_quote_status(quote_id).await?;
 
 
-        match self.localstore.get_melt_quote(quote_id).await? {
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        match tx.get_melt_quote(quote_id).await? {
             Some(quote) => {
             Some(quote) => {
                 let mut quote = quote;
                 let mut quote = quote;
 
 
                 if let Err(e) = self
                 if let Err(e) = self
-                    .add_transaction_for_pending_melt(&quote, &response)
+                    .add_transaction_for_pending_melt(&mut tx, &quote, &response)
                     .await
                     .await
                 {
                 {
                     tracing::error!("Failed to add transaction for pending melt: {}", e);
                     tracing::error!("Failed to add transaction for pending melt: {}", e);
                 }
                 }
 
 
                 quote.state = response.state;
                 quote.state = response.state;
-                self.localstore.add_melt_quote(quote).await?;
+                tx.add_melt_quote(quote).await?;
             }
             }
             None => {
             None => {
                 tracing::info!("Quote melt {} unknown", quote_id);
                 tracing::info!("Quote melt {} unknown", quote_id);
             }
             }
         }
         }
 
 
+        tx.commit().await?;
+
         Ok(response)
         Ok(response)
     }
     }
 
 
@@ -141,8 +147,9 @@ impl Wallet {
         proofs: Proofs,
         proofs: Proofs,
         metadata: HashMap<String, String>,
         metadata: HashMap<String, String>,
     ) -> Result<Melted, Error> {
     ) -> Result<Melted, Error> {
-        let mut quote_info = self
-            .localstore
+        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        let mut quote_info = tx
             .get_melt_quote(quote_id)
             .get_melt_quote(quote_id)
             .await?
             .await?
             .ok_or(Error::UnknownQuote)?;
             .ok_or(Error::UnknownQuote)?;
@@ -163,9 +170,8 @@ impl Wallet {
             .into_iter()
             .into_iter()
             .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
             .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
             .collect::<Result<Vec<ProofInfo>, _>>()?;
-        self.localstore.update_proofs(proofs_info, vec![]).await?;
 
 
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
+        tx.update_proofs(proofs_info, vec![]).await?;
 
 
         // Calculate change accounting for input fees
         // Calculate change accounting for input fees
         // The mint deducts input fees from available funds before calculating change
         // The mint deducts input fees from available funds before calculating change
@@ -187,8 +193,7 @@ impl Wallet {
             );
             );
 
 
             // Atomically get the counter range we need
             // Atomically get the counter range we need
-            let new_counter = self
-                .localstore
+            let new_counter = tx
                 .increment_keyset_counter(&active_keyset_id, num_secrets)
                 .increment_keyset_counter(&active_keyset_id, num_secrets)
                 .await?;
                 .await?;
 
 
@@ -203,6 +208,8 @@ impl Wallet {
             Some(premint_secrets.blinded_messages()),
             Some(premint_secrets.blinded_messages()),
         );
         );
 
 
+        tx.commit().await?;
+
         let melt_response = match quote_info.payment_method {
         let melt_response = match quote_info.payment_method {
             cdk_common::PaymentMethod::Bolt11 => {
             cdk_common::PaymentMethod::Bolt11 => {
                 self.try_proof_operation_or_reclaim(
                 self.try_proof_operation_or_reclaim(
@@ -282,34 +289,35 @@ impl Wallet {
             None => Vec::new(),
             None => Vec::new(),
         };
         };
 
 
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
         quote_info.state = cdk_common::MeltQuoteState::Paid;
         quote_info.state = cdk_common::MeltQuoteState::Paid;
 
 
         let payment_request = quote_info.request.clone();
         let payment_request = quote_info.request.clone();
-
-        self.localstore.add_melt_quote(quote_info).await?;
+        tx.add_melt_quote(quote_info).await?;
 
 
         let deleted_ys = proofs.ys()?;
         let deleted_ys = proofs.ys()?;
-        self.localstore
-            .update_proofs(change_proof_infos, deleted_ys)
-            .await?;
+
+        tx.update_proofs(change_proof_infos, deleted_ys).await?;
 
 
         // Add transaction to store
         // 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,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Outgoing,
+            amount: melted.amount,
+            fee: melted.fee_paid,
+            unit: self.unit.clone(),
+            ys: proofs.ys()?,
+            timestamp: unix_time(),
+            memo: None,
+            metadata,
+            quote_id: Some(quote_id.to_string()),
+            payment_request: Some(payment_request),
+            payment_proof: payment_preimage,
+        })
+        .await?;
+
+        tx.commit().await?;
 
 
         Ok(melted)
         Ok(melted)
     }
     }

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

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

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

@@ -1,8 +1,9 @@
 use std::collections::HashMap;
 use std::collections::HashMap;
 
 
+use cdk_common::database::DynWalletDatabaseTransaction;
 use cdk_common::util::unix_time;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection};
 use cdk_common::wallet::{MeltQuote, Transaction, TransactionDirection};
-use cdk_common::{Error, MeltQuoteBolt11Response, MeltQuoteState, ProofsMethods};
+use cdk_common::{Error, MeltQuoteBolt11Response, MeltQuoteState, ProofsMethods, State};
 use tracing::instrument;
 use tracing::instrument;
 
 
 use crate::Wallet;
 use crate::Wallet;
@@ -48,6 +49,7 @@ impl Wallet {
 
 
     pub(crate) async fn add_transaction_for_pending_melt(
     pub(crate) async fn add_transaction_for_pending_melt(
         &self,
         &self,
+        tx: &mut DynWalletDatabaseTransaction,
         quote: &MeltQuote,
         quote: &MeltQuote,
         response: &MeltQuoteBolt11Response<String>,
         response: &MeltQuoteBolt11Response<String>,
     ) -> Result<(), Error> {
     ) -> Result<(), Error> {
@@ -59,29 +61,30 @@ impl Wallet {
                 response.state
                 response.state
             );
             );
             if response.state == MeltQuoteState::Paid {
             if response.state == MeltQuoteState::Paid {
-                let pending_proofs = self.get_pending_proofs().await?;
+                let pending_proofs = self
+                    .get_proofs_with(Some(tx), Some(vec![State::Pending]), None)
+                    .await?;
                 let proofs_total = pending_proofs.total_amount().unwrap_or_default();
                 let proofs_total = pending_proofs.total_amount().unwrap_or_default();
                 let change_total = response.change_amount().unwrap_or_default();
                 let change_total = response.change_amount().unwrap_or_default();
 
 
-                self.localstore
-                    .add_transaction(Transaction {
-                        mint_url: self.mint_url.clone(),
-                        direction: TransactionDirection::Outgoing,
-                        amount: response.amount,
-                        fee: proofs_total
-                            .checked_sub(response.amount)
-                            .and_then(|amt| amt.checked_sub(change_total))
-                            .unwrap_or_default(),
-                        unit: quote.unit.clone(),
-                        ys: pending_proofs.ys()?,
-                        timestamp: unix_time(),
-                        memo: None,
-                        metadata: HashMap::new(),
-                        quote_id: Some(quote.id.clone()),
-                        payment_request: Some(quote.request.clone()),
-                        payment_proof: response.payment_preimage.clone(),
-                    })
-                    .await?;
+                tx.add_transaction(Transaction {
+                    mint_url: self.mint_url.clone(),
+                    direction: TransactionDirection::Outgoing,
+                    amount: response.amount,
+                    fee: proofs_total
+                        .checked_sub(response.amount)
+                        .and_then(|amt| amt.checked_sub(change_total))
+                        .unwrap_or_default(),
+                    unit: quote.unit.clone(),
+                    ys: pending_proofs.ys()?,
+                    timestamp: unix_time(),
+                    memo: None,
+                    metadata: HashMap::new(),
+                    quote_id: Some(quote.id.clone()),
+                    payment_request: Some(quote.request.clone()),
+                    payment_proof: response.payment_preimage.clone(),
+                })
+                .await?;
             }
             }
         }
         }
         Ok(())
         Ok(())

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

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

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

@@ -8,7 +8,7 @@ use std::sync::Arc;
 use std::time::Duration;
 use std::time::Duration;
 
 
 use cdk_common::amount::FeeAndAmounts;
 use cdk_common::amount::FeeAndAmounts;
-use cdk_common::database::{self, WalletDatabase};
+use cdk_common::database::{self, DynWalletDatabaseTransaction, WalletDatabase};
 use cdk_common::parking_lot::RwLock;
 use cdk_common::parking_lot::RwLock;
 use cdk_common::subscription::WalletParams;
 use cdk_common::subscription::WalletParams;
 use getrandom::getrandom;
 use getrandom::getrandom;
@@ -271,9 +271,10 @@ impl Wallet {
     #[instrument(skip(self))]
     #[instrument(skip(self))]
     pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
     pub async fn update_mint_url(&mut self, new_mint_url: MintUrl) -> Result<(), Error> {
         // Update the mint URL in the wallet DB
         // Update the mint URL in the wallet DB
-        self.localstore
-            .update_mint_url(self.mint_url.clone(), new_mint_url.clone())
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.update_mint_url(self.mint_url.clone(), new_mint_url.clone())
             .await?;
             .await?;
+        tx.commit().await?;
 
 
         // Update the mint URL in the wallet struct field
         // Update the mint URL in the wallet struct field
         self.mint_url = new_mint_url;
         self.mint_url = new_mint_url;
@@ -367,12 +368,15 @@ impl Wallet {
     }
     }
 
 
     /// Get amounts needed to refill proof state
     /// Get amounts needed to refill proof state
-    #[instrument(skip(self))]
-    pub async fn amounts_needed_for_state_target(
+    #[instrument(skip(self, tx))]
+    pub(crate) async fn amounts_needed_for_state_target(
         &self,
         &self,
+        tx: &mut DynWalletDatabaseTransaction,
         fee_and_amounts: &FeeAndAmounts,
         fee_and_amounts: &FeeAndAmounts,
     ) -> Result<Vec<Amount>, Error> {
     ) -> Result<Vec<Amount>, Error> {
-        let unspent_proofs = self.get_unspent_proofs().await?;
+        let unspent_proofs = self
+            .get_proofs_with(Some(tx), Some(vec![State::Unspent]), None)
+            .await?;
 
 
         let amounts_count: HashMap<u64, u64> =
         let amounts_count: HashMap<u64, u64> =
             unspent_proofs
             unspent_proofs
@@ -402,14 +406,15 @@ impl Wallet {
     }
     }
 
 
     /// Determine [`SplitTarget`] for amount based on state
     /// Determine [`SplitTarget`] for amount based on state
-    #[instrument(skip(self))]
+    #[instrument(skip(self, tx))]
     async fn determine_split_target_values(
     async fn determine_split_target_values(
         &self,
         &self,
+        tx: &mut DynWalletDatabaseTransaction,
         change_amount: Amount,
         change_amount: Amount,
         fee_and_amounts: &FeeAndAmounts,
         fee_and_amounts: &FeeAndAmounts,
     ) -> Result<SplitTarget, Error> {
     ) -> Result<SplitTarget, Error> {
         let mut amounts_needed_refill = self
         let mut amounts_needed_refill = self
-            .amounts_needed_for_state_target(fee_and_amounts)
+            .amounts_needed_for_state_target(tx, fee_and_amounts)
             .await?;
             .await?;
 
 
         amounts_needed_refill.sort();
         amounts_needed_refill.sort();
@@ -495,9 +500,10 @@ impl Wallet {
 
 
                 tracing::debug!("Restored {} proofs", proofs.len());
                 tracing::debug!("Restored {} proofs", proofs.len());
 
 
-                self.localstore
-                    .increment_keyset_counter(&keyset.id, proofs.len() as u32)
+                let mut tx = self.localstore.begin_db_transaction().await?;
+                tx.increment_keyset_counter(&keyset.id, proofs.len() as u32)
                     .await?;
                     .await?;
+                tx.commit().await?;
 
 
                 let states = self.check_proofs_spent(proofs.clone()).await?;
                 let states = self.check_proofs_spent(proofs.clone()).await?;
 
 
@@ -523,9 +529,9 @@ impl Wallet {
                     })
                     })
                     .collect::<Result<Vec<ProofInfo>, _>>()?;
                     .collect::<Result<Vec<ProofInfo>, _>>()?;
 
 
-                self.localstore
-                    .update_proofs(unspent_proofs, vec![])
-                    .await?;
+                let mut tx = self.localstore.begin_db_transaction().await?;
+                tx.update_proofs(unspent_proofs, vec![]).await?;
+                tx.commit().await?;
 
 
                 empty_batch = 0;
                 empty_batch = 0;
                 start_counter += 100;
                 start_counter += 100;

+ 45 - 26
crates/cdk/src/wallet/proofs.rs

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

+ 41 - 28
crates/cdk/src/wallet/receive.rs

@@ -33,6 +33,9 @@ impl Wallet {
         let mint_url = &self.mint_url;
         let mint_url = &self.mint_url;
 
 
         let active_keyset_id = self.fetch_active_keyset().await?.id;
         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 keys = self.load_keyset_keys(active_keyset_id).await?;
         let keys = self.load_keyset_keys(active_keyset_id).await?;
 
 
@@ -114,12 +117,21 @@ impl Wallet {
             .into_iter()
             .into_iter()
             .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
             .map(|p| ProofInfo::new(p, self.mint_url.clone(), State::Pending, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
             .collect::<Result<Vec<ProofInfo>, _>>()?;
-        self.localstore
-            .update_proofs(proofs_info.clone(), vec![])
-            .await?;
+
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.update_proofs(proofs_info.clone(), vec![]).await?;
 
 
         let mut pre_swap = self
         let mut pre_swap = self
-            .create_swap(None, opts.amount_split_target, proofs, None, false)
+            .create_swap(
+                tx,
+                active_keyset_id,
+                &fee_and_amounts,
+                None,
+                opts.amount_split_target,
+                proofs,
+                None,
+                false,
+            )
             .await?;
             .await?;
 
 
         if sig_flag.eq(&SigFlag::SigAll) {
         if sig_flag.eq(&SigFlag::SigAll) {
@@ -145,8 +157,8 @@ impl Wallet {
             &keys,
             &keys,
         )?;
         )?;
 
 
-        self.localstore
-            .increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32)
+        let mut tx = self.localstore.begin_db_transaction().await?;
+        tx.increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32)
             .await?;
             .await?;
 
 
         let total_amount = recv_proofs.total_amount()?;
         let total_amount = recv_proofs.total_amount()?;
@@ -155,30 +167,31 @@ impl Wallet {
             .into_iter()
             .into_iter()
             .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, self.unit.clone()))
             .map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent, self.unit.clone()))
             .collect::<Result<Vec<ProofInfo>, _>>()?;
             .collect::<Result<Vec<ProofInfo>, _>>()?;
-        self.localstore
-            .update_proofs(
-                recv_proof_infos,
-                proofs_info.into_iter().map(|p| p.y).collect(),
-            )
-            .await?;
+
+        tx.update_proofs(
+            recv_proof_infos,
+            proofs_info.into_iter().map(|p| p.y).collect(),
+        )
+        .await?;
 
 
         // Add transaction to store
         // Add transaction to store
-        self.localstore
-            .add_transaction(Transaction {
-                mint_url: self.mint_url.clone(),
-                direction: TransactionDirection::Incoming,
-                amount: total_amount,
-                fee: proofs_amount - total_amount,
-                unit: self.unit.clone(),
-                ys: proofs_ys,
-                timestamp: unix_time(),
-                memo,
-                metadata: opts.metadata,
-                quote_id: None,
-                payment_request: None,
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.mint_url.clone(),
+            direction: TransactionDirection::Incoming,
+            amount: total_amount,
+            fee: proofs_amount - total_amount,
+            unit: self.unit.clone(),
+            ys: proofs_ys,
+            timestamp: unix_time(),
+            memo,
+            metadata: opts.metadata,
+            quote_id: None,
+            payment_request: None,
+            payment_proof: None,
+        })
+        .await?;
+
+        tx.commit().await?;
 
 
         Ok(total_amount)
         Ok(total_amount)
     }
     }

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

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

+ 39 - 26
crates/cdk/src/wallet/send.rs

@@ -46,6 +46,7 @@ impl Wallet {
         // Get available proofs matching conditions
         // Get available proofs matching conditions
         let mut available_proofs = self
         let mut available_proofs = self
             .get_proofs_with(
             .get_proofs_with(
+                None,
                 Some(vec![State::Unspent]),
                 Some(vec![State::Unspent]),
                 opts.conditions.clone().map(|c| vec![c]),
                 opts.conditions.clone().map(|c| vec![c]),
             )
             )
@@ -180,9 +181,10 @@ impl Wallet {
         tracing::debug!("Send amounts: {:?}", send_amounts);
         tracing::debug!("Send amounts: {:?}", send_amounts);
         tracing::debug!("Send fee: {:?}", send_fee);
         tracing::debug!("Send fee: {:?}", send_fee);
 
 
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
         // Reserve proofs
         // Reserve proofs
-        self.localstore
-            .update_proofs_state(proofs.ys()?, State::Reserved)
+        tx.update_proofs_state(proofs.ys()?, State::Reserved)
             .await?;
             .await?;
 
 
         // Check if proofs are exact send amount (and does not exceed max_proofs)
         // Check if proofs are exact send amount (and does not exceed max_proofs)
@@ -213,6 +215,8 @@ impl Wallet {
             is_exact_or_offline,
             is_exact_or_offline,
         )?;
         )?;
 
 
+        tx.commit().await?;
+
         // Return prepared send
         // Return prepared send
         Ok(PreparedSend {
         Ok(PreparedSend {
             wallet: self.clone(),
             wallet: self.clone(),
@@ -333,10 +337,13 @@ impl PreparedSend {
             return Err(Error::InsufficientFunds);
             return Err(Error::InsufficientFunds);
         }
         }
 
 
+        let mut tx = self.wallet.localstore.begin_db_transaction().await?;
+
         // Check if proofs are reserved or unspent
         // Check if proofs are reserved or unspent
         let sendable_proof_ys = self
         let sendable_proof_ys = self
             .wallet
             .wallet
             .get_proofs_with(
             .get_proofs_with(
+                Some(&mut tx),
                 Some(vec![State::Reserved, State::Unspent]),
                 Some(vec![State::Reserved, State::Unspent]),
                 self.options.conditions.clone().map(|c| vec![c]),
                 self.options.conditions.clone().map(|c| vec![c]),
             )
             )
@@ -356,9 +363,8 @@ impl PreparedSend {
             "Updating proofs state to pending spent: {:?}",
             "Updating proofs state to pending spent: {:?}",
             proofs_to_send.ys()?
             proofs_to_send.ys()?
         );
         );
-        self.wallet
-            .localstore
-            .update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
+
+        tx.update_proofs_state(proofs_to_send.ys()?, State::PendingSpent)
             .await?;
             .await?;
 
 
         // Include token memo
         // Include token memo
@@ -366,23 +372,23 @@ impl PreparedSend {
         let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None });
         let memo = send_memo.and_then(|m| if m.include_memo { Some(m.memo) } else { None });
 
 
         // Add transaction to store
         // Add transaction to store
-        self.wallet
-            .localstore
-            .add_transaction(Transaction {
-                mint_url: self.wallet.mint_url.clone(),
-                direction: TransactionDirection::Outgoing,
-                amount: self.amount,
-                fee: total_send_fee,
-                unit: self.wallet.unit.clone(),
-                ys: proofs_to_send.ys()?,
-                timestamp: unix_time(),
-                memo: memo.clone(),
-                metadata: self.options.metadata,
-                quote_id: None,
-                payment_request: None,
-                payment_proof: None,
-            })
-            .await?;
+        tx.add_transaction(Transaction {
+            mint_url: self.wallet.mint_url.clone(),
+            direction: TransactionDirection::Outgoing,
+            amount: self.amount,
+            fee: total_send_fee,
+            unit: self.wallet.unit.clone(),
+            ys: proofs_to_send.ys()?,
+            timestamp: unix_time(),
+            memo: memo.clone(),
+            metadata: self.options.metadata,
+            quote_id: None,
+            payment_request: None,
+            payment_proof: None,
+        })
+        .await?;
+
+        tx.commit().await?;
 
 
         // Create and return token
         // Create and return token
         Ok(Token::new(
         Ok(Token::new(
@@ -397,8 +403,15 @@ impl PreparedSend {
     pub async fn cancel(self) -> Result<(), Error> {
     pub async fn cancel(self) -> Result<(), Error> {
         tracing::info!("Cancelling prepared send");
         tracing::info!("Cancelling prepared send");
 
 
+        let mut tx = self.wallet.localstore.begin_db_transaction().await?;
+
         // Double-check proofs state
         // Double-check proofs state
-        let reserved_proofs = self.wallet.get_reserved_proofs().await?.ys()?;
+        let reserved_proofs = self
+            .wallet
+            .get_proofs_with(Some(&mut tx), Some(vec![State::Reserved]), None)
+            .await?
+            .ys()?;
+
         if !self
         if !self
             .proofs()
             .proofs()
             .ys()?
             .ys()?
@@ -408,11 +421,11 @@ impl PreparedSend {
             return Err(Error::UnexpectedProofState);
             return Err(Error::UnexpectedProofState);
         }
         }
 
 
-        self.wallet
-            .localstore
-            .update_proofs_state(self.proofs().ys()?, State::Unspent)
+        tx.update_proofs_state(self.proofs().ys()?, State::Unspent)
             .await?;
             .await?;
 
 
+        tx.commit().await?;
+
         Ok(())
         Ok(())
     }
     }
 }
 }

+ 34 - 28
crates/cdk/src/wallet/swap.rs

@@ -1,4 +1,7 @@
+use cdk_common::amount::FeeAndAmounts;
+use cdk_common::database::DynWalletDatabaseTransaction;
 use cdk_common::nut02::KeySetInfosMethods;
 use cdk_common::nut02::KeySetInfosMethods;
+use cdk_common::Id;
 use tracing::instrument;
 use tracing::instrument;
 
 
 use crate::amount::SplitTarget;
 use crate::amount::SplitTarget;
@@ -24,9 +27,16 @@ impl Wallet {
         tracing::info!("Swapping");
         tracing::info!("Swapping");
         let mint_url = &self.mint_url;
         let mint_url = &self.mint_url;
         let unit = &self.unit;
         let unit = &self.unit;
+        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 pre_swap = self
         let pre_swap = self
             .create_swap(
             .create_swap(
+                self.localstore.begin_db_transaction().await?,
+                active_keyset_id,
+                &fee_and_amounts,
                 amount,
                 amount,
                 amount_split_target.clone(),
                 amount_split_target.clone(),
                 input_proofs.clone(),
                 input_proofs.clone(),
@@ -43,9 +53,6 @@ impl Wallet {
             .await?;
             .await?;
 
 
         let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id;
         let active_keyset_id = pre_swap.pre_mint_secrets.keyset_id;
-        let fee_and_amounts = self
-            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
-            .await?;
 
 
         let active_keys = self.load_keyset_keys(active_keyset_id).await?;
         let active_keys = self.load_keyset_keys(active_keyset_id).await?;
 
 
@@ -123,9 +130,11 @@ impl Wallet {
             .map(|proof| proof.y())
             .map(|proof| proof.y())
             .collect::<Result<Vec<PublicKey>, _>>()?;
             .collect::<Result<Vec<PublicKey>, _>>()?;
 
 
-        self.localstore
-            .update_proofs(added_proofs, deleted_ys)
-            .await?;
+        let mut tx = self.localstore.begin_db_transaction().await?;
+
+        tx.update_proofs(added_proofs, deleted_ys).await?;
+        tx.commit().await?;
+
         Ok(send_proofs)
         Ok(send_proofs)
     }
     }
 
 
@@ -186,9 +195,13 @@ impl Wallet {
     }
     }
 
 
     /// Create Swap Payload
     /// Create Swap Payload
-    #[instrument(skip(self, proofs))]
+    #[instrument(skip(self, proofs, tx))]
+    #[allow(clippy::too_many_arguments)]
     pub async fn create_swap(
     pub async fn create_swap(
         &self,
         &self,
+        mut tx: DynWalletDatabaseTransaction,
+        active_keyset_id: Id,
+        fee_and_amounts: &FeeAndAmounts,
         amount: Option<Amount>,
         amount: Option<Amount>,
         amount_split_target: SplitTarget,
         amount_split_target: SplitTarget,
         proofs: Proofs,
         proofs: Proofs,
@@ -196,17 +209,13 @@ impl Wallet {
         include_fees: bool,
         include_fees: bool,
     ) -> Result<PreSwap, Error> {
     ) -> Result<PreSwap, Error> {
         tracing::info!("Creating swap");
         tracing::info!("Creating swap");
-        let active_keyset_id = self.fetch_active_keyset().await?.id;
 
 
         // Desired amount is either amount passed or value of all proof
         // Desired amount is either amount passed or value of all proof
         let proofs_total = proofs.total_amount()?;
         let proofs_total = proofs.total_amount()?;
+        let fee = self.get_proofs_fee(&proofs).await?;
 
 
         let ys: Vec<PublicKey> = proofs.ys()?;
         let ys: Vec<PublicKey> = proofs.ys()?;
-        self.localstore
-            .update_proofs_state(ys, State::Reserved)
-            .await?;
-
-        let fee = self.get_proofs_fee(&proofs).await?;
+        tx.update_proofs_state(ys, State::Reserved).await?;
 
 
         let total_to_subtract = amount
         let total_to_subtract = amount
             .unwrap_or(Amount::ZERO)
             .unwrap_or(Amount::ZERO)
@@ -217,15 +226,11 @@ impl Wallet {
             .checked_sub(total_to_subtract)
             .checked_sub(total_to_subtract)
             .ok_or(Error::InsufficientFunds)?;
             .ok_or(Error::InsufficientFunds)?;
 
 
-        let fee_and_amounts = self
-            .get_keyset_fees_and_amounts_by_id(active_keyset_id)
-            .await?;
-
         let (send_amount, change_amount) = match include_fees {
         let (send_amount, change_amount) = match include_fees {
             true => {
             true => {
                 let split_count = amount
                 let split_count = amount
                     .unwrap_or(Amount::ZERO)
                     .unwrap_or(Amount::ZERO)
-                    .split_targeted(&SplitTarget::default(), &fee_and_amounts)
+                    .split_targeted(&SplitTarget::default(), fee_and_amounts)
                     .unwrap()
                     .unwrap()
                     .len();
                     .len();
 
 
@@ -249,7 +254,7 @@ impl Wallet {
         // else use state refill
         // else use state refill
         let change_split_target = match amount_split_target {
         let change_split_target = match amount_split_target {
             SplitTarget::None => {
             SplitTarget::None => {
-                self.determine_split_target_values(change_amount, &fee_and_amounts)
+                self.determine_split_target_values(&mut tx, change_amount, fee_and_amounts)
                     .await?
                     .await?
             }
             }
             s => s,
             s => s,
@@ -262,17 +267,17 @@ impl Wallet {
             Some(_) => {
             Some(_) => {
                 // For spending conditions, we only need to count change secrets
                 // For spending conditions, we only need to count change secrets
                 change_amount
                 change_amount
-                    .split_targeted(&change_split_target, &fee_and_amounts)?
+                    .split_targeted(&change_split_target, fee_and_amounts)?
                     .len() as u32
                     .len() as u32
             }
             }
             None => {
             None => {
                 // For no spending conditions, count both send and change secrets
                 // For no spending conditions, count both send and change secrets
                 let send_count = send_amount
                 let send_count = send_amount
                     .unwrap_or(Amount::ZERO)
                     .unwrap_or(Amount::ZERO)
-                    .split_targeted(&SplitTarget::default(), &fee_and_amounts)?
+                    .split_targeted(&SplitTarget::default(), fee_and_amounts)?
                     .len() as u32;
                     .len() as u32;
                 let change_count = change_amount
                 let change_count = change_amount
-                    .split_targeted(&change_split_target, &fee_and_amounts)?
+                    .split_targeted(&change_split_target, fee_and_amounts)?
                     .len() as u32;
                     .len() as u32;
                 send_count + change_count
                 send_count + change_count
             }
             }
@@ -286,8 +291,7 @@ impl Wallet {
                 total_secrets_needed
                 total_secrets_needed
             );
             );
 
 
-            let new_counter = self
-                .localstore
+            let new_counter = tx
                 .increment_keyset_counter(&active_keyset_id, total_secrets_needed)
                 .increment_keyset_counter(&active_keyset_id, total_secrets_needed)
                 .await?;
                 .await?;
 
 
@@ -306,7 +310,7 @@ impl Wallet {
                     &self.seed,
                     &self.seed,
                     change_amount,
                     change_amount,
                     &change_split_target,
                     &change_split_target,
-                    &fee_and_amounts,
+                    fee_and_amounts,
                 )?;
                 )?;
 
 
                 derived_secret_count = change_premint_secrets.len();
                 derived_secret_count = change_premint_secrets.len();
@@ -317,7 +321,7 @@ impl Wallet {
                         send_amount.unwrap_or(Amount::ZERO),
                         send_amount.unwrap_or(Amount::ZERO),
                         &SplitTarget::default(),
                         &SplitTarget::default(),
                         &conditions,
                         &conditions,
-                        &fee_and_amounts,
+                        fee_and_amounts,
                     )?,
                     )?,
                     change_premint_secrets,
                     change_premint_secrets,
                 )
                 )
@@ -329,7 +333,7 @@ impl Wallet {
                     &self.seed,
                     &self.seed,
                     send_amount.unwrap_or(Amount::ZERO),
                     send_amount.unwrap_or(Amount::ZERO),
                     &SplitTarget::default(),
                     &SplitTarget::default(),
-                    &fee_and_amounts,
+                    fee_and_amounts,
                 )?;
                 )?;
 
 
                 count += premint_secrets.len() as u32;
                 count += premint_secrets.len() as u32;
@@ -340,7 +344,7 @@ impl Wallet {
                     &self.seed,
                     &self.seed,
                     change_amount,
                     change_amount,
                     &change_split_target,
                     &change_split_target,
-                    &fee_and_amounts,
+                    fee_and_amounts,
                 )?;
                 )?;
 
 
                 derived_secret_count = change_premint_secrets.len() + premint_secrets.len();
                 derived_secret_count = change_premint_secrets.len() + premint_secrets.len();
@@ -356,6 +360,8 @@ impl Wallet {
 
 
         let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages());
         let swap_request = SwapRequest::new(proofs, desired_messages.blinded_messages());
 
 
+        tx.commit().await?;
+
         Ok(PreSwap {
         Ok(PreSwap {
             pre_mint_secrets: desired_messages,
             pre_mint_secrets: desired_messages,
             swap_request,
             swap_request,

Некоторые файлы не были показаны из-за большого количества измененных файлов