1#![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#[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 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 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 }, 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 }, }
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 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 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 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 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 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 continue;
301 }
302 } else {
303 break;
304 }
305 }
306
307 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 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 0 => Ok(MintQuoteState::Unpaid),
442 1 => Ok(MintQuoteState::Paid),
444 2 => Ok(MintQuoteState::Unpaid),
446 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;
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 return Err(Error::UnknownPaymentStatus.into());
532 }
533 }
534 }
535
536 Err(Error::UnknownPaymentStatus.into())
538 }
539}