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 }