send.rs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. use std::str::FromStr;
  2. use anyhow::{anyhow, Result};
  3. use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
  4. use cdk::wallet::types::SendKind;
  5. use cdk::wallet::{MultiMintWallet, SendMemo, SendOptions};
  6. use cdk::Amount;
  7. use clap::Args;
  8. use crate::sub_commands::balance::mint_balances;
  9. use crate::utils::{
  10. check_sufficient_funds, get_number_input, get_wallet_by_index, get_wallet_by_mint_url,
  11. };
  12. #[derive(Args)]
  13. pub struct SendSubCommand {
  14. /// Token Memo
  15. #[arg(short, long)]
  16. memo: Option<String>,
  17. /// Preimage
  18. #[arg(long, conflicts_with = "hash")]
  19. preimage: Option<String>,
  20. /// Hash for HTLC (alternative to preimage)
  21. #[arg(long, conflicts_with = "preimage")]
  22. hash: Option<String>,
  23. /// Required number of signatures
  24. #[arg(long)]
  25. required_sigs: Option<u64>,
  26. /// Locktime before refund keys can be used
  27. #[arg(short, long)]
  28. locktime: Option<u64>,
  29. /// Pubkey to lock proofs to
  30. #[arg(short, long, action = clap::ArgAction::Append)]
  31. pubkey: Vec<String>,
  32. /// Refund keys that can be used after locktime
  33. #[arg(long, action = clap::ArgAction::Append)]
  34. refund_keys: Vec<String>,
  35. /// Token as V3 token
  36. #[arg(short, long)]
  37. v3: bool,
  38. /// Should the send be offline only
  39. #[arg(short, long)]
  40. offline: bool,
  41. /// Include fee to redeem in token
  42. #[arg(short, long)]
  43. include_fee: bool,
  44. /// Amount willing to overpay to avoid a swap
  45. #[arg(short, long)]
  46. tolerance: Option<u64>,
  47. /// Mint URL to use for sending
  48. #[arg(long)]
  49. mint_url: Option<String>,
  50. /// Currency unit e.g. sat
  51. #[arg(default_value = "sat")]
  52. unit: String,
  53. }
  54. pub async fn send(
  55. multi_mint_wallet: &MultiMintWallet,
  56. sub_command_args: &SendSubCommand,
  57. ) -> Result<()> {
  58. let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
  59. let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
  60. // Get wallet either by mint URL or by index
  61. let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
  62. // Use the provided mint URL
  63. get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit).await?
  64. } else {
  65. // Fallback to the index-based selection
  66. let mint_number: usize = get_number_input("Enter mint number to create token")?;
  67. get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit).await?
  68. };
  69. let token_amount = Amount::from(get_number_input::<u64>("Enter value of token in sats")?);
  70. // Find the mint amount for the selected wallet to check if we have sufficient funds
  71. let mint_url = &wallet.mint_url;
  72. let mint_amount = mints_amounts
  73. .iter()
  74. .find(|(url, _)| url == mint_url)
  75. .map(|(_, amount)| *amount)
  76. .ok_or_else(|| anyhow!("Could not find balance for mint: {}", mint_url))?;
  77. check_sufficient_funds(mint_amount, token_amount)?;
  78. let conditions = match (&sub_command_args.preimage, &sub_command_args.hash) {
  79. (Some(_), Some(_)) => {
  80. // This case shouldn't be reached due to Clap's conflicts_with attribute
  81. unreachable!("Both preimage and hash were provided despite conflicts_with attribute")
  82. }
  83. (Some(preimage), None) => {
  84. let pubkeys = match sub_command_args.pubkey.is_empty() {
  85. true => None,
  86. false => Some(
  87. sub_command_args
  88. .pubkey
  89. .iter()
  90. .map(|p| PublicKey::from_str(p).unwrap())
  91. .collect(),
  92. ),
  93. };
  94. let refund_keys = match sub_command_args.refund_keys.is_empty() {
  95. true => None,
  96. false => Some(
  97. sub_command_args
  98. .refund_keys
  99. .iter()
  100. .map(|p| PublicKey::from_str(p).unwrap())
  101. .collect(),
  102. ),
  103. };
  104. let conditions = Conditions::new(
  105. sub_command_args.locktime,
  106. pubkeys,
  107. refund_keys,
  108. sub_command_args.required_sigs,
  109. None,
  110. None,
  111. )
  112. .unwrap();
  113. Some(SpendingConditions::new_htlc(
  114. preimage.clone(),
  115. Some(conditions),
  116. )?)
  117. }
  118. (None, Some(hash)) => {
  119. let pubkeys = match sub_command_args.pubkey.is_empty() {
  120. true => None,
  121. false => Some(
  122. sub_command_args
  123. .pubkey
  124. .iter()
  125. .map(|p| PublicKey::from_str(p).unwrap())
  126. .collect(),
  127. ),
  128. };
  129. let refund_keys = match sub_command_args.refund_keys.is_empty() {
  130. true => None,
  131. false => Some(
  132. sub_command_args
  133. .refund_keys
  134. .iter()
  135. .map(|p| PublicKey::from_str(p).unwrap())
  136. .collect(),
  137. ),
  138. };
  139. let conditions = Conditions::new(
  140. sub_command_args.locktime,
  141. pubkeys,
  142. refund_keys,
  143. sub_command_args.required_sigs,
  144. None,
  145. None,
  146. )?;
  147. Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?)
  148. }
  149. (None, None) => match sub_command_args.pubkey.is_empty() {
  150. true => None,
  151. false => {
  152. let pubkeys: Vec<PublicKey> = sub_command_args
  153. .pubkey
  154. .iter()
  155. .map(|p| PublicKey::from_str(p).unwrap())
  156. .collect();
  157. let refund_keys: Vec<PublicKey> = sub_command_args
  158. .refund_keys
  159. .iter()
  160. .map(|p| PublicKey::from_str(p).unwrap())
  161. .collect();
  162. let refund_keys = (!refund_keys.is_empty()).then_some(refund_keys);
  163. let data_pubkey = pubkeys[0];
  164. let pubkeys = pubkeys[1..].to_vec();
  165. let pubkeys = (!pubkeys.is_empty()).then_some(pubkeys);
  166. let conditions = Conditions::new(
  167. sub_command_args.locktime,
  168. pubkeys,
  169. refund_keys,
  170. sub_command_args.required_sigs,
  171. None,
  172. None,
  173. )?;
  174. Some(SpendingConditions::P2PKConditions {
  175. data: data_pubkey,
  176. conditions: Some(conditions),
  177. })
  178. }
  179. },
  180. };
  181. let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) {
  182. (true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)),
  183. (true, None) => SendKind::OfflineExact,
  184. (false, Some(amount)) => SendKind::OnlineTolerance(Amount::from(amount)),
  185. (false, None) => SendKind::OnlineExact,
  186. };
  187. let prepared_send = wallet
  188. .prepare_send(
  189. token_amount,
  190. SendOptions {
  191. memo: sub_command_args.memo.clone().map(|memo| SendMemo {
  192. memo,
  193. include_memo: true,
  194. }),
  195. send_kind,
  196. include_fee: sub_command_args.include_fee,
  197. conditions,
  198. ..Default::default()
  199. },
  200. )
  201. .await?;
  202. let token = wallet.send(prepared_send, None).await?;
  203. match sub_command_args.v3 {
  204. true => {
  205. let token = token;
  206. println!("{}", token.to_v3_string());
  207. }
  208. false => {
  209. println!("{token}");
  210. }
  211. }
  212. Ok(())
  213. }