lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit cd171a17c5266c34af010527337f9352786edbd2
parent 9e603005a3f41583cb26def3b3406e2fcc5f3bed
Author: triesap <tyson@radroots.org>
Date:   Tue, 14 Jul 2026 07:57:15 +0000

event: add typed signed event state

- add typed validated event envelopes and tag primitives
- preserve canonical wire JSON in signed event state
- add optional direct Schnorr signature verification
- align codec consumers with private envelope access

Diffstat:
MCargo.lock | 1+
MCargo.toml | 1+
Mcrates/event/Cargo.toml | 2++
Mcrates/event/src/contract.rs | 17+++++++++++------
Mcrates/event/src/draft.rs | 705++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Acrates/event/src/envelope.rs | 542+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event/src/event_head.rs | 93++++++++++++++++++++++++++++++++-----------------------------------------------
Mcrates/event/src/follow.rs | 2+-
Mcrates/event/src/lib.rs | 23++++++-----------------
Mcrates/event/src/wire.rs | 20++++++++++++++++++--
Mcrates/event_codec/src/app_data/decode.rs | 18+++---------------
Mcrates/event_codec/src/article/decode.rs | 18+++---------------
Mcrates/event_codec/src/calendar/decode.rs | 69++++++++++++---------------------------------------------------------
Mcrates/event_codec/src/comment/decode.rs | 18+++---------------
Mcrates/event_codec/src/coop/decode.rs | 19++++---------------
Mcrates/event_codec/src/document/decode.rs | 21++++-----------------
Mcrates/event_codec/src/error.rs | 9+++++++++
Mcrates/event_codec/src/farm/decode.rs | 18+++---------------
Mcrates/event_codec/src/farm/mod.rs | 2+-
Mcrates/event_codec/src/farm_crdt/decode.rs | 18+++---------------
Mcrates/event_codec/src/farm_crdt/mod.rs | 2+-
Mcrates/event_codec/src/farm_file/decode.rs | 18+++---------------
Mcrates/event_codec/src/farm_file/mod.rs | 2+-
Mcrates/event_codec/src/farm_workspace/decode.rs | 18+++---------------
Mcrates/event_codec/src/farm_workspace/mod.rs | 2+-
Mcrates/event_codec/src/file_metadata/decode.rs | 18+++---------------
Mcrates/event_codec/src/follow/decode.rs | 22+++++-----------------
Mcrates/event_codec/src/geochat/decode.rs | 19++++---------------
Mcrates/event_codec/src/gift_wrap/decode.rs | 18+++---------------
Mcrates/event_codec/src/http_auth/decode.rs | 18+++---------------
Mcrates/event_codec/src/http_auth/mod.rs | 2+-
Mcrates/event_codec/src/job/feedback/decode.rs | 22++++++----------------
Mcrates/event_codec/src/job/request/decode.rs | 19++++---------------
Mcrates/event_codec/src/job/result/decode.rs | 20+++++---------------
Mcrates/event_codec/src/job/traits.rs | 16++++++++--------
Mcrates/event_codec/src/knowledge/decode.rs | 146++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/event_codec/src/list/decode.rs | 18+++---------------
Mcrates/event_codec/src/list_set/decode.rs | 20++++----------------
Mcrates/event_codec/src/listing/decode.rs | 36++++++++++++++++++------------------
Mcrates/event_codec/src/message/decode.rs | 19++++---------------
Mcrates/event_codec/src/message_file/decode.rs | 18+++---------------
Mcrates/event_codec/src/order/decode.rs | 387++++++++++++++++++++++++++++++++++++-------------------------------------------
Mcrates/event_codec/src/parsed.rs | 123++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mcrates/event_codec/src/plot/decode.rs | 18+++---------------
Mcrates/event_codec/src/post/decode.rs | 18+++---------------
Mcrates/event_codec/src/profile/decode.rs | 18+++---------------
Mcrates/event_codec/src/reaction/decode.rs | 21+++++----------------
Mcrates/event_codec/src/relay_auth/decode.rs | 18+++---------------
Mcrates/event_codec/src/relay_auth/mod.rs | 2+-
Mcrates/event_codec/src/report/decode.rs | 18+++---------------
Mcrates/event_codec/src/repost/decode.rs | 35++++++-----------------------------
Mcrates/event_codec/src/resource_area/decode.rs | 22++++------------------
Mcrates/event_codec/src/resource_cap/decode.rs | 20++++----------------
Mcrates/event_codec/src/seal/decode.rs | 19++++---------------
Mcrates/event_codec/src/verification.rs | 39++++++++++++++++++++-------------------
Mcrates/event_codec/tests/app_data.rs | 12++++++------
Mcrates/event_codec/tests/article.rs | 2+-
Mcrates/event_codec/tests/calendar.rs | 8++++----
Mcrates/event_codec/tests/comment.rs | 4++--
Mcrates/event_codec/tests/common/mod.rs | 22++++++++++++++++------
Mcrates/event_codec/tests/coverage_edges.rs | 12++++++++----
Mcrates/event_codec/tests/file_metadata.rs | 4++--
Mcrates/event_codec/tests/follow.rs | 4++--
Mcrates/event_codec/tests/geochat.rs | 4++--
Mcrates/event_codec/tests/gift_wrap.rs | 4++--
Mcrates/event_codec/tests/job_traits.rs | 146+++++++++++++++++++++++++++++++++++++------------------------------------------
Mcrates/event_codec/tests/knowledge.rs | 314+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/event_codec/tests/knowledge_fixtures.rs | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mcrates/event_codec/tests/list.rs | 4++--
Mcrates/event_codec/tests/list_set.rs | 4++--
Mcrates/event_codec/tests/listing.rs | 66+++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/event_codec/tests/message.rs | 20+++++++++++++++-----
Mcrates/event_codec/tests/message_file.rs | 4++--
Mcrates/event_codec/tests/post.rs | 12++++++------
Mcrates/event_codec/tests/profile.rs | 34+++++++++++++++++++++++++---------
Mcrates/event_codec/tests/reaction.rs | 4++--
Mcrates/event_codec/tests/report.rs | 2+-
Mcrates/event_codec/tests/repost.rs | 4++--
Mcrates/event_codec/tests/seal.rs | 4++--
Mcrates/event_codec/tests/structured_decode.rs | 12++++++------
80 files changed, 2262 insertions(+), 1468 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -4288,6 +4288,7 @@ dependencies = [ "dto_bindgen", "hex", "radroots_core", + "secp256k1", "serde", "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml @@ -153,6 +153,7 @@ serde = { version = "1", default-features = false, features = [ "alloc", ] } serde_json = { version = "1", default-features = false, features = ["alloc"] } +secp256k1 = { version = "0.29.1", default-features = false, features = ["alloc"] } sha2 = { version = "0.10", default-features = false } sha3 = { version = "0.10", default-features = false } sntrup761 = { version = "0.4.0", default-features = false, features = [ diff --git a/crates/event/Cargo.toml b/crates/event/Cargo.toml @@ -23,6 +23,7 @@ dto-bindgen = [ ] knowledge = [] knowledge-nip54 = ["knowledge"] +signature = ["dep:secp256k1"] std = ["radroots_core/std"] serde = ["dep:serde", "radroots_core/serde"] @@ -37,6 +38,7 @@ serde = { workspace = true, default-features = false, features = [ serde_json = { workspace = true, default-features = false, features = [ "alloc", ] } +secp256k1 = { workspace = true, optional = true } sha2 = { workspace = true, default-features = false } [dev-dependencies] diff --git a/crates/event/src/contract.rs b/crates/event/src/contract.rs @@ -3171,7 +3171,8 @@ pub fn identify_event_contract( pub fn validate_event_contract( event: &RadrootsEventEnvelope, ) -> Result<&'static RadrootsEventContract, RadrootsContractValidationError> { - let contract = match identify_event_contract(event.kind, &event.tags, &event.content) { + let tags = event.tags_as_vec(); + let contract = match identify_event_contract(event.kind_u32(), &tags, event.content()) { Ok(contract) => contract, Err(error) => return Err(RadrootsContractValidationError::ContractMatch { error }), }; @@ -3183,7 +3184,8 @@ pub fn validate_event_contract_shape( event: &RadrootsEventEnvelope, contract_id: &str, ) -> Result<(), RadrootsContractValidationError> { - validate_event_contract_parts(event.kind, &event.tags, event.content.as_str(), contract_id) + let tags = event.tags_as_vec(); + validate_event_contract_parts(event.kind_u32(), &tags, event.content(), contract_id) } pub fn validate_event_contract_parts( @@ -3615,6 +3617,7 @@ fn content_json_string_field_equals(content: &str, field: &str, value: &str) -> #[cfg(test)] mod tests { use super::*; + use crate::RadrootsEventEnvelopeParts; use std::collections::BTreeSet; static AMBIGUOUS_TEST_CONTRACTS: &[RadrootsEventContract] = &[ @@ -3728,7 +3731,7 @@ mod tests { } fn unsigned_event(kind: u32, tags: Vec<Vec<&str>>, content: &str) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: "0".repeat(64), author: "1".repeat(64), created_at: 1_700_000_000, @@ -3739,7 +3742,8 @@ mod tests { .collect(), content: content.to_owned(), sig: "2".repeat(128), - } + }) + .expect("event envelope") } fn unsigned_event_owned( @@ -3747,7 +3751,7 @@ mod tests { tags: Vec<Vec<String>>, content: &str, ) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: "0".repeat(64), author: "1".repeat(64), created_at: 1_700_000_000, @@ -3755,7 +3759,8 @@ mod tests { tags, content: content.to_owned(), sig: "2".repeat(128), - } + }) + .expect("event envelope") } fn hex_64(character: char) -> String { diff --git a/crates/event/src/draft.rs b/crates/event/src/draft.rs @@ -6,7 +6,6 @@ use alloc::{borrow::ToOwned, string::String, vec::Vec}; #[cfg(any(feature = "std", test))] use std::{borrow::ToOwned, string::String, vec::Vec}; -use crate::RadrootsEventEnvelope; use crate::contract::{ RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION, RadrootsContractValidationError, event_contract, validate_event_contract_parts, @@ -15,9 +14,10 @@ use crate::ids::{ RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, RadrootsPublicKey, }; use crate::wire::{ - RadrootsCanonicalEventIdError, canonical_nip01_event_id_preimage, - compute_canonical_nip01_event_id, + RadrootsCanonicalEventIdError, RadrootsEventWireError, RadrootsNip01EventWire, + canonical_nip01_event_id_preimage, compute_canonical_nip01_event_id, }; +use crate::{RadrootsEventEnvelope, RadrootsEventEnvelopeError}; use core::fmt; #[derive(Clone, Debug, PartialEq, Eq)] @@ -42,7 +42,7 @@ pub enum RadrootsDraftError { }, SignedEventCreatedAtMismatch { expected_created_at: u32, - actual_created_at: u32, + actual_created_at: u64, }, SignedEventKindMismatch { expected_kind: u32, @@ -62,6 +62,7 @@ pub enum RadrootsDraftError { }, IdParse(RadrootsIdParseError), CanonicalEventId(RadrootsCanonicalEventIdError), + SignedEvent(RadrootsSignedEventError), } impl fmt::Display for RadrootsDraftError { @@ -134,6 +135,7 @@ impl fmt::Display for RadrootsDraftError { ), Self::IdParse(error) => write!(f, "{error}"), Self::CanonicalEventId(error) => write!(f, "{error}"), + Self::SignedEvent(error) => write!(f, "{error}"), } } } @@ -156,6 +158,12 @@ impl From<RadrootsCanonicalEventIdError> for RadrootsDraftError { } } +impl From<RadrootsSignedEventError> for RadrootsDraftError { + fn from(value: RadrootsSignedEventError) -> Self { + Self::SignedEvent(value) + } +} + #[cfg_attr( any(feature = "serde", test), derive(serde::Serialize, serde::Deserialize) @@ -235,7 +243,7 @@ impl RadrootsEventDraft { pub struct RadrootsSignedEventParts { pub id: String, pub pubkey: String, - pub created_at: u32, + pub created_at: u64, pub kind: u32, pub tags: Vec<Vec<String>>, pub content: String, @@ -243,28 +251,130 @@ pub struct RadrootsSignedEventParts { pub raw_json: String, } -#[cfg_attr( - any(feature = "serde", test), - derive(serde::Serialize, serde::Deserialize) -)] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsSignedEvent { - pub id: String, - pub pubkey: String, - pub created_at: u32, - pub kind: u32, - pub tags: Vec<Vec<String>>, - pub content: String, - pub sig: String, - pub raw_json: String, + envelope: RadrootsEventEnvelope, + wire: RadrootsNip01EventWire, + raw_json: String, +} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for RadrootsSignedEvent { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + + let mut state = serializer.serialize_struct("RadrootsSignedEvent", 2)?; + state.serialize_field("wire", &self.wire)?; + state.serialize_field("raw_json", &self.raw_json)?; + state.end() + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for RadrootsSignedEvent { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + #[derive(serde::Deserialize)] + struct SignedEventSerde { + wire: RadrootsNip01EventWire, + raw_json: String, + } + + let value = SignedEventSerde::deserialize(deserializer)?; + RadrootsSignedEvent::from_wire_verified_id(value.wire, value.raw_json) + .map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsSignedEventError { + Wire(RadrootsEventWireError), + RawJson(RadrootsEventWireError), + RawJsonMismatch, + Envelope(RadrootsEventEnvelopeError), +} + +impl fmt::Display for RadrootsSignedEventError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Wire(error) => write!(f, "signed event wire is invalid: {error}"), + Self::RawJson(error) => write!(f, "signed event raw JSON is invalid: {error}"), + Self::RawJsonMismatch => { + write!( + f, + "signed event raw JSON does not match the provided wire event" + ) + } + Self::Envelope(error) => write!(f, "signed event envelope is invalid: {error}"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsSignedEventError {} + +impl From<RadrootsEventEnvelopeError> for RadrootsSignedEventError { + fn from(value: RadrootsEventEnvelopeError) -> Self { + Self::Envelope(value) + } +} + +#[cfg(feature = "signature")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsSignatureVerificationError { + InvalidEventId, + InvalidPubkey, + InvalidSignature, + VerificationFailed, +} + +#[cfg(feature = "signature")] +impl fmt::Display for RadrootsSignatureVerificationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidEventId => write!( + f, + "signed event id cannot be decoded for signature verification" + ), + Self::InvalidPubkey => write!( + f, + "signed event pubkey cannot be decoded for signature verification" + ), + Self::InvalidSignature => write!( + f, + "signed event signature cannot be decoded for verification" + ), + Self::VerificationFailed => write!(f, "signed event signature verification failed"), + } + } +} + +#[cfg(all(feature = "signature", feature = "std"))] +impl std::error::Error for RadrootsSignatureVerificationError {} + +#[cfg(feature = "signature")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsVerifiedSignedEvent { + signed_event: RadrootsSignedEvent, } impl RadrootsSignedEvent { - pub fn new(parts: RadrootsSignedEventParts) -> Result<Self, RadrootsDraftError> { - let id = RadrootsEventId::parse(parts.id)?.into_string(); - let pubkey = RadrootsPublicKey::parse(parts.pubkey)?.into_string(); - let sig = RadrootsEventSignature::parse(parts.sig)?.into_string(); - Ok(Self { + pub fn new(parts: RadrootsSignedEventParts) -> Result<Self, RadrootsSignedEventError> { + let id = RadrootsEventId::parse(parts.id) + .map_err(RadrootsEventEnvelopeError::InvalidId)? + .into_string(); + let pubkey = RadrootsPublicKey::parse(parts.pubkey) + .map_err(RadrootsEventEnvelopeError::InvalidAuthor)? + .into_string(); + let sig = RadrootsEventSignature::parse(parts.sig) + .map_err(RadrootsEventEnvelopeError::InvalidSignature)? + .into_string(); + let wire = RadrootsNip01EventWire { id, pubkey, created_at: parts.created_at, @@ -272,84 +382,217 @@ impl RadrootsSignedEvent { tags: parts.tags, content: parts.content, sig, - raw_json: parts.raw_json, + extra: Default::default(), + }; + Self::from_wire_verified_id(wire, parts.raw_json) + } + + pub fn from_wire_verified_id( + wire: RadrootsNip01EventWire, + raw_json: impl Into<String>, + ) -> Result<Self, RadrootsSignedEventError> { + wire.verify_id().map_err(RadrootsSignedEventError::Wire)?; + let raw_json = raw_json.into(); + let parsed = RadrootsNip01EventWire::parse_json(raw_json.as_str()) + .map_err(RadrootsSignedEventError::RawJson)?; + if parsed != wire { + return Err(RadrootsSignedEventError::RawJsonMismatch); + } + let envelope = wire + .clone() + .into_envelope() + .map_err(RadrootsSignedEventError::Envelope)?; + Ok(Self { + envelope, + wire, + raw_json, }) } - pub fn from_event( - event: RadrootsEventEnvelope, + pub fn from_wire_unchecked( + wire: RadrootsNip01EventWire, raw_json: impl Into<String>, - ) -> Result<Self, RadrootsDraftError> { - Self::new(RadrootsSignedEventParts { - id: event.id, - pubkey: event.author, - created_at: event.created_at, - kind: event.kind, - tags: event.tags, - content: event.content, - sig: event.sig, + ) -> Result<Self, RadrootsSignedEventError> { + let envelope = wire + .clone() + .into_envelope() + .map_err(RadrootsSignedEventError::Envelope)?; + Ok(Self { + envelope, + wire, raw_json: raw_json.into(), }) } + + #[inline] + pub fn envelope(&self) -> &RadrootsEventEnvelope { + &self.envelope + } + + #[inline] + pub fn wire(&self) -> &RadrootsNip01EventWire { + &self.wire + } + + #[inline] + pub fn raw_json(&self) -> &str { + self.raw_json.as_str() + } + + #[inline] + pub fn id(&self) -> &RadrootsEventId { + self.envelope.id() + } + + #[inline] + pub fn id_str(&self) -> &str { + self.envelope.id_str() + } + + #[inline] + pub fn pubkey(&self) -> &RadrootsPublicKey { + self.envelope.author() + } + + #[inline] + pub fn pubkey_str(&self) -> &str { + self.envelope.author_str() + } + + #[inline] + pub fn created_at(&self) -> u64 { + self.envelope.created_at_u64() + } + + #[inline] + pub fn kind(&self) -> u32 { + self.envelope.kind_u32() + } + + pub fn tags_as_vec(&self) -> Vec<Vec<String>> { + self.envelope.tags_as_vec() + } + + #[inline] + pub fn content(&self) -> &str { + self.envelope.content() + } + + #[inline] + pub fn sig(&self) -> &RadrootsEventSignature { + self.envelope.sig() + } + + #[inline] + pub fn sig_str(&self) -> &str { + self.envelope.sig_str() + } + + #[cfg(feature = "signature")] + pub fn verify_signature( + self, + ) -> Result<RadrootsVerifiedSignedEvent, RadrootsSignatureVerificationError> { + verify_bip340_signature(&self)?; + Ok(RadrootsVerifiedSignedEvent { signed_event: self }) + } +} + +#[cfg(feature = "signature")] +impl RadrootsVerifiedSignedEvent { + #[inline] + pub fn signed_event(&self) -> &RadrootsSignedEvent { + &self.signed_event + } + + #[inline] + pub fn into_signed_event(self) -> RadrootsSignedEvent { + self.signed_event + } } pub fn validate_signed_nostr_event_matches_draft( signed_event: &RadrootsSignedEvent, draft: &RadrootsEventDraft, ) -> Result<(), RadrootsDraftError> { - if signed_event.pubkey.as_str() != draft.expected_pubkey.as_str() { + if signed_event.pubkey_str() != draft.expected_pubkey.as_str() { return Err(RadrootsDraftError::SignedEventPubkeyMismatch { expected_pubkey: draft.expected_pubkey.clone(), - actual_pubkey: signed_event.pubkey.clone(), + actual_pubkey: signed_event.pubkey_str().to_owned(), }); } - if signed_event.id.as_str() != draft.expected_event_id.as_str() { + if signed_event.id_str() != draft.expected_event_id.as_str() { return Err(RadrootsDraftError::SignedEventIdMismatch { expected_event_id: draft.expected_event_id.clone(), - actual_event_id: signed_event.id.clone(), + actual_event_id: signed_event.id_str().to_owned(), }); } - if signed_event.created_at != draft.created_at { + if signed_event.created_at() != u64::from(draft.created_at) { return Err(RadrootsDraftError::SignedEventCreatedAtMismatch { expected_created_at: draft.created_at, - actual_created_at: signed_event.created_at, + actual_created_at: signed_event.created_at(), }); } - if signed_event.kind != draft.kind { + if signed_event.kind() != draft.kind { return Err(RadrootsDraftError::SignedEventKindMismatch { expected_kind: draft.kind, - actual_kind: signed_event.kind, + actual_kind: signed_event.kind(), }); } - if signed_event.tags != draft.tags { + let signed_tags = signed_event.tags_as_vec(); + if signed_tags != draft.tags { return Err(RadrootsDraftError::SignedEventTagsMismatch { expected_len: draft.tags.len(), - actual_len: signed_event.tags.len(), + actual_len: signed_tags.len(), }); } - if signed_event.content != draft.content { + if signed_event.content() != draft.content { return Err(RadrootsDraftError::SignedEventContentMismatch { expected_len: draft.content.len(), - actual_len: signed_event.content.len(), + actual_len: signed_event.content().len(), }); } let computed_event_id = compute_nip01_event_id( - signed_event.pubkey.as_str(), - signed_event.created_at, - signed_event.kind, - &signed_event.tags, - signed_event.content.as_str(), + signed_event.pubkey_str(), + draft.created_at, + signed_event.kind(), + &signed_tags, + signed_event.content(), )? .into_string(); - if computed_event_id.as_str() != signed_event.id.as_str() { + if computed_event_id.as_str() != signed_event.id_str() { return Err(RadrootsDraftError::SignedEventComputedIdMismatch { - expected_event_id: signed_event.id.clone(), + expected_event_id: signed_event.id_str().to_owned(), computed_event_id, }); } Ok(()) } +#[cfg(feature = "signature")] +fn verify_bip340_signature( + signed_event: &RadrootsSignedEvent, +) -> Result<(), RadrootsSignatureVerificationError> { + use secp256k1::{Message, Secp256k1, XOnlyPublicKey, schnorr::Signature}; + + let mut event_id = [0u8; 32]; + hex::decode_to_slice(signed_event.id_str(), &mut event_id) + .map_err(|_| RadrootsSignatureVerificationError::InvalidEventId)?; + let mut pubkey = [0u8; 32]; + hex::decode_to_slice(signed_event.pubkey_str(), &mut pubkey) + .map_err(|_| RadrootsSignatureVerificationError::InvalidPubkey)?; + let mut sig = [0u8; 64]; + hex::decode_to_slice(signed_event.sig_str(), &mut sig) + .map_err(|_| RadrootsSignatureVerificationError::InvalidSignature)?; + let message = Message::from_digest(event_id); + let pubkey = XOnlyPublicKey::from_slice(&pubkey) + .map_err(|_| RadrootsSignatureVerificationError::InvalidPubkey)?; + let sig = Signature::from_slice(&sig) + .map_err(|_| RadrootsSignatureVerificationError::InvalidSignature)?; + Secp256k1::verification_only() + .verify_schnorr(&sig, &message, &pubkey) + .map_err(|_| RadrootsSignatureVerificationError::VerificationFailed) +} + pub fn compute_nip01_event_id( pubkey: &str, created_at: u32, @@ -392,18 +635,84 @@ mod tests { core::iter::repeat_n(character, 64).collect() } + fn hex_128(character: char) -> String { + core::iter::repeat_n(character, 128).collect() + } + + fn raw_json_for_wire(wire: &RadrootsNip01EventWire) -> String { + serde_json::to_string(&serde_json::json!({ + "id": wire.id, + "pubkey": wire.pubkey, + "created_at": wire.created_at, + "kind": wire.kind, + "tags": wire.tags, + "content": wire.content, + "sig": wire.sig, + })) + .expect("raw json") + } + + fn verified_wire( + pubkey: String, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + sig: String, + ) -> RadrootsNip01EventWire { + let id = compute_canonical_nip01_event_id( + pubkey.as_str(), + created_at, + kind, + &tags, + content.as_str(), + ) + .expect("event id") + .into_string(); + RadrootsNip01EventWire { + id, + pubkey, + created_at, + kind, + tags, + content, + sig, + extra: Default::default(), + } + } + + fn unchecked_wire( + id: String, + pubkey: String, + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + sig: String, + ) -> RadrootsNip01EventWire { + RadrootsNip01EventWire { + id, + pubkey, + created_at, + kind, + tags, + content, + sig, + extra: Default::default(), + } + } + fn signed_event_for_draft(draft: &RadrootsEventDraft) -> RadrootsSignedEvent { - RadrootsSignedEvent::new(RadrootsSignedEventParts { - id: draft.expected_event_id.clone(), - pubkey: draft.expected_pubkey.clone(), - created_at: draft.created_at, - kind: draft.kind, - tags: draft.tags.clone(), - content: draft.content.clone(), - sig: "b".repeat(128), - raw_json: "{}".to_owned(), - }) - .expect("signed event") + let wire = verified_wire( + draft.expected_pubkey.clone(), + u64::from(draft.created_at), + draft.kind, + draft.tags.clone(), + draft.content.clone(), + hex_128('b'), + ); + let raw_json = raw_json_for_wire(&wire); + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event") } fn post_draft() -> RadrootsEventDraft { @@ -630,42 +939,52 @@ mod tests { #[test] fn signed_event_validates_ids_and_roundtrips_with_serde() { + let wire = verified_wire( + hex_64('e'), + 10, + KIND_POST, + vec![vec!["t".to_owned(), "soil".to_owned()]], + "hello".to_owned(), + hex_128('f'), + ); + let raw_json = raw_json_for_wire(&wire); let signed = RadrootsSignedEvent::new(RadrootsSignedEventParts { - id: hex_64('d'), - pubkey: hex_64('e'), - created_at: 10, - kind: KIND_POST, - tags: Vec::new(), - content: "hello".to_owned(), - sig: "f".repeat(128), - raw_json: "{\"id\":\"fixture\"}".to_owned(), + id: wire.id.clone(), + pubkey: wire.pubkey.clone(), + created_at: wire.created_at, + kind: wire.kind, + tags: wire.tags.clone(), + content: wire.content.clone(), + sig: wire.sig.clone(), + raw_json: raw_json.clone(), }) .expect("signed event"); let json = serde_json::to_string(&signed).expect("serialize"); let decoded: RadrootsSignedEvent = serde_json::from_str(&json).expect("deserialize"); assert_eq!(decoded, signed); - assert_eq!(decoded.pubkey, hex_64('e')); + assert_eq!(decoded.pubkey_str(), hex_64('e')); + assert_eq!(decoded.raw_json(), raw_json); } #[test] - fn signed_event_from_nostr_event_validates_parts() { - let event = RadrootsEventEnvelope { - id: hex_64('1'), - author: hex_64('2'), - created_at: 42, - kind: KIND_POST, - tags: vec![vec!["t".to_owned(), "soil".to_owned()]], - content: "hello".to_owned(), - sig: "3".repeat(128), - }; - let signed = - RadrootsSignedEvent::from_event(event, "{\"id\":\"fixture\"}").expect("signed event"); + fn signed_event_constructors_validate_wire_and_raw_json() { + let wire = verified_wire( + hex_64('2'), + 42, + KIND_POST, + vec![vec!["t".to_owned(), "soil".to_owned()]], + "hello".to_owned(), + hex_128('3'), + ); + let raw_json = raw_json_for_wire(&wire); + let signed = RadrootsSignedEvent::from_wire_verified_id(wire.clone(), raw_json.clone()) + .expect("signed event"); - assert_eq!(signed.id, hex_64('1')); - assert_eq!(signed.pubkey, hex_64('2')); - assert_eq!(signed.sig, "3".repeat(128)); - assert_eq!(signed.raw_json, "{\"id\":\"fixture\"}"); + assert_eq!(signed.id_str(), wire.id); + assert_eq!(signed.pubkey_str(), hex_64('2')); + assert_eq!(signed.sig_str(), hex_128('3')); + assert_eq!(signed.raw_json(), raw_json); let invalid = RadrootsSignedEvent::new(RadrootsSignedEventParts { id: "not-hex".to_owned(), @@ -678,7 +997,10 @@ mod tests { raw_json: "{}".to_owned(), }) .expect_err("invalid id"); - assert!(matches!(invalid, RadrootsDraftError::IdParse(_))); + assert!(matches!( + invalid, + RadrootsSignedEventError::Envelope(RadrootsEventEnvelopeError::InvalidId(_)) + )); let invalid = RadrootsSignedEvent::new(RadrootsSignedEventParts { id: hex_64('d'), @@ -691,7 +1013,10 @@ mod tests { raw_json: "{}".to_owned(), }) .expect_err("invalid pubkey"); - assert!(matches!(invalid, RadrootsDraftError::IdParse(_))); + assert!(matches!( + invalid, + RadrootsSignedEventError::Envelope(RadrootsEventEnvelopeError::InvalidAuthor(_)) + )); let invalid = RadrootsSignedEvent::new(RadrootsSignedEventParts { id: hex_64('d'), @@ -704,7 +1029,17 @@ mod tests { raw_json: "{}".to_owned(), }) .expect_err("invalid sig"); - assert!(matches!(invalid, RadrootsDraftError::IdParse(_))); + assert!(matches!( + invalid, + RadrootsSignedEventError::Envelope(RadrootsEventEnvelopeError::InvalidSignature(_)) + )); + + let mismatched_raw = + RadrootsSignedEvent::from_wire_verified_id(wire, "{}").expect_err("raw mismatch"); + assert!(matches!( + mismatched_raw, + RadrootsSignedEventError::RawJson(_) + )); } #[test] @@ -719,8 +1054,19 @@ mod tests { fn signed_event_validation_rejects_draft_mismatches() { let draft = post_draft(); - let mut signed = signed_event_for_draft(&draft); - signed.pubkey = hex_64('c'); + let signed = RadrootsSignedEvent::from_wire_unchecked( + unchecked_wire( + draft.expected_event_id.clone(), + hex_64('c'), + u64::from(draft.created_at), + draft.kind, + draft.tags.clone(), + draft.content.clone(), + hex_128('b'), + ), + "{}", + ) + .expect("unchecked signed event"); let error = validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); assert!(matches!( @@ -728,8 +1074,19 @@ mod tests { RadrootsDraftError::SignedEventPubkeyMismatch { .. } )); - let mut signed = signed_event_for_draft(&draft); - signed.id = hex_64('d'); + let signed = RadrootsSignedEvent::from_wire_unchecked( + unchecked_wire( + hex_64('d'), + draft.expected_pubkey.clone(), + u64::from(draft.created_at), + draft.kind, + draft.tags.clone(), + draft.content.clone(), + hex_128('b'), + ), + "{}", + ) + .expect("unchecked signed event"); let error = validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); assert!(matches!( @@ -737,8 +1094,19 @@ mod tests { RadrootsDraftError::SignedEventIdMismatch { .. } )); - let mut signed = signed_event_for_draft(&draft); - signed.created_at += 1; + let signed = RadrootsSignedEvent::from_wire_unchecked( + unchecked_wire( + draft.expected_event_id.clone(), + draft.expected_pubkey.clone(), + u64::from(draft.created_at) + 1, + draft.kind, + draft.tags.clone(), + draft.content.clone(), + hex_128('b'), + ), + "{}", + ) + .expect("unchecked signed event"); let error = validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); assert!(matches!( @@ -746,8 +1114,19 @@ mod tests { RadrootsDraftError::SignedEventCreatedAtMismatch { .. } )); - let mut signed = signed_event_for_draft(&draft); - signed.kind = KIND_PROFILE; + let signed = RadrootsSignedEvent::from_wire_unchecked( + unchecked_wire( + draft.expected_event_id.clone(), + draft.expected_pubkey.clone(), + u64::from(draft.created_at), + KIND_PROFILE, + draft.tags.clone(), + draft.content.clone(), + hex_128('b'), + ), + "{}", + ) + .expect("unchecked signed event"); let error = validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); assert!(matches!( @@ -755,8 +1134,21 @@ mod tests { RadrootsDraftError::SignedEventKindMismatch { .. } )); - let mut signed = signed_event_for_draft(&draft); - signed.tags.push(vec!["p".to_owned(), hex_64('e')]); + let mut tags = draft.tags.clone(); + tags.push(vec!["p".to_owned(), hex_64('e')]); + let signed = RadrootsSignedEvent::from_wire_unchecked( + unchecked_wire( + draft.expected_event_id.clone(), + draft.expected_pubkey.clone(), + u64::from(draft.created_at), + draft.kind, + tags, + draft.content.clone(), + hex_128('b'), + ), + "{}", + ) + .expect("unchecked signed event"); let error = validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); assert!(matches!( @@ -764,8 +1156,19 @@ mod tests { RadrootsDraftError::SignedEventTagsMismatch { .. } )); - let mut signed = signed_event_for_draft(&draft); - signed.content = "changed".to_owned(); + let signed = RadrootsSignedEvent::from_wire_unchecked( + unchecked_wire( + draft.expected_event_id.clone(), + draft.expected_pubkey.clone(), + u64::from(draft.created_at), + draft.kind, + draft.tags.clone(), + "changed".to_owned(), + hex_128('b'), + ), + "{}", + ) + .expect("unchecked signed event"); let error = validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); assert!(matches!( @@ -774,16 +1177,20 @@ mod tests { )); let mut draft = post_draft(); - draft.expected_pubkey = "not-hex".to_owned(); - let mut signed = signed_event_for_draft(&post_draft()); - signed.pubkey = "not-hex".to_owned(); - let error = - validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("id parse"); - assert!(matches!(error, RadrootsDraftError::IdParse(_))); - - let mut draft = post_draft(); draft.expected_event_id = hex_64('f'); - let signed = signed_event_for_draft(&draft); + let signed = RadrootsSignedEvent::from_wire_unchecked( + unchecked_wire( + draft.expected_event_id.clone(), + draft.expected_pubkey.clone(), + u64::from(draft.created_at), + draft.kind, + draft.tags.clone(), + draft.content.clone(), + hex_128('b'), + ), + "{}", + ) + .expect("unchecked signed event"); let error = validate_signed_nostr_event_matches_draft(&signed, &draft).expect_err("mismatch"); assert!(matches!( @@ -865,4 +1272,76 @@ mod tests { compute_nip01_event_id("not-hex", 1, KIND_POST, &[], "").expect_err("invalid pubkey"); assert!(matches!(error, RadrootsDraftError::IdParse(_))); } + + #[cfg(feature = "signature")] + #[test] + fn verified_signed_event_accepts_valid_bip340_signature() { + use secp256k1::{Keypair, Message, Secp256k1, SecretKey}; + + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[3u8; 32]).expect("secret key"); + let keypair = Keypair::from_secret_key(&secp, &secret_key); + let (pubkey, _) = keypair.x_only_public_key(); + let pubkey = pubkey.to_string(); + let tags = vec![vec!["t".to_owned(), "soil".to_owned()]]; + let content = "hello".to_owned(); + let event_id = compute_canonical_nip01_event_id( + pubkey.as_str(), + 1_700_000_000, + KIND_POST, + &tags, + content.as_str(), + ) + .expect("event id") + .into_string(); + let mut event_id_bytes = [0u8; 32]; + hex::decode_to_slice(event_id.as_str(), &mut event_id_bytes).expect("event id bytes"); + let message = Message::from_digest(event_id_bytes); + let sig = secp + .sign_schnorr_no_aux_rand(&message, &keypair) + .to_string(); + let wire = unchecked_wire( + event_id, + pubkey, + 1_700_000_000, + KIND_POST, + tags, + content, + sig, + ); + let raw_json = raw_json_for_wire(&wire); + let signed = + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event"); + let verified = signed.verify_signature().expect("verified event"); + + assert_eq!(verified.signed_event().kind(), KIND_POST); + } + + #[cfg(feature = "signature")] + #[test] + fn verified_signed_event_rejects_invalid_bip340_signature() { + use secp256k1::{Keypair, Secp256k1, SecretKey}; + + let secp = Secp256k1::new(); + let secret_key = SecretKey::from_slice(&[3u8; 32]).expect("secret key"); + let keypair = Keypair::from_secret_key(&secp, &secret_key); + let (pubkey, _) = keypair.x_only_public_key(); + let wire = verified_wire( + pubkey.to_string(), + 1_700_000_000, + KIND_POST, + Vec::new(), + "hello".to_owned(), + hex_128('b'), + ); + let raw_json = raw_json_for_wire(&wire); + let signed = + RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event"); + let error = signed.verify_signature().expect_err("invalid signature"); + + assert_eq!( + error, + RadrootsSignatureVerificationError::VerificationFailed + ); + } } diff --git a/crates/event/src/envelope.rs b/crates/event/src/envelope.rs @@ -0,0 +1,542 @@ +#![forbid(unsafe_code)] + +#[cfg(all(not(feature = "std"), not(test)))] +use alloc::{string::String, string::ToString, vec::Vec}; + +#[cfg(any(feature = "std", test))] +use std::{string::String, vec::Vec}; + +use crate::ids::{ + RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, RadrootsPublicKey, +}; +use crate::wire::RadrootsNip01EventWire; +use core::fmt; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RadrootsEventTimestamp(u64); + +impl RadrootsEventTimestamp { + pub const fn new(value: u64) -> Self { + Self(value) + } + + #[inline] + pub const fn as_u64(self) -> u64 { + self.0 + } +} + +impl From<u64> for RadrootsEventTimestamp { + #[inline] + fn from(value: u64) -> Self { + Self::new(value) + } +} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for RadrootsEventTimestamp { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_u64(self.0) + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for RadrootsEventTimestamp { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let value = u64::deserialize(deserializer)?; + Ok(Self::new(value)) + } +} + +#[cfg(feature = "dto-bindgen")] +impl dto_bindgen::Dto for RadrootsEventTimestamp { + fn describe(ctx: &mut dto_bindgen::__private::DescribeCtx) -> dto_bindgen::__private::TypeRef { + <u64 as dto_bindgen::Dto>::describe(ctx) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RadrootsEventKind(u32); + +impl RadrootsEventKind { + pub const fn new(value: u32) -> Self { + Self(value) + } + + #[inline] + pub const fn as_u32(self) -> u32 { + self.0 + } + + pub const fn class(self) -> RadrootsEventKindClass { + match self.0 { + 10_000..=19_999 => RadrootsEventKindClass::Replaceable, + 20_000..=29_999 => RadrootsEventKindClass::Ephemeral, + 30_000..=39_999 => RadrootsEventKindClass::Addressable, + _ => RadrootsEventKindClass::Regular, + } + } +} + +impl From<u32> for RadrootsEventKind { + #[inline] + fn from(value: u32) -> Self { + Self::new(value) + } +} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for RadrootsEventKind { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + serializer.serialize_u32(self.0) + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for RadrootsEventKind { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let value = u32::deserialize(deserializer)?; + Ok(Self::new(value)) + } +} + +#[cfg(feature = "dto-bindgen")] +impl dto_bindgen::Dto for RadrootsEventKind { + fn describe(ctx: &mut dto_bindgen::__private::DescribeCtx) -> dto_bindgen::__private::TypeRef { + <u32 as dto_bindgen::Dto>::describe(ctx) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsEventKindClass { + Regular, + Replaceable, + Ephemeral, + Addressable, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsEventEnvelopeError { + InvalidId(RadrootsIdParseError), + InvalidAuthor(RadrootsIdParseError), + InvalidSignature(RadrootsIdParseError), + NonCanonicalId, + NonCanonicalAuthor, + NonCanonicalSignature, + EmptyTag { index: usize }, + EmptyTagKey { index: usize }, + ControlCharacterTagKey { index: usize }, +} + +impl fmt::Display for RadrootsEventEnvelopeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidId(error) => write!(f, "event envelope id is invalid: {error}"), + Self::InvalidAuthor(error) => write!(f, "event envelope author is invalid: {error}"), + Self::InvalidSignature(error) => { + write!(f, "event envelope signature is invalid: {error}") + } + Self::NonCanonicalId => write!(f, "event envelope id must be canonical lowercase hex"), + Self::NonCanonicalAuthor => { + write!(f, "event envelope author must be canonical lowercase hex") + } + Self::NonCanonicalSignature => { + write!( + f, + "event envelope signature must be canonical lowercase hex" + ) + } + Self::EmptyTag { index } => write!(f, "event envelope tag {index} is empty"), + Self::EmptyTagKey { index } => write!(f, "event envelope tag {index} key is empty"), + Self::ControlCharacterTagKey { index } => { + write!( + f, + "event envelope tag {index} key contains a control character" + ) + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsEventEnvelopeError {} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsEventTag(Vec<String>); + +impl RadrootsEventTag { + pub fn new(index: usize, values: Vec<String>) -> Result<Self, RadrootsEventEnvelopeError> { + validate_tag(index, &values)?; + Ok(Self(values)) + } + + #[inline] + pub fn as_slice(&self) -> &[String] { + self.0.as_slice() + } + + #[inline] + pub fn into_vec(self) -> Vec<String> { + self.0 + } +} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for RadrootsEventTag { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for RadrootsEventTag { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let values = Vec::<String>::deserialize(deserializer)?; + Self::new(0, values).map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsEventTags(Vec<RadrootsEventTag>); + +impl RadrootsEventTags { + pub fn new(values: Vec<Vec<String>>) -> Result<Self, RadrootsEventEnvelopeError> { + let mut tags = Vec::with_capacity(values.len()); + for (index, tag) in values.into_iter().enumerate() { + tags.push(RadrootsEventTag::new(index, tag)?); + } + Ok(Self(tags)) + } + + #[inline] + pub fn as_slice(&self) -> &[RadrootsEventTag] { + self.0.as_slice() + } + + pub fn to_vec(&self) -> Vec<Vec<String>> { + self.0.iter().map(|tag| tag.as_slice().to_vec()).collect() + } + + #[inline] + pub fn into_vec(self) -> Vec<Vec<String>> { + self.0.into_iter().map(RadrootsEventTag::into_vec).collect() + } + + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +#[cfg(any(feature = "serde", test))] +impl serde::Serialize for RadrootsEventTags { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.to_vec().serialize(serializer) + } +} + +#[cfg(any(feature = "serde", test))] +impl<'de> serde::Deserialize<'de> for RadrootsEventTags { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let values = Vec::<Vec<String>>::deserialize(deserializer)?; + Self::new(values).map_err(serde::de::Error::custom) + } +} + +#[cfg(feature = "dto-bindgen")] +impl dto_bindgen::Dto for RadrootsEventTags { + fn describe(ctx: &mut dto_bindgen::__private::DescribeCtx) -> dto_bindgen::__private::TypeRef { + <Vec<Vec<String>> as dto_bindgen::Dto>::describe(ctx) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsEventEnvelopeParts { + pub id: String, + pub author: String, + pub created_at: u64, + pub kind: u32, + pub tags: Vec<Vec<String>>, + pub content: String, + pub sig: String, +} + +#[cfg_attr( + any(feature = "serde", test), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] +#[cfg_attr(feature = "dto-bindgen", dto(export))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsEventEnvelope { + id: RadrootsEventId, + author: RadrootsPublicKey, + created_at: RadrootsEventTimestamp, + kind: RadrootsEventKind, + tags: RadrootsEventTags, + content: String, + sig: RadrootsEventSignature, +} + +impl RadrootsEventEnvelope { + pub fn new(parts: RadrootsEventEnvelopeParts) -> Result<Self, RadrootsEventEnvelopeError> { + let id = RadrootsEventId::parse(parts.id.as_str()) + .map_err(RadrootsEventEnvelopeError::InvalidId)?; + if id.as_str() != parts.id.as_str() { + return Err(RadrootsEventEnvelopeError::NonCanonicalId); + } + let author = RadrootsPublicKey::parse(parts.author.as_str()) + .map_err(RadrootsEventEnvelopeError::InvalidAuthor)?; + if author.as_str() != parts.author.as_str() { + return Err(RadrootsEventEnvelopeError::NonCanonicalAuthor); + } + let sig = RadrootsEventSignature::parse(parts.sig.as_str()) + .map_err(RadrootsEventEnvelopeError::InvalidSignature)?; + if sig.as_str() != parts.sig.as_str() { + return Err(RadrootsEventEnvelopeError::NonCanonicalSignature); + } + let tags = RadrootsEventTags::new(parts.tags)?; + Ok(Self { + id, + author, + created_at: RadrootsEventTimestamp::new(parts.created_at), + kind: RadrootsEventKind::new(parts.kind), + tags, + content: parts.content, + sig, + }) + } + + #[inline] + pub fn id(&self) -> &RadrootsEventId { + &self.id + } + + #[inline] + pub fn id_str(&self) -> &str { + self.id.as_str() + } + + #[inline] + pub fn author(&self) -> &RadrootsPublicKey { + &self.author + } + + #[inline] + pub fn author_str(&self) -> &str { + self.author.as_str() + } + + #[inline] + pub fn created_at(&self) -> RadrootsEventTimestamp { + self.created_at + } + + #[inline] + pub fn created_at_u64(&self) -> u64 { + self.created_at.as_u64() + } + + #[inline] + pub fn kind(&self) -> RadrootsEventKind { + self.kind + } + + #[inline] + pub fn kind_u32(&self) -> u32 { + self.kind.as_u32() + } + + #[inline] + pub fn kind_class(&self) -> RadrootsEventKindClass { + self.kind.class() + } + + #[inline] + pub fn tags(&self) -> &RadrootsEventTags { + &self.tags + } + + #[inline] + pub fn tag_slices(&self) -> &[RadrootsEventTag] { + self.tags.as_slice() + } + + pub fn tags_as_vec(&self) -> Vec<Vec<String>> { + self.tags.to_vec() + } + + #[inline] + pub fn content(&self) -> &str { + self.content.as_str() + } + + #[inline] + pub fn sig(&self) -> &RadrootsEventSignature { + &self.sig + } + + #[inline] + pub fn sig_str(&self) -> &str { + self.sig.as_str() + } + + pub fn to_nip01_wire(&self) -> RadrootsNip01EventWire { + RadrootsNip01EventWire { + id: self.id.as_str().to_string(), + pubkey: self.author.as_str().to_string(), + created_at: self.created_at.as_u64(), + kind: self.kind.as_u32(), + tags: self.tags.to_vec(), + content: self.content.clone(), + sig: self.sig.as_str().to_string(), + extra: Default::default(), + } + } +} + +fn validate_tag(index: usize, values: &[String]) -> Result<(), RadrootsEventEnvelopeError> { + let Some(key) = values.first() else { + return Err(RadrootsEventEnvelopeError::EmptyTag { index }); + }; + if key.is_empty() { + return Err(RadrootsEventEnvelopeError::EmptyTagKey { index }); + } + if key.chars().any(char::is_control) { + return Err(RadrootsEventEnvelopeError::ControlCharacterTagKey { index }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn hex_64(character: char) -> String { + core::iter::repeat_n(character, 64).collect() + } + + fn hex_128(character: char) -> String { + core::iter::repeat_n(character, 128).collect() + } + + fn event_parts() -> RadrootsEventEnvelopeParts { + RadrootsEventEnvelopeParts { + id: hex_64('1'), + author: hex_64('a'), + created_at: u64::from(u32::MAX) + 1, + kind: 30_023, + tags: vec![vec!["d".to_owned(), "article".to_owned()]], + content: "hello".to_owned(), + sig: hex_128('b'), + } + } + + #[test] + fn envelope_uses_private_typed_state_and_getters() { + let envelope = RadrootsEventEnvelope::new(event_parts()).expect("envelope"); + + assert_eq!(envelope.id_str(), hex_64('1')); + assert_eq!(envelope.author_str(), hex_64('a')); + assert_eq!(envelope.created_at_u64(), u64::from(u32::MAX) + 1); + assert_eq!(envelope.kind_u32(), 30_023); + assert_eq!(envelope.kind_class(), RadrootsEventKindClass::Addressable); + assert_eq!(envelope.content(), "hello"); + assert_eq!(envelope.sig_str(), hex_128('b')); + assert_eq!( + envelope.tags_as_vec(), + vec![vec!["d".to_owned(), "article".to_owned()]] + ); + } + + #[test] + fn envelope_rejects_invalid_parts() { + let mut parts = event_parts(); + parts.id = "bad".to_owned(); + assert!(matches!( + RadrootsEventEnvelope::new(parts), + Err(RadrootsEventEnvelopeError::InvalidId(_)) + )); + + let mut parts = event_parts(); + parts.tags = vec![Vec::new()]; + assert_eq!( + RadrootsEventEnvelope::new(parts), + Err(RadrootsEventEnvelopeError::EmptyTag { index: 0 }) + ); + } + + #[test] + fn kind_classifies_nip01_ranges() { + assert_eq!( + RadrootsEventKind::new(1).class(), + RadrootsEventKindClass::Regular + ); + assert_eq!( + RadrootsEventKind::new(10_000).class(), + RadrootsEventKindClass::Replaceable + ); + assert_eq!( + RadrootsEventKind::new(20_000).class(), + RadrootsEventKindClass::Ephemeral + ); + assert_eq!( + RadrootsEventKind::new(30_000).class(), + RadrootsEventKindClass::Addressable + ); + assert_eq!( + RadrootsEventKind::new(40_000).class(), + RadrootsEventKindClass::Regular + ); + } + + #[test] + fn envelope_serializes_to_domain_shape() { + let envelope = RadrootsEventEnvelope::new(event_parts()).expect("envelope"); + let encoded = serde_json::to_value(&envelope).expect("json"); + + assert_eq!( + encoded.get("id").and_then(serde_json::Value::as_str), + Some(hex_64('1').as_str()) + ); + assert_eq!( + encoded.get("author").and_then(serde_json::Value::as_str), + Some(hex_64('a').as_str()) + ); + assert_eq!( + encoded + .get("created_at") + .and_then(serde_json::Value::as_u64), + Some(u64::from(u32::MAX) + 1) + ); + } +} diff --git a/crates/event/src/event_head.rs b/crates/event/src/event_head.rs @@ -1,14 +1,14 @@ #![forbid(unsafe_code)] #[cfg(not(feature = "std"))] -use alloc::{string::String, vec::Vec}; +use alloc::string::String; -use crate::RadrootsEventEnvelope; use crate::contract::{ RadrootsContractMatchError, RadrootsEventClass, RadrootsEventContract, identify_event_contract, }; use crate::ids::{RadrootsDTag, RadrootsEventId, RadrootsIdParseError, RadrootsPublicKey}; use crate::tags::TAG_D; +use crate::{RadrootsEventEnvelope, RadrootsEventTag}; #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum RadrootsEventHeadCoordinate { @@ -27,14 +27,14 @@ pub enum RadrootsEventHeadCoordinate { pub struct RadrootsEventHeadCandidate { pub coordinate: RadrootsEventHeadCoordinate, pub event_id: RadrootsEventId, - pub created_at: u32, + pub created_at: u64, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsCurrentEventHead { pub coordinate: RadrootsEventHeadCoordinate, pub event_id: RadrootsEventId, - pub created_at: u32, + pub created_at: u64, } impl From<RadrootsEventHeadCandidate> for RadrootsCurrentEventHead { @@ -80,29 +80,15 @@ pub fn event_head_candidate_for_class( RadrootsEventClass::Regular => RadrootsEventHeadCandidateResult::NotHeadSelected, RadrootsEventClass::Ephemeral => RadrootsEventHeadCandidateResult::NotPersisted, RadrootsEventClass::Replaceable | RadrootsEventClass::Addressable => { - let event_id = match RadrootsEventId::parse(&event.id) { - Ok(event_id) => event_id, - Err(error) => { - return RadrootsEventHeadCandidateResult::Malformed( - RadrootsEventHeadMalformed::InvalidEventId(error), - ); - } - }; - let pubkey = match RadrootsPublicKey::parse(&event.author) { - Ok(pubkey) => pubkey, - Err(error) => { - return RadrootsEventHeadCandidateResult::Malformed( - RadrootsEventHeadMalformed::InvalidPubkey(error), - ); - } - }; + let event_id = event.id().clone(); + let pubkey = event.author().clone(); let coordinate = if class == RadrootsEventClass::Replaceable { RadrootsEventHeadCoordinate::Replaceable { - kind: event.kind, + kind: event.kind_u32(), pubkey, } } else { - let Some(d_tag) = first_tag_value(&event.tags, TAG_D) else { + let Some(d_tag) = first_tag_value(event.tag_slices(), TAG_D) else { return RadrootsEventHeadCandidateResult::Malformed( RadrootsEventHeadMalformed::MissingDTag, ); @@ -116,7 +102,7 @@ pub fn event_head_candidate_for_class( } }; RadrootsEventHeadCoordinate::Addressable { - kind: event.kind, + kind: event.kind_u32(), pubkey, d_tag, } @@ -124,7 +110,7 @@ pub fn event_head_candidate_for_class( RadrootsEventHeadCandidateResult::Candidate(RadrootsEventHeadCandidate { coordinate, event_id, - created_at: event.created_at, + created_at: event.created_at_u64(), }) } } @@ -140,7 +126,8 @@ pub fn event_head_candidate_for_contract( pub fn event_head_candidate_for_event( event: &RadrootsEventEnvelope, ) -> Result<RadrootsEventHeadCandidateResult, RadrootsContractMatchError> { - let contract = identify_event_contract(event.kind, &event.tags, &event.content)?; + let tags = event.tags_as_vec(); + let contract = identify_event_contract(event.kind_u32(), &tags, event.content())?; Ok(event_head_candidate_for_contract(event, contract)) } @@ -170,16 +157,17 @@ pub fn select_event_head( } } -fn first_tag_value<'a>(tags: &'a [Vec<String>], name: &str) -> Option<&'a str> { +fn first_tag_value<'a>(tags: &'a [RadrootsEventTag], name: &str) -> Option<&'a str> { tags.iter() - .find(|tag| tag.first().map(String::as_str) == Some(name)) - .and_then(|tag| tag.get(1)) + .find(|tag| tag.as_slice().first().map(String::as_str) == Some(name)) + .and_then(|tag| tag.as_slice().get(1)) .map(String::as_str) } #[cfg(test)] mod tests { use super::*; + use crate::RadrootsEventEnvelopeParts; use crate::contract::RadrootsContractMatchError; use crate::kinds::{ KIND_FOLLOW, KIND_LIST_SET_GENERIC, KIND_ORDER_REQUEST, KIND_POST, KIND_PROFILE, @@ -189,38 +177,50 @@ mod tests { core::iter::repeat_n(character, 64).collect() } + fn hex_128(character: char) -> String { + core::iter::repeat_n(character, 128).collect() + } + fn event( kind: u32, id: &str, author: &str, - created_at: u32, + created_at: u64, tags: Vec<Vec<String>>, ) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: id.to_string(), author: author.to_string(), created_at, kind, tags, content: String::new(), - sig: String::new(), - } + sig: hex_128('f'), + }) + .expect("event envelope") } fn event_with_content( kind: u32, id: &str, author: &str, - created_at: u32, + created_at: u64, tags: Vec<Vec<String>>, content: &str, ) -> RadrootsEventEnvelope { - let mut event = event(kind, id, author, created_at, tags); - event.content = content.to_string(); - event + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: id.to_string(), + author: author.to_string(), + created_at, + kind, + tags, + content: content.to_string(), + sig: hex_128('f'), + }) + .expect("event envelope") } - fn candidate(id: char, created_at: u32) -> RadrootsEventHeadCandidate { + fn candidate(id: char, created_at: u64) -> RadrootsEventHeadCandidate { expect_candidate(event_head_candidate_for_class( &event(10002, &hex_64(id), &hex_64('a'), created_at, Vec::new()), RadrootsEventClass::Replaceable, @@ -309,25 +309,6 @@ mod tests { } #[test] - fn malformed_candidates_report_invalid_event_ids_and_pubkeys() { - let bad_event_id = event(10002, "not-hex", &hex_64('a'), 1, Vec::new()); - assert!(matches!( - event_head_candidate_for_class(&bad_event_id, RadrootsEventClass::Replaceable), - RadrootsEventHeadCandidateResult::Malformed( - RadrootsEventHeadMalformed::InvalidEventId(_) - ) - )); - - let bad_pubkey = event(10002, &hex_64('1'), "not-hex", 1, Vec::new()); - assert!(matches!( - event_head_candidate_for_class(&bad_pubkey, RadrootsEventClass::Replaceable), - RadrootsEventHeadCandidateResult::Malformed(RadrootsEventHeadMalformed::InvalidPubkey( - _ - )) - )); - } - - #[test] fn event_head_selection_uses_nip01_time_and_lowest_id_rules() { let current: RadrootsCurrentEventHead = candidate('3', 10).into(); diff --git a/crates/event/src/follow.rs b/crates/event/src/follow.rs @@ -20,7 +20,7 @@ pub struct RadrootsFollow { )] #[derive(Clone, Debug)] pub struct RadrootsFollowProfile { - pub published_at: u32, + pub published_at: u64, pub public_key: String, pub relay_url: Option<String>, pub contact_name: Option<String>, diff --git a/crates/event/src/lib.rs b/crates/event/src/lib.rs @@ -19,6 +19,7 @@ pub mod document; pub mod draft; #[cfg(feature = "dto-bindgen")] pub mod dto; +pub mod envelope; pub mod event_head; pub mod farm; pub mod farm_crdt; @@ -63,6 +64,11 @@ pub mod tags; pub mod trade_validation; pub mod wire; +pub use envelope::{ + RadrootsEventEnvelope, RadrootsEventEnvelopeError, RadrootsEventEnvelopeParts, + RadrootsEventKind, RadrootsEventKindClass, RadrootsEventTag, RadrootsEventTags, + RadrootsEventTimestamp, +}; pub use wire::{ RadrootsCanonicalEventIdError, RadrootsEventWireError, RadrootsEventWireLimits, RadrootsNip01EventWire, canonical_nip01_event_id_preimage, compute_canonical_nip01_event_id, @@ -75,23 +81,6 @@ pub use wire::{ #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] #[cfg_attr(feature = "dto-bindgen", dto(export))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsEventEnvelope { - pub id: String, - pub author: String, - pub created_at: u32, - pub kind: u32, - pub tags: Vec<Vec<String>>, - pub content: String, - pub sig: String, -} - -#[cfg_attr( - any(feature = "serde", test), - derive(serde::Serialize, serde::Deserialize) -)] -#[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] -#[cfg_attr(feature = "dto-bindgen", dto(export))] -#[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsEventRef { pub id: String, pub author: String, diff --git a/crates/event/src/wire.rs b/crates/event/src/wire.rs @@ -13,6 +13,7 @@ use std::{collections::BTreeMap, string::String, vec::Vec}; use crate::ids::{ RadrootsEventId, RadrootsEventSignature, RadrootsIdParseError, RadrootsPublicKey, }; +use crate::{RadrootsEventEnvelope, RadrootsEventEnvelopeError, RadrootsEventEnvelopeParts}; use core::fmt; use serde_json::{Map, Value}; use sha2::{Digest, Sha256}; @@ -197,7 +198,10 @@ impl From<RadrootsCanonicalEventIdError> for RadrootsEventWireError { } } -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + any(feature = "serde", test), + derive(serde::Serialize, serde::Deserialize) +)] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsNip01EventWire { pub id: String, @@ -207,7 +211,7 @@ pub struct RadrootsNip01EventWire { pub tags: Vec<Vec<String>>, pub content: String, pub sig: String, - #[cfg_attr(feature = "serde", serde(flatten))] + #[cfg_attr(any(feature = "serde", test), serde(flatten))] pub extra: BTreeMap<String, Value>, } @@ -263,6 +267,18 @@ impl RadrootsNip01EventWire { Ok(()) } + pub fn into_envelope(self) -> Result<RadrootsEventEnvelope, RadrootsEventEnvelopeError> { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: self.id, + author: self.pubkey, + created_at: self.created_at, + kind: self.kind, + tags: self.tags, + content: self.content, + sig: self.sig, + }) + } + fn from_json_value( value: Value, limits: RadrootsEventWireLimits, diff --git a/crates/event_codec/src/app_data/decode.rs b/crates/event_codec/src/app_data/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, app_data::{KIND_APP_DATA, RadrootsAppData}, tags::TAG_D, }; @@ -49,7 +48,7 @@ pub fn app_data_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -67,7 +66,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -81,16 +80,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/article/decode.rs b/crates/event_codec/src/article/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, article::RadrootsArticle, farm::RadrootsFarmRef, kinds::{KIND_ARTICLE, KIND_FARM}, @@ -62,7 +61,7 @@ pub fn article_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -80,7 +79,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -94,18 +93,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn parse_farm_anchor( diff --git a/crates/event_codec/src/calendar/decode.rs b/crates/event_codec/src/calendar/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, calendar::{ RadrootsCalendar, RadrootsCalendarDateEvent, RadrootsCalendarEventRsvp, RadrootsCalendarTimeEvent, @@ -195,7 +194,7 @@ pub fn rsvp_from_event( pub fn date_data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -213,7 +212,7 @@ pub fn date_data_from_event( pub fn time_data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -231,7 +230,7 @@ pub fn time_data_from_event( pub fn calendar_data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -249,7 +248,7 @@ pub fn calendar_data_from_event( pub fn rsvp_data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -267,7 +266,7 @@ pub fn rsvp_data_from_event( pub fn date_parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -281,24 +280,13 @@ pub fn date_parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } pub fn time_parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -312,24 +300,13 @@ pub fn time_parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } pub fn calendar_parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -343,24 +320,13 @@ pub fn calendar_parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } pub fn rsvp_parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -374,18 +340,7 @@ pub fn rsvp_parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn parse_required_u64(tags: &[Vec<String>], key: &'static str) -> Result<u64, EventParseError> { diff --git a/crates/event_codec/src/comment/decode.rs b/crates/event_codec/src/comment/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, comment::RadrootsComment, kinds::{KIND_COMMENT, KIND_POST}, social::RadrootsSocialTarget, @@ -215,7 +214,7 @@ fn required_numeric_kind(tags: &[Vec<String>], key: &'static str) -> Result<u32, pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -233,7 +232,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -247,18 +246,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } #[cfg(test)] diff --git a/crates/event_codec/src/coop/decode.rs b/crates/event_codec/src/coop/decode.rs @@ -7,7 +7,7 @@ use alloc::{ vec::Vec, }; -use radroots_event::{RadrootsEventEnvelope, coop::RadrootsCoop, kinds::KIND_COOP, tags::TAG_D}; +use radroots_event::{coop::RadrootsCoop, kinds::KIND_COOP, tags::TAG_D}; use crate::d_tag::validate_d_tag_tag; use crate::error::EventParseError; @@ -61,7 +61,7 @@ pub fn coop_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -79,7 +79,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -93,16 +93,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/document/decode.rs b/crates/event_codec/src/document/decode.rs @@ -7,9 +7,7 @@ use alloc::{ vec::Vec, }; -use radroots_event::{ - RadrootsEventEnvelope, document::RadrootsDocument, kinds::KIND_DOCUMENT, tags::TAG_D, -}; +use radroots_event::{document::RadrootsDocument, kinds::KIND_DOCUMENT, tags::TAG_D}; use crate::d_tag::validate_d_tag_tag; use crate::error::EventParseError; @@ -124,7 +122,7 @@ pub fn document_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -142,7 +140,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -156,18 +154,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } #[cfg(test)] diff --git a/crates/event_codec/src/error.rs b/crates/event_codec/src/error.rs @@ -5,6 +5,7 @@ pub enum EventParseError { MissingTag(&'static str), InvalidTag(&'static str), DuplicateTag(&'static str), + InvalidEnvelope, InvalidKind { expected: &'static str, got: u32 }, InvalidNumber(&'static str, core::num::ParseIntError), InvalidJson(&'static str), @@ -16,6 +17,7 @@ impl EventParseError { Self::MissingTag(_) => "missing_tag", Self::InvalidTag(_) => "invalid_tag", Self::DuplicateTag(_) => "duplicate_tag", + Self::InvalidEnvelope => "invalid_envelope", Self::InvalidKind { .. } => "invalid_kind", Self::InvalidNumber(_, _) => "invalid_number", Self::InvalidJson(_) => "invalid_json", @@ -29,6 +31,7 @@ impl fmt::Display for EventParseError { EventParseError::MissingTag(t) => write!(f, "missing tag: {}", t), EventParseError::InvalidTag(t) => write!(f, "invalid tag structure for '{}'", t), EventParseError::DuplicateTag(t) => write!(f, "duplicate tag: {}", t), + EventParseError::InvalidEnvelope => write!(f, "invalid event envelope"), EventParseError::InvalidKind { expected, got } => { write!(f, "invalid kind {} (expected {})", got, expected) } @@ -48,6 +51,12 @@ impl std::error::Error for EventParseError { } } +impl From<radroots_event::RadrootsEventEnvelopeError> for EventParseError { + fn from(_: radroots_event::RadrootsEventEnvelopeError) -> Self { + Self::InvalidEnvelope + } +} + #[derive(Debug)] pub enum EventEncodeError { InvalidKind(u32), diff --git a/crates/event_codec/src/farm/decode.rs b/crates/event_codec/src/farm/decode.rs @@ -7,7 +7,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, farm::RadrootsFarm, kinds::KIND_FARM, location::{has_textual_locality, is_public_geohash5}, @@ -152,7 +151,7 @@ fn reject_private_farm_ops_content(content: &str) -> Result<(), EventParseError> pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -170,7 +169,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -184,16 +183,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/farm/mod.rs b/crates/event_codec/src/farm/mod.rs @@ -296,7 +296,7 @@ mod tests { "sig".to_string(), ) .expect("parsed farm"); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data.name, "Test Farm"); for (tag, expected) in [ diff --git a/crates/event_codec/src/farm_crdt/decode.rs b/crates/event_codec/src/farm_crdt/decode.rs @@ -2,7 +2,6 @@ use alloc::{string::String, vec::Vec}; use radroots_event::{ - RadrootsEventEnvelope, farm_crdt::{KIND_FARM_CRDT_CHANGE, RADROOTS_FARM_CRDT_TAG, RadrootsFarmCrdtChange}, farm_workspace::KIND_FARM_WORKSPACE_MANIFEST, tags::{TAG_A, TAG_D, TAG_H, TAG_P, TAG_T}, @@ -40,7 +39,7 @@ pub fn farm_crdt_change_from_event_with_author( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -58,7 +57,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -72,18 +71,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn farm_crdt_change_from_event_inner( diff --git a/crates/event_codec/src/farm_crdt/mod.rs b/crates/event_codec/src/farm_crdt/mod.rs @@ -281,7 +281,7 @@ mod tests { "sig".to_string(), ) .expect("parsed event"); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data, change); let no_author_parts = to_wire_parts(&change).expect("crdt wire parts"); diff --git a/crates/event_codec/src/farm_file/decode.rs b/crates/event_codec/src/farm_file/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, farm_crdt::RadrootsFarmCrdtDocumentKind, farm_file::{ KIND_FARM_FILE_METADATA, RadrootsFarmFileDimensions, RadrootsFarmFileMetadata, @@ -99,7 +98,7 @@ pub fn farm_file_metadata_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -117,7 +116,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -131,18 +130,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn required_single_tag_value( diff --git a/crates/event_codec/src/farm_file/mod.rs b/crates/event_codec/src/farm_file/mod.rs @@ -199,7 +199,7 @@ mod tests { "sig".to_string(), ) .expect("parsed event"); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data, metadata); } diff --git a/crates/event_codec/src/farm_workspace/decode.rs b/crates/event_codec/src/farm_workspace/decode.rs @@ -2,7 +2,6 @@ use alloc::{string::String, vec::Vec}; use radroots_event::{ - RadrootsEventEnvelope, farm_workspace::{ KIND_FARM_WORKSPACE_MANIFEST, RADROOTS_FARM_WORKSPACE_TAG, RadrootsFarmWorkspaceManifest, }, @@ -75,7 +74,7 @@ pub fn farm_workspace_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -93,7 +92,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -107,18 +106,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn encode_error_to_parse_error(error: crate::error::EventEncodeError) -> EventParseError { diff --git a/crates/event_codec/src/farm_workspace/mod.rs b/crates/event_codec/src/farm_workspace/mod.rs @@ -198,7 +198,7 @@ mod tests { "sig".to_string(), ) .expect("parsed event"); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_same_manifest(&parsed.data.data, &manifest); } diff --git a/crates/event_codec/src/file_metadata/decode.rs b/crates/event_codec/src/file_metadata/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, file_metadata::RadrootsFileMetadata, kinds::KIND_PUBLIC_FILE_METADATA, social::RadrootsSocialMediaThumbnail, @@ -90,7 +89,7 @@ fn reject_private_farm_file_tags(tags: &[Vec<String>]) -> Result<(), EventParseE pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -108,7 +107,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -122,18 +121,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn optional_hash_tag( diff --git a/crates/event_codec/src/follow/decode.rs b/crates/event_codec/src/follow/decode.rs @@ -2,7 +2,6 @@ use alloc::{string::String, vec::Vec}; use radroots_event::{ - RadrootsEventEnvelope, follow::{RadrootsFollow, RadrootsFollowProfile}, kinds::KIND_FOLLOW, }; @@ -18,7 +17,7 @@ fn looks_like_ws_relay(s: &str) -> bool { fn parse_follow_tag( tag: &[String], - published_at: u32, + published_at: u64, ) -> Result<RadrootsFollowProfile, EventParseError> { let public_key = tag.get(1).ok_or(EventParseError::InvalidTag("p"))?; let (relay_url, contact_name) = match tag.get(2).filter(|s| !s.is_empty()) { @@ -48,7 +47,7 @@ fn parse_follow_tag( pub fn follow_from_tags( kind: u32, tags: &[Vec<String>], - published_at: u32, + published_at: u64, ) -> Result<RadrootsFollow, EventParseError> { if kind != DEFAULT_KIND { return Err(EventParseError::InvalidKind { @@ -69,7 +68,7 @@ pub fn follow_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, _content: String, tags: Vec<Vec<String>>, @@ -87,7 +86,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -101,16 +100,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/geochat/decode.rs b/crates/event_codec/src/geochat/decode.rs @@ -4,7 +4,7 @@ use alloc::{ vec::Vec, }; -use radroots_event::{RadrootsEventEnvelope, geochat::RadrootsGeoChat, kinds::KIND_GEOCHAT}; +use radroots_event::{geochat::RadrootsGeoChat, kinds::KIND_GEOCHAT}; use crate::error::EventParseError; use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent}; @@ -88,7 +88,7 @@ pub fn geochat_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -106,7 +106,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -120,16 +120,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/gift_wrap/decode.rs b/crates/event_codec/src/gift_wrap/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, gift_wrap::{RadrootsGiftWrap, RadrootsGiftWrapRecipient}, kinds::KIND_GIFT_WRAP, }; @@ -78,7 +77,7 @@ pub fn gift_wrap_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -96,7 +95,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -110,16 +109,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/http_auth/decode.rs b/crates/event_codec/src/http_auth/decode.rs @@ -2,7 +2,6 @@ use alloc::{string::String, vec::Vec}; use radroots_event::{ - RadrootsEventEnvelope, http_auth::{KIND_HTTP_AUTH, RadrootsHttpAuth}, tags::{TAG_METHOD, TAG_PAYLOAD, TAG_URL_AUTH}, }; @@ -42,7 +41,7 @@ pub fn http_auth_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -60,7 +59,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -74,16 +73,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/http_auth/mod.rs b/crates/event_codec/src/http_auth/mod.rs @@ -195,7 +195,7 @@ mod tests { "sig".to_string(), ) .expect("parsed event"); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data, auth); } diff --git a/crates/event_codec/src/job/feedback/decode.rs b/crates/event_codec/src/job/feedback/decode.rs @@ -1,6 +1,6 @@ use radroots_event::{ - RadrootsEventEnvelope, RadrootsEventPtr, job::JobPaymentRequest, - job_feedback::RadrootsJobFeedback, kinds::KIND_JOB_FEEDBACK, + RadrootsEventPtr, job::JobPaymentRequest, job_feedback::RadrootsJobFeedback, + kinds::KIND_JOB_FEEDBACK, }; #[cfg(not(feature = "std"))] @@ -77,7 +77,7 @@ pub fn job_feedback_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -98,7 +98,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -112,16 +112,6 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) + .map_err(|_| JobParseError::InvalidTag("event_envelope")) } diff --git a/crates/event_codec/src/job/request/decode.rs b/crates/event_codec/src/job/request/decode.rs @@ -1,5 +1,4 @@ use radroots_event::{ - RadrootsEventEnvelope, job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest}, kinds::is_request_kind, }; @@ -68,7 +67,7 @@ pub fn job_request_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, tags: Vec<Vec<String>>, ) -> Result<RadrootsParsedData<RadrootsJobRequest>, JobParseError> { @@ -88,23 +87,13 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, sig: String, ) -> Result<RadrootsParsedEvent<RadrootsJobRequest>, JobParseError> { let data = data_from_event(id.clone(), author.clone(), published_at, kind, tags.clone())?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) + .map_err(|_| JobParseError::InvalidTag("event_envelope")) } diff --git a/crates/event_codec/src/job/result/decode.rs b/crates/event_codec/src/job/result/decode.rs @@ -1,5 +1,5 @@ use radroots_event::{ - RadrootsEventEnvelope, RadrootsEventPtr, job::JobPaymentRequest, job_request::RadrootsJobInput, + RadrootsEventPtr, job::JobPaymentRequest, job_request::RadrootsJobInput, job_result::RadrootsJobResult, kinds::is_result_kind, }; @@ -73,7 +73,7 @@ pub fn job_result_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -94,7 +94,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -108,16 +108,6 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) + .map_err(|_| JobParseError::InvalidTag("event_envelope")) } diff --git a/crates/event_codec/src/job/traits.rs b/crates/event_codec/src/job/traits.rs @@ -27,7 +27,7 @@ use crate::job::{ pub trait JobEventLike { fn raw_id(&self) -> String; fn raw_author(&self) -> String; - fn raw_published_at(&self) -> u32; + fn raw_published_at(&self) -> u64; fn raw_kind(&self) -> u32; fn raw_content(&self) -> String; fn raw_tags(&self) -> Vec<Vec<String>>; @@ -124,13 +124,13 @@ pub trait JobEventBorrow<'a> { #[derive(Clone, Copy)] pub struct BorrowedEventAdapter<'a, E: JobEventBorrow<'a>> { inner: &'a E, - published_at: u32, + published_at: u64, tags: &'a [Vec<String>], sig: &'a str, } impl<'a, E: JobEventBorrow<'a>> BorrowedEventAdapter<'a, E> { - pub fn new(inner: &'a E, published_at: u32, tags: &'a [Vec<String>], sig: &'a str) -> Self { + pub fn new(inner: &'a E, published_at: u64, tags: &'a [Vec<String>], sig: &'a str) -> Self { Self { inner, published_at, @@ -150,7 +150,7 @@ impl<'a, E: JobEventBorrow<'a>> JobEventLike for BorrowedEventAdapter<'a, E> { self.inner.raw_author().to_owned() } #[inline] - fn raw_published_at(&self) -> u32 { + fn raw_published_at(&self) -> u64 { self.published_at } #[inline] @@ -174,18 +174,18 @@ impl<'a, E: JobEventBorrow<'a>> JobEventLike for BorrowedEventAdapter<'a, E> { impl<'a> JobEventBorrow<'a> for radroots_event::RadrootsEventEnvelope { #[inline] fn raw_id(&'a self) -> &'a str { - &self.id + self.id_str() } #[inline] fn raw_author(&'a self) -> &'a str { - &self.author + self.author_str() } #[inline] fn raw_content(&'a self) -> &'a str { - &self.content + self.content() } #[inline] fn raw_kind(&'a self) -> u32 { - self.kind + self.kind_u32() } } diff --git a/crates/event_codec/src/knowledge/decode.rs b/crates/event_codec/src/knowledge/decode.rs @@ -377,10 +377,10 @@ fn json_content<T: DeserializeOwned>(content: &str) -> Result<T, EventParseError fn parsed<T>(event: RadrootsEventEnvelope, data: T) -> RadrootsParsedEvent<T> { let parsed_data = RadrootsParsedData::new( - event.id.clone(), - event.author.clone(), - event.created_at, - event.kind, + event.id_str().to_string(), + event.author_str().to_string(), + event.created_at_u64(), + event.kind_u32(), data, ); RadrootsParsedEvent::new(event, parsed_data) @@ -444,15 +444,15 @@ fn contains_private_coordinate_key(value: &serde_json::Value) -> bool { pub fn wiki_article_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsWikiArticle>, EventParseError> { - ensure_kind(event.kind, KIND_WIKI_ARTICLE, "wiki article")?; - let d_tag = required_one_value(&event.tags, TAG_D)?; + ensure_kind(event.kind_u32(), KIND_WIKI_ARTICLE, "wiki article")?; + let d_tag = required_one_value(&event.tags_as_vec(), TAG_D)?; validate_wiki_d_tag(&d_tag).map_err(|_| EventParseError::InvalidTag(TAG_D))?; - let title = optional_one_value(&event.tags, TAG_TITLE)?; - let summary = optional_one_value(&event.tags, TAG_SUMMARY)?; - let topics = values(&event.tags, TAG_T); - let references = event_refs(&event.tags, TAG_SOURCE)?; - let forked_from = wiki_version_refs(&event.tags, MARKER_FORK)?; - let mut deferred_refs = wiki_version_refs(&event.tags, MARKER_DEFER)?; + let title = optional_one_value(&event.tags_as_vec(), TAG_TITLE)?; + let summary = optional_one_value(&event.tags_as_vec(), TAG_SUMMARY)?; + let topics = values(&event.tags_as_vec(), TAG_T); + let references = event_refs(&event.tags_as_vec(), TAG_SOURCE)?; + let forked_from = wiki_version_refs(&event.tags_as_vec(), MARKER_FORK)?; + let mut deferred_refs = wiki_version_refs(&event.tags_as_vec(), MARKER_DEFER)?; if deferred_refs.len() > 1 { return Err(EventParseError::InvalidTag(TAG_A)); } @@ -460,7 +460,7 @@ pub fn wiki_article_from_event( let article = RadrootsWikiArticle { d_tag, title, - content_djot: event.content.clone(), + content_djot: event.content().to_string(), summary, topics, references, @@ -474,13 +474,13 @@ pub fn wiki_article_from_event( pub fn wiki_redirect_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsWikiRedirect>, EventParseError> { - ensure_kind(event.kind, KIND_WIKI_REDIRECT, "wiki redirect")?; - if !event.content.is_empty() { + ensure_kind(event.kind_u32(), KIND_WIKI_REDIRECT, "wiki redirect")?; + if !event.content().is_empty() { return Err(EventParseError::InvalidJson("content")); } - let d_tag = required_one_value(&event.tags, TAG_D)?; + let d_tag = required_one_value(&event.tags_as_vec(), TAG_D)?; validate_wiki_d_tag(&d_tag).map_err(|_| EventParseError::InvalidTag(TAG_D))?; - let target = address_from_a_tag(&event.tags, TAG_A)?; + let target = address_from_a_tag(&event.tags_as_vec(), TAG_A)?; if target.kind != KIND_WIKI_ARTICLE { return Err(EventParseError::InvalidTag(TAG_A)); } @@ -492,18 +492,22 @@ pub fn wiki_redirect_from_event( pub fn wiki_merge_request_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsWikiMergeRequest>, EventParseError> { - ensure_kind(event.kind, KIND_WIKI_MERGE_REQUEST, "wiki merge request")?; - let target_article = address_from_a_tag(&event.tags, TAG_A)?; + ensure_kind( + event.kind_u32(), + KIND_WIKI_MERGE_REQUEST, + "wiki merge request", + )?; + let target_article = address_from_a_tag(&event.tags_as_vec(), TAG_A)?; if target_article.kind != KIND_WIKI_ARTICLE { return Err(EventParseError::InvalidTag(TAG_A)); } - let destination_pubkey = required_one_value(&event.tags, TAG_P)?; - let base_version_event_id = wiki_merge_base_event_id(&event.tags)?; - let source_version_event_id = wiki_merge_source_event_id(&event.tags)?; - let explanation = if event.content.is_empty() { + let destination_pubkey = required_one_value(&event.tags_as_vec(), TAG_P)?; + let base_version_event_id = wiki_merge_base_event_id(&event.tags_as_vec())?; + let source_version_event_id = wiki_merge_source_event_id(&event.tags_as_vec())?; + let explanation = if event.content().is_empty() { None } else { - Some(event.content.clone()) + Some(event.content().to_string()) }; let request = RadrootsWikiMergeRequest { target_article, @@ -519,48 +523,56 @@ pub fn wiki_merge_request_from_event( pub fn knowledge_source_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsKnowledgeSource>, EventParseError> { - ensure_kind(event.kind, KIND_KNOWLEDGE_SOURCE, "knowledge source")?; - require_contract_tag(&event.tags, RADROOTS_KNOWLEDGE_SOURCE_SCHEMA)?; - let source: RadrootsKnowledgeSource = json_content(&event.content)?; - let d_tag = required_one_value(&event.tags, TAG_D)?; + ensure_kind(event.kind_u32(), KIND_KNOWLEDGE_SOURCE, "knowledge source")?; + require_contract_tag(&event.tags_as_vec(), RADROOTS_KNOWLEDGE_SOURCE_SCHEMA)?; + let source: RadrootsKnowledgeSource = json_content(event.content())?; + let d_tag = required_one_value(&event.tags_as_vec(), TAG_D)?; if d_tag != source.d_tag { return Err(EventParseError::InvalidTag(TAG_D)); } validate_knowledge_source(&source).map_err(parse_validation_error)?; let expected_tags = knowledge_source_build_tags(&source).map_err(|_| EventParseError::InvalidTag(TAG_D))?; - ensure_mirrored_tags(&event.tags, &expected_tags, &[TAG_D, TAG_T, TAG_SOURCE])?; + ensure_mirrored_tags( + &event.tags_as_vec(), + &expected_tags, + &[TAG_D, TAG_T, TAG_SOURCE], + )?; Ok(parsed(event, source)) } pub fn evidence_bounty_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsEvidenceBounty>, EventParseError> { - ensure_kind(event.kind, KIND_EVIDENCE_BOUNTY, "evidence bounty")?; - require_contract_tag(&event.tags, RADROOTS_EVIDENCE_BOUNTY_SCHEMA)?; - let bounty: RadrootsEvidenceBounty = json_content(&event.content)?; - let d_tag = required_one_value(&event.tags, TAG_D)?; + ensure_kind(event.kind_u32(), KIND_EVIDENCE_BOUNTY, "evidence bounty")?; + require_contract_tag(&event.tags_as_vec(), RADROOTS_EVIDENCE_BOUNTY_SCHEMA)?; + let bounty: RadrootsEvidenceBounty = json_content(event.content())?; + let d_tag = required_one_value(&event.tags_as_vec(), TAG_D)?; if d_tag != bounty.d_tag { return Err(EventParseError::InvalidTag(TAG_D)); } validate_evidence_bounty(&bounty).map_err(parse_validation_error)?; let expected_tags = evidence_bounty_build_tags(&bounty).map_err(|_| EventParseError::InvalidTag(TAG_D))?; - ensure_mirrored_tags(&event.tags, &expected_tags, &[TAG_D, TAG_T, TAG_EVIDENCE])?; + ensure_mirrored_tags( + &event.tags_as_vec(), + &expected_tags, + &[TAG_D, TAG_T, TAG_EVIDENCE], + )?; Ok(parsed(event, bounty)) } pub fn knowledge_claim_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsKnowledgeClaim>, EventParseError> { - ensure_kind(event.kind, KIND_KNOWLEDGE_CLAIM, "knowledge claim")?; - require_contract_tag(&event.tags, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA)?; - let claim: RadrootsKnowledgeClaim = json_content(&event.content)?; + ensure_kind(event.kind_u32(), KIND_KNOWLEDGE_CLAIM, "knowledge claim")?; + require_contract_tag(&event.tags_as_vec(), RADROOTS_KNOWLEDGE_CLAIM_SCHEMA)?; + let claim: RadrootsKnowledgeClaim = json_content(event.content())?; validate_knowledge_claim(&claim).map_err(parse_validation_error)?; let expected_tags = knowledge_claim_build_tags(&claim).map_err(|_| EventParseError::InvalidTag(TAG_SOURCE))?; ensure_mirrored_tags( - &event.tags, + &event.tags_as_vec(), &expected_tags, &[TAG_T, TAG_SOURCE, TAG_CITATION], )?; @@ -570,28 +582,32 @@ pub fn knowledge_claim_from_event( pub fn knowledge_relation_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsKnowledgeRelation>, EventParseError> { - ensure_kind(event.kind, KIND_KNOWLEDGE_RELATION, "knowledge relation")?; - require_contract_tag(&event.tags, RADROOTS_KNOWLEDGE_RELATION_SCHEMA)?; - let relation: RadrootsKnowledgeRelation = json_content(&event.content)?; + ensure_kind( + event.kind_u32(), + KIND_KNOWLEDGE_RELATION, + "knowledge relation", + )?; + require_contract_tag(&event.tags_as_vec(), RADROOTS_KNOWLEDGE_RELATION_SCHEMA)?; + let relation: RadrootsKnowledgeRelation = json_content(event.content())?; validate_knowledge_relation(&relation).map_err(parse_validation_error)?; let expected_tags = knowledge_relation_build_tags(&relation) .map_err(|_| EventParseError::InvalidTag(TAG_SOURCE))?; - ensure_mirrored_tags(&event.tags, &expected_tags, &[TAG_SOURCE])?; + ensure_mirrored_tags(&event.tags_as_vec(), &expected_tags, &[TAG_SOURCE])?; Ok(parsed(event, relation)) } pub fn knowledge_review_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsKnowledgeReview>, EventParseError> { - ensure_kind(event.kind, KIND_KNOWLEDGE_REVIEW, "knowledge review")?; - require_contract_tag(&event.tags, RADROOTS_KNOWLEDGE_REVIEW_SCHEMA)?; - required_one_value(&event.tags, TAG_REVIEW_TARGET)?; - let review: RadrootsKnowledgeReview = json_content(&event.content)?; + ensure_kind(event.kind_u32(), KIND_KNOWLEDGE_REVIEW, "knowledge review")?; + require_contract_tag(&event.tags_as_vec(), RADROOTS_KNOWLEDGE_REVIEW_SCHEMA)?; + required_one_value(&event.tags_as_vec(), TAG_REVIEW_TARGET)?; + let review: RadrootsKnowledgeReview = json_content(event.content())?; validate_knowledge_review(&review).map_err(parse_validation_error)?; let expected_tags = knowledge_review_build_tags(&review) .map_err(|_| EventParseError::InvalidTag(TAG_REVIEW_TARGET))?; ensure_mirrored_tags( - &event.tags, + &event.tags_as_vec(), &expected_tags, &[TAG_REVIEW_TARGET, TAG_EVIDENCE], )?; @@ -602,17 +618,21 @@ pub fn knowledge_field_report_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsKnowledgeFieldReport>, EventParseError> { ensure_kind( - event.kind, + event.kind_u32(), KIND_KNOWLEDGE_FIELD_REPORT, "knowledge field report", )?; - require_contract_tag(&event.tags, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA)?; - reject_private_coordinate_keys(&event.content)?; - let report: RadrootsKnowledgeFieldReport = json_content(&event.content)?; + require_contract_tag(&event.tags_as_vec(), RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA)?; + reject_private_coordinate_keys(event.content())?; + let report: RadrootsKnowledgeFieldReport = json_content(event.content())?; validate_knowledge_field_report(&report).map_err(parse_validation_error)?; let expected_tags = knowledge_field_report_build_tags(&report) .map_err(|_| EventParseError::InvalidTag(TAG_EVIDENCE))?; - ensure_mirrored_tags(&event.tags, &expected_tags, &[TAG_T, TAG_G, TAG_EVIDENCE])?; + ensure_mirrored_tags( + &event.tags_as_vec(), + &expected_tags, + &[TAG_T, TAG_G, TAG_EVIDENCE], + )?; Ok(parsed(event, report)) } @@ -620,16 +640,19 @@ pub fn knowledge_change_proposal_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsKnowledgeChangeProposal>, EventParseError> { ensure_kind( - event.kind, + event.kind_u32(), KIND_KNOWLEDGE_CHANGE_PROPOSAL, "knowledge change proposal", )?; - require_contract_tag(&event.tags, RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA)?; - let proposal: RadrootsKnowledgeChangeProposal = json_content(&event.content)?; + require_contract_tag( + &event.tags_as_vec(), + RADROOTS_KNOWLEDGE_CHANGE_PROPOSAL_SCHEMA, + )?; + let proposal: RadrootsKnowledgeChangeProposal = json_content(event.content())?; validate_knowledge_change_proposal(&proposal).map_err(parse_validation_error)?; let expected_tags = knowledge_change_proposal_build_tags(&proposal) .map_err(|_| EventParseError::InvalidTag(TAG_EVIDENCE))?; - ensure_mirrored_tags(&event.tags, &expected_tags, &[TAG_EVIDENCE])?; + ensure_mirrored_tags(&event.tags_as_vec(), &expected_tags, &[TAG_EVIDENCE])?; Ok(parsed(event, proposal)) } @@ -637,15 +660,18 @@ pub fn contribution_attestation_from_event( event: RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsContributionAttestation>, EventParseError> { ensure_kind( - event.kind, + event.kind_u32(), KIND_CONTRIBUTION_ATTESTATION, "contribution attestation", )?; - require_contract_tag(&event.tags, RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA)?; - let attestation: RadrootsContributionAttestation = json_content(&event.content)?; + require_contract_tag( + &event.tags_as_vec(), + RADROOTS_CONTRIBUTION_ATTESTATION_SCHEMA, + )?; + let attestation: RadrootsContributionAttestation = json_content(event.content())?; validate_contribution_attestation(&attestation).map_err(parse_validation_error)?; let expected_tags = contribution_attestation_build_tags(&attestation) .map_err(|_| EventParseError::InvalidTag(TAG_EVIDENCE))?; - ensure_mirrored_tags(&event.tags, &expected_tags, &[TAG_EVIDENCE])?; + ensure_mirrored_tags(&event.tags_as_vec(), &expected_tags, &[TAG_EVIDENCE])?; Ok(parsed(event, attestation)) } diff --git a/crates/event_codec/src/list/decode.rs b/crates/event_codec/src/list/decode.rs @@ -2,7 +2,6 @@ use alloc::{string::String, vec::Vec}; use radroots_event::{ - RadrootsEventEnvelope, kinds::{KIND_LIST_READ_WRITE_RELAYS, is_nip51_list_set_kind, is_nip51_standard_list_kind}, list::{RadrootsList, RadrootsListEntry}, tags::TAG_R, @@ -93,7 +92,7 @@ fn is_ws_relay_url(value: &str) -> bool { pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -111,7 +110,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -125,18 +124,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } #[cfg(feature = "serde_json")] diff --git a/crates/event_codec/src/list_set/decode.rs b/crates/event_codec/src/list_set/decode.rs @@ -2,8 +2,7 @@ use alloc::{string::String, vec::Vec}; use radroots_event::{ - RadrootsEventEnvelope, kinds::is_nip51_list_set_kind, list::RadrootsListEntry, - list_set::RadrootsListSet, + kinds::is_nip51_list_set_kind, list::RadrootsListEntry, list_set::RadrootsListSet, }; use crate::error::EventParseError; @@ -102,7 +101,7 @@ pub fn list_set_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -120,7 +119,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -134,18 +133,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } #[cfg(feature = "serde_json")] diff --git a/crates/event_codec/src/listing/decode.rs b/crates/event_codec/src/listing/decode.rs @@ -528,7 +528,7 @@ pub fn listing_from_event_parts( let location = location .map(|location| { let geohash = geohash.ok_or(EventParseError::InvalidTag(TAG_GEOHASH))?; - Ok(RadrootsListingPublicLocation { + Ok::<RadrootsListingPublicLocation, EventParseError>(RadrootsListingPublicLocation { primary: location.primary, city: location.city, region: location.region, @@ -582,7 +582,7 @@ pub fn listing_from_event_parts( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -600,7 +600,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -614,7 +614,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent::from_parts( + RadrootsParsedEvent::from_parts( id, author, published_at, @@ -623,19 +623,19 @@ pub fn parsed_from_event( tags, sig, data.data, - )) + ) } pub fn data_from_nostr_event( event: &RadrootsEventEnvelope, ) -> Result<RadrootsParsedData<RadrootsListing>, EventParseError> { data_from_event( - event.id.clone(), - event.author.clone(), - event.created_at, - event.kind, - event.content.clone(), - event.tags.clone(), + event.id_str().to_string(), + event.author_str().to_string(), + event.created_at_u64(), + event.kind_u32(), + event.content().to_string(), + event.tags_as_vec(), ) } @@ -643,13 +643,13 @@ pub fn parsed_from_nostr_event( event: &RadrootsEventEnvelope, ) -> Result<RadrootsParsedEvent<RadrootsListing>, EventParseError> { parsed_from_event( - event.id.clone(), - event.author.clone(), - event.created_at, - event.kind, - event.content.clone(), - event.tags.clone(), - event.sig.clone(), + event.id_str().to_string(), + event.author_str().to_string(), + event.created_at_u64(), + event.kind_u32(), + event.content().to_string(), + event.tags_as_vec(), + event.sig_str().to_string(), ) } diff --git a/crates/event_codec/src/message/decode.rs b/crates/event_codec/src/message/decode.rs @@ -4,7 +4,7 @@ use alloc::{ vec::Vec, }; -use radroots_event::{RadrootsEventEnvelope, kinds::KIND_MESSAGE, message::RadrootsMessage}; +use radroots_event::{kinds::KIND_MESSAGE, message::RadrootsMessage}; use crate::error::EventParseError; use crate::message::tags::{parse_recipients, parse_reply_tag, parse_subject_tag}; @@ -44,7 +44,7 @@ pub fn message_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -62,7 +62,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -76,16 +76,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/message_file/decode.rs b/crates/event_codec/src/message_file/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, kinds::KIND_MESSAGE_FILE, message_file::{RadrootsMessageFile, RadrootsMessageFileDimensions}, }; @@ -156,7 +155,7 @@ pub fn message_file_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -174,7 +173,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -188,16 +187,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/order/decode.rs b/crates/event_codec/src/order/decode.rs @@ -103,34 +103,36 @@ pub struct RadrootsOrderEventContext { pub fn order_envelope_from_event<T: DeserializeOwned>( event: &RadrootsEventEnvelope, ) -> Result<RadrootsOrderEnvelope<T>, RadrootsOrderEnvelopeParseError> { - if !is_order_event_kind(event.kind) { - return Err(RadrootsOrderEnvelopeParseError::InvalidKind(event.kind)); + let event_kind = event.kind_u32(); + let event_tags = event.tags_as_vec(); + if !is_order_event_kind(event_kind) { + return Err(RadrootsOrderEnvelopeParseError::InvalidKind(event_kind)); } - let envelope = serde_json::from_str::<RadrootsOrderEnvelope<T>>(&event.content) + let envelope = serde_json::from_str::<RadrootsOrderEnvelope<T>>(event.content()) .map_err(|_| RadrootsOrderEnvelopeParseError::InvalidJson)?; envelope .validate() .map_err(RadrootsOrderEnvelopeParseError::InvalidEnvelope)?; - if envelope.message_type.kind() != event.kind { + if envelope.message_type.kind() != event_kind { return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, + event_kind, message_type: envelope.message_type, }); } - let listing_addr = required_order_tag_value(&event.tags, "a")?; + let listing_addr = required_order_tag_value(&event_tags, "a")?; if envelope.listing_addr != listing_addr { return Err(RadrootsOrderEnvelopeParseError::ListingAddrTagMismatch); } RadrootsListingAddress::parse(&envelope.listing_addr) .map_err(RadrootsOrderEnvelopeParseError::InvalidListingAddr)?; - let tag_order_id = required_order_tag_value(&event.tags, TAG_D)?; + let tag_order_id = required_order_tag_value(&event_tags, TAG_D)?; if tag_order_id != envelope.order_id { return Err(RadrootsOrderEnvelopeParseError::OrderIdTagMismatch); } - order_event_context_from_tags(envelope.message_type, &event.tags)?; + order_event_context_from_tags(envelope.message_type, &event_tags)?; Ok(envelope) } @@ -141,7 +143,7 @@ pub fn order_request_from_event( let envelope = order_envelope_from_event::<RadrootsOrderRequest>(event)?; if envelope.message_type != RadrootsOrderEventType::OrderRequested { return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, + event_kind: event.kind_u32(), message_type: envelope.message_type, }); } @@ -167,7 +169,7 @@ pub fn order_decision_from_event( let envelope = order_envelope_from_event::<RadrootsOrderDecision>(event)?; if envelope.message_type != RadrootsOrderEventType::OrderDecision { return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, + event_kind: event.kind_u32(), message_type: envelope.message_type, }); } @@ -193,7 +195,7 @@ pub fn order_revision_proposal_from_event( let envelope = order_envelope_from_event::<RadrootsOrderRevisionProposal>(event)?; if envelope.message_type != RadrootsOrderEventType::OrderRevisionProposed { return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, + event_kind: event.kind_u32(), message_type: envelope.message_type, }); } @@ -209,7 +211,7 @@ pub fn order_revision_proposal_from_event( &envelope.payload.seller_pubkey, &envelope.payload.buyer_pubkey, )?; - let context = order_event_context_from_tags(envelope.message_type, &event.tags)?; + let context = order_event_context_from_tags(envelope.message_type, &event.tags_as_vec())?; if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( "root_event_id", @@ -230,7 +232,7 @@ pub fn order_revision_decision_from_event( let envelope = order_envelope_from_event::<RadrootsOrderRevisionDecision>(event)?; if envelope.message_type != RadrootsOrderEventType::OrderRevisionDecision { return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, + event_kind: event.kind_u32(), message_type: envelope.message_type, }); } @@ -246,7 +248,7 @@ pub fn order_revision_decision_from_event( &envelope.payload.buyer_pubkey, &envelope.payload.seller_pubkey, )?; - let context = order_event_context_from_tags(envelope.message_type, &event.tags)?; + let context = order_event_context_from_tags(envelope.message_type, &event.tags_as_vec())?; if context.root_event_id.as_deref() != Some(envelope.payload.root_event_id.as_str()) { return Err(RadrootsOrderEnvelopeParseError::PayloadBindingMismatch( "root_event_id", @@ -267,7 +269,7 @@ pub fn order_cancellation_from_event( let envelope = order_envelope_from_event::<RadrootsOrderCancellation>(event)?; if envelope.message_type != RadrootsOrderEventType::OrderCancelled { return Err(RadrootsOrderEnvelopeParseError::MessageTypeKindMismatch { - event_kind: event.kind, + event_kind: event.kind_u32(), message_type: envelope.message_type, }); } @@ -374,6 +376,9 @@ fn map_tag_parse_error_for_order_envelope( | crate::error::EventParseError::InvalidJson(tag) => { RadrootsOrderEnvelopeParseError::InvalidTag(tag) } + crate::error::EventParseError::InvalidEnvelope => { + RadrootsOrderEnvelopeParseError::InvalidTag("event_envelope") + } } } @@ -396,10 +401,10 @@ fn validate_order_binding<T>( "listing_addr", )); } - if event.author != expected_author { + if event.author_str() != expected_author { return Err(RadrootsOrderEnvelopeParseError::AuthorMismatch); } - let context = order_event_context_from_tags(envelope.message_type, &event.tags)?; + let context = order_event_context_from_tags(envelope.message_type, &event.tags_as_vec())?; if context.counterparty_pubkey.as_str() != expected_counterparty { return Err(RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch); } @@ -426,7 +431,7 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; use radroots_event::{ - RadrootsEventEnvelope, RadrootsEventPtr, + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr, ids::{ RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey, @@ -506,6 +511,28 @@ mod tests { event_id(character).into_string() } + fn event_signature_wire() -> String { + core::iter::repeat_n('f', 128).collect() + } + + fn event_envelope( + author: String, + kind: u32, + tags: Vec<Vec<String>>, + content: String, + ) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: event_id_wire('e'), + author, + created_at: 1, + kind, + tags, + content, + sig: event_signature_wire(), + }) + .unwrap() + } + fn order_request() -> RadrootsOrderRequest { RadrootsOrderRequest { order_id: order_id("order-1"), @@ -654,15 +681,12 @@ mod tests { tags: Vec<Vec<String>>, ) -> RadrootsEventEnvelope { let envelope = RadrootsOrderEnvelope::new(message_type, listing_addr, order_id, payload); - RadrootsEventEnvelope { - id: event_id_wire('e'), + event_envelope( author, - created_at: 1, kind, tags, - content: serde_json::to_string(&envelope).unwrap(), - sig: "sig".into(), - } + serde_json::to_string(&envelope).unwrap(), + ) } #[test] @@ -891,15 +915,7 @@ mod tests { fn order_request_parse_roundtrips_and_validates_tags() { let payload = order_request(); let built = order_request_event_build(&listing_event_ptr(), &payload).unwrap(); - let event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; + let event = event_envelope(buyer_pubkey_wire(), built.kind, built.tags, built.content); let envelope = order_request_from_event(&event).unwrap(); assert_eq!(envelope.payload, payload); @@ -920,15 +936,12 @@ mod tests { payload.order_id.clone(), payload, ); - let event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: serde_json::to_string(&envelope).unwrap(), - sig: "sig".into(), - }; + let event = event_envelope( + buyer_pubkey_wire(), + built.kind, + built.tags, + serde_json::to_string(&envelope).unwrap(), + ); let err = order_request_from_event(&event).unwrap_err(); assert_eq!( err, @@ -946,15 +959,7 @@ mod tests { let root_event_id = event_id('1'); let prev_event_id = event_id('9'); let built = order_decision_event_build(&root_event_id, &prev_event_id, &payload).unwrap(); - let event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; + let event = event_envelope(seller_pubkey_wire(), built.kind, built.tags, built.content); let envelope = order_decision_from_event(&event).unwrap(); assert_eq!(envelope.payload, payload); @@ -968,15 +973,7 @@ mod tests { let prev_event_id = event_id('9'); let built = order_cancellation_event_build(&root_event_id, &prev_event_id, &payload).unwrap(); - let event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; + let event = event_envelope(buyer_pubkey_wire(), built.kind, built.tags, built.content); let envelope = order_cancellation_from_event(&event).unwrap(); assert_eq!(envelope.payload, payload); @@ -995,20 +992,18 @@ mod tests { &payload, ) .unwrap(); - let mut event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; + let event = event_envelope( + seller_pubkey_wire(), + built.kind, + built.tags.clone(), + built.content.clone(), + ); let envelope = order_revision_proposal_from_event(&event).unwrap(); assert_eq!(envelope.payload, payload); - event.author = buyer_pubkey_wire(); - let err = order_revision_proposal_from_event(&event).unwrap_err(); + let wrong_author_event = + event_envelope(buyer_pubkey_wire(), built.kind, built.tags, built.content); + let err = order_revision_proposal_from_event(&wrong_author_event).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); } @@ -1023,20 +1018,18 @@ mod tests { &payload, ) .unwrap(); - let mut event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; + let event = event_envelope( + buyer_pubkey_wire(), + built.kind, + built.tags.clone(), + built.content.clone(), + ); let envelope = order_revision_decision_from_event(&event).unwrap(); assert_eq!(envelope.payload, payload); - event.author = seller_pubkey_wire(); - let err = order_revision_decision_from_event(&event).unwrap_err(); + let wrong_author_event = + event_envelope(seller_pubkey_wire(), built.kind, built.tags, built.content); + let err = order_revision_decision_from_event(&wrong_author_event).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); } @@ -1089,15 +1082,12 @@ mod tests { #[test] fn order_envelope_parse_rejects_content_tag_and_envelope_mismatches() { let payload = serde_json::json!({}); - let invalid_json = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: KIND_ORDER_REQUEST, - tags: Vec::new(), - content: "{".into(), - sig: "sig".into(), - }; + let invalid_json = event_envelope( + buyer_pubkey_wire(), + KIND_ORDER_REQUEST, + Vec::new(), + "{".into(), + ); assert_eq!( order_envelope_from_event::<serde_json::Value>(&invalid_json).unwrap_err(), RadrootsOrderEnvelopeParseError::InvalidJson @@ -1110,15 +1100,12 @@ mod tests { &payload, ); invalid_version_envelope.version = 99; - let invalid_version = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: KIND_ORDER_REQUEST, - tags: order_request_tags(), - content: serde_json::to_string(&invalid_version_envelope).unwrap(), - sig: "sig".into(), - }; + let invalid_version = event_envelope( + buyer_pubkey_wire(), + KIND_ORDER_REQUEST, + order_request_tags(), + serde_json::to_string(&invalid_version_envelope).unwrap(), + ); assert!(matches!( order_envelope_from_event::<serde_json::Value>(&invalid_version).unwrap_err(), RadrootsOrderEnvelopeParseError::InvalidEnvelope( @@ -1303,21 +1290,18 @@ mod tests { request_payload.order_id = order_id("other-order"); let request_built = order_request_event_build(&listing_event_ptr(), &order_request()).unwrap(); - let mut request_event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: request_built.kind, - tags: request_built.tags.clone(), - content: serde_json::to_string(&RadrootsOrderEnvelope::new( + let request_event = event_envelope( + buyer_pubkey_wire(), + request_built.kind, + request_built.tags.clone(), + serde_json::to_string(&RadrootsOrderEnvelope::new( RadrootsOrderEventType::OrderRequested, listing_addr_wire(), "order-1", &request_payload, )) .unwrap(), - sig: "sig".into(), - }; + ); assert_eq!( order_request_from_event(&request_event).unwrap_err(), RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("order_id") @@ -1328,13 +1312,18 @@ mod tests { format!("30402:{}:BBBBBBBBBBBBBBBBBBBBBA", seller_pubkey_wire()) .parse() .unwrap(); - request_event.content = serde_json::to_string(&RadrootsOrderEnvelope::new( - RadrootsOrderEventType::OrderRequested, - listing_addr_wire(), - "order-1", - &request_payload, - )) - .unwrap(); + let request_event = event_envelope( + buyer_pubkey_wire(), + request_built.kind, + request_built.tags, + serde_json::to_string(&RadrootsOrderEnvelope::new( + RadrootsOrderEventType::OrderRequested, + listing_addr_wire(), + "order-1", + &request_payload, + )) + .unwrap(), + ); assert_eq!( order_request_from_event(&request_event).unwrap_err(), RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("listing_addr") @@ -1347,31 +1336,33 @@ mod tests { &proposal_payload, ) .unwrap(); - let mut proposal_event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: proposal_built.kind, - tags: proposal_built.tags.clone(), - content: proposal_built.content.clone(), - sig: "sig".into(), - }; - proposal_event - .tags + let mut proposal_tags = proposal_built.tags.clone(); + proposal_tags .iter_mut() .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) .unwrap()[1] = event_id_wire('4'); + let proposal_event = event_envelope( + seller_pubkey_wire(), + proposal_built.kind, + proposal_tags, + proposal_built.content.clone(), + ); assert_eq!( order_revision_proposal_from_event(&proposal_event).unwrap_err(), RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("root_event_id") ); - proposal_event.tags = proposal_built.tags; - proposal_event - .tags + let mut proposal_tags = proposal_built.tags; + proposal_tags .iter_mut() .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_PREV)) .unwrap()[1] = event_id_wire('5'); + let proposal_event = event_envelope( + seller_pubkey_wire(), + proposal_built.kind, + proposal_tags, + proposal_built.content, + ); assert_eq!( order_revision_proposal_from_event(&proposal_event).unwrap_err(), RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("prev_event_id") @@ -1385,31 +1376,33 @@ mod tests { &revision_decision_payload, ) .unwrap(); - let mut revision_decision_event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: buyer_pubkey_wire(), - created_at: 1, - kind: revision_decision_built.kind, - tags: revision_decision_built.tags.clone(), - content: revision_decision_built.content, - sig: "sig".into(), - }; - revision_decision_event - .tags + let mut revision_decision_tags = revision_decision_built.tags.clone(); + revision_decision_tags .iter_mut() .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) .unwrap()[1] = event_id_wire('6'); + let revision_decision_event = event_envelope( + buyer_pubkey_wire(), + revision_decision_built.kind, + revision_decision_tags, + revision_decision_built.content.clone(), + ); assert_eq!( order_revision_decision_from_event(&revision_decision_event).unwrap_err(), RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("root_event_id") ); - revision_decision_event.tags = revision_decision_built.tags; - revision_decision_event - .tags + let mut revision_decision_tags = revision_decision_built.tags; + revision_decision_tags .iter_mut() .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_PREV)) .unwrap()[1] = event_id_wire('7'); + let revision_decision_event = event_envelope( + buyer_pubkey_wire(), + revision_decision_built.kind, + revision_decision_tags, + revision_decision_built.content, + ); assert_eq!( order_revision_decision_from_event(&revision_decision_event).unwrap_err(), RadrootsOrderEnvelopeParseError::PayloadBindingMismatch("prev_event_id") @@ -1501,21 +1494,18 @@ mod tests { let payload = serde_json::json!({}); let envelope = RadrootsOrderEnvelope::new(message_type, listing_addr_wire(), "order-1", &payload); - let event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, + let event = event_envelope( + seller_pubkey_wire(), kind, - tags: vec![ + vec![ vec!["p".into(), buyer_pubkey_wire()], vec!["a".into(), listing_addr_wire()], vec![TAG_D.into(), "order-1".into()], vec![TAG_E_ROOT.into(), event_id_wire('1')], vec![TAG_E_PREV.into(), event_id_wire('9')], ], - content: serde_json::to_string(&envelope).unwrap(), - sig: "sig".into(), - }; + serde_json::to_string(&envelope).unwrap(), + ); let parsed = order_envelope_from_event::<serde_json::Value>(&event).unwrap(); assert_eq!(parsed.message_type, message_type); @@ -1525,15 +1515,7 @@ mod tests { #[test] fn order_parse_rejects_forbidden_kind() { - let event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: 3431, - tags: Vec::new(), - content: "{}".into(), - sig: "sig".into(), - }; + let event = event_envelope(seller_pubkey_wire(), 3431, Vec::new(), "{}".into()); let err = order_envelope_from_event::<serde_json::Value>(&event).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::InvalidKind(3431)); } @@ -1544,18 +1526,9 @@ mod tests { let root_event_id = event_id('1'); let prev_event_id = event_id('9'); let built = order_decision_event_build(&root_event_id, &prev_event_id, &payload).unwrap(); - let mut event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - event - .tags - .retain(|tag| tag.first().map(String::as_str) != Some(TAG_E_PREV)); + let mut tags = built.tags; + tags.retain(|tag| tag.first().map(String::as_str) != Some(TAG_E_PREV)); + let event = event_envelope(seller_pubkey_wire(), built.kind, tags, built.content); let err = order_decision_from_event(&event).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::MissingTag(TAG_E_PREV)); @@ -1565,21 +1538,20 @@ mod tests { fn order_parse_rejects_author_and_counterparty_mismatch() { let payload = order_request(); let built = order_request_event_build(&listing_event_ptr(), &payload).unwrap(); - let mut event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags.clone(), - content: built.content.clone(), - sig: "sig".into(), - }; + let event = event_envelope( + seller_pubkey_wire(), + built.kind, + built.tags.clone(), + built.content.clone(), + ); let err = order_request_from_event(&event).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); - event.author = buyer_pubkey_wire(); - event.tags[0] = vec!["p".into(), pubkey('c').into_string()]; - let err = order_request_from_event(&event).unwrap_err(); + let mut tags = built.tags; + tags[0] = vec!["p".into(), pubkey('c').into_string()]; + let counterparty_mismatch = + event_envelope(buyer_pubkey_wire(), built.kind, tags, built.content); + let err = order_request_from_event(&counterparty_mismatch).unwrap_err(); assert_eq!( err, RadrootsOrderEnvelopeParseError::CounterpartyTagMismatch @@ -1593,15 +1565,12 @@ mod tests { let prev_event_id = event_id('9'); let cancellation_parts = order_cancellation_event_build(&root_event_id, &prev_event_id, &cancellation).unwrap(); - let cancellation_event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: cancellation_parts.kind, - tags: cancellation_parts.tags, - content: cancellation_parts.content, - sig: "sig".into(), - }; + let cancellation_event = event_envelope( + seller_pubkey_wire(), + cancellation_parts.kind, + cancellation_parts.tags, + cancellation_parts.content, + ); let err = order_cancellation_from_event(&cancellation_event).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::AuthorMismatch); } @@ -1612,28 +1581,26 @@ mod tests { let root_event_id = event_id('1'); let prev_event_id = event_id('9'); let built = order_decision_event_build(&root_event_id, &prev_event_id, &payload).unwrap(); - let mut event = RadrootsEventEnvelope { - id: event_id_wire('e'), - author: seller_pubkey_wire(), - created_at: 1, - kind: built.kind, - tags: built.tags, - content: built.content, - sig: "sig".into(), - }; - - event.tags[0] = vec!["p".into(), "not-a-pubkey".into()]; - let err = order_decision_from_event(&event).unwrap_err(); + let mut tags = built.tags.clone(); + tags[0] = vec!["p".into(), "not-a-pubkey".into()]; + let invalid_counterparty = event_envelope( + seller_pubkey_wire(), + built.kind, + tags, + built.content.clone(), + ); + let err = order_decision_from_event(&invalid_counterparty).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::InvalidTag("p")); - event.tags[0] = vec!["p".into(), buyer_pubkey_wire()]; - let root_tag = event - .tags + let mut tags = built.tags; + tags[0] = vec!["p".into(), buyer_pubkey_wire()]; + let root_tag = tags .iter_mut() .find(|tag| tag.first().map(String::as_str) == Some(TAG_E_ROOT)) .unwrap(); root_tag[1] = "not-an-event-id".into(); - let err = order_decision_from_event(&event).unwrap_err(); + let invalid_root = event_envelope(seller_pubkey_wire(), built.kind, tags, built.content); + let err = order_decision_from_event(&invalid_root).unwrap_err(); assert_eq!(err, RadrootsOrderEnvelopeParseError::InvalidTag(TAG_E_ROOT)); } } diff --git a/crates/event_codec/src/parsed.rs b/crates/event_codec/src/parsed.rs @@ -1,21 +1,22 @@ #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; -use radroots_event::RadrootsEventEnvelope; +use crate::error::EventParseError; +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsParsedData<T> { pub id: String, pub author: String, - pub published_at: u32, + pub published_at: u64, pub kind: u32, pub data: T, } impl<T> RadrootsParsedData<T> { #[inline] - pub fn new(id: String, author: String, published_at: u32, kind: u32, data: T) -> Self { + pub fn new(id: String, author: String, published_at: u64, kind: u32, data: T) -> Self { Self { id, author, @@ -43,32 +44,62 @@ impl<T> RadrootsParsedEvent<T> { pub fn from_parts( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, sig: String, data: T, - ) -> Self { - Self { - event: RadrootsEventEnvelope { - id: id.clone(), - author: author.clone(), - created_at: published_at, - kind, - tags, - content, - sig, - }, - data: RadrootsParsedData::new(id, author, published_at, kind, data), - } + ) -> Result<Self, EventParseError> { + let parsed_data = + RadrootsParsedData::new(id.clone(), author.clone(), published_at, kind, data); + Self::from_event_parts( + id, + author, + published_at, + kind, + content, + tags, + sig, + parsed_data, + ) + } + + #[allow(clippy::too_many_arguments)] + pub fn from_event_parts( + id: String, + author: String, + published_at: u64, + kind: u32, + content: String, + tags: Vec<Vec<String>>, + sig: String, + data: RadrootsParsedData<T>, + ) -> Result<Self, EventParseError> { + let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id, + author, + created_at: published_at, + kind, + tags, + content, + sig, + })?; + Ok(Self { event, data }) } } #[cfg(test)] mod tests { use super::{RadrootsParsedData, RadrootsParsedEvent}; - use radroots_event::RadrootsEventEnvelope; + use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; + + const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const AUTHOR: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + const SIG: &str = concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ); #[test] fn parsed_data_constructor_maps_fields() { @@ -88,58 +119,60 @@ mod tests { #[test] fn parsed_event_constructor_maps_event_and_data() { - let event = RadrootsEventEnvelope { - id: "id".to_string(), - author: "author".to_string(), + let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: EVENT_ID.to_string(), + author: AUTHOR.to_string(), created_at: 22, kind: 1, tags: vec![vec!["k".to_string(), "v".to_string()]], content: "content".to_string(), - sig: "sig".to_string(), - }; + sig: SIG.to_string(), + }) + .unwrap(); let data = RadrootsParsedData::new( - "id".to_string(), - "author".to_string(), + EVENT_ID.to_string(), + AUTHOR.to_string(), 22, 1, "payload".to_string(), ); let out = RadrootsParsedEvent::new(event.clone(), data.clone()); - assert_eq!(out.event.id, event.id); - assert_eq!(out.event.author, event.author); - assert_eq!(out.event.created_at, event.created_at); - assert_eq!(out.event.kind, event.kind); - assert_eq!(out.event.tags, event.tags); - assert_eq!(out.event.content, event.content); - assert_eq!(out.event.sig, event.sig); + assert_eq!(out.event.id(), event.id()); + assert_eq!(out.event.author(), event.author()); + assert_eq!(out.event.created_at(), event.created_at()); + assert_eq!(out.event.kind(), event.kind()); + assert_eq!(out.event.tags(), event.tags()); + assert_eq!(out.event.content(), event.content()); + assert_eq!(out.event.sig(), event.sig()); assert_eq!(out.data, data); } #[test] fn parsed_event_from_parts_builds_consistent_structs() { let out = RadrootsParsedEvent::from_parts( - "id".to_string(), - "author".to_string(), + EVENT_ID.to_string(), + AUTHOR.to_string(), 77, 1111, "hello".to_string(), vec![vec!["e".to_string(), "root".to_string()]], - "sig".to_string(), + SIG.to_string(), "payload".to_string(), - ); - assert_eq!(out.event.id, "id"); - assert_eq!(out.event.author, "author"); - assert_eq!(out.event.created_at, 77); - assert_eq!(out.event.kind, 1111); - assert_eq!(out.event.content, "hello"); + ) + .unwrap(); + assert_eq!(out.event.id_str(), EVENT_ID); + assert_eq!(out.event.author_str(), AUTHOR); + assert_eq!(out.event.created_at_u64(), 77); + assert_eq!(out.event.kind_u32(), 1111); + assert_eq!(out.event.content(), "hello"); assert_eq!( - out.event.tags, + out.event.tags_as_vec(), vec![vec!["e".to_string(), "root".to_string()]] ); - assert_eq!(out.event.sig, "sig"); - assert_eq!(out.data.id, "id"); - assert_eq!(out.data.author, "author"); + assert_eq!(out.event.sig_str(), SIG); + assert_eq!(out.data.id, EVENT_ID); + assert_eq!(out.data.author, AUTHOR); assert_eq!(out.data.published_at, 77); assert_eq!(out.data.kind, 1111); assert_eq!(out.data.data, "payload"); diff --git a/crates/event_codec/src/plot/decode.rs b/crates/event_codec/src/plot/decode.rs @@ -7,7 +7,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, farm::RadrootsFarmRef, kinds::{KIND_FARM, KIND_PLOT}, plot::RadrootsPlot, @@ -126,7 +125,7 @@ pub fn plot_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -144,7 +143,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -158,16 +157,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/post/decode.rs b/crates/event_codec/src/post/decode.rs @@ -6,7 +6,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, farm::RadrootsFarmRef, kinds::{KIND_FARM, KIND_POST}, post::RadrootsPost, @@ -60,7 +59,7 @@ pub fn post_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -78,7 +77,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -92,18 +91,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn farm_anchor_from_tags( diff --git a/crates/event_codec/src/profile/decode.rs b/crates/event_codec/src/profile/decode.rs @@ -6,7 +6,6 @@ use alloc::{ use super::RadrootsProfileData; use radroots_event::{ - RadrootsEventEnvelope, kinds::KIND_PROFILE, profile::{ RADROOTS_PROFILE_TYPE_TAG_KEY, RadrootsProfile, RadrootsProfileType, @@ -70,7 +69,7 @@ pub fn profile_from_content(content: &str) -> Result<RadrootsProfile, EventParse pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -98,7 +97,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -112,16 +111,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/reaction/decode.rs b/crates/event_codec/src/reaction/decode.rs @@ -5,8 +5,8 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, kinds::KIND_REACTION, reaction::RadrootsReaction, - social::RadrootsSocialTarget, tags::TAG_E_ROOT, + kinds::KIND_REACTION, reaction::RadrootsReaction, social::RadrootsSocialTarget, + tags::TAG_E_ROOT, }; use crate::error::EventParseError; @@ -42,7 +42,7 @@ pub fn reaction_from_tags( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -60,7 +60,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -74,18 +74,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn parse_reaction_target(tags: &[Vec<String>]) -> Result<RadrootsSocialTarget, EventParseError> { diff --git a/crates/event_codec/src/relay_auth/decode.rs b/crates/event_codec/src/relay_auth/decode.rs @@ -2,7 +2,6 @@ use alloc::{string::String, vec::Vec}; use radroots_event::{ - RadrootsEventEnvelope, relay_auth::{KIND_RELAY_AUTH, RadrootsRelayAuth}, tags::{TAG_CHALLENGE, TAG_RELAY}, }; @@ -34,7 +33,7 @@ pub fn relay_auth_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -52,7 +51,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -66,16 +65,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/relay_auth/mod.rs b/crates/event_codec/src/relay_auth/mod.rs @@ -135,7 +135,7 @@ mod tests { "sig".to_string(), ) .expect("parsed event"); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data, auth); } diff --git a/crates/event_codec/src/report/decode.rs b/crates/event_codec/src/report/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, kinds::KIND_REPORT, report::RadrootsReport, social::{RadrootsReportFileTarget, RadrootsReportType, RadrootsSocialTarget}, @@ -56,7 +55,7 @@ pub fn report_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -74,7 +73,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -88,18 +87,7 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn parse_event_target( diff --git a/crates/event_codec/src/repost/decode.rs b/crates/event_codec/src/repost/decode.rs @@ -5,7 +5,6 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, kinds::{KIND_GENERIC_REPOST, KIND_POST, KIND_REPOST}, repost::{RadrootsGenericRepost, RadrootsRepost}, social::RadrootsSocialTarget, @@ -102,7 +101,7 @@ pub fn generic_repost_from_event( pub fn repost_data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -120,7 +119,7 @@ pub fn repost_data_from_event( pub fn generic_repost_data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -138,7 +137,7 @@ pub fn generic_repost_data_from_event( pub fn repost_parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -152,24 +151,13 @@ pub fn repost_parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } pub fn generic_repost_parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -183,18 +171,7 @@ pub fn generic_repost_parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } fn find_tag<'a>(tags: &'a [Vec<String>], key: &'static str) -> Option<&'a Vec<String>> { diff --git a/crates/event_codec/src/resource_area/decode.rs b/crates/event_codec/src/resource_area/decode.rs @@ -6,10 +6,7 @@ use alloc::{ vec::Vec, }; -use radroots_event::{ - RadrootsEventEnvelope, kinds::KIND_RESOURCE_AREA, resource_area::RadrootsResourceArea, - tags::TAG_D, -}; +use radroots_event::{kinds::KIND_RESOURCE_AREA, resource_area::RadrootsResourceArea, tags::TAG_D}; use crate::d_tag::validate_d_tag_tag; use crate::error::EventParseError; @@ -63,7 +60,7 @@ pub fn resource_area_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -81,7 +78,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -95,16 +92,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/resource_cap/decode.rs b/crates/event_codec/src/resource_cap/decode.rs @@ -7,8 +7,7 @@ use alloc::{ }; use radroots_event::{ - RadrootsEventEnvelope, kinds::KIND_RESOURCE_HARVEST_CAP, - resource_cap::RadrootsResourceHarvestCap, tags::TAG_D, + kinds::KIND_RESOURCE_HARVEST_CAP, resource_cap::RadrootsResourceHarvestCap, tags::TAG_D, }; use crate::d_tag::validate_d_tag_tag; @@ -63,7 +62,7 @@ pub fn resource_harvest_cap_from_event( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -75,7 +74,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -89,16 +88,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/seal/decode.rs b/crates/event_codec/src/seal/decode.rs @@ -4,7 +4,7 @@ use alloc::{ vec::Vec, }; -use radroots_event::{RadrootsEventEnvelope, kinds::KIND_SEAL, seal::RadrootsSeal}; +use radroots_event::{kinds::KIND_SEAL, seal::RadrootsSeal}; use crate::error::EventParseError; use crate::parsed::{RadrootsParsedData, RadrootsParsedEvent}; @@ -36,7 +36,7 @@ pub fn seal_from_parts( pub fn data_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -54,7 +54,7 @@ pub fn data_from_event( pub fn parsed_from_event( id: String, author: String, - published_at: u32, + published_at: u64, kind: u32, content: String, tags: Vec<Vec<String>>, @@ -68,16 +68,5 @@ pub fn parsed_from_event( content.clone(), tags.clone(), )?; - Ok(RadrootsParsedEvent { - event: RadrootsEventEnvelope { - id, - author, - created_at: published_at, - kind, - content, - tags, - sig, - }, - data, - }) + RadrootsParsedEvent::from_event_parts(id, author, published_at, kind, content, tags, sig, data) } diff --git a/crates/event_codec/src/verification.rs b/crates/event_codec/src/verification.rs @@ -10,7 +10,6 @@ use radroots_event::contract::{ RadrootsContractValidationError, RadrootsEventContract, validate_event_contract as validate_radroots_event_contract, }; -use radroots_event::draft::compute_nip01_event_id; use radroots_event::ids::RadrootsEventId; use radroots_event::knowledge::{ RadrootsContributionAttestation, RadrootsEvidenceBounty, RadrootsKnowledgeChangeProposal, @@ -18,6 +17,7 @@ use radroots_event::knowledge::{ RadrootsKnowledgeReview, RadrootsKnowledgeSource, RadrootsWikiArticle, RadrootsWikiMergeRequest, RadrootsWikiRedirect, }; +use radroots_event::wire::compute_canonical_nip01_event_id; use crate::error::EventParseError; use crate::knowledge::decode::{ @@ -225,21 +225,21 @@ impl RadrootsDecodedEvent { pub fn verify_event_id( event: RadrootsEventEnvelope, ) -> Result<RadrootsIdVerifiedEvent, RadrootsNip01VerificationError> { - RadrootsEventId::parse(event.id.as_str()) + RadrootsEventId::parse(event.id_str()) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; - let expected = compute_nip01_event_id( - event.author.as_str(), - event.created_at, - event.kind, - &event.tags, - event.content.as_str(), + let expected = compute_canonical_nip01_event_id( + event.author_str(), + event.created_at_u64(), + event.kind_u32(), + &event.tags_as_vec(), + event.content(), ) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)? .into_string(); - if event.id != expected { + if event.id_str() != expected { return Err(RadrootsNip01VerificationError::IdMismatch { expected, - actual: event.id, + actual: event.id_str().to_string(), }); } Ok(RadrootsIdVerifiedEvent { event }) @@ -354,28 +354,29 @@ pub fn verify_and_decode_radroots_event( fn raw_event_from_radroots( event: &RadrootsEventEnvelope, ) -> Result<nostr::Event, RadrootsNip01VerificationError> { - let id = nostr::EventId::from_hex(event.id.as_str()) + let id = nostr::EventId::from_hex(event.id_str()) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; - let public_key = nostr::PublicKey::from_hex(event.author.as_str()) + let public_key = nostr::PublicKey::from_hex(event.author_str()) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; - let kind_u16 = - u16::try_from(event.kind).map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; - let mut tags = Vec::with_capacity(event.tags.len()); - for tag in event.tags.iter().cloned() { + let kind_u16 = u16::try_from(event.kind_u32()) + .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; + let tags_vec = event.tags_as_vec(); + let mut tags = Vec::with_capacity(tags_vec.len()); + for tag in tags_vec { tags.push( nostr::Tag::parse(tag) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?, ); } - let sig = nostr::secp256k1::schnorr::Signature::from_str(event.sig.as_str()) + let sig = nostr::secp256k1::schnorr::Signature::from_str(event.sig_str()) .map_err(|_| RadrootsNip01VerificationError::MalformedEnvelope)?; Ok(nostr::Event::new( id, public_key, - nostr::Timestamp::from_secs(u64::from(event.created_at)), + nostr::Timestamp::from_secs(event.created_at_u64()), nostr::Kind::Custom(kind_u16), tags, - event.content.clone(), + event.content().to_string(), sig, )) } diff --git a/crates/event_codec/tests/app_data.rs b/crates/event_codec/tests/app_data.rs @@ -128,12 +128,12 @@ fn app_data_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.id, "id"); - assert_eq!(index.event.author, "author"); - assert_eq!(index.event.created_at, 42); - assert_eq!(index.event.kind, KIND_APP_DATA); - assert_eq!(index.event.content, "payload"); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.id_str(), "id"); + assert_eq!(index.event.author_str(), "author"); + assert_eq!(index.event.created_at_u64(), 42); + assert_eq!(index.event.kind_u32(), KIND_APP_DATA); + assert_eq!(index.event.content(), "payload"); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.d_tag, "radroots.app"); } diff --git a/crates/event_codec/tests/article.rs b/crates/event_codec/tests/article.rs @@ -214,6 +214,6 @@ fn article_wrappers_preserve_event_metadata() { ) .unwrap(); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data.d_tag, VALID_D_TAG); } diff --git a/crates/event_codec/tests/calendar.rs b/crates/event_codec/tests/calendar.rs @@ -744,7 +744,7 @@ fn calendar_wrappers_preserve_event_metadata() { "sig".to_string(), ) .unwrap(); - assert_eq!(date_parsed.event.sig, "sig"); + assert_eq!(date_parsed.event.sig_str(), "sig"); let time = sample_time_event(); let time_parts = time_to_wire_parts(&time).unwrap(); @@ -788,7 +788,7 @@ fn calendar_wrappers_preserve_event_metadata() { "sig".to_string(), ) .unwrap(); - assert_eq!(time_parsed.event.created_at, 8); + assert_eq!(time_parsed.event.created_at_u64(), 8); let calendar = sample_calendar_collection(); let calendar_parts = calendar_to_wire_parts(&calendar).unwrap(); @@ -832,7 +832,7 @@ fn calendar_wrappers_preserve_event_metadata() { "sig".to_string(), ) .unwrap(); - assert_eq!(calendar_parsed.event.sig, "sig"); + assert_eq!(calendar_parsed.event.sig_str(), "sig"); let rsvp = sample_rsvp(); let rsvp_parts = rsvp_to_wire_parts(&rsvp).unwrap(); @@ -876,5 +876,5 @@ fn calendar_wrappers_preserve_event_metadata() { "sig".to_string(), ) .unwrap(); - assert_eq!(rsvp_parsed.event.created_at, 10); + assert_eq!(rsvp_parsed.event.created_at_u64(), 10); } diff --git a/crates/event_codec/tests/comment.rs b/crates/event_codec/tests/comment.rs @@ -542,7 +542,7 @@ fn comment_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.created_at, 77); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.created_at_u64(), 77); + assert_eq!(index.event.sig_str(), "sig"); assert_address_target(&index.data.data.parent, PARENT_AUTHOR, KIND_ARTICLE, D_TAG); } diff --git a/crates/event_codec/tests/common/mod.rs b/crates/event_codec/tests/common/mod.rs @@ -1,6 +1,15 @@ #![allow(dead_code)] -use radroots_event::{RadrootsEventEnvelope, RadrootsEventPtr, RadrootsEventRef}; +use radroots_event::{ + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr, RadrootsEventRef, +}; + +pub const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +pub const AUTHOR: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +pub const EVENT_SIG: &str = concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +); pub fn event_ref(id: &str, author: &str, kind: u32) -> RadrootsEventRef { RadrootsEventRef { @@ -36,13 +45,14 @@ pub fn event_ptr(id: &str, relays: Option<&str>) -> RadrootsEventPtr { } pub fn nostr_event(kind: u32, content: &str, tags: Vec<Vec<String>>) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { - id: "id".to_string(), - author: "author".to_string(), + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: EVENT_ID.to_string(), + author: AUTHOR.to_string(), created_at: 123, kind, tags, content: content.to_string(), - sig: "sig".to_string(), - } + sig: EVENT_SIG.to_string(), + }) + .unwrap() } diff --git a/crates/event_codec/tests/coverage_edges.rs b/crates/event_codec/tests/coverage_edges.rs @@ -17,15 +17,19 @@ fn assert_invalid_kind<T>(result: Result<T, EventParseError>, expected: &'static } } -fn parsed_args() -> (String, String, u32, u32, String, Vec<Vec<String>>, String) { +fn parsed_args() -> (String, String, u64, u32, String, Vec<Vec<String>>, String) { ( - "event-id".to_string(), - "author".to_string(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), 1, KIND_POST, String::new(), Vec::new(), - "sig".to_string(), + concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + .to_string(), ) } diff --git a/crates/event_codec/tests/file_metadata.rs b/crates/event_codec/tests/file_metadata.rs @@ -380,7 +380,7 @@ fn file_metadata_wrappers_preserve_event_metadata() { ) .unwrap(); - assert_eq!(parsed.event.created_at, 90); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.created_at_u64(), 90); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data.sha256, VALID_HASH); } diff --git a/crates/event_codec/tests/follow.rs b/crates/event_codec/tests/follow.rs @@ -187,8 +187,8 @@ fn follow_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_FOLLOW); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_FOLLOW); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.list.len(), 1); } diff --git a/crates/event_codec/tests/geochat.rs b/crates/event_codec/tests/geochat.rs @@ -243,8 +243,8 @@ fn geochat_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_GEOCHAT); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_GEOCHAT); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.geohash, "dr5rsj7"); } diff --git a/crates/event_codec/tests/gift_wrap.rs b/crates/event_codec/tests/gift_wrap.rs @@ -162,8 +162,8 @@ fn gift_wrap_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_GIFT_WRAP); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_GIFT_WRAP); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.recipient.public_key, "pubkey"); } diff --git a/crates/event_codec/tests/job_traits.rs b/crates/event_codec/tests/job_traits.rs @@ -1,18 +1,38 @@ #[path = "../src/test_fixtures.rs"] mod test_fixtures; -use radroots_event::RadrootsEventEnvelope; use radroots_event::job::{JobFeedbackStatus, JobInputType, JobPaymentRequest}; use radroots_event::job_feedback::RadrootsJobFeedback; use radroots_event::job_request::{RadrootsJobInput, RadrootsJobParam, RadrootsJobRequest}; use radroots_event::job_result::RadrootsJobResult; use radroots_event::kinds::{KIND_JOB_FEEDBACK, KIND_JOB_REQUEST_MIN, KIND_JOB_RESULT_MIN}; +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_event_codec::job::feedback::encode::to_wire_parts as to_feedback_wire_parts; use radroots_event_codec::job::request::encode::to_wire_parts as to_request_wire_parts; use radroots_event_codec::job::result::encode::to_wire_parts as to_result_wire_parts; use radroots_event_codec::job::traits::{BorrowedEventAdapter, JobEventLike}; use test_fixtures::{FIXTURE_ALICE_PUBLIC_KEY_HEX, RELAY_PRIMARY_WSS}; +const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const AUTHOR: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const EVENT_SIG: &str = concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +); + +fn event_envelope(kind: u32, tags: Vec<Vec<String>>, content: &str) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: EVENT_ID.to_string(), + author: AUTHOR.to_string(), + created_at: 42, + kind, + tags, + content: content.to_string(), + sig: EVENT_SIG.to_string(), + }) + .unwrap() +} + fn sample_request() -> RadrootsJobRequest { RadrootsJobRequest { kind: (KIND_JOB_REQUEST_MIN + 1) as u16, @@ -40,23 +60,16 @@ fn borrowed_event_adapter_builds_request_metadata() { let req = sample_request(); let parts = to_request_wire_parts(&req, "payload").unwrap(); - let event = RadrootsEventEnvelope { - id: "id".to_string(), - author: "author".to_string(), - created_at: 42, - kind: parts.kind, - tags: parts.tags.clone(), - content: "payload".to_string(), - sig: "sig".to_string(), - }; + let event = event_envelope(parts.kind, parts.tags.clone(), "payload"); + let tags = event.tags_as_vec(); - let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig); + let adapter = BorrowedEventAdapter::new(&event, event.created_at_u64(), &tags, event.sig_str()); let metadata = adapter.to_job_request_metadata().unwrap(); - assert_eq!(metadata.id, event.id); - assert_eq!(metadata.author, event.author); - assert_eq!(metadata.published_at, event.created_at); - assert_eq!(metadata.kind, event.kind); + assert_eq!(metadata.id, event.id_str()); + assert_eq!(metadata.author, event.author_str()); + assert_eq!(metadata.published_at, event.created_at_u64()); + assert_eq!(metadata.kind, event.kind_u32()); assert_eq!(metadata.data, req); } @@ -107,96 +120,75 @@ fn sample_feedback() -> RadrootsJobFeedback { fn borrowed_event_adapter_builds_request_metadata_and_index() { let req = sample_request(); let parts = to_request_wire_parts(&req, "payload").unwrap(); - let event = RadrootsEventEnvelope { - id: "id".to_string(), - author: "author".to_string(), - created_at: 42, - kind: parts.kind, - tags: parts.tags, - content: "payload".to_string(), - sig: "sig".to_string(), - }; - - let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig); - assert_eq!(adapter.raw_id(), "id"); - assert_eq!(adapter.raw_author(), "author"); + let event = event_envelope(parts.kind, parts.tags, "payload"); + let tags = event.tags_as_vec(); + + let adapter = BorrowedEventAdapter::new(&event, event.created_at_u64(), &tags, event.sig_str()); + assert_eq!(adapter.raw_id(), EVENT_ID); + assert_eq!(adapter.raw_author(), AUTHOR); assert_eq!(adapter.raw_published_at(), 42); - assert_eq!(adapter.raw_kind(), event.kind); + assert_eq!(adapter.raw_kind(), event.kind_u32()); assert_eq!(adapter.raw_content(), "payload"); - assert_eq!(adapter.raw_tags().len(), event.tags.len()); - assert_eq!(adapter.raw_sig(), "sig"); + assert_eq!(adapter.raw_tags().len(), tags.len()); + assert_eq!(adapter.raw_sig(), EVENT_SIG); let index = adapter.to_job_request_event_index().unwrap(); - assert_eq!(index.event.id, event.id); - assert_eq!(index.event.author, event.author); - assert_eq!(index.event.created_at, event.created_at); - assert_eq!(index.event.kind, event.kind); - assert_eq!(index.event.content, event.content); - assert_eq!(index.event.sig, event.sig); + assert_eq!(index.event.id_str(), event.id_str()); + assert_eq!(index.event.author_str(), event.author_str()); + assert_eq!(index.event.created_at_u64(), event.created_at_u64()); + assert_eq!(index.event.kind_u32(), event.kind_u32()); + assert_eq!(index.event.content(), event.content()); + assert_eq!(index.event.sig_str(), event.sig_str()); } #[test] fn borrowed_event_adapter_builds_result_metadata_and_index() { let result = sample_result(); let parts = to_result_wire_parts(&result, "payload").unwrap(); - let event = RadrootsEventEnvelope { - id: "id".to_string(), - author: "author".to_string(), - created_at: 42, - kind: parts.kind, - tags: parts.tags, - content: "payload".to_string(), - sig: "sig".to_string(), - }; + let event = event_envelope(parts.kind, parts.tags, "payload"); + let tags = event.tags_as_vec(); - let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig); + let adapter = BorrowedEventAdapter::new(&event, event.created_at_u64(), &tags, event.sig_str()); let metadata = adapter.to_job_result_metadata().unwrap(); - assert_eq!(metadata.id, event.id); - assert_eq!(metadata.author, event.author); - assert_eq!(metadata.published_at, event.created_at); - assert_eq!(metadata.kind, event.kind); + assert_eq!(metadata.id, event.id_str()); + assert_eq!(metadata.author, event.author_str()); + assert_eq!(metadata.published_at, event.created_at_u64()); + assert_eq!(metadata.kind, event.kind_u32()); assert_eq!(metadata.data.kind, result.kind); assert_eq!(metadata.data.request_event.id, "req"); assert_eq!(metadata.data.content.as_deref(), Some("payload")); let index = adapter.to_job_result_event_index().unwrap(); - assert_eq!(index.event.id, event.id); - assert_eq!(index.event.author, event.author); - assert_eq!(index.event.created_at, event.created_at); - assert_eq!(index.event.kind, event.kind); - assert_eq!(index.event.content, event.content); - assert_eq!(index.event.sig, event.sig); + assert_eq!(index.event.id_str(), event.id_str()); + assert_eq!(index.event.author_str(), event.author_str()); + assert_eq!(index.event.created_at_u64(), event.created_at_u64()); + assert_eq!(index.event.kind_u32(), event.kind_u32()); + assert_eq!(index.event.content(), event.content()); + assert_eq!(index.event.sig_str(), event.sig_str()); } #[test] fn borrowed_event_adapter_builds_feedback_metadata_and_index() { let feedback = sample_feedback(); let parts = to_feedback_wire_parts(&feedback, "payload").unwrap(); - let event = RadrootsEventEnvelope { - id: "id".to_string(), - author: "author".to_string(), - created_at: 42, - kind: parts.kind, - tags: parts.tags, - content: "payload".to_string(), - sig: "sig".to_string(), - }; + let event = event_envelope(parts.kind, parts.tags, "payload"); + let tags = event.tags_as_vec(); - let adapter = BorrowedEventAdapter::new(&event, event.created_at, &event.tags, &event.sig); + let adapter = BorrowedEventAdapter::new(&event, event.created_at_u64(), &tags, event.sig_str()); let metadata = adapter.to_job_feedback_metadata().unwrap(); - assert_eq!(metadata.id, event.id); - assert_eq!(metadata.author, event.author); - assert_eq!(metadata.published_at, event.created_at); - assert_eq!(metadata.kind, event.kind); + assert_eq!(metadata.id, event.id_str()); + assert_eq!(metadata.author, event.author_str()); + assert_eq!(metadata.published_at, event.created_at_u64()); + assert_eq!(metadata.kind, event.kind_u32()); assert_eq!(metadata.data.kind, feedback.kind); assert_eq!(metadata.data.request_event.id, "req"); assert_eq!(metadata.data.content.as_deref(), Some("payload")); let index = adapter.to_job_feedback_event_index().unwrap(); - assert_eq!(index.event.id, event.id); - assert_eq!(index.event.author, event.author); - assert_eq!(index.event.created_at, event.created_at); - assert_eq!(index.event.kind, event.kind); - assert_eq!(index.event.content, event.content); - assert_eq!(index.event.sig, event.sig); + assert_eq!(index.event.id_str(), event.id_str()); + assert_eq!(index.event.author_str(), event.author_str()); + assert_eq!(index.event.created_at_u64(), event.created_at_u64()); + assert_eq!(index.event.kind_u32(), event.kind_u32()); + assert_eq!(index.event.content(), event.content()); + assert_eq!(index.event.sig_str(), event.sig_str()); } diff --git a/crates/event_codec/tests/knowledge.rs b/crates/event_codec/tests/knowledge.rs @@ -1,6 +1,5 @@ #![cfg(all(feature = "knowledge", feature = "nostr"))] -use radroots_event::RadrootsEventEnvelope; use radroots_event::contract::validate_event_contract_shape; use radroots_event::kinds::{ KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_REVIEW, KIND_KNOWLEDGE_SOURCE, KIND_WIKI_ARTICLE, @@ -19,6 +18,7 @@ use radroots_event::knowledge::{ RadrootsKnowledgeReviewTarget, RadrootsKnowledgeSource, RadrootsWikiArticle, RadrootsWikiArticleVersionRef, RadrootsWikiMergeRequest, RadrootsWikiRedirect, }; +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_event_codec::error::{EventEncodeError, EventParseError}; use radroots_event_codec::knowledge::{ contribution_attestation_from_event, contribution_attestation_to_wire_parts, @@ -74,7 +74,7 @@ fn article_version_ref_for(event_id_character: char, d_tag: &str) -> RadrootsWik } fn event_from_parts(parts: WireEventParts) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: hex_64('0'), author: hex_64('a'), created_at: 1_800_000_000, @@ -82,17 +82,46 @@ fn event_from_parts(parts: WireEventParts) -> RadrootsEventEnvelope { tags: parts.tags, content: parts.content, sig: "1".repeat(128), - } + }) + .unwrap() +} + +fn event_with_parts( + event: &RadrootsEventEnvelope, + tags: Vec<Vec<String>>, + content: String, +) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: event.id_str().to_string(), + author: event.author_str().to_string(), + created_at: event.created_at_u64(), + kind: event.kind_u32(), + tags, + content, + sig: event.sig_str().to_string(), + }) + .unwrap() +} + +fn mutate_tags(event: &mut RadrootsEventEnvelope, update: impl FnOnce(&mut Vec<Vec<String>>)) { + let mut tags = event.tags_as_vec(); + update(&mut tags); + *event = event_with_parts(event, tags, event.content().to_string()); +} + +fn replace_content(event: &mut RadrootsEventEnvelope, content: String) { + *event = event_with_parts(event, event.tags_as_vec(), content); } fn replace_first_tag_value(event: &mut RadrootsEventEnvelope, name: &str, value: String) { - let tag = event - .tags - .iter_mut() - .find(|tag| tag.first().map(String::as_str) == Some(name)) - .expect("tag"); - let tag_value = tag.get_mut(1).expect("tag value"); - *tag_value = value; + mutate_tags(event, |tags| { + let tag = tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some(name)) + .expect("tag"); + let tag_value = tag.get_mut(1).expect("tag value"); + *tag_value = value; + }); } fn marked_tag(tag: &[String], name: &str, marker: &str) -> bool { @@ -101,7 +130,7 @@ fn marked_tag(tag: &[String], name: &str, marker: &str) -> bool { fn marked_tag_index(event: &RadrootsEventEnvelope, name: &str, marker: &str) -> usize { event - .tags + .tags_as_vec() .iter() .position(|tag| marked_tag(tag, name, marker)) .expect("marked tag") @@ -160,10 +189,10 @@ fn sign_parts(parts: WireEventParts) -> RadrootsEventEnvelope { .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000)) .sign_with_keys(&keys) .expect("signed event"); - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: event.id.to_hex(), author: event.pubkey.to_hex(), - created_at: event.created_at.as_secs() as u32, + created_at: event.created_at.as_secs(), kind: u32::from(event.kind.as_u16()), tags: event .tags @@ -173,7 +202,8 @@ fn sign_parts(parts: WireEventParts) -> RadrootsEventEnvelope { .collect(), content: event.content, sig: event.sig.to_string(), - } + }) + .unwrap() } fn wiki_article() -> RadrootsWikiArticle { @@ -355,28 +385,28 @@ fn attestation() -> RadrootsContributionAttestation { fn knowledge_codecs_roundtrip_all_contracts() { let article_event = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); validate_event_contract_shape(&article_event, "radroots.wiki.article.v1").unwrap(); - assert!(article_event.tags.iter().any(|tag| tag + assert!(article_event.tags_as_vec().iter().any(|tag| tag == &vec![ "a".to_string(), format!("30818:{}:soil-health", hex_64('a')), "wss://relay.radroots.example".to_string(), "fork".to_string() ])); - assert!(article_event.tags.iter().any(|tag| tag + assert!(article_event.tags_as_vec().iter().any(|tag| tag == &vec![ "e".to_string(), hex_64('b'), "wss://relay.radroots.example".to_string(), "fork".to_string() ])); - assert!(article_event.tags.iter().any(|tag| tag + assert!(article_event.tags_as_vec().iter().any(|tag| tag == &vec![ "a".to_string(), format!("30818:{}:soil-health", hex_64('a')), "wss://relay.radroots.example".to_string(), "defer".to_string() ])); - assert!(article_event.tags.iter().any(|tag| tag + assert!(article_event.tags_as_vec().iter().any(|tag| tag == &vec![ "e".to_string(), hex_64('b'), @@ -398,7 +428,7 @@ fn knowledge_codecs_roundtrip_all_contracts() { }; let redirect_event = event_from_parts(wiki_redirect_to_wire_parts(&redirect).unwrap()); validate_event_contract_shape(&redirect_event, "radroots.wiki.redirect.v1").unwrap(); - assert!(redirect_event.tags.iter().any(|tag| tag + assert!(redirect_event.tags_as_vec().iter().any(|tag| tag == &vec![ "a".to_string(), format!("30818:{}:soil-health", hex_64('a')), @@ -423,14 +453,14 @@ fn knowledge_codecs_roundtrip_all_contracts() { }; let merge_event = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); validate_event_contract_shape(&merge_event, "radroots.wiki.merge_request.v1").unwrap(); - assert_eq!(merge_event.content, "Merge synthetic source"); + assert_eq!(merge_event.content(), "Merge synthetic source"); assert!( merge_event - .tags + .tags_as_vec() .iter() .any(|tag| tag == &vec!["e".to_string(), hex_64('e'), String::new()]) ); - assert!(merge_event.tags.iter().any(|tag| tag + assert!(merge_event.tags_as_vec().iter().any(|tag| tag == &vec![ "e".to_string(), hex_64('f'), @@ -665,7 +695,8 @@ fn verified_decode_accepts_signed_claim_and_rejects_mutation() { assert!(matches!(decoded, RadrootsDecodedEvent::KnowledgeClaim(_))); let mut mutated = signed; - mutated.content = mutated.content.replace("Cover crops", "Compost"); + let mutated_content = mutated.content().replace("Cover crops", "Compost"); + replace_content(&mut mutated, mutated_content); let err = verify_and_decode_radroots_event(mutated).unwrap_err(); assert_eq!(err.code(), "nip01_verification"); assert!(matches!( @@ -677,22 +708,22 @@ fn verified_decode_accepts_signed_claim_and_rejects_mutation() { #[test] fn malformed_knowledge_events_return_stable_decode_codes() { let mut missing_contract = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); - missing_contract - .tags - .retain(|tag| tag.first().map(|value| value.as_str()) != Some("contract")); + mutate_tags(&mut missing_contract, |tags| { + tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("contract")); + }); let signed = sign_parts(WireEventParts { - kind: missing_contract.kind, - content: missing_contract.content, - tags: missing_contract.tags, + kind: missing_contract.kind_u32(), + content: missing_contract.content().to_string(), + tags: missing_contract.tags_as_vec(), }); let error = verify_and_decode_radroots_event(signed).unwrap_err(); assert_eq!(error.code(), "contract_validation"); let mut report = event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&report.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(report.content()).unwrap(); value["context"]["latitude"] = serde_json::Value::from("45.0000"); - report.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut report, serde_json::to_string(&value).unwrap()); let parsed_error = knowledge_field_report_from_event(report).unwrap_err(); assert_eq!(parsed_error.code(), "invalid_json"); } @@ -706,11 +737,13 @@ fn malformed_nip54_wiki_shapes_are_rejected() { }) .unwrap(), ); - for tag in &mut redirect.tags { - if tag.first().map(|value| value.as_str()) == Some("a") { - tag[1] = format!("30023:{}:soil-health", hex_64('a')); + mutate_tags(&mut redirect, |tags| { + for tag in tags { + if tag.first().map(|value| value.as_str()) == Some("a") { + tag[1] = format!("30023:{}:soil-health", hex_64('a')); + } } - } + }); assert_parse_error( wiki_redirect_from_event(redirect).unwrap_err(), EventParseError::InvalidTag("a"), @@ -724,9 +757,9 @@ fn malformed_nip54_wiki_shapes_are_rejected() { explanation: Some("Merge synthetic source".to_string()), }; let mut missing_target = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); - missing_target - .tags - .retain(|tag| tag.first().map(|value| value.as_str()) != Some("a")); + mutate_tags(&mut missing_target, |tags| { + tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("a")); + }); assert_parse_error( wiki_merge_request_from_event(missing_target).unwrap_err(), EventParseError::MissingTag("a"), @@ -734,18 +767,20 @@ fn malformed_nip54_wiki_shapes_are_rejected() { let mut missing_destination = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); - missing_destination - .tags - .retain(|tag| tag.first().map(|value| value.as_str()) != Some("p")); + mutate_tags(&mut missing_destination, |tags| { + tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("p")); + }); assert_parse_error( wiki_merge_request_from_event(missing_destination).unwrap_err(), EventParseError::MissingTag("p"), ); let mut missing_source = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); - missing_source.tags.retain(|tag| { - !(tag.first().map(|value| value.as_str()) == Some("e") - && tag.last().map(|value| value.as_str()) == Some("source")) + mutate_tags(&mut missing_source, |tags| { + tags.retain(|tag| { + !(tag.first().map(|value| value.as_str()) == Some("e") + && tag.last().map(|value| value.as_str()) == Some("source")) + }); }); assert_parse_error( wiki_merge_request_from_event(missing_source).unwrap_err(), @@ -753,12 +788,14 @@ fn malformed_nip54_wiki_shapes_are_rejected() { ); let mut duplicate_source = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); - duplicate_source.tags.push(vec![ - "e".to_string(), - hex_64('a'), - String::new(), - "source".to_string(), - ]); + mutate_tags(&mut duplicate_source, |tags| { + tags.push(vec![ + "e".to_string(), + hex_64('a'), + String::new(), + "source".to_string(), + ]); + }); assert_parse_error( wiki_merge_request_from_event(duplicate_source).unwrap_err(), EventParseError::InvalidTag("e"), @@ -766,11 +803,13 @@ fn malformed_nip54_wiki_shapes_are_rejected() { let mut wrong_merge_target = event_from_parts(wiki_merge_request_to_wire_parts(&merge).unwrap()); - for tag in &mut wrong_merge_target.tags { - if tag.first().map(|value| value.as_str()) == Some("a") { - tag[1] = format!("30023:{}:soil-health", hex_64('a')); + mutate_tags(&mut wrong_merge_target, |tags| { + for tag in tags { + if tag.first().map(|value| value.as_str()) == Some("a") { + tag[1] = format!("30023:{}:soil-health", hex_64('a')); + } } - } + }); assert_parse_error( wiki_merge_request_from_event(wrong_merge_target).unwrap_err(), EventParseError::InvalidTag("a"), @@ -778,16 +817,18 @@ fn malformed_nip54_wiki_shapes_are_rejected() { let mut orphan_fork = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let mut removed_fork_event = false; - orphan_fork.tags.retain(|tag| { - if !removed_fork_event - && tag.first().map(|value| value.as_str()) == Some("e") - && tag.last().map(|value| value.as_str()) == Some("fork") - { - removed_fork_event = true; - false - } else { - true - } + mutate_tags(&mut orphan_fork, |tags| { + tags.retain(|tag| { + if !removed_fork_event + && tag.first().map(|value| value.as_str()) == Some("e") + && tag.last().map(|value| value.as_str()) == Some("fork") + { + removed_fork_event = true; + false + } else { + true + } + }); }); assert_parse_error( wiki_article_from_event(orphan_fork).unwrap_err(), @@ -796,20 +837,22 @@ fn malformed_nip54_wiki_shapes_are_rejected() { let mut duplicate_defer = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); - duplicate_defer.tags.extend([ - vec![ - "a".to_string(), - format!("30818:{}:compost", hex_64('a')), - String::new(), - "defer".to_string(), - ], - vec![ - "e".to_string(), - hex_64('c'), - String::new(), - "defer".to_string(), - ], - ]); + mutate_tags(&mut duplicate_defer, |tags| { + tags.extend([ + vec![ + "a".to_string(), + format!("30818:{}:compost", hex_64('a')), + String::new(), + "defer".to_string(), + ], + vec![ + "e".to_string(), + hex_64('c'), + String::new(), + "defer".to_string(), + ], + ]); + }); assert_parse_error( wiki_article_from_event(duplicate_defer).unwrap_err(), EventParseError::InvalidTag("a"), @@ -841,22 +884,22 @@ fn wiki_article_version_refs_require_adjacent_marked_pairs() { let mut grouped = two_forks_event; let fork_addresses = grouped - .tags + .tags_as_vec() .iter() .filter(|tag| marked_tag(tag, "a", "fork")) .cloned() .collect::<Vec<_>>(); let fork_events = grouped - .tags + .tags_as_vec() .iter() .filter(|tag| marked_tag(tag, "e", "fork")) .cloned() .collect::<Vec<_>>(); - grouped - .tags - .retain(|tag| !marked_tag(tag, "a", "fork") && !marked_tag(tag, "e", "fork")); - grouped.tags.extend(fork_addresses); - grouped.tags.extend(fork_events); + mutate_tags(&mut grouped, |tags| { + tags.retain(|tag| !marked_tag(tag, "a", "fork") && !marked_tag(tag, "e", "fork")); + tags.extend(fork_addresses); + tags.extend(fork_events); + }); assert_parse_error( wiki_article_from_event(grouped).unwrap_err(), EventParseError::InvalidTag("a"), @@ -865,7 +908,9 @@ fn wiki_article_version_refs_require_adjacent_marked_pairs() { let mut reversed = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let fork_address_index = marked_tag_index(&reversed, "a", "fork"); let fork_event_index = marked_tag_index(&reversed, "e", "fork"); - reversed.tags.swap(fork_address_index, fork_event_index); + mutate_tags(&mut reversed, |tags| { + tags.swap(fork_address_index, fork_event_index); + }); assert_parse_error( wiki_article_from_event(reversed).unwrap_err(), EventParseError::InvalidTag("e"), @@ -873,7 +918,9 @@ fn wiki_article_version_refs_require_adjacent_marked_pairs() { let mut relay_mismatch = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let fork_event_index = marked_tag_index(&relay_mismatch, "e", "fork"); - relay_mismatch.tags[fork_event_index][2] = "wss://other.radroots.example".to_string(); + mutate_tags(&mut relay_mismatch, |tags| { + tags[fork_event_index][2] = "wss://other.radroots.example".to_string(); + }); assert_parse_error( wiki_article_from_event(relay_mismatch).unwrap_err(), EventParseError::InvalidTag("e"), @@ -882,7 +929,9 @@ fn wiki_article_version_refs_require_adjacent_marked_pairs() { let mut missing_partner = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let fork_event_index = marked_tag_index(&missing_partner, "e", "fork"); - missing_partner.tags.remove(fork_event_index); + mutate_tags(&mut missing_partner, |tags| { + tags.remove(fork_event_index); + }); assert_parse_error( wiki_article_from_event(missing_partner).unwrap_err(), EventParseError::InvalidTag("a"), @@ -891,7 +940,9 @@ fn wiki_article_version_refs_require_adjacent_marked_pairs() { let mut misplaced_marker = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let fork_address_index = marked_tag_index(&misplaced_marker, "a", "fork"); - misplaced_marker.tags[fork_address_index].insert(2, "fork".to_string()); + mutate_tags(&mut misplaced_marker, |tags| { + tags[fork_address_index].insert(2, "fork".to_string()); + }); assert_parse_error( wiki_article_from_event(misplaced_marker).unwrap_err(), EventParseError::InvalidTag("a"), @@ -899,7 +950,9 @@ fn wiki_article_version_refs_require_adjacent_marked_pairs() { let mut wrong_kind = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let fork_address_index = marked_tag_index(&wrong_kind, "a", "fork"); - wrong_kind.tags[fork_address_index][1] = format!("30023:{}:soil-health", hex_64('a')); + mutate_tags(&mut wrong_kind, |tags| { + tags[fork_address_index][1] = format!("30023:{}:soil-health", hex_64('a')); + }); assert_parse_error( wiki_article_from_event(wrong_kind).unwrap_err(), EventParseError::InvalidTag("a"), @@ -913,7 +966,7 @@ fn wiki_article_codec_accepts_missing_title_tag() { let article_event = event_from_parts(wiki_article_to_wire_parts(&article).unwrap()); assert!( !article_event - .tags + .tags_as_vec() .iter() .any(|tag| tag.first().map(String::as_str) == Some("title")) ); @@ -1106,25 +1159,25 @@ fn knowledge_claim_encode_enforces_citation_rules() { #[test] fn semantic_validation_rejects_invalid_decoded_content() { let mut article = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); - article.content = String::new(); + replace_content(&mut article, String::new()); assert_parse_error( wiki_article_from_event(article).unwrap_err(), EventParseError::InvalidJson("content_djot"), ); let mut source_event = event_from_parts(knowledge_source_to_wire_parts(&source()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&source_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(source_event.content()).unwrap(); value["title"] = serde_json::Value::String(String::new()); - source_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut source_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_source_from_event(source_event).unwrap_err(), EventParseError::InvalidJson("title"), ); let mut claim_event = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&claim_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(claim_event.content()).unwrap(); value["citation_spans"][0]["quote_hash"] = serde_json::Value::String("bad".to_string()); - claim_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut claim_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_claim_from_event(claim_event).unwrap_err(), EventParseError::InvalidJson("citation_spans"), @@ -1132,18 +1185,18 @@ fn semantic_validation_rejects_invalid_decoded_content() { let mut relation_event = event_from_parts(knowledge_relation_to_wire_parts(&relation()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&relation_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(relation_event.content()).unwrap(); value["subject"]["external_id"] = serde_json::Value::String("cover-crops".to_string()); - relation_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut relation_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_relation_from_event(relation_event).unwrap_err(), EventParseError::InvalidJson("subject"), ); let mut review_event = event_from_parts(knowledge_review_to_wire_parts(&review()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&review_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(review_event.content()).unwrap(); value["target"]["kind"] = serde_json::Value::from(0); - review_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut review_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_review_from_event(review_event).unwrap_err(), EventParseError::InvalidJson("review_target"), @@ -1151,9 +1204,9 @@ fn semantic_validation_rejects_invalid_decoded_content() { let mut report_event = event_from_parts(knowledge_field_report_to_wire_parts(&field_report()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&report_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(report_event.content()).unwrap(); value["observations"] = serde_json::Value::Array(Vec::new()); - report_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut report_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_field_report_from_event(report_event).unwrap_err(), EventParseError::InvalidJson("observations"), @@ -1171,9 +1224,9 @@ fn semantic_validation_rejects_invalid_decoded_content() { closes_at: None, }; let mut bounty_event = event_from_parts(evidence_bounty_to_wire_parts(&bounty).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&bounty_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(bounty_event.content()).unwrap(); value["target_refs"] = serde_json::Value::Array(Vec::new()); - bounty_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut bounty_event, serde_json::to_string(&value).unwrap()); assert_parse_error( evidence_bounty_from_event(bounty_event).unwrap_err(), EventParseError::InvalidJson("target_refs"), @@ -1191,9 +1244,9 @@ fn semantic_validation_rejects_invalid_decoded_content() { }; let mut proposal_event = event_from_parts(knowledge_change_proposal_to_wire_parts(&proposal).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&proposal_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(proposal_event.content()).unwrap(); value["summary"] = serde_json::Value::String(String::new()); - proposal_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut proposal_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_change_proposal_from_event(proposal_event).unwrap_err(), EventParseError::InvalidJson("summary"), @@ -1210,9 +1263,12 @@ fn semantic_validation_rejects_invalid_decoded_content() { }; let mut attestation_event = event_from_parts(contribution_attestation_to_wire_parts(&attestation).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&attestation_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(attestation_event.content()).unwrap(); value["subject_refs"] = serde_json::Value::Array(Vec::new()); - attestation_event.content = serde_json::to_string(&value).unwrap(); + replace_content( + &mut attestation_event, + serde_json::to_string(&value).unwrap(), + ); assert_parse_error( contribution_attestation_from_event(attestation_event).unwrap_err(), EventParseError::InvalidJson("subject_refs"), @@ -1222,12 +1278,13 @@ fn semantic_validation_rejects_invalid_decoded_content() { #[test] fn knowledge_decode_rejects_invalid_relay_values() { let mut article_event = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); - let source_tag = article_event - .tags - .iter_mut() - .find(|tag| tag.first().map(String::as_str) == Some("source")) - .expect("source tag"); - source_tag[5] = invalid_relay(); + mutate_tags(&mut article_event, |tags| { + let source_tag = tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some("source")) + .expect("source tag"); + source_tag[5] = invalid_relay(); + }); assert_parse_error( wiki_article_from_event(article_event).unwrap_err(), EventParseError::InvalidTag("source"), @@ -1240,32 +1297,33 @@ fn knowledge_decode_rejects_invalid_relay_values() { }) .unwrap(), ); - let target_tag = redirect_event - .tags - .iter_mut() - .find(|tag| tag.first().map(String::as_str) == Some("a")) - .expect("target tag"); - target_tag[2] = invalid_relay(); + mutate_tags(&mut redirect_event, |tags| { + let target_tag = tags + .iter_mut() + .find(|tag| tag.first().map(String::as_str) == Some("a")) + .expect("target tag"); + target_tag[2] = invalid_relay(); + }); assert_parse_error( wiki_redirect_from_event(redirect_event).unwrap_err(), EventParseError::InvalidTag("a"), ); let mut source_event = event_from_parts(knowledge_source_to_wire_parts(&source()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&source_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(source_event.content()).unwrap(); value["artifact_refs"][0]["relays"] = serde_json::Value::Array(vec![serde_json::Value::String(invalid_relay())]); - source_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut source_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_source_from_event(source_event).unwrap_err(), EventParseError::InvalidJson("artifact_refs"), ); let mut review_event = event_from_parts(knowledge_review_to_wire_parts(&review()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&review_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(review_event.content()).unwrap(); value["target"]["relays"] = serde_json::Value::Array(vec![serde_json::Value::String(invalid_relay())]); - review_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut review_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_review_from_event(review_event).unwrap_err(), EventParseError::InvalidJson("review_target"), @@ -1275,10 +1333,10 @@ fn knowledge_decode_rejects_invalid_relay_values() { report.context.location_precision = RadrootsKnowledgeLocationPrecision::ExactPrivateReference; report.context.private_location_ref = Some(event_ref('f', KIND_KNOWLEDGE_SOURCE)); let mut report_event = event_from_parts(knowledge_field_report_to_wire_parts(&report).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&report_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(report_event.content()).unwrap(); value["context"]["private_location_ref"]["relays"] = serde_json::Value::Array(vec![serde_json::Value::String(invalid_relay())]); - report_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut report_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_field_report_from_event(report_event).unwrap_err(), EventParseError::InvalidJson("private_location_ref"), @@ -1288,9 +1346,9 @@ fn knowledge_decode_rejects_invalid_relay_values() { #[test] fn knowledge_claim_decode_enforces_citation_rules() { let mut claim_event = event_from_parts(knowledge_claim_to_wire_parts(&claim()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&claim_event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(claim_event.content()).unwrap(); value["citation_spans"] = serde_json::Value::Array(Vec::new()); - claim_event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut claim_event, serde_json::to_string(&value).unwrap()); assert_parse_error( knowledge_claim_from_event(claim_event).unwrap_err(), EventParseError::InvalidJson("citation_spans"), diff --git a/crates/event_codec/tests/knowledge_fixtures.rs b/crates/event_codec/tests/knowledge_fixtures.rs @@ -2,7 +2,6 @@ use std::collections::BTreeSet; -use radroots_event::RadrootsEventEnvelope; use radroots_event::contract::{ RadrootsContractValidationError, RadrootsEventClass, all_event_contracts, validate_event_contract_shape, @@ -14,6 +13,7 @@ use radroots_event::kinds::{ use radroots_event::knowledge::{ RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RadrootsWikiArticle, }; +use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts}; use radroots_event_codec::error::{EventEncodeError, EventParseError}; use radroots_event_codec::knowledge::{ contribution_attestation_to_wire_parts, evidence_bounty_to_wire_parts, @@ -37,7 +37,7 @@ use radroots_test_fixtures::knowledge::{ }; fn event_from_parts(parts: WireEventParts) -> RadrootsEventEnvelope { - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: hex_64('0'), author: hex_64('a'), created_at: 1_800_000_000, @@ -45,7 +45,8 @@ fn event_from_parts(parts: WireEventParts) -> RadrootsEventEnvelope { tags: parts.tags, content: parts.content, sig: "1".repeat(128), - } + }) + .unwrap() } fn sign_parts(parts: WireEventParts) -> RadrootsEventEnvelope { @@ -63,10 +64,10 @@ fn sign_parts(parts: WireEventParts) -> RadrootsEventEnvelope { .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000)) .sign_with_keys(&keys) .expect("signed event"); - RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { id: event.id.to_hex(), author: event.pubkey.to_hex(), - created_at: event.created_at.as_secs() as u32, + created_at: event.created_at.as_secs(), kind: u32::from(event.kind.as_u16()), tags: event .tags @@ -76,7 +77,50 @@ fn sign_parts(parts: WireEventParts) -> RadrootsEventEnvelope { .collect(), content: event.content, sig: event.sig.to_string(), - } + }) + .unwrap() +} + +fn event_with_parts( + event: &RadrootsEventEnvelope, + tags: Vec<Vec<String>>, + content: String, + sig: String, +) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: event.id_str().to_string(), + author: event.author_str().to_string(), + created_at: event.created_at_u64(), + kind: event.kind_u32(), + tags, + content, + sig, + }) + .unwrap() +} + +fn mutate_tags(event: &mut RadrootsEventEnvelope, update: impl FnOnce(&mut Vec<Vec<String>>)) { + let mut tags = event.tags_as_vec(); + update(&mut tags); + *event = event_with_parts( + event, + tags, + event.content().to_string(), + event.sig_str().to_string(), + ); +} + +fn replace_content(event: &mut RadrootsEventEnvelope, content: String) { + *event = event_with_parts( + event, + event.tags_as_vec(), + content, + event.sig_str().to_string(), + ); +} + +fn replace_sig(event: &mut RadrootsEventEnvelope, sig: String) { + *event = event_with_parts(event, event.tags_as_vec(), event.content().to_string(), sig); } fn parts_for_fixture(fixture: &RadrootsKnowledgeFixture) -> WireEventParts { @@ -151,7 +195,7 @@ fn golden_knowledge_fixtures_cover_every_contract() { for fixture in &fixtures { let event = event_from_parts(parts_for_fixture(&fixture.data)); validate_event_contract_shape(&event, fixture.contract_id).unwrap(); - assert_eq!(event.kind, fixture.kind, "{}", fixture.id); + assert_eq!(event.kind_u32(), fixture.kind, "{}", fixture.id); } let article_parts = parts_for_fixture(&fixture_by_id(&fixtures, "wiki_article_valid").data); @@ -196,10 +240,12 @@ fn adversarial_knowledge_fixtures_reject_at_expected_stages() { .unwrap(); let mut malformed_event = event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); - malformed_event.tags.push(vec![ - "contract".to_string(), - RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), - ]); + mutate_tags(&mut malformed_event, |tags| { + tags.push(vec![ + "contract".to_string(), + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), + ]); + }); let error = validate_event_contract_shape(&malformed_event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) .unwrap_err(); assert_eq!(malformed.pipeline_stage, "contract_validation"); @@ -212,9 +258,12 @@ fn adversarial_knowledge_fixtures_reject_at_expected_stages() { let mut wrong_schema_event = event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); let mut wrong_schema_value: serde_json::Value = - serde_json::from_str(&wrong_schema_event.content).unwrap(); + serde_json::from_str(wrong_schema_event.content()).unwrap(); wrong_schema_value["schema"] = serde_json::Value::from("radroots.knowledge.relation.v1"); - wrong_schema_event.content = serde_json::to_string(&wrong_schema_value).unwrap(); + replace_content( + &mut wrong_schema_event, + serde_json::to_string(&wrong_schema_value).unwrap(), + ); let error = validate_event_contract_shape(&wrong_schema_event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) .unwrap_err(); assert_eq!(wrong_schema.pipeline_stage, "contract_validation"); @@ -226,13 +275,13 @@ fn adversarial_knowledge_fixtures_reject_at_expected_stages() { .unwrap(); let mut missing_contract_event = event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); - missing_contract_event - .tags - .retain(|tag| tag.first().map(|value| value.as_str()) != Some("contract")); + mutate_tags(&mut missing_contract_event, |tags| { + tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("contract")); + }); let signed = sign_parts(WireEventParts { - kind: missing_contract_event.kind, - content: missing_contract_event.content, - tags: missing_contract_event.tags, + kind: missing_contract_event.kind_u32(), + content: missing_contract_event.content().to_string(), + tags: missing_contract_event.tags_as_vec(), }); let error = verify_and_decode_radroots_event(signed).unwrap_err(); assert_eq!(missing_contract.pipeline_stage, error.code()); @@ -245,13 +294,16 @@ fn adversarial_knowledge_fixtures_reject_at_expected_stages() { let mut private_event = event_from_parts(knowledge_field_report_to_wire_parts(&knowledge_field_report()).unwrap()); let mut private_value: serde_json::Value = - serde_json::from_str(&private_event.content).unwrap(); + serde_json::from_str(private_event.content()).unwrap(); private_value["context"]["latitude"] = serde_json::Value::from("45.0000"); - private_event.content = serde_json::to_string(&private_value).unwrap(); + replace_content( + &mut private_event, + serde_json::to_string(&private_value).unwrap(), + ); let signed = sign_parts(WireEventParts { - kind: private_event.kind, - content: private_event.content, - tags: private_event.tags, + kind: private_event.kind_u32(), + content: private_event.content().to_string(), + tags: private_event.tags_as_vec(), }); let error = verify_and_decode_radroots_event(signed).unwrap_err(); assert_eq!(private_coordinates.pipeline_stage, error.code()); @@ -268,15 +320,17 @@ fn adversarial_knowledge_fixtures_reject_at_expected_stages() { .unwrap(); let mut unsupported_event = event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); - for tag in &mut unsupported_event.tags { - if tag.first().map(|value| value.as_str()) == Some("contract") { - tag[1] = "radroots.knowledge.unsupported.v1".to_string(); + mutate_tags(&mut unsupported_event, |tags| { + for tag in tags { + if tag.first().map(|value| value.as_str()) == Some("contract") { + tag[1] = "radroots.knowledge.unsupported.v1".to_string(); + } } - } + }); let signed = sign_parts(WireEventParts { - kind: unsupported_event.kind, - content: unsupported_event.content, - tags: unsupported_event.tags, + kind: unsupported_event.kind_u32(), + content: unsupported_event.content().to_string(), + tags: unsupported_event.tags_as_vec(), }); let error = verify_and_decode_radroots_event(signed).unwrap_err(); assert_eq!(unsupported.pipeline_stage, error.code()); @@ -316,9 +370,11 @@ fn nip54_and_signature_adversarial_fixtures_are_rejected() { .unwrap(); let mut missing_source_event = event_from_parts(wiki_merge_request_to_wire_parts(&wiki_merge_request()).unwrap()); - missing_source_event.tags.retain(|tag| { - !(tag.first().map(|value| value.as_str()) == Some("e") - && tag.last().map(|value| value.as_str()) == Some("source")) + mutate_tags(&mut missing_source_event, |tags| { + tags.retain(|tag| { + !(tag.first().map(|value| value.as_str()) == Some("e") + && tag.last().map(|value| value.as_str()) == Some("source")) + }); }); let error = wiki_merge_request_from_event(missing_source_event).unwrap_err(); assert_eq!(missing_source.pipeline_stage, "event_parse"); @@ -343,16 +399,18 @@ fn nip54_and_signature_adversarial_fixtures_are_rejected() { let mut orphan_fork_event = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let mut removed_fork_event = false; - orphan_fork_event.tags.retain(|tag| { - if !removed_fork_event - && tag.first().map(|value| value.as_str()) == Some("e") - && tag.last().map(|value| value.as_str()) == Some("fork") - { - removed_fork_event = true; - false - } else { - true - } + mutate_tags(&mut orphan_fork_event, |tags| { + tags.retain(|tag| { + if !removed_fork_event + && tag.first().map(|value| value.as_str()) == Some("e") + && tag.last().map(|value| value.as_str()) == Some("fork") + { + removed_fork_event = true; + false + } else { + true + } + }); }); let error = wiki_article_from_event(orphan_fork_event).unwrap_err(); assert_eq!(orphan_fork.pipeline_stage, "event_parse"); @@ -366,16 +424,18 @@ fn nip54_and_signature_adversarial_fixtures_are_rejected() { let mut orphan_defer_event = event_from_parts(wiki_article_to_wire_parts(&wiki_article()).unwrap()); let mut removed_defer_address = false; - orphan_defer_event.tags.retain(|tag| { - if !removed_defer_address - && tag.first().map(|value| value.as_str()) == Some("a") - && tag.last().map(|value| value.as_str()) == Some("defer") - { - removed_defer_address = true; - false - } else { - true - } + mutate_tags(&mut orphan_defer_event, |tags| { + tags.retain(|tag| { + if !removed_defer_address + && tag.first().map(|value| value.as_str()) == Some("a") + && tag.last().map(|value| value.as_str()) == Some("defer") + { + removed_defer_address = true; + false + } else { + true + } + }); }); let error = wiki_article_from_event(orphan_defer_event).unwrap_err(); assert_eq!(orphan_defer.pipeline_stage, "event_parse"); @@ -388,7 +448,8 @@ fn nip54_and_signature_adversarial_fixtures_are_rejected() { .unwrap(); let signed = sign_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); let mut mutated = signed.clone(); - mutated.content = mutated.content.replace("Cover crops", "Compost"); + let mutated_content = mutated.content().replace("Cover crops", "Compost"); + replace_content(&mut mutated, mutated_content); let error = verify_and_decode_radroots_event(mutated).unwrap_err(); assert_eq!(id_mismatch.pipeline_stage, error.code()); match error { @@ -403,7 +464,7 @@ fn nip54_and_signature_adversarial_fixtures_are_rejected() { .find(|fixture| fixture.id == "signature_invalidity") .unwrap(); let mut bad_signature = signed; - bad_signature.sig = "0".repeat(128); + replace_sig(&mut bad_signature, "0".repeat(128)); let error = verify_and_decode_radroots_event(bad_signature).unwrap_err(); assert_eq!(signature_invalidity.pipeline_stage, error.code()); match error { @@ -426,9 +487,9 @@ fn authoritative_knowledge_status_fields_are_rejected() { ] { let mut event = event_from_parts(knowledge_claim_to_wire_parts(&knowledge_claim()).unwrap()); - let mut value: serde_json::Value = serde_json::from_str(&event.content).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(event.content()).unwrap(); value[field] = serde_json::Value::from("approved"); - event.content = serde_json::to_string(&value).unwrap(); + replace_content(&mut event, serde_json::to_string(&value).unwrap()); let error = validate_event_contract_shape(&event, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA).unwrap_err(); assert_eq!( diff --git a/crates/event_codec/tests/list.rs b/crates/event_codec/tests/list.rs @@ -159,8 +159,8 @@ fn list_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_LIST_MUTE); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_LIST_MUTE); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.entries.len(), 2); } diff --git a/crates/event_codec/tests/list_set.rs b/crates/event_codec/tests/list_set.rs @@ -191,8 +191,8 @@ fn list_set_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_LIST_SET_FOLLOW); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_LIST_SET_FOLLOW); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.entries.len(), 2); } diff --git a/crates/event_codec/tests/listing.rs b/crates/event_codec/tests/listing.rs @@ -6,7 +6,7 @@ use radroots_core::{ RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_event::{ - RadrootsEventEnvelope, + RadrootsEventEnvelope, RadrootsEventEnvelopeParts, farm::RadrootsFarmRef, ids::{RadrootsDTag, RadrootsInventoryBinId}, kinds::{ @@ -34,6 +34,13 @@ use radroots_event_codec::listing::tags::{ }; use std::str::FromStr; +const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const AUTHOR: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const EVENT_SIG: &str = concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" +); + fn listing_d_tag(raw: &str) -> RadrootsDTag { raw.parse().unwrap() } @@ -58,6 +65,19 @@ fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<&str *tag = replacement.into_iter().map(str::to_string).collect(); } +fn event_envelope(kind: u32, tags: Vec<Vec<String>>, content: String) -> RadrootsEventEnvelope { + RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts { + id: EVENT_ID.to_string(), + author: AUTHOR.to_string(), + created_at: 7, + kind, + tags, + content, + sig: EVENT_SIG.to_string(), + }) + .unwrap() +} + fn assert_missing_tag(tags: Vec<Vec<String>>, expected: &'static str) { match listing_from_event(KIND_LISTING, &tags, "# Widget") { Err(EventParseError::MissingTag(tag)) => assert_eq!(tag, expected), @@ -898,59 +918,51 @@ fn listing_parsed_wrappers_preserve_event_metadata() { let listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAQ"); let parts = to_wire_parts(&listing).unwrap(); let data = data_from_event( - "event-id".to_string(), - "author-pubkey".to_string(), + EVENT_ID.to_string(), + AUTHOR.to_string(), 7, parts.kind, parts.content.clone(), parts.tags.clone(), ) .unwrap(); - assert_eq!(data.id, "event-id"); - assert_eq!(data.author, "author-pubkey"); + assert_eq!(data.id, EVENT_ID); + assert_eq!(data.author, AUTHOR); assert_eq!(data.published_at, 7); assert_eq!(data.kind, KIND_LISTING); assert_eq!(data.data.d_tag, listing.d_tag); let parsed = parsed_from_event( - "event-id".to_string(), - "author-pubkey".to_string(), + EVENT_ID.to_string(), + AUTHOR.to_string(), 7, parts.kind, parts.content.clone(), parts.tags.clone(), - "sig".to_string(), + EVENT_SIG.to_string(), ) .unwrap(); - assert_eq!(parsed.event.id, "event-id"); - assert_eq!(parsed.event.author, "author-pubkey"); - assert_eq!(parsed.event.created_at, 7); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.id_str(), EVENT_ID); + assert_eq!(parsed.event.author_str(), AUTHOR); + assert_eq!(parsed.event.created_at_u64(), 7); + assert_eq!(parsed.event.sig_str(), EVENT_SIG); assert_eq!(parsed.data.data.d_tag, listing.d_tag); - let event = RadrootsEventEnvelope { - id: "event-id".to_string(), - author: "author-pubkey".to_string(), - created_at: 7, - kind: parts.kind, - tags: parts.tags, - content: parts.content, - sig: "sig".to_string(), - }; + let event = event_envelope(parts.kind, parts.tags, parts.content); let data = data_from_nostr_event(&event).unwrap(); assert_eq!(data.data.d_tag, listing.d_tag); let parsed = parsed_from_nostr_event(&event).unwrap(); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), EVENT_SIG); assert_eq!(parsed.data.data.d_tag, listing.d_tag); let err = parsed_from_event( - "event-id".to_string(), - "author-pubkey".to_string(), + EVENT_ID.to_string(), + AUTHOR.to_string(), 7, KIND_POST, - event.content, - event.tags, - "sig".to_string(), + event.content().to_string(), + event.tags_as_vec(), + EVENT_SIG.to_string(), ) .unwrap_err(); assert!(matches!( diff --git a/crates/event_codec/tests/message.rs b/crates/event_codec/tests/message.rs @@ -272,17 +272,27 @@ fn message_metadata_and_index_from_event_roundtrip() { assert_eq!(metadata.data.subject.as_deref(), Some("topic")); let index = parsed_from_event( - "id".to_string(), - "author".to_string(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), 77, KIND_MESSAGE, "hello".to_string(), tags, - "sig".to_string(), + concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + .to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_MESSAGE); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_MESSAGE); + assert_eq!( + index.event.sig_str(), + concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + ); assert_eq!(index.data.data.recipients.len(), 2); } diff --git a/crates/event_codec/tests/message_file.rs b/crates/event_codec/tests/message_file.rs @@ -413,8 +413,8 @@ fn message_file_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_MESSAGE_FILE); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_MESSAGE_FILE); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.file_type, "image/jpeg"); } diff --git a/crates/event_codec/tests/post.rs b/crates/event_codec/tests/post.rs @@ -711,12 +711,12 @@ fn post_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.id, "id"); - assert_eq!(index.event.author, "author"); - assert_eq!(index.event.created_at, 77); - assert_eq!(index.event.kind, KIND_POST); - assert_eq!(index.event.content, "hello"); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.id_str(), "id"); + assert_eq!(index.event.author_str(), "author"); + assert_eq!(index.event.created_at_u64(), 77); + assert_eq!(index.event.kind_u32(), KIND_POST); + assert_eq!(index.event.content(), "hello"); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.content, "hello"); } diff --git a/crates/event_codec/tests/profile.rs b/crates/event_codec/tests/profile.rs @@ -183,8 +183,8 @@ fn profile_metadata_ignores_short_unknown_and_unrelated_profile_type_tags() { #[test] fn profile_parsed_event_preserves_wire_event_and_decoded_data() { let parsed = parsed_from_event( - "event-id".to_string(), - "author-pubkey".to_string(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(), 42, KIND_PROFILE, "{\"name\":\"alice\"}".to_string(), @@ -192,16 +192,32 @@ fn profile_parsed_event_preserves_wire_event_and_decoded_data() { RADROOTS_PROFILE_TYPE_TAG_KEY.to_string(), RADROOTS_PROFILE_TYPE_TAG_FARM.to_string(), ]], - "event-sig".to_string(), + concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + .to_string(), ) .expect("parsed profile"); - assert_eq!(parsed.event.id, "event-id"); - assert_eq!(parsed.event.author, "author-pubkey"); - assert_eq!(parsed.event.created_at, 42); - assert_eq!(parsed.event.kind, KIND_PROFILE); - assert_eq!(parsed.event.content, "{\"name\":\"alice\"}"); - assert_eq!(parsed.event.sig, "event-sig"); + assert_eq!( + parsed.event.id_str(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); + assert_eq!( + parsed.event.author_str(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ); + assert_eq!(parsed.event.created_at_u64(), 42); + assert_eq!(parsed.event.kind_u32(), KIND_PROFILE); + assert_eq!(parsed.event.content(), "{\"name\":\"alice\"}"); + assert_eq!( + parsed.event.sig_str(), + concat!( + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + ) + ); assert_eq!(parsed.data.data.profile.name, "alice"); assert_eq!( parsed.data.data.profile_type, diff --git a/crates/event_codec/tests/reaction.rs b/crates/event_codec/tests/reaction.rs @@ -432,7 +432,7 @@ fn reaction_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_REACTION); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_REACTION); + assert_eq!(index.event.sig_str(), "sig"); assert_event_target(&index.data.data.target); } diff --git a/crates/event_codec/tests/report.rs b/crates/event_codec/tests/report.rs @@ -448,6 +448,6 @@ fn report_wrappers_preserve_event_metadata() { "sig".to_string(), ) .unwrap(); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); assert_eq!(parsed.data.data.reported_pubkey, REPORTED); } diff --git a/crates/event_codec/tests/repost.rs b/crates/event_codec/tests/repost.rs @@ -428,7 +428,7 @@ fn repost_wrappers_preserve_event_metadata() { "sig".to_string(), ) .unwrap(); - assert_eq!(parsed.event.sig, "sig"); + assert_eq!(parsed.event.sig_str(), "sig"); let generic_parts = generic_repost_to_wire_parts(&generic_article_repost()).unwrap(); let generic_data = generic_repost_data_from_event( @@ -452,5 +452,5 @@ fn repost_wrappers_preserve_event_metadata() { "sig".to_string(), ) .unwrap(); - assert_eq!(generic_parsed.event.created_at, 11); + assert_eq!(generic_parsed.event.created_at_u64(), 11); } diff --git a/crates/event_codec/tests/seal.rs b/crates/event_codec/tests/seal.rs @@ -86,8 +86,8 @@ fn seal_metadata_and_index_from_event_roundtrip() { "sig".to_string(), ) .unwrap(); - assert_eq!(index.event.kind, KIND_SEAL); - assert_eq!(index.event.sig, "sig"); + assert_eq!(index.event.kind_u32(), KIND_SEAL); + assert_eq!(index.event.sig_str(), "sig"); assert_eq!(index.data.data.content, "payload"); } diff --git a/crates/event_codec/tests/structured_decode.rs b/crates/event_codec/tests/structured_decode.rs @@ -280,7 +280,7 @@ fn farm_metadata_and_index_decode_roundtrip() { "sig1".to_string(), ) .expect("farm index"); - assert_eq!(index.event.id, "id1"); + assert_eq!(index.event.id_str(), "id1"); assert_eq!(index.data.data.d_tag, d_tag); } @@ -342,7 +342,7 @@ fn coop_metadata_and_index_decode_roundtrip() { "sig2".to_string(), ) .expect("coop index"); - assert_eq!(index.event.kind, KIND_COOP); + assert_eq!(index.event.kind_u32(), KIND_COOP); assert_eq!(index.data.data.d_tag, d_tag); } @@ -574,7 +574,7 @@ fn plot_metadata_and_index_decode_roundtrip() { "sig3".to_string(), ) .expect("plot index"); - assert_eq!(index.event.author, TEST_PUBKEY_HEX); + assert_eq!(index.event.author_str(), TEST_PUBKEY_HEX); assert_eq!(index.data.data.d_tag, d_tag); } @@ -754,7 +754,7 @@ fn document_metadata_and_index_decode_roundtrip() { "sig4".to_string(), ) .expect("document index"); - assert_eq!(index.event.kind, KIND_DOCUMENT); + assert_eq!(index.event.kind_u32(), KIND_DOCUMENT); assert_eq!(index.data.data.d_tag, d_tag); } @@ -833,7 +833,7 @@ fn resource_area_metadata_and_index_decode_roundtrip() { "sig5".to_string(), ) .expect("area index"); - assert_eq!(index.event.id, "id5"); + assert_eq!(index.event.id_str(), "id5"); assert_eq!(index.data.data.d_tag, d_tag); } @@ -916,6 +916,6 @@ fn resource_cap_metadata_and_index_decode_roundtrip() { "sig6".to_string(), ) .expect("cap index"); - assert_eq!(index.event.sig, "sig6"); + assert_eq!(index.event.sig_str(), "sig6"); assert_eq!(index.data.data.d_tag, d_tag); }