diff --git a/bindings/cdk-js/src/nuts/nut07.rs b/bindings/cdk-js/src/nuts/nut07.rs index 0268e21b..a5129f2d 100644 --- a/bindings/cdk-js/src/nuts/nut07.rs +++ b/bindings/cdk-js/src/nuts/nut07.rs @@ -8,6 +8,7 @@ pub enum JsState { Spent, Unspent, Pending, + Reserved, } impl From for JsState { @@ -16,6 +17,7 @@ impl From for JsState { State::Spent => JsState::Spent, State::Unspent => JsState::Unspent, State::Pending => JsState::Pending, + State::Reserved => JsState::Reserved, } } } @@ -26,6 +28,7 @@ impl From for State { JsState::Spent => State::Spent, JsState::Unspent => State::Unspent, JsState::Pending => State::Pending, + JsState::Reserved => State::Reserved, } } } diff --git a/crates/cdk-redb/Cargo.toml b/crates/cdk-redb/Cargo.toml index b076fb84..35c23201 100644 --- a/crates/cdk-redb/Cargo.toml +++ b/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 diff --git a/crates/cdk-redb/src/error.rs b/crates/cdk-redb/src/error.rs index 8b400d0a..9b03df26 100644 --- a/crates/cdk-redb/src/error.rs +++ b/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 for cdk::cdk_database::Error { diff --git a/crates/cdk-redb/src/wallet.rs b/crates/cdk-redb/src/wallet.rs index 2686d681..1d4b5b7c 100644 --- a/crates/cdk-redb/src/wallet.rs +++ b/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"); +// +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) -> 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,124 +396,69 @@ impl WalletDatabase for RedbWalletDatabase { } #[instrument(skip(self))] - async fn get_proofs(&self, mint_url: UncheckedUrl) -> 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(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(); - - Ok(proofs) - } - - #[instrument(skip(self, proofs))] - async fn remove_proofs( + async fn get_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)?; - - { - let mut table = write_txn - .open_multimap_table(PROOFS_TABLE) - .map_err(Error::from)?; - - 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)?; - } - } - write_txn.commit().map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self, proofs))] - async fn add_pending_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)?; - - { - let mut table = write_txn - .open_multimap_table(PENDING_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)?; - } - } - write_txn.commit().map_err(Error::from)?; - - Ok(()) - } - - #[instrument(skip(self))] - async fn get_pending_proofs( - &self, - mint_url: UncheckedUrl, + mint_url: Option, + state: Option>, ) -> 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()) + 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())) + .filter_map(|(_k, v)| { + let mut proof = None; + + if let Ok(proof_info) = serde_json::from_str::(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), + } + } + + proof + }) .collect(); - Ok(proofs) + if proofs.is_empty() { + return Ok(None); + } + + Ok(Some(proofs)) } #[instrument(skip(self, proofs))] - async fn remove_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 - .remove( - 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)?; @@ -524,6 +466,41 @@ impl WalletDatabase for RedbWalletDatabase { Ok(()) } + #[instrument(skip(self))] + 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_table(PROOFS_TABLE).map_err(Error::from)?; + + 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)?; + + if let Some(proof) = proof { + let mut proof_info = + serde_json::from_str::(proof.value()).map_err(Error::from)?; + + proof_info.state = state; + + { + let mut table = write_txn.open_table(PROOFS_TABLE).map_err(Error::from)?; + table + .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)?; + + Err(Error::UnknownY.into()) + } + #[instrument(skip(self))] async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err> { let db = self.db.lock().await; diff --git a/crates/cdk-rexie/src/wallet.rs b/crates/cdk-rexie/src/wallet.rs index 44ae33f9..2e3d5f9b 100644 --- a/crates/cdk-rexie/src/wallet.rs +++ b/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 for cdk::cdk_database::Error { fn from(e: Error) -> Self { @@ -87,10 +88,6 @@ impl RexieWalletDatabase { ObjectStore::new(MELT_QUOTES) .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) -> 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)?; + 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)?; - 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); - - 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, Self::Err> { + async fn get_proofs( + &self, + mint_url: Option, + state: Option>, + ) -> Result, 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::(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 = 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)?; + for proof in proofs { + let y = serde_wasm_bindgen::to_value(&proof.y()?).map_err(Error::from)?; - let current_proofs: Option = - 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 - .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 proofs_store = transaction.store(PROOFS).map_err(Error::from)?; - let mint_url = serde_wasm_bindgen::to_value(&mint_url).map_err(Error::from)?; + let y = serde_wasm_bindgen::to_value(&y).map_err(Error::from)?; - let current_proofs = proofs_store.get(&mint_url).await.map_err(Error::from)?; + 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 current_proofs: Proofs = - serde_wasm_bindgen::from_value(current_proofs).unwrap_or_default(); + proof.state = state; - 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)?; + 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, 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 = 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 = - 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; diff --git a/crates/cdk/src/cdk_database/mod.rs b/crates/cdk/src/cdk_database/mod.rs index 4e2b7a22..cbd74fd8 100644 --- a/crates/cdk/src/cdk_database/mod.rs +++ b/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), #[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, 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, Self::Err>; - async fn remove_proofs(&self, mint_url: UncheckedUrl, proofs: &Proofs) - -> Result<(), Self::Err>; + async fn add_proofs(&self, proof_info: Vec) -> Result<(), Self::Err>; + async fn get_proofs( + &self, + mint_url: Option, + state: Option>, + ) -> Result, Self::Err>; + async fn remove_proofs(&self, 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, Self::Err>; - async fn remove_pending_proofs( - &self, - mint_url: UncheckedUrl, - 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, Self::Err>; diff --git a/crates/cdk/src/cdk_database/wallet_memory.rs b/crates/cdk/src/cdk_database/wallet_memory.rs index 4a0a21dd..3fa6c881 100644 --- a/crates/cdk/src/cdk_database/wallet_memory.rs +++ b/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>>, melt_quotes: Arc>>, mint_keys: Arc>>, - proofs: Arc>>>, - pending_proofs: Arc>>>, + proofs: Arc>>, keyset_counter: Arc>>, #[cfg(feature = "nostr")] nostr_last_checked: Arc>>, @@ -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) -> 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); - - Ok(()) - } - - async fn get_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { - Ok(self - .proofs - .read() - .await - .get(&mint_url) - .map(|p| p.iter().cloned().collect())) - } - - async fn remove_proofs(&self, mint_url: UncheckedUrl, 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_info in proofs_info.into_iter() { + all_proofs.insert(proof_info.y, proof_info); } Ok(()) } - async fn add_pending_proofs( + async fn get_proofs( &self, - mint_url: UncheckedUrl, - proofs: Proofs, - ) -> Result<(), Error> { - let mut all_proofs = self.pending_proofs.write().await; + mint_url: Option, + state: Option>, + ) -> Result, Error> { + let proofs = self.proofs.read().await; - let mint_proofs = all_proofs.entry(mint_url).or_insert(HashSet::new()); - mint_proofs.extend(proofs); + 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, proofs: &Proofs) -> Result<(), Error> { + let mut mint_proofs = self.proofs.write().await; + + for proof in proofs { + mint_proofs.remove(&proof.y().map_err(Error::from)?); + } Ok(()) } - async fn get_pending_proofs(&self, mint_url: UncheckedUrl) -> Result, Error> { - Ok(self - .pending_proofs - .read() - .await - .get(&mint_url) - .map(|p| p.iter().cloned().collect())) - } + async fn set_proof_state(&self, y: PublicKey, state: State) -> Result<(), Self::Err> { + let mint_proofs = self.proofs.read().await; - async fn remove_pending_proofs( - &self, - mint_url: UncheckedUrl, - proofs: &Proofs, - ) -> Result<(), Error> { - let mut mint_proofs = self.pending_proofs.write().await; + let mint_proof = mint_proofs.get(&y); - if let Some(mint_proofs) = mint_proofs.get_mut(&mint_url) { - for proof in proofs { - mint_proofs.remove(proof); - } + let mut mint_proofs = self.proofs.write().await; + + if let Some(proof_info) = mint_proof { + let mut proof_info = proof_info.clone(); + + proof_info.state = state; + mint_proofs.insert(y, proof_info); } Ok(()) diff --git a/crates/cdk/src/nuts/nut07.rs b/crates/cdk/src/nuts/nut07.rs index 193066e1..039f0eab 100644 --- a/crates/cdk/src/nuts/nut07.rs +++ b/crates/cdk/src/nuts/nut07.rs @@ -2,16 +2,56 @@ //! //! +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 { + 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] diff --git a/crates/cdk/src/types.rs b/crates/cdk/src/types.rs index 8d317991..bf2e5fa8 100644 --- a/crates/cdk/src/types.rs +++ b/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, @@ -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 { + let y = proof + .y() + .map_err(|_| Error::CustomError("Could not find y".to_string()))?; + + Ok(Self { + proof, + y, + mint_url, + state, + }) + } +} diff --git a/crates/cdk/src/wallet.rs b/crates/cdk/src/wallet.rs index f6ad7a38..2dff4718 100644 --- a/crates/cdk/src/wallet.rs +++ b/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 { - 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 { - 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, 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 { 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("e_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;