From ef3ecce3cf976a981c413082e944f3d2bc8300c0 Mon Sep 17 00:00:00 2001 From: Mubarak Muhammad Aminu Date: Fri, 11 Oct 2024 17:02:53 +0100 Subject: [PATCH 1/6] feat(cdk_sdk):remove spent proof and update db state (#393) --- crates/cdk/src/wallet/proofs.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 3eff5cf8..9d652f17 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -111,6 +111,16 @@ impl Wallet { .collect::, _>>()?, ) .await?; + let spent_ys: Vec<_> = spendable + .states + .iter() + .filter_map(|p| match p.state { + State::Spent => Some(p.y), + _ => None, + }) + .collect(); + + self.localstore.update_proofs(vec![], spent_ys).await?; Ok(spendable.states) } From e0f5344e8fee5da982fa5d49687e51f88e837577 Mon Sep 17 00:00:00 2001 From: Ruben Date: Sat, 12 Oct 2024 10:21:20 +0200 Subject: [PATCH 2/6] feat: mint-proofs command fix typo make output more readable. Co-authored-by: Pavol Rusnak --- crates/cdk-cli/src/main.rs | 9 +++- .../src/sub_commands/list_mint_proofs.rs | 41 +++++++++++++++++++ crates/cdk-cli/src/sub_commands/mod.rs | 1 + crates/cdk/src/amount.rs | 6 ++- crates/cdk/src/nuts/nut00/mod.rs | 15 ++++--- crates/cdk/src/wallet/multi_mint_wallet.rs | 16 +++++++- 6 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 crates/cdk-cli/src/sub_commands/list_mint_proofs.rs diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 2da00dc8..0de66bc5 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -70,6 +70,8 @@ enum Commands { Restore(sub_commands::restore::RestoreSubCommand), /// Update Mint Url UpdateMintUrl(sub_commands::update_mint_url::UpdateMintUrlSubCommand), + /// Get proofs from mint. + ListMintProofs, } #[tokio::main] @@ -123,10 +125,10 @@ async fn main() -> Result<()> { let mut rng = rand::thread_rng(); let random_bytes: [u8; 32] = rng.gen(); - let mnemnic = Mnemonic::from_entropy(&random_bytes)?; + let mnemonic = Mnemonic::from_entropy(&random_bytes)?; tracing::info!("Using randomly generated seed you will not be able to restore"); - mnemnic + mnemonic } }; @@ -199,5 +201,8 @@ async fn main() -> Result<()> { sub_commands::update_mint_url::update_mint_url(&multi_mint_wallet, sub_command_args) .await } + Commands::ListMintProofs => { + sub_commands::list_mint_proofs::proofs(&multi_mint_wallet).await + } } } diff --git a/crates/cdk-cli/src/sub_commands/list_mint_proofs.rs b/crates/cdk-cli/src/sub_commands/list_mint_proofs.rs new file mode 100644 index 00000000..f4e8a4d6 --- /dev/null +++ b/crates/cdk-cli/src/sub_commands/list_mint_proofs.rs @@ -0,0 +1,41 @@ +use std::collections::BTreeMap; + +use anyhow::Result; +use cdk::{ + mint_url::MintUrl, + nuts::{CurrencyUnit, Proof}, + wallet::multi_mint_wallet::MultiMintWallet, +}; + +pub async fn proofs(multi_mint_wallet: &MultiMintWallet) -> Result<()> { + list_proofs(multi_mint_wallet).await?; + Ok(()) +} + +async fn list_proofs( + multi_mint_wallet: &MultiMintWallet, +) -> Result, CurrencyUnit))>> { + let wallets_proofs: BTreeMap, CurrencyUnit)> = + multi_mint_wallet.list_proofs().await?; + + let mut proofs_vec = Vec::with_capacity(wallets_proofs.len()); + + for (i, (mint_url, proofs)) in wallets_proofs.iter().enumerate() { + let mint_url = mint_url.clone(); + println!("{i}: {mint_url}"); + println!("| Amount | Unit | Secret | DLEQ proof included"); + println!("|----------|------|------------------------------------------------------------------|--------------------"); + for proof in &proofs.0 { + println!( + "| {:8} | {:4} | {:64} | {}", + proof.amount, + proofs.1, + proof.secret, + proof.dleq.is_some() + ); + } + println!(); + proofs_vec.push((mint_url, proofs.clone())) + } + Ok(proofs_vec) +} diff --git a/crates/cdk-cli/src/sub_commands/mod.rs b/crates/cdk-cli/src/sub_commands/mod.rs index cb79db55..eee3cf32 100644 --- a/crates/cdk-cli/src/sub_commands/mod.rs +++ b/crates/cdk-cli/src/sub_commands/mod.rs @@ -2,6 +2,7 @@ pub mod balance; pub mod burn; pub mod check_spent; pub mod decode_token; +pub mod list_mint_proofs; pub mod melt; pub mod mint; pub mod mint_info; diff --git a/crates/cdk/src/amount.rs b/crates/cdk/src/amount.rs index 7e2d4e26..2d9fa34b 100644 --- a/crates/cdk/src/amount.rs +++ b/crates/cdk/src/amount.rs @@ -138,7 +138,11 @@ impl Default for &Amount { impl fmt::Display for Amount { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) + if let Some(width) = f.width() { + write!(f, "{:width$}", self.0, width = width) + } else { + write!(f, "{}", self.0) + } } } diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 58fff63a..98223792 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -378,11 +378,16 @@ impl FromStr for CurrencyUnit { impl fmt::Display for CurrencyUnit { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - CurrencyUnit::Sat => write!(f, "sat"), - CurrencyUnit::Msat => write!(f, "msat"), - CurrencyUnit::Usd => write!(f, "usd"), - CurrencyUnit::Eur => write!(f, "eur"), + let s = match self { + CurrencyUnit::Sat => "sat", + CurrencyUnit::Msat => "msat", + CurrencyUnit::Usd => "usd", + CurrencyUnit::Eur => "eur", + }; + if let Some(width) = f.width() { + write!(f, "{:width$}", s, width = width) + } else { + write!(f, "{}", s) } } } diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index c2033605..3852fe49 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -16,7 +16,7 @@ use super::types::SendKind; use super::Error; use crate::amount::SplitTarget; use crate::mint_url::MintUrl; -use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token}; +use crate::nuts::{CurrencyUnit, Proof, SecretKey, SpendingConditions, Token}; use crate::types::Melted; use crate::wallet::types::MintQuote; use crate::{Amount, Wallet}; @@ -117,6 +117,20 @@ impl MultiMintWallet { Ok(balances) } + /// List proofs. + #[instrument(skip(self))] + pub async fn list_proofs( + &self, + ) -> Result, CurrencyUnit)>, Error> { + 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?; + mint_proofs.insert(mint_url.clone(), (wallet_proofs, *u)); + } + Ok(mint_proofs) + } + /// Create cashu token #[instrument(skip(self))] pub async fn send( From 4efeb7d1c563780d78891c3415e966d5ca3eff5a Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:08:40 +0200 Subject: [PATCH 3/6] mintd: sort pks by amount in /v1/keys --- crates/cdk/src/amount.rs | 51 +++++++++++++++++++++++++++++++- crates/cdk/src/nuts/nut01/mod.rs | 24 ++++++++------- crates/cdk/src/nuts/nut02.rs | 5 ++-- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/crates/cdk/src/amount.rs b/crates/cdk/src/amount.rs index 2d9fa34b..33181dd3 100644 --- a/crates/cdk/src/amount.rs +++ b/crates/cdk/src/amount.rs @@ -4,8 +4,9 @@ use std::cmp::Ordering; use std::fmt; +use std::str::FromStr; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use thiserror::Error; use crate::nuts::CurrencyUnit; @@ -214,6 +215,54 @@ impl std::ops::Div for Amount { } } +/// String wrapper for an [Amount]. +/// +/// It ser-/deserializes the inner [Amount] to a string, while at the same time using the [u64] +/// value of the [Amount] for comparison and ordering. This helps automatically sort the keys of +/// a [BTreeMap] when [AmountStr] is used as key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AmountStr(Amount); + +impl AmountStr { + pub(crate) fn from(amt: Amount) -> Self { + Self(amt) + } +} + +impl PartialOrd for AmountStr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for AmountStr { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } +} + +impl<'de> Deserialize<'de> for AmountStr { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + u64::from_str(&s) + .map(Amount) + .map(Self) + .map_err(serde::de::Error::custom) + } +} + +impl Serialize for AmountStr { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.0.to_string()) + } +} + /// Kinds of targeting that are supported #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)] pub enum SplitTarget { diff --git a/crates/cdk/src/nuts/nut01/mod.rs b/crates/cdk/src/nuts/nut01/mod.rs index 649e1ed9..12819222 100644 --- a/crates/cdk/src/nuts/nut01/mod.rs +++ b/crates/cdk/src/nuts/nut01/mod.rs @@ -16,7 +16,7 @@ mod secret_key; pub use self::public_key::PublicKey; pub use self::secret_key::SecretKey; use super::nut02::KeySet; -use crate::amount::Amount; +use crate::amount::{Amount, AmountStr}; /// Nut01 Error #[derive(Debug, Error)] @@ -37,16 +37,20 @@ pub enum Error { }, } -/// Mint Keys [NUT-01] +/// Mint public keys per amount. +/// +/// This is a variation of [MintKeys] that only exposes the public keys. +/// +/// See [NUT-01] #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] -pub struct Keys(BTreeMap); +pub struct Keys(BTreeMap); impl From for Keys { fn from(keys: MintKeys) -> Self { Self( keys.0 - .iter() - .map(|(amount, keypair)| (amount.to_string(), keypair.public_key)) + .into_iter() + .map(|(amount, keypair)| (AmountStr::from(amount), keypair.public_key)) .collect(), ) } @@ -55,25 +59,25 @@ impl From for Keys { impl Keys { /// Create new [`Keys`] #[inline] - pub fn new(keys: BTreeMap) -> Self { + pub fn new(keys: BTreeMap) -> Self { Self(keys) } /// Get [`Keys`] #[inline] - pub fn keys(&self) -> &BTreeMap { + pub fn keys(&self) -> &BTreeMap { &self.0 } /// Get [`PublicKey`] for [`Amount`] #[inline] pub fn amount_key(&self, amount: Amount) -> Option { - self.0.get(&amount.to_string()).copied() + self.0.get(&AmountStr::from(amount)).copied() } /// Iterate through the (`Amount`, `PublicKey`) entries in the Map #[inline] - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.0.iter() } } @@ -87,7 +91,7 @@ pub struct KeysResponse { pub keysets: Vec, } -/// Mint keys +/// Mint key pairs per amount #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MintKeys(BTreeMap); diff --git a/crates/cdk/src/nuts/nut02.rs b/crates/cdk/src/nuts/nut02.rs index fbe12ba7..f3885593 100644 --- a/crates/cdk/src/nuts/nut02.rs +++ b/crates/cdk/src/nuts/nut02.rs @@ -25,6 +25,7 @@ use thiserror::Error; use super::nut01::Keys; #[cfg(feature = "mint")] use super::nut01::{MintKeyPair, MintKeys}; +use crate::amount::AmountStr; use crate::nuts::nut00::CurrencyUnit; use crate::util::hex; #[cfg(feature = "mint")] @@ -197,9 +198,9 @@ impl From<&Keys> for Id { 5 - prefix it with a keyset ID version byte */ - let mut keys: Vec<(&String, &super::PublicKey)> = map.iter().collect(); + let mut keys: Vec<(&AmountStr, &super::PublicKey)> = map.iter().collect(); - keys.sort_by_key(|(k, _v)| u64::from_str(k).unwrap()); + keys.sort_by_key(|(amt, _v)| *amt); let pubkeys_concat: Vec = keys .iter() From cd9c098bccd13f55c901a121eeff61b7e45c33a9 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 13 Oct 2024 11:15:15 +0200 Subject: [PATCH 4/6] fix: remove rename --- crates/cdk/src/nuts/nut09.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/cdk/src/nuts/nut09.rs b/crates/cdk/src/nuts/nut09.rs index e48f57c5..672ef8de 100644 --- a/crates/cdk/src/nuts/nut09.rs +++ b/crates/cdk/src/nuts/nut09.rs @@ -19,8 +19,6 @@ pub struct RestoreResponse { /// Outputs pub outputs: Vec, /// Signatures - // TODO: remove rename just for temp compatanlite with nutshell - #[serde(rename = "promises")] pub signatures: Vec, } @@ -29,7 +27,7 @@ mod test { #[test] fn restore_response() { use super::*; - let rs = r#"{"outputs":[{"B_":"0204bbffa045f28ec836117a29ea0a00d77f1d692e38cf94f72a5145bfda6d8f41","amount":0,"id":"00ffd48b8f5ecf80", "witness":null},{"B_":"025f0615ccba96f810582a6885ffdb04bd57c96dbc590f5aa560447b31258988d7","amount":0,"id":"00ffd48b8f5ecf80"}],"promises":[{"C_":"02e9701b804dc05a5294b5a580b428237a27c7ee1690a0177868016799b1761c81","amount":8,"dleq":null,"id":"00ffd48b8f5ecf80"},{"C_":"031246ee046519b15648f1b8d8ffcb8e537409c84724e148c8d6800b2e62deb795","amount":2,"dleq":null,"id":"00ffd48b8f5ecf80"}]}"#; + let rs = r#"{"outputs":[{"B_":"0204bbffa045f28ec836117a29ea0a00d77f1d692e38cf94f72a5145bfda6d8f41","amount":0,"id":"00ffd48b8f5ecf80", "witness":null},{"B_":"025f0615ccba96f810582a6885ffdb04bd57c96dbc590f5aa560447b31258988d7","amount":0,"id":"00ffd48b8f5ecf80"}],"signatures":[{"C_":"02e9701b804dc05a5294b5a580b428237a27c7ee1690a0177868016799b1761c81","amount":8,"dleq":null,"id":"00ffd48b8f5ecf80"},{"C_":"031246ee046519b15648f1b8d8ffcb8e537409c84724e148c8d6800b2e62deb795","amount":2,"dleq":null,"id":"00ffd48b8f5ecf80"}]}"#; let res: RestoreResponse = serde_json::from_str(rs).unwrap(); From f6533a08de2f8c21b67a8ef407a4884507c25cfa Mon Sep 17 00:00:00 2001 From: ok300 <106775972+ok300@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:31:12 +0200 Subject: [PATCH 5/6] Add Proofs trait to consolidate aggregate fns --- crates/cdk-redb/src/mint/mod.rs | 6 ++---- crates/cdk-sqlite/src/mint/mod.rs | 6 ++---- crates/cdk/src/cdk_database/mint_memory.rs | 6 ++---- crates/cdk/src/mint/melt.rs | 20 ++++------------- crates/cdk/src/mint/swap.rs | 8 ++----- crates/cdk/src/nuts/nut00/mod.rs | 22 +++++++++++++++++++ crates/cdk/src/nuts/nut00/token.rs | 3 ++- crates/cdk/src/types.rs | 5 +++-- crates/cdk/src/wallet/melt.rs | 17 ++++++--------- crates/cdk/src/wallet/mint.rs | 3 ++- crates/cdk/src/wallet/mod.rs | 3 ++- crates/cdk/src/wallet/proofs.rs | 25 ++++++---------------- crates/cdk/src/wallet/receive.rs | 3 ++- crates/cdk/src/wallet/send.rs | 20 ++++++----------- crates/cdk/src/wallet/swap.rs | 10 ++++----- 15 files changed, 70 insertions(+), 87 deletions(-) diff --git a/crates/cdk-redb/src/mint/mod.rs b/crates/cdk-redb/src/mint/mod.rs index d53055b2..0ca822e9 100644 --- a/crates/cdk-redb/src/mint/mod.rs +++ b/crates/cdk-redb/src/mint/mod.rs @@ -10,6 +10,7 @@ use async_trait::async_trait; use cdk::cdk_database::MintDatabase; use cdk::dhke::hash_to_curve; use cdk::mint::{MintKeySetInfo, MintQuote}; +use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{ BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, Proof, Proofs, PublicKey, State, @@ -603,10 +604,7 @@ impl MintDatabase for MintRedbDatabase { .filter(|p| &p.keyset_id == keyset_id) .collect::(); - let proof_ys = proofs_for_id - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let proof_ys = proofs_for_id.ys()?; assert_eq!(proofs_for_id.len(), proof_ys.len()); diff --git a/crates/cdk-sqlite/src/mint/mod.rs b/crates/cdk-sqlite/src/mint/mod.rs index 20fc70bb..c72e5220 100644 --- a/crates/cdk-sqlite/src/mint/mod.rs +++ b/crates/cdk-sqlite/src/mint/mod.rs @@ -10,6 +10,7 @@ use bitcoin::bip32::DerivationPath; use cdk::cdk_database::{self, MintDatabase}; use cdk::mint::{MintKeySetInfo, MintQuote}; use cdk::mint_url::MintUrl; +use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::nut05::QuoteState; use cdk::nuts::{ BlindSignature, BlindSignatureDleq, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, @@ -838,10 +839,7 @@ WHERE quote_id=?; .map(sqlite_row_to_proof) .collect::, _>>()?; - proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()? + proofs.ys()? } Err(err) => match err { sqlx::Error::RowNotFound => { diff --git a/crates/cdk/src/cdk_database/mint_memory.rs b/crates/cdk/src/cdk_database/mint_memory.rs index 5ae612aa..3b4e1e79 100644 --- a/crates/cdk/src/cdk_database/mint_memory.rs +++ b/crates/cdk/src/cdk_database/mint_memory.rs @@ -9,6 +9,7 @@ use tokio::sync::{Mutex, RwLock}; use super::{Error, MintDatabase}; use crate::dhke::hash_to_curve; use crate::mint::{self, MintKeySetInfo, MintQuote}; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut07::State; use crate::nuts::{ nut07, BlindSignature, CurrencyUnit, Id, MeltBolt11Request, MeltQuoteState, MintQuoteState, @@ -346,10 +347,7 @@ impl MintDatabase for MintMemoryDatabase { .cloned() .collect(); - let proof_ys = proofs_for_id - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let proof_ys = proofs_for_id.ys()?; assert_eq!(proofs_for_id.len(), proof_ys.len()); diff --git a/crates/cdk/src/mint/melt.rs b/crates/cdk/src/mint/melt.rs index a46452de..f187d282 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -8,7 +8,7 @@ use tracing::instrument; use crate::cdk_lightning; use crate::cdk_lightning::MintLightning; use crate::cdk_lightning::PayInvoiceResponse; -use crate::dhke::hash_to_curve; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut11::enforce_sig_flag; use crate::nuts::nut11::EnforceSigFlag; use crate::{ @@ -264,11 +264,7 @@ impl Mint { } } - let ys = melt_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let ys = melt_request.inputs.ys()?; // Ensure proofs are unique and not being double spent if melt_request.inputs.len() != ys.iter().collect::>().len() { @@ -374,11 +370,7 @@ impl Mint { /// quote should be unpaid #[instrument(skip_all)] pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> { - let input_ys = melt_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let input_ys = melt_request.inputs.ys()?; self.localstore .update_proofs_states(&input_ys, State::Unspent) @@ -615,11 +607,7 @@ impl Mint { .await? .ok_or(Error::UnknownQuote)?; - let input_ys = melt_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let input_ys = melt_request.inputs.ys()?; self.localstore .update_proofs_states(&input_ys, State::Spent) diff --git a/crates/cdk/src/mint/swap.rs b/crates/cdk/src/mint/swap.rs index f763fff1..16a72afe 100644 --- a/crates/cdk/src/mint/swap.rs +++ b/crates/cdk/src/mint/swap.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use tracing::instrument; -use crate::dhke::hash_to_curve; +use crate::nuts::nut00::ProofsMethods; use crate::Error; use super::nut11::{enforce_sig_flag, EnforceSigFlag}; @@ -59,11 +59,7 @@ impl Mint { let proof_count = swap_request.inputs.len(); - let input_ys = swap_request - .inputs - .iter() - .map(|p| hash_to_curve(&p.secret.to_bytes())) - .collect::, _>>()?; + let input_ys = swap_request.inputs.ys()?; self.localstore .add_proofs(swap_request.inputs.clone(), None) diff --git a/crates/cdk/src/nuts/nut00/mod.rs b/crates/cdk/src/nuts/nut00/mod.rs index 98223792..5b03a185 100644 --- a/crates/cdk/src/nuts/nut00/mod.rs +++ b/crates/cdk/src/nuts/nut00/mod.rs @@ -29,6 +29,28 @@ pub use token::{Token, TokenV3, TokenV4}; /// List of [Proof] pub type Proofs = Vec; +/// Utility methods for [Proofs] +pub trait ProofsMethods { + /// Try to sum up the amounts of all [Proof]s + fn total_amount(&self) -> Result; + + /// Try to fetch the pubkeys of all [Proof]s + fn ys(&self) -> Result, Error>; +} + +impl ProofsMethods for Proofs { + fn total_amount(&self) -> Result { + Amount::try_sum(self.iter().map(|p| p.amount)).map_err(Into::into) + } + + fn ys(&self) -> Result, Error> { + self.iter() + .map(|p| p.y()) + .collect::, _>>() + .map_err(Into::into) + } +} + /// NUT00 Error #[derive(Debug, Error)] pub enum Error { diff --git a/crates/cdk/src/nuts/nut00/token.rs b/crates/cdk/src/nuts/nut00/token.rs index f4616620..8fe9e99f 100644 --- a/crates/cdk/src/nuts/nut00/token.rs +++ b/crates/cdk/src/nuts/nut00/token.rs @@ -13,6 +13,7 @@ use url::Url; use super::{Error, Proof, ProofV4, Proofs}; use crate::mint_url::MintUrl; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::{CurrencyUnit, Id}; use crate::Amount; @@ -211,7 +212,7 @@ impl TokenV3 { Ok(Amount::try_sum( self.token .iter() - .map(|t| Amount::try_sum(t.proofs.iter().map(|p| p.amount))) + .map(|t| t.proofs.total_amount()) .collect::, _>>()?, )?) } diff --git a/crates/cdk/src/types.rs b/crates/cdk/src/types.rs index 60c360fe..213a8e7e 100644 --- a/crates/cdk/src/types.rs +++ b/crates/cdk/src/types.rs @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::error::Error; use crate::mint_url::MintUrl; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ CurrencyUnit, MeltQuoteState, PaymentMethod, Proof, Proofs, PublicKey, SpendingConditions, State, @@ -34,9 +35,9 @@ impl Melted { proofs: Proofs, change_proofs: Option, ) -> Result { - let proofs_amount = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let proofs_amount = proofs.total_amount()?; let change_amount = match &change_proofs { - Some(change_proofs) => Amount::try_sum(change_proofs.iter().map(|p| p.amount))?, + Some(change_proofs) => change_proofs.total_amount()?, None => Amount::ZERO, }; let fee_paid = proofs_amount diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index c7217c28..37b3f71e 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -3,9 +3,10 @@ use std::str::FromStr; use lightning_invoice::Bolt11Invoice; use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::{ dhke::construct_proofs, - nuts::{CurrencyUnit, MeltQuoteBolt11Response, PreMintSecrets, Proofs, PublicKey, State}, + nuts::{CurrencyUnit, MeltQuoteBolt11Response, PreMintSecrets, Proofs, State}, types::{Melted, ProofInfo}, util::unix_time, Amount, Error, Wallet, @@ -121,15 +122,12 @@ impl Wallet { return Err(Error::UnknownQuote); }; - let proofs_total = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let proofs_total = proofs.total_amount()?; if proofs_total < quote_info.amount + quote_info.fee_reserve { return Err(Error::InsufficientFunds); } - let ys = proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let ys = proofs.ys()?; self.localstore.set_pending_proofs(ys).await?; let active_keyset_id = self.get_active_mint_keyset().await?.id; @@ -213,7 +211,7 @@ impl Wallet { Some(change_proofs) => { tracing::debug!( "Change amount returned from melt: {}", - Amount::try_sum(change_proofs.iter().map(|p| p.amount))? + change_proofs.total_amount()? ); // Update counter for keyset @@ -238,10 +236,7 @@ impl Wallet { self.localstore.remove_melt_quote("e_info.id).await?; - let deleted_ys = proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let deleted_ys = proofs.ys()?; self.localstore .update_proofs(change_proof_infos, deleted_ys) .await?; diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 6b241452..766f40c8 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -1,6 +1,7 @@ use tracing::instrument; use super::MintQuote; +use crate::nuts::nut00::ProofsMethods; use crate::{ amount::SplitTarget, dhke::construct_proofs, @@ -242,7 +243,7 @@ impl Wallet { &keys, )?; - let minted_amount = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let minted_amount = proofs.total_amount()?; // Remove filled quote from store self.localstore.remove_mint_quote("e_info.id).await?; diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index 93e1b76b..174a83a8 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -35,6 +35,7 @@ mod swap; pub mod types; pub mod util; +use crate::nuts::nut00::ProofsMethods; pub use multi_mint_wallet::MultiMintWallet; pub use types::{MeltQuote, MintQuote, SendKind}; @@ -327,7 +328,7 @@ impl Wallet { .cloned() .collect(); - restored_value += Amount::try_sum(unspent_proofs.iter().map(|p| p.amount))?; + restored_value += unspent_proofs.total_amount()?; let unspent_proofs = unspent_proofs .into_iter() diff --git a/crates/cdk/src/wallet/proofs.rs b/crates/cdk/src/wallet/proofs.rs index 9d652f17..79764a73 100644 --- a/crates/cdk/src/wallet/proofs.rs +++ b/crates/cdk/src/wallet/proofs.rs @@ -2,9 +2,9 @@ use std::collections::HashSet; use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::{ amount::SplitTarget, - dhke::hash_to_curve, nuts::{Proof, ProofState, Proofs, PublicKey, State}, types::ProofInfo, Amount, Error, Wallet, @@ -73,11 +73,7 @@ impl Wallet { /// Checks the stats of [`Proofs`] swapping for a new [`Proof`] if unspent #[instrument(skip(self, proofs))] pub async fn reclaim_unspent(&self, proofs: Proofs) -> Result<(), Error> { - let proof_ys = proofs - .iter() - // Find Y for the secret - .map(|p| hash_to_curve(p.secret.as_bytes())) - .collect::, _>>()?; + let proof_ys = proofs.ys()?; let spendable = self .client @@ -102,14 +98,7 @@ impl Wallet { pub async fn check_proofs_spent(&self, proofs: Proofs) -> Result, Error> { let spendable = self .client - .post_check_state( - self.mint_url.clone().try_into()?, - proofs - .iter() - // Find Y for the secret - .map(|p| hash_to_curve(p.secret.as_bytes())) - .collect::, _>>()?, - ) + .post_check_state(self.mint_url.clone().try_into()?, proofs.ys()?) .await?; let spent_ys: Vec<_> = spendable .states @@ -186,7 +175,7 @@ impl Wallet { ) -> Result { // TODO: Check all proofs are same unit - if Amount::try_sum(proofs.iter().map(|p| p.amount))? < amount { + if proofs.total_amount()? < amount { return Err(Error::InsufficientFunds); } @@ -226,7 +215,7 @@ impl Wallet { } remaining_amount = amount.checked_add(fees).ok_or(Error::AmountOverflow)? - - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + - selected_proofs.total_amount()?; (proofs_larger, proofs_smaller) = proofs_smaller .into_iter() .skip(1) @@ -262,7 +251,7 @@ impl Wallet { for inactive_proof in inactive_proofs { selected_proofs.push(inactive_proof); - let selected_total = Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_total = selected_proofs.total_amount()?; let fees = self.get_proofs_fee(&selected_proofs).await?; if selected_total >= amount + fees { @@ -274,7 +263,7 @@ impl Wallet { for active_proof in active_proofs { selected_proofs.push(active_proof); - let selected_total = Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_total = selected_proofs.total_amount()?; let fees = self.get_proofs_fee(&selected_proofs).await?; if selected_total >= amount + fees { diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index 88dc4a08..898eb40c 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -4,6 +4,7 @@ use bitcoin::hashes::Hash; use bitcoin::{hashes::sha256::Hash as Sha256Hash, XOnlyPublicKey}; use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut10::Kind; use crate::nuts::{Conditions, Token}; use crate::{ @@ -148,7 +149,7 @@ impl Wallet { .increment_keyset_counter(&active_keyset_id, recv_proofs.len() as u32) .await?; - let total_amount = Amount::try_sum(recv_proofs.iter().map(|p| p.amount))?; + let total_amount = recv_proofs.total_amount()?; let recv_proof_infos = recv_proofs .into_iter() diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index a3024bde..1e8b99fc 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -1,8 +1,9 @@ use tracing::instrument; +use crate::nuts::nut00::ProofsMethods; use crate::{ amount::SplitTarget, - nuts::{Proofs, PublicKey, SpendingConditions, State, Token}, + nuts::{Proofs, SpendingConditions, State, Token}, Amount, Error, Wallet, }; @@ -12,10 +13,7 @@ impl Wallet { /// Send specific proofs #[instrument(skip(self))] pub async fn send_proofs(&self, memo: Option, proofs: Proofs) -> Result { - let ys = proofs - .iter() - .map(|p| p.y()) - .collect::, _>>()?; + let ys = proofs.ys()?; self.localstore.reserve_proofs(ys).await?; Ok(Token::new( @@ -114,8 +112,7 @@ impl Wallet { let send_proofs: Proofs = match (send_kind, selected, conditions.clone()) { // Handle exact matches offline (SendKind::OfflineExact, Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, @@ -131,8 +128,7 @@ impl Wallet { // Handle exact matches (SendKind::OnlineExact, Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, @@ -152,8 +148,7 @@ impl Wallet { // Handle offline tolerance (SendKind::OfflineTolerance(tolerance), Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, @@ -178,8 +173,7 @@ impl Wallet { // Handle online tolerance with successful selection (SendKind::OnlineTolerance(tolerance), Ok(selected_proofs), _) => { - let selected_proofs_amount = - Amount::try_sum(selected_proofs.iter().map(|p| p.amount))?; + let selected_proofs_amount = selected_proofs.total_amount()?; let amount_to_send = match include_fees { true => amount + self.get_proofs_fee(&selected_proofs).await?, false => amount, diff --git a/crates/cdk/src/wallet/swap.rs b/crates/cdk/src/wallet/swap.rs index 7f59866a..82274fb9 100644 --- a/crates/cdk/src/wallet/swap.rs +++ b/crates/cdk/src/wallet/swap.rs @@ -2,6 +2,7 @@ use tracing::instrument; use crate::amount::SplitTarget; use crate::dhke::construct_proofs; +use crate::nuts::nut00::ProofsMethods; use crate::nuts::nut10; use crate::nuts::PreMintSecrets; use crate::nuts::PreSwap; @@ -85,8 +86,7 @@ impl Wallet { let mut proofs_to_keep = Vec::new(); for proof in all_proofs { - let proofs_to_send_amount = - Amount::try_sum(proofs_to_send.iter().map(|p| p.amount))?; + let proofs_to_send_amount = proofs_to_send.total_amount()?; if proof.amount + proofs_to_send_amount <= amount + pre_swap.fee { proofs_to_send.push(proof); } else { @@ -98,7 +98,7 @@ impl Wallet { } }; - let send_amount = Amount::try_sum(proofs_to_send.iter().map(|p| p.amount))?; + let send_amount = proofs_to_send.total_amount()?; if send_amount.ne(&(amount + pre_swap.fee)) { tracing::warn!( @@ -199,9 +199,9 @@ impl Wallet { let active_keyset_id = self.get_active_mint_keyset().await?.id; // Desired amount is either amount passed or value of all proof - let proofs_total = Amount::try_sum(proofs.iter().map(|p| p.amount))?; + let proofs_total = proofs.total_amount()?; - let ys: Vec = proofs.iter().map(|p| p.y()).collect::>()?; + let ys: Vec = proofs.ys()?; self.localstore.set_pending_proofs(ys).await?; let fee = self.get_proofs_fee(&proofs).await?; From 84428a9503ff853f90aa877bba6b971034e1365f Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Sun, 20 Oct 2024 10:52:44 +0100 Subject: [PATCH 6/6] feat: verify mint keyset id when getting keys --- crates/cdk/src/nuts/nut02.rs | 16 ++++++++++++++++ crates/cdk/src/wallet/keysets.rs | 2 ++ 2 files changed, 18 insertions(+) diff --git a/crates/cdk/src/nuts/nut02.rs b/crates/cdk/src/nuts/nut02.rs index f3885593..6a6656e8 100644 --- a/crates/cdk/src/nuts/nut02.rs +++ b/crates/cdk/src/nuts/nut02.rs @@ -43,6 +43,9 @@ pub enum Error { /// Unknown version #[error("NUT02: Unknown Version")] UnknownVersion, + /// Keyset id does not match + #[error("Keyset id incorrect")] + IncorrectKeysetId, /// Slice Error #[error(transparent)] Slice(#[from] TryFromSliceError), @@ -242,6 +245,19 @@ pub struct KeySet { pub keys: Keys, } +impl KeySet { + /// Verify the keyset is matches keys + pub fn verify_id(&self) -> Result<(), Error> { + let keys_id: Id = (&self.keys).into(); + + if keys_id != self.id { + return Err(Error::IncorrectKeysetId); + } + + Ok(()) + } +} + #[cfg(feature = "mint")] impl From for KeySet { fn from(keyset: MintKeySet) -> Self { diff --git a/crates/cdk/src/wallet/keysets.rs b/crates/cdk/src/wallet/keysets.rs index a8c1f27a..5a7f680e 100644 --- a/crates/cdk/src/wallet/keysets.rs +++ b/crates/cdk/src/wallet/keysets.rs @@ -21,6 +21,8 @@ impl Wallet { .get_mint_keyset(self.mint_url.clone().try_into()?, keyset_id) .await?; + keys.verify_id()?; + self.localstore.add_keys(keys.keys.clone()).await?; keys.keys