mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-24 08:05:02 +01:00
Melt to amountless invoice (#497)
* feat: melt token with amountless * fix: docs * fix: extra migration
This commit is contained in:
@@ -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 [`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`]
|
||||
///
|
||||
@@ -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<Amount>,
|
||||
/// Amountless
|
||||
#[serde(default)]
|
||||
pub amountless: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
|
||||
@@ -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("e.id).await?;
|
||||
|
||||
println!("Paid invoice: {}", melt.state);
|
||||
|
||||
if let Some(preimage) = melt.preimage {
|
||||
println!("Payment preimage: {}", preimage);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ impl MintPayment for Cln {
|
||||
mpp: true,
|
||||
unit: CurrencyUnit::Msat,
|
||||
invoice_description: true,
|
||||
amountless: true,
|
||||
})?)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -109,6 +109,7 @@ impl MintPayment for FakeWallet {
|
||||
mpp: true,
|
||||
unit: CurrencyUnit::Msat,
|
||||
invoice_description: true,
|
||||
amountless: false,
|
||||
})?)
|
||||
}
|
||||
|
||||
|
||||
@@ -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::{
|
||||
@@ -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,
|
||||
)?;
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ impl LNbits {
|
||||
mpp: false,
|
||||
unit: CurrencyUnit::Sat,
|
||||
invoice_description: true,
|
||||
amountless: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ impl Lnd {
|
||||
mpp: true,
|
||||
unit: CurrencyUnit::Msat,
|
||||
invoice_description: true,
|
||||
amountless: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user