Skip to main content

nula_core/nips/
nip70.rs

1//! [NIP-70] Protected Events.
2//!
3//! NIP-70 lets the author mark an event as *protected*: relays SHOULD
4//! refuse to accept the event from anyone other than its (NIP-42
5//! authenticated) author, and SHOULD strip the event from query results
6//! served to clients that have not authenticated as the author.
7//!
8//! The marker is a single-element tag: `["-"]`. This module exposes the
9//! tag's wire name and two trivial helpers — [`is_protected`] to check an
10//! event and [`EventBuilder::protected`] to attach the marker.
11//!
12//! [NIP-70]: https://github.com/nostr-protocol/nips/blob/master/70.md
13
14use crate::event::{Event, EventBuilder, Tag, TagKind};
15
16/// Wire name of the NIP-70 protected tag (`-`).
17pub const PROTECTED_TAG: &str = "-";
18
19/// True when `event` carries a `["-"]` tag.
20#[must_use]
21pub fn is_protected(event: &Event) -> bool {
22    let kind = TagKind::from_wire(PROTECTED_TAG);
23    event.tags.find_first(&kind).is_some()
24}
25
26impl EventBuilder {
27    /// Attach the NIP-70 protected marker.
28    ///
29    /// The marker is idempotent: the builder skips the operation if a
30    /// `-` tag is already present, so chaining `.protected()` multiple
31    /// times produces exactly one tag.
32    #[must_use]
33    pub fn protected(mut self) -> Self {
34        let kind = TagKind::from_wire(PROTECTED_TAG);
35        // `Tag::with` ships the head string as the tag's first element and
36        // accepts zero further values, producing exactly `["-"]`.
37        let tag = Tag::with(&kind, std::iter::empty::<String>());
38        self.tags_mut().push_unique_kind(tag);
39        self
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use crate::Keys;
47    use crate::types::Timestamp;
48
49    fn keys() -> Keys {
50        Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
51    }
52
53    #[test]
54    fn unmarked_event_is_not_protected() {
55        let event = EventBuilder::text_note("public")
56            .created_at(Timestamp::from_secs(1))
57            .sign_with_keys(&keys())
58            .unwrap();
59        assert!(!is_protected(&event));
60    }
61
62    #[test]
63    fn protected_marker_round_trip() {
64        let event = EventBuilder::text_note("private")
65            .created_at(Timestamp::from_secs(2))
66            .protected()
67            .sign_with_keys(&keys())
68            .unwrap();
69        event.verify().unwrap();
70        assert!(is_protected(&event));
71    }
72
73    #[test]
74    fn protected_is_idempotent() {
75        let event = EventBuilder::text_note("private")
76            .created_at(Timestamp::from_secs(3))
77            .protected()
78            .protected()
79            .protected()
80            .sign_with_keys(&keys())
81            .unwrap();
82        let count = event
83            .tags
84            .iter()
85            .filter(|t| t.kind() == TagKind::from_wire(PROTECTED_TAG))
86            .count();
87        assert_eq!(count, 1);
88    }
89}