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
217#[async_trait]
218impl MintPayment for Lnd {
219 type Err = payment::Error;
220
221 #[instrument(skip_all)]
222 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
223 Ok(self.settings.clone())
224 }
225
226 #[instrument(skip_all)]
227 fn is_payment_event_stream_active(&self) -> bool {
228 self.wait_invoice_is_active.load(Ordering::SeqCst)
229 }
230
231 #[instrument(skip_all)]
232 fn cancel_payment_event_stream(&self) {
233 self.wait_invoice_cancel_token.cancel()
234 }
235
236 #[instrument(skip_all)]
237 async fn wait_payment_event(
238 &self,
239 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
240 let mut lnd_client = self.lnd_client.clone();
241
242 let (last_add_index, last_settle_index) =
244 self.get_last_indices().await.unwrap_or((None, None));
245
246 let stream_req = lnrpc::InvoiceSubscription {
247 add_index: last_add_index.unwrap_or(0),
248 settle_index: last_settle_index.unwrap_or(0),
249 };
250
251 tracing::debug!(
252 "LND: Starting invoice subscription with add_index: {}, settle_index: {}",
253 stream_req.add_index,
254 stream_req.settle_index
255 );
256
257 let stream = lnd_client
258 .lightning()
259 .subscribe_invoices(stream_req)
260 .await
261 .map_err(|_err| {
262 tracing::error!("Could not subscribe to invoice");
263 Error::Connection
264 })?
265 .into_inner();
266
267 let cancel_token = self.wait_invoice_cancel_token.clone();
268 let kv_store = self.kv_store.clone();
269
270 let event_stream = futures::stream::unfold(
271 (
272 stream,
273 cancel_token,
274 Arc::clone(&self.wait_invoice_is_active),
275 kv_store,
276 last_add_index.unwrap_or(0),
277 last_settle_index.unwrap_or(0),
278 ),
279 |(
280 mut stream,
281 cancel_token,
282 is_active,
283 kv_store,
284 mut current_add_index,
285 mut current_settle_index,
286 )| async move {
287 is_active.store(true, Ordering::SeqCst);
288
289 loop {
290 tokio::select! {
291 _ = cancel_token.cancelled() => {
292 is_active.store(false, Ordering::SeqCst);
294 tracing::info!("Waiting for lnd invoice ending");
295 return None;
296 }
297 msg = stream.message() => {
298 match msg {
299 Ok(Some(msg)) => {
300 current_add_index = current_add_index.max(msg.add_index);
302 current_settle_index = current_settle_index.max(msg.settle_index);
303
304 let add_index_str = current_add_index.to_string();
306 let settle_index_str = current_settle_index.to_string();
307
308 if let Ok(mut tx) = kv_store.begin_transaction().await {
309 let mut has_error = false;
310
311 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 {
312 tracing::warn!("LND: Failed to write add_index {} to KV store: {}", current_add_index, e);
313 has_error = true;
314 }
315
316 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 {
317 tracing::warn!("LND: Failed to write settle_index {} to KV store: {}", current_settle_index, e);
318 has_error = true;
319 }
320
321 if !has_error {
322 if let Err(e) = tx.commit().await {
323 tracing::warn!("LND: Failed to commit indices to KV store: {}", e);
324 } else {
325 tracing::debug!("LND: Stored updated indices - add_index: {}, settle_index: {}", current_add_index, current_settle_index);
326 }
327 }
328 } else {
329 tracing::warn!("LND: Failed to begin KV transaction for storing indices");
330 }
331
332 if msg.state() == InvoiceState::Settled {
334 let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
335
336 if let Ok(hash_slice) = hash_slice {
337 let hash = hex::encode(hash_slice);
338
339 tracing::info!("LND: Payment for {} with amount {} msat", hash, msg.amt_paid_msat);
340
341 let wait_response = WaitPaymentResponse {
342 payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
343 payment_amount: Amount::new(msg.amt_paid_msat as u64, CurrencyUnit::Msat),
344 payment_id: hash,
345 };
346 let event = Event::PaymentReceived(wait_response);
347 return Some((event, (stream, cancel_token, is_active, kv_store, current_add_index, current_settle_index)));
348 } else {
349 tracing::error!("LND returned invalid payment hash");
351 continue;
353 }
354 } else {
355 tracing::debug!("LND: Received non-settled invoice, continuing to wait for settled invoices");
357 continue;
359 }
360 }
361 Ok(None) => {
362 is_active.store(false, Ordering::SeqCst);
363 tracing::info!("LND invoice stream ended.");
364 return None;
365 }
366 Err(err) => {
367 is_active.store(false, Ordering::SeqCst);
368 tracing::warn!("Encountered error in LND invoice stream. Stream ending");
369 tracing::error!("{:?}", err);
370 return None;
371 }
372 }
373 }
374 }
375 }
376 },
377 );
378
379 Ok(Box::pin(event_stream))
380 }
381
382 #[instrument(skip_all)]
383 async fn get_payment_quote(
384 &self,
385 unit: &CurrencyUnit,
386 options: OutgoingPaymentOptions,
387 ) -> Result<PaymentQuoteResponse, Self::Err> {
388 match options {
389 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
390 let amount_msat = match bolt11_options.melt_options {
391 Some(MeltOptions::Amountless { amountless }) => {
392 let amount_msat = amountless.amount_msat;
393
394 if let Some(invoice_amount) = bolt11_options.bolt11.amount_milli_satoshis()
395 {
396 if invoice_amount != u64::from(amount_msat) {
397 return Err(payment::Error::AmountMismatch);
398 }
399 }
400
401 amount_msat
402 }
403 Some(MeltOptions::Mpp { mpp }) => mpp.amount,
404 None => bolt11_options
405 .bolt11
406 .amount_milli_satoshis()
407 .ok_or(Error::UnknownInvoiceAmount)?
408 .into(),
409 };
410
411 let amount =
412 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
413
414 let relative_fee_reserve =
415 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
416
417 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
418
419 let fee = max(relative_fee_reserve, absolute_fee_reserve);
420
421 Ok(PaymentQuoteResponse {
422 request_lookup_id: Some(PaymentIdentifier::PaymentHash(
423 *bolt11_options.bolt11.payment_hash().as_ref(),
424 )),
425 amount,
426 fee: Amount::new(fee, unit.clone()),
427 state: MeltQuoteState::Unpaid,
428 extra_json: None,
429 estimated_blocks: None,
430 fee_options: None,
431 })
432 }
433 OutgoingPaymentOptions::Bolt12(_) => {
434 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
435 }
436 OutgoingPaymentOptions::Custom(_) | OutgoingPaymentOptions::Onchain(_) => {
437 Err(payment::Error::UnsupportedPaymentOption)
438 }
439 }
440 }
441
442 #[instrument(skip_all)]
443 async fn make_payment(
444 &self,
445 unit: &CurrencyUnit,
446 options: OutgoingPaymentOptions,
447 ) -> Result<MakePaymentResponse, Self::Err> {
448 match options {
449 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
450 let bolt11 = bolt11_options.bolt11;
451
452 let pay_state = self
453 .check_outgoing_payment(&PaymentIdentifier::PaymentHash(
454 *bolt11.payment_hash().as_ref(),
455 ))
456 .await?;
457
458 match pay_state.status {
459 MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
460 MeltQuoteState::Paid => {
461 tracing::debug!("Melt attempted on invoice already paid");
462 return Err(Self::Err::InvoiceAlreadyPaid);
463 }
464 MeltQuoteState::Pending => {
465 tracing::debug!("Melt attempted on invoice already pending");
466 return Err(Self::Err::InvoicePaymentPending);
467 }
468 }
469
470 match bolt11_options.melt_options {
472 Some(MeltOptions::Mpp { mpp }) => {
473 let amount_msat: u64 = bolt11
474 .amount_milli_satoshis()
475 .ok_or(Error::UnknownInvoiceAmount)?;
476 {
477 let partial_amount_msat = mpp.amount;
478 let invoice = bolt11;
479 let max_fee: Option<Amount<CurrencyUnit>> =
480 bolt11_options.max_fee_amount.clone();
481
482 let pub_key = invoice.get_payee_pub_key();
484 let payer_addr = invoice.payment_secret().0.to_vec();
485 let payment_hash = invoice.payment_hash();
486
487 let mut lnd_client = self.lnd_client.clone();
488
489 for attempt in 0..Self::MAX_ROUTE_RETRIES {
490 let route_req = lnrpc::QueryRoutesRequest {
492 pub_key: hex::encode(pub_key.serialize()),
493 amt_msat: u64::from(partial_amount_msat) as i64,
494 fee_limit: max_fee
495 .clone()
496 .map(|f| {
497 let fee_msat = f.to_msat()?;
498 let limit = Limit::FixedMsat(fee_msat as i64);
499 Ok::<_, Error>(FeeLimit { limit: Some(limit) })
500 })
501 .transpose()?,
502 use_mission_control: true,
503 ..Default::default()
504 };
505
506 let mut routes_response = lnd_client
508 .lightning()
509 .query_routes(route_req)
510 .await
511 .map_err(Error::LndError)?
512 .into_inner();
513
514 let route =
516 routes_response.routes.first_mut().ok_or(Error::NoRoute)?;
517
518 let last_hop: &mut Hop =
520 route.hops.last_mut().ok_or(Error::MissingLastHop)?;
521 let mpp_record = MppRecord {
522 payment_addr: payer_addr.clone(),
523 total_amt_msat: amount_msat as i64,
524 };
525 last_hop.mpp_record = Some(mpp_record);
526
527 let payment_response = lnd_client
528 .router()
529 .send_to_route_v2(routerrpc::SendToRouteRequest {
530 payment_hash: payment_hash.to_byte_array().to_vec(),
531 route: Some(route.clone()),
532 ..Default::default()
533 })
534 .await
535 .map_err(Error::LndError)?
536 .into_inner();
537
538 if let Some(failure) = payment_response.failure {
539 if failure.code == 15 {
540 tracing::debug!(
541 "Attempt number {}: route has failed. Re-querying...",
542 attempt + 1
543 );
544 continue;
545 }
546 }
547
548 let (status, payment_preimage) = match payment_response.status {
550 0 => (MeltQuoteState::Pending, None),
551 1 => (
552 MeltQuoteState::Paid,
553 Some(hex::encode(payment_response.preimage)),
554 ),
555 2 => (MeltQuoteState::Unpaid, None),
556 _ => (MeltQuoteState::Unknown, None),
557 };
558
559 let total_amt_msat: u64 = payment_response
561 .route
562 .map_or(0, |route| route.total_amt_msat as u64);
563
564 return Ok(MakePaymentResponse {
565 payment_lookup_id: PaymentIdentifier::PaymentHash(
566 payment_hash.to_byte_array(),
567 ),
568 payment_proof: payment_preimage,
569 status,
570 total_spent: msat_total_spent_for_unit(total_amt_msat, unit)?,
571 });
572 }
573
574 tracing::error!("Limit of retries reached, payment couldn't succeed.");
577 Err(Error::PaymentFailed.into())
578 }
579 }
580 _ => {
581 let mut lnd_client = self.lnd_client.clone();
582
583 let max_fee: Option<Amount<CurrencyUnit>> = bolt11_options.max_fee_amount;
584
585 let amount_msat = match bolt11_options.melt_options {
586 Some(MeltOptions::Amountless { amountless }) => {
587 let amount_msat = amountless.amount_msat;
588
589 if let Some(invoice_amount) = bolt11.amount_milli_satoshis() {
590 if invoice_amount != u64::from(amount_msat) {
591 return Err(payment::Error::AmountMismatch);
592 }
593 }
594
595 u64::from(amount_msat)
596 }
597 Some(MeltOptions::Mpp { mpp }) => u64::from(mpp.amount),
598 None => 0,
599 };
600
601 let fee_limit_msat = match max_fee {
602 Some(fee) => fee.convert_to(&CurrencyUnit::Msat)?.value() as i64,
603 None => 0,
604 };
605
606 let pay_req = routerrpc::SendPaymentRequest {
607 payment_request: bolt11.to_string(),
608 fee_limit_msat,
609 amt_msat: amount_msat as i64,
610 ..Default::default()
611 };
612
613 let mut payment_stream = lnd_client
614 .router()
615 .send_payment_v2(pay_req)
616 .await
617 .map_err(|err| {
618 tracing::warn!("Lightning payment failed: {}", err);
619 Error::PaymentFailed
620 })?
621 .into_inner();
622
623 while let Some(update) = payment_stream.message().await.map_err(|err| {
624 tracing::warn!("Lightning payment failed: {}", err);
625 Error::PaymentFailed
626 })? {
627 let status = update.status();
628
629 let response_status = match status {
630 PaymentStatus::InFlight | PaymentStatus::Initiated => {
631 continue;
632 }
633 PaymentStatus::Succeeded => MeltQuoteState::Paid,
634 PaymentStatus::Failed => MeltQuoteState::Failed,
635 #[allow(deprecated)]
636 PaymentStatus::Unknown => MeltQuoteState::Unknown,
637 };
638
639 let total_msat = update
640 .value_msat
641 .checked_add(update.fee_msat)
642 .ok_or(Error::AmountOverflow)?;
643
644 let payment_preimage = if update.payment_preimage.is_empty() {
645 None
646 } else {
647 Some(update.payment_preimage)
648 };
649
650 let payment_identifier =
651 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
652
653 return Ok(MakePaymentResponse {
654 payment_lookup_id: payment_identifier,
655 payment_proof: payment_preimage,
656 status: response_status,
657 total_spent: msat_total_spent_for_unit(total_msat as u64, unit)?,
658 });
659 }
660
661 Err(Error::UnknownPaymentStatus.into())
662 }
663 }
664 }
665 OutgoingPaymentOptions::Bolt12(_) => {
666 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
667 }
668 OutgoingPaymentOptions::Custom(_) | OutgoingPaymentOptions::Onchain(_) => {
669 Err(payment::Error::UnsupportedPaymentOption)
670 }
671 }
672 }
673
674 #[instrument(skip(self, options))]
675 async fn create_incoming_payment_request(
676 &self,
677 options: IncomingPaymentOptions,
678 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
679 match options {
680 IncomingPaymentOptions::Bolt11(bolt11_options) => {
681 let description = bolt11_options.description.unwrap_or_default();
682 let amount = bolt11_options.amount;
683 let unix_expiry = bolt11_options.unix_expiry;
684
685 let amount_msat: Amount = amount.convert_to(&CurrencyUnit::Msat)?.into();
686
687 let invoice_request = lnrpc::Invoice {
688 value_msat: u64::from(amount_msat) as i64,
689 memo: description,
690 expiry: unix_expiry
691 .map(|t| {
692 t.checked_sub(unix_time())
693 .ok_or(payment::Error::InvalidExpiry)
694 })
695 .transpose()?
696 .unwrap_or_default() as i64,
697 ..Default::default()
698 };
699
700 let mut lnd_client = self.lnd_client.clone();
701
702 let invoice = lnd_client
703 .lightning()
704 .add_invoice(tonic::Request::new(invoice_request))
705 .await
706 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
707 .into_inner();
708
709 let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
710
711 let payment_identifier =
712 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
713
714 let expiry = bolt11.expires_at().map(|t| t.as_secs());
715
716 Ok(CreateIncomingPaymentResponse {
717 request_lookup_id: payment_identifier,
718 request: bolt11.to_string(),
719 expiry,
720 extra_json: None,
721 })
722 }
723 IncomingPaymentOptions::Bolt12(_) => {
724 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
725 }
726 IncomingPaymentOptions::Custom(_) | IncomingPaymentOptions::Onchain(_) => {
727 Err(payment::Error::UnsupportedPaymentOption)
728 }
729 }
730 }
731
732 #[instrument(skip(self))]
733 async fn check_incoming_payment_status(
734 &self,
735 payment_identifier: &PaymentIdentifier,
736 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
737 let mut lnd_client = self.lnd_client.clone();
738
739 let invoice_request = lnrpc::PaymentHash {
740 r_hash: hex::decode(payment_identifier.to_string())?,
741 ..Default::default()
742 };
743
744 let invoice = lnd_client
745 .lightning()
746 .lookup_invoice(tonic::Request::new(invoice_request))
747 .await
748 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
749 .into_inner();
750
751 if invoice.state() == InvoiceState::Settled {
752 Ok(vec![WaitPaymentResponse {
753 payment_identifier: payment_identifier.clone(),
754 payment_amount: Amount::new(invoice.amt_paid_msat as u64, CurrencyUnit::Msat),
755 payment_id: hex::encode(invoice.r_hash),
756 }])
757 } else {
758 Ok(vec![])
759 }
760 }
761
762 #[instrument(skip(self))]
763 async fn check_outgoing_payment(
764 &self,
765 payment_identifier: &PaymentIdentifier,
766 ) -> Result<MakePaymentResponse, Self::Err> {
767 let mut lnd_client = self.lnd_client.clone();
768
769 let payment_hash = &payment_identifier.to_string();
770
771 let track_request = routerrpc::TrackPaymentRequest {
772 payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
773 no_inflight_updates: true,
774 };
775
776 let payment_response = lnd_client.router().track_payment_v2(track_request).await;
777
778 let mut payment_stream = match payment_response {
779 Ok(stream) => stream.into_inner(),
780 Err(err) => {
781 let err_code = err.code();
782 if err_code == tonic::Code::NotFound {
783 return Ok(MakePaymentResponse {
784 payment_lookup_id: payment_identifier.clone(),
785 payment_proof: None,
786 status: MeltQuoteState::Unknown,
787 total_spent: Amount::new(0, self.unit.clone()),
788 });
789 } else {
790 return Err(payment::Error::UnknownPaymentState);
791 }
792 }
793 };
794
795 while let Some(update_result) = payment_stream.next().await {
796 match update_result {
797 Ok(update) => {
798 let status = update.status();
799
800 let response = match status {
801 #[allow(deprecated)]
802 PaymentStatus::Unknown => MakePaymentResponse {
803 payment_lookup_id: payment_identifier.clone(),
804 payment_proof: Some(update.payment_preimage),
805 status: MeltQuoteState::Unknown,
806 total_spent: Amount::new(0, self.unit.clone()),
807 },
808 PaymentStatus::InFlight | PaymentStatus::Initiated => {
809 continue;
811 }
812 PaymentStatus::Succeeded => {
813 let total_spent = lnrpc_payment_total_spent(&update)?;
814
815 MakePaymentResponse {
816 payment_lookup_id: payment_identifier.clone(),
817 payment_proof: Some(update.payment_preimage),
818 status: MeltQuoteState::Paid,
819 total_spent,
820 }
821 }
822 PaymentStatus::Failed => MakePaymentResponse {
823 payment_lookup_id: payment_identifier.clone(),
824 payment_proof: Some(update.payment_preimage),
825 status: MeltQuoteState::Failed,
826 total_spent: Amount::new(0, self.unit.clone()),
827 },
828 };
829
830 return Ok(response);
831 }
832 Err(_) => {
833 return Err(Error::UnknownPaymentStatus.into());
835 }
836 }
837 }
838
839 Err(Error::UnknownPaymentStatus.into())
841 }
842}
843
844#[cfg(test)]
845mod tests {
846 use super::*;
847
848 #[test]
849 fn lnrpc_payment_total_spent_uses_msat_fields() {
850 let payment = lnrpc::Payment {
851 value_msat: 1500,
852 fee_msat: 500,
853 value_sat: 1,
854 fee_sat: 0,
855 ..Default::default()
856 };
857
858 let total_spent = lnrpc_payment_total_spent(&payment)
859 .expect("sub-sat payment total should be calculated");
860
861 assert_eq!(
862 total_spent
863 .convert_to(&CurrencyUnit::Msat)
864 .expect("msat amount should convert to msat")
865 .value(),
866 2000
867 );
868 }
869
870 #[test]
871 fn lnrpc_payment_total_spent_rejects_overflow() {
872 let payment = lnrpc::Payment {
873 value_msat: i64::MAX,
874 fee_msat: 1,
875 ..Default::default()
876 };
877
878 let err = lnrpc_payment_total_spent(&payment)
879 .expect_err("overflowing payment total should be rejected");
880
881 assert!(matches!(err, Error::AmountOverflow));
882 }
883
884 #[test]
885 fn msat_total_spent_for_unit_rounds_up_sats() {
886 let total_spent = msat_total_spent_for_unit(1501, &CurrencyUnit::Sat)
887 .expect("msat total should convert to sat");
888
889 assert_eq!(total_spent, Amount::new(2, CurrencyUnit::Sat));
890 }
891}