From 34eb10fd9e2eecd11459afd6097d4f326c8e1a5a Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Mon, 5 May 2025 08:41:54 +0100 Subject: [PATCH] Mpp cdk cli (#743) * refactor: Extract user input logic into a helper function * feat: get multiple quotes (hacky) * refactor: cdk-cli * refactor: cdk-cli * feat: refactor balances --- crates/cdk-cli/src/main.rs | 13 +- crates/cdk-cli/src/sub_commands/balance.rs | 6 +- crates/cdk-cli/src/sub_commands/melt.rs | 206 ++++++++++++++------- crates/cdk-cli/src/sub_commands/mint.rs | 16 +- crates/cdk-cli/src/sub_commands/receive.rs | 18 +- crates/cdk-cli/src/sub_commands/send.rs | 38 +--- crates/cdk-cli/src/utils.rs | 83 +++++++++ 7 files changed, 254 insertions(+), 126 deletions(-) create mode 100644 crates/cdk-cli/src/utils.rs diff --git a/crates/cdk-cli/src/main.rs b/crates/cdk-cli/src/main.rs index 3c1c96ad..d54b7e80 100644 --- a/crates/cdk-cli/src/main.rs +++ b/crates/cdk-cli/src/main.rs @@ -20,6 +20,7 @@ use url::Url; mod nostr_storage; mod sub_commands; mod token_storage; +mod utils; const DEFAULT_WORK_DIR: &str = ".cdk-cli"; @@ -183,7 +184,17 @@ async fn main() -> Result<()> { let wallet = builder.build()?; - wallet.get_mint_info().await?; + let wallet_clone = wallet.clone(); + + tokio::spawn(async move { + if let Err(err) = wallet_clone.get_mint_info().await { + tracing::error!( + "Could not get mint quote for {}, {}", + wallet_clone.mint_url, + err + ); + } + }); wallets.push(wallet); } diff --git a/crates/cdk-cli/src/sub_commands/balance.rs b/crates/cdk-cli/src/sub_commands/balance.rs index 6302d62f..4e25e74a 100644 --- a/crates/cdk-cli/src/sub_commands/balance.rs +++ b/crates/cdk-cli/src/sub_commands/balance.rs @@ -19,7 +19,11 @@ pub async fn mint_balances( let mut wallets_vec = Vec::with_capacity(wallets.len()); - for (i, (mint_url, amount)) in wallets.iter().enumerate() { + for (i, (mint_url, amount)) in wallets + .iter() + .filter(|(_, a)| a > &&Amount::ZERO) + .enumerate() + { let mint_url = mint_url.clone(); println!("{i}: {mint_url} {amount} {unit}"); wallets_vec.push((mint_url, *amount)) diff --git a/crates/cdk-cli/src/sub_commands/melt.rs b/crates/cdk-cli/src/sub_commands/melt.rs index 279738b3..51d43e5e 100644 --- a/crates/cdk-cli/src/sub_commands/melt.rs +++ b/crates/cdk-cli/src/sub_commands/melt.rs @@ -1,5 +1,3 @@ -use std::io; -use std::io::Write; use std::str::FromStr; use anyhow::{bail, Result}; @@ -9,8 +7,10 @@ use cdk::wallet::multi_mint_wallet::MultiMintWallet; use cdk::wallet::types::WalletKey; use cdk::Bolt11Invoice; use clap::Args; +use tokio::task::JoinSet; use crate::sub_commands::balance::mint_balances; +use crate::utils::{get_number_input, get_user_input, get_wallet_by_index, validate_mint_number}; #[derive(Args)] pub struct MeltSubCommand { @@ -29,82 +29,148 @@ pub async fn pay( let unit = CurrencyUnit::from_str(&sub_command_args.unit)?; let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?; - println!("Enter mint number to melt from"); + let mut mints = vec![]; + let mut mint_amounts = vec![]; + if sub_command_args.mpp { + loop { + let mint_number: String = + get_user_input("Enter mint number to melt from and -1 when done.")?; - let mut user_input = String::new(); - let stdin = io::stdin(); - io::stdout().flush().unwrap(); - stdin.read_line(&mut user_input)?; - - let mint_number: usize = user_input.trim().parse()?; - - if mint_number.gt(&(mints_amounts.len() - 1)) { - bail!("Invalid mint number"); - } - - let wallet = mints_amounts[mint_number].0.clone(); - - let wallet = multi_mint_wallet - .get_wallet(&WalletKey::new(wallet, unit)) - .await - .expect("Known wallet"); - - println!("Enter bolt11 invoice request"); - - let mut user_input = String::new(); - let stdin = io::stdin(); - io::stdout().flush().unwrap(); - stdin.read_line(&mut user_input)?; - let bolt11 = Bolt11Invoice::from_str(user_input.trim())?; - - let available_funds = - >::into(mints_amounts[mint_number].1) * MSAT_IN_SAT; - - // Determine payment amount and options - let options = if sub_command_args.mpp || bolt11.amount_milli_satoshis().is_none() { - // Get user input for amount - println!( - "Enter the amount you would like to pay in sats for a {} payment.", - if sub_command_args.mpp { - "MPP" - } else { - "amountless invoice" + if mint_number == "-1" || mint_number.is_empty() { + break; } - ); - let mut user_input = String::new(); - io::stdout().flush()?; - io::stdin().read_line(&mut user_input)?; + let mint_number: usize = mint_number.parse()?; + validate_mint_number(mint_number, mints_amounts.len())?; - let user_amount = user_input.trim_end().parse::()? * MSAT_IN_SAT; - - if user_amount > available_funds { - bail!("Not enough funds"); + mints.push(mint_number); + let melt_amount: u64 = + get_number_input("Enter amount to mint from this mint in sats.")?; + mint_amounts.push(melt_amount); } - Some(if sub_command_args.mpp { - MeltOptions::new_mpp(user_amount) - } else { - MeltOptions::new_amountless(user_amount) - }) + let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?; + + let mut quotes = JoinSet::new(); + + for (mint, amount) in mints.iter().zip(mint_amounts) { + let wallet = mints_amounts[*mint].0.clone(); + + let wallet = multi_mint_wallet + .get_wallet(&WalletKey::new(wallet, unit.clone())) + .await + .expect("Known wallet"); + let options = MeltOptions::new_mpp(amount * 1000); + + let bolt11_clone = bolt11.clone(); + + quotes.spawn(async move { + let quote = wallet + .melt_quote(bolt11_clone.to_string(), Some(options)) + .await; + + (wallet, quote) + }); + } + + let quotes = quotes.join_all().await; + + for (wallet, quote) in quotes.iter() { + if let Err(quote) = quote { + tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, quote); + bail!("Could not get melt quote for {}", wallet.mint_url); + } else { + let quote = quote.as_ref().unwrap(); + println!( + "Melt quote {} for mint {} of amount {} with fee {}.", + quote.id, wallet.mint_url, quote.amount, quote.fee_reserve + ); + } + } + + let mut melts = JoinSet::new(); + + for (wallet, quote) in quotes { + let quote = quote.expect("Errors checked above"); + + melts.spawn(async move { + let melt = wallet.melt("e.id).await; + (wallet, melt) + }); + } + + let melts = melts.join_all().await; + + let mut error = false; + + for (wallet, melt) in melts { + match melt { + Ok(melt) => { + println!( + "Melt for {} paid {} with fee of {} ", + wallet.mint_url, melt.amount, melt.fee_paid + ); + } + Err(err) => { + println!("Melt for {} failed with {}", wallet.mint_url, err); + error = true; + } + } + } + + if error { + bail!("Could not complete all melts"); + } } else { - // Check if invoice amount exceeds available funds - let invoice_amount = bolt11.amount_milli_satoshis().unwrap(); - if invoice_amount > available_funds { - bail!("Not enough funds"); + let mint_number: usize = get_number_input("Enter mint number to melt from")?; + + let wallet = + get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone()) + .await?; + + let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?; + + let available_funds = + >::into(mints_amounts[mint_number].1) * MSAT_IN_SAT; + + // Determine payment amount and options + let options = if bolt11.amount_milli_satoshis().is_none() { + // Get user input for amount + let prompt = format!( + "Enter the amount you would like to pay in sats for a {} payment.", + if sub_command_args.mpp { + "MPP" + } else { + "amountless invoice" + } + ); + + let user_amount = get_number_input::(&prompt)? * MSAT_IN_SAT; + + if user_amount > available_funds { + bail!("Not enough funds"); + } + + Some(MeltOptions::new_amountless(user_amount)) + } else { + // Check if invoice amount exceeds available funds + let invoice_amount = bolt11.amount_milli_satoshis().unwrap(); + if invoice_amount > available_funds { + bail!("Not enough funds"); + } + None + }; + + // Process payment + let quote = wallet.melt_quote(bolt11.to_string(), options).await?; + println!("{:?}", quote); + + let melt = wallet.melt("e.id).await?; + println!("Paid invoice: {}", melt.state); + + if let Some(preimage) = melt.preimage { + println!("Payment preimage: {}", preimage); } - None - }; - - // Process payment - let quote = wallet.melt_quote(bolt11.to_string(), options).await?; - println!("{:?}", quote); - - let melt = wallet.melt("e.id).await?; - println!("Paid invoice: {}", melt.state); - - if let Some(preimage) = melt.preimage { - println!("Payment preimage: {}", preimage); } Ok(()) diff --git a/crates/cdk-cli/src/sub_commands/mint.rs b/crates/cdk-cli/src/sub_commands/mint.rs index aa1cdc91..1bfbc03d 100644 --- a/crates/cdk-cli/src/sub_commands/mint.rs +++ b/crates/cdk-cli/src/sub_commands/mint.rs @@ -5,12 +5,13 @@ use cdk::amount::SplitTarget; use cdk::mint_url::MintUrl; use cdk::nuts::nut00::ProofsMethods; use cdk::nuts::{CurrencyUnit, MintQuoteState, NotificationPayload}; -use cdk::wallet::types::WalletKey; use cdk::wallet::{MultiMintWallet, WalletSubscription}; use cdk::Amount; use clap::Args; use serde::{Deserialize, Serialize}; +use crate::utils::get_or_create_wallet; + #[derive(Args, Serialize, Deserialize)] pub struct MintSubCommand { /// Mint url @@ -36,18 +37,7 @@ pub async fn mint( let unit = CurrencyUnit::from_str(&sub_command_args.unit)?; let description: Option = sub_command_args.description.clone(); - let wallet = match multi_mint_wallet - .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone())) - .await - { - Some(wallet) => wallet.clone(), - None => { - tracing::debug!("Wallet does not exist creating.."); - multi_mint_wallet - .create_and_add_wallet(&mint_url.to_string(), unit, None) - .await? - } - }; + let wallet = get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?; let quote_id = match &sub_command_args.quote_id { None => { diff --git a/crates/cdk-cli/src/sub_commands/receive.rs b/crates/cdk-cli/src/sub_commands/receive.rs index 7841c602..4a81c1df 100644 --- a/crates/cdk-cli/src/sub_commands/receive.rs +++ b/crates/cdk-cli/src/sub_commands/receive.rs @@ -14,6 +14,7 @@ use nostr_sdk::nips::nip04; use nostr_sdk::{Filter, Keys, Kind, Timestamp}; use crate::nostr_storage; +use crate::utils::get_or_create_wallet; #[derive(Args)] pub struct ReceiveSubCommand { @@ -137,17 +138,14 @@ async fn receive_token( let token: Token = Token::from_str(token_str)?; let mint_url = token.mint_url()?; + let unit = token.unit().unwrap_or_default(); - let wallet_key = WalletKey::new(mint_url.clone(), token.unit().unwrap_or_default()); - - if multi_mint_wallet.get_wallet(&wallet_key).await.is_none() { - multi_mint_wallet - .create_and_add_wallet( - &mint_url.to_string(), - token.unit().unwrap_or_default(), - None, - ) - .await?; + if multi_mint_wallet + .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone())) + .await + .is_none() + { + get_or_create_wallet(multi_mint_wallet, &mint_url, unit).await?; } let amount = multi_mint_wallet diff --git a/crates/cdk-cli/src/sub_commands/send.rs b/crates/cdk-cli/src/sub_commands/send.rs index 643f8d32..cbae5bbd 100644 --- a/crates/cdk-cli/src/sub_commands/send.rs +++ b/crates/cdk-cli/src/sub_commands/send.rs @@ -1,15 +1,14 @@ -use std::io; -use std::io::Write; use std::str::FromStr; -use anyhow::{bail, Result}; +use anyhow::Result; use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions}; -use cdk::wallet::types::{SendKind, WalletKey}; +use cdk::wallet::types::SendKind; use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions}; use cdk::Amount; use clap::Args; use crate::sub_commands::balance::mint_balances; +use crate::utils::{check_sufficient_funds, get_number_input, get_wallet_by_index}; #[derive(Args)] pub struct SendSubCommand { @@ -55,30 +54,13 @@ pub async fn send( let unit = CurrencyUnit::from_str(&sub_command_args.unit)?; let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?; - println!("Enter mint number to create token"); + let mint_number: usize = get_number_input("Enter mint number to create token")?; - let mut user_input = String::new(); - let stdin = io::stdin(); - io::stdout().flush().unwrap(); - stdin.read_line(&mut user_input)?; + let wallet = get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit).await?; - let mint_number: usize = user_input.trim().parse()?; + let token_amount = Amount::from(get_number_input::("Enter value of token in sats")?); - if mint_number.gt(&(mints_amounts.len() - 1)) { - bail!("Invalid mint number"); - } - - println!("Enter value of token in sats"); - - let mut user_input = String::new(); - let stdin = io::stdin(); - io::stdout().flush().unwrap(); - stdin.read_line(&mut user_input)?; - let token_amount = Amount::from(user_input.trim().parse::()?); - - if token_amount.gt(&mints_amounts[mint_number].1) { - bail!("Not enough funds"); - } + check_sufficient_funds(mints_amounts[mint_number].1, token_amount)?; let conditions = match &sub_command_args.preimage { Some(preimage) => { @@ -156,12 +138,6 @@ pub async fn send( }, }; - let wallet = mints_amounts[mint_number].0.clone(); - let wallet = multi_mint_wallet - .get_wallet(&WalletKey::new(wallet, unit)) - .await - .expect("Known wallet"); - let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) { (true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)), (true, None) => SendKind::OfflineExact, diff --git a/crates/cdk-cli/src/utils.rs b/crates/cdk-cli/src/utils.rs new file mode 100644 index 00000000..6a1ffe1c --- /dev/null +++ b/crates/cdk-cli/src/utils.rs @@ -0,0 +1,83 @@ +use std::io::{self, Write}; +use std::str::FromStr; + +use anyhow::{bail, Result}; +use cdk::mint_url::MintUrl; +use cdk::nuts::CurrencyUnit; +use cdk::wallet::multi_mint_wallet::MultiMintWallet; +use cdk::wallet::types::WalletKey; +use cdk::Amount; + +/// Helper function to get user input with a prompt +pub fn get_user_input(prompt: &str) -> Result { + println!("{}", prompt); + let mut user_input = String::new(); + io::stdout().flush()?; + io::stdin().read_line(&mut user_input)?; + Ok(user_input.trim().to_string()) +} + +/// Helper function to get a number from user input with a prompt +pub fn get_number_input(prompt: &str) -> Result +where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, +{ + let input = get_user_input(prompt)?; + let number = input.parse::()?; + Ok(number) +} + +/// Helper function to validate a mint number against available mints +pub fn validate_mint_number(mint_number: usize, mint_count: usize) -> Result<()> { + if mint_number >= mint_count { + bail!("Invalid mint number"); + } + Ok(()) +} + +/// Helper function to check if there are enough funds for an operation +pub fn check_sufficient_funds(available: Amount, required: Amount) -> Result<()> { + if required.gt(&available) { + bail!("Not enough funds"); + } + Ok(()) +} + +/// Helper function to get a wallet from the multi-mint wallet +pub async fn get_wallet_by_index( + multi_mint_wallet: &MultiMintWallet, + mint_amounts: &[(MintUrl, Amount)], + mint_number: usize, + unit: CurrencyUnit, +) -> Result { + validate_mint_number(mint_number, mint_amounts.len())?; + + let wallet_key = WalletKey::new(mint_amounts[mint_number].0.clone(), unit); + let wallet = multi_mint_wallet + .get_wallet(&wallet_key) + .await + .ok_or_else(|| anyhow::anyhow!("Wallet not found"))?; + + Ok(wallet.clone()) +} + +/// Helper function to create or get a wallet +pub async fn get_or_create_wallet( + multi_mint_wallet: &MultiMintWallet, + mint_url: &MintUrl, + unit: CurrencyUnit, +) -> Result { + match multi_mint_wallet + .get_wallet(&WalletKey::new(mint_url.clone(), unit.clone())) + .await + { + Some(wallet) => Ok(wallet.clone()), + None => { + tracing::debug!("Wallet does not exist creating.."); + multi_mint_wallet + .create_and_add_wallet(&mint_url.to_string(), unit, None) + .await + } + } +}