lib

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

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 }