Skip to main content

cdk_lnd/
lib.rs

1//! CDK lightning backend for LND
2
3// Copyright (c) 2023 Steffen (MIT)
4
5#![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;
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
44/// LND KV Store constants
45const 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/// Lnd mint backend
51#[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    /// Maximum number of attempts at a partial payment
75    pub const MAX_ROUTE_RETRIES: usize = 50;
76
77    /// Create new [`Lnd`]
78    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        // Validate address is not empty
86        if address.is_empty() {
87            return Err(Error::InvalidConfig("LND address cannot be empty".into()));
88        }
89
90        // Validate cert_file exists and is not empty
91        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        // Validate macaroon_file exists and is not empty
98        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                custom: std::collections::HashMap::new(),
135            },
136            unit,
137        })
138    }
139
140    /// Get last add and settle indices from KV store
141    #[instrument(skip_all)]
142    async fn get_last_indices(&self) -> Result<(Option<u64>, Option<u64>), Error> {
143        let add_index = if let Some(stored_index) = self
144            .kv_store
145            .kv_read(
146                LND_KV_PRIMARY_NAMESPACE,
147                LND_KV_SECONDARY_NAMESPACE,
148                LAST_ADD_INDEX_KV_KEY,
149            )
150            .await
151            .map_err(|e| Error::Database(e.to_string()))?
152        {
153            if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
154                index_str.parse::<u64>().ok()
155            } else {
156                None
157            }
158        } else {
159            None
160        };
161
162        let settle_index = if let Some(stored_index) = self
163            .kv_store
164            .kv_read(
165                LND_KV_PRIMARY_NAMESPACE,
166                LND_KV_SECONDARY_NAMESPACE,
167                LAST_SETTLE_INDEX_KV_KEY,
168            )
169            .await
170            .map_err(|e| Error::Database(e.to_string()))?
171        {
172            if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
173                index_str.parse::<u64>().ok()
174            } else {
175                None
176            }
177        } else {
178            None
179        };
180
181        tracing::debug!(
182            "LND: Retrieved last indices from KV store - add_index: {:?}, settle_index: {:?}",
183            add_index,
184            settle_index
185        );
186        Ok((add_index, settle_index))
187    }
188}
189
190#[async_trait]
191impl MintPayment for Lnd {
192    type Err = payment::Error;
193
194    #[instrument(skip_all)]
195    async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
196        Ok(self.settings.clone())
197    }
198
199    #[instrument(skip_all)]
200    fn is_wait_invoice_active(&self) -> bool {
201        self.wait_invoice_is_active.load(Ordering::SeqCst)
202    }
203
204    #[instrument(skip_all)]
205    fn cancel_wait_invoice(&self) {
206        self.wait_invoice_cancel_token.cancel()
207    }
208
209    #[instrument(skip_all)]
210    async fn wait_payment_event(
211        &self,
212    ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
213        let mut lnd_client = self.lnd_client.clone();
214
215        // Get last indices from KV store
216        let (last_add_index, last_settle_index) =
217            self.get_last_indices().await.unwrap_or((None, None));
218
219        let stream_req = lnrpc::InvoiceSubscription {
220            add_index: last_add_index.unwrap_or(0),
221            settle_index: last_settle_index.unwrap_or(0),
222        };
223
224        tracing::debug!(
225            "LND: Starting invoice subscription with add_index: {}, settle_index: {}",
226            stream_req.add_index,
227            stream_req.settle_index
228        );
229
230        let stream = lnd_client
231            .lightning()
232            .subscribe_invoices(stream_req)
233            .await
234            .map_err(|_err| {
235                tracing::error!("Could not subscribe to invoice");
236                Error::Connection
237            })?
238            .into_inner();
239
240        let cancel_token = self.wait_invoice_cancel_token.clone();
241        let kv_store = self.kv_store.clone();
242
243        let event_stream = futures::stream::unfold(
244            (
245                stream,
246                cancel_token,
247                Arc::clone(&self.wait_invoice_is_active),
248                kv_store,
249                last_add_index.unwrap_or(0),
250                last_settle_index.unwrap_or(0),
251            ),
252            |(
253                mut stream,
254                cancel_token,
255                is_active,
256                kv_store,
257                mut current_add_index,
258                mut current_settle_index,
259            )| async move {
260                is_active.store(true, Ordering::SeqCst);
261
262                loop {
263                    tokio::select! {
264                        _ = cancel_token.cancelled() => {
265                            // Stream is cancelled
266                            is_active.store(false, Ordering::SeqCst);
267                            tracing::info!("Waiting for lnd invoice ending");
268                            return None;
269                        }
270                        msg = stream.message() => {
271                            match msg {
272                                Ok(Some(msg)) => {
273                                    // Update indices based on the message
274                                    current_add_index = current_add_index.max(msg.add_index);
275                                    current_settle_index = current_settle_index.max(msg.settle_index);
276
277                                    // Store the updated indices in KV store regardless of settlement status
278                                    let add_index_str = current_add_index.to_string();
279                                    let settle_index_str = current_settle_index.to_string();
280
281                                    if let Ok(mut tx) = kv_store.begin_transaction().await {
282                                        let mut has_error = false;
283
284                                        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 {
285                                            tracing::warn!("LND: Failed to write add_index {} to KV store: {}", current_add_index, e);
286                                            has_error = true;
287                                        }
288
289                                        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 {
290                                            tracing::warn!("LND: Failed to write settle_index {} to KV store: {}", current_settle_index, e);
291                                            has_error = true;
292                                        }
293
294                                        if !has_error {
295                                            if let Err(e) = tx.commit().await {
296                                                tracing::warn!("LND: Failed to commit indices to KV store: {}", e);
297                                            } else {
298                                                tracing::debug!("LND: Stored updated indices - add_index: {}, settle_index: {}", current_add_index, current_settle_index);
299                                            }
300                                        }
301                                    } else {
302                                        tracing::warn!("LND: Failed to begin KV transaction for storing indices");
303                                    }
304
305                                    // Only emit event for settled invoices
306                                    if msg.state() == InvoiceState::Settled {
307                                        let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
308
309                                        if let Ok(hash_slice) = hash_slice {
310                                            let hash = hex::encode(hash_slice);
311
312                                            tracing::info!("LND: Payment for {} with amount {} msat", hash,  msg.amt_paid_msat);
313
314                                            let wait_response = WaitPaymentResponse {
315                                                payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
316                                                payment_amount: Amount::new(msg.amt_paid_msat as u64, CurrencyUnit::Msat),
317                                                payment_id: hash,
318                                            };
319                                            let event = Event::PaymentReceived(wait_response);
320                                            return Some((event, (stream, cancel_token, is_active, kv_store, current_add_index, current_settle_index)));
321                                        } else {
322                                            // Invalid hash, skip this message but continue streaming
323                                            tracing::error!("LND returned invalid payment hash");
324                                            // Continue the loop without yielding
325                                            continue;
326                                        }
327                                    } else {
328                                        // Not a settled invoice, continue but don't emit event
329                                        tracing::debug!("LND: Received non-settled invoice, continuing to wait for settled invoices");
330                                        // Continue the loop without yielding
331                                        continue;
332                                    }
333                                }
334                                Ok(None) => {
335                                    is_active.store(false, Ordering::SeqCst);
336                                    tracing::info!("LND invoice stream ended.");
337                                    return None;
338                                }
339                                Err(err) => {
340                                    is_active.store(false, Ordering::SeqCst);
341                                    tracing::warn!("Encountered error in LND invoice stream. Stream ending");
342                                    tracing::error!("{:?}", err);
343                                    return None;
344                                }
345                            }
346                        }
347                    }
348                }
349            },
350        );
351
352        Ok(Box::pin(event_stream))
353    }
354
355    #[instrument(skip_all)]
356    async fn get_payment_quote(
357        &self,
358        unit: &CurrencyUnit,
359        options: OutgoingPaymentOptions,
360    ) -> Result<PaymentQuoteResponse, Self::Err> {
361        match options {
362            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
363                let amount_msat = match bolt11_options.melt_options {
364                    Some(amount) => amount.amount_msat(),
365                    None => bolt11_options
366                        .bolt11
367                        .amount_milli_satoshis()
368                        .ok_or(Error::UnknownInvoiceAmount)?
369                        .into(),
370                };
371
372                let amount =
373                    Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
374
375                let relative_fee_reserve =
376                    (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
377
378                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
379
380                let fee = max(relative_fee_reserve, absolute_fee_reserve);
381
382                Ok(PaymentQuoteResponse {
383                    request_lookup_id: Some(PaymentIdentifier::PaymentHash(
384                        *bolt11_options.bolt11.payment_hash().as_ref(),
385                    )),
386                    amount,
387                    fee: Amount::new(fee, unit.clone()),
388                    state: MeltQuoteState::Unpaid,
389                })
390            }
391            OutgoingPaymentOptions::Bolt12(_) => {
392                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
393            }
394            OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
395        }
396    }
397
398    #[instrument(skip_all)]
399    async fn make_payment(
400        &self,
401        unit: &CurrencyUnit,
402        options: OutgoingPaymentOptions,
403    ) -> Result<MakePaymentResponse, Self::Err> {
404        match options {
405            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
406                let bolt11 = bolt11_options.bolt11;
407
408                let pay_state = self
409                    .check_outgoing_payment(&PaymentIdentifier::PaymentHash(
410                        *bolt11.payment_hash().as_ref(),
411                    ))
412                    .await?;
413
414                match pay_state.status {
415                    MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
416                    MeltQuoteState::Paid => {
417                        tracing::debug!("Melt attempted on invoice already paid");
418                        return Err(Self::Err::InvoiceAlreadyPaid);
419                    }
420                    MeltQuoteState::Pending => {
421                        tracing::debug!("Melt attempted on invoice already pending");
422                        return Err(Self::Err::InvoicePaymentPending);
423                    }
424                }
425
426                // Detect partial payments
427                match bolt11_options.melt_options {
428                    Some(MeltOptions::Mpp { mpp }) => {
429                        let amount_msat: u64 = bolt11
430                            .amount_milli_satoshis()
431                            .ok_or(Error::UnknownInvoiceAmount)?;
432                        {
433                            let partial_amount_msat = mpp.amount;
434                            let invoice = bolt11;
435                            let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
436
437                            // Extract information from invoice
438                            let pub_key = invoice.get_payee_pub_key();
439                            let payer_addr = invoice.payment_secret().0.to_vec();
440                            let payment_hash = invoice.payment_hash();
441
442                            let mut lnd_client = self.lnd_client.clone();
443
444                            for attempt in 0..Self::MAX_ROUTE_RETRIES {
445                                // Create a request for the routes
446                                let route_req = lnrpc::QueryRoutesRequest {
447                                    pub_key: hex::encode(pub_key.serialize()),
448                                    amt_msat: u64::from(partial_amount_msat) as i64,
449                                    fee_limit: max_fee
450                                        .map(|f| {
451                                            let fee_msat = Amount::new(f.into(), unit.clone())
452                                                .convert_to(&CurrencyUnit::Msat)?
453                                                .value();
454                                            let limit = Limit::FixedMsat(fee_msat as i64);
455                                            Ok::<_, Error>(FeeLimit { limit: Some(limit) })
456                                        })
457                                        .transpose()?,
458                                    use_mission_control: true,
459                                    ..Default::default()
460                                };
461
462                                // Query the routes
463                                let mut routes_response = lnd_client
464                                    .lightning()
465                                    .query_routes(route_req)
466                                    .await
467                                    .map_err(Error::LndError)?
468                                    .into_inner();
469
470                                // update its MPP record,
471                                // attempt it and check the result
472                                let last_hop: &mut Hop = routes_response.routes[0]
473                                    .hops
474                                    .last_mut()
475                                    .ok_or(Error::MissingLastHop)?;
476                                let mpp_record = MppRecord {
477                                    payment_addr: payer_addr.clone(),
478                                    total_amt_msat: amount_msat as i64,
479                                };
480                                last_hop.mpp_record = Some(mpp_record);
481
482                                let payment_response = lnd_client
483                                    .router()
484                                    .send_to_route_v2(routerrpc::SendToRouteRequest {
485                                        payment_hash: payment_hash.to_byte_array().to_vec(),
486                                        route: Some(routes_response.routes[0].clone()),
487                                        ..Default::default()
488                                    })
489                                    .await
490                                    .map_err(Error::LndError)?
491                                    .into_inner();
492
493                                if let Some(failure) = payment_response.failure {
494                                    if failure.code == 15 {
495                                        tracing::debug!(
496                                            "Attempt number {}: route has failed. Re-querying...",
497                                            attempt + 1
498                                        );
499                                        continue;
500                                    }
501                                }
502
503                                // Get status and maybe the preimage
504                                let (status, payment_preimage) = match payment_response.status {
505                                    0 => (MeltQuoteState::Pending, None),
506                                    1 => (
507                                        MeltQuoteState::Paid,
508                                        Some(hex::encode(payment_response.preimage)),
509                                    ),
510                                    2 => (MeltQuoteState::Unpaid, None),
511                                    _ => (MeltQuoteState::Unknown, None),
512                                };
513
514                                // Get the actual amount paid in sats
515                                let mut total_amt: u64 = 0;
516                                if let Some(route) = payment_response.route {
517                                    total_amt = (route.total_amt_msat / 1000) as u64;
518                                }
519
520                                return Ok(MakePaymentResponse {
521                                    payment_lookup_id: PaymentIdentifier::PaymentHash(
522                                        payment_hash.to_byte_array(),
523                                    ),
524                                    payment_proof: payment_preimage,
525                                    status,
526                                    total_spent: Amount::new(total_amt, CurrencyUnit::Sat),
527                                });
528                            }
529
530                            // "We have exhausted all tactical options" -- STEM, Upgrade (2018)
531                            // The payment was not possible within 50 retries.
532                            tracing::error!("Limit of retries reached, payment couldn't succeed.");
533                            Err(Error::PaymentFailed.into())
534                        }
535                    }
536                    _ => {
537                        let mut lnd_client = self.lnd_client.clone();
538
539                        let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
540
541                        let amount_msat = u64::from(
542                            bolt11_options
543                                .melt_options
544                                .map(|a| a.amount_msat())
545                                .unwrap_or_default(),
546                        );
547
548                        let pay_req = lnrpc::SendRequest {
549                            payment_request: bolt11.to_string(),
550                            fee_limit: max_fee
551                                .map(|f| {
552                                    let fee_msat = Amount::new(f.into(), unit.clone())
553                                        .convert_to(&CurrencyUnit::Msat)?
554                                        .value();
555                                    let limit = Limit::FixedMsat(fee_msat as i64);
556                                    Ok::<_, Error>(FeeLimit { limit: Some(limit) })
557                                })
558                                .transpose()?,
559                            amt_msat: amount_msat as i64,
560                            ..Default::default()
561                        };
562
563                        let payment_response = lnd_client
564                            .lightning()
565                            .send_payment_sync(tonic::Request::new(pay_req))
566                            .await
567                            .map_err(|err| {
568                                tracing::warn!("Lightning payment failed: {}", err);
569                                Error::PaymentFailed
570                            })?
571                            .into_inner();
572
573                        let total_amount = payment_response
574                            .payment_route
575                            .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
576                            as u64;
577
578                        let (status, payment_preimage) = match total_amount == 0 {
579                            true => (MeltQuoteState::Unpaid, None),
580                            false => (
581                                MeltQuoteState::Paid,
582                                Some(hex::encode(payment_response.payment_preimage)),
583                            ),
584                        };
585
586                        let payment_identifier =
587                            PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
588
589                        Ok(MakePaymentResponse {
590                            payment_lookup_id: payment_identifier,
591                            payment_proof: payment_preimage,
592                            status,
593                            total_spent: Amount::new(total_amount, CurrencyUnit::Sat),
594                        })
595                    }
596                }
597            }
598            OutgoingPaymentOptions::Bolt12(_) => {
599                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
600            }
601            OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
602        }
603    }
604
605    #[instrument(skip(self, options))]
606    async fn create_incoming_payment_request(
607        &self,
608        unit: &CurrencyUnit,
609        options: IncomingPaymentOptions,
610    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
611        match options {
612            IncomingPaymentOptions::Bolt11(bolt11_options) => {
613                let description = bolt11_options.description.unwrap_or_default();
614                let amount = bolt11_options.amount;
615                let unix_expiry = bolt11_options.unix_expiry;
616
617                let amount_msat: Amount = Amount::new(amount.into(), unit.clone())
618                    .convert_to(&CurrencyUnit::Msat)?
619                    .into();
620
621                let invoice_request = lnrpc::Invoice {
622                    value_msat: u64::from(amount_msat) as i64,
623                    memo: description,
624                    ..Default::default()
625                };
626
627                let mut lnd_client = self.lnd_client.clone();
628
629                let invoice = lnd_client
630                    .lightning()
631                    .add_invoice(tonic::Request::new(invoice_request))
632                    .await
633                    .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
634                    .into_inner();
635
636                let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
637
638                let payment_identifier =
639                    PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
640
641                Ok(CreateIncomingPaymentResponse {
642                    request_lookup_id: payment_identifier,
643                    request: bolt11.to_string(),
644                    expiry: unix_expiry,
645                    extra_json: None,
646                })
647            }
648            IncomingPaymentOptions::Bolt12(_) => {
649                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
650            }
651            IncomingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
652        }
653    }
654
655    #[instrument(skip(self))]
656    async fn check_incoming_payment_status(
657        &self,
658        payment_identifier: &PaymentIdentifier,
659    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
660        let mut lnd_client = self.lnd_client.clone();
661
662        let invoice_request = lnrpc::PaymentHash {
663            r_hash: hex::decode(payment_identifier.to_string())?,
664            ..Default::default()
665        };
666
667        let invoice = lnd_client
668            .lightning()
669            .lookup_invoice(tonic::Request::new(invoice_request))
670            .await
671            .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
672            .into_inner();
673
674        if invoice.state() == InvoiceState::Settled {
675            Ok(vec![WaitPaymentResponse {
676                payment_identifier: payment_identifier.clone(),
677                payment_amount: Amount::new(invoice.amt_paid_msat as u64, CurrencyUnit::Msat),
678                payment_id: hex::encode(invoice.r_hash),
679            }])
680        } else {
681            Ok(vec![])
682        }
683    }
684
685    #[instrument(skip(self))]
686    async fn check_outgoing_payment(
687        &self,
688        payment_identifier: &PaymentIdentifier,
689    ) -> Result<MakePaymentResponse, Self::Err> {
690        let mut lnd_client = self.lnd_client.clone();
691
692        let payment_hash = &payment_identifier.to_string();
693
694        let track_request = routerrpc::TrackPaymentRequest {
695            payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
696            no_inflight_updates: true,
697        };
698
699        let payment_response = lnd_client.router().track_payment_v2(track_request).await;
700
701        let mut payment_stream = match payment_response {
702            Ok(stream) => stream.into_inner(),
703            Err(err) => {
704                let err_code = err.code();
705                if err_code == tonic::Code::NotFound {
706                    return Ok(MakePaymentResponse {
707                        payment_lookup_id: payment_identifier.clone(),
708                        payment_proof: None,
709                        status: MeltQuoteState::Unknown,
710                        total_spent: Amount::new(0, self.unit.clone()),
711                    });
712                } else {
713                    return Err(payment::Error::UnknownPaymentState);
714                }
715            }
716        };
717
718        while let Some(update_result) = payment_stream.next().await {
719            match update_result {
720                Ok(update) => {
721                    let status = update.status();
722
723                    let response = match status {
724                        #[allow(deprecated)]
725                        PaymentStatus::Unknown => MakePaymentResponse {
726                            payment_lookup_id: payment_identifier.clone(),
727                            payment_proof: Some(update.payment_preimage),
728                            status: MeltQuoteState::Unknown,
729                            total_spent: Amount::new(0, self.unit.clone()),
730                        },
731                        PaymentStatus::InFlight | PaymentStatus::Initiated => {
732                            // Continue waiting for the next update
733                            continue;
734                        }
735                        PaymentStatus::Succeeded => MakePaymentResponse {
736                            payment_lookup_id: payment_identifier.clone(),
737                            payment_proof: Some(update.payment_preimage),
738                            status: MeltQuoteState::Paid,
739                            total_spent: Amount::new(
740                                (update
741                                    .value_sat
742                                    .checked_add(update.fee_sat)
743                                    .ok_or(Error::AmountOverflow)?)
744                                    as u64,
745                                CurrencyUnit::Sat,
746                            ),
747                        },
748                        PaymentStatus::Failed => MakePaymentResponse {
749                            payment_lookup_id: payment_identifier.clone(),
750                            payment_proof: Some(update.payment_preimage),
751                            status: MeltQuoteState::Failed,
752                            total_spent: Amount::new(0, self.unit.clone()),
753                        },
754                    };
755
756                    return Ok(response);
757                }
758                Err(_) => {
759                    // Handle the case where the update itself is an error (e.g., stream failure)
760                    return Err(Error::UnknownPaymentStatus.into());
761                }
762            }
763        }
764
765        // If the stream is exhausted without a final status
766        Err(Error::UnknownPaymentStatus.into())
767    }
768}