Explorar o código

Configure internal Wallets of a MultiMintWallet (#1177)

David Caseria hai 3 semanas
pai
achega
5caa7d58ed

+ 6 - 12
crates/cdk-cli/src/sub_commands/cat_device_login.rs

@@ -29,19 +29,13 @@ pub async fn cat_device_login(
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
 
-    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
-        Some(wallet) => wallet.clone(),
-        None => {
-            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
-            multi_mint_wallet
-                .get_wallet(&mint_url)
-                .await
-                .expect("Wallet should exist after adding mint")
-        }
-    };
+    // Ensure the mint exists
+    if !multi_mint_wallet.has_mint(&mint_url).await {
+        multi_mint_wallet.add_mint(mint_url.clone()).await?;
+    }
 
-    let mint_info = wallet
-        .fetch_mint_info()
+    let mint_info = multi_mint_wallet
+        .fetch_mint_info(&mint_url)
         .await?
         .ok_or(anyhow!("Mint info not found"))?;
 

+ 7 - 14
crates/cdk-cli/src/sub_commands/cat_login.rs

@@ -31,20 +31,13 @@ pub async fn cat_login(
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
 
-    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
-        Some(wallet) => wallet.clone(),
-        None => {
-            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
-            multi_mint_wallet
-                .get_wallet(&mint_url)
-                .await
-                .expect("Wallet should exist after adding mint")
-                .clone()
-        }
-    };
-
-    let mint_info = wallet
-        .fetch_mint_info()
+    // Ensure the mint exists
+    if !multi_mint_wallet.has_mint(&mint_url).await {
+        multi_mint_wallet.add_mint(mint_url.clone()).await?;
+    }
+
+    let mint_info = multi_mint_wallet
+        .fetch_mint_info(&mint_url)
         .await?
         .ok_or(anyhow!("Mint info not found"))?;
 

+ 22 - 21
crates/cdk-cli/src/sub_commands/mint_blind_auth.rs

@@ -28,19 +28,12 @@ pub async fn mint_blind_auth(
 ) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
 
-    let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
-        Some(wallet) => wallet.clone(),
-        None => {
-            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
-            multi_mint_wallet
-                .get_wallet(&mint_url)
-                .await
-                .expect("Wallet should exist after adding mint")
-                .clone()
-        }
-    };
+    // Ensure the mint exists
+    if !multi_mint_wallet.has_mint(&mint_url).await {
+        multi_mint_wallet.add_mint(mint_url.clone()).await?;
+    }
 
