This commit is contained in:
thesimplekid
2025-01-05 14:42:44 +00:00
committed by GitHub
parent d6b7d49ea9
commit 6a8a5a7941
27 changed files with 222 additions and 145 deletions

View File

@@ -3,7 +3,8 @@ use std::io::Write;
use std::str::FromStr; use std::str::FromStr;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use cdk::nuts::CurrencyUnit; use cdk::amount::MSAT_IN_SAT;
use cdk::nuts::{CurrencyUnit, MeltOptions};
use cdk::wallet::multi_mint_wallet::{MultiMintWallet, WalletKey}; use cdk::wallet::multi_mint_wallet::{MultiMintWallet, WalletKey};
use cdk::Bolt11Invoice; use cdk::Bolt11Invoice;
use clap::Args; use clap::Args;
@@ -15,6 +16,9 @@ pub struct MeltSubCommand {
/// Currency unit e.g. sat /// Currency unit e.g. sat
#[arg(default_value = "sat")] #[arg(default_value = "sat")]
unit: String, unit: String,
/// Mpp
#[arg(short, long)]
mpp: bool,
} }
pub async fn pay( pub async fn pay(
@@ -52,14 +56,33 @@ 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())?;
if bolt11 let mut options: Option<MeltOptions> = None;
.amount_milli_satoshis()
.unwrap() if sub_command_args.mpp {
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * 1000_u64)) 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)?;
let user_amount = user_input.trim_end().parse::<u64>()?;
if user_amount
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * MSAT_IN_SAT))
{ {
bail!("Not enough funds"); bail!("Not enough funds");
} }
let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
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");
}
let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
println!("{:?}", quote); println!("{:?}", quote);

View File

