mirror of
https://github.com/aljazceru/cdk.git
synced 2025-12-22 23:25:22 +01:00
[NUT-15] LND Support for MPP Payments (#536)
--------- Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
@@ -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))]
|
||||
|
||||
Reference in New Issue
Block a user