1#![doc = include_str!("../README.md")]
6#![warn(missing_docs)]
7#![warn(rustdoc::bare_urls)]
8
9use std::cmp::max;
10use std::path::PathBuf;
11use std::pin::Pin;
12use std::str::FromStr;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15
16use anyhow::anyhow;
17use async_trait::async_trait;
18use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
19use cdk_common::bitcoin::hashes::Hash;
20use cdk_common::common::FeeReserve;
21use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
22use cdk_common::payment::{
23 self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
24 MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
25 PaymentQuoteResponse, WaitPaymentResponse,
26};
27use cdk_common::util::hex;
28use cdk_common::Bolt11Invoice;
29use error::Error;
30use futures::{Stream, StreamExt};
31use lnrpc::fee_limit::Limit;
32use lnrpc::payment::PaymentStatus;
33use lnrpc::{FeeLimit, Hop, MppRecord};
34use tokio_util::sync::CancellationToken;
35use tracing::instrument;
36
37mod client;
38pub mod error;
39
40mod proto;
41pub(crate) use proto::{lnrpc, routerrpc};
42
43use crate::lnrpc::invoice::InvoiceState;
44
45#[derive(Clone)]
47pub struct Lnd {
48 _address: String,
49 _cert_file: PathBuf,
50 _macaroon_file: PathBuf,
51 lnd_client: client::Client,
52 fee_reserve: FeeReserve,
53 wait_invoice_cancel_token: CancellationToken,
54 wait_invoice_is_active: Arc<AtomicBool>,
55 settings: Bolt11Settings,
56}
57
58impl Lnd {
59 pub const MAX_ROUTE_RETRIES: usize = 50;
61
62 pub async fn new(
64 address: String,
65 cert_file: PathBuf,
66 macaroon_file: PathBuf,
67 fee_reserve: FeeReserve,
68 ) -> Result<Self, Error> {
69 if address.is_empty() {
71 return Err(Error::InvalidConfig("LND address cannot be empty".into()));
72 }
73
74 if !cert_file.exists() || cert_file.metadata().map(|m| m.len() == 0).unwrap_or(true) {
76 return Err(Error::InvalidConfig(format!(
77 "LND certificate file not found or empty: {cert_file:?}"
78 )));
79 }
80
81 if !macaroon_file.exists()
83 || macaroon_file
84 .metadata()
85 .map(|m| m.len() == 0)
86 .unwrap_or(true)
87 {
88 return Err(Error::InvalidConfig(format!(
89 "LND macaroon file not found or empty: {macaroon_file:?}"
90 )));
91 }
92
93 let lnd_client = client::connect(&address, &cert_file, &macaroon_file)
94 .await
95 .map_err(|err| {
96 tracing::error!("Connection error: {}", err.to_string());
97 Error::Connection
98 })
99 .unwrap();
100
101 Ok(Self {
102 _address: address,
103 _cert_file: cert_file,
104 _macaroon_file: macaroon_file,
105 lnd_client,
106 fee_reserve,
107 wait_invoice_cancel_token: CancellationToken::new(),
108 wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
109 settings: Bolt11Settings {
110 mpp: true,
111 unit: CurrencyUnit::Msat,
112 invoice_description: true,
113 amountless: true,
114 bolt12: false,
115 },
116 })
117 }
118}
119
120#[async_trait]
121impl MintPayment for Lnd {
122 type Err = payment::Error;
123
124 #[instrument(skip_all)]
125 async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
126 Ok(serde_json::to_value(&self.settings)?)
127 }
128
129 #[instrument(skip_all)]
130 fn is_wait_invoice_active(&self) -> bool {
131 self.wait_invoice_is_active.load(Ordering::SeqCst)
132 }
133
134 #[instrument(skip_all)]
135 fn cancel_wait_invoice(&self) {
136 self.wait_invoice_cancel_token.cancel()
137 }
138
139 #[instrument(skip_all)]
140 async fn wait_any_incoming_payment(
141 &self,
142 ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
143 let mut lnd_client = self.lnd_client.clone();
144
145 let stream_req = lnrpc::InvoiceSubscription {
146 add_index: 0,
147 settle_index: 0,
148 };
149
150 let stream = lnd_client
151 .lightning()
152 .subscribe_invoices(stream_req)
153 .await
154 .map_err(|_err| {
155 tracing::error!("Could not subscribe to invoice");
156 Error::Connection
157 })?
158 .into_inner();
159
160 let cancel_token = self.wait_invoice_cancel_token.clone();
161
162 Ok(futures::stream::unfold(
163 (
164 stream,
165 cancel_token,
166 Arc::clone(&self.wait_invoice_is_active),
167 ),
168 |(mut stream, cancel_token, is_active)| async move {
169 is_active.store(true, Ordering::SeqCst);
170
171 tokio::select! {
172 _ = cancel_token.cancelled() => {
173 is_active.store(false, Ordering::SeqCst);
175 tracing::info!("Waiting for lnd invoice ending");
176 None
177
178 }
179 msg = stream.message() => {
180
181 match msg {
182 Ok(Some(msg)) => {
183 if msg.state() == InvoiceState::Settled {
184
185 let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
186
187 if let Ok(hash_slice) = hash_slice {
188 let hash = hex::encode(hash_slice);
189
190 tracing::info!("LND: Processing payment with hash: {}", hash);
191 let wait_response = WaitPaymentResponse {
192 payment_identifier: PaymentIdentifier::PaymentHash(hash_slice), payment_amount: Amount::from(msg.amt_paid_msat as u64),
193 unit: CurrencyUnit::Msat,
194 payment_id: hash,
195 };
196 tracing::info!("LND: Created WaitPaymentResponse with amount {} msat",
197 msg.amt_paid_msat);
198 Some((wait_response, (stream, cancel_token, is_active)))
199 } else { None }
200 } else {
201 None
202 }
203 }
204 Ok(None) => {
205 is_active.store(false, Ordering::SeqCst);
206 tracing::info!("LND invoice stream ended.");
207 None
208 }, Err(err) => {
210 is_active.store(false, Ordering::SeqCst);
211 tracing::warn!("Encountered error in LND invoice stream. Stream ending");
212 tracing::error!("{:?}", err);
213 None
214
215 }, }
217 }
218 }
219 },
220 )
221 .boxed())
222 }
223
224 #[instrument(skip_all)]
225 async fn get_payment_quote(
226 &self,
227 unit: &CurrencyUnit,
228 options: OutgoingPaymentOptions,
229 ) -> Result<PaymentQuoteResponse, Self::Err> {
230 match options {
231 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
232 let amount_msat = match bolt11_options.melt_options {
233 Some(amount) => amount.amount_msat(),
234 None => bolt11_options
235 .bolt11
236 .amount_milli_satoshis()
237 .ok_or(Error::UnknownInvoiceAmount)?
238 .into(),
239 };
240
241 let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
242
243 let relative_fee_reserve =
244 (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
245
246 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
247
248 let fee = max(relative_fee_reserve, absolute_fee_reserve);
249
250 Ok(PaymentQuoteResponse {
251 request_lookup_id: Some(PaymentIdentifier::PaymentHash(
252 *bolt11_options.bolt11.payment_hash().as_ref(),
253 )),
254 amount,
255 fee: fee.into(),
256 state: MeltQuoteState::Unpaid,
257 unit: unit.clone(),
258 })
259 }
260 OutgoingPaymentOptions::Bolt12(_) => {
261 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
262 }
263 }
264 }
265
266 #[instrument(skip_all)]
267 async fn make_payment(
268 &self,
269 _unit: &CurrencyUnit,
270 options: OutgoingPaymentOptions,
271 ) -> Result<MakePaymentResponse, Self::Err> {
272 match options {
273 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
274 let bolt11 = bolt11_options.bolt11;
275
276 let pay_state = self
277 .check_outgoing_payment(&PaymentIdentifier::PaymentHash(
278 *bolt11.payment_hash().as_ref(),
279 ))
280 .await?;
281
282 match pay_state.status {
283 MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
284 MeltQuoteState::Paid => {
285 tracing::debug!("Melt attempted on invoice already paid");
286 return Err(Self::Err::InvoiceAlreadyPaid);
287 }
288 MeltQuoteState::Pending => {
289 tracing::debug!("Melt attempted on invoice already pending");
290 return Err(Self::Err::InvoicePaymentPending);
291 }
292 }
293
294 match bolt11_options.melt_options {
296 Some(MeltOptions::Mpp { mpp }) => {
297 let amount_msat: u64 = bolt11
298 .amount_milli_satoshis()
299 .ok_or(Error::UnknownInvoiceAmount)?;
300 {
301 let partial_amount_msat = mpp.amount;
302 let invoice = bolt11;
303 let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
304
305 let pub_key = invoice.get_payee_pub_key();
307 let payer_addr = invoice.payment_secret().0.to_vec();
308 let payment_hash = invoice.payment_hash();
309
310 let mut lnd_client = self.lnd_client.clone();
311
312 for attempt in 0..Self::MAX_ROUTE_RETRIES {
313 let route_req = lnrpc::QueryRoutesRequest {
315 pub_key: hex::encode(pub_key.serialize()),
316 amt_msat: u64::from(partial_amount_msat) as i64,
317 fee_limit: max_fee.map(|f| {
318 let limit = Limit::Fixed(u64::from(f) as i64);
319 FeeLimit { limit: Some(limit) }
320 }),
321 use_mission_control: true,
322 ..Default::default()
323 };
324
325 let mut routes_response = lnd_client
327 .lightning()
328 .query_routes(route_req)
329 .await
330 .map_err(Error::LndError)?
331 .into_inner();
332
333 let last_hop: &mut Hop = routes_response.routes[0]
336 .hops
337 .last_mut()
338 .ok_or(Error::MissingLastHop)?;
339 let mpp_record = MppRecord {
340 payment_addr: payer_addr.clone(),
341 total_amt_msat: amount_msat as i64,
342 };
343 last_hop.mpp_record = Some(mpp_record);
344
345 let payment_response = lnd_client
346 .router()
347 .send_to_route_v2(routerrpc::SendToRouteRequest {
348 payment_hash: payment_hash.to_byte_array().to_vec(),
349 route: Some(routes_response.routes[0].clone()),
350 ..Default::default()
351 })
352 .await
353 .map_err(Error::LndError)?
354 .into_inner();
355
356 if let Some(failure) = payment_response.failure {
357 if failure.code == 15 {
358 tracing::debug!(
359 "Attempt number {}: route has failed. Re-querying...",
360 attempt + 1
361 );
362 continue;
363 }
364 }
365
366 let (status, payment_preimage) = match payment_response.status {
368 0 => (MeltQuoteState::Pending, None),
369 1 => (
370 MeltQuoteState::Paid,
371 Some(hex::encode(payment_response.preimage)),
372 ),
373 2 => (MeltQuoteState::Unpaid, None),
374 _ => (MeltQuoteState::Unknown, None),
375 };
376
377 let mut total_amt: u64 = 0;
379 if let Some(route) = payment_response.route {
380 total_amt = (route.total_amt_msat / 1000) as u64;
381 }
382
383 return Ok(MakePaymentResponse {
384 payment_lookup_id: PaymentIdentifier::PaymentHash(
385 payment_hash.to_byte_array(),
386 ),
387 payment_proof: payment_preimage,
388 status,
389 total_spent: total_amt.into(),
390 unit: CurrencyUnit::Sat,
391 });
392 }
393
394 tracing::error!("Limit of retries reached, payment couldn't succeed.");
397 Err(Error::PaymentFailed.into())
398 }
399 }
400 _ => {
401 let mut lnd_client = self.lnd_client.clone();
402
403 let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
404
405 let amount_msat = u64::from(
406 bolt11_options
407 .melt_options
408 .map(|a| a.amount_msat())
409 .unwrap_or_default(),
410 );
411
412 let pay_req = lnrpc::SendRequest {
413 payment_request: bolt11.to_string(),
414 fee_limit: max_fee.map(|f| {
415 let limit = Limit::Fixed(u64::from(f) as i64);
416 FeeLimit { limit: Some(limit) }
417 }),
418 amt_msat: amount_msat as i64,
419 ..Default::default()
420 };
421
422 let payment_response = lnd_client
423 .lightning()
424 .send_payment_sync(tonic::Request::new(pay_req))
425 .await
426 .map_err(|err| {
427 tracing::warn!("Lightning payment failed: {}", err);
428 Error::PaymentFailed
429 })?
430 .into_inner();
431
432 let total_amount = payment_response
433 .payment_route
434 .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
435 as u64;
436
437 let (status, payment_preimage) = match total_amount == 0 {
438 true => (MeltQuoteState::Unpaid, None),
439 false => (
440 MeltQuoteState::Paid,
441 Some(hex::encode(payment_response.payment_preimage)),
442 ),
443 };
444
445 let payment_identifier =
446 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
447
448 Ok(MakePaymentResponse {
449 payment_lookup_id: payment_identifier,
450 payment_proof: payment_preimage,
451 status,
452 total_spent: total_amount.into(),
453 unit: CurrencyUnit::Sat,
454 })
455 }
456 }
457 }
458 OutgoingPaymentOptions::Bolt12(_) => {
459 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
460 }
461 }
462 }
463
464 #[instrument(skip(self, options))]
465 async fn create_incoming_payment_request(
466 &self,
467 unit: &CurrencyUnit,
468 options: IncomingPaymentOptions,
469 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
470 match options {
471 IncomingPaymentOptions::Bolt11(bolt11_options) => {
472 let description = bolt11_options.description.unwrap_or_default();
473 let amount = bolt11_options.amount;
474 let unix_expiry = bolt11_options.unix_expiry;
475
476 let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
477
478 let invoice_request = lnrpc::Invoice {
479 value_msat: u64::from(amount_msat) as i64,
480 memo: description,
481 ..Default::default()
482 };
483
484 let mut lnd_client = self.lnd_client.clone();
485
486 let invoice = lnd_client
487 .lightning()
488 .add_invoice(tonic::Request::new(invoice_request))
489 .await
490 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
491 .into_inner();
492
493 let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
494
495 let payment_identifier =
496 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
497
498 Ok(CreateIncomingPaymentResponse {
499 request_lookup_id: payment_identifier,
500 request: bolt11.to_string(),
501 expiry: unix_expiry,
502 })
503 }
504 IncomingPaymentOptions::Bolt12(_) => {
505 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
506 }
507 }
508 }
509
510 #[instrument(skip(self))]
511 async fn check_incoming_payment_status(
512 &self,
513 payment_identifier: &PaymentIdentifier,
514 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
515 let mut lnd_client = self.lnd_client.clone();
516
517 let invoice_request = lnrpc::PaymentHash {
518 r_hash: hex::decode(payment_identifier.to_string()).unwrap(),
519 ..Default::default()
520 };
521
522 let invoice = lnd_client
523 .lightning()
524 .lookup_invoice(tonic::Request::new(invoice_request))
525 .await
526 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
527 .into_inner();
528
529 if invoice.state() == InvoiceState::Settled {
530 Ok(vec![WaitPaymentResponse {
531 payment_identifier: payment_identifier.clone(),
532 payment_amount: Amount::from(invoice.amt_paid_msat as u64),
533 unit: CurrencyUnit::Msat,
534 payment_id: hex::encode(invoice.r_hash),
535 }])
536 } else {
537 Ok(vec![])
538 }
539 }
540
541 #[instrument(skip(self))]
542 async fn check_outgoing_payment(
543 &self,
544 payment_identifier: &PaymentIdentifier,
545 ) -> Result<MakePaymentResponse, Self::Err> {
546 let mut lnd_client = self.lnd_client.clone();
547
548 let payment_hash = &payment_identifier.to_string();
549
550 let track_request = routerrpc::TrackPaymentRequest {
551 payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
552 no_inflight_updates: true,
553 };
554
555 let payment_response = lnd_client.router().track_payment_v2(track_request).await;
556
557 let mut payment_stream = match payment_response {
558 Ok(stream) => stream.into_inner(),
559 Err(err) => {
560 let err_code = err.code();
561 if err_code == tonic::Code::NotFound {
562 return Ok(MakePaymentResponse {
563 payment_lookup_id: payment_identifier.clone(),
564 payment_proof: None,
565 status: MeltQuoteState::Unknown,
566 total_spent: Amount::ZERO,
567 unit: self.settings.unit.clone(),
568 });
569 } else {
570 return Err(payment::Error::UnknownPaymentState);
571 }
572 }
573 };
574
575 while let Some(update_result) = payment_stream.next().await {
576 match update_result {
577 Ok(update) => {
578 let status = update.status();
579
580 let response = match status {
581 PaymentStatus::Unknown => MakePaymentResponse {
582 payment_lookup_id: payment_identifier.clone(),
583 payment_proof: Some(update.payment_preimage),
584 status: MeltQuoteState::Unknown,
585 total_spent: Amount::ZERO,
586 unit: self.settings.unit.clone(),
587 },
588 PaymentStatus::InFlight | PaymentStatus::Initiated => {
589 continue;
591 }
592 PaymentStatus::Succeeded => MakePaymentResponse {
593 payment_lookup_id: payment_identifier.clone(),
594 payment_proof: Some(update.payment_preimage),
595 status: MeltQuoteState::Paid,
596 total_spent: Amount::from(
597 (update
598 .value_sat
599 .checked_add(update.fee_sat)
600 .ok_or(Error::AmountOverflow)?)
601 as u64,
602 ),
603 unit: CurrencyUnit::Sat,
604 },
605 PaymentStatus::Failed => MakePaymentResponse {
606 payment_lookup_id: payment_identifier.clone(),
607 payment_proof: Some(update.payment_preimage),
608 status: MeltQuoteState::Failed,
609 total_spent: Amount::ZERO,
610 unit: self.settings.unit.clone(),
611 },
612 };
613
614 return Ok(response);
615 }
616 Err(_) => {
617 return Err(Error::UnknownPaymentStatus.into());
619 }
620 }
621 }
622
623 Err(Error::UnknownPaymentStatus.into())
625 }
626}