diff --git a/crates/cashu-sdk/src/client/minreq_client.rs b/crates/cashu-sdk/src/client/minreq_client.rs index ceeac6de..5eeb5509 100644 --- a/crates/cashu-sdk/src/client/minreq_client.rs +++ b/crates/cashu-sdk/src/client/minreq_client.rs @@ -121,10 +121,9 @@ impl Client for HttpClient { let request = MeltQuoteBolt11Request { request, unit }; - let value = minreq::post(url) - .with_json(&request)? - .send()? - .json::()?; + let value = minreq::post(url).with_json(&request)?.send()?; + + let value = value.json::()?; let response: Result = serde_json::from_value(value.clone()); @@ -152,11 +151,9 @@ impl Client for HttpClient { outputs, }; - let value = minreq::post(url) - .with_json(&request)? - .send()? - .json::()?; + let value = minreq::post(url).with_json(&request)?.send()?; + let value = value.json::()?; let response: Result = serde_json::from_value(value.clone()); diff --git a/crates/cashu-sdk/src/mint/localstore/redb_store.rs b/crates/cashu-sdk/src/mint/localstore/redb_store.rs index f72fdd8a..63fbe15f 100644 --- a/crates/cashu-sdk/src/mint/localstore/redb_store.rs +++ b/crates/cashu-sdk/src/mint/localstore/redb_store.rs @@ -8,6 +8,7 @@ use cashu::secret::Secret; use cashu::types::{MeltQuote, MintQuote}; use redb::{Database, ReadableTable, TableDefinition}; use tokio::sync::Mutex; +use tracing::debug; use super::{Error, LocalStore}; @@ -280,16 +281,20 @@ impl LocalStore for RedbLocalStore { } write_txn.commit()?; + debug!("Added spend secret: {}", proof.secret.to_string()); + 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 table = read_txn.open_table(SPENT_PROOFS_TABLE)?; let quote = table.get(secret.to_string().as_str())?; + debug!("Checking secret: {}", secret.to_string()); + Ok(quote.map(|q| serde_json::from_str(q.value()).unwrap())) } diff --git a/crates/cashu-sdk/src/mint/mod.rs b/crates/cashu-sdk/src/mint/mod.rs index a9ef6a95..ffbc754c 100644 --- a/crates/cashu-sdk/src/mint/mod.rs +++ b/crates/cashu-sdk/src/mint/mod.rs @@ -15,7 +15,7 @@ use cashu::types::{MeltQuote, MintQuote}; use cashu::Amount; use serde::{Deserialize, Serialize}; use thiserror::Error; -use tracing::{debug, info}; +use tracing::{debug, error, info}; use crate::Mnemonic; @@ -381,6 +381,7 @@ impl Mint { // in the future it maybe possible to support multiple units but unsupported for // now if keyset_units.len().gt(&1) { + error!("Only one unit is allowed in request: {:?}", keyset_units); return Err(Error::MultipleUnits); } @@ -464,7 +465,7 @@ impl Mint { pub async fn verify_melt_request( &mut self, melt_request: &MeltBolt11Request, - ) -> Result<(), Error> { + ) -> Result { let quote = self .localstore .get_melt_quote(&melt_request.quote) @@ -486,7 +487,7 @@ impl Mint { let input_keyset_ids: HashSet = melt_request.inputs.iter().map(|p| p.keyset_id).collect(); - let mut keyset_units = Vec::with_capacity(input_keyset_ids.capacity()); + let mut keyset_units = HashSet::with_capacity(input_keyset_ids.capacity()); for id in input_keyset_ids { let keyset = self @@ -494,7 +495,7 @@ impl Mint { .get_keyset(&id) .await? .ok_or(Error::UnknownKeySet)?; - keyset_units.push(keyset.unit); + keyset_units.insert(keyset.unit); } if let Some(outputs) = &melt_request.outputs { @@ -517,13 +518,12 @@ impl Mint { if id.ne(&active_keyset_id) { return Err(Error::InactiveKeyset); } - keyset_units.push(keyset.unit); + keyset_units.insert(keyset.unit); } } // Check that all input and output proofs are the same unit - let seen_units: HashSet = HashSet::new(); - if keyset_units.iter().any(|unit| !seen_units.contains(unit)) && seen_units.len() != 1 { + if keyset_units.len().gt(&1) { return Err(Error::MultipleUnits); } @@ -535,10 +535,10 @@ impl Mint { } for proof in &melt_request.inputs { - self.verify_proof(proof).await? + self.verify_proof(proof).await?; } - Ok(()) + Ok(quote) } pub async fn process_melt_request( diff --git a/crates/cashu-sdk/src/wallet/localstore/memory.rs b/crates/cashu-sdk/src/wallet/localstore/memory.rs index feda3166..3e7d2999 100644 --- a/crates/cashu-sdk/src/wallet/localstore/memory.rs +++ b/crates/cashu-sdk/src/wallet/localstore/memory.rs @@ -17,6 +17,7 @@ pub struct MemoryLocalStore { melt_quotes: Arc>>, mint_keys: Arc>>, proofs: Arc>>>, + pending_proofs: Arc>>>, } impl MemoryLocalStore { @@ -38,6 +39,7 @@ impl MemoryLocalStore { mint_keys.into_iter().map(|k| (Id::from(&k), k)).collect(), )), proofs: Arc::new(Mutex::new(HashMap::new())), + pending_proofs: Arc::new(Mutex::new(HashMap::new())), } } } @@ -165,4 +167,42 @@ impl LocalStore for MemoryLocalStore { Ok(()) } + + async fn add_pending_proofs( + &self, + mint_url: UncheckedUrl, + proofs: Proofs, + ) -> Result<(), Error> { + let mut all_proofs = self.pending_proofs.lock().await; + + let mint_proofs = all_proofs.entry(mint_url).or_insert(HashSet::new()); + mint_proofs.extend(proofs); + + Ok(()) + } + + async fn get_pending_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { + Ok(self + .pending_proofs + .lock() + .await + .get(&mint_url) + .map(|p| p.iter().cloned().collect())) + } + + async fn remove_pending_proofs( + &self, + mint_url: UncheckedUrl, + proofs: &Proofs, + ) -> Result<(), Error> { + let mut mint_proofs = self.pending_proofs.lock().await; + + if let Some(mint_proofs) = mint_proofs.get_mut(&mint_url) { + for proof in proofs { + mint_proofs.remove(proof); + } + } + + Ok(()) + } } diff --git a/crates/cashu-sdk/src/wallet/localstore/mod.rs b/crates/cashu-sdk/src/wallet/localstore/mod.rs index 418f5265..84a20d13 100644 --- a/crates/cashu-sdk/src/wallet/localstore/mod.rs +++ b/crates/cashu-sdk/src/wallet/localstore/mod.rs @@ -74,4 +74,12 @@ pub trait LocalStore { async fn add_proofs(&self, mint_url: UncheckedUrl, proof: Proofs) -> Result<(), Error>; async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result, Error>; async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs) -> Result<(), Error>; + + async fn add_pending_proofs(&self, mint_url: UncheckedUrl, proof: Proofs) -> Result<(), Error>; + async fn get_pending_proofs(&self, mint_url: UncheckedUrl) -> Result, Error>; + async fn remove_pending_proofs( + &self, + mint_url: UncheckedUrl, + proofs: &Proofs, + ) -> Result<(), Error>; } diff --git a/crates/cashu-sdk/src/wallet/localstore/redb_store.rs b/crates/cashu-sdk/src/wallet/localstore/redb_store.rs index acbdee02..f8b6bfec 100644 --- a/crates/cashu-sdk/src/wallet/localstore/redb_store.rs +++ b/crates/cashu-sdk/src/wallet/localstore/redb_store.rs @@ -20,6 +20,8 @@ const MINT_QUOTES_TABLE: TableDefinition<&str, &str> = TableDefinition::new("min 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"); +const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> = + MultimapTableDefinition::new("pending_proofs"); #[derive(Debug, Clone)] pub struct RedbLocalStore { @@ -319,4 +321,66 @@ impl LocalStore for RedbLocalStore { Ok(()) } + + async fn add_pending_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(PENDING_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_pending_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(PENDING_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_pending_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(PENDING_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(()) + } } diff --git a/crates/cashu-sdk/src/wallet/mod.rs b/crates/cashu-sdk/src/wallet/mod.rs index 686a37d8..e9b91ba4 100644 --- a/crates/cashu-sdk/src/wallet/mod.rs +++ b/crates/cashu-sdk/src/wallet/mod.rs @@ -17,7 +17,7 @@ use cashu::url::UncheckedUrl; use cashu::{Amount, Bolt11Invoice}; use localstore::LocalStore; use thiserror::Error; -use tracing::warn; +use tracing::{debug, warn}; use crate::client::Client; use crate::utils::unix_time; @@ -508,12 +508,12 @@ impl Wallet { .await?; self.localstore - .add_proofs(mint_url.clone(), keep_proofs) + .add_pending_proofs(mint_url.clone(), proofs) .await?; - // REVIEW: send proofs are not added to the store since they should be - // used. but if they are not they will be lost. There should likely be a - // pendiing proof store + self.localstore + .add_proofs(mint_url.clone(), keep_proofs) + .await?; Ok(send_proofs) } @@ -635,7 +635,7 @@ impl Wallet { .post_melt( mint_url.clone().try_into()?, quote_id.to_string(), - proofs, + proofs.clone(), Some(blinded.blinded_messages()), ) .await?; @@ -653,11 +653,21 @@ impl Wallet { let melted = Melted { paid: true, preimage: melt_response.payment_preimage, - change: change_proofs, + change: change_proofs.clone(), }; + if let Some(change_proofs) = change_proofs { + self.localstore + .add_proofs(mint_url.clone(), change_proofs) + .await?; + } + self.localstore.remove_melt_quote("e_info.id).await?; + self.localstore + .remove_proofs(mint_url.clone(), &proofs) + .await?; + Ok(melted) }