Forráskód Böngészése

refactor: database use one proofs table

thesimplekid 9 hónapja
szülő
commit
10a38247ce

+ 3 - 0
bindings/cdk-js/src/nuts/nut07.rs

@@ -8,6 +8,7 @@ pub enum JsState {
     Spent,
     Unspent,
     Pending,
+    Reserved,
 }
 
 impl From<State> for JsState {
@@ -16,6 +17,7 @@ impl From<State> for JsState {
             State::Spent => JsState::Spent,
             State::Unspent => JsState::Unspent,
             State::Pending => JsState::Pending,
+            State::Reserved => JsState::Reserved,
         }
     }
 }
@@ -26,6 +28,7 @@ impl From<JsState> for State {
             JsState::Spent => State::Spent,
             JsState::Unspent => State::Unspent,
             JsState::Pending => State::Pending,
+            JsState::Reserved => State::Reserved,
         }
     }
 }

+ 1 - 1
crates/cdk-redb/Cargo.toml

@@ -17,7 +17,7 @@ nostr = ["cdk/nostr"]
 [dependencies]
 async-trait.workspace = true
 cdk = { workspace = true, default-features = false }
-redb = "2.0.0"
+redb = "2.1.0"
 tokio.workspace = true
 thiserror.workspace = true
 tracing.workspace = true

+ 3 - 0
crates/cdk-redb/src/error.rs

@@ -43,6 +43,9 @@ pub enum Error {
     /// Unknown Mint Info
     #[error("Unknown Mint Info")]
     UnknownMintInfo,
+    /// Unknown Proof Y
+    #[error("Unknown Proof Y")]
+    UnknownY,
 }
 
 impl From<Error> for cdk::cdk_database::Error {

+ 75 - 98
crates/cdk-redb/src/wallet.rs

@@ -5,10 +5,8 @@ use std::sync::Arc;
 use async_trait::async_trait;
 use cdk::cdk_database;
 use cdk::cdk_database::WalletDatabase;
-#[cfg(feature = "nostr")]
-use cdk::nuts::PublicKey;
-use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs};
-use cdk::types::{MeltQuote, MintQuote};
+use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, State};
+use cdk::types::{MeltQuote, MintQuote, ProofInfo};
 use cdk::url::UncheckedUrl;
 use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition};
 use tokio::sync::Mutex;
@@ -22,9 +20,8 @@ const MINT_KEYSETS_TABLE: MultimapTableDefinition<&str, &str> =
 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");
-const PENDING_PROOFS_TABLE: MultimapTableDefinition<&str, &str> =
-    MultimapTableDefinition::new("pending_proofs");
+// <Y, (Proof, Status, Mint url)>
+const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs");
 const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
 const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
 #[cfg(feature = "nostr")]
@@ -66,7 +63,7 @@ impl RedbWalletDatabase {
                     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)?;
+                    let _ = write_txn.open_table(PROOFS_TABLE)?;
                     let _ = write_txn.open_table(KEYSET_COUNTER)?;
                     #[cfg(feature = "nostr")]
                     let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
@@ -373,22 +370,22 @@ impl WalletDatabase for RedbWalletDatabase {
         Ok(())
     }
 
