1#![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#[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 pub const MAX_ROUTE_RETRIES: usize = 50;
57
58 pub async fn new(
60 address: String,
61 cert_file: PathBuf,
62 macaroon_file: PathBuf,
63 fee_reserve: FeeReserve,
64 ) -> Result<Self, Error> {
65 if address.is_empty() {
67 return Err(Error::InvalidConfig("LND address cannot be empty".into()));
68 }
69
70 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 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 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 }, 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 }, }
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 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 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 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 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 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 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 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 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 0 => Ok(MintQuoteState::Unpaid),
484 1 => Ok(MintQuoteState::Paid),
486 2 => Ok(MintQuoteState::Unpaid),
488 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;
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 return Err(Error::UnknownPaymentStatus.into());
574 }
575 }
576 }
577
578 Err(Error::UnknownPaymentStatus.into())
580 }
581}