Skip to main content

nula_core/nips/
nip62.rs

1//! [NIP-62] Request to Vanish.
2//!
3//! `kind: 62` is a relay-side delete-everything request bound to the
4//! signer's pubkey. The `relay` tag column either targets a specific
5//! relay URL or carries the sentinel [`ALL_RELAYS_SENTINEL`] for a
6//! global request.
7//!
8//! Relays MUST honor the request even against paid / restricted
9//! pubkeys; the spec also pins that NIP-09 deletion-request events
10//! (`kind: 5`) targeting a request-to-vanish event have no effect.
11//!
12//! [NIP-62]: https://github.com/nostr-protocol/nips/blob/master/62.md
13
14use thiserror::Error;
15
16use crate::event::{Event, EventBuilder, Kind, Tag, TagKind};
17use crate::types::{RelayUrl, RelayUrlError};
18
19/// `kind: 62` — request to vanish.
20pub const KIND_REQUEST_TO_VANISH: Kind = Kind::REQUEST_TO_VANISH;
21
22/// Sentinel value the spec reserves for the global request shape.
23pub const ALL_RELAYS_SENTINEL: &str = "ALL_RELAYS";
24
25const RELAY_TAG: &str = "relay";
26
27/// Per-target shape of the `relay` tag on a request to vanish.
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum VanishTarget {
30    /// `["relay", "<relay url>"]` — request bound to a single relay.
31    Relay(RelayUrl),
32    /// `["relay", "ALL_RELAYS"]` — global request targeted at every
33    /// relay the client can reach.
34    AllRelays,
35}
36
37/// Typed bundle for a `kind: 62` request-to-vanish event.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct RequestToVanish {
40    /// Optional reason / legal notice mirrored from `.content`.
41    pub reason: String,
42    /// Targets — at least one is required by spec.
43    pub targets: Vec<VanishTarget>,
44    /// Forward-compatible passthrough for unknown tags.
45    pub extra_tags: Vec<Tag>,
46}
47
48/// Errors raised while parsing a NIP-62 event.
49#[derive(Debug, Error)]
50#[non_exhaustive]
51pub enum VanishError {
52    /// Event kind is not `62`.
53    #[error("unexpected kind for NIP-62 request to vanish: {}", .0.as_u16())]
54    WrongKind(Kind),
55    /// At least one `relay` tag is required by spec.
56    #[error("NIP-62 event missing required `relay` tag")]
57    MissingTarget,
58    /// A `relay` tag was malformed (missing column 1).
59    #[error("`relay` tag missing target")]
60    MalformedTarget,
61    /// Wrapped relay-URL parser error.
62    #[error(transparent)]
63    InvalidRelayUrl(#[from] RelayUrlError),
64}
65
66impl VanishTarget {
67    fn to_tag(&self) -> Tag {
68        let head = TagKind::from_wire(RELAY_TAG);
69        match self {
70            Self::Relay(url) => Tag::with(&head, [url.as_str().to_owned()]),
71            Self::AllRelays => Tag::with(&head, [ALL_RELAYS_SENTINEL.to_owned()]),
72        }
73    }
74
75    fn from_tag(tag: &Tag) -> Result<Self, VanishError> {
76        let raw = tag.get(1).ok_or(VanishError::MalformedTarget)?;
77        if raw == ALL_RELAYS_SENTINEL {
78            Ok(Self::AllRelays)
79        } else {
80            Ok(Self::Relay(RelayUrl::parse(raw)?))
81        }
82    }
83}
84
85impl RequestToVanish {
86    /// Construct a request bound to one or more relays.
87    #[must_use]
88    pub fn relay(reason: impl Into<String>, relays: Vec<RelayUrl>) -> Self {
89        Self {
90            reason: reason.into(),
91            targets: relays.into_iter().map(VanishTarget::Relay).collect(),
92            extra_tags: Vec::new(),
93        }
94    }
95
96    /// Construct a global request hitting every reachable relay.
97    #[must_use]
98    pub fn all_relays(reason: impl Into<String>) -> Self {
99        Self {
100            reason: reason.into(),
101            targets: vec![VanishTarget::AllRelays],
102            extra_tags: Vec::new(),
103        }
104    }
105
106    /// Parse a `kind: 62` request-to-vanish event.
107    ///
108    /// # Errors
109    ///
110    /// See [`VanishError`] for the failure modes.
111    pub fn from_event(event: &Event) -> Result<Self, VanishError> {
112        if event.kind != KIND_REQUEST_TO_VANISH {
113            return Err(VanishError::WrongKind(event.kind));
114        }
115        let mut targets: Vec<VanishTarget> = Vec::new();
116        let mut extra_tags: Vec<Tag> = Vec::new();
117        for tag in &event.tags {
118            if tag.name() == RELAY_TAG {
119                targets.push(VanishTarget::from_tag(tag)?);
120            } else {
121                extra_tags.push(tag.clone());
122            }
123        }
124        if targets.is_empty() {
125            return Err(VanishError::MissingTarget);
126        }
127        Ok(Self {
128            reason: event.content.clone(),
129            targets,
130            extra_tags,
131        })
132    }
133}
134
135impl EventBuilder {
136    /// Author a NIP-62 `kind: 62` request-to-vanish event.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`VanishError::MissingTarget`] when
141    /// [`RequestToVanish::targets`] is empty.
142    pub fn request_to_vanish(req: &RequestToVanish) -> Result<Self, VanishError> {
143        if req.targets.is_empty() {
144            return Err(VanishError::MissingTarget);
145        }
146        let mut builder = Self::new(KIND_REQUEST_TO_VANISH, req.reason.clone());
147        for target in &req.targets {
148            builder = builder.tag(target.to_tag());
149        }
150        for tag in &req.extra_tags {
151            builder = builder.tag(tag.clone());
152        }
153        Ok(builder)
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    use crate::Keys;
161
162    fn keys() -> Keys {
163        Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
164    }
165
166    #[test]
167    fn relay_target_round_trip() {
168        let req = RequestToVanish::relay(
169            "GDPR request",
170            vec![RelayUrl::parse("wss://relay.example/").unwrap()],
171        );
172        let event = EventBuilder::request_to_vanish(&req)
173            .unwrap()
174            .sign_with_keys(&keys())
175            .unwrap();
176        let parsed = RequestToVanish::from_event(&event).unwrap();
177        assert_eq!(parsed, req);
178    }
179
180    #[test]
181    fn all_relays_round_trip() {
182        let req = RequestToVanish::all_relays("legal");
183        let event = EventBuilder::request_to_vanish(&req)
184            .unwrap()
185            .sign_with_keys(&keys())
186            .unwrap();
187        let parsed = RequestToVanish::from_event(&event).unwrap();
188        assert!(matches!(parsed.targets[0], VanishTarget::AllRelays));
189    }
190
191    #[test]
192    fn missing_target_is_rejected() {
193        let req = RequestToVanish {
194            reason: "x".into(),
195            targets: Vec::new(),
196            extra_tags: Vec::new(),
197        };
198        assert!(matches!(
199            EventBuilder::request_to_vanish(&req),
200            Err(VanishError::MissingTarget)
201        ));
202    }
203
204    #[test]
205    fn wrong_kind_is_rejected() {
206        // A text note is not a NIP-62 request \u2014 the parser must reject it.
207        let event = EventBuilder::text_note("nope")
208            .sign_with_keys(&keys())
209            .unwrap();
210        assert!(matches!(
211            RequestToVanish::from_event(&event),
212            Err(VanishError::WrongKind(_))
213        ));
214    }
215
216    #[test]
217    fn parse_event_with_no_relay_tags_rejected() {
218        // A kind-62 event with zero `relay` tags violates the spec.
219        let event = EventBuilder::new(KIND_REQUEST_TO_VANISH, "no relay tag here")
220            .sign_with_keys(&keys())
221            .unwrap();
222        assert!(matches!(
223            RequestToVanish::from_event(&event),
224            Err(VanishError::MissingTarget)
225        ));
226    }
227
228    #[test]
229    fn malformed_relay_tag_is_rejected() {
230        // A `relay` tag with only the head column is structurally
231        // malformed; the parser should surface MalformedTarget.
232        let event = EventBuilder::new(KIND_REQUEST_TO_VANISH, "")
233            .tag(Tag::with(
234                &TagKind::from_wire(RELAY_TAG),
235                Vec::<String>::new(),
236            ))
237            .sign_with_keys(&keys())
238            .unwrap();
239        assert!(matches!(
240            RequestToVanish::from_event(&event),
241            Err(VanishError::MalformedTarget)
242        ));
243    }
244}