Documentation
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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
//! Lexe SDK API request and response types.

use anyhow::Context;
use lexe_api::{
    models::command,
    types::{
        bounded_string::BoundedString,
        invoice::Invoice,
        payments::{
            ClientPaymentId, PaymentCreatedIndex, PaymentHash, PaymentSecret,
        },
    },
};
use lexe_common::{ln::amount::Amount, ppm::Ppm, time::TimestampMs};
use lexe_payment_uri::PaymentMethod;
use serde::{Deserialize, Serialize};

use crate::types::{
    auth::{Measurement, NodePk, UserPk},
    bitcoin::Offer,
    payment::Payment,
};

/// Information about a Lexe node.
// Simple version of `lexe_api::models::command::NodeInfo`.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NodeInfo {
    /// The node's current semver version, e.g. `0.6.9`.
    pub version: semver::Version,
    /// The hex-encoded SGX 'measurement' of the current node.
    /// The measurement is the hash of the enclave binary.
    pub measurement: Measurement,
    /// The hex-encoded ed25519 user public key used to identify a Lexe user.
    /// The user keypair is derived from the root seed.
    pub user_pk: UserPk,
    /// The hex-encoded secp256k1 Lightning node public key; the `node_id`.
    pub node_pk: NodePk,

    /// The sum of our `lightning_balance` and our `onchain_balance`, in sats.
    pub balance: Amount,

    /// Total Lightning balance in sats, summed over all of our channels.
    pub lightning_balance: Amount,
    /// An estimated upper bound, in sats, on how much of our Lightning balance
    /// we can send to most recipients on the Lightning Network, accounting for
    /// Lightning limits such as our channel reserve, pending HTLCs, fees, etc.
    /// You should usually be able to spend this amount.
    // User-facing name for `LightningBalance::sendable`
    pub lightning_sendable_balance: Amount,
    /// A hard upper bound on how much of our Lightning balance can be spent
    /// right now, in sats. This is always >= `lightning_sendable_balance`.
    /// Generally it is only possible to spend exactly this amount if the
    /// recipient is a Lexe user.
    // User-facing name for `LightningBalance::max_sendable`
    pub lightning_max_sendable_balance: Amount,

    /// Total on-chain balance in sats, including unconfirmed funds.
    // `OnchainBalance::total`
    pub onchain_balance: Amount,
    /// Trusted on-chain balance in sats, including only confirmed funds and
    /// unconfirmed outputs originating from our own wallet.
    // Equivalent to BDK's `trusted_spendable`, but with a better name.
    pub onchain_trusted_balance: Amount,

    /// The total number of Lightning channels.
    pub num_channels: usize,
    /// The number of channels which are currently usable, i.e. `channel_ready`
    /// messages have been exchanged and the channel peer is online.
    /// Is always less than or equal to `num_channels`.
    pub num_usable_channels: usize,
}

impl From<command::NodeInfo> for NodeInfo {
    fn from(info: command::NodeInfo) -> Self {
        let lightning_balance = info.lightning_balance.total();
        let onchain_balance = Amount::try_from(info.onchain_balance.total())
            .expect("We're unreasonably rich!");
        let onchain_trusted_balance =
            Amount::try_from(info.onchain_balance.trusted_spendable())
                .expect("We're unreasonably rich!");
        let balance = lightning_balance.saturating_add(onchain_balance);

        Self {
            version: info.version,
            measurement: Measurement::from_unstable(info.measurement),
            user_pk: UserPk::from_unstable(info.user_pk),
            node_pk: NodePk::from_unstable(info.node_pk),

            balance,

            lightning_balance,
            lightning_sendable_balance: info.lightning_balance.sendable,
            lightning_max_sendable_balance: info.lightning_balance.max_sendable,
            onchain_balance,
            onchain_trusted_balance,
            num_channels: info.num_channels,
            num_usable_channels: info.num_usable_channels,
        }
    }
}

/// A request to analyze the contents of a Bitcoin or Lightning payment string.
/// Reveals all payment methods encoded in the string, and gives payment-related
/// details on each. See [`PayableDetails`] for more info.
pub struct AnalyzeRequest {
    /// The Bitcoin or Lightning payment string to analyze.
    pub payable: String,
}

/// Describes basic information for a payable string.
pub struct PayableDetails {
    /// The payable string encoding the payment method.
    pub payable: String,
    /// The deserialized payment method.
    pub method: PaymentMethod,

    /// The description encoded in the `payable`, if any.
    pub description: Option<String>,

