hashtree_network/
root_events.rs1use 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}