1pub mod coordinate;
39pub mod event;
40pub mod hrp;
41pub mod profile;
42pub mod tlv;
43
44use std::str;
45use std::str::Utf8Error;
46
47use bech32::Bech32;
48use bech32::primitives::decode::{CheckedHrpstring, CheckedHrpstringError};
49use thiserror::Error;
50
51pub use self::coordinate::Nip19Coordinate;
52pub use self::event::Nip19Event;
53pub use self::profile::Nip19Profile;
54pub use self::tlv::{Record as TlvRecord, TlvError};
55use crate::event::{EventId, EventIdError, Kind};
56use crate::key::{PublicKey, PublicKeyError, SecretKey, SecretKeyError};
57use crate::types::{RelayUrl, RelayUrlError};
58
59pub const MAX_NIP19_LENGTH: usize = 5000;
65
66#[derive(Debug, Error)]
68#[non_exhaustive]
69pub enum ToBech32Error {
70 #[error("bech32 encoding failed: {0}")]
73 Encode(#[from] bech32::EncodeError),
74 #[error(transparent)]
76 Tlv(#[from] TlvError),
77}
78
79#[derive(Debug, Error)]
81#[non_exhaustive]
82pub enum FromBech32Error {
83 #[error("NIP-19 string is too long: {len} characters (max {max})")]
85 TooLong {
86 len: usize,
88 max: usize,
90 },
91 #[error("bech32 decoding failed: {0}")]
93 Decode(#[from] CheckedHrpstringError),
94 #[error("unknown NIP-19 prefix `{0}`")]
96 UnknownHrp(String),
97 #[error("expected NIP-19 prefix `{expected}`, got `{got}`")]
99 UnexpectedHrp {
100 expected: &'static str,
102 got: String,
104 },
105 #[error("expected {expected} bytes of payload, got {got}")]
107 InvalidPayloadLength {
108 expected: usize,
110 got: usize,
112 },
113 #[error("required TLV record (tag {tag}) is missing")]
115 MissingTlv {
116 tag: u8,
118 },
119 #[error("kind TLV must be 4 bytes (got {got})")]
121 InvalidKindLength {
122 got: usize,
124 },
125 #[error("kind value {raw} exceeds the supported 16-bit range")]
128 KindOutOfRange {
129 raw: u32,
131 },
132 #[error("relay TLV is not valid UTF-8: {0}")]
134 InvalidRelayUtf8(#[from] Utf8Error),
135 #[error(transparent)]
137 Tlv(#[from] TlvError),
138 #[error(transparent)]
140 PublicKey(#[from] PublicKeyError),
141 #[error(transparent)]
143 SecretKey(#[from] SecretKeyError),
144 #[error(transparent)]
146 EventId(#[from] EventIdError),
147 #[error(transparent)]
149 RelayUrl(#[from] RelayUrlError),
150}
151
152pub trait ToBech32: sealed::Sealed {
161 fn to_bech32(&self) -> Result<String, ToBech32Error>;
168}
169
170pub trait FromBech32: sealed::Sealed + Sized {
176 fn from_bech32(s: &str) -> Result<Self, FromBech32Error>;
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
192#[non_exhaustive]
193pub enum Nip19Entity {
194 PublicKey(PublicKey),
196 SecretKey(SecretKey),
198 EventId(EventId),
200 Profile(Nip19Profile),
202 Event(Nip19Event),
204 Coordinate(Nip19Coordinate),
206}
207
208impl ToBech32 for Nip19Entity {
209 fn to_bech32(&self) -> Result<String, ToBech32Error> {
210 match self {
211 Self::PublicKey(pk) => pk.to_bech32(),
212 Self::SecretKey(sk) => sk.to_bech32(),
213 Self::EventId(id) => id.to_bech32(),
214 Self::Profile(p) => p.to_bech32(),
215 Self::Event(e) => e.to_bech32(),
216 Self::Coordinate(c) => c.to_bech32(),
217 }
218 }
219}
220
221impl FromBech32 for Nip19Entity {
222 #[cfg_attr(
223 feature = "tracing",
224 tracing::instrument(
225 level = "trace",
226 name = "nula.nip19.decode",
227 skip(s),
228 fields(nostr.nip = 19_u16, nostr.bech32.length = s.len()),
229 )
230 )]
231 fn from_bech32(s: &str) -> Result<Self, FromBech32Error> {
232 let (hrp_str, data) = decode_bech32(s)?;
233 #[cfg(feature = "tracing")]
234 tracing::trace!(nostr.bech32.hrp = %hrp_str, "dispatching NIP-19 variant");
235 match hrp_str.as_str() {
236 hrp::NPUB => Ok(Self::PublicKey(decode_pubkey(&data)?)),
237 hrp::NSEC => Ok(Self::SecretKey(decode_seckey(&data)?)),
238 hrp::NOTE => Ok(Self::EventId(decode_event_id(&data)?)),
239 hrp::NPROFILE => Ok(Self::Profile(decode_profile(&data)?)),
240 hrp::NEVENT => Ok(Self::Event(decode_nevent(&data)?)),
241 hrp::NADDR => Ok(Self::Coordinate(decode_naddr(&data)?)),
242 _ => Err(FromBech32Error::UnknownHrp(hrp_str)),
243 }
244 }
245}
246
247impl ToBech32 for PublicKey {
248 fn to_bech32(&self) -> Result<String, ToBech32Error> {
249 encode_raw(hrp::NPUB, &self.to_byte_array())
250 }
251}
252
253impl FromBech32 for PublicKey {
254 fn from_bech32(s: &str) -> Result<Self, FromBech32Error> {
255 let data = decode_with_hrp(s, hrp::NPUB)?;
256 decode_pubkey(&data)
257 }
258}
259
260impl ToBech32 for SecretKey {
261 fn to_bech32(&self) -> Result<String, ToBech32Error> {
262 encode_raw(hrp::NSEC, &self.to_byte_array())
263 }
264}
265
266impl FromBech32 for SecretKey {
267 fn from_bech32(s: &str) -> Result<Self, FromBech32Error> {
268 let data = decode_with_hrp(s, hrp::NSEC)?;
269 decode_seckey(&data)
270 }
271}
272
273impl ToBech32 for EventId {
274 fn to_bech32(&self) -> Result<String, ToBech32Error> {
275 encode_raw(hrp::NOTE, &self.to_byte_array())
276 }
277}
278
279impl FromBech32 for EventId {
280 fn from_bech32(s: &str) -> Result<Self, FromBech32Error> {
281 let data = decode_with_hrp(s, hrp::NOTE)?;
282 decode_event_id(&data)
283 }
284}
285
286impl ToBech32 for Nip19Profile {
287 fn to_bech32(&self) -> Result<String, ToBech32Error> {
288 let pk_bytes = self.public_key.to_byte_array();
289
290 let mut records: Vec<(u8, &[u8])> = Vec::with_capacity(1 + self.relays.len());
291 records.push((tlv::SPECIAL, pk_bytes.as_slice()));
292 for relay in &self.relays {
293 records.push((tlv::RELAY, relay.as_str().as_bytes()));
294 }
295
296 let payload = tlv::encode(records)?;
297 encode_raw(hrp::NPROFILE, &payload)
298 }
299}
300
301impl FromBech32 for Nip19Profile {
302 fn from_bech32(s: &str) -> Result<Self, FromBech32Error> {
303 let data = decode_with_hrp(s, hrp::NPROFILE)?;
304 decode_profile(&data)
305 }
306}
307
308impl ToBech32 for Nip19Event {
309 fn to_bech32(&self) -> Result<String, ToBech32Error> {
310 let id_bytes = self.event_id.to_byte_array();
311 let author_bytes = self.author.map(PublicKey::to_byte_array);
312 let kind_bytes = self.kind.map(|k| u32::from(k.as_u16()).to_be_bytes());
313
314 let mut records: Vec<(u8, &[u8])> = Vec::with_capacity(1 + self.relays.len() + 2);
315 records.push((tlv::SPECIAL, id_bytes.as_slice()));
316 for relay in &self.relays {
317 records.push((tlv::RELAY, relay.as_str().as_bytes()));
318 }
319 if let Some(bytes) = author_bytes.as_ref() {
320 records.push((tlv::AUTHOR, bytes.as_slice()));
321 }
322 if let Some(bytes) = kind_bytes.as_ref() {
323 records.push((tlv::KIND, bytes.as_slice()));
324 }
325
326 let payload = tlv::encode(records)?;
327 encode_raw(hrp::NEVENT, &payload)
328 }
329}
330
331impl FromBech32 for Nip19Event {
332 fn from_bech32(s: &str) -> Result<Self, FromBech32Error> {
333 let data = decode_with_hrp(s, hrp::NEVENT)?;
334 decode_nevent(&data)
335 }
336}
337
338impl ToBech32 for Nip19Coordinate {
339 fn to_bech32(&self) -> Result<String, ToBech32Error> {
340 let identifier_bytes = self.coordinate.identifier.as_bytes();
341 let author_bytes = self.coordinate.author.to_byte_array();
342 let kind_bytes = u32::from(self.coordinate.kind.as_u16()).to_be_bytes();
343
344 let mut records: Vec<(u8, &[u8])> = Vec::with_capacity(3 + self.relays.len());
345 records.push((tlv::SPECIAL, identifier_bytes));
346 for relay in &self.relays {
347 records.push((tlv::RELAY, relay.as_str().as_bytes()));
348 }
349 records.push((tlv::AUTHOR, author_bytes.as_slice()));
350 records.push((tlv::KIND, kind_bytes.as_slice()));
351
352 let payload = tlv::encode(records)?;
353 encode_raw(hrp::NADDR, &payload)
354 }
355}
356
357impl FromBech32 for Nip19Coordinate {
358 fn from_bech32(s: &str) -> Result<Self, FromBech32Error> {
359 let data = decode_with_hrp(s, hrp::NADDR)?;
360 decode_naddr(&data)
361 }
362}
363
364mod sealed {
367 use super::{
368 EventId, Nip19Coordinate, Nip19Entity, Nip19Event, Nip19Profile, PublicKey, SecretKey,
369 };
370
371 pub trait Sealed {}
374
375 impl Sealed for PublicKey {}
376 impl Sealed for SecretKey {}
377 impl Sealed for EventId {}
378 impl Sealed for Nip19Profile {}
379 impl Sealed for Nip19Event {}
380 impl Sealed for Nip19Coordinate {}
381 impl Sealed for Nip19Entity {}
382}
383
384fn encode_raw(hrp_value: &'static str, data: &[u8]) -> Result<String, ToBech32Error> {
385 let hrp = hrp::hrp_unchecked(hrp_value);
386 let encoded = bech32::encode::<Bech32>(hrp, data)?;
387 Ok(encoded)
388}
389
390fn decode_bech32(s: &str) -> Result<(String, Vec<u8>), FromBech32Error> {
391 if s.len() > MAX_NIP19_LENGTH {
395 return Err(FromBech32Error::TooLong {
396 len: s.len(),
397 max: MAX_NIP19_LENGTH,
398 });
399 }
400 let parsed = CheckedHrpstring::new::<Bech32>(s)?;
401 let hrp_str = parsed.hrp().to_lowercase();
402 let data: Vec<u8> = parsed.byte_iter().collect();
403 Ok((hrp_str, data))
404}
405
406fn decode_with_hrp(s: &str, expected: &'static str) -> Result<Vec<u8>, FromBech32Error> {
407 let (hrp_str, data) = decode_bech32(s)?;
408 if hrp_str != expected {
409 return Err(FromBech32Error::UnexpectedHrp {
410 expected,
411 got: hrp_str,
412 });
413 }
414 Ok(data)
415}
416
417fn decode_pubkey(data: &[u8]) -> Result<PublicKey, FromBech32Error> {
418 expect_len(data, 32)?;
419 Ok(PublicKey::from_slice(data)?)
420}
421
422fn decode_seckey(data: &[u8]) -> Result<SecretKey, FromBech32Error> {
423 expect_len(data, 32)?;
424 Ok(SecretKey::from_slice(data)?)
425}
426
427fn decode_event_id(data: &[u8]) -> Result<EventId, FromBech32Error> {
428 expect_len(data, 32)?;
429 Ok(EventId::from_slice(data)?)
430}
431
432const fn expect_len(data: &[u8], expected: usize) -> Result<(), FromBech32Error> {
433 if data.len() != expected {
434 return Err(FromBech32Error::InvalidPayloadLength {
435 expected,
436 got: data.len(),
437 });
438 }
439 Ok(())
440}
441
442fn decode_profile(data: &[u8]) -> Result<Nip19Profile, FromBech32Error> {
443 let mut public_key: Option<PublicKey> = None;
444 let mut relays: Vec<RelayUrl> = Vec::new();
445
446 for record in tlv::iter(data) {
447 let record = record?;
448 match record.tag {
449 tlv::SPECIAL => {
450 public_key = Some(decode_pubkey(record.value)?);
451 }
452 tlv::RELAY => {
453 relays.push(parse_relay(record.value)?);
454 }
455 _ => {} }
457 }
458
459 let public_key = public_key.ok_or(FromBech32Error::MissingTlv { tag: tlv::SPECIAL })?;
460 Ok(Nip19Profile { public_key, relays })
461}
462
463fn decode_nevent(data: &[u8]) -> Result<Nip19Event, FromBech32Error> {
464 let mut event_id: Option<EventId> = None;
465 let mut author: Option<PublicKey> = None;
466 let mut kind: Option<Kind> = None;
467 let mut relays: Vec<RelayUrl> = Vec::new();
468
469 for record in tlv::iter(data) {
470 let record = record?;
471 match record.tag {
472 tlv::SPECIAL => event_id = Some(decode_event_id(record.value)?),
473 tlv::RELAY => relays.push(parse_relay(record.value)?),
474 tlv::AUTHOR => author = Some(decode_pubkey(record.value)?),
475 tlv::KIND => kind = Some(decode_kind(record.value)?),
476 _ => {}
477 }
478 }
479
480 let event_id = event_id.ok_or(FromBech32Error::MissingTlv { tag: tlv::SPECIAL })?;
481 Ok(Nip19Event {
482 event_id,
483 author,
484 kind,
485 relays,
486 })
487}
488
489fn decode_naddr(data: &[u8]) -> Result<Nip19Coordinate, FromBech32Error> {
490 let mut identifier: Option<String> = None;
491 let mut author: Option<PublicKey> = None;
492 let mut kind: Option<Kind> = None;
493 let mut relays: Vec<RelayUrl> = Vec::new();
494
495 for record in tlv::iter(data) {
496 let record = record?;
497 match record.tag {
498 tlv::SPECIAL => identifier = Some(str::from_utf8(record.value)?.to_owned()),
499 tlv::RELAY => relays.push(parse_relay(record.value)?),
500 tlv::AUTHOR => author = Some(decode_pubkey(record.value)?),
501 tlv::KIND => kind = Some(decode_kind(record.value)?),
502 _ => {}
503 }
504 }
505
506 let identifier = identifier.ok_or(FromBech32Error::MissingTlv { tag: tlv::SPECIAL })?;
507 let author = author.ok_or(FromBech32Error::MissingTlv { tag: tlv::AUTHOR })?;
508 let kind = kind.ok_or(FromBech32Error::MissingTlv { tag: tlv::KIND })?;
509 Ok(Nip19Coordinate {
510 coordinate: crate::event::Coordinate::new(kind, author, identifier),
511 relays,
512 })
513}
514
515fn decode_kind(value: &[u8]) -> Result<Kind, FromBech32Error> {
516 let bytes: [u8; 4] = value
517 .try_into()
518 .map_err(|_| FromBech32Error::InvalidKindLength { got: value.len() })?;
519 let raw = u32::from_be_bytes(bytes);
520 let narrowed = u16::try_from(raw).map_err(|_| FromBech32Error::KindOutOfRange { raw })?;
521 Ok(Kind::from(narrowed))
522}
523
524fn parse_relay(value: &[u8]) -> Result<RelayUrl, FromBech32Error> {
525 let s = str::from_utf8(value)?;
526 Ok(RelayUrl::parse(s)?)
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532 use crate::Keys;
533
534 fn fixture_keys() -> Keys {
535 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
536 }
537
538 fn relay(url: &str) -> RelayUrl {
539 RelayUrl::parse(url).unwrap()
540 }
541
542 #[test]
543 fn npub_round_trip() {
544 let pk = *fixture_keys().public_key();
545 let encoded = pk.to_bech32().unwrap();
546 assert!(encoded.starts_with("npub1"));
547 let parsed = PublicKey::from_bech32(&encoded).unwrap();
548 assert_eq!(parsed, pk);
549 }
550
551 #[test]
552 fn nsec_round_trip() {
553 let sk = fixture_keys().secret_key().clone();
554 let encoded = sk.to_bech32().unwrap();
555 assert!(encoded.starts_with("nsec1"));
556 let parsed = SecretKey::from_bech32(&encoded).unwrap();
557 assert_eq!(parsed, sk);
558 }
559
560 #[test]
561 fn note_round_trip() {
562 let id = EventId::from_byte_array([0xab; 32]);
563 let encoded = id.to_bech32().unwrap();
564 assert!(encoded.starts_with("note1"));
565 let parsed = EventId::from_bech32(&encoded).unwrap();
566 assert_eq!(parsed, id);
567 }
568
569 #[test]
570 fn nprofile_round_trip_with_relays() {
571 let pk = *fixture_keys().public_key();
572 let profile = Nip19Profile::new(pk, [relay("wss://relay.one"), relay("wss://relay.two")]);
573 let encoded = profile.to_bech32().unwrap();
574 assert!(encoded.starts_with("nprofile1"));
575 let parsed = Nip19Profile::from_bech32(&encoded).unwrap();
576 assert_eq!(parsed, profile);
577 }
578
579 #[test]
580 fn nprofile_round_trip_without_relays() {
581 let pk = *fixture_keys().public_key();
582 let profile = Nip19Profile::new(pk, []);
583 let encoded = profile.to_bech32().unwrap();
584 let parsed = Nip19Profile::from_bech32(&encoded).unwrap();
585 assert_eq!(parsed, profile);
586 }
587
588 #[test]
589 fn nevent_round_trip_full() {
590 let pk = *fixture_keys().public_key();
591 let event = Nip19Event::new(EventId::from_byte_array([0xab; 32]))
592 .with_author(pk)
593 .with_kind(Kind::TEXT_NOTE)
594 .with_relays([relay("wss://relay.example")]);
595 let encoded = event.to_bech32().unwrap();
596 assert!(encoded.starts_with("nevent1"));
597 let parsed = Nip19Event::from_bech32(&encoded).unwrap();
598 assert_eq!(parsed, event);
599 }
600
601 #[test]
602 fn nevent_round_trip_minimal() {
603 let event = Nip19Event::new(EventId::from_byte_array([0x01; 32]));
604 let encoded = event.to_bech32().unwrap();
605 let parsed = Nip19Event::from_bech32(&encoded).unwrap();
606 assert_eq!(parsed, event);
607 }
608
609 #[test]
610 fn naddr_round_trip() {
611 let pk = *fixture_keys().public_key();
612 let coord = Nip19Coordinate::new(
613 "long-form-1",
614 pk,
615 Kind::from(30_023_u16),
616 [relay("wss://relay.example")],
617 );
618 let encoded = coord.to_bech32().unwrap();
619 assert!(encoded.starts_with("naddr1"));
620 let parsed = Nip19Coordinate::from_bech32(&encoded).unwrap();
621 assert_eq!(parsed, coord);
622 }
623
624 #[test]
625 fn entity_dispatch_npub() {
626 let pk = *fixture_keys().public_key();
627 let s = pk.to_bech32().unwrap();
628 let parsed = Nip19Entity::from_bech32(&s).unwrap();
629 assert_eq!(parsed, Nip19Entity::PublicKey(pk));
630 }
631
632 #[test]
633 fn entity_dispatch_naddr() {
634 let pk = *fixture_keys().public_key();
635 let coord = Nip19Coordinate::new("alpha", pk, Kind::from(30_001_u16), []);
636 let s = coord.to_bech32().unwrap();
637 let parsed = Nip19Entity::from_bech32(&s).unwrap();
638 assert_eq!(parsed, Nip19Entity::Coordinate(coord));
639 }
640
641 #[test]
642 fn entity_round_trip_via_to_bech32() {
643 let pk = *fixture_keys().public_key();
644 let entity = Nip19Entity::PublicKey(pk);
645 let s = entity.to_bech32().unwrap();
646 assert_eq!(Nip19Entity::from_bech32(&s).unwrap(), entity);
647 }
648
649 #[test]
650 fn unexpected_hrp_is_rejected() {
651 let pk = *fixture_keys().public_key();
652 let s = pk.to_bech32().unwrap();
653 let err = SecretKey::from_bech32(&s).unwrap_err();
654 assert!(matches!(
655 err,
656 FromBech32Error::UnexpectedHrp {
657 expected: hrp::NSEC,
658 ..
659 }
660 ));
661 }
662
663 #[test]
664 fn unknown_hrp_is_rejected() {
665 let hrp = bech32::Hrp::parse("xyz").unwrap();
667 let bogus = bech32::encode::<Bech32>(hrp, &[0u8; 32]).unwrap();
668 let err = Nip19Entity::from_bech32(&bogus).unwrap_err();
669 assert!(matches!(err, FromBech32Error::UnknownHrp(s) if s == "xyz"));
670 }
671
672 #[test]
673 fn malformed_string_is_rejected() {
674 let err = Nip19Entity::from_bech32("definitely not bech32").unwrap_err();
675 assert!(matches!(err, FromBech32Error::Decode(_)));
676 }
677
678 #[test]
679 fn missing_required_tlv_is_rejected() {
680 let payload = tlv::encode([(tlv::RELAY, b"wss://relay.example".as_slice())]).unwrap();
682 let encoded = encode_raw(hrp::NPROFILE, &payload).unwrap();
683 let err = Nip19Profile::from_bech32(&encoded).unwrap_err();
684 assert!(matches!(
685 err,
686 FromBech32Error::MissingTlv { tag: tlv::SPECIAL }
687 ));
688 }
689
690 #[test]
691 fn naddr_kind_above_u16_is_rejected_not_truncated() {
692 let pk = *fixture_keys().public_key();
703 let oversized_kind: u32 = 70_000; let payload = tlv::encode([
705 (tlv::SPECIAL, b"alpha".as_slice()),
706 (tlv::AUTHOR, pk.to_byte_array().as_slice()),
707 (tlv::KIND, oversized_kind.to_be_bytes().as_slice()),
708 ])
709 .unwrap();
710 let encoded = encode_raw(hrp::NADDR, &payload).unwrap();
711 let err = Nip19Entity::from_bech32(&encoded).unwrap_err();
712 assert!(matches!(
713 err,
714 FromBech32Error::KindOutOfRange { raw } if raw == oversized_kind
715 ));
716 }
717
718 mod nip19_vectors {
722 use super::*;
723
724 #[test]
725 fn npub_matches_spec() {
726 let pubkey = PublicKey::parse(
727 "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e",
728 )
729 .unwrap();
730 let expected = "npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg";
731 assert_eq!(pubkey.to_bech32().unwrap(), expected);
732 assert_eq!(PublicKey::from_bech32(expected).unwrap(), pubkey);
733 }
734
735 #[test]
736 fn nsec_matches_spec() {
737 let sk = SecretKey::parse(
738 "67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa",
739 )
740 .unwrap();
741 let expected = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5";
742 assert_eq!(sk.to_bech32().unwrap(), expected);
743 assert_eq!(SecretKey::from_bech32(expected).unwrap(), sk);
744 }
745
746 #[test]
747 fn nprofile_round_trip_with_canonical_relays() {
748 let pk = PublicKey::parse(
754 "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
755 )
756 .unwrap();
757 let profile = Nip19Profile::new(
758 pk,
759 [
760 RelayUrl::parse("wss://r.x.com/").unwrap(),
761 RelayUrl::parse("wss://djbas.sadkb.com/").unwrap(),
762 ],
763 );
764 let expected = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gppemhxue69uhhytnc9e3k7mf0qyt8wumn8ghj7er2vfshxtnnv9jxkc3wvdhk6tclr7lsh";
765 assert_eq!(profile.to_bech32().unwrap(), expected);
766 assert_eq!(Nip19Profile::from_bech32(expected).unwrap(), profile);
767 }
768 }
769
770 mod cross_impl_fixtures {
777 use super::*;
778
779 #[test]
783 fn habla_news_naddr_decodes() {
784 let raw = "naddr1qq98yetxv4ex2mnrv4esygrl54h466tz4v0re4pyuavvxqptsejl0vxcmnhfl60z3rth2xkpjspsgqqqw4rsf34vl5";
785 let decoded = Nip19Coordinate::from_bech32(raw).unwrap();
786 assert_eq!(
787 decoded.coordinate.author.to_hex(),
788 "7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194"
789 );
790 assert_eq!(decoded.coordinate.kind.as_u16(), 30_023);
791 assert_eq!(decoded.coordinate.identifier, "references");
792 assert!(decoded.relays.is_empty());
793 }
794
795 #[test]
800 fn go_nostr_naddr_with_alternate_tlv_ordering_decodes() {
801 let raw = "naddr1qqrxyctwv9hxzq3q80cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsxpqqqp65wqfwwaehxw309aex2mrp0yhxummnw3ezuetcv9khqmr99ekhjer0d4skjm3wv4uxzmtsd3jjucm0d5q3vamnwvaz7tmwdaehgu3wvfskuctwvyhxxmmd0zfmwx";
802 let decoded = Nip19Coordinate::from_bech32(raw).unwrap();
803 assert_eq!(
804 decoded.coordinate.author.to_hex(),
805 "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
806 );
807 assert_eq!(decoded.coordinate.kind.as_u16(), 30_023);
808 assert_eq!(decoded.coordinate.identifier, "banana");
809 let relay_strs: Vec<&str> = decoded.relays.iter().map(RelayUrl::as_str).collect();
812 assert!(
813 relay_strs
814 .iter()
815 .any(|r| r == &"wss://relay.nostr.example.mydomain.example.com/"),
816 "missing primary relay; got {relay_strs:?}",
817 );
818 assert!(
819 relay_strs.iter().any(|r| r == &"wss://nostr.banana.com/"),
820 "missing secondary relay; got {relay_strs:?}",
821 );
822 }
823
824 #[test]
829 fn nostr_tools_nprofile_no_relays_decodes() {
830 let raw = "nprofile1qqsvc6ulagpn7kwrcwdqgp797xl7usumqa6s3kgcelwq6m75x8fe8yc5usxdg";
831 let decoded = Nip19Profile::from_bech32(raw).unwrap();
832 assert!(decoded.relays.is_empty());
833 assert_eq!(decoded.public_key.to_byte_array().len(), 32);
839 }
840
841 #[test]
844 fn nostr_tools_nevent_with_relays_decodes() {
845 let raw = "nevent1qqst8cujky046negxgwwm5ynqwn53t8aqjr6afd8g59nfqwxpdhylpcpzamhxue69uhhyetvv9ujuetcv9khqmr99e3k7mg8arnc9";
846 let decoded = Nip19Event::from_bech32(raw).unwrap();
847 assert_eq!(decoded.event_id.to_byte_array().len(), 32);
850 assert!(!decoded.relays.is_empty(), "fixture carries relay hints");
851 }
852 }
853
854 #[test]
855 fn rejects_input_above_max_length() {
856 let oversized: String = std::iter::repeat_n('q', MAX_NIP19_LENGTH + 1).collect();
860 let err = Nip19Entity::from_bech32(&oversized).unwrap_err();
861 assert!(matches!(
862 err,
863 FromBech32Error::TooLong {
864 len,
865 max: MAX_NIP19_LENGTH,
866 } if len == MAX_NIP19_LENGTH + 1
867 ));
868 }
869
870 #[test]
871 fn accepts_input_at_max_length_boundary() {
872 let pk = *fixture_keys().public_key();
876 let mut s = pk.to_bech32().unwrap();
877 while s.len() < MAX_NIP19_LENGTH {
878 s.push('q');
879 }
880 assert_eq!(s.len(), MAX_NIP19_LENGTH);
881 let err = Nip19Entity::from_bech32(&s).unwrap_err();
882 assert!(!matches!(err, FromBech32Error::TooLong { .. }));
884 }
885
886 #[test]
887 fn unknown_tlv_tag_is_ignored_for_forward_compat() {
888 let pk = *fixture_keys().public_key();
889 let pk_bytes = pk.to_byte_array();
890 let future_value: &[u8] = b"future";
891 let payload =
892 tlv::encode([(tlv::SPECIAL, pk_bytes.as_slice()), (250_u8, future_value)]).unwrap();
893 let encoded = encode_raw(hrp::NPROFILE, &payload).unwrap();
894 let parsed = Nip19Profile::from_bech32(&encoded).unwrap();
895 assert_eq!(parsed.public_key, pk);
896 }
897}