    /// The amount that should be paid to the `payable`; if `None`, the payer
    /// should specify an amount to pay.
    ///
    /// This will be `None` if `min_amount` or `max_amount` are specified.
    pub amount: Option<Amount>,
    /// The minimum amount that can be paid to the `payable`.
    ///
    /// This will be `None` if `amount` is specified.
    pub min_amount: Option<Amount>,
    /// The maximum amount that can be paid to the `payable`.
    ///
    /// This will be `None` if `amount` is specified.
    pub max_amount: Option<Amount>,

    /// The payable expiration time, in milliseconds since the UNIX epoch.
    pub expires_at: Option<TimestampMs>,
}

/// The response to a string analysis request.
pub struct AnalyzeResponse {
    // TODO: kind: PaymentUri
    /// The valid payment routes encoded in the analyzed string, ordered
    /// by most recommended payment route first, and least recommended payment
    /// route last.
    pub payables: Vec<PayableDetails>,
}

/// A catch-all request to pay a Bitcoin or Lightning payment string.
///
/// The following encodings are supported:
///   - BIP 321 URI: `bitcoin:bc1...`
///   - Lightning URI: `lightning:ln...`
///   - BOLT 11 invoice: `lnbc1...`
///   - BOLT 12 offer: `lno1...`
///   - Onchain bitcoin address: `bc1...`
///   - Human Bitcoin Address: `â‚¿satoshi@lexe.app`
///   - Lightning Address: `satoshi@lexe.app`
///   - LNURL: `lnurl1...` or `lnurlp://domain.com/path`
///
/// See [`PaymentMethod`] for more details on supported payment methods.
///
/// If there exist multiple encoded payment methods, the best recommended
/// payment method will be chosen.
pub struct PayRequest {
    /// The string we will pay.
    pub payable: String,

    /// The amount we will attempt to pay.
    /// If the payable specifies an amount, this field is optional.
    pub amount: Option<Amount>,

    /// An optional message to send to the recipient, supported if sending to:
    ///
    /// - BOLT 12 offers
    /// - Human Bitcoin Addresses which point to an offer
    /// - LNURL recipients whose wallets accept LUD-12 comments
    /// - Lightning Addresses to a wallet that accepts LUD-12 comments
    ///
    /// If the payable doesn't support messages, this will be ignored.
    ///
    /// If provided, it must be non-empty and no longer than 200 chars / 512
    /// UTF-8 bytes.
    pub message: Option<String>,

    /// An optional personal note for this payment.
    ///
    /// The receiver will not see this note.
    ///
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub personal_note: Option<String>,
}

/// The response to a general pay request.
pub struct PayResponse {
    /// Identifier for this outbound payment.
    pub index: PaymentCreatedIndex,
    /// When we tried to pay, in milliseconds since the UNIX epoch.
    pub created_at: TimestampMs,
}

/// A request to create a BOLT 11 invoice.
#[derive(Default, Serialize, Deserialize)]
pub struct CreateInvoiceRequest {
    /// The expiration, in seconds, to encode into the invoice.
    /// If no duration is provided, the expiration time defaults to 86400
    /// (1 day).
    pub expiration_secs: Option<u32>,

    /// Optionally include an amount, in sats, to encode into the invoice.
    /// If no amount is provided, the sender will specify how much to pay.
    pub amount: Option<Amount>,

    /// The description to be encoded into the invoice.
    /// The sender will see this description when they scan the invoice.
    // If `None`, the `description` field inside the invoice will be an empty
    // string (""), as lightning _requires_ a description (or description
    // hash) to be set.
    pub description: Option<String>,

    /// The partner's user_pk, if the partner is setting the fee for this
    /// payment instead of using Lexe's default fees.
    ///
    /// This must be set in order for `partner_prop_fee` and `partner_base_fee`
    /// to take effect.
    // Added in `node-v0.9.6`
    #[serde(default)]
    pub partner_pk: Option<UserPk>,

    /// The partner-chosen proportional fee to charge on this payment.
    /// If `partner_pk` is set, this must be set to [`Some`].
    ///
    /// Minimum: 5000 ppm (`LSP_USERNODE_SKIM_FEE_PPM`)
    /// Maximum: 500,000 ppm (50%)
    // Added in `node-v0.9.6`
    #[serde(default)]
    pub partner_prop_fee: Option<Ppm>,

