Prechádzať zdrojové kódy

Load mint info (#1323)

* refactor(wallet): ensure mint info is loaded before quote operations

Load mint info upfront in issue_bolt11, issue_bolt12, and receive methods
to ensure database has current mint information before processing quotes.

* feat: load mint info in ffi

* refactor(cdk): replace deferred database persistence with synchronous writes in mint metadata cache

Changes background task spawning to direct async/await for database sync operations.
Ensures database consistency by persisting metadata before returning from cache operations.

* feat(cdk-ffi): add metadata cache TTL configuration methods

Add methods to configure metadata cache time-to-live for both Wallet and MultiMintWallet:
- set_metadata_cache_ttl for single wallet
- set_metadata_cache_ttl_for_mint for specific mint in multi-mint wallet
- set_metadata_cache_ttl_for_all_mints for all mints in multi-mint wallet
tsk 1 mesiac pred
rodič
commit
930704323e

+ 45 - 0
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -111,6 +111,51 @@ impl MultiMintWallet {
         self.inner.unit().clone().into()
     }
 
+    /// Set metadata cache TTL (time-to-live) in seconds for a specific mint
+    ///
+    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
+    /// before requiring a refresh from the mint server for a specific mint.
+    ///
+    /// # Arguments
+    ///
+    /// * `mint_url` - The mint URL to set the TTL for
+    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires.
+    pub async fn set_metadata_cache_ttl_for_mint(
+        &self,
+        mint_url: MintUrl,
+        ttl_secs: Option<u64>,
+    ) -> Result<(), FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let wallets = self.inner.get_wallets().await;
+
+        if let Some(wallet) = wallets.iter().find(|w| w.mint_url == cdk_mint_url) {
+            let ttl = ttl_secs.map(std::time::Duration::from_secs);
+            wallet.set_metadata_cache_ttl(ttl);
+            Ok(())
+        } else {
+            Err(FfiError::Generic {
+                msg: format!("Mint not found: {}", cdk_mint_url),
+            })
+        }
+    }
+
+    /// Set metadata cache TTL (time-to-live) in seconds for all mints
+    ///
+    /// Controls how long cached mint metadata is considered fresh for all mints
+    /// in this MultiMintWallet.
+    ///
+    /// # Arguments
+    ///
+    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires for any mint.
+    pub async fn set_metadata_cache_ttl_for_all_mints(&self, ttl_secs: Option<u64>) {
+        let wallets = self.inner.get_wallets().await;
+        let ttl = ttl_secs.map(std::time::Duration::from_secs);
+
+        for wallet in wallets.iter() {
+            wallet.set_metadata_cache_ttl(ttl);
+        }
+    }
+
     /// Add a mint to this MultiMintWallet
     pub async fn add_mint(
         &self,

+ 31 - 0
crates/cdk-ffi/src/wallet.rs

@@ -62,6 +62,29 @@ impl Wallet {
         self.inner.unit.clone().into()
     }
 
+    /// Set metadata cache TTL (time-to-live) in seconds
+    ///
+    /// Controls how long cached mint metadata (keysets, keys, mint info) is considered fresh
+    /// before requiring a refresh from the mint server.
+    ///
+    /// # Arguments
+    ///
+    /// * `ttl_secs` - Optional TTL in seconds. If None, cache never expires and is always used.
+    ///
+    /// # Example
+    ///
+    /// ```ignore
+    /// // Cache expires after 5 minutes
+    /// wallet.set_metadata_cache_ttl(Some(300));
+    ///
+    /// // Cache never expires (default)
+    /// wallet.set_metadata_cache_ttl(None);
+    /// ```
+    pub fn set_metadata_cache_ttl(&self, ttl_secs: Option<u64>) {
+        let ttl = ttl_secs.map(std::time::Duration::from_secs);
+        self.inner.set_metadata_cache_ttl(ttl);
+    }
+
     /// Get total balance
     pub async fn total_balance(&self) -> Result<Amount, FfiError> {
         let balance = self.inner.total_balance().await?;
@@ -86,6 +109,14 @@ impl Wallet {
         Ok(info.map(Into::into))
     }
 
+    /// Load mint info
+    ///
+    /// This will get mint info from cache if it is fresh
+    pub async fn load_mint_info(&self) -> Result<MintInfo, FfiError> {
+        let info = self.inner.load_mint_info().await?;
+        Ok(info.into())
+    }
+
     /// Receive tokens
     pub async fn receive(
         &self,

+ 3 - 5
crates/cdk/src/wallet/issue/issue_bolt11.rs

@@ -49,16 +49,14 @@ impl Wallet {
         amount: Amount,
         description: Option<String>,
     ) -> Result<MintQuote, Error> {
+        let mint_info = self.load_mint_info().await?;
+
         let mint_url = self.mint_url.clone();
         let unit = self.unit.clone();
 
         // If we have a description, we check that the mint supports it.
         if description.is_some() {
-            let settings = self
-                .localstore
-                .get_mint(mint_url.clone())
-                .await?
-                .ok_or(Error::IncorrectMint)?
+            let settings = mint_info
                 .nuts
                 .nut04
                 .get_settings(&unit, &crate::nuts::PaymentMethod::Bolt11)

+ 3 - 5
crates/cdk/src/wallet/issue/issue_bolt12.rs

@@ -26,16 +26,14 @@ impl Wallet {
         amount: Option<Amount>,
         description: Option<String>,
     ) -> Result<MintQuote, Error> {
+        let mint_info = self.load_mint_info().await?;
+
         let mint_url = self.mint_url.clone();
         let unit = &self.unit;
 
         // If we have a description, we check that the mint supports it.
         if description.is_some() {
-            let mint_method_settings = self
-                .localstore
-                .get_mint(mint_url.clone())
-                .await?
-                .ok_or(Error::IncorrectMint)?
+            let mint_method_settings = mint_info
                 .nuts
                 .nut04
                 .get_settings(unit, &crate::nuts::PaymentMethod::Bolt12)

+ 21 - 25
crates/cdk/src/wallet/mint_metadata_cache.rs

@@ -1,13 +1,13 @@
 //! Per-mint cryptographic key and metadata cache
 //!
 //! Provides on-demand fetching and caching of mint metadata (info, keysets, and keys)
-//! with atomic in-memory cache updates and deferred database persistence.
+//! with atomic in-memory cache updates and database persistence.
 //!
 //! # Architecture
 //!
 //! - **Pull-based loading**: Keys fetched on-demand from mint HTTP API
 //! - **Atomic cache**: Single `MintMetadata` snapshot updated via `ArcSwap`
-//! - **Deferred persistence**: Database writes happen asynchronously after cache update
+//! - **Synchronous persistence**: Database writes happen after cache update
 //! - **Multi-database support**: Tracks sync status per storage instance via pointer identity
 //!
 //! # Usage
@@ -34,7 +34,6 @@ use cdk_common::database::{self, WalletDatabase};
 use cdk_common::mint_url::MintUrl;
 use cdk_common::nuts::{KeySetInfo, Keys};
 use cdk_common::parking_lot::RwLock;
-use cdk_common::task::spawn;
 use cdk_common::{KeySet, MintInfo};
 use tokio::sync::Mutex;
 
@@ -99,11 +98,11 @@ pub struct MintMetadata {
     auth_status: FreshnessStatus,
 }
 
-/// On-demand mint metadata cache with deferred database persistence
+/// On-demand mint metadata cache with database persistence
 ///
 /// Manages a single mint's cryptographic keys and metadata. Fetches data from
-/// the mint's HTTP API on-demand and caches it in memory. Database writes are
-/// deferred to background tasks to avoid blocking operations.
+/// the mint's HTTP API on-demand and caches it in memory. Database writes
+/// occur synchronously to ensure persistence.
 ///
 /// # Thread Safety
 ///
@@ -198,8 +197,7 @@ impl MintMetadataCache {
     /// Load metadata from mint server and update cache
     ///
     /// Always performs an HTTP fetch from the mint server to get fresh data.
-    /// Updates the in-memory cache and spawns a background task to persist
-    /// to the database.
+    /// Updates the in-memory cache and persists to the database.
     ///
     /// Uses a mutex to ensure only one fetch runs at a time. If multiple
     /// callers request a fetch simultaneously, only one performs the HTTP
@@ -264,8 +262,8 @@ impl MintMetadataCache {
         #[cfg(not(feature = "auth"))]
         let metadata = self.fetch_from_http(Some(client)).await?;
 
-        // Spawn background task to persist to database (non-blocking)
-        self.spawn_database_sync(storage.clone(), metadata.clone());
+        // Persist to database
+        self.database_sync(storage.clone(), metadata.clone()).await;
 
         Ok(metadata)
     }
@@ -319,10 +317,9 @@ impl MintMetadataCache {
         {
             // Cache is ready - check if database needs updating
             if db_synced_version != cached_metadata.status.version {
-                // Database is stale - sync in background
-                // We spawn rather than await to avoid blocking the caller
-                // and to prevent deadlocks with any existing transactions
-                self.spawn_database_sync(storage.clone(), cached_metadata.clone());
+                // Database is stale - sync before returning
+                self.database_sync(storage.clone(), cached_metadata.clone())
+                    .await;
             }
             return Ok(cached_metadata);
         }
@@ -365,8 +362,9 @@ impl MintMetadataCache {
             && cached_metadata.auth_status.updated_at > Instant::now()
         {
             if db_synced_version != cached_metadata.status.version {
-                // Database needs updating - spawn background sync
-                self.spawn_database_sync(storage.clone(), cached_metadata.clone());
+                // Database needs updating - sync before returning
+                self.database_sync(storage.clone(), cached_metadata.clone())
+                    .await;
             }
             return Ok(cached_metadata);
         }
@@ -405,19 +403,19 @@ impl MintMetadataCache {
         // Auth data not in cache - fetch from mint
         let metadata = self.fetch_from_http(None, Some(auth_client)).await?;
 
-        // Spawn background task to persist
-        self.spawn_database_sync(storage.clone(), metadata.clone());
+        // Persist to database
+        self.database_sync(storage.clone(), metadata.clone()).await;
 
         Ok(metadata)
     }
 
-    /// Spawn a background task to sync metadata to database
+    /// Sync metadata to database
     ///
-    /// This is non-blocking and happens asynchronously. The task will:
+    /// This will:
     /// 1. Check if this sync is still needed (version may be superseded)
     /// 2. Save mint info, keysets, and keys to the database
     /// 3. Update the sync tracking to record this storage has been updated
-    fn spawn_database_sync(
+    async fn database_sync(
         &self,
         storage: Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
         metadata: Arc<MintMetadata>,
@@ -425,12 +423,10 @@ impl MintMetadataCache {
         let mint_url = self.mint_url.clone();
         let db_sync_versions = self.db_sync_versions.clone();
 
-        spawn(async move {
-            Self::persist_to_database(mint_url, storage, metadata, db_sync_versions).await
-        });
+        Self::persist_to_database(mint_url, storage, metadata, db_sync_versions).await
     }
 
-    /// Persist metadata to database (called from background task)
+    /// Persist metadata to database
     ///
     /// Saves mint info, keysets, and keys to the database. Checks version
     /// before writing to avoid redundant work if a newer version has already

+ 4 - 0
crates/cdk/src/wallet/receive.rs

@@ -26,6 +26,10 @@ impl Wallet {
         opts: ReceiveOptions,
         memo: Option<String>,
     ) -> Result<Amount, Error> {
+        // Incase the wallet is getting ecash for the first time
+        // we want to get the mint info for our db
+        let _mint_info = self.load_mint_info().await?;
+
         let mint_url = &self.mint_url;
 
         let active_keyset_id = self.fetch_active_keyset().await?.id;