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 }