Skip to main content

lexe_api_core/models/
command.rs

1use std::collections::BTreeSet;
2
3use bitcoin::{address::NetworkUnchecked, bip32::Xpub};
4#[cfg(doc)]
5use lexe_common::root_seed::RootSeed;
6#[cfg(any(test, feature = "test-utils"))]
7use lexe_common::test_utils::arbitrary;
8use lexe_common::{
9    api::user::{NodePk, UserPk},
10    ln::{
11        amount::Amount,
12        balance::{LightningBalance, OnchainBalance},
13        channel::{ChannelId, LxChannelDetails, UserChannelId},
14        hashes::Txid,
15        priority::ConfirmationPriority,
16        route::LxRoute,
17    },
18    ppm::Ppm,
19    time::TimestampMs,
20};
21use lexe_enclave::enclave::Measurement;
22use lexe_serde::{base64_or_bytes, base64_or_bytes_opt, hexstr_or_bytes};
23#[cfg(any(test, feature = "test-utils"))]
24use proptest_derive::Arbitrary;
25use serde::{Deserialize, Serialize};
26
27use crate::types::{
28    bounded_string::BoundedString,
29    invoice::Invoice,
30    offer::{MaxQuantity, Offer},
31    payments::{
32        ClientPaymentId, PaymentCreatedIndex, PaymentId, PaymentKind,
33        PaymentUpdatedIndex,
34    },
35    username::Username,
36};
37
38// --- General --- //
39
40/// Information about the Lexe node.
41#[derive(Debug, Serialize, Deserialize)]
42pub struct NodeInfo {
43    pub version: semver::Version,
44    pub measurement: Measurement,
45    pub user_pk: UserPk,
46    pub node_pk: NodePk,
47    pub num_peers: usize,
48
49    pub num_usable_channels: usize,
50    pub num_channels: usize,
51    /// Our lightning channel balance.
52    pub lightning_balance: LightningBalance,
53
54    /// Our on-chain wallet balance.
55    pub onchain_balance: OnchainBalance,
56
57    /// The channel manager's best synced block height.
58    pub best_block_height: u32,
59}
60
61/// Diagnostic information for debugging purposes.
62#[derive(Debug, Serialize, Deserialize)]
63pub struct DebugInfo {
64    /// Output descriptors for the on-chain wallet.
65    pub descriptors: OnchainDescriptors,
66    /// Legacy descriptors for wallets created <= node-v0.9.2.
67    /// `None` if the node doesn't have a legacy wallet.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub legacy_descriptors: Option<OnchainDescriptors>,
70
71    /// The total # of UTXOs tracked by BDK.
72    pub num_utxos: usize,
73    /// The # of confirmed UTXOs tracked by BDK.
74    // TODO(max): LSP metrics should warn if this drops too low, as opening
75    // zeroconf with unconfirmed inputs risks double spending of channel funds.
76    pub num_confirmed_utxos: usize,
77    /// The # of unconfirmed UTXOs tracked by BDK.
78    pub num_unconfirmed_utxos: usize,
79
80    /// The number of pending channel monitor updates.
81    /// If this isn't 0, it's likely that at least one channel is paused.
82    // TODO(max): This field is in the wrong place and should be removed.
83    // To my knowledge it is only used by integration tests (in a hacky way) to
84    // wait for a node to reach a quiescent state. The polling should be done
85    // inside the server handler rather than by the client in the test harness.
86    // This field is `Option` precisely so we can easily remove it later.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub pending_monitor_updates: Option<usize>,
89}
90
91/// BIP84 wpkh output descriptors for the on-chain wallet.
92///
93/// Descriptor strings include origin info and checksum. Example:
94/// "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#dwvchw0k"
95///
96/// These are copy-pasteable into other wallets like Sparrow.
97#[derive(Clone, Debug, Serialize, Deserialize)]
98pub struct OnchainDescriptors {
99    /// BIP389 multipath descriptor for both keychains:
100    /// `wpkh([fp/84'/0'/0']xpub.../<0;1>/*)#checksum`
101    pub multipath_descriptor: String,
102
103    /// External (receive) keychain descriptor.
104    pub external_descriptor: String,
105
106    /// Internal (change) keychain descriptor.
107    pub internal_descriptor: String,
108
109    /// Account-level xpub at `m/84'/{coin}'/0'` for legacy tools.
110    pub account_xpub: Xpub,
111}
112
113#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
114pub enum GDriveStatus {
115    Ok,
116    Error(String),
117    Disabled,
118}
119
120#[derive(Debug, Serialize, Deserialize)]
121pub struct BackupInfo {
122    pub gdrive_status: GDriveStatus,
123}
124
125/// Request to query which node enclaves need provisioning, given the client's
126/// trusted measurements.
127#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
128#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
129pub struct EnclavesToProvisionRequest {
130    /// The enclave measurements the client trusts.
131    /// Typically the 3 latest from releases.json.
132    pub trusted_measurements: BTreeSet<Measurement>,
133}
134
135/// The gateway→backend form of [`EnclavesToProvisionRequest`], carrying the
136/// `user_pk` that the gateway verified from the app's bearer auth token.
137#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
138#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
139pub struct EnclavesToProvisionForUser {
140    pub user_pk: UserPk,
141    /// The enclave measurements the client trusts.
142    pub trusted_measurements: BTreeSet<Measurement>,
143}
144
145#[derive(Debug, PartialEq, Serialize, Deserialize)]
146#[cfg_attr(test, derive(Arbitrary))]
147pub struct SetupGDrive {
148    /// The auth `code` which can used to obtain a set of GDrive credentials.
149    /// - Applicable only in staging/prod.
150    /// - If GDrive has not been setup, the node will acquire the full set of
151    ///   GDrive credentials and persist them (encrypted ofc) in Lexe's DB.
152    #[cfg_attr(test, proptest(strategy = "arbitrary::any_string()"))]
153    pub google_auth_code: String,
154
155    /// The password-encrypted [`RootSeed`] which can be backed up in
156    /// GDrive.
157    /// - Applicable only in staging/prod.
158    /// - If Drive backup is not setup, instance will back up this encrypted
159    ///   [`RootSeed`] in Google Drive. If a backup already exists, it is
160    ///   overwritten.
161    /// - We require the client to password-encrypt prior to sending the
162    ///   provision request to prevent leaking the length of the password. It
163    ///   also shifts the burden of running the 600K HMAC iterations from the
164    ///   provision instance to the mobile app.
165    #[serde(with = "hexstr_or_bytes")]
166    pub encrypted_seed: Vec<u8>,
167}
168// --- Channel Management --- //
169
170#[derive(Serialize, Deserialize)]
171pub struct ListChannelsResponse {
172    pub channels: Vec<LxChannelDetails>,
173}
174
175/// The information required for the user node to open a channel to the LSP.
176#[derive(Serialize, Deserialize)]
177pub struct OpenChannelRequest {
178    /// A user-provided id for this channel that's associated with the channel
179    /// throughout its whole lifetime, as the Lightning protocol channel id is
180    /// only known after negotiating the channel and creating the funding tx.
181    ///
182    /// This id is also used for idempotency. Retrying a request with the same
183    /// `user_channel_id` won't accidentally open another channel.
184    pub user_channel_id: UserChannelId,
185    /// The value of the channel we want to open.
186    pub value: Amount,
187}
188
189#[derive(Debug, Serialize, Deserialize)]
190pub struct OpenChannelResponse {
191    /// The Lightning protocol channel id of the newly created channel.
192    pub channel_id: ChannelId,
193}
194
195#[derive(Serialize, Deserialize)]
196pub struct OpenChannelPreflightRequest {
197    /// The value of the channel we want to open.
198    pub value: Amount,
199}
200
201#[derive(Debug, Serialize, Deserialize)]
202pub struct OpenChannelPreflightResponse {
203    /// The estimated on-chain fee required to execute the channel open.
204    pub fee_estimate: Amount,
205}
206
207#[derive(Serialize, Deserialize)]
208pub struct CloseChannelRequest {
209    /// The id of the channel we want to close.
210    pub channel_id: ChannelId,
211    /// Set to true if the channel should be force closed (unilateral).
212    /// Set to false if the channel should be cooperatively closed (bilateral).
213    pub force_close: bool,
214    /// The [`NodePk`] of our counterparty.
215    ///
216    /// If set to [`None`], the counterparty's [`NodePk`] will be determined by
217    /// calling [`list_channels`]. Setting this to [`Some`] allows
218    /// `close_channel` to avoid this relatively expensive [`Vec`] allocation.
219    ///
220    /// [`list_channels`]: lightning::ln::channelmanager::ChannelManager::list_channels
221    pub maybe_counterparty: Option<NodePk>,
222}
223
224pub type CloseChannelPreflightRequest = CloseChannelRequest;
225
226#[derive(Serialize, Deserialize)]
227pub struct CloseChannelPreflightResponse {
228    /// The estimated on-chain fee required to execute the channel close.
229    pub fee_estimate: Amount,
230}
231
232// --- Syncing and updating payments data --- //
233
234/// Upgradeable API struct for a [`PaymentId`].
235#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
236#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
237pub struct PaymentIdStruct {
238    /// The id of the payment to be fetched.
239    pub id: PaymentId,
240}
241
242/// An upgradeable version of [`Vec<PaymentId>`].
243#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
244#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
245pub struct VecPaymentId {
246    pub ids: Vec<PaymentId>,
247}
248
249/// Upgradeable API struct for a payment index.
250#[derive(Debug, PartialEq, Serialize, Deserialize)]
251#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
252pub struct PaymentCreatedIndexStruct {
253    /// The index of the payment to be fetched.
254    pub index: PaymentCreatedIndex,
255}
256
257/// Sync a batch of new payments to local storage.
258/// Results are returned in ascending `(created_at, payment_id)` order.
259#[derive(Debug, PartialEq, Serialize, Deserialize)]
260#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
261pub struct GetNewPayments {
262    /// Optional [`PaymentCreatedIndex`] at which the results should start,
263    /// exclusive. Payments with an index less than or equal to this will
264    /// not be returned.
265    pub start_index: Option<PaymentCreatedIndex>,
266    /// (Optional) the maximum number of results that can be returned.
267    pub limit: Option<u16>,
268}
269
270/// Get a batch of payments in ascending `(updated_at, payment_id)` order.
271#[derive(Debug, PartialEq, Serialize, Deserialize)]
272#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
273pub struct GetUpdatedPayments {
274    /// `(updated_at, id)` index at which the results should start, exclusive.
275    /// Payments with an index less than or equal to this will not be returned.
276    pub start_index: Option<PaymentUpdatedIndex>,
277    /// (Optional) the maximum number of results that can be returned.
278    pub limit: Option<u16>,
279}
280
281/// Get a batch of payment metadata in asc `(updated_at, payment_id)` order.
282#[derive(Debug, PartialEq, Serialize, Deserialize)]
283#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
284pub struct GetUpdatedPaymentMetadata {
285    /// `(updated_at, id)` index at which the results should start, exclusive.
286    /// Metadata with an index less than or equal to this will not be returned.
287    pub start_index: Option<PaymentUpdatedIndex>,
288    /// (Optional) the maximum number of results that can be returned.
289    pub limit: Option<u16>,
290}
291
292/// Upgradeable API struct for a list of [`PaymentCreatedIndex`]s.
293#[derive(Debug, PartialEq, Serialize, Deserialize)]
294#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
295pub struct PaymentCreatedIndexes {
296    /// The string-serialized [`PaymentCreatedIndex`]s of the payments to be
297    /// fetched. Typically, the ids passed here correspond to payments that
298    /// the mobile client currently has stored locally as "pending"; the
299    /// goal is to check whether any of these payments have been updated.
300    pub indexes: Vec<PaymentCreatedIndex>,
301}
302
303/// A request to update the personal note on a payment. Pass `None` to clear.
304#[derive(Clone, Serialize, Deserialize)]
305pub struct UpdatePersonalNote {
306    /// The index of the payment whose personal note should be updated.
307    // TODO(max): The server side only needs the `PaymentId`.
308    // This API should be changed to pass that instead.
309    pub index: PaymentCreatedIndex,
310    /// The updated note, or `None` to clear.
311    // compat: Alias added in node-v0.9.7
312    #[serde(rename = "note", alias = "personal_note")]
313    pub personal_note: Option<BoundedString>,
314}
315
316// --- BOLT11 Invoice Payments --- //
317
318/// The default [`PaymentKind`] for invoice-rail request endpoints.
319fn default_invoice_kind() -> PaymentKind {
320    PaymentKind::Invoice
321}
322
323#[derive(Serialize, Deserialize)]
324pub struct CreateInvoiceRequest {
325    pub expiry_secs: u32,
326
327    /// The amount to encode into the invoice.
328    pub amount: Option<Amount>,
329
330    /// The description to be encoded into the invoice.
331    ///
332    /// If `None`, the `description` field inside the invoice will be an empty
333    /// string (""), as lightning _requires_ a description (or description
334    /// hash) to be set.
335    /// NOTE: If both `description` and `description_hash` are set, node will
336    /// return an error.
337    pub description: Option<String>,
338
339    /// A 256-bit hash. Commonly a hash of a long description.
340    ///
341    /// This field is used to associate description longer than 639 bytes to
342    /// the invoice. Also known as '`h` tag in BOLT11'.
343    ///
344    /// This field is required to build invoices for the LNURL (LUD06)
345    /// receiving flow. Not used in other flows.
346    /// NOTE: If both `description` and `description_hash` are set, node will
347    /// return an error.
348    pub description_hash: Option<[u8; 32]>,
349
350    /// An optional message from the payer, stored with this inbound payment.
351    /// For LNURL-pay, set from the LUD-12 `comment`.
352    // compat: Alias added in node-v0.9.7
353    #[serde(rename = "payer_note", alias = "message")]
354    pub message: Option<BoundedString>,
355
356    /// An optional personal note for this invoice.
357    /// Added in `node-v0.9.10`
358    pub personal_note: Option<BoundedString>,
359
360    /// The [`PaymentKind`] to label this inbound invoice payment with.
361    /// `kind.rail()` must == `PaymentRail::Invoice` (or `kind` must be
362    /// unknown), otherwise the request is rejected.
363    ///
364    /// Defaults to [`PaymentKind::Invoice`] if not set.
365    // Added in `node-v0.9.11`
366    #[serde(default = "default_invoice_kind")]
367    pub kind: PaymentKind,
368
369    /// The partner's user_pk, if the partner is setting the fee for this
370    /// payment instead of using Lexe's default fees.
371    ///
372    /// This must be set in order for `partner_prop_fee` and `partner_base_fee`
373    /// to take effect.
374    // Added in `node-v0.9.6`
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub partner_pk: Option<UserPk>,
377
378    /// The partner-chosen proportional fee to charge on this payment.
379    /// If `partner_pk` is set, this must be set to [`Some`].
380    ///
381    /// Minimum: 5000 ppm (`LSP_USERNODE_SKIM_FEE`)
382    /// Maximum: 500,000 ppm (50%)
383    // Added in `node-v0.9.6`
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub partner_prop_fee: Option<Ppm>,
386
387    /// The partner-chosen base fee to charge on this payment.
388    ///
389    /// If this is set, the invoice `amount` must also be set.
390    // Added in `node-v0.9.6`
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub partner_base_fee: Option<Amount>,
393}
394
395impl Default for CreateInvoiceRequest {
396    fn default() -> Self {
397        Self {
398            expiry_secs: 0,
399            amount: None,
400            description: None,
401            description_hash: None,
402            message: None,
403            personal_note: None,
404            kind: default_invoice_kind(),
405            partner_pk: None,
406            partner_prop_fee: None,
407            partner_base_fee: None,
408        }
409    }
410}
411
412#[derive(Serialize, Deserialize)]
413pub struct CreateInvoiceResponse {
414    pub invoice: Invoice,
415    /// The [`PaymentCreatedIndex`] of the newly created invoice payment.
416    ///
417    /// Is always `Some` starting at `node-v0.8.10` and `lsp-v0.8.11`.
418    //
419    // TODO(max): Make non-Option once all servers are sufficiently upgraded.
420    pub created_index: Option<PaymentCreatedIndex>,
421}
422
423#[derive(Serialize, Deserialize)]
424pub struct PayInvoiceRequest {
425    /// The invoice we want to pay.
426    pub invoice: Invoice,
427    /// Specifies the amount we will pay if the invoice to be paid is
428    /// amountless. This field must be [`Some`] for amountless invoices.
429    pub fallback_amount: Option<Amount>,
430    /// An optional message to persist with this outbound payment. For
431    /// LNURL-pay, this is the LUD-12 `comment` sent during invoice
432    /// negotiation.
433    // compat: Alias added in node-v0.9.7
434    #[serde(rename = "payer_note", alias = "message")]
435    pub message: Option<BoundedString>,
436    /// An optional personal note for this payment, useful if the
437    /// receiver-provided description is insufficient.
438    // compat: Alias added in node-v0.9.7
439    #[serde(rename = "note", alias = "personal_note")]
440    pub personal_note: Option<BoundedString>,
441    /// The [`PaymentKind`] to label this outbound invoice payment with.
442    /// `kind.rail()` must == `PaymentRail::Invoice` (or `kind` must be
443    /// unknown), otherwise the request is rejected.
444    ///
445    /// Defaults to [`PaymentKind::Invoice`] if not set.
446    // Added in `node-v0.9.11`
447    #[serde(default = "default_invoice_kind")]
448    pub kind: PaymentKind,
449    /// The precomputed route from [`PayInvoicePreflightResponse`], which the
450    /// caller should pass here to ensure that:
451    ///
452    /// 1) The preflighted fee that the user sees is actually what they pay
453    /// 2) `pay_invoice` doesn't need to recompute the route, saving time and
454    ///    compute.
455    //
456    // Added in `node-v0.9.12`
457    //
458    // NOTE: `default` is needed to support old clients despite `Option`:
459    // `#[serde(with)]` disables serde's implicit missing-field handling.
460    #[serde(default, with = "base64_or_bytes_opt")]
461    pub ldk_route: Option<Vec<u8>>,
462}
463
464#[derive(Serialize, Deserialize)]
465pub struct PayInvoiceResponse {
466    /// When the node registered this payment.
467    /// Used in the [`PaymentCreatedIndex`].
468    pub created_at: TimestampMs,
469}
470
471#[derive(Serialize, Deserialize)]
472pub struct PayInvoicePreflightRequest {
473    /// The invoice we want to pay.
474    pub invoice: Invoice,
475    /// Specifies the amount we will pay if the invoice to be paid is
476    /// amountless. This field must be [`Some`] for amountless invoices.
477    pub fallback_amount: Option<Amount>,
478    /// The [`PaymentKind`] the real payment will be labeled with.
479    // Added in `node-v0.9.11`
480    #[serde(default = "default_invoice_kind")]
481    pub kind: PaymentKind,
482}
483
484#[derive(Serialize, Deserialize)]
485pub struct PayInvoicePreflightResponse {
486    /// The total amount to-be-paid for the pre-flighted [`Invoice`],
487    /// excluding the fees.
488    ///
489    /// This value may be different from the value originally requested if
490    /// we had to reach `htlc_minimum_msat` for some intermediate hops.
491    pub amount: Amount,
492    /// The total amount of fees to-be-paid for the pre-flighted [`Invoice`].
493    pub fees: Amount,
494    /// The route used in the preflight, to be used for informational purposes.
495    ///
496    /// See `ldk_route` for how to pay the invoice along this route.
497    // Added in `node-v0.7.8`
498    pub route: LxRoute,
499    /// A precomputed route that the caller should pass into
500    /// [`PayInvoiceRequest`] to ensure that:
501    ///
502    /// 1) The preflighted fee that the user sees is actually what they pay
503    /// 2) `pay_invoice` doesn't need to recompute the route, saving time and
504    ///    compute.
505    // Added in `node-v0.9.12`
506    #[serde(with = "base64_or_bytes")]
507    pub ldk_route: Vec<u8>,
508}
509
510// --- BOLT12 Offer payments --- //
511
512#[derive(Default, Serialize, Deserialize)]
513pub struct CreateOfferRequest {
514    /// The description to be encoded into the invoice.
515    ///
516    /// If `None`, the `description` field inside the invoice will be an empty
517    /// string (""), as lightning _requires_ a description to be set.
518    pub description: Option<BoundedString>,
519
520    /// The `min_amount` we're requesting for payments using this offer.
521    ///
522    /// If `None`, the offer is variable amount and the payer can choose any
523    /// value.
524    ///
525    /// If `Some`, the offer amount is lower-bounded and the payer must pay
526    /// this value or higher (per item, see `max_quantity`). The offer amount
527    /// must be a non-zero value if set.
528    // Renamed in node-v0.9.4
529    #[serde(alias = "amount")]
530    pub min_amount: Option<Amount>,
531
532    /// An optional expiration for the offer, in seconds from now.
533    pub expiry_secs: Option<u32>,
534
535    /// The max number of items that can be purchased in any one payment for
536    /// the offer.
537    ///
538    /// NOTE: this is not related to single-use vs reusable offers.
539    ///                                                                        
540    /// The expected amount paid for this offer is `offer.min_amount *
541    /// quantity`, where `offer.min_amount` is the value per item and
542    /// `quantity` is the number of items chosen _by the payer_. The
543    /// payer's chosen `quantity` must be in the range: `0 < quantity <=
544    /// offer.max_quantity`.
545    ///
546    /// If `None`, defaults to `MaxQuantity::ONE`, i.e., the expected paid
547    /// `amount` is just `offer.amount`.
548    pub max_quantity: Option<MaxQuantity>,
549
550    /// The issuer of the offer.
551    ///
552    /// If `Some`, offer will encode the string. Bolt12 spec expects this tring
553    /// to be a domain or a `user@domain` address.
554    /// If `None`, offer issuer will encode "lexe.app" as the issuer.
555    pub issuer: Option<BoundedString>,
556    //
557    // TODO(phlip9): add a `single_use` field to the offer request? right now
558    // all offers are reusable.
559}
560
561#[derive(Serialize, Deserialize)]
562pub struct CreateOfferResponse {
563    pub offer: Offer,
564}
565
566#[derive(Serialize, Deserialize)]
567pub struct PayOfferPreflightRequest {
568    /// The user-provided idempotency id for this payment.
569    pub cid: ClientPaymentId,
570    /// The offer we want to pay.
571    pub offer: Offer,
572    /// Specifies the amount we will pay. If the offer specifies a minimum
573    /// amount, `amount` should satisfy that minimum.
574    // Renamed and made non-optional in node-v0.9.4
575    // The old `fallback_amount = None` is technically valid and incompatible
576    // but rare, due to offers not setting amounts often
577    #[serde(alias = "fallback_amount")]
578    pub amount: Amount,
579}
580
581#[derive(Serialize, Deserialize)]
582pub struct PayOfferPreflightResponse {
583    /// The total amount to-be-paid for the pre-flighted [`Offer`],
584    /// excluding the fees.
585    ///
586    /// This value may be different from the value originally requested if
587    /// we had to reach `htlc_minimum_msat` for some intermediate hops.
588    pub amount: Amount,
589    /// The total amount of fees to-be-paid for the pre-flighted [`Offer`].
590    ///
591    /// Since we only approximate the route atm, we likely underestimate the
592    /// actual fee.
593    pub fees: Amount,
594    /// The route this offer will be paid over.
595    ///
596    /// Because we don't yet fetch the actual BOLT 12 invoice during preflight,
597    /// this route is only an approximation of the final route (we can only
598    /// route to the last public node before the offer's blinded path begins).
599    // Added in node,lsp-v0.7.8
600    // TODO(max): We don't actually pay over this route.
601    pub route: LxRoute,
602}
603
604/// The default [`PaymentKind`] for offer-rail request endpoints.
605fn default_offer_kind() -> PaymentKind {
606    PaymentKind::Offer
607}
608
609#[derive(Serialize, Deserialize)]
610pub struct PayOfferRequest {
611    /// The user-provided idempotency id for this payment.
612    pub cid: ClientPaymentId,
613    /// The offer we want to pay.
614    pub offer: Offer,
615    /// Specifies the amount we will pay. If the offer specifies a minimum
616    /// amount, `amount` should satisfy that minimum.
617    // Renamed and made non-optional in node-v0.9.4
618    // The old `fallback_amount = None` is technically valid and incompatible
619    // but rare, due to offers not setting amounts often
620    #[serde(alias = "fallback_amount")]
621    pub amount: Amount,
622    /// An optional BOLT 12 `payer_note` included with the invoice request and
623    /// visible to the recipient.
624    // compat: Alias added in node-v0.9.7
625    #[serde(rename = "payer_note", alias = "message")]
626    pub message: Option<BoundedString>,
627    /// An optional personal note for this payment, useful if the
628    /// receiver-provided description is insufficient.
629    // compat: Alias added in node-v0.9.7
630    #[serde(rename = "note", alias = "personal_note")]
631    pub personal_note: Option<BoundedString>,
632    /// The [`PaymentKind`] to label this outbound offer payment with.
633    /// `kind.rail()` must == `PaymentRail::Offer` (or `kind` must be unknown),
634    /// otherwise the request is rejected.
635    ///
636    /// Defaults to [`PaymentKind::Offer`] if not set.
637    // Added in `node-v0.9.11`
638    #[serde(default = "default_offer_kind")]
639    pub kind: PaymentKind,
640}
641
642#[derive(Serialize, Deserialize)]
643pub struct PayOfferResponse {
644    /// When the node registered this payment. Used in the
645    /// [`PaymentCreatedIndex`].
646    pub created_at: TimestampMs,
647}
648
649// --- On-chain payments --- //
650
651#[derive(Debug, PartialEq, Serialize, Deserialize)]
652#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
653pub struct GetNextUnusedAddressResponse {
654    #[cfg_attr(
655        any(test, feature = "test-utils"),
656        proptest(strategy = "arbitrary::any_mainnet_addr_unchecked()")
657    )]
658    pub addr: bitcoin::Address<NetworkUnchecked>,
659}
660
661#[derive(Serialize, Deserialize)]
662#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary, Debug))]
663pub struct PayOnchainRequest {
664    /// The identifier to use for this payment.
665    pub cid: ClientPaymentId,
666    /// The address we want to send funds to.
667    #[cfg_attr(
668        any(test, feature = "test-utils"),
669        proptest(strategy = "arbitrary::any_mainnet_addr_unchecked()")
670    )]
671    pub address: bitcoin::Address<NetworkUnchecked>,
672    /// How much Bitcoin we want to send.
673    pub amount: Amount,
674    /// How quickly we want our transaction to be confirmed.
675    /// The higher the priority, the more fees we will pay.
676    // See LexeEsplora for the conversion to the target number of blocks
677    pub priority: ConfirmationPriority,
678    /// An optional personal note for this payment.
679    // compat: Alias added in node-v0.9.7
680    #[serde(rename = "note", alias = "personal_note")]
681    pub personal_note: Option<BoundedString>,
682}
683
684#[derive(Serialize, Deserialize)]
685pub struct PayOnchainResponse {
686    /// When the node registered this payment. Used in the
687    /// [`PaymentCreatedIndex`].
688    pub created_at: TimestampMs,
689    /// The Bitcoin txid for the transaction we just submitted to the mempool.
690    pub txid: Txid,
691}
692
693#[derive(Debug, PartialEq, Serialize, Deserialize)]
694pub struct PayOnchainPreflightRequest {
695    /// The address we want to send funds to.
696    pub address: bitcoin::Address<NetworkUnchecked>,
697    /// How much Bitcoin we want to send.
698    pub amount: Amount,
699}
700
701#[derive(Serialize, Deserialize)]
702pub struct PayOnchainPreflightResponse {
703    /// Corresponds with [`ConfirmationPriority::High`]
704    ///
705    /// The high estimate is optional--we don't want to block the user from
706    /// sending if they only have enough for a normal tx fee.
707    pub high: Option<FeeEstimate>,
708    /// Corresponds with [`ConfirmationPriority::Normal`]
709    pub normal: FeeEstimate,
710    /// Corresponds with [`ConfirmationPriority::Background`]
711    pub background: FeeEstimate,
712}
713
714#[derive(Serialize, Deserialize)]
715pub struct FeeEstimate {
716    /// The fee amount estimate.
717    pub amount: Amount,
718}
719
720// --- Sync --- //
721
722#[derive(Serialize, Deserialize)]
723pub struct ResyncRequest {
724    /// If true, the LSP will full sync the BDK wallet and do a normal LDK
725    /// sync.
726    pub full_sync: bool,
727}
728
729// --- Username --- //
730
731/// Upserts the user's custom human Bitcoin address.
732#[derive(Debug, PartialEq, Serialize, Deserialize)]
733#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
734pub struct UpsertCustomHumanBitcoinAddress {
735    /// Username for BIP-353 and LNURL.
736    pub username: Username,
737    /// Offer to be used to fetch invoices on BIP-353.
738    pub offer: Offer,
739}
740
741/// Claims a generated human Bitcoin address.
742///
743/// This endpoint is used during node initialization to claim an auto-generated
744/// human Bitcoin address. The address will have `is_primary: false` and
745/// `is_generated: true`.
746#[derive(Debug, PartialEq, Serialize, Deserialize)]
747#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
748pub struct ClaimGeneratedHumanBitcoinAddress {
749    /// Offer to be used to fetch invoices on BIP-353.
750    pub offer: Offer,
751    /// The username to claim. This must be the username returned by
752    /// `get_generated_username`.
753    pub username: Username,
754}
755
756/// Response for `get_generated_username` endpoint.
757#[derive(Debug, PartialEq, Serialize, Deserialize)]
758#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
759pub struct GetGeneratedUsernameResponse {
760    /// The generated username that can be used for claiming an HBA.
761    pub username: Username,
762    /// Whether this user already has a claimed generated HBA.
763    /// If true, the caller should skip calling
764    /// `claim_generated_human_bitcoin_address`.
765    pub already_claimed: bool,
766}
767
768/// Response for `get_human_bitcoin_address`.
769#[derive(Debug, PartialEq, Serialize, Deserialize)]
770#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
771pub struct GetHumanBitcoinAddressResponse {
772    /// The user's active HBA.
773    pub hba: ActiveHumanBitcoinAddress,
774}
775
776/// Response for `upsert_custom_human_bitcoin_address`.
777#[derive(Debug, PartialEq, Serialize, Deserialize)]
778#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
779pub struct UpsertHumanBitcoinAddressResponse {
780    /// The active HBA that was just claimed, renamed, or refreshed.
781    pub hba: ActiveHumanBitcoinAddress,
782}
783
784/// The user's active Human Bitcoin Address, paired with whether they may
785/// currently change it. This is what `get`/`upsert_*_human_bitcoin_address`
786/// serve back to the app.
787#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
788#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
789pub struct ActiveHumanBitcoinAddress {
790    /// The active HBA itself.
791    pub hba: HumanBitcoinAddress,
792    /// Whether the user can currently claim a *different* custom username.
793    /// `true` when the user has no active custom HBA (never claimed one, or
794    /// theirs has lapsed), is within the 24h grace period, or is past the
795    /// 90-day freeze; `false` while frozen with an active custom HBA.
796    pub updatable: bool,
797}
798
799/// The user's active Human Bitcoin Address: the active (non-expired) primary
800/// custom HBA if present, otherwise the generated fallback.
801/// Aliases are never returned here.
802#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
803#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
804pub struct HumanBitcoinAddress {
805    /// Username for BIP-353 and LNURL.
806    pub username: Username,
807    /// Offer for fetching invoices on BIP-353.
808    pub offer: Offer,
809    /// The last time this Human Bitcoin Address was updated.
810    pub updated_at: TimestampMs,
811    /// When this Human Bitcoin Address expires, if applicable.
812    ///
813    /// Custom HBAs may or may not expire; generated fallbacks never expire.
814    pub expires_at: Option<TimestampMs>,
815    /// Whether this is the user's generated fallback HBA.
816    pub is_generated: bool,
817}
818
819/// The legacy flat HBA shape returned by the deprecated v1
820/// `*_human_bitcoin_address` endpoints, built from the v2 types above.
821#[derive(Debug, PartialEq, Serialize, Deserialize)]
822#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
823pub struct HumanBitcoinAddressV1 {
824    /// Current username for BIP-353 and LNURL.
825    pub username: Option<Username>,
826    /// Current offer for fetching invoices on BIP-353.
827    pub offer: Option<Offer>,
828    /// Last time the human Bitcoin address was updated.
829    pub updated_at: Option<TimestampMs>,
830    /// Whether the human Bitcoin address can be updated. Always `true` for
831    /// generated addresses; for claimed addresses, depends on time-based
832    /// freeze rules.
833    pub updatable: bool,
834}
835
836impl From<GetHumanBitcoinAddressResponse> for HumanBitcoinAddressV1 {
837    fn from(resp: GetHumanBitcoinAddressResponse) -> Self {
838        Self::from(resp.hba)
839    }
840}
841
842impl From<UpsertHumanBitcoinAddressResponse> for HumanBitcoinAddressV1 {
843    fn from(resp: UpsertHumanBitcoinAddressResponse) -> Self {
844        Self::from(resp.hba)
845    }
846}
847
848impl From<ActiveHumanBitcoinAddress> for HumanBitcoinAddressV1 {
849    fn from(active: ActiveHumanBitcoinAddress) -> Self {
850        Self {
851            username: Some(active.hba.username),
852            offer: Some(active.hba.offer),
853            updated_at: Some(active.hba.updated_at),
854            updatable: active.updatable,
855        }
856    }
857}
858
859#[cfg(any(test, feature = "test-utils"))]
860mod arbitrary_impl {
861    use proptest::{
862        arbitrary::{Arbitrary, any},
863        strategy::{BoxedStrategy, Strategy},
864    };
865
866    use super::*;
867
868    impl Arbitrary for PayOnchainPreflightRequest {
869        type Parameters = ();
870        type Strategy = BoxedStrategy<Self>;
871
872        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
873            (arbitrary::any_mainnet_addr_unchecked(), any::<Amount>())
874                .prop_map(|(address, amount)| Self { address, amount })
875                .boxed()
876        }
877    }
878}
879
880#[cfg(test)]
881mod test {
882    use std::str::FromStr;
883
884    use lexe_common::test_utils::roundtrip;
885
886    use super::*;
887
888    #[test]
889    fn pay_onchain_preflight_roundtrip() {
890        roundtrip::query_string_roundtrip_proptest::<PayOnchainPreflightRequest>(
891        );
892    }
893
894    #[test]
895    fn payment_id_struct_roundtrip() {
896        roundtrip::query_string_roundtrip_proptest::<PaymentIdStruct>();
897    }
898
899    #[test]
900    fn payment_index_struct_roundtrip() {
901        roundtrip::query_string_roundtrip_proptest::<PaymentCreatedIndexStruct>(
902        );
903    }
904
905    #[test]
906    fn get_new_payments_roundtrip() {
907        roundtrip::query_string_roundtrip_proptest::<GetNewPayments>();
908    }
909
910    #[test]
911    fn payment_indexes_roundtrip() {
912        // This is serialized as JSON, not query strings.
913        roundtrip::json_value_roundtrip_proptest::<PaymentCreatedIndexes>();
914    }
915
916    #[test]
917    fn get_address_response_roundtrip() {
918        roundtrip::json_value_roundtrip_proptest::<GetNextUnusedAddressResponse>(
919        );
920    }
921
922    #[test]
923    fn setup_gdrive_request_roundtrip() {
924        roundtrip::json_string_roundtrip_proptest::<SetupGDrive>();
925    }
926
927    #[test]
928    fn upsert_custom_human_bitcoin_address_roundtrip() {
929        roundtrip::json_value_roundtrip_proptest::<
930            UpsertCustomHumanBitcoinAddress,
931        >();
932    }
933
934    #[test]
935    fn claim_generated_human_bitcoin_address_request_roundtrip() {
936        roundtrip::json_value_roundtrip_proptest::<
937            ClaimGeneratedHumanBitcoinAddress,
938        >();
939    }
940
941    #[test]
942    fn get_generated_username_response_roundtrip() {
943        roundtrip::json_value_roundtrip_proptest::<GetGeneratedUsernameResponse>(
944        );
945    }
946
947    #[test]
948    fn human_bitcoin_address_response_roundtrip() {
949        roundtrip::json_value_roundtrip_proptest::<HumanBitcoinAddressV1>();
950    }
951
952    #[test]
953    fn get_human_bitcoin_address_response_roundtrip() {
954        roundtrip::json_value_roundtrip_proptest::<
955            GetHumanBitcoinAddressResponse,
956        >();
957    }
958
959    #[test]
960    fn upsert_human_bitcoin_address_response_roundtrip() {
961        roundtrip::json_value_roundtrip_proptest::<
962            UpsertHumanBitcoinAddressResponse,
963        >();
964    }
965
966    /// Sanity check the `DebugInfo` serialization against a hard-coded string.
967    #[test]
968    fn debug_info_serialization() {
969        // Account-level xpub (from BDK tests)
970        let account_xpub = Xpub::from_str(
971            "xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA"
972        ).unwrap();
973
974        // Descriptors with real checksums (computed via miniscript)
975        let descriptor = "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/<0;1>/*)#c8v4zjyh".to_owned();
976        let external_descriptor = "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#dwvchw0k".to_owned();
977        let internal_descriptor = "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/1/*)#u6fe2mlw".to_owned();
978
979        let debug_info = DebugInfo {
980            descriptors: OnchainDescriptors {
981                multipath_descriptor: descriptor.clone(),
982                external_descriptor: external_descriptor.clone(),
983                internal_descriptor: internal_descriptor.clone(),
984                account_xpub,
985            },
986            legacy_descriptors: None,
987            num_utxos: 5,
988            num_confirmed_utxos: 3,
989            num_unconfirmed_utxos: 2,
990            pending_monitor_updates: Some(0),
991        };
992
993        // Serialize and check against expected JSON.
994        // NOTE: Do NOT remove this raw string check. We're sanity-checking how
995        // it looks in serialized form.
996        let json = serde_json::to_string_pretty(&debug_info).unwrap();
997        let expected = r#"{
998  "descriptors": {
999    "multipath_descriptor": "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/<0;1>/*)#c8v4zjyh",
1000    "external_descriptor": "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#dwvchw0k",
1001    "internal_descriptor": "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/1/*)#u6fe2mlw",
1002    "account_xpub": "xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA"
1003  },
1004  "num_utxos": 5,
1005  "num_confirmed_utxos": 3,
1006  "num_unconfirmed_utxos": 2,
1007  "pending_monitor_updates": 0
1008}"#;
1009        assert_eq!(json, expected);
1010
1011        // Verify deserialization roundtrips
1012        let back: DebugInfo = serde_json::from_str(&json).unwrap();
1013        assert_eq!(back.num_utxos, 5);
1014        assert_eq!(back.num_confirmed_utxos, 3);
1015        assert_eq!(back.num_unconfirmed_utxos, 2);
1016        assert_eq!(back.descriptors.multipath_descriptor, descriptor);
1017        assert_eq!(back.descriptors.external_descriptor, external_descriptor);
1018        assert_eq!(back.descriptors.internal_descriptor, internal_descriptor);
1019        assert_eq!(
1020            back.descriptors.account_xpub,
1021            debug_info.descriptors.account_xpub
1022        );
1023        assert!(back.legacy_descriptors.is_none());
1024        assert_eq!(back.pending_monitor_updates, Some(0));
1025    }
1026
1027    /// Compat: a new node must accept a `PayInvoiceRequest` from an old client
1028    /// that predates (and so omits) the `ldk_route` field.
1029    #[test]
1030    fn pay_invoice_request_compat() {
1031        // A `PayInvoiceRequest` as serialized by a node-v0.9.11 client, which
1032        // predates the `ldk_route` field (added in node-v0.9.12).
1033        let json = r#"{
1034  "invoice": "lnbc1pn79l2rdqqpp5y3u8cttsjvusa34xnx9ceh8watmrvy99qw7pwpsvxjq3zl2mm8wscqpcsp5p4wrl7xfrgxj3w05ksjv2qtccyt0feg2c0suwcjc5pyrawxvlt0q9qyysgqxqyz5vqnp4q0vzagw8x7r9eyalw35t0u6syql8rtqf9tejep0z6xrwkqrua5advrzjqv22wafr68wtchd4vzq7mj7zf2uzpv67xsaxcemfzak7wp7p0r29wrf0egqqy2sqqcqqqqqqqqqqhwqqfqrzjqv22wafr68wtchd4vzq7mj7zf2uzpv67xsaxcemfzak7wp7p0r29wzmk4uqqj5sqqyqqqqqqqqqqhwqqfqrzjqv22wafr68wtchd4vzq7mj7zf2uzpv67xsaxcemfzak7wp7p0r29wz2g6uqqt5cqqcqqqqqqqqqqhwqqfqd5xs0luhzmmdmevhqtcyuwrcr43pq3xpmtdvdenvcsslg8vuhmfyqtcs3y54yxpsw8wlt5epz0y0y64ul7fc37zt5cklumx0u6at2dcphm9mhh",
1035  "fallback_amount": null,
1036  "payer_note": null,
1037  "note": null,
1038  "kind": "invoice"
1039}"#;
1040        let req = serde_json::from_str::<PayInvoiceRequest>(json).unwrap();
1041        assert!(req.ldk_route.is_none());
1042    }
1043}