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