cdk_lnd/
lib.rs

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