diff --git a/crates/cashu-sdk/Cargo.toml b/crates/cashu-sdk/Cargo.toml index 61f2d6295..446af03e1 100644 --- a/crates/cashu-sdk/Cargo.toml +++ b/crates/cashu-sdk/Cargo.toml @@ -35,6 +35,7 @@ gloo = { version = "0.10.0", optional = true, features = ["net"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "time", "macros", "sync"] } minreq = { version = "2.7.0", optional = true, features = ["json-using-serde", "https"] } +redb = "1.4.0" [target.'cfg(target_arch = "wasm32")'.dependencies] tokio = { workspace = true, features = ["rt", "macros", "sync", "time"] } diff --git a/crates/cashu-sdk/src/lib.rs b/crates/cashu-sdk/src/lib.rs index e771cf163..e8f809484 100644 --- a/crates/cashu-sdk/src/lib.rs +++ b/crates/cashu-sdk/src/lib.rs @@ -20,6 +20,8 @@ pub mod wallet; pub use bip39::Mnemonic; pub use cashu::{self, *}; +#[cfg(not(target_arch = "wasm32"))] +pub use localstore::redb_store::RedbLocalStore; #[cfg(feature = "blocking")] static RUNTIME: Lazy = Lazy::new(|| Runtime::new().expect("Can't start Tokio runtime")); diff --git a/crates/cashu-sdk/src/localstore/mod.rs b/crates/cashu-sdk/src/localstore/mod.rs index 71b2825eb..8263fd31e 100644 --- a/crates/cashu-sdk/src/localstore/mod.rs +++ b/crates/cashu-sdk/src/localstore/mod.rs @@ -1,4 +1,8 @@ mod memory; + +#[cfg(not(target_arch = "wasm32"))] +pub mod redb_store; + use async_trait::async_trait; use cashu::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs}; use cashu::types::{MeltQuote, MintQuote}; @@ -6,7 +10,22 @@ use cashu::url::UncheckedUrl; use thiserror::Error; #[derive(Debug, Error)] -pub enum Error {} +pub enum Error { + #[error("`{0}`")] + Redb(#[from] redb::Error), + #[error("`{0}`")] + Database(#[from] redb::DatabaseError), + #[error("`{0}`")] + Transaction(#[from] redb::TransactionError), + #[error("`{0}`")] + Commit(#[from] redb::CommitError), + #[error("`{0}`")] + Table(#[from] redb::TableError), + #[error("`{0}`")] + Storage(#[from] redb::StorageError), + #[error("`{0}`")] + Serde(#[from] serde_json::Error), +} #[async_trait(?Send)] pub trait LocalStore { diff --git a/crates/cashu-sdk/src/localstore/redb_store.rs b/crates/cashu-sdk/src/localstore/redb_store.rs new file mode 100644 index 000000000..290cb8505 --- /dev/null +++ b/crates/cashu-sdk/src/localstore/redb_store.rs @@ -0,0 +1,301 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use cashu::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs}; +use cashu::types::{MeltQuote, MintQuote}; +use cashu::url::UncheckedUrl; +use redb::{ + Database, MultimapTableDefinition, ReadableMultimapTable, ReadableTable, TableDefinition, +}; +use tokio::sync::Mutex; + +use super::{Error, LocalStore}; + +const MINTS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mints_table"); +const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &str> = + MultimapTableDefinition::new("mint_keysets"); +const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_quotes"); +const MELT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("melt_quotes"); +const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_keys"); +const PROOFS_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("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(MINTS_TABLE)?; + let _ = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?; + let _ = write_txn.open_table(MINT_QUOTES_TABLE)?; + let _ = write_txn.open_table(MELT_QUOTES_TABLE)?; + let _ = write_txn.open_table(MINT_KEYS_TABLE)?; + let _ = write_txn.open_multimap_table(PROOFS_TABLE)?; + } + write_txn.commit()?; + + Ok(Self { + db: Arc::new(Mutex::new(db)), + }) + } +} + +#[async_trait(?Send)] +impl LocalStore for RedbLocalStore { + async fn add_mint( + &self, + mint_url: UncheckedUrl, + mint_info: Option, + ) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(MINTS_TABLE)?; + table.insert( + mint_url.to_string().as_str(), + serde_json::to_string(&mint_info)?.as_str(), + )?; + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_mint(&self, mint_url: UncheckedUrl) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(MINTS_TABLE)?; + + if let Some(mint_info) = table.get(mint_url.to_string().as_str())? { + return Ok(serde_json::from_str(mint_info.value())?); + } + + Ok(None) + } + + async fn add_mint_keysets( + &self, + mint_url: UncheckedUrl, + keysets: Vec, + ) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_multimap_table(MINT_KEYSETS_TABLE)?; + + for keyset in keysets { + table.insert( + mint_url.to_string().as_str(), + serde_json::to_string(&keyset)?.as_str(), + )?; + } + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_mint_keysets( + &self, + mint_url: UncheckedUrl, + ) -> Result>, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_multimap_table(MINT_KEYSETS_TABLE)?; + + let keysets = table + .get(mint_url.to_string().as_str())? + .flatten() + .flat_map(|k| serde_json::from_str(k.value())) + .collect(); + + 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)?; + + if let Some(mint_info) = table.get(quote_id)? { + return Ok(serde_json::from_str(mint_info.value())?); + } + + Ok(None) + } + + 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)?; + + if let Some(mint_info) = table.get(quote_id)? { + return Ok(serde_json::from_str(mint_info.value())?); + } + + Ok(None) + } + + 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_keys(&self, keys: Keys) -> Result<(), Error> { + let db = self.db.lock().await; + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(MINT_KEYS_TABLE)?; + table.insert( + Id::from(&keys).to_string().as_str(), + serde_json::to_string(&keys)?.as_str(), + )?; + } + + write_txn.commit()?; + + Ok(()) + } + + async fn get_keys(&self, id: &Id) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_table(MINT_KEYS_TABLE)?; + + if let Some(mint_info) = table.get(id.to_string().as_str())? { + return Ok(serde_json::from_str(mint_info.value())?); + } + + Ok(None) + } + + async fn remove_keys(&self, id: &Id) -> Result<(), Error> { + let db = self.db.lock().await; + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_table(MINT_KEYS_TABLE)?; + + table.remove(id.to_string().as_str())?; + } + + write_txn.commit()?; + + Ok(()) + } + + async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_multimap_table(PROOFS_TABLE)?; + + for proof in proofs { + table.insert( + mint_url.to_string().as_str(), + serde_json::to_string(&proof)?.as_str(), + )?; + } + } + write_txn.commit()?; + + Ok(()) + } + + async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { + let db = self.db.lock().await; + let read_txn = db.begin_read()?; + let table = read_txn.open_multimap_table(PROOFS_TABLE)?; + + let proofs = table + .get(mint_url.to_string().as_str())? + .flatten() + .flat_map(|k| serde_json::from_str(k.value())) + .collect(); + + Ok(proofs) + } + + async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs) -> Result<(), Error> { + let db = self.db.lock().await; + + let write_txn = db.begin_write()?; + + { + let mut table = write_txn.open_multimap_table(PROOFS_TABLE)?; + + for proof in proofs { + table.remove( + mint_url.to_string().as_str(), + serde_json::to_string(&proof)?.as_str(), + )?; + } + } + write_txn.commit()?; + + Ok(()) + } +}