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