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_note::BoundedNote,
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<BoundedNote>,
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<BoundedNote>,
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<BoundedNote>,
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<BoundedNote>,
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 pub expiry_secs: Option<u32>,
451 /// The `amount` we're requesting for payments using this offer.
452 ///
453 /// If `None`, the offer is variable amount and the payer can choose any
454 /// value.
455 ///
456 /// If `Some`, the offer amount is fixed and the payer must pay exactly
457 /// this value (per item, see `max_quantity`). The offer amount must be a
458 /// non-zero value if set; if a variable amount offer is desired, don't set
459 /// an amount.
460 pub amount: Option<Amount>,
461 /// The description to be encoded into the invoice.
462 ///
463 /// If `None`, the `description` field inside the invoice will be an empty
464 /// string (""), as lightning _requires_ a description to be set.
465 pub description: Option<String>,
466 /// The max number of items that can be purchased in any one payment for
467 /// the offer.
468 ///
469 /// NOTE: this is not related to single-use vs reusable offers.
470 ///
471 /// The expected amount paid for this offer is `offer.amount * quantity`,
472 /// where `offer.amount` is the value per item and `quantity` is the number
473 /// of items chosen _by the payer_. The payer's chosen `quantity` must be
474 /// in the range: `0 < quantity <= offer.max_quantity`.
475 ///
476 /// If `None`, defaults to `MaxQuantity::ONE`, i.e., the expected paid
477 /// `amount` is just `offer.amount`.
478 pub max_quantity: Option<MaxQuantity>,
479 /// The issuer of the offer.
480 ///
481 /// If `Some`, offer will encode the string. Bolt12 spec expects this tring
482 /// to be a domain or a `user@domain` address.
483 /// If `None`, offer issuer will encode "lexe.app" as the issuer.
484 pub issuer: Option<String>,
485 //
486 // TODO(phlip9): add a `single_use` field to the offer request? right now
487 // all offers are reusable.
488}
489
490#[derive(Serialize, Deserialize)]
491pub struct CreateOfferResponse {
492 pub offer: Offer,
493}
494
495#[derive(Serialize, Deserialize)]
496pub struct PreflightPayOfferRequest {
497 /// The user-provided idempotency id for this payment.
498 pub cid: ClientPaymentId,
499 /// The offer we want to pay.
500 pub offer: Offer,
501 /// Specifies the amount we will pay if the offer to be paid is
502 /// amountless. This field must be [`Some`] for amountless offers.
503 pub fallback_amount: Option<Amount>,
504}
505
506#[derive(Serialize, Deserialize)]
507pub struct PreflightPayOfferResponse {
508 /// The total amount to-be-paid for the pre-flighted [`Offer`],
509 /// excluding the fees.
510 ///
511 /// This value may be different from the value originally requested if
512 /// we had to reach `htlc_minimum_msat` for some intermediate hops.
513 pub amount: Amount,
514 /// The total amount of fees to-be-paid for the pre-flighted [`Offer`].
515 ///
516 /// Since we only approximate the route atm, we likely underestimate the
517 /// actual fee.
518 pub fees: Amount,
519 /// The route this offer will be paid over.
520 ///
521 /// Because we don't yet fetch the actual BOLT 12 invoice during preflight,
522 /// this route is only an approximation of the final route (we can only
523 /// route to the last public node before the offer's blinded path begins).
524 // Added in node,lsp-v0.7.8
525 // TODO(max): We don't actually pay over this route.
526 pub route: LxRoute,
527}
528
529#[derive(Serialize, Deserialize)]
530pub struct PayOfferRequest {
531 /// The user-provided idempotency id for this payment.
532 pub cid: ClientPaymentId,
533 /// The offer we want to pay.
534 pub offer: Offer,
535 /// Specifies the amount we will pay if the offer to be paid is
536 /// amountless. This field must be [`Some`] for amountless offers.
537 pub fallback_amount: Option<Amount>,
538 /// An optional personal note for this payment, useful if the
539 /// receiver-provided description is insufficient.
540 pub note: Option<BoundedNote>,
541 /// An optional note included in the BOLT12 invoice request and visible to
542 /// the recipient.
543 pub payer_note: Option<BoundedNote>,
544}
545
546#[derive(Serialize, Deserialize)]
547pub struct PayOfferResponse {
548 /// When the node registered this payment. Used in the
549 /// [`PaymentCreatedIndex`].
550 pub created_at: TimestampMs,
551}
552
553// --- On-chain payments --- //
554
555#[derive(Debug, PartialEq, Serialize, Deserialize)]
556#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
557pub struct GetAddressResponse {
558 #[cfg_attr(
559 any(test, feature = "test-utils"),
560 proptest(strategy = "arbitrary::any_mainnet_addr_unchecked()")
561 )]
562 pub addr: bitcoin::Address<NetworkUnchecked>,
563}
564
565#[derive(Serialize, Deserialize)]
566#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary, Debug))]
567pub struct PayOnchainRequest {
568 /// The identifier to use for this payment.
569 pub cid: ClientPaymentId,
570 /// The address we want to send funds to.
571 #[cfg_attr(
572 any(test, feature = "test-utils"),
573 proptest(strategy = "arbitrary::any_mainnet_addr_unchecked()")
574 )]
575 pub address: bitcoin::Address<NetworkUnchecked>,
576 /// How much Bitcoin we want to send.
577 pub amount: Amount,
578 /// How quickly we want our transaction to be confirmed.
579 /// The higher the priority, the more fees we will pay.
580 // See LexeEsplora for the conversion to the target number of blocks
581 pub priority: ConfirmationPriority,
582 /// An optional personal note for this payment.
583 pub note: Option<BoundedNote>,
584}
585
586#[derive(Serialize, Deserialize)]
587pub struct PayOnchainResponse {
588 /// When the node registered this payment. Used in the
589 /// [`PaymentCreatedIndex`].
590 pub created_at: TimestampMs,
591 /// The Bitcoin txid for the transaction we just submitted to the mempool.
592 pub txid: Txid,
593}
594
595#[derive(Debug, PartialEq, Serialize, Deserialize)]
596pub struct PreflightPayOnchainRequest {
597 /// The address we want to send funds to.
598 pub address: bitcoin::Address<NetworkUnchecked>,
599 /// How much Bitcoin we want to send.
600 pub amount: Amount,
601}
602
603#[derive(Serialize, Deserialize)]
604pub struct PreflightPayOnchainResponse {
605 /// Corresponds with [`ConfirmationPriority::High`]
606 ///
607 /// The high estimate is optional--we don't want to block the user from
608 /// sending if they only have enough for a normal tx fee.
609 pub high: Option<FeeEstimate>,
610 /// Corresponds with [`ConfirmationPriority::Normal`]
611 pub normal: FeeEstimate,
612 /// Corresponds with [`ConfirmationPriority::Background`]
613 pub background: FeeEstimate,
614}
615
616#[derive(Serialize, Deserialize)]
617pub struct FeeEstimate {
618 /// The fee amount estimate.
619 pub amount: Amount,
620}
621
622// --- Sync --- //
623
624#[derive(Serialize, Deserialize)]
625pub struct ResyncRequest {
626 /// If true, the LSP will full sync the BDK wallet and do a normal LDK
627 /// sync.
628 pub full_sync: bool,
629}
630
631// --- Username --- //
632
633/// Creates or updates a human Bitcoin address.
634#[derive(Debug, PartialEq, Serialize, Deserialize)]
635#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
636pub struct UpdateHumanBitcoinAddress {
637 /// Username for BIP-353 and LNURL.
638 pub username: Username,
639 /// Offer to be used to fetch invoices on BIP-353.
640 pub offer: Offer,
641}
642
643/// Claims a generated human Bitcoin address.
644///
645/// This endpoint is used during node initialization to claim an auto-generated
646/// human Bitcoin address. The address will have `is_primary: false` and
647/// `is_generated: true`.
648#[derive(Debug, PartialEq, Serialize, Deserialize)]
649#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
650pub struct ClaimGeneratedHumanBitcoinAddress {
651 /// Offer to be used to fetch invoices on BIP-353.
652 pub offer: Offer,
653 /// The username to claim. This must be the username returned by
654 /// `get_generated_username`.
655 pub username: Username,
656}
657
658/// Response for `get_generated_username` endpoint.
659#[derive(Debug, PartialEq, Serialize, Deserialize)]
660#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
661pub struct GetGeneratedUsernameResponse {
662 /// The generated username that can be used for claiming an HBA.
663 pub username: Username,
664 /// Whether this user already has a claimed generated HBA.
665 /// If true, the caller should skip calling
666 /// `claim_generated_human_bitcoin_address`.
667 pub already_claimed: bool,
668}
669
670#[derive(Debug, PartialEq, Serialize, Deserialize)]
671#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
672pub struct HumanBitcoinAddress {
673 /// Current username for BIP-353 and LNURL.
674 pub username: Option<Username>,
675 /// Current offer for fetching invoices on BIP-353.
676 pub offer: Option<Offer>,
677 /// Last time the human Bitcoin address was updated.
678 pub updated_at: Option<TimestampMs>,
679 /// Whether the human Bitcoin address can be updated. Always `true` for
680 /// generated addresses; for claimed addresses, depends on time-based
681 /// freeze rules.
682 pub updatable: bool,
683}
684
685#[cfg(any(test, feature = "test-utils"))]
686mod arbitrary_impl {
687 use proptest::{
688 arbitrary::{Arbitrary, any},
689 strategy::{BoxedStrategy, Strategy},
690 };
691
692 use super::*;
693
694 impl Arbitrary for PreflightPayOnchainRequest {
695 type Parameters = ();
696 type Strategy = BoxedStrategy<Self>;
697
698 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
699 (arbitrary::any_mainnet_addr_unchecked(), any::<Amount>())
700 .prop_map(|(address, amount)| Self { address, amount })
701 .boxed()
702 }
703 }
704}
705
706#[cfg(test)]
707mod test {
708 use std::str::FromStr;
709
710 use lexe_common::test_utils::roundtrip;
711
712 use super::*;
713
714 #[test]
715 fn preflight_pay_onchain_roundtrip() {
716 roundtrip::query_string_roundtrip_proptest::<PreflightPayOnchainRequest>(
717 );
718 }
719
720 #[test]
721 fn payment_id_struct_roundtrip() {
722 roundtrip::query_string_roundtrip_proptest::<PaymentIdStruct>();
723 }
724
725 #[test]
726 fn payment_index_struct_roundtrip() {
727 roundtrip::query_string_roundtrip_proptest::<PaymentCreatedIndexStruct>(
728 );
729 }
730
731 #[test]
732 fn get_new_payments_roundtrip() {
733 roundtrip::query_string_roundtrip_proptest::<GetNewPayments>();
734 }
735
736 #[test]
737 fn payment_indexes_roundtrip() {
738 // This is serialized as JSON, not query strings.
739 roundtrip::json_value_roundtrip_proptest::<PaymentCreatedIndexes>();
740 }
741
742 #[test]
743 fn get_address_response_roundtrip() {
744 roundtrip::json_value_roundtrip_proptest::<GetAddressResponse>();
745 }
746
747 #[test]
748 fn setup_gdrive_request_roundtrip() {
749 roundtrip::json_string_roundtrip_proptest::<SetupGDrive>();
750 }
751
752 #[test]
753 fn human_bitcoin_address_request_roundtrip() {
754 roundtrip::json_value_roundtrip_proptest::<UpdateHumanBitcoinAddress>();
755 }
756
757 #[test]
758 fn claim_generated_human_bitcoin_address_request_roundtrip() {
759 roundtrip::json_value_roundtrip_proptest::<
760 ClaimGeneratedHumanBitcoinAddress,
761 >();
762 }
763
764 #[test]
765 fn get_generated_username_response_roundtrip() {
766 roundtrip::json_value_roundtrip_proptest::<GetGeneratedUsernameResponse>(
767 );
768 }
769
770 #[test]
771 fn human_bitcoin_address_response_roundtrip() {
772 roundtrip::json_value_roundtrip_proptest::<HumanBitcoinAddress>();
773 }
774
775 /// Sanity check the `DebugInfo` serialization against a hard-coded string.
776 #[test]
777 fn debug_info_serialization() {
778 // Account-level xpub (from BDK tests)
779 let account_xpub = Xpub::from_str(
780 "xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA"
781 ).unwrap();
782
783 // Descriptors with real checksums (computed via miniscript)
784 let descriptor = "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/<0;1>/*)#c8v4zjyh".to_owned();
785 let external_descriptor = "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#dwvchw0k".to_owned();
786 let internal_descriptor = "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/1/*)#u6fe2mlw".to_owned();
787
788 let debug_info = DebugInfo {
789 descriptors: OnchainDescriptors {
790 multipath_descriptor: descriptor.clone(),
791 external_descriptor: external_descriptor.clone(),
792 internal_descriptor: internal_descriptor.clone(),
793 account_xpub,
794 },
795 legacy_descriptors: None,
796 num_utxos: 5,
797 num_confirmed_utxos: 3,
798 num_unconfirmed_utxos: 2,
799 pending_monitor_updates: Some(0),
800 };
801
802 // Serialize and check against expected JSON.
803 // NOTE: Do NOT remove this raw string check. We're sanity-checking how
804 // it looks in serialized form.
805 let json = serde_json::to_string_pretty(&debug_info).unwrap();
806 let expected = r#"{
807 "descriptors": {
808 "multipath_descriptor": "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/<0;1>/*)#c8v4zjyh",
809 "external_descriptor": "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/0/*)#dwvchw0k",
810 "internal_descriptor": "wpkh([be83839f/84'/0'/0']xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA/1/*)#u6fe2mlw",
811 "account_xpub": "xpub6DCQ1YcqvZtSwGWMrwHELPehjWV3f2MGZ69yBADTxFEUAoLwb5Mp5GniQK6tTp3AgbngVz9zEFbBJUPVnkG7LFYt8QMTfbrNqs6FNEwAPKA"
812 },
813 "num_utxos": 5,
814 "num_confirmed_utxos": 3,
815 "num_unconfirmed_utxos": 2,
816 "pending_monitor_updates": 0
817}"#;
818 assert_eq!(json, expected);
819
820 // Verify deserialization roundtrips
821 let back: DebugInfo = serde_json::from_str(&json).unwrap();
822 assert_eq!(back.num_utxos, 5);
823 assert_eq!(back.num_confirmed_utxos, 3);
824 assert_eq!(back.num_unconfirmed_utxos, 2);
825 assert_eq!(back.descriptors.multipath_descriptor, descriptor);
826 assert_eq!(back.descriptors.external_descriptor, external_descriptor);
827 assert_eq!(back.descriptors.internal_descriptor, internal_descriptor);
828 assert_eq!(
829 back.descriptors.account_xpub,
830 debug_info.descriptors.account_xpub
831 );
832 assert!(back.legacy_descriptors.is_none());
833 assert_eq!(back.pending_monitor_updates, Some(0));
834 }
835}