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
242fn bolt11_pre_dispatch_response(
244 unit: &CurrencyUnit,
245 bolt11: &Bolt11Invoice,
246 pay_state: MakePaymentResponse,
247) -> Option<MakePaymentResponse> {
248 let payment_lookup_id = PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
249 match pay_state.status {
250 MeltQuoteState::Paid | MeltQuoteState::Pending => Some(MakePaymentResponse {
251 payment_lookup_id,
252 ..pay_state
253 }),
254 MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => {
255 bolt11
259 .is_expired()
260 .then(|| outgoing_payment_failure_response(unit, payment_lookup_id))
261 }
262 }
263}
264
265#[async_trait]
266impl MintPayment for Lnd {
267 type Err = payment::Error;
268
269 #[instrument(skip_all)]
270 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
271 Ok(self.settings.clone())
272 }
273
274 #[instrument(skip_all)]
275 fn is_payment_event_stream_active(&self) -> bool {
276 self.wait_invoice_is_active.load(Ordering::SeqCst)
277 }
278
279 #[instrument(skip_all)]
280 fn cancel_payment_event_stream(&self) {
281 self.wait_invoice_cancel_token.cancel()
282 }
283
284 #[instrument(skip_all)]
285 async fn wait_payment_event(
286 &self,
287 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
288 let mut lnd_client = self.lnd_client.clone();
289
290 let (last_add_index, last_settle_index) =
292 self.get_last_indices().await.unwrap_or((None, None));
293
294 let stream_req = lnrpc::InvoiceSubscription {
295 add_index: last_add_index.unwrap_or(0),
296 settle_index: last_settle_index.unwrap_or(0),
297 };
298
299 tracing::debug!(
300 "LND: Starting invoice subscription with add_index: {}, settle_index: {}",
301 stream_req.add_index,
302 stream_req.settle_index
303 );
304
305 let stream = lnd_client
306 .lightning()
307 .subscribe_invoices(stream_req)
308 .await
309 .map_err(|_err| {
310 tracing::error!("Could not subscribe to invoice");
311 Error::Connection
312 })?
313 .into_inner();
314
315 let cancel_token = self.wait_invoice_cancel_token.clone();
316 let kv_store = self.kv_store.clone();
317
318 let event_stream = futures::stream::unfold(
319 (
320 stream,
321 cancel_token,
322 Arc::clone(&self.wait_invoice_is_active),
323 kv_store,
324 last_add_index.unwrap_or(0),
325 last_settle_index.unwrap_or(0),
326 ),
327 |(
328 mut stream,
329 cancel_token,
330 is_active,
331 kv_store,
332 mut current_add_index,
333 mut current_settle_index,
334 )| async move {
335 is_active.store(true, Ordering::SeqCst);
336
337 loop {
338 tokio::select! {
339 _ = cancel_token.cancelled() => {
340 is_active.store(false, Ordering::SeqCst);
342 tracing::info!("Waiting for lnd invoice ending");
343 return None;
344 }
345 msg = stream.message() => {
346 match msg {
347 Ok(Some(msg)) => {
348 current_add_index = current_add_index.max(msg.add_index);
350 current_settle_index = current_settle_index.max(msg.settle_index);
351
352 let add_index_str = current_add_index.to_string();
354 let settle_index_str = current_settle_index.to_string();
355
356 if let Ok(mut tx) = kv_store.begin_transaction().await {
357 let mut has_error = false;
358
359 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 {
360 tracing::warn!("LND: Failed to write add_index {} to KV store: {}", current_add_index, e);
361 has_error = true;
362 }
363
364 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 {
365 tracing::warn!("LND: Failed to write settle_index {} to KV store: {}", current_settle_index, e);
366 has_error = true;
367 }
368
369 if !has_error {
370 if let Err(e) = tx.commit().await {
371 tracing::warn!("LND: Failed to commit indices to KV store: {}", e);
372 } else {
373 tracing::debug!("LND: Stored updated indices - add_index: {}, settle_index: {}", current_add_index, current_settle_index);
374 }
375 }
376 } else {
377 tracing::warn!("LND: Failed to begin KV transaction for storing indices");
378 }
379
380 if msg.state() == InvoiceState::Settled {
382 let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
383
384 if let Ok(hash_slice) = hash_slice {
385 let hash = hex::encode(hash_slice);
386
387 tracing::info!("LND: Payment for {} with amount {} msat", hash, msg.amt_paid_msat);
388
389 let wait_response = WaitPaymentResponse {
390 payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
391 payment_amount: Amount::new(msg.amt_paid_msat as u64, CurrencyUnit::Msat),
392 payment_id: hash,
393 };
394 let event = Event::PaymentReceived(wait_response);
395 return Some((event, (stream, cancel_token, is_active, kv_store, current_add_index, current_settle_index)));
396 } else {
397 tracing::error!("LND returned invalid payment hash");
399 continue;
401 }
402 } else {
403 tracing::debug!("LND: Received non-settled invoice, continuing to wait for settled invoices");
405 continue;
407 }
408 }
409 Ok(None) => {
410 is_active.store(false, Ordering::SeqCst);
411 tracing::info!("LND invoice stream ended.");
412 return None;
413 }
414 Err(err) => {
415 is_active.store(false, Ordering::SeqCst);
416 tracing::warn!("Encountered error in LND invoice stream. Stream ending");
417 tracing::error!("{:?}", err);
418 return None;
419 }
420 }
421 }
422 }
423 }
424 },
425 );
426
427 Ok(Box::pin(event_stream))
428 }
429
430 #[instrument(skip_all)]
431 async fn get_payment_quote(
432 &self,
433 unit: &CurrencyUnit,
434 options: OutgoingPaymentOptions,
435 ) -> Result<PaymentQuoteResponse, Self::Err> {
436 match options {
437 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
438 let amount_msat = match bolt11_options.melt_options {
439 Some(MeltOptions::Amountless { amountless }) => {
440 let amount_msat = amountless.amount_msat;
441
442 if let Some(invoice_amount) = bolt11_options.bolt11.amount_milli_satoshis()
443 {
444 if invoice_amount != u64::from(amount_msat) {
445 return Err(payment::Error::AmountMismatch);
446 }
447 }
448
449 amount_msat
450 }
451 Some(MeltOptions::Mpp { mpp }) => mpp.amount,
452 None => bolt11_options
453 .bolt11
454 .amount_milli_satoshis()
455 .ok_or(Error::UnknownInvoiceAmount)?
456 .into(),
457 };
458
459 let amount =
460 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
461
462 let relative_fee_reserve =
463 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
464
465 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
466
467 let fee = max(relative_fee_reserve, absolute_fee_reserve);
468
469 Ok(PaymentQuoteResponse {
470 request_lookup_id: Some(PaymentIdentifier::PaymentHash(
471 *bolt11_options.bolt11.payment_hash().as_ref(),
472 )),
473 amount,
474 fee: Amount::new(fee, unit.clone()),
475 state: MeltQuoteState::Unpaid,
476 extra_json: None,
477 estimated_blocks: None,
478 fee_options: None,
479 })
480 }
481 OutgoingPaymentOptions::Bolt12(_) => {
482 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
483 }
484 OutgoingPaymentOptions::Custom(_) | OutgoingPaymentOptions::Onchain(_) => {
485 Err(payment::Error::UnsupportedPaymentOption)
486 }
487 }
488 }
489
490 #[instrument(skip_all)]
491 async fn make_payment(
492 &self,
493 unit: &CurrencyUnit,
494 options: OutgoingPaymentOptions,
495 ) -> Result<MakePaymentResponse, Self::Err> {
496 match options {
497 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
498 let bolt11 = bolt11_options.bolt11;
499 let payment_lookup_id =
500 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
501
502 let pay_state = self.check_outgoing_payment(&payment_lookup_id).await?;
507
508 if let Some(response) = bolt11_pre_dispatch_response(unit, &bolt11, pay_state) {
509 return Ok(response);
510 }
511
512 match bolt11_options.melt_options {
514 Some(MeltOptions::Mpp { mpp }) => {
515 let amount_msat: u64 = match bolt11.amount_milli_satoshis() {
516 Some(amount_msat) => amount_msat,
517 None => {
518 return Ok(outgoing_payment_failure_response(
521 unit,
522 payment_lookup_id,
523 ));
524 }
525 };
526 {
527 let partial_amount_msat = mpp.amount;
528 let invoice = bolt11;
529 let max_fee: Option<Amount<CurrencyUnit>> =
530 bolt11_options.max_fee_amount.clone();
531
532 let pub_key = invoice.get_payee_pub_key();
534 let payer_addr = invoice.payment_secret().0.to_vec();
535 let payment_hash = invoice.payment_hash();
536
537 let mut lnd_client = self.lnd_client.clone();
538
539 for attempt in 0..Self::MAX_ROUTE_RETRIES {
540 let route_req = lnrpc::QueryRoutesRequest {
542 pub_key: hex::encode(pub_key.serialize()),
543 amt_msat: u64::from(partial_amount_msat) as i64,
544 fee_limit: max_fee
545 .clone()
546 .map(|f| {
547 let fee_msat = f.to_msat()?;
548 let limit = Limit::FixedMsat(fee_msat as i64);
549 Ok::<_, Error>(FeeLimit { limit: Some(limit) })
550 })
551 .transpose()?,
552 use_mission_control: true,
553 ..Default::default()
554 };
555
556 let mut routes_response = lnd_client
558 .lightning()
559 .query_routes(route_req)
560 .await
561 .map_err(Error::LndError)?
562 .into_inner();
563
564 let route = match routes_response.routes.first_mut() {
568 Some(route) => route,
569 None => {
570 return Ok(outgoing_payment_failure_response(
571 unit,
572 payment_lookup_id,
573 ));
574 }
575 };
576
577 let last_hop: &mut Hop = match route.hops.last_mut() {
579 Some(last_hop) => last_hop,
580 None => {
581 return Ok(outgoing_payment_failure_response(
582 unit,
583 payment_lookup_id,
584 ));
585 }
586 };
587 let mpp_record = MppRecord {
588 payment_addr: payer_addr.clone(),
589 total_amt_msat: amount_msat as i64,
590 };
591 last_hop.mpp_record = Some(mpp_record);
592
593 let payment_response = lnd_client
594 .router()
595 .send_to_route_v2(routerrpc::SendToRouteRequest {
596 payment_hash: payment_hash.to_byte_array().to_vec(),
597 route: Some(route.clone()),
598 ..Default::default()
599 })
600 .await
601 .map_err(Error::LndError)?
602 .into_inner();
603
604 if let Some(failure) = payment_response.failure {
605 if failure.code == 15 {
606 tracing::debug!(
607 "Attempt number {}: route has failed. Re-querying...",
608 attempt + 1
609 );
610 continue;
611 }
612 }
613
614 let (status, payment_preimage) = match payment_response.status {
616 0 => (MeltQuoteState::Pending, None),
617 1 => (
618 MeltQuoteState::Paid,
619 Some(hex::encode(payment_response.preimage)),
620 ),
621 2 => (MeltQuoteState::Unpaid, None),
622 _ => (MeltQuoteState::Unknown, None),
623 };
624
625 let total_amt_msat: u64 = payment_response
627 .route
628 .map_or(0, |route| route.total_amt_msat as u64);
629
630 return Ok(MakePaymentResponse {
631 payment_lookup_id: PaymentIdentifier::PaymentHash(
632 payment_hash.to_byte_array(),
633 ),
634 payment_proof: payment_preimage,
635 status,
636 total_spent: msat_total_spent_for_unit(total_amt_msat, unit)?,
637 });
638 }
639
640 tracing::error!("Limit of retries reached, payment couldn't succeed.");
644 Ok(outgoing_payment_failure_response(unit, payment_lookup_id))
645 }
646 }
647 _ => {
648 let mut lnd_client = self.lnd_client.clone();
649
650 let max_fee: Option<Amount<CurrencyUnit>> = bolt11_options.max_fee_amount;
651
652 let amount_msat = match bolt11_options.melt_options {
653 Some(MeltOptions::Amountless { amountless }) => {
654 let amount_msat = amountless.amount_msat;
655
656 if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
657 if invoice_amount != u64::from(amount_msat) {
658 return Ok(outgoing_payment_failure_response(
662 unit,
663 payment_lookup_id,
664 ));
665 }
666 }
667
668 u64::from(amount_msat)
669 }
670 Some(MeltOptions::Mpp { mpp }) => u64::from(mpp.amount),
671 None => 0,
672 };
673
674 let fee_limit_msat = match max_fee {
675 Some(fee) => fee.convert_to(&CurrencyUnit::Msat)?.value() as i64,
676 None => 0,
677 };
678
679 let pay_req = routerrpc::SendPaymentRequest {
680 payment_request: bolt11.to_string(),
681 fee_limit_msat,
682 amt_msat: amount_msat as i64,
683 ..Default::default()
684 };
685
686 let mut payment_stream = lnd_client
687 .router()
688 .send_payment_v2(pay_req)
689 .await
690 .map_err(|err| {
691 tracing::warn!("Lightning payment dispatch error: {}", err);
692 Error::AmbiguousDispatch
695 })?
696 .into_inner();
697
698 while let Some(update) = payment_stream.message().await.map_err(|err| {
699 tracing::warn!("Lightning payment stream error: {}", err);
700 Error::AmbiguousDispatch
703 })? {
704 let status = update.status();
705
706 let response_status = match status {
707 PaymentStatus::InFlight | PaymentStatus::Initiated => {
708 continue;
709 }
710 PaymentStatus::Succeeded => MeltQuoteState::Paid,
711 PaymentStatus::Failed => MeltQuoteState::Failed,
712 #[allow(deprecated)]
713 PaymentStatus::Unknown => MeltQuoteState::Unknown,
714 };
715
716 let total_msat = update
717 .value_msat
718 .checked_add(update.fee_msat)
719 .ok_or(Error::AmountOverflow)?;
720
721 let payment_preimage = if update.payment_preimage.is_empty() {
722 None
723 } else {
724 Some(update.payment_preimage)
725 };
726
727 let payment_identifier =
728 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
729
730 return Ok(MakePaymentResponse {
731 payment_lookup_id: payment_identifier,
732 payment_proof: payment_preimage,
733 status: response_status,
734 total_spent: msat_total_spent_for_unit(total_msat as u64, unit)?,
735 });
736 }
737
738 Err(Error::UnknownPaymentStatus.into())
739 }
740 }
741 }
742 OutgoingPaymentOptions::Bolt12(_) => {
743 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
744 }
745 OutgoingPaymentOptions::Custom(_) | OutgoingPaymentOptions::Onchain(_) => {
746 Err(payment::Error::UnsupportedPaymentOption)
747 }
748 }
749 }
750
751 #[instrument(skip(self, options))]
752 async fn create_incoming_payment_request(
753 &self,
754 options: IncomingPaymentOptions,
755 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
756 match options {
757 IncomingPaymentOptions::Bolt11(bolt11_options) => {
758 let description = bolt11_options.description.unwrap_or_default();
759 let amount = bolt11_options.amount;
760 let unix_expiry = bolt11_options.unix_expiry;
761
762 let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
763
764 let invoice_request = lnrpc::Invoice {
765 value_msat: u64::from(amount_msat) as i64,
766 memo: description,
767 expiry: unix_expiry
768 .map(|t| {
769 t.checked_sub(unix_time())
770 .ok_or(payment::Error::InvalidExpiry)
771 })
772 .transpose()?
773 .unwrap_or_default() as i64,
774 ..Default::default()
775 };
776
777 let mut lnd_client = self.lnd_client.clone();
778
779 let invoice = lnd_client
780 .lightning()
781 .add_invoice(tonic::Request::new(invoice_request))
782 .await
783 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
784 .into_inner();
785
786 let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
787
788 let payment_identifier =
789 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
790
791 let expiry = bolt11.expires_at().map(|t| t.as_secs());
792
793 Ok(CreateIncomingPaymentResponse {
794 request_lookup_id: payment_identifier,
795 request: bolt11.to_string(),
796 expiry,
797 extra_json: None,
798 })
799 }
800 IncomingPaymentOptions::Bolt12(_) => {
801 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
802 }
803 IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
804 Err(payment::Error::UnsupportedPaymentOption)
805 }
806 }
807 }
808
809 #[instrument(skip(self))]
810 async fn check_incoming_payment_status(
811 &self,
812 payment_identifier: &PaymentIdentifier,
813 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
814 let mut lnd_client = self.lnd_client.clone();
815
816 let invoice_request = lnrpc::PaymentHash {
817 r_hash: hex::decode(payment_identifier.to_string())?,
818 ..Default::default()
819 };
820
821 let invoice = lnd_client
822 .lightning()
823 .lookup_invoice(tonic::Request::new(invoice_request))
824 .await
825 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
826 .into_inner();
827
828 if invoice.state() == InvoiceState::Settled {
829 Ok(vec![WaitPaymentResponse {
830 payment_identifier: payment_identifier.clone(),
831 payment_amount: Amount::new(invoice.amt_paid_msat as u64, CurrencyUnit::Msat),
832 payment_id: hex::encode(invoice.r_hash),
833 }])
834 } else {
835 Ok(vec![])
836 }
837 }
838
839 #[instrument(skip(self))]
840 async fn check_outgoing_payment(
841 &self,
842 payment_identifier: &PaymentIdentifier,
843 ) -> Result<MakePaymentResponse, Self::Err> {
844 let mut lnd_client = self.lnd_client.clone();
845
846 let payment_hash = &payment_identifier.to_string();
847
848 let track_request = routerrpc::TrackPaymentRequest {
849 payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
850 no_inflight_updates: true,
851 };
852
853 let payment_response = lnd_client.router().track_payment_v2(track_request).await;
854
855 let mut payment_stream = match payment_response {
856 Ok(stream) => stream.into_inner(),
857 Err(err) => {
858 let err_code = err.code();
859 if err_code == tonic::Code::NotFound {
860 return Ok(MakePaymentResponse {
861 payment_lookup_id: payment_identifier.clone(),
862 payment_proof: None,
863 status: MeltQuoteState::Unknown,
864 total_spent: Amount::new(0, self.unit.clone()),
865 });
866 } else {
867 return Err(payment::Error::UnknownPaymentState);
868 }
869 }
870 };
871
872 while let Some(update_result) = payment_stream.next().await {
873 match update_result {
874 Ok(update) => {
875 let status = update.status();
876
877 let response = match status {
878 #[allow(deprecated)]
879 PaymentStatus::Unknown => MakePaymentResponse {
880 payment_lookup_id: payment_identifier.clone(),
881 payment_proof: Some(update.payment_preimage),
882 status: MeltQuoteState::Unknown,
883 total_spent: Amount::new(0, self.unit.clone()),
884 },
885 PaymentStatus::InFlight | PaymentStatus::Initiated => {
886 continue;
888 }
889 PaymentStatus::Succeeded => {
890 let total_spent = lnrpc_payment_total_spent(&update)?;
891
892 MakePaymentResponse {
893 payment_lookup_id: payment_identifier.clone(),
894 payment_proof: Some(update.payment_preimage),
895 status: MeltQuoteState::Paid,
896 total_spent,
897 }
898 }
899 PaymentStatus::Failed => MakePaymentResponse {
900 payment_lookup_id: payment_identifier.clone(),
901 payment_proof: Some(update.payment_preimage),
902 status: MeltQuoteState::Failed,
903 total_spent: Amount::new(0, self.unit.clone()),
904 },
905 };
906
907 return Ok(response);
908 }
909 Err(_) => {
910 return Err(Error::UnknownPaymentStatus.into());
912 }
913 }
914 }
915
916 Err(Error::UnknownPaymentStatus.into())
918 }
919}
920
921#[cfg(test)]
922mod tests {
923 use std::time::Duration;
924
925 use cdk_common::bitcoin::hashes::sha256;
926 use cdk_common::bitcoin::secp256k1::{Secp256k1, SecretKey};
927 use cdk_common::lightning_invoice::{Currency, InvoiceBuilder, PaymentSecret};
928
929 use super::*;
930
931 fn invoice_with_timestamp(timestamp: Duration) -> Bolt11Invoice {
932 let key = SecretKey::from_slice(&[1; 32]).unwrap();
933 InvoiceBuilder::new(Currency::Regtest)
934 .description("expiry test".to_owned())
935 .payment_hash(sha256::Hash::from_byte_array([42; 32]))
936 .payment_secret(PaymentSecret([43; 32]))
937 .duration_since_epoch(timestamp)
938 .expiry_time(Duration::from_secs(3600))
939 .min_final_cltv_expiry_delta(144)
940 .build_signed(|hash| Secp256k1::new().sign_ecdsa_recoverable(hash, &key))
941 .unwrap()
942 }
943
944 #[test]
945 fn expired_invoice_without_active_payment_fails_before_dispatch() {
946 let invoice = invoice_with_timestamp(Duration::from_secs(1));
947 let payment_lookup_id = PaymentIdentifier::PaymentHash(*invoice.payment_hash().as_ref());
948
949 for status in [
950 MeltQuoteState::Unknown,
951 MeltQuoteState::Unpaid,
952 MeltQuoteState::Failed,
953 ] {
954 let pay_state = MakePaymentResponse {
955 status,
956 ..outgoing_payment_failure_response(&CurrencyUnit::Msat, payment_lookup_id.clone())
957 };
958 let response =
959 bolt11_pre_dispatch_response(&CurrencyUnit::Sat, &invoice, pay_state).unwrap();
960
961 assert_eq!(response.status, MeltQuoteState::Failed);
962 assert_eq!(response.payment_lookup_id, payment_lookup_id);
963 assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Sat));
964 assert!(response.payment_proof.is_none());
965 }
966 }
967
968 #[test]
969 fn expired_invoice_preserves_existing_paid_or_pending_payment() {
970 let invoice = invoice_with_timestamp(Duration::from_secs(1));
971 let payment_lookup_id = PaymentIdentifier::PaymentHash(*invoice.payment_hash().as_ref());
972
973 for status in [MeltQuoteState::Paid, MeltQuoteState::Pending] {
974 let pay_state = MakePaymentResponse {
975 payment_lookup_id: payment_lookup_id.clone(),
976 payment_proof: Some("existing preimage".to_owned()),
977 status,
978 total_spent: Amount::new(1234, CurrencyUnit::Msat),
979 };
980 let response =
981 bolt11_pre_dispatch_response(&CurrencyUnit::Sat, &invoice, pay_state).unwrap();
982
983 assert_eq!(response.status, status);
984 assert_eq!(response.payment_lookup_id, payment_lookup_id);
985 assert_eq!(response.total_spent, Amount::new(1234, CurrencyUnit::Msat));
986 assert_eq!(response.payment_proof.as_deref(), Some("existing preimage"));
987 }
988 }
989
990 #[test]
991 fn unexpired_invoice_without_active_payment_can_dispatch() {
992 let invoice = invoice_with_timestamp(Duration::from_secs(unix_time()));
993 let payment_lookup_id = PaymentIdentifier::PaymentHash(*invoice.payment_hash().as_ref());
994
995 for status in [
996 MeltQuoteState::Unknown,
997 MeltQuoteState::Unpaid,
998 MeltQuoteState::Failed,
999 ] {
1000 let pay_state = MakePaymentResponse {
1001 status,
1002 ..outgoing_payment_failure_response(&CurrencyUnit::Msat, payment_lookup_id.clone())
1003 };
1004 assert!(
1005 bolt11_pre_dispatch_response(&CurrencyUnit::Sat, &invoice, pay_state).is_none()
1006 );
1007 }
1008 }
1009
1010 #[test]
1011 fn lnrpc_payment_total_spent_uses_msat_fields() {
1012 let payment = lnrpc::Payment {
1013 value_msat: 1500,
1014 fee_msat: 500,
1015 value_sat: 1,
1016 fee_sat: 0,
1017 ..Default::default()
1018 };
1019
1020 let total_spent = lnrpc_payment_total_spent(&payment)
1021 .expect("sub-sat payment total should be calculated");
1022
1023 assert_eq!(
1024 total_spent
1025 .convert_to(&CurrencyUnit::Msat)
1026 .expect("msat amount should convert to msat")
1027 .value(),
1028 2000
1029 );
1030 }
1031
1032 #[test]
1033 fn lnrpc_payment_total_spent_rejects_overflow() {
1034 let payment = lnrpc::Payment {
1035 value_msat: i64::MAX,
1036 fee_msat: 1,
1037 ..Default::default()
1038 };
1039
1040 let err = lnrpc_payment_total_spent(&payment)
1041 .expect_err("overflowing payment total should be rejected");
1042
1043 assert!(matches!(err, Error::AmountOverflow));
1044 }
1045
1046 #[test]
1047 fn msat_total_spent_for_unit_rounds_up_sats() {
1048 let total_spent = msat_total_spent_for_unit(1501, &CurrencyUnit::Sat)
1049 .expect("msat total should convert to sat");
1050
1051 assert_eq!(total_spent, Amount::new(2, CurrencyUnit::Sat));
1052 }
1053
1054 #[test]
1055 fn authoritative_outgoing_failure_response_is_terminal_and_spends_nothing() {
1056 let payment_lookup_id = PaymentIdentifier::PaymentHash([42; 32]);
1057 let response =
1058 outgoing_payment_failure_response(&CurrencyUnit::Sat, payment_lookup_id.clone());
1059
1060 assert_eq!(response.payment_lookup_id, payment_lookup_id);
1061 assert_eq!(response.status, MeltQuoteState::Failed);
1062 assert_eq!(response.total_spent, Amount::new(0, CurrencyUnit::Sat));
1063 assert!(response.payment_proof.is_none());
1064 }
1065
1066 #[test]
1071 fn dispatch_boundary_errors_are_distinct_from_pre_dispatch_failure() {
1072 assert_ne!(
1076 Error::AmbiguousDispatch.to_string(),
1077 Error::PaymentFailed.to_string()
1078 );
1079 assert_ne!(
1080 Error::UnknownPaymentStatus.to_string(),
1081 Error::PaymentFailed.to_string()
1082 );
1083 }
1084}