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

Refactor KVStore to shared module with generic implementations (#1393)

* Refactor KVStore to shared module with generic implementations

Fixes #1273

Consolidates all KVStore code from mint-specific implementations to shared
modules that can be used by both mint and wallet databases, eliminating code
duplication and ensuring consistency.

Changes:
- Created cdk-common/src/database/kvstore.rs with shared KVStore trait
  definitions
- Created cdk-sql-common/src/keyvalue.rs with generic SQL implementations
- Moved KVStore traits (KVStore, KVStoreDatabase, KVStoreTransaction) to
  top-level database module
- Added DynKVStore type alias at cdk_common::database level
- Removed Mint-prefixed KVStore aliases (MintKVStore, MintKVStoreDatabase,
  MintKVStoreTransaction)
- Updated all code references to use shared KVStore types instead of
  mint-specific ones
- Implemented KVStore traits for SQLWalletDatabase and SQLWalletTransaction
- Refactored mint module to delegate to generic keyvalue functions
- Updated cdk-cln, cdk-lnd, cdk-mintd to use DynKVStore instead of
  DynMintKVStore

Benefits:
- Reduces ~300 lines of duplicated code
- Single source of truth for KVStore functionality
- Wallet databases now have KVStore support
- Improved maintainability and consistency

* Implement KVStore traits for wallet database

Add KVStore support to cdk-redb wallet implementation, completing the
KVStore unification across all database backends. This implementation
uses redb's native tuple keys for efficient namespace-based storage.

Changes:
- Add KV_STORE_TABLE definition with composite key (primary_namespace,
  secondary_namespace, key)
- Implement KVStoreTransaction for RedbWalletTransaction with support
  for transactional read, write, remove, and list operations
- Implement KVStoreDatabase for WalletRedbDatabase with non-transactional
  read and list operations
- Implement KVStore trait to provide transaction creation
- Initialize KV_STORE_TABLE during database creation

The implementation follows the same validation and namespace rules as
the SQL-based implementations, ensuring consistency across all database
backends.

* Update reference for kv transaction

* 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

* Simplify kvstore validation with optional key parameter

Change validate_kvstore_params to accept Option<&str> for the key parameter,
allowing kv_list operations to share the same validation logic without
duplicating namespace validation code.

* Fix kv_list range query to properly iterate by namespace

Replace the invalid range end marker "\u{10FFFF}" with an open-ended range that
breaks when the namespace prefix changes. This correctly iterates only keys
within the specified namespace.

* Deduplicate KV store logic with KvTable enum

Introduce KvTable enum to abstract over read-only and read-write table access,
eliminating duplicate kv_read and kv_list implementations between
KVStoreDatabase and KVStoreTransaction.
C пре 3 недеља
родитељ
комит
4e1b3d90f9
31 измењених фајлова са 1900 додато и 427 уклоњено
  1. 1 1
      crates/cdk-cli/src/main.rs
  2. 3 3
      crates/cdk-cln/src/lib.rs
  3. 135 0
      crates/cdk-common/src/database/kvstore.rs
  4. 6 128
      crates/cdk-common/src/database/mint/mod.rs
  5. 16 16
      crates/cdk-common/src/database/mint/test/kvstore.rs
  6. 2 2
      crates/cdk-common/src/database/mint/test/mod.rs
  7. 33 6
      crates/cdk-common/src/database/mod.rs
  8. 24 24
      crates/cdk-common/src/database/wallet.rs
  9. 313 19
      crates/cdk-ffi/src/database.rs
  10. 21 0
      crates/cdk-ffi/src/postgres.rs
  11. 21 0
      crates/cdk-ffi/src/sqlite.rs
  12. 648 0
      crates/cdk-ffi/tests/test_kvstore.py
  13. 1 1
      crates/cdk-integration-tests/src/init_pure_tests.rs
  14. 3 3
      crates/cdk-integration-tests/src/init_regtest.rs
  15. 3 3
      crates/cdk-lnd/src/lib.rs
  16. 7 7
      crates/cdk-mintd/src/lib.rs
  17. 8 8
      crates/cdk-mintd/src/setup.rs
  18. 227 23
      crates/cdk-redb/src/wallet/mod.rs
  19. 208 0
      crates/cdk-sql-common/src/keyvalue.rs
  20. 1 0
      crates/cdk-sql-common/src/lib.rs
  21. 33 148
      crates/cdk-sql-common/src/mint/mod.rs
  22. 18 0
      crates/cdk-sql-common/src/wallet/migrations/postgres/20251215000000_add_kv_store.sql
  23. 18 0
      crates/cdk-sql-common/src/wallet/migrations/sqlite/20251215000000_add_kv_store.sql
  24. 132 18
      crates/cdk-sql-common/src/wallet/mod.rs
  25. 1 1
      crates/cdk/src/lib.rs
  26. 2 2
      crates/cdk/src/wallet/auth/auth_wallet.rs
  27. 2 2
      crates/cdk/src/wallet/builder.rs
  28. 5 5
      crates/cdk/src/wallet/mint_metadata_cache.rs
  29. 2 2
      crates/cdk/src/wallet/mod.rs
  30. 5 5
      crates/cdk/src/wallet/multi_mint_wallet.rs
  31. 1 0
      justfile

+ 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");

+ 3 - 3
crates/cdk-cln/src/lib.rs

@@ -14,7 +14,7 @@ use async_trait::async_trait;
 use bitcoin::hashes::sha256::Hash;
 use cdk_common::amount::{to_unit, Amount};
 use cdk_common::common::FeeReserve;
-use cdk_common::database::mint::DynMintKVStore;
+use cdk_common::database::DynKVStore;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
     self, Bolt11IncomingPaymentOptions, Bolt11Settings, Bolt12IncomingPaymentOptions,
@@ -54,7 +54,7 @@ pub struct Cln {
     fee_reserve: FeeReserve,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
-    kv_store: DynMintKVStore,
+    kv_store: DynKVStore,
 }
 
 impl Cln {
@@ -62,7 +62,7 @@ impl Cln {
     pub async fn new(
         rpc_socket: PathBuf,
         fee_reserve: FeeReserve,
-        kv_store: DynMintKVStore,
+        kv_store: DynKVStore,
     ) -> Result<Self, Error> {
         Ok(Self {
             rpc_socket,

+ 135 - 0
crates/cdk-common/src/database/kvstore.rs

@@ -0,0 +1,135 @@
+//! Key-Value Store Database traits and utilities
+//!
+//! This module provides shared KVStore functionality that can be used by both
+//! mint and wallet database implementations.
+
+use async_trait::async_trait;
+
+use super::{DbTransactionFinalizer, Error};
+
+/// Valid ASCII characters for namespace and key strings in KV store
+pub const KVSTORE_NAMESPACE_KEY_ALPHABET: &str =
+    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
+
+/// Maximum length for namespace and key strings in KV store
+pub const KVSTORE_NAMESPACE_KEY_MAX_LEN: usize = 120;
+
+/// Validates that a string contains only valid KV store characters and is within length limits
+pub fn validate_kvstore_string(s: &str) -> Result<(), Error> {
+    if s.len() > KVSTORE_NAMESPACE_KEY_MAX_LEN {
+        return Err(Error::KVStoreInvalidKey(format!(
+            "{KVSTORE_NAMESPACE_KEY_MAX_LEN} exceeds maximum length of key characters"
+        )));
+    }
+
+    if !s
+        .chars()
+        .all(|c| KVSTORE_NAMESPACE_KEY_ALPHABET.contains(c))
+    {
+        return Err(Error::KVStoreInvalidKey("key contains invalid characters. Only ASCII letters, numbers, underscore, and hyphen are allowed".to_string()));
+    }
+
+    Ok(())
+}
+
+/// Validates namespace and key parameters for KV store operations
+pub fn validate_kvstore_params(
+    primary_namespace: &str,
+    secondary_namespace: &str,
+    key: Option<&str>,
+) -> Result<(), Error> {
+    // Validate primary namespace
+    validate_kvstore_string(primary_namespace)?;
+
+    // Validate secondary namespace
+    validate_kvstore_string(secondary_namespace)?;
+
+    // Check empty namespace rules
+    if primary_namespace.is_empty() && !secondary_namespace.is_empty() {
+        return Err(Error::KVStoreInvalidKey(
+            "If primary_namespace is empty, secondary_namespace must also be empty".to_string(),
+        ));
+    }
+
+    if let Some(key) = key {
+        // Validate key
+        validate_kvstore_string(key)?;
+
+        // Check for potential collisions between keys and namespaces in the same namespace
+        let namespace_key = format!("{primary_namespace}/{secondary_namespace}");
+        if key == primary_namespace || key == secondary_namespace || key == namespace_key {
+            return Err(Error::KVStoreInvalidKey(format!(
+                "Key '{key}' conflicts with namespace names"
+            )));
+        }
+    }
+
+    Ok(())
+}
+
+/// Key-Value Store Transaction trait
+#[async_trait]
+pub trait KVStoreTransaction<Error>: DbTransactionFinalizer<Err = Error> {
+    /// Read value from key-value store
+    async fn kv_read(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Error>;
+
+    /// Write value to key-value store
+    async fn kv_write(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+        value: &[u8],
+    ) -> Result<(), Error>;
+
+    /// Remove value from key-value store
+    async fn kv_remove(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<(), Error>;
+
+    /// List keys in a namespace
+    async fn kv_list(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Error>;
+}
+
+/// Key-Value Store Database trait
+#[async_trait]
+pub trait KVStoreDatabase {
+    /// KV Store Database Error
+    type Err: Into<Error> + From<Error>;
+
+    /// Read value from key-value store
+    async fn kv_read(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Self::Err>;
+
+    /// List keys in a namespace
+    async fn kv_list(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Self::Err>;
+}
+
+/// Key-Value Store trait combining read operations with transaction support
+#[async_trait]
+pub trait KVStore: KVStoreDatabase {
+    /// Begins a KV transaction
+    async fn begin_transaction(
+        &self,
+    ) -> Result<Box<dyn KVStoreTransaction<Self::Err> + Send + Sync>, Error>;
+}

+ 6 - 128
crates/cdk-common/src/database/mint/mod.rs

@@ -23,63 +23,11 @@ pub mod test;
 #[cfg(feature = "auth")]
 pub use auth::{DynMintAuthDatabase, MintAuthDatabase, MintAuthTransaction};
 
-/// Valid ASCII characters for namespace and key strings in KV store
-pub const KVSTORE_NAMESPACE_KEY_ALPHABET: &str =
-    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
-
-/// Maximum length for namespace and key strings in KV store
-pub const KVSTORE_NAMESPACE_KEY_MAX_LEN: usize = 120;
-
-/// Validates that a string contains only valid KV store characters and is within length limits
-pub fn validate_kvstore_string(s: &str) -> Result<(), Error> {
-    if s.len() > KVSTORE_NAMESPACE_KEY_MAX_LEN {
-        return Err(Error::KVStoreInvalidKey(format!(
-            "{KVSTORE_NAMESPACE_KEY_MAX_LEN} exceeds maximum length of key characters"
-        )));
-    }
-
-    if !s
-        .chars()
-        .all(|c| KVSTORE_NAMESPACE_KEY_ALPHABET.contains(c))
-    {
-        return Err(Error::KVStoreInvalidKey("key contains invalid characters. Only ASCII letters, numbers, underscore, and hyphen are allowed".to_string()));
-    }
-
-    Ok(())
-}
-
-/// Validates namespace and key parameters for KV store operations
-pub fn validate_kvstore_params(
-    primary_namespace: &str,
-    secondary_namespace: &str,
-    key: &str,
-) -> Result<(), Error> {
-    // Validate primary namespace
-    validate_kvstore_string(primary_namespace)?;
-
-    // Validate secondary namespace
-    validate_kvstore_string(secondary_namespace)?;
-
-    // Validate key
-    validate_kvstore_string(key)?;
-
-    // Check empty namespace rules
-    if primary_namespace.is_empty() && !secondary_namespace.is_empty() {
-        return Err(Error::KVStoreInvalidKey(
-            "If primary_namespace is empty, secondary_namespace must also be empty".to_string(),
-        ));
-    }
-
-    // Check for potential collisions between keys and namespaces in the same namespace
-    let namespace_key = format!("{primary_namespace}/{secondary_namespace}");
-    if key == primary_namespace || key == secondary_namespace || key == namespace_key {
-        return Err(Error::KVStoreInvalidKey(format!(
-            "Key '{key}' conflicts with namespace names"
-        )));
-    }
-
-    Ok(())
-}
+// Re-export KVStore types from shared module for backward compatibility
+pub use super::kvstore::{
+    validate_kvstore_params, validate_kvstore_string, KVStore, KVStoreDatabase, KVStoreTransaction,
+    KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN,
+};
 
 /// Information about a melt request stored in the database
 #[derive(Debug, Clone, PartialEq, Eq)]
@@ -439,88 +387,18 @@ pub trait CompletedOperationsDatabase {
     async fn get_completed_operations(&self) -> Result<Vec<mint::Operation>, Self::Err>;
 }
 
-/// Key-Value Store Transaction trait
-#[async_trait]
-pub trait KVStoreTransaction<'a, Error>: DbTransactionFinalizer<Err = Error> {
-    /// Read value from key-value store
-    async fn kv_read(
-        &mut self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-        key: &str,
-    ) -> Result<Option<Vec<u8>>, Error>;
-
-    /// Write value to key-value store
-    async fn kv_write(
-        &mut self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-        key: &str,
-        value: &[u8],
-    ) -> Result<(), Error>;
-
-    /// Remove value from key-value store
-    async fn kv_remove(
-        &mut self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-        key: &str,
-    ) -> Result<(), Error>;
-
-    /// List keys in a namespace
-    async fn kv_list(
-        &mut self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-    ) -> Result<Vec<String>, Error>;
-}
-
 /// Base database writer
 pub trait Transaction<'a, Error>:
     DbTransactionFinalizer<Err = Error>
     + QuotesTransaction<'a, Err = Error>
     + SignaturesTransaction<'a, Err = Error>
     + ProofsTransaction<'a, Err = Error>
-    + KVStoreTransaction<'a, Error>
+    + KVStoreTransaction<Error>
     + SagaTransaction<'a, Err = Error>
     + CompletedOperationsTransaction<'a, Err = Error>
 {
 }
 
-/// Key-Value Store Database trait
-#[async_trait]
-pub trait KVStoreDatabase {
-    /// KV Store Database Error
-    type Err: Into<Error> + From<Error>;
-
-    /// Read value from key-value store
-    async fn kv_read(
-        &self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-        key: &str,
-    ) -> Result<Option<Vec<u8>>, Self::Err>;
-
-    /// List keys in a namespace
-    async fn kv_list(
-        &self,
-        primary_namespace: &str,
-        secondary_namespace: &str,
-    ) -> Result<Vec<String>, Self::Err>;
-}
-
-/// Key-Value Store Database trait
-#[async_trait]
-pub trait KVStore: KVStoreDatabase {
-    /// Begins a KV transaction
-    async fn begin_transaction<'a>(
-        &'a self,
-    ) -> Result<Box<dyn KVStoreTransaction<'a, Self::Err> + Send + Sync + 'a>, Error>;
-}
-
-/// Type alias for Mint Kv store
-pub type DynMintKVStore = std::sync::Arc<dyn KVStore<Err = Error> + Send + Sync>;
-
 /// Mint Database trait
 #[async_trait]
 pub trait Database<Error>:

+ 16 - 16
crates/cdk-common/src/database/mint/test/kvstore.rs

@@ -84,16 +84,16 @@ mod tests {
     #[test]
     fn test_validate_kvstore_params_valid() {
         // Test valid parameter combinations
-        assert!(validate_kvstore_params("primary", "secondary", "key").is_ok());
-        assert!(validate_kvstore_params("primary", "", "key").is_ok());
-        assert!(validate_kvstore_params("", "", "key").is_ok());
-        assert!(validate_kvstore_params("p1", "s1", "different_key").is_ok());
+        assert!(validate_kvstore_params("primary", "secondary", Some("key")).is_ok());
+        assert!(validate_kvstore_params("primary", "", Some("key")).is_ok());
+        assert!(validate_kvstore_params("", "", Some("key")).is_ok());
+        assert!(validate_kvstore_params("p1", "s1", Some("different_key")).is_ok());
     }
 
     #[test]
     fn test_validate_kvstore_params_empty_namespace_rules() {
         // Test empty namespace rules: if primary is empty, secondary must be empty too
-        let result = validate_kvstore_params("", "secondary", "key");
+        let result = validate_kvstore_params("", "secondary", Some("key"));
         assert!(result.is_err());
         assert!(result
             .unwrap_err()
@@ -110,7 +110,7 @@ mod tests {
         ];
 
         for (primary, secondary, key) in test_cases {
-            let result = validate_kvstore_params(primary, secondary, key);
+            let result = validate_kvstore_params(primary, secondary, Some(key));
             assert!(
                 result.is_err(),
                 "Expected collision for key '{}' with namespaces '{}'/'{}'",
@@ -123,20 +123,20 @@ mod tests {
         }
 
         // Test that a combined namespace string would be invalid due to the slash character
-        let result = validate_kvstore_params("primary", "secondary", "primary_secondary");
+        let result = validate_kvstore_params("primary", "secondary", Some("primary_secondary"));
         assert!(result.is_ok(), "This should be valid - no actual collision");
     }
 
     #[test]
     fn test_validate_kvstore_params_invalid_strings() {
         // Test invalid characters in any parameter
-        let result = validate_kvstore_params("primary@", "secondary", "key");
+        let result = validate_kvstore_params("primary@", "secondary", Some("key"));
         assert!(result.is_err());
 
-        let result = validate_kvstore_params("primary", "secondary!", "key");
+        let result = validate_kvstore_params("primary", "secondary!", Some("key"));
         assert!(result.is_err());
 
-        let result = validate_kvstore_params("primary", "secondary", "key with space");
+        let result = validate_kvstore_params("primary", "secondary", Some("key with space"));
         assert!(result.is_err());
     }
 
@@ -179,7 +179,7 @@ mod tests {
 
         for (primary, secondary, key) in valid_examples {
             assert!(
-                validate_kvstore_params(primary, secondary, key).is_ok(),
+                validate_kvstore_params(primary, secondary, Some(key)).is_ok(),
                 "Valid example should pass: '{}'/'{}'/'{}'",
                 primary,
                 secondary,
@@ -196,12 +196,12 @@ mod tests {
         // but ensures naming conflicts don't occur between keys and namespaces.
 
         // These should be valid (different namespaces)
-        assert!(validate_kvstore_params("ns1", "sub1", "key1").is_ok());
-        assert!(validate_kvstore_params("ns2", "sub1", "key1").is_ok()); // same key, different primary namespace
-        assert!(validate_kvstore_params("ns1", "sub2", "key1").is_ok()); // same key, different secondary namespace
+        assert!(validate_kvstore_params("ns1", "sub1", Some("key1")).is_ok());
+        assert!(validate_kvstore_params("ns2", "sub1", Some("key1")).is_ok()); // same key, different primary namespace
+        assert!(validate_kvstore_params("ns1", "sub2", Some("key1")).is_ok()); // same key, different secondary namespace
 
         // These should fail (collision within namespace)
-        assert!(validate_kvstore_params("ns1", "sub1", "ns1").is_err()); // key conflicts with primary namespace
-        assert!(validate_kvstore_params("ns1", "sub1", "sub1").is_err()); // key conflicts with secondary namespace
+        assert!(validate_kvstore_params("ns1", "sub1", Some("ns1")).is_err()); // key conflicts with primary namespace
+        assert!(validate_kvstore_params("ns1", "sub1", Some("sub1")).is_err()); // key conflicts with secondary namespace
     }
 }

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

@@ -13,7 +13,7 @@ use cashu::secret::Secret;
 use cashu::{Amount, CurrencyUnit, SecretKey};
 
 use super::*;
-use crate::database::MintKVStoreDatabase;
+use crate::database::KVStoreDatabase;
 use crate::mint::MintKeySetInfo;
 
 mod keys;
@@ -111,7 +111,7 @@ where
 /// Test KV store functionality including write, read, list, update, and remove operations
 pub async fn kvstore_functionality<DB>(db: DB)
 where
-    DB: Database<crate::database::Error> + MintKVStoreDatabase<Err = crate::database::Error>,
+    DB: Database<crate::database::Error> + KVStoreDatabase<Err = crate::database::Error>,
 {
     // Test basic read/write operations in transaction
     {

+ 33 - 6
crates/cdk-common/src/database/mod.rs

@@ -1,18 +1,27 @@
 //! CDK Database
 
+mod kvstore;
+
 #[cfg(feature = "mint")]
 pub mod mint;
 #[cfg(feature = "wallet")]
 mod wallet;
 
+// Re-export shared KVStore types at the top level for both mint and wallet
+pub use kvstore::{
+    validate_kvstore_params, validate_kvstore_string, KVStore, KVStoreDatabase, KVStoreTransaction,
+    KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN,
+};
+
+/// Arc-wrapped KV store for shared ownership
+pub type DynKVStore = std::sync::Arc<dyn KVStore<Err = Error> + Send + Sync>;
+
 #[cfg(feature = "mint")]
 pub use mint::{
-    Database as MintDatabase, DynMintDatabase, KVStore as MintKVStore,
-    KVStoreDatabase as MintKVStoreDatabase, KVStoreTransaction as MintKVStoreTransaction,
-    KeysDatabase as MintKeysDatabase, KeysDatabaseTransaction as MintKeyDatabaseTransaction,
-    ProofsDatabase as MintProofsDatabase, ProofsTransaction as MintProofsTransaction,
-    QuotesDatabase as MintQuotesDatabase, QuotesTransaction as MintQuotesTransaction,
-    SignaturesDatabase as MintSignaturesDatabase,
+    Database as MintDatabase, DynMintDatabase, KeysDatabase as MintKeysDatabase,
+    KeysDatabaseTransaction as MintKeyDatabaseTransaction, ProofsDatabase as MintProofsDatabase,
+    ProofsTransaction as MintProofsTransaction, QuotesDatabase as MintQuotesDatabase,
+    QuotesTransaction as MintQuotesTransaction, SignaturesDatabase as MintSignaturesDatabase,
     SignaturesTransaction as MintSignatureTransaction, Transaction as MintTransaction,
 };
 #[cfg(all(feature = "mint", feature = "auth"))]
@@ -23,6 +32,24 @@ 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")]
+pub type WalletKVStore = dyn KVStore<Err = Error> + Send + Sync;
+/// Arc-wrapped wallet KV store for shared ownership
+#[cfg(feature = "wallet")]
+pub type DynWalletKVStore = std::sync::Arc<WalletKVStore>;
+/// Wallet Key-Value Store Database trait object
+#[cfg(feature = "wallet")]
+pub type WalletKVStoreDatabase = dyn KVStoreDatabase<Err = Error> + Send + Sync;
+/// Wallet Key-Value Store Transaction trait object
+#[cfg(feature = "wallet")]
+pub type WalletKVStoreTransaction = dyn KVStoreTransaction<Error> + Send + Sync;
+
 /// Data conversion error
 #[derive(thiserror::Error, Debug)]
 pub enum ConversionError {

+ 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

+ 3 - 3
crates/cdk-integration-tests/src/init_regtest.rs

@@ -7,7 +7,7 @@ use std::time::Duration;
 use anyhow::Result;
 use cdk::types::FeeReserve;
 use cdk_cln::Cln as CdkCln;
-use cdk_common::database::mint::DynMintKVStore;
+use cdk_common::database::DynKVStore;
 use cdk_lnd::Lnd as CdkLnd;
 use cdk_sqlite::mint::memory;
 use ldk_node::lightning::ln::msgs::SocketAddress;
@@ -167,7 +167,7 @@ pub async fn create_cln_backend(cln_client: &ClnClient) -> Result<CdkCln> {
         percent_fee_reserve: 1.0,
     };
 
-    let kv_store: DynMintKVStore = Arc::new(memory::empty().await?);
+    let kv_store: DynKVStore = Arc::new(memory::empty().await?);
     Ok(CdkCln::new(rpc_path, fee_reserve, kv_store).await?)
 }
 
@@ -177,7 +177,7 @@ pub async fn create_lnd_backend(lnd_client: &LndClient) -> Result<CdkLnd> {
         percent_fee_reserve: 1.0,
     };
 
-    let kv_store: DynMintKVStore = Arc::new(memory::empty().await?);
+    let kv_store: DynKVStore = Arc::new(memory::empty().await?);
 
     Ok(CdkLnd::new(
         lnd_client.address.clone(),

+ 3 - 3
crates/cdk-lnd/src/lib.rs

@@ -16,7 +16,7 @@ use async_trait::async_trait;
 use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
 use cdk_common::bitcoin::hashes::Hash;
 use cdk_common::common::FeeReserve;
-use cdk_common::database::mint::DynMintKVStore;
+use cdk_common::database::DynKVStore;
 use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
 use cdk_common::payment::{
     self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
@@ -55,7 +55,7 @@ pub struct Lnd {
     _macaroon_file: PathBuf,
     lnd_client: client::Client,
     fee_reserve: FeeReserve,
-    kv_store: DynMintKVStore,
+    kv_store: DynKVStore,
     wait_invoice_cancel_token: CancellationToken,
     wait_invoice_is_active: Arc<AtomicBool>,
     settings: Bolt11Settings,
@@ -71,7 +71,7 @@ impl Lnd {
         cert_file: PathBuf,
         macaroon_file: PathBuf,
         fee_reserve: FeeReserve,
-        kv_store: DynMintKVStore,
+        kv_store: DynKVStore,
     ) -> Result<Self, Error> {
         // Validate address is not empty
         if address.is_empty() {

+ 7 - 7
crates/cdk-mintd/src/lib.rs

@@ -13,7 +13,7 @@ use std::sync::Arc;
 use anyhow::{anyhow, bail, Result};
 use axum::Router;
 use bip39::Mnemonic;
-use cdk::cdk_database::{self, MintDatabase, MintKVStore, MintKeysDatabase};
+use cdk::cdk_database::{self, KVStore, MintDatabase, MintKeysDatabase};
 use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
 #[cfg(any(
     feature = "cln",
@@ -100,7 +100,7 @@ async fn initial_setup(
 ) -> Result<(
     DynMintDatabase,
     Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
-    Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync>,
+    Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
 )> {
     let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
     Ok((localstore, keystore, kv))
@@ -260,14 +260,14 @@ async fn setup_database(
 ) -> Result<(
     DynMintDatabase,
     Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
-    Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync>,
+    Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync>,
 )> {
     match settings.database.engine {
         #[cfg(feature = "sqlite")]
         DatabaseEngine::Sqlite => {
             let db = setup_sqlite_database(_work_dir, _db_password).await?;
             let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
-            let kv: Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
+            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = db.clone();
             let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
             Ok((localstore, keystore, kv))
         }
@@ -288,7 +288,7 @@ async fn setup_database(
             let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
                 pg_db.clone();
             #[cfg(feature = "postgres")]
-            let kv: Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
+            let kv: Arc<dyn KVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
             #[cfg(feature = "postgres")]
             let keystore: Arc<
                 dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
@@ -339,7 +339,7 @@ async fn configure_mint_builder(
     mint_builder: MintBuilder,
     runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
     work_dir: &Path,
-    kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+    kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
 ) -> Result<MintBuilder> {
     // Configure basic mint information
     let mint_builder = configure_basic_info(settings, mint_builder);
@@ -410,7 +410,7 @@ async fn configure_lightning_backend(
     mut mint_builder: MintBuilder,
     _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
     work_dir: &Path,
-    _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+    _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
 ) -> Result<MintBuilder> {
     let mint_melt_limits = MintMeltLimits {
         mint_min: settings.ln.min_mint,

+ 8 - 8
crates/cdk-mintd/src/setup.rs

@@ -12,7 +12,7 @@ use anyhow::bail;
 use async_trait::async_trait;
 #[cfg(feature = "fakewallet")]
 use bip39::rand::{thread_rng, Rng};
-use cdk::cdk_database::MintKVStore;
+use cdk::cdk_database::KVStore;
 use cdk::cdk_payment::MintPayment;
 use cdk::nuts::CurrencyUnit;
 #[cfg(any(
@@ -36,7 +36,7 @@ pub trait LnBackendSetup {
         unit: CurrencyUnit,
         runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         work_dir: &Path,
-        kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<impl MintPayment>;
 }
 
@@ -49,7 +49,7 @@ impl LnBackendSetup for config::Cln {
         _unit: CurrencyUnit,
         _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         _work_dir: &Path,
-        kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_cln::Cln> {
         // Validate required connection field
         if self.rpc_path.as_os_str().is_empty() {
@@ -90,7 +90,7 @@ impl LnBackendSetup for config::LNbits {
         _unit: CurrencyUnit,
         _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         _work_dir: &Path,
-        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_lnbits::LNbits> {
         // Validate required connection fields
         if self.admin_api_key.is_empty() {
@@ -137,7 +137,7 @@ impl LnBackendSetup for config::Lnd {
         _unit: CurrencyUnit,
         _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         _work_dir: &Path,
-        kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+        kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_lnd::Lnd> {
         // Validate required connection fields
         if self.address.is_empty() {
@@ -183,7 +183,7 @@ impl LnBackendSetup for config::FakeWallet {
         unit: CurrencyUnit,
         _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         _work_dir: &Path,
-        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_fake_wallet::FakeWallet> {
         let fee_reserve = FeeReserve {
             min_fee_reserve: self.reserve_fee_min,
@@ -215,7 +215,7 @@ impl LnBackendSetup for config::GrpcProcessor {
         _unit: CurrencyUnit,
         _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         _work_dir: &Path,
-        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
         let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
             &self.addr,
@@ -237,7 +237,7 @@ impl LnBackendSetup for config::LdkNode {
         _unit: CurrencyUnit,
         _runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
         work_dir: &Path,
-        _kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
+        _kv_store: Option<Arc<dyn KVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
     ) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
         use std::net::SocketAddr;
 

+ 227 - 23
crates/cdk-redb/src/wallet/mod.rs

@@ -8,7 +8,10 @@ use std::sync::Arc;
 
 use async_trait::async_trait;
 use cdk_common::common::ProofInfo;
-use cdk_common::database::WalletDatabase;
+use cdk_common::database::{
+    validate_kvstore_params, DbTransactionFinalizer, KVStore, KVStoreDatabase, KVStoreTransaction,
+    WalletDatabase, WalletDatabaseTransaction,
+};
 use cdk_common::mint_url::MintUrl;
 use cdk_common::util::unix_time;
 use cdk_common::wallet::{self, MintQuote, Transaction, TransactionDirection, TransactionId};
@@ -19,6 +22,79 @@ use cdk_common::{
 use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
 use tracing::instrument;
 
+/// Enum to abstract over read-only and read-write table access for KV store operations
+enum KvTable<'txn> {
+    ReadOnly(redb::ReadOnlyTable<(&'static str, &'static str, &'static str), &'static [u8]>),
+    ReadWrite(redb::Table<'txn, (&'static str, &'static str, &'static str), &'static [u8]>),
+}
+
+impl KvTable<'_> {
+    /// Read a value from the KV store table
+    #[inline(always)]
+    fn kv_read(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Error> {
+        let result = match self {
+            KvTable::ReadOnly(table) => table
+                .get((primary_namespace, secondary_namespace, key))
+                .map_err(Error::from)?
+                .map(|v| v.value().to_vec()),
+            KvTable::ReadWrite(table) => table
+                .get((primary_namespace, secondary_namespace, key))
+                .map_err(Error::from)?
+                .map(|v| v.value().to_vec()),
+        };
+
+        Ok(result)
+    }
+
+    /// List all keys in a namespace from the KV store table
+    #[inline(always)]
+    fn kv_list(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Error> {
+        let mut keys = Vec::new();
+
+        // Use range iterator for efficient lookup by namespace prefix
+        let start = (primary_namespace, secondary_namespace, "");
+
+        match self {
+            KvTable::ReadOnly(table) => {
+                for result in table.range(start..).map_err(Error::from)? {
+                    let (key_tuple, _) = result.map_err(Error::from)?;
+                    let (primary_from_db, secondary_from_db, k) = key_tuple.value();
+                    if primary_from_db != primary_namespace
+                        || secondary_from_db != secondary_namespace
+                    {
+                        break;
+                    }
+                    keys.push(k.to_string());
+                }
+            }
+            KvTable::ReadWrite(table) => {
+                for result in table.range(start..).map_err(Error::from)? {
+                    let (key_tuple, _) = result.map_err(Error::from)?;
+                    let (primary_from_db, secondary_from_db, k) = key_tuple.value();
+                    if primary_from_db != primary_namespace
+                        || secondary_from_db != secondary_namespace
+                    {
+                        break;
+                    }
+                    keys.push(k.to_string());
+                }
+            }
+        }
+
+        // Keys are already sorted by the B-tree structure
+        Ok(keys)
+    }
+}
+
 use super::error::Error;
 use crate::migrations::migrate_00_to_01;
 use crate::wallet::migrations::{migrate_01_to_02, migrate_02_to_03, migrate_03_to_04};
@@ -45,6 +121,8 @@ const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_
 const TRANSACTIONS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("transactions");
 
 const KEYSET_U32_MAPPING: TableDefinition<u32, &str> = TableDefinition::new("keyset_u32_mapping");
+// <(primary_namespace, secondary_namespace, key), value>
+const KV_STORE_TABLE: TableDefinition<(&str, &str, &str), &[u8]> = TableDefinition::new("kv_store");
 
 const DATABASE_VERSION: u32 = 4;
 
@@ -180,6 +258,7 @@ impl WalletRedbDatabase {
                         let _ = write_txn.open_table(KEYSET_COUNTER)?;
                         let _ = write_txn.open_table(TRANSACTIONS_TABLE)?;
                         let _ = write_txn.open_table(KEYSET_U32_MAPPING)?;
+                        let _ = write_txn.open_table(KV_STORE_TABLE)?;
                         table.insert("db_version", DATABASE_VERSION.to_string().as_str())?;
                     }
 
@@ -208,11 +287,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)?;
 
@@ -227,7 +304,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
@@ -248,7 +325,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)
@@ -283,7 +360,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)?;
 
@@ -302,7 +382,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)
@@ -316,7 +396,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)
@@ -348,7 +428,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)
@@ -362,7 +445,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)
@@ -377,7 +460,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)?;
 
@@ -398,7 +481,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)?;
@@ -425,7 +508,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());
         }
@@ -462,7 +548,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)
@@ -481,7 +567,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
@@ -510,17 +596,60 @@ impl WalletDatabase for WalletRedbDatabase {
 
     async fn begin_db_transaction(
         &self,
-    ) -> Result<
-        Box<dyn cdk_common::database::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)))
     }
 }
 
 #[async_trait]
-impl cdk_common::database::WalletDatabaseTransaction<database::Error> for RedbWalletTransaction {
+impl KVStoreDatabase for WalletRedbDatabase {
+    type Err = database::Error;
+
+    #[instrument(skip_all)]
+    async fn kv_read(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Self::Err> {
+        // Validate parameters according to KV store requirements
+        validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
+        let table = KvTable::ReadOnly(read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
+
+        Ok(table.kv_read(primary_namespace, secondary_namespace, key)?)
+    }
+
+    #[instrument(skip_all)]
+    async fn kv_list(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Self::Err> {
+        validate_kvstore_params(primary_namespace, secondary_namespace, None)?;
+
+        let read_txn = self.db.begin_read().map_err(Error::from)?;
+        let table = KvTable::ReadOnly(read_txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
+
+        Ok(table.kv_list(primary_namespace, secondary_namespace)?)
+    }
+}
+
+#[async_trait]
+impl KVStore for WalletRedbDatabase {
+    async fn begin_transaction(
+        &self,
+    ) -> Result<Box<dyn KVStoreTransaction<Self::Err> + Send + Sync>, database::Error> {
+        let write_txn = self.db.begin_write().map_err(Error::from)?;
+        Ok(Box::new(RedbWalletTransaction::new(write_txn)))
+    }
+}
+
+#[async_trait]
+impl WalletDatabaseTransaction<database::Error> for RedbWalletTransaction {
     #[instrument(skip(self), fields(keyset_id = %keyset_id))]
     async fn get_keyset_by_id(
         &mut self,
@@ -1012,7 +1141,82 @@ impl cdk_common::database::WalletDatabaseTransaction<database::Error> for RedbWa
 }
 
 #[async_trait]
-impl cdk_common::database::DbTransactionFinalizer for RedbWalletTransaction {
+impl KVStoreTransaction<database::Error> for RedbWalletTransaction {
+    #[instrument(skip_all)]
+    async fn kv_read(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, database::Error> {
+        // Validate parameters according to KV store requirements
+        validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+
+        let txn = self.txn()?;
+        let table = KvTable::ReadWrite(txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
+
+        Ok(table.kv_read(primary_namespace, secondary_namespace, key)?)
+    }
+
+    #[instrument(skip_all)]
+    async fn kv_write(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+        value: &[u8],
+    ) -> Result<(), database::Error> {
+        // Validate parameters according to KV store requirements
+        validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+
+        let txn = self.txn()?;
+        let mut table = txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
+
+        table
+            .insert((primary_namespace, secondary_namespace, key), value)
+            .map_err(Error::from)?;
+
+        Ok(())
+    }
+
+    #[instrument(skip_all)]
+    async fn kv_remove(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<(), database::Error> {
+        // Validate parameters according to KV store requirements
+        validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+
+        let txn = self.txn()?;
+        let mut table = txn.open_table(KV_STORE_TABLE).map_err(Error::from)?;
+
+        table
+            .remove((primary_namespace, secondary_namespace, key))
+            .map_err(Error::from)?;
+
+        Ok(())
+    }
+
+    #[instrument(skip_all)]
+    async fn kv_list(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, database::Error> {
+        // Validate namespace parameters according to KV store requirements
+        validate_kvstore_params(primary_namespace, secondary_namespace, None)?;
+
+        let txn = self.txn()?;
+        let table = KvTable::ReadWrite(txn.open_table(KV_STORE_TABLE).map_err(Error::from)?);
+
+        Ok(table.kv_list(primary_namespace, secondary_namespace)?)
+    }
+}
+
+#[async_trait]
+impl DbTransactionFinalizer for RedbWalletTransaction {
     type Err = database::Error;
 
     async fn commit(mut self: Box<Self>) -> Result<(), database::Error> {

+ 208 - 0
crates/cdk-sql-common/src/keyvalue.rs

@@ -0,0 +1,208 @@
+//! Generic KV Store implementations for SQL databases
+//!
+//! This module provides generic implementations of KVStore traits that can be
+//! used by both mint and wallet database implementations.
+
+use std::sync::Arc;
+
+use cdk_common::database::{validate_kvstore_params, Error};
+use cdk_common::util::unix_time;
+
+use crate::column_as_string;
+use crate::database::ConnectionWithTransaction;
+use crate::pool::{DatabasePool, Pool, PooledResource};
+use crate::stmt::{query, Column};
+
+/// Generic implementation of KVStoreTransaction for SQL databases
+pub(crate) async fn kv_read_in_transaction<RM>(
+    conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+    key: &str,
+) -> Result<Option<Vec<u8>>, Error>
+where
+    RM: DatabasePool,
+{
+    // Validate parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+    Ok(query(
+        r#"
+        SELECT value
+        FROM kv_store
+        WHERE primary_namespace = :primary_namespace
+        AND secondary_namespace = :secondary_namespace
+        AND key = :key
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .bind("key", key.to_owned())
+    .pluck(conn)
+    .await?
+    .and_then(|col| match col {
+        Column::Blob(data) => Some(data),
+        _ => None,
+    }))
+}
+
+/// Generic implementation of kv_write for transactions
+pub(crate) async fn kv_write_in_transaction<RM>(
+    conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+    key: &str,
+    value: &[u8],
+) -> Result<(), Error>
+where
+    RM: DatabasePool,
+{
+    // Validate parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+
+    let current_time = unix_time();
+
+    query(
+        r#"
+        INSERT INTO kv_store
+        (primary_namespace, secondary_namespace, key, value, created_time, updated_time)
+        VALUES (:primary_namespace, :secondary_namespace, :key, :value, :created_time, :updated_time)
+        ON CONFLICT(primary_namespace, secondary_namespace, key)
+        DO UPDATE SET
+            value = excluded.value,
+            updated_time = excluded.updated_time
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .bind("key", key.to_owned())
+    .bind("value", value.to_vec())
+    .bind("created_time", current_time as i64)
+    .bind("updated_time", current_time as i64)
+    .execute(conn)
+    .await?;
+
+    Ok(())
+}
+
+/// Generic implementation of kv_remove for transactions
+pub(crate) async fn kv_remove_in_transaction<RM>(
+    conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+    key: &str,
+) -> Result<(), Error>
+where
+    RM: DatabasePool,
+{
+    // Validate parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+    query(
+        r#"
+        DELETE FROM kv_store
+        WHERE primary_namespace = :primary_namespace
+        AND secondary_namespace = :secondary_namespace
+        AND key = :key
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .bind("key", key.to_owned())
+    .execute(conn)
+    .await?;
+
+    Ok(())
+}
+
+/// Generic implementation of kv_list for transactions
+pub(crate) async fn kv_list_in_transaction<RM>(
+    conn: &ConnectionWithTransaction<RM::Connection, PooledResource<RM>>,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+) -> Result<Vec<String>, Error>
+where
+    RM: DatabasePool,
+{
+    // Validate namespace parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, None)?;
+    query(
+        r#"
+        SELECT key
+        FROM kv_store
+        WHERE primary_namespace = :primary_namespace
+        AND secondary_namespace = :secondary_namespace
+        ORDER BY key
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .fetch_all(conn)
+    .await?
+    .into_iter()
+    .map(|row| Ok(column_as_string!(&row[0])))
+    .collect::<Result<Vec<_>, Error>>()
+}
+
+/// Generic implementation of kv_read for database (non-transactional)
+pub(crate) async fn kv_read<RM>(
+    pool: &Arc<Pool<RM>>,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+    key: &str,
+) -> Result<Option<Vec<u8>>, Error>
+where
+    RM: DatabasePool + 'static,
+{
+    // Validate parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, Some(key))?;
+
+    let conn = pool.get().map_err(|e| Error::Database(Box::new(e)))?;
+    Ok(query(
+        r#"
+        SELECT value
+        FROM kv_store
+        WHERE primary_namespace = :primary_namespace
+        AND secondary_namespace = :secondary_namespace
+        AND key = :key
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .bind("key", key.to_owned())
+    .pluck(&*conn)
+    .await?
+    .and_then(|col| match col {
+        Column::Blob(data) => Some(data),
+        _ => None,
+    }))
+}
+
+/// Generic implementation of kv_list for database (non-transactional)
+pub(crate) async fn kv_list<RM>(
+    pool: &Arc<Pool<RM>>,
+    primary_namespace: &str,
+    secondary_namespace: &str,
+) -> Result<Vec<String>, Error>
+where
+    RM: DatabasePool + 'static,
+{
+    // Validate namespace parameters according to KV store requirements
+    validate_kvstore_params(primary_namespace, secondary_namespace, None)?;
+
+    let conn = pool.get().map_err(|e| Error::Database(Box::new(e)))?;
+    query(
+        r#"
+        SELECT key
+        FROM kv_store
+        WHERE primary_namespace = :primary_namespace
+        AND secondary_namespace = :secondary_namespace
+        ORDER BY key
+        "#,
+    )?
+    .bind("primary_namespace", primary_namespace.to_owned())
+    .bind("secondary_namespace", secondary_namespace.to_owned())
+    .fetch_all(&*conn)
+    .await?
+    .into_iter()
+    .map(|row| Ok(column_as_string!(&row[0])))
+    .collect::<Result<Vec<_>, Error>>()
+}

+ 1 - 0
crates/cdk-sql-common/src/lib.rs

@@ -2,6 +2,7 @@
 
 mod common;
 pub mod database;
+mod keyvalue;
 mod macros;
 pub mod pool;
 pub mod stmt;

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

@@ -16,8 +16,7 @@ use std::sync::Arc;
 use async_trait::async_trait;
 use bitcoin::bip32::DerivationPath;
 use cdk_common::database::mint::{
-    validate_kvstore_params, CompletedOperationsDatabase, CompletedOperationsTransaction,
-    SagaDatabase, SagaTransaction,
+    CompletedOperationsDatabase, CompletedOperationsTransaction, SagaDatabase, SagaTransaction,
 };
 use cdk_common::database::{
     self, ConversionError, DbTransactionFinalizer, Error, MintDatabase, MintKeyDatabaseTransaction,
@@ -1997,7 +1996,7 @@ where
 }
 
 #[async_trait]
-impl<RM> database::MintKVStoreTransaction<'_, Error> for SQLTransaction<RM>
+impl<RM> database::KVStoreTransaction<Error> for SQLTransaction<RM>
 where
     RM: DatabasePool + 'static,
 {
@@ -2007,26 +2006,13 @@ where
         secondary_namespace: &str,
         key: &str,
     ) -> Result<Option<Vec<u8>>, Error> {
-        // Validate parameters according to KV store requirements
-        validate_kvstore_params(primary_namespace, secondary_namespace, key)?;
-        Ok(query(
-            r#"
-            SELECT value
-            FROM kv_store
-            WHERE primary_namespace = :primary_namespace
-            AND secondary_namespace = :secondary_namespace
-            AND key = :key
-            "#,
-        )?
-        .bind("primary_namespace", primary_namespace.to_owned())
-        .bind("secondary_namespace", secondary_namespace.to_owned())
-        .bind("key", key.to_owned())
-        .pluck(&self.inner)
-        .await?
-        .and_then(|col| match col {
-            Column::Blob(data) => Some(data),
-            _ => None,
-        }))
+        crate::keyvalue::kv_read_in_transaction(
+            &self.inner,
+            primary_namespace,
+            secondary_namespace,
+            key,
+        )
+        .await
     }
 
     async fn kv_write(
@@ -2036,32 +2022,14 @@ where
         key: &str,
         value: &[u8],
     ) -> Result<(), Error> {
-        // Validate parameters according to KV store requirements
-        validate_kvstore_params(primary_namespace, secondary_namespace, key)?;
-
-        let current_time = unix_time();
-
-        query(
-            r#"
-            INSERT INTO kv_store
-            (primary_namespace, secondary_namespace, key, value, created_time, updated_time)
-            VALUES (:primary_namespace, :secondary_namespace, :key, :value, :created_time, :updated_time)
-            ON CONFLICT(primary_namespace, secondary_namespace, key)
-            DO UPDATE SET
-                value = excluded.value,
-                updated_time = excluded.updated_time
-            "#,
-        )?
-        .bind("primary_namespace", primary_namespace.to_owned())
-        .bind("secondary_namespace", secondary_namespace.to_owned())
-        .bind("key", key.to_owned())
-        .bind("value", value.to_vec())
-        .bind("created_time", current_time as i64)
-        .bind("updated_time", current_time as i64)
-        .execute(&self.inner)
-        .await?;
-
-        Ok(())
+        crate::keyvalue::kv_write_in_transaction(
+            &self.inner,
+            primary_namespace,
+            secondary_namespace,
+            key,
+            value,
+        )
+        .await
     }
 
     async fn kv_remove(
@@ -2070,23 +2038,13 @@ where
         secondary_namespace: &str,
         key: &str,
     ) -> Result<(), Error> {
-        // Validate parameters according to KV store requirements
-        validate_kvstore_params(primary_namespace, secondary_namespace, key)?;
-        query(
-            r#"
-            DELETE FROM kv_store
-            WHERE primary_namespace = :primary_namespace
-            AND secondary_namespace = :secondary_namespace
-            AND key = :key
-            "#,
-        )?
-        .bind("primary_namespace", primary_namespace.to_owned())
-        .bind("secondary_namespace", secondary_namespace.to_owned())
-        .bind("key", key.to_owned())
-        .execute(&self.inner)
-        .await?;
-
-        Ok(())
+        crate::keyvalue::kv_remove_in_transaction(
+            &self.inner,
+            primary_namespace,
+            secondary_namespace,
+            key,
+        )
+        .await
     }
 
     async fn kv_list(
@@ -2094,37 +2052,13 @@ where
         primary_namespace: &str,
         secondary_namespace: &str,
     ) -> Result<Vec<String>, Error> {
-        // Validate namespace parameters according to KV store requirements
-        cdk_common::database::mint::validate_kvstore_string(primary_namespace)?;
-        cdk_common::database::mint::validate_kvstore_string(secondary_namespace)?;
-
-        // Check empty namespace rules
-        if primary_namespace.is_empty() && !secondary_namespace.is_empty() {
-            return Err(Error::KVStoreInvalidKey(
-                "If primary_namespace is empty, secondary_namespace must also be empty".to_string(),
-            ));
-        }
-        Ok(query(
-            r#"
-            SELECT key
-            FROM kv_store
-            WHERE primary_namespace = :primary_namespace
-            AND secondary_namespace = :secondary_namespace
-            ORDER BY key
-            "#,
-        )?
-        .bind("primary_namespace", primary_namespace.to_owned())
-        .bind("secondary_namespace", secondary_namespace.to_owned())
-        .fetch_all(&self.inner)
-        .await?
-        .into_iter()
-        .map(|row| Ok(column_as_string!(&row[0])))
-        .collect::<Result<Vec<_>, Error>>()?)
+        crate::keyvalue::kv_list_in_transaction(&self.inner, primary_namespace, secondary_namespace)
+            .await
     }
 }
 
 #[async_trait]
-impl<RM> database::MintKVStoreDatabase for SQLMintDatabase<RM>
+impl<RM> database::KVStoreDatabase for SQLMintDatabase<RM>
 where
     RM: DatabasePool + 'static,
 {
@@ -2136,28 +2070,7 @@ where
         secondary_namespace: &str,
         key: &str,
     ) -> Result<Option<Vec<u8>>, Error> {
-        // Validate parameters according to KV store requirements
-        validate_kvstore_params(primary_namespace, secondary_namespace, key)?;
-
-        let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
-        Ok(query(
-            r#"
-            SELECT value
-            FROM kv_store
-            WHERE primary_namespace = :primary_namespace
-            AND secondary_namespace = :secondary_namespace
-            AND key = :key
-            "#,
-        )?
-        .bind("primary_namespace", primary_namespace.to_owned())
-        .bind("secondary_namespace", secondary_namespace.to_owned())
-        .bind("key", key.to_owned())
-        .pluck(&*conn)
-        .await?
-        .and_then(|col| match col {
-            Column::Blob(data) => Some(data),
-            _ => None,
-        }))
+        crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await
     }
 
     async fn kv_list(
@@ -2165,46 +2078,18 @@ where
         primary_namespace: &str,
         secondary_namespace: &str,
     ) -> Result<Vec<String>, Error> {
-        // Validate namespace parameters according to KV store requirements
-        cdk_common::database::mint::validate_kvstore_string(primary_namespace)?;
-        cdk_common::database::mint::validate_kvstore_string(secondary_namespace)?;
-
-        // Check empty namespace rules
-        if primary_namespace.is_empty() && !secondary_namespace.is_empty() {
-            return Err(Error::KVStoreInvalidKey(
-                "If primary_namespace is empty, secondary_namespace must also be empty".to_string(),
-            ));
-        }
-
-        let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
-        Ok(query(
-            r#"
-            SELECT key
-            FROM kv_store
-            WHERE primary_namespace = :primary_namespace
-            AND secondary_namespace = :secondary_namespace
-            ORDER BY key
-            "#,
-        )?
-        .bind("primary_namespace", primary_namespace.to_owned())
-        .bind("secondary_namespace", secondary_namespace.to_owned())
-        .fetch_all(&*conn)
-        .await?
-        .into_iter()
-        .map(|row| Ok(column_as_string!(&row[0])))
-        .collect::<Result<Vec<_>, Error>>()?)
+        crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await
     }
 }
 
 #[async_trait]
-impl<RM> database::MintKVStore for SQLMintDatabase<RM>
+impl<RM> database::KVStore for SQLMintDatabase<RM>
 where
     RM: DatabasePool + 'static,
 {
-    async fn begin_transaction<'a>(
-        &'a self,
-    ) -> Result<Box<dyn database::MintKVStoreTransaction<'a, Self::Err> + Send + Sync + 'a>, Error>
-    {
+    async fn begin_transaction(
+        &self,
+    ) -> Result<Box<dyn database::KVStoreTransaction<Self::Err> + Send + Sync>, Error> {
         Ok(Box::new(SQLTransaction {
             inner: ConnectionWithTransaction::new(
                 self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,

+ 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);

+ 132 - 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(
@@ -1536,3 +1544,109 @@ fn sql_row_to_transaction(row: Vec<Column>) -> Result<Transaction, Error> {
         payment_proof: column_as_nullable_string!(payment_proof),
     })
 }
+
+// KVStore implementations for wallet
+
+#[async_trait]
+impl<RM> database::KVStoreTransaction<Error> for SQLWalletTransaction<RM>
+where
+    RM: DatabasePool + 'static,
+{
+    async fn kv_read(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Error> {
+        crate::keyvalue::kv_read_in_transaction(
+            &self.inner,
+            primary_namespace,
+            secondary_namespace,
+            key,
+        )
+        .await
+    }
+
+    async fn kv_write(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+        value: &[u8],
+    ) -> Result<(), Error> {
+        crate::keyvalue::kv_write_in_transaction(
+            &self.inner,
+            primary_namespace,
+            secondary_namespace,
+            key,
+            value,
+        )
+        .await
+    }
+
+    async fn kv_remove(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<(), Error> {
+        crate::keyvalue::kv_remove_in_transaction(
+            &self.inner,
+            primary_namespace,
+            secondary_namespace,
+            key,
+        )
+        .await
+    }
+
+    async fn kv_list(
+        &mut self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Error> {
+        crate::keyvalue::kv_list_in_transaction(&self.inner, primary_namespace, secondary_namespace)
+            .await
+    }
+}
+
+#[async_trait]
+impl<RM> database::KVStoreDatabase for SQLWalletDatabase<RM>
+where
+    RM: DatabasePool + 'static,
+{
+    type Err = Error;
+
+    async fn kv_read(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+        key: &str,
+    ) -> Result<Option<Vec<u8>>, Error> {
+        crate::keyvalue::kv_read(&self.pool, primary_namespace, secondary_namespace, key).await
+    }
+
+    async fn kv_list(
+        &self,
+        primary_namespace: &str,
+        secondary_namespace: &str,
+    ) -> Result<Vec<String>, Error> {
+        crate::keyvalue::kv_list(&self.pool, primary_namespace, secondary_namespace).await
+    }
+}
+
+#[async_trait]
+impl<RM> database::KVStore for SQLWalletDatabase<RM>
+where
+    RM: DatabasePool + 'static,
+{
+    async fn begin_transaction(
+        &self,
+    ) -> Result<Box<dyn database::KVStoreTransaction<Self::Err> + Send + Sync>, Error> {
+        Ok(Box::new(SQLWalletTransaction {
+            inner: ConnectionWithTransaction::new(
+                self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,
+            )
+            .await?,
+        }))
+    }
+}

+ 1 - 1
crates/cdk/src/lib.rs

@@ -14,7 +14,7 @@ pub mod cdk_database {
     pub use cdk_common::database::WalletDatabase;
     #[cfg(feature = "mint")]
     pub use cdk_common::database::{
-        MintDatabase, MintKVStore, MintKVStoreDatabase, MintKVStoreTransaction, MintKeysDatabase,
+        KVStore, KVStoreDatabase, KVStoreTransaction, MintDatabase, MintKeysDatabase,
         MintProofsDatabase, MintQuotesDatabase, MintSignaturesDatabase, MintTransaction,
     };
 }

+ 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)