[NUT-15] LND Support for MPP Payments (#536)

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
lollerfirst
2025-02-06 10:30:48 +01:00
committed by GitHub
parent dcb9ab3140
commit f2e1940cc7
8 changed files with 317 additions and 97 deletions

View File

@@ -19,12 +19,13 @@ use cdk::cdk_lightning::{
};
use cdk::mint::FeeReserve;
use cdk::nuts::{CurrencyUnit, MeltQuoteBolt11Request, MeltQuoteState, MintQuoteState};
use cdk::secp256k1::hashes::Hash;
use cdk::util::{hex, unix_time};
use cdk::{mint, Bolt11Invoice};
use error::Error;
use fedimint_tonic_lnd::lnrpc::fee_limit::Limit;
use fedimint_tonic_lnd::lnrpc::payment::PaymentStatus;
use fedimint_tonic_lnd::lnrpc::FeeLimit;
use fedimint_tonic_lnd::lnrpc::{FeeLimit, Hop, HtlcAttempt, MppRecord};
use fedimint_tonic_lnd::tonic::Code;
use fedimint_tonic_lnd::Client;
use futures::{Stream, StreamExt};
@@ -80,7 +81,7 @@ impl MintLightning for Lnd {
#[instrument(skip_all)]
fn get_settings(&self) -> Settings {
Settings {
mpp: false,
mpp: true,
unit: CurrencyUnit::Msat,
invoice_description: true,
}
@@ -200,7 +201,7 @@ 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;
@@ -222,60 +223,159 @@ impl MintLightning for Lnd {
}
}
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 bolt11 = Bolt11Invoice::from_str(&payment_request)?;
let amount_msat: u64 = match bolt11.amount_milli_satoshis() {
Some(amount_msat) => amount_msat,
None => melt_quote
.msat_to_pay
.ok_or(Error::UnknownInvoiceAmount)?
.into(),
};
// Detect partial payments
match partial_amount {
Some(part_amt) => {
let partial_amount_msat = to_unit(part_amt, &melt_quote.unit, &CurrencyUnit::Msat)?;
let invoice = Bolt11Invoice::from_str(&payment_request)?;
// Extract information from invoice
let pub_key = invoice.get_payee_pub_key();
let payer_addr = invoice.payment_secret().0.to_vec();
let payment_hash = invoice.payment_hash();
// Create a request for the routes
let route_req = fedimint_tonic_lnd::lnrpc::QueryRoutesRequest {
pub_key: hex::encode(pub_key.serialize()),
amt_msat: u64::from(partial_amount_msat) as i64,
fee_limit: max_fee.map(|f| {
let limit = Limit::Fixed(u64::from(f) as i64);
FeeLimit { limit: Some(limit) }
}),
..Default::default()
};
// Query the routes
let routes_response: fedimint_tonic_lnd::lnrpc::QueryRoutesResponse = self
.client
.lock()
.await
.lightning()
.query_routes(route_req)
.await
.map_err(Error::LndError)?
.into_inner();
let mut payment_response: HtlcAttempt = HtlcAttempt {
..Default::default()
};
// For each route:
// update its MPP record,
// attempt it and check the result
for mut route in routes_response.routes.into_iter() {
let last_hop: &mut Hop = route.hops.last_mut().ok_or(Error::MissingLastHop)?;
let mpp_record = MppRecord {
payment_addr: payer_addr.clone(),
total_amt_msat: amount_msat as i64,
};
last_hop.mpp_record = Some(mpp_record);
tracing::debug!("sendToRouteV2 needle");
payment_response = self
.client
.lock()
.await
.router()
.send_to_route_v2(fedimint_tonic_lnd::routerrpc::SendToRouteRequest {
payment_hash: payment_hash.to_byte_array().to_vec(),
route: Some(route),
..Default::default()
})
.await
.map_err(Error::LndError)?
.into_inner();
if let Some(failure) = payment_response.failure {
if failure.code == 15 {
// Try a different route
continue;
}
} else {
break;
}
}
// Get status and maybe the preimage
let (status, payment_preimage) = match payment_response.status {
0 => (MeltQuoteState::Pending, None),
1 => (
MeltQuoteState::Paid,
Some(hex::encode(payment_response.preimage)),
),
2 => (MeltQuoteState::Unpaid, None),
_ => (MeltQuoteState::Unknown, None),
};
// Get the actual amount paid in sats
let mut total_amt: u64 = 0;
if let Some(route) = payment_response.route {
total_amt = (route.total_amt_msat / 1000) as u64;
}
Ok(PayInvoiceResponse {
payment_lookup_id: hex::encode(payment_hash),
payment_preimage,
status,
total_spent: total_amt.into(),
unit: CurrencyUnit::Sat,
})
}
};
None => {
let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
payment_request,
fee_limit: max_fee.map(|f| {
let limit = Limit::Fixed(u64::from(f) as i64);
let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
payment_request,
fee_limit: max_fee.map(|f| {
let limit = Limit::Fixed(u64::from(f) as i64);
FeeLimit { limit: Some(limit) }
}),
amt_msat: amount_msat as i64,
..Default::default()
};
FeeLimit { limit: Some(limit) }
}),
amt_msat: amount_msat as i64,
..Default::default()
};
let payment_response = self
.client
.lock()
.await
.lightning()
.send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req))
.await
.map_err(|err| {
tracing::warn!("Lightning payment failed: {}", err);
Error::PaymentFailed
})?
.into_inner();
let payment_response = self
.client
.lock()
.await
.lightning()
.send_payment_sync(fedimint_tonic_lnd::tonic::Request::new(pay_req))
.await
.map_err(|err| {
tracing::warn!("Lightning payment failed: {}", err);
Error::PaymentFailed
})?
.into_inner();
let total_amount = payment_response
.payment_route
.map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
as u64;
let total_amount = payment_response
.payment_route
.map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
as u64;
let (status, payment_preimage) = match total_amount == 0 {
true => (MeltQuoteState::Unpaid, None),
false => (
MeltQuoteState::Paid,
Some(hex::encode(payment_response.payment_preimage)),
),
};
let (status, payment_preimage) = match total_amount == 0 {
true => (MeltQuoteState::Unpaid, None),
false => (
MeltQuoteState::Paid,
Some(hex::encode(payment_response.payment_preimage)),
),
};
Ok(PayInvoiceResponse {
payment_lookup_id: hex::encode(payment_response.payment_hash),
payment_preimage,
status,
total_spent: total_amount.into(),
unit: CurrencyUnit::Sat,
})
Ok(PayInvoiceResponse {
payment_lookup_id: hex::encode(payment_response.payment_hash),
payment_preimage,
status,
total_spent: total_amount.into(),
unit: CurrencyUnit::Sat,
})
}
}
}
#[instrument(skip(self, description))]