authored.rs (10602B)
1 #[cfg(not(feature = "std"))] 2 use alloc::{ 3 string::{String, ToString}, 4 vec, 5 vec::Vec, 6 }; 7 8 use radroots_event::{ 9 envelope::kind::KIND_COMMENT, 10 post::comment::{ 11 AuthoredNip22Comment, Nip22AddressRootReference, Nip22CommentParentReference, 12 Nip22CommentPosition, Nip22CommentRoot, Nip22EventRootReference, 13 }, 14 tag::relay_hint::NostrRelayHint, 15 wire::Nip01EventWireParts, 16 }; 17 use radroots_identity::PublicKey; 18 19 /// Builds deterministic unsigned kind-1111 wire parts for a strict NIP-22 20 /// comment. 21 pub fn authored_nip22_comment_to_wire_parts(comment: &AuthoredNip22Comment) -> Nip01EventWireParts { 22 let root = comment.root(); 23 let position = comment.position(); 24 let mut tags = Vec::with_capacity(6); 25 match root { 26 Nip22CommentRoot::Event(reference) => { 27 tags.push(event_reference_tag("E", reference)); 28 } 29 Nip22CommentRoot::Address(reference) => { 30 tags.push(address_reference_tag("A", reference)); 31 } 32 } 33 tags.push(vec!["K".to_string(), root.kind().as_u32().to_string()]); 34 tags.push(participant_tag("P", root.author(), root.relay())); 35 36 match (root, position) { 37 (Nip22CommentRoot::Event(reference), Nip22CommentPosition::TopLevelEvent) => { 38 tags.push(event_reference_tag("e", reference)); 39 tags.push(vec!["k".to_string(), root.kind().as_u32().to_string()]); 40 tags.push(participant_tag("p", reference.author(), reference.relay())); 41 } 42 ( 43 Nip22CommentRoot::Address(reference), 44 Nip22CommentPosition::TopLevelAddress { current_revision }, 45 ) => { 46 tags.push(address_reference_tag("a", reference)); 47 tags.push(optional_relay_tag( 48 "e", 49 current_revision.to_hex().as_str(), 50 reference.relay(), 51 )); 52 tags.push(vec!["k".to_string(), root.kind().as_u32().to_string()]); 53 tags.push(participant_tag("p", reference.author(), reference.relay())); 54 } 55 (_, Nip22CommentPosition::Nested { parent }) => { 56 tags.push(parent_reference_tag(parent)); 57 tags.push(vec!["k".to_string(), KIND_COMMENT.to_string()]); 58 tags.push(participant_tag("p", parent.author(), parent.relay())); 59 } 60 _ => unreachable!("authored Comment constructors preserve root and position compatibility"), 61 } 62 63 Nip01EventWireParts { 64 kind: KIND_COMMENT, 65 content: comment.content().to_string(), 66 tags, 67 } 68 } 69 70 fn event_reference_tag(name: &str, reference: &Nip22EventRootReference) -> Vec<String> { 71 vec![ 72 name.to_string(), 73 reference.event_id().to_hex(), 74 reference.relay_or_empty().to_string(), 75 reference.author().to_hex(), 76 ] 77 } 78 79 fn address_reference_tag(name: &str, reference: &Nip22AddressRootReference) -> Vec<String> { 80 optional_relay_tag(name, reference.coordinate().as_str(), reference.relay()) 81 } 82 83 fn parent_reference_tag(reference: &Nip22CommentParentReference) -> Vec<String> { 84 vec![ 85 "e".to_string(), 86 reference.event_id().to_hex(), 87 reference.relay_or_empty().to_string(), 88 reference.author().to_hex(), 89 ] 90 } 91 92 fn participant_tag(name: &str, author: &PublicKey, relay: Option<&NostrRelayHint>) -> Vec<String> { 93 optional_relay_tag(name, &author.to_hex(), relay) 94 } 95 96 fn optional_relay_tag(name: &str, value: &str, relay: Option<&NostrRelayHint>) -> Vec<String> { 97 let mut tag = vec![name.to_string(), value.to_string()]; 98 if let Some(relay) = relay { 99 tag.push(relay.as_str().to_string()); 100 } 101 tag 102 } 103 104 #[cfg(test)] 105 mod tests { 106 use super::*; 107 use radroots_event::{ 108 envelope::kind::{ 109 KIND_CALENDAR_DATE_EVENT, KIND_CALENDAR_TIME_EVENT, KIND_CLASSIFIED_LISTING, 110 }, 111 post::comment::{ 112 Nip22AddressRootReference, Nip22CommentError, Nip22CommentParentReference, 113 Nip22EventRootReference, RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES, 114 RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES, 115 }, 116 }; 117 118 fn h(character: char) -> String { 119 crate::test_fixtures::fixture_public_key_hex(character) 120 } 121 122 fn event_root(relay: Option<&str>) -> Nip22EventRootReference { 123 Nip22EventRootReference::parse(h('a'), h('b'), KIND_CLASSIFIED_LISTING, relay) 124 .expect("event root") 125 } 126 127 fn address_root(relay: Option<&str>) -> Nip22AddressRootReference { 128 Nip22AddressRootReference::parse(format!("31922:{}:market-day", h('b')), relay) 129 .expect("address root") 130 } 131 132 fn parent(relay: Option<&str>) -> Nip22CommentParentReference { 133 Nip22CommentParentReference::parse(h('c'), h('d'), relay).expect("parent") 134 } 135 136 #[derive(Clone, Copy)] 137 enum AuthoredShape { 138 TopEvent, 139 TopAddress, 140 NestedEvent, 141 NestedAddress, 142 } 143 144 fn authored_shape( 145 shape: AuthoredShape, 146 content: String, 147 ) -> Result<AuthoredNip22Comment, Nip22CommentError> { 148 match shape { 149 AuthoredShape::TopEvent => { 150 AuthoredNip22Comment::top_level_event(content, event_root(None)) 151 } 152 AuthoredShape::TopAddress => { 153 AuthoredNip22Comment::parse_top_level_address(content, address_root(None), h('e')) 154 } 155 AuthoredShape::NestedEvent => { 156 AuthoredNip22Comment::nested(content, event_root(None), parent(None)) 157 } 158 AuthoredShape::NestedAddress => { 159 AuthoredNip22Comment::nested(content, address_root(None), parent(None)) 160 } 161 } 162 } 163 164 #[test] 165 fn emits_exact_top_level_event_tags_with_empty_relay_slot() { 166 let comment = 167 AuthoredNip22Comment::top_level_event("Comment", event_root(None)).expect("comment"); 168 assert_eq!( 169 authored_nip22_comment_to_wire_parts(&comment).tags, 170 vec![ 171 vec!["E".to_string(), h('a'), String::new(), h('b'),], 172 vec!["K".to_string(), KIND_CLASSIFIED_LISTING.to_string()], 173 vec!["P".to_string(), h('b')], 174 vec!["e".to_string(), h('a'), String::new(), h('b'),], 175 vec!["k".to_string(), KIND_CLASSIFIED_LISTING.to_string()], 176 vec!["p".to_string(), h('b')], 177 ] 178 ); 179 } 180 181 #[test] 182 fn emits_exact_top_level_address_tags_with_revision_without_author() { 183 let comment = AuthoredNip22Comment::parse_top_level_address( 184 "Comment", 185 address_root(Some("wss://relay.example")), 186 h('e'), 187 ) 188 .expect("comment"); 189 let tags = authored_nip22_comment_to_wire_parts(&comment).tags; 190 assert_eq!(tags.len(), 7); 191 assert_eq!( 192 tags.iter().map(|tag| tag[0].as_str()).collect::<Vec<_>>(), 193 vec!["A", "K", "P", "a", "e", "k", "p"] 194 ); 195 assert_eq!( 196 tags[4], 197 vec!["e".to_string(), h('e'), "wss://relay.example".to_string()] 198 ); 199 } 200 201 #[test] 202 fn emits_exact_nested_event_and_address_shapes() { 203 for root in [ 204 Nip22CommentRoot::from( 205 Nip22EventRootReference::parse( 206 h('a'), 207 h('b'), 208 KIND_CALENDAR_TIME_EVENT, 209 Some("wss://root.example"), 210 ) 211 .expect("event root"), 212 ), 213 Nip22CommentRoot::from( 214 Nip22AddressRootReference::parse( 215 format!("31922:{}:market", h('b')), 216 Some("wss://root.example"), 217 ) 218 .expect("address root"), 219 ), 220 ] { 221 let comment = 222 AuthoredNip22Comment::nested("Reply", root, parent(Some("wss://parent.example"))) 223 .expect("comment"); 224 let tags = authored_nip22_comment_to_wire_parts(&comment).tags; 225 assert_eq!(tags.len(), 6); 226 assert_eq!(tags[4], vec!["k".to_string(), KIND_COMMENT.to_string()]); 227 assert_eq!( 228 tags[5], 229 vec!["p".to_string(), h('d'), "wss://parent.example".to_string()] 230 ); 231 } 232 233 assert_eq!(KIND_CALENDAR_DATE_EVENT, 31_922); 234 } 235 236 #[test] 237 fn core_wire_estimator_matches_emitted_json_for_all_authored_shapes() { 238 for shape in [ 239 AuthoredShape::TopEvent, 240 AuthoredShape::TopAddress, 241 AuthoredShape::NestedEvent, 242 AuthoredShape::NestedAddress, 243 ] { 244 let mut lower = 1usize; 245 let mut upper = RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES; 246 while lower < upper { 247 let candidate = lower + (upper - lower).div_ceil(2); 248 if authored_shape(shape, "\u{0001}".repeat(candidate)).is_ok() { 249 lower = candidate; 250 } else { 251 upper = candidate - 1; 252 } 253 } 254 255 let exact = authored_shape(shape, "\u{0001}".repeat(lower)) 256 .expect("largest content fitting the signed wire"); 257 let parts = authored_nip22_comment_to_wire_parts(&exact); 258 let exact_json = serde_json::json!({ 259 "id": "0".repeat(64), 260 "pubkey": "0".repeat(64), 261 "created_at": u64::MAX, 262 "kind": KIND_COMMENT, 263 "tags": &parts.tags, 264 "content": exact.content(), 265 "sig": "0".repeat(128), 266 }) 267 .to_string(); 268 assert!(exact_json.len() <= RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES); 269 270 let overflow_content = "\u{0001}".repeat(lower + 1); 271 let error = authored_shape(shape, overflow_content.clone()) 272 .expect_err("one more escaped byte must cross the signed wire limit"); 273 let overflow_json = serde_json::json!({ 274 "id": "0".repeat(64), 275 "pubkey": "0".repeat(64), 276 "created_at": u64::MAX, 277 "kind": KIND_COMMENT, 278 "tags": &parts.tags, 279 "content": overflow_content, 280 "sig": "0".repeat(128), 281 }) 282 .to_string(); 283 assert!(matches!( 284 error, 285 Nip22CommentError::EventWireTooLarge { max, actual } 286 if max == RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES 287 && actual == overflow_json.len() 288 )); 289 } 290 } 291 }