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#![warn(missing_docs)]
7#![warn(rustdoc::bare_urls)]
8
9use std::cmp::max;
10use std::path::PathBuf;
11use std::pin::Pin;
12use std::str::FromStr;
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::Arc;
15
16use anyhow::anyhow;
17use async_trait::async_trait;
18use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
19use cdk_common::bitcoin::hashes::Hash;
20use cdk_common::common::FeeReserve;
21use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
22use cdk_common::payment::{
23    self, Bolt11Settings, CreateIncomingPaymentResponse, IncomingPaymentOptions,
24    MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
25    PaymentQuoteResponse, WaitPaymentResponse,
26};
27use cdk_common::util::hex;
28use cdk_common::Bolt11Invoice;
29use error::Error;
30use futures::{Stream, StreamExt};
31use lnrpc::fee_limit::Limit;
32use lnrpc::payment::PaymentStatus;
33use lnrpc::{FeeLimit, Hop, MppRecord};
34use tokio_util::sync::CancellationToken;
35use tracing::instrument;
36
37mod client;
38pub mod error;
39
40mod proto;
41pub(crate) use proto::{lnrpc, routerrpc};
42
43use crate::lnrpc::invoice::InvoiceState;
44
45/// Lnd mint backend
46#[derive(Clone)]
47pub struct Lnd {
48    _address: String,
49    _cert_file: PathBuf,
50    _macaroon_file: PathBuf,
51    lnd_client: client::Client,
52    fee_reserve: FeeReserve,
53    wait_invoice_cancel_token: CancellationToken,
54    wait_invoice_is_active: Arc<AtomicBool>,
55    settings: Bolt11Settings,
56}
57
58impl Lnd {
59    /// Maximum number of attempts at a partial payment
60    pub const MAX_ROUTE_RETRIES: usize = 50;
61
62    /// Create new [`Lnd`]
63    pub async fn new(
64        address: String,
65        cert_file: PathBuf,
66        macaroon_file: PathBuf,
67        fee_reserve: FeeReserve,
68    ) -> Result<Self, Error> {
69        // Validate address is not empty
70        if address.is_empty() {
71            return Err(Error::InvalidConfig("LND address cannot be empty".into()));
72        }
73
74        // Validate cert_file exists and is not empty
75        if !cert_file.exists() || cert_file.metadata().map(|m| m.len() == 0).unwrap_or(true) {
76            return Err(Error::InvalidConfig(format!(
77                "LND certificate file not found or empty: {cert_file:?}"
78            )));
79        }
80
81        // Validate macaroon_file exists and is not empty
82        if !macaroon_file.exists()
83            || macaroon_file
84                .metadata()
85                .map(|m| m.len() == 0)
86                .unwrap_or(true)
87        {
88            return Err(Error::InvalidConfig(format!(
89                "LND macaroon file not found or empty: {macaroon_file:?}"
90            )));
91        }
92
93        let lnd_client = client::connect(&address, &cert_file, &macaroon_file)
94            .await
95            .map_err(|err| {
96                tracing::error!("Connection error: {}", err.to_string());
97                Error::Connection
98            })
99            .unwrap();
100
101        Ok(Self {
102            _address: address,
103            _cert_file: cert_file,
104            _macaroon_file: macaroon_file,
105            lnd_client,
106            fee_reserve,
107            wait_invoice_cancel_token: CancellationToken::new(),
108            wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
109            settings: Bolt11Settings {
110                mpp: true,
111                unit: CurrencyUnit::Msat,
112                invoice_description: true,
113                amountless: true,
114                bolt12: false,
115            },
116        })
117    }
118}
119
120#[async_trait]
121impl MintPayment for Lnd {
122    type Err = payment::Error;
123
124    #[instrument(skip_all)]
125    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
126        Ok(serde_json::to_value(&self.settings)?)
127    }
128
129    #[instrument(skip_all)]
130    fn is_wait_invoice_active(&self) -> bool {
131        self.wait_invoice_is_active.load(Ordering::SeqCst)
132    }
133
134    #[instrument(skip_all)]
135    fn cancel_wait_invoice(&self) {
136        self.wait_invoice_cancel_token.cancel()
137    }
138
139    #[instrument(skip_all)]
140    async fn wait_any_incoming_payment(
141        &self,
142    ) -> Result<Pin<Box<dyn Stream<Item = WaitPaymentResponse> + Send>>, Self::Err> {
143        let mut lnd_client = self.lnd_client.clone();
144
145        let stream_req = lnrpc::InvoiceSubscription {
146            add_index: 0,
147            settle_index: 0,
148        };
149
150        let stream = lnd_client
151            .lightning()
152            .subscribe_invoices(stream_req)
153            .await
154            .map_err(|_err| {
155                tracing::error!("Could not subscribe to invoice");
156                Error::Connection
157            })?
158            .into_inner();
159
160        let cancel_token = self.wait_invoice_cancel_token.clone();
161
162        Ok(futures::stream::unfold(
163            (
164                stream,
165                cancel_token,
166                Arc::clone(&self.wait_invoice_is_active),
167            ),
168            |(mut stream, cancel_token, is_active)| async move {
169                is_active.store(true, Ordering::SeqCst);
170
171                tokio::select! {
172                    _ = cancel_token.cancelled() => {
173                    // Stream is cancelled
174                    is_active.store(false, Ordering::SeqCst);
175                    tracing::info!("Waiting for lnd invoice ending");
176                    None
177
178                    }
179                    msg = stream.message() => {
180
181                match msg {
182                    Ok(Some(msg)) => {
183                        if msg.state() == InvoiceState::Settled {
184
185                            let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
186
187                            if let Ok(hash_slice) = hash_slice {
188                            let hash = hex::encode(hash_slice);
189
190                              tracing::info!("LND: Processing payment with hash: {}", hash);
191                                            let wait_response = WaitPaymentResponse {
192                                                payment_identifier: PaymentIdentifier::PaymentHash(hash_slice), payment_amount: Amount::from(msg.amt_paid_msat as u64),
193                                                unit: CurrencyUnit::Msat,
194                                                payment_id: hash,
195                                            };
196                                            tracing::info!("LND: Created WaitPaymentResponse with amount {} msat", 
197                                                         msg.amt_paid_msat);
198                                            Some((wait_response, (stream, cancel_token, is_active)))
199                            }  else { None }
200                        } else {
201                            None
202                        }
203                    }
204                    Ok(None) => {
205                    is_active.store(false, Ordering::SeqCst);
206                    tracing::info!("LND invoice stream ended.");
207                        None
208                    }, // End of stream
209                    Err(err) => {
210                    is_active.store(false, Ordering::SeqCst);
211                    tracing::warn!("Encountered error in LND invoice stream. Stream ending");
212                    tracing::error!("{:?}", err);
213                    None
214
215                    },   // Handle errors gracefully, ends the stream on error
216                }
217                    }
218                }
219            },
220        )
221        .boxed())
222    }
223
224    #[instrument(skip_all)]
225    async fn get_payment_quote(
226        &self,
227        unit: &CurrencyUnit,
228        options: OutgoingPaymentOptions,
229    ) -> Result<PaymentQuoteResponse, Self::Err> {
230        match options {
231            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
232                let amount_msat = match bolt11_options.melt_options {
233                    Some(amount) => amount.amount_msat(),
234                    None => bolt11_options
235                        .bolt11
236                        .amount_milli_satoshis()
237                        .ok_or(Error::UnknownInvoiceAmount)?
238                        .into(),
239                };
240
241                let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
242
243                let relative_fee_reserve =
244                    (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
245
246                let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
247
248                let fee = max(relative_fee_reserve, absolute_fee_reserve);
249
250                Ok(PaymentQuoteResponse {
251                    request_lookup_id: Some(PaymentIdentifier::PaymentHash(
252                        *bolt11_options.bolt11.payment_hash().as_ref(),
253                    )),
254                    amount,
255                    fee: fee.into(),
256                    state: MeltQuoteState::Unpaid,
257                    unit: unit.clone(),
258                })
259            }
260            OutgoingPaymentOptions::Bolt12(_) => {
261                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
262            }
263        }
264    }
265
266    #[instrument(skip_all)]
267    async fn make_payment(
268        &self,
269        _unit: &CurrencyUnit,
270        options: OutgoingPaymentOptions,
271    ) -> Result<MakePaymentResponse, Self::Err> {
272        match options {
273            OutgoingPaymentOptions::Bolt11(bolt11_options) => {
274                let bolt11 = bolt11_options.bolt11;
275
276                let pay_state = self
277                    .check_outgoing_payment(&PaymentIdentifier::PaymentHash(
278                        *bolt11.payment_hash().as_ref(),
279                    ))
280                    .await?;
281
282                match pay_state.status {
283                    MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
284                    MeltQuoteState::Paid => {
285                        tracing::debug!("Melt attempted on invoice already paid");
286                        return Err(Self::Err::InvoiceAlreadyPaid);
287                    }
288                    MeltQuoteState::Pending => {
289                        tracing::debug!("Melt attempted on invoice already pending");
290                        return Err(Self::Err::InvoicePaymentPending);
291                    }
292                }
293
294                // Detect partial payments
295                match bolt11_options.melt_options {
296                    Some(MeltOptions::Mpp { mpp }) => {
297                        let amount_msat: u64 = bolt11
298                            .amount_milli_satoshis()
299                            .ok_or(Error::UnknownInvoiceAmount)?;
300                        {
301                            let partial_amount_msat = mpp.amount;
302                            let invoice = bolt11;
303                            let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
304
305                            // Extract information from invoice
306                            let pub_key = invoice.get_payee_pub_key();
307                            let payer_addr = invoice.payment_secret().0.to_vec();
308                            let payment_hash = invoice.payment_hash();
309
310                            let mut lnd_client = self.lnd_client.clone();
311
312                            for attempt in 0..Self::MAX_ROUTE_RETRIES {
313                                // Create a request for the routes
314                                let route_req = lnrpc::QueryRoutesRequest {
315                                    pub_key: hex::encode(pub_key.serialize()),
316                                    amt_msat: u64::from(partial_amount_msat) as i64,
317                                    fee_limit: max_fee.map(|f| {
318                                        let limit = Limit::Fixed(u64::from(f) as i64);
319                                        FeeLimit { limit: Some(limit) }
320                                    }),
321                                    use_mission_control: true,
322                                    ..Default::default()
323                                };
324
325                                // Query the routes
326                                let mut routes_response = lnd_client
327                                    .lightning()
328                                    .query_routes(route_req)
329                                    .await
330                                    .map_err(Error::LndError)?
331                                    .into_inner();
332
333                                // update its MPP record,
334                                // attempt it and check the result
335                                let last_hop: &mut Hop = routes_response.routes[0]
336                                    .hops
337                                    .last_mut()
338                                    .ok_or(Error::MissingLastHop)?;
339                                let mpp_record = MppRecord {
340                                    payment_addr: payer_addr.clone(),
341                                    total_amt_msat: amount_msat as i64,
342                                };
343                                last_hop.mpp_record = Some(mpp_record);
344
345                                let payment_response = lnd_client
346                                    .router()
347                                    .send_to_route_v2(routerrpc::SendToRouteRequest {
348                                        payment_hash: payment_hash.to_byte_array().to_vec(),
349                                        route: Some(routes_response.routes[0].clone()),
350                                        ..Default::default()
351                                    })
352                                    .await
353                                    .map_err(Error::LndError)?
354                                    .into_inner();
355
356                                if let Some(failure) = payment_response.failure {
357                                    if failure.code == 15 {
358                                        tracing::debug!(
359                                            "Attempt number {}: route has failed. Re-querying...",
360                                            attempt + 1
361                                        );
362                                        continue;
363                                    }
364                                }
365
366                                // Get status and maybe the preimage
367                                let (status, payment_preimage) = match payment_response.status {
368                                    0 => (MeltQuoteState::Pending, None),
369                                    1 => (
370                                        MeltQuoteState::Paid,
371                                        Some(hex::encode(payment_response.preimage)),
372                                    ),
373                                    2 => (MeltQuoteState::Unpaid, None),
374                                    _ => (MeltQuoteState::Unknown, None),
375                                };
376
377                                // Get the actual amount paid in sats
378                                let mut total_amt: u64 = 0;
379                                if let Some(route) = payment_response.route {
380                                    total_amt = (route.total_amt_msat / 1000) as u64;
381                                }
382
383                                return Ok(MakePaymentResponse {
384                                    payment_lookup_id: PaymentIdentifier::PaymentHash(
385                                        payment_hash.to_byte_array(),
386                                    ),
387                                    payment_proof: payment_preimage,
388                                    status,
389                                    total_spent: total_amt.into(),
390                                    unit: CurrencyUnit::Sat,
391                                });
392                            }
393
394                            // "We have exhausted all tactical options" -- STEM, Upgrade (2018)
395                            // The payment was not possible within 50 retries.
396                            tracing::error!("Limit of retries reached, payment couldn't succeed.");
397                            Err(Error::PaymentFailed.into())
398                        }
399                    }
400                    _ => {
401                        let mut lnd_client = self.lnd_client.clone();
402
403                        let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
404
405                        let amount_msat = u64::from(
406                            bolt11_options
407                                .melt_options
408                                .map(|a| a.amount_msat())
409                                .unwrap_or_default(),
410                        );
411
412                        let pay_req = lnrpc::SendRequest {
413                            payment_request: bolt11.to_string(),
414                            fee_limit: max_fee.map(|f| {
415                                let limit = Limit::Fixed(u64::from(f) as i64);
416                                FeeLimit { limit: Some(limit) }
417                            }),
418                            amt_msat: amount_msat as i64,
419                            ..Default::default()
420                        };
421
422                        let payment_response = lnd_client
423                            .lightning()
424                            .send_payment_sync(tonic::Request::new(pay_req))
425                            .await
426                            .map_err(|err| {
427                                tracing::warn!("Lightning payment failed: {}", err);
428                                Error::PaymentFailed
429                            })?
430                            .into_inner();
431
432                        let total_amount = payment_response
433                            .payment_route
434                            .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
435                            as u64;
436
437                        let (status, payment_preimage) = match total_amount == 0 {
438                            true => (MeltQuoteState::Unpaid, None),
439                            false => (
440                                MeltQuoteState::Paid,
441                                Some(hex::encode(payment_response.payment_preimage)),
442                            ),
443                        };
444
445                        let payment_identifier =
446                            PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
447
448                        Ok(MakePaymentResponse {
449                            payment_lookup_id: payment_identifier,
450                            payment_proof: payment_preimage,
451                            status,
452                            total_spent: total_amount.into(),
453                            unit: CurrencyUnit::Sat,
454                        })
455                    }
456                }
457            }
458            OutgoingPaymentOptions::Bolt12(_) => {
459                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
460            }
461        }
462    }
463
464    #[instrument(skip(self, options))]
465    async fn create_incoming_payment_request(
466        &self,
467        unit: &CurrencyUnit,
468        options: IncomingPaymentOptions,
469    ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
470        match options {
471            IncomingPaymentOptions::Bolt11(bolt11_options) => {
472                let description = bolt11_options.description.unwrap_or_default();
473                let amount = bolt11_options.amount;
474                let unix_expiry = bolt11_options.unix_expiry;
475
476                let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
477
478                let invoice_request = lnrpc::Invoice {
479                    value_msat: u64::from(amount_msat) as i64,
480                    memo: description,
481                    ..Default::default()
482                };
483
484                let mut lnd_client = self.lnd_client.clone();
485
486                let invoice = lnd_client
487                    .lightning()
488                    .add_invoice(tonic::Request::new(invoice_request))
489                    .await
490                    .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
491                    .into_inner();
492
493                let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
494
495                let payment_identifier =
496                    PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
497
498                Ok(CreateIncomingPaymentResponse {
499                    request_lookup_id: payment_identifier,
500                    request: bolt11.to_string(),
501                    expiry: unix_expiry,
502                })
503            }
504            IncomingPaymentOptions::Bolt12(_) => {
505                Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
506            }
507        }
508    }
509
510    #[instrument(skip(self))]
511    async fn check_incoming_payment_status(
512        &self,
513        payment_identifier: &PaymentIdentifier,
514    ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
515        let mut lnd_client = self.lnd_client.clone();
516
517        let invoice_request = lnrpc::PaymentHash {
518            r_hash: hex::decode(payment_identifier.to_string()).unwrap(),
519            ..Default::default()
520        };
521
522        let invoice = lnd_client
523            .lightning()
524            .lookup_invoice(tonic::Request::new(invoice_request))
525            .await
526            .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
527            .into_inner();
528
529        if invoice.state() == InvoiceState::Settled {
530            Ok(vec![WaitPaymentResponse {
531                payment_identifier: payment_identifier.clone(),
532                payment_amount: Amount::from(invoice.amt_paid_msat as u64),
533                unit: CurrencyUnit::Msat,
534                payment_id: hex::encode(invoice.r_hash),
535            }])
536        } else {
537            Ok(vec![])
538        }
539    }
540
541    #[instrument(skip(self))]
542    async fn check_outgoing_payment(
543        &self,
544        payment_identifier: &PaymentIdentifier,
545    ) -> Result<MakePaymentResponse, Self::Err> {
546        let mut lnd_client = self.lnd_client.clone();
547
548        let payment_hash = &payment_identifier.to_string();
549
550        let track_request = routerrpc::TrackPaymentRequest {
551            payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
552            no_inflight_updates: true,
553        };
554
555        let payment_response = lnd_client.router().track_payment_v2(track_request).await;
556
557        let mut payment_stream = match payment_response {
558            Ok(stream) => stream.into_inner(),
559            Err(err) => {
560                let err_code = err.code();
561                if err_code == tonic::Code::NotFound {
562                    return Ok(MakePaymentResponse {
563                        payment_lookup_id: payment_identifier.clone(),
564                        payment_proof: None,
565                        status: MeltQuoteState::Unknown,
566                        total_spent: Amount::ZERO,
567                        unit: self.settings.unit.clone(),
568                    });
569                } else {
570                    return Err(payment::Error::UnknownPaymentState);
571                }
572            }
573        };
574
575        while let Some(update_result) = payment_stream.next().await {
576            match update_result {
577                Ok(update) => {
578                    let status = update.status();
579
580                    let response = match status {
581                        PaymentStatus::Unknown => MakePaymentResponse {
582                            payment_lookup_id: payment_identifier.clone(),
583                            payment_proof: Some(update.payment_preimage),
584                            status: MeltQuoteState::Unknown,
585                            total_spent: Amount::ZERO,
586                            unit: self.settings.unit.clone(),
587                        },
588                        PaymentStatus::InFlight | PaymentStatus::Initiated => {
589                            // Continue waiting for the next update
590                            continue;
591                        }
592                        PaymentStatus::Succeeded => MakePaymentResponse {
593                            payment_lookup_id: payment_identifier.clone(),
594                            payment_proof: Some(update.payment_preimage),
595                            status: MeltQuoteState::Paid,
596                            total_spent: Amount::from(
597                                (update
598                                    .value_sat
599                                    .checked_add(update.fee_sat)
600                                    .ok_or(Error::AmountOverflow)?)
601                                    as u64,
602                            ),
603                            unit: CurrencyUnit::Sat,
604                        },
605                        PaymentStatus::Failed => MakePaymentResponse {
606                            payment_lookup_id: payment_identifier.clone(),
607                            payment_proof: Some(update.payment_preimage),
608                            status: MeltQuoteState::Failed,
609                            total_spent: Amount::ZERO,
610                            unit: self.settings.unit.clone(),
611                        },
612                    };
613
614                    return Ok(response);
615                }
616                Err(_) => {
617                    // Handle the case where the update itself is an error (e.g., stream failure)
618                    return Err(Error::UnknownPaymentStatus.into());
619                }
620            }
621        }
622
623        // If the stream is exhausted without a final status
624        Err(Error::UnknownPaymentStatus.into())
625    }
626}