@@ -11,7 +11,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use cdk::amount::{to_unit, Amount}; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
use cdk::cdk_lightning::{ use cdk::cdk_lightning::{
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
}; };
@@ -196,16 +196,9 @@ impl MintLightning for Cln {
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, melt_quote_request: &MeltQuoteBolt11Request,
) -> Result<PaymentQuoteResponse, Self::Err> { ) -> Result<PaymentQuoteResponse, Self::Err> {
let invoice_amount_msat = melt_quote_request let amount = melt_quote_request.amount_msat()?;
.request
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?;
let amount = to_unit( let amount = amount / MSAT_IN_SAT.into();
invoice_amount_msat,
&CurrencyUnit::Msat,
&melt_quote_request.unit,
)?;
let relative_fee_reserve = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -248,11 +241,15 @@ impl MintLightning for Cln {
} }
} }
let amount_msat = melt_quote
.msat_to_pay
.map(|a| CLN_Amount::from_msat(a.into()));
let mut cln_client = self.cln_client.lock().await; let mut cln_client = self.cln_client.lock().await;
let cln_response = cln_client let cln_response = cln_client
.call(Request::Pay(PayRequest { .call(Request::Pay(PayRequest {
bolt11: melt_quote.request.to_string(), bolt11: melt_quote.request.to_string(),
amount_msat: None, amount_msat,
label: None, label: None,
riskfactor: None, riskfactor: None,
maxfeepercent: None, maxfeepercent: None,
@@ -264,9 +261,7 @@ impl MintLightning for Cln {
maxfee: max_fee maxfee: max_fee
.map(|a| { .map(|a| {
let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?; let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat( Ok::<CLN_Amount, Self::Err>(CLN_Amount::from_msat(msat.into()))
msat.into(),
))
}) })
.transpose()?, .transpose()?,
description: None, description: None,
@@ -289,6 +284,7 @@ impl MintLightning for Cln {
PayStatus::PENDING => MeltQuoteState::Pending, PayStatus::PENDING => MeltQuoteState::Pending,
PayStatus::FAILED => MeltQuoteState::Failed, PayStatus::FAILED => MeltQuoteState::Failed,
}; };
PayInvoiceResponse { PayInvoiceResponse {
payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())), payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
payment_lookup_id: pay_response.payment_hash.to_string(), payment_lookup_id: pay_response.payment_hash.to_string(),
@@ -301,6 +297,10 @@ impl MintLightning for Cln {
unit: melt_quote.unit, unit: melt_quote.unit,
} }
} }
Err(err) => {
tracing::error!("Could not pay invoice: {}", err);
return Err(Error::ClnRpc(err).into());
}
_ => { _ => {
tracing::error!( tracing::error!(
"Error attempting to pay invoice: {}", "Error attempting to pay invoice: {}",

View File

@@ -14,7 +14,7 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use bitcoin::hashes::{sha256, Hash}; use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::{Secp256k1, SecretKey}; use bitcoin::secp256k1::{Secp256k1, SecretKey};
use cdk::amount::{to_unit, Amount}; use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
use cdk::cdk_lightning::{ use cdk::cdk_lightning::{
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings, self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
}; };
@@ -127,16 +127,9 @@ impl MintLightning for FakeWallet {
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, melt_quote_request: &MeltQuoteBolt11Request,
) -> Result<PaymentQuoteResponse, Self::Err> { ) -> Result<PaymentQuoteResponse, Self::Err> {
let invoice_amount_msat = melt_quote_request let amount = melt_quote_request.amount_msat()?;
.request
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?;
let amount = to_unit( let amount = amount / MSAT_IN_SAT.into();
invoice_amount_msat,
&CurrencyUnit::Msat,
&melt_quote_request.unit,
)?;
let relative_fee_reserve = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;

View File

@@ -19,7 +19,6 @@ use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
use ln_regtest_rs::lnd::Lnd; use ln_regtest_rs::lnd::Lnd;
use tokio::sync::Notify; use tokio::sync::Notify;
use tower_http::cors::CorsLayer; use tower_http::cors::CorsLayer;
use tracing_subscriber::EnvFilter;
const BITCOIND_ADDR: &str = "127.0.0.1:18443"; const BITCOIND_ADDR: &str = "127.0.0.1:18443";
const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332"; const ZMQ_RAW_BLOCK: &str = "tcp://127.0.0.1:28332";
@@ -193,19 +192,6 @@ pub async fn start_cln_mint<D>(addr: &str, port: u16, database: D) -> Result<()>
where where
D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static, D: MintDatabase<Err = cdk_database::Error> + Send + Sync + 'static,
{ {
let default_filter = "debug";
let sqlx_filter = "sqlx=warn";
let hyper_filter = "hyper=warn";
let env_filter = EnvFilter::new(format!(
"{},{},{}",
default_filter, sqlx_filter, hyper_filter
));
// Parse input
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let cln_client = init_cln_client().await?; let cln_client = init_cln_client().await?;
let cln_backend = create_cln_backend(&cln_client).await?; let cln_backend = create_cln_backend(&cln_client).await?;

View File

@@ -8,9 +8,24 @@ use cdk_integration_tests::init_regtest::{
}; };
use cdk_redb::MintRedbDatabase; use cdk_redb::MintRedbDatabase;
use cdk_sqlite::MintSqliteDatabase; use cdk_sqlite::MintSqliteDatabase;
use tracing_subscriber::EnvFilter;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let default_filter = "debug";
let sqlx_filter = "sqlx=warn";
let hyper_filter = "hyper=warn";
let h2_filter = "h2=warn";
let env_filter = EnvFilter::new(format!(
"{},{},{},{}",
default_filter, sqlx_filter, hyper_filter, h2_filter
));
// Parse input
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let mut bitcoind = init_bitcoind(); let mut bitcoind = init_bitcoind();
bitcoind.start_bitcoind()?; bitcoind.start_bitcoind()?;

View File

@@ -77,7 +77,7 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> {
let mint_quote = wallet.mint_quote(100.into(), None).await?; let mint_quote = wallet.mint_quote(100.into(), None).await?;
lnd_client.pay_invoice(mint_quote.request).await?; lnd_client.pay_invoice(mint_quote.request).await.unwrap();
let proofs = wallet let proofs = wallet
.mint(&mint_quote.id, SplitTarget::default(), None) .mint(&mint_quote.id, SplitTarget::default(), None)

View File

@@ -152,16 +152,9 @@ impl MintLightning for LNbits {
return Err(Self::Err::Anyhow(anyhow!("Unsupported unit"))); return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
} }
let invoice_amount_msat = melt_quote_request let amount = melt_quote_request.amount_msat()?;
.request
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?;
let amount = to_unit( let amount = amount / MSAT_IN_SAT.into();
invoice_amount_msat,
&CurrencyUnit::Msat,
&melt_quote_request.unit,
)?;
let relative_fee_reserve = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;

View File

@@ -77,7 +77,7 @@ impl MintLightning for Lnd {
fn get_settings(&self) -> Settings { fn get_settings(&self) -> Settings {
Settings { Settings {
mpp: true, mpp: false,
unit: CurrencyUnit::Msat, unit: CurrencyUnit::Msat,
invoice_description: true, invoice_description: true,
} }
@@ -164,16 +164,9 @@ impl MintLightning for Lnd {
&self, &self,
melt_quote_request: &MeltQuoteBolt11Request, melt_quote_request: &MeltQuoteBolt11Request,
) -> Result<PaymentQuoteResponse, Self::Err> { ) -> Result<PaymentQuoteResponse, Self::Err> {
let invoice_amount_msat = melt_quote_request let amount = melt_quote_request.amount_msat()?;
.request
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?;
let amount = to_unit( let amount = amount / MSAT_IN_SAT.into();
invoice_amount_msat,
&CurrencyUnit::Msat,
&melt_quote_request.unit,
)?;
let relative_fee_reserve = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -196,11 +189,21 @@ impl MintLightning for Lnd {
async fn pay_invoice( async fn pay_invoice(
&self, &self,
melt_quote: mint::MeltQuote, melt_quote: mint::MeltQuote,
partial_amount: Option<Amount>, _partial_amount: Option<Amount>,
max_fee: Option<Amount>, max_fee: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<PayInvoiceResponse, Self::Err> {
let payment_request = melt_quote.request; let payment_request = melt_quote.request;
let amount_msat: u64 = match melt_quote.msat_to_pay {
Some(amount_msat) => amount_msat.into(),
None => {
let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
bolt11
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?
}
};
let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest { let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
payment_request, payment_request,
fee_limit: max_fee.map(|f| { fee_limit: max_fee.map(|f| {
@@ -208,13 +211,7 @@ impl MintLightning for Lnd {
FeeLimit { limit: Some(limit) } FeeLimit { limit: Some(limit) }
}), }),
amt_msat: partial_amount amt_msat: amount_msat as i64,
.map(|a| {
let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat).unwrap();
u64::from(msat) as i64
})
.unwrap_or_default(),
..Default::default() ..Default::default()
}; };

View File

@@ -81,7 +81,6 @@ impl MintLightning for Phoenixd {
invoice_description: true, invoice_description: true,
} }
} }
fn is_wait_invoice_active(&self) -> bool { fn is_wait_invoice_active(&self) -> bool {
self.wait_invoice_is_active.load(Ordering::SeqCst) self.wait_invoice_is_active.load(Ordering::SeqCst)
} }
@@ -162,16 +161,9 @@ impl MintLightning for Phoenixd {
return Err(Error::UnsupportedUnit.into()); return Err(Error::UnsupportedUnit.into());
} }
let invoice_amount_msat = melt_quote_request let amount = melt_quote_request.amount_msat()?;
.request
.amount_milli_satoshis()
.ok_or(Error::UnknownInvoiceAmount)?;
let amount = to_unit( let amount = amount / MSAT_IN_SAT.into();
invoice_amount_msat,
&CurrencyUnit::Msat,
&melt_quote_request.unit,
)?;
let relative_fee_reserve = let relative_fee_reserve =
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64; (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
@@ -197,12 +189,16 @@ impl MintLightning for Phoenixd {
async fn pay_invoice( async fn pay_invoice(
&self, &self,
melt_quote: mint::MeltQuote, melt_quote: mint::MeltQuote,
partial_amount: Option<Amount>, _partial_amount: Option<Amount>,
_max_fee_msats: Option<Amount>, _max_fee_msats: Option<Amount>,
) -> Result<PayInvoiceResponse, Self::Err> { ) -> Result<PayInvoiceResponse, Self::Err> {
let msat_to_pay: Option<u64> = melt_quote
.msat_to_pay
.map(|a| <cdk::Amount as Into<u64>>::into(a) / MSAT_IN_SAT);
let pay_response = self let pay_response = self
.phoenixd_api .phoenixd_api
.pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into())) .pay_bolt11_invoice(&melt_quote.request, msat_to_pay)
.await?; .await?;
// The pay invoice response does not give the needed fee info so we have to check. // The pay invoice response does not give the needed fee info so we have to check.

View File

@@ -0,0 +1 @@
ALTER TABLE melt_quote ADD COLUMN msat_to_pay INTEGER;

View File

@@ -468,8 +468,8 @@ WHERE id=?
let res = sqlx::query( let res = sqlx::query(
r#" r#"
INSERT OR REPLACE INTO melt_quote INSERT OR REPLACE INTO melt_quote
(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id) (id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id, msat_to_pay)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#, "#,
) )
.bind(quote.id.to_string()) .bind(quote.id.to_string())
@@ -481,6 +481,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
.bind(quote.expiry as i64) .bind(quote.expiry as i64)
.bind(quote.payment_preimage) .bind(quote.payment_preimage)
.bind(quote.request_lookup_id) .bind(quote.request_lookup_id)
.bind(quote.msat_to_pay.map(|a| u64::from(a) as i64))
.execute(&mut transaction) .execute(&mut transaction)
.await; .await;
@@ -804,11 +805,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
.map(|row| { .map(|row| {
PublicKey::from_slice(row.get("y")) PublicKey::from_slice(row.get("y"))
.map_err(Error::from) .map_err(Error::from)
.and_then(|y| { .and_then(|y| sqlite_row_to_proof(row).map(|proof| (y, proof)))
sqlite_row_to_proof(row)
.map_err(Error::from)
.map(|proof| (y, proof))
})
}) })
.collect::<Result<HashMap<_, _>, _>>()?; .collect::<Result<HashMap<_, _>, _>>()?;
@@ -1060,11 +1057,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
.map(|row| { .map(|row| {
PublicKey::from_slice(row.get("y")) PublicKey::from_slice(row.get("y"))
.map_err(Error::from) .map_err(Error::from)
.and_then(|y| { .and_then(|y| sqlite_row_to_blind_signature(row).map(|blinded| (y, blinded)))
sqlite_row_to_blind_signature(row)
.map_err(Error::from)
.map(|blinded| (y, blinded))
})
}) })
.collect::<Result<HashMap<_, _>, _>>()?; .collect::<Result<HashMap<_, _>, _>>()?;
@@ -1307,6 +1300,8 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<mint::MeltQuote, Error> {
let request_lookup_id = row_request_lookup.unwrap_or(row_request.clone()); let request_lookup_id = row_request_lookup.unwrap_or(row_request.clone());
let row_msat_to_pay: Option<i64> = row.try_get("msat_to_pay").map_err(Error::from)?;
Ok(mint::MeltQuote { Ok(mint::MeltQuote {
id: row_id.into_uuid(), id: row_id.into_uuid(),
amount: Amount::from(row_amount as u64), amount: Amount::from(row_amount as u64),
@@ -1317,6 +1312,7 @@ fn sqlite_row_to_melt_quote(row: SqliteRow) -> Result<mint::MeltQuote, Error> {
expiry: row_expiry as u64, expiry: row_expiry as u64,
payment_preimage: row_preimage, payment_preimage: row_preimage,
request_lookup_id, request_lookup_id,
msat_to_pay: row_msat_to_pay.map(|a| Amount::from(a as u64)),
}) })
} }

View File

@@ -22,3 +22,5 @@ tracing = { version = "0.1", default-features = false, features = ["attributes",
thiserror = "1" thiserror = "1"
uuid = { version = "1", features = ["v4"] } uuid = { version = "1", features = ["v4"] }
strike-rs = "0.4.0" strike-rs = "0.4.0"
# strike-rs = { path = "../../../../strike-rs" }
# strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "577ad9591" }

View File

@@ -167,9 +167,11 @@ impl MintLightning for Strike {
let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?; let fee = from_strike_amount(quote.lightning_network_fee, &melt_quote_request.unit)?;
let amount = from_strike_amount(quote.amount, &melt_quote_request.unit)?.into();
Ok(PaymentQuoteResponse { Ok(PaymentQuoteResponse {
request_lookup_id: quote.payment_quote_id, request_lookup_id: quote.payment_quote_id,
amount: from_strike_amount(quote.amount, &melt_quote_request.unit)?.into(), amount,
fee: fee.into(), fee: fee.into(),
state: MeltQuoteState::Unpaid, state: MeltQuoteState::Unpaid,
}) })

View File

@@ -367,8 +367,6 @@ impl MintDatabase for MintMemoryDatabase {
if let Some(quote_id) = quote_id { if let Some(quote_id) = quote_id {
let mut current_quote_signatures = self.quote_signatures.write().await; let mut current_quote_signatures = self.quote_signatures.write().await;
current_quote_signatures.insert(quote_id, blind_signatures.to_vec()); current_quote_signatures.insert(quote_id, blind_signatures.to_vec());
let t = current_quote_signatures.get(&quote_id);
println!("after insert: {:?}", t);
} }
Ok(()) Ok(())

View File

@@ -96,8 +96,7 @@ impl WalletDatabase for WalletMemoryDatabase {
) -> Result<(), Self::Err> { ) -> Result<(), Self::Err> {
let proofs = self let proofs = self
.get_proofs(Some(old_mint_url), None, None, None) .get_proofs(Some(old_mint_url), None, None, None)
.await .await?;
.map_err(Error::from)?;
// Update proofs // Update proofs
{ {

View File

@@ -41,6 +41,9 @@ pub enum Error {
/// Amount Error /// Amount Error
#[error(transparent)] #[error(transparent)]
Amount(#[from] crate::amount::Error), Amount(#[from] crate::amount::Error),
/// NUT05 Error
#[error(transparent)]
NUT05(#[from] crate::nuts::nut05::Error),
} }
/// MintLighting Trait /// MintLighting Trait

View File

@@ -48,6 +48,9 @@ pub enum Error {
/// Witness missing or invalid /// Witness missing or invalid
#[error("Signature missing or invalid")] #[error("Signature missing or invalid")]
SignatureMissingOrInvalid, SignatureMissingOrInvalid,
/// Amountless Invoice Not supported
#[error("Amount Less Invoice is not allowed")]
AmountLessNotAllowed,
// Mint Errors // Mint Errors
/// Minting is disabled /// Minting is disabled
@@ -60,7 +63,7 @@ pub enum Error {
#[error("Expired quote: Expired: `{0}`, Time: `{1}`")] #[error("Expired quote: Expired: `{0}`, Time: `{1}`")]
ExpiredQuote(u64, u64), ExpiredQuote(u64, u64),
/// Amount is outside of allowed range /// Amount is outside of allowed range
#[error("Amount but be between `{0}` and `{1}` is `{2}`")] #[error("Amount must be between `{0}` and `{1}` is `{2}`")]
AmountOutofLimitRange(Amount, Amount, Amount), AmountOutofLimitRange(Amount, Amount, Amount),
/// Quote is not paiud /// Quote is not paiud
#[error("Quote not paid")] #[error("Quote not paid")]

View File

@@ -60,21 +60,14 @@ impl Mint {
request, request,
unit, unit,
options: _, options: _,
..
} = melt_request; } = melt_request;
let amount = match melt_request.options { let amount_msats = melt_request.amount_msat()?;
Some(mpp_amount) => mpp_amount.amount,
None => {
let amount_msat = request
.amount_milli_satoshis()
.ok_or(Error::InvoiceAmountUndefined)?;
to_unit(amount_msat, &CurrencyUnit::Msat, unit) let amount_quote_unit = to_unit(amount_msats, &CurrencyUnit::Msat, unit)?;
.map_err(|_err| Error::UnsupportedUnit)?
}
};
self.check_melt_request_acceptable(amount, unit.clone(), PaymentMethod::Bolt11)?; self.check_melt_request_acceptable(amount_quote_unit, unit.clone(), PaymentMethod::Bolt11)?;
let ln = self let ln = self
.ln .ln
@@ -95,6 +88,14 @@ impl Mint {
Error::UnitUnsupported Error::UnitUnsupported
})?; })?;
// We only want to set the msats_to_pay of the melt quote if the invoice is amountless
// or we want to ignore the amount and do an mpp payment
let msats_to_pay = if request.amount_milli_satoshis().is_some() {
None
} else {
Some(amount_msats)
};
let quote = MeltQuote::new( let quote = MeltQuote::new(
request.to_string(), request.to_string(),
unit.clone(), unit.clone(),
@@ -102,12 +103,13 @@ impl Mint {
payment_quote.fee, payment_quote.fee,
unix_time() + self.config.quote_ttl().melt_ttl, unix_time() + self.config.quote_ttl().melt_ttl,
payment_quote.request_lookup_id.clone(), payment_quote.request_lookup_id.clone(),
msats_to_pay,
); );
tracing::debug!( tracing::debug!(
"New melt quote {} for {} {} with request id {}", "New melt quote {} for {} {} with request id {}",
quote.id, quote.id,
amount, amount_quote_unit,
unit, unit,
payment_quote.request_lookup_id payment_quote.request_lookup_id
); );
@@ -182,10 +184,13 @@ impl Mint {
let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat) let quote_msats = to_unit(melt_quote.amount, &melt_quote.unit, &CurrencyUnit::Msat)
.expect("Quote unit is checked above that it can convert to msat"); .expect("Quote unit is checked above that it can convert to msat");
let invoice_amount_msats: Amount = invoice let invoice_amount_msats: Amount = match melt_quote.msat_to_pay {
Some(amount) => amount,
None => invoice
.amount_milli_satoshis() .amount_milli_satoshis()
.ok_or(Error::InvoiceAmountUndefined)? .ok_or(Error::InvoiceAmountUndefined)?
.into(); .into(),
};
let partial_amount = match invoice_amount_msats > quote_msats { let partial_amount = match invoice_amount_msats > quote_msats {
true => { true => {
@@ -582,7 +587,6 @@ impl Mint {
Ok(res) Ok(res)
} }
/// Process melt request marking [`Proofs`] as spent /// Process melt request marking [`Proofs`] as spent
/// The melt request must be verifyed using [`Self::verify_melt_request`] /// The melt request must be verifyed using [`Self::verify_melt_request`]
/// before calling [`Self::process_melt_request`] /// before calling [`Self::process_melt_request`]

View File

@@ -591,10 +591,7 @@ fn create_new_keyset<C: secp256k1::Signing>(
} }
fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option<DerivationPath> { fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option<DerivationPath> {
let unit_index = match unit.derivation_index() { let unit_index = unit.derivation_index()?;
Some(index) => index,
None => return None,
};
Some(DerivationPath::from(vec![ Some(DerivationPath::from(vec![
ChildNumber::from_hardened_idx(0).expect("0 is a valid index"), ChildNumber::from_hardened_idx(0).expect("0 is a valid index"),

View File

@@ -79,6 +79,10 @@ pub struct MeltQuote {
pub payment_preimage: Option<String>, pub payment_preimage: Option<String>,
/// Value used by ln backend to look up state of request /// Value used by ln backend to look up state of request
pub request_lookup_id: String, pub request_lookup_id: String,
/// Msat to pay
///
/// Used for an amountless invoice
pub msat_to_pay: Option<Amount>,
} }
impl MeltQuote { impl MeltQuote {
@@ -90,6 +94,7 @@ impl MeltQuote {
fee_reserve: Amount, fee_reserve: Amount,
expiry: u64, expiry: u64,
request_lookup_id: String, request_lookup_id: String,
msat_to_pay: Option<Amount>,
) -> Self { ) -> Self {
let id = Uuid::new_v4(); let id = Uuid::new_v4();
@@ -103,6 +108,7 @@ impl MeltQuote {
expiry, expiry,
payment_preimage: None, payment_preimage: None,
request_lookup_id, request_lookup_id,
msat_to_pay,
} }
} }
} }

View File

@@ -25,7 +25,7 @@ pub mod nut20;
pub use nut00::{ pub use nut00::{
BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof, BlindSignature, BlindedMessage, CurrencyUnit, PaymentMethod, PreMint, PreMintSecrets, Proof,
Proofs, Token, TokenV3, TokenV4, Witness, Proofs, ProofsMethods, Token, TokenV3, TokenV4, Witness,
}; };
pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey}; pub use nut01::{Keys, KeysResponse, PublicKey, SecretKey};
#[cfg(feature = "mint")] #[cfg(feature = "mint")]
@@ -39,8 +39,8 @@ pub use nut04::{
MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings, MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
}; };
pub use nut05::{ pub use nut05::{
MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response, MeltBolt11Request, MeltMethodSettings, MeltOptions, MeltQuoteBolt11Request,
QuoteState as MeltQuoteState, Settings as NUT05Settings, MeltQuoteBolt11Response, QuoteState as MeltQuoteState, Settings as NUT05Settings,
}; };
pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts}; pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State}; pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};

View File

@@ -47,7 +47,6 @@ impl ProofsMethods for Proofs {
self.iter() self.iter()
.map(|p| p.y()) .map(|p| p.y())
.collect::<Result<Vec<PublicKey>, _>>() .collect::<Result<Vec<PublicKey>, _>>()
.map_err(Into::into)
} }
} }

View File

@@ -28,6 +28,12 @@ pub enum Error {
/// Amount overflow /// Amount overflow
#[error("Amount Overflow")] #[error("Amount Overflow")]
AmountOverflow, AmountOverflow,
/// Invalid Amount
#[error("Invalid Request")]
InvalidAmountRequest,
/// Unsupported unit
#[error("Unsupported unit")]
UnsupportedUnit,
} }
/// Melt quote request [NUT-05] /// Melt quote request [NUT-05]
@@ -40,7 +46,63 @@ pub struct MeltQuoteBolt11Request {
/// Unit wallet would like to pay with /// Unit wallet would like to pay with
pub unit: CurrencyUnit, pub unit: CurrencyUnit,
/// Payment Options /// Payment Options
pub options: Option<Mpp>, pub options: Option<MeltOptions>,
}
/// Melt Options
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
pub enum MeltOptions {
/// Mpp Options
Mpp {
/// MPP
mpp: Mpp,
},
}
impl MeltOptions {
/// Create new [`Options::Mpp`]
pub fn new_mpp<A>(amount: A) -> Self
where
A: Into<Amount>,
{
Self::Mpp {
mpp: Mpp {
amount: amount.into(),
},
}
}
/// Payment amount
pub fn amount_msat(&self) -> Amount {
match self {
Self::Mpp { mpp } => mpp.amount,
}
}
}
impl MeltQuoteBolt11Request {
/// Amount from [`MeltQuoteBolt11Request`]
///
/// Amount can either be defined in the bolt11 invoice,
/// in the request for an amountless bolt11 or in MPP option.
pub fn amount_msat(&self) -> Result<Amount, Error> {
let MeltQuoteBolt11Request {
request,
unit: _,
options,
..
} = self;
match options {
None => Ok(request
.amount_milli_satoshis()
.ok_or(Error::InvalidAmountRequest)?
.into()),
Some(MeltOptions::Mpp { mpp }) => Ok(mpp.amount),
}
}
} }
/// Possible states of a quote /// Possible states of a quote

View File

@@ -40,6 +40,7 @@ impl Melted {
Some(change_proofs) => change_proofs.total_amount()?, Some(change_proofs) => change_proofs.total_amount()?,
None => Amount::ZERO, None => Amount::ZERO,
}; };
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)?;

View File

@@ -85,7 +85,7 @@ where
for i in (0..len).step_by(2) { for i in (0..len).step_by(2) {
let high = val(hex[i], i)?; let high = val(hex[i], i)?;
let low = val(hex[i + 1], i + 1)?; let low = val(hex[i + 1], i + 1)?;
bytes.push(high << 4 | low); bytes.push((high << 4) | low);
} }
Ok(bytes) Ok(bytes)

View File

@@ -4,15 +4,15 @@ use lightning_invoice::Bolt11Invoice;
use tracing::instrument; use tracing::instrument;
use super::MeltQuote; use super::MeltQuote;
use crate::amount::to_unit;
use crate::dhke::construct_proofs; use crate::dhke::construct_proofs;
use crate::nuts::nut00::ProofsMethods;
use crate::nuts::{ use crate::nuts::{
CurrencyUnit, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, Mpp, CurrencyUnit, MeltBolt11Request, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
PreMintSecrets, Proofs, State, PreMintSecrets, Proofs, ProofsMethods, State,
}; };
use crate::types::{Melted, ProofInfo}; use crate::types::{Melted, ProofInfo};
use crate::util::unix_time; use crate::util::unix_time;
use crate::{Amount, Error, Wallet}; use crate::{Error, Wallet};
impl Wallet { impl Wallet {
/// Melt Quote /// Melt Quote
@@ -43,21 +43,16 @@ impl Wallet {
pub async fn melt_quote( pub async fn melt_quote(
&self, &self,
request: String, request: String,
mpp: Option<Amount>, options: Option<MeltOptions>,
) -> Result<MeltQuote, Error> { ) -> Result<MeltQuote, Error> {
let invoice = Bolt11Invoice::from_str(&request)?; let invoice = Bolt11Invoice::from_str(&request)?;
let request_amount = invoice let amount_msat = options
.amount_milli_satoshis() .map(|opt| opt.amount_msat().into())
.or_else(|| invoice.amount_milli_satoshis())
.ok_or(Error::InvoiceAmountUndefined)?; .ok_or(Error::InvoiceAmountUndefined)?;
let amount = match self.unit { let amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit).unwrap();
CurrencyUnit::Sat => Amount::from(request_amount / 1000),
CurrencyUnit::Msat => Amount::from(request_amount),
_ => return Err(Error::UnitUnsupported),
};
let options = mpp.map(|amount| Mpp { amount });
let quote_request = MeltQuoteBolt11Request { let quote_request = MeltQuoteBolt11Request {
request: Bolt11Invoice::from_str(&request)?, request: Bolt11Invoice::from_str(&request)?,
@@ -67,13 +62,18 @@ impl Wallet {
let quote_res = self.client.post_melt_quote(quote_request).await?; let quote_res = self.client.post_melt_quote(quote_request).await?;
if quote_res.amount != amount { if quote_res.amount != amount_quote_unit {
tracing::warn!(
"Mint returned incorrect quote amount. Expected {}, got {}",
amount_quote_unit,
quote_res.amount
);
return Err(Error::IncorrectQuoteAmount); return Err(Error::IncorrectQuoteAmount);
} }
let quote = MeltQuote { let quote = MeltQuote {
id: quote_res.quote, id: quote_res.quote,
amount, amount: amount_quote_unit,
request, request,
unit: self.unit.clone(), unit: self.unit.clone(),
fee_reserve: quote_res.fee_reserve, fee_reserve: quote_res.fee_reserve,

View File

@@ -16,7 +16,7 @@ use super::types::SendKind;
use super::Error; use super::Error;
use crate::amount::SplitTarget; use crate::amount::SplitTarget;
use crate::mint_url::MintUrl; use crate::mint_url::MintUrl;
use crate::nuts::{CurrencyUnit, Proof, Proofs, SecretKey, SpendingConditions, Token}; use crate::nuts::{CurrencyUnit, MeltOptions, Proof, Proofs, SecretKey, SpendingConditions, Token};
use crate::types::Melted; use crate::types::Melted;
use crate::wallet::types::MintQuote; use crate::wallet::types::MintQuote;
use crate::{Amount, Wallet}; use crate::{Amount, Wallet};
@@ -281,6 +281,7 @@ impl MultiMintWallet {
pub async fn pay_invoice_for_wallet( pub async fn pay_invoice_for_wallet(
&self, &self,
bolt11: &str, bolt11: &str,
options: Option<MeltOptions>,
wallet_key: &WalletKey, wallet_key: &WalletKey,
max_fee: Option<Amount>, max_fee: Option<Amount>,
) -> Result<Melted, Error> { ) -> Result<Melted, Error> {
@@ -289,7 +290,7 @@ impl MultiMintWallet {
.await .await
.ok_or(Error::UnknownWallet(wallet_key.clone()))?; .ok_or(Error::UnknownWallet(wallet_key.clone()))?;
let quote = wallet.melt_quote(bolt11.to_string(), None).await?; let quote = wallet.melt_quote(bolt11.to_string(), options).await?;
if let Some(max_fee) = max_fee { if let Some(max_fee) = max_fee {
if quote.fee_reserve > max_fee { if quote.fee_reserve > max_fee {
return Err(Error::MaxFeeExceeded); return Err(Error::MaxFeeExceeded);