Skip to main content

wire/
nostr_relay.rs

1//! RFC-007 D3.2b-i: the NIP-01 relay protocol — the JSON-array messages a Nostr
2//! relay speaks. Pure serialize (client→relay) + parse (relay→client); the
3//! WebSocket that actually carries them (`NostrWs`) is the D3.2b-ii slice.
4//!
5//! NIP-01 messages are JSON arrays whose first element is a type tag:
6//!
7//! - client→relay: `["EVENT", <event>]`, `["REQ", <sub_id>, <filter>…]`,
8//!   `["CLOSE", <sub_id>]`
9//! - relay→client: `["EVENT", <sub_id>, <event>]`, `["OK", <id>, <bool>, <msg>]`,
10//!   `["EOSE", <sub_id>]`, `["CLOSED", <sub_id>, <msg>]`, `["NOTICE", <msg>]`
11//!
12//! Wire uses this to publish a [`NostrEvent`] (`EVENT`) and to pull events
13//! addressed to its npub (`REQ` with a `#p` filter, read `EVENT`/`EOSE`).
14
15use serde_json::{Value, json};
16
17use crate::nostr_event::NostrEvent;
18
19/// A NIP-01 subscription filter. Only the fields wire uses; every field is
20/// optional and omitted from the JSON when empty/`None` (NIP-01 treats an
21/// absent field as "no constraint").
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct Filter {
24    /// Match these event ids (hex).
25    pub ids: Vec<String>,
26    /// Match these author pubkeys (hex x-only).
27    pub authors: Vec<String>,
28    /// Match these kinds.
29    pub kinds: Vec<u32>,
30    /// `#p` tag — match events that p-tag these pubkeys (i.e. addressed to me).
31    pub p_tags: Vec<String>,
32    /// Only events at/after this unix time.
33    pub since: Option<i64>,
34    /// Only events at/before this unix time.
35    pub until: Option<i64>,
36    /// Cap the number of stored events the relay returns.
37    pub limit: Option<usize>,
38}
39
40impl Filter {
41    /// The NIP-01 JSON object for this filter (empty fields omitted).
42    pub fn to_json(&self) -> Value {
43        let mut m = serde_json::Map::new();
44        if !self.ids.is_empty() {
45            m.insert("ids".into(), json!(self.ids));
46        }
47        if !self.authors.is_empty() {
48            m.insert("authors".into(), json!(self.authors));
49        }
50        if !self.kinds.is_empty() {
51            m.insert("kinds".into(), json!(self.kinds));
52        }
53        if !self.p_tags.is_empty() {
54            m.insert("#p".into(), json!(self.p_tags));
55        }
56        if let Some(s) = self.since {
57            m.insert("since".into(), json!(s));
58        }
59        if let Some(u) = self.until {
60            m.insert("until".into(), json!(u));
61        }
62        if let Some(l) = self.limit {
63            m.insert("limit".into(), json!(l));
64        }
65        Value::Object(m)
66    }
67}
68
69/// A client→relay message.
70#[derive(Debug, Clone, PartialEq)]
71pub enum ClientMessage {
72    /// Publish an event.
73    Event(NostrEvent),
74    /// Open a subscription `sub_id` matching any of `filters`.
75    Req {
76        sub_id: String,
77        filters: Vec<Filter>,
78    },
79    /// Close a subscription.
80    Close(String),
81}
82
83impl ClientMessage {
84    /// Serialize to the NIP-01 wire string the relay expects.
85    pub fn to_json_string(&self) -> String {
86        let v = match self {
87            ClientMessage::Event(e) => json!(["EVENT", e]),
88            ClientMessage::Req { sub_id, filters } => {
89                let mut arr = vec![json!("REQ"), json!(sub_id)];
90                arr.extend(filters.iter().map(Filter::to_json));
91                Value::Array(arr)
92            }
93            ClientMessage::Close(sub) => json!(["CLOSE", sub]),
94        };
95        serde_json::to_string(&v).expect("client message always serializes")
96    }
97}
98
99/// A relay→client message.
100#[derive(Debug, Clone, PartialEq)]
101pub enum RelayMessage {
102    /// A stored/live event matching subscription `sub_id`.
103    Event { sub_id: String, event: NostrEvent },
104    /// Result of a publish: `accepted` + a human message.
105    Ok {
106        event_id: String,
107        accepted: bool,
108        message: String,
109    },
110    /// End of stored events for `sub_id` (live events follow).
111    Eose(String),
112    /// The relay closed subscription `sub_id` with a reason.
113    Closed { sub_id: String, message: String },
114    /// A human-readable relay notice.
115    Notice(String),
116    /// A message type this client doesn't model (forward-compat — never an error
117    /// so a relay extension can't wedge the read loop).
118    Unknown(String),
119}
120
121#[derive(Debug, PartialEq, Eq)]
122pub enum RelayParseError {
123    /// Not valid JSON.
124    NotJson,
125    /// Not a JSON array, or empty.
126    NotArray,
127    /// First element isn't a string type-tag.
128    NoType,
129    /// The message had the right tag but the wrong arity/field types.
130    BadShape,
131    /// An `EVENT` message's event payload didn't parse as a `NostrEvent`.
132    BadEvent,
133}
134
135impl std::fmt::Display for RelayParseError {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        let s = match self {
138            RelayParseError::NotJson => "relay message is not JSON",
139            RelayParseError::NotArray => "relay message is not a JSON array",
140            RelayParseError::NoType => "relay message has no string type tag",
141            RelayParseError::BadShape => "relay message has the wrong shape for its type",
142            RelayParseError::BadEvent => "relay EVENT carried a malformed event",
143        };
144        write!(f, "{s}")
145    }
146}
147
148impl RelayMessage {
149    /// Parse a relay→client message string. Unknown type tags are returned as
150    /// [`RelayMessage::Unknown`] rather than erroring.
151    pub fn parse(s: &str) -> Result<RelayMessage, RelayParseError> {
152        let v: Value = serde_json::from_str(s).map_err(|_| RelayParseError::NotJson)?;
153        let arr = v.as_array().filter(|a| !a.is_empty());
154        let arr = arr.ok_or(RelayParseError::NotArray)?;
155        let tag = arr
156            .first()
157            .and_then(Value::as_str)
158            .ok_or(RelayParseError::NoType)?;
159        match tag {
160            "EVENT" => {
161                let sub_id = str_at(arr, 1)?;
162                let event_val = arr.get(2).ok_or(RelayParseError::BadShape)?.clone();
163                let event: NostrEvent =
164                    serde_json::from_value(event_val).map_err(|_| RelayParseError::BadEvent)?;
165                Ok(RelayMessage::Event { sub_id, event })
166            }
167            "OK" => {
168                let event_id = str_at(arr, 1)?;
169                let accepted = arr
170                    .get(2)
171                    .and_then(Value::as_bool)
172                    .ok_or(RelayParseError::BadShape)?;
173                // Per NIP-01 the message is REQUIRED but commonly empty; tolerate
174                // its absence.
175                let message = arr.get(3).and_then(Value::as_str).unwrap_or("").to_string();
176                Ok(RelayMessage::Ok {
177                    event_id,
178                    accepted,
179                    message,
180                })
181            }
182            "EOSE" => Ok(RelayMessage::Eose(str_at(arr, 1)?)),
183            "CLOSED" => Ok(RelayMessage::Closed {
184                sub_id: str_at(arr, 1)?,
185                message: arr.get(2).and_then(Value::as_str).unwrap_or("").to_string(),
186            }),
187            "NOTICE" => Ok(RelayMessage::Notice(
188                arr.get(1).and_then(Value::as_str).unwrap_or("").to_string(),
189            )),
190            other => Ok(RelayMessage::Unknown(other.to_string())),
191        }
192    }
193}
194
195/// The string at array index `i`, or `BadShape`.
196fn str_at(arr: &[Value], i: usize) -> Result<String, RelayParseError> {
197    arr.get(i)
198        .and_then(Value::as_str)
199        .map(str::to_string)
200        .ok_or(RelayParseError::BadShape)
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::nostr_event::wire_to_nostr;
207    use crate::nostr_key::generate_transport_key;
208    use crate::signing::{generate_keypair, sign_message_v31};
209
210    fn an_event() -> NostrEvent {
211        let (sk, pk) = generate_keypair();
212        let msg = json!({
213            "timestamp": "2026-06-14T12:00:00Z",
214            "from": "did:wire:slate-lotus-1",
215            "kind": 1,
216            "body": {"content": "hi"},
217        });
218        let wire = sign_message_v31(&msg, &sk, &pk, "slate-lotus").unwrap();
219        let (nsk, _x) = generate_transport_key();
220        wire_to_nostr(&wire, &nsk).unwrap()
221    }
222
223    #[test]
224    fn filter_omits_empty_fields() {
225        let f = Filter {
226            p_tags: vec!["abcd".into()],
227            kinds: vec![1, 4],
228            since: Some(1700000000),
229            ..Default::default()
230        };
231        let v = f.to_json();
232        assert_eq!(v["#p"], json!(["abcd"]));
233        assert_eq!(v["kinds"], json!([1, 4]));
234        assert_eq!(v["since"], json!(1700000000));
235        // Empty / None fields are absent (no constraint).
236        assert!(v.get("ids").is_none());
237        assert!(v.get("authors").is_none());
238        assert!(v.get("until").is_none());
239        assert!(v.get("limit").is_none());
240    }
241
242    #[test]
243    fn client_event_serializes_to_nip01() {
244        let ev = an_event();
245        let s = ClientMessage::Event(ev.clone()).to_json_string();
246        let v: Value = serde_json::from_str(&s).unwrap();
247        assert_eq!(v[0], "EVENT");
248        assert_eq!(v[1]["id"], ev.id);
249        assert_eq!(v[1]["sig"], ev.sig);
250    }
251
252    #[test]
253    fn client_req_serializes_tag_subid_and_filters() {
254        let req = ClientMessage::Req {
255            sub_id: "wire-sub-1".into(),
256            filters: vec![Filter {
257                p_tags: vec!["mypub".into()],
258                kinds: vec![1],
259                ..Default::default()
260            }],
261        };
262        let v: Value = serde_json::from_str(&req.to_json_string()).unwrap();
263        assert_eq!(v[0], "REQ");
264        assert_eq!(v[1], "wire-sub-1");
265        assert_eq!(v[2]["#p"], json!(["mypub"]));
266    }
267
268    #[test]
269    fn client_close_serializes() {
270        let v: Value =
271            serde_json::from_str(&ClientMessage::Close("s1".into()).to_json_string()).unwrap();
272        assert_eq!(v, json!(["CLOSE", "s1"]));
273    }
274
275    #[test]
276    fn parse_relay_event() {
277        let ev = an_event();
278        // A relay echoes the event under a subscription id.
279        let s = serde_json::to_string(&json!(["EVENT", "sub-1", ev])).unwrap();
280        match RelayMessage::parse(&s).unwrap() {
281            RelayMessage::Event { sub_id, event } => {
282                assert_eq!(sub_id, "sub-1");
283                assert_eq!(event, ev);
284            }
285            other => panic!("expected Event, got {other:?}"),
286        }
287    }
288
289    #[test]
290    fn parse_ok_eose_closed_notice() {
291        assert_eq!(
292            RelayMessage::parse(r#"["OK","abc123",true,"saved"]"#).unwrap(),
293            RelayMessage::Ok {
294                event_id: "abc123".into(),
295                accepted: true,
296                message: "saved".into()
297            }
298        );
299        // OK with missing message tolerated.
300        assert_eq!(
301            RelayMessage::parse(r#"["OK","abc123",false]"#).unwrap(),
302            RelayMessage::Ok {
303                event_id: "abc123".into(),
304                accepted: false,
305                message: String::new()
306            }
307        );
308        assert_eq!(
309            RelayMessage::parse(r#"["EOSE","sub-1"]"#).unwrap(),
310            RelayMessage::Eose("sub-1".into())
311        );
312        assert_eq!(
313            RelayMessage::parse(r#"["CLOSED","sub-1","rate-limited"]"#).unwrap(),
314            RelayMessage::Closed {
315                sub_id: "sub-1".into(),
316                message: "rate-limited".into()
317            }
318        );
319        assert_eq!(
320            RelayMessage::parse(r#"["NOTICE","hello"]"#).unwrap(),
321            RelayMessage::Notice("hello".into())
322        );
323    }
324
325    #[test]
326    fn parse_unknown_type_is_not_an_error() {
327        // Forward-compat: a relay extension type must not wedge the read loop.
328        assert_eq!(
329            RelayMessage::parse(r#"["AUTH","challenge"]"#).unwrap(),
330            RelayMessage::Unknown("AUTH".into())
331        );
332    }
333
334    #[test]
335    fn parse_rejects_malformed() {
336        assert_eq!(
337            RelayMessage::parse("not json"),
338            Err(RelayParseError::NotJson)
339        );
340        assert_eq!(RelayMessage::parse("{}"), Err(RelayParseError::NotArray));
341        assert_eq!(RelayMessage::parse("[]"), Err(RelayParseError::NotArray));
342        assert_eq!(RelayMessage::parse("[123]"), Err(RelayParseError::NoType));
343        // EVENT with a non-event payload.
344        assert_eq!(
345            RelayMessage::parse(r#"["EVENT","sub",{"not":"an event"}]"#),
346            Err(RelayParseError::BadEvent)
347        );
348        // EOSE without a sub id.
349        assert_eq!(
350            RelayMessage::parse(r#"["EOSE"]"#),
351            Err(RelayParseError::BadShape)
352        );
353    }
354
355    #[test]
356    fn client_event_then_relay_event_roundtrip() {
357        // Publish shape and the relay-echo shape both carry the same event bytes.
358        let ev = an_event();
359        let published = ClientMessage::Event(ev.clone()).to_json_string();
360        let pv: Value = serde_json::from_str(&published).unwrap();
361        // Relay re-emits it with a sub id prepended.
362        let echoed = serde_json::to_string(&json!(["EVENT", "s", pv[1]])).unwrap();
363        match RelayMessage::parse(&echoed).unwrap() {
364            RelayMessage::Event { event, .. } => assert_eq!(event, ev),
365            other => panic!("expected Event, got {other:?}"),
366        }
367    }
368}