From f9f29683fb395a4310214ac2fc3527853bcf00f0 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 21 Nov 2024 20:44:49 +0100 Subject: [PATCH] feat: melt token with amountless --- crates/cdk-cli/src/sub_commands/melt.rs | 53 ++++++++++++------- crates/cdk-cln/src/lib.rs | 1 + crates/cdk-fake-wallet/src/lib.rs | 1 + crates/cdk-integration-tests/tests/regtest.rs | 42 +++++++++++++++ crates/cdk-lnbits/src/lib.rs | 1 + crates/cdk-lnd/src/lib.rs | 1 + crates/cdk-phoenixd/src/lib.rs | 1 + .../20241209163341_amount_to_pay_msats.sql | 1 + crates/cdk-strike/src/lib.rs | 1 + crates/cdk/src/cdk_lightning/mod.rs | 2 + crates/cdk/src/mint/builder.rs | 1 + crates/cdk/src/nuts/nut05.rs | 39 ++++++++++++++ crates/cdk/src/types.rs | 3 +- crates/cdk/src/wallet/melt.rs | 2 +- 14 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 crates/cdk-sqlite/src/mint/migrations/20241209163341_amount_to_pay_msats.sql diff --git a/crates/cdk-cli/src/sub_commands/melt.rs b/crates/cdk-cli/src/sub_commands/melt.rs index b152db20d..88ebda499 100644 --- a/crates/cdk-cli/src/sub_commands/melt.rs +++ b/crates/cdk-cli/src/sub_commands/melt.rs @@ -56,39 +56,52 @@ pub async fn pay( stdin.read_line(&mut user_input)?; let bolt11 = Bolt11Invoice::from_str(user_input.trim())?; - let mut options: Option = None; + let available_funds = + >::into(mints_amounts[mint_number].1) * MSAT_IN_SAT; + + // Determine payment amount and options + let options = if sub_command_args.mpp || bolt11.amount_milli_satoshis().is_none() { + // Get user input for amount + println!( + "Enter the amount you would like to pay in sats for a {} payment.", + if sub_command_args.mpp { + "MPP" + } else { + "amountless invoice" + } + ); - if sub_command_args.mpp { - println!("Enter the amount you would like to pay in sats, for a mpp payment."); let mut user_input = String::new(); - let stdin = io::stdin(); - io::stdout().flush().unwrap(); - stdin.read_line(&mut user_input)?; + io::stdout().flush()?; + io::stdin().read_line(&mut user_input)?; - let user_amount = user_input.trim_end().parse::()?; + let user_amount = user_input.trim_end().parse::()? * MSAT_IN_SAT; - if user_amount - .gt(&(>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT)) - { + if user_amount > available_funds { bail!("Not enough funds"); } - options = Some(MeltOptions::new_mpp(user_amount * MSAT_IN_SAT)); - } else if bolt11 - .amount_milli_satoshis() - .unwrap() - .gt(&(>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT)) - { - bail!("Not enough funds"); - } + Some(if sub_command_args.mpp { + MeltOptions::new_mpp(user_amount) + } else { + MeltOptions::new_amountless(user_amount) + }) + } else { + // Check if invoice amount exceeds available funds + let invoice_amount = bolt11.amount_milli_satoshis().unwrap(); + if invoice_amount > available_funds { + bail!("Not enough funds"); + } + None + }; + // Process payment let quote = wallet.melt_quote(bolt11.to_string(), options).await?; - println!("{:?}", quote); let melt = wallet.melt("e.id).await?; - println!("Paid invoice: {}", melt.state); + if let Some(preimage) = melt.preimage { println!("Payment preimage: {}", preimage); } diff --git a/crates/cdk-cln/src/lib.rs b/crates/cdk-cln/src/lib.rs index 1d5f076b7..9065dde82 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -70,6 +70,7 @@ impl MintLightning for Cln { mpp: true, unit: CurrencyUnit::Msat, invoice_description: true, + amountless: true, } } diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index 5dd9662ef..bb48e6708 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -104,6 +104,7 @@ impl MintLightning for FakeWallet { mpp: true, unit: CurrencyUnit::Msat, invoice_description: true, + amountless: false, } } diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 1f31f6cd3..74e1806bb 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -8,6 +8,7 @@ use bip39::Mnemonic; use cdk::amount::{Amount, SplitTarget}; use cdk::cdk_database::WalletMemoryDatabase; use cdk::nuts::nut00::ProofsMethods; +use cdk::nuts::nut05::Options; use cdk::nuts::{ CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload, PreMintSecrets, State, @@ -404,3 +405,44 @@ async fn test_cached_mint() -> Result<()> { assert!(response == response1); Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_regtest_melt_amountless() -> Result<()> { + let lnd_client = init_lnd_client().await?; + + let wallet = Wallet::new( + &get_mint_url(), + CurrencyUnit::Sat, + Arc::new(WalletMemoryDatabase::default()), + &Mnemonic::generate(12)?.to_seed_normalized(""), + None, + )?; + + let mint_amount = Amount::from(100); + + let mint_quote = wallet.mint_quote(mint_amount, None).await?; + + assert_eq!(mint_quote.amount, mint_amount); + + lnd_client.pay_invoice(mint_quote.request).await?; + + let proofs = wallet + .mint(&mint_quote.id, SplitTarget::default(), None) + .await?; + + let amount = proofs.total_amount()?; + + assert!(mint_amount == amount); + + let invoice = lnd_client.create_invoice(None).await?; + + let options = Options::new_amountless(5_000); + + let melt_quote = wallet.melt_quote(invoice.clone(), Some(options)).await?; + + let melt = wallet.melt(&melt_quote.id).await.unwrap(); + + assert!(melt.amount == 5.into()); + + Ok(()) +} diff --git a/crates/cdk-lnbits/src/lib.rs b/crates/cdk-lnbits/src/lib.rs index b9c07159c..2cf74895c 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -72,6 +72,7 @@ impl MintLightning for LNbits { mpp: false, unit: CurrencyUnit::Sat, invoice_description: true, + amountless: false, } } diff --git a/crates/cdk-lnd/src/lib.rs b/crates/cdk-lnd/src/lib.rs index 03e45170e..3795f3765 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -80,6 +80,7 @@ impl MintLightning for Lnd { mpp: false, unit: CurrencyUnit::Msat, invoice_description: true, + amountless: true, } } diff --git a/crates/cdk-phoenixd/src/lib.rs b/crates/cdk-phoenixd/src/lib.rs index 151ab2e74..ad9d6ed7d 100644 --- a/crates/cdk-phoenixd/src/lib.rs +++ b/crates/cdk-phoenixd/src/lib.rs @@ -79,6 +79,7 @@ impl MintLightning for Phoenixd { mpp: false, unit: CurrencyUnit::Sat, invoice_description: true, + amountless: true, } } fn is_wait_invoice_active(&self) -> bool { diff --git a/crates/cdk-sqlite/src/mint/migrations/20241209163341_amount_to_pay_msats.sql b/crates/cdk-sqlite/src/mint/migrations/20241209163341_amount_to_pay_msats.sql new file mode 100644 index 000000000..184bcc577 --- /dev/null +++ b/crates/cdk-sqlite/src/mint/migrations/20241209163341_amount_to_pay_msats.sql @@ -0,0 +1 @@ +ALTER TABLE melt_quote ADD COLUMN msat_to_pay INTEGER; diff --git a/crates/cdk-strike/src/lib.rs b/crates/cdk-strike/src/lib.rs index d55332636..8a234ff94 100644 --- a/crates/cdk-strike/src/lib.rs +++ b/crates/cdk-strike/src/lib.rs @@ -70,6 +70,7 @@ impl MintLightning for Strike { mpp: false, unit: self.unit.clone(), invoice_description: true, + amountless: false, } } diff --git a/crates/cdk/src/cdk_lightning/mod.rs b/crates/cdk/src/cdk_lightning/mod.rs index 639882d94..b57f45c56 100644 --- a/crates/cdk/src/cdk_lightning/mod.rs +++ b/crates/cdk/src/cdk_lightning/mod.rs @@ -152,4 +152,6 @@ pub struct Settings { pub unit: CurrencyUnit, /// Invoice Description supported pub invoice_description: bool, + /// Paying amountless invoices supported + pub amountless: bool, } diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index b61ae8e0d..acf15a3a4 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -167,6 +167,7 @@ impl MintBuilder { unit, min_amount: Some(limits.melt_min), max_amount: Some(limits.melt_max), + amountless: settings.amountless, }; self.mint_info.nuts.nut05.methods.push(melt_method_settings); self.mint_info.nuts.nut05.disabled = false; diff --git a/crates/cdk/src/nuts/nut05.rs b/crates/cdk/src/nuts/nut05.rs index ab042c8ee..022f83642 100644 --- a/crates/cdk/src/nuts/nut05.rs +++ b/crates/cdk/src/nuts/nut05.rs @@ -59,6 +59,11 @@ pub enum MeltOptions { /// MPP mpp: Mpp, }, + /// Amountless options + Amountless { + /// Amountless + amountless: Amountless, + }, } impl MeltOptions { @@ -74,14 +79,35 @@ impl MeltOptions { } } + /// Create new [`Options::Amountless`] + pub fn new_amountless(amount_msat: A) -> Self + where + A: Into, + { + Self::Amountless { + amountless: Amountless { + amount_msat: amount_msat.into(), + }, + } + } + /// Payment amount pub fn amount_msat(&self) -> Amount { match self { Self::Mpp { mpp } => mpp.amount, + Self::Amountless { amountless } => amountless.amount_msat, } } } +/// Amountless payment +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))] +pub struct Amountless { + /// Amount to pay in msat + pub amount_msat: Amount, +} + impl MeltQuoteBolt11Request { /// Amount from [`MeltQuoteBolt11Request`] /// @@ -101,6 +127,15 @@ impl MeltQuoteBolt11Request { .ok_or(Error::InvalidAmountRequest)? .into()), Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount), + Some(MeltOptions::Amountless { amountless }) => { + let amount = amountless.amount_msat; + if let Some(amount_msat) = request.amount_milli_satoshis() { + if amount != amount_msat.into() { + return Err(Error::InvalidAmountRequest); + } + } + Ok(amount) + } } } } @@ -377,6 +412,9 @@ pub struct MeltMethodSettings { /// Max Amount #[serde(skip_serializing_if = "Option::is_none")] pub max_amount: Option, + /// Amountless + #[serde(default)] + pub amountless: bool, } impl Settings { @@ -418,6 +456,7 @@ impl Default for Settings { unit: CurrencyUnit::Sat, min_amount: Some(Amount::from(1)), max_amount: Some(Amount::from(1000000)), + amountless: false, }; Settings { diff --git a/crates/cdk/src/types.rs b/crates/cdk/src/types.rs index b2fd8a4d0..1e9309390 100644 --- a/crates/cdk/src/types.rs +++ b/crates/cdk/src/types.rs @@ -43,7 +43,8 @@ impl Melted { let fee_paid = proofs_amount .checked_sub(amount + change_amount) - .ok_or(Error::AmountOverflow)?; + .ok_or(Error::AmountOverflow) + .unwrap(); Ok(Self { state, diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index 81aacffd4..dd27b5c20 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -60,7 +60,7 @@ impl Wallet { options, }; - let quote_res = self.client.post_melt_quote(quote_request).await?; + let quote_res = self.client.post_melt_quote(quote_request).await.unwrap(); if quote_res.amount != amount_quote_unit { tracing::warn!(