1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
use std::ops::Add;

use crate::amount::{AsSats, ToAmount};
use crate::config::WithTimezone;
use crate::lnurl::parse_metadata;
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 {
    /// The payment was created and is in progress.
    Created,
    /// The payment succeeded.
    Succeeded,
    /// The payment failed. If it is an [`OutgoingPaymentInfo`], it can be retried.
    Failed,
    /// A payment retrial is in progress.
    Retried,
    /// The invoice associated with this payment has expired.
    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,
        }
    }
}

/// Information about a payment.
#[derive(PartialEq, Debug, Clone)]
pub struct PaymentInfo {
    pub payment_state: PaymentState,
    /// Hex representation of payment hash.
    pub hash: String,
    /// Actual amount payed or received.
    pub amount: Amount,
    pub invoice_details: InvoiceDetails,
    pub created_at: TzTime,
    /// The description embedded in the invoice. Given the length limit of this data,
    /// it is possible that a hex hash of the description is provided instead, but that is uncommon.
    pub description: String,
    /// Hex representation of the preimage. Will only be present on successful payments.
    pub preimage: Option<String>,
    /// A personal note previously added to this payment through [`LightningNode::set_payment_personal_note`](crate::LightningNode::set_payment_personal_note)
    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);

        // Use invoice timestamp for receiving payments and breez_payment.payment_time for sending ones
        // Reasoning: for receiving payments, Breez returns the time the invoice was paid. Given that
        // now we show pending invoices, this can result in a receiving payment jumping around in the
        // list when it gets paid.
        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,
        })
    }
}

/// Information about an incoming payment.
#[derive(PartialEq, Debug)]
pub struct IncomingPaymentInfo {
    pub payment_info: PaymentInfo,
    /// Nominal amount specified in the invoice.
    pub requested_amount: Amount,
    /// LSP fees paid. The amount is only paid if successful.
    pub lsp_fees: Amount,
    /// Which Lightning Address / Phone number this payment was received on.
    pub received_on: Option<Recipient>,
    /// Optional comment sent by the payer of an LNURL payment.
    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,
        })
    }
}

/// Information about an outgoing payment.
#[derive(PartialEq, Debug)]
pub struct OutgoingPaymentInfo {
    pub payment_info: PaymentInfo,
    /// Routing fees paid. Will only be present if the payment was successful.
    pub network_fees: Amount,
    /// Information about a payment's recipient.
    pub recipient: Recipient,
    /// Comment sent to the recipient.
    /// Only set for LNURL-pay and lightning address payments where a comment has been sent.
    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,
        })
    }
}

/// User-friendly representation of an outgoing payment's 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(),
            },
        }
    }
}