Skip to content

Commit d224cc5

Browse files
authored
Melt to amountless invoice (cashubtc#497)
* feat: melt token with amountless * fix: docs * fix: extra migration
1 parent 09f339e commit d224cc5

File tree

16 files changed

+161
-28
lines changed

16 files changed

+161
-28
lines changed

crates/cashu/src/nuts/nut05.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ pub enum MeltOptions {
5858
/// MPP
5959
mpp: Mpp,
6060
},
61+
/// Amountless options
62+
Amountless {
63+
/// Amountless
64+
amountless: Amountless,
65+
},
6166
}
6267

6368
impl MeltOptions {
@@ -73,14 +78,35 @@ impl MeltOptions {
7378
}
7479
}
7580

81+
/// Create new [`MeltOptions::Amountless`]
82+
pub fn new_amountless<A>(amount_msat: A) -> Self
83+
where
84+
A: Into<Amount>,
85+
{
86+
Self::Amountless {
87+
amountless: Amountless {
88+
amount_msat: amount_msat.into(),
89+
},
90+
}
91+
}
92+
7693
/// Payment amount
7794
pub fn amount_msat(&self) -> Amount {
7895
match self {
7996
Self::Mpp { mpp } => mpp.amount,
97+
Self::Amountless { amountless } => amountless.amount_msat,
8098
}
8199
}
82100
}
83101

102+
/// Amountless payment
103+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
104+
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
105+
pub struct Amountless {
106+
/// Amount to pay in msat
107+
pub amount_msat: Amount,
108+
}
109+
84110
impl MeltQuoteBolt11Request {
85111
/// Amount from [`MeltQuoteBolt11Request`]
86112
///
@@ -100,6 +126,15 @@ impl MeltQuoteBolt11Request {
100126
.ok_or(Error::InvalidAmountRequest)?
101127
.into()),
102128
Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
129+
Some(MeltOptions::Amountless { amountless }) => {
130+
let amount = amountless.amount_msat;
131+
if let Some(amount_msat) = request.amount_milli_satoshis() {
132+
if amount != amount_msat.into() {
133+
return Err(Error::InvalidAmountRequest);
134+
}
135+
}
136+
Ok(amount)
137+
}
103138
}
104139
}
105140
}
@@ -392,6 +427,9 @@ pub struct MeltMethodSettings {
392427
/// Max Amount
393428
#[serde(skip_serializing_if = "Option::is_none")]
394429
pub max_amount: Option<Amount>,
430+
/// Amountless
431+
#[serde(default)]
432+
pub amountless: bool,
395433
}
396434

397435
impl Settings {

crates/cdk-cli/src/sub_commands/melt.rs

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,39 +57,52 @@ pub async fn pay(
5757
stdin.read_line(&mut user_input)?;
5858
let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;
5959

60-
let mut options: Option<MeltOptions> = None;
60+
let available_funds =
61+
<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT;
62+
63+
// Determine payment amount and options
64+
let options = if sub_command_args.mpp || bolt11.amount_milli_satoshis().is_none() {
65+
// Get user input for amount
66+
println!(
67+
"Enter the amount you would like to pay in sats for a {} payment.",
68+
if sub_command_args.mpp {
69+
"MPP"
70+
} else {
71+
"amountless invoice"
72+
}
73+
);
6174

62-
if sub_command_args.mpp {
63-
println!("Enter the amount you would like to pay in sats, for a mpp payment.");
6475
let mut user_input = String::new();
65-
let stdin = io::stdin();
66-
io::stdout().flush().unwrap();
67-
stdin.read_line(&mut user_input)?;
76+
io::stdout().flush()?;
77+
io::stdin().read_line(&mut user_input)?;
6878

69-
let user_amount = user_input.trim_end().parse::<u64>()?;
79+
let user_amount = user_input.trim_end().parse::<u64>()? * MSAT_IN_SAT;
7080

71-
if user_amount
72-
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
73-
{
81+
if user_amount > available_funds {
7482
bail!("Not enough funds");
7583
}
7684

77-
options = Some(MeltOptions::new_mpp(user_amount * MSAT_IN_SAT));
78-
} else if bolt11
79-
.amount_milli_satoshis()
80-
.unwrap()
81-
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
82-
{
83-
bail!("Not enough funds");
84-
}
85+
Some(if sub_command_args.mpp {
86+
MeltOptions::new_mpp(user_amount)
87+
} else {
88+
MeltOptions::new_amountless(user_amount)
89+
})
90+
} else {
91+
// Check if invoice amount exceeds available funds
92+
let invoice_amount = bolt11.amount_milli_satoshis().unwrap();
93+
if invoice_amount > available_funds {
94+
bail!("Not enough funds");
95+
}
96+
None
97+
};
8598

99+
// Process payment
86100
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
87-
88101
println!("{:?}", quote);
89102

90103
let melt = wallet.melt(&quote.id).await?;
91-
92104
println!("Paid invoice: {}", melt.state);
105+
93106
if let Some(preimage) = melt.preimage {
94107
println!("Payment preimage: {}", preimage);
95108
}

crates/cdk-cln/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ impl MintPayment for Cln {
7272
mpp: true,
7373
unit: CurrencyUnit::Msat,
7474
invoice_description: true,
75+
amountless: true,
7576
})?)
7677
}
7778

