Merge pull request #901 from thesimplekid/refresh_keys

feat: refactor wallet keyset management for better clarity
This commit is contained in:
thesimplekid
2025-07-23 20:49:42 +01:00
committed by GitHub
parent 8ab545ae44
commit d2e9f1a626
17 changed files with 236 additions and 162 deletions

View File

@@ -497,6 +497,28 @@ pub struct KeySetInfo {
pub final_expiry: Option<u64>, 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> fn deserialize_input_fee_ppk<'de, D>(deserializer: D) -> Result<u64, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,

View File

@@ -101,7 +101,7 @@ pub async fn pay_request(
.await? .await?
{ {
Some(keysets_info) => keysets_info, 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)?; let proofs = token.proofs(&keysets_info)?;

View File

@@ -277,7 +277,7 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> {
assert_eq!(state.amount_paid, Amount::ZERO); assert_eq!(state.amount_paid, Amount::ZERO);
assert_eq!(state.amount_issued, 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; let pay_amount_msats = 10_000;

View File

@@ -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 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 = let premint_secrets =
PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap(); 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 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 = let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap(); 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 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 = let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap(); PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();
@@ -573,7 +573,7 @@ async fn test_fake_mint_inflated() {
.await .await
.unwrap(); .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 = let pre_mint =
PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None).unwrap(); PreMintSecrets::random(active_keyset_id, 500.into(), &SplitTarget::None).unwrap();
@@ -631,7 +631,7 @@ async fn test_fake_mint_multiple_units() {
.await .await
.unwrap(); .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(); 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"); .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 = let usd_pre_mint =
PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap(); PreMintSecrets::random(active_keyset_id, 50.into(), &SplitTarget::None).unwrap();
@@ -733,7 +733,7 @@ async fn test_fake_mint_multiple_unit_swap() {
.await .await
.unwrap(); .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![ 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 inputs: Proofs = proofs.into_iter().take(2).collect();
let total_inputs = inputs.total_amount().unwrap(); 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 input_amount: u64 = inputs.total_amount().unwrap().into();
let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string()); let invoice = create_fake_invoice((input_amount - 1) * 1000, "".to_string());
let active_keyset_id = wallet.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.get_active_mint_keyset().await.unwrap().id; let usd_active_keyset_id = wallet_usd.fetch_active_keyset().await.unwrap().id;
let usd_pre_mint = PreMintSecrets::random( let usd_pre_mint = PreMintSecrets::random(
usd_active_keyset_id, usd_active_keyset_id,
@@ -952,7 +952,7 @@ async fn test_fake_mint_input_output_mismatch() {
None, None,
) )
.expect("failed to create new usd wallet"); .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; let inputs = proofs;
@@ -1001,7 +1001,7 @@ async fn test_fake_mint_swap_inflated() {
.mint(&mint_quote.id, SplitTarget::None, None) .mint(&mint_quote.id, SplitTarget::None, None)
.await .await
.unwrap(); .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 = let pre_mint =
PreMintSecrets::random(active_keyset_id, 101.into(), &SplitTarget::None).unwrap(); 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) .mint(&mint_quote.id, SplitTarget::None, None)
.await .await
.unwrap(); .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 = let pre_mint =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap(); 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) .mint(&mint_quote.id, SplitTarget::None, None)
.await .await
.unwrap(); .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 = let pre_mint =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap(); PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::None).unwrap();
@@ -1189,7 +1189,7 @@ async fn test_fake_mint_duplicate_proofs_swap() {
.await .await
.unwrap(); .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()]; let inputs = vec![proofs[0].clone(), proofs[0].clone()];

View File

@@ -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 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 = let premint_secrets =
PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap(); PreMintSecrets::random(keyset.id, 100.into(), &SplitTarget::default()).unwrap();

View File

@@ -308,7 +308,7 @@ async fn test_cached_mint() {
.await .await
.unwrap(); .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 http_client = HttpClient::new(get_mint_url_from_env().parse().unwrap(), None);
let premint_secrets = let premint_secrets =
PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap(); PreMintSecrets::random(active_keyset_id, 100.into(), &SplitTarget::default()).unwrap();

View File

@@ -8,6 +8,7 @@ use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload}; use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload};
use cdk::wallet::{Wallet, WalletSubscription}; use cdk::wallet::{Wallet, WalletSubscription};
use cdk::Amount; use cdk::Amount;
use cdk_common::nut02::KeySetInfosMethods;
use cdk_sqlite::wallet::memory; use cdk_sqlite::wallet::memory;
use rand::random; use rand::random;
@@ -62,9 +63,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Select proofs to send // Select proofs to send
let amount = Amount::from(64); let amount = Amount::from(64);
let active_keyset_ids = wallet let active_keyset_ids = wallet
.get_active_mint_keysets() .refresh_keysets()
.await? .await?
.into_iter() .active()
.map(|keyset| keyset.id) .map(|keyset| keyset.id)
.collect(); .collect();
let selected = let selected =

View File

@@ -3,6 +3,7 @@ use std::sync::Arc;
use cdk_common::database::{self, WalletDatabase}; use cdk_common::database::{self, WalletDatabase};
use cdk_common::mint_url::MintUrl; use cdk_common::mint_url::MintUrl;
use cdk_common::nut02::KeySetInfosMethods;
use cdk_common::{AuthProof, Id, Keys, MintInfo}; use cdk_common::{AuthProof, Id, Keys, MintInfo};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::RwLock; use tokio::sync::RwLock;
@@ -167,12 +168,12 @@ impl AuthWallet {
self.client.get_mint_info().await.map(Some).or(Ok(None)) 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 /// Returns keys from local database if they are already stored.
/// If they are not known queries mint for keyset id and stores the [`Keys`] /// 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))] #[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? { let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? {
keys keys
} else { } else {
@@ -188,62 +189,88 @@ impl AuthWallet {
Ok(keys) 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 /// First checks the local database for cached blind auth keysets. If keysets are not found locally,
/// keysets /// 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))] #[instrument(skip(self))]
pub async fn get_active_mint_blind_auth_keysets(&self) -> Result<Vec<KeySetInfo>, Error> { pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, 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::<Vec<KeySetInfo>>();
match self match self
.localstore .localstore
.get_mint_keysets(self.mint_url.clone()) .get_mint_keysets(self.mint_url.clone())
.await? .await?
{ {
Some(known_keysets) => { Some(keysets_info) => {
let unknown_keysets: Vec<&KeySetInfo> = keysets let auth_keysets: Vec<KeySetInfo> =
.iter() keysets_info.unit(CurrencyUnit::Sat).cloned().collect();
.filter(|k| known_keysets.contains(k)) if auth_keysets.is_empty() {
.collect(); // If we don't have any auth keysets, fetch them from the mint
let keysets = self.refresh_keysets().await?;
for keyset in unknown_keysets { Ok(keysets)
self.get_keyset_keys(keyset.id).await?; } else {
Ok(auth_keysets)
} }
} }
None => { None => {
for keyset in keysets { // If we don't have any keysets, fetch them from the mint
self.get_keyset_keys(keyset.id).await?; 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 /// This method always goes online to fetch the latest blind auth keyset information from the mint.
/// keysets /// 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))] #[instrument(skip(self))]
pub async fn get_active_mint_blind_auth_keyset(&self) -> Result<KeySetInfo, Error> { pub async fn refresh_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
let active_keysets = self.get_active_mint_blind_auth_keysets().await?; 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::<Vec<KeySetInfo>>();
// 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<KeySetInfo, Error> {
let auth_keysets = self.refresh_keysets().await?;
let keyset = auth_keysets.first().ok_or(Error::NoActiveKeyset)?;
Ok(keyset.clone()) 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))] #[instrument(skip(self))]
pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Error> { pub async fn get_unspent_auth_proofs(&self) -> Result<Vec<AuthProof>, Error> {
Ok(self Ok(self
@@ -334,9 +361,6 @@ impl AuthWallet {
let auth_token = self.client.get_auth_token().await?; 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 { match &auth_token {
AuthToken::ClearAuth(cat) => { AuthToken::ClearAuth(cat) => {
if cat.is_empty() { 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 = let premint_secrets =
PreMintSecrets::random(active_keyset_id, amount, &SplitTarget::Value(1.into()))?; 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 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 // Verify the signature DLEQ is valid
{ {
assert!(mint_res.signatures.len() == premint_secrets.secrets.len()); assert!(mint_res.signatures.len() == premint_secrets.secrets.len());
for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { 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)?; let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
Ok(_) => (), Ok(_) => (),

View File

@@ -227,7 +227,7 @@ impl Wallet {
tracing::warn!("Attempting to mint with expired quote."); 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 let count = self
.localstore .localstore
@@ -264,12 +264,12 @@ impl Wallet {
let mint_res = self.client.post_mint(request).await?; 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 // Verify the signature DLEQ is valid
{ {
for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { 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)?; let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
Ok(_) | Err(nut12::Error::MissingDleqProof) => (), Ok(_) | Err(nut12::Error::MissingDleqProof) => (),

View File

@@ -105,7 +105,7 @@ impl Wallet {
return Err(Error::UnknownQuote); 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 let count = self
.localstore .localstore
@@ -161,12 +161,12 @@ impl Wallet {
let mint_res = self.client.post_mint(request).await?; 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 // Verify the signature DLEQ is valid
{ {
for (sig, premint) in mint_res.signatures.iter().zip(&premint_secrets.secrets) { 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)?; let key = keys.amount_key(sig.amount).ok_or(Error::AmountKey)?;
match sig.verify_dleq(key, premint.blinded_message.blinded_secret) { match sig.verify_dleq(key, premint.blinded_message.blinded_secret) {
Ok(_) | Err(nut12::Error::MissingDleqProof) => (), Ok(_) | Err(nut12::Error::MissingDleqProof) => (),

View File

@@ -1,17 +1,18 @@
use std::collections::HashMap; use std::collections::HashMap;
use cdk_common::nut02::{KeySetInfos, KeySetInfosMethods};
use tracing::instrument; use tracing::instrument;
use crate::nuts::{Id, KeySetInfo, Keys}; use crate::nuts::{Id, KeySetInfo, Keys};
use crate::{Error, Wallet}; use crate::{Error, Wallet};
impl Wallet { impl Wallet {
/// Get keys for mint keyset /// Fetch keys for mint keyset
/// ///
/// Selected keys from localstore if they are already known /// Returns keys from local database if they are already stored.
/// If they are not known queries mint for keyset id and stores the [`Keys`] /// 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))] #[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? { let keys = if let Some(keys) = self.localstore.get_keys(&keyset_id).await? {
keys keys
} else { } else {
@@ -27,10 +28,12 @@ impl Wallet {
Ok(keys) 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 /// First checks the local database for cached keysets. If keysets are not found locally,
/// it can't find any. /// 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))] #[instrument(skip(self))]
pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> { pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
match self match self
@@ -39,86 +42,105 @@ impl Wallet {
.await? .await?
{ {
Some(keysets_info) => Ok(keysets_info), 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))] #[instrument(skip(self))]
pub async fn get_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> { 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)
}
/// 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<Vec<KeySetInfo>, 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::<Vec<KeySetInfo>>();
match self match self
.localstore .localstore
.get_mint_keysets(self.mint_url.clone()) .get_mint_keysets(self.mint_url.clone())
.await? .await?
{ {
Some(known_keysets) => { Some(keysets_info) => Ok(keysets_info),
let unknown_keysets: Vec<&KeySetInfo> = keysets None => Err(Error::UnknownKeySet),
.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?;
}
} }
} }
Ok(active_keysets) /// Refresh keysets by fetching the latest from mint - always goes online
}
/// Get active keyset for mint with the lowest fees
/// ///
/// Queries mint for current keysets then gets [`Keys`] for any unknown /// This method always goes online to fetch the latest keyset information from the mint.
/// keysets /// 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))] #[instrument(skip(self))]
pub async fn get_active_mint_keyset(&self) -> Result<KeySetInfo, Error> { pub async fn refresh_keysets(&self) -> Result<KeySetInfos, Error> {
// Important tracing::debug!("Refreshing keysets and ensuring we have keys");
let _ = self.get_mint_info().await?; let _ = self.get_mint_info().await?;
let active_keysets = self.get_active_mint_keysets().await?;
let keyset_with_lowest_fee = active_keysets // Fetch all current keysets from mint
.into_iter() let keysets_response = self.client.get_mint_keysets().await?;
.min_by_key(|key| key.input_fee_ppk) let all_keysets = keysets_response.keysets;
.ok_or(Error::NoActiveKeyset)?;
Ok(keyset_with_lowest_fee) // 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?;
}
} }
/// Get keyset fees for mint Ok(keysets)
}
/// Get the active keyset with the lowest fees - always goes online
///
/// 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 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)
}
/// 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 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> { pub async fn get_keyset_fees(&self) -> Result<HashMap<Id, u64>, Error> {
let keysets = self let keysets = self
.localstore .localstore
@@ -134,7 +156,11 @@ impl Wallet {
Ok(fees) 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> { pub async fn get_keyset_fees_by_id(&self, keyset_id: Id) -> Result<u64, Error> {
self.get_keyset_fees() self.get_keyset_fees()
.await? .await?

View File

@@ -146,7 +146,7 @@ impl Wallet {
.update_proofs_state(ys, State::Pending) .update_proofs_state(ys, State::Pending)
.await?; .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 let count = self
.localstore .localstore
@@ -317,7 +317,7 @@ impl Wallet {
let available_proofs = self.get_unspent_proofs().await?; let available_proofs = self.get_unspent_proofs().await?;
let active_keyset_ids = self let active_keyset_ids = self
.get_active_mint_keysets() .refresh_keysets()
.await? .await?
.into_iter() .into_iter()
.map(|k| k.id) .map(|k| k.id)

View File

@@ -378,12 +378,12 @@ impl Wallet {
self.get_mint_info().await?; 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; let mut restored_value = Amount::ZERO;
for keyset in keysets { 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 empty_batch = 0;
let mut start_counter = 0; let mut start_counter = 0;
@@ -632,7 +632,7 @@ impl Wallet {
let mint_pubkey = match keys_cache.get(&proof.keyset_id) { let mint_pubkey = match keys_cache.get(&proof.keyset_id) {
Some(keys) => keys.amount_key(proof.amount), Some(keys) => keys.amount_key(proof.amount),
None => { 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); let key = keys.amount_key(proof.amount);
keys_cache.insert(proof.keyset_id, keys); keys_cache.insert(proof.keyset_id, keys);

View File

@@ -293,7 +293,7 @@ impl MultiMintWallet {
{ {
Some(keysets_info) => keysets_info, Some(keysets_info) => keysets_info,
// Hit the keysets endpoint if we don't have the keysets for this Mint // 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)?; let proofs = token_data.proofs(&keysets_info)?;

View File

@@ -38,11 +38,11 @@ impl Wallet {
self.get_mint_info().await?; 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; let mut proofs = proofs;
@@ -70,7 +70,7 @@ impl Wallet {
for proof in &mut proofs { for proof in &mut proofs {
// Verify that proof DLEQ is valid // Verify that proof DLEQ is valid
if proof.dleq.is_some() { 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)?; let key = keys.amount_key(proof.amount).ok_or(Error::AmountKey)?;
proof.verify_dleq(key)?; proof.verify_dleq(key)?;
} }

View File

@@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
use cdk_common::nut02::KeySetInfosMethods;
use cdk_common::util::unix_time; use cdk_common::util::unix_time;
use cdk_common::wallet::{Transaction, TransactionDirection}; use cdk_common::wallet::{Transaction, TransactionDirection};
use tracing::instrument; use tracing::instrument;
@@ -32,11 +33,8 @@ impl Wallet {
// If online send check mint for current keysets fees // If online send check mint for current keysets fees
if opts.send_kind.is_online() { if opts.send_kind.is_online() {
if let Err(e) = self.get_active_mint_keyset().await { if let Err(e) = self.refresh_keysets().await {
tracing::error!( tracing::error!("Error refreshing keysets: {:?}. Using stored keysets", e);
"Error fetching active mint keyset: {:?}. Using stored keysets",
e
);
} }
} }
@@ -78,11 +76,12 @@ impl Wallet {
// Select proofs // Select proofs
let active_keyset_ids = self let active_keyset_ids = self
.get_active_mint_keysets() .get_mint_keysets()
.await? .await?
.into_iter() .active()
.map(|k| k.id) .map(|k| k.id)
.collect(); .collect();
let selected_proofs = Wallet::select_proofs( let selected_proofs = Wallet::select_proofs(
amount, amount,
available_proofs, available_proofs,
@@ -131,7 +130,7 @@ impl Wallet {
) -> Result<PreparedSend, Error> { ) -> Result<PreparedSend, Error> {
// Split amount with fee if necessary // Split amount with fee if necessary
let (send_amounts, send_fee) = if opts.include_fee { 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?; let keyset_fee_ppk = self.get_keyset_fees_by_id(active_keyset_id).await?;
tracing::debug!("Keyset fee per proof: {:?}", keyset_fee_ppk); tracing::debug!("Keyset fee per proof: {:?}", keyset_fee_ppk);
let send_split = amount.split_with_fee(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; let mut proofs_to_send = send.proofs_to_send;
// Get active keyset ID // 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); tracing::debug!("Active keyset ID: {:?}", active_keyset_id);
// Get keyset fees // Get keyset fees

View File

@@ -1,3 +1,4 @@
use cdk_common::nut02::KeySetInfosMethods;
use tracing::instrument; use tracing::instrument;
use crate::amount::SplitTarget; use crate::amount::SplitTarget;
@@ -167,11 +168,12 @@ impl Wallet {
ensure_cdk!(proofs_sum >= amount, Error::InsufficientFunds); ensure_cdk!(proofs_sum >= amount, Error::InsufficientFunds);
let active_keyset_ids = self let active_keyset_ids = self
.get_active_mint_keysets() .refresh_keysets()
.await? .await?
.into_iter() .active()
.map(|k| k.id) .map(|k| k.id)
.collect(); .collect();
let keyset_fees = self.get_keyset_fees().await?; let keyset_fees = self.get_keyset_fees().await?;
let proofs = Wallet::select_proofs( let proofs = Wallet::select_proofs(
amount, amount,
@@ -203,7 +205,7 @@ impl Wallet {
include_fees: bool, include_fees: bool,
) -> Result<PreSwap, Error> { ) -> Result<PreSwap, Error> {
tracing::info!("Creating swap"); 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 // Desired amount is either amount passed or value of all proof
let proofs_total = proofs.total_amount()?; let proofs_total = proofs.total_amount()?;