use std::path::Path;
use anyhow::{anyhow, Result};
use cdk::mint_url::MintUrl;
use cdk::nuts::{CurrencyUnit, MintInfo};
use cdk::wallet::WalletRepository;
use cdk::{Amount, OidcClient};
use clap::Args;
use serde::{Deserialize, Serialize};
use crate::token_storage;
use crate::utils::get_or_create_wallet;
#[derive(Args, Serialize, Deserialize)]
pub struct MintBlindAuthSubCommand {
mint_url: MintUrl,
amount: Option<u64>,
#[arg(long)]
cat: Option<String>,
}
pub async fn mint_blind_auth(
wallet_repository: &WalletRepository,
sub_command_args: &MintBlindAuthSubCommand,
work_dir: &Path,
unit: &CurrencyUnit,
) -> Result<()> {
let mint_url = sub_command_args.mint_url.clone();
if !wallet_repository.has_mint(&mint_url).await {
wallet_repository.add_wallet(mint_url.clone()).await?;
}
wallet_repository.fetch_mint_info(&mint_url).await?;
let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
let cat = match &sub_command_args.cat {
Some(token) => token.clone(),
None => {
match token_storage::get_token_for_mint(work_dir, &mint_url).await {
Ok(Some(token_data)) => {
println!("Using access token from cashu_tokens.json");
token_data.access_token
}
Ok(None) => {
return Err(anyhow::anyhow!(
"No access token provided and no token found in cashu_tokens.json for this mint"
));
}
Err(e) => {
return Err(anyhow::anyhow!(
"Failed to read token from cashu_tokens.json: {}",
e
));
}
}
}
};
if let Err(err) = wallet.set_cat(cat.clone()).await {
tracing::error!("Could not set cat: {}", err);
if let Ok(Some(token_data)) = token_storage::get_token_for_mint(work_dir, &mint_url).await {
println!("Attempting to refresh the access token...");
let mint_info = wallet_repository.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");
if let Err(e) = token_storage::save_tokens(
work_dir,
&mint_url,
&new_access_token,
&new_refresh_token,
)
.await
{
println!("Warning: Failed to save refreshed tokens: {e}");
}
if let Err(err) = wallet.set_cat(new_access_token).await {
tracing::error!("Could not set refreshed cat: {}", err);
return Err(anyhow::anyhow!(
"Authentication failed even after token refresh"
));
}
wallet.set_refresh_token(new_refresh_token).await?;
}
Err(e) => {
tracing::error!("Failed to refresh token: {}", e);
return Err(anyhow::anyhow!("Failed to refresh access token: {}", e));
}
}
} else {
return Err(anyhow::anyhow!(
"Authentication failed and no refresh token available"
));
}
} else {
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?;
}
}
println!("Attempting to mint blind auth");
let amount = match sub_command_args.amount {
Some(amount) => amount,
None => {
let mint_info = wallet_repository.fetch_mint_info(&mint_url).await?;
mint_info
.bat_max_mint()
.ok_or(anyhow!("Unknown max bat mint"))?
}
};
let proofs = wallet.mint_blind_auth(Amount::from(amount)).await?;
println!("Received {} auth proofs for mint {mint_url}", proofs.len());
Ok(())
}
async fn refresh_access_token(
mint_info: &MintInfo,
refresh_token: &str,
) -> Result<(String, String)> {
let openid_discovery = mint_info
.nuts
.nut21
.clone()
.ok_or_else(|| anyhow::anyhow!("OIDC discovery information not available"))?
.openid_discovery;
let oidc_client = OidcClient::new(openid_discovery, None);
let token_url = oidc_client.get_oidc_config().await?.token_endpoint;
let params = [
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
("client_id", "cashu-client"), ];
let client = cdk_common::HttpClient::new();
let response = client.post(&token_url).form(¶ms).send().await?;
if !response.is_success() {
return Err(anyhow::anyhow!(
"Token refresh failed with status: {}",
response.status()
));
}
let token_response: serde_json::Value = response.json().await?;
let access_token = token_response["access_token"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("No access token in refresh response"))?
.to_string();
let new_refresh_token = token_response["refresh_token"]
.as_str()
.unwrap_or(refresh_token)
.to_string();
Ok((access_token, new_refresh_token))
}