diff --git a/.helix/languages.toml b/.helix/languages.toml index 188aa050..91096861 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,4 +1,4 @@ [[language]] name = "rust" -config = { cargo = { features = [ "wallet", "mint" ] } } +config = { cargo = { features = [ "wallet", "mint", "redb" ] } } diff --git a/crates/cashu-sdk/src/mint/localstore/memory.rs b/crates/cashu-sdk/src/mint/localstore/memory.rs index 0e6bcac7..5c7df2f0 100644 --- a/crates/cashu-sdk/src/mint/localstore/memory.rs +++ b/crates/cashu-sdk/src/mint/localstore/memory.rs @@ -84,8 +84,11 @@ impl LocalStore for MemoryLocalStore { Ok(()) } - async fn add_spent_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error> { - self.spent_proofs.lock().await.insert(secret, proof); + async fn add_spent_proof(&self, proof: Proof) -> Result<(), Error> { + self.spent_proofs + .lock() + .await + .insert(proof.secret.clone(), proof); Ok(()) } @@ -93,8 +96,11 @@ impl LocalStore for MemoryLocalStore { Ok(self.spent_proofs.lock().await.get(secret).cloned()) } - async fn add_pending_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error> { - self.pending_proofs.lock().await.insert(secret, proof); + async fn add_pending_proof(&self, proof: Proof) -> Result<(), Error> { + self.pending_proofs + .lock() + .await + .insert(proof.secret.clone(), proof); Ok(()) } diff --git a/crates/cashu-sdk/src/mint/localstore/mod.rs b/crates/cashu-sdk/src/mint/localstore/mod.rs index 6f510e8b..11788370 100644 --- a/crates/cashu-sdk/src/mint/localstore/mod.rs +++ b/crates/cashu-sdk/src/mint/localstore/mod.rs @@ -1,4 +1,6 @@ mod memory; +#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] +mod redb_store; use std::collections::HashMap; @@ -7,6 +9,8 @@ use cashu::nuts::nut02::mint::KeySet; use cashu::nuts::{CurrencyUnit, Id, Proof}; use cashu::secret::Secret; use cashu::types::{MeltQuote, MintQuote}; +#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] +pub use redb_store::RedbLocalStore; use thiserror::Error; #[derive(Debug, Error)] @@ -52,10 +56,10 @@ pub trait LocalStore { async fn get_keyset(&self, id: &Id) -> Result, Error>; async fn get_keysets(&self) -> Result, Error>; - async fn add_spent_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error>; + async fn add_spent_proof(&self, proof: Proof) -> Result<(), Error>; async fn get_spent_proof(&self, secret: &Secret) -> Result, Error>; - async fn add_pending_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error>; + async fn add_pending_proof(&self, proof: Proof) -> Result<(), Error>; async fn get_pending_proof(&self, secret: &Secret) -> Result, Error>; async fn remove_pending_proof(&self, secret: &Secret) -> Result<(), Error>; } diff --git a/crates/cashu-sdk/src/mint/localstore/redb_store.rs b/crates/cashu-sdk/src/mint/localstore/redb_store.rs new file mode 100644 index 00000000..3ca013e9 --- /dev/null +++ b/crates/cashu-sdk/src/mint/localstore/redb_store.rs @@ -0,0 +1,282 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +use async_trait::async_trait; +use cashu::nuts::{CurrencyUnit, Id, MintKeySet as KeySet, Proof}; +use cashu::secret::Secret; +use cashu::types::{MeltQuote, MintQuote}; +use redb::{Database, ReadableTable, TableDefinition}; +use tokio::sync::Mutex; + +use super::{Error, LocalStore}; + +const ACTIVE_KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("active_keysets"); +const KEYSETS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("keysets"); +const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes"); +const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes"); +const PENDING_PROOFS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("pending_proofs"); +const SPENT_PROOFS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("spent_proofs"); + +#[derive(Debug, Clone)] +pub struct RedbLocalStore { + db: Arc>, +} + +impl RedbLocalStore { + pub fn new(path: &str) -> Result { + let db = Database::create(path)?; + + let write_txn = db.begin_write()?; + { + let _ = write_txn.open_table(ACTIVE_KEYSETS_TABLE)?; + let _ = write_txn.open_table(KEYSETS_TABLE)?; + let _ = write_txn.open_table(MINT_QUOTES_TABLE)?; + let _ = write_txn.open_table(MELT_QUOTES_TABLE)?; + let _ = write_txn.open_table(PENDING_PROOFS_TABLE)?; + let _ = write_txn.open_table(SPENT_PROOFS_TABLE)?; + } + write_txn.commit()?; + + Ok(Self { + db: Arc::new(Mutex::new(db)), + }) + } +} + +#[async_trait(?Send)] +impl LocalStore for RedbLocalStore { + async fn add_active_keyset(&self, unit: CurrencyUnit, id: Id) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(ACTIVE_KEYSETS_TABLE)?; + table.insert(unit.to_string().as_str(), id.to_string().as_str())?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_active_keyset_id(&self, unit: &CurrencyUnit) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(ACTIVE_KEYSETS_TABLE)?; + + if let Some(id) = table.get(unit.to_string().as_str())? { + return Ok(Some(Id::from_str(id.value()).unwrap())); + } + + Ok(None) + } + + async fn get_active_keysets(&self) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(ACTIVE_KEYSETS_TABLE)?; + + let mut active_keysets = HashMap::new(); + + for keyset in table.iter()? { + if let Ok((unit, id)) = keyset { + let unit = serde_json::from_str(unit.value())?; + let id = serde_json::from_str(id.value())?; + + active_keysets.insert(unit, id); + } + } + + Ok(active_keysets) + } + + async fn add_keyset(&self, keyset: KeySet) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(KEYSETS_TABLE)?; + table.insert( + Id::from(keyset.clone()).to_string().as_str(), + serde_json::to_string(&keyset)?.as_str(), + )?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_keyset(&self, keyset_id: &Id) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(KEYSETS_TABLE)?; + + let keyset = table.get(keyset_id.to_string().as_str())?; + + Ok(keyset.map(|k| serde_json::from_str(&k.value()).unwrap())) + } + + async fn get_keysets(&self) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(KEYSETS_TABLE)?; + + let mut keysets = Vec::new(); + + for keyset in table.iter()? { + if let Ok((_id, keyset)) = keyset { + let keyset = serde_json::from_str(keyset.value())?; + + keysets.push(keyset) + } + } + + Ok(keysets) + } + + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(MINT_QUOTES_TABLE)?; + table.insert(quote.id.as_str(), serde_json::to_string("e)?.as_str())?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_mint_quote(&self, quote_id: &str) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(MINT_QUOTES_TABLE)?; + + let quote = table.get(quote_id)?; + + Ok(quote.map(|q| serde_json::from_str(&q.value()).unwrap())) + } + + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(MINT_QUOTES_TABLE)?; + table.remove(quote_id)?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(MELT_QUOTES_TABLE)?; + table.insert(quote.id.as_str(), serde_json::to_string("e)?.as_str())?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_melt_quote(&self, quote_id: &str) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(MELT_QUOTES_TABLE)?; + + let quote = table.get(quote_id)?; + + Ok(quote.map(|q| serde_json::from_str(&q.value()).unwrap())) + } + + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(MELT_QUOTES_TABLE)?; + table.remove(quote_id)?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn add_spent_proof(&self, proof: Proof) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(SPENT_PROOFS_TABLE)?; + table.insert( + proof.secret.to_string().as_str(), + serde_json::to_string(&proof)?.as_str(), + )?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_spent_proof(&self, secret: &Secret) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(MELT_QUOTES_TABLE)?; + + let quote = table.get(secret.to_string().as_str())?; + + Ok(quote.map(|q| serde_json::from_str(&q.value()).unwrap())) + } + + async fn add_pending_proof(&self, proof: Proof) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(PENDING_PROOFS_TABLE)?; + table.insert( + proof.secret.to_string().as_str(), + serde_json::to_string(&proof)?.as_str(), + )?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_pending_proof(&self, secret: &Secret) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(MELT_QUOTES_TABLE)?; + + let quote = table.get(secret.to_string().as_str())?; + + Ok(quote.map(|q| serde_json::from_str(&q.value()).unwrap())) + } + + async fn remove_pending_proof(&self, secret: &Secret) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(PENDING_PROOFS_TABLE)?; + table.remove(secret.to_string().as_str())?; + } + write_txn.commit()?; + + Ok(()) + } +} diff --git a/crates/cashu-sdk/src/mint/mod.rs b/crates/cashu-sdk/src/mint/mod.rs index 414295d6..aa9f9887 100644 --- a/crates/cashu-sdk/src/mint/mod.rs +++ b/crates/cashu-sdk/src/mint/mod.rs @@ -20,7 +20,9 @@ use crate::Mnemonic; mod localstore; -use localstore::LocalStore; +pub use localstore::LocalStore; +#[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] +pub use localstore::RedbLocalStore; #[derive(Debug, Error)] pub enum Error { @@ -314,10 +316,7 @@ impl Mint { } for proof in swap_request.inputs { - self.localstore - .add_spent_proof(proof.secret.clone(), proof) - .await - .unwrap(); + self.localstore.add_spent_proof(proof).await.unwrap(); } let mut promises = Vec::with_capacity(swap_request.outputs.len()); @@ -484,7 +483,7 @@ impl Mint { for input in &melt_request.inputs { self.localstore - .add_spent_proof(input.secret.clone(), input.clone()) + .add_spent_proof(input.clone()) .await .unwrap(); }