-    #[instrument(skip(self, proofs))]
-    async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Self::Err> {
+    #[instrument(skip(self, proofs_info))]
+    async fn add_proofs(&self, proofs_info: Vec<ProofInfo>) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
 
         let write_txn = db.begin_write().map_err(Error::from)?;
 
         {
-            let mut table = write_txn
-                .open_multimap_table(PROOFS_TABLE)
-                .map_err(Error::from)?;
+            let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
 
-            for proof in proofs {
+            for proof_info in proofs_info.iter() {
                 table
                     .insert(
-                        mint_url.to_string().as_str(),
-                        serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
+                        proof_info.y.to_bytes().as_slice(),
+                        serde_json::to_string(&proof_info)
+                            .map_err(Error::from)?
+                            .as_str(),
                     )
                     .map_err(Error::from)?;
             }
@@ -399,74 +396,69 @@ impl WalletDatabase for RedbWalletDatabase {
     }
 
     #[instrument(skip(self))]
-    async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Self::Err> {
+    async fn get_proofs(
+        &self,
+        mint_url: Option<UncheckedUrl>,
+        state: Option<Vec<State>>,
+    ) -> Result<Option<Proofs>, Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
-        let table = read_txn
-            .open_multimap_table(PROOFS_TABLE)
-            .map_err(Error::from)?;
 
-        let proofs = table
-            .get(mint_url.to_string().as_str())
+        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
+
+        let proofs: Proofs = table
+            .iter()
             .map_err(Error::from)?
             .flatten()
-            .flat_map(|k| serde_json::from_str(k.value()))
-            .collect();
-
-        Ok(proofs)
-    }
-
-    #[instrument(skip(self, proofs))]
-    async fn remove_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: &Proofs,
-    ) -> Result<(), Self::Err> {
-        let db = self.db.lock().await;
-
-        let write_txn = db.begin_write().map_err(Error::from)?;
+            .filter_map(|(_k, v)| {
+                let mut proof = None;
+
+                if let Ok(proof_info) = serde_json::from_str::<ProofInfo>(v.value()) {
+                    match (&mint_url, &state) {
+                        (Some(mint_url), Some(state)) => {
+                            if state.contains(&proof_info.state)
+                                && mint_url.eq(&proof_info.mint_url)
+                            {
+                                proof = Some(proof_info.proof);
+                            }
+                        }
+                        (Some(mint_url), None) => {
+                            if mint_url.eq(&proof_info.mint_url) {
+                                proof = Some(proof_info.proof);
+                            }
+                        }
+                        (None, Some(state)) => {
+                            if state.contains(&proof_info.state) {
+                                proof = Some(proof_info.proof);
+                            }
+                        }
+                        (None, None) => proof = Some(proof_info.proof),
+                    }
+                }
 
-        {
-            let mut table = write_txn
-                .open_multimap_table(PROOFS_TABLE)
-                .map_err(Error::from)?;
+                proof
+            })
+            .collect();
 
-            for proof in proofs {
-                table
-                    .remove(
-                        mint_url.to_string().as_str(),
-                        serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
-                    )
-                    .map_err(Error::from)?;
-            }
+        if proofs.is_empty() {
+            return Ok(None);
         }
-        write_txn.commit().map_err(Error::from)?;
 
-        Ok(())
+        Ok(Some(proofs))
     }
 
     #[instrument(skip(self, proofs))]
-    async fn add_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: Proofs,
-    ) -> Result<(), Self::Err> {
+    async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
 
         let write_txn = db.begin_write().map_err(Error::from)?;
 
         {
-            let mut table = write_txn
-                .open_multimap_table(PENDING_PROOFS_TABLE)
-                .map_err(Error::from)?;
+            let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
 
             for proof in proofs {
-                table
-                    .insert(
-                        mint_url.to_string().as_str(),
-                        serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
-                    )
-                    .map_err(Error::from)?;
+                let y_slice = proof.y().map_err(Error::from)?.to_bytes();
+                table.remove(y_slice.as_slice()).map_err(Error::from)?;
             }
         }
         write_txn.commit().map_err(Error::from)?;
@@ -475,53 +467,38 @@ impl WalletDatabase for RedbWalletDatabase {
     }
 
     #[instrument(skip(self))]
-    async fn get_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-    ) -> Result<Option<Proofs>, Self::Err> {
+    async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> {
         let db = self.db.lock().await;
         let read_txn = db.begin_read().map_err(Error::from)?;
-        let table = read_txn
-            .open_multimap_table(PENDING_PROOFS_TABLE)
-            .map_err(Error::from)?;
-
-        let proofs = table
-            .get(mint_url.to_string().as_str())
-            .map_err(Error::from)?
-            .flatten()
-            .flat_map(|k| serde_json::from_str(k.value()))
-            .collect();
+        let table = read_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
 
-        Ok(proofs)
-    }
-
-    #[instrument(skip(self, proofs))]
-    async fn remove_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: &Proofs,
-    ) -> Result<(), Self::Err> {
-        let db = self.db.lock().await;
+        let y_slice = y.to_bytes();
+        let proof = table.get(y_slice.as_slice()).map_err(Error::from)?;
 
         let write_txn = db.begin_write().map_err(Error::from)?;
 
-        {
-            let mut table = write_txn
-                .open_multimap_table(PENDING_PROOFS_TABLE)
-                .map_err(Error::from)?;
+        if let Some(proof) = proof {
+            let mut proof_info =
+                serde_json::from_str::<ProofInfo>(proof.value()).map_err(Error::from)?;
 
-            for proof in proofs {
+            proof_info.state = state;
+
+            {
+                let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?;
                 table
-                    .remove(
-                        mint_url.to_string().as_str(),
-                        serde_json::to_string(&proof).map_err(Error::from)?.as_str(),
+                    .insert(
+                        y_slice.as_slice(),
+                        serde_json::to_string(&proof_info)
+                            .map_err(Error::from)?
+                            .as_str(),
                     )
                     .map_err(Error::from)?;
             }
         }
+
         write_txn.commit().map_err(Error::from)?;
 
-        Ok(())
+        Err(Error::UnknownY.into())
     }
 
     #[instrument(skip(self))]

+ 72 - 132
crates/cdk-rexie/src/wallet.rs

@@ -4,8 +4,8 @@ use std::result::Result;
 
 use async_trait::async_trait;
 use cdk::cdk_database::WalletDatabase;
-use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs};
-use cdk::types::{MeltQuote, MintQuote};
+use cdk::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, State};
+use cdk::types::{MeltQuote, MintQuote, ProofInfo};
 use cdk::url::UncheckedUrl;
 use rexie::*;
 use thiserror::Error;
@@ -18,7 +18,6 @@ const MINT_KEYS: &str = "mint_keys";
 const MINT_QUOTES: &str = "mint_quotes";
 const MELT_QUOTES: &str = "melt_quotes";
 const PROOFS: &str = "proofs";
-const PENDING_PROOFS: &str = "pending_proofs";
 const CONFIG: &str = "config";
 const KEYSET_COUNTER: &str = "keyset_counter";
 
@@ -35,6 +34,8 @@ pub enum Error {
     /// Serde Wasm Error
     #[error(transparent)]
     SerdeBindgen(#[from] serde_wasm_bindgen::Error),
+    #[error(transparent)]
+    NUT00(cdk::nuts::nut00::Error),
 }
 impl From<Error> for cdk::cdk_database::Error {
     fn from(e: Error) -> Self {
@@ -88,10 +89,6 @@ impl RexieWalletDatabase {
                     .add_index(Index::new("keyset_id", "keyset_id").unique(true)),
             )
             .add_object_store(
-                ObjectStore::new(PENDING_PROOFS)
-                    .add_index(Index::new("keyset_id", "keyset_id").unique(true)),
-            )
-            .add_object_store(
                 ObjectStore::new(CONFIG)
                     .add_index(Index::new("keyset_id", "keyset_id").unique(true)),
             )
@@ -423,7 +420,7 @@ impl WalletDatabase for RexieWalletDatabase {
         Ok(())
     }
 
-    async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Self::Err> {
+    async fn add_proofs(&self, proofs: Vec<ProofInfo>) -> Result<(), Self::Err> {
         let rexie = self.db.lock().await;
 
         let transaction = rexie
@@ -432,33 +429,27 @@ impl WalletDatabase for RexieWalletDatabase {
 
         let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
 
-        let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
-
-        let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
-
-        let current_proofs: Proofs =
-            serde_wasm_bindgen::from_value(current_proofs).unwrap_or_default();
-
-        let all_proofs: Proofs = current_proofs
-            .into_iter()
-            .chain(proofs.into_iter())
-            .collect();
-
-        let all_proofs = serde_wasm_bindgen::to_value(&all_proofs).map_err(Error::from)?;
-
-        web_sys::console::log_1(&all_proofs);
+        for proof in proofs {
+            let y = proof.y;
+            let y = serde_wasm_bindgen::to_value(&y).map_err(Error::from)?;
+            let proof = serde_wasm_bindgen::to_value(&proof).map_err(Error::from)?;
 
-        proofs_store
-            .put(&all_proofs, Some(&mint_url))
-            .await
-            .map_err(Error::from)?;
+            proofs_store
+                .put(&proof, Some(&y))
+                .await
+                .map_err(Error::from)?;
+        }
 
         transaction.done().await.map_err(Error::from)?;
 
         Ok(())
     }
 
-    async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Self::Err> {
+    async fn get_proofs(
+        &self,
+        mint_url: Option<UncheckedUrl>,
+        state: Option<Vec<State>>,
+    ) -> Result<Option<Proofs>, Self::Err> {
         let rexie = self.db.lock().await;
 
         let transaction = rexie
@@ -467,21 +458,53 @@ impl WalletDatabase for RexieWalletDatabase {
 
         let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
 
-        let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
-        let proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
+        let proofs = proofs_store
+            .get_all(None, None, None, None)
+            .await
+            .map_err(Error::from)?;
+
+        let proofs: Proofs = proofs
+            .into_iter()
+            .filter_map(|(_k, v)| {
+                let mut proof = None;
+
+                if let Ok(proof_info) = serde_wasm_bindgen::from_value::<ProofInfo>(v) {
+                    match (&mint_url, &state) {
+                        (Some(mint_url), Some(state)) => {
+                            if state.contains(&proof_info.state)
+                                && mint_url.eq(&proof_info.mint_url)
+                            {
+                                proof = Some(proof_info.proof);
+                            }
+                        }
+                        (Some(mint_url), None) => {
+                            if mint_url.eq(&proof_info.mint_url) {
+                                proof = Some(proof_info.proof);
+                            }
+                        }
+                        (None, Some(state)) => {
+                            if state.contains(&proof_info.state) {
+                                proof = Some(proof_info.proof);
+                            }
+                        }
+                        (None, None) => proof = Some(proof_info.proof),
+                    }
+                }
+
+                proof
+            })
+            .collect();
 
         transaction.done().await.map_err(Error::from)?;
 
-        let proofs: Option<Proofs> = serde_wasm_bindgen::from_value(proofs).map_err(Error::from)?;
+        if proofs.is_empty() {
+            return Ok(None);
+        }
 
-        Ok(proofs)
+        Ok(Some(proofs))
     }
 
-    async fn remove_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: &Proofs,
-    ) -> Result<(), Self::Err> {
+    async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Self::Err> {
         let rexie = self.db.lock().await;
 
         let transaction = rexie
@@ -490,24 +513,10 @@ impl WalletDatabase for RexieWalletDatabase {
 
         let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
 
-        let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
-        let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
-
-        let current_proofs: Option<Proofs> =
-            serde_wasm_bindgen::from_value(current_proofs).map_err(Error::from)?;
-
-        if let Some(current_proofs) = current_proofs {
-            let proofs: Proofs = current_proofs
-                .into_iter()
-                .filter(|p| !proofs.contains(p))
-                .collect();
-
-            let proofs = serde_wasm_bindgen::to_value(&proofs).map_err(Error::from)?;
+        for proof in proofs {
+            let y = serde_wasm_bindgen::to_value(&proof.y()?).map_err(Error::from)?;
 
-            proofs_store
-                .put(&proofs, Some(&mint_url))
-                .await
-                .map_err(Error::from)?;
+            proofs_store.delete(&y).await.map_err(Error::from)?;
         }
 
         transaction.done().await.map_err(Error::from)?;
@@ -515,35 +524,26 @@ impl WalletDatabase for RexieWalletDatabase {
         Ok(())
     }
 
-    async fn add_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: Proofs,
-    ) -> Result<(), Self::Err> {
+    async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> {
         let rexie = self.db.lock().await;
 
         let transaction = rexie
-            .transaction(&[PENDING_PROOFS], TransactionMode::ReadWrite)
+            .transaction(&[PROOFS], TransactionMode::ReadWrite)
             .map_err(Error::from)?;
 
-        let proofs_store = transaction.store(PENDING_PROOFS).map_err(Error::from)?;
-
-        let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
+        let proofs_store = transaction.store(PROOFS).map_err(Error::from)?;
 
-        let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
+        let y = serde_wasm_bindgen::to_value(&y).map_err(Error::from)?;
 
-        let current_proofs: Proofs =
-            serde_wasm_bindgen::from_value(current_proofs).unwrap_or_default();
+        let proof = proofs_store.get(&y).await.map_err(Error::from)?;
+        let mut proof: ProofInfo = serde_wasm_bindgen::from_value(proof).map_err(Error::from)?;
 
-        let all_proofs: Proofs = current_proofs
-            .into_iter()
-            .chain(proofs.into_iter())
-            .collect();
+        proof.state = state;
 
-        let all_proofs = serde_wasm_bindgen::to_value(&all_proofs).map_err(Error::from)?;
+        let proof = serde_wasm_bindgen::to_value(&proof).map_err(Error::from)?;
 
         proofs_store
-            .put(&all_proofs, Some(&mint_url))
+            .put(&proof, Some(&y))
             .await
             .map_err(Error::from)?;
 
@@ -552,66 +552,6 @@ impl WalletDatabase for RexieWalletDatabase {
         Ok(())
     }
 
-    async fn get_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-    ) -> Result<Option<Proofs>, Self::Err> {
-        let rexie = self.db.lock().await;
-
-        let transaction = rexie
-            .transaction(&[PENDING_PROOFS], TransactionMode::ReadOnly)
-            .map_err(Error::from)?;
-
-        let proofs_store = transaction.store(PENDING_PROOFS).map_err(Error::from)?;
-
-        let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
-        let proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
-
-        transaction.done().await.map_err(Error::from)?;
-
-        let proofs: Option<Proofs> = serde_wasm_bindgen::from_value(proofs).unwrap_or(None);
-
-        Ok(proofs)
-    }
-
-    async fn remove_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: &Proofs,
-    ) -> Result<(), Self::Err> {
-        let rexie = self.db.lock().await;
-
-        let transaction = rexie
-            .transaction(&[PENDING_PROOFS], TransactionMode::ReadWrite)
-            .map_err(Error::from)?;
-
-        let proofs_store = transaction.store(PENDING_PROOFS).map_err(Error::from)?;
-
-        let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?;
-        let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?;
-
-        let current_proofs: Option<Proofs> =
-            serde_wasm_bindgen::from_value(current_proofs).map_err(Error::from)?;
-
-        if let Some(current_proofs) = current_proofs {
-            let proofs: Proofs = current_proofs
-                .into_iter()
-                .filter(|p| !proofs.contains(p))
-                .collect();
-
-            let proofs = serde_wasm_bindgen::to_value(&proofs).map_err(Error::from)?;
-
-            proofs_store
-                .add(&proofs, Some(&mint_url))
-                .await
-                .map_err(Error::from)?;
-        }
-
-        transaction.done().await.map_err(Error::from)?;
-
-        Ok(())
-    }
-
     async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> {
         let rexie = self.db.lock().await;
 

+ 15 - 19
crates/cdk/src/cdk_database/mod.rs

@@ -9,16 +9,18 @@ use thiserror::Error;
 
 #[cfg(feature = "mint")]
 use crate::mint::MintKeySetInfo;
-#[cfg(any(feature = "nostr", feature = "mint"))]
-use crate::nuts::PublicKey;
+#[cfg(feature = "wallet")]
+use crate::nuts::State;
 #[cfg(feature = "mint")]
 use crate::nuts::{BlindSignature, CurrencyUnit, Proof};
 #[cfg(any(feature = "wallet", feature = "mint"))]
-use crate::nuts::{Id, MintInfo};
+use crate::nuts::{Id, MintInfo, PublicKey};
 #[cfg(feature = "wallet")]
 use crate::nuts::{KeySetInfo, Keys, Proofs};
 #[cfg(feature = "mint")]
 use crate::secret::Secret;
+#[cfg(feature = "wallet")]
+use crate::types::ProofInfo;
 #[cfg(any(feature = "wallet", feature = "mint"))]
 use crate::types::{MeltQuote, MintQuote};
 #[cfg(feature = "wallet")]
@@ -35,6 +37,8 @@ pub enum Error {
     Database(Box<dyn std::error::Error + Send + Sync>),
     #[error(transparent)]
     Cdk(#[from] crate::error::Error),
+    #[error(transparent)]
+    NUT01(#[from] crate::nuts::nut00::Error),
 }
 
 #[cfg(feature = "wallet")]
@@ -73,23 +77,15 @@ pub trait WalletDatabase {
     async fn get_keys(&self, id: &Id) -> Result<Option<Keys>, Self::Err>;
     async fn remove_keys(&self, id: &Id) -> Result<(), Self::Err>;
 
-    async fn add_proofs(&self, mint_url: UncheckedUrl, proof: Proofs) -> Result<(), Self::Err>;
-    async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Self::Err>;
-    async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs)
-        -> Result<(), Self::Err>;
-
-    async fn add_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proof: Proofs,
-    ) -> Result<(), Self::Err>;
-    async fn get_pending_proofs(&self, mint_url: UncheckedUrl)
-        -> Result<Option<Proofs>, Self::Err>;
-    async fn remove_pending_proofs(
+    async fn add_proofs(&self, proof_info: Vec<ProofInfo>) -> Result<(), Self::Err>;
+    async fn get_proofs(
         &self,
-        mint_url: UncheckedUrl,
-        proofs: &Proofs,
-    ) -> Result<(), Self::Err>;
+        mint_url: Option<UncheckedUrl>,
+        state: Option<Vec<State>>,
+    ) -> Result<Option<Proofs>, Self::Err>;
+    async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Self::Err>;
+
+    async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err>;
 
     async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
     async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;

+ 59 - 51
crates/cdk/src/cdk_database/wallet_memory.rs

@@ -8,10 +8,8 @@ use tokio::sync::RwLock;
 
 use super::WalletDatabase;
 use crate::cdk_database::Error;
-#[cfg(feature = "nostr")]
-use crate::nuts::PublicKey;
-use crate::nuts::{Id, KeySetInfo, Keys, MintInfo, Proof, Proofs};
-use crate::types::{MeltQuote, MintQuote};
+use crate::nuts::{Id, KeySetInfo, Keys, MintInfo, Proofs, PublicKey, State};
+use crate::types::{MeltQuote, MintQuote, ProofInfo};
 use crate::url::UncheckedUrl;
 
 // TODO: Change these all to RwLocks
@@ -22,8 +20,7 @@ pub struct WalletMemoryDatabase {
     mint_quotes: Arc<RwLock<HashMap<String, MintQuote>>>,
     melt_quotes: Arc<RwLock<HashMap<String, MeltQuote>>>,
     mint_keys: Arc<RwLock<HashMap<Id, Keys>>>,
-    proofs: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Proof>>>>,
-    pending_proofs: Arc<RwLock<HashMap<UncheckedUrl, HashSet<Proof>>>>,
+    proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
     keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
     #[cfg(feature = "nostr")]
     nostr_last_checked: Arc<RwLock<HashMap<PublicKey, u32>>>,
@@ -50,7 +47,6 @@ impl WalletMemoryDatabase {
                 mint_keys.into_iter().map(|k| (Id::from(&k), k)).collect(),
             )),
             proofs: Arc::new(RwLock::new(HashMap::new())),
-            pending_proofs: Arc::new(RwLock::new(HashMap::new())),
             keyset_counter: Arc::new(RwLock::new(keyset_counter)),
             #[cfg(feature = "nostr")]
             nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)),
@@ -160,69 +156,81 @@ impl WalletDatabase for WalletMemoryDatabase {
         Ok(())
     }
 
-    async fn add_proofs(&self, mint_url: UncheckedUrl, proofs: Proofs) -> Result<(), Error> {
+    async fn add_proofs(&self, proofs_info: Vec<ProofInfo>) -> Result<(), Error> {
         let mut all_proofs = self.proofs.write().await;
 
-        let mint_proofs = all_proofs.entry(mint_url).or_insert(HashSet::new());
-        mint_proofs.extend(proofs);
+        for proof_info in proofs_info.into_iter() {
+            all_proofs.insert(proof_info.y, proof_info);
+        }
 
         Ok(())
     }
 
-    async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
-        Ok(self
-            .proofs
-            .read()
-            .await
-            .get(&mint_url)
-            .map(|p| p.iter().cloned().collect()))
+    async fn get_proofs(
+        &self,
+        mint_url: Option<UncheckedUrl>,
+        state: Option<Vec<State>>,
+    ) -> Result<Option<Proofs>, Error> {
+        let proofs = self.proofs.read().await;
+
+        let proofs: Proofs = proofs
+            .clone()
+            .into_values()
+            .filter_map(|proof_info| match (mint_url.clone(), state.clone()) {
+                (Some(mint_url), Some(state)) => {
+                    if state.contains(&proof_info.state) && mint_url.eq(&proof_info.mint_url) {
+                        Some(proof_info.proof)
+                    } else {
+                        None
+                    }
+                }
+                (Some(mint_url), None) => {
+                    if proof_info.mint_url.eq(&mint_url) {
+                        Some(proof_info.proof)
+                    } else {
+                        None
+                    }
+                }
+                (None, Some(state)) => {
+                    if state.contains(&proof_info.state) {
+                        Some(proof_info.proof)
+                    } else {
+                        None
+                    }
+                }
+                (None, None) => Some(proof_info.proof),
+            })
+            .collect();
+
+        if proofs.is_empty() {
+            return Ok(None);
+        }
+
+        Ok(Some(proofs))
     }
 
-    async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs) -> Result<(), Error> {
+    async fn remove_proofs(&self, proofs: &Proofs) -> Result<(), Error> {
         let mut mint_proofs = self.proofs.write().await;
 
-        if let Some(mint_proofs) = mint_proofs.get_mut(&mint_url) {
-            for proof in proofs {
-                mint_proofs.remove(proof);
-            }
+        for proof in proofs {
+            mint_proofs.remove(&proof.y().map_err(Error::from)?);
         }
 
         Ok(())
     }
 
-    async fn add_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: Proofs,
-    ) -> Result<(), Error> {
-        let mut all_proofs = self.pending_proofs.write().await;
+    async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> {
+        let mint_proofs = self.proofs.read().await;
 
-        let mint_proofs = all_proofs.entry(mint_url).or_insert(HashSet::new());
-        mint_proofs.extend(proofs);
+        let mint_proof = mint_proofs.get(&y);
 
-        Ok(())
-    }
-
-    async fn get_pending_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
-        Ok(self
-            .pending_proofs
-            .read()
-            .await
-            .get(&mint_url)
-            .map(|p| p.iter().cloned().collect()))
-    }
+        let mut mint_proofs = self.proofs.write().await;
 
-    async fn remove_pending_proofs(
-        &self,
-        mint_url: UncheckedUrl,
-        proofs: &Proofs,
-    ) -> Result<(), Error> {
-        let mut mint_proofs = self.pending_proofs.write().await;
+        if let Some(proof_info) = mint_proof {
+            let mut proof_info = proof_info.clone();
 
-        if let Some(mint_proofs) = mint_proofs.get_mut(&mint_url) {
-            for proof in proofs {
-                mint_proofs.remove(proof);
-            }
+            proof_info.state = state;
+            mint_proofs.insert(y, proof_info);
         }
 
         Ok(())

+ 41 - 1
crates/cdk/src/nuts/nut07.rs

@@ -2,16 +2,56 @@
 //!
 //! <https://github.com/cashubtc/nuts/blob/main/07.md>
 
+use std::fmt;
+use std::str::FromStr;
+
 use serde::{Deserialize, Serialize};
+use thiserror::Error;
 
 use super::nut01::PublicKey;
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+/// NUT07 Error
+#[derive(Debug, Error, PartialEq, Eq)]
+pub enum Error {
+    /// Unknown State error
+    #[error("Unknown State")]
+    UnknownState,
+}
+
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(rename_all = "UPPERCASE")]
 pub enum State {
     Spent,
     Unspent,
     Pending,
+    Reserved,
+}
+
+impl fmt::Display for State {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let s = match self {
+            State::Spent => "SPENT",
+            State::Unspent => "UNSPENT",
+            State::Pending => "PENDING",
+            State::Reserved => "RESERVED",
+        };
+
+        write!(f, "{}", s)
+    }
+}
+
+impl FromStr for State {
+    type Err = Error;
+
+    fn from_str(state: &str) -> Result<Self, Self::Err> {
+        match state {
+            "SPENT" => Ok(Self::Spent),
+            "UNSPENT" => Ok(Self::Unspent),
+            "PENDING" => Ok(Self::Pending),
+            "RESERVED" => Ok(Self::Reserved),
+            _ => Err(Error::UnknownState),
+        }
+    }
 }
 
 /// Check spendabale request [NUT-07]

+ 29 - 5
crates/cdk/src/types.rs

@@ -3,12 +3,13 @@
 use serde::{Deserialize, Serialize};
 use uuid::Uuid;
 
-use crate::nuts::{CurrencyUnit, Proofs};
+use crate::error::Error;
+use crate::nuts::{CurrencyUnit, Proof, Proofs, PublicKey, State};
 use crate::url::UncheckedUrl;
 use crate::Amount;
 
 /// Melt response with proofs
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
 pub struct Melted {
     pub paid: bool,
     pub preimage: Option<String>,
@@ -16,7 +17,7 @@ pub struct Melted {
 }
 
 /// Possible states of an invoice
-#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub enum InvoiceStatus {
     Unpaid,
     Paid,
@@ -25,7 +26,7 @@ pub enum InvoiceStatus {
 }
 
 /// Mint Quote Info
-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MintQuote {
     pub id: String,
     pub mint_url: UncheckedUrl,
@@ -59,7 +60,7 @@ impl MintQuote {
 }
 
 /// Melt Quote Info
-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
 pub struct MeltQuote {
     pub id: String,
     pub unit: CurrencyUnit,
@@ -91,3 +92,26 @@ impl MeltQuote {
         }
     }
 }
+
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct ProofInfo {
+    pub proof: Proof,
+    pub y: PublicKey,
+    pub mint_url: UncheckedUrl,
+    pub state: State,
+}
+
+impl ProofInfo {
+    pub fn new(proof: Proof, mint_url: UncheckedUrl, state: State) -> Result<Self, Error> {
+        let y = proof
+            .y()
+            .map_err(|_| Error::CustomError("Could not find y".to_string()))?;
+
+        Ok(Self {
+            proof,
+            y,
+            mint_url,
+            state,
+        })
+    }
+}

+ 73 - 43
crates/cdk/src/wallet.rs

@@ -26,7 +26,7 @@ use crate::nuts::{
     ProofState, Proofs, PublicKey, RestoreRequest, SecretKey, SigFlag, SpendingConditions, State,
     SwapRequest, Token,
 };
-use crate::types::{MeltQuote, Melted, MintQuote};
+use crate::types::{MeltQuote, Melted, MintQuote, ProofInfo};
 use crate::url::UncheckedUrl;
 use crate::util::{hex, unix_time};
 use crate::{Amount, Bolt11Invoice};
@@ -139,15 +139,16 @@ impl Wallet {
     /// Total Balance of wallet
     #[instrument(skip(self))]
     pub async fn total_balance(&self) -> Result<Amount, Error> {
-        let mints = self.localstore.get_mints().await?;
         let mut balance = Amount::ZERO;
 
-        for (mint, _) in mints {
-            if let Some(proofs) = self.localstore.get_proofs(mint.clone()).await? {
-                let amount = proofs.iter().map(|p| p.amount).sum();
+        if let Some(proofs) = self
+            .localstore
+            .get_proofs(None, Some(vec![State::Unspent]))
+            .await?
+        {
+            let amount = proofs.iter().map(|p| p.amount).sum();
 
-                balance += amount;
-            }
+            balance += amount;
         }
 
         Ok(balance)
@@ -156,15 +157,16 @@ impl Wallet {
     /// Total Balance of wallet
     #[instrument(skip(self))]
     pub async fn total_pending_balance(&self) -> Result<Amount, Error> {
-        let mints = self.localstore.get_mints().await?;
         let mut balance = Amount::ZERO;
 
-        for (mint, _) in mints {
-            if let Some(proofs) = self.localstore.get_pending_proofs(mint.clone()).await? {
-                let amount = proofs.iter().map(|p| p.amount).sum();
+        if let Some(proofs) = self
+            .localstore
+            .get_proofs(None, Some(vec![State::Pending]))
+            .await?
+        {
+            let amount = proofs.iter().map(|p| p.amount).sum();
 
-                balance += amount;
-            }
+            balance += amount;
         }
 
         Ok(balance)
@@ -177,7 +179,7 @@ impl Wallet {
         let mut balances = HashMap::new();
 
         for (mint, _) in mints {
-            if let Some(proofs) = self.localstore.get_proofs(mint.clone()).await? {
+            if let Some(proofs) = self.localstore.get_proofs(Some(mint.clone()), None).await? {
                 let amount = proofs.iter().map(|p| p.amount).sum();
 
                 balances.insert(mint, amount);
@@ -191,7 +193,10 @@ impl Wallet {
 
     #[instrument(skip(self), fields(mint_url = %mint_url))]
     pub async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result<Option<Proofs>, Error> {
-        Ok(self.localstore.get_proofs(mint_url).await?)
+        Ok(self
+            .localstore
+            .get_proofs(Some(mint_url), Some(vec![State::Unspent]))
+            .await?)
     }
 
     #[instrument(skip(self), fields(mint_url = %mint_url))]
@@ -347,7 +352,14 @@ impl Wallet {
         let mut balance = Amount::ZERO;
 
         for (mint, _) in mints {
-            if let Some(proofs) = self.localstore.get_pending_proofs(mint.clone()).await? {
+            if let Some(proofs) = self
+                .localstore
+                .get_proofs(
+                    Some(mint.clone()),
+                    Some(vec![State::Unspent, State::Pending]),
+                )
+                .await?
+            {
                 let states = self
                     .check_proofs_spent(mint.clone(), proofs.clone())
                     .await?;
@@ -367,9 +379,7 @@ impl Wallet {
 
                 let amount = pending_proofs.iter().map(|p| p.amount).sum();
 
-                self.localstore
-                    .remove_pending_proofs(mint, &non_pending_proofs)
-                    .await?;
+                self.localstore.remove_proofs(&non_pending_proofs).await?;
 
                 balance += amount;
             }
@@ -594,8 +604,13 @@ impl Wallet {
             .increment_keyset_counter(&active_keyset_id, proofs.len() as u32)
             .await?;
 
+        let proofs = proofs
+            .into_iter()
+            .flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
+            .collect();
+
         // Add new proofs to store
-        self.localstore.add_proofs(mint_url, proofs).await?;
+        self.localstore.add_proofs(proofs).await?;
 
         Ok(minted_amount)
     }
@@ -680,9 +695,13 @@ impl Wallet {
                     );
                 }
 
-                self.localstore
-                    .add_pending_proofs(mint_url.clone(), send_proofs.clone())
-                    .await?;
+                let send_proofs_info = send_proofs
+                    .clone()
+                    .into_iter()
+                    .flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Reserved))
+                    .collect();
+
+                self.localstore.add_proofs(send_proofs_info).await?;
 
                 proofs_to_send = Some(send_proofs);
             }
@@ -692,17 +711,20 @@ impl Wallet {
             }
         }
 
-        self.localstore
-            .remove_proofs(mint_url.clone(), &input_proofs)
-            .await?;
+        self.localstore.remove_proofs(&input_proofs).await?;
 
-        self.localstore
-            .add_pending_proofs(mint_url.clone(), input_proofs)
-            .await?;
+        for proof in input_proofs {
+            self.localstore
+                .set_proof_state(proof.y()?, State::Reserved)
+                .await?;
+        }
 
-        self.localstore
-            .add_proofs(mint_url.clone(), keep_proofs)
-            .await?;
+        let keep_proofs = keep_proofs
+            .into_iter()
+            .flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
+            .collect();
+
+        self.localstore.add_proofs(keep_proofs).await?;
 
         Ok(proofs_to_send)
     }
@@ -913,7 +935,7 @@ impl Wallet {
     ) -> Result<Proofs, Error> {
         let mint_proofs = self
             .localstore
-            .get_proofs(mint_url.clone())
+            .get_proofs(Some(mint_url.clone()), Some(vec![State::Unspent]))
             .await?
             .ok_or(Error::InsufficientFunds)?;
 
@@ -1048,16 +1070,17 @@ impl Wallet {
                 .increment_keyset_counter(&active_keyset_id, change_proofs.len() as u32)
                 .await?;
 
-            self.localstore
-                .add_proofs(mint_url.clone(), change_proofs)
-                .await?;
+            let change_proofs_info = change_proofs
+                .into_iter()
+                .flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
+                .collect();
+
+            self.localstore.add_proofs(change_proofs_info).await?;
         }
 
         self.localstore.remove_melt_quote(&quote_info.id).await?;
 
-        self.localstore
-            .remove_proofs(mint_url.clone(), &proofs)
-            .await?;
+        self.localstore.remove_proofs(&proofs).await?;
 
         Ok(melted)
     }
@@ -1209,7 +1232,11 @@ impl Wallet {
         let mut total_amount = Amount::ZERO;
         for (mint, proofs) in received_proofs {
             total_amount += proofs.iter().map(|p| p.amount).sum();
-            self.localstore.add_proofs(mint, proofs).await?;
+            let proofs = proofs
+                .into_iter()
+                .flat_map(|proof| ProofInfo::new(proof, mint.clone(), State::Unspent))
+                .collect();
+            self.localstore.add_proofs(proofs).await?;
         }
 
         Ok(total_amount)
@@ -1402,9 +1429,12 @@ impl Wallet {
 
                 restored_value += unspent_proofs.iter().map(|p| p.amount).sum();
 
-                self.localstore
-                    .add_proofs(mint_url.clone(), unspent_proofs)
-                    .await?;
+                let unspent_proofs = unspent_proofs
+                    .into_iter()
+                    .flat_map(|proof| ProofInfo::new(proof, mint_url.clone(), State::Unspent))
+                    .collect();
+
+                self.localstore.add_proofs(unspent_proofs).await?;
 
                 empty_batch = 0;
                 start_counter += 100;