소스 검색

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)