Ver Fonte

fix(mint): redb localstore checking wrong table for spent proofs
feat(wallet): store pending proofs

thesimplekid há 1 ano atrás
pai
commit
28cc181982

+ 5 - 8
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::<Value>()?;
+        let value = minreq::post(url).with_json(&request)?.send()?;
+
+        let value = value.json::<Value>()?;
 
         let response: Result<MeltQuoteBolt11Response, serde_json::Error> =
             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::<Value>()?;
+        let value = minreq::post(url).with_json(&request)?.send()?;
 
+        let value = value.json::<Value>()?;
         let response: Result<MeltBolt11Response, serde_json::Error> =
             serde_json::from_value(value.clone());
 

+ 6 - 1
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<Option<Proof>, 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()))
     }
 

+ 9 - 9
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<MeltQuote, Error> {
         let quote = self
             .localstore
             .get_melt_quote(&melt_request.quote)
@@ -486,7 +487,7 @@ impl Mint {
         let input_keyset_ids: HashSet<Id> =
             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<CurrencyUnit> = 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(

+ 40 - 0
crates/cashu-sdk/src/wallet/localstore/memory.rs

@@ -17,6 +17,7 @@ pub struct MemoryLocalStore {
     melt_quotes: Arc<Mutex<HashMap<String, MeltQuote>>>,
     mint_keys: Arc<Mutex<HashMap<Id, Keys>>>,
     proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
+    pending_proofs: Arc<Mutex<HashMap<UncheckedUrl, HashSet<Proof>>>>,
 }
 
 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<Option<Proofs>, 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(())
+    }
 }

+ 8 - 0
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<Option<Proofs>, 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<Option<Proofs>, Error>;
+    async fn remove_pending_proofs(
+        &self,
+        mint_url: UncheckedUrl,
+        proofs: &Proofs,
+    ) -> Result<(), Error>;
 }

+ 64 - 0
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<Option<Proofs>, 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(())
+    }
 }

+ 17 - 7
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<C: Client, L: LocalStore> Wallet<C, L> {
             .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<C: Client, L: LocalStore> Wallet<C, L> {
             .post_melt(
                 mint_url.clone().try_into()?,
                 quote_id.to_string(),
-                proofs,
+                proofs.clone(),
                 Some(blinded.blinded_messages()),
             )
             .await?;
@@ -653,11 +653,21 @@ impl<C: Client, L: LocalStore> Wallet<C, L> {
         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(&quote_info.id).await?;
 
+        self.localstore
+            .remove_proofs(mint_url.clone(), &proofs)
+            .await?;
+
         Ok(melted)
     }