فهرست منبع

Merge pull request #901 from thesimplekid/refresh_keys

feat: refactor wallet keyset management for better clarity
thesimplekid 3 ماه پیش
والد
کامیت
d2e9f1a626

+ 22 - 0
crates/cashu/src/nuts/nut02.rs

@@ -497,6 +497,28 @@ pub struct KeySetInfo {
     pub final_expiry: Option<u64>,
 }
 
+/// List of [KeySetInfo]
+pub type KeySetInfos = Vec<KeySetInfo>;
+
+/// Utility methods for [KeySetInfos]
+pub trait KeySetInfosMethods {
+    /// Filter for active keysets
+    fn active(&self) -> impl Iterator<Item = &KeySetInfo> + '_;
+
+    /// Filter keysets for specific unit
+    fn unit(&self, unit: CurrencyUnit) -> impl Iterator<Item = &KeySetInfo> + '_;
+}
+
+impl KeySetInfosMethods for KeySetInfos {
+    fn active(&self) -> impl Iterator<Item = &KeySetInfo> + '_ {
+        self.iter().filter(|k| k.active)
+    }
+
+    fn unit(&self, unit: CurrencyUnit) -> impl Iterator<Item = &KeySetInfo> + '_ {
+        self.iter().filter(move |k| k.unit == unit)
+    }
+}
+
 fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
 where
     D: Deserializer<'de>,

+ 1 - 1
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)?;
 

+ 1 - 1
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;
 

+ 15 - 15
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()];
 

+ 1 - 1
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();

+ 1 - 1
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();

+ 3 - 2
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<dyn std::error::Error>> {
     // 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 =

+ 69 - 45
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<Keys, Error> {
+    pub async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
         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<Vec<KeySetInfo>, Error> {
-        let keysets = self.client.get_mint_blind_auth_keysets().await?;
-        let keysets = keysets.keysets;
+    pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
+        match self
+            .localstore
+            .get_mint_keysets(self.mint_url.clone())
+            .await?
+        {
+            Some(keysets_info) => {
+                let auth_keysets: Vec<KeySetInfo> =
+                    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 => {
+                // If we don't have any keysets, fetch them from the mint
+                let keysets = self.refresh_keysets().await?;
+                Ok(keysets)
+            }
+        }
+    }
+
+    /// Refresh blind auth keysets by fetching the latest from mint - always goes online
+    ///
+    /// 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 refresh_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
+        let keysets_response = self.client.get_mint_blind_auth_keysets().await?;
+        let keysets = keysets_response.keysets;
 
+        // Update local store with keysets
         self.localstore
             .add_mint_keysets(self.mint_url.clone(), keysets.clone())
             .await?;
 
-        let active_keysets = keysets
+        // Filter for auth keysets
+        let auth_keysets = keysets
             .clone()
             .into_iter()
             .filter(|k| k.unit == CurrencyUnit::Auth)
             .collect::<Vec<KeySetInfo>>();
 
-        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?;
-                }
-            }
-            None => {
-                for keyset in keysets {
-                    self.get_keyset_keys(keyset.id).await?;
-                }
+        // 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(active_keysets)
+
+        Ok(auth_keysets)
     }
 
-    /// Get active keyset for mint
+    /// Get the first active blind auth keyset - 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 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 get_active_mint_blind_auth_keyset(&self) -> Result<KeySetInfo, Error> {
-        let active_keysets = self.get_active_mint_blind_auth_keysets().await?;
-
-        let keyset = active_keysets.first().ok_or(Error::NoActiveKeyset)?;
+    pub async fn fetch_active_keyset(&self) -> Result<KeySetInfo, Error> {
+        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<Vec<AuthProof>, 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(_) => (),

+ 3 - 3
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) => (),

+ 3 - 3
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) => (),

+ 89 - 63
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<Keys, Error> {
+    pub async fn fetch_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
         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<Vec<KeySetInfo>, 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<Vec<KeySetInfo>, 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)
+        match self
+            .localstore
+            .get_mint_keysets(self.mint_url.clone())
+            .await?
+        {
+            Some(keysets_info) => Ok(keysets_info),
+            None => Err(Error::UnknownKeySet),
+        }
     }
 
-    /// Get active keyset for mint
+    /// Refresh 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 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 get_active_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
-        let keysets = self.client.get_mint_keysets().await?;
-        let keysets = keysets.keysets;
+    pub async fn refresh_keysets(&self) -> Result<KeySetInfos, Error> {
+        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(), keysets.clone())
+            .add_mint_keysets(self.mint_url.clone(), all_keysets.clone())
             .await?;
 
-        let active_keysets = keysets
-            .clone()
-            .into_iter()
-            .filter(|k| k.active && k.unit == self.unit)
-            .collect::<Vec<KeySetInfo>>();
+        // Filter for active keysets matching our unit
+        let keysets: KeySetInfos = all_keysets.unit(self.unit.clone()).cloned().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();
-
-                for keyset in unknown_keysets {
-                    self.get_keyset_keys(keyset.id).await?;
-                }
-            }
-            None => {
-                for keyset in keysets {
-                    self.get_keyset_keys(keyset.id).await?;
-                }
+        // 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<KeySetInfo, Error> {
-        // Important
-        let _ = self.get_mint_info().await?;
-        let active_keysets = self.get_active_mint_keysets().await?;
+    pub async fn fetch_active_keyset(&self) -> Result<KeySetInfo, Error> {
+        self.refresh_keysets()
+            .await?
+            .active()
+            .min_by_key(|k| k.input_fee_ppk)
+            .cloned()
+            .ok_or(Error::NoActiveKeyset)
+    }
 
-        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)
+    /// 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<KeySetInfo, Error> {
+        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
+    /// 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<HashMap<Id, u64>, 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<u64, Error> {
         self.get_keyset_fees()
             .await?

+ 2 - 2
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)

+ 3 - 3
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);

+ 1 - 1
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)?;
 

+ 4 - 4
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)?;
             }

+ 8 - 9
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<PreparedSend, Error> {
         // 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

+ 5 - 3
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<PreSwap, Error> {
         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()?;