mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 23:55:01 +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: Mpp,
|
mpp: Mpp,
|
||||||
},
|
},
|
||||||
|
/// Amountless options
|
||||||
|
Amountless {
|
||||||
|
/// Amountless
|
||||||
|
amountless: Amountless,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MeltOptions {
|
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
|
/// Payment amount
|
||||||
pub fn amount_msat(&self) -> Amount {
|
pub fn amount_msat(&self) -> Amount {
|
||||||
match self {
|
match self {
|
||||||
Self::Mpp { mpp } => mpp.amount,
|
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 {
|
impl MeltQuoteBolt11Request {
|
||||||
/// Amount from [`MeltQuoteBolt11Request`]
|
/// Amount from [`MeltQuoteBolt11Request`]
|
||||||
///
|
///
|
||||||
@@ -100,6 +126,15 @@ impl MeltQuoteBolt11Request {
|
|||||||
.ok_or(Error::InvalidAmountRequest)?
|
.ok_or(Error::InvalidAmountRequest)?
|
||||||
.into()),
|
.into()),
|
||||||
Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
|
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
|
/// Max Amount
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub max_amount: Option<Amount>,
|
pub max_amount: Option<Amount>,
|
||||||
|
/// Amountless
|
||||||
|
#[serde(default)]
|
||||||
|
pub amountless: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
|
|||||||
@@ -57,39 +57,52 @@ pub async fn pay(
|
|||||||
stdin.read_line(&mut user_input)?;
|
stdin.read_line(&mut user_input)?;
|
||||||
let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;
|
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 mut user_input = String::new();
|
||||||
let stdin = io::stdin();
|
io::stdout().flush()?;
|
||||||
io::stdout().flush().unwrap();
|
io::stdin().read_line(&mut user_input)?;
|
||||||
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
|
if user_amount > available_funds {
|
||||||
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
|
|
||||||
{
|
|
||||||
bail!("Not enough funds");
|
bail!("Not enough funds");
|
||||||
}
|
}
|
||||||
|
|
||||||
options = Some(MeltOptions::new_mpp(user_amount * MSAT_IN_SAT));
|
Some(if sub_command_args.mpp {
|
||||||
} else if bolt11
|
MeltOptions::new_mpp(user_amount)
|
||||||
.amount_milli_satoshis()
|
} else {
|
||||||
.unwrap()
|
MeltOptions::new_amountless(user_amount)
|
||||||
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
|
})
|
||||||
{
|
} else {
|
||||||
bail!("Not enough funds");
|
// 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?;
|
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
|
||||||
|
|
||||||
println!("{:?}", quote);
|
println!("{:?}", quote);
|
||||||
|
|
||||||
let melt = wallet.melt("e.id).await?;
|
let melt = wallet.melt("e.id).await?;
|
||||||
|
|
||||||
println!("Paid invoice: {}", melt.state);
|
println!("Paid invoice: {}", melt.state);
|
||||||
|
|
||||||
if let Some(preimage) = melt.preimage {
|
if let Some(preimage) = melt.preimage {
|
||||||
println!("Payment preimage: {}", preimage);
|
println!("Payment preimage: {}", preimage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ impl MintPayment for Cln {
|
|||||||
mpp: true,
|
mpp: true,
|
||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
|
amountless: true,
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ impl Melted {
|
|||||||
|
|
||||||
let fee_paid = proofs_amount
|
let fee_paid = proofs_amount
|
||||||
.checked_sub(amount + change_amount)
|
.checked_sub(amount + change_amount)
|
||||||
.ok_or(Error::AmountOverflow)?;
|
.ok_or(Error::AmountOverflow)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
state,
|
state,
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ pub enum Error {
|
|||||||
/// Could not get mint info
|
/// Could not get mint info
|
||||||
#[error("Could not get mint info")]
|
#[error("Could not get mint info")]
|
||||||
CouldNotGetMintInfo,
|
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
|
// Mint Errors
|
||||||
/// Minting is disabled
|
/// Minting is disabled
|
||||||
|
|||||||
@@ -165,6 +165,8 @@ pub struct Bolt11Settings {
|
|||||||
pub unit: CurrencyUnit,
|
pub unit: CurrencyUnit,
|
||||||
/// Invoice Description supported
|
/// Invoice Description supported
|
||||||
pub invoice_description: bool,
|
pub invoice_description: bool,
|
||||||
|
/// Paying amountless invoices supported
|
||||||
|
pub amountless: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Bolt11Settings> for Value {
|
impl TryFrom<Bolt11Settings> for Value {
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ impl MintPayment for FakeWallet {
|
|||||||
mpp: true,
|
mpp: true,
|
||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
|
amountless: false,
|
||||||
})?)
|
})?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use bip39::Mnemonic;
|
use bip39::Mnemonic;
|
||||||
use cashu::{MeltOptions, Mpp};
|
use cashu::ProofsMethods;
|
||||||
use cdk::amount::{Amount, SplitTarget};
|
use cdk::amount::{Amount, SplitTarget};
|
||||||
use cdk::nuts::{
|
use cdk::nuts::{
|
||||||
CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload,
|
CurrencyUnit, MeltOptions, MeltQuoteState, MintBolt11Request, MintQuoteState, Mpp,
|
||||||
PreMintSecrets,
|
NotificationPayload, PreMintSecrets,
|
||||||
};
|
};
|
||||||
use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
|
use cdk::wallet::{HttpClient, MintConnector, Wallet, WalletSubscription};
|
||||||
use cdk_integration_tests::init_regtest::{
|
use cdk_integration_tests::init_regtest::{
|
||||||
@@ -189,17 +189,21 @@ async fn test_websocket_connection() -> Result<()> {
|
|||||||
async fn test_multimint_melt() -> Result<()> {
|
async fn test_multimint_melt() -> Result<()> {
|
||||||
let lnd_client = init_lnd_client().await;
|
let lnd_client = init_lnd_client().await;
|
||||||
|
|
||||||
|
let db = Arc::new(memory::empty().await?);
|
||||||
let wallet1 = Wallet::new(
|
let wallet1 = Wallet::new(
|
||||||
&get_mint_url_from_env(),
|
&get_mint_url_from_env(),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(memory::empty().await?),
|
db,
|
||||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let db = Arc::new(memory::empty().await?);
|
||||||
|
db.migrate().await;
|
||||||
let wallet2 = Wallet::new(
|
let wallet2 = Wallet::new(
|
||||||
&get_second_mint_url_from_env(),
|
&get_second_mint_url_from_env(),
|
||||||
CurrencyUnit::Sat,
|
CurrencyUnit::Sat,
|
||||||
Arc::new(memory::empty().await?),
|
db,
|
||||||
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
&Mnemonic::generate(12)?.to_seed_normalized(""),
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
@@ -293,3 +297,44 @@ async fn test_cached_mint() -> Result<()> {
|
|||||||
assert!(response == response1);
|
assert!(response == response1);
|
||||||
Ok(())
|
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,
|
mpp: false,
|
||||||
unit: CurrencyUnit::Sat,
|
unit: CurrencyUnit::Sat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
|
amountless: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ impl Lnd {
|
|||||||
mpp: true,
|
mpp: true,
|
||||||
unit: CurrencyUnit::Msat,
|
unit: CurrencyUnit::Msat,
|
||||||
invoice_description: true,
|
invoice_description: true,
|
||||||
|
amountless: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -527,6 +527,10 @@ impl CdkMint for MintRPCServer {
|
|||||||
.max
|
.max
|
||||||
.map(Amount::from)
|
.map(Amount::from)
|
||||||
.or_else(|| current_nut05_settings.as_ref().and_then(|s| s.max_amount)),
|
.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);
|
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 {
|
cdk_common::MeltOptions::Mpp { mpp } => Self::Mpp(Mpp {
|
||||||
amount: mpp.amount.into(),
|
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");
|
let options = value.options.expect("option defined");
|
||||||
match options {
|
match options {
|
||||||
Options::Mpp(mpp) => cdk_common::MeltOptions::new_mpp(mpp.amount),
|
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;
|
uint64 amount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message Amountless {
|
||||||
|
uint64 amount_msat = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message MeltOptions {
|
message MeltOptions {
|
||||||
oneof options {
|
oneof options {
|
||||||
Mpp mpp = 1;
|
Mpp mpp = 1;
|
||||||
|
Amountless amountless = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,10 +202,11 @@ impl MintBuilder {
|
|||||||
self.mint_info.nuts.nut04.disabled = false;
|
self.mint_info.nuts.nut04.disabled = false;
|
||||||
|
|
||||||
let melt_method_settings = MeltMethodSettings {
|
let melt_method_settings = MeltMethodSettings {
|
||||||
method: method.clone(),
|
method,
|
||||||
unit,
|
unit,
|
||||||
min_amount: Some(limits.melt_min),
|
min_amount: Some(limits.melt_min),
|
||||||
max_amount: Some(limits.melt_max),
|
max_amount: Some(limits.melt_max),
|
||||||
|
amountless: settings.amountless,
|
||||||
};
|
};
|
||||||
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
|
self.mint_info.nuts.nut05.methods.push(melt_method_settings);
|
||||||
self.mint_info.nuts.nut05.disabled = false;
|
self.mint_info.nuts.nut05.disabled = false;
|
||||||
|
|||||||
@@ -61,6 +61,16 @@ impl Mint {
|
|||||||
// because should have already been converted to the partial amount
|
// because should have already been converted to the partial amount
|
||||||
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,
|
None => amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ impl Wallet {
|
|||||||
options,
|
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 {
|
if quote_res.amount != amount_quote_unit {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|||||||
Reference in New Issue
Block a user