use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::compute_event_id;
use super::event::Event;
use super::id::EventId;
use super::kind::Kind;
use super::tag::Tags;
use crate::key::{Keys, PublicKey};
use crate::types::Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum UnsignedEventError {
#[error("signer public key does not match event pubkey")]
SignerMismatch,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UnsignedEvent {
pub id: EventId,
pub pubkey: PublicKey,
pub created_at: Timestamp,
pub kind: Kind,
pub tags: Tags,
pub content: String,
}
impl UnsignedEvent {
#[must_use]
pub fn new(
pubkey: PublicKey,
created_at: Timestamp,
kind: Kind,
tags: Tags,
content: impl Into<String>,
) -> Self {
let content = content.into();
let id = compute_event_id(&pubkey, created_at, kind, &tags, &content);
Self {
id,
pubkey,
created_at,
kind,
tags,
content,
}
}
#[must_use]
pub fn compute_id(&self) -> EventId {
compute_event_id(
&self.pubkey,
self.created_at,
self.kind,
&self.tags,
&self.content,
)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "debug",
name = "nula.event.sign",
skip(self, keys),
fields(
nostr.event.kind = self.kind.as_u16(),
nostr.event.content_size = self.content.len(),
nostr.event.tag_count = self.tags.len(),
),
)
)]
pub fn sign_with_keys(self, keys: &Keys) -> Result<Event, UnsignedEventError> {
if keys.public_key() != &self.pubkey {
#[cfg(feature = "tracing")]
tracing::debug!("signer public key does not match unsigned event author");
return Err(UnsignedEventError::SignerMismatch);
}
let canonical_id = self.compute_id();
let signature = keys.sign_schnorr(&canonical_id.to_byte_array());
Ok(Event::from_parts(
canonical_id,
self.pubkey,
self.created_at,
self.kind,
self.tags,
self.content,
signature,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
#[test]
fn new_computes_id() {
let keys = fixture_keys();
let unsigned = UnsignedEvent::new(
*keys.public_key(),
Timestamp::from_secs(1_700_000_000),
Kind::TEXT_NOTE,
Tags::new(),
"hello",
);
assert_eq!(unsigned.id, unsigned.compute_id());
}
#[test]
fn sign_with_keys_produces_event() {
let keys = fixture_keys();
let unsigned = UnsignedEvent::new(
*keys.public_key(),
Timestamp::from_secs(1_700_000_000),
Kind::TEXT_NOTE,
Tags::new(),
"hello",
);
let event = unsigned.clone().sign_with_keys(&keys).unwrap();
assert_eq!(event.id, unsigned.id);
assert_eq!(event.pubkey, unsigned.pubkey);
assert_eq!(event.content, unsigned.content);
event.verify().unwrap();
}
#[test]
fn sign_with_keys_rejects_mismatch() {
let alice = fixture_keys();
let bob = Keys::parse("0000000000000000000000000000000000000000000000000000000000000005")
.unwrap();
let unsigned = UnsignedEvent::new(
*alice.public_key(),
Timestamp::from_secs(1_700_000_000),
Kind::TEXT_NOTE,
Tags::new(),
"hello",
);
let err = unsigned.sign_with_keys(&bob).unwrap_err();
assert_eq!(err, UnsignedEventError::SignerMismatch);
}
#[test]
fn serde_round_trip() {
let keys = fixture_keys();
let unsigned = UnsignedEvent::new(
*keys.public_key(),
Timestamp::from_secs(1),
Kind::TEXT_NOTE,
Tags::new(),
"hi",
);
let json = serde_json::to_string(&unsigned).unwrap();
let parsed: UnsignedEvent = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, unsigned);
}
}