Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions crates/cashu/src/nuts/nut05.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ pub enum MeltOptions {
/// MPP
mpp: Mpp,
},
/// Amountless options
Amountless {
/// Amountless
amountless: Amountless,
},
}

impl MeltOptions {
Expand All @@ -73,14 +78,35 @@ impl MeltOptions {
}
}

/// Create new [`MeltOptions::Amountless`]
pub fn new_amountless<A>(amount_msat: A) -> Self
where
A: Into<Amount>,
{
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`]
///
Expand All @@ -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)
}
}
}
}
Expand Down Expand Up @@ -392,6 +427,9 @@ pub struct MeltMethodSettings {
/// Max Amount
#[serde(skip_serializing_if = "Option::is_none")]
pub max_amount: Option<Amount>,
/// Amountless
#[serde(default)]
pub amountless: bool,
}

impl Settings {
Expand Down
53 changes: 33 additions & 20 deletions crates/cdk-cli/src/sub_commands/melt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MeltOptions> = None;
let available_funds =
<cdk::Amount as Into<u64>>::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::<u64>()?;
let user_amount = user_input.trim_end().parse::<u64>()? * MSAT_IN_SAT;

if user_amount
.gt(&(<cdk::Amount as Into<u64>>::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(&(<cdk::Amount as Into<u64>>::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(&quote.id).await?;

println!("Paid invoice: {}", melt.state);

if let Some(preimage) = melt.preimage {
println!("Payment preimage: {}", preimage);
}
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-cln/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ impl MintPayment for Cln {
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
amountless: true,
})?)
}

Expand Down
3 changes: 2 additions & 1 deletion crates/cdk-common/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions crates/cdk-common/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions crates/cdk-common/src/payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Bolt11Settings> for Value {
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-fake-wallet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ impl MintPayment for FakeWallet {
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
amountless: false,
})?)
}

Expand Down
55 changes: 50 additions & 5 deletions crates/cdk-integration-tests/tests/regtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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,
)?;
Expand Down Expand Up @@ -293,3 +297,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(())
}
1 change: 1 addition & 0 deletions crates/cdk-lnbits/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ impl LNbits {
mpp: false,
unit: CurrencyUnit::Sat,
invoice_description: true,
amountless: false,
},
})
}
Expand Down
1 change: 1 addition & 0 deletions crates/cdk-lnd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ impl Lnd {
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
amountless: true,
},
})
}
Expand Down
4 changes: 4 additions & 0 deletions crates/cdk-mint-rpc/src/proto/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions crates/cdk-payment-processor/src/proto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ impl From<cdk_common::nut05::MeltOptions> 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(),
}),
}
}
}
Expand All @@ -106,6 +109,9 @@ impl From<MeltOptions> 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)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
3 changes: 2 additions & 1 deletion crates/cdk/src/mint/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions crates/cdk/src/mint/melt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
2 changes: 1 addition & 1 deletion crates/cdk/src/wallet/melt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
Loading