diff --git a/crates/cashu-sdk/src/lib.rs b/crates/cashu-sdk/src/lib.rs index 79083942..e771cf16 100644 --- a/crates/cashu-sdk/src/lib.rs +++ b/crates/cashu-sdk/src/lib.rs @@ -11,6 +11,7 @@ use tokio::runtime::Runtime; #[cfg(feature = "wallet")] pub mod client; +mod localstore; #[cfg(feature = "mint")] pub mod mint; pub mod utils; diff --git a/crates/cashu-sdk/src/localstore/memory.rs b/crates/cashu-sdk/src/localstore/memory.rs new file mode 100644 index 00000000..f2bc14e6 --- /dev/null +++ b/crates/cashu-sdk/src/localstore/memory.rs @@ -0,0 +1,110 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use async_trait::async_trait; +use cashu::nuts::{Id, KeySetInfo, Keys, MintInfo}; +use cashu::types::{MeltQuote, MintQuote}; +use cashu::url::UncheckedUrl; +use tokio::sync::Mutex; + +use super::{Error, LocalStore}; + +#[derive(Default, Debug, Clone)] +pub struct MemoryLocalStore { + mints: Arc>>>, + mint_keysets: Arc>>>, + mint_quotes: Arc>>, + melt_quotes: Arc>>, + mint_keys: Arc>>, +} + +#[async_trait(?Send)] +impl LocalStore for MemoryLocalStore { + async fn add_mint( + &self, + mint_url: UncheckedUrl, + mint_info: Option, + ) -> Result<(), Error> { + self.mints.lock().await.insert(mint_url, mint_info); + Ok(()) + } + + async fn get_mint(&self, mint_url: UncheckedUrl) -> Result, Error> { + Ok(self.mints.lock().await.get(&mint_url).cloned().flatten()) + } + + async fn add_mint_keysets( + &self, + mint_url: UncheckedUrl, + keysets: Vec, + ) -> Result<(), Error> { + let mut current_keysets = self.mint_keysets.lock().await; + + let current_keysets = current_keysets.entry(mint_url).or_insert(HashSet::new()); + current_keysets.extend(keysets); + + Ok(()) + } + + async fn get_mint_keysets( + &self, + mint_url: UncheckedUrl, + ) -> Result>, Error> { + Ok(self + .mint_keysets + .lock() + .await + .get(&mint_url) + .map(|ks| ks.iter().cloned().collect())) + } + + 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_keys(&self, keys: Keys) -> Result<(), Error> { + self.mint_keys.lock().await.insert(Id::from(&keys), keys); + Ok(()) + } + + async fn get_keys(&self, id: &Id) -> Result, Error> { + Ok(self.mint_keys.lock().await.get(id).cloned()) + } + + async fn remove_keys(&self, id: &Id) -> Result<(), Error> { + self.mint_keys.lock().await.remove(id); + Ok(()) + } +} diff --git a/crates/cashu-sdk/src/localstore/mod.rs b/crates/cashu-sdk/src/localstore/mod.rs new file mode 100644 index 00000000..dd60a7e8 --- /dev/null +++ b/crates/cashu-sdk/src/localstore/mod.rs @@ -0,0 +1,41 @@ +mod memory; +use async_trait::async_trait; +use cashu::nuts::{Id, KeySetInfo, Keys, MintInfo}; +use cashu::types::{MeltQuote, MintQuote}; +use cashu::url::UncheckedUrl; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error {} + +#[async_trait(?Send)] +pub trait LocalStore { + async fn add_mint( + &self, + mint_url: UncheckedUrl, + mint_info: Option, + ) -> Result<(), Error>; + async fn get_mint(&self, mint_url: UncheckedUrl) -> Result, Error>; + + async fn add_mint_keysets( + &self, + mint_url: UncheckedUrl, + keysets: Vec, + ) -> Result<(), Error>; + async fn get_mint_keysets( + &self, + mint_url: UncheckedUrl, + ) -> Result>, Error>; + + 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_keys(&self, keys: Keys) -> Result<(), Error>; + async fn get_keys(&self, id: &Id) -> Result, Error>; + async fn remove_keys(&self, id: &Id) -> Result<(), Error>; +} diff --git a/crates/cashu-sdk/src/wallet.rs b/crates/cashu-sdk/src/wallet.rs index 481aae97..792aeb26 100644 --- a/crates/cashu-sdk/src/wallet.rs +++ b/crates/cashu-sdk/src/wallet.rs @@ -1,5 +1,5 @@ //! Cashu Wallet -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::str::FromStr; use bip39::Mnemonic; @@ -7,8 +7,8 @@ use cashu::dhke::{construct_proofs, unblind_message}; #[cfg(feature = "nut07")] use cashu::nuts::nut00::mint; use cashu::nuts::{ - BlindedSignature, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, PreSwap, Proof, - Proofs, SwapRequest, Token, + BlindedSignature, CurrencyUnit, Id, Keys, PreMintSecrets, PreSwap, Proof, Proofs, SwapRequest, + Token, }; #[cfg(feature = "nut07")] use cashu::types::ProofsStatus; @@ -20,6 +20,7 @@ use thiserror::Error; use tracing::warn; use crate::client::Client; +use crate::localstore::LocalStore; use crate::utils::unix_time; #[derive(Debug, Error)] @@ -39,6 +40,8 @@ pub enum Error { #[error("Quote Unknown")] QuoteUnknown, #[error("`{0}`")] + LocalStore(#[from] super::localstore::Error), + #[error("`{0}`")] Custom(String), } @@ -49,36 +52,37 @@ pub struct BackupInfo { } #[derive(Clone, Debug)] -pub struct Wallet { +pub struct Wallet { backup_info: Option, pub client: C, - pub mints: HashMap>, - pub mint_keysets: HashMap>, - pub mint_quotes: HashMap, - pub melt_quotes: HashMap, - pub mint_keys: HashMap, - pub balance: Amount, + pub localstore: L, } -impl Wallet { - pub fn new( +impl Wallet { + pub async fn new( client: C, - mints: HashMap>, - mint_keysets: HashMap>, + localstore: L, mint_quotes: Vec, melt_quotes: Vec, backup_info: Option, - mint_keys: HashMap, + mint_keys: Vec, ) -> Self { + for quote in mint_quotes { + localstore.add_mint_quote(quote).await.ok(); + } + + for quote in melt_quotes { + localstore.add_melt_quote(quote).await.ok(); + } + + for keys in mint_keys { + localstore.add_keys(keys).await.ok(); + } + Self { backup_info, client, - mints, - mint_keysets, - mint_keys, - mint_quotes: mint_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(), - melt_quotes: melt_quotes.into_iter().map(|q| (q.id.clone(), q)).collect(), - balance: Amount::ZERO, + localstore, } } @@ -168,8 +172,7 @@ impl Wallet { expiry: quote_res.expiry, }; - self.mint_quotes - .insert(quote_res.quote.clone(), quote.clone()); + self.localstore.add_mint_quote(quote.clone()).await?; Ok(quote) } @@ -179,7 +182,7 @@ impl Wallet { mint_url: &UncheckedUrl, unit: &CurrencyUnit, ) -> Result, Error> { - if let Some(keysets) = self.mint_keysets.get(mint_url) { + if let Some(keysets) = self.localstore.get_mint_keysets(mint_url.clone()).await? { for keyset in keysets { if keyset.unit.eq(unit) && keyset.active { return Ok(Some(keyset.id)); @@ -188,8 +191,9 @@ impl Wallet { } else { let keysets = self.client.get_mint_keysets(mint_url.try_into()?).await?; - self.mint_keysets - .insert(mint_url.clone(), keysets.keysets.into_iter().collect()); + self.localstore + .add_mint_keysets(mint_url.clone(), keysets.keysets.into_iter().collect()) + .await?; } Ok(None) @@ -204,7 +208,7 @@ impl Wallet { let mut keys = None; - if let Some(k) = self.mint_keys.get(&active_keyset_id) { + if let Some(k) = self.localstore.get_keys(&active_keyset_id).await? { keys = Some(k.clone()) } else { let keysets = self.client.get_mint_keys(mint_url.try_into()?).await?; @@ -213,7 +217,7 @@ impl Wallet { if keyset.id.eq(&active_keyset_id) { keys = Some(keyset.keys.clone()) } - self.mint_keys.insert(keyset.id, keyset.keys); + self.localstore.add_keys(keyset.keys).await?; } } @@ -222,7 +226,7 @@ impl Wallet { /// Mint pub async fn mint(&mut self, mint_url: UncheckedUrl, quote_id: &str) -> Result { - let quote_info = self.mint_quotes.get(quote_id); + let quote_info = self.localstore.get_mint_quote(quote_id).await?; let quote_info = if let Some(quote) = quote_info { if quote.expiry.le(&unix_time()) { @@ -258,16 +262,16 @@ impl Wallet { ) .await?; - let keys = self.mint_keys.get(&active_keyset_id).unwrap(); + let keys = self.localstore.get_keys(&active_keyset_id).await?.unwrap(); let proofs = construct_proofs( mint_res.signatures, premint_secrets.rs(), premint_secrets.secrets(), - keys, + &keys, )?; - self.mint_quotes.remove("e_info.id); + self.localstore.remove_mint_quote("e_info.id).await?; Ok(proofs) } @@ -295,7 +299,7 @@ impl Wallet { // TODO: if none fetch keyset for mint - let keys = self.mint_keys.get(&active_keyset_id.unwrap()).cloned(); + let keys = self.localstore.get_keys(&active_keyset_id.unwrap()).await?; // Sum amount of all proofs let amount: Amount = token.proofs.iter().map(|p| p.amount).sum(); @@ -359,7 +363,7 @@ impl Wallet { }) } - pub fn process_split_response( + pub async fn process_split_response( &self, blinded_messages: PreMintSecrets, promises: Vec, @@ -368,8 +372,9 @@ impl Wallet { for (promise, premint) in promises.iter().zip(blinded_messages) { let a = self - .mint_keys - .get(&promise.keyset_id) + .localstore + .get_keys(&promise.keyset_id) + .await? .unwrap() .amount_key(promise.amount) .unwrap() @@ -475,7 +480,7 @@ impl Wallet { expiry: quote_res.expiry, }; - self.melt_quotes.insert(quote.id.clone(), quote.clone()); + self.localstore.add_melt_quote(quote.clone()).await?; Ok(quote) } @@ -487,7 +492,7 @@ impl Wallet { quote_id: &str, proofs: Proofs, ) -> Result { - let quote_info = self.melt_quotes.get(quote_id); + let quote_info = self.localstore.get_melt_quote(quote_id).await?; let quote_info = if let Some(quote) = quote_info { if quote.expiry.le(&unix_time()) { @@ -532,6 +537,8 @@ impl Wallet { change: change_proofs, }; + self.localstore.remove_melt_quote("e_info.id).await?; + Ok(melted) }