use std::str::FromStr;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use cdk::amount::SplitTarget;
use cdk::mint_url::MintUrl;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{CurrencyUnit, PaymentMethod};
use cdk::wallet::{Wallet, WalletRepository, WalletSubscription};
use cdk::{Amount, StreamExt};
use cdk_common::nut00::KnownMethod;
use cdk_common::NotificationPayload;
use clap::Args;
use serde::{Deserialize, Serialize};
use tokio::sync::Notify;
use crate::utils::get_or_create_wallet;
#[derive(Args, Serialize, Deserialize)]
pub struct MintSubCommand {
mint_url: MintUrl,
amount: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[arg(short, long)]
quote_id: Option<String>,
#[arg(long, default_value = "bolt11")]
method: String,
#[arg(short, long)]
expiry: Option<u64>,
#[arg(short, long)]
single_use: Option<bool>,
#[arg(long, default_value = "30")]
wait_duration: u64,
}
pub async fn mint(
wallet_repository: &WalletRepository,
sub_command_args: &MintSubCommand,
unit: &CurrencyUnit,
) -> Result<()> {
let mint_url = sub_command_args.mint_url.clone();
let description: Option<String> = sub_command_args.description.clone();
let wallet = get_or_create_wallet(wallet_repository, &mint_url, unit).await?;
let payment_method = PaymentMethod::from_str(&sub_command_args.method)?;
let quote = match &sub_command_args.quote_id {
None => match payment_method {
PaymentMethod::Known(KnownMethod::Bolt11) => {
let amount = sub_command_args
.amount
.ok_or(anyhow!("Amount must be defined"))?;
let quote = wallet
.mint_quote(
PaymentMethod::BOLT11,
Some(Amount::from(amount)),
description,
None,
)
.await?;
println!(
"Quote: id={}, state={}, amount={}, expiry={}",
quote.id,
quote.state,
quote.amount.map_or("none".to_string(), |a| a.to_string()),
quote.expiry
);
println!("Please pay: {}", quote.request);
quote
}
PaymentMethod::Known(KnownMethod::Bolt12) => {
let amount = sub_command_args.amount;
println!(
"Single use: {}",
sub_command_args
.single_use
.map_or("none".to_string(), |b| b.to_string())
);
let quote = wallet
.mint_quote(
payment_method.clone(),
amount.map(|a| a.into()),
description,
None,
)
.await?;
println!(
"Quote: id={}, amount={}, expiry={}",
quote.id,
quote.amount.map_or("none".to_string(), |a| a.to_string()),
quote.expiry
);
println!("Please pay: {}", quote.request);
quote
}
PaymentMethod::Known(KnownMethod::Onchain) => {
let amount = sub_command_args.amount;
let quote = wallet
.mint_quote(payment_method.clone(), amount.map(|a| a.into()), None, None)
.await?;
println!("Quote: id={}, expiry={}", quote.id, quote.expiry);
println!("Send sats to: {}", quote.request);
quote
}
_ => {
let amount = sub_command_args.amount;
println!(
"Single use: {}",
sub_command_args
.single_use
.map_or("none".to_string(), |b| b.to_string())
);
let quote = wallet
.mint_quote(payment_method.clone(), amount.map(|a| a.into()), None, None)
.await?;
println!(
"Quote: id={}, amount={}, expiry={}",
quote.id,
quote.amount.map_or("none".to_string(), |a| a.to_string()),
quote.expiry
);
println!("Please pay: {}", quote.request);
quote
}
},
Some(quote_id) => wallet
.localstore
.get_mint_quote(quote_id)
.await?
.ok_or(anyhow!("Unknown quote"))?,
};
tracing::debug!("Attempting mint for: {}", payment_method);
let stop_progress = Arc::new(Notify::new());
let progress_handle = spawn_progress_task(
wallet.clone(),
quote.id.clone(),
payment_method.clone(),
stop_progress.clone(),
)
.await;
let mut amount_minted = Amount::ZERO;
let mut proof_streams = wallet.proof_stream(quote, SplitTarget::default(), None);
while let Some(proofs) = proof_streams.next().await {
let proofs = match proofs {
Ok(proofs) => proofs,
Err(err) => {
tracing::error!("Proof streams ended with {:?}", err);
break;
}
};
let batch = proofs.total_amount()?;
amount_minted += batch;
println!("Minted {batch} {unit} (total: {amount_minted} {unit})");
}
stop_progress.notify_waiters();
if let Some(handle) = progress_handle {
let _ = handle.await;
}
println!("Received {amount_minted} from mint {mint_url}");
Ok(())
}
async fn spawn_progress_task(
wallet: Wallet,
quote_id: String,
payment_method: PaymentMethod,
stop: Arc<Notify>,
) -> Option<tokio::task::JoinHandle<()>> {
let subscription_filter = match payment_method {
PaymentMethod::Known(KnownMethod::Bolt11) => {
WalletSubscription::Bolt11MintQuoteState(vec![quote_id.clone()])
}
PaymentMethod::Known(KnownMethod::Bolt12) => {
WalletSubscription::Bolt12MintQuoteState(vec![quote_id.clone()])
}
PaymentMethod::Known(KnownMethod::Onchain) => {
WalletSubscription::MintQuoteOnchainState(vec![quote_id.clone()])
}
_ => return None,
};
let mut subscription = match wallet.subscribe(subscription_filter).await {
Ok(sub) => sub,
Err(err) => {
tracing::warn!("Failed to subscribe to mint quote updates: {}", err);
return None;
}
};
Some(tokio::spawn(async move {
let mut last_state: Option<String> = None;
let mut last_amount_paid: Option<Amount> = None;
let mut last_amount_issued: Option<Amount> = None;
loop {
tokio::select! {
biased;
_ = stop.notified() => break,
maybe_event = subscription.recv() => {
let Some(event) = maybe_event else { break };
match event.into_inner() {
NotificationPayload::MintQuoteBolt11Response(info)
if info.quote == quote_id =>
{
let state_str = info.state.to_string();
if last_state.as_deref() != Some(state_str.as_str()) {
println!("Quote state: {}", state_str);
last_state = Some(state_str);
}
}
NotificationPayload::MintQuoteBolt12Response(info)
if info.quote == quote_id
&& (last_amount_paid != Some(info.amount_paid)
|| last_amount_issued != Some(info.amount_issued)) =>
{
println!(
"Payment observed: amount_paid={}, amount_issued={}",
info.amount_paid, info.amount_issued
);
last_amount_paid = Some(info.amount_paid);
last_amount_issued = Some(info.amount_issued);
}
NotificationPayload::MintQuoteOnchainResponse(info)
if info.quote == quote_id
&& (last_amount_paid != Some(info.amount_paid)
|| last_amount_issued != Some(info.amount_issued)) =>
{
if info.amount_paid == Amount::ZERO {
println!("Waiting for onchain payment...");
} else if info.amount_paid > info.amount_issued {
println!(
"Payment confirmed: amount_paid={}, amount_issued={} (minting...)",
info.amount_paid, info.amount_issued
);
} else {
println!(
"Payment observed: amount_paid={}, amount_issued={}",
info.amount_paid, info.amount_issued
);
}
last_amount_paid = Some(info.amount_paid);
last_amount_issued = Some(info.amount_issued);
}
_ => {}
}
}
}
}
}))
}