lexe/types/
command.rs

1//! Lexe SDK API request and response types.
2
3use anyhow::Context;
4use lexe_api::{
5    models::command,
6    types::{
7        bounded_note::BoundedNote,
8        invoice::Invoice,
9        payments::{PaymentCreatedIndex, PaymentHash, PaymentSecret},
10    },
11};
12use lexe_common::{ln::amount::Amount, time::TimestampMs};
13use serde::{Deserialize, Serialize};
14
15use crate::types::{
16    auth::{Measurement, NodePk, UserPk},
17    payment::Payment,
18};
19
20/// Information about a Lexe node.
21// Simple version of `lexe_api::models::command::NodeInfo`.
22#[derive(Clone, Debug, Serialize, Deserialize)]
23pub struct NodeInfo {
24    /// The node's current semver version, e.g. `0.6.9`.
25    pub version: semver::Version,
26    /// The hex-encoded SGX 'measurement' of the current node.
27    /// The measurement is the hash of the enclave binary.
28    pub measurement: Measurement,
29    /// The hex-encoded ed25519 user public key used to identify a Lexe user.
30    /// The user keypair is derived from the root seed.
31    pub user_pk: UserPk,
32    /// The hex-encoded secp256k1 Lightning node public key; the `node_id`.
33    pub node_pk: NodePk,
34
35    /// The sum of our `lightning_balance` and our `onchain_balance`, in sats.
36    pub balance: Amount,
37
38    /// Total Lightning balance in sats, summed over all of our channels.
39    pub lightning_balance: Amount,
40    /// An estimated upper bound, in sats, on how much of our Lightning balance
41    /// we can send to most recipients on the Lightning Network, accounting for
42    /// Lightning limits such as our channel reserve, pending HTLCs, fees, etc.
43    /// You should usually be able to spend this amount.
44    // User-facing name for `LightningBalance::sendable`
45    pub lightning_sendable_balance: Amount,
46    /// A hard upper bound on how much of our Lightning balance can be spent
47    /// right now, in sats. This is always >= `lightning_sendable_balance`.
48    /// Generally it is only possible to spend exactly this amount if the
49    /// recipient is a Lexe user.
50    // User-facing name for `LightningBalance::max_sendable`
51    pub lightning_max_sendable_balance: Amount,
52
53    /// Total on-chain balance in sats, including unconfirmed funds.
54    // `OnchainBalance::total`
55    pub onchain_balance: Amount,
56    /// Trusted on-chain balance in sats, including only confirmed funds and
57    /// unconfirmed outputs originating from our own wallet.
58    // Equivalent to BDK's `trusted_spendable`, but with a better name.
59    pub onchain_trusted_balance: Amount,
60
61    /// The total number of Lightning channels.
62    pub num_channels: usize,
63    /// The number of channels which are currently usable, i.e. `channel_ready`
64    /// messages have been exchanged and the channel peer is online.
65    /// Is always less than or equal to `num_channels`.
66    pub num_usable_channels: usize,
67}
68
69impl From<command::NodeInfo> for NodeInfo {
70    fn from(info: command::NodeInfo) -> Self {
71        let lightning_balance = info.lightning_balance.total();
72        let onchain_balance = Amount::try_from(info.onchain_balance.total())
73            .expect("We're unreasonably rich!");
74        let onchain_trusted_balance =
75            Amount::try_from(info.onchain_balance.trusted_spendable())
76                .expect("We're unreasonably rich!");
77        let balance = lightning_balance.saturating_add(onchain_balance);
78
79        Self {
80            version: info.version,
81            measurement: Measurement::from_unstable(info.measurement),
82            user_pk: UserPk::from_unstable(info.user_pk),
83            node_pk: NodePk::from_unstable(info.node_pk),
84
85            balance,
86
87            lightning_balance,
88            lightning_sendable_balance: info.lightning_balance.sendable,
89            lightning_max_sendable_balance: info.lightning_balance.max_sendable,
90            onchain_balance,
91            onchain_trusted_balance,
92            num_channels: info.num_channels,
93            num_usable_channels: info.num_usable_channels,
94        }
95    }
96}
97
98/// A request to create a BOLT 11 invoice.
99#[derive(Default, Serialize, Deserialize)]
100pub struct CreateInvoiceRequest {
101    /// The expiration, in seconds, to encode into the invoice.
102    /// If no duration is provided, the expiration time defaults to 86400
103    /// (1 day).
104    pub expiration_secs: Option<u32>,
105
106    /// Optionally include an amount, in sats, to encode into the invoice.
107    /// If no amount is provided, the sender will specify how much to pay.
108    pub amount: Option<Amount>,
109
110    /// The description to be encoded into the invoice.
111    /// The sender will see this description when they scan the invoice.
112    // If `None`, the `description` field inside the invoice will be an empty
113    // string (""), as lightning _requires_ a description (or description
114    // hash) to be set.
115    pub description: Option<String>,
116
117    /// An optional note received from the payer out-of-band via LNURL-pay
118    /// that is stored with this inbound payment. If provided, it must be
119    /// non-empty and no longer than 200 chars / 512 UTF-8 bytes.
120    #[serde(default)]
121    pub payer_note: Option<String>,
122}
123
124/// The response to a BOLT 11 invoice request.
125#[derive(Serialize, Deserialize)]
126pub struct CreateInvoiceResponse {
127    /// Identifier for this inbound invoice payment.
128    pub index: PaymentCreatedIndex,
129    /// The string-encoded BOLT 11 invoice.
130    pub invoice: Invoice,
131    /// The description encoded in the invoice, if one was provided.
132    pub description: Option<String>,
133    /// The amount encoded in the invoice, if there was one.
134    /// Returning `null` means we created an amountless invoice.
135    pub amount: Option<Amount>,
136    /// The invoice creation time, in milliseconds since the UNIX epoch.
137    pub created_at: TimestampMs,
138    /// The invoice expiration time, in milliseconds since the UNIX epoch.
139    pub expires_at: TimestampMs,
140    /// The hex-encoded payment hash of the invoice.
141    pub payment_hash: PaymentHash,
142    /// The payment secret of the invoice.
143    pub payment_secret: PaymentSecret,
144}
145
146impl CreateInvoiceResponse {
147    /// Build a [`CreateInvoiceResponse`] from an index and invoice.
148    pub fn new(index: PaymentCreatedIndex, invoice: Invoice) -> Self {
149        let description = invoice.description_str().map(|s| s.to_owned());
150        let amount_sats = invoice.amount();
151        let created_at = invoice.saturating_created_at();
152        let expires_at = invoice.saturating_expires_at();
153        let payment_hash = invoice.payment_hash();
154        let payment_secret = invoice.payment_secret();
155
156        Self {
157            index,
158            invoice,
159            description,
160            amount: amount_sats,
161            created_at,
162            expires_at,
163            payment_hash,
164            payment_secret,
165        }
166    }
167}
168
169impl TryFrom<CreateInvoiceRequest> for command::CreateInvoiceRequest {
170    type Error = anyhow::Error;
171
172    fn try_from(req: CreateInvoiceRequest) -> anyhow::Result<Self> {
173        /// The default expiration we use if none is provided.
174        const DEFAULT_EXPIRATION_SECS: u32 = 60 * 60 * 24; // 1 day
175
176        Ok(Self {
177            expiry_secs: req.expiration_secs.unwrap_or(DEFAULT_EXPIRATION_SECS),
178            amount: req.amount,
179            description: req.description,
180            // TODO(maurice): Add description_hash if we really need it.
181            description_hash: None,
182            payer_note: req
183                .payer_note
184                .map(BoundedNote::new)
185                .transpose()
186                .context(
187                    "Invalid payer_note (must be non-empty and <=200 chars / \
188                     <=512 UTF-8 bytes)",
189                )?,
190        })
191    }
192}
193
194/// A request to pay a BOLT 11 invoice.
195#[derive(Serialize, Deserialize)]
196pub struct PayInvoiceRequest {
197    /// The invoice we want to pay.
198    pub invoice: Invoice,
199    /// Specifies the amount we will pay if the invoice to be paid is
200    /// amountless. This field must be set if the invoice is amountless.
201    pub fallback_amount: Option<Amount>,
202    /// An optional personal note for this payment.
203    /// The receiver will not see this note.
204    /// If provided, it must be non-empty and no longer than 200 chars /
205    /// 512 UTF-8 bytes.
206    pub note: Option<String>,
207    /// An optional note that was sent to the receiver out-of-band via
208    /// LNURL-pay that is stored with this outbound payment. Unlike `note`,
209    /// this is visible to the recipient. If provided, it must be non-empty and
210    /// no longer than 200 chars / 512 UTF-8 bytes.
211    pub payer_note: Option<String>,
212}
213
214impl TryFrom<PayInvoiceRequest> for command::PayInvoiceRequest {
215    type Error = anyhow::Error;
216
217    fn try_from(req: PayInvoiceRequest) -> anyhow::Result<Self> {
218        Ok(Self {
219            invoice: req.invoice,
220            fallback_amount: req.fallback_amount,
221            note: req.note.map(BoundedNote::new).transpose().context(
222                "Invalid note (must be non-empty and <=200 chars / \
223                     <=512 UTF-8 bytes)",
224            )?,
225            payer_note: req
226                .payer_note
227                .map(BoundedNote::new)
228                .transpose()
229                .context(
230                    "Invalid payer_note (must be non-empty and <=200 chars / \
231                     <=512 UTF-8 bytes)",
232                )?,
233        })
234    }
235}
236
237/// The response to a request to pay a BOLT 11 invoice.
238#[derive(Serialize, Deserialize)]
239pub struct PayInvoiceResponse {
240    /// Identifier for this outbound invoice payment.
241    pub index: PaymentCreatedIndex,
242    /// When we tried to pay this invoice, in milliseconds since the UNIX
243    /// epoch.
244    pub created_at: TimestampMs,
245}
246
247/// A request to update the personal note on an existing payment.
248/// Pass `None` to clear the note.
249#[derive(Serialize, Deserialize)]
250pub struct UpdatePaymentNoteRequest {
251    /// Identifier for the payment to be updated.
252    pub index: PaymentCreatedIndex,
253    /// The updated note, or `None` to clear.
254    /// If provided, it must be non-empty and no longer than 200 chars /
255    /// 512 UTF-8 bytes.
256    pub note: Option<String>,
257}
258
259impl TryFrom<UpdatePaymentNoteRequest> for command::UpdatePaymentNote {
260    type Error = anyhow::Error;
261
262    fn try_from(sdk: UpdatePaymentNoteRequest) -> anyhow::Result<Self> {
263        Ok(Self {
264            index: sdk.index,
265            note: sdk.note.map(BoundedNote::new).transpose().context(
266                "Invalid note (must be non-empty and <=200 chars / \
267                 <=512 UTF-8 bytes)",
268            )?,
269        })
270    }
271}
272
273/// A request to get information about a payment by its index.
274#[derive(Serialize, Deserialize)]
275pub struct GetPaymentRequest {
276    /// Identifier for this payment.
277    pub index: PaymentCreatedIndex,
278}
279
280/// A response to a request to get information about a payment by its index.
281#[derive(Serialize, Deserialize)]
282pub struct GetPaymentResponse {
283    /// Information about this payment, if it exists.
284    pub payment: Option<Payment>,
285}
286
287/// Response from listing payments.
288#[derive(Serialize, Deserialize)]
289pub struct ListPaymentsResponse {
290    /// Payments in the requested page.
291    pub payments: Vec<Payment>,
292    /// Cursor for fetching the next page. `None` when there are no more
293    /// results. Pass this as the `after` argument to get the next page.
294    pub next_index: Option<PaymentCreatedIndex>,
295}
296
297/// Summary of changes from a payment sync operation.
298#[derive(Debug)]
299pub struct PaymentSyncSummary {
300    /// Number of new payments added to the local database.
301    pub num_new: usize,
302    /// Number of existing payments that were updated.
303    pub num_updated: usize,
304}