nula_core/event/
unsigned.rs1use serde::{Deserialize, Serialize};
11use thiserror::Error;
12
13use super::compute_event_id;
14use super::event::Event;
15use super::id::EventId;
16use super::kind::Kind;
17use super::tag::Tags;
18use crate::key::{Keys, PublicKey};
19use crate::types::Timestamp;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
23#[non_exhaustive]
24pub enum UnsignedEventError {
25 #[error("signer public key does not match event pubkey")]
27 SignerMismatch,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub struct UnsignedEvent {
36 pub id: EventId,
38 pub pubkey: PublicKey,
40 pub created_at: Timestamp,
42 pub kind: Kind,
44 pub tags: Tags,
46 pub content: String,
48}
49
50impl UnsignedEvent {
51 #[must_use]
54 pub fn new(
55 pubkey: PublicKey,
56 created_at: Timestamp,
57 kind: Kind,
58 tags: Tags,
59 content: impl Into<String>,
60 ) -> Self {
61 let content = content.into();
62 let id = compute_event_id(&pubkey, created_at, kind, &tags, &content);
63 Self {
64 id,
65 pubkey,
66 created_at,
67 kind,
68 tags,
69 content,
70 }
71 }
72
73 #[must_use]
77 pub fn compute_id(&self) -> EventId {
78 compute_event_id(
79 &self.pubkey,
80 self.created_at,
81 self.kind,
82 &self.tags,
83 &self.content,
84 )
85 }
86
87 #[cfg_attr(
104 feature = "tracing",
105 tracing::instrument(
106 level = "debug",
107 name = "nula.event.sign",
108 skip(self, keys),
109 fields(
110 nostr.event.kind = self.kind.as_u16(),
111 nostr.event.content_size = self.content.len(),
112 nostr.event.tag_count = self.tags.len(),
113 ),
114 )
115 )]
116 pub fn sign_with_keys(self, keys: &Keys) -> Result<Event, UnsignedEventError> {
117 if keys.public_key() != &self.pubkey {
118 #[cfg(feature = "tracing")]
119 tracing::debug!("signer public key does not match unsigned event author");
120 return Err(UnsignedEventError::SignerMismatch);
121 }
122
123 let canonical_id = self.compute_id();
127 let signature = keys.sign_schnorr(&canonical_id.to_byte_array());
128
129 Ok(Event::from_parts(
130 canonical_id,
131 self.pubkey,
132 self.created_at,
133 self.kind,
134 self.tags,
135 self.content,
136 signature,
137 ))
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 fn fixture_keys() -> Keys {
146 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
147 }
148
149 #[test]
150 fn new_computes_id() {
151 let keys = fixture_keys();
152 let unsigned = UnsignedEvent::new(
153 *keys.public_key(),
154 Timestamp::from_secs(1_700_000_000),
155 Kind::TEXT_NOTE,
156 Tags::new(),
157 "hello",
158 );
159 assert_eq!(unsigned.id, unsigned.compute_id());
160 }
161
162 #[test]
163 fn sign_with_keys_produces_event() {
164 let keys = fixture_keys();
165 let unsigned = UnsignedEvent::new(
166 *keys.public_key(),
167 Timestamp::from_secs(1_700_000_000),
168 Kind::TEXT_NOTE,
169 Tags::new(),
170 "hello",
171 );
172 let event = unsigned.clone().sign_with_keys(&keys).unwrap();
173 assert_eq!(event.id, unsigned.id);
174 assert_eq!(event.pubkey, unsigned.pubkey);
175 assert_eq!(event.content, unsigned.content);
176 event.verify().unwrap();
177 }
178
179 #[test]
180 fn sign_with_keys_rejects_mismatch() {
181 let alice = fixture_keys();
182 let bob = Keys::parse("0000000000000000000000000000000000000000000000000000000000000005")
183 .unwrap();
184 let unsigned = UnsignedEvent::new(
185 *alice.public_key(),
186 Timestamp::from_secs(1_700_000_000),
187 Kind::TEXT_NOTE,
188 Tags::new(),
189 "hello",
190 );
191 let err = unsigned.sign_with_keys(&bob).unwrap_err();
192 assert_eq!(err, UnsignedEventError::SignerMismatch);
193 }
194
195 #[test]
196 fn serde_round_trip() {
197 let keys = fixture_keys();
198 let unsigned = UnsignedEvent::new(
199 *keys.public_key(),
200 Timestamp::from_secs(1),
201 Kind::TEXT_NOTE,
202 Tags::new(),
203 "hi",
204 );
205 let json = serde_json::to_string(&unsigned).unwrap();
206 let parsed: UnsignedEvent = serde_json::from_str(&json).unwrap();
207 assert_eq!(parsed, unsigned);
208 }
209}