From bc3d3acd6d6ff13074f74600563043040debd606 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 21 Nov 2024 20:44:49 +0100 Subject: [PATCH 1/3] feat: melt token with amountless --- crates/cashu/src/nuts/nut05.rs | 38 +++++++++++++ crates/cdk-cli/src/sub_commands/melt.rs | 53 ++++++++++++------- crates/cdk-cln/src/lib.rs | 1 + crates/cdk-common/src/common.rs | 3 +- crates/cdk-common/src/error.rs | 3 ++ crates/cdk-common/src/payment.rs | 2 + crates/cdk-fake-wallet/src/lib.rs | 1 + crates/cdk-integration-tests/tests/regtest.rs | 47 ++++++++++++++-- crates/cdk-lnbits/src/lib.rs | 1 + crates/cdk-lnd/src/lib.rs | 1 + crates/cdk-mint-rpc/src/proto/server.rs | 4 ++ crates/cdk-payment-processor/src/proto/mod.rs | 6 +++ .../src/proto/payment_processor.proto | 6 +++ crates/cdk/src/mint/builder.rs | 3 +- crates/cdk/src/mint/melt.rs | 10 ++++ crates/cdk/src/wallet/melt.rs | 2 +- 16 files changed, 155 insertions(+), 26 deletions(-) diff --git a/crates/cashu/src/nuts/nut05.rs b/crates/cashu/src/nuts/nut05.rs index 7aef6f955f..9ee78d043e 100644 --- a/crates/cashu/src/nuts/nut05.rs +++ b/crates/cashu/src/nuts/nut05.rs @@ -58,6 +58,11 @@ pub enum MeltOptions { /// MPP mpp: Mpp, }, + /// Amountless options + Amountless { + /// Amountless + amountless: Amountless, + }, } impl MeltOptions { @@ -73,14 +78,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`] /// @@ -100,6 +126,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) + } } } } @@ -392,6 +427,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 { diff --git a/crates/cdk-cli/src/sub_commands/melt.rs b/crates/cdk-cli/src/sub_commands/melt.rs index 2f56edbf18..279738b35f 100644 --- a/crates/cdk-cli/src/sub_commands/melt.rs +++ b/crates/cdk-cli/src/sub_commands/melt.rs @@ -57,39 +57,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 f0a20add56..a1254c3663 100644 --- a/crates/cdk-cln/src/lib.rs +++ b/crates/cdk-cln/src/lib.rs @@ -72,6 +72,7 @@ impl MintPayment for Cln { mpp: true, unit: CurrencyUnit::Msat, invoice_description: true, + amountless: true, })?) } diff --git a/crates/cdk-common/src/common.rs b/crates/cdk-common/src/common.rs index f0314c004d..b2920bb39e 100644 --- a/crates/cdk-common/src/common.rs +++ b/crates/cdk-common/src/common.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-common/src/error.rs b/crates/cdk-common/src/error.rs index 7bec4fe073..bcac4a9b22 100644 --- a/crates/cdk-common/src/error.rs +++ b/crates/cdk-common/src/error.rs @@ -88,6 +88,9 @@ pub enum Error { /// Could not get mint info #[error("Could not get mint info")] CouldNotGetMintInfo, + /// Multi-Part Payment not supported for unit and method + #[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")] + AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod), // Mint Errors /// Minting is disabled diff --git a/crates/cdk-common/src/payment.rs b/crates/cdk-common/src/payment.rs index 1be2360e73..357eeeccfd 100644 --- a/crates/cdk-common/src/payment.rs +++ b/crates/cdk-common/src/payment.rs @@ -165,6 +165,8 @@ pub struct Bolt11Settings { pub unit: CurrencyUnit, /// Invoice Description supported pub invoice_description: bool, + /// Paying amountless invoices supported + pub amountless: bool, } impl TryFrom for Value { diff --git a/crates/cdk-fake-wallet/src/lib.rs b/crates/cdk-fake-wallet/src/lib.rs index 228cd8562f..9fdd4b911f 100644 --- a/crates/cdk-fake-wallet/src/lib.rs +++ b/crates/cdk-fake-wallet/src/lib.rs @@ -109,6 +109,7 @@ impl MintPayment 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 d523f1474f..84590f7629 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -4,11 +4,11 @@ use std::time::Duration; use anyhow::{bail, Result}; use bip39::Mnemonic; -use cashu::{MeltOptions, Mpp}; +use cashu::ProofsMethods; use cdk::amount::{Amount, SplitTarget}; use cdk::nuts::{ - CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload, - PreMintSecrets, + CurrencyUnit, MeltOptions, MeltQuoteState, MintBolt11Request, MintQuoteState, Mpp, + NotificationPayload, PreMintSecrets, }; use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription}; use cdk_integration_tests::init_regtest::{ @@ -293,3 +293,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_from_env(), + CurrencyUnit::Sat, + Arc::new(memory::empty().await?), + &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 = MeltOptions::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 f925b4c924..9d0562b541 100644 --- a/crates/cdk-lnbits/src/lib.rs +++ b/crates/cdk-lnbits/src/lib.rs @@ -69,6 +69,7 @@ impl 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 8253b53b04..e45822a6e6 100644 --- a/crates/cdk-lnd/src/lib.rs +++ b/crates/cdk-lnd/src/lib.rs @@ -104,6 +104,7 @@ impl Lnd { mpp: true, unit: CurrencyUnit::Msat, invoice_description: true, + amountless: true, }, }) } diff --git a/crates/cdk-mint-rpc/src/proto/server.rs b/crates/cdk-mint-rpc/src/proto/server.rs index 33d714070a..873a592f32 100644 --- a/crates/cdk-mint-rpc/src/proto/server.rs +++ b/crates/cdk-mint-rpc/src/proto/server.rs @@ -527,6 +527,10 @@ impl CdkMint for MintRPCServer { .max .map(Amount::from) .or_else(|| current_nut05_settings.as_ref().and_then(|s| s.max_amount)), + amountless: current_nut05_settings + .as_ref() + .map(|s| s.amountless) + .unwrap_or_default(), }; methods.push(updated_method_settings); diff --git a/crates/cdk-payment-processor/src/proto/mod.rs b/crates/cdk-payment-processor/src/proto/mod.rs index e0185fc807..b8389720da 100644 --- a/crates/cdk-payment-processor/src/proto/mod.rs +++ b/crates/cdk-payment-processor/src/proto/mod.rs @@ -97,6 +97,9 @@ impl From for Options { cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp { amount: mpp.amount.into(), }), + cdk_common::MeltOptions::Amountless { amountless } => Self::Amountless(Amountless { + amount_msat: amountless.amount_msat.into(), + }), } } } @@ -106,6 +109,9 @@ impl From for cdk_common::nut05::MeltOptions { let options = value.options.expect("option defined"); match options { Options::Mpp(mpp) => cdk_common::MeltOptions::new_mpp(mpp.amount), + Options::Amountless(amountless) => { + cdk_common::MeltOptions::new_amountless(amountless.amount_msat) + } } } } diff --git a/crates/cdk-payment-processor/src/proto/payment_processor.proto b/crates/cdk-payment-processor/src/proto/payment_processor.proto index a476ca6a35..3ac427d7a0 100644 --- a/crates/cdk-payment-processor/src/proto/payment_processor.proto +++ b/crates/cdk-payment-processor/src/proto/payment_processor.proto @@ -35,9 +35,15 @@ message Mpp { uint64 amount = 1; } + +message Amountless { + uint64 amount_msat = 1; +} + message MeltOptions { oneof options { Mpp mpp = 1; + Amountless amountless = 2; } } diff --git a/crates/cdk/src/mint/builder.rs b/crates/cdk/src/mint/builder.rs index 711f1e9b02..5d43bbf978 100644 --- a/crates/cdk/src/mint/builder.rs +++ b/crates/cdk/src/mint/builder.rs @@ -202,10 +202,11 @@ impl MintBuilder { self.mint_info.nuts.nut04.disabled = false; let melt_method_settings = MeltMethodSettings { - method: method.clone(), + method, 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/mint/melt.rs b/crates/cdk/src/mint/melt.rs index 73419d044a..49c6fe44d1 100644 --- a/crates/cdk/src/mint/melt.rs +++ b/crates/cdk/src/mint/melt.rs @@ -61,6 +61,16 @@ impl Mint { // because should have already been converted to the partial amount amount } + Some(MeltOptions::Amountless { amountless: _ }) => { + if !nut15 + .methods + .into_iter() + .any(|m| m.method == method && m.unit == unit) + { + return Err(Error::AmountlessInvoiceNotSupported(unit, method)); + } + amount + } None => amount, }; diff --git a/crates/cdk/src/wallet/melt.rs b/crates/cdk/src/wallet/melt.rs index 9e6bada5f3..851c202aaa 100644 --- a/crates/cdk/src/wallet/melt.rs +++ b/crates/cdk/src/wallet/melt.rs @@ -62,7 +62,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!( From b8e8ef33f1204d91c89fc30dda720e7c15911470 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Thu, 3 Apr 2025 17:16:31 +0100 Subject: [PATCH 2/3] fix: docs --- crates/cashu/src/nuts/nut05.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cashu/src/nuts/nut05.rs b/crates/cashu/src/nuts/nut05.rs index 9ee78d043e..5a3ab1b546 100644 --- a/crates/cashu/src/nuts/nut05.rs +++ b/crates/cashu/src/nuts/nut05.rs @@ -78,7 +78,7 @@ impl MeltOptions { } } - /// Create new [`Options::Amountless`] + /// Create new [`MeltOptions::Amountless`] pub fn new_amountless(amount_msat: A) -> Self where A: Into, From 1d1ac0465d2a3efd53a34ea1c3d892cd3c4c9899 Mon Sep 17 00:00:00 2001 From: thesimplekid Date: Fri, 4 Apr 2025 12:23:48 +0100 Subject: [PATCH 3/3] fix: extra migration --- crates/cdk-integration-tests/tests/regtest.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/cdk-integration-tests/tests/regtest.rs b/crates/cdk-integration-tests/tests/regtest.rs index 84590f7629..4a7f3de424 100644 --- a/crates/cdk-integration-tests/tests/regtest.rs +++ b/crates/cdk-integration-tests/tests/regtest.rs @@ -189,17 +189,21 @@ async fn test_websocket_connection() -> Result<()> { async fn test_multimint_melt() -> Result<()> { let lnd_client = init_lnd_client().await; + let db = Arc::new(memory::empty().await?); let wallet1 = Wallet::new( &get_mint_url_from_env(), CurrencyUnit::Sat, - Arc::new(memory::empty().await?), + db, &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?; + + let db = Arc::new(memory::empty().await?); + db.migrate().await; let wallet2 = Wallet::new( &get_second_mint_url_from_env(), CurrencyUnit::Sat, - Arc::new(memory::empty().await?), + db, &Mnemonic::generate(12)?.to_seed_normalized(""), None, )?;