Skip to main content

lexe_api_core/models/
nwc.rs

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