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}