Skip to main content

lexe_api_core/models/
nwc.rs

1use lexe_byte_array::ByteArray;
2#[cfg(any(test, feature = "test-utils"))]
3use lexe_common::test_utils::arbitrary;
4use lexe_common::{RefCast, time::TimestampMs};
5use lexe_serde::{base64_or_bytes, hexstr_or_bytes};
6#[cfg(any(test, feature = "test-utils"))]
7use proptest_derive::Arbitrary;
8use serde::{Deserialize, Serialize};
9
10/// A 32-byte Nostr event id.
11#[derive(Copy, Clone, Eq, Hash, PartialEq, RefCast)]
12#[derive(Serialize, Deserialize)]
13#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
14#[repr(transparent)]
15pub struct NostrEventId(#[serde(with = "hexstr_or_bytes")] pub [u8; 32]);
16
17lexe_byte_array::impl_byte_array!(NostrEventId, 32);
18lexe_byte_array::impl_debug_display_as_hex!(NostrEventId);
19
20/// A 32-byte Nostr public key.
21#[derive(Copy, Clone, Eq, Hash, PartialEq, RefCast)]
22#[derive(Serialize, Deserialize)]
23#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
24#[repr(transparent)]
25pub struct NostrPk(#[serde(with = "hexstr_or_bytes")] pub [u8; 32]);
26
27lexe_byte_array::impl_byte_array!(NostrPk, 32);
28lexe_byte_array::impl_debug_display_as_hex!(NostrPk);
29
30/// A 32-byte Nostr secret key.
31#[derive(Copy, Clone, Eq, Hash, PartialEq, RefCast)]
32#[derive(Serialize, Deserialize)]
33#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
34#[repr(transparent)]
35pub struct NostrSk(#[serde(with = "hexstr_or_bytes")] [u8; 32]);
36
37lexe_byte_array::impl_byte_array!(NostrSk, 32);
38lexe_byte_array::impl_debug_display_redacted!(NostrSk);
39
40/// Upgradeable API struct for a NostrPk.
41#[derive(Debug, PartialEq, Serialize, Deserialize)]
42#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
43pub struct NostrPkStruct {
44    pub nostr_pk: NostrPk,
45}
46
47/// A NWC client as represented in the DB, minus the timestamp fields.
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
50pub struct DbNwcClientFields {
51    /// The NWC client app's Nostr public key (identifies the caller).
52    pub client_nostr_pk: NostrPk,
53    /// The wallet service's Nostr public key (identifies this wallet).
54    pub wallet_nostr_pk: NostrPk,
55    /// VFS-encrypted client secret data (wallet SK + label).
56    #[serde(with = "base64_or_bytes")]
57    pub ciphertext: Vec<u8>,
58}
59
60/// Full NWC client record from the DB.
61#[derive(Debug, PartialEq, Serialize, Deserialize)]
62#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
63pub struct DbNwcClient {
64    #[serde(flatten)]
65    pub fields: DbNwcClientFields,
66    pub created_at: TimestampMs,
67    pub updated_at: TimestampMs,
68}
69
70/// Information about an existing NWC client.
71///
72/// This is used for listing clients to the app.
73#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
74#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
75pub struct NwcClientInfo {
76    /// The client public key (identifies the caller of this connection).
77    pub client_nostr_pk: NostrPk,
78    /// The wallet service public key (identifies this connection).
79    pub wallet_nostr_pk: NostrPk,
80    /// Human-readable label for this connection.
81    #[cfg_attr(
82        any(test, feature = "test-utils"),
83        proptest(strategy = "arbitrary::any_string()")
84    )]
85    pub label: String,
86    /// When this connection was created.
87    pub created_at: TimestampMs,
88    /// When this connection was last updated.
89    pub updated_at: TimestampMs,
90}
91
92// ---- Requests and responses App <-> Backend ---- //
93
94/// Response to list NWC clients.
95#[derive(Debug, PartialEq, Serialize, Deserialize)]
96#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
97pub struct ListNwcClientResponse {
98    pub clients: Vec<NwcClientInfo>,
99}
100
101/// Request to create a new NWC client.
102///
103/// Keys are generated on the Node and stored safely encrypted in the DB.
104#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
105#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
106pub struct CreateNwcClientRequest {
107    /// Human-readable label for this client.
108    #[cfg_attr(
109        any(test, feature = "test-utils"),
110        proptest(strategy = "arbitrary::any_string()")
111    )]
112    pub label: String,
113}
114
115/// Request to update an existing NWC client.
116// TODO(a-mpch): Add option to update budget limits, budget restriction type
117// (single-use, monthly, yearly, total, etc.).
118#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
119#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
120pub struct UpdateNwcClientRequest {
121    /// The client public key identifying the client to update.
122    pub client_nostr_pk: NostrPk,
123    /// Updated human-readable label for this client. If `None`, the label is
124    /// not updated.
125    #[cfg_attr(
126        any(test, feature = "test-utils"),
127        proptest(strategy = "arbitrary::any_option_string()")
128    )]
129    pub label: Option<String>,
130}
131
132/// Response for creating a new NWC client.
133#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
134#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
135pub struct CreateNwcClientResponse {
136    /// The wallet service public key for this wallet.
137    pub wallet_nostr_pk: NostrPk,
138    /// The client public key for this client.
139    pub client_nostr_pk: NostrPk,
140    /// Human-readable label for this client.
141    #[cfg_attr(
142        any(test, feature = "test-utils"),
143        proptest(strategy = "arbitrary::any_string()")
144    )]
145    pub label: String,
146    /// The NWC connection string (nostr+walletconnect://..).
147    #[cfg_attr(
148        any(test, feature = "test-utils"),
149        proptest(strategy = "arbitrary::any_string()")
150    )]
151    pub connection_string: String,
152}
153
154/// Response for updating an existing NWC client.
155///
156/// NOTE: this response does not contain the connection string.
157#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
158#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
159pub struct UpdateNwcClientResponse {
160    /// Information about the updated NWC client.
161    pub client_info: NwcClientInfo,
162}
163
164// ---- Requests and responses Node <-> Backend ---- //
165
166/// Query parameters to search for NWC clients.
167///
168/// This params adds optinal filtering besides the user_pk.
169#[derive(Debug, PartialEq, Serialize, Deserialize)]
170#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
171pub struct GetNwcClients {
172    /// Optionally filter by the client's Nostr PK.
173    pub client_nostr_pk: Option<NostrPk>,
174}
175
176#[derive(Debug, PartialEq, Serialize, Deserialize)]
177#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
178pub struct VecDbNwcClient {
179    pub nwc_clients: Vec<DbNwcClient>,
180}
181
182// ---- Requests and responses  Nostr-bridge <-> Node ---- //
183
184/// Request from nostr-bridge to user node with an encrypted NWC request.
185#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
186#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
187pub struct NwcRequest {
188    /// The nostr PK of the sender of the message (also, the NWC client app).
189    pub client_nostr_pk: NostrPk,
190    /// The Nostr PK of the recipient (the wallet service PK).
191    pub wallet_nostr_pk: NostrPk,
192    /// The nostr event hex id. Used to build the response nostr event.
193    pub event_id: NostrEventId,
194    /// The NIP-44 v2 encrypted payload containing the NWC request.
195    #[serde(with = "base64_or_bytes")]
196    pub nip44_payload: Vec<u8>,
197}
198
199/// Generic signed nostr event.
200///
201/// Used for to forward nostr events from the node to nostr-bridge.
202#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
203#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
204pub struct NostrSignedEvent {
205    /// Base64 encoded string of the Json-encoded event.
206    #[serde(with = "base64_or_bytes")]
207    pub event: Vec<u8>,
208}
209
210/// NIP-47 protocol structures.
211pub mod nip47 {
212    use std::fmt;
213
214    #[cfg(any(test, feature = "test-utils"))]
215    use lexe_common::test_utils::arbitrary;
216    #[cfg(any(test, feature = "test-utils"))]
217    use proptest_derive::Arbitrary;
218    use serde::{Deserialize, Serialize};
219
220    /// NWC request method.
221    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
222    #[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
223    #[serde(rename_all = "snake_case")]
224    pub enum NwcMethod {
225        GetInfo,
226        MakeInvoice,
227        LookupInvoice,
228        ListTransactions,
229        GetBalance,
230        MultiPayKeysend,
231        PayKeysend,
232        MultiPayInvoice,
233        PayInvoice,
234    }
235
236    /// Parameters for `make_invoice` command.
237    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238    pub struct MakeInvoiceParams {
239        /// Amount in millisats.
240        #[serde(rename = "amount")]
241        pub amount_msat: u64,
242        /// Invoice description.
243        #[serde(skip_serializing_if = "Option::is_none")]
244        pub description: Option<String>,
245        /// Invoice description hash (32 bytes hex).
246        #[serde(skip_serializing_if = "Option::is_none")]
247        pub description_hash: Option<String>,
248        /// Invoice expiry in seconds.
249        #[serde(rename = "expiry", skip_serializing_if = "Option::is_none")]
250        pub expiry_secs: Option<u32>,
251        /// Generic metadata (e.g., zap/boostagram details). Optional and
252        /// ignored.
253        #[serde(skip_serializing_if = "Option::is_none")]
254        pub metadata: Option<serde_json::Value>,
255    }
256
257    /// NWC request payload (decrypted).
258    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
259    pub struct NwcRequestPayload {
260        pub method: NwcMethod,
261        pub params: serde_json::Value,
262    }
263
264    /// Result for `get_info` command.
265    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
266    pub struct GetInfoResult {
267        /// LN node alias (e.g., "lexe-abc12345").
268        pub alias: String,
269        /// RGB hex string (e.g., "000000").
270        pub color: String,
271        /// LN node public key as hex string.
272        pub pubkey: String,
273        /// Network name: "mainnet", "testnet", "signet", or "regtest".
274        pub network: String,
275        /// Current block height.
276        pub block_height: u32,
277        /// Hex-encoded block hash.
278        pub block_hash: String,
279        /// List of supported NWC methods for this connection.
280        pub methods: Vec<NwcMethod>,
281    }
282
283    /// Result for `make_invoice` command.
284    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
285    pub struct MakeInvoiceResult {
286        /// BOLT11 invoice string.
287        pub invoice: String,
288        /// Payment hash (hex).
289        pub payment_hash: String,
290    }
291
292    /// NWC error codes.
293    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
294    #[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
295    #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
296    pub enum NwcErrorCode {
297        RateLimited,
298        NotImplemented,
299        InsufficientBalance,
300        QuotaExceeded,
301        Restricted,
302        Unauthorized,
303        Internal,
304        Other,
305    }
306
307    /// NWC error response.
308    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
309    #[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
310    pub struct NwcError {
311        pub code: NwcErrorCode,
312        #[cfg_attr(
313            any(test, feature = "test-utils"),
314            proptest(strategy = "arbitrary::any_string()")
315        )]
316        pub message: String,
317    }
318
319    impl NwcError {
320        pub fn new(code: NwcErrorCode, message: String) -> Self {
321            Self { code, message }
322        }
323
324        pub fn not_implemented(message: impl fmt::Display) -> Self {
325            let message = format!("Not implemented: {message:#}");
326            Self {
327                code: NwcErrorCode::NotImplemented,
328                message,
329            }
330        }
331
332        pub fn other(message: impl fmt::Display) -> Self {
333            let message = format!("Other error: {message:#}");
334            Self {
335                code: NwcErrorCode::Other,
336                message,
337            }
338        }
339
340        pub fn internal(message: impl fmt::Display) -> Self {
341            let message = format!("Internal error: {message:#}");
342            Self {
343                code: NwcErrorCode::Internal,
344                message,
345            }
346        }
347    }
348
349    /// NWC response payload (to be encrypted).
350    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
351    #[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
352    pub struct NwcResponsePayload {
353        pub result_type: NwcMethod,
354        #[serde(skip_serializing_if = "Option::is_none")]
355        #[cfg_attr(
356            any(test, feature = "test-utils"),
357            proptest(
358                strategy = "arbitrary::any_option_json_value_skip_none()"
359            )
360        )]
361        pub result: Option<serde_json::Value>,
362        #[serde(skip_serializing_if = "Option::is_none")]
363        pub error: Option<NwcError>,
364    }
365}
366
367#[cfg(test)]
368mod test {
369    use lexe_common::test_utils::roundtrip;
370
371    use super::*;
372
373    #[test]
374    fn db_nwc_client_fields_roundtrip() {
375        roundtrip::json_value_roundtrip_proptest::<DbNwcClientFields>();
376    }
377
378    #[test]
379    fn create_nwc_client_request_roundtrip() {
380        roundtrip::json_value_roundtrip_proptest::<CreateNwcClientRequest>();
381    }
382
383    #[test]
384    fn update_nwc_client_request_roundtrip() {
385        roundtrip::json_value_roundtrip_proptest::<UpdateNwcClientRequest>();
386    }
387
388    #[test]
389    fn create_nwc_client_response_roundtrip() {
390        roundtrip::json_value_roundtrip_proptest::<CreateNwcClientResponse>();
391    }
392
393    #[test]
394    fn update_nwc_client_response_roundtrip() {
395        roundtrip::json_value_roundtrip_proptest::<UpdateNwcClientResponse>();
396    }
397
398    #[test]
399    fn nwc_client_roundtrip() {
400        roundtrip::json_value_roundtrip_proptest::<DbNwcClient>();
401    }
402
403    #[test]
404    fn nostr_client_pk_roundtrip() {
405        roundtrip::json_value_roundtrip_proptest::<NostrPkStruct>();
406    }
407
408    #[test]
409    fn vec_nostr_client_pk_roundtrip() {
410        roundtrip::json_value_roundtrip_proptest::<VecDbNwcClient>();
411    }
412
413    #[test]
414    fn nwc_request_roundtrip() {
415        roundtrip::json_value_roundtrip_proptest::<NwcRequest>();
416    }
417
418    #[test]
419    fn nostr_signed_event_roundtrip() {
420        roundtrip::json_value_roundtrip_proptest::<NostrSignedEvent>();
421    }
422
423    #[test]
424    fn nostr_event_id_roundtrip() {
425        roundtrip::json_value_roundtrip_proptest::<NostrEventId>();
426    }
427
428    #[test]
429    fn nostr_sk_roundtrip() {
430        roundtrip::json_value_roundtrip_proptest::<NostrSk>();
431    }
432
433    #[test]
434    fn nwc_response_payload_roundtrip() {
435        roundtrip::json_value_roundtrip_proptest::<nip47::NwcResponsePayload>();
436    }
437}