Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Melt to amountless invoice #497

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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 @@ -56,39 +56,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 @@ -70,6 +70,7 @@ impl MintLightning for Cln {
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
amountless: true,
}
}

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 @@ -104,6 +104,7 @@ impl MintLightning for FakeWallet {
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
amountless: false,
}
}

Expand Down
45 changes: 43 additions & 2 deletions crates/cdk-integration-tests/tests/regtest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use cdk::amount::{Amount, SplitTarget};
use cdk::cdk_database::WalletMemoryDatabase;
use cdk::nuts::nut00::ProofsMethods;
use cdk::nuts::{
CurrencyUnit, MeltQuoteState, MintBolt11Request, MintQuoteState, NotificationPayload,
PreMintSecrets, State,
CurrencyUnit, MeltOptions, MeltQuoteState, MintBolt11Request, MintQuoteState,
NotificationPayload, PreMintSecrets, State,
};
use cdk::wallet::client::{HttpClient, MintConnector};
use cdk::wallet::{Wallet, WalletSubscription};
Expand Down Expand Up @@ -404,3 +404,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(),
CurrencyUnit::Sat,
Arc::new(WalletMemoryDatabase::default()),
&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 @@ -72,6 +72,7 @@ impl MintLightning for 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 @@ -80,6 +80,7 @@ impl MintLightning for Lnd {
mpp: false,
unit: CurrencyUnit::Msat,
invoice_description: true,
amountless: true,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/cdk-phoenixd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl MintLightning for Phoenixd {
mpp: false,
unit: CurrencyUnit::Sat,
invoice_description: true,
amountless: true,
}
}
fn is_wait_invoice_active(&self) -> bool {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE melt_quote ADD COLUMN msat_to_pay INTEGER;
1 change: 1 addition & 0 deletions crates/cdk-strike/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ impl MintLightning for Strike {
mpp: false,
unit: self.unit.clone(),
invoice_description: true,
amountless: false,
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/cdk/src/cdk_lightning/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,6 @@ pub struct Settings {
pub unit: CurrencyUnit,
/// Invoice Description supported
pub invoice_description: bool,
/// Paying amountless invoices supported
pub amountless: bool,
}
1 change: 1 addition & 0 deletions crates/cdk/src/mint/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ impl MintBuilder {
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
39 changes: 39 additions & 0 deletions crates/cdk/src/nuts/nut05.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ pub enum MeltOptions {
/// MPP
mpp: Mpp,
},
/// Amountless options
Amountless {
/// Amountless
amountless: Amountless,
},
}

impl MeltOptions {
Expand All @@ -74,14 +79,35 @@ impl MeltOptions {
}
}

/// Create new [`Options::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 @@ -101,6 +127,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 @@ -377,6 +412,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 Expand Up @@ -418,6 +456,7 @@ impl Default for Settings {
unit: CurrencyUnit::Sat,
min_amount: Some(Amount::from(1)),
max_amount: Some(Amount::from(1000000)),
amountless: false,
};

Settings {
Expand Down
3 changes: 2 additions & 1 deletion crates/cdk/src/types.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
2 changes: 1 addition & 1 deletion crates/cdk/src/wallet/melt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,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