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::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#[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 async fn new(
57 address: String,
58 cert_file: PathBuf,
59 macaroon_file: PathBuf,
60 fee_reserve: FeeReserve,
61 ) -> Result<Self, Error> {
62 if address.is_empty() {
64 return Err(Error::InvalidConfig("LND address cannot be empty".into()));
65 }
66
67 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 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 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 }, 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 }, }
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 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 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 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 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 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 continue;
340 }
341 } else {
342 break;
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 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 0 => Ok(MintQuoteState::Unpaid),
478 1 => Ok(MintQuoteState::Paid),
480 2 => Ok(MintQuoteState::Unpaid),
482 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;
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 return Err(Error::UnknownPaymentStatus.into());
568 }
569 }
570 }
571
572 Err(Error::UnknownPaymentStatus.into())
574 }
575}