| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747 |
- //! CDK lightning backend for LND
- // Copyright (c) 2023 Steffen (MIT)
- #![doc = include_str!("../README.md")]
- #![warn(missing_docs)]
- #![warn(rustdoc::bare_urls)]
- use std::cmp::max;
- use std::path::PathBuf;
- use std::pin::Pin;
- use std::str::FromStr;
- use std::sync::atomic::{AtomicBool, Ordering};
- use std::sync::Arc;
- use anyhow::anyhow;
- use async_trait::async_trait;
- use cdk_common::amount::{to_unit, Amount, MSAT_IN_SAT};
- use cdk_common::bitcoin::hashes::Hash;
- use cdk_common::common::FeeReserve;
- use cdk_common::database::mint::DynMintKVStore;
- use cdk_common::nuts::{CurrencyUnit, MeltOptions, MeltQuoteState};
- use cdk_common::payment::{
- self, Bolt11Settings, CreateIncomingPaymentResponse, Event, IncomingPaymentOptions,
- MakePaymentResponse, MintPayment, OutgoingPaymentOptions, PaymentIdentifier,
- PaymentQuoteResponse, WaitPaymentResponse,
- };
- use cdk_common::util::hex;
- use cdk_common::Bolt11Invoice;
- use error::Error;
- use futures::{Stream, StreamExt};
- use lnrpc::fee_limit::Limit;
- use lnrpc::payment::PaymentStatus;
- use lnrpc::{FeeLimit, Hop, MppRecord};
- use tokio_util::sync::CancellationToken;
- use tracing::instrument;
- mod client;
- pub mod error;
- mod proto;
- pub(crate) use proto::{lnrpc, routerrpc};
- use crate::lnrpc::invoice::InvoiceState;
- /// LND KV Store constants
- const LND_KV_PRIMARY_NAMESPACE: &str = "cdk_lnd_lightning_backend";
- const LND_KV_SECONDARY_NAMESPACE: &str = "payment_indices";
- const LAST_ADD_INDEX_KV_KEY: &str = "last_add_index";
- const LAST_SETTLE_INDEX_KV_KEY: &str = "last_settle_index";
- /// Lnd mint backend
- #[derive(Clone)]
- pub struct Lnd {
- _address: String,
- _cert_file: PathBuf,
- _macaroon_file: PathBuf,
- lnd_client: client::Client,
- fee_reserve: FeeReserve,
- kv_store: DynMintKVStore,
- wait_invoice_cancel_token: CancellationToken,
- wait_invoice_is_active: Arc<AtomicBool>,
- settings: Bolt11Settings,
- }
- impl Lnd {
- /// Maximum number of attempts at a partial payment
- pub const MAX_ROUTE_RETRIES: usize = 50;
- /// Create new [`Lnd`]
- pub async fn new(
- address: String,
- cert_file: PathBuf,
- macaroon_file: PathBuf,
- fee_reserve: FeeReserve,
- kv_store: DynMintKVStore,
- ) -> Result<Self, Error> {
- // Validate address is not empty
- if address.is_empty() {
- return Err(Error::InvalidConfig("LND address cannot be empty".into()));
- }
- // Validate cert_file exists and is not empty
- if !cert_file.exists() || cert_file.metadata().map(|m| m.len() == 0).unwrap_or(true) {
- return Err(Error::InvalidConfig(format!(
- "LND certificate file not found or empty: {cert_file:?}"
- )));
- }
- // Validate macaroon_file exists and is not empty
- if !macaroon_file.exists()
- || macaroon_file
- .metadata()
- .map(|m| m.len() == 0)
- .unwrap_or(true)
- {
- return Err(Error::InvalidConfig(format!(
- "LND macaroon file not found or empty: {macaroon_file:?}"
- )));
- }
- let lnd_client = client::connect(&address, &cert_file, &macaroon_file)
- .await
- .map_err(|err| {
- tracing::error!("Connection error: {}", err.to_string());
- Error::Connection
- })
- .unwrap();
- Ok(Self {
- _address: address,
- _cert_file: cert_file,
- _macaroon_file: macaroon_file,
- lnd_client,
- fee_reserve,
- kv_store,
- wait_invoice_cancel_token: CancellationToken::new(),
- wait_invoice_is_active: Arc::new(AtomicBool::new(false)),
- settings: Bolt11Settings {
- mpp: true,
- unit: CurrencyUnit::Msat,
- invoice_description: true,
- amountless: true,
- bolt12: false,
- },
- })
- }
- /// Get last add and settle indices from KV store
- #[instrument(skip_all)]
- async fn get_last_indices(&self) -> Result<(Option<u64>, Option<u64>), Error> {
- let add_index = if let Some(stored_index) = self
- .kv_store
- .kv_read(
- LND_KV_PRIMARY_NAMESPACE,
- LND_KV_SECONDARY_NAMESPACE,
- LAST_ADD_INDEX_KV_KEY,
- )
- .await
- .map_err(|e| Error::Database(e.to_string()))?
- {
- if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
- index_str.parse::<u64>().ok()
- } else {
- None
- }
- } else {
- None
- };
- let settle_index = if let Some(stored_index) = self
- .kv_store
- .kv_read(
- LND_KV_PRIMARY_NAMESPACE,
- LND_KV_SECONDARY_NAMESPACE,
- LAST_SETTLE_INDEX_KV_KEY,
- )
- .await
- .map_err(|e| Error::Database(e.to_string()))?
- {
- if let Ok(index_str) = std::str::from_utf8(stored_index.as_slice()) {
- index_str.parse::<u64>().ok()
- } else {
- None
- }
- } else {
- None
- };
- tracing::debug!(
- "LND: Retrieved last indices from KV store - add_index: {:?}, settle_index: {:?}",
- add_index,
- settle_index
- );
- Ok((add_index, settle_index))
- }
- }
- #[async_trait]
- impl MintPayment for Lnd {
- type Err = payment::Error;
- #[instrument(skip_all)]
- async fn get_settings(&self) -> Result<serde_json::Value, Self::Err> {
- Ok(serde_json::to_value(&self.settings)?)
- }
- #[instrument(skip_all)]
- fn is_wait_invoice_active(&self) -> bool {
- self.wait_invoice_is_active.load(Ordering::SeqCst)
- }
- #[instrument(skip_all)]
- fn cancel_wait_invoice(&self) {
- self.wait_invoice_cancel_token.cancel()
- }
- #[instrument(skip_all)]
- async fn wait_payment_event(
- &self,
- ) -> Result<Pin<Box<dyn Stream<Item = Event> + Send>>, Self::Err> {
- let mut lnd_client = self.lnd_client.clone();
- // Get last indices from KV store
- let (last_add_index, last_settle_index) =
- self.get_last_indices().await.unwrap_or((None, None));
- let stream_req = lnrpc::InvoiceSubscription {
- add_index: last_add_index.unwrap_or(0),
- settle_index: last_settle_index.unwrap_or(0),
- };
- tracing::debug!(
- "LND: Starting invoice subscription with add_index: {}, settle_index: {}",
- stream_req.add_index,
- stream_req.settle_index
- );
- let stream = lnd_client
- .lightning()
- .subscribe_invoices(stream_req)
- .await
- .map_err(|_err| {
- tracing::error!("Could not subscribe to invoice");
- Error::Connection
- })?
- .into_inner();
- let cancel_token = self.wait_invoice_cancel_token.clone();
- let kv_store = self.kv_store.clone();
- let event_stream = futures::stream::unfold(
- (
- stream,
- cancel_token,
- Arc::clone(&self.wait_invoice_is_active),
- kv_store,
- last_add_index.unwrap_or(0),
- last_settle_index.unwrap_or(0),
- ),
- |(
- mut stream,
- cancel_token,
- is_active,
- kv_store,
- mut current_add_index,
- mut current_settle_index,
- )| async move {
- is_active.store(true, Ordering::SeqCst);
- loop {
- tokio::select! {
- _ = cancel_token.cancelled() => {
- // Stream is cancelled
- is_active.store(false, Ordering::SeqCst);
- tracing::info!("Waiting for lnd invoice ending");
- return None;
- }
- msg = stream.message() => {
- match msg {
- Ok(Some(msg)) => {
- // Update indices based on the message
- current_add_index = current_add_index.max(msg.add_index);
- current_settle_index = current_settle_index.max(msg.settle_index);
- // Store the updated indices in KV store regardless of settlement status
- let add_index_str = current_add_index.to_string();
- let settle_index_str = current_settle_index.to_string();
- if let Ok(mut tx) = kv_store.begin_transaction().await {
- let mut has_error = false;
- 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 {
- tracing::warn!("LND: Failed to write add_index {} to KV store: {}", current_add_index, e);
- has_error = true;
- }
- 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 {
- tracing::warn!("LND: Failed to write settle_index {} to KV store: {}", current_settle_index, e);
- has_error = true;
- }
- if !has_error {
- if let Err(e) = tx.commit().await {
- tracing::warn!("LND: Failed to commit indices to KV store: {}", e);
- } else {
- tracing::debug!("LND: Stored updated indices - add_index: {}, settle_index: {}", current_add_index, current_settle_index);
- }
- }
- } else {
- tracing::warn!("LND: Failed to begin KV transaction for storing indices");
- }
- // Only emit event for settled invoices
- if msg.state() == InvoiceState::Settled {
- let hash_slice: Result<[u8;32], _> = msg.r_hash.try_into();
- if let Ok(hash_slice) = hash_slice {
- let hash = hex::encode(hash_slice);
- tracing::info!("LND: Payment for {} with amount {} msat", hash, msg.amt_paid_msat);
- let wait_response = WaitPaymentResponse {
- payment_identifier: PaymentIdentifier::PaymentHash(hash_slice),
- payment_amount: Amount::from(msg.amt_paid_msat as u64),
- unit: CurrencyUnit::Msat,
- payment_id: hash,
- };
- let event = Event::PaymentReceived(wait_response);
- return Some((event, (stream, cancel_token, is_active, kv_store, current_add_index, current_settle_index)));
- } else {
- // Invalid hash, skip this message but continue streaming
- tracing::error!("LND returned invalid payment hash");
- // Continue the loop without yielding
- continue;
- }
- } else {
- // Not a settled invoice, continue but don't emit event
- tracing::debug!("LND: Received non-settled invoice, continuing to wait for settled invoices");
- // Continue the loop without yielding
- continue;
- }
- }
- Ok(None) => {
- is_active.store(false, Ordering::SeqCst);
- tracing::info!("LND invoice stream ended.");
- return None;
- }
- Err(err) => {
- is_active.store(false, Ordering::SeqCst);
- tracing::warn!("Encountered error in LND invoice stream. Stream ending");
- tracing::error!("{:?}", err);
- return None;
- }
- }
- }
- }
- }
- },
- );
- Ok(Box::pin(event_stream))
- }
- #[instrument(skip_all)]
- async fn get_payment_quote(
- &self,
- unit: &CurrencyUnit,
- options: OutgoingPaymentOptions,
- ) -> Result<PaymentQuoteResponse, Self::Err> {
- match options {
- OutgoingPaymentOptions::Bolt11(bolt11_options) => {
- let amount_msat = match bolt11_options.melt_options {
- Some(amount) => amount.amount_msat(),
- None => bolt11_options
- .bolt11
- .amount_milli_satoshis()
- .ok_or(Error::UnknownInvoiceAmount)?
- .into(),
- };
- let amount = to_unit(amount_msat, &CurrencyUnit::Msat, unit)?;
- let relative_fee_reserve =
- (self.fee_reserve.percent_fee_reserve * u64::from(amount) as f32) as u64;
- let absolute_fee_reserve: u64 = self.fee_reserve.min_fee_reserve.into();
- let fee = max(relative_fee_reserve, absolute_fee_reserve);
- Ok(PaymentQuoteResponse {
- request_lookup_id: Some(PaymentIdentifier::PaymentHash(
- *bolt11_options.bolt11.payment_hash().as_ref(),
- )),
- amount,
- fee: fee.into(),
- state: MeltQuoteState::Unpaid,
- unit: unit.clone(),
- })
- }
- OutgoingPaymentOptions::Bolt12(_) => {
- Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
- }
- }
- }
- #[instrument(skip_all)]
- async fn make_payment(
- &self,
- _unit: &CurrencyUnit,
- options: OutgoingPaymentOptions,
- ) -> Result<MakePaymentResponse, Self::Err> {
- match options {
- OutgoingPaymentOptions::Bolt11(bolt11_options) => {
- let bolt11 = bolt11_options.bolt11;
- let pay_state = self
- .check_outgoing_payment(&PaymentIdentifier::PaymentHash(
- *bolt11.payment_hash().as_ref(),
- ))
- .await?;
- match pay_state.status {
- MeltQuoteState::Unpaid | MeltQuoteState::Unknown | MeltQuoteState::Failed => (),
- MeltQuoteState::Paid => {
- tracing::debug!("Melt attempted on invoice already paid");
- return Err(Self::Err::InvoiceAlreadyPaid);
- }
- MeltQuoteState::Pending => {
- tracing::debug!("Melt attempted on invoice already pending");
- return Err(Self::Err::InvoicePaymentPending);
- }
- }
- // Detect partial payments
- match bolt11_options.melt_options {
- Some(MeltOptions::Mpp { mpp }) => {
- let amount_msat: u64 = bolt11
- .amount_milli_satoshis()
- .ok_or(Error::UnknownInvoiceAmount)?;
- {
- let partial_amount_msat = mpp.amount;
- let invoice = bolt11;
- let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
- // Extract information from invoice
- let pub_key = invoice.get_payee_pub_key();
- let payer_addr = invoice.payment_secret().0.to_vec();
- let payment_hash = invoice.payment_hash();
- let mut lnd_client = self.lnd_client.clone();
- for attempt in 0..Self::MAX_ROUTE_RETRIES {
- // Create a request for the routes
- let route_req = lnrpc::QueryRoutesRequest {
- pub_key: hex::encode(pub_key.serialize()),
- amt_msat: u64::from(partial_amount_msat) as i64,
- fee_limit: max_fee.map(|f| {
- let limit = Limit::Fixed(u64::from(f) as i64);
- FeeLimit { limit: Some(limit) }
- }),
- use_mission_control: true,
- ..Default::default()
- };
- // Query the routes
- let mut routes_response = lnd_client
- .lightning()
- .query_routes(route_req)
- .await
- .map_err(Error::LndError)?
- .into_inner();
- // update its MPP record,
- // attempt it and check the result
- let last_hop: &mut Hop = routes_response.routes[0]
- .hops
- .last_mut()
- .ok_or(Error::MissingLastHop)?;
- let mpp_record = MppRecord {
- payment_addr: payer_addr.clone(),
- total_amt_msat: amount_msat as i64,
- };
- last_hop.mpp_record = Some(mpp_record);
- let payment_response = lnd_client
- .router()
- .send_to_route_v2(routerrpc::SendToRouteRequest {
- payment_hash: payment_hash.to_byte_array().to_vec(),
- route: Some(routes_response.routes[0].clone()),
- ..Default::default()
- })
- .await
- .map_err(Error::LndError)?
- .into_inner();
- if let Some(failure) = payment_response.failure {
- if failure.code == 15 {
- tracing::debug!(
- "Attempt number {}: route has failed. Re-querying...",
- attempt + 1
- );
- continue;
- }
- }
- // Get status and maybe the preimage
- let (status, payment_preimage) = match payment_response.status {
- 0 => (MeltQuoteState::Pending, None),
- 1 => (
- MeltQuoteState::Paid,
- Some(hex::encode(payment_response.preimage)),
- ),
- 2 => (MeltQuoteState::Unpaid, None),
- _ => (MeltQuoteState::Unknown, None),
- };
- // Get the actual amount paid in sats
- let mut total_amt: u64 = 0;
- if let Some(route) = payment_response.route {
- total_amt = (route.total_amt_msat / 1000) as u64;
- }
- return Ok(MakePaymentResponse {
- payment_lookup_id: PaymentIdentifier::PaymentHash(
- payment_hash.to_byte_array(),
- ),
- payment_proof: payment_preimage,
- status,
- total_spent: total_amt.into(),
- unit: CurrencyUnit::Sat,
- });
- }
- // "We have exhausted all tactical options" -- STEM, Upgrade (2018)
- // The payment was not possible within 50 retries.
- tracing::error!("Limit of retries reached, payment couldn't succeed.");
- Err(Error::PaymentFailed.into())
- }
- }
- _ => {
- let mut lnd_client = self.lnd_client.clone();
- let max_fee: Option<Amount> = bolt11_options.max_fee_amount;
- let amount_msat = u64::from(
- bolt11_options
- .melt_options
- .map(|a| a.amount_msat())
- .unwrap_or_default(),
- );
- let pay_req = lnrpc::SendRequest {
- payment_request: bolt11.to_string(),
- fee_limit: max_fee.map(|f| {
- let limit = Limit::Fixed(u64::from(f) as i64);
- FeeLimit { limit: Some(limit) }
- }),
- amt_msat: amount_msat as i64,
- ..Default::default()
- };
- let payment_response = lnd_client
- .lightning()
- .send_payment_sync(tonic::Request::new(pay_req))
- .await
- .map_err(|err| {
- tracing::warn!("Lightning payment failed: {}", err);
- Error::PaymentFailed
- })?
- .into_inner();
- let total_amount = payment_response
- .payment_route
- .map_or(0, |route| route.total_amt_msat / MSAT_IN_SAT as i64)
- as u64;
- let (status, payment_preimage) = match total_amount == 0 {
- true => (MeltQuoteState::Unpaid, None),
- false => (
- MeltQuoteState::Paid,
- Some(hex::encode(payment_response.payment_preimage)),
- ),
- };
- let payment_identifier =
- PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
- Ok(MakePaymentResponse {
- payment_lookup_id: payment_identifier,
- payment_proof: payment_preimage,
- status,
- total_spent: total_amount.into(),
- unit: CurrencyUnit::Sat,
- })
- }
- }
- }
- OutgoingPaymentOptions::Bolt12(_) => {
- Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
- }
- }
- }
- #[instrument(skip(self, options))]
- async fn create_incoming_payment_request(
- &self,
- unit: &CurrencyUnit,
- options: IncomingPaymentOptions,
- ) -> Result<CreateIncomingPaymentResponse, Self::Err> {
- match options {
- IncomingPaymentOptions::Bolt11(bolt11_options) => {
- let description = bolt11_options.description.unwrap_or_default();
- let amount = bolt11_options.amount;
- let unix_expiry = bolt11_options.unix_expiry;
- let amount_msat = to_unit(amount, unit, &CurrencyUnit::Msat)?;
- let invoice_request = lnrpc::Invoice {
- value_msat: u64::from(amount_msat) as i64,
- memo: description,
- ..Default::default()
- };
- let mut lnd_client = self.lnd_client.clone();
- let invoice = lnd_client
- .lightning()
- .add_invoice(tonic::Request::new(invoice_request))
- .await
- .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
- .into_inner();
- let bolt11 = Bolt11Invoice::from_str(&invoice.payment_request)?;
- let payment_identifier =
- PaymentIdentifier::PaymentHash(*bolt11.payment_hash().as_ref());
- Ok(CreateIncomingPaymentResponse {
- request_lookup_id: payment_identifier,
- request: bolt11.to_string(),
- expiry: unix_expiry,
- })
- }
- IncomingPaymentOptions::Bolt12(_) => {
- Err(Self::Err::Anyhow(anyhow!("BOLT12 not supported by LND")))
- }
- }
- }
- #[instrument(skip(self))]
- async fn check_incoming_payment_status(
- &self,
- payment_identifier: &PaymentIdentifier,
- ) -> Result<Vec<WaitPaymentResponse>, Self::Err> {
- let mut lnd_client = self.lnd_client.clone();
- let invoice_request = lnrpc::PaymentHash {
- r_hash: hex::decode(payment_identifier.to_string()).unwrap(),
- ..Default::default()
- };
- let invoice = lnd_client
- .lightning()
- .lookup_invoice(tonic::Request::new(invoice_request))
- .await
- .map_err(|e| payment::Error::Anyhow(anyhow!(e)))?
- .into_inner();
- if invoice.state() == InvoiceState::Settled {
- Ok(vec![WaitPaymentResponse {
- payment_identifier: payment_identifier.clone(),
- payment_amount: Amount::from(invoice.amt_paid_msat as u64),
- unit: CurrencyUnit::Msat,
- payment_id: hex::encode(invoice.r_hash),
- }])
- } else {
- Ok(vec![])
- }
- }
- #[instrument(skip(self))]
- async fn check_outgoing_payment(
- &self,
- payment_identifier: &PaymentIdentifier,
- ) -> Result<MakePaymentResponse, Self::Err> {
- let mut lnd_client = self.lnd_client.clone();
- let payment_hash = &payment_identifier.to_string();
- let track_request = routerrpc::TrackPaymentRequest {
- payment_hash: hex::decode(payment_hash).map_err(|_| Error::InvalidHash)?,
- no_inflight_updates: true,
- };
- let payment_response = lnd_client.router().track_payment_v2(track_request).await;
- let mut payment_stream = match payment_response {
- Ok(stream) => stream.into_inner(),
- Err(err) => {
- let err_code = err.code();
- if err_code == tonic::Code::NotFound {
- return Ok(MakePaymentResponse {
- payment_lookup_id: payment_identifier.clone(),
- payment_proof: None,
- status: MeltQuoteState::Unknown,
- total_spent: Amount::ZERO,
- unit: self.settings.unit.clone(),
- });
- } else {
- return Err(payment::Error::UnknownPaymentState);
- }
- }
- };
- while let Some(update_result) = payment_stream.next().await {
- match update_result {
- Ok(update) => {
- let status = update.status();
- let response = match status {
- PaymentStatus::Unknown => MakePaymentResponse {
- payment_lookup_id: payment_identifier.clone(),
- payment_proof: Some(update.payment_preimage),
- status: MeltQuoteState::Unknown,
- total_spent: Amount::ZERO,
- unit: self.settings.unit.clone(),
- },
- PaymentStatus::InFlight | PaymentStatus::Initiated => {
- // Continue waiting for the next update
- continue;
- }
- PaymentStatus::Succeeded => MakePaymentResponse {
- payment_lookup_id: payment_identifier.clone(),
- payment_proof: Some(update.payment_preimage),
- status: MeltQuoteState::Paid,
- total_spent: Amount::from(
- (update
- .value_sat
- .checked_add(update.fee_sat)
- .ok_or(Error::AmountOverflow)?)
- as u64,
- ),
- unit: CurrencyUnit::Sat,
- },
- PaymentStatus::Failed => MakePaymentResponse {
- payment_lookup_id: payment_identifier.clone(),
- payment_proof: Some(update.payment_preimage),
- status: MeltQuoteState::Failed,
- total_spent: Amount::ZERO,
- unit: self.settings.unit.clone(),
- },
- };
- return Ok(response);
- }
- Err(_) => {
- // Handle the case where the update itself is an error (e.g., stream failure)
- return Err(Error::UnknownPaymentStatus.into());
- }
- }
- }
- // If the stream is exhausted without a final status
- Err(Error::UnknownPaymentStatus.into())
- }
- }
|