lib

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

id.rs (48426B)


      1 #![forbid(unsafe_code)]
      2 
      3 #[cfg(not(feature = "std"))]
      4 use alloc::{format, string::String, string::ToString, vec::Vec};
      5 
      6 #[cfg(feature = "std")]
      7 use std::{string::String, vec::Vec};
      8 
      9 use core::{
     10     fmt::{self, Write as _},
     11     str::FromStr,
     12 };
     13 use radroots_identity::PublicKey;
     14 use url_nostd::Url;
     15 
     16 use crate::envelope::kind::KIND_CLASSIFIED_LISTING;
     17 
     18 #[derive(Clone, Debug, PartialEq, Eq)]
     19 pub enum ParseError {
     20     Empty,
     21     InvalidFormat,
     22     InvalidLength { expected: usize, actual: usize },
     23     InvalidCharacter,
     24     InvalidPublicKey,
     25     UnexpectedKind { expected: u32, actual: u32 },
     26     TooLong { max: usize, actual: usize },
     27 }
     28 
     29 impl fmt::Display for ParseError {
     30     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     31         match self {
     32             Self::Empty => write!(f, "identifier is empty"),
     33             Self::InvalidFormat => write!(f, "identifier has invalid format"),
     34             Self::InvalidLength { expected, actual } => {
     35                 write!(
     36                     f,
     37                     "identifier length {actual} does not match required length {expected}"
     38                 )
     39             }
     40             Self::InvalidCharacter => write!(f, "identifier contains an invalid character"),
     41             Self::InvalidPublicKey => {
     42                 write!(f, "identifier is not a valid secp256k1 x-only public key")
     43             }
     44             Self::UnexpectedKind { expected, actual } => {
     45                 write!(
     46                     f,
     47                     "identifier kind {actual} does not match required kind {expected}"
     48                 )
     49             }
     50             Self::TooLong { max, actual } => {
     51                 write!(f, "identifier length {actual} exceeds maximum length {max}")
     52             }
     53         }
     54     }
     55 }
     56 
     57 #[cfg(feature = "std")]
     58 impl std::error::Error for ParseError {}
     59 
     60 macro_rules! validated_string_id {
     61     ($name:ident, $validator:ident) => {
     62         #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))]
     63         #[cfg_attr(all(test, feature = "std"), dto(as = "string"))]
     64         #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
     65         pub struct $name(String);
     66 
     67         impl $name {
     68             pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
     69                 $validator(value.as_ref()).map(Self)
     70             }
     71 
     72             #[inline]
     73             pub fn as_str(&self) -> &str {
     74                 self.0.as_str()
     75             }
     76 
     77             #[inline]
     78             pub fn into_string(self) -> String {
     79                 self.0
     80             }
     81         }
     82 
     83         impl AsRef<str> for $name {
     84             #[inline]
     85             fn as_ref(&self) -> &str {
     86                 self.as_str()
     87             }
     88         }
     89 
     90         impl From<$name> for String {
     91             #[inline]
     92             fn from(value: $name) -> Self {
     93                 value.into_string()
     94             }
     95         }
     96 
     97         impl fmt::Display for $name {
     98             fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
     99                 f.write_str(self.as_str())
    100             }
    101         }
    102 
    103         impl FromStr for $name {
    104             type Err = ParseError;
    105 
    106             fn from_str(value: &str) -> Result<Self, Self::Err> {
    107                 Self::parse(value)
    108             }
    109         }
    110 
    111         impl TryFrom<&str> for $name {
    112             type Error = ParseError;
    113 
    114             fn try_from(value: &str) -> Result<Self, Self::Error> {
    115                 Self::parse(value)
    116             }
    117         }
    118 
    119         impl TryFrom<String> for $name {
    120             type Error = ParseError;
    121 
    122             fn try_from(value: String) -> Result<Self, Self::Error> {
    123                 Self::parse(value)
    124             }
    125         }
    126 
    127         #[cfg(any(feature = "serde", test))]
    128         impl serde::Serialize for $name {
    129             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    130             where
    131                 S: serde::Serializer,
    132             {
    133                 serializer.serialize_str(self.as_str())
    134             }
    135         }
    136 
    137         #[cfg(any(feature = "serde", test))]
    138         impl<'de> serde::Deserialize<'de> for $name {
    139             fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    140             where
    141                 D: serde::Deserializer<'de>,
    142             {
    143                 let value = String::deserialize(deserializer)?;
    144                 Self::parse(value).map_err(serde::de::Error::custom)
    145             }
    146         }
    147     };
    148 }
    149 
    150 macro_rules! validated_hex_id {
    151     ($name:ident, $byte_len:expr) => {
    152         #[cfg_attr(all(test, feature = "std"), derive(dto_bindgen::Dto))]
    153         #[cfg_attr(all(test, feature = "std"), dto(as = "string"))]
    154         #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    155         pub struct $name([u8; $byte_len]);
    156 
    157         impl $name {
    158             pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
    159                 decode_hex::<$byte_len>(value.as_ref()).map(Self)
    160             }
    161 
    162             #[inline]
    163             pub const fn from_bytes(bytes: [u8; $byte_len]) -> Self {
    164                 Self(bytes)
    165             }
    166 
    167             #[inline]
    168             pub const fn as_bytes(&self) -> &[u8; $byte_len] {
    169                 &self.0
    170             }
    171 
    172             #[inline]
    173             pub const fn into_bytes(self) -> [u8; $byte_len] {
    174                 self.0
    175             }
    176 
    177             pub fn to_hex(&self) -> String {
    178                 encode_hex(self.0.as_slice())
    179             }
    180 
    181             #[inline]
    182             pub fn into_string(self) -> String {
    183                 self.to_hex()
    184             }
    185         }
    186 
    187         impl AsRef<[u8]> for $name {
    188             #[inline]
    189             fn as_ref(&self) -> &[u8] {
    190                 self.0.as_slice()
    191             }
    192         }
    193 
    194         impl From<[u8; $byte_len]> for $name {
    195             #[inline]
    196             fn from(bytes: [u8; $byte_len]) -> Self {
    197                 Self::from_bytes(bytes)
    198             }
    199         }
    200 
    201         impl From<$name> for [u8; $byte_len] {
    202             #[inline]
    203             fn from(value: $name) -> Self {
    204                 value.into_bytes()
    205             }
    206         }
    207 
    208         impl fmt::Display for $name {
    209             fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    210                 write_hex(formatter, self.0.as_slice())
    211             }
    212         }
    213 
    214         impl FromStr for $name {
    215             type Err = ParseError;
    216 
    217             fn from_str(value: &str) -> Result<Self, Self::Err> {
    218                 Self::parse(value)
    219             }
    220         }
    221 
    222         impl TryFrom<&str> for $name {
    223             type Error = ParseError;
    224 
    225             fn try_from(value: &str) -> Result<Self, Self::Error> {
    226                 Self::parse(value)
    227             }
    228         }
    229 
    230         impl TryFrom<String> for $name {
    231             type Error = ParseError;
    232 
    233             fn try_from(value: String) -> Result<Self, Self::Error> {
    234                 Self::parse(value)
    235             }
    236         }
    237 
    238         #[cfg(any(feature = "serde", test))]
    239         impl serde::Serialize for $name {
    240             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    241             where
    242                 S: serde::Serializer,
    243             {
    244                 serializer.collect_str(self)
    245             }
    246         }
    247 
    248         #[cfg(any(feature = "serde", test))]
    249         impl<'de> serde::Deserialize<'de> for $name {
    250             fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    251             where
    252                 D: serde::Deserializer<'de>,
    253             {
    254                 let value = String::deserialize(deserializer)?;
    255                 Self::parse(value).map_err(serde::de::Error::custom)
    256             }
    257         }
    258     };
    259 }
    260 
    261 validated_hex_id!(EventId, 32);
    262 validated_hex_id!(EventSignature, 64);
    263 validated_hex_id!(TradeId, 16);
    264 validated_hex_id!(CandidateId, 32);
    265 validated_hex_id!(MutationId, 32);
    266 validated_string_id!(DTag, validate_d_tag);
    267 validated_string_id!(AddressableCoordinate, validate_addressable_coordinate);
    268 validated_string_id!(
    269     ClassifiedListingAddress,
    270     validate_classified_listing_address
    271 );
    272 validated_string_id!(OrderId, validate_commercial_id);
    273 validated_string_id!(OrderQuoteId, validate_commercial_id);
    274 validated_string_id!(InventoryBinId, validate_commercial_id);
    275 validated_string_id!(EconomicsDigest, validate_economics_digest);
    276 validated_hex_id!(EventPointer, 32);
    277 validated_string_id!(RelayUrl, validate_relay_url);
    278 
    279 pub(crate) fn parse_public_key(value: impl AsRef<str>) -> Result<PublicKey, ParseError> {
    280     PublicKey::from_hex(value.as_ref()).map_err(|error| match error {
    281         radroots_identity::Error::InvalidHexLength { expected, actual }
    282         | radroots_identity::Error::InvalidByteLength { expected, actual } => {
    283             ParseError::InvalidLength { expected, actual }
    284         }
    285         radroots_identity::Error::InvalidHexCharacter { .. } => ParseError::InvalidCharacter,
    286         _ => ParseError::InvalidPublicKey,
    287     })
    288 }
    289 
    290 /// Radroots tag-element policy for a NIP-01 coordinate.
    291 ///
    292 /// NIP-01 does not define this resource limit.
    293 pub const RADROOTS_NIP01_COORDINATE_MAX_BYTES: usize =
    294     crate::wire::v1::DEFAULT_TAG_ELEMENT_MAX_BYTES;
    295 
    296 #[non_exhaustive]
    297 #[derive(Clone, Debug, PartialEq, Eq)]
    298 pub enum Nip01CoordinateParseError {
    299     Empty,
    300     InvalidFormat,
    301     Pubkey(ParseError),
    302     UnsupportedKind { actual: u32 },
    303     IdentifierMustBeEmpty { kind: u32 },
    304     TooLong { max: usize, actual: usize },
    305 }
    306 
    307 impl fmt::Display for Nip01CoordinateParseError {
    308     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    309         match self {
    310             Self::Empty => formatter.write_str("NIP-01 coordinate is empty"),
    311             Self::InvalidFormat => formatter.write_str("NIP-01 coordinate has invalid format"),
    312             Self::Pubkey(error) => {
    313                 write!(formatter, "NIP-01 coordinate pubkey is invalid: {error}")
    314             }
    315             Self::UnsupportedKind { actual } => write!(
    316                 formatter,
    317                 "NIP-01 coordinate kind {actual} is not replaceable or addressable"
    318             ),
    319             Self::IdentifierMustBeEmpty { kind } => write!(
    320                 formatter,
    321                 "NIP-01 coordinate identifier must be empty for replaceable kind {kind}"
    322             ),
    323             Self::TooLong { max, actual } => write!(
    324                 formatter,
    325                 "NIP-01 coordinate is {actual} bytes; Radroots tag-element max is {max}"
    326             ),
    327         }
    328     }
    329 }
    330 
    331 #[cfg(feature = "std")]
    332 impl std::error::Error for Nip01CoordinateParseError {
    333     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
    334         match self {
    335             Self::Pubkey(error) => Some(error),
    336             _ => None,
    337         }
    338     }
    339 }
    340 
    341 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    342 pub struct Nip01CoordinateParts {
    343     pub kind: u32,
    344     pub pubkey: PublicKey,
    345     pub identifier: String,
    346 }
    347 
    348 impl Nip01CoordinateParts {
    349     pub fn parse(value: impl AsRef<str>) -> Result<Self, Nip01CoordinateParseError> {
    350         Nip01Coordinate::parse(value).map(Nip01Coordinate::into_parts)
    351     }
    352 }
    353 
    354 /// A canonical NIP-01 replaceable or addressable event coordinate.
    355 ///
    356 /// Parsing splits only the first two `:` delimiters. The remaining identifier
    357 /// is opaque protocol data and is preserved byte-for-byte.
    358 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    359 pub struct Nip01Coordinate {
    360     canonical: String,
    361     kind: u32,
    362     pubkey: PublicKey,
    363     identifier: String,
    364 }
    365 
    366 impl Nip01Coordinate {
    367     pub fn parse(value: impl AsRef<str>) -> Result<Self, Nip01CoordinateParseError> {
    368         let value = value.as_ref();
    369         if value.is_empty() {
    370             return Err(Nip01CoordinateParseError::Empty);
    371         }
    372         if value.len() > RADROOTS_NIP01_COORDINATE_MAX_BYTES {
    373             return Err(Nip01CoordinateParseError::TooLong {
    374                 max: RADROOTS_NIP01_COORDINATE_MAX_BYTES,
    375                 actual: value.len(),
    376             });
    377         }
    378 
    379         let (kind, remainder) = value
    380             .split_once(':')
    381             .ok_or(Nip01CoordinateParseError::InvalidFormat)?;
    382         let (pubkey, identifier) = remainder
    383             .split_once(':')
    384             .ok_or(Nip01CoordinateParseError::InvalidFormat)?;
    385         let kind = kind
    386             .parse::<u32>()
    387             .map_err(|_| Nip01CoordinateParseError::InvalidFormat)?;
    388         let requires_empty_identifier = matches!(kind, 0 | 3) || (10_000..=19_999).contains(&kind);
    389         if !requires_empty_identifier && !(30_000..=39_999).contains(&kind) {
    390             return Err(Nip01CoordinateParseError::UnsupportedKind { actual: kind });
    391         }
    392         if requires_empty_identifier && !identifier.is_empty() {
    393             return Err(Nip01CoordinateParseError::IdentifierMustBeEmpty { kind });
    394         }
    395 
    396         let pubkey = parse_public_key(pubkey).map_err(Nip01CoordinateParseError::Pubkey)?;
    397         let identifier = identifier.to_string();
    398         let canonical = format!("{kind}:{pubkey}:{identifier}");
    399         Ok(Self {
    400             canonical,
    401             kind,
    402             pubkey,
    403             identifier,
    404         })
    405     }
    406 
    407     #[inline]
    408     pub fn as_str(&self) -> &str {
    409         self.canonical.as_str()
    410     }
    411 
    412     #[inline]
    413     pub const fn kind(&self) -> u32 {
    414         self.kind
    415     }
    416 
    417     #[inline]
    418     pub const fn pubkey(&self) -> &PublicKey {
    419         &self.pubkey
    420     }
    421 
    422     #[inline]
    423     pub fn identifier(&self) -> &str {
    424         self.identifier.as_str()
    425     }
    426 
    427     #[inline]
    428     pub fn parts(&self) -> Nip01CoordinateParts {
    429         Nip01CoordinateParts {
    430             kind: self.kind,
    431             pubkey: self.pubkey,
    432             identifier: self.identifier.clone(),
    433         }
    434     }
    435 
    436     #[inline]
    437     pub fn into_parts(self) -> Nip01CoordinateParts {
    438         Nip01CoordinateParts {
    439             kind: self.kind,
    440             pubkey: self.pubkey,
    441             identifier: self.identifier,
    442         }
    443     }
    444 
    445     #[inline]
    446     pub fn into_string(self) -> String {
    447         self.canonical
    448     }
    449 }
    450 
    451 impl AsRef<str> for Nip01Coordinate {
    452     #[inline]
    453     fn as_ref(&self) -> &str {
    454         self.as_str()
    455     }
    456 }
    457 
    458 impl From<Nip01Coordinate> for String {
    459     #[inline]
    460     fn from(value: Nip01Coordinate) -> Self {
    461         value.into_string()
    462     }
    463 }
    464 
    465 impl fmt::Display for Nip01Coordinate {
    466     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    467         formatter.write_str(self.as_str())
    468     }
    469 }
    470 
    471 impl FromStr for Nip01Coordinate {
    472     type Err = Nip01CoordinateParseError;
    473 
    474     fn from_str(value: &str) -> Result<Self, Self::Err> {
    475         Self::parse(value)
    476     }
    477 }
    478 
    479 impl TryFrom<&str> for Nip01Coordinate {
    480     type Error = Nip01CoordinateParseError;
    481 
    482     fn try_from(value: &str) -> Result<Self, Self::Error> {
    483         Self::parse(value)
    484     }
    485 }
    486 
    487 impl TryFrom<String> for Nip01Coordinate {
    488     type Error = Nip01CoordinateParseError;
    489 
    490     fn try_from(value: String) -> Result<Self, Self::Error> {
    491         Self::parse(value)
    492     }
    493 }
    494 
    495 #[cfg(any(feature = "serde", test))]
    496 impl serde::Serialize for Nip01Coordinate {
    497     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    498     where
    499         S: serde::Serializer,
    500     {
    501         serializer.serialize_str(self.as_str())
    502     }
    503 }
    504 
    505 #[cfg(any(feature = "serde", test))]
    506 impl<'de> serde::Deserialize<'de> for Nip01Coordinate {
    507     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    508     where
    509         D: serde::Deserializer<'de>,
    510     {
    511         let value = <String as serde::Deserialize>::deserialize(deserializer)?;
    512         Self::parse(value).map_err(serde::de::Error::custom)
    513     }
    514 }
    515 
    516 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    517 pub struct AddressableCoordinateParts {
    518     pub kind: u32,
    519     pub pubkey: PublicKey,
    520     pub d_tag: DTag,
    521 }
    522 
    523 impl AddressableCoordinateParts {
    524     pub fn parse(value: impl AsRef<str>) -> Result<Self, ParseError> {
    525         parse_addressable_coordinate_parts(value.as_ref())
    526     }
    527 }
    528 
    529 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
    530 pub struct EventEnvelopePointer {
    531     pub event_id: EventId,
    532     pub relays: Vec<String>,
    533 }
    534 
    535 impl EventEnvelopePointer {
    536     pub fn new<I, S>(event_id: EventId, relays: I) -> Result<Self, ParseError>
    537     where
    538         I: IntoIterator<Item = S>,
    539         S: Into<String>,
    540     {
    541         let mut canonical_relays = Vec::new();
    542         for relay in relays {
    543             let relay = relay.into();
    544             RelayUrl::parse(relay.as_str())?;
    545             canonical_relays.push(relay);
    546         }
    547         Ok(Self {
    548             event_id,
    549             relays: canonical_relays,
    550         })
    551     }
    552 }
    553 
    554 fn decode_hex<const N: usize>(value: &str) -> Result<[u8; N], ParseError> {
    555     let expected = N * 2;
    556     if value.len() != expected {
    557         return Err(ParseError::InvalidLength {
    558             expected,
    559             actual: value.len(),
    560         });
    561     }
    562 
    563     let mut decoded = [0_u8; N];
    564     for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() {
    565         decoded[index] = (decode_hex_nibble(pair[0])? << 4) | decode_hex_nibble(pair[1])?;
    566     }
    567     Ok(decoded)
    568 }
    569 
    570 fn decode_hex_nibble(value: u8) -> Result<u8, ParseError> {
    571     match value {
    572         b'0'..=b'9' => Ok(value - b'0'),
    573         b'a'..=b'f' => Ok(value - b'a' + 10),
    574         b'A'..=b'F' => Ok(value - b'A' + 10),
    575         _ => Err(ParseError::InvalidCharacter),
    576     }
    577 }
    578 
    579 fn encode_hex(bytes: &[u8]) -> String {
    580     const LOWER_HEX: &[u8; 16] = b"0123456789abcdef";
    581     let mut encoded = String::with_capacity(bytes.len() * 2);
    582     for byte in bytes {
    583         encoded.push(LOWER_HEX[usize::from(byte >> 4)] as char);
    584         encoded.push(LOWER_HEX[usize::from(byte & 0x0f)] as char);
    585     }
    586     encoded
    587 }
    588 
    589 fn write_hex(formatter: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
    590     for byte in bytes {
    591         formatter.write_char(lower_hex_digit(byte >> 4))?;
    592         formatter.write_char(lower_hex_digit(byte & 0x0f))?;
    593     }
    594     Ok(())
    595 }
    596 
    597 const fn lower_hex_digit(value: u8) -> char {
    598     match value {
    599         0..=9 => (b'0' + value) as char,
    600         _ => (b'a' + value - 10) as char,
    601     }
    602 }
    603 
    604 fn validate_hex(value: &str, expected_len: usize) -> Result<String, ParseError> {
    605     if value.len() != expected_len {
    606         return Err(ParseError::InvalidLength {
    607             expected: expected_len,
    608             actual: value.len(),
    609         });
    610     }
    611 
    612     let mut canonical = String::with_capacity(expected_len);
    613     for byte in value.bytes() {
    614         match byte {
    615             b'0'..=b'9' => canonical.push(byte as char),
    616             b'a'..=b'f' => canonical.push(byte as char),
    617             b'A'..=b'F' => canonical.push((byte + 32) as char),
    618             _ => return Err(ParseError::InvalidCharacter),
    619         }
    620     }
    621     Ok(canonical)
    622 }
    623 
    624 fn validate_d_tag(value: &str) -> Result<String, ParseError> {
    625     validate_visible_token(value, 512)
    626 }
    627 
    628 fn validate_commercial_id(value: &str) -> Result<String, ParseError> {
    629     validate_visible_token(value, 128)
    630 }
    631 
    632 fn validate_economics_digest(value: &str) -> Result<String, ParseError> {
    633     if let Some(hex) = value.strip_prefix("sha256:") {
    634         validate_hex(hex, 64)?;
    635         return Ok(value.to_string());
    636     }
    637     validate_visible_token(value, 128)
    638 }
    639 
    640 fn validate_addressable_coordinate(value: &str) -> Result<String, ParseError> {
    641     parse_addressable_coordinate_parts(value)?;
    642     Ok(value.to_string())
    643 }
    644 
    645 fn validate_classified_listing_address(value: &str) -> Result<String, ParseError> {
    646     let parts = parse_addressable_coordinate_parts(value)?;
    647     if parts.kind != KIND_CLASSIFIED_LISTING {
    648         return Err(ParseError::UnexpectedKind {
    649             expected: KIND_CLASSIFIED_LISTING,
    650             actual: parts.kind,
    651         });
    652     }
    653     Ok(format!(
    654         "{}:{}:{}",
    655         KIND_CLASSIFIED_LISTING, parts.pubkey, parts.d_tag
    656     ))
    657 }
    658 
    659 fn parse_addressable_coordinate_parts(
    660     value: &str,
    661 ) -> Result<AddressableCoordinateParts, ParseError> {
    662     let (kind, remainder) = value.split_once(':').ok_or(ParseError::InvalidFormat)?;
    663     let (pubkey, d_tag) = remainder.split_once(':').ok_or(ParseError::InvalidFormat)?;
    664     let kind = kind.parse::<u32>().map_err(|_| ParseError::InvalidFormat)?;
    665     let pubkey = parse_public_key(pubkey)?;
    666     let d_tag = DTag::parse(d_tag)?;
    667     Ok(AddressableCoordinateParts {
    668         kind,
    669         pubkey,
    670         d_tag,
    671     })
    672 }
    673 
    674 fn validate_visible_token(value: &str, max_len: usize) -> Result<String, ParseError> {
    675     if value.is_empty() {
    676         return Err(ParseError::Empty);
    677     }
    678     if value.len() > max_len {
    679         return Err(ParseError::TooLong {
    680             max: max_len,
    681             actual: value.len(),
    682         });
    683     }
    684     if value.trim() != value
    685         || value
    686             .chars()
    687             .any(|character| character.is_control() || character.is_whitespace())
    688     {
    689         return Err(ParseError::InvalidCharacter);
    690     }
    691     Ok(value.to_string())
    692 }
    693 
    694 pub fn relay_url_is_valid(value: &str) -> bool {
    695     validate_relay_url(value).is_ok()
    696 }
    697 
    698 fn validate_relay_url(value: &str) -> Result<String, ParseError> {
    699     if value.is_empty() {
    700         return Err(ParseError::Empty);
    701     }
    702     if value
    703         .chars()
    704         .any(|character| character.is_control() || character.is_whitespace())
    705     {
    706         return Err(ParseError::InvalidCharacter);
    707     }
    708     if !(value.starts_with("ws://") || value.starts_with("wss://")) {
    709         return Err(ParseError::InvalidFormat);
    710     }
    711 
    712     let parsed = Url::parse(value).map_err(|_| ParseError::InvalidFormat)?;
    713     if !matches!(parsed.scheme(), "ws" | "wss")
    714         || parsed.host_str().is_none_or(str::is_empty)
    715         || !parsed.username().is_empty()
    716         || parsed.password().is_some()
    717         || parsed.fragment().is_some()
    718         || parsed.port() == Some(0)
    719     {
    720         return Err(ParseError::InvalidFormat);
    721     }
    722 
    723     // `Url::username` cannot distinguish absent userinfo from an empty userinfo field.
    724     let authority = value
    725         .split_once("://")
    726         .map(|(_, remainder)| remainder.split(['/', '?', '#']).next().unwrap_or(remainder))
    727         .ok_or(ParseError::InvalidFormat)?;
    728     if authority.contains('@') {
    729         return Err(ParseError::InvalidFormat);
    730     }
    731 
    732     Ok(value.to_string())
    733 }
    734 
    735 #[cfg(test)]
    736 #[cfg_attr(coverage_nightly, coverage(off))]
    737 mod tests {
    738     use super::*;
    739 
    740     macro_rules! assert_string_identifier_impls {
    741         ($ty:ty, $value:expr) => {{
    742             let value = $value.to_owned();
    743             let value = value.as_str();
    744             let id = <$ty>::parse(value).expect("parse");
    745 
    746             assert_eq!(id.as_str(), value);
    747             assert_eq!(id.as_ref(), value);
    748             assert_eq!(id.to_string(), value);
    749             assert_eq!(
    750                 <$ty as core::str::FromStr>::from_str(value).expect("from str"),
    751                 id
    752             );
    753             assert_eq!(
    754                 <$ty as TryFrom<&str>>::try_from(value).expect("try from str"),
    755                 id
    756             );
    757             assert_eq!(
    758                 <$ty as TryFrom<String>>::try_from(value.to_owned()).expect("try from string"),
    759                 id
    760             );
    761             let id = <$ty>::parse(value).expect("parse");
    762             let converted: String = String::from(id.clone());
    763             assert_eq!(converted, value);
    764             assert_eq!(id.into_string(), value.to_owned());
    765 
    766             #[cfg(any(feature = "serde", test))]
    767             {
    768                 let id = <$ty>::parse(value).expect("parse");
    769                 let encoded = serde_json::to_string(&id).expect("serialize");
    770                 let decoded: $ty = serde_json::from_str(&encoded).expect("deserialize");
    771                 assert_eq!(decoded.as_str(), value);
    772             }
    773         }};
    774     }
    775 
    776     macro_rules! assert_hex_identifier_impls {
    777         ($ty:ty, $value:expr, $byte_len:expr) => {{
    778             let value = $value.to_owned();
    779             let value = value.as_str();
    780             let id = <$ty>::parse(value).expect("parse");
    781 
    782             assert_eq!(id.to_hex(), value);
    783             assert_eq!(id.as_bytes().len(), $byte_len);
    784             assert_eq!(id.as_ref(), id.as_bytes());
    785             assert_eq!(id.to_string(), value);
    786             assert_eq!(
    787                 <$ty as core::str::FromStr>::from_str(value).expect("from str"),
    788                 id
    789             );
    790             assert_eq!(
    791                 <$ty as TryFrom<&str>>::try_from(value).expect("try from str"),
    792                 id
    793             );
    794             assert_eq!(
    795                 <$ty as TryFrom<String>>::try_from(value.to_owned()).expect("try from string"),
    796                 id
    797             );
    798 
    799             let bytes = id.into_bytes();
    800             assert_eq!(<$ty>::from_bytes(bytes), id);
    801             assert_eq!(id.into_string(), value.to_owned());
    802 
    803             #[cfg(any(feature = "serde", test))]
    804             {
    805                 let encoded = serde_json::to_string(&id).expect("serialize");
    806                 let decoded: $ty = serde_json::from_str(&encoded).expect("deserialize");
    807                 assert_eq!(decoded.to_hex(), value);
    808             }
    809         }};
    810     }
    811 
    812     fn hex_64(character: char) -> String {
    813         crate::test_valid_hex_64(character)
    814     }
    815 
    816     fn hex_32(character: char) -> String {
    817         core::iter::repeat_n(character, 32).collect()
    818     }
    819 
    820     fn hex_128(character: char) -> String {
    821         core::iter::repeat_n(character, 128).collect()
    822     }
    823 
    824     #[test]
    825     fn public_keys_and_event_ids_require_64_hex_chars() {
    826         let upper = "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF";
    827         let public_key = parse_public_key(upper).expect("public key");
    828         assert_eq!(
    829             public_key.to_hex(),
    830             "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"
    831         );
    832 
    833         let event_id = EventId::parse(hex_64('f')).expect("event id");
    834         assert_eq!(event_id.to_hex(), hex_64('f'));
    835         assert_eq!(
    836             EventId::parse(" ".repeat(64)).unwrap_err(),
    837             ParseError::InvalidCharacter
    838         );
    839         assert_eq!(
    840             EventId::parse("a".repeat(63)).unwrap_err(),
    841             ParseError::InvalidLength {
    842                 expected: 64,
    843                 actual: 63
    844             }
    845         );
    846     }
    847 
    848     #[test]
    849     fn id_parse_errors_have_stable_display_messages() {
    850         let errors = [
    851             ParseError::Empty,
    852             ParseError::InvalidFormat,
    853             ParseError::InvalidLength {
    854                 expected: 64,
    855                 actual: 7,
    856             },
    857             ParseError::InvalidCharacter,
    858             ParseError::InvalidPublicKey,
    859             ParseError::UnexpectedKind {
    860                 expected: KIND_CLASSIFIED_LISTING,
    861                 actual: 30023,
    862             },
    863             ParseError::TooLong {
    864                 max: 128,
    865                 actual: 129,
    866             },
    867         ];
    868 
    869         for error in errors {
    870             assert!(!error.to_string().is_empty());
    871         }
    872         assert_eq!(
    873             ParseError::UnexpectedKind {
    874                 expected: KIND_CLASSIFIED_LISTING,
    875                 actual: 30023,
    876             }
    877             .to_string(),
    878             "identifier kind 30023 does not match required kind 30402"
    879         );
    880     }
    881 
    882     #[test]
    883     fn signatures_require_128_hex_chars() {
    884         let signature = EventSignature::parse(hex_128('B')).expect("signature");
    885         assert_eq!(signature.to_hex(), "b".repeat(128));
    886         assert_eq!(
    887             EventSignature::parse(hex_64('b')).unwrap_err(),
    888             ParseError::InvalidLength {
    889                 expected: 128,
    890                 actual: 64
    891             }
    892         );
    893     }
    894 
    895     #[test]
    896     fn trade_semantic_ids_use_protocol_sized_hex() {
    897         let trade_id = TradeId::parse(hex_32('A')).expect("trade id");
    898         assert_eq!(trade_id.to_hex(), hex_32('a'));
    899         assert_eq!(
    900             TradeId::parse(hex_64('a')).unwrap_err(),
    901             ParseError::InvalidLength {
    902                 expected: 32,
    903                 actual: 64
    904             }
    905         );
    906         assert_hex_identifier_impls!(TradeId, &hex_32('a'), 16);
    907         assert_hex_identifier_impls!(CandidateId, &hex_64('b'), 32);
    908         assert_hex_identifier_impls!(MutationId, &hex_64('c'), 32);
    909     }
    910 
    911     #[test]
    912     fn d_tags_reject_empty_control_and_whitespace() {
    913         assert_eq!(DTag::parse("").unwrap_err(), ParseError::Empty);
    914         assert_eq!(
    915             DTag::parse(" listing").unwrap_err(),
    916             ParseError::InvalidCharacter
    917         );
    918         assert_eq!(
    919             DTag::parse("listing\none").unwrap_err(),
    920             ParseError::InvalidCharacter
    921         );
    922         assert_eq!(
    923             DTag::parse("farm:farm-1:members").expect("d tag").as_str(),
    924             "farm:farm-1:members"
    925         );
    926     }
    927 
    928     #[test]
    929     fn addressable_coordinates_validate_kind_pubkey_and_d_tag() {
    930         let addr = format!("30402:{}:listing-1", hex_64('0'));
    931         assert_eq!(
    932             AddressableCoordinate::parse(&addr)
    933                 .expect("coordinate")
    934                 .as_str(),
    935             addr
    936         );
    937         assert_eq!(
    938             ClassifiedListingAddress::parse("30402:not_hex:listing-1").unwrap_err(),
    939             ParseError::InvalidLength {
    940                 expected: 64,
    941                 actual: 7
    942             }
    943         );
    944         assert_eq!(
    945             ClassifiedListingAddress::parse(format!("30023:{}:listing-1", hex_64('0')))
    946                 .unwrap_err(),
    947             ParseError::UnexpectedKind {
    948                 expected: KIND_CLASSIFIED_LISTING,
    949                 actual: 30023,
    950             }
    951         );
    952         let canonical_pubkey = hex_64('a');
    953         for noncanonical_kind in ["030402", "+30402"] {
    954             assert_eq!(
    955                 ClassifiedListingAddress::parse(format!(
    956                     "{noncanonical_kind}:{}:listing-1",
    957                     hex_64('A')
    958                 ))
    959                 .expect("classified listing coordinate")
    960                 .as_str(),
    961                 format!("30402:{canonical_pubkey}:listing-1")
    962             );
    963         }
    964         assert_eq!(
    965             AddressableCoordinate::parse("30402").unwrap_err(),
    966             ParseError::InvalidFormat
    967         );
    968         assert_eq!(
    969             AddressableCoordinate::parse(format!("bad:{}:listing-1", hex_64('a'))).unwrap_err(),
    970             ParseError::InvalidFormat
    971         );
    972         assert_eq!(
    973             AddressableCoordinate::parse(format!("30402:{}:bad d", hex_64('0'))).unwrap_err(),
    974             ParseError::InvalidCharacter
    975         );
    976 
    977         let noncanonical = format!("030402:{}:listing-1", hex_64('A'));
    978         assert_eq!(
    979             AddressableCoordinate::parse(&noncanonical)
    980                 .expect("validated addressable coordinate")
    981                 .as_str(),
    982             noncanonical
    983         );
    984     }
    985 
    986     #[test]
    987     fn binary_identifier_order_matches_canonical_hex_order() {
    988         let lower = EventId::from_bytes([0_u8; 32]);
    989         let mut upper_bytes = [0_u8; 32];
    990         upper_bytes[31] = 1;
    991         let upper = EventId::from_bytes(upper_bytes);
    992 
    993         assert!(lower < upper);
    994         assert!(lower.to_hex() < upper.to_hex());
    995 
    996         let mut identifiers = [upper, lower];
    997         identifiers.sort();
    998         assert_eq!(identifiers, [lower, upper]);
    999     }
   1000 
   1001     #[test]
   1002     fn addressable_coordinate_parts_parse_kind_pubkey_and_d_tag() {
   1003         let addr = format!("30402:{}:farm:farm-1:members", hex_64('A'));
   1004         let parts = AddressableCoordinateParts::parse(&addr).expect("coordinate parts");
   1005         assert_eq!(parts.kind, 30402);
   1006         assert_eq!(parts.pubkey.to_hex(), hex_64('a'));
   1007         assert_eq!(parts.d_tag.as_str(), "farm:farm-1:members");
   1008     }
   1009 
   1010     #[test]
   1011     fn nip01_coordinates_cover_replaceable_and_addressable_kinds() {
   1012         for kind in [0, 3, 10_000, 19_999] {
   1013             let coordinate = Nip01Coordinate::parse(format!("+0{kind}:{}:", hex_64('A')))
   1014                 .expect("replaceable coordinate");
   1015             assert_eq!(coordinate.kind(), kind);
   1016             assert_eq!(coordinate.pubkey().to_hex(), hex_64('a'));
   1017             assert_eq!(coordinate.identifier(), "");
   1018             assert_eq!(coordinate.as_str(), format!("{kind}:{}:", hex_64('a')));
   1019         }
   1020 
   1021         for kind in [30_000, 39_999] {
   1022             let coordinate = Nip01Coordinate::parse(format!("{kind}:{}:", hex_64('b')))
   1023                 .expect("empty addressable coordinate");
   1024             assert_eq!(coordinate.kind(), kind);
   1025             assert_eq!(coordinate.identifier(), "");
   1026         }
   1027     }
   1028 
   1029     #[test]
   1030     fn nip01_coordinate_identifier_is_opaque_after_second_colon() {
   1031         let identifier = "  victoria:\u{0000}seed:\u{2603}\n";
   1032         let coordinate = Nip01Coordinate::parse(format!("030402:{}:{identifier}", hex_64('A')))
   1033             .expect("opaque addressable coordinate");
   1034 
   1035         assert_eq!(coordinate.kind(), 30_402);
   1036         assert_eq!(coordinate.pubkey().to_hex(), hex_64('a'));
   1037         assert_eq!(coordinate.identifier().as_bytes(), identifier.as_bytes());
   1038         assert_eq!(
   1039             coordinate.as_str().as_bytes(),
   1040             format!("30402:{}:{identifier}", hex_64('a')).as_bytes()
   1041         );
   1042         let parts = coordinate.parts();
   1043         assert_eq!(parts.kind, 30_402);
   1044         assert_eq!(parts.pubkey.to_hex(), hex_64('a'));
   1045         assert_eq!(parts.identifier.as_bytes(), identifier.as_bytes());
   1046         assert_eq!(
   1047             Nip01CoordinateParts::parse(coordinate.as_str()).expect("parts"),
   1048             parts
   1049         );
   1050     }
   1051 
   1052     #[test]
   1053     fn nip01_coordinates_reject_unsupported_shapes_and_kinds() {
   1054         let pubkey = hex_64('a');
   1055         assert_eq!(
   1056             Nip01Coordinate::parse("").unwrap_err(),
   1057             Nip01CoordinateParseError::Empty
   1058         );
   1059         assert_eq!(
   1060             Nip01Coordinate::parse("30000").unwrap_err(),
   1061             Nip01CoordinateParseError::InvalidFormat
   1062         );
   1063         assert_eq!(
   1064             Nip01Coordinate::parse("30000:bad:identifier").unwrap_err(),
   1065             Nip01CoordinateParseError::Pubkey(ParseError::InvalidLength {
   1066                 expected: 64,
   1067                 actual: 3
   1068             })
   1069         );
   1070         for kind in [1, 9_999, 20_000, 29_999, 40_000, u32::MAX] {
   1071             assert_eq!(
   1072                 Nip01Coordinate::parse(format!("{kind}:{pubkey}:")).unwrap_err(),
   1073                 Nip01CoordinateParseError::UnsupportedKind { actual: kind }
   1074             );
   1075         }
   1076         for kind in [0, 3, 10_000, 19_999] {
   1077             assert_eq!(
   1078                 Nip01Coordinate::parse(format!("{kind}:{pubkey}:not-empty")).unwrap_err(),
   1079                 Nip01CoordinateParseError::IdentifierMustBeEmpty { kind }
   1080             );
   1081         }
   1082         assert_eq!(
   1083             Nip01Coordinate::parse(format!("30000:{pubkey}")).unwrap_err(),
   1084             Nip01CoordinateParseError::InvalidFormat
   1085         );
   1086     }
   1087 
   1088     #[test]
   1089     fn nip01_coordinate_errors_and_policy_limit_are_explicit() {
   1090         assert_eq!(
   1091             RADROOTS_NIP01_COORDINATE_MAX_BYTES,
   1092             crate::wire::v1::DEFAULT_TAG_ELEMENT_MAX_BYTES
   1093         );
   1094         let errors = [
   1095             Nip01CoordinateParseError::Empty,
   1096             Nip01CoordinateParseError::InvalidFormat,
   1097             Nip01CoordinateParseError::Pubkey(ParseError::InvalidCharacter),
   1098             Nip01CoordinateParseError::UnsupportedKind { actual: 20_000 },
   1099             Nip01CoordinateParseError::IdentifierMustBeEmpty { kind: 10_000 },
   1100             Nip01CoordinateParseError::TooLong {
   1101                 max: RADROOTS_NIP01_COORDINATE_MAX_BYTES,
   1102                 actual: RADROOTS_NIP01_COORDINATE_MAX_BYTES + 1,
   1103             },
   1104         ];
   1105         for error in errors {
   1106             assert!(!error.to_string().is_empty());
   1107         }
   1108     }
   1109 
   1110     #[test]
   1111     fn nip01_coordinate_enforces_entire_element_byte_limit() {
   1112         let prefix = format!("30000:{}:", hex_64('a'));
   1113         let exact = format!(
   1114             "{prefix}{}",
   1115             "x".repeat(RADROOTS_NIP01_COORDINATE_MAX_BYTES - prefix.len())
   1116         );
   1117         let coordinate = Nip01Coordinate::parse(&exact).expect("exact coordinate byte limit");
   1118         assert_eq!(
   1119             coordinate.as_str().len(),
   1120             RADROOTS_NIP01_COORDINATE_MAX_BYTES
   1121         );
   1122 
   1123         assert_eq!(
   1124             Nip01Coordinate::parse(format!("{exact}x")).unwrap_err(),
   1125             Nip01CoordinateParseError::TooLong {
   1126                 max: RADROOTS_NIP01_COORDINATE_MAX_BYTES,
   1127                 actual: RADROOTS_NIP01_COORDINATE_MAX_BYTES + 1,
   1128             }
   1129         );
   1130     }
   1131 
   1132     #[test]
   1133     fn nip01_coordinate_enforces_multibyte_boundary_by_utf8_bytes() {
   1134         let prefix = format!("30000:{}:", hex_64('a'));
   1135         let remaining = RADROOTS_NIP01_COORDINATE_MAX_BYTES - prefix.len();
   1136         let identifier = format!("{}x", "\u{00e9}".repeat((remaining - 1) / 2));
   1137         let exact = format!("{prefix}{identifier}");
   1138         assert_eq!(exact.len(), RADROOTS_NIP01_COORDINATE_MAX_BYTES);
   1139         Nip01Coordinate::parse(&exact).expect("exact multibyte coordinate limit");
   1140 
   1141         let overflow = format!("{exact}x");
   1142         assert_eq!(
   1143             Nip01Coordinate::parse(&overflow).unwrap_err(),
   1144             Nip01CoordinateParseError::TooLong {
   1145                 max: RADROOTS_NIP01_COORDINATE_MAX_BYTES,
   1146                 actual: RADROOTS_NIP01_COORDINATE_MAX_BYTES + 1,
   1147             }
   1148         );
   1149     }
   1150 
   1151     #[test]
   1152     fn nip01_coordinate_exposes_explicit_text_and_validating_serde() {
   1153         let value = format!("30000:{}:farm:victoria", hex_64('a'));
   1154         let coordinate = Nip01Coordinate::parse(&value).expect("coordinate");
   1155         assert_eq!(coordinate.as_ref(), value);
   1156         assert_eq!(coordinate.to_string(), value);
   1157         assert_eq!(
   1158             Nip01Coordinate::from_str(&value).expect("from str"),
   1159             coordinate
   1160         );
   1161         assert_eq!(
   1162             Nip01Coordinate::try_from(value.clone()).expect("try from string"),
   1163             coordinate
   1164         );
   1165         assert_eq!(String::from(coordinate.clone()), value);
   1166 
   1167         let encoded = serde_json::to_string(&coordinate).expect("serialize");
   1168         assert_eq!(
   1169             serde_json::from_str::<Nip01Coordinate>(&encoded).expect("deserialize"),
   1170             coordinate
   1171         );
   1172         assert!(serde_json::from_str::<Nip01Coordinate>("\"1:bad:\"").is_err());
   1173     }
   1174 
   1175     #[test]
   1176     fn commercial_ids_reject_empty_whitespace_control_and_long_values() {
   1177         assert_eq!(
   1178             OrderId::parse("order-1").expect("order id").as_str(),
   1179             "order-1"
   1180         );
   1181         assert_eq!(
   1182             InventoryBinId::parse("a".repeat(129)).unwrap_err(),
   1183             ParseError::TooLong {
   1184                 max: 128,
   1185                 actual: 129
   1186             }
   1187         );
   1188     }
   1189 
   1190     #[test]
   1191     fn economics_digest_accepts_sha256_and_existing_wire_tokens() {
   1192         let digest = format!("sha256:{}", hex_64('c'));
   1193         assert_eq!(
   1194             EconomicsDigest::parse(&digest).expect("digest").as_str(),
   1195             digest
   1196         );
   1197         assert_eq!(
   1198             EconomicsDigest::parse("digest-1")
   1199                 .expect("wire v1 digest")
   1200                 .as_str(),
   1201             "digest-1"
   1202         );
   1203         assert_eq!(
   1204             EconomicsDigest::parse("sha256:not-hex").unwrap_err(),
   1205             ParseError::InvalidLength {
   1206                 expected: 64,
   1207                 actual: 7
   1208             }
   1209         );
   1210     }
   1211 
   1212     #[test]
   1213     fn validated_types_do_not_offer_infallible_string_conversion() {
   1214         let id = OrderQuoteId::try_from(String::from("quote-1")).expect("quote id");
   1215         assert_eq!(id.as_ref(), "quote-1");
   1216         let parsed: EventPointer = hex_64('d').parse().expect("event pointer");
   1217         assert_eq!(parsed.to_hex(), hex_64('d'));
   1218     }
   1219 
   1220     #[test]
   1221     fn validated_identifier_wrappers_expose_consistent_traits() {
   1222         let addressable = format!("30402:{}:listing-1", hex_64('0'));
   1223 
   1224         assert_hex_identifier_impls!(EventId, hex_64('b').as_str(), 32);
   1225         assert_hex_identifier_impls!(EventSignature, hex_128('c').as_str(), 64);
   1226         assert_string_identifier_impls!(DTag, "listing-1");
   1227         assert_string_identifier_impls!(AddressableCoordinate, addressable.as_str());
   1228         assert_string_identifier_impls!(ClassifiedListingAddress, addressable.as_str());
   1229         assert_string_identifier_impls!(OrderId, "order-1");
   1230         assert_string_identifier_impls!(OrderQuoteId, "quote-1");
   1231         assert_string_identifier_impls!(InventoryBinId, "bin-1");
   1232         assert_string_identifier_impls!(EconomicsDigest, "digest-1");
   1233         assert_hex_identifier_impls!(EventPointer, hex_64('d').as_str(), 32);
   1234         assert_string_identifier_impls!(RelayUrl, "wss://relay.example.com");
   1235     }
   1236 
   1237     #[test]
   1238     fn relay_urls_require_valid_websocket_urls() {
   1239         assert_eq!(
   1240             RelayUrl::parse("ws://relay.example.com")
   1241                 .expect("relay url")
   1242                 .as_str(),
   1243             "ws://relay.example.com"
   1244         );
   1245         assert_eq!(
   1246             RelayUrl::parse("wss://relay.example.com")
   1247                 .expect("relay url")
   1248                 .as_str(),
   1249             "wss://relay.example.com"
   1250         );
   1251         assert!(relay_url_is_valid("wss://relay.example.com"));
   1252         assert_eq!(RelayUrl::parse("").unwrap_err(), ParseError::Empty);
   1253         assert_eq!(
   1254             RelayUrl::parse("http://relay.example.com").unwrap_err(),
   1255             ParseError::InvalidFormat
   1256         );
   1257         assert_eq!(
   1258             RelayUrl::parse("WSS://relay.example.com").unwrap_err(),
   1259             ParseError::InvalidFormat
   1260         );
   1261         assert_eq!(
   1262             RelayUrl::parse("ws://").unwrap_err(),
   1263             ParseError::InvalidFormat
   1264         );
   1265         assert_eq!(
   1266             RelayUrl::parse("wss://").unwrap_err(),
   1267             ParseError::InvalidFormat
   1268         );
   1269         assert_eq!(
   1270             RelayUrl::parse(" wss://relay.example.com").unwrap_err(),
   1271             ParseError::InvalidCharacter
   1272         );
   1273         assert_eq!(
   1274             RelayUrl::parse("wss://relay.example.com\nmiddle").unwrap_err(),
   1275             ParseError::InvalidCharacter
   1276         );
   1277         assert_eq!(
   1278             RelayUrl::parse("wss://user@relay.example.com").unwrap_err(),
   1279             ParseError::InvalidFormat
   1280         );
   1281         assert_eq!(
   1282             RelayUrl::parse("wss://user:secret@relay.example.com").unwrap_err(),
   1283             ParseError::InvalidFormat
   1284         );
   1285         assert_eq!(
   1286             RelayUrl::parse("wss://relay.example.com#read").unwrap_err(),
   1287             ParseError::InvalidFormat
   1288         );
   1289         assert_eq!(
   1290             RelayUrl::parse("wss://relay.example.com:0").unwrap_err(),
   1291             ParseError::InvalidFormat
   1292         );
   1293         assert_eq!(
   1294             RelayUrl::parse("wss://relay.example.com/nostr/v1?region=ca-bc")
   1295                 .expect("relay URL with path and query")
   1296                 .as_str(),
   1297             "wss://relay.example.com/nostr/v1?region=ca-bc"
   1298         );
   1299     }
   1300 
   1301     #[test]
   1302     fn nostr_event_pointers_validate_relay_values() {
   1303         let event_id = EventId::parse(hex_64('e')).expect("event id");
   1304         let pointer = EventEnvelopePointer::new(
   1305             event_id,
   1306             ["wss://relay.one.example", "wss://relay.two.example"],
   1307         )
   1308         .expect("pointer");
   1309 
   1310         assert_eq!(pointer.event_id, event_id);
   1311         assert_eq!(
   1312             pointer.relays,
   1313             vec![
   1314                 "wss://relay.one.example".to_owned(),
   1315                 "wss://relay.two.example".to_owned()
   1316             ]
   1317         );
   1318 
   1319         assert_eq!(
   1320             EventEnvelopePointer::new(EventId::parse(hex_64('e')).expect("event id"), [""],)
   1321                 .unwrap_err(),
   1322             ParseError::Empty
   1323         );
   1324         assert_eq!(
   1325             EventEnvelopePointer::new(
   1326                 EventId::parse(hex_64('e')).expect("event id"),
   1327                 ["http://relay.example"],
   1328             )
   1329             .unwrap_err(),
   1330             ParseError::InvalidFormat
   1331         );
   1332         assert_eq!(
   1333             EventEnvelopePointer::new(
   1334                 EventId::parse(hex_64('e')).expect("event id"),
   1335                 [" wss://relay.example"],
   1336             )
   1337             .unwrap_err(),
   1338             ParseError::InvalidCharacter
   1339         );
   1340         assert_eq!(
   1341             EventEnvelopePointer::new(
   1342                 EventId::parse(hex_64('e')).expect("event id"),
   1343                 ["wss://relay.example\n"],
   1344             )
   1345             .unwrap_err(),
   1346             ParseError::InvalidCharacter
   1347         );
   1348     }
   1349 
   1350     #[cfg(feature = "serde")]
   1351     #[test]
   1352     fn serde_deserialization_validates_identifiers() {
   1353         let encoded = format!("\"{}\"", hex_64('E'));
   1354         let event_id: EventId = serde_json::from_str(&encoded).expect("event id");
   1355         assert_eq!(event_id.to_hex(), hex_64('e'));
   1356 
   1357         let invalid = serde_json::from_str::<OrderId>("\"bad id\"");
   1358         assert!(invalid.is_err());
   1359         assert_eq!(
   1360             serde_json::to_string(&event_id).expect("json"),
   1361             format!("\"{}\"", hex_64('e'))
   1362         );
   1363     }
   1364 
   1365     #[cfg(feature = "serde")]
   1366     #[test]
   1367     fn serde_missing_fields_exercise_identifier_deserializers() {
   1368         #[allow(dead_code)]
   1369         #[derive(Debug, serde::Deserialize)]
   1370         struct MissingPublicKey {
   1371             value: PublicKey,
   1372         }
   1373         #[allow(dead_code)]
   1374         #[derive(Debug, serde::Deserialize)]
   1375         struct MissingEventId {
   1376             value: EventId,
   1377         }
   1378         #[allow(dead_code)]
   1379         #[derive(Debug, serde::Deserialize)]
   1380         struct MissingEventSignature {
   1381             value: EventSignature,
   1382         }
   1383         #[allow(dead_code)]
   1384         #[derive(Debug, serde::Deserialize)]
   1385         struct MissingTradeId {
   1386             value: TradeId,
   1387         }
   1388         #[allow(dead_code)]
   1389         #[derive(Debug, serde::Deserialize)]
   1390         struct MissingTradeCandidateId {
   1391             value: CandidateId,
   1392         }
   1393         #[allow(dead_code)]
   1394         #[derive(Debug, serde::Deserialize)]
   1395         struct MissingTradeMutationId {
   1396             value: MutationId,
   1397         }
   1398         #[allow(dead_code)]
   1399         #[derive(Debug, serde::Deserialize)]
   1400         struct MissingDTag {
   1401             value: DTag,
   1402         }
   1403         #[allow(dead_code)]
   1404         #[derive(Debug, serde::Deserialize)]
   1405         struct MissingClassifiedListingAddress {
   1406             value: ClassifiedListingAddress,
   1407         }
   1408         #[allow(dead_code)]
   1409         #[derive(Debug, serde::Deserialize)]
   1410         struct MissingAddressableCoordinate {
   1411             value: AddressableCoordinate,
   1412         }
   1413         #[allow(dead_code)]
   1414         #[derive(Debug, serde::Deserialize)]
   1415         struct MissingOrderId {
   1416             value: OrderId,
   1417         }
   1418         #[allow(dead_code)]
   1419         #[derive(Debug, serde::Deserialize)]
   1420         struct MissingOrderQuoteId {
   1421             value: OrderQuoteId,
   1422         }
   1423         #[allow(dead_code)]
   1424         #[derive(Debug, serde::Deserialize)]
   1425         struct MissingInventoryBinId {
   1426             value: InventoryBinId,
   1427         }
   1428 
   1429         fn missing_field_message<T>() -> String
   1430         where
   1431             T: serde::de::DeserializeOwned + core::fmt::Debug,
   1432         {
   1433             serde_json::from_str::<T>("{}")
   1434                 .expect_err("missing field")
   1435                 .to_string()
   1436         }
   1437 
   1438         let missing = "missing field `value` at line 1 column 2";
   1439         assert_eq!(missing_field_message::<MissingPublicKey>(), missing);
   1440         assert_eq!(missing_field_message::<MissingEventId>(), missing);
   1441         assert_eq!(missing_field_message::<MissingEventSignature>(), missing);
   1442         assert_eq!(missing_field_message::<MissingTradeId>(), missing);
   1443         assert_eq!(missing_field_message::<MissingTradeCandidateId>(), missing);
   1444         assert_eq!(missing_field_message::<MissingTradeMutationId>(), missing);
   1445         assert_eq!(missing_field_message::<MissingDTag>(), missing);
   1446         assert_eq!(
   1447             missing_field_message::<MissingClassifiedListingAddress>(),
   1448             missing
   1449         );
   1450         assert_eq!(
   1451             missing_field_message::<MissingAddressableCoordinate>(),
   1452             missing
   1453         );
   1454         assert_eq!(missing_field_message::<MissingOrderId>(), missing);
   1455         assert_eq!(missing_field_message::<MissingOrderQuoteId>(), missing);
   1456         assert_eq!(missing_field_message::<MissingInventoryBinId>(), missing);
   1457 
   1458         let order: OrderId =
   1459             serde_json::from_value(serde_json::json!("order-1")).expect("order from value");
   1460         let listing: ClassifiedListingAddress = serde_json::from_value(serde_json::json!(format!(
   1461             "30402:{}:listing-1",
   1462             hex_64('a')
   1463         )))
   1464         .expect("listing from value");
   1465         let quote: OrderQuoteId =
   1466             serde_json::from_value(serde_json::json!("quote-1")).expect("quote from value");
   1467         assert_eq!(order.as_str(), "order-1");
   1468         assert_eq!(listing.as_str().split(':').next(), Some("30402"));
   1469         assert_eq!(quote.as_str(), "quote-1");
   1470     }
   1471 }