melt.rs 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. use std::str::FromStr;
  2. use anyhow::{bail, Result};
  3. use cdk::amount::MSAT_IN_SAT;
  4. use cdk::nuts::{CurrencyUnit, MeltOptions};
  5. use cdk::wallet::multi_mint_wallet::MultiMintWallet;
  6. use cdk::wallet::types::WalletKey;
  7. use cdk::Bolt11Invoice;
  8. use clap::Args;
  9. use tokio::task::JoinSet;
  10. use crate::sub_commands::balance::mint_balances;
  11. use crate::utils::{
  12. get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
  13. validate_mint_number,
  14. };
  15. #[derive(Args)]
  16. pub struct MeltSubCommand {
  17. /// Currency unit e.g. sat
  18. #[arg(default_value = "sat")]
  19. unit: String,
  20. /// Mpp
  21. #[arg(short, long, conflicts_with = "mint_url")]
  22. mpp: bool,
  23. /// Mint URL to use for melting
  24. #[arg(long, conflicts_with = "mpp")]
  25. mint_url: Option<String>,
  26. }
  27. pub async fn pay(
  28. multi_mint_wallet: &MultiMintWallet,
  29. sub_command_args: &MeltSubCommand,
  30. ) -> Result<()> {
  31. let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
  32. let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
  33. let mut mints = vec![];
  34. let mut mint_amounts = vec![];
  35. if sub_command_args.mpp {
  36. // MPP functionality expects multiple mints, so mint_url flag doesn't fully apply here,
  37. // but we can offer to use the specified mint as the first one if provided
  38. if let Some(mint_url) = &sub_command_args.mint_url {
  39. println!("Using mint URL {mint_url} as the first mint for MPP payment.");
  40. // Check if the mint exists
  41. if let Ok(_wallet) =
  42. get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await
  43. {
  44. // Find the index of this mint in the mints_amounts list
  45. if let Some(mint_index) = mints_amounts
  46. .iter()
  47. .position(|(url, _)| url.to_string() == *mint_url)
  48. {
  49. mints.push(mint_index);
  50. let melt_amount: u64 =
  51. get_number_input("Enter amount to mint from this mint in sats.")?;
  52. mint_amounts.push(melt_amount);
  53. } else {
  54. println!("Warning: Mint URL exists but no balance found. Continuing with manual selection.");
  55. }
  56. } else {
  57. println!("Warning: Could not find wallet for the specified mint URL. Continuing with manual selection.");
  58. }
  59. }
  60. loop {
  61. let mint_number: String =
  62. get_user_input("Enter mint number to melt from and -1 when done.")?;
  63. if mint_number == "-1" || mint_number.is_empty() {
  64. break;
  65. }
  66. let mint_number: usize = mint_number.parse()?;
  67. validate_mint_number(mint_number, mints_amounts.len())?;
  68. mints.push(mint_number);
  69. let melt_amount: u64 =
  70. get_number_input("Enter amount to mint from this mint in sats.")?;
  71. mint_amounts.push(melt_amount);
  72. }
  73. let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
  74. let mut quotes = JoinSet::new();
  75. for (mint, amount) in mints.iter().zip(mint_amounts) {
  76. let wallet = mints_amounts[*mint].0.clone();
  77. let wallet = multi_mint_wallet
  78. .get_wallet(&WalletKey::new(wallet, unit.clone()))
  79. .await
  80. .expect("Known wallet");
  81. let options = MeltOptions::new_mpp(amount * 1000);
  82. let bolt11_clone = bolt11.clone();
  83. quotes.spawn(async move {
  84. let quote = wallet
  85. .melt_quote(bolt11_clone.to_string(), Some(options))
  86. .await;
  87. (wallet, quote)
  88. });
  89. }
  90. let quotes = quotes.join_all().await;
  91. for (wallet, quote) in quotes.iter() {
  92. if let Err(quote) = quote {
  93. tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, quote);
  94. bail!("Could not get melt quote for {}", wallet.mint_url);
  95. } else {
  96. let quote = quote.as_ref().unwrap();
  97. println!(
  98. "Melt quote {} for mint {} of amount {} with fee {}.",
  99. quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
  100. );
  101. }
  102. }
  103. let mut melts = JoinSet::new();
  104. for (wallet, quote) in quotes {
  105. let quote = quote.expect("Errors checked above");
  106. melts.spawn(async move {
  107. let melt = wallet.melt(&quote.id).await;
  108. (wallet, melt)
  109. });
  110. }
  111. let melts = melts.join_all().await;
  112. let mut error = false;
  113. for (wallet, melt) in melts {
  114. match melt {
  115. Ok(melt) => {
  116. println!(
  117. "Melt for {} paid {} with fee of {} ",
  118. wallet.mint_url, melt.amount, melt.fee_paid
  119. );
  120. }
  121. Err(err) => {
  122. println!("Melt for {} failed with {}", wallet.mint_url, err);
  123. error = true;
  124. }
  125. }
  126. }
  127. if error {
  128. bail!("Could not complete all melts");
  129. }
  130. } else {
  131. // Get wallet either by mint URL or by index
  132. let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
  133. // Use the provided mint URL
  134. get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await?
  135. } else {
  136. // Fallback to the index-based selection
  137. let mint_number: usize = get_number_input("Enter mint number to melt from")?;
  138. get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone())
  139. .await?
  140. };
  141. // Find the mint amount for the selected wallet to check available funds
  142. let mint_url = &wallet.mint_url;
  143. let mint_amount = mints_amounts
  144. .iter()
  145. .find(|(url, _)| url == mint_url)
  146. .map(|(_, amount)| *amount)
  147. .ok_or_else(|| anyhow::anyhow!("Could not find balance for mint: {}", mint_url))?;
  148. let available_funds = <cdk::Amount as Into<u64>>::into(mint_amount) * MSAT_IN_SAT;
  149. let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
  150. // Determine payment amount and options
  151. let options = if bolt11.amount_milli_satoshis().is_none() {
  152. // Get user input for amount
  153. let prompt = format!(
  154. "Enter the amount you would like to pay in sats for a {} payment.",
  155. if sub_command_args.mpp {
  156. "MPP"
  157. } else {
  158. "amountless invoice"
  159. }
  160. );
  161. let user_amount = get_number_input::<u64>(&prompt)? * MSAT_IN_SAT;
  162. if user_amount > available_funds {
  163. bail!("Not enough funds");
  164. }
  165. Some(MeltOptions::new_amountless(user_amount))
  166. } else {
  167. // Check if invoice amount exceeds available funds
  168. let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
  169. if invoice_amount > available_funds {
  170. bail!("Not enough funds");
  171. }
  172. None
  173. };
  174. // Process payment
  175. let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
  176. println!("{quote:?}");
  177. let melt = wallet.melt(&quote.id).await?;
  178. println!("Paid invoice: {}", melt.state);
  179. if let Some(preimage) = melt.preimage {
  180. println!("Payment preimage: {preimage}");
  181. }
  182. }
  183. Ok(())
  184. }