1#![doc = include_str!("../README.md")]
6
7use std::cmp::max;
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_common::amount::{Amount, MSAT_IN_SAT};
17use cdk_common::bitcoin::hashes::Hash;
18use cdk_common::common::FeeReserve;
19use cdk_common::database::DynKVStore;
20use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
21use cdk_common::payment::{
22 self, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions, MakePaymentResponse,
23 MintPayment, OutgoingPaymentOptions, PaymentIdentifier, PaymentQuoteResponse, SettingsResponse,
24 WaitPaymentResponse,
25};
26use cdk_common::util::hex;
27use cdk_common::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
42use crate::lnrpc::invoice::InvoiceState;
43
44const LND_KV_PRIMARY_NAMESPACE: &str = "cdk_lnd_lightning_backend";
46const LND_KV_SECONDARY_NAMESPACE: &str = "payment_indices";
47const LAST_ADD_INDEX_KV_KEY: &str = "last_add_index";
48const LAST_SETTLE_INDEX_KV_KEY: &str = "last_settle_index";
49
50#[derive(Clone)]
52pub struct Lnd {
53 _address: String,
54 _cert_file: PathBuf,
55 _macaroon_file: PathBuf,
56 lnd_client: client::Client,
57 fee_reserve: FeeReserve,
58 kv_store: DynKVStore,
59 wait_invoice_cancel_token: CancellationToken,
60 wait_invoice_is_active: Arc<AtomicBool>,
61 settings: SettingsResponse,
62 unit: CurrencyUnit,
63}
64
65impl std::fmt::Debug for Lnd {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("Lnd")
68 .field("fee_reserve", &self.fee_reserve)
69 .finish_non_exhaustive()
70 }
71}
72
73impl Lnd {
74 pub const MAX_ROUTE_RETRIES: usize = 50;
76
77 pub async fn new(
79 address: String,
80 cert_file: PathBuf,
81 macaroon_file: PathBuf,
82 fee_reserve: FeeReserve,
83 kv_store: DynKVStore,
84 ) -> Result<Self, Error> {
85 if address.is_empty() {
87 return Err(Error::InvalidConfig("LND address cannot be empty".into()));
88 }
89
90 if !cert_file.exists() || cert_file.metadata().map(|m| m.len() == 0).unwrap_or(true) {
92 return Err(Error::InvalidConfig(format!(
93 "LND certificate file not found or empty: {cert_file:?}"
94 )));
95 }
96
97 if !macaroon_file.exists()
99 || macaroon_file
100 .metadata()
101 .map(|m| m.len() == 0)
102 .unwrap_or(true)
103 {
104 return Err(Error::InvalidConfig(format!(
105 "LND macaroon file not found or empty: {macaroon_file:?}"
106 )));
107 }
108
109 let lnd_client = client::connect(&address, &cert_file, &macaroon_file)
110 .await
111 .map_err(|err| {
112 tracing::error!("Connection error: {}", err.to_string());
113 Error::Connection
114 })?;
115
116 let unit = CurrencyUnit::Msat;
117 Ok(Self {
118 _address: address,
119 _cert_file: cert_file,
120 _macaroon_file: macaroon_file,
121 lnd_client,
122 fee_reserve,
123 kv_store,
124 wait_invoice_cancel_token: CancellationToken::new(),
125 wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
126 settings: SettingsResponse {
127 unit: unit.to_string(),
128 bolt11: Some(payment::Bolt11Settings {
129 mpp: true,
130 amountless: true,
131 invoice_description: true,
132 }),
133 bolt12: None,
134 custom: std::collections::HashMap::new(),
135 },
136 unit,
137 })
138 }
139
140 #[instrument(skip_all)]
142 async fn get_last_indices(&self) -> Result<(Option<u64>, Option<u64>), Error> {
143 let add_index = if let Some(stored_index) = self
144 .kv_store
145 .kv_read(
146 LND_KV_PRIMARY_NAMESPACE,
147 LND_KV_SECONDARY_NAMESPACE,
148 LAST_ADD_INDEX_KV_KEY,
149 )
150 .await
151 .map_err(|e| Error::Database(e.to_string()))?
152 {
153 if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
154 index_str.parse::<u64>().ok()
155 } else {
156 None
157 }
158 } else {
159 None
160 };
161
162 let settle_index = if let Some(stored_index) = self
163 .kv_store
164 .kv_read(
165 LND_KV_PRIMARY_NAMESPACE,
166 LND_KV_SECONDARY_NAMESPACE,
167 LAST_SETTLE_INDEX_KV_KEY,
168 )
169 .await
170 .map_err(|e| Error::Database(e.to_string()))?
171 {
172 if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
173 index_str.parse::<u64>().ok()
174 } else {
175 None
176 }
177 } else {
178 None
179 };
180
181 tracing::debug!(
182 "LND: Retrieved last indices from KV store - add_index: {:?}, settle_index: {:?}",
183 add_index,
184 settle_index
185 );
186 Ok((add_index, settle_index))
187 }
188}
189
190#[async_trait]
191impl MintPayment for Lnd {
192 type Err = payment::Error;
193
194 #[instrument(skip_all)]
195 async fn get_settings(&self) -> Result<SettingsResponse, Self::Err> {
196 Ok(self.settings.clone())
197 }
198
199 #[instrument(skip_all)]
200 fn is_wait_invoice_active(&self) -> bool {
201 self.wait_invoice_is_active.load(Ordering::SeqCst)
202 }
203
204 #[instrument(skip_all)]
205 fn cancel_wait_invoice(&self) {
206 self.wait_invoice_cancel_token.cancel()
207 }
208
209 #[instrument(skip_all)]
210 async fn wait_payment_event(
211 &self,
212 ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
213 let mut lnd_client = self.lnd_client.clone();
214
215 let (last_add_index, last_settle_index) =
217 self.get_last_indices().await.unwrap_or((None, None));
218
219 let stream_req = lnrpc::InvoiceSubscription {
220 add_index: last_add_index.unwrap_or(0),
221 settle_index: last_settle_index.unwrap_or(0),
222 };
223
224 tracing::debug!(
225 "LND: Starting invoice subscription with add_index: {}, settle_index: {}",
226 stream_req.add_index,
227 stream_req.settle_index
228 );
229
230 let stream = lnd_client
231 .lightning()
232 .subscribe_invoices(stream_req)
233 .await
234 .map_err(|_err| {
235 tracing::error!("Could not subscribe to invoice");
236 Error::Connection
237 })?
238 .into_inner();
239
240 let cancel_token = self.wait_invoice_cancel_token.clone();
241 let kv_store = self.kv_store.clone();
242
243 let event_stream = futures::stream::unfold(
244 (
245 stream,
246 cancel_token,
247 Arc::clone(&self.wait_invoice_is_active),
248 kv_store,
249 last_add_index.unwrap_or(0),
250 last_settle_index.unwrap_or(0),
251 ),
252 |(
253 mut stream,
254 cancel_token,
255 is_active,
256 kv_store,
257 mut current_add_index,
258 mut current_settle_index,
259 )| async move {
260 is_active.store(true, Ordering::SeqCst);
261
262 loop {
263 tokio::select! {
264 _ = cancel_token.cancelled() => {
265 is_active.store(false, Ordering::SeqCst);
267 tracing::info!("Waiting for lnd invoice ending");
268 return None;
269 }
270 msg = stream.message() => {
271 match msg {
272 Ok(Some(msg)) => {
273 current_add_index = current_add_index.max(msg.add_index);
275 current_settle_index = current_settle_index.max(msg.settle_index);
276
277 let add_index_str = current_add_index.to_string();
279 let settle_index_str = current_settle_index.to_string();
280
281 if let Ok(mut tx) = kv_store.begin_transaction().await {
282 let mut has_error = false;
283
284 if let Err(e) = tx.kv_write(LND_KV_PRIMARY_NAMESPACE, LND_KV_SECONDARY_NAMESPACE, LAST_ADD_INDEX_KV_KEY, add_index_str.as_bytes()).await {
285 tracing::warn!("LND: Failed to write add_index {} to KV store: {}", current_add_index, e);
286 has_error = true;
287 }
288
289 if let Err(e) = tx.kv_write(LND_KV_PRIMARY_NAMESPACE, LND_KV_SECONDARY_NAMESPACE, LAST_SETTLE_INDEX_KV_KEY, settle_index_str.as_bytes()).await {
290 tracing::warn!("LND: Failed to write settle_index {} to KV store: {}", current_settle_index, e);
291 has_error = true;
292 }
293
294 if !has_error {
295 if let Err(e) = tx.commit().await {
296 tracing::warn!("LND: Failed to commit indices to KV store: {}", e);
297 } else {
298 tracing::debug!("LND: Stored updated indices - add_index: {}, settle_index: {}", current_add_index, current_settle_index);
299 }
300 }
301 } else {
302 tracing::warn!("LND: Failed to begin KV transaction for storing indices");
303 }
304
305 if msg.state() == InvoiceState::Settled {
307 let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
308
309 if let Ok(hash_slice) = hash_slice {
310 let hash = hex::encode(hash_slice);
311
312 tracing::info!("LND: Payment for {} with amount {} msat", hash, msg.amt_paid_msat);
313
314 let wait_response = WaitPaymentResponse {
315 payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
316 payment_amount: Amount::new(msg.amt_paid_msat as u64, CurrencyUnit::Msat),
317 payment_id: hash,
318 };
319 let event = Event::PaymentReceived(wait_response);
320 return Some((event, (stream, cancel_token, is_active, kv_store, current_add_index, current_settle_index)));
321 } else {
322 tracing::error!("LND returned invalid payment hash");
324 continue;
326 }
327 } else {
328 tracing::debug!("LND: Received non-settled invoice, continuing to wait for settled invoices");
330 continue;
332 }
333 }
334 Ok(None) => {
335 is_active.store(false, Ordering::SeqCst);
336 tracing::info!("LND invoice stream ended.");
337 return None;
338 }
339 Err(err) => {
340 is_active.store(false, Ordering::SeqCst);
341 tracing::warn!("Encountered error in LND invoice stream. Stream ending");
342 tracing::error!("{:?}", err);
343 return None;
344 }
345 }
346 }
347 }
348 }
349 },
350 );
351
352 Ok(Box::pin(event_stream))
353 }
354
355 #[instrument(skip_all)]
356 async fn get_payment_quote(
357 &self,
358 unit: &CurrencyUnit,
359 options: OutgoingPaymentOptions,
360 ) -> Result<PaymentQuoteResponse, Self::Err> {
361 match options {
362 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
363 let amount_msat = match bolt11_options.melt_options {
364 Some(amount) => amount.amount_msat(),
365 None => bolt11_options
366 .bolt11
367 .amount_milli_satoshis()
368 .ok_or(Error::UnknownInvoiceAmount)?
369 .into(),
370 };
371
372 let amount =
373 Amount::new(amount_msat.into(), CurrencyUnit::Msat).convert_to(unit)?;
374
375 let relative_fee_reserve =
376 (self.fee_reserve.percent_fee_reserve * amount.value() as f32) as u64;
377
378 let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
379
380 let fee = max(relative_fee_reserve, absolute_fee_reserve);
381
382 Ok(PaymentQuoteResponse {
383 request_lookup_id: Some(PaymentIdentifier::PaymentHash(
384 *bolt11_options.bolt11.payment_hash().as_ref(),
385 )),
386 amount,
387 fee: Amount::new(fee, unit.clone()),
388 state: MeltQuoteState::Unpaid,
389 })
390 }
391 OutgoingPaymentOptions::Bolt12(_) => {
392 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
393 }
394 OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
395 }
396 }
397
398 #[instrument(skip_all)]
399 async fn make_payment(
400 &self,
401 unit: &CurrencyUnit,
402 options: OutgoingPaymentOptions,
403 ) -> Result<MakePaymentResponse, Self::Err> {
404 match options {
405 OutgoingPaymentOptions::Bolt11(bolt11_options) => {
406 let bolt11 = bolt11_options.bolt11;
407
408 let pay_state = self
409 .check_outgoing_payment(&PaymentIdentifier::PaymentHash(
410 *bolt11.payment_hash().as_ref(),
411 ))
412 .await?;
413
414 match pay_state.status {
415 MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
416 MeltQuoteState::Paid => {
417 tracing::debug!("Melt attempted on invoice already paid");
418 return Err(Self::Err::InvoiceAlreadyPaid);
419 }
420 MeltQuoteState::Pending => {
421 tracing::debug!("Melt attempted on invoice already pending");
422 return Err(Self::Err::InvoicePaymentPending);
423 }
424 }
425
426 match bolt11_options.melt_options {
428 Some(MeltOptions::Mpp { mpp }) => {
429 let amount_msat: u64 = bolt11
430 .amount_milli_satoshis()
431 .ok_or(Error::UnknownInvoiceAmount)?;
432 {
433 let partial_amount_msat = mpp.amount;
434 let invoice = bolt11;
435 let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
436
437 let pub_key = invoice.get_payee_pub_key();
439 let payer_addr = invoice.payment_secret().0.to_vec();
440 let payment_hash = invoice.payment_hash();
441
442 let mut lnd_client = self.lnd_client.clone();
443
444 for attempt in 0..Self::MAX_ROUTE_RETRIES {
445 let route_req = lnrpc::QueryRoutesRequest {
447 pub_key: hex::encode(pub_key.serialize()),
448 amt_msat: u64::from(partial_amount_msat) as i64,
449 fee_limit: max_fee
450 .map(|f| {
451 let fee_msat = Amount::new(f.into(), unit.clone())
452 .convert_to(&CurrencyUnit::Msat)?
453 .value();
454 let limit = Limit::FixedMsat(fee_msat as i64);
455 Ok::<_, Error>(FeeLimit { limit: Some(limit) })
456 })
457 .transpose()?,
458 use_mission_control: true,
459 ..Default::default()
460 };
461
462 let mut routes_response = lnd_client
464 .lightning()
465 .query_routes(route_req)
466 .await
467 .map_err(Error::LndError)?
468 .into_inner();
469
470 let last_hop: &mut Hop = routes_response.routes[0]
473 .hops
474 .last_mut()
475 .ok_or(Error::MissingLastHop)?;
476 let mpp_record = MppRecord {
477 payment_addr: payer_addr.clone(),
478 total_amt_msat: amount_msat as i64,
479 };
480 last_hop.mpp_record = Some(mpp_record);
481
482 let payment_response = lnd_client
483 .router()
484 .send_to_route_v2(routerrpc::SendToRouteRequest {
485 payment_hash: payment_hash.to_byte_array().to_vec(),
486 route: Some(routes_response.routes[0].clone()),
487 ..Default::default()
488 })
489 .await
490 .map_err(Error::LndError)?
491 .into_inner();
492
493 if let Some(failure) = payment_response.failure {
494 if failure.code == 15 {
495 tracing::debug!(
496 "Attempt number {}: route has failed. Re-querying...",
497 attempt + 1
498 );
499 continue;
500 }
501 }
502
503 let (status, payment_preimage) = match payment_response.status {
505 0 => (MeltQuoteState::Pending, None),
506 1 => (
507 MeltQuoteState::Paid,
508 Some(hex::encode(payment_response.preimage)),
509 ),
510 2 => (MeltQuoteState::Unpaid, None),
511 _ => (MeltQuoteState::Unknown, None),
512 };
513
514 let mut total_amt: u64 = 0;
516 if let Some(route) = payment_response.route {
517 total_amt = (route.total_amt_msat / 1000) as u64;
518 }
519
520 return Ok(MakePaymentResponse {
521 payment_lookup_id: PaymentIdentifier::PaymentHash(
522 payment_hash.to_byte_array(),
523 ),
524 payment_proof: payment_preimage,
525 status,
526 total_spent: Amount::new(total_amt, CurrencyUnit::Sat),
527 });
528 }
529
530 tracing::error!("Limit of retries reached, payment couldn't succeed.");
533 Err(Error::PaymentFailed.into())
534 }
535 }
536 _ => {
537 let mut lnd_client = self.lnd_client.clone();
538
539 let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
540
541 let amount_msat = u64::from(
542 bolt11_options
543 .melt_options
544 .map(|a| a.amount_msat())
545 .unwrap_or_default(),
546 );
547
548 let pay_req = lnrpc::SendRequest {
549 payment_request: bolt11.to_string(),
550 fee_limit: max_fee
551 .map(|f| {
552 let fee_msat = Amount::new(f.into(), unit.clone())
553 .convert_to(&CurrencyUnit::Msat)?
554 .value();
555 let limit = Limit::FixedMsat(fee_msat as i64);
556 Ok::<_, Error>(FeeLimit { limit: Some(limit) })
557 })
558 .transpose()?,
559 amt_msat: amount_msat as i64,
560 ..Default::default()
561 };
562
563 let payment_response = lnd_client
564 .lightning()
565 .send_payment_sync(tonic::Request::new(pay_req))
566 .await
567 .map_err(|err| {
568 tracing::warn!("Lightning payment failed: {}", err);
569 Error::PaymentFailed
570 })?
571 .into_inner();
572
573 let total_amount = payment_response
574 .payment_route
575 .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
576 as u64;
577
578 let (status, payment_preimage) = match total_amount == 0 {
579 true => (MeltQuoteState::Unpaid, None),
580 false => (
581 MeltQuoteState::Paid,
582 Some(hex::encode(payment_response.payment_preimage)),
583 ),
584 };
585
586 let payment_identifier =
587 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
588
589 Ok(MakePaymentResponse {
590 payment_lookup_id: payment_identifier,
591 payment_proof: payment_preimage,
592 status,
593 total_spent: Amount::new(total_amount, CurrencyUnit::Sat),
594 })
595 }
596 }
597 }
598 OutgoingPaymentOptions::Bolt12(_) => {
599 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
600 }
601 OutgoingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
602 }
603 }
604
605 #[instrument(skip(self, options))]
606 async fn create_incoming_payment_request(
607 &self,
608 unit: &CurrencyUnit,
609 options: IncomingPaymentOptions,
610 ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
611 match options {
612 IncomingPaymentOptions::Bolt11(bolt11_options) => {
613 let description = bolt11_options.description.unwrap_or_default();
614 let amount = bolt11_options.amount;
615 let unix_expiry = bolt11_options.unix_expiry;
616
617 let amount_msat: Amount = Amount::new(amount.into(), unit.clone())
618 .convert_to(&CurrencyUnit::Msat)?
619 .into();
620
621 let invoice_request = lnrpc::Invoice {
622 value_msat: u64::from(amount_msat) as i64,
623 memo: description,
624 ..Default::default()
625 };
626
627 let mut lnd_client = self.lnd_client.clone();
628
629 let invoice = lnd_client
630 .lightning()
631 .add_invoice(tonic::Request::new(invoice_request))
632 .await
633 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
634 .into_inner();
635
636 let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
637
638 let payment_identifier =
639 PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
640
641 Ok(CreateIncomingPaymentResponse {
642 request_lookup_id: payment_identifier,
643 request: bolt11.to_string(),
644 expiry: unix_expiry,
645 extra_json: None,
646 })
647 }
648 IncomingPaymentOptions::Bolt12(_) => {
649 Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
650 }
651 IncomingPaymentOptions::Custom(_) => Err(payment::Error::UnsupportedPaymentOption),
652 }
653 }
654
655 #[instrument(skip(self))]
656 async fn check_incoming_payment_status(
657 &self,
658 payment_identifier: &PaymentIdentifier,
659 ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
660 let mut lnd_client = self.lnd_client.clone();
661
662 let invoice_request = lnrpc::PaymentHash {
663 r_hash: hex::decode(payment_identifier.to_string())?,
664 ..Default::default()
665 };
666
667 let invoice = lnd_client
668 .lightning()
669 .lookup_invoice(tonic::Request::new(invoice_request))
670 .await
671 .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
672 .into_inner();
673
674 if invoice.state() == InvoiceState::Settled {
675 Ok(vec![WaitPaymentResponse {
676 payment_identifier: payment_identifier.clone(),
677 payment_amount: Amount::new(invoice.amt_paid_msat as u64, CurrencyUnit::Msat),
678 payment_id: hex::encode(invoice.r_hash),
679 }])
680 } else {
681 Ok(vec![])
682 }
683 }
684
685 #[instrument(skip(self))]
686 async fn check_outgoing_payment(
687 &self,
688 payment_identifier: &PaymentIdentifier,
689 ) -> Result<MakePaymentResponse, Self::Err> {
690 let mut lnd_client = self.lnd_client.clone();
691
692 let payment_hash = &payment_identifier.to_string();
693
694 let track_request = routerrpc::TrackPaymentRequest {
695 payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
696 no_inflight_updates: true,
697 };
698
699 let payment_response = lnd_client.router().track_payment_v2(track_request).await;
700
701 let mut payment_stream = match payment_response {
702 Ok(stream) => stream.into_inner(),
703 Err(err) => {
704 let err_code = err.code();
705 if err_code == tonic::Code::NotFound {
706 return Ok(MakePaymentResponse {
707 payment_lookup_id: payment_identifier.clone(),
708 payment_proof: None,
709 status: MeltQuoteState::Unknown,
710 total_spent: Amount::new(0, self.unit.clone()),
711 });
712 } else {
713 return Err(payment::Error::UnknownPaymentState);
714 }
715 }
716 };
717
718 while let Some(update_result) = payment_stream.next().await {
719 match update_result {
720 Ok(update) => {
721 let status = update.status();
722
723 let response = match status {
724 #[allow(deprecated)]
725 PaymentStatus::Unknown => MakePaymentResponse {
726 payment_lookup_id: payment_identifier.clone(),
727 payment_proof: Some(update.payment_preimage),
728 status: MeltQuoteState::Unknown,
729 total_spent: Amount::new(0, self.unit.clone()),
730 },
731 PaymentStatus::InFlight | PaymentStatus::Initiated => {
732 continue;
734 }
735 PaymentStatus::Succeeded => MakePaymentResponse {
736 payment_lookup_id: payment_identifier.clone(),
737 payment_proof: Some(update.payment_preimage),
738 status: MeltQuoteState::Paid,
739 total_spent: Amount::new(
740 (update
741 .value_sat
742 .checked_add(update.fee_sat)
743 .ok_or(Error::AmountOverflow)?)
744 as u64,
745 CurrencyUnit::Sat,
746 ),
747 },
748 PaymentStatus::Failed => MakePaymentResponse {
749 payment_lookup_id: payment_identifier.clone(),
750 payment_proof: Some(update.payment_preimage),
751 status: MeltQuoteState::Failed,
752 total_spent: Amount::new(0, self.unit.clone()),
753 },
754 };
755
756 return Ok(response);
757 }
758 Err(_) => {
759 return Err(Error::UnknownPaymentStatus.into());
761 }
762 }
763 }
764
765 Err(Error::UnknownPaymentStatus.into())
767 }
768}