send.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. use std::str::FromStr;
  2. use anyhow::{anyhow, Result};
  3. use cdk::mint_url::MintUrl;
  4. use cdk::nuts::{Conditions, PublicKey, SpendingConditions};
  5. use cdk::wallet::types::SendKind;
  6. use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
  7. use cdk::Amount;
  8. use clap::Args;
  9. use crate::utils::get_number_input;
  10. #[derive(Args)]
  11. pub struct SendSubCommand {
  12. /// Token Memo
  13. #[arg(short, long)]
  14. memo: Option<String>,
  15. /// Preimage
  16. #[arg(long, conflicts_with = "hash")]
  17. preimage: Option<String>,
  18. /// Hash for HTLC (alternative to preimage)
  19. #[arg(long, conflicts_with = "preimage")]
  20. hash: Option<String>,
  21. /// Required number of signatures
  22. #[arg(long)]
  23. required_sigs: Option<u64>,
  24. /// Locktime before refund keys can be used
  25. #[arg(short, long)]
  26. locktime: Option<u64>,
  27. /// Pubkey to lock proofs to
  28. #[arg(short, long, action = clap::ArgAction::Append)]
  29. pubkey: Vec<String>,
  30. /// Refund keys that can be used after locktime
  31. #[arg(long, action = clap::ArgAction::Append)]
  32. refund_keys: Vec<String>,
  33. /// Token as V3 token
  34. #[arg(short, long)]
  35. v3: bool,
  36. /// Should the send be offline only
  37. #[arg(short, long)]
  38. offline: bool,
  39. /// Include fee to redeem in token
  40. #[arg(short, long)]
  41. include_fee: bool,
  42. /// Amount willing to overpay to avoid a swap
  43. #[arg(short, long)]
  44. tolerance: Option<u64>,
  45. /// Mint URL to use for sending
  46. #[arg(long)]
  47. mint_url: Option<String>,
  48. /// Allow transferring funds from other mints if the target mint has insufficient balance
  49. #[arg(long)]
  50. allow_transfer: bool,
  51. /// Maximum amount to transfer from other mints
  52. #[arg(long)]
  53. max_transfer_amount: Option<u64>,
  54. /// Specific mints to exclude from transfers (can be specified multiple times)
  55. #[arg(long, action = clap::ArgAction::Append)]
  56. excluded_mints: Vec<String>,
  57. /// Amount to send
  58. #[arg(short, long)]
  59. amount: Option<u64>,
  60. }
  61. pub async fn send(
  62. multi_mint_wallet: &MultiMintWallet,
  63. sub_command_args: &SendSubCommand,
  64. ) -> Result<()> {
  65. // Determine which mint to use for sending BEFORE asking for amount
  66. let selected_mint = if let Some(mint_url) = &sub_command_args.mint_url {
  67. Some(MintUrl::from_str(mint_url)?)
  68. } else {
  69. // Get all mints with their balances
  70. let balances_map = multi_mint_wallet.get_balances().await?;
  71. if balances_map.is_empty() {
  72. return Err(anyhow!("No mints available in the wallet"));
  73. }
  74. let balances_vec: Vec<(MintUrl, Amount)> = balances_map.into_iter().collect();
  75. // If only one mint exists, automatically select it
  76. if balances_vec.len() == 1 {
  77. Some(balances_vec[0].0.clone())
  78. } else {
  79. // Display all mints with their balances and let user select
  80. println!("\nAvailable mints and balances:");
  81. for (index, (mint_url, balance)) in balances_vec.iter().enumerate() {
  82. println!(
  83. " {}: {} - {} {}",
  84. index,
  85. mint_url,
  86. balance,
  87. multi_mint_wallet.unit()
  88. );
  89. }
  90. println!(" {}: Any mint (auto-select best)", balances_vec.len());
  91. let selection = loop {
  92. let selection: usize =
  93. get_number_input("Enter mint number to send from (or select Any)")?;
  94. if selection == balances_vec.len() {
  95. break None; // "Any" option selected
  96. }
  97. if let Some((mint_url, _)) = balances_vec.get(selection) {
  98. break Some(mint_url.clone());
  99. }
  100. println!("Invalid selection, please try again.");
  101. };
  102. selection
  103. }
  104. };
  105. let token_amount = match sub_command_args.amount {
  106. Some(amount) => Amount::from(amount),
  107. None => Amount::from(get_number_input::<u64>(&format!(
  108. "Enter value of token in {}",
  109. multi_mint_wallet.unit()
  110. ))?),
  111. };
  112. // Check total balance across all wallets
  113. let total_balance = multi_mint_wallet.total_balance().await?;
  114. if total_balance < token_amount {
  115. return Err(anyhow!(
  116. "Insufficient funds. Total balance: {}, Required: {}",
  117. total_balance,
  118. token_amount
  119. ));
  120. }
  121. let conditions = match (&sub_command_args.preimage, &sub_command_args.hash) {
  122. (Some(_), Some(_)) => {
  123. // This case shouldn't be reached due to Clap's conflicts_with attribute
  124. unreachable!("Both preimage and hash were provided despite conflicts_with attribute")
  125. }
  126. (Some(preimage), None) => {
  127. let pubkeys = match sub_command_args.pubkey.is_empty() {
  128. true => None,
  129. false => Some(
  130. sub_command_args
  131. .pubkey
  132. .iter()
  133. .map(|p| PublicKey::from_str(p))
  134. .collect::<Result<Vec<_>, _>>()?,
  135. ),
  136. };
  137. let refund_keys = match sub_command_args.refund_keys.is_empty() {
  138. true => None,
  139. false => Some(
  140. sub_command_args
  141. .refund_keys
  142. .iter()
  143. .map(|p| PublicKey::from_str(p))
  144. .collect::<Result<Vec<_>, _>>()?,
  145. ),
  146. };
  147. let conditions = Conditions::new(
  148. sub_command_args.locktime,
  149. pubkeys,
  150. refund_keys,
  151. sub_command_args.required_sigs,
  152. None,
  153. None,
  154. )?;
  155. Some(SpendingConditions::new_htlc(
  156. preimage.clone(),
  157. Some(conditions),
  158. )?)
  159. }
  160. (None, Some(hash)) => {
  161. let pubkeys = match sub_command_args.pubkey.is_empty() {
  162. true => None,
  163. false => Some(
  164. sub_command_args
  165. .pubkey
  166. .iter()
  167. .map(|p| PublicKey::from_str(p))
  168. .collect::<Result<Vec<_>, _>>()?,
  169. ),
  170. };
  171. let refund_keys = match sub_command_args.refund_keys.is_empty() {
  172. true => None,
  173. false => Some(
  174. sub_command_args
  175. .refund_keys
  176. .iter()
  177. .map(|p| PublicKey::from_str(p))
  178. .collect::<Result<Vec<_>, _>>()?,
  179. ),
  180. };
  181. let conditions = Conditions::new(
  182. sub_command_args.locktime,
  183. pubkeys,
  184. refund_keys,
  185. sub_command_args.required_sigs,
  186. None,
  187. None,
  188. )?;
  189. Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?)
  190. }
  191. (None, None) => match sub_command_args.pubkey.is_empty() {
  192. true => None,
  193. false => {
  194. let pubkeys: Vec<PublicKey> = sub_command_args
  195. .pubkey
  196. .iter()
  197. .map(|p| PublicKey::from_str(p))
  198. .collect::<Result<Vec<_>, _>>()?;
  199. let refund_keys: Vec<PublicKey> = sub_command_args
  200. .refund_keys
  201. .iter()
  202. .map(|p| PublicKey::from_str(p))
  203. .collect::<Result<Vec<_>, _>>()?;
  204. let refund_keys = (!refund_keys.is_empty()).then_some(refund_keys);
  205. let data_pubkey = pubkeys[0];
  206. let pubkeys = pubkeys[1..].to_vec();
  207. let pubkeys = (!pubkeys.is_empty()).then_some(pubkeys);
  208. let conditions = Conditions::new(
  209. sub_command_args.locktime,
  210. pubkeys,
  211. refund_keys,
  212. sub_command_args.required_sigs,
  213. None,
  214. None,
  215. )?;
  216. Some(SpendingConditions::P2PKConditions {
  217. data: data_pubkey,
  218. conditions: Some(conditions),
  219. })
  220. }
  221. },
  222. };
  223. let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) {
  224. (true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)),
  225. (true, None) => SendKind::OfflineExact,
  226. (false, Some(amount)) => SendKind::OnlineTolerance(Amount::from(amount)),
  227. (false, None) => SendKind::OnlineExact,
  228. };
  229. let send_options = SendOptions {
  230. memo: sub_command_args.memo.clone().map(|memo| SendMemo {
  231. memo,
  232. include_memo: true,
  233. }),
  234. send_kind,
  235. include_fee: sub_command_args.include_fee,
  236. conditions,
  237. ..Default::default()
  238. };
  239. // Parse excluded mints from CLI arguments
  240. let excluded_mints: Result<Vec<MintUrl>, _> = sub_command_args
  241. .excluded_mints
  242. .iter()
  243. .map(|url| MintUrl::from_str(url))
  244. .collect();
  245. let excluded_mints = excluded_mints?;
  246. // Prepare and confirm the send based on mint selection
  247. let token = if let Some(specific_mint) = selected_mint {
  248. // User selected a specific mint
  249. let multi_mint_options = cdk::wallet::multi_mint_wallet::MultiMintSendOptions {
  250. allow_transfer: sub_command_args.allow_transfer,
  251. max_transfer_amount: sub_command_args.max_transfer_amount.map(Amount::from),
  252. allowed_mints: vec![specific_mint.clone()], // Use selected mint as the only allowed mint
  253. excluded_mints,
  254. send_options: send_options.clone(),
  255. };
  256. let prepared = multi_mint_wallet
  257. .prepare_send(specific_mint, token_amount, multi_mint_options)
  258. .await?;
  259. let memo = send_options.memo.clone();
  260. prepared.confirm(memo).await?
  261. } else {
  262. // User selected "Any" - find the first mint with sufficient balance
  263. let balances = multi_mint_wallet.get_balances().await?;
  264. let best_mint = balances
  265. .into_iter()
  266. .find(|(_, balance)| *balance >= token_amount)
  267. .map(|(mint_url, _)| mint_url)
  268. .ok_or_else(|| anyhow!("No mint has sufficient balance for the requested amount"))?;
  269. let multi_mint_options = cdk::wallet::multi_mint_wallet::MultiMintSendOptions {
  270. allow_transfer: sub_command_args.allow_transfer,
  271. max_transfer_amount: sub_command_args.max_transfer_amount.map(Amount::from),
  272. allowed_mints: vec![best_mint.clone()], // Use the best mint as the only allowed mint
  273. excluded_mints,
  274. send_options: send_options.clone(),
  275. };
  276. let prepared = multi_mint_wallet
  277. .prepare_send(best_mint, token_amount, multi_mint_options)
  278. .await?;
  279. let memo = send_options.memo.clone();
  280. prepared.confirm(memo).await?
  281. };
  282. match sub_command_args.v3 {
  283. true => {
  284. let token = token;
  285. println!("{}", token.to_v3_string());
  286. }
  287. false => {
  288. println!("{token}");
  289. }
  290. }
  291. Ok(())
  292. }