mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-23 15:44:50 +01:00
fix: mpp (#523)
This commit is contained in:
@@ -3,7 +3,8 @@ use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
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::Bolt11Invoice;
|
||||
use clap::Args;
|
||||
@@ -15,6 +16,9 @@ pub struct MeltSubCommand {
|
||||
/// Currency unit e.g. sat
|
||||
#[arg(default_value = "sat")]
|
||||
unit: String,
|
||||
/// Mpp
|
||||
#[arg(short, long)]
|
||||
mpp: bool,
|
||||
}
|
||||
|
||||
pub async fn pay(
|
||||
@@ -52,14 +56,33 @@ pub async fn pay(
|
||||
stdin.read_line(&mut user_input)?;
|
||||
let bolt11 = Bolt11Invoice::from_str(user_input.trim())?;
|
||||
|
||||
if bolt11
|
||||
.amount_milli_satoshis()
|
||||
.unwrap()
|
||||
.gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * 1000_u64))
|
||||
let mut options: Option<MeltOptions> = None;
|
||||
|
||||
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)?;
|
||||
|
||||
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");
|
||||
}
|
||||
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);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use cdk::amount::{to_unit, Amount};
|
||||
use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
|
||||
use cdk::cdk_lightning::{
|
||||
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
|
||||
};
|
||||
@@ -196,16 +196,9 @@ impl MintLightning for Cln {
|
||||
&self,
|
||||
melt_quote_request: &MeltQuoteBolt11Request,
|
||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||
let invoice_amount_msat = melt_quote_request
|
||||
.request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::UnknownInvoiceAmount)?;
|
||||
let amount = melt_quote_request.amount_msat()?;
|
||||
|
||||
let amount = to_unit(
|
||||
invoice_amount_msat,
|
||||
&CurrencyUnit::Msat,
|
||||
&melt_quote_request.unit,
|
||||
)?;
|
||||
let amount = amount / MSAT_IN_SAT.into();
|
||||
|
||||
let relative_fee_reserve =
|
||||
(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 cln_response = cln_client
|
||||
.call(Request::Pay(PayRequest {
|
||||
bolt11: melt_quote.request.to_string(),
|
||||
amount_msat: None,
|
||||
amount_msat,
|
||||
label: None,
|
||||
riskfactor: None,
|
||||
maxfeepercent: None,
|
||||
@@ -264,9 +261,7 @@ impl MintLightning for Cln {
|
||||
maxfee: max_fee
|
||||
.map(|a| {
|
||||
let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat)?;
|
||||
Ok::<cln_rpc::primitives::Amount, Self::Err>(CLN_Amount::from_msat(
|
||||
msat.into(),
|
||||
))
|
||||
Ok::<CLN_Amount, Self::Err>(CLN_Amount::from_msat(msat.into()))
|
||||
})
|
||||
.transpose()?,
|
||||
description: None,
|
||||
@@ -289,6 +284,7 @@ impl MintLightning for Cln {
|
||||
PayStatus::PENDING => MeltQuoteState::Pending,
|
||||
PayStatus::FAILED => MeltQuoteState::Failed,
|
||||
};
|
||||
|
||||
PayInvoiceResponse {
|
||||
payment_preimage: Some(hex::encode(pay_response.payment_preimage.to_vec())),
|
||||
payment_lookup_id: pay_response.payment_hash.to_string(),
|
||||
@@ -301,6 +297,10 @@ impl MintLightning for Cln {
|
||||
unit: melt_quote.unit,
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Could not pay invoice: {}", err);
|
||||
return Err(Error::ClnRpc(err).into());
|
||||
}
|
||||
_ => {
|
||||
tracing::error!(
|
||||
"Error attempting to pay invoice: {}",
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::secp256k1::{Secp256k1, SecretKey};
|
||||
use cdk::amount::{to_unit, Amount};
|
||||
use cdk::amount::{to_unit, Amount, MSAT_IN_SAT};
|
||||
use cdk::cdk_lightning::{
|
||||
self, CreateInvoiceResponse, MintLightning, PayInvoiceResponse, PaymentQuoteResponse, Settings,
|
||||
};
|
||||
@@ -127,16 +127,9 @@ impl MintLightning for FakeWallet {
|
||||
&self,
|
||||
melt_quote_request: &MeltQuoteBolt11Request,
|
||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||
let invoice_amount_msat = melt_quote_request
|
||||
.request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::UnknownInvoiceAmount)?;
|
||||
let amount = melt_quote_request.amount_msat()?;
|
||||
|
||||
let amount = to_unit(
|
||||
invoice_amount_msat,
|
||||
&CurrencyUnit::Msat,
|
||||
&melt_quote_request.unit,
|
||||
)?;
|
||||
let amount = amount / MSAT_IN_SAT.into();
|
||||
|
||||
let relative_fee_reserve =
|
||||
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
|
||||
|
||||
@@ -19,7 +19,6 @@ use ln_regtest_rs::ln_client::{ClnClient, LightningClient, LndClient};
|
||||
use ln_regtest_rs::lnd::Lnd;
|
||||
use tokio::sync::Notify;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
const BITCOIND_ADDR: &str = "127.0.0.1:18443";
|
||||
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
|
||||
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_backend = create_cln_backend(&cln_client).await?;
|
||||
|
||||
@@ -8,9 +8,24 @@ use cdk_integration_tests::init_regtest::{
|
||||
};
|
||||
use cdk_redb::MintRedbDatabase;
|
||||
use cdk_sqlite::MintSqliteDatabase;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[tokio::main]
|
||||
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();
|
||||
bitcoind.start_bitcoind()?;
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ async fn test_regtest_mint_melt_round_trip() -> Result<()> {
|
||||
|
||||
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
|
||||
.mint(&mint_quote.id, SplitTarget::default(), None)
|
||||
|
||||
@@ -152,16 +152,9 @@ impl MintLightning for LNbits {
|
||||
return Err(Self::Err::Anyhow(anyhow!("Unsupported unit")));
|
||||
}
|
||||
|
||||
let invoice_amount_msat = melt_quote_request
|
||||
.request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::UnknownInvoiceAmount)?;
|
||||
let amount = melt_quote_request.amount_msat()?;
|
||||
|
||||
let amount = to_unit(
|
||||
invoice_amount_msat,
|
||||
&CurrencyUnit::Msat,
|
||||
&melt_quote_request.unit,
|
||||
)?;
|
||||
let amount = amount / MSAT_IN_SAT.into();
|
||||
|
||||
let relative_fee_reserve =
|
||||
(self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
|
||||
|
||||
@@ -77,7 +77,7 @@ impl MintLightning for Lnd {
|
||||
|
||||
fn get_settings(&self) -> Settings {
|
||||
Settings {
|
||||
mpp: true,
|
||||
mpp: false,
|
||||
unit: CurrencyUnit::Msat,
|
||||
invoice_description: true,
|
||||
}
|
||||
@@ -164,16 +164,9 @@ impl MintLightning for Lnd {
|
||||
&self,
|
||||
melt_quote_request: &MeltQuoteBolt11Request,
|
||||
) -> Result<PaymentQuoteResponse, Self::Err> {
|
||||
let invoice_amount_msat = melt_quote_request
|
||||
.request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::UnknownInvoiceAmount)?;
|
||||
let amount = melt_quote_request.amount_msat()?;
|
||||
|
||||
let amount = to_unit(
|
||||
invoice_amount_msat,
|
||||
&CurrencyUnit::Msat,
|
||||
&melt_quote_request.unit,
|
||||
)?;
|
||||
let amount = amount / MSAT_IN_SAT.into();
|
||||
|
||||
let relative_fee_reserve =
|
||||
(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(
|
||||
&self,
|
||||
melt_quote: mint::MeltQuote,
|
||||
partial_amount: Option<Amount>,
|
||||
_partial_amount: Option<Amount>,
|
||||
max_fee: Option<Amount>,
|
||||
) -> Result<PayInvoiceResponse, Self::Err> {
|
||||
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 {
|
||||
payment_request,
|
||||
fee_limit: max_fee.map(|f| {
|
||||
@@ -208,13 +211,7 @@ impl MintLightning for Lnd {
|
||||
|
||||
FeeLimit { limit: Some(limit) }
|
||||
}),
|
||||
amt_msat: partial_amount
|
||||
.map(|a| {
|
||||
let msat = to_unit(a, &melt_quote.unit, &CurrencyUnit::Msat).unwrap();
|
||||
|
||||
u64::from(msat) as i64
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
amt_msat: amount_msat as i64,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ impl MintLightning for Phoenixd {
|
||||
invoice_description: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_wait_invoice_active(&self) -> bool {
|
||||
self.wait_invoice_is_active.load(Ordering::SeqCst)
|
||||
}
|
||||
@@ -162,16 +161,9 @@ impl MintLightning for Phoenixd {
|
||||
return Err(Error::UnsupportedUnit.into());
|
||||
}
|
||||
|
||||
let invoice_amount_msat = melt_quote_request
|
||||
.request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::UnknownInvoiceAmount)?;
|
||||
let amount = melt_quote_request.amount_msat()?;
|
||||
|
||||
let amount = to_unit(
|
||||
invoice_amount_msat,
|
||||
&CurrencyUnit::Msat,
|
||||
&melt_quote_request.unit,
|
||||
)?;
|
||||
let amount = amount / MSAT_IN_SAT.into();
|
||||
|
||||
let relative_fee_reserve =
|
||||
(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(
|
||||
&self,
|
||||
melt_quote: mint::MeltQuote,
|
||||
partial_amount: Option<Amount>,
|
||||
_partial_amount: Option<Amount>,
|
||||
_max_fee_msats: Option<Amount>,
|
||||
) -> 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
|
||||
.phoenixd_api
|
||||
.pay_bolt11_invoice(&melt_quote.request, partial_amount.map(|a| a.into()))
|
||||
.pay_bolt11_invoice(&melt_quote.request, msat_to_pay)
|
||||
.await?;
|
||||
|
||||
// The pay invoice response does not give the needed fee info so we have to check.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE melt_quote ADD COLUMN msat_to_pay INTEGER;
|
||||
@@ -468,8 +468,8 @@ WHERE id=?
|
||||
let res = sqlx::query(
|
||||
r#"
|
||||
INSERT OR REPLACE INTO melt_quote
|
||||
(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
(id, unit, amount, request, fee_reserve, state, expiry, payment_preimage, request_lookup_id, msat_to_pay)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
)
|
||||
.bind(quote.id.to_string())
|
||||
@@ -481,6 +481,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
.bind(quote.expiry as i64)
|
||||
.bind(quote.payment_preimage)
|
||||
.bind(quote.request_lookup_id)
|
||||
.bind(quote.msat_to_pay.map(|a| u64::from(a) as i64))
|
||||
.execute(&mut transaction)
|
||||
.await;
|
||||
|
||||
@@ -804,11 +805,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||
.map(|row| {
|
||||
PublicKey::from_slice(row.get("y"))
|
||||
.map_err(Error::from)
|
||||
.and_then(|y| {
|
||||
sqlite_row_to_proof(row)
|
||||
.map_err(Error::from)
|
||||
.map(|proof| (y, proof))
|
||||
})
|
||||
.and_then(|y| sqlite_row_to_proof(row).map(|proof| (y, proof)))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
|
||||
@@ -1060,11 +1057,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||
.map(|row| {
|
||||
PublicKey::from_slice(row.get("y"))
|
||||
.map_err(Error::from)
|
||||
.and_then(|y| {
|
||||
sqlite_row_to_blind_signature(row)
|
||||
.map_err(Error::from)
|
||||
.map(|blinded| (y, blinded))
|
||||
})
|
||||
.and_then(|y| sqlite_row_to_blind_signature(row).map(|blinded| (y, blinded)))
|
||||
})
|
||||
.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 row_msat_to_pay: Option<i64> = row.try_get("msat_to_pay").map_err(Error::from)?;
|
||||
|
||||
Ok(mint::MeltQuote {
|
||||
id: row_id.into_uuid(),
|
||||
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,
|
||||
payment_preimage: row_preimage,
|
||||
request_lookup_id,
|
||||
msat_to_pay: row_msat_to_pay.map(|a| Amount::from(a as u64)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,3 +22,5 @@ tracing = { version = "0.1", default-features = false, features = ["attributes",
|
||||
thiserror = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
strike-rs = "0.4.0"
|
||||
# strike-rs = { path = "../../../../strike-rs" }
|
||||
# strike-rs = { git = "https://github.com/thesimplekid/strike-rs.git", rev = "577ad9591" }
|
||||
|
||||
@@ -167,9 +167,11 @@ impl MintLightning for Strike {
|
||||
|
||||
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 {
|
||||
request_lookup_id: quote.payment_quote_id,
|
||||
amount: from_strike_amount(quote.amount, &melt_quote_request.unit)?.into(),
|
||||
amount,
|
||||
fee: fee.into(),
|
||||
state: MeltQuoteState::Unpaid,
|
||||
})
|
||||
|
||||
@@ -367,8 +367,6 @@ impl MintDatabase for MintMemoryDatabase {
|
||||
if let Some(quote_id) = quote_id {
|
||||
let mut current_quote_signatures = self.quote_signatures.write().await;
|
||||
current_quote_signatures.insert(quote_id, blind_signatures.to_vec());
|
||||
let t = current_quote_signatures.get("e_id);
|
||||
println!("after insert: {:?}", t);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -96,8 +96,7 @@ impl WalletDatabase for WalletMemoryDatabase {
|
||||
) -> Result<(), Self::Err> {
|
||||
let proofs = self
|
||||
.get_proofs(Some(old_mint_url), None, None, None)
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
.await?;
|
||||
|
||||
// Update proofs
|
||||
{
|
||||
|
||||
@@ -41,6 +41,9 @@ pub enum Error {
|
||||
/// Amount Error
|
||||
#[error(transparent)]
|
||||
Amount(#[from] crate::amount::Error),
|
||||
/// NUT05 Error
|
||||
#[error(transparent)]
|
||||
NUT05(#[from] crate::nuts::nut05::Error),
|
||||
}
|
||||
|
||||
/// MintLighting Trait
|
||||
|
||||
@@ -48,6 +48,9 @@ pub enum Error {
|
||||
/// Witness missing or invalid
|
||||
#[error("Signature missing or invalid")]
|
||||
SignatureMissingOrInvalid,
|
||||
/// Amountless Invoice Not supported
|
||||
#[error("Amount Less Invoice is not allowed")]
|
||||
AmountLessNotAllowed,
|
||||
|
||||
// Mint Errors
|
||||
/// Minting is disabled
|
||||
@@ -60,7 +63,7 @@ pub enum Error {
|
||||
#[error("Expired quote: Expired: `{0}`, Time: `{1}`")]
|
||||
ExpiredQuote(u64, u64),
|
||||
/// 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),
|
||||
/// Quote is not paiud
|
||||
#[error("Quote not paid")]
|
||||
|
||||
@@ -60,21 +60,14 @@ impl Mint {
|
||||
request,
|
||||
unit,
|
||||
options: _,
|
||||
..
|
||||
} = melt_request;
|
||||
|
||||
let amount = match melt_request.options {
|
||||
Some(mpp_amount) => mpp_amount.amount,
|
||||
None => {
|
||||
let amount_msat = request
|
||||
.amount_milli_satoshis()
|
||||
.ok_or(Error::InvoiceAmountUndefined)?;
|
||||
let amount_msats = melt_request.amount_msat()?;
|
||||
|
||||
to_unit(amount_msat, &CurrencyUnit::Msat, unit)
|
||||
.map_err(|_err| Error::UnsupportedUnit)?
|
||||
}
|
||||
};
|
||||
let amount_quote_unit = to_unit(amount_msats, &CurrencyUnit::Msat, unit)?;
|
||||
|
||||
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
|
||||
.ln
|
||||
@@ -95,6 +88,14 @@ impl Mint {
|
||||
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(
|
||||
request.to_string(),
|
||||
unit.clone(),
|
||||
@@ -102,12 +103,13 @@ impl Mint {
|
||||
payment_quote.fee,
|
||||
unix_time() + self.config.quote_ttl().melt_ttl,
|
||||
payment_quote.request_lookup_id.clone(),
|
||||
msats_to_pay,
|
||||
);
|
||||
|
||||
tracing::debug!(
|
||||
"New melt quote {} for {} {} with request id {}",
|
||||
quote.id,
|
||||
amount,
|
||||
amount_quote_unit,
|
||||
unit,
|
||||
payment_quote.request_lookup_id
|
||||
);
|
||||
@@ -182,10 +184,13 @@ impl Mint {
|
||||
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");
|
||||
|
||||
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()
|
||||
.ok_or(Error::InvoiceAmountUndefined)?
|
||||
.into();
|
||||
.into(),
|
||||
};
|
||||
|
||||
let partial_amount = match invoice_amount_msats > quote_msats {
|
||||
true => {
|
||||
@@ -582,7 +587,6 @@ impl Mint {
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Process melt request marking [`Proofs`] as spent
|
||||
/// The melt request must be verifyed using [`Self::verify_melt_request`]
|
||||
/// before calling [`Self::process_melt_request`]
|
||||
|
||||
@@ -591,10 +591,7 @@ fn create_new_keyset<C: secp256k1::Signing>(
|
||||
}
|
||||
|
||||
fn derivation_path_from_unit(unit: CurrencyUnit, index: u32) -> Option<DerivationPath> {
|
||||
let unit_index = match unit.derivation_index() {
|
||||
Some(index) => index,
|
||||
None => return None,
|
||||
};
|
||||
let unit_index = unit.derivation_index()?;
|
||||
|
||||
Some(DerivationPath::from(vec![
|
||||
ChildNumber::from_hardened_idx(0).expect("0 is a valid index"),
|
||||
|
||||
@@ -79,6 +79,10 @@ pub struct MeltQuote {
|
||||
pub payment_preimage: Option<String>,
|
||||
/// Value used by ln backend to look up state of request
|
||||
pub request_lookup_id: String,
|
||||
/// Msat to pay
|
||||
///
|
||||
/// Used for an amountless invoice
|
||||
pub msat_to_pay: Option<Amount>,
|
||||
}
|
||||
|
||||
impl MeltQuote {
|
||||
@@ -90,6 +94,7 @@ impl MeltQuote {
|
||||
fee_reserve: Amount,
|
||||
expiry: u64,
|
||||
request_lookup_id: String,
|
||||
msat_to_pay: Option<Amount>,
|
||||
) -> Self {
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
@@ -103,6 +108,7 @@ impl MeltQuote {
|
||||
expiry,
|
||||
payment_preimage: None,
|
||||
request_lookup_id,
|
||||
msat_to_pay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ pub mod nut20;
|
||||
|
||||
pub use nut00::{
|
||||
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};
|
||||
#[cfg(feature = "mint")]
|
||||
@@ -39,8 +39,8 @@ pub use nut04::{
|
||||
MintQuoteBolt11Response, QuoteState as MintQuoteState, Settings as NUT04Settings,
|
||||
};
|
||||
pub use nut05::{
|
||||
MeltBolt11Request, MeltMethodSettings, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
|
||||
QuoteState as MeltQuoteState, Settings as NUT05Settings,
|
||||
MeltBolt11Request, MeltMethodSettings, MeltOptions, MeltQuoteBolt11Request,
|
||||
MeltQuoteBolt11Response, QuoteState as MeltQuoteState, Settings as NUT05Settings,
|
||||
};
|
||||
pub use nut06::{ContactInfo, MintInfo, MintVersion, Nuts};
|
||||
pub use nut07::{CheckStateRequest, CheckStateResponse, ProofState, State};
|
||||
|
||||
@@ -47,7 +47,6 @@ impl ProofsMethods for Proofs {
|
||||
self.iter()
|
||||
.map(|p| p.y())
|
||||
.collect::<Result<Vec<PublicKey>, _>>()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,12 @@ pub enum Error {
|
||||
/// Amount overflow
|
||||
#[error("Amount Overflow")]
|
||||
AmountOverflow,
|
||||
/// Invalid Amount
|
||||
#[error("Invalid Request")]
|
||||
InvalidAmountRequest,
|
||||
/// Unsupported unit
|
||||
#[error("Unsupported unit")]
|
||||
UnsupportedUnit,
|
||||
}
|
||||
|
||||
/// Melt quote request [NUT-05]
|
||||
@@ -40,7 +46,63 @@ pub struct MeltQuoteBolt11Request {
|
||||
/// Unit wallet would like to pay with
|
||||
pub unit: CurrencyUnit,
|
||||
/// 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
|
||||
|
||||
@@ -40,6 +40,7 @@ impl Melted {
|
||||
Some(change_proofs) => change_proofs.total_amount()?,
|
||||
None => Amount::ZERO,
|
||||
};
|
||||
|
||||
let fee_paid = proofs_amount
|
||||
.checked_sub(amount + change_amount)
|
||||
.ok_or(Error::AmountOverflow)?;
|
||||
|
||||
@@ -85,7 +85,7 @@ where
|
||||
for i in (0..len).step_by(2) {
|
||||
let high = val(hex[i], i)?;
|
||||
let low = val(hex[i + 1], i + 1)?;
|
||||
bytes.push(high << 4 | low);
|
||||
bytes.push((high << 4) | low);
|
||||
}
|
||||
|
||||
Ok(bytes)
|
||||
|
||||
@@ -4,15 +4,15 @@ use lightning_invoice::Bolt11Invoice;
|
||||
use tracing::instrument;
|
||||
|
||||
use super::MeltQuote;
|
||||
use crate::amount::to_unit;
|
||||
use crate::dhke::construct_proofs;
|
||||
use crate::nuts::nut00::ProofsMethods;
|
||||
use crate::nuts::{
|
||||
CurrencyUnit, MeltBolt11Request, MeltQuoteBolt11Request, MeltQuoteBolt11Response, Mpp,
|
||||
PreMintSecrets, Proofs, State,
|
||||
CurrencyUnit, MeltBolt11Request, MeltOptions, MeltQuoteBolt11Request, MeltQuoteBolt11Response,
|
||||
PreMintSecrets, Proofs, ProofsMethods, State,
|
||||
};
|
||||
use crate::types::{Melted, ProofInfo};
|
||||
use crate::util::unix_time;
|
||||
use crate::{Amount, Error, Wallet};
|
||||
use crate::{Error, Wallet};
|
||||
|
||||
impl Wallet {
|
||||
/// Melt Quote
|
||||
@@ -43,21 +43,16 @@ impl Wallet {
|
||||
pub async fn melt_quote(
|
||||
&self,
|
||||
request: String,
|
||||
mpp: Option<Amount>,
|
||||
options: Option<MeltOptions>,
|
||||
) -> Result<MeltQuote, Error> {
|
||||
let invoice = Bolt11Invoice::from_str(&request)?;
|
||||
|
||||
let request_amount = invoice
|
||||
.amount_milli_satoshis()
|
||||
let amount_msat = options
|
||||
.map(|opt| opt.amount_msat().into())
|
||||
.or_else(|| invoice.amount_milli_satoshis())
|
||||
.ok_or(Error::InvoiceAmountUndefined)?;
|
||||
|
||||
let amount = match self.unit {
|
||||
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 amount_quote_unit = to_unit(amount_msat, &CurrencyUnit::Msat, &self.unit).unwrap();
|
||||
|
||||
let quote_request = MeltQuoteBolt11Request {
|
||||
request: Bolt11Invoice::from_str(&request)?,
|
||||
@@ -67,13 +62,18 @@ impl Wallet {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let quote = MeltQuote {
|
||||
id: quote_res.quote,
|
||||
amount,
|
||||
amount: amount_quote_unit,
|
||||
request,
|
||||
unit: self.unit.clone(),
|
||||
fee_reserve: quote_res.fee_reserve,
|
||||
|
||||
@@ -16,7 +16,7 @@ use super::types::SendKind;
|
||||
use super::Error;
|
||||
use crate::amount::SplitTarget;
|
||||
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::wallet::types::MintQuote;
|
||||
use crate::{Amount, Wallet};
|
||||
@@ -281,6 +281,7 @@ impl MultiMintWallet {
|
||||
pub async fn pay_invoice_for_wallet(
|
||||
&self,
|
||||
bolt11: &str,
|
||||
options: Option<MeltOptions>,
|
||||
wallet_key: &WalletKey,
|
||||
max_fee: Option<Amount>,
|
||||
) -> Result<Melted, Error> {
|
||||
@@ -289,7 +290,7 @@ impl MultiMintWallet {
|
||||
.await
|
||||
.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 quote.fee_reserve > max_fee {
|
||||
return Err(Error::MaxFeeExceeded);
|
||||
|
||||
Reference in New Issue
Block a user