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

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
Cesar Rodas 1 месяц назад
Родитель
Сommit
9046d63e35

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

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

@@ -0,0 +1,133 @@
+//! 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: &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(())
+}
+
+/// 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>;
+}
+
+/// 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<'a>(
+        &'a self,
+    ) -> Result<Box<dyn KVStoreTransaction<'a, Self::Err> + Send + Sync + 'a>, Error>;
+}

+ 5 - 127
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,42 +387,6 @@ 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>
@@ -487,40 +399,6 @@ pub trait Transaction<'a, 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>:

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

+ 29 - 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,20 @@ pub use wallet::{
     DynWalletDatabaseTransaction,
 };
 
+// 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<'a> = dyn KVStoreTransaction<'a, Error> + Send + Sync + 'a;
+
 /// Data conversion error
 #[derive(thiserror::Error, Debug)]
 pub enum ConversionError {

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

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

@@ -0,0 +1,224 @@
+//! 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, validate_kvstore_string, 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, 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, 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, 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_string(primary_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(),
+        ));
+    }
+    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, 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_string(primary_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(),
+        ));
+    }
+
+    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;

+ 31 - 145
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,45 +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>
+    ) -> Result<Box<dyn database::KVStoreTransaction<'a, Self::Err> + Send + Sync + 'a>, Error>
     {
         Ok(Box::new(SQLTransaction {
             inner: ConnectionWithTransaction::new(

+ 107 - 0
crates/cdk-sql-common/src/wallet/mod.rs

@@ -1536,3 +1536,110 @@ 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<'a>(
+        &'a self,
+    ) -> Result<Box<dyn database::KVStoreTransaction<'a, Self::Err> + Send + Sync + 'a>, 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,
     };
 }