diff --git a/crates/cashu/src/nuts/nut02.rs b/crates/cashu/src/nuts/nut02.rs index bfb3f07e..84e0859a 100644 --- a/crates/cashu/src/nuts/nut02.rs +++ b/crates/cashu/src/nuts/nut02.rs @@ -497,6 +497,28 @@ pub struct KeySetInfo { pub final_expiry: Option, } +/// List of [KeySetInfo] +pub type KeySetInfos = Vec; + +/// Utility methods for [KeySetInfos] +pub trait KeySetInfosMethods { + /// Filter for active keysets + fn active(&self) -> impl Iterator + '_; + + /// Filter keysets for specific unit + fn unit(&self, unit: CurrencyUnit) -> impl Iterator + '_; +} + +impl KeySetInfosMethods for KeySetInfos { + fn active(&self) -> impl Iterator + '_ { + self.iter().filter(|k| k.active) + } + + fn unit(&self, unit: CurrencyUnit) -> impl Iterator + '_ { + self.iter().filter(move |k| k.unit == unit) + } +} + fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/crates/cdk-cli/src/sub_commands/pay_request.rs b/crates/cdk-cli/src/sub_commands/pay_request.rs index 0da97609..dc4bddec 100644 --- a/crates/cdk-cli/src/sub_commands/pay_request.rs +++ b/crates/cdk-cli/src/sub_commands/pay_request.rs @@ -101,7 +101,7 @@ pub async fn pay_request( .await? { Some(keysets_info) => keysets_info, - None => matching_wallet.get_mint_keysets().await?, // Hit the keysets endpoint if we don't have the keysets for this Mint + None => matching_wallet.load_mint_keysets().await?, // Hit the keysets endpoint if we don't have the keysets for this Mint }; let proofs = token.proofs(&keysets_info)?; diff --git a/crates/cdk-integration-tests/tests/bolt12.rs b/crates/cdk-integration-tests/tests/bolt12.rs index 5a265c89..fc279885 100644 --- a/crates/cdk-integration-tests/tests/bolt12.rs +++ b/crates/cdk-integration-tests/tests/bolt12.rs @@ -277,7 +277,7 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> { assert_eq!(state.amount_paid, Amount::ZERO); assert_eq!(state.amount_issued, Amount::ZERO); - let active_keyset_id = wallet.get_active_mint_keyset().await?.id; + let active_keyset_id = wallet.fetch_active_keyset().await?.id; let pay_amount_msats = 10_000; diff --git a/crates/cdk-integration-tests/tests/fake_wallet.rs b/crates/cdk-integration-tests/tests/fake_wallet.rs index e2de57c4..a4843463 100644 --- a/crates/cdk-integration-tests/tests/fake_wallet.rs +++ b/crates/cdk-integration-tests/tests/fake_wallet.rs @@ -381,7 +381,7 @@ async fn test_fake_melt_change_in_quote() { let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap(); - let keyset = wallet.get_active_mint_keyset().await.unwrap(); + let keyset = wallet.fetch_active_keyset().await.unwrap(); let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap(); @@ -489,7 +489,7 @@ async fn test_fake_mint_without_witness() { let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let premint_secrets = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap(); @@ -529,7 +529,7 @@ async fn test_fake_mint_with_wrong_witness() { let http_client = HttpClient::new(MINT_URL.parse().unwrap(), None); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let premint_secrets = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap(); @@ -573,7 +573,7 @@ async fn test_fake_mint_inflated() { .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let pre_mint = PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None).unwrap(); @@ -631,7 +631,7 @@ async fn test_fake_mint_multiple_units() { .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap(); @@ -644,7 +644,7 @@ async fn test_fake_mint_multiple_units() { ) .expect("failed to create new wallet"); - let active_keyset_id = wallet_usd.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id; let usd_pre_mint = PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap(); @@ -733,7 +733,7 @@ async fn test_fake_mint_multiple_unit_swap() { .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; { let inputs: Proofs = vec![ @@ -767,7 +767,7 @@ async fn test_fake_mint_multiple_unit_swap() { } { - let usd_active_keyset_id = wallet_usd.get_active_mint_keyset().await.unwrap().id; + let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id; let inputs: Proofs = proofs.into_iter().take(2).collect(); let total_inputs = inputs.total_amount().unwrap(); @@ -883,8 +883,8 @@ async fn test_fake_mint_multiple_unit_melt() { let input_amount: u64 = inputs.total_amount().unwrap().into(); let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string()); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; - let usd_active_keyset_id = wallet_usd.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; + let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id; let usd_pre_mint = PreMintSecrets::random( usd_active_keyset_id, @@ -952,7 +952,7 @@ async fn test_fake_mint_input_output_mismatch() { None, ) .expect("failed to create new usd wallet"); - let usd_active_keyset_id = wallet_usd.get_active_mint_keyset().await.unwrap().id; + let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id; let inputs = proofs; @@ -1001,7 +1001,7 @@ async fn test_fake_mint_swap_inflated() { .mint(&mint_quote.id, SplitTarget::None, None) .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let pre_mint = PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap(); @@ -1045,7 +1045,7 @@ async fn test_fake_mint_swap_spend_after_fail() { .mint(&mint_quote.id, SplitTarget::None, None) .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap(); @@ -1116,7 +1116,7 @@ async fn test_fake_mint_melt_spend_after_fail() { .mint(&mint_quote.id, SplitTarget::None, None) .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let pre_mint = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap(); @@ -1189,7 +1189,7 @@ async fn test_fake_mint_duplicate_proofs_swap() { .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let inputs = vec![proofs[0].clone(), proofs[0].clone()]; diff --git a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs index e9311196..5d42aff7 100644 --- a/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs +++ b/crates/cdk-integration-tests/tests/happy_path_mint_wallet.rs @@ -358,7 +358,7 @@ async fn test_fake_melt_change_in_quote() { let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap(); - let keyset = wallet.get_active_mint_keyset().await.unwrap(); + let keyset = wallet.fetch_active_keyset().await.unwrap(); let premint_secrets = PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap(); diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index f73d432b..df80f155 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -308,7 +308,7 @@ async fn test_cached_mint() { .await .unwrap(); - let active_keyset_id = wallet.get_active_mint_keyset().await.unwrap().id; + let active_keyset_id = wallet.fetch_active_keyset().await.unwrap().id; let http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None); let premint_secrets = PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap(); diff --git a/crates/cdk/examples/proof-selection.rs b/crates/cdk/examples/proof-selection.rs index 1532de61..d7060cf1 100644 --- a/crates/cdk/examples/proof-selection.rs +++ b/crates/cdk/examples/proof-selection.rs @@ -8,6 +8,7 @@ use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload}; use cdk::wallet::{Wallet, WalletSubscription}; use cdk::Amount; +use cdk_common::nut02::KeySetInfosMethods; use cdk_sqlite::wallet::memory; use rand::random; @@ -62,9 +63,9 @@ async fn main() -> Result<(), Box> { // Select proofs to send let amount = Amount::from(64); let active_keyset_ids = wallet - .get_active_mint_keysets() + .refresh_keysets() .await? - .into_iter() + .active() .map(|keyset| keyset.id) .collect(); let selected = diff --git a/crates/cdk/src/wallet/auth/auth_wallet.rs b/crates/cdk/src/wallet/auth/auth_wallet.rs index fa6718ea..5be0df2e 100644 --- a/crates/cdk/src/wallet/auth/auth_wallet.rs +++ b/crates/cdk/src/wallet/auth/auth_wallet.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use cdk_common::database::{self, WalletDatabase}; use cdk_common::mint_url::MintUrl; +use cdk_common::nut02::KeySetInfosMethods; use cdk_common::{AuthProof, Id, Keys, MintInfo}; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; @@ -167,12 +168,12 @@ impl AuthWallet { self.client.get_mint_info().await.map(Some).or(Ok(None)) } - /// Get keys for mint keyset + /// Fetch keys for mint keyset /// - /// Selected keys from localstore if they are already known - /// If they are not known queries mint for keyset id and stores the [`Keys`] + /// Returns keys from local database if they are already stored. + /// If keys are not found locally, goes online to query the mint for the keyset and stores the [`Keys`] in local database. #[instrument(skip(self))] - pub async fn get_keyset_keys(&self, keyset_id: Id) -> Result { + pub async fn load_keyset_keys(&self, keyset_id: Id) -> Result { let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? { keys } else { @@ -188,62 +189,88 @@ impl AuthWallet { Ok(keys) } - /// Get active keyset for mint + /// Get blind auth keysets from local database or go online if missing /// - /// Queries mint for current keysets then gets [`Keys`] for any unknown - /// keysets + /// First checks the local database for cached blind auth keysets. If keysets are not found locally, + /// goes online to refresh keysets from the mint and updates the local database. + /// This is the main method for getting auth keysets in operations that can work offline + /// but will fall back to online if needed. #[instrument(skip(self))] - pub async fn get_active_mint_blind_auth_keysets(&self) -> Result, Error> { - let keysets = self.client.get_mint_blind_auth_keysets().await?; - let keysets = keysets.keysets; - - self.localstore - .add_mint_keysets(self.mint_url.clone(), keysets.clone()) - .await?; - - let active_keysets = keysets - .clone() - .into_iter() - .filter(|k| k.unit == CurrencyUnit::Auth) - .collect::>(); - + pub async fn load_mint_keysets(&self) -> Result, Error> { match self .localstore .get_mint_keysets(self.mint_url.clone()) .await? { - Some(known_keysets) => { - let unknown_keysets: Vec<&KeySetInfo> = keysets - .iter() - .filter(|k| known_keysets.contains(k)) - .collect(); - - for keyset in unknown_keysets { - self.get_keyset_keys(keyset.id).await?; + Some(keysets_info) => { + let auth_keysets: Vec = + keysets_info.unit(CurrencyUnit::Sat).cloned().collect(); + if auth_keysets.is_empty() { + // If we don't have any auth keysets, fetch them from the mint + let keysets = self.refresh_keysets().await?; + Ok(keysets) + } else { + Ok(auth_keysets) } } None => { - for keyset in keysets { - self.get_keyset_keys(keyset.id).await?; - } + // If we don't have any keysets, fetch them from the mint + let keysets = self.refresh_keysets().await?; + Ok(keysets) } } - Ok(active_keysets) } - /// Get active keyset for mint + /// Refresh blind auth keysets by fetching the latest from mint - always goes online /// - /// Queries mint for current keysets then gets [`Keys`] for any unknown - /// keysets + /// This method always goes online to fetch the latest blind auth keyset information from the mint. + /// It updates the local database with the fetched keysets and ensures we have keys for all keysets. + /// Returns only the keysets with Auth currency unit. This is used when operations need the most + /// up-to-date keyset information and are willing to go online. #[instrument(skip(self))] - pub async fn get_active_mint_blind_auth_keyset(&self) -> Result { - let active_keysets = self.get_active_mint_blind_auth_keysets().await?; + pub async fn refresh_keysets(&self) -> Result, Error> { + let keysets_response = self.client.get_mint_blind_auth_keysets().await?; + let keysets = keysets_response.keysets; - let keyset = active_keysets.first().ok_or(Error::NoActiveKeyset)?; + // Update local store with keysets + self.localstore + .add_mint_keysets(self.mint_url.clone(), keysets.clone()) + .await?; + + // Filter for auth keysets + let auth_keysets = keysets + .clone() + .into_iter() + .filter(|k| k.unit == CurrencyUnit::Auth) + .collect::>(); + + // Ensure we have keys for all auth keysets + for keyset in &auth_keysets { + if self.localstore.get_keys(&keyset.id).await?.is_none() { + tracing::debug!("Fetching missing keys for auth keyset {}", keyset.id); + self.load_keyset_keys(keyset.id).await?; + } + } + + Ok(auth_keysets) + } + + /// Get the first active blind auth keyset - always goes online + /// + /// This method always goes online to refresh keysets from the mint and then returns + /// the first active keyset found. Use this when you need the most up-to-date + /// keyset information for blind auth operations. + #[instrument(skip(self))] + pub async fn fetch_active_keyset(&self) -> Result { + let auth_keysets = self.refresh_keysets().await?; + let keyset = auth_keysets.first().ok_or(Error::NoActiveKeyset)?; Ok(keyset.clone()) } - /// Get unspent proofs for mint + /// Get unspent auth proofs from local database only - offline operation + /// + /// Returns auth proofs from the local database that are in the Unspent state. + /// This is an offline operation that does not contact the mint. #[instrument(skip(self))] pub async fn get_unspent_auth_proofs(&self) -> Result, Error> { Ok(self @@ -334,9 +361,6 @@ impl AuthWallet { let auth_token = self.client.get_auth_token().await?; - let active_keyset_id = self.get_active_mint_blind_auth_keysets().await?; - tracing::debug!("Active ketset: {:?}", active_keyset_id); - match &auth_token { AuthToken::ClearAuth(cat) => { if cat.is_empty() { @@ -369,7 +393,7 @@ impl AuthWallet { } } - let active_keyset_id = self.get_active_mint_blind_auth_keyset().await?.id; + let active_keyset_id = self.fetch_active_keyset().await?.id; let premint_secrets = PreMintSecrets::random(active_keyset_id, amount, &SplitTarget::Value(1.into()))?; @@ -380,13 +404,13 @@ impl AuthWallet { let mint_res = self.client.post_mint_blind_auth(request).await?; - let keys = self.get_keyset_keys(active_keyset_id).await?; + let keys = self.load_keyset_keys(active_keyset_id).await?; // Verify the signature DLEQ is valid { assert!(mint_res.signatures.len() == premint_secrets.secrets.len()); for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { - let keys = self.get_keyset_keys(sig.keyset_id).await?; + let keys = self.load_keyset_keys(sig.keyset_id).await?; let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { Ok(_) => (), diff --git a/crates/cdk/src/wallet/issue/issue_bolt11.rs b/crates/cdk/src/wallet/issue/issue_bolt11.rs index c6316804..f33b0390 100644 --- a/crates/cdk/src/wallet/issue/issue_bolt11.rs +++ b/crates/cdk/src/wallet/issue/issue_bolt11.rs @@ -227,7 +227,7 @@ impl Wallet { tracing::warn!("Attempting to mint with expired quote."); } - let active_keyset_id = self.get_active_mint_keyset().await?.id; + let active_keyset_id = self.fetch_active_keyset().await?.id; let count = self .localstore @@ -264,12 +264,12 @@ impl Wallet { let mint_res = self.client.post_mint(request).await?; - let keys = self.get_keyset_keys(active_keyset_id).await?; + let keys = self.fetch_keyset_keys(active_keyset_id).await?; // Verify the signature DLEQ is valid { for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { - let keys = self.get_keyset_keys(sig.keyset_id).await?; + let keys = self.fetch_keyset_keys(sig.keyset_id).await?; let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { Ok(_) | Err(nut12::Error::MissingDleqProof) => (), diff --git a/crates/cdk/src/wallet/issue/issue_bolt12.rs b/crates/cdk/src/wallet/issue/issue_bolt12.rs index 9fb591a8..7d5c5c3a 100644 --- a/crates/cdk/src/wallet/issue/issue_bolt12.rs +++ b/crates/cdk/src/wallet/issue/issue_bolt12.rs @@ -105,7 +105,7 @@ impl Wallet { return Err(Error::UnknownQuote); }; - let active_keyset_id = self.get_active_mint_keyset().await?.id; + let active_keyset_id = self.fetch_active_keyset().await?.id; let count = self .localstore @@ -161,12 +161,12 @@ impl Wallet { let mint_res = self.client.post_mint(request).await?; - let keys = self.get_keyset_keys(active_keyset_id).await?; + let keys = self.fetch_keyset_keys(active_keyset_id).await?; // Verify the signature DLEQ is valid { for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { - let keys = self.get_keyset_keys(sig.keyset_id).await?; + let keys = self.fetch_keyset_keys(sig.keyset_id).await?; let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?; match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { Ok(_) | Err(nut12::Error::MissingDleqProof) => (), diff --git a/crates/cdk/src/wallet/keysets.rs b/crates/cdk/src/wallet/keysets.rs index 09520194..0d40818b 100644 --- a/crates/cdk/src/wallet/keysets.rs +++ b/crates/cdk/src/wallet/keysets.rs @@ -1,17 +1,18 @@ use std::collections::HashMap; +use cdk_common::nut02::{KeySetInfos, KeySetInfosMethods}; use tracing::instrument; use crate::nuts::{Id, KeySetInfo, Keys}; use crate::{Error, Wallet}; impl Wallet { - /// Get keys for mint keyset + /// Fetch keys for mint keyset /// - /// Selected keys from localstore if they are already known - /// If they are not known queries mint for keyset id and stores the [`Keys`] + /// Returns keys from local database if they are already stored. + /// If keys are not found locally, goes online to query the mint for the keyset and stores the [`Keys`] in local database. #[instrument(skip(self))] - pub async fn get_keyset_keys(&self, keyset_id: Id) -> Result { + pub async fn fetch_keyset_keys(&self, keyset_id: Id) -> Result { let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? { keys } else { @@ -27,10 +28,12 @@ impl Wallet { Ok(keys) } - /// Get keysets from DB or fetch them + /// Get keysets from local database or go online if missing /// - /// Checks the database for keysets and queries the Mint if - /// it can't find any. + /// First checks the local database for cached keysets. If keysets are not found locally, + /// goes online to refresh keysets from the mint and updates the local database. + /// This is the main method for getting keysets in token operations that can work offline + /// but will fall back to online if needed. #[instrument(skip(self))] pub async fn load_mint_keysets(&self) -> Result, Error> { match self @@ -39,86 +42,105 @@ impl Wallet { .await? { Some(keysets_info) => Ok(keysets_info), - None => self.get_mint_keysets().await, // Hit the keysets endpoint if we don't have the keysets for this Mint + None => { + // If we don't have any keysets, fetch them from the mint + let keysets = self.refresh_keysets().await?; + Ok(keysets) + } } } - /// Get keysets for wallet's mint + /// Get keysets from local database only - pure offline operation /// - /// Queries mint for all keysets + /// Only checks the local database for cached keysets. If keysets are not found locally, + /// returns an error without going online. This is used for operations that must remain + /// offline and rely on previously cached keyset data. #[instrument(skip(self))] pub async fn get_mint_keysets(&self) -> Result, Error> { - let keysets = self.client.get_mint_keysets().await?; - - self.localstore - .add_mint_keysets(self.mint_url.clone(), keysets.keysets.clone()) - .await?; - - Ok(keysets.keysets) - } - - /// Get active keyset for mint - /// - /// Queries mint for current keysets then gets [`Keys`] for any unknown - /// keysets - #[instrument(skip(self))] - pub async fn get_active_mint_keysets(&self) -> Result, Error> { - let keysets = self.client.get_mint_keysets().await?; - let keysets = keysets.keysets; - - self.localstore - .add_mint_keysets(self.mint_url.clone(), keysets.clone()) - .await?; - - let active_keysets = keysets - .clone() - .into_iter() - .filter(|k| k.active && k.unit == self.unit) - .collect::>(); - match self .localstore .get_mint_keysets(self.mint_url.clone()) .await? { - Some(known_keysets) => { - let unknown_keysets: Vec<&KeySetInfo> = keysets - .iter() - .filter(|k| known_keysets.contains(k)) - .collect(); + Some(keysets_info) => Ok(keysets_info), + None => Err(Error::UnknownKeySet), + } + } - for keyset in unknown_keysets { - self.get_keyset_keys(keyset.id).await?; - } - } - None => { - for keyset in keysets { - self.get_keyset_keys(keyset.id).await?; - } + /// Refresh keysets by fetching the latest from mint - always goes online + /// + /// This method always goes online to fetch the latest keyset information from the mint. + /// It updates the local database with the fetched keysets and ensures we have keys + /// for all active keysets. This is used when operations need the most up-to-date + /// keyset information and are willing to go online. + #[instrument(skip(self))] + pub async fn refresh_keysets(&self) -> Result { + tracing::debug!("Refreshing keysets and ensuring we have keys"); + let _ = self.get_mint_info().await?; + + // Fetch all current keysets from mint + let keysets_response = self.client.get_mint_keysets().await?; + let all_keysets = keysets_response.keysets; + + // Update local storage with keyset info + self.localstore + .add_mint_keysets(self.mint_url.clone(), all_keysets.clone()) + .await?; + + // Filter for active keysets matching our unit + let keysets: KeySetInfos = all_keysets.unit(self.unit.clone()).cloned().collect(); + + // Ensure we have keys for all active keysets + for keyset in &keysets { + if self.localstore.get_keys(&keyset.id).await?.is_none() { + tracing::debug!("Fetching missing keys for keyset {}", keyset.id); + self.fetch_keyset_keys(keyset.id).await?; } } - Ok(active_keysets) + Ok(keysets) } - /// Get active keyset for mint with the lowest fees + /// Get the active keyset with the lowest fees - always goes online /// - /// Queries mint for current keysets then gets [`Keys`] for any unknown - /// keysets + /// This method always goes online to refresh keysets from the mint and then returns + /// the active keyset with the minimum input fees. Use this when you need the most + /// up-to-date keyset information for operations. #[instrument(skip(self))] - pub async fn get_active_mint_keyset(&self) -> Result { - // Important - let _ = self.get_mint_info().await?; - let active_keysets = self.get_active_mint_keysets().await?; - - 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) + pub async fn fetch_active_keyset(&self) -> Result { + self.refresh_keysets() + .await? + .active() + .min_by_key(|k| k.input_fee_ppk) + .cloned() + .ok_or(Error::NoActiveKeyset) } - /// Get keyset fees for mint + /// Get the active keyset with the lowest fees from local database only - offline operation + /// + /// Returns the active keyset with minimum input fees from cached keysets in the local database. + /// This is an offline operation that does not contact the mint. If no keysets are found locally, + /// returns an error. Use this for offline operations or when you want to avoid network calls. + #[instrument(skip(self))] + pub async fn get_active_keyset(&self) -> Result { + match self + .localstore + .get_mint_keysets(self.mint_url.clone()) + .await? + { + Some(keysets_info) => keysets_info + .into_iter() + .min_by_key(|k| k.input_fee_ppk) + .ok_or(Error::NoActiveKeyset), + None => Err(Error::UnknownKeySet), + } + } + + /// Get keyset fees for mint from local database only - offline operation + /// + /// Returns a HashMap of keyset IDs to their input fee rates (per-proof-per-thousand) + /// from cached keysets in the local database. This is an offline operation that does + /// not contact the mint. If no keysets are found locally, returns an error. pub async fn get_keyset_fees(&self) -> Result, Error> { let keysets = self .localstore @@ -134,7 +156,11 @@ impl Wallet { Ok(fees) } - /// Get keyset fees for mint by keyset id + /// Get keyset fees for mint by keyset id from local database only - offline operation + /// + /// Returns the input fee rate (per-proof-per-thousand) for a specific keyset ID from + /// cached keysets in the local database. This is an offline operation that does not + /// contact the mint. If the keyset is not found locally, returns an error. pub async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result { self.get_keyset_fees() .await? diff --git a/crates/cdk/src/wallet/melt/melt_bolt11.rs b/crates/cdk/src/wallet/melt/melt_bolt11.rs index afe3b438..01444331 100644 --- a/crates/cdk/src/wallet/melt/melt_bolt11.rs +++ b/crates/cdk/src/wallet/melt/melt_bolt11.rs @@ -146,7 +146,7 @@ impl Wallet { .update_proofs_state(ys, State::Pending) .await?; - let active_keyset_id = self.get_active_mint_keyset().await?.id; + let active_keyset_id = self.fetch_active_keyset().await?.id; let count = self .localstore @@ -317,7 +317,7 @@ impl Wallet { let available_proofs = self.get_unspent_proofs().await?; let active_keyset_ids = self - .get_active_mint_keysets() + .refresh_keysets() .await? .into_iter() .map(|k| k.id) diff --git a/crates/cdk/src/wallet/mod.rs b/crates/cdk/src/wallet/mod.rs index bfec189f..a0bf863c 100644 --- a/crates/cdk/src/wallet/mod.rs +++ b/crates/cdk/src/wallet/mod.rs @@ -378,12 +378,12 @@ impl Wallet { self.get_mint_info().await?; } - let keysets = self.get_mint_keysets().await?; + let keysets = self.load_mint_keysets().await?; let mut restored_value = Amount::ZERO; for keyset in keysets { - let keys = self.get_keyset_keys(keyset.id).await?; + let keys = self.fetch_keyset_keys(keyset.id).await?; let mut empty_batch = 0; let mut start_counter = 0; @@ -632,7 +632,7 @@ impl Wallet { let mint_pubkey = match keys_cache.get(&proof.keyset_id) { Some(keys) => keys.amount_key(proof.amount), None => { - let keys = self.get_keyset_keys(proof.keyset_id).await?; + let keys = self.fetch_keyset_keys(proof.keyset_id).await?; let key = keys.amount_key(proof.amount); keys_cache.insert(proof.keyset_id, keys); diff --git a/crates/cdk/src/wallet/multi_mint_wallet.rs b/crates/cdk/src/wallet/multi_mint_wallet.rs index bea3ab3b..cfac4e1b 100644 --- a/crates/cdk/src/wallet/multi_mint_wallet.rs +++ b/crates/cdk/src/wallet/multi_mint_wallet.rs @@ -293,7 +293,7 @@ impl MultiMintWallet { { Some(keysets_info) => keysets_info, // Hit the keysets endpoint if we don't have the keysets for this Mint - None => wallet.get_mint_keysets().await?, + None => wallet.load_mint_keysets().await?, }; let proofs = token_data.proofs(&keysets_info)?; diff --git a/crates/cdk/src/wallet/receive.rs b/crates/cdk/src/wallet/receive.rs index 04afa728..84b3aff3 100644 --- a/crates/cdk/src/wallet/receive.rs +++ b/crates/cdk/src/wallet/receive.rs @@ -38,11 +38,11 @@ impl Wallet { self.get_mint_info().await?; } - let _ = self.get_active_mint_keyset().await?; + let _ = self.fetch_active_keyset().await?; - let active_keyset_id = self.get_active_mint_keyset().await?.id; + let active_keyset_id = self.fetch_active_keyset().await?.id; - let keys = self.get_keyset_keys(active_keyset_id).await?; + let keys = self.fetch_keyset_keys(active_keyset_id).await?; let mut proofs = proofs; @@ -70,7 +70,7 @@ impl Wallet { for proof in &mut proofs { // Verify that proof DLEQ is valid if proof.dleq.is_some() { - let keys = self.get_keyset_keys(proof.keyset_id).await?; + let keys = self.fetch_keyset_keys(proof.keyset_id).await?; let key = keys.amount_key(proof.amount).ok_or(Error::AmountKey)?; proof.verify_dleq(key)?; } diff --git a/crates/cdk/src/wallet/send.rs b/crates/cdk/src/wallet/send.rs index a98547b4..9d6b4764 100644 --- a/crates/cdk/src/wallet/send.rs +++ b/crates/cdk/src/wallet/send.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::fmt::Debug; +use cdk_common::nut02::KeySetInfosMethods; use cdk_common::util::unix_time; use cdk_common::wallet::{Transaction, TransactionDirection}; use tracing::instrument; @@ -32,11 +33,8 @@ impl Wallet { // If online send check mint for current keysets fees if opts.send_kind.is_online() { - if let Err(e) = self.get_active_mint_keyset().await { - tracing::error!( - "Error fetching active mint keyset: {:?}. Using stored keysets", - e - ); + if let Err(e) = self.refresh_keysets().await { + tracing::error!("Error refreshing keysets: {:?}. Using stored keysets", e); } } @@ -78,11 +76,12 @@ impl Wallet { // Select proofs let active_keyset_ids = self - .get_active_mint_keysets() + .get_mint_keysets() .await? - .into_iter() + .active() .map(|k| k.id) .collect(); + let selected_proofs = Wallet::select_proofs( amount, available_proofs, @@ -131,7 +130,7 @@ impl Wallet { ) -> Result { // Split amount with fee if necessary let (send_amounts, send_fee) = if opts.include_fee { - let active_keyset_id = self.get_active_mint_keyset().await?.id; + let active_keyset_id = self.get_active_keyset().await?.id; let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?; tracing::debug!("Keyset fee per proof: {:?}", keyset_fee_ppk); let send_split = amount.split_with_fee(keyset_fee_ppk)?; @@ -209,7 +208,7 @@ impl Wallet { let mut proofs_to_send = send.proofs_to_send; // Get active keyset ID - let active_keyset_id = self.get_active_mint_keyset().await?.id; + let active_keyset_id = self.fetch_active_keyset().await?.id; tracing::debug!("Active keyset ID: {:?}", active_keyset_id); // Get keyset fees diff --git a/crates/cdk/src/wallet/swap.rs b/crates/cdk/src/wallet/swap.rs index 5e9f15aa..85c82bbf 100644 --- a/crates/cdk/src/wallet/swap.rs +++ b/crates/cdk/src/wallet/swap.rs @@ -1,3 +1,4 @@ +use cdk_common::nut02::KeySetInfosMethods; use tracing::instrument; use crate::amount::SplitTarget; @@ -167,11 +168,12 @@ impl Wallet { ensure_cdk!(proofs_sum >= amount, Error::InsufficientFunds); let active_keyset_ids = self - .get_active_mint_keysets() + .refresh_keysets() .await? - .into_iter() + .active() .map(|k| k.id) .collect(); + let keyset_fees = self.get_keyset_fees().await?; let proofs = Wallet::select_proofs( amount, @@ -203,7 +205,7 @@ impl Wallet { include_fees: bool, ) -> Result { tracing::info!("Creating swap"); - let active_keyset_id = self.get_active_mint_keyset().await?.id; + let active_keyset_id = self.fetch_active_keyset().await?.id; // Desired amount is either amount passed or value of all proof let proofs_total = proofs.total_amount()?;