Skip to main content

hashtree_network/
root_events.rs

1use nostr_sdk::nostr::{
2    nips::nip19::FromBech32, Alphabet, Event, Filter, Kind, PublicKey, SingleLetterTag,
3};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct PeerRootEvent {
7    pub hash: String,
8    pub key: Option<String>,
9    pub encrypted_key: Option<String>,
10    pub self_encrypted_key: Option<String>,
11    pub event_id: String,
12    pub created_at: u64,
13    pub peer_id: String,
14}
15
16pub const HASHTREE_KIND: u16 = 30078;
17pub const HASHTREE_LABEL: &str = "hashtree";
18
19pub fn build_root_filter(owner_pubkey: &str, tree_name: &str) -> Option<Filter> {
20    let author = PublicKey::from_hex(owner_pubkey)
21        .or_else(|_| PublicKey::from_bech32(owner_pubkey))
22        .ok()?;
23    Some(
24        Filter::new()
25            .kind(Kind::Custom(HASHTREE_KIND))
26            .author(author)
27            .custom_tag(
28                SingleLetterTag::lowercase(Alphabet::D),
29                vec![tree_name.to_string()],
30            )
31            .custom_tag(
32                SingleLetterTag::lowercase(Alphabet::L),
33                vec![HASHTREE_LABEL.to_string()],
34            )
35            .limit(50),
36    )
37}
38
39pub fn hashtree_event_identifier(event: &Event) -> Option<String> {
40    event.tags.iter().find_map(|tag| {
41        let slice = tag.as_slice();
42        if slice.len() >= 2 && slice[0].as_str() == "d" {
43            Some(slice[1].to_string())
44        } else {
45            None
46        }
47    })
48}
49
50pub fn is_hashtree_labeled_event(event: &Event) -> bool {
51    event.tags.iter().any(|tag| {
52        let slice = tag.as_slice();
53        slice.len() >= 2 && slice[0].as_str() == "l" && slice[1].as_str() == HASHTREE_LABEL
54    })
55}
56
57pub fn pick_latest_event<'a, I>(events: I) -> Option<&'a Event>
58where
59    I: IntoIterator<Item = &'a Event>,
60{
61    events.into_iter().max_by(|a, b| {
62        let ordering = a.created_at.cmp(&b.created_at);
63        if ordering == std::cmp::Ordering::Equal {
64            a.id.cmp(&b.id)
65        } else {
66            ordering
67        }
68    })
69}
70
71pub fn root_event_from_peer(
72    event: &Event,
73    peer_id: &str,
74    tree_name: &str,
75) -> Option<PeerRootEvent> {
76    if hashtree_event_identifier(event).as_deref() != Some(tree_name)
77        || !is_hashtree_labeled_event(event)
78    {
79        return None;
80    }
81
82    let mut key = None;
83    let mut encrypted_key = None;
84    let mut self_encrypted_key = None;
85    let mut hash_tag = None;
86
87    for tag in &event.tags {
88        let slice = tag.as_slice();
89        if slice.len() < 2 {
90            continue;
91        }
92        match slice[0].as_str() {
93            "hash" => hash_tag = Some(slice[1].to_string()),
94            "key" => key = Some(slice[1].to_string()),
95            "encryptedKey" => encrypted_key = Some(slice[1].to_string()),
96            "selfEncryptedKey" => self_encrypted_key = Some(slice[1].to_string()),
97            _ => {}
98        }
99    }
100
101    let hash = hash_tag.or_else(|| {
102        if event.content.is_empty() {
103            None
104        } else {
105            Some(event.content.clone())
106        }
107    })?;
108
109    Some(PeerRootEvent {
110        hash,
111        key,
112        encrypted_key,
113        self_encrypted_key,
114        event_id: event.id.to_hex(),
115        created_at: event.created_at.as_u64(),
116        peer_id: peer_id.to_string(),
117    })
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use nostr_sdk::nostr::{EventBuilder, Keys, Tag, Timestamp};
124
125    #[test]
126    fn root_event_from_peer_extracts_tags() {
127        let keys = Keys::generate();
128        let hash = "ab".repeat(32);
129        let event = EventBuilder::new(
130            Kind::Custom(HASHTREE_KIND),
131            "",
132            [
133                Tag::parse(&["d", "repo"]).expect("d tag"),
134                Tag::parse(&["l", HASHTREE_LABEL]).expect("label tag"),
135                Tag::parse(&["hash", &hash]).expect("hash tag"),
136                Tag::parse(&["encryptedKey", &"11".repeat(32)]).expect("encryptedKey tag"),
137            ],
138        )
139        .to_event(&keys)
140        .expect("event");
141
142        let parsed = root_event_from_peer(&event, "peer-a", "repo").expect("root event");
143        let expected_encrypted = "11".repeat(32);
144        assert_eq!(parsed.hash, hash);
145        assert_eq!(parsed.peer_id, "peer-a");
146        assert_eq!(
147            parsed.encrypted_key.as_deref(),
148            Some(expected_encrypted.as_str())
149        );
150        assert!(parsed.key.is_none());
151    }
152
153    #[test]
154    fn pick_latest_event_prefers_higher_event_id_on_timestamp_tie() {
155        let keys = Keys::generate();
156        let created_at = Timestamp::from_secs(1_700_000_000);
157        let event_a = EventBuilder::new(Kind::Custom(HASHTREE_KIND), "", [])
158            .custom_created_at(created_at)
159            .to_event(&keys)
160            .expect("event a");
161        let event_b = EventBuilder::new(Kind::Custom(HASHTREE_KIND), "", [])
162            .custom_created_at(created_at)
163            .to_event(&keys)
164            .expect("event b");
165
166        let expected = if event_a.id > event_b.id {
167            event_a.id
168        } else {
169            event_b.id
170        };
171        let picked = pick_latest_event([&event_a, &event_b]).expect("picked event");
172        assert_eq!(picked.id, expected);
173    }
174}