notecrumbs

a nostr opengraph server build on nostrdb and egui
git clone git://jb55.com/notecrumbs
Log | Files | Refs | README | LICENSE

nip19.rs (8193B)


      1 use nostr::nips::nip19::{Nip19, Nip19Coordinate, Nip19Event};
      2 use nostr_sdk::prelude::*;
      3 
      4 /// Do we have relays for this request? If so we can use these when
      5 /// looking for missing data
      6 pub fn nip19_relays(nip19: &Nip19) -> Vec<RelayUrl> {
      7     match nip19 {
      8         Nip19::Event(ev) => ev.relays.clone(),
      9         Nip19::Coordinate(coord) => coord.relays.clone(),
     10         Nip19::Profile(p) => p.relays.clone(),
     11         _ => vec![],
     12     }
     13 }
     14 
     15 /// Generate a bech32 string with source relay hints.
     16 /// If source_relays is empty, uses the original nip19 relays.
     17 /// Otherwise, replaces the relays with source_relays.
     18 /// Preserves author/kind fields when present.
     19 pub fn bech32_with_relays(nip19: &Nip19, source_relays: &[RelayUrl]) -> Option<String> {
     20     // If no source relays, use original
     21     if source_relays.is_empty() {
     22         return nip19.to_bech32().ok();
     23     }
     24 
     25     match nip19 {
     26         Nip19::Event(ev) => {
     27             // Preserve author and kind from original nevent
     28             let mut new_event = Nip19Event::new(ev.event_id).relays(source_relays.iter().cloned());
     29             if let Some(author) = ev.author {
     30                 new_event = new_event.author(author);
     31             }
     32             if let Some(kind) = ev.kind {
     33                 new_event = new_event.kind(kind);
     34             }
     35             new_event
     36                 .to_bech32()
     37                 .ok()
     38                 .or_else(|| nip19.to_bech32().ok())
     39         }
     40         Nip19::Coordinate(coord) => {
     41             let new_coord =
     42                 Nip19Coordinate::new(coord.coordinate.clone(), source_relays.iter().cloned());
     43             new_coord
     44                 .to_bech32()
     45                 .ok()
     46                 .or_else(|| nip19.to_bech32().ok())
     47         }
     48         // For other types (note, pubkey), just use original - they don't support relays
     49         _ => nip19.to_bech32().ok(),
     50     }
     51 }
     52 
     53 #[cfg(test)]
     54 mod tests {
     55     use super::*;
     56     use nostr::nips::nip01::Coordinate;
     57     use nostr::prelude::Keys;
     58 
     59     #[test]
     60     fn bech32_with_relays_adds_relay_to_nevent() {
     61         let event_id = EventId::from_slice(&[1u8; 32]).unwrap();
     62         let nip19_event = Nip19Event::new(event_id);
     63         let nip19 = Nip19::Event(nip19_event);
     64 
     65         let source_relays = vec![RelayUrl::parse("wss://relay.damus.io").unwrap()];
     66         let result = bech32_with_relays(&nip19, &source_relays).expect("should encode");
     67 
     68         // Result should be longer than original (includes relay hint)
     69         let original = nip19.to_bech32().unwrap();
     70         assert!(
     71             result.len() > original.len(),
     72             "bech32 with relay should be longer"
     73         );
     74 
     75         // Decode and verify relay is included
     76         let decoded = Nip19::from_bech32(&result).unwrap();
     77         match decoded {
     78             Nip19::Event(ev) => {
     79                 assert!(!ev.relays.is_empty(), "should have relay hints");
     80                 assert!(ev.relays[0].to_string().contains("relay.damus.io"));
     81             }
     82             _ => panic!("expected Nip19::Event"),
     83         }
     84     }
     85 
     86     #[test]
     87     fn bech32_with_relays_adds_relay_to_naddr() {
     88         let keys = Keys::generate();
     89         let coordinate =
     90             Coordinate::new(Kind::LongFormTextNote, keys.public_key()).identifier("test-article");
     91         let nip19_coord = Nip19Coordinate::new(coordinate.clone(), Vec::<RelayUrl>::new());
     92         let nip19 = Nip19::Coordinate(nip19_coord);
     93 
     94         let source_relays = vec![RelayUrl::parse("wss://nostr.wine").unwrap()];
     95         let result = bech32_with_relays(&nip19, &source_relays).expect("should encode");
     96 
     97         // Result should be longer than original (includes relay hint)
     98         let original = nip19.to_bech32().unwrap();
     99         assert!(
    100             result.len() > original.len(),
    101             "bech32 with relay should be longer"
    102         );
    103 
    104         // Decode and verify relay is included
    105         let decoded = Nip19::from_bech32(&result).unwrap();
    106         match decoded {
    107             Nip19::Coordinate(coord) => {
    108                 assert!(!coord.relays.is_empty(), "should have relay hints");
    109                 assert!(coord.relays[0].to_string().contains("nostr.wine"));
    110             }
    111             _ => panic!("expected Nip19::Coordinate"),
    112         }
    113     }
    114 
    115     #[test]
    116     fn bech32_with_relays_empty_returns_original() {
    117         let event_id = EventId::from_slice(&[2u8; 32]).unwrap();
    118         let relay = RelayUrl::parse("wss://original.relay").unwrap();
    119         let nip19_event = Nip19Event::new(event_id).relays([relay.clone()]);
    120         let nip19 = Nip19::Event(nip19_event);
    121 
    122         // Empty source_relays should preserve original
    123         let result = bech32_with_relays(&nip19, &[]).expect("should encode");
    124         let original = nip19.to_bech32().unwrap();
    125 
    126         assert_eq!(
    127             result, original,
    128             "empty source_relays should return original bech32"
    129         );
    130     }
    131 
    132     #[test]
    133     fn bech32_with_relays_replaces_existing_relays() {
    134         let event_id = EventId::from_slice(&[3u8; 32]).unwrap();
    135         let original_relay = RelayUrl::parse("wss://original.relay").unwrap();
    136         let nip19_event = Nip19Event::new(event_id).relays([original_relay]);
    137         let nip19 = Nip19::Event(nip19_event);
    138 
    139         let new_relay = RelayUrl::parse("wss://new.relay").unwrap();
    140         let result = bech32_with_relays(&nip19, &[new_relay.clone()]).expect("should encode");
    141 
    142         // Decode and verify new relay replaced original
    143         let decoded = Nip19::from_bech32(&result).unwrap();
    144         match decoded {
    145             Nip19::Event(ev) => {
    146                 assert_eq!(ev.relays.len(), 1, "should have exactly one relay");
    147                 assert!(ev.relays[0].to_string().contains("new.relay"));
    148             }
    149             _ => panic!("expected Nip19::Event"),
    150         }
    151     }
    152 
    153     #[test]
    154     fn bech32_with_relays_preserves_author_and_kind() {
    155         let event_id = EventId::from_slice(&[5u8; 32]).unwrap();
    156         let keys = Keys::generate();
    157         let nip19_event = Nip19Event::new(event_id)
    158             .author(keys.public_key())
    159             .kind(Kind::TextNote);
    160         let nip19 = Nip19::Event(nip19_event);
    161 
    162         let source_relays = vec![RelayUrl::parse("wss://test.relay").unwrap()];
    163         let result = bech32_with_relays(&nip19, &source_relays).expect("should encode");
    164 
    165         // Decode and verify author/kind are preserved
    166         let decoded = Nip19::from_bech32(&result).unwrap();
    167         match decoded {
    168             Nip19::Event(ev) => {
    169                 assert!(ev.author.is_some(), "author should be preserved");
    170                 assert_eq!(ev.author.unwrap(), keys.public_key());
    171                 assert!(ev.kind.is_some(), "kind should be preserved");
    172                 assert_eq!(ev.kind.unwrap(), Kind::TextNote);
    173                 assert!(!ev.relays.is_empty(), "should have relay");
    174             }
    175             _ => panic!("expected Nip19::Event"),
    176         }
    177     }
    178 
    179     #[test]
    180     fn nip19_relays_extracts_from_event() {
    181         let event_id = EventId::from_slice(&[4u8; 32]).unwrap();
    182         let relay = RelayUrl::parse("wss://test.relay").unwrap();
    183         let nip19_event = Nip19Event::new(event_id).relays([relay.clone()]);
    184         let nip19 = Nip19::Event(nip19_event);
    185 
    186         let relays = nip19_relays(&nip19);
    187         assert_eq!(relays.len(), 1);
    188         assert!(relays[0].to_string().contains("test.relay"));
    189     }
    190 
    191     #[test]
    192     fn nip19_relays_extracts_from_coordinate() {
    193         let keys = Keys::generate();
    194         let coordinate =
    195             Coordinate::new(Kind::LongFormTextNote, keys.public_key()).identifier("article");
    196         let relay = RelayUrl::parse("wss://coord.relay").unwrap();
    197         let nip19_coord = Nip19Coordinate::new(coordinate, [relay.clone()]);
    198         let nip19 = Nip19::Coordinate(nip19_coord);
    199 
    200         let relays = nip19_relays(&nip19);
    201         assert_eq!(relays.len(), 1);
    202         assert!(relays[0].to_string().contains("coord.relay"));
    203     }
    204 
    205     #[test]
    206     fn nip19_relays_returns_empty_for_pubkey() {
    207         let keys = Keys::generate();
    208         let nip19 = Nip19::Pubkey(keys.public_key());
    209 
    210         let relays = nip19_relays(&nip19);
    211         assert!(relays.is_empty(), "pubkey nip19 should have no relays");
    212     }
    213 }