Skip to main content

nula_core/message/
relay.rs

1//! Messages a relay sends to a client.
2//!
3//! Per [NIP-01], every relay-to-client message is a JSON array tagged by its
4//! command. NIP-20 documents the [`MachineReadablePrefix`] applied to `OK`
5//! and `CLOSED` reasons; this module exposes the parsed prefix so callers can
6//! switch on it without re-parsing the wire string.
7//!
8//! [NIP-01]: https://github.com/nostr-protocol/nips/blob/master/01.md
9
10use std::fmt;
11use std::str::FromStr;
12
13use serde::de::{self, SeqAccess, Visitor};
14use serde::ser::{SerializeSeq, Serializer};
15use serde::{Deserialize, Deserializer, Serialize};
16use thiserror::Error;
17
18use super::subscription_id::SubscriptionId;
19use crate::event::{Event, EventId};
20
21const TAG_EVENT: &str = "EVENT";
22const TAG_OK: &str = "OK";
23const TAG_EOSE: &str = "EOSE";
24const TAG_CLOSED: &str = "CLOSED";
25const TAG_NOTICE: &str = "NOTICE";
26const TAG_AUTH: &str = "AUTH";
27const TAG_COUNT: &str = "COUNT";
28const TAG_NEG_MSG: &str = "NEG-MSG";
29const TAG_NEG_ERR: &str = "NEG-ERR";
30
31/// Errors raised when constructing a [`MachineReadablePrefix`].
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
33#[non_exhaustive]
34pub enum MachineReadablePrefixError {
35    /// The prefix string was not one of the known NIP-20 prefixes.
36    #[error("unknown machine-readable prefix")]
37    Unknown,
38}
39
40/// Standardised reason prefix used in `OK` / `CLOSED` reasons (NIP-20).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[non_exhaustive]
43pub enum MachineReadablePrefix {
44    /// `duplicate:` — the relay already had the event.
45    Duplicate,
46    /// `pow:` — proof-of-work requirements were not met.
47    Pow,
48    /// `blocked:` — the author or pubkey is blocked by the relay.
49    Blocked,
50    /// `rate-limited:` — the client hit a rate limit.
51    RateLimited,
52    /// `invalid:` — the event failed validation.
53    Invalid,
54    /// `error:` — the relay encountered an internal error.
55    Error,
56    /// `restricted:` — the author lacks permission (e.g. NIP-42 not done).
57    Restricted,
58    /// `mute:` — an ephemeral event nobody was listening to (NIP-01 §OK
59    /// examples).
60    Mute,
61    /// `auth-required:` — NIP-42 authentication is required.
62    AuthRequired,
63    /// `payment-required:` — paid relay; the client has not paid yet.
64    PaymentRequired,
65}
66
67impl MachineReadablePrefix {
68    /// Static wire string (without the trailing colon).
69    #[must_use]
70    pub const fn as_str(self) -> &'static str {
71        match self {
72            Self::Duplicate => "duplicate",
73            Self::Pow => "pow",
74            Self::Blocked => "blocked",
75            Self::RateLimited => "rate-limited",
76            Self::Invalid => "invalid",
77            Self::Error => "error",
78            Self::Restricted => "restricted",
79            Self::Mute => "mute",
80            Self::AuthRequired => "auth-required",
81            Self::PaymentRequired => "payment-required",
82        }
83    }
84
85    /// Try to extract the prefix from a NIP-20 reason such as `"pow: 24"`.
86    /// Returns `None` if the string does not start with a known prefix
87    /// followed by `:`.
88    #[must_use]
89    pub fn from_reason(reason: &str) -> Option<Self> {
90        let (prefix, _rest) = reason.split_once(':')?;
91        prefix.parse().ok()
92    }
93}
94
95impl fmt::Display for MachineReadablePrefix {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.write_str(self.as_str())
98    }
99}
100
101impl FromStr for MachineReadablePrefix {
102    type Err = MachineReadablePrefixError;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        let value = match s {
106            "duplicate" => Self::Duplicate,
107            "pow" => Self::Pow,
108            "blocked" => Self::Blocked,
109            "rate-limited" => Self::RateLimited,
110            "invalid" => Self::Invalid,
111            "error" => Self::Error,
112            "restricted" => Self::Restricted,
113            "mute" => Self::Mute,
114            "auth-required" => Self::AuthRequired,
115            "payment-required" => Self::PaymentRequired,
116            _ => return Err(MachineReadablePrefixError::Unknown),
117        };
118        Ok(value)
119    }
120}
121
122/// Errors raised when parsing a [`RelayMessage`].
123#[derive(Debug, Clone, Error)]
124#[non_exhaustive]
125pub enum RelayMessageError {
126    /// The wire array was empty.
127    #[error("relay message must not be empty")]
128    Empty,
129    /// The message tag was not recognised.
130    #[error("unknown relay message tag `{0}`")]
131    UnknownTag(String),
132    /// The message tag was recognised but the payload was malformed.
133    #[error("malformed `{tag}` message: {reason}")]
134    Malformed {
135        /// The wire tag string.
136        tag: &'static str,
137        /// Human-readable explanation.
138        reason: String,
139    },
140}
141
142/// Messages sent from a relay to a client.
143#[derive(Debug, Clone, PartialEq, Eq)]
144#[non_exhaustive]
145#[allow(
146    clippy::large_enum_variant,
147    reason = "EVENT inherently carries a full Event while the control variants \
148              (OK/EOSE/CLOSED/NOTICE/AUTH) are small; boxing it would add \
149              allocation churn on the relay-send and pool-receive hot paths, \
150              where EVENT is by far the most common message and is moved \
151              straight into/out of the enum. rust-nostr makes the same \
152              trade-off via a Cow<Event> EVENT variant."
153)]
154pub enum RelayMessage {
155    /// A subscription event match.
156    ///
157    /// Wire form: `["EVENT", <subscription_id>, <event>]`.
158    Event {
159        /// Subscription identifier originally supplied by the client.
160        subscription_id: SubscriptionId,
161        /// The matched event.
162        event: Event,
163    },
164    /// Acknowledgement for a published [`crate::Event`].
165    ///
166    /// Wire form: `["OK", <event_id>, <accepted>, <message>]`.
167    Ok {
168        /// Event id the relay is acknowledging.
169        event_id: EventId,
170        /// `true` if the event was accepted.
171        accepted: bool,
172        /// Human-readable reason. Use [`MachineReadablePrefix::from_reason`]
173        /// to recover a structured reason.
174        message: String,
175    },
176    /// End-of-stored-events sentinel: the relay has finished sending stored
177    /// matches; future events arrive in real time.
178    ///
179    /// Wire form: `["EOSE", <subscription_id>]`.
180    EndOfStoredEvents(SubscriptionId),
181    /// The relay closed the subscription.
182    ///
183    /// Wire form: `["CLOSED", <subscription_id>, <reason>]`.
184    Closed {
185        /// Subscription identifier.
186        subscription_id: SubscriptionId,
187        /// Reason string. Use [`MachineReadablePrefix::from_reason`] to
188        /// recover a structured reason.
189        message: String,
190    },
191    /// A free-form notice intended for end-user display.
192    ///
193    /// Wire form: `["NOTICE", <message>]`.
194    Notice(String),
195    /// NIP-42 authentication challenge.
196    ///
197    /// Wire form: `["AUTH", <challenge>]`.
198    Auth(String),
199    /// Count reply for a previously issued `COUNT` request (NIP-45).
200    ///
201    /// Wire form: `["COUNT", <subscription_id>, {"count": <n>}]`.
202    Count {
203        /// Subscription identifier.
204        subscription_id: SubscriptionId,
205        /// Number of matching events.
206        count: u64,
207    },
208    /// One step of an in-flight NIP-77 reconciliation, from the relay
209    /// back to the client.
210    ///
211    /// Wire form: `["NEG-MSG", <subscription_id>, <message_hex>]`.
212    NegMsg {
213        /// Subscription identifier from the original `NEG-OPEN`.
214        subscription_id: SubscriptionId,
215        /// Reconciliation payload, lowercase hex-encoded.
216        message: String,
217    },
218    /// Terminal error frame for a NIP-77 reconciliation session.
219    ///
220    /// Wire form: `["NEG-ERR", <subscription_id>, <reason>]`.
221    NegErr {
222        /// Subscription identifier the relay is failing.
223        subscription_id: SubscriptionId,
224        /// Reason string. Conventional prefixes (e.g.
225        /// `"blocked: …"`) are observable via
226        /// [`MachineReadablePrefix::from_reason`].
227        message: String,
228    },
229}
230
231impl Serialize for RelayMessage {
232    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
233    where
234        S: Serializer,
235    {
236        match self {
237            Self::Event {
238                subscription_id,
239                event,
240            } => {
241                let mut seq = serializer.serialize_seq(Some(3))?;
242                seq.serialize_element(TAG_EVENT)?;
243                seq.serialize_element(subscription_id)?;
244                seq.serialize_element(event)?;
245                seq.end()
246            }
247            Self::Ok {
248                event_id,
249                accepted,
250                message,
251            } => {
252                let mut seq = serializer.serialize_seq(Some(4))?;
253                seq.serialize_element(TAG_OK)?;
254                seq.serialize_element(event_id)?;
255                seq.serialize_element(accepted)?;
256                seq.serialize_element(message)?;
257                seq.end()
258            }
259            Self::EndOfStoredEvents(id) => {
260                let mut seq = serializer.serialize_seq(Some(2))?;
261                seq.serialize_element(TAG_EOSE)?;
262                seq.serialize_element(id)?;
263                seq.end()
264            }
265            Self::Closed {
266                subscription_id,
267                message,
268            } => {
269                let mut seq = serializer.serialize_seq(Some(3))?;
270                seq.serialize_element(TAG_CLOSED)?;
271                seq.serialize_element(subscription_id)?;
272                seq.serialize_element(message)?;
273                seq.end()
274            }
275            Self::Notice(message) => {
276                let mut seq = serializer.serialize_seq(Some(2))?;
277                seq.serialize_element(TAG_NOTICE)?;
278                seq.serialize_element(message)?;
279                seq.end()
280            }
281            Self::Auth(challenge) => {
282                let mut seq = serializer.serialize_seq(Some(2))?;
283                seq.serialize_element(TAG_AUTH)?;
284                seq.serialize_element(challenge)?;
285                seq.end()
286            }
287            Self::Count {
288                subscription_id,
289                count,
290            } => {
291                #[derive(Serialize)]
292                struct CountPayload {
293                    count: u64,
294                }
295
296                let mut seq = serializer.serialize_seq(Some(3))?;
297                seq.serialize_element(TAG_COUNT)?;
298                seq.serialize_element(subscription_id)?;
299                seq.serialize_element(&CountPayload { count: *count })?;
300                seq.end()
301            }
302            Self::NegMsg {
303                subscription_id,
304                message,
305            } => {
306                let mut seq = serializer.serialize_seq(Some(3))?;
307                seq.serialize_element(TAG_NEG_MSG)?;
308                seq.serialize_element(subscription_id)?;
309                seq.serialize_element(message)?;
310                seq.end()
311            }
312            Self::NegErr {
313                subscription_id,
314                message,
315            } => {
316                let mut seq = serializer.serialize_seq(Some(3))?;
317                seq.serialize_element(TAG_NEG_ERR)?;
318                seq.serialize_element(subscription_id)?;
319                seq.serialize_element(message)?;
320                seq.end()
321            }
322        }
323    }
324}
325
326impl<'de> Deserialize<'de> for RelayMessage {
327    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
328    where
329        D: Deserializer<'de>,
330    {
331        struct RelayVisitor;
332
333        impl<'de> Visitor<'de> for RelayVisitor {
334            type Value = RelayMessage;
335
336            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337                f.write_str("a Nostr relay message array")
338            }
339
340            fn visit_seq<A>(self, mut seq: A) -> Result<RelayMessage, A::Error>
341            where
342                A: SeqAccess<'de>,
343            {
344                let tag: String = seq
345                    .next_element()?
346                    .ok_or_else(|| de::Error::custom(RelayMessageError::Empty))?;
347                match tag.as_str() {
348                    TAG_EVENT => decode_event(&mut seq),
349                    TAG_OK => decode_ok(&mut seq),
350                    TAG_EOSE => decode_eose(&mut seq),
351                    TAG_CLOSED => decode_closed(&mut seq),
352                    TAG_NOTICE => decode_notice(&mut seq),
353                    TAG_AUTH => decode_auth(&mut seq),
354                    TAG_COUNT => decode_count(&mut seq),
355                    TAG_NEG_MSG => decode_neg_msg(&mut seq),
356                    TAG_NEG_ERR => decode_neg_err(&mut seq),
357                    other => Err(de::Error::custom(RelayMessageError::UnknownTag(
358                        other.to_owned(),
359                    ))),
360                }
361            }
362        }
363
364        deserializer.deserialize_seq(RelayVisitor)
365    }
366}
367
368fn malformed<E: de::Error>(tag: &'static str, reason: &str) -> E {
369    E::custom(RelayMessageError::Malformed {
370        tag,
371        reason: reason.to_owned(),
372    })
373}
374
375fn decode_event<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
376where
377    A: SeqAccess<'de>,
378{
379    let subscription_id: SubscriptionId = seq
380        .next_element()?
381        .ok_or_else(|| malformed::<A::Error>(TAG_EVENT, "missing subscription id"))?;
382    let event: Event = seq
383        .next_element()?
384        .ok_or_else(|| malformed::<A::Error>(TAG_EVENT, "missing event"))?;
385    Ok(RelayMessage::Event {
386        subscription_id,
387        event,
388    })
389}
390
391fn decode_ok<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
392where
393    A: SeqAccess<'de>,
394{
395    let event_id: EventId = seq
396        .next_element()?
397        .ok_or_else(|| malformed::<A::Error>(TAG_OK, "missing event id"))?;
398    let accepted: bool = seq
399        .next_element()?
400        .ok_or_else(|| malformed::<A::Error>(TAG_OK, "missing accepted flag"))?;
401    // NIP-01: "The 4th parameter MUST always be present, but MAY be an
402    // empty string when the 3rd is true". An absent message is malformed.
403    let message: String = seq
404        .next_element()?
405        .ok_or_else(|| malformed::<A::Error>(TAG_OK, "missing message"))?;
406    Ok(RelayMessage::Ok {
407        event_id,
408        accepted,
409        message,
410    })
411}
412
413fn decode_eose<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
414where
415    A: SeqAccess<'de>,
416{
417    let id: SubscriptionId = seq
418        .next_element()?
419        .ok_or_else(|| malformed::<A::Error>(TAG_EOSE, "missing subscription id"))?;
420    Ok(RelayMessage::EndOfStoredEvents(id))
421}
422
423fn decode_closed<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
424where
425    A: SeqAccess<'de>,
426{
427    let subscription_id: SubscriptionId = seq
428        .next_element()?
429        .ok_or_else(|| malformed::<A::Error>(TAG_CLOSED, "missing subscription id"))?;
430    let message: String = seq.next_element()?.unwrap_or_default();
431    Ok(RelayMessage::Closed {
432        subscription_id,
433        message,
434    })
435}
436
437fn decode_notice<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
438where
439    A: SeqAccess<'de>,
440{
441    let message: String = seq
442        .next_element()?
443        .ok_or_else(|| malformed::<A::Error>(TAG_NOTICE, "missing message"))?;
444    Ok(RelayMessage::Notice(message))
445}
446
447fn decode_auth<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
448where
449    A: SeqAccess<'de>,
450{
451    let challenge: String = seq
452        .next_element()?
453        .ok_or_else(|| malformed::<A::Error>(TAG_AUTH, "missing challenge"))?;
454    Ok(RelayMessage::Auth(challenge))
455}
456
457fn decode_count<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
458where
459    A: SeqAccess<'de>,
460{
461    #[derive(Deserialize)]
462    struct CountPayload {
463        count: u64,
464    }
465
466    let subscription_id: SubscriptionId = seq
467        .next_element()?
468        .ok_or_else(|| malformed::<A::Error>(TAG_COUNT, "missing subscription id"))?;
469    let payload: CountPayload = seq
470        .next_element()?
471        .ok_or_else(|| malformed::<A::Error>(TAG_COUNT, "missing count payload"))?;
472    Ok(RelayMessage::Count {
473        subscription_id,
474        count: payload.count,
475    })
476}
477
478fn decode_neg_msg<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
479where
480    A: SeqAccess<'de>,
481{
482    let subscription_id: SubscriptionId = seq
483        .next_element()?
484        .ok_or_else(|| malformed::<A::Error>(TAG_NEG_MSG, "missing subscription id"))?;
485    let message: String = seq
486        .next_element()?
487        .ok_or_else(|| malformed::<A::Error>(TAG_NEG_MSG, "missing message"))?;
488    Ok(RelayMessage::NegMsg {
489        subscription_id,
490        message,
491    })
492}
493
494fn decode_neg_err<'de, A>(seq: &mut A) -> Result<RelayMessage, A::Error>
495where
496    A: SeqAccess<'de>,
497{
498    let subscription_id: SubscriptionId = seq
499        .next_element()?
500        .ok_or_else(|| malformed::<A::Error>(TAG_NEG_ERR, "missing subscription id"))?;
501    let message: String = seq
502        .next_element()?
503        .ok_or_else(|| malformed::<A::Error>(TAG_NEG_ERR, "missing message"))?;
504    Ok(RelayMessage::NegErr {
505        subscription_id,
506        message,
507    })
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::Keys;
514    use crate::event::EventBuilder;
515
516    fn keys() -> Keys {
517        Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
518    }
519
520    fn signed_event() -> Event {
521        EventBuilder::text_note("hello")
522            .sign_with_keys(&keys())
523            .unwrap()
524    }
525
526    fn sub() -> SubscriptionId {
527        SubscriptionId::new("sub-1").unwrap()
528    }
529
530    #[test]
531    fn event_round_trip() {
532        let msg = RelayMessage::Event {
533            subscription_id: sub(),
534            event: signed_event(),
535        };
536        let json = serde_json::to_string(&msg).unwrap();
537        assert!(json.starts_with("[\"EVENT\",\"sub-1\","));
538        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
539        assert_eq!(parsed, msg);
540    }
541
542    #[test]
543    fn ok_round_trip_with_message() {
544        let msg = RelayMessage::Ok {
545            event_id: signed_event().id,
546            accepted: false,
547            message: "blocked: spam".to_owned(),
548        };
549        let json = serde_json::to_string(&msg).unwrap();
550        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
551        assert_eq!(parsed, msg);
552    }
553
554    #[test]
555    fn ok_with_empty_message_round_trip() {
556        let msg = RelayMessage::Ok {
557            event_id: signed_event().id,
558            accepted: true,
559            message: String::new(),
560        };
561        let json = serde_json::to_string(&msg).unwrap();
562        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
563        assert_eq!(parsed, msg);
564    }
565
566    #[test]
567    fn eose_round_trip() {
568        let msg = RelayMessage::EndOfStoredEvents(sub());
569        let json = serde_json::to_string(&msg).unwrap();
570        assert_eq!(json, "[\"EOSE\",\"sub-1\"]");
571        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
572        assert_eq!(parsed, msg);
573    }
574
575    #[test]
576    fn closed_round_trip() {
577        let msg = RelayMessage::Closed {
578            subscription_id: sub(),
579            message: "auth-required: please authenticate".to_owned(),
580        };
581        let json = serde_json::to_string(&msg).unwrap();
582        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
583        assert_eq!(parsed, msg);
584    }
585
586    #[test]
587    fn notice_round_trip() {
588        let msg = RelayMessage::Notice("welcome".to_owned());
589        let json = serde_json::to_string(&msg).unwrap();
590        assert_eq!(json, "[\"NOTICE\",\"welcome\"]");
591        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
592        assert_eq!(parsed, msg);
593    }
594
595    #[test]
596    fn auth_challenge_round_trip() {
597        let msg = RelayMessage::Auth("challenge-string".to_owned());
598        let json = serde_json::to_string(&msg).unwrap();
599        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
600        assert_eq!(parsed, msg);
601    }
602
603    #[test]
604    fn count_round_trip() {
605        let msg = RelayMessage::Count {
606            subscription_id: sub(),
607            count: 42,
608        };
609        let json = serde_json::to_string(&msg).unwrap();
610        assert_eq!(json, "[\"COUNT\",\"sub-1\",{\"count\":42}]");
611        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
612        assert_eq!(parsed, msg);
613    }
614
615    #[test]
616    fn machine_readable_prefix_parses() {
617        assert_eq!(
618            MachineReadablePrefix::from_reason("blocked: spam"),
619            Some(MachineReadablePrefix::Blocked)
620        );
621        assert_eq!(
622            MachineReadablePrefix::from_reason("auth-required: please"),
623            Some(MachineReadablePrefix::AuthRequired)
624        );
625        assert_eq!(
626            MachineReadablePrefix::from_reason("mute: nobody listening"),
627            Some(MachineReadablePrefix::Mute)
628        );
629        assert!(MachineReadablePrefix::from_reason("no prefix").is_none());
630        assert!(MachineReadablePrefix::from_reason("unknown: thing").is_none());
631    }
632
633    #[test]
634    fn ok_round_trips_mute_prefix() {
635        let msg = RelayMessage::Ok {
636            event_id: signed_event().id,
637            accepted: false,
638            message: "mute: nobody was listening".to_owned(),
639        };
640        let json = serde_json::to_string(&msg).unwrap();
641        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
642        assert_eq!(parsed, msg);
643    }
644
645    #[test]
646    fn ok_rejects_missing_message_per_nip01() {
647        // NIP-01: "The 4th parameter MUST always be present"
648        let json = format!(r#"["OK","{}",true]"#, signed_event().id.to_hex());
649        let err = serde_json::from_str::<RelayMessage>(&json).unwrap_err();
650        assert!(err.to_string().contains("missing message"));
651    }
652
653    #[test]
654    fn unknown_tag_rejected() {
655        let json = "[\"WAT\",\"x\"]";
656        let err = serde_json::from_str::<RelayMessage>(json).unwrap_err();
657        assert!(err.to_string().contains("unknown relay message tag"));
658    }
659
660    #[test]
661    fn neg_msg_round_trip() {
662        let msg = RelayMessage::NegMsg {
663            subscription_id: sub(),
664            message: "deadbeef".to_owned(),
665        };
666        let json = serde_json::to_string(&msg).unwrap();
667        assert_eq!(json, "[\"NEG-MSG\",\"sub-1\",\"deadbeef\"]");
668        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
669        assert_eq!(parsed, msg);
670    }
671
672    #[test]
673    fn neg_err_round_trip() {
674        let msg = RelayMessage::NegErr {
675            subscription_id: sub(),
676            message: "blocked: spam".to_owned(),
677        };
678        let json = serde_json::to_string(&msg).unwrap();
679        let parsed: RelayMessage = serde_json::from_str(&json).unwrap();
680        assert_eq!(parsed, msg);
681    }
682
683    #[test]
684    fn neg_msg_missing_payload_rejected() {
685        let json = "[\"NEG-MSG\",\"sub-1\"]";
686        let err = serde_json::from_str::<RelayMessage>(json).unwrap_err();
687        assert!(err.to_string().contains("missing message"));
688    }
689}