lib

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

v1.rs (6894B)


      1 #![forbid(unsafe_code)]
      2 
      3 //! Frozen addressable-feed-v1 head-selection semantics.
      4 
      5 #[cfg(not(feature = "std"))]
      6 use alloc::string::String;
      7 
      8 use crate::contract::registry_v7::{
      9     ContractMatchError, EventClass, EventContract, identify_event_contract,
     10 };
     11 use crate::envelope::{EventEnvelope, EventKindClass, EventTag};
     12 use crate::id::{DTag, EventId, ParseError};
     13 use crate::tag::name::TAG_D;
     14 use radroots_identity::PublicKey;
     15 
     16 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     17 pub enum EventHeadCoordinate {
     18     Replaceable {
     19         kind: u32,
     20         pubkey: PublicKey,
     21     },
     22     Addressable {
     23         kind: u32,
     24         pubkey: PublicKey,
     25         d_tag: String,
     26     },
     27 }
     28 
     29 #[derive(Clone, Debug, PartialEq, Eq)]
     30 pub struct EventHeadCandidate {
     31     pub coordinate: EventHeadCoordinate,
     32     pub event_id: EventId,
     33     pub created_at: u64,
     34 }
     35 
     36 #[derive(Clone, Debug, PartialEq, Eq)]
     37 pub struct CurrentEventHead {
     38     pub coordinate: EventHeadCoordinate,
     39     pub event_id: EventId,
     40     pub created_at: u64,
     41 }
     42 
     43 impl From<EventHeadCandidate> for CurrentEventHead {
     44     fn from(candidate: EventHeadCandidate) -> Self {
     45         Self {
     46             coordinate: candidate.coordinate,
     47             event_id: candidate.event_id,
     48             created_at: candidate.created_at,
     49         }
     50     }
     51 }
     52 
     53 #[derive(Clone, Debug, PartialEq, Eq)]
     54 pub enum EventHeadMalformed {
     55     InvalidEventId(ParseError),
     56     InvalidPubkey(ParseError),
     57     MissingDTag,
     58     InvalidDTag(ParseError),
     59 }
     60 
     61 #[derive(Clone, Debug, PartialEq, Eq)]
     62 pub enum EventHeadCandidateResult {
     63     Candidate(EventHeadCandidate),
     64     NotHeadSelected,
     65     NotPersisted,
     66     Malformed(EventHeadMalformed),
     67 }
     68 
     69 #[derive(Clone, Debug, PartialEq, Eq)]
     70 pub enum EventHeadDecision {
     71     Applied(CurrentEventHead),
     72     SkippedDuplicate,
     73     SkippedOlder,
     74     SkippedSameTimestampHigherEventId,
     75     CoordinateMismatch,
     76 }
     77 
     78 pub fn event_head_candidate_for_class(
     79     event: &EventEnvelope,
     80     class: EventClass,
     81 ) -> EventHeadCandidateResult {
     82     match class {
     83         EventClass::Regular => EventHeadCandidateResult::NotHeadSelected,
     84         EventClass::Ephemeral => EventHeadCandidateResult::NotPersisted,
     85         EventClass::Replaceable | EventClass::Addressable => {
     86             let event_id = *event.id();
     87             let pubkey = *event.author();
     88             let coordinate = if class == EventClass::Replaceable {
     89                 EventHeadCoordinate::Replaceable {
     90                     kind: event.kind_u32(),
     91                     pubkey,
     92                 }
     93             } else {
     94                 let Some(d_tag) = first_tag_value(event.tag_slices(), TAG_D) else {
     95                     return EventHeadCandidateResult::Malformed(EventHeadMalformed::MissingDTag);
     96                 };
     97                 let d_tag = match DTag::parse(d_tag) {
     98                     Ok(d_tag) => d_tag,
     99                     Err(error) => {
    100                         return EventHeadCandidateResult::Malformed(
    101                             EventHeadMalformed::InvalidDTag(error),
    102                         );
    103                     }
    104                 };
    105                 EventHeadCoordinate::Addressable {
    106                     kind: event.kind_u32(),
    107                     pubkey,
    108                     d_tag: d_tag.into_string(),
    109                 }
    110             };
    111             EventHeadCandidateResult::Candidate(EventHeadCandidate {
    112                 coordinate,
    113                 event_id,
    114                 created_at: event.created_at_u64(),
    115             })
    116         }
    117     }
    118 }
    119 
    120 /// Derives the raw NIP-01 head candidate from the numeric event-kind class.
    121 ///
    122 /// This deliberately does not identify or validate a Radroots product
    123 /// contract. Raw replacement ordering must include every signature-verified
    124 /// replaceable or addressable event, including unsupported product shapes.
    125 pub fn event_head_candidate_for_nip01_event(event: &EventEnvelope) -> EventHeadCandidateResult {
    126     event_head_candidate_for_nip01_event_v1(event)
    127 }
    128 
    129 /// Derives a raw head candidate with addressable-feed-v1 semantics.
    130 pub fn event_head_candidate_for_nip01_event_v1(event: &EventEnvelope) -> EventHeadCandidateResult {
    131     let coordinate = match event.kind_class() {
    132         EventKindClass::Regular => {
    133             return EventHeadCandidateResult::NotHeadSelected;
    134         }
    135         EventKindClass::Ephemeral => {
    136             return EventHeadCandidateResult::NotPersisted;
    137         }
    138         EventKindClass::Replaceable => EventHeadCoordinate::Replaceable {
    139             kind: event.kind_u32(),
    140             pubkey: *event.author(),
    141         },
    142         EventKindClass::Addressable => EventHeadCoordinate::Addressable {
    143             kind: event.kind_u32(),
    144             pubkey: *event.author(),
    145             d_tag: String::from(first_tag_value(event.tag_slices(), TAG_D).unwrap_or("")),
    146         },
    147     };
    148     EventHeadCandidateResult::Candidate(EventHeadCandidate {
    149         coordinate,
    150         event_id: *event.id(),
    151         created_at: event.created_at_u64(),
    152     })
    153 }
    154 
    155 pub fn event_head_candidate_for_contract(
    156     event: &EventEnvelope,
    157     contract: &EventContract,
    158 ) -> EventHeadCandidateResult {
    159     event_head_candidate_for_class(event, contract.class)
    160 }
    161 
    162 pub fn event_head_candidate_for_event(
    163     event: &EventEnvelope,
    164 ) -> Result<EventHeadCandidateResult, ContractMatchError> {
    165     let tags = event.tags_as_vec();
    166     let contract = identify_event_contract(event.kind_u32(), &tags, event.content())?;
    167     Ok(event_head_candidate_for_contract(event, contract))
    168 }
    169 
    170 pub fn select_event_head(
    171     candidate: EventHeadCandidate,
    172     current: Option<&CurrentEventHead>,
    173 ) -> EventHeadDecision {
    174     select_event_head_v1(candidate, current)
    175 }
    176 
    177 /// Selects a raw head with addressable-feed-v1 ordering semantics.
    178 pub fn select_event_head_v1(
    179     candidate: EventHeadCandidate,
    180     current: Option<&CurrentEventHead>,
    181 ) -> EventHeadDecision {
    182     let Some(current) = current else {
    183         return EventHeadDecision::Applied(candidate.into());
    184     };
    185     if candidate.coordinate != current.coordinate {
    186         return EventHeadDecision::CoordinateMismatch;
    187     }
    188     if candidate.event_id == current.event_id {
    189         return EventHeadDecision::SkippedDuplicate;
    190     }
    191     if candidate.created_at > current.created_at {
    192         return EventHeadDecision::Applied(candidate.into());
    193     }
    194     if candidate.created_at < current.created_at {
    195         return EventHeadDecision::SkippedOlder;
    196     }
    197     if candidate.event_id < current.event_id {
    198         EventHeadDecision::Applied(candidate.into())
    199     } else {
    200         EventHeadDecision::SkippedSameTimestampHigherEventId
    201     }
    202 }
    203 
    204 fn first_tag_value<'a>(tags: &'a [EventTag], name: &str) -> Option<&'a str> {
    205     tags.iter()
    206         .find(|tag| tag.as_slice().first().map(String::as_str) == Some(name))
    207         .and_then(|tag| tag.as_slice().get(1))
    208         .map(String::as_str)
    209 }
    210 
    211 #[cfg(test)]
    212 #[path = "v1/tests.rs"]
    213 #[cfg_attr(coverage_nightly, coverage(off))]
    214 mod tests;