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