-    wallet.fetch_mint_info().await?;
+    multi_mint_wallet.fetch_mint_info(&mint_url).await?;
 
     // Try to get the token from the provided argument or from the stored file
     let cat = match &sub_command_args.cat {
@@ -68,7 +61,7 @@ pub async fn mint_blind_auth(
     };
 
     // Try to set the access token
-    if let Err(err) = wallet.set_cat(cat.clone()).await {
+    if let Err(err) = multi_mint_wallet.set_cat(&mint_url, cat.clone()).await {
         tracing::error!("Could not set cat: {}", err);
 
         // Try to refresh the token if we have a refresh token
@@ -76,7 +69,7 @@ pub async fn mint_blind_auth(
             println!("Attempting to refresh the access token...");
 
             // Get the mint info to access OIDC configuration
-            if let Some(mint_info) = wallet.fetch_mint_info().await? {
+            if let Some(mint_info) = multi_mint_wallet.fetch_mint_info(&mint_url).await? {
                 match refresh_access_token(&mint_info, &token_data.refresh_token).await {
                     Ok((new_access_token, new_refresh_token)) => {
                         println!("Successfully refreshed access token");
@@ -94,7 +87,9 @@ pub async fn mint_blind_auth(
                         }
 
                         // Try setting the new access token
-                        if let Err(err) = wallet.set_cat(new_access_token).await {
+                        if let Err(err) =
+                            multi_mint_wallet.set_cat(&mint_url, new_access_token).await
+                        {
                             tracing::error!("Could not set refreshed cat: {}", err);
                             return Err(anyhow::anyhow!(
                                 "Authentication failed even after token refresh"
@@ -102,7 +97,9 @@ pub async fn mint_blind_auth(
                         }
 
                         // Set the refresh token
-                        wallet.set_refresh_token(new_refresh_token).await?;
+                        multi_mint_wallet
+                            .set_refresh_token(&mint_url, new_refresh_token)
+                            .await?;
                     }
                     Err(e) => {
                         tracing::error!("Failed to refresh token: {}", e);
@@ -119,8 +116,10 @@ pub async fn mint_blind_auth(
         // If we have a refresh token, set it
         if let Ok(Some(token_data)) = token_storage::get_token_for_mint(work_dir, &mint_url).await {
             tracing::info!("Attempting to use refresh access token to refresh auth token");
-            wallet.set_refresh_token(token_data.refresh_token).await?;
-            wallet.refresh_access_token().await?;
+            multi_mint_wallet
+                .set_refresh_token(&mint_url, token_data.refresh_token)
+                .await?;
+            multi_mint_wallet.refresh_access_token(&mint_url).await?;
         }
     }
 
@@ -129,8 +128,8 @@ pub async fn mint_blind_auth(
     let amount = match sub_command_args.amount {
         Some(amount) => amount,
         None => {
-            let mint_info = wallet
-                .fetch_mint_info()
+            let mint_info = multi_mint_wallet
+                .fetch_mint_info(&mint_url)
                 .await?
                 .ok_or(anyhow!("Unknown mint info"))?;
             mint_info
@@ -139,7 +138,9 @@ pub async fn mint_blind_auth(
         }
     };
 
-    let proofs = wallet.mint_blind_auth(Amount::from(amount)).await?;
+    let proofs = multi_mint_wallet
+        .mint_blind_auth(&mint_url, Amount::from(amount))
+        .await?;
 
     println!("Received {} auth proofs for mint {mint_url}", proofs.len());
 

+ 1 - 1
crates/cdk-cli/src/sub_commands/restore.rs

@@ -18,7 +18,7 @@ pub async fn restore(
     let wallet = match multi_mint_wallet.get_wallet(&mint_url).await {
         Some(wallet) => wallet.clone(),
         None => {
-            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
+            multi_mint_wallet.add_mint(mint_url.clone()).await?;
             multi_mint_wallet
                 .get_wallet(&mint_url)
                 .await

+ 1 - 1
crates/cdk-cli/src/utils.rs

@@ -34,7 +34,7 @@ pub async fn get_or_create_wallet(
         Some(wallet) => Ok(wallet.clone()),
         None => {
             tracing::debug!("Wallet does not exist creating..");
-            multi_mint_wallet.add_mint(mint_url.clone(), None).await?;
+            multi_mint_wallet.add_mint(mint_url.clone()).await?;
             Ok(multi_mint_wallet
                 .get_wallet(mint_url)
                 .await

+ 72 - 3
crates/cdk-ffi/src/multi_mint_wallet.rs

@@ -118,9 +118,16 @@ impl MultiMintWallet {
         target_proof_count: Option<u32>,
     ) -> Result<(), FfiError> {
         let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
-        self.inner
-            .add_mint(cdk_mint_url, target_proof_count.map(|c| c as usize))
-            .await?;
+
+        if let Some(count) = target_proof_count {
+            let config = cdk::wallet::multi_mint_wallet::WalletConfig::new()
+                .with_target_proof_count(count as usize);
+            self.inner
+                .add_mint_with_config(cdk_mint_url, config)
+                .await?;
+        } else {
+            self.inner.add_mint(cdk_mint_url).await?;
+        }
         Ok(())
     }
 
@@ -380,6 +387,68 @@ impl MultiMintWallet {
         self.inner.verify_token_dleq(&cdk_token).await?;
         Ok(())
     }
+
+    /// Query mint for current mint information
+    pub async fn fetch_mint_info(&self, mint_url: MintUrl) -> Result<Option<MintInfo>, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let mint_info = self.inner.fetch_mint_info(&cdk_mint_url).await?;
+        Ok(mint_info.map(Into::into))
+    }
+}
+
+/// Auth methods for MultiMintWallet
+#[uniffi::export(async_runtime = "tokio")]
+impl MultiMintWallet {
+    /// Set Clear Auth Token (CAT) for a specific mint
+    pub async fn set_cat(&self, mint_url: MintUrl, cat: String) -> Result<(), FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        self.inner.set_cat(&cdk_mint_url, cat).await?;
+        Ok(())
+    }
+
+    /// Set refresh token for a specific mint
+    pub async fn set_refresh_token(
+        &self,
+        mint_url: MintUrl,
+        refresh_token: String,
+    ) -> Result<(), FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        self.inner
+            .set_refresh_token(&cdk_mint_url, refresh_token)
+            .await?;
+        Ok(())
+    }
+
+    /// Refresh access token for a specific mint using the stored refresh token
+    pub async fn refresh_access_token(&self, mint_url: MintUrl) -> Result<(), FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        self.inner.refresh_access_token(&cdk_mint_url).await?;
+        Ok(())
+    }
+
+    /// Mint blind auth tokens at a specific mint
+    pub async fn mint_blind_auth(
+        &self,
+        mint_url: MintUrl,
+        amount: Amount,
+    ) -> Result<Proofs, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let proofs = self
+            .inner
+            .mint_blind_auth(&cdk_mint_url, amount.into())
+            .await?;
+        Ok(proofs.into_iter().map(|p| Arc::new(p.into())).collect())
+    }
+
+    /// Get unspent auth proofs for a specific mint
+    pub async fn get_unspent_auth_proofs(
+        &self,
+        mint_url: MintUrl,
+    ) -> Result<Vec<AuthProof>, FfiError> {
+        let cdk_mint_url: cdk::mint_url::MintUrl = mint_url.try_into()?;
+        let auth_proofs = self.inner.get_unspent_auth_proofs(&cdk_mint_url).await?;
+        Ok(auth_proofs.into_iter().map(Into::into).collect())
+    }
 }
 
 /// Transfer mode for mint-to-mint transfers

+ 13 - 0
crates/cdk/src/wallet/auth/mod.rs

@@ -65,4 +65,17 @@ impl Wallet {
         }
         Ok(())
     }
+
+    /// Set the auth client (AuthWallet) for this wallet
+    ///
+    /// This allows updating the auth wallet without recreating the wallet.
+    /// Also updates the client's auth wallet to keep them in sync.
+    #[instrument(skip_all)]
+    pub async fn set_auth_client(&self, auth_wallet: Option<AuthWallet>) {
+        let mut auth_wallet_guard = self.auth_wallet.write().await;
+        *auth_wallet_guard = auth_wallet.clone();
+
+        // Also update the client's auth wallet to keep them in sync
+        self.client.set_auth_wallet(auth_wallet).await;
+    }
 }

+ 14 - 0
crates/cdk/src/wallet/mod.rs

@@ -673,6 +673,20 @@ impl Wallet {
 
         Ok(())
     }
+
+    /// Set the client (MintConnector) for this wallet
+    ///
+    /// This allows updating the connector without recreating the wallet.
+    pub fn set_client(&mut self, client: Arc<dyn MintConnector + Send + Sync>) {
+        self.client = client;
+    }
+
+    /// Set the target proof count for this wallet
+    ///
+    /// This controls how many proofs of each denomination the wallet tries to maintain.
+    pub fn set_target_proof_count(&mut self, count: usize) {
+        self.target_proof_count = count;
+    }
 }
 
 impl Drop for Wallet {

+ 289 - 21
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -60,6 +60,50 @@ pub struct TransferResult {
     pub target_balance_after: Amount,
 }
 
+/// Configuration for individual wallets within MultiMintWallet
+#[derive(Clone, Default, Debug)]
+pub struct WalletConfig {
+    /// Custom mint connector implementation
+    pub mint_connector: Option<Arc<dyn super::MintConnector + Send + Sync>>,
+    /// Custom auth connector implementation
+    #[cfg(feature = "auth")]
+    pub auth_connector: Option<Arc<dyn super::auth::AuthMintConnector + Send + Sync>>,
+    /// Target number of proofs to maintain at each denomination
+    pub target_proof_count: Option<usize>,
+}
+
+impl WalletConfig {
+    /// Create a new empty WalletConfig
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Set custom mint connector
+    pub fn with_mint_connector(
+        mut self,
+        connector: Arc<dyn super::MintConnector + Send + Sync>,
+    ) -> Self {
+        self.mint_connector = Some(connector);
+        self
+    }
+
+    /// Set custom auth connector
+    #[cfg(feature = "auth")]
+    pub fn with_auth_connector(
+        mut self,
+        connector: Arc<dyn super::auth::AuthMintConnector + Send + Sync>,
+    ) -> Self {
+        self.auth_connector = Some(connector);
+        self
+    }
+
+    /// Set target proof count
+    pub fn with_target_proof_count(mut self, count: usize) -> Self {
+        self.target_proof_count = Some(count);
+        self
+    }
+}
+
 /// Multi Mint Wallet
 ///
 /// A wallet that manages multiple mints but supports only one currency unit.
@@ -89,8 +133,8 @@ pub struct TransferResult {
 /// // Add mints to the wallet
 /// let mint_url1: MintUrl = "https://mint1.example.com".parse()?;
 /// let mint_url2: MintUrl = "https://mint2.example.com".parse()?;
-/// wallet.add_mint(mint_url1.clone(), None).await?;
-/// wallet.add_mint(mint_url2, None).await?;
+/// wallet.add_mint(mint_url1.clone()).await?;
+/// wallet.add_mint(mint_url2).await?;
 ///
 /// // Check total balance across all mints
 /// let balance = wallet.total_balance().await?;
@@ -199,12 +243,149 @@ impl MultiMintWallet {
     }
 
     /// Adds a mint to this [MultiMintWallet]
+    ///
+    /// Creates a wallet for the specified mint using default or global settings.
+    /// For custom configuration, use `add_mint_with_config()`.
     #[instrument(skip(self))]
-    pub async fn add_mint(
+    pub async fn add_mint(&self, mint_url: MintUrl) -> Result<(), Error> {
+        // Create wallet with default settings
+        let wallet = self
+            .create_wallet_with_config(mint_url.clone(), None)
+            .await?;
+
+        // Insert into wallets map
+        let mut wallets = self.wallets.write().await;
+        wallets.insert(mint_url, wallet);
+
+        Ok(())
+    }
+
+    /// Adds a mint to this [MultiMintWallet] with custom configuration
+    ///
+    /// The provided configuration is used to create the wallet with custom connectors
+    /// and settings. Configuration is stored within the Wallet instance itself.
+    #[instrument(skip(self))]
+    pub async fn add_mint_with_config(
+        &self,
+        mint_url: MintUrl,
+        config: WalletConfig,
+    ) -> Result<(), Error> {
+        // Create wallet with the provided config
+        let wallet = self
+            .create_wallet_with_config(mint_url.clone(), Some(&config))
+            .await?;
+
+        // Insert into wallets map
+        let mut wallets = self.wallets.write().await;
+        wallets.insert(mint_url, wallet);
+
+        Ok(())
+    }
+
+    /// Set or update configuration for a mint
+    ///
+    /// If the wallet already exists, it will be updated with the new config.
+    /// If the wallet doesn't exist, it will be created with the specified config.
+    #[instrument(skip(self))]
+    pub async fn set_mint_config(
         &self,
         mint_url: MintUrl,
-        target_proof_count: Option<usize>,
+        config: WalletConfig,
     ) -> Result<(), Error> {
+        // Check if wallet already exists
+        if self.has_mint(&mint_url).await {
+            // Update existing wallet in place
+            let mut wallets = self.wallets.write().await;
+            if let Some(wallet) = wallets.get_mut(&mint_url) {
+                // Update target_proof_count if provided
+                if let Some(count) = config.target_proof_count {
+                    wallet.set_target_proof_count(count);
+                }
+
+                // Update connector if provided
+                if let Some(connector) = config.mint_connector {
+                    wallet.set_client(connector);
+                }
+
+                // TODO: Handle auth_connector if provided
+                #[cfg(feature = "auth")]
+                if let Some(_auth_connector) = config.auth_connector {
+                    // For now, we can't easily inject auth_connector into the wallet
+                    // This would require additional work on the Wallet API
+                    // We'll note this as a future enhancement
+                }
+            }
+            Ok(())
+        } else {
+            // Wallet doesn't exist, create it with the provided config
+            self.add_mint_with_config(mint_url, config).await
+        }
+    }
+
+    /// Set the auth client (AuthWallet) for a specific mint
+    ///
+    /// This allows updating the auth wallet for an existing mint wallet without recreating it.
+    #[cfg(feature = "auth")]
+    #[instrument(skip_all)]
+    pub async fn set_auth_client(
+        &self,
+        mint_url: &MintUrl,
+        auth_wallet: Option<super::auth::AuthWallet>,
+    ) -> Result<(), Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.set_auth_client(auth_wallet).await;
+        Ok(())
+    }
+
+    /// Remove mint from MultiMintWallet
+    #[instrument(skip(self))]
+    pub async fn remove_mint(&self, mint_url: &MintUrl) {
+        let mut wallets = self.wallets.write().await;
+        wallets.remove(mint_url);
+    }
+
+    /// Internal: Create wallet with optional custom configuration
+    ///
+    /// Priority order for configuration:
+    /// 1. Custom connector from config (if provided)
+    /// 2. Global settings (proxy/Tor)
+    /// 3. Default HttpClient
+    async fn create_wallet_with_config(
+        &self,
+        mint_url: MintUrl,
+        config: Option<&WalletConfig>,
+    ) -> Result<Wallet, Error> {
+        // Check if custom connector is provided in config
+        if let Some(cfg) = config {
+            if let Some(custom_connector) = &cfg.mint_connector {
+                // Use custom connector with WalletBuilder
+                let builder = WalletBuilder::new()
+                    .mint_url(mint_url.clone())
+                    .unit(self.unit.clone())
+                    .localstore(self.localstore.clone())
+                    .seed(self.seed)
+                    .target_proof_count(cfg.target_proof_count.unwrap_or(3))
+                    .shared_client(custom_connector.clone());
+
+                // TODO: Handle auth_connector if provided
+                #[cfg(feature = "auth")]
+                if let Some(_auth_connector) = &cfg.auth_connector {
+                    // For now, we can't easily inject auth_connector into the wallet
+                    // This would require additional work on the Wallet/WalletBuilder API
+                    // We'll note this as a future enhancement
+                }
+
+                return builder.build();
+            }
+        }
+
+        // Fall back to existing logic: proxy/Tor/default
+        let target_proof_count = config.and_then(|c| c.target_proof_count).unwrap_or(3);
+
         let wallet = if let Some(proxy_url) = &self.proxy_config {
             // Create wallet with proxy-configured client
             let client = crate::wallet::HttpClient::with_proxy(
@@ -228,7 +409,7 @@ impl MultiMintWallet {
                 .unit(self.unit.clone())
                 .localstore(self.localstore.clone())
                 .seed(self.seed)
-                .target_proof_count(target_proof_count.unwrap_or(3))
+                .target_proof_count(target_proof_count)
                 .client(client)
                 .build()?
         } else {
@@ -256,7 +437,7 @@ impl MultiMintWallet {
                     .unit(self.unit.clone())
                     .localstore(self.localstore.clone())
                     .seed(self.seed)
-                    .target_proof_count(target_proof_count.unwrap_or(3))
+                    .target_proof_count(target_proof_count)
                     .client(client)
                     .build()?
             } else {
@@ -266,7 +447,7 @@ impl MultiMintWallet {
                     self.unit.clone(),
                     self.localstore.clone(),
                     self.seed,
-                    target_proof_count,
+                    Some(target_proof_count),
                 )?
             }
 
@@ -278,22 +459,12 @@ impl MultiMintWallet {
                     self.unit.clone(),
                     self.localstore.clone(),
                     self.seed,
-                    target_proof_count,
+                    Some(target_proof_count),
                 )?
             }
         };
 
-        let mut wallets = self.wallets.write().await;
-        wallets.insert(mint_url, wallet);
-
-        Ok(())
-    }
-
-    /// Remove mint from MultiMintWallet
-    #[instrument(skip(self))]
-    pub async fn remove_mint(&self, mint_url: &MintUrl) {
-        let mut wallets = self.wallets.write().await;
-        wallets.remove(mint_url);
+        Ok(wallet)
     }
 
     /// Load all wallets from database that have proofs for this currency unit
@@ -317,7 +488,7 @@ impl MultiMintWallet {
             if mint_has_proofs_for_unit {
                 // Add mint to the MultiMintWallet if not already present
                 if !self.has_mint(&mint_url).await {
-                    self.add_mint(mint_url.clone(), None).await?
+                    self.add_mint(mint_url.clone()).await?
                 }
             }
         }
@@ -980,7 +1151,7 @@ impl MultiMintWallet {
 
         // Add the untrusted mint temporarily if needed
         if !is_trusted {
-            self.add_mint(mint_url.clone(), None).await?;
+            self.add_mint(mint_url.clone()).await?;
         }
 
         let wallets = self.wallets.read().await;
@@ -1414,6 +1585,103 @@ impl MultiMintWallet {
 
         Ok(total_consolidated)
     }
+
+    /// Mint blind auth tokens for a specific mint
+    ///
+    /// This is a convenience method that calls the underlying wallet's mint_blind_auth.
+    #[cfg(feature = "auth")]
+    #[instrument(skip_all)]
+    pub async fn mint_blind_auth(
+        &self,
+        mint_url: &MintUrl,
+        amount: Amount,
+    ) -> Result<Proofs, Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.mint_blind_auth(amount).await
+    }
+
+    /// Get unspent auth proofs for a specific mint
+    ///
+    /// This is a convenience method that calls the underlying wallet's get_unspent_auth_proofs.
+    #[cfg(feature = "auth")]
+    #[instrument(skip_all)]
+    pub async fn get_unspent_auth_proofs(
+        &self,
+        mint_url: &MintUrl,
+    ) -> Result<Vec<cdk_common::AuthProof>, Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.get_unspent_auth_proofs().await
+    }
+
+    /// Set Clear Auth Token (CAT) for authentication at a specific mint
+    ///
+    /// This is a convenience method that calls the underlying wallet's set_cat.
+    #[cfg(feature = "auth")]
+    #[instrument(skip_all)]
+    pub async fn set_cat(&self, mint_url: &MintUrl, cat: String) -> Result<(), Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.set_cat(cat).await
+    }
+
+    /// Set refresh token for authentication at a specific mint
+    ///
+    /// This is a convenience method that calls the underlying wallet's set_refresh_token.
+    #[cfg(feature = "auth")]
+    #[instrument(skip_all)]
+    pub async fn set_refresh_token(
+        &self,
+        mint_url: &MintUrl,
+        refresh_token: String,
+    ) -> Result<(), Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.set_refresh_token(refresh_token).await
+    }
+
+    /// Refresh CAT token for a specific mint
+    ///
+    /// This is a convenience method that calls the underlying wallet's refresh_access_token.
+    #[cfg(feature = "auth")]
+    #[instrument(skip(self))]
+    pub async fn refresh_access_token(&self, mint_url: &MintUrl) -> Result<(), Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.refresh_access_token().await
+    }
+
+    /// Query mint for current mint information
+    ///
+    /// This is a convenience method that calls the underlying wallet's fetch_mint_info.
+    #[instrument(skip(self))]
+    pub async fn fetch_mint_info(
+        &self,
+        mint_url: &MintUrl,
+    ) -> Result<Option<crate::nuts::MintInfo>, Error> {
+        let wallets = self.wallets.read().await;
+        let wallet = wallets.get(mint_url).ok_or(Error::UnknownMint {
+            mint_url: mint_url.to_string(),
+        })?;
+
+        wallet.fetch_mint_info().await
+    }
 }
 
 impl Drop for MultiMintWallet {