    /// The partner-chosen base fee to charge on this payment.
    ///
    /// If this is set, the invoice `amount` must also be set.
    // Added in `node-v0.9.6`
    #[serde(default)]
    pub partner_base_fee: Option<Amount>,
}

/// The response to a BOLT 11 invoice request.
#[derive(Serialize, Deserialize)]
pub struct CreateInvoiceResponse {
    /// Identifier for this inbound invoice payment.
    pub index: PaymentCreatedIndex,
    /// The BOLT 11 invoice.
    pub invoice: Invoice,
    /// The description encoded in the invoice, if one was provided.
    pub description: Option<String>,
    /// The amount encoded in the invoice, if there was one.
    /// Returning `None` means we created an amountless invoice.
    pub amount: Option<Amount>,
    /// The invoice creation time, in milliseconds since the UNIX epoch.
    pub created_at: TimestampMs,
    /// The invoice expiration time, in milliseconds since the UNIX epoch.
    pub expires_at: TimestampMs,
    /// The hex-encoded payment hash of the invoice.
    pub payment_hash: PaymentHash,
    /// The payment secret of the invoice.
    pub payment_secret: PaymentSecret,
}

impl CreateInvoiceResponse {
    /// Build a [`CreateInvoiceResponse`] from an index and invoice.
    pub fn new(index: PaymentCreatedIndex, invoice: Invoice) -> Self {
        let description = invoice.description_str().map(|s| s.to_owned());
        let amount_sats = invoice.amount();
        let created_at = invoice.saturating_created_at();
        let expires_at = invoice.saturating_expires_at();
        let payment_hash = invoice.payment_hash();
        let payment_secret = invoice.payment_secret();

        Self {
            index,
            invoice,
            description,
            amount: amount_sats,
            created_at,
            expires_at,
            payment_hash,
            payment_secret,
        }
    }
}

impl TryFrom<CreateInvoiceRequest> for command::CreateInvoiceRequest {
    type Error = anyhow::Error;

    fn try_from(req: CreateInvoiceRequest) -> anyhow::Result<Self> {
        /// The default expiration we use if none is provided.
        const DEFAULT_EXPIRATION_SECS: u32 = 60 * 60 * 24; // 1 day

        Ok(Self {
            expiry_secs: req.expiration_secs.unwrap_or(DEFAULT_EXPIRATION_SECS),
            amount: req.amount,
            description: req.description,
            // TODO(maurice): Add description_hash if we really need it.
            description_hash: None,
            message: None,
            partner_pk: req.partner_pk.map(|pk| pk.unstable()),
            partner_prop_fee: req.partner_prop_fee,
            partner_base_fee: req.partner_base_fee,
        })
    }
}

/// A request to pay a BOLT 11 invoice.
#[derive(Serialize, Deserialize)]
pub struct PayInvoiceRequest {
    /// The invoice we want to pay.
    pub invoice: Invoice,
    /// Specifies the amount we will pay if the invoice to be paid is
    /// amountless. This field must be set if the invoice is amountless.
    pub fallback_amount: Option<Amount>,
    /// An optional personal note for this payment.
    /// The receiver will not see this note.
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub personal_note: Option<String>,
}

impl TryFrom<PayInvoiceRequest> for command::PayInvoiceRequest {
    type Error = anyhow::Error;

    fn try_from(req: PayInvoiceRequest) -> anyhow::Result<Self> {
        Ok(Self {
            invoice: req.invoice,
            fallback_amount: req.fallback_amount,
            message: None,
            personal_note: req
                .personal_note
                .map(BoundedString::new)
                .transpose()
                .context("Invalid personal note")?,
        })
    }
}

/// The response to a request to pay a BOLT 11 invoice.
#[derive(Serialize, Deserialize)]
pub struct PayInvoiceResponse {
    /// Identifier for this outbound invoice payment.
    pub index: PaymentCreatedIndex,
    /// When we tried to pay this invoice, in milliseconds since the UNIX
    /// epoch.
    pub created_at: TimestampMs,
}

/// A request to create a BOLT 12 offer to receive Lightning payments.
///
/// Unlike invoices, offers are reusable: multiple payments can be made to
/// it, including from multiple payers.
#[derive(Default, Serialize, Deserialize)]
pub struct CreateOfferRequest {
    /// An optional description to encode into the offer.
    ///
    /// The sender will see this description when they scan the offer.
    ///
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub description: Option<String>,

    /// An optional minimum payment size for payments to this offer.
    /// If not set, the payer can send any amount.
    pub min_amount: Option<Amount>,

