diff --git a/crates/cashu-sdk/src/mint/localstore/memory.rs b/crates/cashu-sdk/src/mint/localstore/memory.rs new file mode 100644 index 00000000..b4fad0e7 --- /dev/null +++ b/crates/cashu-sdk/src/mint/localstore/memory.rs @@ -0,0 +1,91 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use cashu::nuts::nut02::mint::KeySet; +use cashu::nuts::{Id, Proof}; +use cashu::secret::Secret; +use cashu::types::{MeltQuote, MintQuote}; +use tokio::sync::Mutex; + +use super::{Error, LocalStore}; + +#[derive(Default, Debug, Clone)] +pub struct MemoryLocalStore { + keysets: Arc>>, + mint_quotes: Arc>>, + melt_quotes: Arc>>, + pending_proofs: Arc>>, + spent_proofs: Arc>>, +} + +#[async_trait(?Send)] +impl LocalStore for MemoryLocalStore { + async fn add_keyset(&self, keyset: KeySet) -> Result<(), Error> { + self.keysets.lock().await.insert(keyset.id, keyset); + Ok(()) + } + + async fn get_keyset(&self, keyset_id: &Id) -> Result, Error> { + Ok(self.keysets.lock().await.get(keyset_id).cloned()) + } + + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error> { + self.mint_quotes + .lock() + .await + .insert(quote.id.clone(), quote); + Ok(()) + } + + async fn get_mint_quote(&self, quote_id: &str) -> Result, Error> { + Ok(self.mint_quotes.lock().await.get(quote_id).cloned()) + } + + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> { + self.mint_quotes.lock().await.remove(quote_id); + + Ok(()) + } + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> { + self.melt_quotes + .lock() + .await + .insert(quote.id.clone(), quote); + Ok(()) + } + + async fn get_melt_quote(&self, quote_id: &str) -> Result, Error> { + Ok(self.melt_quotes.lock().await.get(quote_id).cloned()) + } + + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> { + self.melt_quotes.lock().await.remove(quote_id); + + Ok(()) + } + + async fn add_spent_proof(&self, secret: Secret, proof: Proof) -> Result<(), Error> { + self.spent_proofs.lock().await.insert(secret, proof); + Ok(()) + } + + async fn get_spent_proof(&self, secret: &Secret) -> Result, Error> { + 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); + Ok(()) + } + + async fn get_pending_proof(&self, secret: &Secret) -> Result, Error> { + Ok(self.pending_proofs.lock().await.get(secret).cloned()) + } + + async fn remove_pending_proof(&self, secret: &Secret) -> Result<(), Error> { + self.pending_proofs.lock().await.remove(secret); + Ok(()) + } +} diff --git a/crates/cashu-sdk/src/mint/localstore/mod.rs b/crates/cashu-sdk/src/mint/localstore/mod.rs new file mode 100644 index 00000000..f5295640 --- /dev/null +++ b/crates/cashu-sdk/src/mint/localstore/mod.rs @@ -0,0 +1,54 @@ +mod memory; + +use async_trait::async_trait; +use cashu::nuts::nut02::mint::KeySet; +use cashu::nuts::{Id, Proof}; +use cashu::secret::Secret; +use cashu::types::{MeltQuote, MintQuote}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] + #[error("`{0}`")] + Redb(#[from] redb::Error), + #[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] + #[error("`{0}`")] + Database(#[from] redb::DatabaseError), + #[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] + #[error("`{0}`")] + Transaction(#[from] redb::TransactionError), + #[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] + #[error("`{0}`")] + Commit(#[from] redb::CommitError), + #[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] + #[error("`{0}`")] + Table(#[from] redb::TableError), + #[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] + #[error("`{0}`")] + Storage(#[from] redb::StorageError), + #[cfg(all(not(target_arch = "wasm32"), feature = "redb"))] + #[error("`{0}`")] + Serde(#[from] serde_json::Error), +} + +#[async_trait(?Send)] +pub trait LocalStore { + async fn add_mint_quote(&self, quote: MintQuote) -> Result<(), Error>; + async fn get_mint_quote(&self, quote_id: &str) -> Result, Error>; + async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error>; + + async fn add_melt_quote(&self, quote: MeltQuote) -> Result<(), Error>; + async fn get_melt_quote(&self, quote_id: &str) -> Result, Error>; + async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error>; + + async fn add_keyset(&self, keyset: KeySet) -> Result<(), Error>; + async fn get_keyset(&self, id: &Id) -> Result, Error>; + + async fn add_spent_proof(&self, secret: Secret, 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 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.rs b/crates/cashu-sdk/src/mint/mod.rs similarity index 69% rename from crates/cashu-sdk/src/mint.rs rename to crates/cashu-sdk/src/mint/mod.rs index 2e95c320..4d52be7d 100644 --- a/crates/cashu-sdk/src/mint.rs +++ b/crates/cashu-sdk/src/mint/mod.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap, HashSet}; use cashu::dhke::{sign_message, verify_message}; -pub use cashu::error::mint::Error; use cashu::nuts::{ BlindedMessage, BlindedSignature, MeltBolt11Request, MeltBolt11Response, Proof, SwapRequest, SwapResponse, *, @@ -11,40 +10,61 @@ use cashu::nuts::{CheckSpendableRequest, CheckSpendableResponse}; use cashu::secret::Secret; use cashu::Amount; use serde::{Deserialize, Serialize}; +use thiserror::Error; use tracing::{debug, info}; -use crate::types::MeltQuote; use crate::utils::unix_time; use crate::Mnemonic; -pub struct Mint { +mod localstore; + +use localstore::LocalStore; + +#[derive(Debug, Error)] +pub enum Error { + /// Unknown Keyset + #[error("Unknown Keyset")] + UnknownKeySet, + /// Inactive Keyset + #[error("Inactive Keyset")] + InactiveKeyset, + #[error("No key for amount")] + AmountKey, + #[error("Amount")] + Amount, + #[error("Duplicate proofs")] + DuplicateProofs, + #[error("Token Spent")] + TokenSpent, + #[error("`{0}`")] + Custom(String), + #[error("`{0}`")] + Cashu(#[from] cashu::error::mint::Error), + #[error("`{0}`")] + Localstore(#[from] localstore::Error), +} + +pub struct Mint { // pub pubkey: PublicKey - pub keysets: HashMap, pub keysets_info: HashMap, // pub pubkey: PublicKey, mnemonic: Mnemonic, - pub spent_secrets: HashSet, - pub pending_secrets: HashSet, pub fee_reserve: FeeReserve, - pub melt_quotes: HashMap, + localstore: L, } -impl Mint { - pub fn new( +impl Mint { + pub async fn new( + localstore: L, mnemonic: Mnemonic, keysets_info: HashSet, - spent_secrets: HashSet, - melt_quotes: Vec, min_fee_reserve: Amount, percent_fee_reserve: f32, - ) -> Self { - let mut keysets = HashMap::default(); + ) -> Result { let mut info = HashMap::default(); let mut active_units: HashSet = HashSet::default(); - let melt_quotes = melt_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(); - // Check that there is only one active keyset per unit for keyset_info in keysets_info { if keyset_info.active && !active_units.insert(keyset_info.unit.clone()) { @@ -59,38 +79,35 @@ impl Mint { keyset_info.max_order, ); - keysets.insert(keyset.id, keyset); - info.insert(keyset_info.id, keyset_info); + + localstore.add_keyset(keyset).await?; } - Self { + Ok(Self { + localstore, mnemonic, - keysets, - melt_quotes, keysets_info: info, - spent_secrets, - pending_secrets: HashSet::new(), fee_reserve: FeeReserve { min_fee_reserve, percent_fee_reserve, }, - } + }) } /// Retrieve the public keys of the active keyset for distribution to /// wallet clients - pub fn keyset_pubkeys(&self, keyset_id: &Id) -> Option { - let keyset = match self.keysets.get(keyset_id) { + pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result, Error> { + let keyset = match self.localstore.get_keyset(keyset_id).await? { Some(keyset) => keyset.clone(), None => { - return None; + return Ok(None); } }; - Some(KeysResponse { + Ok(Some(KeysResponse { keysets: vec![keyset.into()], - }) + })) } /// Return a list of all supported keysets @@ -104,13 +121,22 @@ impl Mint { KeysetResponse { keysets } } - pub fn keyset(&self, id: &Id) -> Option { - self.keysets.get(id).map(|ks| ks.clone().into()) + pub async fn keyset(&self, id: &Id) -> Result, Error> { + Ok(self + .localstore + .get_keyset(id) + .await? + .map(|ks| ks.clone().into())) } /// Add current keyset to inactive keysets /// Generate new keyset - pub fn rotate_keyset(&mut self, unit: CurrencyUnit, derivation_path: &str, max_order: u8) { + pub async fn rotate_keyset( + &mut self, + unit: CurrencyUnit, + derivation_path: &str, + max_order: u8, + ) -> Result<(), Error> { let new_keyset = MintKeySet::generate( &self.mnemonic.to_seed_normalized(""), unit.clone(), @@ -118,7 +144,7 @@ impl Mint { max_order, ); - self.keysets.insert(new_keyset.id, new_keyset.clone()); + self.localstore.add_keyset(new_keyset.clone()).await?; for mint_keyset_info in self.keysets_info.values_mut() { if mint_keyset_info.active && mint_keyset_info.unit.eq(&unit) { @@ -137,16 +163,17 @@ impl Mint { }; self.keysets_info.insert(new_keyset.id, mint_keyset_info); + Ok(()) } - pub fn process_mint_request( + pub async fn process_mint_request( &mut self, mint_request: nut04::MintBolt11Request, ) -> Result { let mut blind_signatures = Vec::with_capacity(mint_request.outputs.len()); for blinded_message in mint_request.outputs { - blind_signatures.push(self.blind_sign(&blinded_message)?); + blind_signatures.push(self.blind_sign(&blinded_message).await?); } Ok(nut04::MintBolt11Response { @@ -154,14 +181,21 @@ impl Mint { }) } - fn blind_sign(&self, blinded_message: &BlindedMessage) -> Result { + async fn blind_sign( + &self, + blinded_message: &BlindedMessage, + ) -> Result { let BlindedMessage { amount, b, keyset_id, } = blinded_message; - let keyset = self.keysets.get(keyset_id).ok_or(Error::UnknownKeySet)?; + let keyset = self + .localstore + .get_keyset(keyset_id) + .await? + .ok_or(Error::UnknownKeySet)?; // Check that the keyset is active and should be used to sign if !self @@ -187,7 +221,7 @@ impl Mint { }) } - pub fn process_swap_request( + pub async fn process_swap_request( &mut self, swap_request: SwapRequest, ) -> Result { @@ -213,30 +247,41 @@ impl Mint { } for proof in &swap_request.inputs { - self.verify_proof(proof)? + self.verify_proof(proof).await? } - for secret in secrets { - self.spent_secrets.insert(secret); + for (secret, proof) in secrets.iter().zip(swap_request.inputs) { + self.localstore + .add_spent_proof(secret.clone(), proof) + .await + .unwrap(); } - let promises: Vec = swap_request - .outputs - .iter() - .map(|b| self.blind_sign(b).unwrap()) - .collect(); + let mut promises = Vec::with_capacity(swap_request.outputs.len()); + + for output in swap_request.outputs { + let promise = self.blind_sign(&output).await?; + promises.push(promise); + } Ok(SwapResponse::new(promises)) } - fn verify_proof(&self, proof: &Proof) -> Result<(), Error> { - if self.spent_secrets.contains(&proof.secret) { + async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> { + if self + .localstore + .get_spent_proof(&proof.secret) + .await + .unwrap() + .is_some() + { return Err(Error::TokenSpent); } let keyset = self - .keysets - .get(&proof.keyset_id) + .localstore + .get_keyset(&proof.keyset_id) + .await? .ok_or(Error::UnknownKeySet)?; let Some(keypair) = keyset.keys.0.get(&proof.amount) else { @@ -253,7 +298,7 @@ impl Mint { } #[cfg(feature = "nut07")] - pub fn check_spendable( + pub async fn check_spendable( &self, check_spendable: &CheckSpendableRequest, ) -> Result { @@ -261,15 +306,40 @@ impl Mint { let mut pending = Vec::with_capacity(check_spendable.proofs.len()); for proof in &check_spendable.proofs { - spendable.push(!self.spent_secrets.contains(&proof.secret)); - pending.push(self.pending_secrets.contains(&proof.secret)); + spendable.push( + self.localstore + .get_spent_proof(&proof.secret) + .await + .unwrap() + .is_none(), + ); + pending.push( + self.localstore + .get_pending_proof(&proof.secret) + .await + .unwrap() + .is_some(), + ); } Ok(CheckSpendableResponse { spendable, pending }) } - pub fn verify_melt_request(&mut self, melt_request: &MeltBolt11Request) -> Result<(), Error> { - let quote = self.melt_quotes.get(&melt_request.quote).unwrap(); + pub async fn verify_melt_request( + &mut self, + melt_request: &MeltBolt11Request, + ) -> Result<(), Error> { + let quote = self + .localstore + .get_melt_quote(&melt_request.quote) + .await + .unwrap(); + let quote = if let Some(quote) = quote { + quote + } else { + return Err(Error::Custom("Unknown Quote".to_string())); + }; + let proofs_total = melt_request.proofs_amount(); let required_total = quote.amount + quote.fee_reserve; @@ -290,23 +360,25 @@ impl Mint { } for proof in &melt_request.inputs { - self.verify_proof(proof)? + self.verify_proof(proof).await? } Ok(()) } - pub fn process_melt_request( + pub async fn process_melt_request( &mut self, melt_request: &MeltBolt11Request, preimage: &str, total_spent: Amount, ) -> Result { - self.verify_melt_request(melt_request)?; + self.verify_melt_request(melt_request).await?; - let secrets = Vec::with_capacity(melt_request.inputs.len()); - for secret in secrets { - self.spent_secrets.insert(secret); + for input in &melt_request.inputs { + self.localstore + .add_spent_proof(input.secret.clone(), input.clone()) + .await + .unwrap(); } let mut change = None; @@ -333,7 +405,7 @@ impl Mint { let mut blinded_message = blinded_message; blinded_message.amount = *amount; - let signature = self.blind_sign(&blinded_message)?; + let signature = self.blind_sign(&blinded_message).await?; change_sigs.push(signature) }