From 2023939a9eb4fa9d1b1cbb88bb5907c75550caae Mon Sep 17 00:00:00 2001 From: vnprc Date: Fri, 14 Feb 2025 17:35:15 -0500 Subject: [PATCH 1/3] feat: BYO network transport --- crates/cashu/src/amount.rs | 10 + crates/cdk-common/src/database/wallet.rs | 14 ++ crates/cdk-redb/src/wallet/mod.rs | 18 +- crates/cdk-sqlite/src/wallet/mod.rs | 19 +- crates/cdk/src/cdk_database/wallet_memory.rs | 22 +++ crates/cdk/src/wallet/keysets.rs | 130 +++++++++++++ crates/cdk/src/wallet/mint.rs | 194 ++++++++++++++++++- 7 files changed, 403 insertions(+), 4 deletions(-) diff --git a/crates/cashu/src/amount.rs b/crates/cashu/src/amount.rs index d2c599d04..0e602271a 100644 --- a/crates/cashu/src/amount.rs +++ b/crates/cashu/src/amount.rs @@ -228,6 +228,16 @@ impl AmountStr { pub(crate) fn from(amt: Amount) -> Self { Self(amt) } + + pub fn inner(&self) -> Amount { + self.0 + } +} + +impl From for AmountStr { + fn from(amt: Amount) -> Self { + Self(amt) + } } impl PartialOrd for AmountStr { diff --git a/crates/cdk-common/src/database/wallet.rs b/crates/cdk-common/src/database/wallet.rs index e9213a1e3..3c8ac7911 100644 --- a/crates/cdk-common/src/database/wallet.rs +++ b/crates/cdk-common/src/database/wallet.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fmt::Debug; use async_trait::async_trait; +use cashu::PreMintSecrets; use super::Error; use crate::common::ProofInfo; @@ -117,4 +118,17 @@ pub trait Database: Debug { verifying_key: PublicKey, last_checked: u32, ) -> Result<(), Self::Err>; + + /// Store premint secrets + async fn add_premint_secrets( + &self, + quote_id: &str, + premint_secrets: &PreMintSecrets, + ) -> Result<(), Self::Err>; + + /// Retrieve premint secrets + async fn get_premint_secrets( + &self, + quote_id: &str, + ) -> Result, Self::Err>; } diff --git a/crates/cdk-redb/src/wallet/mod.rs b/crates/cdk-redb/src/wallet/mod.rs index ff7a84788..72c1a52ee 100644 --- a/crates/cdk-redb/src/wallet/mod.rs +++ b/crates/cdk-redb/src/wallet/mod.rs @@ -13,7 +13,8 @@ use cdk_common::mint_url::MintUrl; use cdk_common::util::unix_time; use cdk_common::wallet::{self, MintQuote}; use cdk_common::{ - database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PublicKey, SpendingConditions, State, + database, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, PublicKey, + SpendingConditions, State, }; use redb::{Database, MultimapTableDefinition, ReadableTable, TableDefinition}; use tracing::instrument; @@ -740,4 +741,19 @@ impl WalletDatabase for WalletRedbDatabase { Ok(()) } + + async fn add_premint_secrets( + &self, + quote_id: &str, + premint_secrets: &PreMintSecrets, + ) -> Result<(), Self::Err> { + todo!() + } + + async fn get_premint_secrets( + &self, + quote_id: &str, + ) -> Result, Self::Err> { + todo!() + } } diff --git a/crates/cdk-sqlite/src/wallet/mod.rs b/crates/cdk-sqlite/src/wallet/mod.rs index fa2bb4899..b1445faf1 100644 --- a/crates/cdk-sqlite/src/wallet/mod.rs +++ b/crates/cdk-sqlite/src/wallet/mod.rs @@ -12,8 +12,8 @@ use cdk_common::nuts::{MeltQuoteState, MintQuoteState}; use cdk_common::secret::Secret; use cdk_common::wallet::{self, MintQuote}; use cdk_common::{ - database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, Proof, PublicKey, SecretKey, - SpendingConditions, State, + database, Amount, CurrencyUnit, Id, KeySetInfo, Keys, MintInfo, PreMintSecrets, Proof, + PublicKey, SecretKey, SpendingConditions, State, }; use error::Error; use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqliteRow}; @@ -778,6 +778,21 @@ VALUES (?, ?); Ok(()) } + + async fn add_premint_secrets( + &self, + quote_id: &str, + premint_secrets: &PreMintSecrets, + ) -> Result<(), Self::Err> { + todo!() + } + + async fn get_premint_secrets( + &self, + quote_id: &str, + ) -> Result, Self::Err> { + todo!() + } } fn sqlite_row_to_mint_info(row: &SqliteRow) -> Result { diff --git a/crates/cdk/src/cdk_database/wallet_memory.rs b/crates/cdk/src/cdk_database/wallet_memory.rs index 5c2785db4..f8f711e45 100644 --- a/crates/cdk/src/cdk_database/wallet_memory.rs +++ b/crates/cdk/src/cdk_database/wallet_memory.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use async_trait::async_trait; use cdk_common::database::{Error, WalletDatabase}; +use cdk_common::PreMintSecrets; use tokio::sync::RwLock; use crate::mint_url::MintUrl; @@ -28,6 +29,7 @@ pub struct WalletMemoryDatabase { proofs: Arc>>, keyset_counter: Arc>>, nostr_last_checked: Arc>>, + premint_secrets: Arc>>, } impl WalletMemoryDatabase { @@ -38,6 +40,7 @@ impl WalletMemoryDatabase { mint_keys: Vec, keyset_counter: HashMap, nostr_last_checked: HashMap, + premint_secrets: HashMap, ) -> Self { Self { mints: Arc::new(RwLock::new(HashMap::new())), @@ -55,6 +58,7 @@ impl WalletMemoryDatabase { proofs: Arc::new(RwLock::new(HashMap::new())), keyset_counter: Arc::new(RwLock::new(keyset_counter)), nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)), + premint_secrets: Arc::new(RwLock::new(premint_secrets)), } } } @@ -353,4 +357,22 @@ impl WalletDatabase for WalletMemoryDatabase { Ok(()) } + + async fn add_premint_secrets( + &self, + quote_id: &str, + premint_secrets: &PreMintSecrets, + ) -> Result<(), Self::Err> { + self.premint_secrets + .write() + .await + .insert(quote_id.to_string(), premint_secrets.clone()); + Ok(()) + } + async fn get_premint_secrets( + &self, + quote_id: &str, + ) -> Result, Self::Err> { + Ok(self.premint_secrets.read().await.get(quote_id).cloned()) + } } diff --git a/crates/cdk/src/wallet/keysets.rs b/crates/cdk/src/wallet/keysets.rs index a605a060c..de692f514 100644 --- a/crates/cdk/src/wallet/keysets.rs +++ b/crates/cdk/src/wallet/keysets.rs @@ -25,6 +25,31 @@ impl Wallet { Ok(keys) } + /// Straight up yolo those keys into the db no cap + pub async fn add_keyset( + &self, + keys: Keys, + active: bool, + input_fee_ppk: u64, + ) -> Result<(), Error> { + // add to localstore + self.localstore.add_keys(keys.clone()).await?; + + let keyset_info = KeySetInfo { + id: Id::from(&keys), + active, + unit: self.unit.clone(), + input_fee_ppk, + }; + + // somehow also add to localstore...why do I need to make two different calls? + self.localstore + .add_mint_keysets(self.mint_url.clone(), vec![keyset_info]) + .await?; + + Ok(()) + } + /// Get keysets for mint /// /// Queries mint for all keysets @@ -97,4 +122,109 @@ impl Wallet { .ok_or(Error::NoActiveKeyset)?; Ok(keyset_with_lowest_fee) } + + /// Get active keyset for mint from local without querying the mint + #[instrument(skip(self))] + pub async fn get_active_mint_keyset_local(&self) -> Result { + let active_keysets = match self + .localstore + .get_mint_keysets(self.mint_url.clone()) + .await? + { + Some(keysets) => keysets + .into_iter() + .filter(|k| k.active && k.unit == self.unit) + .collect::>(), + None => { + vec![] + } + }; + + let keyset_with_lowest_fee = active_keysets + .into_iter() + .min_by_key(|key| key.input_fee_ppk) + .ok_or(Error::NoActiveKeyset)?; + + Ok(keyset_with_lowest_fee) + } +} + +#[cfg(test)] +mod test { + use crate::cdk_database::WalletMemoryDatabase; + use crate::nuts; + use crate::Wallet; + use bitcoin::bip32::DerivationPath; + use bitcoin::bip32::Xpriv; + use bitcoin::key::Secp256k1; + use cdk_common::KeySet; + use cdk_common::KeySetInfo; + use cdk_common::MintKeySet; + use nuts::CurrencyUnit; + use std::sync::Arc; + + fn create_new_keyset() -> (KeySet, KeySetInfo) { + let secp = Secp256k1::new(); + let seed = [0u8; 32]; // Default seed for testing + let xpriv = Xpriv::new_master(bitcoin::Network::Bitcoin, &seed).expect("RNG busted"); + + let derivation_path = DerivationPath::default(); + let unit = CurrencyUnit::Custom("HASH".to_string()); + let max_order = 64; + + let keyset: KeySet = MintKeySet::generate( + &secp, + xpriv + .derive_priv(&secp, &derivation_path) + .expect("RNG busted"), + unit.clone(), + max_order, + ) + .into(); + + let keyset_info = KeySetInfo { + id: keyset.id, + unit: keyset.unit.clone(), + active: true, + input_fee_ppk: 0, + }; + + (keyset, keyset_info) + } + + fn create_wallet() -> Wallet { + use rand::Rng; + + let seed = rand::thread_rng().gen::<[u8; 32]>(); + let mint_url = "https://testnut.cashu.space"; + + let localstore = WalletMemoryDatabase::default(); + Wallet::new( + mint_url, + CurrencyUnit::Custom("HASH".to_string()), + Arc::new(localstore), + &seed, + None, + ) + .unwrap() + } + + #[tokio::test] + async fn test_add_and_get_active_mint_keysets_local() { + let (keyset, keyset_info) = create_new_keyset(); + + let wallet = create_wallet(); + + // Add the keyset + wallet.add_keyset(keyset.keys, true, 0).await.unwrap(); + + // Retrieve the keysets locally + let active_keyset = wallet.get_active_mint_keyset_local().await.unwrap(); + + // Validate the retrieved keyset + assert_eq!(active_keyset.id, keyset_info.id); + assert_eq!(active_keyset.active, keyset_info.active); + assert_eq!(active_keyset.unit, keyset_info.unit); + assert_eq!(active_keyset.input_fee_ppk, keyset_info.input_fee_ppk); + } } diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index e340cca3e..efe13ca2c 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -1,3 +1,4 @@ +use cdk_common::{BlindSignature, CurrencyUnit}; use tracing::instrument; use super::MintQuote; @@ -5,7 +6,7 @@ use crate::amount::SplitTarget; use crate::dhke::construct_proofs; use crate::nuts::nut00::ProofsMethods; use crate::nuts::{ - nut12, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, + nut12, Id, MintBolt11Request, MintQuoteBolt11Request, MintQuoteBolt11Response, PreMintSecrets, Proofs, SecretKey, SpendingConditions, State, }; use crate::types::ProofInfo; @@ -289,4 +290,195 @@ impl Wallet { Ok(proofs) } + + /// Generates blinded secrets to send to the mint for signing. This function + /// is appropriate if the caller is providing their own network + /// transport. Otherwise use `mint`, which makes a network request to + /// the mint. + /// + /// # Parameters + /// + /// - `&self`: A reference to the current instance + /// - `active_keyset_id`: The ID of the active keyset + /// - `quote_info_amount`: The amount to be minted + /// - `amount_split_target`: Strategy for splitting amount into discrete + /// tokens + /// - `spending_conditions`: Optional spending conditions to apply to the + /// minted tokens + /// - `count`: How many tokens were previously generated from this keyset + + /// 1 + /// + /// # Returns + /// + /// A `Result` containing `PreMintSecrets` if successful, or an `Error` + /// otherwise. + /// + /// # Errors + /// + /// This function will return an error if the creation of `PreMintSecrets` + /// fails. + /// + /// ``` + pub fn generate_premint_secrets( + &self, + active_keyset_id: Id, + quote_info_amount: Amount, + amount_split_target: &SplitTarget, + spending_conditions: Option<&SpendingConditions>, + count: u32, + ) -> Result { + let premint_secrets = match &spending_conditions { + Some(spending_conditions) => PreMintSecrets::with_conditions( + active_keyset_id, + quote_info_amount, + amount_split_target, + spending_conditions, + )?, + None => PreMintSecrets::from_xpriv( + active_keyset_id, + count, + self.xpriv, + quote_info_amount, + amount_split_target, + )?, + }; + + Ok(premint_secrets) + } + + pub async fn gen_ehash_premint_secrets( + &self, + amount: u64, + quote_id: &str, + mint_url: &str, + ) -> Result { + // check for existing quote + if let Some(_) = self.localstore.get_mint_quote(quote_id).await? { + return Err(Error::PaidQuote); + } + self.localstore + .add_mint_quote(MintQuote { + id: quote_id.to_string(), + mint_url: mint_url.parse()?, + amount: Amount::from(amount), + unit: CurrencyUnit::Custom("HASH".to_string()), + // TODO what to put here? needs to identify the mining share + // probably channel_id:sequence_number + request: "todo".to_string(), + state: MintQuoteState::Paid, + // TODO should we set an expiry? + expiry: u64::MAX, + secret_key: None, + }) + .await?; + + let active_keyset_id = self.get_active_mint_keyset_local().await?.id; + + let count = self + .localstore + .get_keyset_counter(&active_keyset_id) + .await?; + + let count = count.map_or(0, |c| c + 1); + + let premint_secrets = self.generate_premint_secrets( + active_keyset_id, + // TODO when do we want to set amount? + Amount::from(amount), + &SplitTarget::None, + None, + count, + )?; + + self.localstore + .add_premint_secrets(quote_id, &premint_secrets) + .await?; + + Ok(premint_secrets) + } + + pub async fn gen_ehash_proofs( + &self, + signatures: [Option; 64], + quote_id: &str, + ) -> Result { + // TODO pass this in, it will break if the keyset changes before getting proofs + let active_keyset_id = self.get_active_mint_keyset_local().await?.id; + let keys = self.get_keyset_keys(active_keyset_id).await?; + let premint_secrets = match self.localstore.get_premint_secrets(quote_id).await? { + Some(premint_secrets) => premint_secrets, + None => return Err(Error::UnknownQuote), + }; + + if premint_secrets.keyset_id != active_keyset_id { + return Err(Error::UnknownKeySet); + } + + let mut verified_signatures = Vec::new(); + let mut premint_rs = Vec::new(); + let mut premint_secrets_vec = Vec::new(); + + // verify each signature + for sig_opt in signatures.iter() { + if let Some(sig) = sig_opt { + // Find the secret corresponding to the signature using amount field + if let Some(matching_secret) = premint_secrets + .secrets + .iter() + .find(|secret| secret.amount == sig.amount) + { + // Ensure the keyset for the signature matches the active keyset + let keys = self.get_keyset_keys(sig.keyset_id).await?; + let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; + + match sig.verify_dleq(key, matching_secret.blinded_message.blinded_secret) { + Ok(_) | Err(nut12::Error::MissingDleqProof) => { + // Add verified signature and related premint data + verified_signatures.push(sig.clone()); + premint_rs.push(matching_secret.r.clone()); + premint_secrets_vec.push(matching_secret.secret.clone()); + } + Err(_) => return Err(Error::CouldNotVerifyDleq), + } + } else { + return Err(Error::Custom(String::from("Secret not found"))); + } + } + } + + // Construct proofs + let proofs = construct_proofs(verified_signatures, premint_rs, premint_secrets_vec, &keys)?; + // TODO rebase to latest master and remove this (proofs are returned now) + println!("proofs {:?}", proofs); + + let minted_amount = proofs.total_amount()?; + + // update keyset token counter + self.localstore + .increment_keyset_counter(&active_keyset_id, proofs.len() as u32) + .await?; + + let proofs = proofs + .into_iter() + .map(|proof| { + ProofInfo::new( + proof, + self.mint_url.clone(), + State::Unspent, + CurrencyUnit::Custom("HASH".to_string()), + ) + }) + .collect::, _>>()?; + + // store new proofs in wallet + self.localstore + .update_proofs(proofs.clone(), vec![]) + .await?; + + // Remove Quote + // TODO handle result + self.localstore.remove_mint_quote(quote_id).await; + + Ok(minted_amount) + } } From 1f33ba2d684685ddc36c1352e3d82fad85752fe9 Mon Sep 17 00:00:00 2001 From: vnprc Date: Sun, 16 Feb 2025 14:28:14 -0500 Subject: [PATCH 2/3] fix: code cleanup - rename gen_ehash_premint_secrets to create_premint_secrets - rename gen_ehash_proofs to verify_and_store_proofs - make generate_premint_secrets private instead of public - add CurrencyUnit function param to create_premint_secrets - other code readability improvements --- crates/cdk/src/wallet/keysets.rs | 4 +- crates/cdk/src/wallet/mint.rs | 190 ++++++++++++++++--------------- 2 files changed, 101 insertions(+), 93 deletions(-) diff --git a/crates/cdk/src/wallet/keysets.rs b/crates/cdk/src/wallet/keysets.rs index de692f514..9b48fa840 100644 --- a/crates/cdk/src/wallet/keysets.rs +++ b/crates/cdk/src/wallet/keysets.rs @@ -25,14 +25,13 @@ impl Wallet { Ok(keys) } - /// Straight up yolo those keys into the db no cap + /// Add a keyset to the local database and update keyset info pub async fn add_keyset( &self, keys: Keys, active: bool, input_fee_ppk: u64, ) -> Result<(), Error> { - // add to localstore self.localstore.add_keys(keys.clone()).await?; let keyset_info = KeySetInfo { @@ -42,7 +41,6 @@ impl Wallet { input_fee_ppk, }; - // somehow also add to localstore...why do I need to make two different calls? self.localstore .add_mint_keysets(self.mint_url.clone(), vec![keyset_info]) .await?; diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index efe13ca2c..21553956c 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -1,5 +1,5 @@ use cdk_common::{BlindSignature, CurrencyUnit}; -use tracing::instrument; +use tracing::{instrument, warn}; use super::MintQuote; use crate::amount::SplitTarget; @@ -291,35 +291,7 @@ impl Wallet { Ok(proofs) } - /// Generates blinded secrets to send to the mint for signing. This function - /// is appropriate if the caller is providing their own network - /// transport. Otherwise use `mint`, which makes a network request to - /// the mint. - /// - /// # Parameters - /// - /// - `&self`: A reference to the current instance - /// - `active_keyset_id`: The ID of the active keyset - /// - `quote_info_amount`: The amount to be minted - /// - `amount_split_target`: Strategy for splitting amount into discrete - /// tokens - /// - `spending_conditions`: Optional spending conditions to apply to the - /// minted tokens - /// - `count`: How many tokens were previously generated from this keyset + - /// 1 - /// - /// # Returns - /// - /// A `Result` containing `PreMintSecrets` if successful, or an `Error` - /// otherwise. - /// - /// # Errors - /// - /// This function will return an error if the creation of `PreMintSecrets` - /// fails. - /// - /// ``` - pub fn generate_premint_secrets( + fn generate_premint_secrets( &self, active_keyset_id: Id, quote_info_amount: Amount, @@ -346,44 +318,65 @@ impl Wallet { Ok(premint_secrets) } - pub async fn gen_ehash_premint_secrets( + /// Creates and stores pre-mint secrets for a given quote. + /// + /// This function checks whether a mint quote already exists, creates a new quote if necessary, + /// generates pre-mint secrets, and stores them in the local wallet store. + /// + /// # Arguments + /// + /// * `amount` - The amount for which pre-mint secrets should be generated. + /// * `quote_id` - A unique identifier for the mint quote. + /// * `mint_url` - The URL of the mint. + /// * `currency_unit` - nut00 currency unit + /// + /// # Returns + /// + /// Returns a `PreMintSecrets` object containing the generated secrets. + /// + /// # Errors + /// + /// Returns an error if: + /// - The quote ID already exists (`Error::PaidQuote`). + /// - Adding the quote or pre-mint secrets fails. + pub async fn create_premint_secrets( &self, amount: u64, quote_id: &str, mint_url: &str, + currency_unit: CurrencyUnit, ) -> Result { - // check for existing quote - if let Some(_) = self.localstore.get_mint_quote(quote_id).await? { + // Ensure the quote does not already exist + if self.localstore.get_mint_quote(quote_id).await?.is_some() { return Err(Error::PaidQuote); } - self.localstore - .add_mint_quote(MintQuote { - id: quote_id.to_string(), - mint_url: mint_url.parse()?, - amount: Amount::from(amount), - unit: CurrencyUnit::Custom("HASH".to_string()), - // TODO what to put here? needs to identify the mining share - // probably channel_id:sequence_number - request: "todo".to_string(), - state: MintQuoteState::Paid, - // TODO should we set an expiry? - expiry: u64::MAX, - secret_key: None, - }) - .await?; + + // Create and store a new mint quote + let mint_quote = MintQuote { + id: quote_id.to_string(), + mint_url: mint_url.parse()?, + amount: Amount::from(amount), + unit: currency_unit, + request: "todo".to_string(), // TODO: what does request do? + state: MintQuoteState::Paid, + expiry: u64::MAX, // TODO: expiry param? + secret_key: None, + }; + + self.localstore.add_mint_quote(mint_quote).await?; let active_keyset_id = self.get_active_mint_keyset_local().await?.id; + // Retrieve the keyset counter, defaulting to 0 if not found let count = self .localstore .get_keyset_counter(&active_keyset_id) - .await?; - - let count = count.map_or(0, |c| c + 1); + .await? + .unwrap_or(0) + + 1; let premint_secrets = self.generate_premint_secrets( active_keyset_id, - // TODO when do we want to set amount? Amount::from(amount), &SplitTarget::None, None, @@ -397,7 +390,31 @@ impl Wallet { Ok(premint_secrets) } - pub async fn gen_ehash_proofs( + /// Verifies signatures, constructs proofs, and stores them in the wallet. + /// + /// This function: + /// - Ensures that the quote exists and the keyset matches. + /// - Verifies each provided signature against the pre-mint secrets. + /// - Constructs proofs based on verified signatures. + /// - Updates the keyset counter and stores the proofs. + /// - Removes the corresponding mint quote, logging a warning if removal fails. + /// + /// # Arguments + /// + /// * `signatures` - An array of optional `BlindSignature`s, each corresponding to a pre-minted secret. + /// * `quote_id` - The quote identifier associated with the signatures. + /// + /// # Returns + /// + /// Returns the total minted amount. + /// + /// # Errors + /// + /// Returns an error if: + /// - The quote is unknown or has an invalid keyset (`Error::UnknownQuote` or `Error::UnknownKeySet`). + /// - Signature verification fails (`Error::CouldNotVerifyDleq`). + /// - Constructing proofs fails. + pub async fn verify_and_store_proofs( &self, signatures: [Option; 64], quote_id: &str, @@ -405,10 +422,12 @@ impl Wallet { // TODO pass this in, it will break if the keyset changes before getting proofs let active_keyset_id = self.get_active_mint_keyset_local().await?.id; let keys = self.get_keyset_keys(active_keyset_id).await?; - let premint_secrets = match self.localstore.get_premint_secrets(quote_id).await? { - Some(premint_secrets) => premint_secrets, - None => return Err(Error::UnknownQuote), - }; + + let premint_secrets = self + .localstore + .get_premint_secrets(quote_id) + .await? + .ok_or(Error::UnknownQuote)?; if premint_secrets.keyset_id != active_keyset_id { return Err(Error::UnknownKeySet); @@ -419,65 +438,56 @@ impl Wallet { let mut premint_secrets_vec = Vec::new(); // verify each signature - for sig_opt in signatures.iter() { - if let Some(sig) = sig_opt { - // Find the secret corresponding to the signature using amount field - if let Some(matching_secret) = premint_secrets - .secrets - .iter() - .find(|secret| secret.amount == sig.amount) - { - // Ensure the keyset for the signature matches the active keyset - let keys = self.get_keyset_keys(sig.keyset_id).await?; - let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; - - match sig.verify_dleq(key, matching_secret.blinded_message.blinded_secret) { - Ok(_) | Err(nut12::Error::MissingDleqProof) => { - // Add verified signature and related premint data - verified_signatures.push(sig.clone()); - premint_rs.push(matching_secret.r.clone()); - premint_secrets_vec.push(matching_secret.secret.clone()); - } - Err(_) => return Err(Error::CouldNotVerifyDleq), - } - } else { - return Err(Error::Custom(String::from("Secret not found"))); + for sig in signatures.iter().flatten() { + let matching_secret = premint_secrets + .secrets + .iter() + .find(|secret| secret.amount == sig.amount) + .ok_or_else(|| Error::Custom("Secret not found".to_string()))?; + + let keys = self.get_keyset_keys(sig.keyset_id).await?; + let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; + + match sig.verify_dleq(key, matching_secret.blinded_message.blinded_secret) { + Ok(_) | Err(nut12::Error::MissingDleqProof) => { + // TODO can we clean up these clones? + verified_signatures.push(sig.clone()); + premint_rs.push(matching_secret.r.clone()); + premint_secrets_vec.push(matching_secret.secret.clone()); } + Err(_) => return Err(Error::CouldNotVerifyDleq), } } - // Construct proofs let proofs = construct_proofs(verified_signatures, premint_rs, premint_secrets_vec, &keys)?; - // TODO rebase to latest master and remove this (proofs are returned now) - println!("proofs {:?}", proofs); - let minted_amount = proofs.total_amount()?; - // update keyset token counter self.localstore .increment_keyset_counter(&active_keyset_id, proofs.len() as u32) .await?; - let proofs = proofs + let proofs: Vec = proofs .into_iter() .map(|proof| { ProofInfo::new( proof, self.mint_url.clone(), State::Unspent, + // TODO retrieve currency unit from quote CurrencyUnit::Custom("HASH".to_string()), ) }) - .collect::, _>>()?; + .collect::>()?; // store new proofs in wallet self.localstore .update_proofs(proofs.clone(), vec![]) .await?; - // Remove Quote - // TODO handle result - self.localstore.remove_mint_quote(quote_id).await; + // Remove Quote and warn log any error + if let Err(err) = self.localstore.remove_mint_quote(quote_id).await { + warn!("Failed to remove mint quote {}: {}", quote_id, err); + } Ok(minted_amount) } From b646927eb330e315d4986b9d8ce3127b17739eee Mon Sep 17 00:00:00 2001 From: vnprc Date: Mon, 17 Feb 2025 15:12:55 -0500 Subject: [PATCH 3/3] fix: retrieve currency unit from mint quote --- crates/cdk/src/wallet/mint.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/cdk/src/wallet/mint.rs b/crates/cdk/src/wallet/mint.rs index 21553956c..73ad06cc6 100644 --- a/crates/cdk/src/wallet/mint.rs +++ b/crates/cdk/src/wallet/mint.rs @@ -168,7 +168,7 @@ impl Wallet { /// let minted_amount = minted_proofs.total_amount()?; /// /// Ok(()) - /// } + /// ``` #[instrument(skip(self))] pub async fn mint( @@ -368,6 +368,7 @@ impl Wallet { let active_keyset_id = self.get_active_mint_keyset_local().await?.id; // Retrieve the keyset counter, defaulting to 0 if not found + // TODO do we need to update the counter if it was not returned? let count = self .localstore .get_keyset_counter(&active_keyset_id) @@ -419,6 +420,12 @@ impl Wallet { signatures: [Option; 64], quote_id: &str, ) -> Result { + let quote = self + .localstore + .get_mint_quote(quote_id) + .await? + .expect("No quote found"); + // TODO pass this in, it will break if the keyset changes before getting proofs let active_keyset_id = self.get_active_mint_keyset_local().await?.id; let keys = self.get_keyset_keys(active_keyset_id).await?; @@ -473,8 +480,7 @@ impl Wallet { proof, self.mint_url.clone(), State::Unspent, - // TODO retrieve currency unit from quote - CurrencyUnit::Custom("HASH".to_string()), + quote.unit.clone(), ) }) .collect::>()?;