use std::ops::Add;
use crate::amount::{AsSats, ToAmount};
use crate::lightning::lnurl::parse_metadata;
use crate::node_config::WithTimezone;
use crate::phone_number::lightning_address_to_phone_number;
use crate::util::unix_timestamp_to_system_time;
use crate::{Amount, ExchangeRate, InvoiceDetails, Result, TzConfig, TzTime};
use breez_sdk_core::{parse_invoice, LnPaymentDetails, PaymentDetails, PaymentStatus};
use perro::{permanent_failure, MapToError};
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
#[repr(u8)]
pub enum PaymentState {
Created,
Succeeded,
Failed,
Retried,
InvoiceExpired,
}
impl From<PaymentStatus> for PaymentState {
fn from(status: PaymentStatus) -> Self {
match status {
PaymentStatus::Pending => PaymentState::Created,
PaymentStatus::Complete => PaymentState::Succeeded,
PaymentStatus::Failed => PaymentState::Failed,
}
}
}
impl PaymentState {
pub(crate) fn is_pending(&self) -> bool {
match self {
PaymentState::Created | PaymentState::Retried => true,
PaymentState::Succeeded | PaymentState::Failed | PaymentState::InvoiceExpired => false,
}
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct PaymentInfo {
pub payment_state: PaymentState,
pub hash: String,
pub amount: Amount,
pub invoice_details: InvoiceDetails,
pub created_at: TzTime,
pub description: String,
pub preimage: Option<String>,
pub personal_note: Option<String>,
}
impl PaymentInfo {
pub(crate) fn new(
breez_payment: breez_sdk_core::Payment,
exchange_rate: &Option<ExchangeRate>,
tz_config: TzConfig,
personal_note: Option<String>,
) -> Result<Self> {
let payment_details = match breez_payment.details {
PaymentDetails::Ln { data } => data,
PaymentDetails::ClosedChannel { .. } => {
permanent_failure!("PaymentInfo cannot be created from channel close")
}
};
let invoice = parse_invoice(&payment_details.bolt11).map_to_permanent_failure(format!(
"Invalid invoice provided by the Breez SDK: {}",
payment_details.bolt11
))?;
let invoice_details = InvoiceDetails::from_ln_invoice(invoice, exchange_rate);
let time = match breez_payment.payment_type {
breez_sdk_core::PaymentType::Sent => {
unix_timestamp_to_system_time(breez_payment.payment_time as u64)
}
breez_sdk_core::PaymentType::Received => invoice_details.creation_timestamp,
breez_sdk_core::PaymentType::ClosedChannel => {
permanent_failure!(
"Current interface doesn't support PaymentDetails::ClosedChannel"
)
}
};
let time = time.with_timezone(tz_config.clone());
let amount = match breez_payment.payment_type {
breez_sdk_core::PaymentType::Sent => (breez_payment.amount_msat
+ breez_payment.fee_msat)
.as_msats()
.to_amount_up(exchange_rate),
breez_sdk_core::PaymentType::Received => breez_payment
.amount_msat
.as_msats()
.to_amount_down(exchange_rate),
breez_sdk_core::PaymentType::ClosedChannel => {
permanent_failure!("PaymentInfo cannot be created from channel close")
}
};
let description = match payment_details
.lnurl_metadata
.as_deref()
.map(parse_metadata)
{
Some(Ok((short_description, _long_description))) => short_description,
_ => invoice_details.description.clone(),
};
let preimage = if payment_details.payment_preimage.is_empty() {
None
} else {
Some(payment_details.payment_preimage)
};
Ok(PaymentInfo {
payment_state: breez_payment.status.into(),
hash: payment_details.payment_hash,
amount,
invoice_details,
created_at: time,
description,
preimage,
personal_note,
})
}
}
#[derive(PartialEq, Debug)]
pub struct IncomingPaymentInfo {
pub payment_info: PaymentInfo,
pub requested_amount: Amount,
pub lsp_fees: Amount,
pub received_on: Option<Recipient>,
pub received_lnurl_comment: Option<String>,
}
impl IncomingPaymentInfo {
pub(crate) fn new(
breez_payment: breez_sdk_core::Payment,
exchange_rate: &Option<ExchangeRate>,
tz_config: TzConfig,
personal_note: Option<String>,
received_on: Option<String>,
received_lnurl_comment: Option<String>,
lipa_lightning_domain: &str,
) -> Result<Self> {
let lsp_fees = breez_payment
.fee_msat
.as_msats()
.to_amount_up(exchange_rate);
let requested_amount = breez_payment
.amount_msat
.add(breez_payment.fee_msat)
.as_msats()
.to_amount_down(exchange_rate);
let payment_info =
PaymentInfo::new(breez_payment, exchange_rate, tz_config, personal_note)?;
let received_on =
received_on.map(|r| Recipient::from_lightning_address(&r, lipa_lightning_domain));
Ok(Self {
payment_info,
requested_amount,
lsp_fees,
received_on,
received_lnurl_comment,
})
}
}
#[derive(PartialEq, Debug)]
pub struct OutgoingPaymentInfo {
pub payment_info: PaymentInfo,
pub network_fees: Amount,
pub recipient: Recipient,
pub comment_for_recipient: Option<String>,
}
impl OutgoingPaymentInfo {
pub(crate) fn new(
breez_payment: breez_sdk_core::Payment,
exchange_rate: &Option<ExchangeRate>,
tz_config: TzConfig,
personal_note: Option<String>,
lipa_lightning_domain: &str,
) -> Result<Self> {
let network_fees = breez_payment
.fee_msat
.as_msats()
.to_amount_up(exchange_rate);
let data = match breez_payment.details {
PaymentDetails::Ln { ref data } => data,
PaymentDetails::ClosedChannel { .. } => {
permanent_failure!("OutgoingPaymentInfo cannot be created from channel close")
}
};
let recipient = Recipient::from_ln_payment_details(data, lipa_lightning_domain);
let comment_for_recipient = data.lnurl_pay_comment.clone();
let payment_info =
PaymentInfo::new(breez_payment, exchange_rate, tz_config, personal_note)?;
Ok(Self {
payment_info,
network_fees,
recipient,
comment_for_recipient,
})
}
}
#[derive(PartialEq, Debug)]
pub enum Recipient {
LightningAddress { address: String },
LnUrlPayDomain { domain: String },
PhoneNumber { e164: String },
Unknown,
}
impl Recipient {
pub(crate) fn from_ln_payment_details(
payment_details: &LnPaymentDetails,
lipa_lightning_domain: &str,
) -> Self {
if let Some(address) = &payment_details.ln_address {
if let Some(e164) = lightning_address_to_phone_number(address, lipa_lightning_domain) {
return Recipient::PhoneNumber { e164 };
}
Recipient::LightningAddress {
address: address.to_string(),
}
} else if let Some(lnurlp_domain) = &payment_details.lnurl_pay_domain {
Recipient::LnUrlPayDomain {
domain: lnurlp_domain.to_string(),
}
} else {
Recipient::Unknown
}
}
pub(crate) fn from_lightning_address(address: &str, lipa_lightning_domain: &str) -> Self {
match lightning_address_to_phone_number(address, lipa_lightning_domain) {
Some(e164) => Recipient::PhoneNumber { e164 },
None => Recipient::LightningAddress {
address: address.to_string(),
},
}
}
}