use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{bail, Result};
use cdk::amount::SplitTarget;
use cdk::mint_url::MintUrl;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::CurrencyUnit;
use cdk::wallet::{Wallet, WalletRepository};
use cdk::StreamExt;
use clap::Subcommand;
use nostr_sdk::ToBech32;
async fn get_wallet_for_mint(
wallet_repository: &WalletRepository,
mint_url_str: &str,
) -> Result<Arc<Wallet>> {
let mint_url = MintUrl::from_str(mint_url_str)?;
if !wallet_repository.has_mint(&mint_url).await {
wallet_repository.add_wallet(mint_url.clone()).await?;
}
match wallet_repository
.get_wallet(&mint_url, &CurrencyUnit::Sat)
.await
{
Ok(wallet) => Ok(Arc::new(wallet)),
Err(_) => Ok(Arc::new(
wallet_repository
.create_wallet(mint_url, CurrencyUnit::Sat, None)
.await?,
)),
}
}
#[derive(Subcommand)]
pub enum NpubCashSubCommand {
Sync,
List {
#[arg(long)]
since: Option<u64>,
#[arg(long, default_value = "table")]
format: String,
},
Subscribe,
SetMint {
url: String,
},
ShowKeys,
}
pub async fn npubcash(
wallet_repository: &WalletRepository,
mint_url: &str,
sub_command: &NpubCashSubCommand,
npubcash_url: Option<String>,
) -> Result<()> {
let base_url = npubcash_url.unwrap_or_else(|| "https://npubx.cash".to_string());
match sub_command {
NpubCashSubCommand::Sync => sync(wallet_repository, mint_url, &base_url).await,
NpubCashSubCommand::List { since, format } => {
list(wallet_repository, mint_url, &base_url, *since, format).await
}
NpubCashSubCommand::Subscribe => subscribe(wallet_repository, mint_url, &base_url).await,
NpubCashSubCommand::SetMint { url } => {
set_mint(wallet_repository, mint_url, &base_url, url).await
}
NpubCashSubCommand::ShowKeys => show_keys(wallet_repository, mint_url).await,
}
}
async fn ensure_active_mint(wallet_repository: &WalletRepository, mint_url: &str) -> Result<()> {
let mint_url_struct = MintUrl::from_str(mint_url)?;
match wallet_repository.get_active_npubcash_mint().await? {
Some(active_mint) => {
if active_mint != mint_url_struct {
bail!(
"Active NpubCash mint mismatch!\n\
Current active mint: {}\n\
Requested mint: {}\n\n\
You can only have one active mint for NpubCash at a time.\n\
Use 'set-mint' command to switch active mint.",
active_mint,
mint_url
);
}
}
None => {
wallet_repository
.set_active_npubcash_mint(mint_url_struct)
.await?;
println!("✓ Set {} as active NpubCash mint", mint_url);
}
}
Ok(())
}
async fn sync(wallet_repository: &WalletRepository, mint_url: &str, base_url: &str) -> Result<()> {
ensure_active_mint(wallet_repository, mint_url).await?;
println!("Syncing quotes from NpubCash...");
let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
wallet.enable_npubcash(base_url.to_string()).await?;
let quotes = wallet.sync_npubcash_quotes().await?;
println!("✓ Synced {} quotes successfully", quotes.len());
Ok(())
}
async fn list(
wallet_repository: &WalletRepository,
mint_url: &str,
base_url: &str,
since: Option<u64>,
format: &str,
) -> Result<()> {
ensure_active_mint(wallet_repository, mint_url).await?;
let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
wallet.enable_npubcash(base_url.to_string()).await?;
let quotes = if let Some(since_ts) = since {
wallet.sync_npubcash_quotes_since(since_ts).await?
} else {
wallet.sync_npubcash_quotes().await?
};
match format {
"json" => {
let json = serde_json::to_string_pretty("es)?;
println!("{}", json);
}
"table" => {
if quotes.is_empty() {
println!("No quotes found");
} else {
println!("\nQuotes:");
println!("{:-<80}", "");
for (i, quote) in quotes.iter().enumerate() {
println!("{}. ID: {}", i + 1, quote.id);
let amount_str = quote
.amount
.map_or("unknown".to_string(), |a| a.to_string());
println!(" Amount: {} {}", amount_str, quote.unit);
println!("{:-<80}", "");
}
println!("\nTotal: {} quotes", quotes.len());
}
}
_ => bail!("Invalid format '{}'. Use 'table' or 'json'", format),
}
Ok(())
}
async fn subscribe(
wallet_repository: &WalletRepository,
mint_url: &str,
base_url: &str,
) -> Result<()> {
ensure_active_mint(wallet_repository, mint_url).await?;
println!("=== NpubCash Quote Subscription ===\n");
let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
wallet.enable_npubcash(base_url.to_string()).await?;
println!("✓ NpubCash integration enabled\n");
let keys = wallet.get_npubcash_keys()?;
let display_url = base_url
.trim_start_matches("https://")
.trim_start_matches("http://");
println!("Your npub.cash address:");
println!(" {}@{}\n", keys.public_key().to_bech32()?, display_url);
println!("Send sats to this address to see them appear!\n");
println!("Auto-mint is ENABLED - paid quotes will be automatically minted\n");
println!("Starting quote polling...");
println!("Press Ctrl+C to stop.\n");
let mut stream =
wallet.npubcash_proof_stream(SplitTarget::default(), None, Duration::from_secs(5));
tokio::select! {
_ = async {
while let Some(result) = stream.next().await {
match result {
Ok((quote, proofs)) => {
let amount_str = quote.amount.map_or("unknown".to_string(), |a| a.to_string());
println!("Received payment for quote {}", quote.id);
println!(" ├─ Amount: {} {}", amount_str, quote.unit);
match proofs.total_amount() {
Ok(amount) => {
println!(" └─ Successfully minted {} sats!", amount);
if let Ok(balance) = wallet.total_balance().await {
println!(" Wallet balance: {} sats", balance);
}
}
Err(e) => println!(" └─ Failed to calculate amount: {}", e),
}
println!();
}
Err(e) => {
println!("Error processing payment: {}", e);
}
}
}
} => {}
_ = tokio::signal::ctrl_c() => {
println!("\nStopping quote polling...");
}
}
let balance = wallet.total_balance().await?;
println!("Final wallet balance: {} sats\n", balance);
Ok(())
}
async fn set_mint(
wallet_repository: &WalletRepository,
mint_url: &str,
base_url: &str,
url: &str,
) -> Result<()> {
println!("Setting NpubCash mint URL to: {}", url);
let mint_url_struct = MintUrl::from_str(mint_url)?;
wallet_repository
.set_active_npubcash_mint(mint_url_struct)
.await?;
let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
wallet.enable_npubcash(base_url.to_string()).await?;
match wallet.set_npubcash_mint_url(url).await {
Ok(_) => {
println!("✓ Mint URL updated successfully on NpubCash server");
println!("\nThe NpubCash server will now include this mint URL");
println!("when creating quotes for your npub address.");
}
Err(e) => {
let error_msg = e.to_string();
if error_msg.contains("API error (404)") {
println!("⚠️ Warning: NpubCash server does not support setting mint URL");
println!("\nThis means:");
println!(
" • The server at '{}' does not have the settings endpoint",
base_url
);
println!(" • Quotes will use whatever mint URL the server defaults to");
println!(" • You can still mint using your local wallet's mint configuration");
println!("\nNote: The official npubx.cash server supports this feature.");
println!(" Custom servers may not have it implemented.");
} else {
return Err(e.into());
}
}
}
Ok(())
}
async fn show_keys(wallet_repository: &WalletRepository, mint_url: &str) -> Result<()> {
let wallet = get_wallet_for_mint(wallet_repository, mint_url).await?;
let keys = wallet.get_npubcash_keys()?;
let npub = keys.public_key().to_bech32()?;
let nsec = keys.secret_key().to_bech32()?;
println!(
r#"
╔═══════════════════════════════════════════════════════════════════════════╗
║ NpubCash Nostr Keys ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ ║
║ These keys are automatically derived from your wallet seed and are ║
║ used for authenticating with the NpubCash service. ║
║ ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ ║
║ Public Key (npub): ║
║ {} ║
║ ║
║ NpubCash Address: ║
║ {}@npubx.cash ║
║ ║
╠═══════════════════════════════════════════════════════════════════════════╣
║ ║
║ Secret Key (nsec): ║
║ {} ║
║ ║
║ ⚠️ KEEP THIS SECRET! Anyone with this key can access your npubcash ║
║ account and authenticate as you. ║
║ ║
╚═══════════════════════════════════════════════════════════════════════════╝
"#,
npub, npub, nsec
);
Ok(())
}