mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-18 13:14:59 +01:00
feat(cdk): add generic key-value store functionality for mint databases (#1022)
* feat(cdk): add generic key-value store functionality for mint databases Implements a comprehensive KV store system with transaction support, namespace-based organization, and validation for mint databases. - Add KVStoreDatabase and KVStoreTransaction traits for generic storage - Include namespace and key validation with ASCII character restrictions - Add database migrations for kv_store table in SQLite and PostgreSQL - Implement comprehensive test suite for KV store functionality - Integrate KV store traits into existing Database and Transaction bounds
This commit is contained in:
@@ -10,6 +10,12 @@
|
||||
- cdk-common: New `Event` enum for payment event handling with `PaymentReceived` variant ([thesimplekid]).
|
||||
- cdk-common: Added `payment_method` field to `MeltQuote` struct for tracking payment method type ([thesimplekid]).
|
||||
- cdk-sql-common: Database migration to add `payment_method` column to melt_quote table for SQLite and PostgreSQL ([thesimplekid]).
|
||||
- cdk-common: New `MintKVStoreDatabase` trait providing generic key-value storage functionality for mint databases ([thesimplekid]).
|
||||
- cdk-common: Added `KVStoreTransaction` trait for transactional key-value operations with read, write, remove, and list capabilities ([thesimplekid]).
|
||||
- cdk-common: Added validation functions for KV store namespace and key parameters with ASCII character and length restrictions ([thesimplekid]).
|
||||
- cdk-common: Added comprehensive test module for KV store functionality with transaction and isolation testing ([thesimplekid]).
|
||||
- cdk-sql-common: Database migration to add `kv_store` table for generic key-value storage in SQLite and PostgreSQL ([thesimplekid]).
|
||||
- cdk-sql-common: Implementation of `MintKVStoreDatabase` trait for SQL-based databases with namespace support ([thesimplekid]).
|
||||
|
||||
### Changed
|
||||
- cdk-common: Refactored `MintPayment` trait method `wait_any_incoming_payment` to `wait_payment_event` with event-driven architecture ([thesimplekid]).
|
||||
|
||||
@@ -142,19 +142,17 @@ mod tests {
|
||||
#[test]
|
||||
pub fn test_public_key_from_hex() {
|
||||
// Compressed
|
||||
assert!(
|
||||
(PublicKey::from_hex(
|
||||
"02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
.is_ok())
|
||||
);
|
||||
assert!(PublicKey::from_hex(
|
||||
"02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_invalid_public_key_from_hex() {
|
||||
// Uncompressed (is valid but is cashu must be compressed?)
|
||||
assert!((PublicKey::from_hex("04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481")
|
||||
.is_err()))
|
||||
assert!(PublicKey::from_hex("04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481")
|
||||
.is_err())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,66 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 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!(
|
||||
"{} exceeds maximum length of key characters",
|
||||
KVSTORE_NAMESPACE_KEY_MAX_LEN
|
||||
)));
|
||||
}
|
||||
|
||||
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 '{}' conflicts with namespace names",
|
||||
key
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cashu::quote_id::QuoteId;
|
||||
use cashu::{Amount, MintInfo};
|
||||
@@ -20,6 +80,9 @@ mod auth;
|
||||
#[cfg(feature = "test")]
|
||||
pub mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_kvstore;
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
pub use auth::{MintAuthDatabase, MintAuthTransaction};
|
||||
|
||||
@@ -263,6 +326,42 @@ pub trait DbTransactionFinalizer {
|
||||
async fn rollback(self: Box<Self>) -> Result<(), 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
|
||||
#[async_trait]
|
||||
pub trait Transaction<'a, Error>:
|
||||
@@ -270,6 +369,7 @@ pub trait Transaction<'a, Error>:
|
||||
+ QuotesTransaction<'a, Err = Error>
|
||||
+ SignaturesTransaction<'a, Err = Error>
|
||||
+ ProofsTransaction<'a, Err = Error>
|
||||
+ KVStoreTransaction<'a, Error>
|
||||
{
|
||||
/// Set [`QuoteTTL`]
|
||||
async fn set_quote_ttl(&mut self, quote_ttl: QuoteTTL) -> Result<(), Error>;
|
||||
@@ -278,10 +378,44 @@ pub trait Transaction<'a, Error>:
|
||||
async fn set_mint_info(&mut self, mint_info: MintInfo) -> Result<(), 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 {
|
||||
/// Beings a KV transaction
|
||||
async fn begin_transaction<'a>(
|
||||
&'a self,
|
||||
) -> Result<Box<dyn KVStoreTransaction<'a, Self::Err> + Send + Sync + 'a>, Error>;
|
||||
}
|
||||
|
||||
/// Mint Database trait
|
||||
#[async_trait]
|
||||
pub trait Database<Error>:
|
||||
QuotesDatabase<Err = Error> + ProofsDatabase<Err = Error> + SignaturesDatabase<Err = Error>
|
||||
KVStoreDatabase<Err = Error>
|
||||
+ QuotesDatabase<Err = Error>
|
||||
+ ProofsDatabase<Err = Error>
|
||||
+ SignaturesDatabase<Err = Error>
|
||||
{
|
||||
/// Beings a transaction
|
||||
async fn begin_transaction<'a>(
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
//! implementation
|
||||
use std::str::FromStr;
|
||||
|
||||
// For derivation path parsing
|
||||
use bitcoin::bip32::DerivationPath;
|
||||
use cashu::secret::Secret;
|
||||
use cashu::{Amount, CurrencyUnit, SecretKey};
|
||||
|
||||
use super::*;
|
||||
use crate::database;
|
||||
use crate::database::MintKVStoreDatabase;
|
||||
use crate::mint::MintKeySetInfo;
|
||||
|
||||
#[inline]
|
||||
async fn setup_keyset<DB>(db: &DB) -> Id
|
||||
where
|
||||
DB: KeysDatabase<Err = database::Error>,
|
||||
DB: KeysDatabase<Err = crate::database::Error>,
|
||||
{
|
||||
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
|
||||
let keyset_info = MintKeySetInfo {
|
||||
@@ -23,7 +25,7 @@ where
|
||||
active: true,
|
||||
valid_from: 0,
|
||||
final_expiry: None,
|
||||
derivation_path: bitcoin::bip32::DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||
derivation_path: DerivationPath::from_str("m/0'/0'/0'").unwrap(),
|
||||
derivation_path_index: Some(0),
|
||||
max_order: 32,
|
||||
input_fee_ppk: 0,
|
||||
@@ -37,7 +39,7 @@ where
|
||||
/// State transition test
|
||||
pub async fn state_transition<DB>(db: DB)
|
||||
where
|
||||
DB: Database<database::Error> + KeysDatabase<Err = database::Error>,
|
||||
DB: Database<crate::database::Error> + KeysDatabase<Err = crate::database::Error>,
|
||||
{
|
||||
let keyset_id = setup_keyset(&db).await;
|
||||
|
||||
@@ -83,7 +85,7 @@ where
|
||||
/// other tests
|
||||
pub async fn add_and_find_proofs<DB>(db: DB)
|
||||
where
|
||||
DB: Database<database::Error> + KeysDatabase<Err = database::Error>,
|
||||
DB: Database<crate::database::Error> + KeysDatabase<Err = crate::database::Error>,
|
||||
{
|
||||
let keyset_id = setup_keyset(&db).await;
|
||||
|
||||
@@ -124,12 +126,99 @@ where
|
||||
assert_eq!(proofs_from_db.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
{
|
||||
// Test basic read/write operations in transaction
|
||||
{
|
||||
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||
|
||||
// Write some test data
|
||||
tx.kv_write("test_namespace", "sub_namespace", "key1", b"value1")
|
||||
.await
|
||||
.unwrap();
|
||||
tx.kv_write("test_namespace", "sub_namespace", "key2", b"value2")
|
||||
.await
|
||||
.unwrap();
|
||||
tx.kv_write("test_namespace", "other_sub", "key3", b"value3")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Read back the data in the transaction
|
||||
let value1 = tx
|
||||
.kv_read("test_namespace", "sub_namespace", "key1")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(value1, Some(b"value1".to_vec()));
|
||||
|
||||
// List keys in namespace
|
||||
let keys = tx.kv_list("test_namespace", "sub_namespace").await.unwrap();
|
||||
assert_eq!(keys, vec!["key1", "key2"]);
|
||||
|
||||
// Commit transaction
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
// Test read operations after commit
|
||||
{
|
||||
let value1 = db
|
||||
.kv_read("test_namespace", "sub_namespace", "key1")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(value1, Some(b"value1".to_vec()));
|
||||
|
||||
let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
|
||||
assert_eq!(keys, vec!["key1", "key2"]);
|
||||
|
||||
let other_keys = db.kv_list("test_namespace", "other_sub").await.unwrap();
|
||||
assert_eq!(other_keys, vec!["key3"]);
|
||||
}
|
||||
|
||||
// Test update and remove operations
|
||||
{
|
||||
let mut tx = Database::begin_transaction(&db).await.unwrap();
|
||||
|
||||
// Update existing key
|
||||
tx.kv_write("test_namespace", "sub_namespace", "key1", b"updated_value1")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Remove a key
|
||||
tx.kv_remove("test_namespace", "sub_namespace", "key2")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
}
|
||||
|
||||
// Verify updates
|
||||
{
|
||||
let value1 = db
|
||||
.kv_read("test_namespace", "sub_namespace", "key1")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(value1, Some(b"updated_value1".to_vec()));
|
||||
|
||||
let value2 = db
|
||||
.kv_read("test_namespace", "sub_namespace", "key2")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(value2, None);
|
||||
|
||||
let keys = db.kv_list("test_namespace", "sub_namespace").await.unwrap();
|
||||
assert_eq!(keys, vec!["key1"]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Unit test that is expected to be passed for a correct database implementation
|
||||
#[macro_export]
|
||||
macro_rules! mint_db_test {
|
||||
($make_db_fn:ident) => {
|
||||
mint_db_test!(state_transition, $make_db_fn);
|
||||
mint_db_test!(add_and_find_proofs, $make_db_fn);
|
||||
mint_db_test!(kvstore_functionality, $make_db_fn);
|
||||
};
|
||||
($name:ident, $make_db_fn:ident) => {
|
||||
#[tokio::test]
|
||||
|
||||
207
crates/cdk-common/src/database/mint/test_kvstore.rs
Normal file
207
crates/cdk-common/src/database/mint/test_kvstore.rs
Normal file
@@ -0,0 +1,207 @@
|
||||
//! Tests for KV store validation requirements
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::database::mint::{
|
||||
validate_kvstore_params, validate_kvstore_string, KVSTORE_NAMESPACE_KEY_ALPHABET,
|
||||
KVSTORE_NAMESPACE_KEY_MAX_LEN,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_validate_kvstore_string_valid_inputs() {
|
||||
// Test valid strings
|
||||
assert!(validate_kvstore_string("").is_ok());
|
||||
assert!(validate_kvstore_string("abc").is_ok());
|
||||
assert!(validate_kvstore_string("ABC").is_ok());
|
||||
assert!(validate_kvstore_string("123").is_ok());
|
||||
assert!(validate_kvstore_string("test_key").is_ok());
|
||||
assert!(validate_kvstore_string("test-key").is_ok());
|
||||
assert!(validate_kvstore_string("test_KEY-123").is_ok());
|
||||
|
||||
// Test max length string
|
||||
let max_length_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN);
|
||||
assert!(validate_kvstore_string(&max_length_str).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_kvstore_string_invalid_length() {
|
||||
// Test string too long
|
||||
let too_long_str = "a".repeat(KVSTORE_NAMESPACE_KEY_MAX_LEN + 1);
|
||||
let result = validate_kvstore_string(&too_long_str);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("exceeds maximum length"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_kvstore_string_invalid_characters() {
|
||||
// Test invalid characters
|
||||
let invalid_chars = vec![
|
||||
"test@key", // @
|
||||
"test key", // space
|
||||
"test.key", // .
|
||||
"test/key", // /
|
||||
"test\\key", // \
|
||||
"test+key", // +
|
||||
"test=key", // =
|
||||
"test!key", // !
|
||||
"test#key", // #
|
||||
"test$key", // $
|
||||
"test%key", // %
|
||||
"test&key", // &
|
||||
"test*key", // *
|
||||
"test(key", // (
|
||||
"test)key", // )
|
||||
"test[key", // [
|
||||
"test]key", // ]
|
||||
"test{key", // {
|
||||
"test}key", // }
|
||||
"test|key", // |
|
||||
"test;key", // ;
|
||||
"test:key", // :
|
||||
"test'key", // '
|
||||
"test\"key", // "
|
||||
"test<key", // <
|
||||
"test>key", // >
|
||||
"test,key", // ,
|
||||
"test?key", // ?
|
||||
"test~key", // ~
|
||||
"test`key", // `
|
||||
];
|
||||
|
||||
for invalid_str in invalid_chars {
|
||||
let result = validate_kvstore_string(invalid_str);
|
||||
assert!(result.is_err(), "Expected '{}' to be invalid", invalid_str);
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("invalid characters"));
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
|
||||
#[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");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("If primary_namespace is empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_kvstore_params_collision_prevention() {
|
||||
// Test collision prevention between keys and namespaces
|
||||
let test_cases = vec![
|
||||
("primary", "secondary", "primary"), // key matches primary namespace
|
||||
("primary", "secondary", "secondary"), // key matches secondary namespace
|
||||
];
|
||||
|
||||
for (primary, secondary, key) in test_cases {
|
||||
let result = validate_kvstore_params(primary, secondary, key);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected collision for key '{}' with namespaces '{}'/'{}'",
|
||||
key,
|
||||
primary,
|
||||
secondary
|
||||
);
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(error_msg.contains("conflicts with namespace"));
|
||||
}
|
||||
|
||||
// Test that a combined namespace string would be invalid due to the slash character
|
||||
let result = validate_kvstore_params("primary", "secondary", "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");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = validate_kvstore_params("primary", "secondary!", "key");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = validate_kvstore_params("primary", "secondary", "key with space");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alphabet_constants() {
|
||||
// Verify the alphabet constant is as expected
|
||||
assert_eq!(
|
||||
KVSTORE_NAMESPACE_KEY_ALPHABET,
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
|
||||
);
|
||||
assert_eq!(KVSTORE_NAMESPACE_KEY_MAX_LEN, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alphabet_coverage() {
|
||||
// Test that all valid characters are actually accepted
|
||||
for ch in KVSTORE_NAMESPACE_KEY_ALPHABET.chars() {
|
||||
let test_str = ch.to_string();
|
||||
assert!(
|
||||
validate_kvstore_string(&test_str).is_ok(),
|
||||
"Character '{}' should be valid",
|
||||
ch
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_namespace_segmentation_examples() {
|
||||
// Test realistic namespace segmentation scenarios
|
||||
|
||||
// Valid segmentation examples
|
||||
let valid_examples = vec![
|
||||
("wallets", "user123", "balance"),
|
||||
("quotes", "mint", "quote_12345"),
|
||||
("keysets", "", "active_keyset"),
|
||||
("", "", "global_config"),
|
||||
("auth", "session_456", "token"),
|
||||
("mint_info", "", "version"),
|
||||
];
|
||||
|
||||
for (primary, secondary, key) in valid_examples {
|
||||
assert!(
|
||||
validate_kvstore_params(primary, secondary, key).is_ok(),
|
||||
"Valid example should pass: '{}'/'{}'/'{}'",
|
||||
primary,
|
||||
secondary,
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_per_namespace_uniqueness() {
|
||||
// This test documents the requirement that implementations should ensure
|
||||
// per-namespace key uniqueness. The validation function doesn't enforce
|
||||
// database-level uniqueness (that's handled by the database schema),
|
||||
// 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
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,11 @@ mod wallet;
|
||||
#[cfg(feature = "mint")]
|
||||
pub use mint::{
|
||||
Database as MintDatabase, DbTransactionFinalizer as MintDbWriterFinalizer,
|
||||
KeysDatabase as MintKeysDatabase, KeysDatabaseTransaction as MintKeyDatabaseTransaction,
|
||||
ProofsDatabase as MintProofsDatabase, ProofsTransaction as MintProofsTransaction,
|
||||
QuotesDatabase as MintQuotesDatabase, QuotesTransaction as MintQuotesTransaction,
|
||||
SignaturesDatabase as MintSignaturesDatabase,
|
||||
KVStore as MintKVStore, KVStoreDatabase as MintKVStoreDatabase,
|
||||
KVStoreTransaction as MintKVStoreTransaction, KeysDatabase as MintKeysDatabase,
|
||||
KeysDatabaseTransaction as MintKeyDatabaseTransaction, ProofsDatabase as MintProofsDatabase,
|
||||
ProofsTransaction as MintProofsTransaction, QuotesDatabase as MintQuotesDatabase,
|
||||
QuotesTransaction as MintQuotesTransaction, SignaturesDatabase as MintSignaturesDatabase,
|
||||
SignaturesTransaction as MintSignatureTransaction, Transaction as MintTransaction,
|
||||
};
|
||||
#[cfg(all(feature = "mint", feature = "auth"))]
|
||||
@@ -187,6 +188,10 @@ pub enum Error {
|
||||
/// QuoteNotFound
|
||||
#[error("Quote not found")]
|
||||
QuoteNotFound,
|
||||
|
||||
/// KV Store invalid key or namespace
|
||||
#[error("Invalid KV store key or namespace: {0}")]
|
||||
KVStoreInvalidKey(String),
|
||||
}
|
||||
|
||||
#[cfg(feature = "mint")]
|
||||
|
||||
@@ -271,6 +271,9 @@ pub enum Error {
|
||||
/// Transaction not found
|
||||
#[error("Transaction not found")]
|
||||
TransactionNotFound,
|
||||
/// KV Store invalid key or namespace
|
||||
#[error("Invalid KV store key or namespace: {0}")]
|
||||
KVStoreInvalidKey(String),
|
||||
/// Custom Error
|
||||
#[error("`{0}`")]
|
||||
Custom(String),
|
||||
|
||||
@@ -14,7 +14,7 @@ use anyhow::{anyhow, bail, Result};
|
||||
use axum::Router;
|
||||
use bip39::Mnemonic;
|
||||
// internal crate modules
|
||||
use cdk::cdk_database::{self, MintDatabase, MintKeysDatabase};
|
||||
use cdk::cdk_database::{self, MintDatabase, MintKVStore, MintKeysDatabase};
|
||||
use cdk::cdk_payment;
|
||||
use cdk::cdk_payment::MintPayment;
|
||||
use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
|
||||
@@ -94,9 +94,10 @@ async fn initial_setup(
|
||||
) -> Result<(
|
||||
Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
|
||||
Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
|
||||
Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync>,
|
||||
)> {
|
||||
let (localstore, keystore) = setup_database(settings, work_dir, db_password).await?;
|
||||
Ok((localstore, keystore))
|
||||
let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
|
||||
Ok((localstore, keystore, kv))
|
||||
}
|
||||
|
||||
/// Sets up and initializes a tracing subscriber with custom log filtering.
|
||||
@@ -253,14 +254,16 @@ async fn setup_database(
|
||||
) -> Result<(
|
||||
Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
|
||||
Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync>,
|
||||
Arc<dyn MintKVStore<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 keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
|
||||
Ok((localstore, keystore))
|
||||
Ok((localstore, keystore, kv))
|
||||
}
|
||||
#[cfg(feature = "postgres")]
|
||||
DatabaseEngine::Postgres => {
|
||||
@@ -279,11 +282,13 @@ 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();
|
||||
#[cfg(feature = "postgres")]
|
||||
let keystore: Arc<
|
||||
dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
|
||||
> = pg_db;
|
||||
#[cfg(feature = "postgres")]
|
||||
return Ok((localstore, keystore));
|
||||
return Ok((localstore, keystore, kv));
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
|
||||
@@ -326,6 +331,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>>,
|
||||
) -> Result<(MintBuilder, Vec<Router>)> {
|
||||
let mut ln_routers = vec![];
|
||||
|
||||
@@ -333,9 +339,15 @@ async fn configure_mint_builder(
|
||||
let mint_builder = configure_basic_info(settings, mint_builder);
|
||||
|
||||
// Configure lightning backend
|
||||
let mint_builder =
|
||||
configure_lightning_backend(settings, mint_builder, &mut ln_routers, runtime, work_dir)
|
||||
.await?;
|
||||
let mint_builder = configure_lightning_backend(
|
||||
settings,
|
||||
mint_builder,
|
||||
&mut ln_routers,
|
||||
runtime,
|
||||
work_dir,
|
||||
kv_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Configure caching
|
||||
let mint_builder = configure_cache(settings, mint_builder);
|
||||
@@ -400,6 +412,7 @@ async fn configure_lightning_backend(
|
||||
ln_routers: &mut Vec<Router>,
|
||||
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||
work_dir: &Path,
|
||||
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||
) -> Result<MintBuilder> {
|
||||
let mint_melt_limits = MintMeltLimits {
|
||||
mint_min: settings.ln.min_mint,
|
||||
@@ -418,7 +431,14 @@ async fn configure_lightning_backend(
|
||||
.clone()
|
||||
.expect("Config checked at load that cln is some");
|
||||
let cln = cln_settings
|
||||
.setup(ln_routers, settings, CurrencyUnit::Msat, None, work_dir)
|
||||
.setup(
|
||||
ln_routers,
|
||||
settings,
|
||||
CurrencyUnit::Msat,
|
||||
None,
|
||||
work_dir,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
@@ -434,7 +454,14 @@ async fn configure_lightning_backend(
|
||||
LnBackend::LNbits => {
|
||||
let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
|
||||
let lnbits = lnbits_settings
|
||||
.setup(ln_routers, settings, CurrencyUnit::Sat, None, work_dir)
|
||||
.setup(
|
||||
ln_routers,
|
||||
settings,
|
||||
CurrencyUnit::Sat,
|
||||
None,
|
||||
work_dir,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
@@ -450,7 +477,14 @@ async fn configure_lightning_backend(
|
||||
LnBackend::Lnd => {
|
||||
let lnd_settings = settings.clone().lnd.expect("Checked at config load");
|
||||
let lnd = lnd_settings
|
||||
.setup(ln_routers, settings, CurrencyUnit::Msat, None, work_dir)
|
||||
.setup(
|
||||
ln_routers,
|
||||
settings,
|
||||
CurrencyUnit::Msat,
|
||||
None,
|
||||
work_dir,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
@@ -469,7 +503,14 @@ async fn configure_lightning_backend(
|
||||
|
||||
for unit in fake_wallet.clone().supported_units {
|
||||
let fake = fake_wallet
|
||||
.setup(ln_routers, settings, unit.clone(), None, work_dir)
|
||||
.setup(
|
||||
ln_routers,
|
||||
settings,
|
||||
unit.clone(),
|
||||
None,
|
||||
work_dir,
|
||||
_kv_store.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
@@ -498,7 +539,7 @@ async fn configure_lightning_backend(
|
||||
for unit in grpc_processor.clone().supported_units {
|
||||
tracing::debug!("Adding unit: {:?}", unit);
|
||||
let processor = grpc_processor
|
||||
.setup(ln_routers, settings, unit.clone(), None, work_dir)
|
||||
.setup(ln_routers, settings, unit.clone(), None, work_dir, None)
|
||||
.await?;
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
@@ -517,7 +558,14 @@ async fn configure_lightning_backend(
|
||||
tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
|
||||
|
||||
let ldk_node = ldk_node_settings
|
||||
.setup(ln_routers, settings, CurrencyUnit::Sat, _runtime, work_dir)
|
||||
.setup(
|
||||
ln_routers,
|
||||
settings,
|
||||
CurrencyUnit::Sat,
|
||||
_runtime,
|
||||
work_dir,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
mint_builder = configure_backend_for_unit(
|
||||
@@ -1015,12 +1063,12 @@ pub async fn run_mintd_with_shutdown(
|
||||
db_password: Option<String>,
|
||||
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||
) -> Result<()> {
|
||||
let (localstore, keystore) = initial_setup(work_dir, settings, db_password.clone()).await?;
|
||||
let (localstore, keystore, kv) = initial_setup(work_dir, settings, db_password.clone()).await?;
|
||||
|
||||
let mint_builder = MintBuilder::new(localstore);
|
||||
|
||||
let (mint_builder, ln_routers) =
|
||||
configure_mint_builder(settings, mint_builder, runtime, work_dir).await?;
|
||||
configure_mint_builder(settings, mint_builder, runtime, work_dir, Some(kv)).await?;
|
||||
#[cfg(feature = "auth")]
|
||||
let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::collections::HashMap;
|
||||
#[cfg(feature = "fakewallet")]
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "cln")]
|
||||
use anyhow::anyhow;
|
||||
@@ -10,6 +11,7 @@ use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
#[cfg(feature = "fakewallet")]
|
||||
use bip39::rand::{thread_rng, Rng};
|
||||
use cdk::cdk_database::MintKVStore;
|
||||
use cdk::cdk_payment::MintPayment;
|
||||
use cdk::nuts::CurrencyUnit;
|
||||
#[cfg(any(
|
||||
@@ -34,6 +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>>,
|
||||
) -> anyhow::Result<impl MintPayment>;
|
||||
}
|
||||
|
||||
@@ -47,6 +50,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>>,
|
||||
) -> anyhow::Result<cdk_cln::Cln> {
|
||||
let cln_socket = expand_path(
|
||||
self.rpc_path
|
||||
@@ -76,6 +80,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>>,
|
||||
) -> anyhow::Result<cdk_lnbits::LNbits> {
|
||||
let admin_api_key = &self.admin_api_key;
|
||||
let invoice_api_key = &self.invoice_api_key;
|
||||
@@ -110,6 +115,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>>,
|
||||
) -> anyhow::Result<cdk_lnd::Lnd> {
|
||||
let address = &self.address;
|
||||
let cert_file = &self.cert_file;
|
||||
@@ -142,6 +148,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>>,
|
||||
) -> anyhow::Result<cdk_fake_wallet::FakeWallet> {
|
||||
let fee_reserve = FeeReserve {
|
||||
min_fee_reserve: self.reserve_fee_min,
|
||||
@@ -174,6 +181,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>>,
|
||||
) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
|
||||
let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
|
||||
&self.addr,
|
||||
@@ -196,6 +204,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>>,
|
||||
) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
|
||||
use std::net::SocketAddr;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#[cfg(feature = "fake")]
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
|
||||
@@ -110,6 +110,9 @@ async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
#[cfg(feature = "fake")]
|
||||
"FAKEWALLET" => {
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
let fee_reserve = FeeReserve {
|
||||
min_fee_reserve: 1.into(),
|
||||
percent_fee_reserve: 0.0,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
pub static MIGRATIONS: &[(&str, &str, &str)] = &[
|
||||
("postgres", "1_initial.sql", include_str!(r#"./migrations/postgres/1_initial.sql"#)),
|
||||
("postgres", "2_remove_request_lookup_kind_constraints.sql", include_str!(r#"./migrations/postgres/2_remove_request_lookup_kind_constraints.sql"#)),
|
||||
("postgres", "3_add_kv_store.sql", include_str!(r#"./migrations/postgres/3_add_kv_store.sql"#)),
|
||||
("sqlite", "1_fix_sqlx_migration.sql", include_str!(r#"./migrations/sqlite/1_fix_sqlx_migration.sql"#)),
|
||||
("sqlite", "20240612124932_init.sql", include_str!(r#"./migrations/sqlite/20240612124932_init.sql"#)),
|
||||
("sqlite", "20240618195700_quote_state.sql", include_str!(r#"./migrations/sqlite/20240618195700_quote_state.sql"#)),
|
||||
@@ -27,4 +28,5 @@ pub static MIGRATIONS: &[(&str, &str, &str)] = &[
|
||||
("sqlite", "20250706101057_bolt12.sql", include_str!(r#"./migrations/sqlite/20250706101057_bolt12.sql"#)),
|
||||
("sqlite", "20250812132015_drop_melt_request.sql", include_str!(r#"./migrations/sqlite/20250812132015_drop_melt_request.sql"#)),
|
||||
("sqlite", "20250819200000_remove_request_lookup_kind_constraints.sql", include_str!(r#"./migrations/sqlite/20250819200000_remove_request_lookup_kind_constraints.sql"#)),
|
||||
("sqlite", "20250901090000_add_kv_store.sql", include_str!(r#"./migrations/sqlite/20250901090000_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);
|
||||
@@ -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);
|
||||
@@ -16,6 +16,7 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bitcoin::bip32::DerivationPath;
|
||||
use cdk_common::common::QuoteTTL;
|
||||
use cdk_common::database::mint::validate_kvstore_params;
|
||||
use cdk_common::database::{
|
||||
self, ConversionError, Error, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction,
|
||||
MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase, MintQuotesTransaction,
|
||||
@@ -1582,6 +1583,224 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<RM> database::MintKVStoreTransaction<'_, Error> for SQLTransaction<RM>
|
||||
where
|
||||
RM: DatabasePool + 'static,
|
||||
{
|
||||
async fn kv_read(
|
||||
&mut self,
|
||||
primary_namespace: &str,
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn kv_write(
|
||||
&mut self,
|
||||
primary_namespace: &str,
|
||||
secondary_namespace: &str,
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn kv_remove(
|
||||
&mut self,
|
||||
primary_namespace: &str,
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn kv_list(
|
||||
&mut self,
|
||||
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>>()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<RM> database::MintKVStoreDatabase for SQLMintDatabase<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> {
|
||||
// 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,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn kv_list(
|
||||
&self,
|
||||
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>>()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<RM> database::MintKVStore 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>
|
||||
{
|
||||
Ok(Box::new(SQLTransaction {
|
||||
inner: ConnectionWithTransaction::new(
|
||||
self.pool.get().map_err(|e| Error::Database(Box::new(e)))?,
|
||||
)
|
||||
.await?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<RM> MintDatabase<Error> for SQLMintDatabase<RM>
|
||||
where
|
||||
|
||||
@@ -12,8 +12,8 @@ pub mod cdk_database {
|
||||
pub use cdk_common::database::WalletDatabase;
|
||||
#[cfg(feature = "mint")]
|
||||
pub use cdk_common::database::{
|
||||
MintDatabase, MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase,
|
||||
MintSignaturesDatabase, MintTransaction,
|
||||
MintDatabase, MintKVStore, MintKVStoreDatabase, MintKVStoreTransaction, MintKeysDatabase,
|
||||
MintProofsDatabase, MintQuotesDatabase, MintSignaturesDatabase, MintTransaction,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user