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