melt.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. use std::str::FromStr;
  2. use anyhow::{bail, Result};
  3. use cdk::amount::{amount_for_offer, Amount, MSAT_IN_SAT};
  4. use cdk::mint_url::MintUrl;
  5. use cdk::nuts::{CurrencyUnit, MeltOptions};
  6. use cdk::wallet::MultiMintWallet;
  7. use cdk::Bolt11Invoice;
  8. use clap::{Args, ValueEnum};
  9. use lightning::offers::offer::Offer;
  10. use crate::utils::{get_number_input, get_user_input};
  11. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
  12. pub enum PaymentType {
  13. /// BOLT11 invoice
  14. Bolt11,
  15. /// BOLT12 offer
  16. Bolt12,
  17. /// Bip353
  18. Bip353,
  19. }
  20. #[derive(Args)]
  21. pub struct MeltSubCommand {
  22. /// Mpp
  23. #[arg(short, long, conflicts_with = "mint_url")]
  24. mpp: bool,
  25. /// Mint URL to use for melting
  26. #[arg(long, conflicts_with = "mpp")]
  27. mint_url: Option<String>,
  28. /// Payment method (bolt11, bolt12, or bip353)
  29. #[arg(long, default_value = "bolt11")]
  30. method: PaymentType,
  31. }
  32. /// Helper function to check if there are enough funds and create appropriate MeltOptions
  33. fn create_melt_options(
  34. available_funds: u64,
  35. payment_amount: Option<u64>,
  36. prompt: &str,
  37. ) -> Result<Option<MeltOptions>> {
  38. match payment_amount {
  39. Some(amount) => {
  40. // Payment has a specified amount
  41. if amount > available_funds {
  42. bail!("Not enough funds; payment requires {} msats", amount);
  43. }
  44. Ok(None) // Use default options
  45. }
  46. None => {
  47. // Payment doesn't have an amount, ask user for it
  48. let user_amount = get_number_input::<u64>(prompt)? * MSAT_IN_SAT;
  49. if user_amount > available_funds {
  50. bail!("Not enough funds");
  51. }
  52. Ok(Some(MeltOptions::new_amountless(user_amount)))
  53. }
  54. }
  55. }
  56. pub async fn pay(
  57. multi_mint_wallet: &MultiMintWallet,
  58. sub_command_args: &MeltSubCommand,
  59. ) -> Result<()> {
  60. // Check total balance across all wallets
  61. let total_balance = multi_mint_wallet.total_balance().await?;
  62. if total_balance == Amount::ZERO {
  63. bail!("No funds available");
  64. }
  65. // Determine which mint to use for melting BEFORE processing payment (unless using MPP)
  66. let selected_mint = if sub_command_args.mpp {
  67. None // MPP mode handles mint selection differently
  68. } else if let Some(mint_url) = &sub_command_args.mint_url {
  69. Some(MintUrl::from_str(mint_url)?)
  70. } else {
  71. // Display all mints with their balances and let user select
  72. let balances_map = multi_mint_wallet.get_balances().await?;
  73. if balances_map.is_empty() {
  74. bail!("No mints available in the wallet");
  75. }
  76. let balances_vec: Vec<(MintUrl, Amount)> = balances_map.into_iter().collect();
  77. println!("\nAvailable mints and balances:");
  78. for (index, (mint_url, balance)) in balances_vec.iter().enumerate() {
  79. println!(
  80. " {}: {} - {} {}",
  81. index,
  82. mint_url,
  83. balance,
  84. multi_mint_wallet.unit()
  85. );
  86. }
  87. println!(" {}: Any mint (auto-select best)", balances_vec.len());
  88. let selection = loop {
  89. let selection: usize =
  90. get_number_input("Enter mint number to melt from (or select Any)")?;
  91. if selection == balances_vec.len() {
  92. break None; // "Any" option selected
  93. }
  94. if let Some((mint_url, _)) = balances_vec.get(selection) {
  95. break Some(mint_url.clone());
  96. }
  97. println!("Invalid selection, please try again.");
  98. };
  99. selection
  100. };
  101. if sub_command_args.mpp {
  102. // Manual MPP - user specifies which mints and amounts to use
  103. if !matches!(sub_command_args.method, PaymentType::Bolt11) {
  104. bail!("MPP is only supported for BOLT11 invoices");
  105. }
  106. let bolt11_str = get_user_input("Enter bolt11 invoice")?;
  107. let _bolt11 = Bolt11Invoice::from_str(&bolt11_str)?; // Validate invoice format
  108. // Show available mints and balances
  109. let balances = multi_mint_wallet.get_balances().await?;
  110. println!("\nAvailable mints and balances:");
  111. for (i, (mint_url, balance)) in balances.iter().enumerate() {
  112. println!(
  113. " {}: {} - {} {}",
  114. i,
  115. mint_url,
  116. balance,
  117. multi_mint_wallet.unit()
  118. );
  119. }
  120. // Collect mint selections and amounts
  121. let mut mint_amounts = Vec::new();
  122. loop {
  123. let mint_input = get_user_input("Enter mint number to use (or 'done' to finish)")?;
  124. if mint_input.to_lowercase() == "done" || mint_input.is_empty() {
  125. break;
  126. }
  127. let mint_index: usize = mint_input.parse()?;
  128. let mint_url = balances
  129. .iter()
  130. .nth(mint_index)
  131. .map(|(url, _)| url.clone())
  132. .ok_or_else(|| anyhow::anyhow!("Invalid mint index"))?;
  133. let amount: u64 = get_number_input(&format!(
  134. "Enter amount to use from this mint ({})",
  135. multi_mint_wallet.unit()
  136. ))?;
  137. mint_amounts.push((mint_url, Amount::from(amount)));
  138. }
  139. if mint_amounts.is_empty() {
  140. bail!("No mints selected for MPP payment");
  141. }
  142. // Get quotes for each mint
  143. println!("\nGetting melt quotes...");
  144. let quotes = multi_mint_wallet
  145. .mpp_melt_quote(bolt11_str, mint_amounts)
  146. .await?;
  147. // Display quotes
  148. println!("\nMelt quotes obtained:");
  149. for (mint_url, quote) in &quotes {
  150. println!(" {} - Quote ID: {}", mint_url, quote.id);
  151. println!(" Amount: {}, Fee: {}", quote.amount, quote.fee_reserve);
  152. }
  153. // Execute the melts
  154. let quotes_to_execute: Vec<(MintUrl, String)> = quotes
  155. .iter()
  156. .map(|(url, quote)| (url.clone(), quote.id.clone()))
  157. .collect();
  158. println!("\nExecuting MPP payment...");
  159. let results = multi_mint_wallet.mpp_melt(quotes_to_execute).await?;
  160. // Display results
  161. println!("\nPayment results:");
  162. let mut total_paid = Amount::ZERO;
  163. let mut total_fees = Amount::ZERO;
  164. for (mint_url, melted) in results {
  165. println!(
  166. " {} - Paid: {}, Fee: {}",
  167. mint_url, melted.amount, melted.fee_paid
  168. );
  169. total_paid += melted.amount;
  170. total_fees += melted.fee_paid;
  171. if let Some(preimage) = melted.preimage {
  172. println!(" Preimage: {}", preimage);
  173. }
  174. }
  175. println!("\nTotal paid: {} {}", total_paid, multi_mint_wallet.unit());
  176. println!("Total fees: {} {}", total_fees, multi_mint_wallet.unit());
  177. } else {
  178. let available_funds = <cdk::Amount as Into<u64>>::into(total_balance) * MSAT_IN_SAT;
  179. // Process payment based on payment method using new unified interface
  180. match sub_command_args.method {
  181. PaymentType::Bolt11 => {
  182. // Process BOLT11 payment
  183. let bolt11_str = get_user_input("Enter bolt11 invoice")?;
  184. let bolt11 = Bolt11Invoice::from_str(&bolt11_str)?;
  185. // Determine payment amount and options
  186. let prompt = format!(
  187. "Enter the amount you would like to pay in {} for this amountless invoice.",
  188. multi_mint_wallet.unit()
  189. );
  190. let options =
  191. create_melt_options(available_funds, bolt11.amount_milli_satoshis(), &prompt)?;
  192. // Use selected mint or auto-select
  193. let melted = if let Some(mint_url) = selected_mint {
  194. // User selected a specific mint - use the new mint-specific functions
  195. let quote = multi_mint_wallet
  196. .melt_quote(&mint_url, bolt11_str.clone(), options)
  197. .await?;
  198. println!("Melt quote created:");
  199. println!(" Quote ID: {}", quote.id);
  200. println!(" Amount: {}", quote.amount);
  201. println!(" Fee Reserve: {}", quote.fee_reserve);
  202. // Execute the melt
  203. multi_mint_wallet
  204. .melt_with_mint(&mint_url, &quote.id)
  205. .await?
  206. } else {
  207. // User selected "Any" - let the wallet auto-select the best mint
  208. multi_mint_wallet.melt(&bolt11_str, options, None).await?
  209. };
  210. println!("Payment successful: {:?}", melted);
  211. if let Some(preimage) = melted.preimage {
  212. println!("Payment preimage: {}", preimage);
  213. }
  214. }
  215. PaymentType::Bolt12 => {
  216. // Process BOLT12 payment (offer)
  217. let offer_str = get_user_input("Enter BOLT12 offer")?;
  218. let offer = Offer::from_str(&offer_str)
  219. .map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
  220. // Determine if offer has an amount
  221. let prompt = format!(
  222. "Enter the amount you would like to pay in {} for this amountless offer:",
  223. multi_mint_wallet.unit()
  224. );
  225. let amount_msat = match amount_for_offer(&offer, &CurrencyUnit::Msat) {
  226. Ok(amount) => Some(u64::from(amount)),
  227. Err(_) => None,
  228. };
  229. let options = create_melt_options(available_funds, amount_msat, &prompt)?;
  230. // Get wallet for BOLT12 using the selected mint
  231. let mint_url = if let Some(specific_mint) = selected_mint {
  232. specific_mint
  233. } else {
  234. // User selected "Any" - just pick the first mint with any balance
  235. let balances = multi_mint_wallet.get_balances().await?;
  236. balances
  237. .into_iter()
  238. .find(|(_, balance)| *balance > Amount::ZERO)
  239. .map(|(mint_url, _)| mint_url)
  240. .ok_or_else(|| anyhow::anyhow!("No mint available for BOLT12 payment"))?
  241. };
  242. let wallet = multi_mint_wallet
  243. .get_wallet(&mint_url)
  244. .await
  245. .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?;
  246. // Get melt quote for BOLT12
  247. let quote = wallet.melt_bolt12_quote(offer_str, options).await?;
  248. // Display quote info
  249. println!("Melt quote created:");
  250. println!(" Quote ID: {}", quote.id);
  251. println!(" Amount: {}", quote.amount);
  252. println!(" Fee Reserve: {}", quote.fee_reserve);
  253. println!(" State: {}", quote.state);
  254. println!(" Expiry: {}", quote.expiry);
  255. // Execute the melt
  256. let melted = wallet.melt(&quote.id).await?;
  257. println!(
  258. "Payment successful: Paid {} with fee {}",
  259. melted.amount, melted.fee_paid
  260. );
  261. if let Some(preimage) = melted.preimage {
  262. println!("Payment preimage: {}", preimage);
  263. }
  264. }
  265. PaymentType::Bip353 => {
  266. let bip353_addr = get_user_input("Enter Bip353 address")?;
  267. let prompt = format!(
  268. "Enter the amount you would like to pay in {} for this amountless offer:",
  269. multi_mint_wallet.unit()
  270. );
  271. // BIP353 payments are always amountless for now
  272. let options = create_melt_options(available_funds, None, &prompt)?;
  273. // Get wallet for BIP353 using the selected mint
  274. let mint_url = if let Some(specific_mint) = selected_mint {
  275. specific_mint
  276. } else {
  277. // User selected "Any" - just pick the first mint with any balance
  278. let balances = multi_mint_wallet.get_balances().await?;
  279. balances
  280. .into_iter()
  281. .find(|(_, balance)| *balance > Amount::ZERO)
  282. .map(|(mint_url, _)| mint_url)
  283. .ok_or_else(|| anyhow::anyhow!("No mint available for BIP353 payment"))?
  284. };
  285. let wallet = multi_mint_wallet
  286. .get_wallet(&mint_url)
  287. .await
  288. .ok_or_else(|| anyhow::anyhow!("Mint {} not found", mint_url))?;
  289. // Get melt quote for BIP353 address (internally resolves and gets BOLT12 quote)
  290. let quote = wallet
  291. .melt_bip353_quote(
  292. &bip353_addr,
  293. options.expect("Amount is required").amount_msat(),
  294. )
  295. .await?;
  296. // Display quote info
  297. println!("Melt quote created:");
  298. println!(" Quote ID: {}", quote.id);
  299. println!(" Amount: {}", quote.amount);
  300. println!(" Fee Reserve: {}", quote.fee_reserve);
  301. println!(" State: {}", quote.state);
  302. println!(" Expiry: {}", quote.expiry);
  303. // Execute the melt
  304. let melted = wallet.melt(&quote.id).await?;
  305. println!(
  306. "Payment successful: Paid {} with fee {}",
  307. melted.amount, melted.fee_paid
  308. );
  309. if let Some(preimage) = melted.preimage {
  310. println!("Payment preimage: {}", preimage);
  311. }
  312. }
  313. }
  314. }
  315. Ok(())
  316. }