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