nostr_types/types/
nevent.rs1use super::{EventKind, Id, PublicKey, UncheckedUrl};
2use crate::Error;
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "speedy")]
5use speedy::{Readable, Writable};
6
7#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
9#[cfg_attr(feature = "speedy", derive(Readable, Writable))]
10pub struct NEvent {
11 pub id: Id,
13
14 pub relays: Vec<UncheckedUrl>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 #[serde(default)]
20 pub kind: Option<EventKind>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 #[serde(default)]
25 pub author: Option<PublicKey>,
26}
27
28impl NEvent {
29 pub fn as_bech32_string(&self) -> String {
31 let mut tlv: Vec<u8> = Vec::new();
33
34 tlv.push(0); tlv.push(32); tlv.extend(self.id.0);
38
39 for relay in &self.relays {
41 tlv.push(1); let len = relay.0.len() as u8;
43 tlv.push(len); tlv.extend(&relay.0.as_bytes()[..len as usize]);
45 }
46
47 if let Some(kind) = self.kind {
49 let kindnum: u32 = From::from(kind);
50 let bytes = kindnum.to_be_bytes();
51 tlv.push(3); tlv.push(bytes.len() as u8); tlv.extend(bytes);
54 }
55
56 if let Some(pubkey) = self.author {
58 tlv.push(2); tlv.push(32); tlv.extend(pubkey.as_bytes());
61 }
62
63 bech32::encode::<bech32::Bech32>(*crate::HRP_NEVENT, &tlv).unwrap()
64 }
65
66 pub fn try_from_bech32_string(s: &str) -> Result<NEvent, Error> {
68 let data = bech32::decode(s)?;
69 if data.0 != *crate::HRP_NEVENT {
70 Err(Error::WrongBech32(
71 crate::HRP_NEVENT.to_lowercase(),
72 data.0.to_lowercase(),
73 ))
74 } else {
75 let mut relays: Vec<UncheckedUrl> = Vec::new();
76 let mut id: Option<Id> = None;
77 let mut kind: Option<EventKind> = None;
78 let mut author: Option<PublicKey> = None;
79
80 let tlv = data.1;
81 let mut pos = 0;
82 loop {
83 if pos > tlv.len() - 2 {
85 break;
86 }
87 let ty = tlv[pos];
88 let len = tlv[pos + 1] as usize;
89 pos += 2;
90 if pos + len > tlv.len() {
91 return Err(Error::InvalidProfile);
92 }
93 let raw = &tlv[pos..pos + len];
94 match ty {
95 0 => {
96 if len != 32 {
98 return Err(Error::InvalidNEvent);
99 }
100 id = Some(Id(raw
101 .try_into()
102 .map_err(|_| Error::WrongLengthHexString)?));
103 }
104 1 => {
105 let relay_str = std::str::from_utf8(raw)?;
107 let relay = UncheckedUrl::from_str(relay_str);
108 relays.push(relay);
109 }
110 2 => {
111 if let Ok(pk) = PublicKey::from_bytes(raw, true) {
117 author = Some(pk);
118 }
119 }
120 3 => {
121 let kindnum = u32::from_be_bytes(
123 raw.try_into().map_err(|_| Error::WrongLengthKindBytes)?,
124 );
125 kind = Some(kindnum.into());
126 }
127 _ => {} }
129 pos += len;
130 }
131 if let Some(id) = id {
132 Ok(NEvent {
133 id,
134 relays,
135 kind,
136 author,
137 })
138 } else {
139 Err(Error::InvalidNEvent)
140 }
141 }
142 }
143
144 #[allow(dead_code)]
146 pub(crate) fn mock() -> NEvent {
147 let id = Id::try_from_hex_string(
148 "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9",
149 )
150 .unwrap();
151
152 NEvent {
153 id,
154 relays: vec![
155 UncheckedUrl::from_str("wss://relay.example.com"),
156 UncheckedUrl::from_str("wss://relay2.example.com"),
157 ],
158 kind: None,
159 author: None,
160 }
161 }
162}
163
164#[cfg(test)]
165mod test {
166 use super::*;
167
168 test_serde! {NEvent, test_nevent_serde}
169
170 #[test]
171 fn test_profile_bech32() {
172 let bech32 = NEvent::mock().as_bech32_string();
173 println!("{bech32}");
174 assert_eq!(
175 NEvent::mock(),
176 NEvent::try_from_bech32_string(&bech32).unwrap()
177 );
178 }
179
180 #[test]
181 fn test_nip19_example() {
182 let nevent = NEvent {
183 id: Id::try_from_hex_string(
184 "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
185 )
186 .unwrap(),
187 relays: vec![
188 UncheckedUrl::from_str("wss://r.x.com"),
189 UncheckedUrl::from_str("wss://djbas.sadkb.com"),
190 ],
191 kind: None,
192 author: None,
193 };
194
195 let bech32 = "nevent1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaks343fay";
197
198 assert_eq!(nevent.as_bech32_string(), bech32);
200
201 assert_eq!(nevent, NEvent::try_from_bech32_string(bech32).unwrap());
203
204 let bech32 =
206 "nevent1qqstxx3lk7zqfyn8cyyptvujfxq9w6mad4205x54772tdkmyqaay9scrqsqqqpp8x4vwhf";
207 let _ = NEvent::try_from_bech32_string(bech32).unwrap();
208 }
210
211 #[test]
212 fn test_nevent_alt_fields() {
213 let nevent = NEvent {
214 id: Id::try_from_hex_string(
215 "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
216 )
217 .unwrap(),
218 relays: vec![
219 UncheckedUrl::from_str("wss://r.x.com"),
220 UncheckedUrl::from_str("wss://djbas.sadkb.com"),
221 ],
222 kind: Some(EventKind::TextNote),
223 author: Some(
224 PublicKey::try_from_hex_string(
225 "000000000332c7831d9c5a99f183afc2813a6f69a16edda7f6fc0ed8110566e6",
226 true,
227 )
228 .unwrap(),
229 ),
230 };
231
232 let bech32 = "nevent1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksxpqqqqqqzq3qqqqqqqqrxtrcx8vut2vlrqa0c2qn5mmf59hdmflkls8dsyg9vmnqu25v0j";
234
235 assert_eq!(nevent.as_bech32_string(), bech32);
237
238 assert_eq!(nevent, NEvent::try_from_bech32_string(bech32).unwrap());
240 }
241
242 #[test]
243 fn test_ones_that_were_failing() {
244 let bech32 = "nevent1qqswrqr63ddwk8l3zfqrgdxh2lxh2jlcxl36k3h33g25gtchzchx8agpp4mhxue69uhkummn9ekx7mqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3yamnwvaz7tm0venxx6rpd9hzuur4vgpyqdmyxs6rzdmyx4jxvdpnx4snjdmz8pnr2dtr8pnryefhv5ex2e34xvek2v3nxuckxef4v5ckxenxvs6njdtrxymnjcfnv4skvvekvs6qfe99uy";
245
246 let _ne = NEvent::try_from_bech32_string(bech32).unwrap();
247 }
248}