diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ae781c07..8f79b367e 120000 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1 +1 @@ -/nix/store/hjc1qcdcir47vpjxds5sdiiw1sw1n36q-pre-commit-config.json \ No newline at end of file +/nix/store/v4wkgb0g7safy7b8s1qjfsvgqzjjdvss-pre-commit-config.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index f3477baf9..bb43cef52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,8 @@ - cdk: Move unit conversion util fn to amount module ([davidcaseria]). - cdk: Remove spent proofs from db when check state is called ([mubarak23]). - cdk: Use `MintUrl` directly in wallet client ([ok300]). -- cdk-cli: Change cdk-cli pay command to melt ([mubarak23]). +- cdk-cli: Change cdk-cli pay command to melt ([mubarak23]). +- cdk: Rename `Wallet::get_proofs` to `Wallet::get_unspent_proofs` ([ok300]). ### Added @@ -49,6 +50,7 @@ - cdk: Wallet verifiys keyset id when first fetching keys ([thesimplekid]). - cdk-mind: Add swagger docs ([ok300]). - cdk: NUT18 payment request support ([thesimplekid]). +- cdk: Add `Wallet::get_proofs_with` ([ok300]). ### Removed - cdk: Remove `MintMeltSettings` since it is no longer used ([lollerfirst]). diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index e120a4a2d..9b6f3ddb7 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -104,7 +104,7 @@ async fn test_fake_melt_payment_fail() -> Result<()> { assert!(melt.is_err()); // The mint should have unset proofs from pending since payment failed - let all_proof = wallet.get_proofs().await?; + let all_proof = wallet.get_unspent_proofs().await?; let states = wallet.check_proofs_spent(all_proof).await?; for state in states { assert!(state.state == State::Unspent); @@ -344,7 +344,7 @@ async fn test_fake_melt_change_in_quote() -> Result<()> { let invoice = create_fake_invoice(9000, serde_json::to_string(&fake_description).unwrap()); - let proofs = wallet.get_proofs().await?; + let proofs = wallet.get_unspent_proofs().await?; let melt_quote = wallet.melt_quote(invoice.to_string(), None).await?; diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 110a0ccc0..da378f2a3 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -184,7 +184,7 @@ async fn test_restore() -> Result<()> { assert!(wallet_2.total_balance().await? == 0.into()); let restored = wallet_2.restore().await?; - let proofs = wallet_2.get_proofs().await?; + let proofs = wallet_2.get_unspent_proofs().await?; wallet_2 .swap(None, SplitTarget::default(), proofs, None, false) @@ -194,7 +194,7 @@ async fn test_restore() -> Result<()> { assert!(wallet_2.total_balance().await? == 100.into()); - let proofs = wallet.get_proofs().await?; + let proofs = wallet.get_unspent_proofs().await?; let states = wallet.check_proofs_spent(proofs).await?; diff --git a/crates/cdk/examples/proof-selection.rs b/crates/cdk/examples/proof-selection.rs index 46d687a29..210b77319 100644 --- a/crates/cdk/examples/proof-selection.rs +++ b/crates/cdk/examples/proof-selection.rs @@ -48,7 +48,7 @@ async fn main() { println!("Minted {}", receive_amount); } - let proofs = wallet.get_proofs().await.unwrap(); + let proofs = wallet.get_unspent_proofs().await.unwrap(); let selected = wallet .select_proofs_to_send(Amount::from(64), proofs, false) diff --git a/crates/cdk/src/mint/mint_nut04.rs b/crates/cdk/src/mint/mint_nut04.rs index cffd159ed..9b9e31ca1 100644 --- a/crates/cdk/src/mint/mint_nut04.rs +++ b/crates/cdk/src/mint/mint_nut04.rs @@ -122,7 +122,7 @@ impl Mint { Ok(quote) } - /// Check mint quote + /// CheckD mint quote #[instrument(skip(self))] pub async fn check_mint_quote(&self, quote_id: &str) -> Result { let quote = self @@ -131,8 +131,6 @@ impl Mint { .await? .ok_or(Error::UnknownQuote)?; - let paid = quote.state == MintQuoteState::Paid; - // Since the pending state is not part of the NUT it should not be part of the // response. In practice the wallet should not be checking the state of // a quote while waiting for the mint response. @@ -144,7 +142,6 @@ impl Mint { Ok(MintQuoteBolt11Response { quote: quote.id, request: quote.request, - paid: Some(paid), state, expiry: Some(quote.expiry), }) @@ -206,13 +203,41 @@ impl Mint { .await { tracing::debug!( - "Quote {} paid by lookup id {}", - mint_quote.id, - request_lookup_id + "Received payment notification for mint quote {}", + mint_quote.id ); - self.localstore - .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) - .await?; + + if mint_quote.state != MintQuoteState::Issued + && mint_quote.state != MintQuoteState::Paid + { + let unix_time = unix_time(); + + if mint_quote.expiry < unix_time { + tracing::warn!( + "Mint quote {} paid at {} expired at {}, leaving current state", + mint_quote.id, + mint_quote.expiry, + unix_time, + ); + return Err(Error::ExpiredQuote(mint_quote.expiry, unix_time)); + } + + tracing::debug!( + "Marking quote {} paid by lookup id {}", + mint_quote.id, + request_lookup_id + ); + + self.localstore + .update_mint_quote_state(&mint_quote.id, MintQuoteState::Paid) + .await?; + } else { + tracing::debug!( + "{} Quote already {} continuing", + mint_quote.id, + mint_quote.state + ); + } self.pubsub_manager .mint_quote_bolt11_status(&mint_quote, MintQuoteState::Paid); @@ -226,18 +251,12 @@ impl Mint { &self, mint_request: nut04::MintBolt11Request, ) -> Result { - // Check quote is known and not expired - let mint_quote = match self.localstore.get_mint_quote(&mint_request.quote).await? { - Some(quote) => { - if quote.expiry < unix_time() { - return Err(Error::ExpiredQuote(quote.expiry, unix_time())); - } + let mint_quote = + if let Some(quote) = self.localstore.get_mint_quote(&mint_request.quote).await? { quote - } - None => { + } else { return Err(Error::UnknownQuote); - } - }; + }; let state = self .localstore diff --git a/crates/cdk/src/nuts/nut04.rs b/crates/cdk/src/nuts/nut04.rs index 3067628f6..207a7a9a3 100644 --- a/crates/cdk/src/nuts/nut04.rs +++ b/crates/cdk/src/nuts/nut04.rs @@ -5,8 +5,7 @@ use std::fmt; use std::str::FromStr; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; +use serde::{Deserialize, Serialize}; use thiserror::Error; use super::nut00::{BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod}; @@ -80,96 +79,25 @@ impl FromStr for QuoteState { } /// Mint quote response [NUT-04] -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] pub struct MintQuoteBolt11Response { /// Quote Id pub quote: String, /// Payment request to fulfil pub request: String, - // TODO: To be deprecated - /// Whether the the request haas be paid - /// Deprecated - pub paid: Option, /// Quote State pub state: MintQuoteState, /// Unix timestamp until the quote is valid pub expiry: Option, } -// A custom deserializer is needed until all mints -// update some will return without the required state. -impl<'de> Deserialize<'de> for MintQuoteBolt11Response { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = Value::deserialize(deserializer)?; - - let quote: String = serde_json::from_value( - value - .get("quote") - .ok_or(serde::de::Error::missing_field("quote"))? - .clone(), - ) - .map_err(|_| serde::de::Error::custom("Invalid quote id string"))?; - - let request: String = serde_json::from_value( - value - .get("request") - .ok_or(serde::de::Error::missing_field("request"))? - .clone(), - ) - .map_err(|_| serde::de::Error::custom("Invalid request string"))?; - - let paid: Option = value.get("paid").and_then(|p| p.as_bool()); - - let state: Option = value - .get("state") - .and_then(|s| serde_json::from_value(s.clone()).ok()); - - let (state, paid) = match (state, paid) { - (None, None) => return Err(serde::de::Error::custom("State or paid must be defined")), - (Some(state), _) => { - let state: QuoteState = QuoteState::from_str(&state) - .map_err(|_| serde::de::Error::custom("Unknown state"))?; - let paid = state == QuoteState::Paid; - - (state, paid) - } - (None, Some(paid)) => { - let state = if paid { - QuoteState::Paid - } else { - QuoteState::Unpaid - }; - (state, paid) - } - }; - - let expiry = value - .get("expiry") - .ok_or(serde::de::Error::missing_field("expiry"))? - .as_u64(); - - Ok(Self { - quote, - request, - paid: Some(paid), - state, - expiry, - }) - } -} - #[cfg(feature = "mint")] impl From for MintQuoteBolt11Response { fn from(mint_quote: crate::mint::MintQuote) -> MintQuoteBolt11Response { - let paid = mint_quote.state == QuoteState::Paid; MintQuoteBolt11Response { quote: mint_quote.id, request: mint_quote.request, - paid: Some(paid), state: mint_quote.state, expiry: Some(mint_quote.expiry), } diff --git a/crates/cdk/src/nuts/nut17.rs b/crates/cdk/src/nuts/nut17.rs index 7024fc5cc..089fae9a5 100644 --- a/crates/cdk/src/nuts/nut17.rs +++ b/crates/cdk/src/nuts/nut17.rs @@ -168,7 +168,6 @@ impl From<&MintQuote> for MintQuoteBolt11Response { MintQuoteBolt11Response { quote: mint_quote.id.clone(), request: mint_quote.request.clone(), - paid: Some(mint_quote.state == MintQuoteState::Paid), state: mint_quote.state, expiry: Some(mint_quote.expiry), } @@ -199,7 +198,7 @@ impl PubSubManager { new_state: MintQuoteState, ) { let mut event = quote.into(); - event.paid = Some(new_state == MintQuoteState::Paid); + event.state = new_state; self.broadcast(event.into()); } diff --git a/crates/cdk/src/wallet/balance.rs b/crates/cdk/src/wallet/balance.rs index 8fe277b9d..6d1276462 100644 --- a/crates/cdk/src/wallet/balance.rs +++ b/crates/cdk/src/wallet/balance.rs @@ -2,44 +2,24 @@ use std::collections::HashMap; use tracing::instrument; -use crate::{ - nuts::{CurrencyUnit, State}, - Amount, Error, Wallet, -}; +use crate::nuts::nut00::ProofsMethods; +use crate::{nuts::CurrencyUnit, Amount, Error, Wallet}; impl Wallet { /// Total unspent balance of wallet #[instrument(skip(self))] pub async fn total_balance(&self) -> Result { - let proofs = self - .localstore - .get_proofs( - Some(self.mint_url.clone()), - Some(self.unit), - Some(vec![State::Unspent]), - None, - ) - .await?; - let balance = Amount::try_sum(proofs.iter().map(|p| p.proof.amount))?; - - Ok(balance) + Ok(self.get_unspent_proofs().await?.total_amount()?) } /// Total pending balance #[instrument(skip(self))] pub async fn total_pending_balance(&self) -> Result, Error> { - let proofs = self - .localstore - .get_proofs( - Some(self.mint_url.clone()), - Some(self.unit), - Some(vec![State::Pending]), - None, - ) - .await?; + let proofs = self.get_pending_proofs().await?; + // TODO If only the proofs for this wallet's unit are retrieved, why build a map with key = unit? let balances = proofs.iter().fold(HashMap::new(), |mut acc, proof| { - *acc.entry(proof.unit).or_insert(Amount::ZERO) += proof.proof.amount; + *acc.entry(self.unit).or_insert(Amount::ZERO) += proof.amount; acc }); @@ -49,18 +29,11 @@ impl Wallet { /// Total reserved balance #[instrument(skip(self))] pub async fn total_reserved_balance(&self) -> Result, Error> { - let proofs = self - .localstore - .get_proofs( - Some(self.mint_url.clone()), - Some(self.unit), - Some(vec![State::Reserved]), - None, - ) - .await?; + let proofs = self.get_reserved_proofs().await?; + // TODO If only the proofs for this wallet's unit are retrieved, why build a map with key = unit? let balances = proofs.iter().fold(HashMap::new(), |mut acc, proof| { - *acc.entry(proof.unit).or_insert(Amount::ZERO) += proof.proof.amount; + *acc.entry(self.unit).or_insert(Amount::ZERO) += proof.amount; acc }); diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index 6d0f709b2..c31f2ff49 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -286,7 +286,7 @@ impl Wallet { let inputs_needed_amount = quote_info.amount + quote_info.fee_reserve; - let available_proofs = self.get_proofs().await?; + let available_proofs = self.get_unspent_proofs().await?; let input_proofs = self .select_proofs_to_swap(inputs_needed_amount, available_proofs) diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index c0488c2f6..85045eaa8 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -181,7 +181,7 @@ impl Wallet { /// Get amounts needed to refill proof state #[instrument(skip(self))] pub async fn amounts_needed_for_state_target(&self) -> Result, Error> { - let unspent_proofs = self.get_proofs().await?; + let unspent_proofs = self.get_unspent_proofs().await?; let amounts_count: HashMap = unspent_proofs diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index 6f3c33341..8d26e9e46 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -125,7 +125,7 @@ impl MultiMintWallet { let mut mint_proofs = BTreeMap::new(); for (WalletKey { mint_url, unit: u }, wallet) in self.wallets.lock().await.iter() { - let wallet_proofs = wallet.get_proofs().await?; + let wallet_proofs = wallet.get_unspent_proofs().await?; mint_proofs.insert(mint_url.clone(), (wallet_proofs, *u)); } Ok(mint_proofs) diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 5ea4f53ba..12fed0ef5 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -5,7 +5,7 @@ use tracing::instrument; use crate::nuts::nut00::ProofsMethods; use crate::{ amount::SplitTarget, - nuts::{Proof, ProofState, Proofs, PublicKey, State}, + nuts::{Proof, ProofState, Proofs, PublicKey, SpendingConditions, State}, types::ProofInfo, Amount, Error, Wallet, }; @@ -13,48 +13,36 @@ use crate::{ impl Wallet { /// Get unspent proofs for mint #[instrument(skip(self))] - pub async fn get_proofs(&self) -> Result { - Ok(self - .localstore - .get_proofs( - Some(self.mint_url.clone()), - Some(self.unit), - Some(vec![State::Unspent]), - None, - ) - .await? - .into_iter() - .map(|p| p.proof) - .collect()) + pub async fn get_unspent_proofs(&self) -> Result { + self.get_proofs_with(Some(vec![State::Unspent]), None).await } /// Get pending [`Proofs`] #[instrument(skip(self))] pub async fn get_pending_proofs(&self) -> Result { - Ok(self - .localstore - .get_proofs( - Some(self.mint_url.clone()), - Some(self.unit), - Some(vec![State::Pending]), - None, - ) - .await? - .into_iter() - .map(|p| p.proof) - .collect()) + self.get_proofs_with(Some(vec![State::Pending]), None).await } /// Get reserved [`Proofs`] #[instrument(skip(self))] pub async fn get_reserved_proofs(&self) -> Result { + self.get_proofs_with(Some(vec![State::Reserved]), None) + .await + } + + /// Get this wallet's [Proofs] that match the args + pub async fn get_proofs_with( + &self, + state: Option>, + spending_conditions: Option>, + ) -> Result { Ok(self .localstore .get_proofs( Some(self.mint_url.clone()), Some(self.unit), - Some(vec![State::Reserved]), - None, + state, + spending_conditions, ) .await? .into_iter() diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index f9a62b9a7..7885db5cb 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -43,19 +43,14 @@ impl Wallet { } } - let mint_url = &self.mint_url; - let unit = &self.unit; let available_proofs = self - .localstore - .get_proofs( - Some(mint_url.clone()), - Some(*unit), + .get_proofs_with( Some(vec![State::Unspent]), conditions.clone().map(|c| vec![c]), ) .await?; - let (available_proofs, proofs_sum) = available_proofs.into_iter().map(|p| p.proof).fold( + let (available_proofs, proofs_sum) = available_proofs.into_iter().fold( (Vec::new(), Amount::ZERO), |(mut acc1, mut acc2), p| { acc2 += p.amount; @@ -66,20 +61,9 @@ impl Wallet { let available_proofs = if proofs_sum < amount { match &conditions { Some(conditions) => { - let available_proofs = self - .localstore - .get_proofs( - Some(mint_url.clone()), - Some(*unit), - Some(vec![State::Unspent]), - None, - ) - .await?; - - let available_proofs = available_proofs.into_iter().map(|p| p.proof).collect(); + let unspent_proofs = self.get_unspent_proofs().await?; - let proofs_to_swap = - self.select_proofs_to_swap(amount, available_proofs).await?; + let proofs_to_swap = self.select_proofs_to_swap(amount, unspent_proofs).await?; let proofs_with_conditions = self .swap( @@ -90,12 +74,10 @@ impl Wallet { include_fees, ) .await?; - proofs_with_conditions.ok_or(Error::InsufficientFunds)? + proofs_with_conditions.ok_or(Error::InsufficientFunds) } - None => { - return Err(Error::InsufficientFunds); - } - } + None => Err(Error::InsufficientFunds), + }? } else { available_proofs }; diff --git a/flake.lock b/flake.lock index 85d425eb6..d8d8de915 100644 --- a/flake.lock +++ b/flake.lock @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1727540905, - "narHash": "sha256-40J9tW7Y794J7Uw4GwcAKlMxlX2xISBl6IBigo83ih8=", + "lastModified": 1730137625, + "narHash": "sha256-9z8oOgFZiaguj+bbi3k4QhAD6JabWrnv7fscC/mt0KE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fbca5e745367ae7632731639de5c21f29c8744ed", + "rev": "64b80bfb316b57cdb8919a9110ef63393d74382a", "type": "github" }, "original": { @@ -111,11 +111,11 @@ "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1727514110, - "narHash": "sha256-0YRcOxJG12VGDFH8iS8pJ0aYQQUAgo/r3ZAL+cSh9nk=", + "lastModified": 1730302582, + "narHash": "sha256-W1MIJpADXQCgosJZT8qBYLRuZls2KSiKdpnTVdKBuvU=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "85f7a7177c678de68224af3402ab8ee1bcee25c8", + "rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf", "type": "github" }, "original": { @@ -139,11 +139,11 @@ ] }, "locked": { - "lastModified": 1727663505, - "narHash": "sha256-83j/GrHsx8GFUcQofKh+PRPz6pz8sxAsZyT/HCNdey8=", + "lastModified": 1730341826, + "narHash": "sha256-RFaeY7EWzXOmAL2IQEACbnrEza3TgD5UQApHR4hGHhY=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c2099c6c7599ea1980151b8b6247a8f93e1806ee", + "rev": "815d1b3ee71716fc91a7bd149801e1f04d45fbc5", "type": "github" }, "original": {