1#![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#[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 pub async fn new(
56 address: String,
57 cert_file: PathBuf,
58 macaroon_file: PathBuf,
59 fee_reserve: FeeReserve,
60 ) -> Result<Self, Error> {
61 if address.is_empty() {
63 return Err(Error::InvalidConfig("LND address cannot be empty".into()));
64 }
65
66 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 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 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 }, 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 }, }
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 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 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 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 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 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 continue;
338 }
339 } else {
340 break;
341 }
342 }
343
344 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 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 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 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;
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 return Err(Error::UnknownPaymentStatus.into());
566 }
567 }
568 }
569
570 Err(Error::UnknownPaymentStatus.into())
572 }
573}