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