melt.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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::multi_mint_wallet::MultiMintWallet;
  7. use cdk::wallet::types::WalletKey;
  8. use cdk::wallet::{MeltQuote, Wallet};
  9. use cdk::Bolt11Invoice;
  10. use clap::{Args, ValueEnum};
  11. use lightning::offers::offer::Offer;
  12. use tokio::task::JoinSet;
  13. use crate::sub_commands::balance::mint_balances;
  14. use crate::utils::{
  15. get_number_input, get_user_input, get_wallet_by_index, get_wallet_by_mint_url,
  16. validate_mint_number,
  17. };
  18. #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
  19. pub enum PaymentType {
  20. /// BOLT11 invoice
  21. Bolt11,
  22. /// BOLT12 offer
  23. Bolt12,
  24. /// Bip353
  25. Bip353,
  26. }
  27. #[derive(Args)]
  28. pub struct MeltSubCommand {
  29. /// Currency unit e.g. sat
  30. #[arg(default_value = "sat")]
  31. unit: String,
  32. /// Mpp
  33. #[arg(short, long, conflicts_with = "mint_url")]
  34. mpp: bool,
  35. /// Mint URL to use for melting
  36. #[arg(long, conflicts_with = "mpp")]
  37. mint_url: Option<String>,
  38. /// Payment method (bolt11 or bolt12)
  39. #[arg(long, default_value = "bolt11")]
  40. method: PaymentType,
  41. }
  42. /// Helper function to process a melt quote and execute the payment
  43. async fn process_payment(wallet: &Wallet, quote: MeltQuote) -> Result<()> {
  44. // Display quote information
  45. println!("Quote ID: {}", quote.id);
  46. println!("Amount: {}", quote.amount);
  47. println!("Fee Reserve: {}", quote.fee_reserve);
  48. println!("State: {}", quote.state);
  49. println!("Expiry: {}", quote.expiry);
  50. // Execute the payment
  51. let melt = wallet.melt(&quote.id).await?;
  52. println!("Paid: {}", melt.state);
  53. if let Some(preimage) = melt.preimage {
  54. println!("Payment preimage: {preimage}");
  55. }
  56. Ok(())
  57. }
  58. /// Helper function to check if there are enough funds and create appropriate MeltOptions
  59. fn create_melt_options(
  60. available_funds: u64,
  61. payment_amount: Option<u64>,
  62. prompt: &str,
  63. ) -> Result<Option<MeltOptions>> {
  64. match payment_amount {
  65. Some(amount) => {
  66. // Payment has a specified amount
  67. if amount > available_funds {
  68. bail!("Not enough funds; payment requires {} msats", amount);
  69. }
  70. Ok(None) // Use default options
  71. }
  72. None => {
  73. // Payment doesn't have an amount, ask user for it
  74. let user_amount = get_number_input::<u64>(prompt)? * MSAT_IN_SAT;
  75. if user_amount > available_funds {
  76. bail!("Not enough funds");
  77. }
  78. Ok(Some(MeltOptions::new_amountless(user_amount)))
  79. }
  80. }
  81. }
  82. pub async fn pay(
  83. multi_mint_wallet: &MultiMintWallet,
  84. sub_command_args: &MeltSubCommand,
  85. ) -> Result<()> {
  86. let unit = CurrencyUnit::from_str(&sub_command_args.unit)?;
  87. let mints_amounts = mint_balances(multi_mint_wallet, &unit).await?;
  88. if sub_command_args.mpp {
  89. // MPP logic only works with BOLT11 currently
  90. if !matches!(sub_command_args.method, PaymentType::Bolt11) {
  91. bail!("MPP is only supported for BOLT11 invoices");
  92. }
  93. // Collect mint numbers and amounts for MPP
  94. let (mints, mint_amounts) = collect_mpp_inputs(&mints_amounts, &sub_command_args.mint_url)?;
  95. // Process BOLT11 MPP payment
  96. let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice request")?)?;
  97. // Get quotes from all mints
  98. let quotes = get_mpp_quotes(
  99. multi_mint_wallet,
  100. &mints_amounts,
  101. &mints,
  102. &mint_amounts,
  103. &unit,
  104. &bolt11,
  105. )
  106. .await?;
  107. // Execute all melts
  108. execute_mpp_melts(quotes).await?;
  109. } else {
  110. // Get wallet either by mint URL or by index
  111. let wallet = if let Some(mint_url) = &sub_command_args.mint_url {
  112. // Use the provided mint URL
  113. get_wallet_by_mint_url(multi_mint_wallet, mint_url, unit.clone()).await?
  114. } else {
  115. // Fallback to the index-based selection
  116. let mint_number: usize = get_number_input("Enter mint number to melt from")?;
  117. get_wallet_by_index(multi_mint_wallet, &mints_amounts, mint_number, unit.clone())
  118. .await?
  119. };
  120. // Find the mint amount for the selected wallet to check available funds
  121. let mint_url = &wallet.mint_url;
  122. let mint_amount = mints_amounts
  123. .iter()
  124. .find(|(url, _)| url == mint_url)
  125. .map(|(_, amount)| *amount)
  126. .ok_or_else(|| anyhow::anyhow!("Could not find balance for mint: {}", mint_url))?;
  127. let available_funds = <cdk::Amount as Into<u64>>::into(mint_amount) * MSAT_IN_SAT;
  128. // Process payment based on payment method
  129. match sub_command_args.method {
  130. PaymentType::Bolt11 => {
  131. // Process BOLT11 payment
  132. let bolt11 = Bolt11Invoice::from_str(&get_user_input("Enter bolt11 invoice")?)?;
  133. // Determine payment amount and options
  134. let prompt =
  135. "Enter the amount you would like to pay in sats for this amountless invoice.";
  136. let options =
  137. create_melt_options(available_funds, bolt11.amount_milli_satoshis(), prompt)?;
  138. // Process payment
  139. let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
  140. process_payment(&wallet, quote).await?;
  141. }
  142. PaymentType::Bolt12 => {
  143. // Process BOLT12 payment (offer)
  144. let offer_str = get_user_input("Enter BOLT12 offer")?;
  145. let offer = Offer::from_str(&offer_str)
  146. .map_err(|e| anyhow::anyhow!("Invalid BOLT12 offer: {:?}", e))?;
  147. // Determine if offer has an amount
  148. let prompt =
  149. "Enter the amount you would like to pay in sats for this amountless offer:";
  150. let amount_msat = match amount_for_offer(&offer, &CurrencyUnit::Msat) {
  151. Ok(amount) => Some(u64::from(amount)),
  152. Err(_) => None,
  153. };
  154. let options = create_melt_options(available_funds, amount_msat, prompt)?;
  155. // Get melt quote for BOLT12
  156. let quote = wallet.melt_bolt12_quote(offer_str, options).await?;
  157. process_payment(&wallet, quote).await?;
  158. }
  159. PaymentType::Bip353 => {
  160. let bip353_addr = get_user_input("Enter Bip353 address.")?;
  161. let prompt =
  162. "Enter the amount you would like to pay in sats for this amountless offer:";
  163. // BIP353 payments are always amountless for now
  164. let options = create_melt_options(available_funds, None, prompt)?;
  165. // Get melt quote for BIP353 address (internally resolves and gets BOLT12 quote)
  166. let quote = wallet
  167. .melt_bip353_quote(
  168. &bip353_addr,
  169. options.expect("Amount is required").amount_msat(),
  170. )
  171. .await?;
  172. process_payment(&wallet, quote).await?;
  173. }
  174. }
  175. }
  176. Ok(())
  177. }
  178. /// Collect mint numbers and amounts for MPP payments
  179. fn collect_mpp_inputs(
  180. mints_amounts: &[(MintUrl, Amount)],
  181. mint_url_opt: &Option<String>,
  182. ) -> Result<(Vec<usize>, Vec<u64>)> {
  183. let mut mints = Vec::new();
  184. let mut mint_amounts = Vec::new();
  185. // If a specific mint URL was provided, try to use it as the first mint
  186. if let Some(mint_url) = mint_url_opt {
  187. println!("Using mint URL {mint_url} as the first mint for MPP payment.");
  188. // Find the index of this mint in the mints_amounts list
  189. if let Some(mint_index) = mints_amounts
  190. .iter()
  191. .position(|(url, _)| url.to_string() == *mint_url)
  192. {
  193. mints.push(mint_index);
  194. let melt_amount: u64 =
  195. get_number_input("Enter amount to mint from this mint in sats.")?;
  196. mint_amounts.push(melt_amount);
  197. } else {
  198. println!(
  199. "Warning: Mint URL not found or no balance. Continuing with manual selection."
  200. );
  201. }
  202. }
  203. // Continue with regular mint selection
  204. loop {
  205. let mint_number: String =
  206. get_user_input("Enter mint number to melt from and -1 when done.")?;
  207. if mint_number == "-1" || mint_number.is_empty() {
  208. break;
  209. }
  210. let mint_number: usize = mint_number.parse()?;
  211. validate_mint_number(mint_number, mints_amounts.len())?;
  212. mints.push(mint_number);
  213. let melt_amount: u64 = get_number_input("Enter amount to mint from this mint in sats.")?;
  214. mint_amounts.push(melt_amount);
  215. }
  216. if mints.is_empty() {
  217. bail!("No mints selected for MPP payment");
  218. }
  219. Ok((mints, mint_amounts))
  220. }
  221. /// Get quotes from all mints for MPP payment
  222. async fn get_mpp_quotes(
  223. multi_mint_wallet: &MultiMintWallet,
  224. mints_amounts: &[(MintUrl, Amount)],
  225. mints: &[usize],
  226. mint_amounts: &[u64],
  227. unit: &CurrencyUnit,
  228. bolt11: &Bolt11Invoice,
  229. ) -> Result<Vec<(Wallet, MeltQuote)>> {
  230. let mut quotes = JoinSet::new();
  231. for (mint, amount) in mints.iter().zip(mint_amounts) {
  232. let wallet = mints_amounts[*mint].0.clone();
  233. let wallet = multi_mint_wallet
  234. .get_wallet(&WalletKey::new(wallet, unit.clone()))
  235. .await
  236. .expect("Known wallet");
  237. let options = MeltOptions::new_mpp(*amount * 1000);
  238. let bolt11_clone = bolt11.clone();
  239. quotes.spawn(async move {
  240. let quote = wallet
  241. .melt_quote(bolt11_clone.to_string(), Some(options))
  242. .await;
  243. (wallet, quote)
  244. });
  245. }
  246. let quotes_results = quotes.join_all().await;
  247. // Validate all quotes succeeded
  248. let mut valid_quotes = Vec::new();
  249. for (wallet, quote_result) in quotes_results {
  250. match quote_result {
  251. Ok(quote) => {
  252. println!(
  253. "Melt quote {} for mint {} of amount {} with fee {}.",
  254. quote.id, wallet.mint_url, quote.amount, quote.fee_reserve
  255. );
  256. valid_quotes.push((wallet, quote));
  257. }
  258. Err(err) => {
  259. tracing::error!("Could not get quote for {}: {:?}", wallet.mint_url, err);
  260. bail!("Could not get melt quote for {}", wallet.mint_url);
  261. }
  262. }
  263. }
  264. Ok(valid_quotes)
  265. }
  266. /// Execute all melts for MPP payment
  267. async fn execute_mpp_melts(quotes: Vec<(Wallet, MeltQuote)>) -> Result<()> {
  268. let mut melts = JoinSet::new();
  269. for (wallet, quote) in quotes {
  270. melts.spawn(async move {
  271. let melt = wallet.melt(&quote.id).await;
  272. (wallet, melt)
  273. });
  274. }
  275. let melts = melts.join_all().await;
  276. let mut error = false;
  277. for (wallet, melt) in melts {
  278. match melt {
  279. Ok(melt) => {
  280. println!(
  281. "Melt for {} paid {} with fee of {} ",
  282. wallet.mint_url, melt.amount, melt.fee_paid
  283. );
  284. }
  285. Err(err) => {
  286. println!("Melt for {} failed with {}", wallet.mint_url, err);
  287. error = true;
  288. }
  289. }
  290. }
  291. if error {
  292. bail!("Could not complete all melts");
  293. }
  294. Ok(())
  295. }