send.rs 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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. )
  111. .unwrap();
  112. Some(SpendingConditions::new_htlc(
  113. preimage.clone(),
  114. Some(conditions),
  115. )?)
  116. }
  117. (None, Some(hash)) => {
  118. let pubkeys = match sub_command_args.pubkey.is_empty() {
  119. true => None,
  120. false => Some(
  121. sub_command_args
  122. .pubkey
  123. .iter()
  124. .map(|p| PublicKey::from_str(p).unwrap())
  125. .collect(),
  126. ),
  127. };
  128. let refund_keys = match sub_command_args.refund_keys.is_empty() {
  129. true => None,
  130. false => Some(
  131. sub_command_args
  132. .refund_keys
  133. .iter()
  134. .map(|p| PublicKey::from_str(p).unwrap())
  135. .collect(),
  136. ),
  137. };
  138. let conditions = Conditions::new(
  139. sub_command_args.locktime,
  140. pubkeys,
  141. refund_keys,
  142. sub_command_args.required_sigs,
  143. None,
  144. )
  145. .unwrap();
  146. Some(SpendingConditions::new_htlc_hash(hash, Some(conditions))?)
  147. }
  148. (None, None) => match sub_command_args.pubkey.is_empty() {
  149. true => None,
  150. false => {
  151. let pubkeys: Vec<PublicKey> = sub_command_args
  152. .pubkey
  153. .iter()
  154. .map(|p| PublicKey::from_str(p).unwrap())
  155. .collect();
  156. let refund_keys: Vec<PublicKey> = sub_command_args
  157. .refund_keys
  158. .iter()
  159. .map(|p| PublicKey::from_str(p).unwrap())
  160. .collect();
  161. let refund_keys = (!refund_keys.is_empty()).then_some(refund_keys);
  162. let data_pubkey = pubkeys[0];
  163. let pubkeys = pubkeys[1..].to_vec();
  164. let pubkeys = (!pubkeys.is_empty()).then_some(pubkeys);
  165. let conditions = Conditions::new(
  166. sub_command_args.locktime,
  167. pubkeys,
  168. refund_keys,
  169. sub_command_args.required_sigs,
  170. None,
  171. )
  172. .unwrap();
  173. Some(SpendingConditions::P2PKConditions {
  174. data: data_pubkey,
  175. conditions: Some(conditions),
  176. })
  177. }
  178. },
  179. };
  180. let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) {
  181. (true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)),
  182. (true, None) => SendKind::OfflineExact,
  183. (false, Some(amount)) => SendKind::OnlineTolerance(Amount::from(amount)),
  184. (false, None) => SendKind::OnlineExact,
  185. };
  186. let prepared_send = wallet
  187. .prepare_send(
  188. token_amount,
  189. SendOptions {
  190. memo: sub_command_args.memo.clone().map(|memo| SendMemo {
  191. memo,
  192. include_memo: true,
  193. }),
  194. send_kind,
  195. include_fee: sub_command_args.include_fee,
  196. conditions,
  197. ..Default::default()
  198. },
  199. )
  200. .await?;
  201. let token = wallet.send(prepared_send, None).await?;
  202. match sub_command_args.v3 {
  203. true => {
  204. let token = token;
  205. println!("{}", token.to_v3_string());
  206. }
  207. false => {
  208. println!("{token}");
  209. }
  210. }
  211. Ok(())
  212. }