lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }