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::amount::{to_unit, Amount, MSAT_IN_SAT};
19use cdk::cdk_payment::{
20    self, Bolt11Settings, CreateIncomingPaymentResponse, MakePaymentResponse, MintPayment,
21    PaymentQuoteResponse,
22};
23use cdk::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState, MintQuoteState};
24use cdk::secp256k1::hashes::Hash;
25use cdk::types::FeeReserve;
26use cdk::util::hex;
27use cdk::{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, HtlcAttempt, 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    /// Create new [`Lnd`]
56    pub async fn new(
57        address: String,
58        cert_file: PathBuf,
59        macaroon_file: PathBuf,
60        fee_reserve: FeeReserve,
61    ) -> Result<Self, Error> {
62        // Validate address is not empty
63        if address.is_empty() {
64            return Err(Error::InvalidConfig("LND address cannot be empty".into()));
65        }
66
67        // Validate cert_file exists and is not empty
68        if !cert_file.exists() || cert_file.metadata().map(|m| m.len() == 0).unwrap_or(true) {
69            return Err(Error::InvalidConfig(format!(
70                "LND certificate file not found or empty: {:?}",
71                cert_file
72            )));
73        }
74
75        // Validate macaroon_file exists and is not empty
76        if !macaroon_file.exists()
77            || macaroon_file
78                .metadata()
79                .map(|m| m.len() == 0)
80                .unwrap_or(true)
81        {
82            return Err(Error::InvalidConfig(format!(
83                "LND macaroon file not found or empty: {:?}",
84                macaroon_file
85            )));
86        }
87
88        let client = fedimint_tonic_lnd::connect(address.to_string(), &cert_file, &macaroon_file)
89            .await
90            .map_err(|err| {
91                tracing::error!("Connection error: {}", err.to_string());
92                Error::Connection
93            })?;
94
95        Ok(Self {
96            address,
97            cert_file,
98            macaroon_file,
99            client: Arc::new(Mutex::new(client)),
100            fee_reserve,
101            wait_invoice_cancel_token: CancellationToken::new(),
102            wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
103            settings: Bolt11Settings {
104                mpp: true,
105                unit: CurrencyUnit::Msat,
106                invoice_description: true,
107                amountless: true,
108            },
109        })
110    }
111}
112
113#[async_trait]
114impl MintPayment for Lnd {
115    type Err = cdk_payment::Error;
116
117    #[instrument(skip_all)]
118    async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
119        Ok(serde_json::to_value(&self.settings)?)
120    }
121
122    #[instrument(skip_all)]
123    fn is_wait_invoice_active(&self) -> bool {
124        self.wait_invoice_is_active.load(Ordering::SeqCst)
125    }
126
127    #[instrument(skip_all)]
128    fn cancel_wait_invoice(&self) {
129        self.wait_invoice_cancel_token.cancel()
130    }
131
132    #[instrument(skip_all)]
133    async fn wait_any_incoming_payment(
134        &self,
135    ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
136        let mut client =
137            fedimint_tonic_lnd::connect(self.address.clone(), &self.cert_file, &self.macaroon_file)
138                .await
139                .map_err(|_| Error::Connection)?;
140
141        let stream_req = fedimint_tonic_lnd::lnrpc::InvoiceSubscription {
142            add_index: 0,
143            settle_index: 0,
144        };
145
146        let stream = 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            fee: fee.into(),
235            state: MeltQuoteState::Unpaid,
236        })
237    }
238
239    #[instrument(skip_all)]
240    async fn make_payment(
241        &self,
242        melt_quote: mint::MeltQuote,
243        partial_amount: Option<Amount>,
244        max_fee: Option<Amount>,
245    ) -> Result<MakePaymentResponse, Self::Err> {
246        let payment_request = melt_quote.request;
247        let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
248
249        let pay_state = self
250            .check_outgoing_payment(&bolt11.payment_hash().to_string())
251            .await?;
252
253        match pay_state.status {
254            MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
255            MeltQuoteState::Paid => {
256                tracing::debug!("Melt attempted on invoice already paid");
257                return Err(Self::Err::InvoiceAlreadyPaid);
258            }
259            MeltQuoteState::Pending => {
260                tracing::debug!("Melt attempted on invoice already pending");
261                return Err(Self::Err::InvoicePaymentPending);
262            }
263        }
264
265        let bolt11 = Bolt11Invoice::from_str(&payment_request)?;
266        let amount_msat: u64 = match bolt11.amount_milli_satoshis() {
267            Some(amount_msat) => amount_msat,
268            None => melt_quote
269                .msat_to_pay
270                .ok_or(Error::UnknownInvoiceAmount)?
271                .into(),
272        };
273
274        // Detect partial payments
275        match partial_amount {
276            Some(part_amt) => {
277                let partial_amount_msat = to_unit(part_amt, &melt_quote.unit, &CurrencyUnit::Msat)?;
278                let invoice = Bolt11Invoice::from_str(&payment_request)?;
279
280                // Extract information from invoice
281                let pub_key = invoice.get_payee_pub_key();
282                let payer_addr = invoice.payment_secret().0.to_vec();
283                let payment_hash = invoice.payment_hash();
284
285                // Create a request for the routes
286                let route_req = fedimint_tonic_lnd::lnrpc::QueryRoutesRequest {
287                    pub_key: hex::encode(pub_key.serialize()),
288                    amt_msat: u64::from(partial_amount_msat) as i64,
289                    fee_limit: max_fee.map(|f| {
290                        let limit = Limit::Fixed(u64::from(f) as i64);
291                        FeeLimit { limit: Some(limit) }
292                    }),
293                    ..Default::default()
294                };
295
296                // Query the routes
297                let routes_response: fedimint_tonic_lnd::lnrpc::QueryRoutesResponse = self
298                    .client
299                    .lock()
300                    .await
301                    .lightning()
302                    .query_routes(route_req)
303                    .await
304                    .map_err(Error::LndError)?
305                    .into_inner();
306
307                let mut payment_response: HtlcAttempt = HtlcAttempt {
308                    ..Default::default()
309                };
310
311                // For each route:
312                // update its MPP record,
313                // attempt it and check the result
314                for mut route in routes_response.routes.into_iter() {
315                    let last_hop: &mut Hop = route.hops.last_mut().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                    tracing::debug!("sendToRouteV2 needle");
322                    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(route),
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                            // Try a different route
339                            continue;
340                        }
341                    } else {
342                        break;
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                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            None => {
372                let pay_req = fedimint_tonic_lnd::lnrpc::SendRequest {
373                    payment_request,
374                    fee_limit: max_fee.map(|f| {
375                        let limit = Limit::Fixed(u64::from(f) as i64);
376
377                        FeeLimit { limit: Some(limit) }
378                    }),
379                    amt_msat: amount_msat as i64,
380                    ..Default::default()
381                };
382
383                let payment_response = self
384                    .client
385                    .lock()
386                    .await
387                    .lightning()
388                    .send_payment_sync(fedimint_tonic_lnd::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 = fedimint_tonic_lnd::lnrpc::Invoice {
431            value_msat: u64::from(amount) as i64,
432            memo: description,
433            ..Default::default()
434        };
435
436        let invoice = self
437            .client
438            .lock()
439            .await
440            .lightning()
441            .add_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
442            .await
443            .unwrap()
444            .into_inner();
445
446        let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
447
448        Ok(CreateIncomingPaymentResponse {
449            request_lookup_id: bolt11.payment_hash().to_string(),
450            request: bolt11.to_string(),
451            expiry: unix_expiry,
452        })
453    }
454
455    #[instrument(skip(self))]
456    async fn check_incoming_payment_status(
457        &self,
458        request_lookup_id: &str,
459    ) -> Result<MintQuoteState, Self::Err> {
460        let invoice_request = fedimint_tonic_lnd::lnrpc::PaymentHash {
461            r_hash: hex::decode(request_lookup_id).unwrap(),
462            ..Default::default()
463        };
464
465        let invoice = self
466            .client
467            .lock()
468            .await
469            .lightning()
470            .lookup_invoice(fedimint_tonic_lnd::tonic::Request::new(invoice_request))
471            .await
472            .unwrap()
473            .into_inner();
474
475        match invoice.state {
476            // Open
477            0 => Ok(MintQuoteState::Unpaid),
478            // Settled
479            1 => Ok(MintQuoteState::Paid),
480            // Canceled
481            2 => Ok(MintQuoteState::Unpaid),
482            // Accepted
483            3 => Ok(MintQuoteState::Unpaid),
484            _ => Err(Self::Err::Anyhow(anyhow!("Invalid status"))),
485        }
486    }
487
488    #[instrument(skip(self))]
489    async fn check_outgoing_payment(
490        &self,
491        payment_hash: &str,
492    ) -> Result<MakePaymentResponse, Self::Err> {
493        let track_request = fedimint_tonic_lnd::routerrpc::TrackPaymentRequest {
494            payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
495            no_inflight_updates: true,
496        };
497
498        let payment_response = self
499            .client
500            .lock()
501            .await
502            .router()
503            .track_payment_v2(track_request)
504            .await;
505
506        let mut payment_stream = match payment_response {
507            Ok(stream) => stream.into_inner(),
508            Err(err) => {
509                let err_code = err.code();
510                if err_code == Code::NotFound {
511                    return Ok(MakePaymentResponse {
512                        payment_lookup_id: payment_hash.to_string(),
513                        payment_proof: None,
514                        status: MeltQuoteState::Unknown,
515                        total_spent: Amount::ZERO,
516                        unit: self.settings.unit.clone(),
517                    });
518                } else {
519                    return Err(cdk_payment::Error::UnknownPaymentState);
520                }
521            }
522        };
523
524        while let Some(update_result) = payment_stream.next().await {
525            match update_result {
526                Ok(update) => {
527                    let status = update.status();
528
529                    let response = match status {
530                        PaymentStatus::Unknown => MakePaymentResponse {
531                            payment_lookup_id: payment_hash.to_string(),
532                            payment_proof: Some(update.payment_preimage),
533                            status: MeltQuoteState::Unknown,
534                            total_spent: Amount::ZERO,
535                            unit: self.settings.unit.clone(),
536                        },
537                        PaymentStatus::InFlight => {
538                            // Continue waiting for the next update
539                            continue;
540                        }
541                        PaymentStatus::Succeeded => MakePaymentResponse {
542                            payment_lookup_id: payment_hash.to_string(),
543                            payment_proof: Some(update.payment_preimage),
544                            status: MeltQuoteState::Paid,
545                            total_spent: Amount::from(
546                                (update
547                                    .value_sat
548                                    .checked_add(update.fee_sat)
549                                    .ok_or(Error::AmountOverflow)?)
550                                    as u64,
551                            ),
552                            unit: CurrencyUnit::Sat,
553                        },
554                        PaymentStatus::Failed => MakePaymentResponse {
555                            payment_lookup_id: payment_hash.to_string(),
556                            payment_proof: Some(update.payment_preimage),
557                            status: MeltQuoteState::Failed,
558                            total_spent: Amount::ZERO,
559                            unit: self.settings.unit.clone(),
560                        },
561                    };
562
563                    return Ok(response);
564                }
565                Err(_) => {
566                    // Handle the case where the update itself is an error (e.g., stream failure)
567                    return Err(Error::UnknownPaymentStatus.into());
568                }
569            }
570        }
571
572        // If the stream is exhausted without a final status
573        Err(Error::UnknownPaymentStatus.into())
574    }
575}