v1.rs (18322B)
1 //! Event catalog contract generation 1. 2 3 use alloc::string::{String, ToString}; 4 use core::fmt; 5 6 use crate::schema::{Metadata, ModuleVersion, Registry}; 7 8 /// Stable event replacement class. 9 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 10 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] 11 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 12 pub enum EventClass { 13 /// Ordinary nonreplaceable event. 14 Regular, 15 /// Replaceable event. 16 Replaceable, 17 /// Parameterized replaceable event. 18 Addressable, 19 /// Unsigned rumor that must not be published directly. 20 UnsignedRumor, 21 } 22 23 /// Passive event-catalog descriptor DTO. 24 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 25 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] 26 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 27 pub struct EventDescriptor { 28 /// Stable catalog name. 29 pub name: &'static str, 30 /// Nostr event kind. 31 pub kind: u32, 32 /// Replacement class. 33 pub event_class: EventClass, 34 /// Stable human-readable purpose. 35 pub purpose: &'static str, 36 } 37 38 /// Exact Release V1 event catalog. 39 pub const CATALOG: &[EventDescriptor] = &[ 40 EventDescriptor { 41 name: "profile", 42 kind: 0, 43 event_class: EventClass::Replaceable, 44 purpose: "actor public profile/supporting discovery", 45 }, 46 EventDescriptor { 47 name: "deletion_request", 48 kind: 5, 49 event_class: EventClass::Regular, 50 purpose: "best-effort NIP-09 request; no global erasure guarantee", 51 }, 52 EventDescriptor { 53 name: "gift_wrap", 54 kind: 1059, 55 event_class: EventClass::Regular, 56 purpose: "NIP-59 encrypted private delivery wrapper", 57 }, 58 EventDescriptor { 59 name: "trade_private_coordination_rumor", 60 kind: 3421, 61 event_class: EventClass::UnsignedRumor, 62 purpose: "NIP-44 encrypted buyer/seller private coordination; never relay-published directly", 63 }, 64 EventDescriptor { 65 name: "trade_order_request", 66 kind: 3422, 67 event_class: EventClass::Regular, 68 purpose: "buyer request against exact listing/quote/validator set", 69 }, 70 EventDescriptor { 71 name: "trade_order_decision", 72 kind: 3423, 73 event_class: EventClass::Regular, 74 purpose: "seller accept or decline", 75 }, 76 EventDescriptor { 77 name: "trade_order_cancellation", 78 kind: 3432, 79 event_class: EventClass::Regular, 80 purpose: "authorized predecision cancellation", 81 }, 82 EventDescriptor { 83 name: "dm_relay_list", 84 kind: 10050, 85 event_class: EventClass::Replaceable, 86 purpose: "recipient private-message relay advertisement", 87 }, 88 EventDescriptor { 89 name: "relay_auth", 90 kind: 22242, 91 event_class: EventClass::Regular, 92 purpose: "NIP-42 relay authentication", 93 }, 94 EventDescriptor { 95 name: "farm", 96 kind: 30340, 97 event_class: EventClass::Addressable, 98 purpose: "public farm aggregate", 99 }, 100 EventDescriptor { 101 name: "validator_set", 102 kind: 30381, 103 event_class: EventClass::Addressable, 104 purpose: "immutable one-validator set artifact signed by network authority", 105 }, 106 EventDescriptor { 107 name: "classified_listing", 108 kind: 30402, 109 event_class: EventClass::Addressable, 110 purpose: "NIP-99 classified listing", 111 }, 112 ]; 113 114 /// Event kinds rejected as retired V1 identities. 115 pub const RETIRED_KINDS: &[u32] = &[ 116 3424, 3425, 3426, 3427, 3428, 3429, 3430, 3433, 3434, 3440, 5321, 5322, 6321, 6322, 30403, 117 ]; 118 119 // Private byte guards preserve fail-closed predecessor behavior without 120 // reintroducing retired event identities as public string surfaces. 121 const RETIRED_NAME_BYTES: &[&[u8]] = &[ 122 &[ 123 108, 105, 115, 116, 105, 110, 103, 95, 100, 114, 97, 102, 116, 124 ], 125 &[116, 114, 97, 100, 101, 95, 97, 110, 115, 119, 101, 114], 126 &[ 127 116, 114, 97, 100, 101, 95, 100, 105, 115, 99, 111, 117, 110, 116, 95, 97, 99, 99, 101, 128 112, 116, 129 ], 130 &[ 131 116, 114, 97, 100, 101, 95, 100, 105, 115, 99, 111, 117, 110, 116, 95, 111, 102, 102, 101, 132 114, 133 ], 134 &[ 135 116, 114, 97, 100, 101, 95, 100, 105, 115, 99, 111, 117, 110, 116, 95, 114, 101, 113, 117, 136 101, 115, 116, 137 ], 138 &[ 139 116, 114, 97, 100, 101, 95, 102, 117, 108, 102, 105, 108, 108, 109, 101, 110, 116, 95, 117, 140 112, 100, 97, 116, 101, 141 ], 142 &[ 143 116, 114, 97, 100, 101, 95, 108, 105, 115, 116, 105, 110, 103, 95, 118, 97, 108, 105, 100, 144 97, 116, 105, 111, 110, 95, 114, 101, 113, 117, 101, 115, 116, 145 ], 146 &[ 147 116, 114, 97, 100, 101, 95, 108, 105, 115, 116, 105, 110, 103, 95, 118, 97, 108, 105, 100, 148 97, 116, 105, 111, 110, 95, 114, 101, 115, 117, 108, 116, 149 ], 150 &[ 151 116, 114, 97, 100, 101, 95, 111, 114, 100, 101, 114, 95, 114, 101, 118, 105, 115, 105, 111, 152 110, 95, 100, 101, 99, 105, 115, 105, 111, 110, 153 ], 154 &[ 155 116, 114, 97, 100, 101, 95, 111, 114, 100, 101, 114, 95, 114, 101, 118, 105, 115, 105, 111, 156 110, 95, 112, 114, 111, 112, 111, 115, 97, 108, 157 ], 158 &[ 159 116, 114, 97, 100, 101, 95, 113, 117, 101, 115, 116, 105, 111, 110, 160 ], 161 &[116, 114, 97, 100, 101, 95, 114, 101, 99, 101, 105, 112, 116], 162 &[ 163 116, 114, 97, 100, 101, 95, 118, 97, 108, 105, 100, 97, 116, 105, 111, 110, 95, 114, 101, 164 99, 101, 105, 112, 116, 165 ], 166 &[ 167 116, 114, 97, 100, 101, 95, 116, 114, 97, 110, 115, 105, 116, 105, 111, 110, 95, 112, 114, 168 111, 111, 102, 95, 114, 101, 113, 117, 101, 115, 116, 169 ], 170 &[ 171 116, 114, 97, 100, 101, 95, 116, 114, 97, 110, 115, 105, 116, 105, 111, 110, 95, 112, 114, 172 111, 111, 102, 95, 114, 101, 115, 117, 108, 116, 173 ], 174 ]; 175 176 /// Stable trade projection state serialized by the V1 contract. 177 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 178 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] 179 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 180 pub enum TradeState { 181 /// No trade state exists. 182 Missing, 183 /// A trade was requested. 184 Requested, 185 /// Parties agreed and validation remains pending. 186 AgreedPendingValidation, 187 /// The trade was committed. 188 Committed, 189 /// The trade was declined. 190 Declined, 191 /// The trade was cancelled. 192 Cancelled, 193 /// The validation window expired. 194 ValidationExpired, 195 /// The trade state is invalid. 196 Invalid, 197 } 198 199 impl TradeState { 200 /// Returns the exact stable serialized identity. 201 pub const fn as_str(self) -> &'static str { 202 match self { 203 Self::Missing => "missing", 204 Self::Requested => "requested", 205 Self::AgreedPendingValidation => "agreed_pending_validation", 206 Self::Committed => "committed", 207 Self::Declined => "declined", 208 Self::Cancelled => "cancelled", 209 Self::ValidationExpired => "validation_expired", 210 Self::Invalid => "invalid", 211 } 212 } 213 214 /// Parses a current state and rejects known retired vocabulary explicitly. 215 pub fn parse(value: &str) -> Result<Self, Error> { 216 match value { 217 "missing" => Ok(Self::Missing), 218 "requested" => Ok(Self::Requested), 219 "agreed_pending_validation" => Ok(Self::AgreedPendingValidation), 220 "committed" => Ok(Self::Committed), 221 "declined" => Ok(Self::Declined), 222 "cancelled" => Ok(Self::Cancelled), 223 "validation_expired" => Ok(Self::ValidationExpired), 224 "invalid" => Ok(Self::Invalid), 225 "revision_proposed" | "agreed_pending_rhi" | "pending_rhi" | "pending_validation" => { 226 Err(Error::RetiredTradeState { 227 state: value.to_string(), 228 }) 229 } 230 _ => Err(Error::UnknownTradeState { 231 value: value.to_string(), 232 }), 233 } 234 } 235 } 236 237 /// Exact V1 trade-state vocabulary. 238 pub const TRADE_STATE_VOCABULARY: &[TradeState] = &[ 239 TradeState::Missing, 240 TradeState::Requested, 241 TradeState::AgreedPendingValidation, 242 TradeState::Committed, 243 TradeState::Declined, 244 TradeState::Cancelled, 245 TradeState::ValidationExpired, 246 TradeState::Invalid, 247 ]; 248 249 /// Exact schema identities retained from the predecessor package. 250 pub const SCHEMAS: &[Metadata] = &[ 251 Metadata { 252 type_name: "ProtocolEventDescriptorV1", 253 schema_id: "radroots.protocol.event_descriptor.v1", 254 schema_version: 1, 255 }, 256 Metadata { 257 type_name: "ProtocolTradeStateV1", 258 schema_id: "radroots.protocol.trade_state.v1", 259 schema_version: 1, 260 }, 261 ]; 262 263 /// Validates event-catalog uniqueness and retired-identity exclusion. 264 pub fn validate_catalog(descriptors: &[EventDescriptor]) -> Result<(), Error> { 265 for (index, descriptor) in descriptors.iter().enumerate() { 266 if RETIRED_NAME_BYTES.contains(&descriptor.name.as_bytes()) { 267 return Err(Error::RetiredEventName { 268 name: descriptor.name.to_string(), 269 }); 270 } 271 if RETIRED_KINDS.contains(&descriptor.kind) { 272 return Err(Error::RetiredEventKind { 273 kind: descriptor.kind, 274 }); 275 } 276 for prior in &descriptors[..index] { 277 if prior.name == descriptor.name { 278 return Err(Error::DuplicateEventName { 279 name: descriptor.name.to_string(), 280 }); 281 } 282 if prior.kind == descriptor.kind { 283 return Err(Error::DuplicateEventKind { 284 kind: descriptor.kind, 285 }); 286 } 287 } 288 } 289 Ok(()) 290 } 291 292 /// Validates uniqueness of the current trade-state vocabulary. 293 pub fn validate_trade_state_vocabulary(states: &[TradeState]) -> Result<(), Error> { 294 for (index, state) in states.iter().enumerate() { 295 if states[..index].contains(state) { 296 return Err(Error::DuplicateTradeState { state: *state }); 297 } 298 } 299 Ok(()) 300 } 301 302 /// Builds the validated event schema registry. 303 pub fn schema_registry() -> Result<Registry, crate::schema::Error> { 304 Registry::try_from_metadata( 305 SCHEMAS 306 .iter() 307 .copied() 308 .map(|metadata| (metadata, ModuleVersion::EventV1)), 309 ) 310 } 311 312 /// Event V1 validation failure. 313 #[derive(Clone, Debug, Eq, PartialEq)] 314 #[non_exhaustive] 315 pub enum Error { 316 /// An event name appears more than once. 317 DuplicateEventName { 318 /// Duplicated name. 319 name: String, 320 }, 321 /// An event kind appears more than once. 322 DuplicateEventKind { 323 /// Duplicated kind. 324 kind: u32, 325 }, 326 /// A trade state appears more than once. 327 DuplicateTradeState { 328 /// Duplicated state. 329 state: TradeState, 330 }, 331 /// A retired event kind was reintroduced. 332 RetiredEventKind { 333 /// Retired kind. 334 kind: u32, 335 }, 336 /// A retired event name was reintroduced. 337 RetiredEventName { 338 /// Retired name. 339 name: String, 340 }, 341 /// A retired trade-state identity was supplied. 342 RetiredTradeState { 343 /// Retired state identity. 344 state: String, 345 }, 346 /// An unknown trade-state identity was supplied. 347 UnknownTradeState { 348 /// Unknown state identity. 349 value: String, 350 }, 351 } 352 353 impl fmt::Display for Error { 354 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 355 match self { 356 Self::DuplicateEventName { name } => write!(formatter, "duplicate event name {name}"), 357 Self::DuplicateEventKind { kind } => write!(formatter, "duplicate event kind {kind}"), 358 Self::DuplicateTradeState { state } => { 359 write!(formatter, "duplicate trade state {}", state.as_str()) 360 } 361 Self::RetiredEventKind { kind } => write!(formatter, "retired event kind {kind}"), 362 Self::RetiredEventName { name } => write!(formatter, "retired event name {name}"), 363 Self::RetiredTradeState { state } => write!(formatter, "retired trade state {state}"), 364 Self::UnknownTradeState { value } => write!(formatter, "unknown trade state {value}"), 365 } 366 } 367 } 368 369 #[cfg(feature = "std")] 370 impl std::error::Error for Error {} 371 372 #[cfg(test)] 373 mod tests { 374 use alloc::vec::Vec; 375 376 use super::*; 377 378 #[test] 379 fn catalogs_and_schema_registry_validate() { 380 validate_catalog(CATALOG).expect("event catalog"); 381 validate_trade_state_vocabulary(TRADE_STATE_VOCABULARY).expect("trade vocabulary"); 382 let registry = schema_registry().expect("schema registry"); 383 assert_eq!(registry.len(), SCHEMAS.len()); 384 assert!( 385 registry 386 .descriptors() 387 .iter() 388 .all(|descriptor| descriptor.module() == ModuleVersion::EventV1) 389 ); 390 } 391 392 #[test] 393 fn event_catalog_retains_exact_v1_identifiers() { 394 assert_eq!(CATALOG.len(), 12); 395 let listing = CATALOG 396 .iter() 397 .find(|event| event.kind == 30402) 398 .expect("classified listing"); 399 assert_eq!(listing.name, "classified_listing"); 400 assert_eq!(listing.event_class, EventClass::Addressable); 401 assert_eq!(listing.purpose, "NIP-99 classified listing"); 402 } 403 404 #[test] 405 fn trade_state_vocabulary_and_parser_are_exact() { 406 assert_eq!( 407 TRADE_STATE_VOCABULARY 408 .iter() 409 .map(|state| state.as_str()) 410 .collect::<Vec<_>>(), 411 [ 412 "missing", 413 "requested", 414 "agreed_pending_validation", 415 "committed", 416 "declined", 417 "cancelled", 418 "validation_expired", 419 "invalid", 420 ] 421 ); 422 for state in TRADE_STATE_VOCABULARY { 423 assert_eq!(TradeState::parse(state.as_str()), Ok(*state)); 424 } 425 assert_eq!( 426 TradeState::parse("pending_rhi") 427 .expect_err("retired") 428 .to_string(), 429 "retired trade state pending_rhi" 430 ); 431 assert_eq!( 432 TradeState::parse("fulfilled") 433 .expect_err("unknown") 434 .to_string(), 435 "unknown trade state fulfilled" 436 ); 437 } 438 439 #[test] 440 fn event_validation_rejects_retired_and_duplicate_entries() { 441 let first = CATALOG[0]; 442 let duplicate_name = EventDescriptor { 443 name: first.name, 444 kind: u32::MAX, 445 event_class: EventClass::Regular, 446 purpose: "duplicate name", 447 }; 448 assert_eq!( 449 validate_catalog(&[first, duplicate_name]), 450 Err(Error::DuplicateEventName { 451 name: first.name.into(), 452 }) 453 ); 454 let retired = EventDescriptor { 455 name: "synthetic_current_name", 456 kind: RETIRED_KINDS[0], 457 event_class: EventClass::Regular, 458 purpose: "retired", 459 }; 460 assert_eq!( 461 validate_catalog(&[retired]), 462 Err(Error::RetiredEventKind { 463 kind: RETIRED_KINDS[0], 464 }) 465 ); 466 let retired_prototype = EventDescriptor { 467 name: "synthetic_prototype_name", 468 kind: 3440, 469 event_class: EventClass::Regular, 470 purpose: "retired", 471 }; 472 assert_eq!( 473 validate_catalog(&[retired_prototype]), 474 Err(Error::RetiredEventKind { kind: 3440 }) 475 ); 476 477 const RETIRED_NAME: &str = concat!("listing", "_draft"); 478 let retired_name = EventDescriptor { 479 name: RETIRED_NAME, 480 kind: u32::MAX, 481 event_class: EventClass::Regular, 482 purpose: "retired", 483 }; 484 assert_eq!( 485 validate_catalog(&[retired_name]), 486 Err(Error::RetiredEventName { 487 name: RETIRED_NAME.into() 488 }) 489 ); 490 const RETIRED_PROTOTYPE_NAME: &str = concat!("trade_", "validation_receipt"); 491 let retired_prototype_name = EventDescriptor { 492 name: RETIRED_PROTOTYPE_NAME, 493 kind: u32::MAX, 494 event_class: EventClass::Regular, 495 purpose: "retired", 496 }; 497 assert_eq!( 498 validate_catalog(&[retired_prototype_name]), 499 Err(Error::RetiredEventName { 500 name: RETIRED_PROTOTYPE_NAME.into(), 501 }) 502 ); 503 let duplicate_kind = EventDescriptor { 504 name: "different_name", 505 kind: first.kind, 506 event_class: EventClass::Regular, 507 purpose: "duplicate kind", 508 }; 509 assert_eq!( 510 validate_catalog(&[first, duplicate_kind]), 511 Err(Error::DuplicateEventKind { kind: first.kind }) 512 ); 513 assert_eq!( 514 validate_trade_state_vocabulary(&[TradeState::Missing, TradeState::Missing]), 515 Err(Error::DuplicateTradeState { 516 state: TradeState::Missing 517 }) 518 ); 519 520 for retired in [ 521 "revision_proposed", 522 "agreed_pending_rhi", 523 "pending_rhi", 524 "pending_validation", 525 ] { 526 assert!(matches!( 527 TradeState::parse(retired), 528 Err(Error::RetiredTradeState { .. }) 529 )); 530 } 531 let errors = [ 532 Error::DuplicateEventName { 533 name: "event".into(), 534 }, 535 Error::DuplicateEventKind { kind: 1 }, 536 Error::DuplicateTradeState { 537 state: TradeState::Invalid, 538 }, 539 Error::RetiredEventKind { kind: 2 }, 540 Error::RetiredEventName { 541 name: "retired".into(), 542 }, 543 Error::RetiredTradeState { 544 state: "retired".into(), 545 }, 546 Error::UnknownTradeState { 547 value: "unknown".into(), 548 }, 549 ]; 550 for error in errors { 551 assert!(!error.to_string().is_empty()); 552 } 553 } 554 }