crates/cdk-common/src/common.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ impl Melted {
4343

4444
let fee_paid = proofs_amount
4545
.checked_sub(amount + change_amount)
46-
.ok_or(Error::AmountOverflow)?;
46+
.ok_or(Error::AmountOverflow)
47+
.unwrap();
4748

4849
Ok(Self {
4950
state,

crates/cdk-common/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ pub enum Error {
8888
/// Could not get mint info
8989
#[error("Could not get mint info")]
9090
CouldNotGetMintInfo,
91+
/// Multi-Part Payment not supported for unit and method
92+
#[error("Amountless invoices are not supported for unit `{0}` and method `{1}`")]
93+
AmountlessInvoiceNotSupported(CurrencyUnit, PaymentMethod),
9194

9295
// Mint Errors
9396
/// Minting is disabled

crates/cdk-common/src/payment.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ pub struct Bolt11Settings {
165165
pub unit: CurrencyUnit,
166166
/// Invoice Description supported
167167
pub invoice_description: bool,
168+
/// Paying amountless invoices supported
169+
pub amountless: bool,
168170
}
169171

170172
impl TryFrom<Bolt11Settings> for Value {

crates/cdk-fake-wallet/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ impl MintPayment for FakeWallet {
109109
mpp: true,
110110
unit: CurrencyUnit::Msat,
111111
invoice_description: true,
112+
amountless: false,
112113
})?)
113114
}
114115

crates/cdk-integration-tests/tests/regtest.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ use std::time::Duration;
44

55
use anyhow::{bail, Result};
66
use bip39::Mnemonic;
7-
use cashu::{MeltOptions, Mpp};
7+
use cashu::ProofsMethods;
88
use cdk::amount::{Amount, SplitTarget};
99
use cdk::nuts::{
10-
CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload,
11-
PreMintSecrets,
10+
CurrencyUnit, MeltOptions, MeltQuoteState, MintBolt11Request, MintQuoteState, Mpp,
11+
NotificationPayload, PreMintSecrets,
1212
};
1313
use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
1414
use cdk_integration_tests::init_regtest::{
@@ -189,17 +189,21 @@ async fn test_websocket_connection() -> Result<()> {
189189
async fn test_multimint_melt() -> Result<()> {
190190
let lnd_client = init_lnd_client().await;
191191

192+
let db = Arc::new(memory::empty().await?);
192193
let wallet1 = Wallet::new(
193194
&get_mint_url_from_env(),
194195
CurrencyUnit::Sat,
195-
Arc::new(memory::empty().await?),
196+
db,
196197
&Mnemonic::generate(12)?.to_seed_normalized(""),
197198
None,
198199
)?;
200+
201+
let db = Arc::new(memory::empty().await?);
202+
db.migrate().await;
199203
let wallet2 = Wallet::new(
200204
&get_second_mint_url_from_env(),
201205
CurrencyUnit::Sat,
202-
Arc::new(memory::empty().await?),
206+
db,
203207
&Mnemonic::generate(12)?.to_seed_normalized(""),
204208
None,
205209
)?;
@@ -293,3 +297,44 @@ async fn test_cached_mint() -> Result<()> {
293297
assert!(response == response1);
294298
Ok(())
295299
}
300+
301+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
302+
async fn test_regtest_melt_amountless() -> Result<()> {
303+
let lnd_client = init_lnd_client().await;
304+
305+
let wallet = Wallet::new(
306+
&get_mint_url_from_env(),
307+
CurrencyUnit::Sat,
308+
Arc::new(memory::empty().await?),
309+
&Mnemonic::generate(12)?.to_seed_normalized(""),
310+
None,
311+
)?;
312+
313+
let mint_amount = Amount::from(100);
314+
315+
let mint_quote = wallet.mint_quote(mint_amount, None).await?;
316+
317+
assert_eq!(mint_quote.amount, mint_amount);
318+
319+
lnd_client.pay_invoice(mint_quote.request).await?;
320+
321+
let proofs = wallet
322+
.mint(&mint_quote.id, SplitTarget::default(), None)
323+
.await?;
324+
325+
let amount = proofs.total_amount()?;
326+
327+
assert!(mint_amount == amount);
328+
329+
let invoice = lnd_client.create_invoice(None).await?;
330+
331+
let options = MeltOptions::new_amountless(5_000);
332+
333+
let melt_quote = wallet.melt_quote(invoice.clone(), Some(options)).await?;
334+
335+
let melt = wallet.melt(&melt_quote.id).await.unwrap();
336+
337+
assert!(melt.amount == 5.into());
338+
339+
Ok(())
340+
}

crates/cdk-lnbits/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ impl LNbits {
6969
mpp: false,
7070
unit: CurrencyUnit::Sat,
7171
invoice_description: true,
72+
amountless: false,
7273
},
7374
})
7475
}

crates/cdk-lnd/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ impl Lnd {
104104
mpp: true,
105105
unit: CurrencyUnit::Msat,
106106
invoice_description: true,
107+
amountless: true,
107108
},
108109
})
109110
}

0 commit comments

Comments
 (0)