use std::str::FromStr; use anyhow::{anyhow, Result}; use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions}; 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, get_wallet_by_mint_url, }; #[derive(Args)] pub struct SendSubCommand { /// Token Memo #[arg(short, long)] memo: Option, /// Preimage #[arg(long, conflicts_with = "hash")] preimage: Option, /// Hash for HTLC (alternative to preimage) #[arg(long, conflicts_with = "preimage")] hash: Option, /// Required number of signatures #[arg(long)] required_sigs: Option, /// Locktime before refund keys can be used #[arg(short, long)] locktime: Option, /// Pubkey to lock proofs to #[arg(short, long, action = clap::ArgAction::Append)] pubkey: Vec, /// Refund keys that can be used after locktime #[arg(long, action = clap::ArgAction::Append)] refund_keys: Vec, /// Token as V3 token #[arg(short, long)] v3: bool, /// Should the send be offline only #[arg(short, long)] offline: bool, /// Include fee to redeem in token #[arg(short, long)] include_fee: bool, /// Amount willing to overpay to avoid a swap #[arg(short, long)] tolerance: Option, /// Mint URL to use for sending #[arg(long)] mint_url: Option, /// Currency unit e.g. sat #[arg(default_value = "sat")] unit: String, } pub async fn send( multi_mint_wallet: &MultiMintWallet, sub_command_args: &SendSubCommand, ) -> Result<()> { let unit = CurrencyUnit::from_str(&sub_command_args.unit)?; let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?; // Get wallet either by mint URL or by index let wallet = if let Some(mint_url) = &sub_command_args.mint_url { // Use the provided mint URL get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit).await? } else { // Fallback to the index-based selection let mint_number: usize = get_number_input("Enter mint number to create token")?; get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit).await? }; let token_amount = Amount::from(get_number_input::("Enter value of token in sats")?); // Find the mint amount for the selected wallet to check if we have sufficient funds let mint_url = &wallet.mint_url; let mint_amount = mints_amounts .iter() .find(|(url, _)| url == mint_url) .map(|(_, amount)| *amount) .ok_or_else(|| anyhow!("Could not find balance for mint: {}", mint_url))?; check_sufficient_funds(mint_amount, token_amount)?; let conditions = match (&sub_command_args.preimage, &sub_command_args.hash) { (Some(_), Some(_)) => { // This case shouldn't be reached due to Clap's conflicts_with attribute unreachable!("Both preimage and hash were provided despite conflicts_with attribute") } (Some(preimage), None) => { let pubkeys = match sub_command_args.pubkey.is_empty() { true => None, false => Some( sub_command_args .pubkey .iter() .map(|p| PublicKey::from_str(p).unwrap()) .collect(), ), }; let refund_keys = match sub_command_args.refund_keys.is_empty() { true => None, false => Some( sub_command_args .refund_keys .iter() .map(|p| PublicKey::from_str(p).unwrap()) .collect(), ), }; let conditions = Conditions::new( sub_command_args.locktime, pubkeys, refund_keys, sub_command_args.required_sigs, None, ) .unwrap(); Some(SpendingConditions::new_htlc( preimage.clone(), Some(conditions), )?) } (None, Some(hash)) => { let pubkeys = match sub_command_args.pubkey.is_empty() { true => None, false => Some( sub_command_args .pubkey .iter() .map(|p| PublicKey::from_str(p).unwrap()) .collect(), ), }; let refund_keys = match sub_command_args.refund_keys.is_empty() { true => None, false => Some( sub_command_args .refund_keys .iter() .map(|p| PublicKey::from_str(p).unwrap()) .collect(), ), }; let conditions = Conditions::new( sub_command_args.locktime, pubkeys, refund_keys, sub_command_args.required_sigs, None, ) .unwrap(); Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?) } (None, None) => match sub_command_args.pubkey.is_empty() { true => None, false => { let pubkeys: Vec = sub_command_args .pubkey .iter() .map(|p| PublicKey::from_str(p).unwrap()) .collect(); let refund_keys: Vec = sub_command_args .refund_keys .iter() .map(|p| PublicKey::from_str(p).unwrap()) .collect(); let refund_keys = (!refund_keys.is_empty()).then_some(refund_keys); let data_pubkey = pubkeys[0]; let pubkeys = pubkeys[1..].to_vec(); let pubkeys = (!pubkeys.is_empty()).then_some(pubkeys); let conditions = Conditions::new( sub_command_args.locktime, pubkeys, refund_keys, sub_command_args.required_sigs, None, ) .unwrap(); Some(SpendingConditions::P2PKConditions { data: data_pubkey, conditions: Some(conditions), }) } }, }; let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) { (true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)), (true, None) => SendKind::OfflineExact, (false, Some(amount)) => SendKind::OnlineTolerance(Amount::from(amount)), (false, None) => SendKind::OnlineExact, }; let prepared_send = wallet .prepare_send( token_amount, SendOptions { memo: sub_command_args.memo.clone().map(|memo| SendMemo { memo, include_memo: true, }), send_kind, include_fee: sub_command_args.include_fee, conditions, ..Default::default() }, ) .await?; let token = wallet.send(prepared_send, None).await?; match sub_command_args.v3 { true => { let token = token; println!("{}", token.to_v3_string()); } false => { println!("{token}"); } } Ok(()) }