Configure internal Wallets of a MultiMintWallet (#1177)

This commit is contained in:
David Caseria
2025-10-13 04:53:48 -04:00
committed by GitHub
parent f173b2da47
commit 5caa7d58ed
9 changed files with 424 additions and 72 deletions

View File

@@ -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"))?;

View File

@@ -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()
}
};
// 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"))?;

View File

@@ -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());

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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,
target_proof_count: Option<usize>,
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,
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 {