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 futures::{Stream, StreamExt};
30use lnrpc::fee_limit::Limit;
31use lnrpc::payment::PaymentStatus;
32use lnrpc::{FeeLimit, Hop, MppRecord};
33use tokio_util::sync::CancellationToken;
34use tracing::instrument;
35
36mod client;
37pub mod error;
38
39mod proto;
40pub(crate) use proto::{lnrpc, routerrpc};
41
42#[derive(Clone)]
44pub struct Lnd {
45 _address: String,
46 _cert_file: PathBuf,
47 _macaroon_file: PathBuf,
48 lnd_client: client::Client,
49 fee_reserve: FeeReserve,
50 wait_invoice_cancel_token: CancellationToken,
51 wait_invoice_is_active: Arc<AtomicBool>,
52 settings: Bolt11Settings,
53}
54
55impl Lnd {
56 pub const MAX_ROUTE_RETRIES: usize = 50;
58
59 pub async fn new(
61 address: String,
62 cert_file: PathBuf,
63 macaroon_file: PathBuf,
64 fee_reserve: FeeReserve,
65 ) -> Result<Self, Error> {
66 if address.is_empty() {
68 return Err(Error::InvalidConfig("LND address cannot be empty".into()));
69 }
70
71 if !cert_file.exists() || cert_file.metadata().map(|m| m.len() == 0).unwrap_or(true) {
73 return Err(Error::InvalidConfig(format!(
74 "LND certificate file not found or empty: {cert_file:?}"
75 )));
76 }
77
78 if !macaroon_file.exists()
80 || macaroon_file
81 .metadata()
82 .map(|m| m.len() == 0)
83 .unwrap_or(true)
84 {
85 return Err(Error::InvalidConfig(format!(
86 "LND macaroon file not found or empty: {macaroon_file:?}"
87 )));
88 }
89
90 let lnd_client = client::connect(&address, &cert_file, &macaroon_file)
91 .await
92 .map_err(|err| {
93 tracing::error!("Connection error: {}", err.to_string());
94 Error::Connection
95 })
96 .unwrap();
97
98 Ok(Self {
99 _address: address,
100 _cert_file: cert_file,
101 _macaroon_file: macaroon_file,
102 lnd_client,
103 fee_reserve,
104 wait_invoice_cancel_token: CancellationToken::new(),
105 wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
106 settings: Bolt11Settings {
107 mpp: true,
108 unit: CurrencyUnit::Msat,
109 invoice_description: true,
110 amountless: true,
111 },
112 })
113 }
114}
115
116#[async_trait]
117impl MintPayment for Lnd {
118 type Err = payment::Error;
119
120 #[instrument(skip_all)]
121 async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
122 Ok(serde_json::to_value(&self.settings)?)
123 }
124
125 #[instrument(skip_all)]
126 fn is_wait_invoice_active(&self) -> bool {
127 self.wait_invoice_is_active.load(Ordering::SeqCst)
128 }
129
130 #[instrument(skip_all)]
131 fn cancel_wait_invoice(&self) {
132 self.wait_invoice_cancel_token.cancel()
133 }
134
135 #[instrument(skip_all)]
136 async fn wait_any_incoming_payment(
137 &self,
138 ) -> Result<Pin<Box<dyn Stream<Item = String> + Send>>, Self::Err> {
139 let mut lnd_client = self.lnd_client.clone();
140
141 let stream_req = lnrpc::InvoiceSubscription {
142 add_index: 0,
143 settle_index: 0,
144 };
145
146 let stream = lnd_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 unit: unit.clone(),
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 let mut lnd_client = self.lnd_client.clone();
287
288 for attempt in 0..Self::MAX_ROUTE_RETRIES {
289 let route_req = lnrpc::QueryRoutesRequest {
291 pub_key: hex::encode(pub_key.serialize()),
292 amt_msat: u64::from(partial_amount_msat) as i64,
293 fee_limit: max_fee.map(|f| {
294 let limit = Limit::Fixed(u64::from(f) as i64);
295 FeeLimit { limit: Some(limit) }
296 }),
297 use_mission_control: true,
298 ..Default::default()
299 };
300
301 let mut routes_response = lnd_client
303 .lightning()
304 .query_routes(route_req)
305 .await
306 .map_err(Error::LndError)?
307 .into_inner();
308
309 let last_hop: &mut Hop = routes_response.routes[0]
312 .hops
313 .last_mut()
314 .ok_or(Error::MissingLastHop)?;
315 let mpp_record = MppRecord {
316 payment_addr: payer_addr.clone(),
317 total_amt_msat: amount_msat as i64,
318 };
319 last_hop.mpp_record = Some(mpp_record);
320
321 let payment_response = lnd_client
322 .router()
323 .send_to_route_v2(routerrpc::SendToRouteRequest {
324 payment_hash: payment_hash.to_byte_array().to_vec(),
325 route: Some(routes_response.routes[0].clone()),
326 ..Default::default()
327 })
328 .await
329 .map_err(Error::LndError)?
330 .into_inner();
331
332 if let Some(failure) = payment_response.failure {
333 if failure.code == 15 {
334 tracing::debug!(
335 "Attempt number {}: route has failed. Re-querying...",
336 attempt + 1
337 );
338 continue;
339 }
340 }
341
342 let (status, payment_preimage) = match payment_response.status {
344 0 => (MeltQuoteState::Pending, None),
345 1 => (
346 MeltQuoteState::Paid,
347 Some(hex::encode(payment_response.preimage)),
348 ),
349 2 => (MeltQuoteState::Unpaid, None),
350 _ => (MeltQuoteState::Unknown, None),
351 };
352
353 let mut total_amt: u64 = 0;
355 if let Some(route) = payment_response.route {
356 total_amt = (route.total_amt_msat / 1000) as u64;
357 }
358
359 return Ok(MakePaymentResponse {
360 payment_lookup_id: hex::encode(payment_hash),
361 payment_proof: payment_preimage,
362 status,
363 total_spent: total_amt.into(),
364 unit: CurrencyUnit::Sat,
365 });
366 }
367
368 tracing::error!("Limit of retries reached, payment couldn't succeed.");
371 Err(Error::PaymentFailed.into())
372 }
373 None => {
374 let mut lnd_client = self.lnd_client.clone();
375
376 let pay_req = lnrpc::SendRequest {
377 payment_request,
378 fee_limit: max_fee.map(|f| {
379 let limit = Limit::Fixed(u64::from(f) as i64);
380 FeeLimit { limit: Some(limit) }
381 }),
382 amt_msat: amount_msat as i64,
383 ..Default::default()
384 };
385
386 let payment_response = lnd_client
387 .lightning()
388 .send_payment_sync(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 = lnrpc::Invoice {
431 value_msat: u64::from(amount) as i64,
432 memo: description,
433 ..Default::default()
434 };
435
436 let mut lnd_client = self.lnd_client.clone();
437
438 let invoice = lnd_client
439 .lightning()
440 .add_invoice(tonic::Request::new(invoice_request))
441 .await
442 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
443 .into_inner();
444
445 let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
446
447 Ok(CreateIncomingPaymentResponse {
448 request_lookup_id: bolt11.payment_hash().to_string(),
449 request: bolt11.to_string(),
450 expiry: unix_expiry,
451 })
452 }
453
454 #[instrument(skip(self))]
455 async fn check_incoming_payment_status(
456 &self,
457 request_lookup_id: &str,
458 ) -> Result<MintQuoteState, Self::Err> {
459 let mut lnd_client = self.lnd_client.clone();
460
461 let invoice_request = lnrpc::PaymentHash {
462 r_hash: hex::decode(request_lookup_id).unwrap(),
463 ..Default::default()
464 };
465
466 let invoice = lnd_client
467 .lightning()
468 .lookup_invoice(tonic::Request::new(invoice_request))
469 .await
470 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
471 .into_inner();
472
473 match invoice.state {
474 0 => Ok(MintQuoteState::Unpaid),
476 1 => Ok(MintQuoteState::Paid),
478 2 => Ok(MintQuoteState::Unpaid),
480 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 mut lnd_client = self.lnd_client.clone();
492
493 let track_request = routerrpc::TrackPaymentRequest {
494 payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
495 no_inflight_updates: true,
496 };
497
498 let payment_response = lnd_client.router().track_payment_v2(track_request).await;
499
500 let mut payment_stream = match payment_response {
501 Ok(stream) => stream.into_inner(),
502 Err(err) => {
503 let err_code = err.code();
504 if err_code == tonic::Code::NotFound {
505 return Ok(MakePaymentResponse {
506 payment_lookup_id: payment_hash.to_string(),
507 payment_proof: None,
508 status: MeltQuoteState::Unknown,
509 total_spent: Amount::ZERO,
510 unit: self.settings.unit.clone(),
511 });
512 } else {
513 return Err(payment::Error::UnknownPaymentState);
514 }
515 }
516 };
517
518 while let Some(update_result) = payment_stream.next().await {
519 match update_result {
520 Ok(update) => {
521 let status = update.status();
522
523 let response = match status {
524 PaymentStatus::Unknown => MakePaymentResponse {
525 payment_lookup_id: payment_hash.to_string(),
526 payment_proof: Some(update.payment_preimage),
527 status: MeltQuoteState::Unknown,
528 total_spent: Amount::ZERO,
529 unit: self.settings.unit.clone(),
530 },
531 PaymentStatus::InFlight | PaymentStatus::Initiated => {
532 continue;
534 }
535 PaymentStatus::Succeeded => MakePaymentResponse {
536 payment_lookup_id: payment_hash.to_string(),
537 payment_proof: Some(update.payment_preimage),
538 status: MeltQuoteState::Paid,
539 total_spent: Amount::from(
540 (update
541 .value_sat
542 .checked_add(update.fee_sat)
543 .ok_or(Error::AmountOverflow)?)
544 as u64,
545 ),
546 unit: CurrencyUnit::Sat,
547 },
548 PaymentStatus::Failed => MakePaymentResponse {
549 payment_lookup_id: payment_hash.to_string(),
550 payment_proof: Some(update.payment_preimage),
551 status: MeltQuoteState::Failed,
552 total_spent: Amount::ZERO,
553 unit: self.settings.unit.clone(),
554 },
555 };
556
557 return Ok(response);
558 }
559 Err(_) => {
560 return Err(Error::UnknownPaymentStatus.into());
562 }
563 }
564 }
565
566 Err(Error::UnknownPaymentStatus.into())
568 }
569}