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