#![warn(
clippy::all,
clippy::missing_errors_doc,
clippy::style,
clippy::unseparated_literal_suffix,
clippy::pedantic,
clippy::nursery
)]
#[cfg(all(feature = "k256", feature = "secp256k1"))]
compile_error!("features `k256` and `secp256k1` are mutually exclusive; pick exactly one");
pub mod errors;
pub mod event;
pub(crate) mod hash;
mod note;
mod relay_events;
mod subscriptions;
mod tags;
pub mod validation;
pub mod view;
#[cfg(target_arch = "wasm32")]
mod wasm;
pub use event::NostrEvent;
pub use note::{NostrNote, NostrNoteBuilder};
pub use relay_events::{NostrClientEvent, NostrRelayEvent, RelayEventTag};
pub use subscriptions::NostrSubscription;
pub use tags::NostrTags;
pub use view::{
NostrClientEventView, NostrNoteView, NostrRelayEventView, NostrSubscriptionView, TagsView,
};
pub use nostro2_traits::{NostrKeypair, NostrSigner, SignerError};
pub type Result<T> = std::result::Result<T, errors::NostrErrors>;
#[cfg(test)]
mod tests {
const PUB: &str = "4f6ddf3e79731d1b7039e28feb394e41e9117c93e383d31e8b88719095c6b17d";
use super::event::NostrEvent;
use super::note::{NostrNote, NostrNoteBuilder};
#[test]
fn unsigned_note_does_not_verify() {
let content_of_note = "- .... .. ... / .. ... / .- / -- . ... ... .- --. .";
let mut note = NostrNote {
pubkey: PUB.into(),
kind: 300,
content: content_of_note.into(),
..Default::default()
};
assert!(!note.verify(), "unsigned note must not verify");
note.serialize_id().expect("id serialization");
assert!(note.id.is_some());
assert!(note.sig.is_none());
assert!(
!note.verify(),
"id-only note (no sig) must still fail verification"
);
}
#[test]
fn test_create_tagged_note() {
let content_of_note = "- .... .. ... / .. ... / .- / -- . ... ... .- --. .";
let mut signed_note = NostrNote {
pubkey: PUB.into(),
kind: 300,
content: content_of_note.into(),
..Default::default()
};
signed_note.tags.add_custom_tag("t", "test");
signed_note.tags.add_event_tag("adsfasdfadsfadsfasdfadfs");
signed_note
.tags
.add_pubkey_tag("adsfasdfadsfadsfasdfadfs", None);
let t_tags = signed_note.tags.find_tags("t");
let t_tag = t_tags.first().expect("Failed to get tag!");
assert_eq!(t_tag, "test");
let p_tag = signed_note
.tags
.first_tagged_pubkey()
.expect("Failed to get tag!");
assert_eq!(p_tag, "adsfasdfadsfadsfasdfadfs");
let e_tag = signed_note
.tags
.first_tagged_event()
.expect("Failed to get tag!");
assert_eq!(e_tag, "adsfasdfadsfadsfasdfadfs");
}
#[test]
fn nostr_note_bourne_round_trip() {
let mut note = NostrNote {
pubkey: PUB.into(),
kind: u32::MAX,
created_at: i64::MIN,
content: "every escape: \\ \" \n \t \0 — and unicode 🦀".into(),
id: Some("a".repeat(64)),
sig: Some("b".repeat(128)),
..Default::default()
};
note.tags.add_pubkey_tag(PUB, Some("wss://relay"));
note.tags.add_event_tag(PUB);
note.tags.add_custom_tag("x", "y");
let json = json_bourne::to_string(¬e).expect("serialize");
let round_trip: NostrNote = json_bourne::parse_str(&json).expect("parse back");
assert_eq!(note, round_trip);
}
#[test]
fn test_try_p_and_e_tags() {
let content_of_note = "- .... .. ... / .. ... / .- / -- . ... ... .- --. .";
let mut signed_note = NostrNote {
pubkey: PUB.to_string(),
kind: 300,
content: content_of_note.to_string(),
..Default::default()
};
signed_note.tags.add_pubkey_tag(PUB, None);
assert_eq!(
signed_note.tags.first_tagged_pubkey(),
Some(PUB.to_string())
);
}
#[test]
fn test_note_builder() {
let note = NostrNoteBuilder::new()
.content("Hello, Nostr!")
.kind(1)
.tag_pubkey("abc123")
.tag_event("event123")
.tag("t", "nostr")
.build();
assert_eq!(note.content, "Hello, Nostr!");
assert_eq!(note.kind, 1);
assert_eq!(note.tags.len(), 3);
}
#[test]
fn test_text_note() {
let note = NostrNoteBuilder::text_note("Hello, world!").build();
assert_eq!(note.kind, 1);
assert_eq!(note.content, "Hello, world!");
}
#[test]
fn test_metadata_note() {
let metadata = r#"{"name":"Alice"}"#;
let note = NostrNoteBuilder::metadata(metadata).build();
assert_eq!(note.kind, 0);
assert_eq!(note.content, metadata);
}
#[test]
fn test_with_kind() {
let note = NostrNoteBuilder::new().kind(4).build();
assert_eq!(note.kind, 4);
}
#[test]
fn test_with_timestamp() {
let note = NostrNoteBuilder::text_note("Hello")
.timestamp(1_234_567_890)
.build();
assert_eq!(note.created_at, 1_234_567_890);
}
#[test]
fn test_with_content() {
let note = NostrNoteBuilder::new()
.kind(1)
.content("New content")
.build();
assert_eq!(note.content, "New content");
}
#[test]
fn test_note_now() {
let timestamp = NostrNoteBuilder::new().build().created_at;
assert!(timestamp > 0);
assert!(timestamp > 1_577_836_800);
}
#[test]
fn test_builder_chaining() {
let note = NostrNoteBuilder::new()
.kind(1)
.content("Test")
.timestamp(1_234_567_890)
.tag_pubkey("pubkey1")
.tag_event("event1")
.tag_parameter("param1")
.tag("custom", "value")
.tag_relay("wss://relay.example.com")
.build();
assert_eq!(note.kind, 1);
assert_eq!(note.content, "Test");
assert_eq!(note.created_at, 1_234_567_890);
assert_eq!(note.tags.len(), 5);
}
#[cfg(feature = "k256")]
#[test]
fn sign_with_then_verify_round_trips() {
use nostro2_traits::NostrKeypair as _;
let kp = nostro2_signer::NostrKeypair::generate();
let mut note = NostrNoteBuilder::text_note("round trip").build();
note.tags.add_custom_tag("t", "nostr");
note.tags.add_pubkey_tag(&"a".repeat(64), None);
note.sign_with(&kp).expect("sign");
assert!(note.verify(), "freshly signed note must verify");
note.content.push('!');
assert!(!note.verify(), "tampered content must not verify");
}
#[cfg(not(target_arch = "wasm32"))]
mod proptests {
use super::*;
use crate::event::NostrEvent;
use proptest::prelude::*;
fn arb_note() -> impl Strategy<Value = NostrNote> {
(
"[a-zA-Z0-9]{0,64}",
any::<i64>(),
any::<u32>(),
"[a-zA-Z0-9 ]{0,128}",
proptest::collection::vec(("[a-zA-Z0-9]{1,4}", "[a-zA-Z0-9]{0,32}"), 0..8),
)
.prop_map(|(pubkey, created_at, kind, content, tag_pairs)| {
let mut note = NostrNote {
pubkey,
created_at,
kind,
content,
..Default::default()
};
for (name, value) in tag_pairs {
note.tags.add_custom_tag(&name, &value);
}
note
})
}
proptest! {
#[test]
fn json_round_trip(note in arb_note()) {
let json = json_bourne::to_string(¬e).unwrap();
let back: NostrNote = json_bourne::parse_str(&json).unwrap();
prop_assert_eq!(¬e, &back);
}
#[test]
fn serialize_id_is_deterministic(note in arb_note()) {
let mut a = note.clone();
let mut b = note;
a.serialize_id().unwrap();
b.serialize_id().unwrap();
prop_assert_eq!(&a.id, &b.id);
}
#[test]
fn view_id_matches_owned_id(note in arb_note()) {
let mut owned = note;
owned.serialize_id().unwrap();
let json = json_bourne::to_string(&owned).unwrap();
let view: crate::view::NostrNoteView<'_> =
json_bourne::parse_str(&json).unwrap();
let view_id = view.compute_id_bytes();
let owned_id = owned.id_bytes().unwrap();
prop_assert_eq!(owned_id, view_id);
}
}
}
}