1#![doc = include_str!("../README.md")]
6
7use std::cmp::max;
8use std::path::PathBuf;
9use std::pin::Pin;
10use std::str::FromStr;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13
14use anyhow::anyhow;
15use async_trait::async_trait;
16use cdk_common::amount::{Amount, MSAT_IN_SAT};
17use cdk_common::bitcoin::hashes::Hash;
18use cdk_common::common::FeeReserve;
19use cdk_common::database::DynKVStore;
20use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
21use cdk_common::payment::{
22 self, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse,
23 MintPayment, OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
24 WaitPaymentResponse,
25};
26use cdk_common::util::{hex, unix_time};
27use cdk_common::Bolt11Invoice;
28use error::Error;
29use futures::{Stream, StreamExt};
30use lnrpc::fee_limit::Limit;
31use lnrpc::payment::PaymentStatus;
32use lnrpc::{FeeLimit, Hop, MppRecord};
33use tokio_util::sync::CancellationToken;
34use tracing::instrument;
35
36mod client;
37pub mod error;
38
39mod proto;
40pub(crate) use proto::{lnrpc, routerrpc};
41
42use crate::lnrpc::invoice::InvoiceState;
43
44const LND_KV_PRIMARY_NAMESPACE: &str = "cdk_lnd_lightning_backend";
46const LND_KV_SECONDARY_NAMESPACE: &str = "payment_indices";
47const LAST_ADD_INDEX_KV_KEY: &str = "last_add_index";
48const LAST_SETTLE_INDEX_KV_KEY: &str = "last_settle_index";
49
50#[derive(Clone)]
52pub struct Lnd {
53 _address: String,
54 _cert_file: PathBuf,
55 _macaroon_file: PathBuf,
56 lnd_client: client::Client,
57 fee_reserve: FeeReserve,
58 kv_store: DynKVStore,
59 wait_invoice_cancel_token: CancellationToken,
60 wait_invoice_is_active: Arc<AtomicBool>,
61 settings: SettingsResponse,
62 unit: CurrencyUnit,
63}
64
65impl std::fmt::Debug for Lnd {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("Lnd")
68 .field("fee_reserve", &self.fee_reserve)
69 .finish_non_exhaustive()
70 }
71}
72
73impl Lnd {
74 pub const MAX_ROUTE_RETRIES: usize = 50;
76
77 pub async fn new(
79 address: String,
80 cert_file: PathBuf,
81 macaroon_file: PathBuf,
82 fee_reserve: FeeReserve,
83 kv_store: DynKVStore,
84 ) -> Result<Self, Error> {
85 if address.is_empty() {
87 return Err(Error::InvalidConfig("LND address cannot be empty".into()));
88 }
89
90 if !cert_file.exists() || cert_file.metadata().map(|m| m.len() == 0).unwrap_or(true) {
92 return Err(Error::InvalidConfig(format!(
93 "LND certificate file not found or empty: {cert_file:?}"
94 )));
95 }
96
97 if !macaroon_file.exists()
99 || macaroon_file
100 .metadata()
101 .map(|m| m.len() == 0)
102 .unwrap_or(true)
103 {
104 return Err(Error::InvalidConfig(format!(
105 "LND macaroon file not found or empty: {macaroon_file:?}"
106 )));
107 }
108
109 let lnd_client = client::connect(&address, &cert_file, &macaroon_file)
110 .await
111 .map_err(|err| {
112 tracing::error!("Connection error: {}", err.to_string());
113 Error::Connection
114 })?;
115
116 let unit = CurrencyUnit::Msat;
117 Ok(Self {
118 _address: address,
119 _cert_file: cert_file,
120 _macaroon_file: macaroon_file,
121 lnd_client,
122 fee_reserve,
123 kv_store,
124 wait_invoice_cancel_token: CancellationToken::new(),
125 wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
126 settings: SettingsResponse {
127 unit: unit.to_string(),
128 bolt11: Some(payment::Bolt11Settings {
129 mpp: true,
130 amountless: true,
131 invoice_description: true,
132 }),
133 bolt12: None,
134 onchain: None,
135 custom: std::collections::HashMap::new(),
136 },
137 unit,
138 })
139 }
140
141 #[instrument(skip_all)]
143 async fn get_last_indices(&self) -> Result<(Option<u64>, Option<u64>), Error> {
144 let add_index = if let Some(stored_index) = self
145 .kv_store
146 .kv_read(
147 LND_KV_PRIMARY_NAMESPACE,
148 LND_KV_SECONDARY_NAMESPACE,
149 LAST_ADD_INDEX_KV_KEY,
150 )
151 .await
152 .map_err(|e| Error::Database(e.to_string()))?
153 {
154 if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
155 index_str.parse::<u64>().ok()
156 } else {
157 None
158 }
159 } else {
160 None
161 };
162
163 let settle_index = if let Some(stored_index) = self
164 .kv_store
165 .kv_read(
166 LND_KV_PRIMARY_NAMESPACE,
167 LND_KV_SECONDARY_NAMESPACE,
168 LAST_SETTLE_INDEX_KV_KEY,
169 )
170 .await
171 .map_err(|e| Error::Database(e.to_string()))?
172 {
173 if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
174 index_str.parse::<u64>().ok()
175 } else {
176 None
177 }
178 } else {
179 None
180 };
181
182 tracing::debug!(
183 "LND: Retrieved last indices from KV store - add_index: {:?}, settle_index: {:?}",
184 add_index,
185 settle_index
186 );
187 Ok((add_index, settle_index))
188 }
189}
190
191fn lnrpc_payment_total_spent(payment: &lnrpc::Payment) -> Result<Amount<CurrencyUnit>, Error> {
192 let total_msat = payment
193 .value_msat
194 .checked_add(payment.fee_msat)
195 .ok_or(Error::AmountOverflow)?;
196 let total_msat = u64::try_from(total_msat).map_err(|_| Error::AmountOverflow)?;
197
198 Ok(Amount::new(total_msat, CurrencyUnit::Msat))
199}
200
201fn msat_total_spent_for_unit(
202 total_msat: u64,
203 unit: &CurrencyUnit,
204) -> Result<Amount<CurrencyUnit>, Error> {
205 match unit {
206 CurrencyUnit::Msat => Ok(Amount::new(total_msat, CurrencyUnit::Msat)),
207 CurrencyUnit::Sat => Ok(Amount::new(
208 total_msat.div_ceil(MSAT_IN_SAT),
209 CurrencyUnit::Sat,
210 )),
211 _ => Amount::new(total_msat, CurrencyUnit::Msat)
212 .convert_to(unit)
213 .map_err(Error::from),
214 }
215}
216
217fn outgoing_payment_failure_response(
231 unit: &CurrencyUnit,
232 payment_lookup_id: PaymentIdentifier,
233) -> MakePaymentResponse {
234 MakePaymentResponse {
235 payment_lookup_id,
236 payment_proof: None,
237 status: MeltQuoteState::Failed,
238 total_spent: Amount::new(0, unit.clone()),
239 }
240}
241
242#[async_trait]
243impl MintPayment for Lnd {
244 type Err = payment::Error;
245
246 #[instrument(skip_all)]
247 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
248 Ok(self.settings.clone())
249 }
250
251 #[instrument(skip_all)]
252 fn is_payment_event_stream_active(&self) -> bool {
253 self.wait_invoice_is_active.load(Ordering::SeqCst)
254 }
255
256 #[instrument(skip_all)]
257 fn cancel_payment_event_stream(&self) {
258 self.wait_invoice_cancel_token.cancel()
259 }
260
261 #[instrument(skip_all)]
262 async fn wait_payment_event(
263 &self,
264 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
265 let mut lnd_client = self.lnd_client.clone();
266
267 let (last_add_index, last_settle_index) =
269 self.get_last_indices().await.unwrap_or((None, None));
270
271 let stream_req = lnrpc::InvoiceSubscription {
272 add_index: last_add_index.unwrap_or(0),
273 settle_index: last_settle_index.unwrap_or(0),
274 };
275
276 tracing::debug!(
277 "LND: Starting invoice subscription with add_index: {}, settle_index: {}",
278 stream_req.add_index,
279 stream_req.settle_index
280 );
281
282 let stream = lnd_client
283 .lightning()
284 .subscribe_invoices(stream_req)
285 .await
286 .map_err(|_err| {
287 tracing::error!("Could not subscribe to invoice");
288 Error::Connection
289 })?
290 .into_inner();
291
292 let cancel_token = self.wait_invoice_cancel_token.clone();
293 let kv_store = self.kv_store.clone();
294
295 let event_stream = futures::stream::unfold(
296 (
297 stream,
298 cancel_token,
299 Arc::clone(&self.wait_invoice_is_active),
300 kv_store,
301 last_add_index.unwrap_or(0),
302 last_settle_index.unwrap_or(0),
303 ),
304 |(
305 mut stream,
306 cancel_token,
307 is_active,
308 kv_store,
309 mut current_add_index,
310 mut current_settle_index,
311 )| async move {
312 is_active.store(true, Ordering::SeqCst);
313
314 loop {
315 tokio::select! {
316 _ = cancel_token.cancelled() => {
317 is_active.store(false, Ordering::SeqCst);
319 tracing::info!("Waiting for lnd invoice ending");
320 return None;
321 }
322 msg = stream.message() => {
323 match msg {
324 Ok(Some(msg)) => {
325 current_add_index = current_add_index.max(msg.add_index);
327 current_settle_index = current_settle_index.max(msg.settle_index);
328
329 let add_index_str = current_add_index.to_string();
331 let settle_index_str = current_settle_index.to_string();
332
333 if let Ok(mut tx) = kv_store.begin_transaction().await {
334 let mut has_error = false;
335
336 if let Err(e) = tx.kv_write(LND_KV_PRIMARY_NAMESPACE, LND_KV_SECONDARY_NAMESPACE, LAST_ADD_INDEX_KV_KEY, add_index_str.as_bytes()).await {
337 tracing::warn!("LND: Failed to write add_index {} to KV store: {}", current_add_index, e);
338 has_error = true;
339 }
340
341 if let Err(e) = tx.kv_write(LND_KV_PRIMARY_NAMESPACE, LND_KV_SECONDARY_NAMESPACE, LAST_SETTLE_INDEX_KV_KEY, settle_index_str.as_bytes()).await {
342 tracing::warn!("LND: Failed to write settle_index {} to KV store: {}", current_settle_index, e);
343 has_error = true;
344 }
345
346 if !has_error {
347 if let Err(e) = tx.commit().await {
348 tracing::warn!("LND: Failed to commit indices to KV store: {}", e);
349 } else {
350 tracing::debug!("LND: Stored updated indices - add_index: {}, settle_index: {}", current_add_index, current_settle_index);
351 }
352 }
353 } else {
354 tracing::warn!("LND: Failed to begin KV transaction for storing indices");
355 }
356
357 if msg.state() == InvoiceState::Settled {
359 let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
360
361 if let Ok(hash_slice) = hash_slice {
362 let hash = hex::encode(hash_slice);
363
364 tracing::info!("LND: Payment for {} with amount {} msat", hash, msg.amt_paid_msat);
365
366 let wait_response = WaitPaymentResponse {
367 payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
368 payment_amount: Amount::new(msg.amt_paid_msat as u64, CurrencyUnit::Msat),
369 payment_id: hash,
370 };
371 let event = Event::PaymentReceived(wait_response);
372 return Some((event, (stream, cancel_token, is_active, kv_store, current_add_index, current_settle_index)));
373 } else {
374 tracing::error!("LND returned invalid payment hash");
376 continue;
378 }
379 } else {
380 tracing::debug!("LND: Received non-settled invoice, continuing to wait for settled invoices");
382 continue;
384 }
385 }
386 Ok(None) => {
387 is_active.store(false, Ordering::SeqCst);
388 tracing::info!("LND invoice stream ended.");
389 return None;
390 }
391 Err(err) => {
392 is_active.store(false, Ordering::SeqCst);
393 tracing::warn!("Encountered error in LND invoice stream. Stream ending");
394 tracing::error!("{:?}", err);
395 return None;
396 }
397 }
398 }
399 }
400 }
401 },
402 );
403
404 Ok(Box::pin(event_stream))
405 }
406
407 #[instrument(skip_all)]
408 async fn get_payment_quote(
409 &self,
410 unit: &CurrencyUnit,
411 options: OutgoingPaymentOptions,
412 ) -> Result<PaymentQuoteResponse, Self::Err> {
413 match options {
414 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
415 let amount_msat = match bolt11_options.melt_options {
416 Some(MeltOptions::Amountless { amountless }) => {
417 let amount_msat = amountless.amount_msat;
418
419 if let Some(invoice_amount) = bolt11_options.bolt11.amount_milli_satoshis()
420 {
421 if invoice_amount != u64::from(amount_msat) {
422 return Err(payment::Error::AmountMismatch);
423 }
424 }
425
426 amount_msat
427 }
428 Some(MeltOptions::Mpp { mpp }) => mpp.amount,
429 None => bolt11_options
430 .bolt11
431 .amount_milli_satoshis()
432 .ok_or(Error::UnknownInvoiceAmount)?
433 .into(),
434 };
435
436 let amount =
437 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
438
439 let relative_fee_reserve =
440 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
441
442 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
443
444 let fee = max(relative_fee_reserve, absolute_fee_reserve);
445
446 Ok(PaymentQuoteResponse {
447 request_lookup_id: Some(PaymentIdentifier::PaymentHash(
448 *bolt11_options.bolt11.payment_hash().as_ref(),
449 )),
450 amount,
451 fee: Amount::new(fee, unit.clone()),
452 state: MeltQuoteState::Unpaid,
453 extra_json: None,
454 estimated_blocks: None,
455 fee_options: None,
456 })
457 }
458 OutgoingPaymentOptions::Bolt12(_) => {
459 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
460 }
461 OutgoingPaymentOptions::Custom(_) | OutgoingPaymentOptions::Onchain(_) => {
462 Err(payment::Error::UnsupportedPaymentOption)
463 }
464 }
465 }
466
467 #[instrument(skip_all)]
468 async fn make_payment(
469 &self,
470 unit: &CurrencyUnit,
471 options: OutgoingPaymentOptions,
472 ) -> Result<MakePaymentResponse, Self::Err> {
473 match options {
474 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
475 let bolt11 = bolt11_options.bolt11;
476 let payment_lookup_id =
477 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
478
479 let pay_state = self.check_outgoing_payment(&payment_lookup_id).await?;
484
485 match pay_state.status {
486 MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
487 MeltQuoteState::Paid => {
488 tracing::debug!("Melt attempted on invoice already paid");
489 return Ok(MakePaymentResponse {
490 payment_lookup_id: payment_lookup_id.clone(),
491 ..pay_state
492 });
493 }
494 MeltQuoteState::Pending => {
495 tracing::debug!("Melt attempted on invoice already pending");
496 return Ok(MakePaymentResponse {
497 payment_lookup_id: payment_lookup_id.clone(),
498 ..pay_state
499 });
500 }
501 }
502
503 match bolt11_options.melt_options {
505 Some(MeltOptions::Mpp { mpp }) => {
506 let amount_msat: u64 = match bolt11.amount_milli_satoshis() {
507 Some(amount_msat) => amount_msat,
508 None => {
509 return Ok(outgoing_payment_failure_response(
512 unit,
513 payment_lookup_id,
514 ));
515 }
516 };
517 {
518 let partial_amount_msat = mpp.amount;
519 let invoice = bolt11;
520 let max_fee: Option<Amount<CurrencyUnit>> =
521 bolt11_options.max_fee_amount.clone();
522
523 let pub_key = invoice.get_payee_pub_key();
525 let payer_addr = invoice.payment_secret().0.to_vec();
526 let payment_hash = invoice.payment_hash();
527
528 let mut lnd_client = self.lnd_client.clone();
529
530 for attempt in 0..Self::MAX_ROUTE_RETRIES {
531 let route_req = lnrpc::QueryRoutesRequest {
533 pub_key: hex::encode(pub_key.serialize()),
534 amt_msat: u64::from(partial_amount_msat) as i64,
535 fee_limit: max_fee
536 .clone()
537 .map(|f| {
538 let fee_msat = f.to_msat()?;
539 let limit = Limit::FixedMsat(fee_msat as i64);
540 Ok::<_, Error>(FeeLimit { limit: Some(limit) })
541 })
542 .transpose()?,
543 use_mission_control: true,
544 ..Default::default()
545 };
546
547 let mut routes_response = lnd_client
549 .lightning()
550 .query_routes(route_req)
551 .await
552 .map_err(Error::LndError)?
553 .into_inner();
554
555 let route = match routes_response.routes.first_mut() {
559 Some(route) => route,
560 None => {
561 return Ok(outgoing_payment_failure_response(
562 unit,
563 payment_lookup_id,
564 ));
565 }
566 };
567
568 let last_hop: &mut Hop = match route.hops.last_mut() {
570 Some(last_hop) => last_hop,
571 None => {
572 return Ok(outgoing_payment_failure_response(
573 unit,
574 payment_lookup_id,
575 ));
576 }
577 };
578 let mpp_record = MppRecord {
579 payment_addr: payer_addr.clone(),
580 total_amt_msat: amount_msat as i64,
581 };
582 last_hop.mpp_record = Some(mpp_record);
583
584 let payment_response = lnd_client
585 .router()
586 .send_to_route_v2(routerrpc::SendToRouteRequest {
587 payment_hash: payment_hash.to_byte_array().to_vec(),
588 route: Some(route.clone()),
589 ..Default::default()
590 })
591 .await
592 .map_err(Error::LndError)?
593 .into_inner();
594
595 if let Some(failure) = payment_response.failure {
596 if failure.code == 15 {
597 tracing::debug!(
598 "Attempt number {}: route has failed. Re-querying...",
599 attempt + 1
600 );
601 continue;
602 }
603 }
604
605 let (status, payment_preimage) = match payment_response.status {
607 0 => (MeltQuoteState::Pending, None),
608 1 => (
609 MeltQuoteState::Paid,
610 Some(hex::encode(payment_response.preimage)),
611 ),
612 2 => (MeltQuoteState::Unpaid, None),
613 _ => (MeltQuoteState::Unknown, None),
614 };
615
616 let total_amt_msat: u64 = payment_response
618 .route
619 .map_or(0, |route| route.total_amt_msat as u64);
620
621 return Ok(MakePaymentResponse {
622 payment_lookup_id: PaymentIdentifier::PaymentHash(
623 payment_hash.to_byte_array(),
624 ),
625 payment_proof: payment_preimage,
626 status,
627 total_spent: msat_total_spent_for_unit(total_amt_msat, unit)?,
628 });
629 }
630
631 tracing::error!("Limit of retries reached, payment couldn't succeed.");
635 Ok(outgoing_payment_failure_response(unit, payment_lookup_id))
636 }
637 }
638 _ => {
639 let mut lnd_client = self.lnd_client.clone();
640
641 let max_fee: Option<Amount<CurrencyUnit>> = bolt11_options.max_fee_amount;
642
643 let amount_msat = match bolt11_options.melt_options {
644 Some(MeltOptions::Amountless { amountless }) => {
645 let amount_msat = amountless.amount_msat;
646
647 if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
648 if invoice_amount != u64::from(amount_msat) {
649 return Ok(outgoing_payment_failure_response(
653 unit,
654 payment_lookup_id,
655 ));
656 }
657 }
658
659 u64::from(amount_msat)
660 }
661 Some(MeltOptions::Mpp { mpp }) => u64::from(mpp.amount),
662 None => 0,
663 };
664
665 let fee_limit_msat = match max_fee {
666 Some(fee) => fee.convert_to(&CurrencyUnit::Msat)?.value() as i64,
667 None => 0,
668 };
669
670 let pay_req = routerrpc::SendPaymentRequest {
671 payment_request: bolt11.to_string(),
672 fee_limit_msat,
673 amt_msat: amount_msat as i64,
674 ..Default::default()
675 };
676
677 let mut payment_stream = lnd_client
678 .router()
679 .send_payment_v2(pay_req)
680 .await
681 .map_err(|err| {
682 tracing::warn!("Lightning payment dispatch error: {}", err);
683 Error::AmbiguousDispatch
686 })?
687 .into_inner();
688
689 while let Some(update) = payment_stream.message().await.map_err(|err| {
690 tracing::warn!("Lightning payment stream error: {}", err);
691 Error::AmbiguousDispatch
694 })? {
695 let status = update.status();
696
697 let response_status = match status {
698 PaymentStatus::InFlight | PaymentStatus::Initiated => {
699 continue;
700 }
701 PaymentStatus::Succeeded => MeltQuoteState::Paid,
702 PaymentStatus::Failed => MeltQuoteState::Failed,
703 #[allow(deprecated)]
704 PaymentStatus::Unknown => MeltQuoteState::Unknown,
705 };
706
707 let total_msat = update
708 .value_msat
709 .checked_add(update.fee_msat)
710 .ok_or(Error::AmountOverflow)?;
711
712 let payment_preimage = if update.payment_preimage.is_empty() {
713 None
714 } else {
715 Some(update.payment_preimage)
716 };
717
718 let payment_identifier =
719 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
720
721 return Ok(MakePaymentResponse {
722 payment_lookup_id: payment_identifier,
723 payment_proof: payment_preimage,
724 status: response_status,
725 total_spent: msat_total_spent_for_unit(total_msat as u64, unit)?,
726 });
727 }
728
729 Err(Error::UnknownPaymentStatus.into())
730 }
731 }
732 }
733 OutgoingPaymentOptions::Bolt12(_) => {
734 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
735 }
736 OutgoingPaymentOptions::Custom(_) | OutgoingPaymentOptions::Onchain(_) => {
737 Err(payment::Error::UnsupportedPaymentOption)
738 }
739 }
740 }
741
742 #[instrument(skip(self, options))]
743 async fn create_incoming_payment_request(
744 &self,
745 options: IncomingPaymentOptions,
746 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
747 match options {
748 IncomingPaymentOptions::Bolt11(bolt11_options) => {
749 let description = bolt11_options.description.unwrap_or_default();
750 let amount = bolt11_options.amount;
751 let unix_expiry = bolt11_options.unix_expiry;
752
753 let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
754
755 let invoice_request = lnrpc::Invoice {
756 value_msat: u64::from(amount_msat) as i64,
757 memo: description,
758 expiry: unix_expiry
759 .map(|t| {
760 t.checked_sub(unix_time())
761 .ok_or(payment::Error::InvalidExpiry)
762 })
763 .transpose()?
764 .unwrap_or_default() as i64,
765 ..Default::default()
766 };
767
768 let mut lnd_client = self.lnd_client.clone();
769
770 let invoice = lnd_client
771 .lightning()
772 .add_invoice(tonic::Request::new(invoice_request))
773 .await
774 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
775 .into_inner();
776
777 let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
778
779 let payment_identifier =
780 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
781
782 let expiry = bolt11.expires_at().map(|t| t.as_secs());
783
784 Ok(CreateIncomingPaymentResponse {
785 request_lookup_id: payment_identifier,
786 request: bolt11.to_string(),
787 expiry,
788 extra_json: None,
789 })
790 }
791 IncomingPaymentOptions::Bolt12(_) => {
792 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
793 }
794 IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
795 Err(payment::Error::UnsupportedPaymentOption)
796 }
797 }
798 }
799
800 #[instrument(skip(self))]
801 async fn check_incoming_payment_status(
802 &self,
803 payment_identifier: &PaymentIdentifier,
804 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
805 let mut lnd_client = self.lnd_client.clone();
806
807 let invoice_request = lnrpc::PaymentHash {
808 r_hash: hex::decode(payment_identifier.to_string())?,
809 ..Default::default()
810 };
811
812 let invoice = lnd_client
813 .lightning()
814 .lookup_invoice(tonic::Request::new(invoice_request))
815 .await
816 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
817 .into_inner();
818
819 if invoice.state() == InvoiceState::Settled {
820 Ok(vec![WaitPaymentResponse {
821 payment_identifier: payment_identifier.clone(),
822 payment_amount: Amount::new(invoice.amt_paid_msat as u64, CurrencyUnit::Msat),
823 payment_id: hex::encode(invoice.r_hash),
824 }])
825 } else {
826 Ok(vec![])
827 }
828 }
829
830 #[instrument(skip(self))]
831 async fn check_outgoing_payment(
832 &self,
833 payment_identifier: &PaymentIdentifier,
834 ) -> Result<MakePaymentResponse, Self::Err> {
835 let mut lnd_client = self.lnd_client.clone();
836
837 let payment_hash = &payment_identifier.to_string();
838
839 let track_request = routerrpc::TrackPaymentRequest {
840 payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
841 no_inflight_updates: true,
842 };
843
844 let payment_response = lnd_client.router().track_payment_v2(track_request).await;
845
846 let mut payment_stream = match payment_response {
847 Ok(stream) => stream.into_inner(),
848 Err(err) => {
849 let err_code = err.code();
850 if err_code == tonic::Code::NotFound {
851 return Ok(MakePaymentResponse {
852 payment_lookup_id: payment_identifier.clone(),
853 payment_proof: None,
854 status: MeltQuoteState::Unknown,
855 total_spent: Amount::new(0, self.unit.clone()),
856 });
857 } else {
858 return Err(payment::Error::UnknownPaymentState);
859 }
860 }
861 };
862
863 while let Some(update_result) = payment_stream.next().await {
864 match update_result {
865 Ok(update) => {
866 let status = update.status();
867
868 let response = match status {
869 #[allow(deprecated)]
870 PaymentStatus::Unknown => MakePaymentResponse {
871 payment_lookup_id: payment_identifier.clone(),
872 payment_proof: Some(update.payment_preimage),
873 status: MeltQuoteState::Unknown,
874 total_spent: Amount::new(0, self.unit.clone()),
875 },
876 PaymentStatus::InFlight | PaymentStatus::Initiated => {
877 continue;
879 }
880 PaymentStatus::Succeeded => {
881 let total_spent = lnrpc_payment_total_spent(&update)?;
882
883 MakePaymentResponse {
884 payment_lookup_id: payment_identifier.clone(),
885 payment_proof: Some(update.payment_preimage),
886 status: MeltQuoteState::Paid,
887 total_spent,
888 }
889 }
890 PaymentStatus::Failed => MakePaymentResponse {
891 payment_lookup_id: payment_identifier.clone(),
892 payment_proof: Some(update.payment_preimage),
893 status: MeltQuoteState::Failed,
894 total_spent: Amount::new(0, self.unit.clone()),
895 },
896 };
897
898 return Ok(response);
899 }
900 Err(_) => {
901 return Err(Error::UnknownPaymentStatus.into());
903 }
904 }
905 }
906
907 Err(Error::UnknownPaymentStatus.into())
909 }
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915
916 #[test]
917 fn lnrpc_payment_total_spent_uses_msat_fields() {
918 let payment = lnrpc::Payment {
919 value_msat: 1500,
920 fee_msat: 500,
921 value_sat: 1,
922 fee_sat: 0,
923 ..Default::default()
924 };
925
926 let total_spent = lnrpc_payment_total_spent(&payment)
927 .expect("sub-sat payment total should be calculated");
928
929 assert_eq!(
930 total_spent
931 .convert_to(&CurrencyUnit::Msat)
932 .expect("msat amount should convert to msat")
933 .value(),
934 2000
935 );
936 }
937
938 #[test]
939 fn lnrpc_payment_total_spent_rejects_overflow() {
940 let payment = lnrpc::Payment {
941 value_msat: i64::MAX,
942 fee_msat: 1,
943 ..Default::default()
944 };
945
946 let err = lnrpc_payment_total_spent(&payment)
947 .expect_err("overflowing payment total should be rejected");
948
949 assert!(matches!(err, Error::AmountOverflow));
950 }
951
952 #[test]
953 fn msat_total_spent_for_unit_rounds_up_sats() {
954 let total_spent = msat_total_spent_for_unit(1501, &CurrencyUnit::Sat)
955 .expect("msat total should convert to sat");
956
957 assert_eq!(total_spent, Amount::new(2, CurrencyUnit::Sat));
958 }
959
960 #[test]
961 fn authoritative_outgoing_failure_response_is_terminal_and_spends_nothing() {
962 let payment_lookup_id = PaymentIdentifier::PaymentHash([42; 32]);
963 let response =
964 outgoing_payment_failure_response(&CurrencyUnit::Sat, payment_lookup_id.clone());
965
966 assert_eq!(response.payment_lookup_id, payment_lookup_id);
967 assert_eq!(response.status, MeltQuoteState::Failed);
968 assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Sat));
969 assert!(response.payment_proof.is_none());
970 }
971
972 #[test]
977 fn dispatch_boundary_errors_are_distinct_from_pre_dispatch_failure() {
978 assert_ne!(
982 Error::AmbiguousDispatch.to_string(),
983 Error::PaymentFailed.to_string()
984 );
985 assert_ne!(
986 Error::UnknownPaymentStatus.to_string(),
987 Error::PaymentFailed.to_string()
988 );
989 }
990}