diff --git a/crates/cdk-cli/src/sub_commands/melt.rs b/crates/cdk-cli/src/sub_commands/melt.rs index a07db2ff..39d7c867 100644 --- a/crates/cdk-cli/src/sub_commands/melt.rs +++ b/crates/cdk-cli/src/sub_commands/melt.rs @@ -71,6 +71,50 @@ pub async fn pay( bail!("No funds available"); } + // Determine which mint to use for melting BEFORE processing payment (unless using MPP) + let selected_mint = if sub_command_args.mpp { + None // MPP mode handles mint selection differently + } else if let Some(mint_url) = &sub_command_args.mint_url { + Some(MintUrl::from_str(mint_url)?) + } else { + // Display all mints with their balances and let user select + let balances_map = multi_mint_wallet.get_balances().await?; + if balances_map.is_empty() { + bail!("No mints available in the wallet"); + } + + let balances_vec: Vec<(MintUrl, Amount)> = balances_map.into_iter().collect(); + + println!("\nAvailable mints and balances:"); + for (index, (mint_url, balance)) in balances_vec.iter().enumerate() { + println!( + " {}: {} - {} {}", + index, + mint_url, + balance, + multi_mint_wallet.unit() + ); + } + println!(" {}: Any mint (auto-select best)", balances_vec.len()); + + let selection = loop { + let selection: usize = + get_number_input("Enter mint number to melt from (or select Any)")?; + + if selection == balances_vec.len() { + break None; // "Any" option selected + } + + if let Some((mint_url, _)) = balances_vec.get(selection) { + break Some(mint_url.clone()); + } + + println!("Invalid selection, please try again."); + }; + + selection + }; + if sub_command_args.mpp { // Manual MPP - user specifies which mints and amounts to use if !matches!(sub_command_args.method, PaymentType::Bolt11) { @@ -180,12 +224,9 @@ pub async fn pay( let options = create_melt_options(available_funds, bolt11.amount_milli_satoshis(), &prompt)?; - // Use mint-specific functions or auto-select - let melted = if let Some(mint_url) = &sub_command_args.mint_url { - // User specified a mint - use the new mint-specific functions - let mint_url = MintUrl::from_str(mint_url)?; - - // Create a melt quote for the specific mint + // Use selected mint or auto-select + let melted = if let Some(mint_url) = selected_mint { + // User selected a specific mint - use the new mint-specific functions let quote = multi_mint_wallet .melt_quote(&mint_url, bolt11_str.clone(), options) .await?; @@ -200,7 +241,7 @@ pub async fn pay( .melt_with_mint(&mint_url, "e.id) .await? } else { - // Let the wallet automatically select the best mint + // User selected "Any" - let the wallet auto-select the best mint multi_mint_wallet.melt(&bolt11_str, options, None).await? }; @@ -227,41 +268,25 @@ pub async fn pay( let options = create_melt_options(available_funds, amount_msat, &prompt)?; - // Get wallet for BOLT12 - let wallet = if let Some(mint_url) = &sub_command_args.mint_url { - // User specified a mint - let mint_url = MintUrl::from_str(mint_url)?; - multi_mint_wallet - .get_wallet(&mint_url) - .await - .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))? + // Get wallet for BOLT12 using the selected mint + let mint_url = if let Some(specific_mint) = selected_mint { + specific_mint } else { - // Show available mints and let user select + // User selected "Any" - just pick the first mint with any balance let balances = multi_mint_wallet.get_balances().await?; - println!("\nAvailable mints:"); - for (i, (mint_url, balance)) in balances.iter().enumerate() { - println!( - " {}: {} - {} {}", - i, - mint_url, - balance, - multi_mint_wallet.unit() - ); - } - let mint_number: usize = get_number_input("Enter mint number to melt from")?; - let selected_mint = balances - .iter() - .nth(mint_number) - .map(|(url, _)| url) - .ok_or_else(|| anyhow::anyhow!("Invalid mint number"))?; - - multi_mint_wallet - .get_wallet(selected_mint) - .await - .ok_or_else(|| anyhow::anyhow!("Mint {} not found", selected_mint))? + balances + .into_iter() + .find(|(_, balance)| *balance > Amount::ZERO) + .map(|(mint_url, _)| mint_url) + .ok_or_else(|| anyhow::anyhow!("No mint available for BOLT12 payment"))? }; + let wallet = multi_mint_wallet + .get_wallet(&mint_url) + .await + .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?; + // Get melt quote for BOLT12 let quote = wallet.melt_bolt12_quote(offer_str, options).await?; @@ -293,41 +318,25 @@ pub async fn pay( // BIP353 payments are always amountless for now let options = create_melt_options(available_funds, None, &prompt)?; - // Get wallet for BIP353 - let wallet = if let Some(mint_url) = &sub_command_args.mint_url { - // User specified a mint - let mint_url = MintUrl::from_str(mint_url)?; - multi_mint_wallet - .get_wallet(&mint_url) - .await - .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))? + // Get wallet for BIP353 using the selected mint + let mint_url = if let Some(specific_mint) = selected_mint { + specific_mint } else { - // Show available mints and let user select + // User selected "Any" - just pick the first mint with any balance let balances = multi_mint_wallet.get_balances().await?; - println!("\nAvailable mints:"); - for (i, (mint_url, balance)) in balances.iter().enumerate() { - println!( - " {}: {} - {} {}", - i, - mint_url, - balance, - multi_mint_wallet.unit() - ); - } - let mint_number: usize = get_number_input("Enter mint number to melt from")?; - let selected_mint = balances - .iter() - .nth(mint_number) - .map(|(url, _)| url) - .ok_or_else(|| anyhow::anyhow!("Invalid mint number"))?; - - multi_mint_wallet - .get_wallet(selected_mint) - .await - .ok_or_else(|| anyhow::anyhow!("Mint {} not found", selected_mint))? + balances + .into_iter() + .find(|(_, balance)| *balance > Amount::ZERO) + .map(|(mint_url, _)| mint_url) + .ok_or_else(|| anyhow::anyhow!("No mint available for BIP353 payment"))? }; + let wallet = multi_mint_wallet + .get_wallet(&mint_url) + .await + .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?; + // Get melt quote for BIP353 address (internally resolves and gets BOLT12 quote) let quote = wallet .melt_bip353_quote( diff --git a/crates/cdk-cli/src/sub_commands/send.rs b/crates/cdk-cli/src/sub_commands/send.rs index 0c8fb61f..8e2dc4af 100644 --- a/crates/cdk-cli/src/sub_commands/send.rs +++ b/crates/cdk-cli/src/sub_commands/send.rs @@ -54,9 +54,7 @@ pub struct SendSubCommand { /// Maximum amount to transfer from other mints #[arg(long)] max_transfer_amount: Option, - /// Specific mints allowed for transfers (can be specified multiple times) - #[arg(long, action = clap::ArgAction::Append)] - allowed_mints: Vec, + /// Specific mints to exclude from transfers (can be specified multiple times) #[arg(long, action = clap::ArgAction::Append)] excluded_mints: Vec, @@ -66,6 +64,48 @@ pub async fn send( multi_mint_wallet: &MultiMintWallet, sub_command_args: &SendSubCommand, ) -> Result<()> { + // Determine which mint to use for sending BEFORE asking for amount + let selected_mint = if let Some(mint_url) = &sub_command_args.mint_url { + Some(MintUrl::from_str(mint_url)?) + } else { + // Display all mints with their balances and let user select + let balances_map = multi_mint_wallet.get_balances().await?; + if balances_map.is_empty() { + return Err(anyhow!("No mints available in the wallet")); + } + + let balances_vec: Vec<(MintUrl, Amount)> = balances_map.into_iter().collect(); + + println!("\nAvailable mints and balances:"); + for (index, (mint_url, balance)) in balances_vec.iter().enumerate() { + println!( + " {}: {} - {} {}", + index, + mint_url, + balance, + multi_mint_wallet.unit() + ); + } + println!(" {}: Any mint (auto-select best)", balances_vec.len()); + + let selection = loop { + let selection: usize = + get_number_input("Enter mint number to send from (or select Any)")?; + + if selection == balances_vec.len() { + break None; // "Any" option selected + } + + if let Some((mint_url, _)) = balances_vec.get(selection) { + break Some(mint_url.clone()); + } + + println!("Invalid selection, please try again."); + }; + + selection + }; + let token_amount = Amount::from(get_number_input::(&format!( "Enter value of token in {}", multi_mint_wallet.unit() @@ -214,14 +254,7 @@ pub async fn send( ..Default::default() }; - // Parse allowed and excluded mints from CLI arguments - let allowed_mints: Result, _> = sub_command_args - .allowed_mints - .iter() - .map(|url| MintUrl::from_str(url)) - .collect(); - let allowed_mints = allowed_mints?; - + // Parse excluded mints from CLI arguments let excluded_mints: Result, _> = sub_command_args .excluded_mints .iter() @@ -229,45 +262,44 @@ pub async fn send( .collect(); let excluded_mints = excluded_mints?; - // Create MultiMintSendOptions from CLI arguments - let multi_mint_options = cdk::wallet::multi_mint_wallet::MultiMintSendOptions { - allow_transfer: sub_command_args.allow_transfer, - max_transfer_amount: sub_command_args.max_transfer_amount.map(Amount::from), - allowed_mints, - excluded_mints, - send_options: send_options.clone(), - }; + // Prepare and confirm the send based on mint selection + let token = if let Some(specific_mint) = selected_mint { + // User selected a specific mint + let multi_mint_options = cdk::wallet::multi_mint_wallet::MultiMintSendOptions { + allow_transfer: sub_command_args.allow_transfer, + max_transfer_amount: sub_command_args.max_transfer_amount.map(Amount::from), + allowed_mints: vec![specific_mint.clone()], // Use selected mint as the only allowed mint + excluded_mints, + send_options: send_options.clone(), + }; - // Use the new unified interface - let token = if let Some(mint_url) = &sub_command_args.mint_url { - // User specified a mint, use that specific wallet - let mint_url = cdk::mint_url::MintUrl::from_str(mint_url)?; let prepared = multi_mint_wallet - .prepare_send(mint_url, token_amount, multi_mint_options) + .prepare_send(specific_mint, token_amount, multi_mint_options) .await?; - // Confirm the prepared send (single mint) let memo = send_options.memo.clone(); prepared.confirm(memo).await? } else { - // Let the wallet automatically select the best mint - // First, get balances to find a mint with sufficient funds + // User selected "Any" - find the first mint with sufficient balance let balances = multi_mint_wallet.get_balances().await?; - - // Find a mint with sufficient balance - let mint_url = balances + let best_mint = balances .into_iter() .find(|(_, balance)| *balance >= token_amount) .map(|(mint_url, _)| mint_url) - .ok_or_else(|| { - anyhow::anyhow!("No mint has sufficient balance for the requested amount") - })?; + .ok_or_else(|| anyhow!("No mint has sufficient balance for the requested amount"))?; + + let multi_mint_options = cdk::wallet::multi_mint_wallet::MultiMintSendOptions { + allow_transfer: sub_command_args.allow_transfer, + max_transfer_amount: sub_command_args.max_transfer_amount.map(Amount::from), + allowed_mints: vec![best_mint.clone()], // Use the best mint as the only allowed mint + excluded_mints, + send_options: send_options.clone(), + }; let prepared = multi_mint_wallet - .prepare_send(mint_url, token_amount, multi_mint_options) + .prepare_send(best_mint, token_amount, multi_mint_options) .await?; - // Confirm the prepared send (multi mint) let memo = send_options.memo.clone(); prepared.confirm(memo).await? };