    /// An optional expiration for the offer, in seconds from now.
    pub expiration_secs: Option<u32>,
}

impl TryFrom<CreateOfferRequest> for command::CreateOfferRequest {
    type Error = anyhow::Error;

    fn try_from(req: CreateOfferRequest) -> anyhow::Result<Self> {
        let description = req
            .description
            .map(BoundedString::new)
            .transpose()
            .context("Invalid description")?;

        Ok(Self {
            description,
            min_amount: req.min_amount,
            expiry_secs: req.expiration_secs,
            max_quantity: None,
            issuer: None,
        })
    }
}

/// The response to a BOLT 12 offer creation request.
#[derive(Serialize, Deserialize)]
pub struct CreateOfferResponse {
    /// The BOLT 12 offer.
    pub offer: Offer,
}

/// A request to pay a BOLT 12 offer over Lightning.
#[derive(Serialize, Deserialize)]
pub struct PayOfferRequest {
    /// The offer we want to pay.
    pub offer: Offer,
    /// The amount we will pay. If the offer specifies a minimum amount,
    /// this value must satisfy that minimum.
    pub amount: Amount,
    /// An optional message (sent as a BOLT 12 `payer_note`) included with the
    /// invoice request and visible to the recipient. If provided, it must be
    /// non-empty and no longer than 200 chars / 512 UTF-8 bytes.
    pub message: Option<String>,
    /// An optional personal note for this payment.
    /// The receiver will not see this note.
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    pub personal_note: Option<String>,
}

impl PayOfferRequest {
    /// Build a [`command::PayOfferRequest`] from this SDK request.
    pub(crate) fn into_unstable(
        self,
        cid: ClientPaymentId,
    ) -> anyhow::Result<command::PayOfferRequest> {
        Ok(command::PayOfferRequest {
            cid,
            offer: self.offer,
            amount: self.amount,
            message: self
                .message
                .map(BoundedString::new)
                .transpose()
                .context("Invalid message")?,
            personal_note: self
                .personal_note
                .map(BoundedString::new)
                .transpose()
                .context("Invalid personal note")?,
        })
    }
}

/// The response to a request to pay a BOLT 12 offer.
#[derive(Serialize, Deserialize)]
pub struct PayOfferResponse {
    /// Identifier for this outbound offer payment.
    pub index: PaymentCreatedIndex,
    /// When we tried to pay this offer, in milliseconds since the UNIX epoch.
    pub created_at: TimestampMs,
}

/// A request to update the personal note on an existing payment.
/// Pass `None` to clear the note.
#[derive(Serialize, Deserialize)]
pub struct UpdatePersonalNoteRequest {
    /// Identifier for the payment to be updated.
    pub index: PaymentCreatedIndex,
    /// The updated note, or `None` to clear.
    /// If provided, it must be non-empty and no longer than 200 chars /
    /// 512 UTF-8 bytes.
    // compat: Alias added in node-v0.9.7
    #[serde(rename = "note", alias = "personal_note")]
    pub personal_note: Option<String>,
}

impl TryFrom<UpdatePersonalNoteRequest> for command::UpdatePersonalNote {
    type Error = anyhow::Error;

    fn try_from(sdk: UpdatePersonalNoteRequest) -> anyhow::Result<Self> {
        Ok(Self {
            index: sdk.index,
            personal_note: sdk
                .personal_note
                .map(BoundedString::new)
                .transpose()
                .context("Invalid note")?,
        })
    }
}

/// A request to get information about a payment by its index.
#[derive(Serialize, Deserialize)]
pub struct GetPaymentRequest {
    /// Identifier for this payment.
    pub index: PaymentCreatedIndex,
}

/// A response to a request to get information about a payment by its index.
#[derive(Serialize, Deserialize)]
pub struct GetPaymentResponse {
    /// Information about this payment, if it exists.
    pub payment: Option<Payment>,
}

/// Response from listing payments.
#[derive(Serialize, Deserialize)]
pub struct ListPaymentsResponse {
    /// Payments in the requested page.
    pub payments: Vec<Payment>,
    /// Cursor for fetching the next page. `None` when there are no more
    /// results. Pass this as the `after` argument to get the next page.
    pub next_index: Option<PaymentCreatedIndex>,
}

/// Summary of changes from a payment sync operation.
#[derive(Debug)]
pub struct PaymentSyncSummary {
    /// Number of new payments added to the local database.
    pub num_new: usize,
    /// Number of existing payments that were updated.
    pub num_updated: usize,
}