lib

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

generic_draft.rs (10549B)


      1 //! Generic-only authored event input.
      2 
      3 #[cfg(all(not(feature = "std"), not(test)))]
      4 use alloc::{borrow::ToOwned, string::String, vec::Vec};
      5 #[cfg(any(feature = "std", test))]
      6 use std::{borrow::ToOwned, string::String, vec::Vec};
      7 
      8 use radroots_identity::PublicKey;
      9 
     10 use crate::{
     11     contract::{
     12         ContractKey, EventAuthoringPolicy, RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION,
     13         validate_event_contract_parts,
     14     },
     15     draft::DraftError,
     16     envelope::{EventKind, EventTags, EventTimestamp},
     17     id::{EventId, parse_public_key},
     18     wire::{canonical_nip01_event_id_preimage, compute_canonical_nip01_event_id},
     19 };
     20 
     21 /// A validated generic-only authored event input.
     22 ///
     23 /// Typed-only and read-only contracts cannot cross this boundary. Typed
     24 /// authoring is owned by explicit codec conversions instead.
     25 ///
     26 /// ```compile_fail
     27 /// use radroots_event::{GenericEventDraft, post::AuthoredUpdate};
     28 ///
     29 /// let update = AuthoredUpdate::new("harvest").unwrap();
     30 /// let _ = GenericEventDraft::from_authored_update(&update, 1, "00");
     31 /// ```
     32 #[cfg_attr(any(feature = "serde", test), derive(serde::Serialize))]
     33 #[derive(Clone, Debug, PartialEq, Eq)]
     34 pub struct GenericEventDraft {
     35     contract: ContractKey,
     36     kind: EventKind,
     37     created_at: EventTimestamp,
     38     tags: EventTags,
     39     content: String,
     40     expected_pubkey: PublicKey,
     41     expected_event_id: EventId,
     42 }
     43 
     44 impl GenericEventDraft {
     45     pub fn new(
     46         contract_id: impl Into<String>,
     47         kind: u32,
     48         created_at: u64,
     49         tags: Vec<Vec<String>>,
     50         content: impl Into<String>,
     51         expected_pubkey: impl AsRef<str>,
     52     ) -> Result<Self, DraftError> {
     53         let raw_contract_id = contract_id.into();
     54         let contract = ContractKey::current(raw_contract_id.clone())
     55             .map_err(|_| DraftError::UnknownContract(raw_contract_id))?;
     56         let definition = contract.contract();
     57         crate::require_invariant(definition.kind == kind, &|| {
     58             DraftError::ContractKindMismatch {
     59                 contract_id: definition.id.to_owned(),
     60                 expected_kind: definition.kind,
     61                 actual_kind: kind,
     62             }
     63         })?;
     64         crate::require_invariant(
     65             definition.authoring_policy() == EventAuthoringPolicy::GenericDraft,
     66             &|| DraftError::ContractNotDraftAuthorable {
     67                 contract_id: definition.id.to_owned(),
     68             },
     69         )?;
     70         let content = content.into();
     71         validate_event_contract_parts(kind, &tags, &content, definition.id).map_err(|error| {
     72             DraftError::ContractShape {
     73                 contract_id: definition.id.to_owned(),
     74                 error,
     75             }
     76         })?;
     77         let tags = EventTags::new(tags)?;
     78         let expected_pubkey = parse_public_key(expected_pubkey.as_ref())?;
     79         let expected_event_id = compute_canonical_nip01_event_id(
     80             &expected_pubkey.to_hex(),
     81             created_at,
     82             kind,
     83             &tags.to_vec(),
     84             &content,
     85         )?;
     86         Ok(Self {
     87             contract,
     88             kind: EventKind::new(kind),
     89             created_at: EventTimestamp::new(created_at),
     90             tags,
     91             content,
     92             expected_pubkey,
     93             expected_event_id,
     94         })
     95     }
     96 
     97     pub fn validate_for_authoring(&self) -> Result<(), DraftError> {
     98         let reconstructed = Self::new(
     99             self.contract.contract_id().as_str(),
    100             self.kind_u32(),
    101             self.created_at_u64(),
    102             self.tags_as_vec(),
    103             self.content.clone(),
    104             self.expected_pubkey.to_hex(),
    105         )?;
    106         crate::require_invariant(reconstructed.contract == self.contract, &|| {
    107             DraftError::ContractRegistryVersionMismatch {
    108                 expected: RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION,
    109                 actual: self.contract.registry_version().get(),
    110             }
    111         })?;
    112         crate::require_invariant(
    113             reconstructed.expected_event_id == self.expected_event_id,
    114             &|| DraftError::DraftExpectedEventIdMismatch {
    115                 expected_event_id: reconstructed.expected_event_id.to_hex(),
    116                 actual_event_id: self.expected_event_id.to_hex(),
    117             },
    118         )
    119     }
    120 
    121     pub fn nip01_preimage(&self) -> Result<String, DraftError> {
    122         Ok(canonical_nip01_event_id_preimage(
    123             &self.expected_pubkey.to_hex(),
    124             self.created_at_u64(),
    125             self.kind_u32(),
    126             &self.tags_as_vec(),
    127             self.content(),
    128         )?)
    129     }
    130 
    131     #[must_use]
    132     pub const fn contract(&self) -> &ContractKey {
    133         &self.contract
    134     }
    135 
    136     #[must_use]
    137     pub fn contract_id(&self) -> &str {
    138         self.contract.contract_id().as_str()
    139     }
    140 
    141     #[must_use]
    142     pub const fn kind(&self) -> EventKind {
    143         self.kind
    144     }
    145 
    146     #[must_use]
    147     pub const fn kind_u32(&self) -> u32 {
    148         self.kind.as_u32()
    149     }
    150 
    151     #[must_use]
    152     pub const fn created_at(&self) -> EventTimestamp {
    153         self.created_at
    154     }
    155 
    156     #[must_use]
    157     pub const fn created_at_u64(&self) -> u64 {
    158         self.created_at.as_u64()
    159     }
    160 
    161     #[must_use]
    162     pub const fn tags(&self) -> &EventTags {
    163         &self.tags
    164     }
    165 
    166     #[must_use]
    167     pub fn tags_as_vec(&self) -> Vec<Vec<String>> {
    168         self.tags.to_vec()
    169     }
    170 
    171     #[must_use]
    172     pub fn content(&self) -> &str {
    173         &self.content
    174     }
    175 
    176     #[must_use]
    177     pub const fn expected_pubkey(&self) -> &PublicKey {
    178         &self.expected_pubkey
    179     }
    180 
    181     #[must_use]
    182     pub const fn expected_event_id(&self) -> &EventId {
    183         &self.expected_event_id
    184     }
    185 
    186     #[must_use]
    187     pub fn expected_event_id_hex(&self) -> String {
    188         self.expected_event_id.to_hex()
    189     }
    190 }
    191 
    192 #[cfg(any(feature = "serde", test))]
    193 impl<'de> serde::Deserialize<'de> for GenericEventDraft {
    194     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    195     where
    196         D: serde::Deserializer<'de>,
    197     {
    198         #[derive(serde::Deserialize)]
    199         #[serde(deny_unknown_fields)]
    200         struct GenericDraftSerde {
    201             contract: ContractKey,
    202             kind: EventKind,
    203             created_at: EventTimestamp,
    204             tags: EventTags,
    205             content: String,
    206             expected_pubkey: PublicKey,
    207             expected_event_id: EventId,
    208         }
    209 
    210         let value = GenericDraftSerde::deserialize(deserializer)?;
    211         let draft = Self::new(
    212             value.contract.contract_id().as_str(),
    213             value.kind.as_u32(),
    214             value.created_at.as_u64(),
    215             value.tags.to_vec(),
    216             value.content,
    217             value.expected_pubkey.to_hex(),
    218         )
    219         .map_err(serde::de::Error::custom)?;
    220         if draft.contract != value.contract {
    221             return Err(serde::de::Error::custom(
    222                 "generic draft contract key mismatch",
    223             ));
    224         }
    225         if draft.expected_event_id != value.expected_event_id {
    226             return Err(serde::de::Error::custom(
    227                 DraftError::DraftExpectedEventIdMismatch {
    228                     expected_event_id: draft.expected_event_id.to_hex(),
    229                     actual_event_id: value.expected_event_id.to_hex(),
    230                 },
    231             ));
    232         }
    233         Ok(draft)
    234     }
    235 }
    236 
    237 #[cfg(test)]
    238 mod tests {
    239     use super::*;
    240     use crate::{
    241         contract::{EventAuthoringPolicy, RegistryVersion, all_event_contracts},
    242         envelope::kind::KIND_GEOCHAT,
    243     };
    244 
    245     const PUBLIC_KEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    246 
    247     fn draft() -> GenericEventDraft {
    248         GenericEventDraft::new(
    249             "radroots.social.geochat.v1",
    250             KIND_GEOCHAT,
    251             1_700_000_000,
    252             Vec::new(),
    253             "hello",
    254             PUBLIC_KEY,
    255         )
    256         .expect("generic draft")
    257     }
    258 
    259     #[test]
    260     fn generic_input_binds_exact_contract_author_timestamp_and_wire() {
    261         let draft = draft();
    262         assert_eq!(draft.contract_id(), "radroots.social.geochat.v1");
    263         assert_eq!(
    264             draft.contract().registry_version(),
    265             RegistryVersion::CURRENT
    266         );
    267         assert_eq!(draft.kind_u32(), KIND_GEOCHAT);
    268         assert_eq!(draft.created_at_u64(), 1_700_000_000);
    269         assert_eq!(draft.expected_pubkey().to_hex(), PUBLIC_KEY);
    270         assert_eq!(
    271             draft.nip01_preimage().expect("preimage"),
    272             "[0,\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",1700000000,20000,[],\"hello\"]"
    273         );
    274         draft.validate_for_authoring().expect("valid draft");
    275     }
    276 
    277     #[test]
    278     fn generic_input_rejects_every_non_generic_registry_contract() {
    279         for contract in all_event_contracts()
    280             .iter()
    281             .filter(|contract| contract.authoring_policy() != EventAuthoringPolicy::GenericDraft)
    282         {
    283             let error = GenericEventDraft::new(
    284                 contract.id,
    285                 contract.kind,
    286                 1,
    287                 Vec::new(),
    288                 "hello",
    289                 PUBLIC_KEY,
    290             )
    291             .expect_err("typed-only and read-only contracts must fail before shape validation");
    292             assert_eq!(
    293                 error,
    294                 DraftError::ContractNotDraftAuthorable {
    295                     contract_id: contract.id.to_owned(),
    296                 },
    297                 "{}",
    298                 contract.id
    299             );
    300         }
    301     }
    302 
    303     #[test]
    304     fn serde_reconstruction_revalidates_all_private_fields() {
    305         let draft = draft();
    306         let value = serde_json::to_value(&draft).expect("draft JSON");
    307         assert_eq!(
    308             serde_json::from_value::<GenericEventDraft>(value.clone())
    309                 .expect("decoded generic draft"),
    310             draft
    311         );
    312 
    313         for (field, replacement) in [
    314             ("kind", serde_json::json!(1)),
    315             ("content", serde_json::json!("")),
    316             ("expected_event_id", serde_json::json!("f".repeat(64))),
    317         ] {
    318             let mut invalid = value.clone();
    319             invalid[field] = replacement;
    320             assert!(serde_json::from_value::<GenericEventDraft>(invalid).is_err());
    321         }
    322         let mut invalid = value.clone();
    323         invalid["contract"]["contract_id"] = serde_json::json!("radroots.social.update.v1");
    324         assert!(serde_json::from_value::<GenericEventDraft>(invalid).is_err());
    325         let mut invalid = value;
    326         invalid["unexpected"] = serde_json::json!(true);
    327         assert!(serde_json::from_value::<GenericEventDraft>(invalid).is_err());
    328     }
    329 }