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, 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
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                onchain: None,
135                custom: std::collections::HashMap::new(),
136            },
137            unit,
138        })
139    }
140
141    /// Get last add and settle indices from KV store
142    #[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        // Get last indices from KV store
243        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                            // Stream is cancelled
293                            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                                    // Update indices based on the message
301                                    current_add_index = current_add_index.max(msg.add_index);
302                                    current_settle_index = current_settle_index.max(msg.settle_index);
303
304                                    // Store the updated indices in KV store regardless of settlement status
305                                    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                                    // Only emit event for settled invoices
333                                    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                                            // Invalid hash, skip this message but continue streaming
350                                            tracing::error!("LND returned invalid payment hash");
351                                            // Continue the loop without yielding
352                                            continue;
353                                        }
354                                    } else {
355                                        // Not a settled invoice, continue but don't emit event
356                                        tracing::debug!("LND: Received non-settled invoice, continuing to wait for settled invoices");
357                                        // Continue the loop without yielding
358                                        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                // Detect partial payments
471                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                            // Extract information from invoice
483                            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                                // Create a request for the routes
491                                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                                // Query the routes
507                                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                                // Get first route and update its MPP record
515                                let route =
516                                    routes_response.routes.first_mut().ok_or(Error::NoRoute)?;
517
518                                // attempt it and check the result
519                                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                                // Get status and maybe the preimage
549                                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                                // Get the actual amount paid in msats
560                                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                            // "We have exhausted all tactical options" -- STEM, Upgrade (2018)
575                            // The payment was not possible within 50 retries.
576                            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 waiting for the next update
810                            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                    // Handle the case where the update itself is an error (e.g., stream failure)
834                    return Err(Error::UnknownPaymentStatus.into());
835                }
836            }
837        }
838
839        // If the stream is exhausted without a final status
840        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}