mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-19 05:35:18 +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: 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-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-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
|
### Changed
|
||||||
- cdk-common: Refactored `MintPayment` trait method `wait_any_incoming_payment` to `wait_payment_event` with event-driven architecture ([thesimplekid]).
|
- 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]
|
#[test]
|
||||||
pub fn test_public_key_from_hex() {
|
pub fn test_public_key_from_hex() {
|
||||||
// Compressed
|
// Compressed
|
||||||
assert!(
|
assert!(PublicKey::from_hex(
|
||||||
(PublicKey::from_hex(
|
"02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
||||||
"02194603ffa36356f4a56b7df9371fc3192472351453ec7398b8da8117e7c3e104"
|
)
|
||||||
)
|
.is_ok());
|
||||||
.is_ok())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_invalid_public_key_from_hex() {
|
pub fn test_invalid_public_key_from_hex() {
|
||||||
// Uncompressed (is valid but is cashu must be compressed?)
|
// Uncompressed (is valid but is cashu must be compressed?)
|
||||||
assert!((PublicKey::from_hex("04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481")
|
assert!(PublicKey::from_hex("04fd4ce5a16b65576145949e6f99f445f8249fee17c606b688b504a849cdc452de3625246cb2c27dac965cb7200a5986467eee92eb7d496bbf1453b074e223e481")
|
||||||
.is_err()))
|
.is_err())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,66 @@
|
|||||||
|
|
||||||
use std::collections::HashMap;
|
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 async_trait::async_trait;
|
||||||
use cashu::quote_id::QuoteId;
|
use cashu::quote_id::QuoteId;
|
||||||
use cashu::{Amount, MintInfo};
|
use cashu::{Amount, MintInfo};
|
||||||
@@ -20,6 +80,9 @@ mod auth;
|
|||||||
#[cfg(feature = "test")]
|
#[cfg(feature = "test")]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_kvstore;
|
||||||
|
|
||||||
#[cfg(feature = "auth")]
|
#[cfg(feature = "auth")]
|
||||||
pub use auth::{MintAuthDatabase, MintAuthTransaction};
|
pub use auth::{MintAuthDatabase, MintAuthTransaction};
|
||||||
|
|
||||||
@@ -263,6 +326,42 @@ pub trait DbTransactionFinalizer {
|
|||||||
async fn rollback(self: Box<Self>) -> Result<(), Self::Err>;
|
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
|
/// Base database writer
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Transaction<'a, Error>:
|
pub trait Transaction<'a, Error>:
|
||||||
@@ -270,6 +369,7 @@ pub trait Transaction<'a, Error>:
|
|||||||
+ QuotesTransaction<'a, Err = Error>
|
+ QuotesTransaction<'a, Err = Error>
|
||||||
+ SignaturesTransaction<'a, Err = Error>
|
+ SignaturesTransaction<'a, Err = Error>
|
||||||
+ ProofsTransaction<'a, Err = Error>
|
+ ProofsTransaction<'a, Err = Error>
|
||||||
|
+ KVStoreTransaction<'a, Error>
|
||||||
{
|
{
|
||||||
/// Set [`QuoteTTL`]
|
/// Set [`QuoteTTL`]
|
||||||
async fn set_quote_ttl(&mut self, quote_ttl: QuoteTTL) -> Result<(), Error>;
|
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>;
|
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
|
/// Mint Database trait
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Database<Error>:
|
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
|
/// Beings a transaction
|
||||||
async fn begin_transaction<'a>(
|
async fn begin_transaction<'a>(
|
||||||
|
|||||||
@@ -4,17 +4,19 @@
|
|||||||
//! implementation
|
//! implementation
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
// For derivation path parsing
|
||||||
|
use bitcoin::bip32::DerivationPath;
|
||||||
use cashu::secret::Secret;
|
use cashu::secret::Secret;
|
||||||
use cashu::{Amount, CurrencyUnit, SecretKey};
|
use cashu::{Amount, CurrencyUnit, SecretKey};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::database;
|
use crate::database::MintKVStoreDatabase;
|
||||||
use crate::mint::MintKeySetInfo;
|
use crate::mint::MintKeySetInfo;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
async fn setup_keyset<DB>(db: &DB) -> Id
|
async fn setup_keyset<DB>(db: &DB) -> Id
|
||||||
where
|
where
|
||||||
DB: KeysDatabase<Err = database::Error>,
|
DB: KeysDatabase<Err = crate::database::Error>,
|
||||||
{
|
{
|
||||||
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
|
let keyset_id = Id::from_str("00916bbf7ef91a36").unwrap();
|
||||||
let keyset_info = MintKeySetInfo {
|
let keyset_info = MintKeySetInfo {
|
||||||
@@ -23,7 +25,7 @@ where
|
|||||||
active: true,
|
active: true,
|
||||||
valid_from: 0,
|
valid_from: 0,
|
||||||
final_expiry: None,
|
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),
|
derivation_path_index: Some(0),
|
||||||
max_order: 32,
|
max_order: 32,
|
||||||
input_fee_ppk: 0,
|
input_fee_ppk: 0,
|
||||||
@@ -37,7 +39,7 @@ where
|
|||||||
/// State transition test
|
/// State transition test
|
||||||
pub async fn state_transition<DB>(db: DB)
|
pub async fn state_transition<DB>(db: DB)
|
||||||
where
|
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;
|
let keyset_id = setup_keyset(&db).await;
|
||||||
|
|
||||||
@@ -83,7 +85,7 @@ where
|
|||||||
/// other tests
|
/// other tests
|
||||||
pub async fn add_and_find_proofs<DB>(db: DB)
|
pub async fn add_and_find_proofs<DB>(db: DB)
|
||||||
where
|
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;
|
let keyset_id = setup_keyset(&db).await;
|
||||||
|
|
||||||
@@ -124,12 +126,99 @@ where
|
|||||||
assert_eq!(proofs_from_db.unwrap().len(), 2);
|
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
|
/// Unit test that is expected to be passed for a correct database implementation
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! mint_db_test {
|
macro_rules! mint_db_test {
|
||||||
($make_db_fn:ident) => {
|
($make_db_fn:ident) => {
|
||||||
mint_db_test!(state_transition, $make_db_fn);
|
mint_db_test!(state_transition, $make_db_fn);
|
||||||
mint_db_test!(add_and_find_proofs, $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) => {
|
($name:ident, $make_db_fn:ident) => {
|
||||||
#[tokio::test]
|
#[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")]
|
#[cfg(feature = "mint")]
|
||||||
pub use mint::{
|
pub use mint::{
|
||||||
Database as MintDatabase, DbTransactionFinalizer as MintDbWriterFinalizer,
|
Database as MintDatabase, DbTransactionFinalizer as MintDbWriterFinalizer,
|
||||||
KeysDatabase as MintKeysDatabase, KeysDatabaseTransaction as MintKeyDatabaseTransaction,
|
KVStore as MintKVStore, KVStoreDatabase as MintKVStoreDatabase,
|
||||||
ProofsDatabase as MintProofsDatabase, ProofsTransaction as MintProofsTransaction,
|
KVStoreTransaction as MintKVStoreTransaction, KeysDatabase as MintKeysDatabase,
|
||||||
QuotesDatabase as MintQuotesDatabase, QuotesTransaction as MintQuotesTransaction,
|
KeysDatabaseTransaction as MintKeyDatabaseTransaction, ProofsDatabase as MintProofsDatabase,
|
||||||
SignaturesDatabase as MintSignaturesDatabase,
|
ProofsTransaction as MintProofsTransaction, QuotesDatabase as MintQuotesDatabase,
|
||||||
|
QuotesTransaction as MintQuotesTransaction, SignaturesDatabase as MintSignaturesDatabase,
|
||||||
SignaturesTransaction as MintSignatureTransaction, Transaction as MintTransaction,
|
SignaturesTransaction as MintSignatureTransaction, Transaction as MintTransaction,
|
||||||
};
|
};
|
||||||
#[cfg(all(feature = "mint", feature = "auth"))]
|
#[cfg(all(feature = "mint", feature = "auth"))]
|
||||||
@@ -187,6 +188,10 @@ pub enum Error {
|
|||||||
/// QuoteNotFound
|
/// QuoteNotFound
|
||||||
#[error("Quote not found")]
|
#[error("Quote not found")]
|
||||||
QuoteNotFound,
|
QuoteNotFound,
|
||||||
|
|
||||||
|
/// KV Store invalid key or namespace
|
||||||
|
#[error("Invalid KV store key or namespace: {0}")]
|
||||||
|
KVStoreInvalidKey(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
|
|||||||
@@ -271,6 +271,9 @@ pub enum Error {
|
|||||||
/// Transaction not found
|
/// Transaction not found
|
||||||
#[error("Transaction not found")]
|
#[error("Transaction not found")]
|
||||||
TransactionNotFound,
|
TransactionNotFound,
|
||||||
|
/// KV Store invalid key or namespace
|
||||||
|
#[error("Invalid KV store key or namespace: {0}")]
|
||||||
|
KVStoreInvalidKey(String),
|
||||||
/// Custom Error
|
/// Custom Error
|
||||||
#[error("`{0}`")]
|
#[error("`{0}`")]
|
||||||
Custom(String),
|
Custom(String),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use anyhow::{anyhow, bail, Result};
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use bip39::Mnemonic;
|
use bip39::Mnemonic;
|
||||||
// internal crate modules
|
// 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;
|
||||||
use cdk::cdk_payment::MintPayment;
|
use cdk::cdk_payment::MintPayment;
|
||||||
use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
|
use cdk::mint::{Mint, MintBuilder, MintMeltLimits};
|
||||||
@@ -94,9 +94,10 @@ async fn initial_setup(
|
|||||||
) -> Result<(
|
) -> Result<(
|
||||||
Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
|
Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
|
||||||
Arc<dyn MintKeysDatabase<Err = 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?;
|
let (localstore, keystore, kv) = setup_database(settings, work_dir, db_password).await?;
|
||||||
Ok((localstore, keystore))
|
Ok((localstore, keystore, kv))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up and initializes a tracing subscriber with custom log filtering.
|
/// Sets up and initializes a tracing subscriber with custom log filtering.
|
||||||
@@ -253,14 +254,16 @@ async fn setup_database(
|
|||||||
) -> Result<(
|
) -> Result<(
|
||||||
Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
|
Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync>,
|
||||||
Arc<dyn MintKeysDatabase<Err = 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 {
|
match settings.database.engine {
|
||||||
#[cfg(feature = "sqlite")]
|
#[cfg(feature = "sqlite")]
|
||||||
DatabaseEngine::Sqlite => {
|
DatabaseEngine::Sqlite => {
|
||||||
let db = setup_sqlite_database(_work_dir, _db_password).await?;
|
let db = setup_sqlite_database(_work_dir, _db_password).await?;
|
||||||
let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> = db.clone();
|
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;
|
let keystore: Arc<dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync> = db;
|
||||||
Ok((localstore, keystore))
|
Ok((localstore, keystore, kv))
|
||||||
}
|
}
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
DatabaseEngine::Postgres => {
|
DatabaseEngine::Postgres => {
|
||||||
@@ -279,11 +282,13 @@ async fn setup_database(
|
|||||||
let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
|
let localstore: Arc<dyn MintDatabase<cdk_database::Error> + Send + Sync> =
|
||||||
pg_db.clone();
|
pg_db.clone();
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
|
let kv: Arc<dyn MintKVStore<Err = cdk_database::Error> + Send + Sync> = pg_db.clone();
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
let keystore: Arc<
|
let keystore: Arc<
|
||||||
dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
|
dyn MintKeysDatabase<Err = cdk_database::Error> + Send + Sync,
|
||||||
> = pg_db;
|
> = pg_db;
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
return Ok((localstore, keystore));
|
return Ok((localstore, keystore, kv));
|
||||||
|
|
||||||
#[cfg(not(feature = "postgres"))]
|
#[cfg(not(feature = "postgres"))]
|
||||||
bail!("PostgreSQL support not compiled in. Enable the 'postgres' feature to use PostgreSQL database.")
|
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,
|
mint_builder: MintBuilder,
|
||||||
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
work_dir: &Path,
|
work_dir: &Path,
|
||||||
|
kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> Result<(MintBuilder, Vec<Router>)> {
|
) -> Result<(MintBuilder, Vec<Router>)> {
|
||||||
let mut ln_routers = vec![];
|
let mut ln_routers = vec![];
|
||||||
|
|
||||||
@@ -333,9 +339,15 @@ async fn configure_mint_builder(
|
|||||||
let mint_builder = configure_basic_info(settings, mint_builder);
|
let mint_builder = configure_basic_info(settings, mint_builder);
|
||||||
|
|
||||||
// Configure lightning backend
|
// Configure lightning backend
|
||||||
let mint_builder =
|
let mint_builder = configure_lightning_backend(
|
||||||
configure_lightning_backend(settings, mint_builder, &mut ln_routers, runtime, work_dir)
|
settings,
|
||||||
.await?;
|
mint_builder,
|
||||||
|
&mut ln_routers,
|
||||||
|
runtime,
|
||||||
|
work_dir,
|
||||||
|
kv_store,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Configure caching
|
// Configure caching
|
||||||
let mint_builder = configure_cache(settings, mint_builder);
|
let mint_builder = configure_cache(settings, mint_builder);
|
||||||
@@ -400,6 +412,7 @@ async fn configure_lightning_backend(
|
|||||||
ln_routers: &mut Vec<Router>,
|
ln_routers: &mut Vec<Router>,
|
||||||
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
work_dir: &Path,
|
work_dir: &Path,
|
||||||
|
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> Result<MintBuilder> {
|
) -> Result<MintBuilder> {
|
||||||
let mint_melt_limits = MintMeltLimits {
|
let mint_melt_limits = MintMeltLimits {
|
||||||
mint_min: settings.ln.min_mint,
|
mint_min: settings.ln.min_mint,
|
||||||
@@ -418,7 +431,14 @@ async fn configure_lightning_backend(
|
|||||||
.clone()
|
.clone()
|
||||||
.expect("Config checked at load that cln is some");
|
.expect("Config checked at load that cln is some");
|
||||||
let cln = cln_settings
|
let cln = cln_settings
|
||||||
.setup(ln_routers, settings, CurrencyUnit::Msat, None, work_dir)
|
.setup(
|
||||||
|
ln_routers,
|
||||||
|
settings,
|
||||||
|
CurrencyUnit::Msat,
|
||||||
|
None,
|
||||||
|
work_dir,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
mint_builder = configure_backend_for_unit(
|
mint_builder = configure_backend_for_unit(
|
||||||
@@ -434,7 +454,14 @@ async fn configure_lightning_backend(
|
|||||||
LnBackend::LNbits => {
|
LnBackend::LNbits => {
|
||||||
let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
|
let lnbits_settings = settings.clone().lnbits.expect("Checked on config load");
|
||||||
let lnbits = lnbits_settings
|
let lnbits = lnbits_settings
|
||||||
.setup(ln_routers, settings, CurrencyUnit::Sat, None, work_dir)
|
.setup(
|
||||||
|
ln_routers,
|
||||||
|
settings,
|
||||||
|
CurrencyUnit::Sat,
|
||||||
|
None,
|
||||||
|
work_dir,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
mint_builder = configure_backend_for_unit(
|
mint_builder = configure_backend_for_unit(
|
||||||
@@ -450,7 +477,14 @@ async fn configure_lightning_backend(
|
|||||||
LnBackend::Lnd => {
|
LnBackend::Lnd => {
|
||||||
let lnd_settings = settings.clone().lnd.expect("Checked at config load");
|
let lnd_settings = settings.clone().lnd.expect("Checked at config load");
|
||||||
let lnd = lnd_settings
|
let lnd = lnd_settings
|
||||||
.setup(ln_routers, settings, CurrencyUnit::Msat, None, work_dir)
|
.setup(
|
||||||
|
ln_routers,
|
||||||
|
settings,
|
||||||
|
CurrencyUnit::Msat,
|
||||||
|
None,
|
||||||
|
work_dir,
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
mint_builder = configure_backend_for_unit(
|
mint_builder = configure_backend_for_unit(
|
||||||
@@ -469,7 +503,14 @@ async fn configure_lightning_backend(
|
|||||||
|
|
||||||
for unit in fake_wallet.clone().supported_units {
|
for unit in fake_wallet.clone().supported_units {
|
||||||
let fake = fake_wallet
|
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?;
|
.await?;
|
||||||
|
|
||||||
mint_builder = configure_backend_for_unit(
|
mint_builder = configure_backend_for_unit(
|
||||||
@@ -498,7 +539,7 @@ async fn configure_lightning_backend(
|
|||||||
for unit in grpc_processor.clone().supported_units {
|
for unit in grpc_processor.clone().supported_units {
|
||||||
tracing::debug!("Adding unit: {:?}", unit);
|
tracing::debug!("Adding unit: {:?}", unit);
|
||||||
let processor = grpc_processor
|
let processor = grpc_processor
|
||||||
.setup(ln_routers, settings, unit.clone(), None, work_dir)
|
.setup(ln_routers, settings, unit.clone(), None, work_dir, None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
mint_builder = configure_backend_for_unit(
|
mint_builder = configure_backend_for_unit(
|
||||||
@@ -517,7 +558,14 @@ async fn configure_lightning_backend(
|
|||||||
tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
|
tracing::info!("Using LDK Node backend: {:?}", ldk_node_settings);
|
||||||
|
|
||||||
let ldk_node = 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?;
|
.await?;
|
||||||
|
|
||||||
mint_builder = configure_backend_for_unit(
|
mint_builder = configure_backend_for_unit(
|
||||||
@@ -1015,12 +1063,12 @@ pub async fn run_mintd_with_shutdown(
|
|||||||
db_password: Option<String>,
|
db_password: Option<String>,
|
||||||
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
) -> Result<()> {
|
) -> 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 = MintBuilder::new(localstore);
|
||||||
|
|
||||||
let (mint_builder, ln_routers) =
|
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")]
|
#[cfg(feature = "auth")]
|
||||||
let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
|
let mint_builder = setup_authentication(settings, work_dir, mint_builder, db_password).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::collections::HashMap;
|
|||||||
#[cfg(feature = "fakewallet")]
|
#[cfg(feature = "fakewallet")]
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[cfg(feature = "cln")]
|
#[cfg(feature = "cln")]
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
@@ -10,6 +11,7 @@ use async_trait::async_trait;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
#[cfg(feature = "fakewallet")]
|
#[cfg(feature = "fakewallet")]
|
||||||
use bip39::rand::{thread_rng, Rng};
|
use bip39::rand::{thread_rng, Rng};
|
||||||
|
use cdk::cdk_database::MintKVStore;
|
||||||
use cdk::cdk_payment::MintPayment;
|
use cdk::cdk_payment::MintPayment;
|
||||||
use cdk::nuts::CurrencyUnit;
|
use cdk::nuts::CurrencyUnit;
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
@@ -34,6 +36,7 @@ pub trait LnBackendSetup {
|
|||||||
unit: CurrencyUnit,
|
unit: CurrencyUnit,
|
||||||
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
work_dir: &Path,
|
work_dir: &Path,
|
||||||
|
kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> anyhow::Result<impl MintPayment>;
|
) -> anyhow::Result<impl MintPayment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ impl LnBackendSetup for config::Cln {
|
|||||||
_unit: CurrencyUnit,
|
_unit: CurrencyUnit,
|
||||||
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
_work_dir: &Path,
|
_work_dir: &Path,
|
||||||
|
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> anyhow::Result<cdk_cln::Cln> {
|
) -> anyhow::Result<cdk_cln::Cln> {
|
||||||
let cln_socket = expand_path(
|
let cln_socket = expand_path(
|
||||||
self.rpc_path
|
self.rpc_path
|
||||||
@@ -76,6 +80,7 @@ impl LnBackendSetup for config::LNbits {
|
|||||||
_unit: CurrencyUnit,
|
_unit: CurrencyUnit,
|
||||||
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
_work_dir: &Path,
|
_work_dir: &Path,
|
||||||
|
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> anyhow::Result<cdk_lnbits::LNbits> {
|
) -> anyhow::Result<cdk_lnbits::LNbits> {
|
||||||
let admin_api_key = &self.admin_api_key;
|
let admin_api_key = &self.admin_api_key;
|
||||||
let invoice_api_key = &self.invoice_api_key;
|
let invoice_api_key = &self.invoice_api_key;
|
||||||
@@ -110,6 +115,7 @@ impl LnBackendSetup for config::Lnd {
|
|||||||
_unit: CurrencyUnit,
|
_unit: CurrencyUnit,
|
||||||
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
_work_dir: &Path,
|
_work_dir: &Path,
|
||||||
|
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> anyhow::Result<cdk_lnd::Lnd> {
|
) -> anyhow::Result<cdk_lnd::Lnd> {
|
||||||
let address = &self.address;
|
let address = &self.address;
|
||||||
let cert_file = &self.cert_file;
|
let cert_file = &self.cert_file;
|
||||||
@@ -142,6 +148,7 @@ impl LnBackendSetup for config::FakeWallet {
|
|||||||
unit: CurrencyUnit,
|
unit: CurrencyUnit,
|
||||||
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
_work_dir: &Path,
|
_work_dir: &Path,
|
||||||
|
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> anyhow::Result<cdk_fake_wallet::FakeWallet> {
|
) -> anyhow::Result<cdk_fake_wallet::FakeWallet> {
|
||||||
let fee_reserve = FeeReserve {
|
let fee_reserve = FeeReserve {
|
||||||
min_fee_reserve: self.reserve_fee_min,
|
min_fee_reserve: self.reserve_fee_min,
|
||||||
@@ -174,6 +181,7 @@ impl LnBackendSetup for config::GrpcProcessor {
|
|||||||
_unit: CurrencyUnit,
|
_unit: CurrencyUnit,
|
||||||
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
_runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
_work_dir: &Path,
|
_work_dir: &Path,
|
||||||
|
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
|
) -> anyhow::Result<cdk_payment_processor::PaymentProcessorClient> {
|
||||||
let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
|
let payment_processor = cdk_payment_processor::PaymentProcessorClient::new(
|
||||||
&self.addr,
|
&self.addr,
|
||||||
@@ -196,6 +204,7 @@ impl LnBackendSetup for config::LdkNode {
|
|||||||
_unit: CurrencyUnit,
|
_unit: CurrencyUnit,
|
||||||
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
runtime: Option<std::sync::Arc<tokio::runtime::Runtime>>,
|
||||||
work_dir: &Path,
|
work_dir: &Path,
|
||||||
|
_kv_store: Option<Arc<dyn MintKVStore<Err = cdk::cdk_database::Error> + Send + Sync>>,
|
||||||
) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
|
) -> anyhow::Result<cdk_ldk_node::CdkLdkNode> {
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#[cfg(feature = "fake")]
|
#[cfg(feature = "fake")]
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::HashSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
|
#[cfg(any(feature = "cln", feature = "lnd", feature = "fake"))]
|
||||||
@@ -110,6 +110,9 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
#[cfg(feature = "fake")]
|
#[cfg(feature = "fake")]
|
||||||
"FAKEWALLET" => {
|
"FAKEWALLET" => {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
let fee_reserve = FeeReserve {
|
let fee_reserve = FeeReserve {
|
||||||
min_fee_reserve: 1.into(),
|
min_fee_reserve: 1.into(),
|
||||||
percent_fee_reserve: 0.0,
|
percent_fee_reserve: 0.0,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
pub static MIGRATIONS: &[(&str, &str, &str)] = &[
|
pub static MIGRATIONS: &[(&str, &str, &str)] = &[
|
||||||
("postgres", "1_initial.sql", include_str!(r#"./migrations/postgres/1_initial.sql"#)),
|
("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", "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", "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", "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"#)),
|
("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", "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", "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", "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 async_trait::async_trait;
|
||||||
use bitcoin::bip32::DerivationPath;
|
use bitcoin::bip32::DerivationPath;
|
||||||
use cdk_common::common::QuoteTTL;
|
use cdk_common::common::QuoteTTL;
|
||||||
|
use cdk_common::database::mint::validate_kvstore_params;
|
||||||
use cdk_common::database::{
|
use cdk_common::database::{
|
||||||
self, ConversionError, Error, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction,
|
self, ConversionError, Error, MintDatabase, MintDbWriterFinalizer, MintKeyDatabaseTransaction,
|
||||||
MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase, MintQuotesTransaction,
|
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]
|
#[async_trait]
|
||||||
impl<RM> MintDatabase<Error> for SQLMintDatabase<RM>
|
impl<RM> MintDatabase<Error> for SQLMintDatabase<RM>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ pub mod cdk_database {
|
|||||||
pub use cdk_common::database::WalletDatabase;
|
pub use cdk_common::database::WalletDatabase;
|
||||||
#[cfg(feature = "mint")]
|
#[cfg(feature = "mint")]
|
||||||
pub use cdk_common::database::{
|
pub use cdk_common::database::{
|
||||||
MintDatabase, MintKeysDatabase, MintProofsDatabase, MintQuotesDatabase,
|
MintDatabase, MintKVStore, MintKVStoreDatabase, MintKVStoreTransaction, MintKeysDatabase,
|
||||||
MintSignaturesDatabase, MintTransaction,
|
MintProofsDatabase, MintQuotesDatabase, MintSignaturesDatabase, MintTransaction,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user