mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 07:35:03 +01:00
Merge pull request #901 from thesimplekid/refresh_keys
feat: refactor wallet keyset management for better clarity
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()];
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
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>>();
|
||||
|
||||
pub async fn load_mint_keysets(&self) -> Result<Vec<KeySetInfo>, 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<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 => {
|
||||
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<KeySetInfo, Error> {
|
||||
let active_keysets = self.get_active_mint_blind_auth_keysets().await?;
|
||||
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;
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/// 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(_) => (),
|
||||
|
||||
@@ -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) => (),
|
||||
|
||||
@@ -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) => (),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/// 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
|
||||
.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?;
|
||||
}
|
||||
Some(keysets_info) => Ok(keysets_info),
|
||||
None => Err(Error::UnknownKeySet),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(active_keysets)
|
||||
}
|
||||
|
||||
/// Get active keyset for mint with the lowest fees
|
||||
/// 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_keyset(&self) -> Result<KeySetInfo, Error> {
|
||||
// Important
|
||||
pub async fn refresh_keysets(&self) -> Result<KeySetInfos, Error> {
|
||||
tracing::debug!("Refreshing keysets and ensuring we have keys");
|
||||
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)
|
||||
// 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?;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
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?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
Reference in New Issue
Block a user