Преглед изворни кода

Expose KVStore traits to FFI

Add FFI bindings for KVStore and KVStoreDatabase traits, following the same
pattern as wallet database traits. This enables foreign language bindings to
access key-value store functionality with full transaction support.

Changes:
- Add KVStoreDatabase FFI trait with kv_read and kv_list methods
- Add KVStore FFI trait extending KVStoreDatabase with begin_kv_transaction
- Add KVStoreTransaction FFI trait with read/write/remove/list and
  commit/rollback methods
- Add KVStoreTransactionWrapper as UniFFI-exported object wrapper
- Implement FfiKVStoreTransaction with auto-rollback on drop
- Implement KVStoreDatabase and KVStore for FfiWalletSQLDatabase
- Implement KVStoreDatabase and KVStore for WalletSqliteDatabase
- Implement KVStoreDatabase and KVStore for WalletPostgresDatabase
Cesar Rodas пре 1 месец
родитељ
комит
a1024e4678

+ 1 - 1
crates/cdk-cli/src/main.rs

@@ -137,7 +137,7 @@ async fn main() -> Result<()> {
         fs::create_dir_all(&work_dir)?;
     }
 
-    let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
+    let localstore: Arc<dyn WalletDatabase<cdk_database::Error> + Send + Sync> =
         match args.engine.as_str() {
             "sqlite" => {
                 let sql_path = work_dir.join("cdk-cli.sqlite");

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

@@ -32,6 +32,10 @@ pub use wallet::{
     DynWalletDatabaseTransaction,
 };
 
+/// Type alias for dynamic Wallet Database
+#[cfg(feature = "wallet")]
+pub type DynWalletDatabase = std::sync::Arc<dyn WalletDatabase<Error> + Send + Sync>;
+
 // Wallet-specific KVStore type aliases
 /// Wallet Key-Value Store trait object
 #[cfg(feature = "wallet")]

+ 24 - 24
crates/cdk-common/src/database/wallet.rs

@@ -8,6 +8,7 @@ use cashu::KeySet;
 
 use super::{DbTransactionFinalizer, Error};
 use crate::common::ProofInfo;
+use crate::database::{KVStoreDatabase, KVStoreTransaction};
 use crate::mint_url::MintUrl;
 use crate::nuts::{
     CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State,
@@ -24,7 +25,9 @@ pub type DynWalletDatabaseTransaction = Box<dyn DatabaseTransaction<super::Error
 /// This trait encapsulates all the changes to be done in the wallet
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-pub trait DatabaseTransaction<Error>: DbTransactionFinalizer<Err = Error> {
+pub trait DatabaseTransaction<Error>:
+    KVStoreTransaction<Error> + DbTransactionFinalizer<Err = Error>
+{
     /// Add Mint to storage
     async fn add_mint(
         &mut self,
@@ -112,48 +115,45 @@ pub trait DatabaseTransaction<Error>: DbTransactionFinalizer<Err = 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>;
-
+pub trait Database<Err>: KVStoreDatabase<Err = Err> + Debug
+where
+    Err: Into<Error> + From<Error>,
+{
     /// Begins a DB transaction
     async fn begin_db_transaction(
         &self,
-    ) -> Result<Box<dyn DatabaseTransaction<Self::Err> + Send + Sync>, Self::Err>;
+    ) -> Result<Box<dyn DatabaseTransaction<Err> + Send + Sync>, Err>;
 
     /// Get mint from storage
-    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Self::Err>;
+    async fn get_mint(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, Err>;
 
     /// Get all mints from storage
-    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err>;
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Err>;
 
     /// Get mint keysets for mint url
-    async fn get_mint_keysets(
-        &self,
-        mint_url: MintUrl,
-    ) -> Result<Option<Vec<KeySetInfo>>, Self::Err>;
+    async fn get_mint_keysets(&self, mint_url: MintUrl) -> Result<Option<Vec<KeySetInfo>>, Err>;
 
     /// 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>, Err>;
 
     /// 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>, Err>;
 
     /// 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>, Err>;
     /// Get unissued mint quotes from storage
     /// Returns bolt11 quotes where nothing has been issued yet (amount_issued = 0) and all bolt12 quotes.
     /// Includes unpaid bolt11 quotes to allow checking with the mint if they've been paid (wallet state may be outdated).
-    async fn get_unissued_mint_quotes(&self) -> Result<Vec<WalletMintQuote>, Self::Err>;
+    async fn get_unissued_mint_quotes(&self) -> Result<Vec<WalletMintQuote>, Err>;
 
     /// 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>, Err>;
 
     /// 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>, Err>;
 
     /// 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>, Err>;
 
     /// Get proofs from storage
     async fn get_proofs(
@@ -162,10 +162,10 @@ pub trait Database: Debug {
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
-    ) -> Result<Vec<ProofInfo>, Self::Err>;
+    ) -> Result<Vec<ProofInfo>, Err>;
 
     /// 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>, Err>;
 
     /// Get balance
     async fn get_balance(
@@ -173,13 +173,13 @@ pub trait Database: Debug {
         mint_url: Option<MintUrl>,
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
-    ) -> Result<u64, Self::Err>;
+    ) -> Result<u64, Err>;
 
     /// Get transaction from storage
     async fn get_transaction(
         &self,
         transaction_id: TransactionId,
-    ) -> Result<Option<Transaction>, Self::Err>;
+    ) -> Result<Option<Transaction>, Err>;
 
     /// List transactions from storage
     async fn list_transactions(
@@ -187,5 +187,5 @@ pub trait Database: Debug {
         mint_url: Option<MintUrl>,
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
-    ) -> Result<Vec<Transaction>, Self::Err>;
+    ) -> Result<Vec<Transaction>, Err>;
 }

+ 313 - 19
crates/cdk-ffi/src/database.rs

@@ -5,8 +5,8 @@ use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
 
 use cdk_common::database::{
-    DbTransactionFinalizer, DynWalletDatabaseTransaction, WalletDatabase as CdkWalletDatabase,
-    WalletDatabaseTransaction as CdkWalletDatabaseTransaction,
+    DbTransactionFinalizer, DynWalletDatabaseTransaction, KVStoreDatabase as CdkKVStoreDatabase,
+    WalletDatabase as CdkWalletDatabase, WalletDatabaseTransaction as CdkWalletDatabaseTransaction,
 };
 use cdk_common::task::spawn;
 use cdk_sql_common::pool::DatabasePool;
@@ -95,6 +95,21 @@ pub trait WalletDatabase: Send + Sync {
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
     ) -> Result<Vec<Transaction>, FfiError>;
+
+    /// Read a value from the KV store
+    async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, FfiError>;
+
+    /// List keys in a namespace
+    async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, FfiError>;
 }
 
 /// FFI-compatible transaction trait for wallet database write operations
@@ -201,6 +216,39 @@ pub trait WalletDatabaseTransaction: Send + Sync {
 
     /// Remove transaction from storage
     async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError>;
+
+    // KV Store Methods
+    /// Read a value from the KV store
+    async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, FfiError>;
+
+    /// Write a value to the KV store
+    async fn kv_write(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+        value: Vec<u8>,
+    ) -> Result<(), FfiError>;
+
+    /// Remove a value from the KV store
+    async fn kv_remove(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<(), FfiError>;
+
+    /// List keys in the KV store
+    async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, FfiError>;
 }
 
 /// Wallet database transaction wrapper
@@ -352,6 +400,54 @@ impl WalletDatabaseTransactionWrapper {
     pub async fn remove_transaction(&self, transaction_id: TransactionId) -> Result<(), FfiError> {
         self.inner.remove_transaction(transaction_id).await
     }
+
+    /// Read a value from the KV store
+    pub async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, FfiError> {
+        self.inner
+            .kv_read(primary_namespace, secondary_namespace, key)
+            .await
+    }
+
+    /// Write a value to the KV store
+    pub async fn kv_write(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+        value: Vec<u8>,
+    ) -> Result<(), FfiError> {
+        self.inner
+            .kv_write(primary_namespace, secondary_namespace, key, value)
+            .await
+    }
+
+    /// Remove a value from the KV store
+    pub async fn kv_remove(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<(), FfiError> {
+        self.inner
+            .kv_remove(primary_namespace, secondary_namespace, key)
+            .await
+    }
+
+    /// List keys in the KV store
+    pub async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, FfiError> {
+        self.inner
+            .kv_list(primary_namespace, secondary_namespace)
+            .await
+    }
 }
 
 /// Internal bridge trait to convert from the FFI trait to the CDK database trait
@@ -373,14 +469,47 @@ impl std::fmt::Debug for WalletDatabaseBridge {
 }
 
 #[async_trait::async_trait]
-impl CdkWalletDatabase for WalletDatabaseBridge {
+impl cdk_common::database::KVStoreDatabase for WalletDatabaseBridge {
     type Err = cdk::cdk_database::Error;
 
+    async fn kv_read(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Self::Err> {
+        self.ffi_db
+            .kv_read(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+                key.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Self::Err> {
+        self.ffi_db
+            .kv_list(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+}
+
+#[async_trait::async_trait]
+impl CdkWalletDatabase<cdk::cdk_database::Error> for WalletDatabaseBridge {
     // Mint Management
     async fn get_mint(
         &self,
         mint_url: cdk::mint_url::MintUrl,
-    ) -> Result<Option<cdk::nuts::MintInfo>, Self::Err> {
+    ) -> Result<Option<cdk::nuts::MintInfo>, cdk::cdk_database::Error> {
         let ffi_mint_url = mint_url.into();
         let result = self
             .ffi_db
@@ -392,7 +521,10 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
 
     async fn get_mints(
         &self,
-    ) -> Result<HashMap<cdk::mint_url::MintUrl, Option<cdk::nuts::MintInfo>>, Self::Err> {
+    ) -> Result<
+        HashMap<cdk::mint_url::MintUrl, Option<cdk::nuts::MintInfo>>,
+        cdk::cdk_database::Error,
+    > {
         let result = self
             .ffi_db
             .get_mints()
@@ -413,7 +545,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     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>>, cdk::cdk_database::Error> {
         let ffi_mint_url = mint_url.into();
         let result = self
             .ffi_db
@@ -426,7 +558,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     async fn get_keyset_by_id(
         &self,
         keyset_id: &cdk::nuts::Id,
-    ) -> Result<Option<cdk::nuts::KeySetInfo>, Self::Err> {
+    ) -> Result<Option<cdk::nuts::KeySetInfo>, cdk::cdk_database::Error> {
         let ffi_id = (*keyset_id).into();
         let result = self
             .ffi_db
@@ -440,7 +572,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     async fn get_mint_quote(
         &self,
         quote_id: &str,
-    ) -> Result<Option<cdk::wallet::MintQuote>, Self::Err> {
+    ) -> Result<Option<cdk::wallet::MintQuote>, cdk::cdk_database::Error> {
         let result = self
             .ffi_db
             .get_mint_quote(quote_id.to_string())
@@ -454,7 +586,9 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
             .transpose()?)
     }
 
-    async fn get_mint_quotes(&self) -> Result<Vec<cdk::wallet::MintQuote>, Self::Err> {
+    async fn get_mint_quotes(
+        &self,
+    ) -> Result<Vec<cdk::wallet::MintQuote>, cdk::cdk_database::Error> {
         let result = self
             .ffi_db
             .get_mint_quotes()
@@ -488,7 +622,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     async fn get_melt_quote(
         &self,
         quote_id: &str,
-    ) -> Result<Option<cdk::wallet::MeltQuote>, Self::Err> {
+    ) -> Result<Option<cdk::wallet::MeltQuote>, cdk::cdk_database::Error> {
         let result = self
             .ffi_db
             .get_melt_quote(quote_id.to_string())
@@ -502,7 +636,9 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
             .transpose()?)
     }
 
-    async fn get_melt_quotes(&self) -> Result<Vec<cdk::wallet::MeltQuote>, Self::Err> {
+    async fn get_melt_quotes(
+        &self,
+    ) -> Result<Vec<cdk::wallet::MeltQuote>, cdk::cdk_database::Error> {
         let result = self
             .ffi_db
             .get_melt_quotes()
@@ -518,7 +654,10 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     }
 
     // Keys Management
-    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>, cdk::cdk_database::Error> {
         let ffi_id: Id = (*id).into();
         let result = self
             .ffi_db
@@ -543,7 +682,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
         unit: Option<cdk::nuts::CurrencyUnit>,
         state: Option<Vec<cdk::nuts::State>>,
         spending_conditions: Option<Vec<cdk::nuts::SpendingConditions>>,
-    ) -> Result<Vec<cdk::types::ProofInfo>, Self::Err> {
+    ) -> 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());
@@ -589,7 +728,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     async fn get_proofs_by_ys(
         &self,
         ys: Vec<cdk::nuts::PublicKey>,
-    ) -> Result<Vec<cdk::types::ProofInfo>, Self::Err> {
+    ) -> Result<Vec<cdk::types::ProofInfo>, cdk::cdk_database::Error> {
         let ffi_ys: Vec<PublicKey> = ys.into_iter().map(Into::into).collect();
 
         let result = self
@@ -633,7 +772,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
         mint_url: Option<cdk::mint_url::MintUrl>,
         unit: Option<cdk::nuts::CurrencyUnit>,
         state: Option<Vec<cdk::nuts::State>>,
-    ) -> Result<u64, Self::Err> {
+    ) -> Result<u64, 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());
@@ -648,7 +787,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
     async fn get_transaction(
         &self,
         transaction_id: cdk::wallet::types::TransactionId,
-    ) -> Result<Option<cdk::wallet::types::Transaction>, Self::Err> {
+    ) -> Result<Option<cdk::wallet::types::Transaction>, cdk::cdk_database::Error> {
         let ffi_id = transaction_id.into();
         let result = self
             .ffi_db
@@ -667,7 +806,7 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
         mint_url: Option<cdk::mint_url::MintUrl>,
         direction: Option<cdk::wallet::types::TransactionDirection>,
         unit: Option<cdk::nuts::CurrencyUnit>,
-    ) -> Result<Vec<cdk::wallet::types::Transaction>, Self::Err> {
+    ) -> Result<Vec<cdk::wallet::types::Transaction>, cdk::cdk_database::Error> {
         let ffi_mint_url = mint_url.map(Into::into);
         let ffi_direction = direction.map(Into::into);
         let ffi_unit = unit.map(Into::into);
@@ -687,7 +826,10 @@ impl CdkWalletDatabase for WalletDatabaseBridge {
 
     async fn begin_db_transaction(
         &self,
-    ) -> Result<Box<dyn CdkWalletDatabaseTransaction<Self::Err> + Send + Sync>, Self::Err> {
+    ) -> Result<
+        Box<dyn CdkWalletDatabaseTransaction<cdk::cdk_database::Error> + Send + Sync>,
+        cdk::cdk_database::Error,
+    > {
         let ffi_tx = self
             .ffi_db
             .begin_db_transaction()
@@ -991,6 +1133,75 @@ impl CdkWalletDatabaseTransaction<cdk::cdk_database::Error> for WalletDatabaseTr
 }
 
 #[async_trait::async_trait]
+impl cdk_common::database::KVStoreTransaction<cdk::cdk_database::Error>
+    for WalletDatabaseTransactionBridge
+{
+    async fn kv_read(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, cdk::cdk_database::Error> {
+        self.ffi_tx
+            .kv_read(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+                key.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn kv_write(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+        value: &[u8],
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.ffi_tx
+            .kv_write(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+                key.to_string(),
+                value.to_vec(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn kv_remove(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<(), cdk::cdk_database::Error> {
+        self.ffi_tx
+            .kv_remove(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+                key.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+
+    async fn kv_list(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, cdk::cdk_database::Error> {
+        self.ffi_tx
+            .kv_list(
+                primary_namespace.to_string(),
+                secondary_namespace.to_string(),
+            )
+            .await
+            .map_err(|e| cdk::cdk_database::Error::Database(e.to_string().into()))
+    }
+}
+
+#[async_trait::async_trait]
 impl DbTransactionFinalizer for WalletDatabaseTransactionBridge {
     type Err = cdk::cdk_database::Error;
 
@@ -1277,6 +1488,29 @@ where
 
         Ok(result.into_iter().map(Into::into).collect())
     }
+
+    async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, FfiError> {
+        self.inner
+            .kv_read(&primary_namespace, &secondary_namespace, &key)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, FfiError> {
+        self.inner
+            .kv_list(&primary_namespace, &secondary_namespace)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
 }
 
 // Implement WalletDatabaseTransactionFfi trait - all write methods
@@ -1608,6 +1842,66 @@ impl WalletDatabaseTransaction for FfiWalletTransaction {
             .await
             .map_err(|e| FfiError::Database { msg: e.to_string() })
     }
+
+    async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, 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.kv_read(&primary_namespace, &secondary_namespace, &key)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn kv_write(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+        value: Vec<u8>,
+    ) -> 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.kv_write(&primary_namespace, &secondary_namespace, &key, &value)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn kv_remove(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: 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.kv_remove(&primary_namespace, &secondary_namespace, &key)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, 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.kv_list(&primary_namespace, &secondary_namespace)
+            .await
+            .map_err(|e| FfiError::Database { msg: e.to_string() })
+    }
 }
 
 /// FFI-safe database type enum
@@ -1641,6 +1935,6 @@ pub fn create_wallet_db(backend: WalletDbBackend) -> Result<Arc<dyn WalletDataba
 /// Helper function to create a CDK database from the FFI trait
 pub fn create_cdk_database_from_ffi(
     ffi_db: Arc<dyn WalletDatabase>,
-) -> Arc<dyn CdkWalletDatabase<Err = cdk::cdk_database::Error> + Send + Sync> {
+) -> Arc<dyn CdkWalletDatabase<cdk::cdk_database::Error> + Send + Sync> {
     Arc::new(WalletDatabaseBridge::new(ffi_db))
 }

+ 21 - 0
crates/cdk-ffi/src/postgres.rs

@@ -153,4 +153,25 @@ impl WalletDatabase for WalletPostgresDatabase {
             .list_transactions(mint_url, direction, unit)
             .await
     }
+
+    async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, FfiError> {
+        self.inner
+            .kv_read(primary_namespace, secondary_namespace, key)
+            .await
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, FfiError> {
+        self.inner
+            .kv_list(primary_namespace, secondary_namespace)
+            .await
+    }
 }

+ 21 - 0
crates/cdk-ffi/src/sqlite.rs

@@ -158,4 +158,25 @@ impl WalletDatabase for WalletSqliteDatabase {
             .list_transactions(mint_url, direction, unit)
             .await
     }
+
+    async fn kv_read(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+        key: String,
+    ) -> Result<Option<Vec<u8>>, FfiError> {
+        self.inner
+            .kv_read(primary_namespace, secondary_namespace, key)
+            .await
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: String,
+        secondary_namespace: String,
+    ) -> Result<Vec<String>, FfiError> {
+        self.inner
+            .kv_list(primary_namespace, secondary_namespace)
+            .await
+    }
 }

+ 648 - 0
crates/cdk-ffi/tests/test_kvstore.py

@@ -0,0 +1,648 @@
+#!/usr/bin/env python3
+"""
+Test suite for CDK FFI Key-Value Store operations
+
+Tests the KVStore trait functionality exposed through the FFI bindings,
+including read, write, list, and remove operations with transaction support.
+"""
+
+import asyncio
+import os
+import sys
+import tempfile
+from pathlib import Path
+
+# Setup paths before importing cdk_ffi
+repo_root = Path(__file__).parent.parent.parent.parent
+bindings_path = repo_root / "target" / "bindings" / "python"
+lib_path = repo_root / "target" / "release"
+
+# Copy the library to the bindings directory so Python can find it
+import shutil
+
+lib_file = "libcdk_ffi.dylib" if sys.platform == "darwin" else "libcdk_ffi.so"
+src_lib = lib_path / lib_file
+dst_lib = bindings_path / lib_file
+
+if src_lib.exists() and not dst_lib.exists():
+    shutil.copy2(src_lib, dst_lib)
+
+# Add target/bindings/python to path to load cdk_ffi module
+sys.path.insert(0, str(bindings_path))
+
+import cdk_ffi
+
+# Helper functions
+
+def create_test_db():
+    """Create a temporary SQLite database for testing"""
+    tmp = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
+    db_path = tmp.name
+    tmp.close()
+    backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+    db = cdk_ffi.create_wallet_db(backend)
+    return db, db_path
+
+
+def cleanup_db(db_path):
+    """Clean up the temporary database file"""
+    if os.path.exists(db_path):
+        os.unlink(db_path)
+
+
+# Basic KV Store Tests
+
+async def test_kv_write_and_read():
+    """Test basic write and read operations"""
+    print("\n=== Test: KV Write and Read ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write a value using KV transaction
+        kv_tx = await db.begin_db_transaction()
+        test_data = b"Hello, KVStore!"
+        await kv_tx.kv_write("app", "config", "greeting", test_data)
+        await kv_tx.commit()
+        print("  Written value to KV store")
+
+        # Read it back using a new transaction
+        kv_tx2 = await db.begin_db_transaction()
+        result = await kv_tx2.kv_read("app", "config", "greeting")
+        await kv_tx2.rollback()
+
+        assert result is not None, "Expected to read back the value"
+        assert bytes(result) == test_data, f"Expected {test_data}, got {bytes(result)}"
+        print("  Read back correct value")
+
+        print("  Test passed: KV write and read work")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_read_nonexistent():
+    """Test reading a key that doesn't exist"""
+    print("\n=== Test: KV Read Nonexistent Key ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        kv_tx = await db.begin_db_transaction()
+        result = await kv_tx.kv_read("nonexistent", "namespace", "key")
+        await kv_tx.rollback()
+
+        assert result is None, f"Expected None for nonexistent key, got {result}"
+        print("  Correctly returns None for nonexistent key")
+
+        print("  Test passed: Reading nonexistent key returns None")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_overwrite():
+    """Test overwriting an existing value"""
+    print("\n=== Test: KV Overwrite ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write initial value
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("app", "data", "counter", b"1")
+        await kv_tx.commit()
+        print("  Written initial value")
+
+        # Overwrite with new value
+        kv_tx2 = await db.begin_db_transaction()
+        await kv_tx2.kv_write("app", "data", "counter", b"42")
+        await kv_tx2.commit()
+        print("  Overwrote with new value")
+
+        # Read back
+        kv_tx3 = await db.begin_db_transaction()
+        result = await kv_tx3.kv_read("app", "data", "counter")
+        await kv_tx3.rollback()
+
+        assert result is not None, "Expected to read back the value"
+        assert bytes(result) == b"42", f"Expected b'42', got {bytes(result)}"
+        print("  Read back overwritten value")
+
+        print("  Test passed: KV overwrite works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_remove():
+    """Test removing a key"""
+    print("\n=== Test: KV Remove ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write a value
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("app", "temp", "to_delete", b"delete me")
+        await kv_tx.commit()
+        print("  Written value to delete")
+
+        # Verify it exists
+        kv_tx2 = await db.begin_db_transaction()
+        result = await kv_tx2.kv_read("app", "temp", "to_delete")
+        await kv_tx2.rollback()
+        assert result is not None, "Value should exist before removal"
+        print("  Verified value exists")
+
+        # Remove it
+        kv_tx3 = await db.begin_db_transaction()
+        await kv_tx3.kv_remove("app", "temp", "to_delete")
+        await kv_tx3.commit()
+        print("  Removed value")
+
+        # Verify it's gone
+        kv_tx4 = await db.begin_db_transaction()
+        result_after = await kv_tx4.kv_read("app", "temp", "to_delete")
+        await kv_tx4.rollback()
+
+        assert result_after is None, f"Expected None after removal, got {result_after}"
+        print("  Verified value is removed")
+
+        print("  Test passed: KV remove works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_list_keys():
+    """Test listing keys in a namespace"""
+    print("\n=== Test: KV List Keys ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write multiple keys
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("myapp", "settings", "theme", b"dark")
+        await kv_tx.kv_write("myapp", "settings", "language", b"en")
+        await kv_tx.kv_write("myapp", "settings", "timezone", b"UTC")
+        await kv_tx.kv_write("myapp", "other", "unrelated", b"data")
+        await kv_tx.commit()
+        print("  Written multiple keys")
+
+        # List keys in the settings namespace
+        kv_tx2 = await db.begin_db_transaction()
+        keys = await kv_tx2.kv_list("myapp", "settings")
+        await kv_tx2.rollback()
+
+        assert len(keys) == 3, f"Expected 3 keys, got {len(keys)}"
+        assert "theme" in keys, "Expected 'theme' in keys"
+        assert "language" in keys, "Expected 'language' in keys"
+        assert "timezone" in keys, "Expected 'timezone' in keys"
+        assert "unrelated" not in keys, "'unrelated' should not be in settings namespace"
+        print(f"  Listed keys: {keys}")
+
+        print("  Test passed: KV list works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_list_empty_namespace():
+    """Test listing keys in an empty or nonexistent namespace"""
+    print("\n=== Test: KV List Empty Namespace ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        kv_tx = await db.begin_db_transaction()
+        keys = await kv_tx.kv_list("nonexistent", "namespace")
+        await kv_tx.rollback()
+
+        assert isinstance(keys, list), "Expected a list"
+        assert len(keys) == 0, f"Expected empty list, got {keys}"
+        print("  Empty namespace returns empty list")
+
+        print("  Test passed: KV list on empty namespace works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+# Transaction Tests
+
+async def test_kv_transaction_commit():
+    """Test that KV changes persist after commit"""
+    print("\n=== Test: KV Transaction Commit ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write and commit
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("test", "commit", "key1", b"committed")
+        await kv_tx.commit()
+        print("  Written and committed")
+
+        # Verify in new transaction
+        kv_tx2 = await db.begin_db_transaction()
+        result = await kv_tx2.kv_read("test", "commit", "key1")
+        await kv_tx2.rollback()
+
+        assert result is not None, "Value should persist after commit"
+        assert bytes(result) == b"committed", f"Expected b'committed', got {bytes(result)}"
+        print("  Value persists after commit")
+
+        print("  Test passed: KV transaction commit works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_transaction_rollback():
+    """Test that KV changes are reverted after rollback"""
+    print("\n=== Test: KV Transaction Rollback ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write and rollback
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("test", "rollback", "key1", b"should_not_persist")
+        await kv_tx.rollback()
+        print("  Written and rolled back")
+
+        # Verify not persisted
+        kv_tx2 = await db.begin_db_transaction()
+        result = await kv_tx2.kv_read("test", "rollback", "key1")
+        await kv_tx2.rollback()
+
+        assert result is None, f"Value should not persist after rollback, got {bytes(result) if result else None}"
+        print("  Value not persisted after rollback")
+
+        print("  Test passed: KV transaction rollback works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_transaction_atomicity():
+    """Test that multiple operations in a transaction are atomic"""
+    print("\n=== Test: KV Transaction Atomicity ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Perform multiple writes in one transaction
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("atomic", "test", "key1", b"value1")
+        await kv_tx.kv_write("atomic", "test", "key2", b"value2")
+        await kv_tx.kv_write("atomic", "test", "key3", b"value3")
+
+        # Read within same transaction (should see uncommitted values)
+        keys_before = await kv_tx.kv_list("atomic", "test")
+        print(f"  Keys within transaction: {keys_before}")
+
+        # Rollback all
+        await kv_tx.rollback()
+        print("  Rolled back transaction")
+
+        # Verify none persisted
+        kv_tx2 = await db.begin_db_transaction()
+        keys_after = await kv_tx2.kv_list("atomic", "test")
+        await kv_tx2.rollback()
+
+        assert len(keys_after) == 0, f"Expected no keys after rollback, got {keys_after}"
+        print("  No keys persisted after rollback")
+
+        # Now do the same but commit
+        kv_tx3 = await db.begin_db_transaction()
+        await kv_tx3.kv_write("atomic", "test", "key1", b"value1")
+        await kv_tx3.kv_write("atomic", "test", "key2", b"value2")
+        await kv_tx3.kv_write("atomic", "test", "key3", b"value3")
+        await kv_tx3.commit()
+        print("  Committed transaction with 3 keys")
+
+        # Verify all persisted
+        kv_tx4 = await db.begin_db_transaction()
+        keys_final = await kv_tx4.kv_list("atomic", "test")
+        await kv_tx4.rollback()
+
+        assert len(keys_final) == 3, f"Expected 3 keys after commit, got {len(keys_final)}"
+        print(f"  All 3 keys persisted: {keys_final}")
+
+        print("  Test passed: KV transaction atomicity works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_read_within_transaction():
+    """Test reading values within the same transaction they were written"""
+    print("\n=== Test: KV Read Within Transaction ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        kv_tx = await db.begin_db_transaction()
+
+        # Write a value
+        await kv_tx.kv_write("intra", "tx", "mykey", b"myvalue")
+
+        # Read it back in same transaction
+        result = await kv_tx.kv_read("intra", "tx", "mykey")
+
+        assert result is not None, "Should be able to read uncommitted value in same tx"
+        assert bytes(result) == b"myvalue", f"Expected b'myvalue', got {bytes(result)}"
+        print("  Can read uncommitted value within transaction")
+
+        await kv_tx.rollback()
+
+        print("  Test passed: KV read within transaction works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+# Namespace Isolation Tests
+
+async def test_kv_namespace_isolation():
+    """Test that different namespaces are isolated"""
+    print("\n=== Test: KV Namespace Isolation ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write same key in different namespaces
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("app1", "config", "key", b"app1_value")
+        await kv_tx.kv_write("app2", "config", "key", b"app2_value")
+        await kv_tx.kv_write("app1", "other", "key", b"app1_other_value")
+        await kv_tx.commit()
+        print("  Written same key in different namespaces")
+
+        # Read from each namespace
+        kv_tx2 = await db.begin_db_transaction()
+
+        result1 = await kv_tx2.kv_read("app1", "config", "key")
+        result2 = await kv_tx2.kv_read("app2", "config", "key")
+        result3 = await kv_tx2.kv_read("app1", "other", "key")
+
+        await kv_tx2.rollback()
+
+        assert bytes(result1) == b"app1_value", f"Expected b'app1_value', got {bytes(result1)}"
+        assert bytes(result2) == b"app2_value", f"Expected b'app2_value', got {bytes(result2)}"
+        assert bytes(result3) == b"app1_other_value", f"Expected b'app1_other_value', got {bytes(result3)}"
+        print("  Each namespace has correct value")
+
+        print("  Test passed: KV namespace isolation works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+# Binary Data Tests
+
+async def test_kv_binary_data():
+    """Test storing and retrieving binary data"""
+    print("\n=== Test: KV Binary Data ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Various binary data types
+        test_cases = [
+            ("empty", b""),
+            ("null_byte", b"\x00"),
+            ("all_bytes", bytes(range(256))),
+            ("utf8_special", "Hello World".encode("utf-8")),
+            ("random_binary", bytes([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE])),
+        ]
+
+        kv_tx = await db.begin_db_transaction()
+        for name, data in test_cases:
+            await kv_tx.kv_write("binary", "test", name, data)
+        await kv_tx.commit()
+        print(f"  Written {len(test_cases)} binary test cases")
+
+        # Read back and verify
+        kv_tx2 = await db.begin_db_transaction()
+        for name, expected_data in test_cases:
+            result = await kv_tx2.kv_read("binary", "test", name)
+            assert result is not None, f"Expected data for {name}"
+            actual_data = bytes(result)
+            assert actual_data == expected_data, f"Mismatch for {name}: expected {expected_data!r}, got {actual_data!r}"
+            print(f"    '{name}': OK ({len(actual_data)} bytes)")
+        await kv_tx2.rollback()
+
+        print("  Test passed: KV binary data works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+async def test_kv_large_value():
+    """Test storing a large value"""
+    print("\n=== Test: KV Large Value ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Create a 1MB value
+        large_data = bytes([i % 256 for i in range(1024 * 1024)])
+
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("large", "data", "megabyte", large_data)
+        await kv_tx.commit()
+        print(f"  Written {len(large_data)} bytes")
+
+        # Read back
+        kv_tx2 = await db.begin_db_transaction()
+        result = await kv_tx2.kv_read("large", "data", "megabyte")
+        await kv_tx2.rollback()
+
+        assert result is not None, "Expected to read large value"
+        result_bytes = bytes(result)
+        assert len(result_bytes) == len(large_data), f"Size mismatch: {len(result_bytes)} vs {len(large_data)}"
+        assert result_bytes == large_data, "Data mismatch"
+        print(f"  Read back {len(result_bytes)} bytes correctly")
+
+        print("  Test passed: KV large value works")
+
+    finally:
+        cleanup_db(db_path)
+
+
+# Key Name Tests
+
+async def test_kv_special_key_names():
+    """Test keys with special characters"""
+    print("\n=== Test: KV Special Key Names ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        special_keys = [
+            "simple",
+            "with-dashes",
+            "with_underscores",
+            "MixedCase",
+            "numbers123",
+            "unicode_",  # Note: Using underscore instead of actual unicode for simplicity
+            "empty_value",
+        ]
+
+        kv_tx = await db.begin_db_transaction()
+        for i, key in enumerate(special_keys):
+            print(key)
+            await kv_tx.kv_write("special", "keys", key, f"value_{i}".encode())
+        await kv_tx.commit()
+        print(f"  Written {len(special_keys)} special keys")
+
+        # List and verify
+        kv_tx2 = await db.begin_db_transaction()
+        keys = await kv_tx2.kv_list("special", "keys")
+        await kv_tx2.rollback()
+
+        assert len(keys) == len(special_keys), f"Expected {len(special_keys)} keys, got {len(keys)}"
+        for key in special_keys:
+            assert key in keys, f"Key '{key}' not found in list"
+        print(f"  All special keys stored and listed correctly")
+
+        print("  Test passed: KV special key names work")
+
+    finally:
+        cleanup_db(db_path)
+
+
+# Database Read Method Tests
+
+async def test_kv_database_read_methods():
+    """Test kv_read and kv_list methods on the database object (not transaction)"""
+    print("\n=== Test: KV Database Read Methods ===")
+
+    db, db_path = create_test_db()
+
+    try:
+        # Write some data first
+        kv_tx = await db.begin_db_transaction()
+        await kv_tx.kv_write("dbread", "test", "key1", b"value1")
+        await kv_tx.kv_write("dbread", "test", "key2", b"value2")
+        await kv_tx.commit()
+        print("  Written test data")
+
+        # Read back using database-level kv_read (not transaction)
+        result = await db.kv_read("dbread", "test", "key1")
+        assert result is not None, "Expected to read key1"
+        assert bytes(result) == b"value1", f"Expected b'value1', got {bytes(result)}"
+        print("  db.kv_read() works")
+
+        keys = await db.kv_list("dbread", "test")
+        assert len(keys) == 2, f"Expected 2 keys, got {len(keys)}"
+        print(f"  db.kv_list() works: {keys}")
+
+        print("  Test passed: KV database read methods work")
+
+    finally:
+        cleanup_db(db_path)
+
+
+# Persistence Test
+
+async def test_kv_persistence_across_instances():
+    """Test that KV data persists when reopening the database"""
+    print("\n=== Test: KV Persistence Across Instances ===")
+
+    db_path = None
+    try:
+        # Create and write
+        with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as tmp:
+            db_path = tmp.name
+
+        backend = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db1 = cdk_ffi.create_wallet_db(backend)
+
+        kv_tx = await db1.begin_db_transaction()
+        await kv_tx.kv_write("persist", "test", "mykey", b"persistent_value")
+        await kv_tx.commit()
+        print("  Written and committed with first db instance")
+
+        # Delete reference to first db (simulating closing)
+        del db1
+        await asyncio.sleep(0.1)
+        print("  First db instance closed")
+
+        # Reopen and read
+        backend2 = cdk_ffi.WalletDbBackend.SQLITE(path=db_path)
+        db2 = cdk_ffi.create_wallet_db(backend2)
+
+        kv_tx2 = await db2.begin_db_transaction()
+        result = await kv_tx2.kv_read("persist", "test", "mykey")
+        await kv_tx2.rollback()
+
+        assert result is not None, "Data should persist across db instances"
+        assert bytes(result) == b"persistent_value", f"Expected b'persistent_value', got {bytes(result)}"
+        print("  Data persisted across db instances")
+
+        print("  Test passed: KV persistence across instances works")
+
+    finally:
+        if db_path and os.path.exists(db_path):
+            os.unlink(db_path)
+
+
+async def main():
+    """Run all KV store tests"""
+    print("Starting CDK FFI Key-Value Store Tests")
+    print("=" * 60)
+
+    tests = [
+        # Basic operations
+        ("KV Write and Read", test_kv_write_and_read),
+        ("KV Read Nonexistent", test_kv_read_nonexistent),
+        ("KV Overwrite", test_kv_overwrite),
+        ("KV Remove", test_kv_remove),
+        ("KV List Keys", test_kv_list_keys),
+        ("KV List Empty Namespace", test_kv_list_empty_namespace),
+        # Transaction tests
+        ("KV Transaction Commit", test_kv_transaction_commit),
+        ("KV Transaction Rollback", test_kv_transaction_rollback),
+        ("KV Transaction Atomicity", test_kv_transaction_atomicity),
+        ("KV Read Within Transaction", test_kv_read_within_transaction),
+        # Namespace tests
+        ("KV Namespace Isolation", test_kv_namespace_isolation),
+        # Data tests
+        ("KV Binary Data", test_kv_binary_data),
+        ("KV Large Value", test_kv_large_value),
+        ("KV Special Key Names", test_kv_special_key_names),
+        # Database methods
+        ("KV Database Read Methods", test_kv_database_read_methods),
+        # Persistence
+        ("KV Persistence Across Instances", test_kv_persistence_across_instances),
+    ]
+
+    passed = 0
+    failed = 0
+
+    for test_name, test_func in tests:
+        try:
+            await test_func()
+            passed += 1
+        except Exception as e:
+            failed += 1
+            print(f"\n  Test failed: {test_name}")
+            print(f"  Error: {e}")
+            import traceback
+            traceback.print_exc()
+
+    print("\n" + "=" * 60)
+    print(f"Test Results: {passed} passed, {failed} failed")
+    print("=" * 60)
+
+    return 0 if failed == 0 else 1
+
+
+if __name__ == "__main__":
+    exit_code = asyncio.run(main())
+    sys.exit(exit_code)

+ 1 - 1
crates/cdk-integration-tests/src/init_pure_tests.rs

@@ -315,7 +315,7 @@ pub async fn create_test_wallet_for_mint(mint: Mint) -> Result<Wallet> {
     // Read environment variable to determine database type
     let db_type = env::var("CDK_TEST_DB_TYPE").expect("Database type set");
 
-    let localstore: Arc<dyn WalletDatabase<Err = cdk_database::Error> + Send + Sync> =
+    let localstore: Arc<dyn WalletDatabase<cdk_database::Error> + Send + Sync> =
         match db_type.to_lowercase().as_str() {
             "sqlite" => {
                 // Create a temporary directory for SQLite database

+ 25 - 17
crates/cdk-redb/src/wallet/mod.rs

@@ -214,11 +214,9 @@ impl WalletRedbDatabase {
 }
 
 #[async_trait]
-impl WalletDatabase for WalletRedbDatabase {
-    type Err = database::Error;
-
+impl WalletDatabase<database::Error> for WalletRedbDatabase {
     #[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>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
 
@@ -233,7 +231,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip(self))]
-    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn.open_table(MINTS_TABLE).map_err(Error::from)?;
         let mints = table
@@ -254,7 +252,7 @@ impl WalletDatabase for WalletRedbDatabase {
     async fn get_mint_keysets(
         &self,
         mint_url: MintUrl,
-    ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
+    ) -> Result<Option<Vec<KeySetInfo>>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn
             .open_multimap_table(MINT_KEYSETS_TABLE)
@@ -289,7 +287,10 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip(self), fields(keyset_id = %keyset_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>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
 
@@ -308,7 +309,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[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>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn
             .open_table(MINT_QUOTES_TABLE)
@@ -322,7 +323,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip_all)]
-    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Into::<Error>::into)?;
         let table = read_txn
             .open_table(MINT_QUOTES_TABLE)
@@ -354,7 +355,10 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[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>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn
             .open_table(MELT_QUOTES_TABLE)
@@ -368,7 +372,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip_all)]
-    async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, Self::Err> {
+    async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn
             .open_table(MELT_QUOTES_TABLE)
@@ -383,7 +387,7 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[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>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn.open_table(MINT_KEYS_TABLE).map_err(Error::from)?;
 
@@ -404,7 +408,7 @@ impl WalletDatabase for WalletRedbDatabase {
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
-    ) -> Result<Vec<ProofInfo>, Self::Err> {
+    ) -> Result<Vec<ProofInfo>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
 
         let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
@@ -431,7 +435,10 @@ impl WalletDatabase for WalletRedbDatabase {
     }
 
     #[instrument(skip(self, ys))]
-    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>, database::Error> {
         if ys.is_empty() {
             return Ok(Vec::new());
         }
@@ -468,7 +475,7 @@ impl WalletDatabase for WalletRedbDatabase {
     async fn get_transaction(
         &self,
         transaction_id: TransactionId,
-    ) -> Result<Option<Transaction>, Self::Err> {
+    ) -> Result<Option<Transaction>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
         let table = read_txn
             .open_table(TRANSACTIONS_TABLE)
@@ -487,7 +494,7 @@ impl WalletDatabase for WalletRedbDatabase {
         mint_url: Option<MintUrl>,
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
-    ) -> Result<Vec<Transaction>, Self::Err> {
+    ) -> Result<Vec<Transaction>, database::Error> {
         let read_txn = self.db.begin_read().map_err(Error::from)?;
 
         let table = read_txn
@@ -516,7 +523,8 @@ impl WalletDatabase for WalletRedbDatabase {
 
     async fn begin_db_transaction(
         &self,
-    ) -> Result<Box<dyn WalletDatabaseTransaction<Self::Err> + Send + Sync>, Self::Err> {
+    ) -> Result<Box<dyn WalletDatabaseTransaction<database::Error> + Send + Sync>, database::Error>
+    {
         let write_txn = self.db.begin_write().map_err(Error::from)?;
         Ok(Box::new(RedbWalletTransaction::new(write_txn)))
     }

+ 18 - 0
crates/cdk-sql-common/src/wallet/migrations/postgres/20251215000000_add_kv_store.sql

@@ -0,0 +1,18 @@
+-- Add kv_store table for generic key-value storage
+CREATE TABLE IF NOT EXISTS kv_store (
+    primary_namespace TEXT NOT NULL,
+    secondary_namespace TEXT NOT NULL,
+    key TEXT NOT NULL,
+    value BYTEA NOT NULL,
+    created_time BIGINT NOT NULL,
+    updated_time BIGINT NOT NULL,
+    PRIMARY KEY (primary_namespace, secondary_namespace, key)
+);
+
+-- Index for efficient listing of keys by namespace
+CREATE INDEX IF NOT EXISTS idx_kv_store_namespaces
+ON kv_store (primary_namespace, secondary_namespace);
+
+-- Index for efficient querying by update time
+CREATE INDEX IF NOT EXISTS idx_kv_store_updated_time
+ON kv_store (updated_time);

+ 18 - 0
crates/cdk-sql-common/src/wallet/migrations/sqlite/20251215000000_add_kv_store.sql

@@ -0,0 +1,18 @@
+-- Add kv_store table for generic key-value storage
+CREATE TABLE IF NOT EXISTS kv_store (
+    primary_namespace TEXT NOT NULL,
+    secondary_namespace TEXT NOT NULL,
+    key TEXT NOT NULL,
+    value BLOB NOT NULL,
+    created_time INTEGER NOT NULL,
+    updated_time INTEGER NOT NULL,
+    PRIMARY KEY (primary_namespace, secondary_namespace, key)
+);
+
+-- Index for efficient listing of keys by namespace
+CREATE INDEX IF NOT EXISTS idx_kv_store_namespaces
+ON kv_store (primary_namespace, secondary_namespace);
+
+-- Index for efficient querying by update time
+CREATE INDEX IF NOT EXISTS idx_kv_store_updated_time
+ON kv_store (updated_time);

+ 26 - 18
crates/cdk-sql-common/src/wallet/mod.rs

@@ -884,15 +884,14 @@ where
 }
 
 #[async_trait]
-impl<RM> WalletDatabase for SQLWalletDatabase<RM>
+impl<RM> WalletDatabase<database::Error> for SQLWalletDatabase<RM>
 where
     RM: DatabasePool + 'static,
 {
-    type Err = database::Error;
-
     async fn begin_db_transaction(
         &self,
-    ) -> Result<Box<dyn WalletDatabaseTransaction<Self::Err> + Send + Sync>, Self::Err> {
+    ) -> Result<Box<dyn WalletDatabaseTransaction<database::Error> + Send + Sync>, database::Error>
+    {
         Ok(Box::new(SQLWalletTransaction {
             inner: ConnectionWithTransaction::new(
                 self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,
@@ -902,7 +901,7 @@ where
     }
 
     #[instrument(skip(self))]
-    async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, Self::Err> {
+    async fn get_melt_quotes(&self) -> Result<Vec<wallet::MeltQuote>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
 
         Ok(query(
@@ -929,7 +928,7 @@ where
     }
 
     #[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>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         Ok(query(
             r#"
@@ -959,7 +958,7 @@ where
     }
 
     #[instrument(skip(self))]
-    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, Self::Err> {
+    async fn get_mints(&self) -> Result<HashMap<MintUrl, Option<MintInfo>>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         Ok(query(
             r#"
@@ -999,7 +998,7 @@ where
     async fn get_mint_keysets(
         &self,
         mint_url: MintUrl,
-    ) -> Result<Option<Vec<KeySetInfo>>, Self::Err> {
+    ) -> Result<Option<Vec<KeySetInfo>>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
 
         let keysets = query(
@@ -1029,19 +1028,22 @@ where
     }
 
     #[instrument(skip(self), fields(keyset_id = %keyset_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>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         get_keyset_by_id_inner(&*conn, keyset_id, false).await
     }
 
     #[instrument(skip(self))]
-    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>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         get_mint_quote_inner(&*conn, quote_id, false).await
     }
 
     #[instrument(skip(self))]
-    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, Self::Err> {
+    async fn get_mint_quotes(&self) -> Result<Vec<MintQuote>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         Ok(query(
             r#"
@@ -1101,13 +1103,16 @@ where
     }
 
     #[instrument(skip(self))]
-    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>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         get_melt_quote_inner(&*conn, quote_id, false).await
     }
 
     #[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>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         get_keys_inner(&*conn, keyset_id).await
     }
@@ -1119,13 +1124,16 @@ where
         unit: Option<CurrencyUnit>,
         state: Option<Vec<State>>,
         spending_conditions: Option<Vec<SpendingConditions>>,
-    ) -> Result<Vec<ProofInfo>, Self::Err> {
+    ) -> Result<Vec<ProofInfo>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         get_proofs_inner(&*conn, mint_url, unit, state, spending_conditions, false).await
     }
 
     #[instrument(skip(self, ys))]
-    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>, database::Error> {
         if ys.is_empty() {
             return Ok(Vec::new());
         }
@@ -1164,7 +1172,7 @@ where
         mint_url: Option<MintUrl>,
         unit: Option<CurrencyUnit>,
         states: Option<Vec<State>>,
-    ) -> Result<u64, Self::Err> {
+    ) -> Result<u64, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
 
         let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string();
@@ -1227,7 +1235,7 @@ where
     async fn get_transaction(
         &self,
         transaction_id: TransactionId,
-    ) -> Result<Option<Transaction>, Self::Err> {
+    ) -> Result<Option<Transaction>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
         Ok(query(
             r#"
@@ -1263,7 +1271,7 @@ where
         mint_url: Option<MintUrl>,
         direction: Option<TransactionDirection>,
         unit: Option<CurrencyUnit>,
-    ) -> Result<Vec<Transaction>, Self::Err> {
+    ) -> Result<Vec<Transaction>, database::Error> {
         let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
 
         Ok(query(

+ 2 - 2
crates/cdk/src/wallet/auth/auth_wallet.rs

@@ -39,7 +39,7 @@ pub struct AuthWallet {
     /// Mint Url
     pub mint_url: MintUrl,
     /// Storage backend
-    pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+    pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
     /// Mint metadata cache (lock-free cached access to keys, keysets, and mint info)
     pub metadata_cache: Arc<MintMetadataCache>,
     /// Protected methods
@@ -56,7 +56,7 @@ impl AuthWallet {
     pub fn new(
         mint_url: MintUrl,
         cat: Option<AuthToken>,
-        localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         metadata_cache: Arc<MintMetadataCache>,
         protected_endpoints: HashMap<ProtectedEndpoint, AuthRequired>,
         oidc_client: Option<OidcClient>,

+ 2 - 2
crates/cdk/src/wallet/builder.rs

@@ -22,7 +22,7 @@ use crate::wallet::{HttpClient, MintConnector, SubscriptionManager, Wallet};
 pub struct WalletBuilder {
     mint_url: Option<MintUrl>,
     unit: Option<CurrencyUnit>,
-    localstore: Option<Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>>,
+    localstore: Option<Arc<dyn WalletDatabase<database::Error> + Send + Sync>>,
     target_proof_count: Option<usize>,
     #[cfg(feature = "auth")]
     auth_wallet: Option<AuthWallet>,
@@ -93,7 +93,7 @@ impl WalletBuilder {
     /// Set the local storage backend
     pub fn localstore(
         mut self,
-        localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
     ) -> Self {
         self.localstore = Some(localstore);
         self

+ 5 - 5
crates/cdk/src/wallet/mint_metadata_cache.rs

@@ -224,7 +224,7 @@ impl MintMetadataCache {
     #[inline(always)]
     pub async fn load_from_mint(
         &self,
-        storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        storage: &Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
     ) -> Result<Arc<MintMetadata>, Error> {
         // Acquire lock to ensure only one fetch at a time
@@ -296,7 +296,7 @@ impl MintMetadataCache {
     #[inline(always)]
     pub async fn load(
         &self,
-        storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        storage: &Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
         ttl: Option<Duration>,
     ) -> Result<Arc<MintMetadata>, Error> {
@@ -345,7 +345,7 @@ impl MintMetadataCache {
     #[cfg(feature = "auth")]
     pub async fn load_auth(
         &self,
-        storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        storage: &Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         auth_client: &Arc<dyn AuthMintConnector + Send + Sync>,
     ) -> Result<Arc<MintMetadata>, Error> {
         let cached_metadata = self.metadata.load().clone();
@@ -418,7 +418,7 @@ impl MintMetadataCache {
     /// 3. Update the sync tracking to record this storage has been updated
     async fn database_sync(
         &self,
-        storage: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        storage: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         metadata: Arc<MintMetadata>,
     ) {
         let mint_url = self.mint_url.clone();
@@ -441,7 +441,7 @@ impl MintMetadataCache {
     /// * `db_sync_versions` - Shared version tracker
     async fn persist_to_database(
         mint_url: MintUrl,
-        storage: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        storage: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         metadata: Arc<MintMetadata>,
         db_sync_versions: Arc<RwLock<HashMap<usize, usize>>>,
     ) {

+ 2 - 2
crates/cdk/src/wallet/mod.rs

@@ -90,7 +90,7 @@ pub struct Wallet {
     /// Unit
     pub unit: CurrencyUnit,
     /// Storage backend
-    pub localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+    pub localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
     /// Mint metadata cache for this mint (lock-free cached access to keys, keysets, and mint info)
     pub metadata_cache: Arc<MintMetadataCache>,
     /// The targeted amount of proofs to have at each size
@@ -190,7 +190,7 @@ impl Wallet {
     pub fn new(
         mint_url: &str,
         unit: CurrencyUnit,
-        localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         seed: [u8; 64],
         target_proof_count: Option<usize>,
     ) -> Result<Self, Error> {

+ 5 - 5
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -165,7 +165,7 @@ impl WalletConfig {
 #[derive(Clone)]
 pub struct MultiMintWallet {
     /// Storage backend
-    localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+    localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
     seed: [u8; 64],
     /// The currency unit this wallet supports
     unit: CurrencyUnit,
@@ -181,7 +181,7 @@ pub struct MultiMintWallet {
 impl MultiMintWallet {
     /// Create a new [MultiMintWallet] for a specific currency unit
     pub async fn new(
-        localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         seed: [u8; 64],
         unit: CurrencyUnit,
     ) -> Result<Self, Error> {
@@ -206,7 +206,7 @@ impl MultiMintWallet {
     /// All wallets in this MultiMintWallet will use the specified proxy.
     /// This allows you to route all mint connections through a proxy server.
     pub async fn new_with_proxy(
-        localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         seed: [u8; 64],
         unit: CurrencyUnit,
         proxy_url: url::Url,
@@ -235,7 +235,7 @@ impl MultiMintWallet {
     /// is bootstrapped and shared across wallets.
     #[cfg(all(feature = "tor", not(target_arch = "wasm32")))]
     pub async fn new_with_tor(
-        localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
+        localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync>,
         seed: [u8; 64],
         unit: CurrencyUnit,
     ) -> Result<Self, Error> {
@@ -2055,7 +2055,7 @@ mod tests {
     use super::*;
 
     async fn create_test_multi_wallet() -> MultiMintWallet {
-        let localstore: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync> = Arc::new(
+        let localstore: Arc<dyn WalletDatabase<database::Error> + Send + Sync> = Arc::new(
             cdk_sqlite::wallet::memory::empty()
                 .await
                 .expect("Failed to create in-memory database"),

+ 1 - 0
justfile

@@ -596,6 +596,7 @@ ffi-test: ffi-generate-python
   set -euo pipefail
   echo "🧪 Running Python FFI tests..."
   python3 crates/cdk-ffi/tests/test_transactions.py
+  python3 crates/cdk-ffi/tests/test_kvstore.py
   echo "✅ Tests completed!"
 
 # Build debug version and generate Python bindings quickly (for development)