1use bech32::{Bech32, Hrp};
4
5use crate::{NostrEventId, NostrEventKind, NostrPublicKey, SoftchatError};
6
7pub const MAX_NIP19_IDENTIFIER_CHARS: usize = 5_000;
11
12pub const MAX_NIP19_RELAYS: usize = 8;
14
15const MAX_RELAY_HINT_BYTES: usize = 255;
16const TLV_SPECIAL: u8 = 0;
17const TLV_RELAY: u8 = 1;
18const TLV_AUTHOR: u8 = 2;
19const TLV_KIND: u8 = 3;
20
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23#[cfg_attr(feature = "native-bindings", derive(uniffi::Enum))]
24pub enum Nip19IdentifierKind {
25 PublicKey,
27 EventId,
29 Profile,
31 Event,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
38pub struct Nip19Tlv {
39 pub type_id: u32,
41 pub value: Vec<u8>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct Nip19Identifier {
48 kind: Nip19IdentifierKind,
49 primary_hex: String,
50 relays: Vec<String>,
51 author_hex: Option<String>,
52 event_kind: Option<NostrEventKind>,
53 unknown_tlvs: Vec<Nip19Tlv>,
54}
55
56impl Nip19Identifier {
57 pub fn parse(encoded: &str) -> Result<Self, SoftchatError> {
67 validate_encoded_shape(encoded)?;
68 let (hrp, data) =
69 bech32::decode(encoded).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
70 match hrp.as_str() {
71 "npub" => parse_bare(Nip19IdentifierKind::PublicKey, &data),
72 "note" => parse_bare(Nip19IdentifierKind::EventId, &data),
73 "nprofile" => parse_tlv(Nip19IdentifierKind::Profile, &data),
74 "nevent" => parse_tlv(Nip19IdentifierKind::Event, &data),
75 _ => Err(SoftchatError::InvalidNip19Identifier),
76 }
77 }
78
79 pub fn encode(&self) -> Result<String, SoftchatError> {
86 let (hrp, bytes) = match self.kind {
87 Nip19IdentifierKind::PublicKey => {
88 let key = NostrPublicKey::from_hex(&self.primary_hex)
89 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
90 ("npub", key.as_inner().to_bytes().to_vec())
91 }
92 Nip19IdentifierKind::EventId => {
93 let id = NostrEventId::from_hex(&self.primary_hex)
94 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
95 ("note", id.as_inner().as_bytes().to_vec())
96 }
97 Nip19IdentifierKind::Profile | Nip19IdentifierKind::Event => {
98 let bytes = self.encode_tlvs()?;
99 let hrp = if self.kind == Nip19IdentifierKind::Profile {
100 "nprofile"
101 } else {
102 "nevent"
103 };
104 (hrp, bytes)
105 }
106 };
107 let hrp = Hrp::parse(hrp).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
108 let encoded = bech32::encode::<Bech32>(hrp, &bytes)
109 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
110 validate_encoded_shape(&encoded)?;
111 Ok(encoded)
112 }
113
114 #[must_use]
116 pub const fn kind(&self) -> Nip19IdentifierKind {
117 self.kind
118 }
119
120 #[must_use]
122 pub fn primary_hex(&self) -> &str {
123 &self.primary_hex
124 }
125
126 #[must_use]
128 pub fn relays(&self) -> &[String] {
129 &self.relays
130 }
131
132 #[must_use]
134 pub fn author_hex(&self) -> Option<&str> {
135 self.author_hex.as_deref()
136 }
137
138 #[must_use]
140 pub const fn event_kind(&self) -> Option<NostrEventKind> {
141 self.event_kind
142 }
143
144 #[must_use]
146 pub fn unknown_tlvs(&self) -> &[Nip19Tlv] {
147 &self.unknown_tlvs
148 }
149
150 fn encode_tlvs(&self) -> Result<Vec<u8>, SoftchatError> {
151 let mut output = Vec::new();
152 let special = match self.kind {
153 Nip19IdentifierKind::Profile => NostrPublicKey::from_hex(&self.primary_hex)
154 .map(|key| key.as_inner().to_bytes().to_vec()),
155 Nip19IdentifierKind::Event => NostrEventId::from_hex(&self.primary_hex)
156 .map(|id| id.as_inner().as_bytes().to_vec()),
157 Nip19IdentifierKind::PublicKey | Nip19IdentifierKind::EventId => {
158 return Err(SoftchatError::InvalidNip19Identifier);
159 }
160 }
161 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
162 push_tlv(&mut output, TLV_SPECIAL, &special)?;
163 validate_relays(&self.relays)?;
164 for relay in &self.relays {
165 push_tlv(&mut output, TLV_RELAY, relay.as_bytes())?;
166 }
167 if self.kind == Nip19IdentifierKind::Profile
168 && (self.author_hex.is_some() || self.event_kind.is_some())
169 {
170 return Err(SoftchatError::InvalidNip19Identifier);
171 }
172 if let Some(author) = &self.author_hex {
173 let author = NostrPublicKey::from_hex(author)
174 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
175 push_tlv(&mut output, TLV_AUTHOR, &author.as_inner().to_bytes())?;
176 }
177 if let Some(kind) = self.event_kind {
178 push_tlv(
179 &mut output,
180 TLV_KIND,
181 &u32::from(kind.as_u16()).to_be_bytes(),
182 )?;
183 }
184 for unknown in &self.unknown_tlvs {
185 let type_id =
186 u8::try_from(unknown.type_id).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
187 if matches!(type_id, TLV_SPECIAL | TLV_RELAY | TLV_AUTHOR | TLV_KIND) {
188 return Err(SoftchatError::InvalidNip19Identifier);
189 }
190 push_tlv(&mut output, type_id, &unknown.value)?;
191 }
192 Ok(output)
193 }
194}
195
196#[derive(Clone, Debug, Eq, PartialEq)]
198#[cfg_attr(feature = "native-bindings", derive(uniffi::Record))]
199pub struct Nip19Value {
200 pub kind: Nip19IdentifierKind,
202 pub primary_hex: String,
204 pub relays: Vec<String>,
206 pub author_hex: Option<String>,
208 pub event_kind: Option<u32>,
210 pub unknown_tlvs: Vec<Nip19Tlv>,
212}
213
214impl From<&Nip19Identifier> for Nip19Value {
215 fn from(value: &Nip19Identifier) -> Self {
216 Self {
217 kind: value.kind,
218 primary_hex: value.primary_hex.clone(),
219 relays: value.relays.clone(),
220 author_hex: value.author_hex.clone(),
221 event_kind: value.event_kind.map(|kind| u32::from(kind.as_u16())),
222 unknown_tlvs: value.unknown_tlvs.clone(),
223 }
224 }
225}
226
227impl TryFrom<Nip19Value> for Nip19Identifier {
228 type Error = SoftchatError;
229
230 fn try_from(value: Nip19Value) -> Result<Self, Self::Error> {
231 let event_kind = value
232 .event_kind
233 .map(NostrEventKind::try_from_u32)
234 .transpose()
235 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
236 let value = Self {
237 kind: value.kind,
238 primary_hex: value.primary_hex,
239 relays: value.relays,
240 author_hex: value.author_hex,
241 event_kind,
242 unknown_tlvs: value.unknown_tlvs,
243 };
244 value.encode()?;
245 Ok(value)
246 }
247}
248
249#[cfg_attr(feature = "native-bindings", uniffi::export)]
251pub fn parse_nip19_identifier(encoded: String) -> Result<Nip19Value, SoftchatError> {
252 Nip19Identifier::parse(&encoded).map(|value| Nip19Value::from(&value))
253}
254
255#[cfg_attr(feature = "native-bindings", uniffi::export)]
257pub fn encode_nip19_identifier(value: Nip19Value) -> Result<String, SoftchatError> {
258 Nip19Identifier::try_from(value)?.encode()
259}
260
261fn validate_encoded_shape(encoded: &str) -> Result<(), SoftchatError> {
262 if encoded.is_empty() || encoded.len() > MAX_NIP19_IDENTIFIER_CHARS {
263 return Err(SoftchatError::InvalidNip19Identifier);
264 }
265 let has_lower = encoded.bytes().any(|byte| byte.is_ascii_lowercase());
266 let has_upper = encoded.bytes().any(|byte| byte.is_ascii_uppercase());
267 if has_lower && has_upper {
268 return Err(SoftchatError::InvalidNip19Identifier);
269 }
270 Ok(())
271}
272
273fn parse_bare(kind: Nip19IdentifierKind, data: &[u8]) -> Result<Nip19Identifier, SoftchatError> {
274 if data.len() != 32 {
275 return Err(SoftchatError::InvalidNip19Identifier);
276 }
277 let primary_hex = match kind {
278 Nip19IdentifierKind::PublicKey => {
279 NostrPublicKey::from_hex(&hex::encode(data)).map(|key| key.to_hex())
280 }
281 Nip19IdentifierKind::EventId => {
282 NostrEventId::from_hex(&hex::encode(data)).map(NostrEventId::to_hex)
283 }
284 Nip19IdentifierKind::Profile | Nip19IdentifierKind::Event => {
285 return Err(SoftchatError::InvalidNip19Identifier);
286 }
287 }
288 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
289 Ok(Nip19Identifier {
290 kind,
291 primary_hex,
292 relays: Vec::new(),
293 author_hex: None,
294 event_kind: None,
295 unknown_tlvs: Vec::new(),
296 })
297}
298
299fn parse_tlv(kind: Nip19IdentifierKind, data: &[u8]) -> Result<Nip19Identifier, SoftchatError> {
300 let mut cursor = data;
301 let mut primary_hex = None;
302 let mut relays = Vec::new();
303 let mut author_hex = None;
304 let mut event_kind = None;
305 let mut unknown_tlvs = Vec::new();
306 while !cursor.is_empty() {
307 let type_id = *cursor
308 .first()
309 .ok_or(SoftchatError::InvalidNip19Identifier)?;
310 let length = usize::from(*cursor.get(1).ok_or(SoftchatError::InvalidNip19Identifier)?);
311 let value = cursor
312 .get(2..2 + length)
313 .ok_or(SoftchatError::InvalidNip19Identifier)?;
314 cursor = cursor
315 .get(2 + length..)
316 .ok_or(SoftchatError::InvalidNip19Identifier)?;
317 match type_id {
318 TLV_SPECIAL => {
319 if primary_hex.is_some() || value.len() != 32 {
320 return Err(SoftchatError::InvalidNip19Identifier);
321 }
322 let hex = hex::encode(value);
323 primary_hex = Some(match kind {
324 Nip19IdentifierKind::Profile => NostrPublicKey::from_hex(&hex)
325 .map(|key| key.to_hex())
326 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
327 Nip19IdentifierKind::Event => NostrEventId::from_hex(&hex)
328 .map(NostrEventId::to_hex)
329 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
330 Nip19IdentifierKind::PublicKey | Nip19IdentifierKind::EventId => {
331 return Err(SoftchatError::InvalidNip19Identifier);
332 }
333 });
334 }
335 TLV_RELAY => {
336 let relay = std::str::from_utf8(value)
337 .map_err(|_| SoftchatError::InvalidNip19Identifier)?
338 .to_owned();
339 validate_relay(&relay)?;
340 relays.push(relay);
341 if relays.len() > MAX_NIP19_RELAYS {
342 return Err(SoftchatError::InvalidNip19Identifier);
343 }
344 }
345 TLV_AUTHOR if kind == Nip19IdentifierKind::Event => {
346 if author_hex.is_some() || value.len() != 32 {
347 return Err(SoftchatError::InvalidNip19Identifier);
348 }
349 author_hex = Some(
350 NostrPublicKey::from_hex(&hex::encode(value))
351 .map(|key| key.to_hex())
352 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
353 );
354 }
355 TLV_KIND if kind == Nip19IdentifierKind::Event => {
356 if event_kind.is_some() || value.len() != 4 {
357 return Err(SoftchatError::InvalidNip19Identifier);
358 }
359 let bytes: [u8; 4] = value
360 .try_into()
361 .map_err(|_| SoftchatError::InvalidNip19Identifier)?;
362 event_kind = Some(
363 NostrEventKind::try_from_u32(u32::from_be_bytes(bytes))
364 .map_err(|_| SoftchatError::InvalidNip19Identifier)?,
365 );
366 }
367 _ => unknown_tlvs.push(Nip19Tlv {
368 type_id: u32::from(type_id),
369 value: value.to_vec(),
370 }),
371 }
372 }
373 validate_relays(&relays)?;
374 Ok(Nip19Identifier {
375 kind,
376 primary_hex: primary_hex.ok_or(SoftchatError::InvalidNip19Identifier)?,
377 relays,
378 author_hex,
379 event_kind,
380 unknown_tlvs,
381 })
382}
383
384fn validate_relays(relays: &[String]) -> Result<(), SoftchatError> {
385 if relays.len() > MAX_NIP19_RELAYS {
386 return Err(SoftchatError::InvalidNip19Identifier);
387 }
388 for relay in relays {
389 validate_relay(relay)?;
390 }
391 Ok(())
392}
393
394fn validate_relay(relay: &str) -> Result<(), SoftchatError> {
395 if relay.is_empty()
396 || relay.len() > MAX_RELAY_HINT_BYTES
397 || (!relay.starts_with("ws://") && !relay.starts_with("wss://"))
398 || nostr::RelayUrl::parse(relay).is_err()
399 {
400 return Err(SoftchatError::InvalidNip19Identifier);
401 }
402 Ok(())
403}
404
405fn push_tlv(output: &mut Vec<u8>, type_id: u8, value: &[u8]) -> Result<(), SoftchatError> {
406 let length = u8::try_from(value.len()).map_err(|_| SoftchatError::InvalidNip19Identifier)?;
407 output.push(type_id);
408 output.push(length);
409 output.extend_from_slice(value);
410 Ok(())
411}
412
413#[cfg(test)]
414mod tests {
415 use serde::Deserialize;
416
417 use super::*;
418
419 #[derive(Deserialize)]
420 #[serde(rename_all = "camelCase")]
421 struct BareExample {
422 encoded: String,
423 public_key: String,
424 }
425
426 #[derive(Deserialize)]
427 #[serde(rename_all = "camelCase")]
428 struct ProfileExample {
429 encoded: String,
430 public_key: String,
431 relays: Vec<String>,
432 }
433
434 #[derive(Deserialize)]
435 struct Examples {
436 npub: BareExample,
437 nprofile: ProfileExample,
438 }
439
440 #[derive(Deserialize)]
441 struct Fixture {
442 examples: Examples,
443 }
444
445 fn official_fixture() -> Result<Fixture, serde_json::Error> {
446 serde_json::from_str(include_str!(
447 "../../../fixtures/nostr/nip19/nip19-official.json"
448 ))
449 }
450
451 #[test]
452 fn parses_official_public_key_and_profile_examples() -> Result<(), SoftchatError> {
453 let fixture = official_fixture().map_err(|_| SoftchatError::InvalidNip19Identifier)?;
454 let public_key = Nip19Identifier::parse(&fixture.examples.npub.encoded)?;
455 assert_eq!(public_key.kind(), Nip19IdentifierKind::PublicKey);
456 assert_eq!(public_key.primary_hex(), fixture.examples.npub.public_key);
457 assert_eq!(public_key.encode()?, fixture.examples.npub.encoded);
458
459 let profile = Nip19Identifier::parse(&fixture.examples.nprofile.encoded)?;
460 assert_eq!(profile.kind(), Nip19IdentifierKind::Profile);
461 assert_eq!(profile.primary_hex(), fixture.examples.nprofile.public_key);
462 assert_eq!(profile.relays(), fixture.examples.nprofile.relays);
463 assert_eq!(profile.encode()?, fixture.examples.nprofile.encoded);
464 Ok(())
465 }
466
467 #[test]
468 fn round_trips_nevent_unknown_tlv_and_rejects_mixed_case() -> Result<(), SoftchatError> {
469 let value = Nip19Value {
470 kind: Nip19IdentifierKind::Event,
471 primary_hex: "44900586091b284416a0c001f677f9c49f7639a55c3f1e2ec130a8e1a7998e1b"
472 .to_owned(),
473 relays: vec!["wss://relay.example".to_owned()],
474 author_hex: Some(
475 "918e2da906df4ccd12c8ac672d8335add131a4cf9d27ce42b3bb3625755f0788".to_owned(),
476 ),
477 event_kind: Some(14),
478 unknown_tlvs: vec![Nip19Tlv {
479 type_id: 99,
480 value: vec![0, 1, 2],
481 }],
482 };
483 let encoded = encode_nip19_identifier(value.clone())?;
484 assert_eq!(parse_nip19_identifier(encoded.clone())?, value);
485
486 let mut mixed = encoded;
487 mixed.replace_range(0..1, "N");
488 assert!(matches!(
489 Nip19Identifier::parse(&mixed),
490 Err(SoftchatError::InvalidNip19Identifier)
491 ));
492 Ok(())
493 }
494}