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