lib

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

registry_v7.rs (53322B)


      1 //! Frozen NIP-22 comment semantics for event-contract registry v7.
      2 
      3 #[cfg(not(feature = "std"))]
      4 use alloc::{format, string::String, vec::Vec};
      5 use core::fmt;
      6 
      7 use radroots_event::{
      8     envelope::kind::KIND_COMMENT,
      9     id::{AddressableCoordinate, AddressableCoordinateParts, EventId, ParseError},
     10     post::comment::{
     11         Nip22CommentRootKind, RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES,
     12         RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES, RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES,
     13         RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT, RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT,
     14         RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES,
     15     },
     16     tag::relay_hint::NostrRelayHint,
     17 };
     18 use radroots_identity::PublicKey;
     19 
     20 use crate::verification::v1::RadrootsSignatureVerifiedEvent;
     21 
     22 const RADROOTS_NIP22_COMMENT_SIGNED_EVENT_FIXED_MAX_BYTES: usize = "{\"id\":\"".len()
     23     + 64
     24     + "\",\"pubkey\":\"".len()
     25     + 64
     26     + "\",\"created_at\":".len()
     27     + ",\"kind\":1111,\"tags\":".len()
     28     + ",\"content\":".len()
     29     + ",\"sig\":\"".len()
     30     + 128
     31     + "\"}".len();
     32 
     33 #[derive(Clone, Debug, PartialEq, Eq)]
     34 #[non_exhaustive]
     35 pub enum RadrootsNip22CommentDiagnostic {
     36     RootRelayIgnored {
     37         tag_index: usize,
     38         raw_tag: Vec<String>,
     39     },
     40     RootAuthorRelayIgnored {
     41         tag_index: usize,
     42         raw_tag: Vec<String>,
     43     },
     44     RootAuthorHintIgnored {
     45         tag_index: usize,
     46         raw_tag: Vec<String>,
     47     },
     48     ParentRelayIgnored {
     49         tag_index: usize,
     50         raw_tag: Vec<String>,
     51     },
     52     ParentAuthorRelayIgnored {
     53         tag_index: usize,
     54         raw_tag: Vec<String>,
     55     },
     56     ParentAuthorHintIgnored {
     57         tag_index: usize,
     58         raw_tag: Vec<String>,
     59     },
     60     ParentAuthorShapeIgnored {
     61         tag_index: usize,
     62         raw_tag: Vec<String>,
     63     },
     64     ParentAuthorInvalidIgnored {
     65         tag_index: usize,
     66         raw_tag: Vec<String>,
     67     },
     68     ParentAuthorDuplicateIgnored {
     69         tag_index: usize,
     70         raw_tag: Vec<String>,
     71     },
     72     RevisionRelayIgnored {
     73         tag_index: usize,
     74         raw_tag: Vec<String>,
     75     },
     76 }
     77 
     78 impl RadrootsNip22CommentDiagnostic {
     79     pub const fn code(&self) -> &'static str {
     80         match self {
     81             Self::RootRelayIgnored { .. } => "comment_root_relay_ignored",
     82             Self::RootAuthorRelayIgnored { .. } => "comment_root_author_relay_ignored",
     83             Self::RootAuthorHintIgnored { .. } => "comment_root_author_hint_ignored",
     84             Self::ParentRelayIgnored { .. } => "comment_parent_relay_ignored",
     85             Self::ParentAuthorRelayIgnored { .. } => "comment_parent_author_relay_ignored",
     86             Self::ParentAuthorHintIgnored { .. } => "comment_parent_author_hint_ignored",
     87             Self::ParentAuthorShapeIgnored { .. } => "comment_parent_author_shape_ignored",
     88             Self::ParentAuthorInvalidIgnored { .. } => "comment_parent_author_invalid_ignored",
     89             Self::ParentAuthorDuplicateIgnored { .. } => "comment_parent_author_duplicate_ignored",
     90             Self::RevisionRelayIgnored { .. } => "comment_revision_relay_ignored",
     91         }
     92     }
     93 
     94     pub const fn tag_index(&self) -> usize {
     95         match self {
     96             Self::RootRelayIgnored { tag_index, .. }
     97             | Self::RootAuthorRelayIgnored { tag_index, .. }
     98             | Self::RootAuthorHintIgnored { tag_index, .. }
     99             | Self::ParentRelayIgnored { tag_index, .. }
    100             | Self::ParentAuthorRelayIgnored { tag_index, .. }
    101             | Self::ParentAuthorHintIgnored { tag_index, .. }
    102             | Self::ParentAuthorShapeIgnored { tag_index, .. }
    103             | Self::ParentAuthorInvalidIgnored { tag_index, .. }
    104             | Self::ParentAuthorDuplicateIgnored { tag_index, .. }
    105             | Self::RevisionRelayIgnored { tag_index, .. } => *tag_index,
    106         }
    107     }
    108 
    109     pub fn raw_tag(&self) -> &[String] {
    110         match self {
    111             Self::RootRelayIgnored { raw_tag, .. }
    112             | Self::RootAuthorRelayIgnored { raw_tag, .. }
    113             | Self::RootAuthorHintIgnored { raw_tag, .. }
    114             | Self::ParentRelayIgnored { raw_tag, .. }
    115             | Self::ParentAuthorRelayIgnored { raw_tag, .. }
    116             | Self::ParentAuthorHintIgnored { raw_tag, .. }
    117             | Self::ParentAuthorShapeIgnored { raw_tag, .. }
    118             | Self::ParentAuthorInvalidIgnored { raw_tag, .. }
    119             | Self::ParentAuthorDuplicateIgnored { raw_tag, .. }
    120             | Self::RevisionRelayIgnored { raw_tag, .. } => raw_tag,
    121         }
    122     }
    123 }
    124 
    125 #[derive(Clone, Debug, PartialEq, Eq)]
    126 pub struct RadrootsInboundNip22Participant {
    127     tag_index: usize,
    128     pubkey: PublicKey,
    129     relay: Option<NostrRelayHint>,
    130     raw_tag: Vec<String>,
    131 }
    132 
    133 impl RadrootsInboundNip22Participant {
    134     pub const fn tag_index(&self) -> usize {
    135         self.tag_index
    136     }
    137 
    138     pub const fn pubkey(&self) -> &PublicKey {
    139         &self.pubkey
    140     }
    141 
    142     pub const fn relay(&self) -> Option<&NostrRelayHint> {
    143         self.relay.as_ref()
    144     }
    145 
    146     pub fn raw_tag(&self) -> &[String] {
    147         &self.raw_tag
    148     }
    149 }
    150 
    151 #[derive(Clone, Debug, PartialEq, Eq)]
    152 pub struct RadrootsInboundNip22EventRoot {
    153     tag_index: usize,
    154     event_id: EventId,
    155     kind_tag_index: usize,
    156     kind_raw_tag: Vec<String>,
    157     kind: Nip22CommentRootKind,
    158     relay: Option<NostrRelayHint>,
    159     author_hint: Option<PublicKey>,
    160     author: RadrootsInboundNip22Participant,
    161     raw_tag: Vec<String>,
    162 }
    163 
    164 impl RadrootsInboundNip22EventRoot {
    165     pub const fn tag_index(&self) -> usize {
    166         self.tag_index
    167     }
    168 
    169     pub const fn event_id(&self) -> &EventId {
    170         &self.event_id
    171     }
    172 
    173     pub const fn kind_tag_index(&self) -> usize {
    174         self.kind_tag_index
    175     }
    176 
    177     pub fn kind_raw_tag(&self) -> &[String] {
    178         &self.kind_raw_tag
    179     }
    180 
    181     pub const fn kind(&self) -> Nip22CommentRootKind {
    182         self.kind
    183     }
    184 
    185     pub const fn relay(&self) -> Option<&NostrRelayHint> {
    186         self.relay.as_ref()
    187     }
    188 
    189     pub const fn author_hint(&self) -> Option<&PublicKey> {
    190         self.author_hint.as_ref()
    191     }
    192 
    193     pub const fn author(&self) -> &RadrootsInboundNip22Participant {
    194         &self.author
    195     }
    196 
    197     pub fn raw_tag(&self) -> &[String] {
    198         &self.raw_tag
    199     }
    200 }
    201 
    202 #[derive(Clone, Debug, PartialEq, Eq)]
    203 pub struct RadrootsInboundNip22AddressRoot {
    204     tag_index: usize,
    205     coordinate: AddressableCoordinate,
    206     kind_tag_index: usize,
    207     kind_raw_tag: Vec<String>,
    208     kind: Nip22CommentRootKind,
    209     relay: Option<NostrRelayHint>,
    210     author: RadrootsInboundNip22Participant,
    211     raw_tag: Vec<String>,
    212 }
    213 
    214 impl RadrootsInboundNip22AddressRoot {
    215     pub const fn tag_index(&self) -> usize {
    216         self.tag_index
    217     }
    218 
    219     pub const fn coordinate(&self) -> &AddressableCoordinate {
    220         &self.coordinate
    221     }
    222 
    223     pub const fn kind_tag_index(&self) -> usize {
    224         self.kind_tag_index
    225     }
    226 
    227     pub fn kind_raw_tag(&self) -> &[String] {
    228         &self.kind_raw_tag
    229     }
    230 
    231     pub const fn kind(&self) -> Nip22CommentRootKind {
    232         self.kind
    233     }
    234 
    235     pub const fn relay(&self) -> Option<&NostrRelayHint> {
    236         self.relay.as_ref()
    237     }
    238 
    239     pub const fn author(&self) -> &RadrootsInboundNip22Participant {
    240         &self.author
    241     }
    242 
    243     pub fn raw_tag(&self) -> &[String] {
    244         &self.raw_tag
    245     }
    246 }
    247 
    248 #[derive(Clone, Debug, PartialEq, Eq)]
    249 pub enum RadrootsInboundNip22CommentRoot {
    250     Event(RadrootsInboundNip22EventRoot),
    251     Address(RadrootsInboundNip22AddressRoot),
    252 }
    253 
    254 impl RadrootsInboundNip22CommentRoot {
    255     pub const fn author(&self) -> &RadrootsInboundNip22Participant {
    256         match self {
    257             Self::Event(root) => root.author(),
    258             Self::Address(root) => root.author(),
    259         }
    260     }
    261 
    262     pub const fn kind(&self) -> Nip22CommentRootKind {
    263         match self {
    264             Self::Event(root) => root.kind(),
    265             Self::Address(root) => root.kind(),
    266         }
    267     }
    268 }
    269 
    270 #[derive(Clone, Debug, PartialEq, Eq)]
    271 pub struct RadrootsInboundNip22CommentParent {
    272     tag_index: usize,
    273     event_id: EventId,
    274     kind_tag_index: usize,
    275     kind_raw_tag: Vec<String>,
    276     kind: u32,
    277     relay: Option<NostrRelayHint>,
    278     author_hint: Option<PublicKey>,
    279     author: RadrootsInboundNip22Participant,
    280     raw_tag: Vec<String>,
    281 }
    282 
    283 impl RadrootsInboundNip22CommentParent {
    284     pub const fn tag_index(&self) -> usize {
    285         self.tag_index
    286     }
    287 
    288     pub const fn event_id(&self) -> &EventId {
    289         &self.event_id
    290     }
    291 
    292     pub const fn kind_tag_index(&self) -> usize {
    293         self.kind_tag_index
    294     }
    295 
    296     pub fn kind_raw_tag(&self) -> &[String] {
    297         &self.kind_raw_tag
    298     }
    299 
    300     pub const fn kind(&self) -> u32 {
    301         self.kind
    302     }
    303 
    304     pub const fn relay(&self) -> Option<&NostrRelayHint> {
    305         self.relay.as_ref()
    306     }
    307 
    308     pub const fn author_hint(&self) -> Option<&PublicKey> {
    309         self.author_hint.as_ref()
    310     }
    311 
    312     pub const fn author(&self) -> &RadrootsInboundNip22Participant {
    313         &self.author
    314     }
    315 
    316     pub fn raw_tag(&self) -> &[String] {
    317         &self.raw_tag
    318     }
    319 }
    320 
    321 #[derive(Clone, Debug, PartialEq, Eq)]
    322 pub struct RadrootsInboundNip22CurrentRevision {
    323     tag_index: usize,
    324     event_id: EventId,
    325     relay: Option<NostrRelayHint>,
    326     raw_tag: Vec<String>,
    327 }
    328 
    329 impl RadrootsInboundNip22CurrentRevision {
    330     pub const fn tag_index(&self) -> usize {
    331         self.tag_index
    332     }
    333 
    334     pub const fn event_id(&self) -> &EventId {
    335         &self.event_id
    336     }
    337 
    338     pub const fn relay(&self) -> Option<&NostrRelayHint> {
    339         self.relay.as_ref()
    340     }
    341 
    342     pub fn raw_tag(&self) -> &[String] {
    343         &self.raw_tag
    344     }
    345 }
    346 
    347 #[derive(Clone, Debug, PartialEq, Eq)]
    348 pub struct RadrootsInboundNip22TopLevelEventReference {
    349     tag_index: usize,
    350     event_id: EventId,
    351     kind_tag_index: usize,
    352     kind_raw_tag: Vec<String>,
    353     kind: u32,
    354     relay: Option<NostrRelayHint>,
    355     author_hint: Option<PublicKey>,
    356     author: RadrootsInboundNip22Participant,
    357     raw_tag: Vec<String>,
    358 }
    359 
    360 #[derive(Clone, Debug, PartialEq, Eq)]
    361 pub struct RadrootsInboundNip22TopLevelAddressReference {
    362     tag_index: usize,
    363     coordinate: AddressableCoordinate,
    364     kind_tag_index: usize,
    365     kind_raw_tag: Vec<String>,
    366     kind: u32,
    367     relay: Option<NostrRelayHint>,
    368     author: RadrootsInboundNip22Participant,
    369     raw_tag: Vec<String>,
    370 }
    371 
    372 impl RadrootsInboundNip22TopLevelAddressReference {
    373     pub const fn tag_index(&self) -> usize {
    374         self.tag_index
    375     }
    376 
    377     pub const fn coordinate(&self) -> &AddressableCoordinate {
    378         &self.coordinate
    379     }
    380 
    381     pub const fn kind_tag_index(&self) -> usize {
    382         self.kind_tag_index
    383     }
    384 
    385     pub fn kind_raw_tag(&self) -> &[String] {
    386         &self.kind_raw_tag
    387     }
    388 
    389     pub const fn kind(&self) -> u32 {
    390         self.kind
    391     }
    392 
    393     pub const fn relay(&self) -> Option<&NostrRelayHint> {
    394         self.relay.as_ref()
    395     }
    396 
    397     pub const fn author(&self) -> &RadrootsInboundNip22Participant {
    398         &self.author
    399     }
    400 
    401     pub fn raw_tag(&self) -> &[String] {
    402         &self.raw_tag
    403     }
    404 }
    405 
    406 impl RadrootsInboundNip22TopLevelEventReference {
    407     pub const fn tag_index(&self) -> usize {
    408         self.tag_index
    409     }
    410 
    411     pub const fn event_id(&self) -> &EventId {
    412         &self.event_id
    413     }
    414 
    415     pub const fn kind_tag_index(&self) -> usize {
    416         self.kind_tag_index
    417     }
    418 
    419     pub fn kind_raw_tag(&self) -> &[String] {
    420         &self.kind_raw_tag
    421     }
    422 
    423     pub const fn kind(&self) -> u32 {
    424         self.kind
    425     }
    426 
    427     pub const fn relay(&self) -> Option<&NostrRelayHint> {
    428         self.relay.as_ref()
    429     }
    430 
    431     pub const fn author_hint(&self) -> Option<&PublicKey> {
    432         self.author_hint.as_ref()
    433     }
    434 
    435     pub const fn author(&self) -> &RadrootsInboundNip22Participant {
    436         &self.author
    437     }
    438 
    439     pub fn raw_tag(&self) -> &[String] {
    440         &self.raw_tag
    441     }
    442 }
    443 
    444 #[derive(Clone, Debug, PartialEq, Eq)]
    445 pub enum RadrootsInboundNip22CommentPosition {
    446     TopLevelEvent {
    447         reference: RadrootsInboundNip22TopLevelEventReference,
    448     },
    449     TopLevelAddress {
    450         reference: RadrootsInboundNip22TopLevelAddressReference,
    451         current_revision: RadrootsInboundNip22CurrentRevision,
    452     },
    453     Nested {
    454         parent: RadrootsInboundNip22CommentParent,
    455     },
    456 }
    457 
    458 #[derive(Clone, Debug, PartialEq, Eq)]
    459 pub struct RadrootsInboundNip22CommentProjection {
    460     root: RadrootsInboundNip22CommentRoot,
    461     position: RadrootsInboundNip22CommentPosition,
    462     mentions: Vec<RadrootsInboundNip22Participant>,
    463     diagnostics: Vec<RadrootsNip22CommentDiagnostic>,
    464     raw_tags: Vec<Vec<String>>,
    465 }
    466 
    467 impl RadrootsInboundNip22CommentProjection {
    468     pub const fn root(&self) -> &RadrootsInboundNip22CommentRoot {
    469         &self.root
    470     }
    471 
    472     pub const fn position(&self) -> &RadrootsInboundNip22CommentPosition {
    473         &self.position
    474     }
    475 
    476     /// Valid distinct lowercase `p` mentions other than the selected parent
    477     /// author, in source order.
    478     pub fn mentions(&self) -> &[RadrootsInboundNip22Participant] {
    479         &self.mentions
    480     }
    481 
    482     pub const fn is_direct(&self) -> bool {
    483         !matches!(
    484             &self.position,
    485             RadrootsInboundNip22CommentPosition::Nested { .. }
    486         )
    487     }
    488 
    489     pub fn diagnostics(&self) -> &[RadrootsNip22CommentDiagnostic] {
    490         &self.diagnostics
    491     }
    492 
    493     /// Exact inbound tags, including supplemental tags, in source order.
    494     pub fn raw_tags(&self) -> &[Vec<String>] {
    495         &self.raw_tags
    496     }
    497 
    498     pub const fn contract_id(&self) -> &'static str {
    499         "radroots.social.comment.v1"
    500     }
    501 }
    502 
    503 #[non_exhaustive]
    504 #[derive(Clone, Debug, PartialEq, Eq)]
    505 pub enum RadrootsNip22CommentProjectionError {
    506     UnsupportedKind {
    507         actual: u32,
    508     },
    509     ContentMissing,
    510     ContentTooLarge {
    511         max: usize,
    512         actual: usize,
    513     },
    514     RootFormUnsupported {
    515         tag_index: usize,
    516     },
    517     ParentFormUnsupported {
    518         tag_index: usize,
    519     },
    520     RootCardinality {
    521         actual: usize,
    522     },
    523     RootKindCardinality {
    524         actual: usize,
    525     },
    526     RootAuthorCardinality {
    527         actual: usize,
    528     },
    529     ParentKindCardinality {
    530         actual: usize,
    531     },
    532     RootKindUnsupported {
    533         tag_index: usize,
    534     },
    535     RootAuthorInvalid {
    536         tag_index: usize,
    537     },
    538     RootReferenceShape {
    539         tag_index: usize,
    540     },
    541     RootEventIdInvalid {
    542         tag_index: usize,
    543         error: ParseError,
    544     },
    545     RootCoordinateInvalid {
    546         tag_index: usize,
    547     },
    548     RootKindMismatch {
    549         tag_index: usize,
    550     },
    551     RootAuthorMismatch {
    552         tag_index: usize,
    553     },
    554     ParentKindInvalid {
    555         tag_index: usize,
    556     },
    557     ParentCardinality {
    558         event_count: usize,
    559         address_count: usize,
    560     },
    561     ParentReferenceShape {
    562         tag_index: usize,
    563     },
    564     ParentEventIdInvalid {
    565         tag_index: usize,
    566         error: ParseError,
    567     },
    568     ParentCoordinateInvalid {
    569         tag_index: usize,
    570     },
    571     ParentReferenceMismatch {
    572         tag_index: usize,
    573     },
    574     RevisionMissing {
    575         actual: usize,
    576     },
    577     RevisionShape {
    578         tag_index: usize,
    579     },
    580     RevisionEventIdInvalid {
    581         tag_index: usize,
    582         error: ParseError,
    583     },
    584     ParentAuthorMissing,
    585     ParentAuthorAmbiguous,
    586     ParentAuthorMismatch,
    587     TagCountExceeded {
    588         max: usize,
    589         actual: usize,
    590     },
    591     TagElementCountExceeded {
    592         max: usize,
    593         actual: usize,
    594     },
    595     TagElementTooLarge {
    596         max: usize,
    597         actual: usize,
    598         tag_index: usize,
    599         element_index: usize,
    600     },
    601     TagBytesExceeded {
    602         max: usize,
    603         actual: usize,
    604     },
    605     EventWireTooLarge {
    606         max: usize,
    607         actual: usize,
    608     },
    609 }
    610 
    611 impl RadrootsNip22CommentProjectionError {
    612     pub const fn code(&self) -> &'static str {
    613         match self {
    614             Self::UnsupportedKind { .. } => "unsupported_kind",
    615             Self::ContentMissing => "comment_content_missing",
    616             Self::ContentTooLarge { .. } => "comment_content_too_large",
    617             Self::RootFormUnsupported { .. } => "comment_root_form_unsupported",
    618             Self::ParentFormUnsupported { .. } => "comment_parent_form_unsupported",
    619             Self::RootCardinality { .. } => "comment_root_cardinality",
    620             Self::RootKindCardinality { .. } => "comment_root_kind_cardinality",
    621             Self::RootAuthorCardinality { .. } => "comment_root_author_cardinality",
    622             Self::ParentKindCardinality { .. } => "comment_parent_kind_cardinality",
    623             Self::RootKindUnsupported { .. } => "comment_root_kind_unsupported",
    624             Self::RootAuthorInvalid { .. } => "comment_root_author_invalid",
    625             Self::RootReferenceShape { .. } => "comment_root_reference_shape",
    626             Self::RootEventIdInvalid { .. } => "comment_root_event_id_invalid",
    627             Self::RootCoordinateInvalid { .. } => "comment_root_coordinate_invalid",
    628             Self::RootKindMismatch { .. } => "comment_root_kind_mismatch",
    629             Self::RootAuthorMismatch { .. } => "comment_root_author_mismatch",
    630             Self::ParentKindInvalid { .. } => "comment_parent_kind_invalid",
    631             Self::ParentCardinality { .. } => "comment_parent_cardinality",
    632             Self::ParentReferenceShape { .. } => "comment_parent_reference_shape",
    633             Self::ParentEventIdInvalid { .. } => "comment_parent_event_id_invalid",
    634             Self::ParentCoordinateInvalid { .. } => "comment_parent_coordinate_invalid",
    635             Self::ParentReferenceMismatch { .. } => "comment_parent_reference_mismatch",
    636             Self::RevisionMissing { .. } => "comment_revision_missing",
    637             Self::RevisionShape { .. } => "comment_revision_shape",
    638             Self::RevisionEventIdInvalid { .. } => "comment_revision_event_id_invalid",
    639             Self::ParentAuthorMissing => "comment_parent_author_missing",
    640             Self::ParentAuthorAmbiguous => "comment_parent_author_ambiguous",
    641             Self::ParentAuthorMismatch => "comment_parent_author_mismatch",
    642             Self::TagCountExceeded { .. } => "comment_tag_count_exceeded",
    643             Self::TagElementCountExceeded { .. } => "comment_tag_element_count_exceeded",
    644             Self::TagElementTooLarge { .. } => "comment_tag_element_too_large",
    645             Self::TagBytesExceeded { .. } => "comment_tag_bytes_exceeded",
    646             Self::EventWireTooLarge { .. } => "comment_event_wire_too_large",
    647         }
    648     }
    649 }
    650 
    651 impl fmt::Display for RadrootsNip22CommentProjectionError {
    652     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    653         match self {
    654             Self::UnsupportedKind { actual } => {
    655                 write!(formatter, "NIP-22 comment kind must be 1111, got {actual}")
    656             }
    657             Self::ContentMissing => formatter.write_str("NIP-22 comment content is blank"),
    658             Self::ContentTooLarge { max, actual } => {
    659                 write!(
    660                     formatter,
    661                     "NIP-22 comment content is {actual} bytes; max is {max}"
    662                 )
    663             }
    664             Self::RootFormUnsupported { tag_index } => write!(
    665                 formatter,
    666                 "NIP-22 comment tag {tag_index} uses unsupported external root authority"
    667             ),
    668             Self::ParentFormUnsupported { tag_index } => write!(
    669                 formatter,
    670                 "NIP-22 comment tag {tag_index} uses unsupported external parent authority"
    671             ),
    672             Self::RootCardinality { actual } => write!(
    673                 formatter,
    674                 "NIP-22 comment has {actual} E/A root authority tags; expected exactly one"
    675             ),
    676             Self::RootKindCardinality { actual } => write!(
    677                 formatter,
    678                 "NIP-22 comment has {actual} K tags; expected exactly one"
    679             ),
    680             Self::RootAuthorCardinality { actual } => write!(
    681                 formatter,
    682                 "NIP-22 comment has {actual} P tags; expected exactly one"
    683             ),
    684             Self::ParentKindCardinality { actual } => write!(
    685                 formatter,
    686                 "NIP-22 comment has {actual} k tags; expected exactly one"
    687             ),
    688             Self::RootKindUnsupported { tag_index } => write!(
    689                 formatter,
    690                 "NIP-22 comment root kind at tag {tag_index} is unsupported or noncanonical"
    691             ),
    692             Self::RootAuthorInvalid { tag_index } => {
    693                 write!(
    694                     formatter,
    695                     "NIP-22 comment root author tag {tag_index} is invalid"
    696                 )
    697             }
    698             Self::RootReferenceShape { tag_index } => {
    699                 write!(
    700                     formatter,
    701                     "NIP-22 comment root tag {tag_index} has an invalid shape"
    702                 )
    703             }
    704             Self::RootEventIdInvalid { tag_index, error } => write!(
    705                 formatter,
    706                 "NIP-22 comment root tag {tag_index} has an invalid event id: {error}"
    707             ),
    708             Self::RootCoordinateInvalid { tag_index } => write!(
    709                 formatter,
    710                 "NIP-22 comment root tag {tag_index} has an invalid coordinate"
    711             ),
    712             Self::RootKindMismatch { tag_index } => write!(
    713                 formatter,
    714                 "NIP-22 comment root tag {tag_index} conflicts with K authority"
    715             ),
    716             Self::RootAuthorMismatch { tag_index } => write!(
    717                 formatter,
    718                 "NIP-22 comment root tag {tag_index} conflicts with P authority"
    719             ),
    720             Self::ParentKindInvalid { tag_index } => write!(
    721                 formatter,
    722                 "NIP-22 comment parent kind tag {tag_index} is invalid"
    723             ),
    724             Self::ParentCardinality {
    725                 event_count,
    726                 address_count,
    727             } => write!(
    728                 formatter,
    729                 "NIP-22 comment has {event_count} e and {address_count} a parent tags"
    730             ),
    731             Self::ParentReferenceShape { tag_index } => write!(
    732                 formatter,
    733                 "NIP-22 comment parent tag {tag_index} has an invalid shape"
    734             ),
    735             Self::ParentEventIdInvalid { tag_index, error } => write!(
    736                 formatter,
    737                 "NIP-22 comment parent tag {tag_index} has an invalid event id: {error}"
    738             ),
    739             Self::ParentCoordinateInvalid { tag_index } => write!(
    740                 formatter,
    741                 "NIP-22 comment parent tag {tag_index} has an invalid coordinate"
    742             ),
    743             Self::ParentReferenceMismatch { tag_index } => write!(
    744                 formatter,
    745                 "NIP-22 comment parent tag {tag_index} conflicts with root authority"
    746             ),
    747             Self::RevisionMissing { actual } => write!(
    748                 formatter,
    749                 "NIP-22 comment has {actual} current-revision e tags; expected exactly one"
    750             ),
    751             Self::RevisionShape { tag_index } => write!(
    752                 formatter,
    753                 "NIP-22 comment revision tag {tag_index} has an invalid shape"
    754             ),
    755             Self::RevisionEventIdInvalid { tag_index, error } => write!(
    756                 formatter,
    757                 "NIP-22 comment revision tag {tag_index} has an invalid event id: {error}"
    758             ),
    759             Self::ParentAuthorMissing => {
    760                 formatter.write_str("NIP-22 comment parent author is missing")
    761             }
    762             Self::ParentAuthorAmbiguous => {
    763                 formatter.write_str("NIP-22 comment parent author is ambiguous")
    764             }
    765             Self::ParentAuthorMismatch => {
    766                 formatter.write_str("NIP-22 comment parent author conflicts with authority")
    767             }
    768             Self::TagCountExceeded { max, actual } => {
    769                 write!(formatter, "NIP-22 comment has {actual} tags; max is {max}")
    770             }
    771             Self::TagElementCountExceeded { max, actual } => write!(
    772                 formatter,
    773                 "NIP-22 comment has {actual} total tag elements; max is {max}"
    774             ),
    775             Self::TagElementTooLarge {
    776                 max,
    777                 actual,
    778                 tag_index,
    779                 element_index,
    780             } => write!(
    781                 formatter,
    782                 "NIP-22 comment tag {tag_index} element {element_index} is {actual} bytes; max is {max}"
    783             ),
    784             Self::TagBytesExceeded { max, actual } => write!(
    785                 formatter,
    786                 "NIP-22 comment tag bytes are {actual}; max is {max}"
    787             ),
    788             Self::EventWireTooLarge { max, actual } => write!(
    789                 formatter,
    790                 "NIP-22 comment compact signed event is {actual} bytes; max is {max}"
    791             ),
    792         }
    793     }
    794 }
    795 
    796 #[cfg(feature = "std")]
    797 impl std::error::Error for RadrootsNip22CommentProjectionError {}
    798 
    799 /// Projects a signature-and-id verified kind-1111 event as a NIP-22 comment.
    800 ///
    801 /// Authority tags are order-independent. Supplemental tags and valid distinct
    802 /// lowercase `p` mentions are retained. Relay hints and event-reference author
    803 /// hints are advisory inputs: malformed values are diagnosed, while
    804 /// well-formed hints that conflict with required `P`/`p` authority fail
    805 /// projection.
    806 pub fn project_verified_nip22_comment_event(
    807     verified_event: &RadrootsSignatureVerifiedEvent,
    808 ) -> Result<RadrootsInboundNip22CommentProjection, RadrootsNip22CommentProjectionError> {
    809     project_verified_nip22_comment_event_registry_v7(verified_event)
    810 }
    811 
    812 /// Projects a verified NIP-22 comment with contract-registry-v7 semantics.
    813 pub fn project_verified_nip22_comment_event_registry_v7(
    814     verified_event: &RadrootsSignatureVerifiedEvent,
    815 ) -> Result<RadrootsInboundNip22CommentProjection, RadrootsNip22CommentProjectionError> {
    816     let event = verified_event.event();
    817     project_nip22_comment_parts(
    818         event.kind_u32(),
    819         &event.tags_as_vec(),
    820         event.content(),
    821         decimal_digits(event.created_at_u64()),
    822     )
    823 }
    824 
    825 pub(crate) fn project_nip22_comment_parts(
    826     kind: u32,
    827     tags: &[Vec<String>],
    828     content: &str,
    829     created_at_digits: usize,
    830 ) -> Result<RadrootsInboundNip22CommentProjection, RadrootsNip22CommentProjectionError> {
    831     if kind != KIND_COMMENT {
    832         return Err(RadrootsNip22CommentProjectionError::UnsupportedKind { actual: kind });
    833     }
    834     if content.chars().all(char::is_whitespace) {
    835         return Err(RadrootsNip22CommentProjectionError::ContentMissing);
    836     }
    837     if content.len() > RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES {
    838         return Err(RadrootsNip22CommentProjectionError::ContentTooLarge {
    839             max: RADROOTS_NIP22_COMMENT_CONTENT_MAX_BYTES,
    840             actual: content.len(),
    841         });
    842     }
    843     validate_tag_and_wire_budgets(tags, content, created_at_digits)?;
    844 
    845     if let Some((tag_index, _)) = find_tags(tags, "I").first() {
    846         return Err(RadrootsNip22CommentProjectionError::RootFormUnsupported {
    847             tag_index: *tag_index,
    848         });
    849     }
    850     if let Some((tag_index, _)) = find_tags(tags, "i").first() {
    851         return Err(RadrootsNip22CommentProjectionError::ParentFormUnsupported {
    852             tag_index: *tag_index,
    853         });
    854     }
    855     let event_roots = find_tags(tags, "E");
    856     let address_roots = find_tags(tags, "A");
    857     let root_count = event_roots.len() + address_roots.len();
    858     if root_count != 1 {
    859         return Err(RadrootsNip22CommentProjectionError::RootCardinality { actual: root_count });
    860     }
    861     let kind_tag = exactly_one_authority_tag(tags, "K", |actual| {
    862         RadrootsNip22CommentProjectionError::RootKindCardinality { actual }
    863     })?;
    864     let root_author_tag = exactly_one_authority_tag(tags, "P", |actual| {
    865         RadrootsNip22CommentProjectionError::RootAuthorCardinality { actual }
    866     })?;
    867     let parent_kind_tag = exactly_one_authority_tag(tags, "k", |actual| {
    868         RadrootsNip22CommentProjectionError::ParentKindCardinality { actual }
    869     })?;
    870 
    871     let mut diagnostics = Vec::new();
    872     let root_kind = parse_root_kind(kind_tag)?;
    873     let root_author = parse_required_root_author(root_author_tag, &mut diagnostics)?;
    874     let participants = project_participants(tags, &mut diagnostics);
    875     let parent_event_tags = find_tags(tags, "e");
    876     let parent_address_tags = find_tags(tags, "a");
    877 
    878     let (root, position, mut mentions) = if let Some(root_tag) = event_roots.first().copied() {
    879         let event_reference =
    880             parse_event_reference(root_tag, ReferenceRole::Root, &mut diagnostics)?;
    881         if event_reference
    882             .author_hint
    883             .as_ref()
    884             .is_some_and(|author_hint| author_hint != root_author.pubkey())
    885         {
    886             return Err(RadrootsNip22CommentProjectionError::RootAuthorMismatch {
    887                 tag_index: root_tag.0,
    888             });
    889         }
    890         let root = RadrootsInboundNip22EventRoot {
    891             tag_index: root_tag.0,
    892             event_id: event_reference.event_id,
    893             kind_tag_index: kind_tag.0,
    894             kind_raw_tag: kind_tag.1.clone(),
    895             kind: root_kind,
    896             relay: event_reference.relay,
    897             author_hint: event_reference.author_hint,
    898             author: root_author.clone(),
    899             raw_tag: root_tag.1.clone(),
    900         };
    901         let parent_kind = parse_parent_kind(parent_kind_tag)?;
    902         if parent_kind != root_kind.as_u32() && parent_kind != KIND_COMMENT {
    903             return Err(RadrootsNip22CommentProjectionError::ParentKindInvalid {
    904                 tag_index: parent_kind_tag.0,
    905             });
    906         }
    907 
    908         if parent_kind == root_kind.as_u32() {
    909             require_parent_cardinality(&parent_event_tags, &parent_address_tags, 1, 0)?;
    910             let parent_event_tag = parent_event_tags[0];
    911             let parent =
    912                 parse_event_reference(parent_event_tag, ReferenceRole::Parent, &mut diagnostics)?;
    913             if parent.event_id != root.event_id {
    914                 return Err(
    915                     RadrootsNip22CommentProjectionError::ParentReferenceMismatch {
    916                         tag_index: parent_event_tag.0,
    917                     },
    918                 );
    919             }
    920             let (selected_author, mentions) = select_direct_parent_author(
    921                 participants,
    922                 root.author.pubkey(),
    923                 parent.author_hint.as_ref(),
    924             )?;
    925             let position = RadrootsInboundNip22CommentPosition::TopLevelEvent {
    926                 reference: RadrootsInboundNip22TopLevelEventReference {
    927                     tag_index: parent_event_tag.0,
    928                     event_id: parent.event_id,
    929                     kind_tag_index: parent_kind_tag.0,
    930                     kind_raw_tag: parent_kind_tag.1.clone(),
    931                     kind: parent_kind,
    932                     relay: parent.relay,
    933                     author_hint: parent.author_hint,
    934                     author: selected_author,
    935                     raw_tag: parent_event_tag.1.clone(),
    936                 },
    937             };
    938             (
    939                 RadrootsInboundNip22CommentRoot::Event(root),
    940                 position,
    941                 mentions,
    942             )
    943         } else {
    944             require_parent_cardinality(&parent_event_tags, &parent_address_tags, 1, 0)?;
    945             let parent_event_tag = parent_event_tags[0];
    946             let parent_reference =
    947                 parse_event_reference(parent_event_tag, ReferenceRole::Parent, &mut diagnostics)?;
    948             if parent_reference.event_id == root.event_id {
    949                 return Err(
    950                     RadrootsNip22CommentProjectionError::ParentReferenceMismatch {
    951                         tag_index: parent_event_tag.0,
    952                     },
    953                 );
    954             }
    955             let (selected_author, mentions) =
    956                 select_nested_parent_author(participants, parent_reference.author_hint.as_ref())?;
    957             let parent = RadrootsInboundNip22CommentParent {
    958                 tag_index: parent_event_tag.0,
    959                 event_id: parent_reference.event_id,
    960                 kind_tag_index: parent_kind_tag.0,
    961                 kind_raw_tag: parent_kind_tag.1.clone(),
    962                 kind: parent_kind,
    963                 relay: parent_reference.relay,
    964                 author_hint: parent_reference.author_hint,
    965                 author: selected_author,
    966                 raw_tag: parent_event_tag.1.clone(),
    967             };
    968             (
    969                 RadrootsInboundNip22CommentRoot::Event(root),
    970                 RadrootsInboundNip22CommentPosition::Nested { parent },
    971                 mentions,
    972             )
    973         }
    974     } else {
    975         let root_tag = address_roots[0];
    976         let address_reference =
    977             parse_address_reference(root_tag, ReferenceRole::Root, &mut diagnostics)?;
    978         if address_reference.kind != root_kind {
    979             return Err(RadrootsNip22CommentProjectionError::RootKindMismatch {
    980                 tag_index: root_tag.0,
    981             });
    982         }
    983         if address_reference.author != *root_author.pubkey() {
    984             return Err(RadrootsNip22CommentProjectionError::RootAuthorMismatch {
    985                 tag_index: root_tag.0,
    986             });
    987         }
    988         let root = RadrootsInboundNip22AddressRoot {
    989             tag_index: root_tag.0,
    990             coordinate: address_reference.coordinate,
    991             kind_tag_index: kind_tag.0,
    992             kind_raw_tag: kind_tag.1.clone(),
    993             kind: root_kind,
    994             relay: address_reference.relay,
    995             author: root_author.clone(),
    996             raw_tag: root_tag.1.clone(),
    997         };
    998         let parent_kind = parse_parent_kind(parent_kind_tag)?;
    999         if parent_kind != root_kind.as_u32() && parent_kind != KIND_COMMENT {
   1000             return Err(RadrootsNip22CommentProjectionError::ParentKindInvalid {
   1001                 tag_index: parent_kind_tag.0,
   1002             });
   1003         }
   1004 
   1005         if parent_kind == root_kind.as_u32() {
   1006             if parent_address_tags.len() != 1 {
   1007                 return Err(RadrootsNip22CommentProjectionError::ParentCardinality {
   1008                     event_count: parent_event_tags.len(),
   1009                     address_count: parent_address_tags.len(),
   1010                 });
   1011             }
   1012             if parent_event_tags.len() != 1 {
   1013                 return Err(RadrootsNip22CommentProjectionError::RevisionMissing {
   1014                     actual: parent_event_tags.len(),
   1015                 });
   1016             }
   1017             let parent_address = parse_address_reference(
   1018                 parent_address_tags[0],
   1019                 ReferenceRole::Parent,
   1020                 &mut diagnostics,
   1021             )?;
   1022             if parent_address.coordinate != root.coordinate {
   1023                 return Err(
   1024                     RadrootsNip22CommentProjectionError::ParentReferenceMismatch {
   1025                         tag_index: parent_address_tags[0].0,
   1026                     },
   1027                 );
   1028             }
   1029             let current_revision = parse_current_revision(parent_event_tags[0], &mut diagnostics)?;
   1030             let (selected_author, mentions) =
   1031                 select_direct_parent_author(participants, root.author.pubkey(), None)?;
   1032             let position = RadrootsInboundNip22CommentPosition::TopLevelAddress {
   1033                 reference: RadrootsInboundNip22TopLevelAddressReference {
   1034                     tag_index: parent_address_tags[0].0,
   1035                     coordinate: parent_address.coordinate,
   1036                     kind_tag_index: parent_kind_tag.0,
   1037                     kind_raw_tag: parent_kind_tag.1.clone(),
   1038                     kind: parent_kind,
   1039                     relay: parent_address.relay,
   1040                     author: selected_author,
   1041                     raw_tag: parent_address_tags[0].1.clone(),
   1042                 },
   1043                 current_revision,
   1044             };
   1045             (
   1046                 RadrootsInboundNip22CommentRoot::Address(root),
   1047                 position,
   1048                 mentions,
   1049             )
   1050         } else {
   1051             require_parent_cardinality(&parent_event_tags, &parent_address_tags, 1, 0)?;
   1052             let parent_event_tag = parent_event_tags[0];
   1053             let parent_reference =
   1054                 parse_event_reference(parent_event_tag, ReferenceRole::Parent, &mut diagnostics)?;
   1055             let (selected_author, mentions) =
   1056                 select_nested_parent_author(participants, parent_reference.author_hint.as_ref())?;
   1057             let parent = RadrootsInboundNip22CommentParent {
   1058                 tag_index: parent_event_tag.0,
   1059                 event_id: parent_reference.event_id,
   1060                 kind_tag_index: parent_kind_tag.0,
   1061                 kind_raw_tag: parent_kind_tag.1.clone(),
   1062                 kind: parent_kind,
   1063                 relay: parent_reference.relay,
   1064                 author_hint: parent_reference.author_hint,
   1065                 author: selected_author,
   1066                 raw_tag: parent_event_tag.1.clone(),
   1067             };
   1068             (
   1069                 RadrootsInboundNip22CommentRoot::Address(root),
   1070                 RadrootsInboundNip22CommentPosition::Nested { parent },
   1071                 mentions,
   1072             )
   1073         }
   1074     };
   1075 
   1076     mentions.sort_by_key(RadrootsInboundNip22Participant::tag_index);
   1077     diagnostics.sort_by_key(RadrootsNip22CommentDiagnostic::tag_index);
   1078 
   1079     Ok(RadrootsInboundNip22CommentProjection {
   1080         root,
   1081         position,
   1082         mentions,
   1083         diagnostics,
   1084         raw_tags: tags.to_vec(),
   1085     })
   1086 }
   1087 
   1088 type IndexedTag<'a> = (usize, &'a Vec<String>);
   1089 
   1090 struct ParsedEventReference {
   1091     event_id: EventId,
   1092     relay: Option<NostrRelayHint>,
   1093     author_hint: Option<PublicKey>,
   1094 }
   1095 
   1096 struct ParsedAddressReference {
   1097     coordinate: AddressableCoordinate,
   1098     author: PublicKey,
   1099     kind: Nip22CommentRootKind,
   1100     relay: Option<NostrRelayHint>,
   1101 }
   1102 
   1103 #[derive(Clone, Copy)]
   1104 enum ReferenceRole {
   1105     Root,
   1106     Parent,
   1107 }
   1108 
   1109 impl ReferenceRole {
   1110     const fn relay_role(self) -> RelayRole {
   1111         match self {
   1112             Self::Root => RelayRole::Root,
   1113             Self::Parent => RelayRole::Parent,
   1114         }
   1115     }
   1116 
   1117     fn author_hint_diagnostic(
   1118         self,
   1119         tag_index: usize,
   1120         raw_tag: Vec<String>,
   1121     ) -> RadrootsNip22CommentDiagnostic {
   1122         match self {
   1123             Self::Root => {
   1124                 RadrootsNip22CommentDiagnostic::RootAuthorHintIgnored { tag_index, raw_tag }
   1125             }
   1126             Self::Parent => {
   1127                 RadrootsNip22CommentDiagnostic::ParentAuthorHintIgnored { tag_index, raw_tag }
   1128             }
   1129         }
   1130     }
   1131 
   1132     const fn reference_shape_error(self, tag_index: usize) -> RadrootsNip22CommentProjectionError {
   1133         match self {
   1134             Self::Root => RadrootsNip22CommentProjectionError::RootReferenceShape { tag_index },
   1135             Self::Parent => RadrootsNip22CommentProjectionError::ParentReferenceShape { tag_index },
   1136         }
   1137     }
   1138 
   1139     const fn event_id_error(
   1140         self,
   1141         tag_index: usize,
   1142         error: ParseError,
   1143     ) -> RadrootsNip22CommentProjectionError {
   1144         match self {
   1145             Self::Root => {
   1146                 RadrootsNip22CommentProjectionError::RootEventIdInvalid { tag_index, error }
   1147             }
   1148             Self::Parent => {
   1149                 RadrootsNip22CommentProjectionError::ParentEventIdInvalid { tag_index, error }
   1150             }
   1151         }
   1152     }
   1153 
   1154     const fn coordinate_error(self, tag_index: usize) -> RadrootsNip22CommentProjectionError {
   1155         match self {
   1156             Self::Root => RadrootsNip22CommentProjectionError::RootCoordinateInvalid { tag_index },
   1157             Self::Parent => {
   1158                 RadrootsNip22CommentProjectionError::ParentCoordinateInvalid { tag_index }
   1159             }
   1160         }
   1161     }
   1162 }
   1163 
   1164 #[derive(Clone, Copy)]
   1165 enum RelayRole {
   1166     Root,
   1167     RootAuthor,
   1168     Parent,
   1169     ParentAuthor,
   1170     Revision,
   1171 }
   1172 
   1173 impl RelayRole {
   1174     fn diagnostic(self, tag_index: usize, raw_tag: Vec<String>) -> RadrootsNip22CommentDiagnostic {
   1175         match self {
   1176             Self::Root => RadrootsNip22CommentDiagnostic::RootRelayIgnored { tag_index, raw_tag },
   1177             Self::RootAuthor => {
   1178                 RadrootsNip22CommentDiagnostic::RootAuthorRelayIgnored { tag_index, raw_tag }
   1179             }
   1180             Self::Parent => {
   1181                 RadrootsNip22CommentDiagnostic::ParentRelayIgnored { tag_index, raw_tag }
   1182             }
   1183             Self::ParentAuthor => {
   1184                 RadrootsNip22CommentDiagnostic::ParentAuthorRelayIgnored { tag_index, raw_tag }
   1185             }
   1186             Self::Revision => {
   1187                 RadrootsNip22CommentDiagnostic::RevisionRelayIgnored { tag_index, raw_tag }
   1188             }
   1189         }
   1190     }
   1191 }
   1192 
   1193 fn find_tags<'a>(tags: &'a [Vec<String>], name: &str) -> Vec<IndexedTag<'a>> {
   1194     tags.iter()
   1195         .enumerate()
   1196         .filter(|(_, tag)| tag.first().is_some_and(|actual| actual == name))
   1197         .collect()
   1198 }
   1199 
   1200 fn exactly_one_authority_tag<'a>(
   1201     tags: &'a [Vec<String>],
   1202     name: &str,
   1203     error: impl FnOnce(usize) -> RadrootsNip22CommentProjectionError,
   1204 ) -> Result<IndexedTag<'a>, RadrootsNip22CommentProjectionError> {
   1205     let matches = find_tags(tags, name);
   1206     match matches.as_slice() {
   1207         [tag] => Ok(*tag),
   1208         _ => Err(error(matches.len())),
   1209     }
   1210 }
   1211 
   1212 fn parse_root_kind(
   1213     (tag_index, tag): IndexedTag<'_>,
   1214 ) -> Result<Nip22CommentRootKind, RadrootsNip22CommentProjectionError> {
   1215     let kind = tag
   1216         .get(1)
   1217         .filter(|_| tag.len() == 2)
   1218         .and_then(|value| parse_canonical_kind_token(value))
   1219         .ok_or(RadrootsNip22CommentProjectionError::RootKindUnsupported { tag_index })?;
   1220     Nip22CommentRootKind::parse(kind)
   1221         .map_err(|_| RadrootsNip22CommentProjectionError::RootKindUnsupported { tag_index })
   1222 }
   1223 
   1224 fn parse_parent_kind(
   1225     (tag_index, tag): IndexedTag<'_>,
   1226 ) -> Result<u32, RadrootsNip22CommentProjectionError> {
   1227     let kind = tag
   1228         .get(1)
   1229         .filter(|_| tag.len() == 2)
   1230         .and_then(|value| parse_canonical_kind_token(value))
   1231         .ok_or(RadrootsNip22CommentProjectionError::ParentKindInvalid { tag_index })?;
   1232     if kind == KIND_COMMENT || Nip22CommentRootKind::parse(kind).is_ok() {
   1233         Ok(kind)
   1234     } else {
   1235         Err(RadrootsNip22CommentProjectionError::ParentKindInvalid { tag_index })
   1236     }
   1237 }
   1238 
   1239 fn parse_canonical_kind_token(value: &str) -> Option<u32> {
   1240     if value.is_empty()
   1241         || !value.bytes().all(|byte| byte.is_ascii_digit())
   1242         || value.len() > 1 && value.starts_with('0')
   1243     {
   1244         return None;
   1245     }
   1246     value.parse().ok()
   1247 }
   1248 
   1249 fn parse_required_root_author(
   1250     (tag_index, tag): IndexedTag<'_>,
   1251     diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>,
   1252 ) -> Result<RadrootsInboundNip22Participant, RadrootsNip22CommentProjectionError> {
   1253     if !matches!(tag.len(), 2 | 3) {
   1254         return Err(RadrootsNip22CommentProjectionError::RootAuthorInvalid { tag_index });
   1255     }
   1256     let pubkey = PublicKey::from_hex(&tag[1])
   1257         .map_err(|_| RadrootsNip22CommentProjectionError::RootAuthorInvalid { tag_index })?;
   1258     let relay = project_relay(tag_index, tag, 2, RelayRole::RootAuthor, diagnostics);
   1259     Ok(RadrootsInboundNip22Participant {
   1260         tag_index,
   1261         pubkey,
   1262         relay,
   1263         raw_tag: tag.clone(),
   1264     })
   1265 }
   1266 
   1267 fn project_participants(
   1268     tags: &[Vec<String>],
   1269     diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>,
   1270 ) -> Vec<RadrootsInboundNip22Participant> {
   1271     let mut participants = Vec::new();
   1272     for (tag_index, tag) in find_tags(tags, "p") {
   1273         if !matches!(tag.len(), 2 | 3) {
   1274             diagnostics.push(RadrootsNip22CommentDiagnostic::ParentAuthorShapeIgnored {
   1275                 tag_index,
   1276                 raw_tag: tag.clone(),
   1277             });
   1278             continue;
   1279         }
   1280         let Ok(pubkey) = PublicKey::from_hex(&tag[1]) else {
   1281             diagnostics.push(RadrootsNip22CommentDiagnostic::ParentAuthorInvalidIgnored {
   1282                 tag_index,
   1283                 raw_tag: tag.clone(),
   1284             });
   1285             continue;
   1286         };
   1287         let relay = project_relay(tag_index, tag, 2, RelayRole::ParentAuthor, diagnostics);
   1288         if participants
   1289             .iter()
   1290             .any(|participant: &RadrootsInboundNip22Participant| participant.pubkey == pubkey)
   1291         {
   1292             diagnostics.push(
   1293                 RadrootsNip22CommentDiagnostic::ParentAuthorDuplicateIgnored {
   1294                     tag_index,
   1295                     raw_tag: tag.clone(),
   1296                 },
   1297             );
   1298             continue;
   1299         }
   1300         participants.push(RadrootsInboundNip22Participant {
   1301             tag_index,
   1302             pubkey,
   1303             relay,
   1304             raw_tag: tag.clone(),
   1305         });
   1306     }
   1307     participants
   1308 }
   1309 
   1310 fn parse_event_reference(
   1311     (tag_index, tag): IndexedTag<'_>,
   1312     role: ReferenceRole,
   1313     diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>,
   1314 ) -> Result<ParsedEventReference, RadrootsNip22CommentProjectionError> {
   1315     if !(2..=4).contains(&tag.len()) {
   1316         return Err(role.reference_shape_error(tag_index));
   1317     }
   1318     let event_id =
   1319         EventId::parse(&tag[1]).map_err(|error| role.event_id_error(tag_index, error))?;
   1320     let relay = project_relay(tag_index, tag, 2, role.relay_role(), diagnostics);
   1321     let author_hint = tag
   1322         .get(3)
   1323         .and_then(|value| match PublicKey::from_hex(value) {
   1324             Ok(author) => Some(author),
   1325             Err(_) => {
   1326                 diagnostics.push(role.author_hint_diagnostic(tag_index, tag.clone()));
   1327                 None
   1328             }
   1329         });
   1330     Ok(ParsedEventReference {
   1331         event_id,
   1332         relay,
   1333         author_hint,
   1334     })
   1335 }
   1336 
   1337 fn parse_address_reference(
   1338     (tag_index, tag): IndexedTag<'_>,
   1339     role: ReferenceRole,
   1340     diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>,
   1341 ) -> Result<ParsedAddressReference, RadrootsNip22CommentProjectionError> {
   1342     if !matches!(tag.len(), 2 | 3) {
   1343         return Err(role.reference_shape_error(tag_index));
   1344     }
   1345     let canonical_kind = tag[1]
   1346         .split_once(':')
   1347         .and_then(|(kind, _)| parse_canonical_kind_token(kind))
   1348         .ok_or_else(|| role.coordinate_error(tag_index))?;
   1349     let parts =
   1350         AddressableCoordinateParts::parse(&tag[1]).map_err(|_| role.coordinate_error(tag_index))?;
   1351     if parts.kind != canonical_kind {
   1352         return Err(role.coordinate_error(tag_index));
   1353     }
   1354     let kind =
   1355         Nip22CommentRootKind::parse(parts.kind).map_err(|_| role.coordinate_error(tag_index))?;
   1356     let coordinate = AddressableCoordinate::parse(format_coordinate(&parts))
   1357         .map_err(|_| role.coordinate_error(tag_index))?;
   1358     let relay = project_relay(tag_index, tag, 2, role.relay_role(), diagnostics);
   1359     Ok(ParsedAddressReference {
   1360         coordinate,
   1361         author: parts.pubkey,
   1362         kind,
   1363         relay,
   1364     })
   1365 }
   1366 
   1367 fn format_coordinate(parts: &AddressableCoordinateParts) -> String {
   1368     format!("{}:{}:{}", parts.kind, parts.pubkey, parts.d_tag)
   1369 }
   1370 
   1371 fn parse_current_revision(
   1372     (tag_index, tag): IndexedTag<'_>,
   1373     diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>,
   1374 ) -> Result<RadrootsInboundNip22CurrentRevision, RadrootsNip22CommentProjectionError> {
   1375     if !matches!(tag.len(), 2 | 3) {
   1376         return Err(RadrootsNip22CommentProjectionError::RevisionShape { tag_index });
   1377     }
   1378     let event_id = EventId::parse(&tag[1]).map_err(|error| {
   1379         RadrootsNip22CommentProjectionError::RevisionEventIdInvalid { tag_index, error }
   1380     })?;
   1381     let relay = project_relay(tag_index, tag, 2, RelayRole::Revision, diagnostics);
   1382     Ok(RadrootsInboundNip22CurrentRevision {
   1383         tag_index,
   1384         event_id,
   1385         relay,
   1386         raw_tag: tag.clone(),
   1387     })
   1388 }
   1389 
   1390 fn require_parent_cardinality(
   1391     event_tags: &[IndexedTag<'_>],
   1392     address_tags: &[IndexedTag<'_>],
   1393     expected_event_count: usize,
   1394     expected_address_count: usize,
   1395 ) -> Result<(), RadrootsNip22CommentProjectionError> {
   1396     if event_tags.len() == expected_event_count && address_tags.len() == expected_address_count {
   1397         Ok(())
   1398     } else {
   1399         Err(RadrootsNip22CommentProjectionError::ParentCardinality {
   1400             event_count: event_tags.len(),
   1401             address_count: address_tags.len(),
   1402         })
   1403     }
   1404 }
   1405 
   1406 fn select_direct_parent_author(
   1407     mut participants: Vec<RadrootsInboundNip22Participant>,
   1408     expected_author: &PublicKey,
   1409     author_hint: Option<&PublicKey>,
   1410 ) -> Result<
   1411     (
   1412         RadrootsInboundNip22Participant,
   1413         Vec<RadrootsInboundNip22Participant>,
   1414     ),
   1415     RadrootsNip22CommentProjectionError,
   1416 > {
   1417     if participants.is_empty() {
   1418         return Err(RadrootsNip22CommentProjectionError::ParentAuthorMissing);
   1419     }
   1420     if author_hint.is_some_and(|hint| hint != expected_author) {
   1421         return Err(RadrootsNip22CommentProjectionError::ParentAuthorMismatch);
   1422     }
   1423     let Some(selected_index) = participants
   1424         .iter()
   1425         .position(|participant| participant.pubkey() == expected_author)
   1426     else {
   1427         return Err(RadrootsNip22CommentProjectionError::ParentAuthorMismatch);
   1428     };
   1429     let selected = participants.remove(selected_index);
   1430     Ok((selected, participants))
   1431 }
   1432 
   1433 fn select_nested_parent_author(
   1434     mut participants: Vec<RadrootsInboundNip22Participant>,
   1435     author_hint: Option<&PublicKey>,
   1436 ) -> Result<
   1437     (
   1438         RadrootsInboundNip22Participant,
   1439         Vec<RadrootsInboundNip22Participant>,
   1440     ),
   1441     RadrootsNip22CommentProjectionError,
   1442 > {
   1443     if participants.is_empty() {
   1444         return Err(RadrootsNip22CommentProjectionError::ParentAuthorMissing);
   1445     }
   1446     let selected_index = if let Some(author_hint) = author_hint {
   1447         participants
   1448             .iter()
   1449             .position(|participant| participant.pubkey() == author_hint)
   1450             .ok_or(RadrootsNip22CommentProjectionError::ParentAuthorMismatch)?
   1451     } else {
   1452         if participants.len() > 1 {
   1453             return Err(RadrootsNip22CommentProjectionError::ParentAuthorAmbiguous);
   1454         }
   1455         0
   1456     };
   1457     let selected = participants.remove(selected_index);
   1458     Ok((selected, participants))
   1459 }
   1460 
   1461 fn project_relay(
   1462     tag_index: usize,
   1463     tag: &[String],
   1464     element_index: usize,
   1465     role: RelayRole,
   1466     diagnostics: &mut Vec<RadrootsNip22CommentDiagnostic>,
   1467 ) -> Option<NostrRelayHint> {
   1468     let value = tag.get(element_index)?;
   1469     if value.is_empty() {
   1470         return None;
   1471     }
   1472     match NostrRelayHint::parse(value) {
   1473         Ok(relay) => Some(relay),
   1474         Err(_) => {
   1475             diagnostics.push(role.diagnostic(tag_index, tag.to_vec()));
   1476             None
   1477         }
   1478     }
   1479 }
   1480 
   1481 fn validate_tag_and_wire_budgets(
   1482     tags: &[Vec<String>],
   1483     content: &str,
   1484     created_at_digits: usize,
   1485 ) -> Result<(), RadrootsNip22CommentProjectionError> {
   1486     if tags.len() > RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT {
   1487         return Err(RadrootsNip22CommentProjectionError::TagCountExceeded {
   1488             max: RADROOTS_NIP22_COMMENT_TAG_MAX_COUNT,
   1489             actual: tags.len(),
   1490         });
   1491     }
   1492     let tag_element_count = tags
   1493         .iter()
   1494         .fold(0usize, |total, tag| total.saturating_add(tag.len()));
   1495     if tag_element_count > RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT {
   1496         return Err(
   1497             RadrootsNip22CommentProjectionError::TagElementCountExceeded {
   1498                 max: RADROOTS_NIP22_COMMENT_TAG_TOTAL_ELEMENT_MAX_COUNT,
   1499                 actual: tag_element_count,
   1500             },
   1501         );
   1502     }
   1503     let mut tag_bytes = 0usize;
   1504     let mut tags_json_bytes = 2usize;
   1505     for (tag_index, tag) in tags.iter().enumerate() {
   1506         if tag_index > 0 {
   1507             tags_json_bytes = tags_json_bytes.saturating_add(1);
   1508         }
   1509         tags_json_bytes = tags_json_bytes.saturating_add(2);
   1510         for (element_index, element) in tag.iter().enumerate() {
   1511             if element.len() > RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES {
   1512                 return Err(RadrootsNip22CommentProjectionError::TagElementTooLarge {
   1513                     max: RADROOTS_NIP22_COMMENT_TAG_ELEMENT_MAX_BYTES,
   1514                     actual: element.len(),
   1515                     tag_index,
   1516                     element_index,
   1517                 });
   1518             }
   1519             if element_index > 0 {
   1520                 tags_json_bytes = tags_json_bytes.saturating_add(1);
   1521             }
   1522             tags_json_bytes = tags_json_bytes.saturating_add(canonical_json_string_bytes(element));
   1523             tag_bytes = tag_bytes.saturating_add(element.len());
   1524         }
   1525     }
   1526     if tag_bytes > RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES {
   1527         return Err(RadrootsNip22CommentProjectionError::TagBytesExceeded {
   1528             max: RADROOTS_NIP22_COMMENT_TAG_TOTAL_MAX_BYTES,
   1529             actual: tag_bytes,
   1530         });
   1531     }
   1532     let actual = RADROOTS_NIP22_COMMENT_SIGNED_EVENT_FIXED_MAX_BYTES
   1533         .saturating_add(created_at_digits)
   1534         .saturating_add(tags_json_bytes)
   1535         .saturating_add(canonical_json_string_bytes(content));
   1536     if actual > RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES {
   1537         return Err(RadrootsNip22CommentProjectionError::EventWireTooLarge {
   1538             max: RADROOTS_NIP22_COMMENT_EVENT_WIRE_MAX_BYTES,
   1539             actual,
   1540         });
   1541     }
   1542     Ok(())
   1543 }
   1544 
   1545 fn canonical_json_string_bytes(value: &str) -> usize {
   1546     value.chars().fold(2usize, |total, character| {
   1547         total.saturating_add(match character {
   1548             '"' | '\\' | '\u{0008}' | '\t' | '\n' | '\u{000c}' | '\r' => 2,
   1549             '\u{0000}'..='\u{001f}' => 6,
   1550             _ => character.len_utf8(),
   1551         })
   1552     })
   1553 }
   1554 
   1555 const fn decimal_digits(mut value: u64) -> usize {
   1556     let mut digits = 1usize;
   1557     while value >= 10 {
   1558         value /= 10;
   1559         digits += 1;
   1560     }
   1561     digits
   1562 }
   1563 
   1564 #[cfg(test)]
   1565 mod tests;