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