commit c0da3c5ae7b45a671a0df16c1789d82aa89f7afe
parent 4bf3178cd52c2200e3bbbfc471e0d268b5627c8c
Author: triesap <tyson@radroots.org>
Date: Tue, 14 Jul 2026 09:21:23 +0000
store: persist canonical raw event wire
- derive store ingest from verified signed event wire
- persist outbox signed and raw JSON from canonical NIP-01 material
- align downstream Nostr, trade, transport, and authority callers
- verify event-store/outbox tests plus contract and hygiene gates
Diffstat:
28 files changed, 1156 insertions(+), 963 deletions(-)
diff --git a/crates/authority/src/authorization.rs b/crates/authority/src/authorization.rs
@@ -2,12 +2,12 @@
use crate::{RadrootsActorContext, RadrootsAuthorityError, RadrootsEventSigner};
use radroots_event::contract::{RadrootsEventContract, event_contract};
-#[cfg(test)]
-use radroots_event::draft::RadrootsSignedEventParts;
use radroots_event::draft::{
RadrootsDraftError, RadrootsEventDraft, RadrootsSignedEvent,
validate_signed_nostr_event_matches_draft,
};
+#[cfg(test)]
+use radroots_event::wire::RadrootsNip01EventWire;
#[cfg(not(feature = "std"))]
use alloc::{borrow::ToOwned, string::ToString};
@@ -32,22 +32,22 @@ pub fn authorize_actor_for_draft(
actor: &RadrootsActorContext,
draft: &RadrootsEventDraft,
) -> Result<&'static RadrootsEventContract, RadrootsAuthorityError> {
- let contract = event_contract(draft.contract_id.as_str()).ok_or_else(|| {
+ let contract = event_contract(draft.contract_id()).ok_or_else(|| {
RadrootsAuthorityError::UnknownContract {
- contract_id: draft.contract_id.clone(),
+ contract_id: draft.contract_id().to_owned(),
}
})?;
- if contract.kind != draft.kind {
+ if contract.kind != draft.kind_u32() {
return Err(RadrootsAuthorityError::DraftKindMismatch {
- contract_id: draft.contract_id.clone(),
+ contract_id: draft.contract_id().to_owned(),
expected_kind: contract.kind,
- actual_kind: draft.kind,
+ actual_kind: draft.kind_u32(),
});
}
authorize_actor_for_contract(actor, contract)?;
- if actor.pubkey().as_str() != draft.expected_pubkey.as_str() {
+ if actor.pubkey().as_str() != draft.expected_pubkey_str() {
return Err(RadrootsAuthorityError::ActorPubkeyMismatch {
- expected_pubkey: draft.expected_pubkey.clone(),
+ expected_pubkey: draft.expected_pubkey_str().to_owned(),
actor_pubkey: actor.pubkey().as_str().to_owned(),
});
}
@@ -61,11 +61,11 @@ pub fn authorize_signer_for_draft<S>(
where
S: RadrootsEventSigner + ?Sized,
{
- if signer.pubkey().as_str() == draft.expected_pubkey.as_str() {
+ if signer.pubkey().as_str() == draft.expected_pubkey_str() {
Ok(())
} else {
Err(RadrootsAuthorityError::SignerPubkeyMismatch {
- expected_pubkey: draft.expected_pubkey.clone(),
+ expected_pubkey: draft.expected_pubkey_str().to_owned(),
signer_pubkey: signer.pubkey().as_str().to_owned(),
})
}
@@ -190,7 +190,7 @@ mod tests {
#[derive(Default)]
struct SignedEventOverrides {
event_id: Option<String>,
- created_at: Option<u32>,
+ created_at: Option<u64>,
kind: Option<u32>,
tags: Option<Vec<Vec<String>>>,
content: Option<String>,
@@ -236,46 +236,69 @@ mod tests {
&self,
draft: &RadrootsEventDraft,
) -> Result<RadrootsSignedEvent, RadrootsSignerError> {
- RadrootsSignedEvent::new(RadrootsSignedEventParts {
+ let wire = RadrootsNip01EventWire {
id: self
.overrides
.event_id
.as_deref()
- .unwrap_or(draft.expected_event_id.as_str())
+ .unwrap_or(draft.expected_event_id_str())
.to_owned(),
pubkey: self.pubkey.to_string(),
- created_at: self.overrides.created_at.unwrap_or(draft.created_at),
- kind: self.overrides.kind.unwrap_or(draft.kind),
+ created_at: self.overrides.created_at.unwrap_or(draft.created_at_u64()),
+ kind: self.overrides.kind.unwrap_or(draft.kind_u32()),
tags: self
.overrides
.tags
.clone()
- .unwrap_or_else(|| draft.tags.clone()),
+ .unwrap_or_else(|| draft.tags_as_vec()),
content: self
.overrides
.content
.clone()
- .unwrap_or_else(|| draft.content.clone()),
+ .unwrap_or_else(|| draft.content().to_owned()),
sig: hex_128('f'),
- raw_json: "{}".to_owned(),
- })
- .map_err(|error| RadrootsSignerError::SigningFailed {
- message: error.to_string(),
+ extra: Default::default(),
+ };
+ RadrootsSignedEvent::from_wire_unchecked(wire, "{}").map_err(|error| {
+ RadrootsSignerError::SigningFailed {
+ message: error.to_string(),
+ }
})
}
}
fn signed_event_from_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: hex_128('f'),
- raw_json: "{}".to_owned(),
- })
+ signed_event_from_parts(
+ draft.expected_event_id_str().to_owned(),
+ draft.expected_pubkey_str().to_owned(),
+ draft.created_at_u64(),
+ draft.kind_u32(),
+ draft.tags_as_vec(),
+ draft.content().to_owned(),
+ )
+ }
+
+ fn signed_event_from_parts(
+ id: String,
+ pubkey: String,
+ created_at: u64,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ ) -> RadrootsSignedEvent {
+ RadrootsSignedEvent::from_wire_unchecked(
+ RadrootsNip01EventWire {
+ id,
+ pubkey,
+ created_at,
+ kind,
+ tags,
+ content,
+ sig: hex_128('f'),
+ extra: Default::default(),
+ },
+ "{}",
+ )
.expect("signed event")
}
@@ -344,44 +367,6 @@ mod tests {
}
#[test]
- fn unknown_contract_and_kind_mismatch_fail() {
- let actor = seller_actor(hex_64('a').as_str());
- let unknown = RadrootsEventDraft {
- contract_id: "radroots.unknown.v1".to_owned(),
- contract_registry_version: 1,
- kind: KIND_LISTING,
- created_at: 1_700_000_000,
- tags: Vec::new(),
- content: "{}".to_owned(),
- expected_pubkey: hex_64('a'),
- expected_event_id: hex_64('e'),
- };
- assert!(matches!(
- authorize_actor_for_draft(&actor, &unknown),
- Err(RadrootsAuthorityError::UnknownContract { .. })
- ));
-
- let wrong_kind = RadrootsEventDraft {
- contract_id: "radroots.listing.published.v1".to_owned(),
- contract_registry_version: 1,
- kind: KIND_POST,
- created_at: 1_700_000_000,
- tags: Vec::new(),
- content: "{}".to_owned(),
- expected_pubkey: hex_64('a'),
- expected_event_id: hex_64('e'),
- };
- assert!(matches!(
- authorize_actor_for_draft(&actor, &wrong_kind),
- Err(RadrootsAuthorityError::DraftKindMismatch {
- expected_kind: KIND_LISTING,
- actual_kind: KIND_POST,
- ..
- })
- ));
- }
-
- #[test]
fn signed_event_id_mismatch_fails() {
let pubkey = hex_64('a');
let draft = listing_draft(pubkey.as_str());
@@ -402,7 +387,7 @@ mod tests {
let signer = StaticSigner::with_overrides(
pubkey.as_str(),
SignedEventOverrides {
- created_at: Some(draft.created_at + 1),
+ created_at: Some(draft.created_at_u64() + 1),
..SignedEventOverrides::default()
},
);
@@ -500,17 +485,14 @@ mod tests {
fn signed_event_pubkey_mismatch_fails() {
let pubkey = hex_64('a');
let draft = listing_draft(pubkey.as_str());
- let signed = RadrootsSignedEvent::new(RadrootsSignedEventParts {
- id: draft.expected_event_id.clone(),
- pubkey: hex_64('b'),
- created_at: draft.created_at,
- kind: draft.kind,
- tags: draft.tags.clone(),
- content: draft.content.clone(),
- sig: hex_128('f'),
- raw_json: "{}".to_owned(),
- })
- .expect("signed event");
+ let signed = signed_event_from_parts(
+ draft.expected_event_id_str().to_owned(),
+ hex_64('b'),
+ draft.created_at_u64(),
+ draft.kind_u32(),
+ draft.tags_as_vec(),
+ draft.content().to_owned(),
+ );
assert!(matches!(
validate_signed_event_matches_draft(&signed, &draft),
@@ -531,54 +513,10 @@ mod tests {
}
#[test]
- fn signed_event_computed_id_mismatch_fails() {
- let pubkey = hex_64('a');
- let inconsistent_draft = RadrootsEventDraft {
- contract_id: "radroots.listing.published.v1".to_owned(),
- contract_registry_version: 1,
- kind: KIND_LISTING,
- created_at: 1_700_000_000,
- tags: vec![vec!["d".to_owned(), "listing-a".to_owned()]],
- content: "{}".to_owned(),
- expected_pubkey: pubkey,
- expected_event_id: hex_64('e'),
- };
- let signed = signed_event_from_draft(&inconsistent_draft);
-
- assert!(matches!(
- validate_signed_event_matches_draft(&signed, &inconsistent_draft),
- Err(RadrootsAuthorityError::SignedEventComputedIdMismatch { .. })
- ));
- }
-
- #[test]
- fn sign_authorized_draft_calls_full_integrity_check() {
- let pubkey = hex_64('a');
- let inconsistent_draft = RadrootsEventDraft {
- contract_id: "radroots.listing.published.v1".to_owned(),
- contract_registry_version: 1,
- kind: KIND_LISTING,
- created_at: 1_700_000_000,
- tags: vec![vec!["d".to_owned(), "listing-a".to_owned()]],
- content: "{}".to_owned(),
- expected_pubkey: pubkey.clone(),
- expected_event_id: hex_64('e'),
- };
- let actor = seller_actor(pubkey.as_str());
- let signer = StaticSigner::new(pubkey.as_str());
-
- assert!(matches!(
- sign_authorized_draft(&actor, &signer, &inconsistent_draft),
- Err(RadrootsAuthorityError::SignedEventComputedIdMismatch { .. })
- ));
- }
-
- #[test]
fn static_signer_maps_invalid_signed_event_parts() {
let pubkey = hex_64('a');
- let mut draft = listing_draft(pubkey.as_str());
- draft.expected_event_id = "bad-id".to_owned();
- let signer = StaticSigner::new(pubkey.as_str());
+ let draft = listing_draft(pubkey.as_str());
+ let signer = StaticSigner::with_event_id(pubkey.as_str(), "bad-id".to_owned());
assert!(matches!(
signer.sign_frozen_draft(&draft),
@@ -595,9 +533,9 @@ mod tests {
let signed = sign_authorized_draft(&actor, &signer, &draft).expect("signed");
- assert_eq!(signed.id, draft.expected_event_id);
- assert_eq!(signed.pubkey, draft.expected_pubkey);
- assert_eq!(signed.kind, KIND_LISTING);
+ assert_eq!(signed.id_str(), draft.expected_event_id_str());
+ assert_eq!(signed.pubkey_str(), draft.expected_pubkey_str());
+ assert_eq!(signed.kind(), KIND_LISTING);
}
#[test]
diff --git a/crates/authority/src/error.rs b/crates/authority/src/error.rs
@@ -59,8 +59,8 @@ pub enum RadrootsAuthorityError {
},
SignedEventCreatedAtMismatch {
- expected_created_at: u32,
- actual_created_at: u32,
+ expected_created_at: u64,
+ actual_created_at: u64,
},
SignedEventKindMismatch {
diff --git a/crates/authority/src/local_signer.rs b/crates/authority/src/local_signer.rs
@@ -68,15 +68,7 @@ mod tests {
}
fn verification_event(signed: &RadrootsSignedEvent) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
- id: signed.id.clone(),
- author: signed.pubkey.clone(),
- created_at: signed.created_at,
- kind: signed.kind,
- tags: signed.tags.clone(),
- content: signed.content.clone(),
- sig: signed.sig.clone(),
- }
+ signed.envelope().clone()
}
#[test]
@@ -93,8 +85,8 @@ mod tests {
let signed = signer.sign_frozen_draft(&draft).expect("signed");
- assert_eq!(signed.id, draft.expected_event_id);
- assert_eq!(signed.pubkey, draft.expected_pubkey);
+ assert_eq!(signed.id_str(), draft.expected_event_id_str());
+ assert_eq!(signed.pubkey_str(), draft.expected_pubkey_str());
assert_eq!(
radroots_nostr_verify_event(&verification_event(&signed)),
RadrootsNostrEventVerification::Verified
diff --git a/crates/authority/src/signer.rs b/crates/authority/src/signer.rs
@@ -1,10 +1,10 @@
#![forbid(unsafe_code)]
use crate::{RadrootsAuthorityError, RadrootsSignerError};
-#[cfg(test)]
-use radroots_event::draft::RadrootsSignedEventParts;
use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent};
use radroots_event::ids::RadrootsPublicKey;
+#[cfg(test)]
+use radroots_event::wire::RadrootsNip01EventWire;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsSignerIdentity {
@@ -60,6 +60,7 @@ mod tests {
struct MockSigner {
pubkey: RadrootsPublicKey,
failure: Option<RadrootsSignerError>,
+ event_id: Option<String>,
}
impl MockSigner {
@@ -67,6 +68,7 @@ mod tests {
Self {
pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"),
failure: None,
+ event_id: None,
}
}
@@ -74,6 +76,15 @@ mod tests {
Self {
pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"),
failure: Some(failure),
+ event_id: None,
+ }
+ }
+
+ fn with_event_id(pubkey: &str, event_id: String) -> Self {
+ Self {
+ pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"),
+ failure: None,
+ event_id: Some(event_id),
}
}
}
@@ -98,16 +109,24 @@ mod tests {
}
});
}
- RadrootsSignedEvent::new(RadrootsSignedEventParts {
- id: draft.expected_event_id.to_string(),
- pubkey: self.pubkey.to_string(),
- created_at: draft.created_at,
- kind: draft.kind,
- tags: draft.tags.clone(),
- content: draft.content.clone(),
- sig: hex_128('f'),
- raw_json: "{}".to_owned(),
- })
+ let id = self
+ .event_id
+ .as_deref()
+ .unwrap_or(draft.expected_event_id_str())
+ .to_owned();
+ RadrootsSignedEvent::from_wire_unchecked(
+ RadrootsNip01EventWire {
+ id,
+ pubkey: self.pubkey.to_string(),
+ created_at: draft.created_at_u64(),
+ kind: draft.kind_u32(),
+ tags: draft.tags_as_vec(),
+ content: draft.content().to_owned(),
+ sig: hex_128('f'),
+ extra: Default::default(),
+ },
+ "{}",
+ )
.map_err(|error| RadrootsSignerError::SigningFailed {
message: error.to_string(),
})
@@ -142,9 +161,9 @@ mod tests {
let signed = signer.sign_frozen_draft(&draft).expect("signed");
- assert_eq!(signed.id, draft.expected_event_id);
- assert_eq!(signed.pubkey, pubkey);
- assert_eq!(signed.kind, KIND_POST);
+ assert_eq!(signed.id_str(), draft.expected_event_id_str());
+ assert_eq!(signed.pubkey_str(), pubkey);
+ assert_eq!(signed.kind(), KIND_POST);
}
#[test]
@@ -175,9 +194,8 @@ mod tests {
#[test]
fn mock_signer_maps_invalid_signed_event_parts() {
let pubkey = hex_64('a');
- let signer = MockSigner::new(pubkey.as_str());
- let mut draft = draft_for(pubkey.as_str());
- draft.expected_event_id = "bad-id".to_string();
+ let signer = MockSigner::with_event_id(pubkey.as_str(), "bad-id".to_string());
+ let draft = draft_for(pubkey.as_str());
let err = signer.sign_frozen_draft(&draft).expect_err("failure");
diff --git a/crates/event_store/src/error.rs b/crates/event_store/src/error.rs
@@ -1,6 +1,8 @@
use radroots_event::contract::RadrootsContractMatchError;
+use radroots_event::draft::RadrootsSignedEventError;
use radroots_event::event_head::RadrootsEventHeadMalformed;
use radroots_event::ids::RadrootsIdParseError;
+use radroots_event::wire::RadrootsEventWireError;
use radroots_transport::RadrootsTransportError;
#[derive(Debug, thiserror::Error)]
@@ -15,6 +17,10 @@ pub enum RadrootsEventStoreError {
EventHeadMalformed(RadrootsEventHeadMalformed),
#[error("identifier parse error: {0}")]
IdParse(#[from] RadrootsIdParseError),
+ #[error("event wire error: {0}")]
+ EventWire(#[from] RadrootsEventWireError),
+ #[error("signed event error: {0}")]
+ SignedEvent(#[from] RadrootsSignedEventError),
#[error("transport contract error: {0}")]
Transport(RadrootsTransportError),
#[error("stored event `{0}` was not found")]
@@ -40,6 +46,8 @@ pub enum RadrootsEventStoreError {
},
#[error("integer value `{value}` is outside {field} range")]
IntegerRange { field: &'static str, value: i64 },
+ #[error("unsigned integer value `{value}` is outside {field} range")]
+ UnsignedIntegerRange { field: &'static str, value: u64 },
}
impl From<RadrootsTransportError> for RadrootsEventStoreError {
diff --git a/crates/event_store/src/model.rs b/crates/event_store/src/model.rs
@@ -3,7 +3,9 @@ use radroots_event::RadrootsEventEnvelope;
use radroots_event::contract::{
RadrootsContractMatchError, RadrootsEventClass, RadrootsTagSemantic, RadrootsTagValueType,
};
+use radroots_event::draft::RadrootsSignedEvent;
use radroots_event::event_head::RadrootsEventHeadDecision;
+use radroots_event::wire::RadrootsNip01EventWire;
use radroots_transport::{
RadrootsTransportKind, RadrootsTransportTargetFingerprint, RadrootsTransportTargetUri,
};
@@ -215,31 +217,42 @@ impl RadrootsTransportObservation {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RadrootsEventIngest {
- pub event: RadrootsEventEnvelope,
- pub raw_json: Option<String>,
+ pub signed_event: RadrootsSignedEvent,
pub observed_at_ms: i64,
pub transport_observation: Option<RadrootsTransportObservation>,
}
impl RadrootsEventIngest {
- pub fn new(event: RadrootsEventEnvelope, observed_at_ms: i64) -> Self {
+ pub fn new(signed_event: RadrootsSignedEvent, observed_at_ms: i64) -> Self {
Self {
- event,
- raw_json: None,
+ signed_event,
observed_at_ms,
transport_observation: None,
}
}
- pub fn with_raw_json(mut self, raw_json: impl Into<String>) -> Self {
- self.raw_json = Some(raw_json.into());
- self
+ pub fn from_raw_json(
+ raw_json: impl Into<String>,
+ observed_at_ms: i64,
+ ) -> Result<Self, RadrootsEventStoreError> {
+ let raw_json = raw_json.into();
+ let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str())?;
+ let signed_event = RadrootsSignedEvent::from_wire_verified_id(wire, raw_json)?;
+ Ok(Self::new(signed_event, observed_at_ms))
}
pub fn with_observation(mut self, observation: RadrootsTransportObservation) -> Self {
self.transport_observation = Some(observation);
self
}
+
+ pub fn event(&self) -> &RadrootsEventEnvelope {
+ self.signed_event.envelope()
+ }
+
+ pub fn raw_json(&self) -> &str {
+ self.signed_event.raw_json()
+ }
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -295,7 +308,7 @@ pub struct RadrootsStoredEvent {
pub seq: i64,
pub event_id: String,
pub pubkey: String,
- pub created_at: u32,
+ pub created_at: u64,
pub kind: u32,
pub tags_json: String,
pub content: String,
@@ -329,7 +342,7 @@ pub struct RadrootsStoredEventHead {
pub pubkey: String,
pub d_tag: Option<String>,
pub event_id: String,
- pub created_at: u32,
+ pub created_at: u64,
pub updated_at_ms: i64,
}
diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs
@@ -16,7 +16,7 @@ use radroots_event::event_head::{
RadrootsEventHeadCoordinate, RadrootsEventHeadDecision, event_head_candidate_for_contract,
select_event_head,
};
-use radroots_event::ids::{RadrootsEventId, RadrootsEventSignature, RadrootsPublicKey};
+use radroots_event::ids::{RadrootsEventId, RadrootsPublicKey};
use radroots_nostr::prelude::{RadrootsNostrEventVerification, radroots_nostr_verify_event};
use radroots_transport::{
RadrootsTransportKind, RadrootsTransportTargetFingerprint, RadrootsTransportTargetUri,
@@ -105,22 +105,20 @@ impl RadrootsEventStore {
&self,
ingest: RadrootsEventIngest,
) -> Result<RadrootsEventIngestReceipt, RadrootsEventStoreError> {
- validate_event_identity(&ingest.event)?;
- let verification_status = verify_event(&ingest.event);
- let classification = classify_event(&ingest.event);
- let raw_json = ingest
- .raw_json
- .clone()
- .map(Ok)
- .unwrap_or_else(|| serde_json::to_string(&ingest.event))?;
- let tags_json = serde_json::to_string(&ingest.event.tags)?;
+ let event = ingest.event();
+ validate_event_identity(event)?;
+ let verification_status = verify_event(event);
+ let classification = classify_event(event);
+ let tags = event.tags_as_vec();
+ let tags_json = serde_json::to_string(&tags)?;
+ let event_id = event.id_str().to_owned();
let mut tx = self.pool.begin().await?;
let insert = insert_raw_event(
&mut tx,
&ingest,
&classification,
verification_status,
- raw_json.as_str(),
+ ingest.raw_json(),
tags_json.as_str(),
)
.await?;
@@ -129,12 +127,11 @@ impl RadrootsEventStore {
let mut projection_eligible = classification.base_projection_eligible(verification_status);
if inserted {
- insert_tags(&mut tx, &ingest.event, classification.contract).await?;
+ insert_tags(&mut tx, event, classification.contract).await?;
if let Some(contract) = classification.contract {
if projection_eligible {
let head =
- apply_event_head(&mut tx, &ingest.event, contract, ingest.observed_at_ms)
- .await?;
+ apply_event_head(&mut tx, event, contract, ingest.observed_at_ms).await?;
projection_eligible = head.projection_eligible;
head_decision = head.decision;
sqlx::query(
@@ -142,7 +139,7 @@ impl RadrootsEventStore {
)
.bind(bool_i64(projection_eligible))
.bind(ingest.observed_at_ms)
- .bind(ingest.event.id.as_str())
+ .bind(event_id.as_str())
.execute(&mut *tx)
.await?;
} else {
@@ -155,14 +152,14 @@ impl RadrootsEventStore {
}
if let Some(observation) = ingest.transport_observation.as_ref() {
- upsert_observation(&mut tx, ingest.event.id.as_str(), observation).await?;
+ upsert_observation(&mut tx, event_id.as_str(), observation).await?;
}
tx.commit().await?;
Ok(RadrootsEventIngestReceipt {
seq: insert.seq,
- event_id: ingest.event.id,
+ event_id,
inserted,
verification_status,
contract_status: classification.contract_status,
@@ -450,14 +447,14 @@ async fn query_string(pool: &SqlitePool, sql: &str) -> Result<String, RadrootsEv
}
fn validate_event_identity(event: &RadrootsEventEnvelope) -> Result<(), RadrootsEventStoreError> {
- RadrootsEventId::parse(event.id.as_str())?;
- RadrootsPublicKey::parse(event.author.as_str())?;
- RadrootsEventSignature::parse(event.sig.as_str())?;
+ RadrootsEventId::parse(event.id_str())?;
+ RadrootsPublicKey::parse(event.author_str())?;
Ok(())
}
fn classify_event(event: &RadrootsEventEnvelope) -> EventClassification {
- match identify_event_contract(event.kind, &event.tags, &event.content) {
+ let tags = event.tags_as_vec();
+ match identify_event_contract(event.kind_u32(), &tags, event.content()) {
Ok(contract) => EventClassification {
contract_status: RadrootsEventContractStatus::Supported,
contract: Some(contract),
@@ -497,7 +494,7 @@ async fn insert_raw_event(
raw_json: &str,
tags_json: &str,
) -> Result<InsertRawEventResult, RadrootsEventStoreError> {
- let event = &ingest.event;
+ let event = ingest.event();
let contract_id = classification.contract.map(|contract| contract.id);
let event_class = classification
.contract
@@ -506,13 +503,13 @@ async fn insert_raw_event(
let result = sqlx::query(
"INSERT OR IGNORE INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
- .bind(event.id.as_str())
- .bind(event.author.as_str())
- .bind(i64::from(event.created_at))
- .bind(i64::from(event.kind))
+ .bind(event.id_str())
+ .bind(event.author_str())
+ .bind(i64_from_u64("created_at", event.created_at_u64())?)
+ .bind(i64::from(event.kind_u32()))
.bind(tags_json)
- .bind(event.content.as_str())
- .bind(event.sig.as_str())
+ .bind(event.content())
+ .bind(event.sig_str())
.bind(raw_json)
.bind(verification_status.as_str())
.bind(classification.contract_status.as_str())
@@ -524,7 +521,7 @@ async fn insert_raw_event(
.execute(&mut **tx)
.await?;
let inserted = result.rows_affected() > 0;
- let seq = event_seq(tx, event.id.as_str()).await?;
+ let seq = event_seq(tx, event.id_str()).await?;
Ok(InsertRawEventResult { inserted, seq })
}
@@ -546,10 +543,11 @@ async fn insert_tags(
event: &RadrootsEventEnvelope,
contract: Option<&'static RadrootsEventContract>,
) -> Result<(), RadrootsEventStoreError> {
- for (index, tag) in event.tags.iter().enumerate() {
- let tag_name = tag.first().map(String::as_str).unwrap_or("");
- let tag_value = tag.get(1).map(String::as_str);
- let tag_json = serde_json::to_string(tag)?;
+ for (index, tag) in event.tag_slices().iter().enumerate() {
+ let tag_values = tag.as_slice();
+ let tag_name = tag_values.first().map(String::as_str).unwrap_or("");
+ let tag_value = tag_values.get(1).map(String::as_str);
+ let tag_json = serde_json::to_string(tag_values)?;
let tag_contract = contract.and_then(|contract| {
contract
.tags
@@ -562,7 +560,7 @@ async fn insert_tags(
sqlx::query(
"INSERT INTO event_envelope_tags(event_id, tag_index, tag_name, tag_value, tag_json, contract_semantic, contract_value_type, relay_indexed) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
)
- .bind(event.id.as_str())
+ .bind(event.id_str())
.bind(i64::try_from(index).map_err(|_| RadrootsEventStoreError::IntegerRange {
field: "tag_index",
value: i64::MAX,
@@ -675,7 +673,7 @@ async fn current_event_head(
Ok(RadrootsCurrentEventHead {
coordinate: coordinate.clone(),
event_id: RadrootsEventId::parse(event_id)?,
- created_at: u32_from_i64("created_at", created_at)?,
+ created_at: u64_from_i64("created_at", created_at)?,
})
})
.transpose()
@@ -703,7 +701,7 @@ async fn upsert_head(
.bind(i64::from(*kind))
.bind(pubkey.as_str())
.bind(candidate.event_id.as_str())
- .bind(i64::from(candidate.created_at))
+ .bind(i64_from_u64("created_at", candidate.created_at)?)
.bind(updated_at_ms)
.execute(&mut **tx)
.await?;
@@ -728,7 +726,7 @@ async fn upsert_head(
.bind(pubkey.as_str())
.bind(d_tag.as_str())
.bind(candidate.event_id.as_str())
- .bind(i64::from(candidate.created_at))
+ .bind(i64_from_u64("created_at", candidate.created_at)?)
.bind(updated_at_ms)
.execute(&mut **tx)
.await?;
@@ -742,7 +740,7 @@ fn stored_event_from_row(
row: sqlx::sqlite::SqliteRow,
) -> Result<RadrootsStoredEvent, RadrootsEventStoreError> {
let kind = u32_from_i64("kind", row.try_get("kind")?)?;
- let created_at = u32_from_i64("created_at", row.try_get("created_at")?)?;
+ let created_at = u64_from_i64("created_at", row.try_get("created_at")?)?;
let verification_status =
RadrootsEventVerificationStatus::parse(row.try_get("verification_status")?)?;
let contract_status =
@@ -798,7 +796,7 @@ fn stored_head_from_row(
pubkey: row.try_get("pubkey")?,
d_tag: row.try_get("d_tag")?,
event_id: row.try_get("event_id")?,
- created_at: u32_from_i64("created_at", row.try_get("created_at")?)?,
+ created_at: u64_from_i64("created_at", row.try_get("created_at")?)?,
updated_at_ms: row.try_get("updated_at_ms")?,
})
}
@@ -859,6 +857,16 @@ fn u32_from_i64(field: &'static str, value: i64) -> Result<u32, RadrootsEventSto
u32::try_from(value).map_err(|_| RadrootsEventStoreError::IntegerRange { field, value })
}
+#[cfg_attr(coverage_nightly, coverage(off))]
+fn u64_from_i64(field: &'static str, value: i64) -> Result<u64, RadrootsEventStoreError> {
+ u64::try_from(value).map_err(|_| RadrootsEventStoreError::IntegerRange { field, value })
+}
+
+#[cfg_attr(coverage_nightly, coverage(off))]
+fn i64_from_u64(field: &'static str, value: u64) -> Result<i64, RadrootsEventStoreError> {
+ i64::try_from(value).map_err(|_| RadrootsEventStoreError::UnsignedIntegerRange { field, value })
+}
+
fn bool_i64(value: bool) -> i64 {
if value { 1 } else { 0 }
}
@@ -900,13 +908,15 @@ where
#[cfg(test)]
mod tests {
use super::*;
+ use radroots_event::draft::RadrootsSignedEvent;
use radroots_event::event_head::event_head_candidate_for_event;
use radroots_event::kinds::{
KIND_GEOCHAT, KIND_LISTING, KIND_ORDER_REQUEST, KIND_POST, KIND_PROFILE,
};
+ use radroots_event::wire::{RadrootsNip01EventWire, compute_canonical_nip01_event_id};
use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp,
- radroots_event_from_nostr, radroots_nostr_build_event,
+ radroots_nostr_build_event,
};
const FIXTURE_ALICE_SECRET_KEY_HEX: &str =
@@ -929,27 +939,72 @@ mod tests {
created_at: u32,
tags: Vec<Vec<String>>,
content: &str,
- ) -> RadrootsEventEnvelope {
+ ) -> RadrootsSignedEvent {
let raw_event = radroots_nostr_build_event(kind, content, tags)
.expect("builder")
.custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at)))
.sign_with_keys(&fixture_keys())
.expect("signed event");
- radroots_event_from_nostr(&raw_event)
+ signed_event_from_raw_json(serde_json::to_string(&raw_event).expect("raw json"))
+ }
+
+ fn signed_event_from_raw_json(raw_json: String) -> RadrootsSignedEvent {
+ let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str()).expect("wire");
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
+ }
+
+ fn synthetic_signed_event(
+ kind: u32,
+ created_at: u64,
+ tags: Vec<Vec<String>>,
+ content: &str,
+ ) -> RadrootsSignedEvent {
+ let pubkey = FIXTURE_ALICE_PUBLIC_KEY_HEX.to_owned();
+ let content = content.to_owned();
+ let id = compute_canonical_nip01_event_id(
+ pubkey.as_str(),
+ created_at,
+ kind,
+ &tags,
+ content.as_str(),
+ )
+ .expect("event id")
+ .into_string();
+ let wire = RadrootsNip01EventWire {
+ id,
+ pubkey,
+ created_at,
+ kind,
+ tags,
+ content,
+ sig: event_id('f').repeat(2),
+ extra: Default::default(),
+ };
+ let raw_json = serde_json::to_string(&wire).expect("raw json");
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
}
- fn tamper_signature(event: &mut RadrootsEventEnvelope) {
- let replacement = if event.sig.starts_with('0') { "1" } else { "0" };
- event.sig.replace_range(0..1, replacement);
+ fn tamper_signature(event: &RadrootsSignedEvent) -> RadrootsSignedEvent {
+ let mut wire = event.wire().clone();
+ let replacement = if wire.sig.starts_with('0') { "1" } else { "0" };
+ wire.sig.replace_range(0..1, replacement);
+ let raw_json = serde_json::to_string(&wire).expect("raw json");
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
+ }
+
+ fn tampered_content_raw_json(event: &RadrootsSignedEvent, content: &str) -> String {
+ let mut wire = event.wire().clone();
+ wire.content = content.to_owned();
+ serde_json::to_string(&wire).expect("raw json")
}
fn listing_tags(d_tag: &str) -> Vec<Vec<String>> {
vec![vec!["d".to_owned(), d_tag.to_owned()]]
}
- fn head_coordinate_for_event(event: &RadrootsEventEnvelope) -> RadrootsEventHeadCoordinate {
+ fn head_coordinate_for_event(event: &RadrootsSignedEvent) -> RadrootsEventHeadCoordinate {
let RadrootsEventHeadCandidateResult::Candidate(candidate) =
- event_head_candidate_for_event(event).expect("head candidate")
+ event_head_candidate_for_event(event.envelope()).expect("head candidate")
else {
panic!("event should select a head");
};
@@ -1153,8 +1208,7 @@ mod tests {
vec![vec!["t".to_owned(), "soil".to_owned()]],
"hello",
);
- let ingest =
- RadrootsEventIngest::new(event.clone(), 1_000).with_raw_json("{\"fixture\":true}");
+ let ingest = RadrootsEventIngest::new(event.clone(), 1_000);
let first = store
.ingest_event(ingest.clone())
@@ -1162,7 +1216,7 @@ mod tests {
.expect("first ingest");
let second = store.ingest_event(ingest).await.expect("second ingest");
let stored = store
- .get_event(event.id.as_str())
+ .get_event(event.id_str())
.await
.expect("get")
.expect("stored");
@@ -1179,7 +1233,7 @@ mod tests {
RadrootsEventVerificationStatus::Verified
);
assert_eq!(stored.seq, first.seq);
- assert_eq!(stored.raw_json, "{\"fixture\":true}");
+ assert_eq!(stored.raw_json, event.raw_json());
assert_eq!(stored.content, "hello");
assert_eq!(stored.tags_json, "[[\"t\",\"soil\"]]");
assert_eq!(
@@ -1189,7 +1243,7 @@ mod tests {
assert!(stored.projection_eligible);
assert_eq!(
store
- .tags_for_event(event.id.as_str())
+ .tags_for_event(event.id_str())
.await
.expect("tags")
.len(),
@@ -1198,6 +1252,17 @@ mod tests {
}
#[tokio::test]
+ async fn wrapper_json_is_rejected_as_event_authority() {
+ let event = signed_event(KIND_POST, 10, Vec::new(), "hello");
+ let wrapper_json = serde_json::to_string(&event).expect("wrapper json");
+
+ let error = RadrootsEventIngest::from_raw_json(wrapper_json, 1_000)
+ .expect_err("wrapper json should not parse as event wire");
+
+ assert!(matches!(error, RadrootsEventStoreError::EventWire(_)));
+ }
+
+ #[tokio::test]
async fn unsupported_verified_events_are_stored_but_not_projected() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let event = signed_event(999, 11, Vec::new(), "unsupported");
@@ -1206,7 +1271,7 @@ mod tests {
.await
.expect("ingest");
let stored = store
- .get_event(event.id.as_str())
+ .get_event(event.id_str())
.await
.expect("get")
.expect("stored");
@@ -1234,15 +1299,15 @@ mod tests {
#[test]
fn test_helpers_cover_signature_and_non_head_branches() {
- let mut zero_sig = signed_event(KIND_POST, 12, Vec::new(), "zero");
- zero_sig.sig.replace_range(0..1, "0");
- tamper_signature(&mut zero_sig);
- assert!(zero_sig.sig.starts_with('1'));
-
- let mut nonzero_sig = signed_event(KIND_POST, 12, Vec::new(), "nonzero");
- nonzero_sig.sig.replace_range(0..1, "1");
- tamper_signature(&mut nonzero_sig);
- assert!(nonzero_sig.sig.starts_with('0'));
+ let zero_sig = synthetic_signed_event(KIND_POST, 12, Vec::new(), "zero");
+ let zero_sig = tamper_signature(&zero_sig);
+ assert!(zero_sig.sig_str().starts_with('0'));
+
+ let nonzero_sig = tamper_signature(&signed_event(KIND_POST, 12, Vec::new(), "nonzero"));
+ assert_ne!(
+ nonzero_sig.sig_str(),
+ signed_event(KIND_POST, 12, Vec::new(), "nonzero").sig_str()
+ );
}
#[test]
@@ -1253,33 +1318,14 @@ mod tests {
}
#[tokio::test]
- async fn id_mismatch_events_are_stored_but_not_projected() {
+ async fn id_mismatch_raw_json_is_rejected_before_storage() {
let store = RadrootsEventStore::open_memory().await.expect("open");
- let mut event = signed_event(KIND_POST, 12, Vec::new(), "hello");
- event.content = "tampered".to_owned();
- let receipt = store
- .ingest_event(RadrootsEventIngest::new(event.clone(), 2_100))
- .await
- .expect("ingest");
- let stored = store
- .get_event(event.id.as_str())
- .await
- .expect("get")
- .expect("stored");
+ let event = signed_event(KIND_POST, 12, Vec::new(), "hello");
+ let raw_json = tampered_content_raw_json(&event, "tampered");
- assert_eq!(
- receipt.contract_status,
- RadrootsEventContractStatus::Supported
- );
- assert_eq!(
- receipt.verification_status,
- RadrootsEventVerificationStatus::IdMismatch
- );
- assert_eq!(
- stored.verification_status,
- RadrootsEventVerificationStatus::IdMismatch
- );
- assert!(!stored.projection_eligible);
+ let error = RadrootsEventIngest::from_raw_json(raw_json, 2_100).expect_err("id mismatch");
+
+ assert!(matches!(error, RadrootsEventStoreError::EventWire(_)));
assert!(
store
.events_since_cursor("social", 10)
@@ -1292,14 +1338,13 @@ mod tests {
#[tokio::test]
async fn signature_invalid_events_are_stored_but_not_projected() {
let store = RadrootsEventStore::open_memory().await.expect("open");
- let mut event = signed_event(KIND_POST, 13, Vec::new(), "hello");
- tamper_signature(&mut event);
+ let event = tamper_signature(&signed_event(KIND_POST, 13, Vec::new(), "hello"));
let receipt = store
.ingest_event(RadrootsEventIngest::new(event.clone(), 2_200))
.await
.expect("ingest");
let stored = store
- .get_event(event.id.as_str())
+ .get_event(event.id_str())
.await
.expect("get")
.expect("stored");
@@ -1325,15 +1370,14 @@ mod tests {
#[tokio::test]
async fn malformed_envelope_events_are_stored_but_not_projected() {
let store = RadrootsEventStore::open_memory().await.expect("open");
- let mut event = signed_event(KIND_POST, 13, Vec::new(), "hello");
- event.kind = u32::from(u16::MAX) + 1;
+ let event = synthetic_signed_event(u32::from(u16::MAX) + 1, 13, Vec::new(), "hello");
let receipt = store
.ingest_event(RadrootsEventIngest::new(event.clone(), 2_250))
.await
.expect("ingest");
let stored = store
- .get_event(event.id.as_str())
+ .get_event(event.id_str())
.await
.expect("get")
.expect("stored");
@@ -1359,7 +1403,7 @@ mod tests {
.await
.expect("ingest");
let stored = store
- .get_event(event.id.as_str())
+ .get_event(event.id_str())
.await
.expect("get")
.expect("stored");
@@ -1380,11 +1424,11 @@ mod tests {
async fn event_head_helper_maps_not_persisted_candidates() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let event = signed_event(KIND_GEOCHAT, 17, Vec::new(), "hello");
- let classification = classify_event(&event);
+ let classification = classify_event(event.envelope());
let contract = classification.contract.expect("contract");
let mut tx = store.pool.begin().await.expect("tx");
- let head = apply_event_head(&mut tx, &event, contract, 2_280)
+ let head = apply_event_head(&mut tx, event.envelope(), contract, 2_280)
.await
.expect("head");
@@ -1402,7 +1446,7 @@ mod tests {
.await
.expect("ingest");
let stored = store
- .get_event(event.id.as_str())
+ .get_event(event.id_str())
.await
.expect("get")
.expect("stored");
@@ -1420,7 +1464,7 @@ mod tests {
}
#[tokio::test]
- async fn id_mismatch_addressable_events_do_not_update_heads() {
+ async fn id_mismatch_addressable_raw_json_does_not_update_heads() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let original = signed_event(KIND_LISTING, 17, listing_tags("listing-1"), "{}");
let first = store
@@ -1428,18 +1472,9 @@ mod tests {
.await
.expect("first");
let coordinate = head_coordinate_for_event(&original);
- let mut invalid = signed_event(KIND_LISTING, 18, listing_tags("listing-1"), "{}");
- invalid.content = "{\"tampered\":true}".to_owned();
-
- let receipt = store
- .ingest_event(RadrootsEventIngest::new(invalid.clone(), 2_400))
- .await
- .expect("invalid");
- let stored = store
- .get_event(invalid.id.as_str())
- .await
- .expect("get")
- .expect("stored");
+ let invalid = signed_event(KIND_LISTING, 18, listing_tags("listing-1"), "{}");
+ let raw_json = tampered_content_raw_json(&invalid, "{\"tampered\":true}");
+ let error = RadrootsEventIngest::from_raw_json(raw_json, 2_400).expect_err("id mismatch");
let head = store
.event_head(&coordinate)
.await
@@ -1447,17 +1482,8 @@ mod tests {
.expect("stored head");
assert_eq!(first.head_decision, RadrootsEventHeadStoreDecision::Applied);
- assert_eq!(
- receipt.verification_status,
- RadrootsEventVerificationStatus::IdMismatch
- );
- assert_eq!(
- receipt.head_decision,
- RadrootsEventHeadStoreDecision::NotProjectionEligible
- );
- assert!(!receipt.projection_eligible);
- assert!(!stored.projection_eligible);
- assert_eq!(head.event_id, original.id);
+ assert!(matches!(error, RadrootsEventStoreError::EventWire(_)));
+ assert_eq!(head.event_id, original.id_str());
}
#[tokio::test]
@@ -1469,8 +1495,12 @@ mod tests {
.await
.expect("first");
let coordinate = head_coordinate_for_event(&original);
- let mut invalid = signed_event(KIND_LISTING, 20, listing_tags("listing-2"), "{}");
- tamper_signature(&mut invalid);
+ let invalid = tamper_signature(&signed_event(
+ KIND_LISTING,
+ 20,
+ listing_tags("listing-2"),
+ "{}",
+ ));
let receipt = store
.ingest_event(RadrootsEventIngest::new(invalid.clone(), 2_600))
@@ -1491,7 +1521,7 @@ mod tests {
RadrootsEventHeadStoreDecision::NotProjectionEligible
);
assert!(!receipt.projection_eligible);
- assert_eq!(head.event_id, original.id);
+ assert_eq!(head.event_id, original.id_str());
}
#[tokio::test]
@@ -1503,8 +1533,12 @@ mod tests {
.await
.expect("original");
let coordinate = head_coordinate_for_event(&original);
- let mut invalid = signed_event(KIND_LISTING, 22, listing_tags("listing-3"), "{}");
- invalid.content = "{\"tampered\":true}".to_owned();
+ let invalid = tamper_signature(&signed_event(
+ KIND_LISTING,
+ 22,
+ listing_tags("listing-3"),
+ "{}",
+ ));
let first_invalid = store
.ingest_event(RadrootsEventIngest::new(invalid.clone(), 2_800))
@@ -1531,7 +1565,7 @@ mod tests {
second_invalid.head_decision,
RadrootsEventHeadStoreDecision::SkippedDuplicate
);
- assert_eq!(head.event_id, original.id);
+ assert_eq!(head.event_id, original.id_str());
}
#[tokio::test]
@@ -1562,7 +1596,7 @@ mod tests {
second.head_decision,
RadrootsEventHeadStoreDecision::SkippedDuplicate
);
- assert_eq!(head.event_id, event.id);
+ assert_eq!(head.event_id, event.id_str());
}
#[tokio::test]
@@ -1575,7 +1609,7 @@ mod tests {
.await
.expect("ingest");
let stored = store
- .get_event(event.id.as_str())
+ .get_event(event.id_str())
.await
.expect("get")
.expect("stored");
@@ -1651,8 +1685,8 @@ mod tests {
.await
.expect("tag query");
assert_eq!(events.len(), 2);
- assert_eq!(events[0].event_id, high_created_at.id);
- assert_eq!(events[1].event_id, low_created_at.id);
+ assert_eq!(events[0].event_id, high_created_at.id_str());
+ assert_eq!(events[1].event_id, low_created_at.id_str());
assert!(events.iter().all(|event| event.projection_eligible));
let limited = store
@@ -1660,7 +1694,7 @@ mod tests {
.await
.expect("limited tag query");
assert_eq!(limited.len(), 1);
- assert_eq!(limited[0].event_id, high_created_at.id);
+ assert_eq!(limited[0].event_id, high_created_at.id_str());
}
#[tokio::test]
@@ -1746,7 +1780,7 @@ mod tests {
.await
.expect("contract tag query");
assert_eq!(events.len(), 1);
- assert_eq!(events[0].event_id, matching_order.id);
+ assert_eq!(events[0].event_id, matching_order.id_str());
assert_eq!(
events[0].contract_id.as_deref(),
Some("radroots.order.request.v1")
@@ -1771,7 +1805,7 @@ mod tests {
.ingest_event(RadrootsEventIngest::new(event.clone(), 3_000))
.await
.expect("ingest");
- let tags = store.tags_for_event(event.id.as_str()).await.expect("tags");
+ let tags = store.tags_for_event(event.id_str()).await.expect("tags");
assert_eq!(tags[0].tag_index, 0);
assert_eq!(tags[0].tag_name, "p");
@@ -1811,7 +1845,7 @@ mod tests {
.ingest_event(RadrootsEventIngest::new(event.clone(), 3_100))
.await
.expect("ingest");
- let tags = store.tags_for_event(event.id.as_str()).await.expect("tags");
+ let tags = store.tags_for_event(event.id_str()).await.expect("tags");
let listing_tag = tags
.iter()
.find(|tag| tag.tag_name == "listing_event")
@@ -1864,7 +1898,7 @@ mod tests {
store.ingest_event(ingest).await.expect("older duplicate");
let observations = store
- .observations_for_event(event.id.as_str())
+ .observations_for_event(event.id_str())
.await
.expect("stale duplicate observations");
assert_eq!(observations.len(), 1);
@@ -1912,7 +1946,7 @@ mod tests {
.expect("newer duplicate without message");
let observations = store
- .observations_for_event(event.id.as_str())
+ .observations_for_event(event.id_str())
.await
.expect("observations");
assert_eq!(observations.len(), 1);
@@ -1943,7 +1977,7 @@ mod tests {
signed_event(KIND_PROFILE, 20, Vec::new(), "{\"name\":\"a\"}"),
signed_event(KIND_PROFILE, 20, Vec::new(), "{\"name\":\"b\"}"),
];
- events.sort_by(|left, right| left.id.cmp(&right.id));
+ events.sort_by(|left, right| left.id_str().cmp(right.id_str()));
let lower = events[0].clone();
let higher = events[1].clone();
@@ -1967,7 +2001,7 @@ mod tests {
second.head_decision,
RadrootsEventHeadStoreDecision::Applied
);
- assert_eq!(head.event_id, lower.id);
+ assert_eq!(head.event_id, lower.id_str());
let store = RadrootsEventStore::open_memory().await.expect("open");
store
@@ -1988,7 +2022,7 @@ mod tests {
second.head_decision,
RadrootsEventHeadStoreDecision::SkippedSameTimestampHigherEventId
);
- assert_eq!(head.event_id, lower.id);
+ assert_eq!(head.event_id, lower.id_str());
}
#[tokio::test]
@@ -2011,8 +2045,8 @@ mod tests {
.await
.expect("initial replay");
assert_eq!(replay.len(), 2);
- assert_eq!(replay[0].event_id, first.id);
- assert_eq!(replay[1].event_id, second.id);
+ assert_eq!(replay[0].event_id, first.id_str());
+ assert_eq!(replay[1].event_id, second.id_str());
store
.update_projection_cursor(&RadrootsProjectionCursor {
projection_id: "social".to_owned(),
@@ -2027,7 +2061,7 @@ mod tests {
.await
.expect("next replay");
assert_eq!(replay.len(), 1);
- assert_eq!(replay[0].event_id, second.id);
+ assert_eq!(replay[0].event_id, second.id_str());
}
#[tokio::test]
diff --git a/crates/nostr/src/codec_adapters.rs b/crates/nostr/src/codec_adapters.rs
@@ -3,7 +3,6 @@ use alloc::{string::String, vec::Vec};
use crate::types::RadrootsNostrEvent;
-use crate::util::created_at_u32_saturating;
use radroots_event::{
job_feedback::RadrootsJobFeedback, job_request::RadrootsJobRequest,
job_result::RadrootsJobResult,
@@ -22,8 +21,8 @@ fn author(e: &RadrootsNostrEvent) -> String {
e.pubkey.to_hex()
}
-fn published_at(e: &RadrootsNostrEvent) -> u32 {
- created_at_u32_saturating(e.created_at)
+fn published_at(e: &RadrootsNostrEvent) -> u64 {
+ e.created_at.as_secs()
}
fn kind_u32(e: &RadrootsNostrEvent) -> u32 {
diff --git a/crates/nostr/src/draft_signing.rs b/crates/nostr/src/draft_signing.rs
@@ -1,39 +1,42 @@
#![forbid(unsafe_code)]
use crate::error::RadrootsNostrError;
-use crate::event_convert::radroots_event_from_nostr;
use crate::events::radroots_nostr_build_event;
use crate::types::{RadrootsNostrKeys, RadrootsNostrTimestamp};
use nostr::JsonUtil;
use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent};
+use radroots_event::wire::RadrootsNip01EventWire;
pub fn radroots_nostr_sign_frozen_draft(
keys: &RadrootsNostrKeys,
draft: &RadrootsEventDraft,
) -> Result<RadrootsSignedEvent, RadrootsNostrError> {
let actual_pubkey = keys.public_key().to_hex();
- if actual_pubkey != draft.expected_pubkey {
+ if actual_pubkey != draft.expected_pubkey_str() {
return Err(RadrootsNostrError::FrozenDraftPubkeyMismatch {
- expected_pubkey: draft.expected_pubkey.clone(),
+ expected_pubkey: draft.expected_pubkey_str().to_owned(),
actual_pubkey,
});
}
- let event = radroots_nostr_build_event(draft.kind, draft.content.clone(), draft.tags.clone())?
- .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(
- draft.created_at,
- )))
- .sign_with_keys(keys)?;
+ let event = radroots_nostr_build_event(
+ draft.kind_u32(),
+ draft.content().to_owned(),
+ draft.tags_as_vec(),
+ )?
+ .custom_created_at(RadrootsNostrTimestamp::from_secs(draft.created_at_u64()))
+ .sign_with_keys(keys)?;
let actual_event_id = event.id.to_hex();
- if actual_event_id != draft.expected_event_id {
+ if actual_event_id != draft.expected_event_id_str() {
return Err(RadrootsNostrError::FrozenDraftEventIdMismatch {
- expected_event_id: draft.expected_event_id.clone(),
+ expected_event_id: draft.expected_event_id_str().to_owned(),
actual_event_id,
});
}
let raw_json = event.as_json();
- RadrootsSignedEvent::from_event(radroots_event_from_nostr(&event), raw_json).map_err(Into::into)
+ let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str())?;
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).map_err(Into::into)
}
#[cfg(test)]
@@ -69,17 +72,17 @@ mod tests {
let draft = post_draft(FIXTURE_ALICE.public_key_hex);
let signed = radroots_nostr_sign_frozen_draft(&keys, &draft).expect("signed event");
- assert_eq!(signed.id, draft.expected_event_id);
- assert_eq!(signed.pubkey, draft.expected_pubkey);
- assert_eq!(signed.created_at, draft.created_at);
- assert_eq!(signed.kind, draft.kind);
- assert_eq!(signed.tags, draft.tags);
- assert_eq!(signed.content, draft.content);
+ assert_eq!(signed.id_str(), draft.expected_event_id_str());
+ assert_eq!(signed.pubkey_str(), draft.expected_pubkey_str());
+ assert_eq!(signed.created_at(), draft.created_at_u64());
+ assert_eq!(signed.kind(), draft.kind_u32());
+ assert_eq!(signed.tags_as_vec(), draft.tags_as_vec());
+ assert_eq!(signed.content(), draft.content());
- let raw_event = crate::types::RadrootsNostrEvent::from_json(signed.raw_json.as_str())
- .expect("raw json");
- assert_eq!(raw_event.id.to_hex(), signed.id);
- assert_eq!(raw_event.created_at.as_secs(), u64::from(draft.created_at));
+ let raw_event =
+ crate::types::RadrootsNostrEvent::from_json(signed.raw_json()).expect("raw json");
+ assert_eq!(raw_event.id.to_hex(), signed.id_str());
+ assert_eq!(raw_event.created_at.as_secs(), draft.created_at_u64());
}
#[test]
@@ -97,8 +100,10 @@ mod tests {
#[test]
fn sign_frozen_draft_rejects_event_id_mismatch() {
let keys = fixture_keys(FIXTURE_ALICE.secret_key_hex);
- let mut draft = post_draft(FIXTURE_ALICE.public_key_hex);
- draft.expected_event_id = "f".repeat(64);
+ let draft = post_draft(FIXTURE_ALICE.public_key_hex);
+ let mut raw = serde_json::to_value(&draft).expect("draft json");
+ raw["expected_event_id"] = serde_json::Value::String("f".repeat(64));
+ let draft: RadrootsEventDraft = serde_json::from_value(raw).expect("draft");
let error = radroots_nostr_sign_frozen_draft(&keys, &draft).expect_err("id mismatch");
assert!(matches!(
diff --git a/crates/nostr/src/error.rs b/crates/nostr/src/error.rs
@@ -28,6 +28,14 @@ pub enum RadrootsNostrError {
DraftError(#[from] radroots_event::draft::RadrootsDraftError),
#[cfg(feature = "events")]
+ #[error("Event wire error: {0}")]
+ EventWire(#[from] radroots_event::wire::RadrootsEventWireError),
+
+ #[cfg(feature = "events")]
+ #[error("Signed event error: {0}")]
+ SignedEvent(#[from] radroots_event::draft::RadrootsSignedEventError),
+
+ #[cfg(feature = "events")]
#[error(
"Frozen draft signer public key mismatch: expected {expected_pubkey}, got {actual_pubkey}"
)]
diff --git a/crates/nostr/src/event_adapters.rs b/crates/nostr/src/event_adapters.rs
@@ -13,14 +13,11 @@ use radroots_event_codec::profile::RadrootsProfileData;
use crate::types::{RadrootsNostrEvent, RadrootsNostrMetadata};
#[cfg(feature = "events")]
-use crate::util::created_at_u32_saturating;
-
-#[cfg(feature = "events")]
pub fn to_post_event_metadata(e: &RadrootsNostrEvent) -> RadrootsParsedData<RadrootsPost> {
RadrootsParsedData::new(
e.id.to_string(),
e.pubkey.to_string(),
- created_at_u32_saturating(e.created_at),
+ e.created_at.as_secs(),
e.kind.as_u16() as u32,
RadrootsPost {
content: e.content.clone(),
@@ -56,7 +53,7 @@ pub fn to_profile_event_metadata(
return Some(RadrootsParsedData::new(
e.id.to_string(),
e.pubkey.to_string(),
- created_at_u32_saturating(e.created_at),
+ e.created_at.as_secs(),
e.kind.as_u16() as u32,
RadrootsProfileData {
profile_type,
@@ -81,7 +78,7 @@ pub fn to_profile_event_metadata(
return Some(RadrootsParsedData::new(
e.id.to_string(),
e.pubkey.to_string(),
- created_at_u32_saturating(e.created_at),
+ e.created_at.as_secs(),
e.kind.as_u16() as u32,
RadrootsProfileData {
profile_type,
diff --git a/crates/nostr/src/event_convert.rs b/crates/nostr/src/event_convert.rs
@@ -1,20 +1,19 @@
#![forbid(unsafe_code)]
use crate::types::RadrootsNostrEvent as RadrootsNostrRawEvent;
-use radroots_event::{RadrootsEventEnvelope, RadrootsEventPtr};
-
-use crate::util::event_created_at_u32_saturating;
+use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr};
pub fn radroots_event_from_nostr(event: &RadrootsNostrRawEvent) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: event.id.to_string(),
author: event.pubkey.to_string(),
- created_at: event_created_at_u32_saturating(event),
+ created_at: event.created_at.as_secs(),
kind: event.kind.as_u16() as u32,
tags: event.tags.iter().map(|t| t.as_slice().to_vec()).collect(),
content: event.content.clone(),
sig: event.sig.to_string(),
- }
+ })
+ .expect("nostr event is canonical")
}
pub fn radroots_event_ptr_from_nostr(event: &RadrootsNostrRawEvent) -> RadrootsEventPtr {
diff --git a/crates/nostr/src/event_verify.rs b/crates/nostr/src/event_verify.rs
@@ -48,21 +48,21 @@ pub fn radroots_nostr_verify_event_id(
}
fn raw_event_from_radroots(event: &RadrootsEventEnvelope) -> Option<RadrootsNostrRawEvent> {
- let id = RadrootsNostrEventId::from_hex(event.id.as_str()).ok()?;
- let public_key = RadrootsNostrPublicKey::from_hex(event.author.as_str()).ok()?;
- let kind_u16 = u16::try_from(event.kind).ok()?;
- let mut tags = Vec::with_capacity(event.tags.len());
- for tag in event.tags.iter().cloned() {
- tags.push(RadrootsNostrTag::parse(tag).ok()?);
+ let id = RadrootsNostrEventId::from_hex(event.id_str()).ok()?;
+ let public_key = RadrootsNostrPublicKey::from_hex(event.author_str()).ok()?;
+ let kind_u16 = u16::try_from(event.kind_u32()).ok()?;
+ let mut tags = Vec::with_capacity(event.tag_slices().len());
+ for tag in event.tag_slices() {
+ tags.push(RadrootsNostrTag::parse(tag.as_slice().to_vec()).ok()?);
}
- let sig = Signature::from_str(event.sig.as_str()).ok()?;
+ let sig = Signature::from_str(event.sig_str()).ok()?;
Some(RadrootsNostrRawEvent::new(
id,
public_key,
- RadrootsNostrTimestamp::from_secs(u64::from(event.created_at)),
+ RadrootsNostrTimestamp::from_secs(event.created_at_u64()),
RadrootsNostrKind::Custom(kind_u16),
tags,
- event.content.clone(),
+ event.content().to_owned(),
sig,
))
}
@@ -74,7 +74,7 @@ mod tests {
use crate::events::radroots_nostr_build_event;
use crate::test_fixtures::FIXTURE_ALICE;
use crate::types::{RadrootsNostrKeys, RadrootsNostrSecretKey};
- use radroots_event::kinds::KIND_POST;
+ use radroots_event::{RadrootsEventEnvelopeParts, kinds::KIND_POST};
fn fixture_keys() -> RadrootsNostrKeys {
let secret_key =
@@ -95,6 +95,24 @@ mod tests {
radroots_event_from_nostr(&raw_event)
}
+ fn envelope_with(
+ event: &RadrootsEventEnvelope,
+ content: String,
+ kind: u32,
+ sig: String,
+ ) -> RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
+ id: event.id_str().to_owned(),
+ author: event.author_str().to_owned(),
+ created_at: event.created_at_u64(),
+ kind,
+ tags: event.tags_as_vec(),
+ content,
+ sig,
+ })
+ .expect("envelope")
+ }
+
#[test]
fn verifies_signed_event_id_and_signature() {
let event = signed_event();
@@ -111,8 +129,13 @@ mod tests {
#[test]
fn reports_id_mismatch_before_signature_checks() {
- let mut event = signed_event();
- event.content = "tampered".to_owned();
+ let original = signed_event();
+ let event = envelope_with(
+ &original,
+ "tampered".to_owned(),
+ original.kind_u32(),
+ original.sig_str().to_owned(),
+ );
assert_eq!(
radroots_nostr_verify_event(&event),
@@ -122,9 +145,16 @@ mod tests {
#[test]
fn reports_signature_invalid_for_valid_id_with_wrong_signature() {
- let mut event = signed_event();
- let replacement = if event.sig.starts_with('0') { "1" } else { "0" };
- event.sig.replace_range(0..1, replacement);
+ let original = signed_event();
+ let mut sig = original.sig_str().to_owned();
+ let replacement = if sig.starts_with('0') { "1" } else { "0" };
+ sig.replace_range(0..1, replacement);
+ let event = envelope_with(
+ &original,
+ original.content().to_owned(),
+ original.kind_u32(),
+ sig,
+ );
assert_eq!(
radroots_nostr_verify_event(&event),
@@ -134,8 +164,13 @@ mod tests {
#[test]
fn reports_malformed_envelope_for_unparseable_wire_fields() {
- let mut event = signed_event();
- event.kind = u32::from(u16::MAX) + 1;
+ let original = signed_event();
+ let event = envelope_with(
+ &original,
+ original.content().to_owned(),
+ u32::from(u16::MAX) + 1,
+ original.sig_str().to_owned(),
+ );
assert_eq!(
radroots_nostr_verify_event(&event),
diff --git a/crates/nostr/src/job_adapter.rs b/crates/nostr/src/job_adapter.rs
@@ -60,8 +60,8 @@ impl JobEventLike for RadrootsNostrEventAdapter<'_> {
fn raw_author(&self) -> String {
self.author_hex.clone()
}
- fn raw_published_at(&self) -> u32 {
- self.evt.created_at.as_secs() as u32
+ fn raw_published_at(&self) -> u64 {
+ self.evt.created_at.as_secs()
}
fn raw_kind(&self) -> u32 {
match self.evt.kind {
diff --git a/crates/nostr/src/nip17.rs b/crates/nostr/src/nip17.rs
@@ -13,15 +13,13 @@ use thiserror::Error;
use radroots_event::kinds::{KIND_MESSAGE, KIND_MESSAGE_FILE};
use radroots_event::message::RadrootsMessage;
use radroots_event::message_file::RadrootsMessageFile;
+use radroots_event::wire::RadrootsNip01EventWireParts;
use radroots_event_codec::error::{EventEncodeError, EventParseError};
use radroots_event_codec::message::decode as message_decode;
use radroots_event_codec::message::encode as message_encode;
use radroots_event_codec::message_file::decode as message_file_decode;
use radroots_event_codec::message_file::encode as message_file_encode;
use radroots_event_codec::parsed::RadrootsParsedData;
-use radroots_event_codec::wire::WireEventParts;
-
-use crate::util::created_at_u32_saturating;
#[derive(Debug, Error)]
pub enum RadrootsNip17Error {
@@ -50,7 +48,7 @@ pub enum RadrootsNip17Rumor {
#[derive(Clone, Debug)]
pub struct RadrootsNip17WrapOptions {
pub include_sender: bool,
- pub rumor_created_at: Option<u32>,
+ pub rumor_created_at: Option<u64>,
pub gift_wrap_tags: Vec<Vec<String>>,
}
@@ -78,13 +76,13 @@ fn tags_from_slices(tag_slices: &[Vec<String>]) -> Vec<Tag> {
}
fn rumor_from_parts(
- parts: WireEventParts,
+ parts: RadrootsNip01EventWireParts,
author: PublicKey,
- created_at: Option<u32>,
+ created_at: Option<u64>,
) -> UnsignedEvent {
let tags = tags_from_slices(&parts.tags);
let timestamp = match created_at {
- Some(ts) => Timestamp::from_secs(ts as u64),
+ Some(ts) => Timestamp::from_secs(ts),
None => Timestamp::now(),
};
let mut rumor = UnsignedEvent::new(
@@ -180,7 +178,7 @@ where
let mut rumor = unwrapped.rumor;
let id = rumor.id().to_string();
let author = rumor.pubkey.to_string();
- let published_at = created_at_u32_saturating(rumor.created_at);
+ let published_at = rumor.created_at.as_secs();
let kind = rumor.kind.as_u16() as u32;
let tags: Vec<Vec<String>> = rumor
.tags
diff --git a/crates/outbox/src/error.rs b/crates/outbox/src/error.rs
@@ -17,6 +17,12 @@ pub enum RadrootsOutboxError {
#[error("Signed event does not match frozen draft: {0}")]
SignedEventDraftMismatch(#[from] radroots_event::draft::RadrootsDraftError),
+ #[error("Event wire error: {0}")]
+ EventWire(#[from] radroots_event::wire::RadrootsEventWireError),
+
+ #[error("Signed event error: {0}")]
+ SignedEvent(#[from] radroots_event::draft::RadrootsSignedEventError),
+
#[error("delivery targets cannot be empty")]
EmptyDeliveryTargets,
@@ -65,6 +71,12 @@ pub enum RadrootsOutboxError {
#[error("Signed event missing for outbox event {0}")]
MissingSignedEvent(i64),
+ #[error("Stored signed event JSON missing raw event JSON for outbox event {0}")]
+ StoredSignedEventMissingRawJson(i64),
+
+ #[error("Stored raw event JSON missing signed event JSON for outbox event {0}")]
+ StoredRawEventMissingSignedEvent(i64),
+
#[error("Signed event ID mismatch: expected {expected_event_id}, got {actual_event_id}")]
SignedEventIdMismatch {
expected_event_id: String,
diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs
@@ -13,10 +13,10 @@ use crate::model::{
RadrootsOutboxReticulumPreviewBehavior, RadrootsOutboxReticulumPreviewEventRecord,
RadrootsOutboxSignedOperationInput, RadrootsOutboxStatusSummary,
};
-use radroots_event::RadrootsEventEnvelope;
use radroots_event::draft::{
RadrootsEventDraft, RadrootsSignedEvent, validate_signed_nostr_event_matches_draft,
};
+use radroots_event::wire::RadrootsNip01EventWire;
use radroots_event_store::{
RadrootsEventIngest, RadrootsEventStore, RadrootsTransportObservation,
RadrootsTransportObservationType,
@@ -134,10 +134,11 @@ impl RadrootsOutbox {
input: &RadrootsOutboxSignedOperationInput,
) -> Result<RadrootsOutboxIdempotencyPreflight, RadrootsOutboxError> {
validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?;
- let prepared = prepare_delivery_plan(&input.draft.expected_event_id, &input.delivery_plan)?;
+ let prepared =
+ prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?;
let operation_digest = operation_idempotency_digest(
input.operation_kind.as_str(),
- input.draft.expected_pubkey.as_str(),
+ input.draft.expected_pubkey_str(),
&input.draft,
);
@@ -145,7 +146,7 @@ impl RadrootsOutbox {
&& let Some(existing) = existing_idempotent_operation_for_pool(
&self.pool,
input.operation_kind.as_str(),
- input.draft.expected_pubkey.as_str(),
+ input.draft.expected_pubkey_str(),
idempotency_key,
)
.await?
@@ -153,7 +154,7 @@ impl RadrootsOutbox {
{
return Err(RadrootsOutboxError::IdempotencyConflict {
operation_kind: input.operation_kind.clone(),
- expected_pubkey: input.draft.expected_pubkey.clone(),
+ expected_pubkey: input.draft.expected_pubkey_str().to_owned(),
idempotency_key: idempotency_key.to_owned(),
existing_digest: existing.operation_idempotency_digest,
new_digest: operation_digest,
@@ -170,10 +171,11 @@ impl RadrootsOutbox {
&self,
input: RadrootsOutboxOperationInput,
) -> Result<RadrootsOutboxEnqueueReceipt, RadrootsOutboxError> {
- let prepared = prepare_delivery_plan(&input.draft.expected_event_id, &input.delivery_plan)?;
+ let prepared =
+ prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?;
let operation_digest = operation_idempotency_digest(
input.operation_kind.as_str(),
- input.draft.expected_pubkey.as_str(),
+ input.draft.expected_pubkey_str(),
&input.draft,
);
let mut tx = self.pool.begin().await?;
@@ -182,7 +184,7 @@ impl RadrootsOutbox {
&& let Some(existing) = existing_idempotent_operation(
&mut tx,
input.operation_kind.as_str(),
- input.draft.expected_pubkey.as_str(),
+ input.draft.expected_pubkey_str(),
idempotency_key,
)
.await?
@@ -190,7 +192,7 @@ impl RadrootsOutbox {
if existing.operation_idempotency_digest != operation_digest {
return Err(RadrootsOutboxError::IdempotencyConflict {
operation_kind: input.operation_kind,
- expected_pubkey: input.draft.expected_pubkey,
+ expected_pubkey: input.draft.expected_pubkey_str().to_owned(),
idempotency_key: idempotency_key.to_owned(),
existing_digest: existing.operation_idempotency_digest,
new_digest: operation_digest,
@@ -223,7 +225,7 @@ impl RadrootsOutbox {
"INSERT INTO outbox_operations(operation_kind, expected_pubkey, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)",
)
.bind(input.operation_kind.as_str())
- .bind(input.draft.expected_pubkey.as_str())
+ .bind(input.draft.expected_pubkey_str())
.bind(input.idempotency_key.as_deref())
.bind(operation_digest.as_str())
.bind(RadrootsOutboxOperationStatus::Queued.as_str())
@@ -237,8 +239,8 @@ impl RadrootsOutbox {
"INSERT INTO outbox_event(operation_id, event_id, expected_pubkey, draft_json, state, attempt_count, next_attempt_after_ms, event_store_ingested, event_store_inserted, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, 0, ?, 0, 0, ?, ?)",
)
.bind(operation_id)
- .bind(input.draft.expected_event_id.as_str())
- .bind(input.draft.expected_pubkey.as_str())
+ .bind(input.draft.expected_event_id_str())
+ .bind(input.draft.expected_pubkey_str())
.bind(draft_json.as_str())
.bind(RadrootsOutboxEventState::DraftQueued.as_str())
.bind(input.created_at_ms)
@@ -256,7 +258,7 @@ impl RadrootsOutbox {
operation_id,
outbox_event_id,
delivery_plan_id: plan.delivery_plan_id,
- expected_event_id: input.draft.expected_event_id,
+ expected_event_id: input.draft.expected_event_id_str().to_owned(),
operation_idempotency_digest: operation_digest,
delivery_plan_idempotency_digest: prepared.delivery_plan_idempotency_digest,
})
@@ -267,10 +269,11 @@ impl RadrootsOutbox {
input: RadrootsOutboxSignedOperationInput,
) -> Result<RadrootsOutboxEnqueueReceipt, RadrootsOutboxError> {
validate_signed_nostr_event_matches_draft(&input.signed_event, &input.draft)?;
- let prepared = prepare_delivery_plan(&input.draft.expected_event_id, &input.delivery_plan)?;
+ let prepared =
+ prepare_delivery_plan(input.draft.expected_event_id_str(), &input.delivery_plan)?;
let operation_digest = operation_idempotency_digest(
input.operation_kind.as_str(),
- input.draft.expected_pubkey.as_str(),
+ input.draft.expected_pubkey_str(),
&input.draft,
);
let mut tx = self.pool.begin().await?;
@@ -279,7 +282,7 @@ impl RadrootsOutbox {
&& let Some(existing) = existing_idempotent_operation(
&mut tx,
input.operation_kind.as_str(),
- input.draft.expected_pubkey.as_str(),
+ input.draft.expected_pubkey_str(),
idempotency_key,
)
.await?
@@ -287,7 +290,7 @@ impl RadrootsOutbox {
if existing.operation_idempotency_digest != operation_digest {
return Err(RadrootsOutboxError::IdempotencyConflict {
operation_kind: input.operation_kind,
- expected_pubkey: input.draft.expected_pubkey,
+ expected_pubkey: input.draft.expected_pubkey_str().to_owned(),
idempotency_key: idempotency_key.to_owned(),
existing_digest: existing.operation_idempotency_digest,
new_digest: operation_digest,
@@ -326,7 +329,7 @@ impl RadrootsOutbox {
"INSERT INTO outbox_operations(operation_kind, expected_pubkey, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)",
)
.bind(input.operation_kind.as_str())
- .bind(input.draft.expected_pubkey.as_str())
+ .bind(input.draft.expected_pubkey_str())
.bind(input.idempotency_key.as_deref())
.bind(operation_digest.as_str())
.bind(RadrootsOutboxOperationStatus::Queued.as_str())
@@ -336,16 +339,16 @@ impl RadrootsOutbox {
.await?;
let operation_id = operation.last_insert_rowid();
let draft_json = serde_json::to_string(&input.draft)?;
- let signed_event_json = serde_json::to_string(&input.signed_event)?;
+ let signed_event_json = signed_event_wire_json(&input.signed_event)?;
let event = sqlx::query(
"INSERT INTO outbox_event(operation_id, event_id, expected_pubkey, draft_json, signed_event_json, raw_event_json, state, attempt_count, next_attempt_after_ms, event_store_ingested, event_store_inserted, event_store_ingested_at_ms, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, 1, ?, ?, ?, ?)",
)
.bind(operation_id)
- .bind(input.draft.expected_event_id.as_str())
- .bind(input.draft.expected_pubkey.as_str())
+ .bind(input.draft.expected_event_id_str())
+ .bind(input.draft.expected_pubkey_str())
.bind(draft_json.as_str())
.bind(signed_event_json.as_str())
- .bind(input.signed_event.raw_json.as_str())
+ .bind(input.signed_event.raw_json())
.bind(RadrootsOutboxEventState::Signed.as_str())
.bind(input.created_at_ms)
.bind(bool_i64(input.event_store_inserted))
@@ -365,7 +368,7 @@ impl RadrootsOutbox {
operation_id,
outbox_event_id,
delivery_plan_id: plan.delivery_plan_id,
- expected_event_id: input.draft.expected_event_id,
+ expected_event_id: input.draft.expected_event_id_str().to_owned(),
operation_idempotency_digest: operation_digest,
delivery_plan_idempotency_digest: prepared.delivery_plan_idempotency_digest,
})
@@ -622,18 +625,18 @@ impl RadrootsOutbox {
if stored != Some(claim_token) {
return Err(RadrootsOutboxError::ClaimTokenMismatch { outbox_event_id });
}
- if signed_event.id != record.event_id {
+ if signed_event.id_str() != record.event_id {
return Err(RadrootsOutboxError::SignedEventIdMismatch {
expected_event_id: record.event_id,
- actual_event_id: signed_event.id,
+ actual_event_id: signed_event.id_str().to_owned(),
});
}
- let signed_event_json = serde_json::to_string(&signed_event)?;
+ let signed_event_json = signed_event_wire_json(&signed_event)?;
let changed = sqlx::query(
"UPDATE outbox_event SET signed_event_json = ?, raw_event_json = ?, state = ?, claim_token = NULL, claim_owner = NULL, claim_expires_at_ms = NULL, active_delivery_plan_id = NULL, last_error = NULL, updated_at_ms = ? WHERE outbox_event_id = ? AND claim_token = ?",
)
.bind(signed_event_json.as_str())
- .bind(signed_event.raw_json.as_str())
+ .bind(signed_event.raw_json())
.bind(RadrootsOutboxEventState::Signed.as_str())
.bind(now_ms)
.bind(outbox_event_id)
@@ -724,15 +727,13 @@ impl RadrootsOutbox {
let signed_event = record
.signed_event
.ok_or(RadrootsOutboxError::MissingSignedEvent(outbox_event_id))?;
- let event = event_from_signed(&signed_event);
let observation = RadrootsTransportObservation::new(
RadrootsTransportKind::Local,
"local:outbox",
RadrootsTransportObservationType::LocalImport,
observed_at_ms,
)?;
- let ingest = RadrootsEventIngest::new(event, observed_at_ms)
- .with_raw_json(signed_event.raw_json.clone())
+ let ingest = RadrootsEventIngest::new(signed_event.clone(), observed_at_ms)
.with_observation(observation);
let receipt = event_store.ingest_event(ingest).await?;
let changed = sqlx::query(
@@ -1794,12 +1795,12 @@ async fn ensure_event_signed(
event_store_inserted: bool,
event_store_ingested_at_ms: i64,
) -> Result<(), RadrootsOutboxError> {
- let signed_event_json = serde_json::to_string(signed_event)?;
+ let signed_event_json = signed_event_wire_json(signed_event)?;
sqlx::query(
"UPDATE outbox_event SET signed_event_json = ?, raw_event_json = ?, state = CASE WHEN state IN ('draft_queued', 'sign_retryable', 'signing') THEN ? ELSE state END, event_store_ingested = 1, event_store_inserted = ?, event_store_ingested_at_ms = ? WHERE outbox_event_id = ? AND signed_event_json IS NULL",
)
.bind(signed_event_json.as_str())
- .bind(signed_event.raw_json.as_str())
+ .bind(signed_event.raw_json())
.bind(RadrootsOutboxEventState::Signed.as_str())
.bind(bool_i64(event_store_inserted))
.bind(event_store_ingested_at_ms)
@@ -2226,17 +2227,28 @@ fn operation_from_row(
fn event_from_row(
row: sqlx::sqlite::SqliteRow,
) -> Result<RadrootsOutboxEventRecord, RadrootsOutboxError> {
+ let outbox_event_id = row.try_get("outbox_event_id")?;
+ let event_id = row.try_get::<String, _>("event_id")?;
let draft: RadrootsEventDraft =
serde_json::from_str(row.try_get::<String, _>("draft_json")?.as_str())?;
- let signed_event = row
- .try_get::<Option<String>, _>("signed_event_json")?
- .map(|json| serde_json::from_str(json.as_str()))
- .transpose()?;
+ let signed_event = signed_event_from_storage(
+ outbox_event_id,
+ row.try_get("signed_event_json")?,
+ row.try_get("raw_event_json")?,
+ )?;
+ if let Some(signed_event) = signed_event.as_ref()
+ && signed_event.id_str() != event_id
+ {
+ return Err(RadrootsOutboxError::SignedEventIdMismatch {
+ expected_event_id: event_id,
+ actual_event_id: signed_event.id_str().to_owned(),
+ });
+ }
let state = RadrootsOutboxEventState::parse(row.try_get::<String, _>("state")?.as_str())?;
Ok(RadrootsOutboxEventRecord {
- outbox_event_id: row.try_get("outbox_event_id")?,
+ outbox_event_id,
operation_id: row.try_get("operation_id")?,
- event_id: row.try_get("event_id")?,
+ event_id,
expected_pubkey: row.try_get("expected_pubkey")?,
draft,
signed_event,
@@ -2257,6 +2269,34 @@ fn event_from_row(
})
}
+fn signed_event_from_storage(
+ outbox_event_id: i64,
+ signed_event_json: Option<String>,
+ raw_event_json: Option<String>,
+) -> Result<Option<RadrootsSignedEvent>, RadrootsOutboxError> {
+ match (signed_event_json, raw_event_json) {
+ (None, None) => Ok(None),
+ (Some(_), None) => Err(RadrootsOutboxError::StoredSignedEventMissingRawJson(
+ outbox_event_id,
+ )),
+ (None, Some(_)) => Err(RadrootsOutboxError::StoredRawEventMissingSignedEvent(
+ outbox_event_id,
+ )),
+ (Some(signed_json), Some(raw_json)) => {
+ let wire = RadrootsNip01EventWire::parse_json(signed_json.as_str())?;
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json)
+ .map(Some)
+ .map_err(Into::into)
+ }
+ }
+}
+
+fn signed_event_wire_json(
+ signed_event: &RadrootsSignedEvent,
+) -> Result<String, RadrootsOutboxError> {
+ serde_json::to_string(signed_event.wire()).map_err(Into::into)
+}
+
fn delivery_plan_from_row(
row: sqlx::sqlite::SqliteRow,
) -> Result<RadrootsOutboxDeliveryPlanRecord, RadrootsOutboxError> {
@@ -2427,18 +2467,6 @@ fn parse_transport_outcome_kind(
}
}
-fn event_from_signed(signed_event: &RadrootsSignedEvent) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
- id: signed_event.id.clone(),
- author: signed_event.pubkey.clone(),
- created_at: signed_event.created_at,
- kind: signed_event.kind,
- tags: signed_event.tags.clone(),
- content: signed_event.content.clone(),
- sig: signed_event.sig.clone(),
- }
-}
-
#[derive(Serialize)]
struct OperationDigestInput<'a> {
operation_kind: &'a str,
@@ -3253,7 +3281,7 @@ mod tests {
assert_ne!(scoped.fingerprint, rescaled.fingerprint);
let first_prepared = prepare_delivery_plan(
- draft.expected_event_id.as_str(),
+ draft.expected_event_id_str(),
&RadrootsOutboxDeliveryPlanInput::new(
"transport.nostr.local",
1,
@@ -3263,7 +3291,7 @@ mod tests {
)
.expect("first plan");
let relabeled_prepared = prepare_delivery_plan(
- draft.expected_event_id.as_str(),
+ draft.expected_event_id_str(),
&RadrootsOutboxDeliveryPlanInput::new(
"transport.nostr.local",
1,
@@ -3273,7 +3301,7 @@ mod tests {
)
.expect("relabeled plan");
let rescaled_prepared = prepare_delivery_plan(
- draft.expected_event_id.as_str(),
+ draft.expected_event_id_str(),
&RadrootsOutboxDeliveryPlanInput::new(
"transport.nostr.local",
1,
@@ -5323,7 +5351,7 @@ mod tests {
.ingest_signed_event_local(&event_store, receipt.outbox_event_id, "claim-b", 2_200)
.await
.expect("first ingest");
- assert_eq!(first.event_id, signed.id);
+ assert_eq!(first.event_id, signed.id_str());
assert!(!first.already_ingested);
let second = outbox
@@ -5332,7 +5360,7 @@ mod tests {
.expect("second ingest");
assert!(second.already_ingested);
let observations = event_store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_eq!(observations.len(), 1);
diff --git a/crates/trade/src/dvm.rs b/crates/trade/src/dvm.rs
@@ -401,15 +401,16 @@ pub fn build_transition_proof_request_tags(
pub fn parse_transition_proof_request_event(
event: &RadrootsEventEnvelope,
) -> Result<RadrootsTradeTransitionProofRequestEnvelope, RadrootsTradeDvmError> {
- if event.kind != KIND_TRADE_TRANSITION_PROOF_REQUEST {
+ if event.kind_u32() != KIND_TRADE_TRANSITION_PROOF_REQUEST {
return Err(RadrootsTradeDvmError::UnsupportedKind {
expected: KIND_TRADE_TRANSITION_PROOF_REQUEST,
- actual: event.kind,
+ actual: event.kind_u32(),
});
}
- let tags = parse_transition_proof_request_tags(&event.tags)?;
+ let event_tags = event.tags_as_vec();
+ let tags = parse_transition_proof_request_tags(&event_tags)?;
let content: RadrootsTradeTransitionProofRequestV1 =
- serde_json::from_str(&event.content).map_err(RadrootsTradeDvmError::InvalidContent)?;
+ serde_json::from_str(event.content()).map_err(RadrootsTradeDvmError::InvalidContent)?;
validate_transition_proof_request_binding(&tags, &content)?;
validate_transition_proof_request_content(&content)?;
Ok(RadrootsTradeTransitionProofRequestEnvelope { tags, content })
@@ -437,13 +438,7 @@ pub fn build_transition_proof_result_tags(
inputs: &[RadrootsTradeDvmInputTag],
binding: &RadrootsTradeTransitionProofResultBinding,
) -> Result<Vec<Vec<String>>, RadrootsTradeDvmError> {
- let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).map_err(|source| {
- RadrootsTradeDvmError::InvalidTag {
- tag: TAG_E,
- value: request_event.id.clone(),
- source,
- }
- })?;
+ let request_event_id = request_event.id().clone();
let request_json = serialize_request_event(request_event)?;
let mut tags = vec![
vec![RADROOTS_DVM_TAG_REQUEST.to_string(), request_json],
@@ -514,13 +509,13 @@ pub fn parse_transition_proof_result_tags(
let request_event_json = required_tag_value(tags, RADROOTS_DVM_TAG_REQUEST)?;
let request_event: RadrootsEventEnvelope = serde_json::from_str(request_event_json)
.map_err(RadrootsTradeDvmError::InvalidRequestEvent)?;
- if request_event.kind != KIND_TRADE_TRANSITION_PROOF_REQUEST {
+ if request_event.kind_u32() != KIND_TRADE_TRANSITION_PROOF_REQUEST {
return Err(RadrootsTradeDvmError::RequestEventKind {
- kind: request_event.kind,
+ kind: request_event.kind_u32(),
});
}
let request_event_id = parse_event_id_tag(TAG_E, required_tag_value(tags, TAG_E)?)?;
- if request_event.id != request_event_id.as_str() {
+ if request_event.id() != &request_event_id {
return Err(RadrootsTradeDvmError::RequestEventIdMismatch);
}
let customer_pubkey = parse_pubkey_tag(TAG_P, required_tag_value(tags, TAG_P)?)?;
@@ -860,15 +855,66 @@ mod tests {
}
fn request_event(content: &RadrootsTradeTransitionProofRequestV1) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
- id: event_id(10).into_string(),
+ request_event_with_parts(
+ event_id(10),
+ KIND_TRADE_TRANSITION_PROOF_REQUEST,
+ build_transition_proof_request_tags(&public_key(WORKER), content),
+ serde_json::to_string(content).expect("content"),
+ )
+ }
+
+ fn request_event_with_parts(
+ id: RadrootsEventId,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ ) -> RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(radroots_event::RadrootsEventEnvelopeParts {
+ id: id.into_string(),
author: BUYER.to_string(),
created_at: 1,
- kind: KIND_TRADE_TRANSITION_PROOF_REQUEST,
- tags: build_transition_proof_request_tags(&public_key(WORKER), content),
- content: serde_json::to_string(content).expect("content"),
- sig: "sig".to_string(),
- }
+ kind,
+ tags,
+ content,
+ sig: "f".repeat(128),
+ })
+ .expect("request event")
+ }
+
+ fn request_event_with_tags(
+ content: &RadrootsTradeTransitionProofRequestV1,
+ tags: Vec<Vec<String>>,
+ ) -> RadrootsEventEnvelope {
+ request_event_with_parts(
+ event_id(10),
+ KIND_TRADE_TRANSITION_PROOF_REQUEST,
+ tags,
+ serde_json::to_string(content).expect("content"),
+ )
+ }
+
+ fn request_event_with_kind(
+ content: &RadrootsTradeTransitionProofRequestV1,
+ kind: u32,
+ ) -> RadrootsEventEnvelope {
+ request_event_with_parts(
+ event_id(10),
+ kind,
+ build_transition_proof_request_tags(&public_key(WORKER), content),
+ serde_json::to_string(content).expect("content"),
+ )
+ }
+
+ fn request_event_with_content(
+ content: &RadrootsTradeTransitionProofRequestV1,
+ event_content: String,
+ ) -> RadrootsEventEnvelope {
+ request_event_with_parts(
+ event_id(10),
+ KIND_TRADE_TRANSITION_PROOF_REQUEST,
+ build_transition_proof_request_tags(&public_key(WORKER), content),
+ event_content,
+ )
}
#[test]
@@ -887,8 +933,8 @@ mod tests {
fn transition_proof_result_tags_bind_stringified_request() {
let content = request_content();
let request_event = request_event(&content);
- let request_tags =
- parse_transition_proof_request_tags(&request_event.tags).expect("request tags");
+ let request_tags = parse_transition_proof_request_tags(&request_event.tags_as_vec())
+ .expect("request tags");
let binding = RadrootsTradeTransitionProofResultBinding {
listing_event_id: content.listing_event_id.clone(),
root_event_id: content.request_event_id.clone(),
@@ -937,10 +983,9 @@ mod tests {
#[test]
fn transition_proof_request_rejects_missing_input_tags() {
let content = request_content();
- let mut event = request_event(&content);
- event
- .tags
- .retain(|tag| tag.first().map(String::as_str) != Some(TAG_I));
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.retain(|tag| tag.first().map(String::as_str) != Some(TAG_I));
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
@@ -1013,8 +1058,7 @@ mod tests {
#[test]
fn transition_proof_request_parser_rejects_kind_tag_content_and_hash_edges() {
let content = request_content();
- let mut event = request_event(&content);
- event.kind = KIND_PROFILE;
+ let event = request_event_with_kind(&content, KIND_PROFILE);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::UnsupportedKind {
@@ -1023,41 +1067,43 @@ mod tests {
})
));
- let mut event = request_event(&content);
- event.content = "{".to_string();
+ let event = request_event_with_content(&content, "{".to_string());
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::InvalidContent(_))
));
- let mut event = request_event(&content);
- event.tags.iter_mut().for_each(|tag| {
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.iter_mut().for_each(|tag| {
if tag.first().map(String::as_str) == Some(TAG_P) {
tag[1] = "bad".to_string();
}
});
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_P, .. })
));
- let mut event = request_event(&content);
- event.tags.iter_mut().for_each(|tag| {
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.iter_mut().for_each(|tag| {
if tag.first().map(String::as_str) == Some(TAG_A) {
tag[1] = "bad".to_string();
}
});
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_A, .. })
));
- let mut event = request_event(&content);
- event.tags.iter_mut().for_each(|tag| {
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.iter_mut().for_each(|tag| {
if tag.first().map(String::as_str) == Some(TAG_A) {
tag[1] = format!("{KIND_LISTING}:{SELLER}:BBBBBBBBBBBBBBBBBBBBBg");
}
});
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::ContentMismatch {
@@ -1065,8 +1111,8 @@ mod tests {
})
));
- let mut event = request_event(&content);
- event.tags.iter_mut().for_each(|tag| {
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.iter_mut().for_each(|tag| {
if tag.first().map(String::as_str) == Some(TAG_I)
&& tag.get(3).map(String::as_str)
== Some(RadrootsTradeDvmInputRole::Listing.as_str())
@@ -1074,6 +1120,7 @@ mod tests {
tag[1] = event_id(99).into_string();
}
});
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::ContentMismatch {
@@ -1142,43 +1189,47 @@ mod tests {
));
}
- let mut event = request_event(&content);
- event.tags.push(vec![TAG_I.to_string()]);
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.push(vec![TAG_I.to_string()]);
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::MissingTag { tag: TAG_I })
));
- let mut event = request_event(&content);
- event.tags.push(vec![
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.push(vec![
TAG_I.to_string(),
event_id(44).into_string(),
"url".to_string(),
RadrootsTradeDvmInputRole::Listing.as_str().to_string(),
]);
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_I, .. })
));
- let mut event = request_event(&content);
- event.tags.push(vec![
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.push(vec![
TAG_I.to_string(),
event_id(44).into_string(),
RADROOTS_DVM_INPUT_TYPE_EVENT.to_string(),
]);
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::MissingTag { tag: TAG_I })
));
- let mut event = request_event(&content);
- event.tags.push(vec![
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.push(vec![
TAG_I.to_string(),
event_id(44).into_string(),
RADROOTS_DVM_INPUT_TYPE_EVENT.to_string(),
"radroots:legacy_event".to_string(),
]);
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::InvalidInputRole { .. })
@@ -1214,27 +1265,29 @@ mod tests {
event_id(97),
),
] {
- let mut event = request_event(&content);
- event.tags.iter_mut().for_each(|tag| {
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.iter_mut().for_each(|tag| {
if tag.first().map(String::as_str) == Some(TAG_I)
&& tag.get(3).map(String::as_str) == Some(role.as_str())
{
tag[1] = event_id.clone().into_string();
}
});
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::ContentMismatch { field: got }) if got == field
));
}
- let mut event = request_event(&content);
- event.tags.push(vec![
+ let mut tags = request_event(&content).tags_as_vec();
+ tags.push(vec![
TAG_I.to_string(),
"bad".to_string(),
RADROOTS_DVM_INPUT_TYPE_EVENT.to_string(),
RadrootsTradeDvmInputRole::Listing.as_str().to_string(),
]);
+ let event = request_event_with_tags(&content, tags);
assert!(matches!(
parse_transition_proof_request_event(&event),
Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_I, .. })
@@ -1254,8 +1307,8 @@ mod tests {
fn transition_proof_result_and_feedback_parsers_cover_error_edges() {
let content = request_content();
let request_event = request_event(&content);
- let request_tags =
- parse_transition_proof_request_tags(&request_event.tags).expect("request tags");
+ let request_tags = parse_transition_proof_request_tags(&request_event.tags_as_vec())
+ .expect("request tags");
let binding = RadrootsTradeTransitionProofResultBinding {
listing_event_id: content.listing_event_id.clone(),
root_event_id: content.request_event_id.clone(),
@@ -1300,18 +1353,6 @@ mod tests {
})
));
- let mut bad_request_event = request_event.clone();
- bad_request_event.id = "bad".to_string();
- assert!(matches!(
- build_transition_proof_result_tags(
- &bad_request_event,
- &public_key(BUYER),
- &request_tags.inputs,
- &binding,
- ),
- Err(RadrootsTradeDvmError::InvalidTag { tag: TAG_E, .. })
- ));
-
let mut invalid_json = tags.clone();
invalid_json[0][1] = "{".to_string();
assert!(matches!(
@@ -1319,8 +1360,7 @@ mod tests {
Err(RadrootsTradeDvmError::InvalidRequestEvent(_))
));
- let mut wrong_kind_request = request_event.clone();
- wrong_kind_request.kind = KIND_PROFILE;
+ let wrong_kind_request = request_event_with_kind(&content, KIND_PROFILE);
let mut wrong_kind = tags.clone();
wrong_kind[0][1] = serde_json::to_string(&wrong_kind_request).unwrap();
assert!(matches!(
diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs
@@ -129,10 +129,11 @@ fn ensure_public_listing_kind(kind: u32) -> Result<(), RadrootsPublicListingAddr
pub fn parse_listing_event(
event: &RadrootsEventEnvelope,
) -> Result<RadrootsListing, ListingParseError> {
- if !is_listing_kind(event.kind) {
- return Err(ListingParseError::InvalidKind(event.kind));
+ if !is_listing_kind(event.kind_u32()) {
+ return Err(ListingParseError::InvalidKind(event.kind_u32()));
}
- self::codec::listing_from_event_parts(&event.tags, &event.content)
+ let tags = event.tags_as_vec();
+ self::codec::listing_from_event_parts(&tags, event.content())
}
#[cfg(test)]
@@ -143,7 +144,7 @@ mod tests {
parse_public_listing_address,
};
use radroots_event::{
- RadrootsEventEnvelope,
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts,
ids::RadrootsListingAddress,
kinds::{KIND_LISTING, KIND_LISTING_DRAFT, KIND_PROFILE},
order::RadrootsListingParseError,
@@ -152,9 +153,9 @@ mod tests {
const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
fn listing_event() -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
- id: "event-1".into(),
- author: SELLER.into(),
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
+ id: "9".repeat(64),
+ author: SELLER.to_string(),
created_at: 1,
kind: KIND_LISTING,
tags: vec![
@@ -182,21 +183,23 @@ mod tests {
],
],
content: String::new(),
- sig: String::new(),
- }
+ sig: "f".repeat(128),
+ })
+ .expect("listing event")
}
#[test]
fn parse_listing_event_rejects_non_listing_kind() {
- let event = RadrootsEventEnvelope {
- id: "event-1".into(),
- author: "seller".into(),
+ let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
+ id: "8".repeat(64),
+ author: SELLER.to_string(),
created_at: 1,
kind: KIND_PROFILE,
tags: vec![],
content: String::new(),
- sig: String::new(),
- };
+ sig: "f".repeat(128),
+ })
+ .expect("profile event");
assert!(matches!(
parse_listing_event(&event),
diff --git a/crates/trade/src/listing/mutation.rs b/crates/trade/src/listing/mutation.rs
@@ -21,7 +21,7 @@ use radroots_event::{
kinds::{KIND_LISTING, KIND_LISTING_DRAFT},
};
#[cfg(feature = "serde_json")]
-use radroots_event_codec::{listing::encode::to_json_wire_parts_with_kind, wire::to_frozen_draft};
+use radroots_event_codec::listing::encode::to_json_wire_parts_with_kind;
use thiserror::Error;
use crate::listing::draft::RadrootsCanonicalListingDraft;
@@ -120,7 +120,7 @@ impl RadrootsListingMutation {
#[cfg(feature = "serde_json")]
pub fn build_listing_mutation_draft(
mutation: &RadrootsListingMutation,
- created_at: u32,
+ created_at: u64,
) -> Result<RadrootsEventDraft, RadrootsListingMutationError> {
let (draft, kind, contract_id) = match mutation {
RadrootsListingMutation::Publish { draft } | RadrootsListingMutation::Update { draft } => {
@@ -135,11 +135,13 @@ pub fn build_listing_mutation_draft(
};
let parts = to_json_wire_parts_with_kind(draft.listing(), kind)
.map_err(|error| RadrootsListingMutationError::EncodeListing(error.to_string()))?;
- to_frozen_draft(
- parts,
+ RadrootsEventDraft::new(
contract_id,
- draft.seller_pubkey().as_str(),
+ parts.kind,
created_at,
+ parts.tags,
+ parts.content,
+ draft.seller_pubkey().as_str(),
)
.map_err(RadrootsListingMutationError::FrozenDraft)
}
@@ -151,7 +153,7 @@ mod tests {
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
use radroots_event::{
- RadrootsEventEnvelope,
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts,
farm::RadrootsFarmRef,
ids::{RadrootsDTag, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsPublicKey},
kinds::{KIND_LISTING, KIND_LISTING_DRAFT},
@@ -364,16 +366,16 @@ mod tests {
let publish_draft = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft");
let update_draft = build_listing_mutation_draft(&update, 1_700_000_000).expect("draft");
- assert_eq!(publish_draft.kind, KIND_LISTING);
- assert_eq!(publish_draft.contract_id, LISTING_PUBLISHED_CONTRACT_ID);
- assert_eq!(publish_draft.expected_pubkey, SELLER);
- assert_eq!(publish_draft.created_at, 1_700_000_000);
+ assert_eq!(publish_draft.kind_u32(), KIND_LISTING);
+ assert_eq!(publish_draft.contract_id(), LISTING_PUBLISHED_CONTRACT_ID);
+ assert_eq!(publish_draft.expected_pubkey_str(), SELLER);
+ assert_eq!(publish_draft.created_at_u64(), 1_700_000_000);
let published_content: RadrootsListing =
- serde_json::from_str(&publish_draft.content).expect("listing json");
+ serde_json::from_str(publish_draft.content()).expect("listing json");
assert_eq!(published_content.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
- assert_eq!(update_draft.kind, KIND_LISTING);
- assert_eq!(update_draft.contract_id, LISTING_PUBLISHED_CONTRACT_ID);
- assert_eq!(update_draft.expected_pubkey, SELLER);
+ assert_eq!(update_draft.kind_u32(), KIND_LISTING);
+ assert_eq!(update_draft.contract_id(), LISTING_PUBLISHED_CONTRACT_ID);
+ assert_eq!(update_draft.expected_pubkey_str(), SELLER);
}
#[test]
@@ -382,12 +384,12 @@ mod tests {
let draft = build_listing_mutation_draft(&save_draft, 1_700_000_000).expect("draft");
- assert_eq!(draft.kind, KIND_LISTING_DRAFT);
- assert_eq!(draft.contract_id, LISTING_DRAFT_CONTRACT_ID);
- assert_eq!(draft.expected_pubkey, SELLER);
- assert_eq!(draft.created_at, 1_700_000_000);
+ assert_eq!(draft.kind_u32(), KIND_LISTING_DRAFT);
+ assert_eq!(draft.contract_id(), LISTING_DRAFT_CONTRACT_ID);
+ assert_eq!(draft.expected_pubkey_str(), SELLER);
+ assert_eq!(draft.created_at_u64(), 1_700_000_000);
let draft_content: RadrootsListing =
- serde_json::from_str(&draft.content).expect("listing json");
+ serde_json::from_str(draft.content()).expect("listing json");
assert_eq!(draft_content.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg");
}
@@ -435,10 +437,13 @@ mod tests {
let first = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft");
let second = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft");
- assert_eq!(first.expected_event_id, second.expected_event_id);
- assert_eq!(first.expected_event_id.len(), 64);
- assert_eq!(first.tags, second.tags);
- assert_eq!(first.content, second.content);
+ assert_eq!(
+ first.expected_event_id_str(),
+ second.expected_event_id_str()
+ );
+ assert_eq!(first.expected_event_id_str().len(), 64);
+ assert_eq!(first.tags_as_vec(), second.tags_as_vec());
+ assert_eq!(first.content(), second.content());
}
#[test]
@@ -446,15 +451,16 @@ mod tests {
let publish = RadrootsListingMutation::publish(canonical_draft());
let draft = build_listing_mutation_draft(&publish, 1_700_000_000).expect("draft");
- let event = RadrootsEventEnvelope {
- id: String::new(),
- author: draft.expected_pubkey.clone(),
- created_at: draft.created_at,
- kind: draft.kind,
- tags: draft.tags,
- content: draft.content,
- sig: String::new(),
- };
+ let event = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
+ id: draft.expected_event_id_str().to_owned(),
+ author: draft.expected_pubkey_str().to_owned(),
+ created_at: draft.created_at_u64(),
+ kind: draft.kind_u32(),
+ tags: draft.tags_as_vec(),
+ content: draft.content().to_owned(),
+ sig: "f".repeat(128),
+ })
+ .expect("listing event");
let validated = validate_listing_event(&event).expect("validated listing");
assert_eq!(validated.seller_pubkey, SELLER);
diff --git a/crates/trade/src/listing/validation.rs b/crates/trade/src/listing/validation.rs
@@ -44,19 +44,22 @@ pub struct RadrootsTradeListing {
pub fn validate_listing_event(
event: &RadrootsEventEnvelope,
) -> Result<RadrootsTradeListing, TradeListingValidationError> {
- if !is_listing_kind(event.kind) {
- return Err(TradeListingValidationError::InvalidKind { kind: event.kind });
+ if !is_listing_kind(event.kind_u32()) {
+ return Err(TradeListingValidationError::InvalidKind {
+ kind: event.kind_u32(),
+ });
}
- let listing = listing_from_event_parts(&event.tags, &event.content)
+ let tags = event.tags_as_vec();
+ let listing = listing_from_event_parts(&tags, event.content())
.map_err(|error| TradeListingValidationError::ParseError { error })?;
let listing_id = listing.d_tag.trim().to_string();
- let seller_pubkey = event.author.clone();
+ let seller_pubkey = event.author_str().to_owned();
if listing.farm.pubkey != seller_pubkey {
return Err(TradeListingValidationError::InvalidSeller);
}
- let listing_addr_raw = format!("{}:{}:{}", event.kind, seller_pubkey, listing_id);
+ let listing_addr_raw = format!("{}:{}:{}", event.kind_u32(), seller_pubkey, listing_id);
let listing_addr = RadrootsListingAddress::parse(&listing_addr_raw)
.map_err(|_| TradeListingValidationError::ParseError {
error: RadrootsListingParseError::InvalidTag("listing_addr".to_string()),
@@ -271,12 +274,10 @@ mod tests {
}
fn base_event(listing: &RadrootsListing) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
- id: "evt".into(),
- author: SELLER.into(),
- created_at: 0,
- kind: KIND_LISTING,
- tags: vec![
+ event_with_parts(
+ SELLER,
+ KIND_LISTING,
+ vec![
vec!["d".into(), listing.d_tag.to_string()],
vec!["p".into(), listing.farm.pubkey.clone()],
vec![
@@ -284,9 +285,26 @@ mod tests {
format!("30340:{}:{}", listing.farm.pubkey, listing.farm.d_tag),
],
],
- content: serde_json::to_string(listing).unwrap(),
- sig: "sig".into(),
- }
+ serde_json::to_string(listing).unwrap(),
+ )
+ }
+
+ fn event_with_parts(
+ author: &str,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ ) -> RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(radroots_event::RadrootsEventEnvelopeParts {
+ id: "9".repeat(64),
+ author: author.to_string(),
+ created_at: 0,
+ kind,
+ tags,
+ content,
+ sig: "f".repeat(128),
+ })
+ .expect("event")
}
fn assert_validation_err(listing: RadrootsListing, expected: TradeListingValidationError) {
@@ -305,8 +323,12 @@ mod tests {
#[test]
fn validate_draft_listing_ok() {
let listing = base_listing();
- let mut event = base_event(&listing);
- event.kind = KIND_LISTING_DRAFT;
+ let event = event_with_parts(
+ SELLER,
+ KIND_LISTING_DRAFT,
+ base_event(&listing).tags_as_vec(),
+ serde_json::to_string(&listing).unwrap(),
+ );
let validated = validate_listing_event(&event).expect("draft listing");
assert_eq!(
validated.listing_addr,
@@ -317,8 +339,12 @@ mod tests {
#[test]
fn validate_listing_rejects_missing_d_tag() {
let listing = base_listing();
- let mut event = base_event(&listing);
- event.tags.clear();
+ let event = event_with_parts(
+ SELLER,
+ KIND_LISTING,
+ Vec::new(),
+ serde_json::to_string(&listing).unwrap(),
+ );
let err = validate_listing_event(&event).unwrap_err();
assert_eq!(
err,
@@ -330,39 +356,42 @@ mod tests {
#[test]
fn validate_listing_rejects_invalid_currency() {
- let mut event = base_event(&base_listing());
- event.content = String::new();
- event.tags = vec![
- vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()],
- vec!["p".into(), SELLER.into()],
- vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")],
- vec!["key".into(), "coffee".into()],
- vec!["title".into(), "Coffee".into()],
- vec!["category".into(), "coffee".into()],
- vec!["summary".into(), "Single origin".into()],
- vec![
- "quantity".into(),
- "1".into(),
- "lb".into(),
- "bag".into(),
- "5".into(),
- ],
+ let event = event_with_parts(
+ SELLER,
+ KIND_LISTING,
vec![
- "price".into(),
- "20".into(),
- "US".into(),
- "1".into(),
- "lb".into(),
- ],
- vec![
- "location".into(),
- "Farm".into(),
- "Town".into(),
- "Region".into(),
+ vec!["d".into(), "AAAAAAAAAAAAAAAAAAAAAg".into()],
+ vec!["p".into(), SELLER.into()],
+ vec!["a".into(), format!("30340:{SELLER}:AAAAAAAAAAAAAAAAAAAAAA")],
+ vec!["key".into(), "coffee".into()],
+ vec!["title".into(), "Coffee".into()],
+ vec!["category".into(), "coffee".into()],
+ vec!["summary".into(), "Single origin".into()],
+ vec![
+ "quantity".into(),
+ "1".into(),
+ "lb".into(),
+ "bag".into(),
+ "5".into(),
+ ],
+ vec![
+ "price".into(),
+ "20".into(),
+ "US".into(),
+ "1".into(),
+ "lb".into(),
+ ],
+ vec![
+ "location".into(),
+ "Farm".into(),
+ "Town".into(),
+ "Region".into(),
+ ],
+ vec!["status".into(), "active".into()],
+ vec!["delivery".into(), "pickup".into()],
],
- vec!["status".into(), "active".into()],
- vec!["delivery".into(), "pickup".into()],
- ];
+ String::new(),
+ );
let err = validate_listing_event(&event).unwrap_err();
assert!(format!("{err:?}").starts_with("ParseError"));
}
@@ -370,31 +399,17 @@ mod tests {
#[test]
fn validate_listing_rejects_mismatched_seller() {
let listing = base_listing();
- let mut event = base_event(&listing);
- event.author = OTHER_SELLER.into();
+ let event = event_with_parts(
+ OTHER_SELLER,
+ KIND_LISTING,
+ base_event(&listing).tags_as_vec(),
+ serde_json::to_string(&listing).unwrap(),
+ );
let err = validate_listing_event(&event).unwrap_err();
assert_eq!(err, TradeListingValidationError::InvalidSeller);
}
#[test]
- fn validate_listing_rejects_invalid_listing_address_parts() {
- let mut listing = base_listing();
- listing.farm.pubkey = "not-a-pubkey".into();
- let mut event = base_event(&listing);
- event.author = "not-a-pubkey".into();
- let err = validate_listing_event(&event).unwrap_err();
-
- assert_eq!(
- err,
- TradeListingValidationError::ParseError {
- error: crate::listing::codec::ListingParseError::InvalidTag(
- "listing_addr".to_string()
- )
- }
- );
- }
-
- #[test]
fn validate_listing_rejects_missing_inventory() {
let mut listing = base_listing();
listing.inventory_available = None;
@@ -406,8 +421,12 @@ mod tests {
#[test]
fn validate_listing_rejects_invalid_kind() {
let listing = base_listing();
- let mut event = base_event(&listing);
- event.kind = 0;
+ let event = event_with_parts(
+ SELLER,
+ 0,
+ base_event(&listing).tags_as_vec(),
+ serde_json::to_string(&listing).unwrap(),
+ );
let err = validate_listing_event(&event).unwrap_err();
assert_eq!(err, TradeListingValidationError::InvalidKind { kind: 0 });
}
diff --git a/crates/trade/src/order.rs b/crates/trade/src/order.rs
@@ -8,6 +8,8 @@ use alloc::{
#[cfg(feature = "serde_json")]
use radroots_event::RadrootsEventEnvelope;
+#[cfg(feature = "event_store")]
+use radroots_event::RadrootsEventEnvelopeParts;
use radroots_event::ids::{
RadrootsEventId, RadrootsIdParseError, RadrootsInventoryBinId, RadrootsListingAddress,
RadrootsOrderId, RadrootsPublicKey,
@@ -169,13 +171,15 @@ pub enum RadrootsOrderEventDecodeError {
pub fn order_event_record_from_event(
event: &RadrootsEventEnvelope,
) -> Result<RadrootsOrderEventRecord, RadrootsOrderEventDecodeError> {
- let message_type = RadrootsOrderEventType::from_kind(event.kind)
- .ok_or(RadrootsOrderEventDecodeError::UnsupportedKind { kind: event.kind })?;
- let context = order_event_context_from_tags(message_type, &event.tags)?;
- let event_id =
- RadrootsEventId::parse(&event.id).map_err(RadrootsOrderEventDecodeError::InvalidEventId)?;
- let author_pubkey = RadrootsPublicKey::parse(&event.author)
- .map_err(RadrootsOrderEventDecodeError::InvalidAuthor)?;
+ let message_type = RadrootsOrderEventType::from_kind(event.kind_u32()).ok_or(
+ RadrootsOrderEventDecodeError::UnsupportedKind {
+ kind: event.kind_u32(),
+ },
+ )?;
+ let tags = event.tags_as_vec();
+ let context = order_event_context_from_tags(message_type, &tags)?;
+ let event_id = event.id().clone();
+ let author_pubkey = event.author().clone();
match message_type {
RadrootsOrderEventType::OrderRequested => {
@@ -255,6 +259,11 @@ pub enum RadrootsOrderStoreQueryError {
event_id: String,
source: serde_json::Error,
},
+ #[error("stored order event {event_id} contains invalid envelope fields: {source}")]
+ InvalidStoredEnvelope {
+ event_id: String,
+ source: radroots_event::RadrootsEventEnvelopeError,
+ },
#[error("stored order event {event_id} could not decode as an order record: {source}")]
Decode {
event_id: String,
@@ -364,7 +373,7 @@ fn stored_order_event_to_nostr_event(
source,
}
})?;
- Ok(RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: stored_event.event_id.clone(),
author: stored_event.pubkey.clone(),
created_at: stored_event.created_at,
@@ -373,6 +382,12 @@ fn stored_order_event_to_nostr_event(
content: stored_event.content.clone(),
sig: stored_event.sig.clone(),
})
+ .map_err(
+ |source| RadrootsOrderStoreQueryError::InvalidStoredEnvelope {
+ event_id: stored_event.event_id.clone(),
+ source,
+ },
+ )
}
#[cfg(feature = "serde_json")]
@@ -2666,7 +2681,7 @@ mod tests {
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
};
use radroots_event::{
- RadrootsEventEnvelope, RadrootsEventPtr,
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts, RadrootsEventPtr,
ids::{
RadrootsEventId, RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId,
RadrootsOrderQuoteId, RadrootsOrderRevisionId, RadrootsPublicKey,
@@ -2679,14 +2694,12 @@ mod tests {
RadrootsOrderRevisionDecision, RadrootsOrderRevisionOutcome,
RadrootsOrderRevisionProposal,
},
+ wire::RadrootsNip01EventWireParts,
};
#[cfg(feature = "serde_json")]
- use radroots_event_codec::{
- order::{
- order_cancellation_event_build, order_decision_event_build, order_request_event_build,
- order_revision_decision_event_build, order_revision_proposal_event_build,
- },
- wire::WireEventParts,
+ use radroots_event_codec::order::{
+ order_cancellation_event_build, order_decision_event_build, order_request_event_build,
+ order_revision_decision_event_build, order_revision_proposal_event_build,
};
const BUYER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
@@ -2743,16 +2756,21 @@ mod tests {
}
#[cfg(feature = "serde_json")]
- fn event_from_parts(raw_id: u8, author: &str, parts: WireEventParts) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
+ fn event_from_parts(
+ raw_id: u8,
+ author: &str,
+ parts: RadrootsNip01EventWireParts,
+ ) -> RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: event_id(raw_id).into_string(),
- author: author.into(),
+ author: author.to_string(),
created_at: 1,
kind: parts.kind,
tags: parts.tags,
content: parts.content,
- sig: "sig".into(),
- }
+ sig: "f".repeat(128),
+ })
+ .expect("event")
}
fn economics(bin_count: u32) -> RadrootsOrderEconomics {
@@ -3356,37 +3374,20 @@ mod tests {
&& record.payload.reason == "changed plans"
));
- let unsupported = RadrootsEventEnvelope {
+ let unsupported = RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: event_id(16).into_string(),
- author: BUYER.into(),
+ author: BUYER.to_string(),
created_at: 1,
kind: 1,
tags: Vec::new(),
content: "{}".into(),
- sig: "sig".into(),
- };
+ sig: "f".repeat(128),
+ })
+ .expect("unsupported event");
assert!(matches!(
super::order_event_record_from_event(&unsupported),
Err(super::RadrootsOrderEventDecodeError::UnsupportedKind { kind: 1 })
));
-
- let request_parts =
- order_request_event_build(&listing_event_ptr(), &request.payload).unwrap();
- let mut invalid_id_event = event_from_parts(17, BUYER, request_parts);
- invalid_id_event.id = "not-an-event-id".into();
- assert!(matches!(
- super::order_event_record_from_event(&invalid_id_event),
- Err(super::RadrootsOrderEventDecodeError::InvalidEventId(_))
- ));
-
- let request_parts =
- order_request_event_build(&listing_event_ptr(), &request.payload).unwrap();
- let mut invalid_author_event = event_from_parts(18, BUYER, request_parts);
- invalid_author_event.author = "not-a-pubkey".into();
- assert!(matches!(
- super::order_event_record_from_event(&invalid_author_event),
- Err(super::RadrootsOrderEventDecodeError::InvalidAuthor(_))
- ));
}
#[cfg(feature = "serde_json")]
diff --git a/crates/trade/src/projection.rs b/crates/trade/src/projection.rs
@@ -3,7 +3,7 @@
use std::collections::{BTreeMap, BTreeSet};
use radroots_event::{
- RadrootsEventEnvelope,
+ RadrootsEventEnvelope, RadrootsEventEnvelopeError, RadrootsEventEnvelopeParts,
ids::{RadrootsEventId, RadrootsIdParseError, RadrootsListingAddress, RadrootsOrderId},
kinds::{KIND_TRADE_VALIDATION_RECEIPT, is_listing_kind, is_order_event_kind},
listing::{RadrootsListingAvailability, RadrootsListingDeliveryMethod, RadrootsListingStatus},
@@ -60,6 +60,13 @@ pub enum RadrootsTradeProjectionError {
event_id: String,
source: serde_json::Error,
},
+ #[error("stored event {event_id} contains invalid envelope data: {source}")]
+ InvalidStoredEnvelope {
+ event_id: String,
+ source: RadrootsEventEnvelopeError,
+ },
+ #[error("stored event {event_id} created_at {created_at} exceeds sqlite integer range")]
+ StoredCreatedAtRange { event_id: String, created_at: u64 },
#[error("stored listing event {event_id} failed validation: {source}")]
ListingValidation {
event_id: String,
@@ -379,7 +386,12 @@ async fn upsert_listing_projection(
.bind(location.geohash.as_str())
.bind(listing_json)
.bind(stored_event.seq)
- .bind(i64::from(stored_event.created_at))
+ .bind(i64::try_from(stored_event.created_at).map_err(|_| {
+ RadrootsTradeProjectionError::StoredCreatedAtRange {
+ event_id: stored_event.event_id.clone(),
+ created_at: stored_event.created_at,
+ }
+ })?)
.bind(updated_at_ms)
.execute(store.pool())
.await?;
@@ -709,18 +721,18 @@ fn push_order_record(
fn request_listing_event_id(
event: &RadrootsEventEnvelope,
) -> Result<Option<RadrootsEventId>, RadrootsTradeProjectionError> {
- let context =
- order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &event.tags)
- .map_err(|source| RadrootsTradeProjectionError::OrderContext {
- event_id: event.id.clone(),
- source,
- })?;
+ let tags = event.tags_as_vec();
+ let context = order_event_context_from_tags(RadrootsOrderEventType::OrderRequested, &tags)
+ .map_err(|source| RadrootsTradeProjectionError::OrderContext {
+ event_id: event.id_str().to_owned(),
+ source,
+ })?;
context
.listing_event
.map(|listing_event| {
RadrootsEventId::parse(listing_event.id.as_str()).map_err(|source| {
RadrootsTradeProjectionError::ValidationReceiptEventId {
- event_id: event.id.clone(),
+ event_id: event.id_str().to_owned(),
source,
}
})
@@ -757,7 +769,7 @@ fn stored_event_to_nostr_event(
source,
}
})?;
- Ok(RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: stored_event.event_id.clone(),
author: stored_event.pubkey.clone(),
created_at: stored_event.created_at,
@@ -766,6 +778,12 @@ fn stored_event_to_nostr_event(
content: stored_event.content.clone(),
sig: stored_event.sig.clone(),
})
+ .map_err(
+ |source| RadrootsTradeProjectionError::InvalidStoredEnvelope {
+ event_id: stored_event.event_id.clone(),
+ source,
+ },
+ )
}
async fn transport_observation_count_for_events(
@@ -908,6 +926,7 @@ mod tests {
};
use radroots_event::{
RadrootsEventPtr,
+ draft::RadrootsSignedEvent,
farm::RadrootsFarmRef,
ids::RadrootsOrderQuoteId,
kinds::KIND_LISTING,
@@ -920,6 +939,7 @@ mod tests {
RadrootsOrderEconomics, RadrootsOrderInventoryCommitment, RadrootsOrderItem,
RadrootsOrderPricingBasis, RadrootsOrderRequest,
},
+ wire::RadrootsNip01EventWire,
};
use radroots_event_codec::order::{order_decision_event_build, order_request_event_build};
use radroots_event_store::{
@@ -927,7 +947,7 @@ mod tests {
};
use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp,
- radroots_event_from_nostr, radroots_nostr_build_event,
+ radroots_nostr_build_event,
};
use radroots_transport::RadrootsTransportKind;
@@ -1003,7 +1023,7 @@ mod tests {
}
}
- fn signed_listing_event() -> RadrootsEventEnvelope {
+ fn signed_listing_event() -> RadrootsSignedEvent {
let parts = radroots_event_codec::listing::encode::to_wire_parts(&listing())
.expect("listing parts");
sign_parts(
@@ -1015,11 +1035,11 @@ mod tests {
)
}
- fn listing_addr(event: &RadrootsEventEnvelope) -> RadrootsListingAddress {
+ fn listing_addr(event: &RadrootsSignedEvent) -> RadrootsListingAddress {
RadrootsListingAddress::parse(format!(
"{}:{}:{}",
KIND_LISTING,
- event.author,
+ event.pubkey_str(),
listing().d_tag
))
.expect("listing address")
@@ -1054,7 +1074,7 @@ mod tests {
}
}
- fn order_request(listing_event: &RadrootsEventEnvelope) -> RadrootsOrderRequest {
+ fn order_request(listing_event: &RadrootsSignedEvent) -> RadrootsOrderRequest {
RadrootsOrderRequest {
order_id: order_id(),
listing_addr: listing_addr(listing_event),
@@ -1068,17 +1088,17 @@ mod tests {
}
}
- fn signed_order_request_event(listing_event: &RadrootsEventEnvelope) -> RadrootsEventEnvelope {
+ fn signed_order_request_event(listing_event: &RadrootsSignedEvent) -> RadrootsSignedEvent {
signed_order_request_event_at(listing_event, 1_700_000_010)
}
fn signed_order_request_event_at(
- listing_event: &RadrootsEventEnvelope,
+ listing_event: &RadrootsSignedEvent,
created_at: u32,
- ) -> RadrootsEventEnvelope {
+ ) -> RadrootsSignedEvent {
let parts = order_request_event_build(
&RadrootsEventPtr {
- id: listing_event.id.clone(),
+ id: listing_event.id_str().to_owned(),
relays: Some("wss://relay.example.test".to_owned()),
},
&order_request(listing_event),
@@ -1094,9 +1114,9 @@ mod tests {
}
fn signed_order_decision_event(
- request: &RadrootsEventEnvelope,
- listing_event: &RadrootsEventEnvelope,
- ) -> RadrootsEventEnvelope {
+ request: &RadrootsSignedEvent,
+ listing_event: &RadrootsSignedEvent,
+ ) -> RadrootsSignedEvent {
let decision = RadrootsOrderDecision {
order_id: order_id(),
listing_addr: listing_addr(listing_event),
@@ -1109,7 +1129,7 @@ mod tests {
}],
},
};
- let root = RadrootsEventId::parse(request.id.as_str()).expect("root");
+ let root = request.id().clone();
let parts = order_decision_event_build(&root, &root, &decision).expect("decision parts");
sign_parts(
parts.kind,
@@ -1121,14 +1141,14 @@ mod tests {
}
fn signed_receipt_event(
- listing_event: &RadrootsEventEnvelope,
- request: &RadrootsEventEnvelope,
- decision: &RadrootsEventEnvelope,
+ listing_event: &RadrootsSignedEvent,
+ request: &RadrootsSignedEvent,
+ decision: &RadrootsSignedEvent,
result: RadrootsValidationReceiptResult,
- ) -> RadrootsEventEnvelope {
- let request_id = RadrootsEventId::parse(request.id.as_str()).expect("request");
- let listing_event_id = RadrootsEventId::parse(listing_event.id.as_str()).expect("listing");
- let decision_id = RadrootsEventId::parse(decision.id.as_str()).expect("decision");
+ ) -> RadrootsSignedEvent {
+ let request_id = request.id().clone();
+ let listing_event_id = listing_event.id().clone();
+ let decision_id = decision.id().clone();
let receipt = RadrootsTradeValidationReceipt {
changed_records_root: hash32('a'),
domain: "radroots.receipt".to_owned(),
@@ -1180,13 +1200,15 @@ mod tests {
tags: Vec<Vec<String>>,
created_at: u32,
keys: &RadrootsNostrKeys,
- ) -> RadrootsEventEnvelope {
+ ) -> RadrootsSignedEvent {
let raw_event = radroots_nostr_build_event(kind, content, tags)
.expect("builder")
.custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at)))
.sign_with_keys(keys)
.expect("signed");
- radroots_event_from_nostr(&raw_event)
+ let raw_json = serde_json::to_string(&raw_event).expect("raw event json");
+ let wire = RadrootsNip01EventWire::parse_json(raw_json.as_str()).expect("wire");
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).expect("signed event")
}
fn hash32(character: char) -> String {
@@ -1265,10 +1287,10 @@ mod tests {
);
assert_eq!(
status.projection.validation_receipt_event_id,
- Some(RadrootsEventId::parse(receipt_event.id).expect("receipt"))
+ Some(receipt_event.id().clone())
);
- let root_event_id = RadrootsEventId::parse(request_event.id).expect("request");
+ let root_event_id = request_event.id().clone();
let trade_row = sqlx::query(
"SELECT root_event_id, projection_version, status, rhi_state, transport_observation_count, source_event_count, evidence_hash FROM trade_projection WHERE order_id = ? AND root_event_id = ? AND projection_version = ?",
)
@@ -1319,8 +1341,8 @@ mod tests {
let listing_event = signed_listing_event();
let first_request_event = signed_order_request_event_at(&listing_event, 1_700_000_010);
let second_request_event = signed_order_request_event_at(&listing_event, 1_700_000_011);
- let first_root = RadrootsEventId::parse(first_request_event.id.clone()).expect("first");
- let second_root = RadrootsEventId::parse(second_request_event.id.clone()).expect("second");
+ let first_root = first_request_event.id().clone();
+ let second_root = second_request_event.id().clone();
store
.ingest_event(RadrootsEventIngest::new(listing_event.clone(), 10))
diff --git a/crates/trade/src/validation_receipt.rs b/crates/trade/src/validation_receipt.rs
@@ -10,9 +10,8 @@ use alloc::{
use base64::Engine as _;
use radroots_event::{
RadrootsEventEnvelope, ids::RadrootsPublicKey, kinds::KIND_TRADE_VALIDATION_RECEIPT,
- tags::TAG_D,
+ tags::TAG_D, wire::RadrootsNip01EventWireParts,
};
-use radroots_event_codec::wire::WireEventParts;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
@@ -566,8 +565,8 @@ pub fn validation_receipt_tags_from_tags(
pub fn validation_receipt_event_build(
order_id: &str,
receipt: &RadrootsTradeValidationReceipt,
-) -> Result<WireEventParts, RadrootsValidationReceiptError> {
- Ok(WireEventParts {
+) -> Result<RadrootsNip01EventWireParts, RadrootsValidationReceiptError> {
+ Ok(RadrootsNip01EventWireParts {
kind: KIND_TRADE_VALIDATION_RECEIPT,
content: validation_receipt_canonical_content(receipt)?,
tags: validation_receipt_tags(order_id, receipt)?,
@@ -584,15 +583,16 @@ pub fn verify_validation_receipt_event(
event: &RadrootsEventEnvelope,
expected: RadrootsValidationReceiptExpectedBinding<'_>,
) -> Result<RadrootsVerifiedValidationReceipt, RadrootsValidationReceiptError> {
- if event.kind != KIND_TRADE_VALIDATION_RECEIPT {
+ if event.kind_u32() != KIND_TRADE_VALIDATION_RECEIPT {
return Err(RadrootsValidationReceiptError::InvalidKind {
expected: KIND_TRADE_VALIDATION_RECEIPT,
- got: event.kind,
+ got: event.kind_u32(),
});
}
- let receipt = validation_receipt_content_from_str(&event.content)?;
- let tags = validation_receipt_tags_from_tags(&event.tags)?;
+ let receipt = validation_receipt_content_from_str(event.content())?;
+ let event_tags = event.tags_as_vec();
+ let tags = validation_receipt_tags_from_tags(&event_tags)?;
if tags.listing_event_id != receipt.statement.listing_event_id {
return Err(RadrootsValidationReceiptError::TagMismatch(
@@ -864,8 +864,8 @@ mod tests {
verify_validation_receipt_event,
};
use radroots_event::{
- RadrootsEventEnvelope, ids::RadrootsPublicKey, kinds::KIND_TRADE_VALIDATION_RECEIPT,
- tags::TAG_D,
+ RadrootsEventEnvelope, RadrootsEventEnvelopeParts, ids::RadrootsPublicKey,
+ kinds::KIND_TRADE_VALIDATION_RECEIPT, tags::TAG_D,
};
fn hash32(c: char) -> String {
@@ -923,15 +923,30 @@ mod tests {
fn sample_validation_receipt_event() -> RadrootsEventEnvelope {
let receipt = sample_validation_receipt();
let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts");
- RadrootsEventEnvelope {
+ validation_receipt_event_with_parts(parts.kind, parts.tags, parts.content)
+ }
+
+ fn validation_receipt_event_with_parts(
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ ) -> RadrootsEventEnvelope {
+ RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: event_id('9'),
author: event_id('a'),
created_at: 1,
- kind: parts.kind,
- tags: parts.tags,
- content: parts.content,
- sig: "signature".to_string(),
- }
+ kind,
+ tags,
+ content,
+ sig: "f".repeat(128),
+ })
+ .expect("receipt event")
+ }
+
+ fn validation_receipt_event_with_tags(tags: Vec<Vec<String>>) -> RadrootsEventEnvelope {
+ let receipt = sample_validation_receipt();
+ let parts = validation_receipt_event_build("order-1", &receipt).expect("event parts");
+ validation_receipt_event_with_parts(parts.kind, tags, parts.content)
}
#[test]
@@ -1449,8 +1464,9 @@ mod tests {
#[test]
fn validation_receipt_verifier_rejects_each_tag_mismatch() {
- let mut event = sample_validation_receipt_event();
- event.tags[1][1] = event_id('3');
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[1][1] = event_id('3');
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch(
@@ -1458,15 +1474,17 @@ mod tests {
))
);
- let mut event = sample_validation_receipt_event();
- event.tags[2][1] = event_id('3');
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[2][1] = event_id('3');
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch("root_event_id"))
);
- let mut event = sample_validation_receipt_event();
- event.tags[3][1] = event_id('3');
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[3][1] = event_id('3');
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch(
@@ -1474,8 +1492,9 @@ mod tests {
))
);
- let mut event = sample_validation_receipt_event();
- event.tags[4][1] = hash32('d');
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[4][1] = hash32('d');
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch(
@@ -1483,8 +1502,9 @@ mod tests {
))
);
- let mut event = sample_validation_receipt_event();
- event.tags[5][1] = hash32('d');
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[5][1] = hash32('d');
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch(
@@ -1492,8 +1512,9 @@ mod tests {
))
);
- let mut event = sample_validation_receipt_event();
- event.tags[6][1] = hash32('d');
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[6][1] = hash32('d');
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch(
@@ -1501,15 +1522,17 @@ mod tests {
))
);
- let mut event = sample_validation_receipt_event();
- event.tags[7][1] = "sp1_core".to_string();
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[7][1] = "sp1_core".to_string();
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch("proof_system"))
);
- let mut event = sample_validation_receipt_event();
- event.tags[8][1] = "listing_validation".to_string();
+ let mut tags = sample_validation_receipt_event().tags_as_vec();
+ tags[8][1] = "listing_validation".to_string();
+ let event = validation_receipt_event_with_tags(tags);
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::TagMismatch("receipt_type"))
@@ -1659,7 +1682,7 @@ mod tests {
);
let event = sample_validation_receipt_event();
- assert_eq!(event.kind, KIND_TRADE_VALIDATION_RECEIPT);
+ assert_eq!(event.kind_u32(), KIND_TRADE_VALIDATION_RECEIPT);
let verified = validation_receipt_from_event(&event).expect("verified receipt");
assert_eq!(verified.tags.order_id, "order-1");
assert_eq!(verified.tags.listing_event_id, event_id('0'));
@@ -1813,8 +1836,12 @@ mod tests {
#[test]
fn validation_receipt_verifier_rejects_non_validation_receipt_kind() {
- let mut event = sample_validation_receipt_event();
- event.kind = 3434;
+ let sample = sample_validation_receipt_event();
+ let event = validation_receipt_event_with_parts(
+ 3434,
+ sample.tags_as_vec(),
+ sample.content().to_owned(),
+ );
assert_eq!(
validation_receipt_from_event(&event),
Err(RadrootsValidationReceiptError::InvalidKind {
@@ -1840,22 +1867,23 @@ mod tests {
))
);
- let mut missing_event_set = event.clone();
- missing_event_set
- .tags
- .retain(|tag| tag.first().map(|value| value.as_str()) != Some("event_set_root"));
+ let mut tags = event.tags_as_vec();
+ tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some("event_set_root"));
+ let missing_event_set =
+ validation_receipt_event_with_parts(event.kind_u32(), tags, event.content().to_owned());
assert_eq!(
validation_receipt_from_event(&missing_event_set),
Err(RadrootsValidationReceiptError::MissingTag("event_set_root"))
);
- let mut wrong_reducer_output = event.clone();
- let reducer_tag = wrong_reducer_output
- .tags
+ let mut tags = event.tags_as_vec();
+ let reducer_tag = tags
.iter_mut()
.find(|tag| tag.first().map(|value| value.as_str()) == Some("reducer_output_root"))
.expect("reducer output tag");
reducer_tag[1] = hash32('8');
+ let wrong_reducer_output =
+ validation_receipt_event_with_parts(event.kind_u32(), tags, event.content().to_owned());
assert_eq!(
validation_receipt_from_event(&wrong_reducer_output),
Err(RadrootsValidationReceiptError::TagMismatch(
@@ -1863,13 +1891,14 @@ mod tests {
))
);
- let mut wrong_public_values = event.clone();
- let public_values_tag = wrong_public_values
- .tags
+ let mut tags = event.tags_as_vec();
+ let public_values_tag = tags
.iter_mut()
.find(|tag| tag.first().map(|value| value.as_str()) == Some("public_values_hash"))
.expect("public values tag");
public_values_tag[1] = hash32('b');
+ let wrong_public_values =
+ validation_receipt_event_with_parts(event.kind_u32(), tags, event.content().to_owned());
assert_eq!(
validation_receipt_from_event(&wrong_public_values),
Err(RadrootsValidationReceiptError::TagMismatch(
@@ -1898,9 +1927,7 @@ mod tests {
receipt.proof.proof_reference = Some(format!("radroots-proof://sha256/{}", "1".repeat(64)));
let parts = validation_receipt_event_build("order-1", &receipt).expect("sp1 event parts");
- let mut event = sample_validation_receipt_event();
- event.content = parts.content;
- event.tags = parts.tags;
+ let event = validation_receipt_event_with_parts(parts.kind, parts.tags, parts.content);
let verified = verify_validation_receipt_event(
&event,
RadrootsValidationReceiptExpectedBinding {
@@ -2036,9 +2063,7 @@ mod tests {
fn validation_receipt_expected_binding_enforces_sp1_identity() {
let receipt = sample_sp1_reference_receipt();
let parts = validation_receipt_event_build("order-1", &receipt).expect("sp1 event parts");
- let mut event = sample_validation_receipt_event();
- event.content = parts.content;
- event.tags = parts.tags;
+ let event = validation_receipt_event_with_parts(parts.kind, parts.tags, parts.content);
verify_validation_receipt_event(
&event,
diff --git a/crates/transport_nostr/src/fetch.rs b/crates/transport_nostr/src/fetch.rs
@@ -8,9 +8,7 @@ use radroots_event_store::{
RadrootsEventContractStatus, RadrootsEventIngest, RadrootsEventStore,
RadrootsTransportObservation, RadrootsTransportObservationType,
};
-use radroots_nostr::prelude::{
- RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, radroots_event_from_nostr,
-};
+use radroots_nostr::prelude::{RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter};
use radroots_transport::RadrootsTransportKind;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex, PoisonError};
@@ -360,7 +358,6 @@ where
raw_json,
observed_at_ms,
}) => {
- let event = radroots_event_from_nostr(&raw_event);
let observation_type = match mode {
RadrootsRelayFetchMode::Fetch => RadrootsTransportObservationType::Fetch,
RadrootsRelayFetchMode::Subscription => {
@@ -373,9 +370,26 @@ where
observation_type,
observed_at_ms,
)?;
- let ingest = RadrootsEventIngest::new(event, observed_at_ms)
- .with_raw_json(raw_json)
- .with_observation(observation);
+ let ingest = match RadrootsEventIngest::from_raw_json(raw_json, observed_at_ms) {
+ Ok(ingest) => ingest.with_observation(observation),
+ Err(error) => {
+ receipt.malformed_count += 1;
+ receipt.events.push(RadrootsRelayFetchEventReceipt {
+ relay_url,
+ event_id: Some(raw_event.id.to_hex()),
+ inserted: false,
+ duplicate: false,
+ unsupported: false,
+ malformed: true,
+ out_of_filter: false,
+ skipped_over_limit: false,
+ projection_eligible: false,
+ verification_status: None,
+ message: Some(error.to_string()),
+ });
+ continue;
+ }
+ };
match event_store.ingest_event(ingest).await {
Ok(store_receipt) => {
let unsupported =
diff --git a/crates/transport_nostr/src/outbox.rs b/crates/transport_nostr/src/outbox.rs
@@ -6,7 +6,6 @@ use crate::{
RadrootsRelayTransportError, RadrootsRelayUrlPolicy, publish_signed_event,
verified_signed_event_payload,
};
-use radroots_event::RadrootsEventEnvelope;
use radroots_event::draft::RadrootsSignedEvent;
use radroots_event_store::{
RadrootsEventIngest, RadrootsEventStore, RadrootsTransportObservation,
@@ -113,7 +112,7 @@ where
.await?;
return Ok(RadrootsOutboxPublishReceipt {
local_ingest,
- event_id: signed_event.id,
+ event_id: signed_event.id_str().to_owned(),
attempted_count: 0,
accepted_count: publishable.accepted_count,
retryable_count: 0,
@@ -144,13 +143,13 @@ where
.with_idempotency_key(outbox_publish_idempotency_key(
claimed.outbox_event_id,
claimed.attempt_count,
- signed_event.id.as_str(),
+ signed_event.id_str(),
active_delivery_plan_id,
));
let publish = match publish_signed_event(adapter, request).await {
Ok(receipt) => receipt,
Err(RadrootsRelayTransportError::Transport(message)) => adapter_transport_failure_receipt(
- signed_event.id.clone(),
+ signed_event.id_str().to_owned(),
target_strings,
publishable.remaining_satisfaction_count,
message,
@@ -259,7 +258,7 @@ where
.await?;
return Ok(RadrootsOutboxPublishReceipt {
local_ingest,
- event_id: signed_event.id,
+ event_id: signed_event.id_str().to_owned(),
attempted_count: 0,
accepted_count: publishable.accepted_count,
retryable_count: 0,
@@ -283,7 +282,7 @@ where
let request_id = outbox_publish_idempotency_key(
claimed.outbox_event_id,
claimed.attempt_count,
- signed_event.id.as_str(),
+ signed_event.id_str(),
publishable.active_delivery_plan_id,
);
let payload =
@@ -337,7 +336,7 @@ where
Ok(RadrootsOutboxPublishReceipt {
local_ingest,
- event_id: signed_event.id,
+ event_id: signed_event.id_str().to_owned(),
attempted_count: target_receipts
.iter()
.filter(|receipt| receipt.attempted)
@@ -1030,25 +1029,12 @@ async fn ingest_publish_observation(
if let Some(message) = message {
observation = observation.with_redacted_message(message);
}
- let ingest = RadrootsEventIngest::new(event_from_signed(signed_event), observed_at_ms)
- .with_raw_json(signed_event.raw_json.clone())
+ let ingest = RadrootsEventIngest::new(signed_event.clone(), observed_at_ms)
.with_observation(observation);
event_store.ingest_event(ingest).await?;
Ok(())
}
-fn event_from_signed(signed_event: &RadrootsSignedEvent) -> RadrootsEventEnvelope {
- RadrootsEventEnvelope {
- id: signed_event.id.clone(),
- author: signed_event.pubkey.clone(),
- created_at: signed_event.created_at,
- kind: signed_event.kind,
- tags: signed_event.tags.clone(),
- content: signed_event.content.clone(),
- sig: signed_event.sig.clone(),
- }
-}
-
#[cfg(test)]
mod tests {
use super::{
diff --git a/crates/transport_nostr/src/publish.rs b/crates/transport_nostr/src/publish.rs
@@ -4,7 +4,7 @@ use crate::{RadrootsRelayOutcome, RadrootsRelayTargetSet, RadrootsRelayTransport
#[cfg(feature = "client")]
use core::time::Duration;
use futures::future::BoxFuture;
-use radroots_event::draft::{RadrootsSignedEvent, RadrootsSignedEventParts};
+use radroots_event::{draft::RadrootsSignedEvent, wire::RadrootsNip01EventWire};
use radroots_transport::{
RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt,
RadrootsTransportDeliveryRequest, RadrootsTransportError, RadrootsTransportFetchReceipt,
@@ -114,26 +114,20 @@ pub fn verified_signed_event_payload(
) -> Result<RadrootsTransportPayload, RadrootsTransportError> {
verify_signed_event_raw_json_matches_event(signed_event)?;
RadrootsTransportPayload::unchecked_signed_event_json(
- signed_event.id.as_str(),
- signed_event.raw_json.as_str(),
+ signed_event.id_str(),
+ signed_event.raw_json(),
)
}
fn verify_signed_event_raw_json_matches_event(
signed_event: &RadrootsSignedEvent,
) -> Result<(), RadrootsTransportError> {
- let wire: SignedEventJsonWire = serde_json::from_str(signed_event.raw_json.as_str())
+ let wire = RadrootsNip01EventWire::parse_json(signed_event.raw_json())
.map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?;
- if wire.id != signed_event.id {
+ if wire.id != signed_event.id_str() {
return Err(RadrootsTransportError::InvalidPayloadId);
}
- if wire.pubkey != signed_event.pubkey
- || wire.created_at != signed_event.created_at
- || wire.kind != signed_event.kind
- || wire.tags != signed_event.tags
- || wire.content != signed_event.content
- || wire.sig != signed_event.sig
- {
+ if &wire != signed_event.wire() {
return Err(RadrootsTransportError::InvalidPayloadBytes);
}
Ok(())
@@ -272,17 +266,6 @@ fn nostr_error_to_transport_error(error: RadrootsRelayTransportError) -> Radroot
}
}
-#[derive(Deserialize)]
-struct SignedEventJsonWire {
- id: String,
- pubkey: String,
- created_at: u32,
- kind: u32,
- tags: Vec<Vec<String>>,
- content: String,
- sig: String,
-}
-
fn signed_event_from_transport_payload(
payload: &RadrootsTransportPayload,
) -> Result<RadrootsSignedEvent, RadrootsTransportError> {
@@ -292,22 +275,13 @@ fn signed_event_from_transport_payload(
else {
return Err(RadrootsTransportError::InvalidPayloadBytes);
};
- let wire: SignedEventJsonWire =
- serde_json::from_str(raw_json).map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?;
+ let wire = RadrootsNip01EventWire::parse_json(raw_json)
+ .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)?;
if wire.id != *event_id {
return Err(RadrootsTransportError::InvalidPayloadId);
}
- RadrootsSignedEvent::new(RadrootsSignedEventParts {
- id: wire.id,
- pubkey: wire.pubkey,
- created_at: wire.created_at,
- kind: wire.kind,
- tags: wire.tags,
- content: wire.content,
- sig: wire.sig,
- raw_json: raw_json.clone(),
- })
- .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json.clone())
+ .map_err(|_| RadrootsTransportError::InvalidPayloadBytes)
}
fn relay_targets_from_transport_targets(
@@ -376,7 +350,7 @@ pub async fn publish_signed_event<A>(
where
A: RadrootsRelayPublishAdapter,
{
- let event_id = request.signed_event.id.clone();
+ let event_id = request.signed_event.id_str().to_owned();
let satisfaction_policy = request.satisfaction_policy.clone();
let target_count = request.targets.len();
let quorum = satisfaction_policy.required_target_count(target_count)?;
@@ -497,7 +471,7 @@ impl RadrootsRelayPublishAdapter for RadrootsMockRelayPublishAdapter {
self.captured_raw_events
.lock()
.map_err(captured_raw_event_lock_error)?
- .push(request.signed_event.raw_json.clone());
+ .push(request.signed_event.raw_json().to_owned());
Ok(request
.targets
.relays()
@@ -543,7 +517,7 @@ impl RadrootsRelayPublishAdapter for RadrootsNostrClientPublishAdapter {
) -> BoxFuture<'a, Result<Vec<RadrootsRelayPublishRelayReceipt>, RadrootsRelayTransportError>>
{
Box::pin(async move {
- let event = RadrootsNostrEvent::from_json(request.signed_event.raw_json.as_str())
+ let event = RadrootsNostrEvent::from_json(request.signed_event.raw_json())
.map_err(|error| RadrootsRelayTransportError::NostrEventJson(error.to_string()))?;
ensure_raw_event_matches_signed_event(&event, &request.signed_event)?;
let target_strings = request.targets.relay_strings();
@@ -661,24 +635,32 @@ fn ensure_raw_event_matches_signed_event(
signed_event: &RadrootsSignedEvent,
) -> Result<(), RadrootsRelayTransportError> {
let mismatches = [
- ("id", event.id.to_hex(), signed_event.id.clone()),
- ("pubkey", event.pubkey.to_hex(), signed_event.pubkey.clone()),
+ ("id", event.id.to_hex(), signed_event.id_str().to_owned()),
+ (
+ "pubkey",
+ event.pubkey.to_hex(),
+ signed_event.pubkey_str().to_owned(),
+ ),
(
"created_at",
event.created_at.as_secs().to_string(),
- signed_event.created_at.to_string(),
+ signed_event.created_at().to_string(),
),
(
"kind",
(event.kind.as_u16() as u32).to_string(),
- signed_event.kind.to_string(),
+ signed_event.kind().to_string(),
),
(
"content",
event.content.clone(),
- signed_event.content.clone(),
+ signed_event.content().to_owned(),
+ ),
+ (
+ "sig",
+ event.sig.to_string(),
+ signed_event.sig_str().to_owned(),
),
- ("sig", event.sig.to_string(), signed_event.sig.clone()),
];
for (field, raw, wrapped) in mismatches {
if raw != wrapped {
@@ -692,7 +674,7 @@ fn ensure_raw_event_matches_signed_event(
.iter()
.map(|tag| tag.as_slice().to_vec())
.collect::<Vec<_>>();
- if raw_tags != signed_event.tags {
+ if raw_tags != signed_event.tags_as_vec() {
return Err(RadrootsRelayTransportError::NostrEventJson(
"raw event JSON tags do not match signed event tags".to_owned(),
));
@@ -706,6 +688,7 @@ mod tests {
use nostr::JsonUtil;
use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent};
use radroots_event::kinds::KIND_POST;
+ use radroots_event::wire::RadrootsNip01EventWire;
use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft,
};
@@ -729,8 +712,7 @@ mod tests {
)
.expect("draft");
let signed_event = radroots_nostr_sign_frozen_draft(&keys, &draft).expect("signed event");
- let raw_event =
- RadrootsNostrEvent::from_json(signed_event.raw_json.as_str()).expect("raw event");
+ let raw_event = RadrootsNostrEvent::from_json(signed_event.raw_json()).expect("raw event");
(raw_event, signed_event)
}
@@ -738,39 +720,45 @@ mod tests {
assert!(ensure_raw_event_matches_signed_event(raw_event, &signed_event).is_err());
}
+ fn signed_event_with_wire(
+ original: &RadrootsSignedEvent,
+ wire: RadrootsNip01EventWire,
+ ) -> RadrootsSignedEvent {
+ RadrootsSignedEvent::from_wire_unchecked(wire, original.raw_json().to_owned())
+ .expect("signed event")
+ }
+
#[test]
fn raw_event_match_guard_accepts_exact_event_and_rejects_field_mismatches() {
let (raw_event, signed_event) = signed_post("matched");
ensure_raw_event_matches_signed_event(&raw_event, &signed_event).expect("matching event");
- let mut mismatched = signed_event.clone();
- mismatched.id = "00".repeat(32);
- assert_mismatch(&raw_event, mismatched);
+ let mut wire = signed_event.wire().clone();
+ wire.id = "00".repeat(32);
+ assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire));
- let mut mismatched = signed_event.clone();
- mismatched.pubkey = "11".repeat(32);
- assert_mismatch(&raw_event, mismatched);
+ let mut wire = signed_event.wire().clone();
+ wire.pubkey = "11".repeat(32);
+ assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire));
- let mut mismatched = signed_event.clone();
- mismatched.created_at += 1;
- assert_mismatch(&raw_event, mismatched);
+ let mut wire = signed_event.wire().clone();
+ wire.created_at += 1;
+ assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire));
- let mut mismatched = signed_event.clone();
- mismatched.kind += 1;
- assert_mismatch(&raw_event, mismatched);
+ let mut wire = signed_event.wire().clone();
+ wire.kind += 1;
+ assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire));
- let mut mismatched = signed_event.clone();
- mismatched.content.push_str(" changed");
- assert_mismatch(&raw_event, mismatched);
+ let mut wire = signed_event.wire().clone();
+ wire.content.push_str(" changed");
+ assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire));
- let mut mismatched = signed_event.clone();
- mismatched.sig = "22".repeat(64);
- assert_mismatch(&raw_event, mismatched);
+ let mut wire = signed_event.wire().clone();
+ wire.sig = "22".repeat(64);
+ assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire));
- let mut mismatched = signed_event;
- mismatched
- .tags
- .push(vec!["t".to_owned(), "compost".to_owned()]);
- assert_mismatch(&raw_event, mismatched);
+ let mut wire = signed_event.wire().clone();
+ wire.tags.push(vec!["t".to_owned(), "compost".to_owned()]);
+ assert_mismatch(&raw_event, signed_event_with_wire(&signed_event, wire));
}
}
diff --git a/crates/transport_nostr/tests/transport.rs b/crates/transport_nostr/tests/transport.rs
@@ -329,8 +329,7 @@ fn post_relay_fetch_request(observed_at_ms: i64, max_events: usize) -> RadrootsR
fn tampered_raw_event() -> String {
let signed = signed_post("trusted");
- let mut value =
- serde_json::from_str::<serde_json::Value>(signed.raw_json.as_str()).expect("raw json");
+ let mut value = serde_json::from_str::<serde_json::Value>(signed.raw_json()).expect("raw json");
value["content"] = serde_json::Value::String("tampered".to_owned());
serde_json::to_string(&value).expect("tampered json")
}
@@ -712,7 +711,10 @@ async fn mock_publish_preserves_exact_raw_json_and_counts_outcomes() {
.await
.expect("publish");
- assert_eq!(adapter.captured_raw_events(), vec![signed.raw_json]);
+ assert_eq!(
+ adapter.captured_raw_events(),
+ vec![signed.raw_json().to_owned()]
+ );
assert_eq!(receipt.attempted_count, 3);
assert_eq!(receipt.accepted_count, 2);
assert_eq!(receipt.retryable_count, 1);
@@ -729,8 +731,8 @@ async fn nostr_transport_facade_delivers_signed_event_payloads() {
let request = RadrootsTransportDeliveryRequest::new(
"facade-request-1",
RadrootsTransportPayload::unchecked_signed_event_json(
- signed.id.clone(),
- signed.raw_json.clone(),
+ signed.id_str().to_owned(),
+ signed.raw_json().to_owned(),
)
.expect("payload"),
RadrootsTransportTargetSet::new(vec![target.clone()]).expect("targets"),
@@ -740,7 +742,10 @@ async fn nostr_transport_facade_delivers_signed_event_payloads() {
let receipt = transport.deliver(request).await.expect("delivery");
let status = transport.status().await.expect("status");
- assert_eq!(adapter.captured_raw_events(), vec![signed.raw_json]);
+ assert_eq!(
+ adapter.captured_raw_events(),
+ vec![signed.raw_json().to_owned()]
+ );
assert!(status.capabilities.deliver);
assert!(!status.capabilities.fetch);
assert_eq!(receipt.request_id, "facade-request-1");
@@ -770,12 +775,12 @@ fn verified_signed_event_payload_preserves_transport_payload_identity() {
panic!("signed event payload expected");
};
- assert_eq!(event_id, signed.id);
- assert_eq!(raw_json, signed.raw_json);
+ assert_eq!(event_id, signed.id_str());
+ assert_eq!(raw_json, signed.raw_json().to_owned());
assert_eq!(digest.len(), 64);
- let mut mismatched = signed;
- mismatched.raw_json = "{}".to_owned();
+ let mismatched =
+ RadrootsSignedEvent::from_wire_unchecked(signed.wire().clone(), "{}").expect("mismatch");
assert_eq!(
verified_signed_event_payload(&mismatched).expect_err("mismatched raw json"),
RadrootsTransportError::InvalidPayloadBytes
@@ -820,8 +825,8 @@ async fn nostr_transport_facade_rejects_unsupported_payloads_and_targets() {
.deliver(RadrootsTransportDeliveryRequest::new(
"facade-request-target",
RadrootsTransportPayload::unchecked_signed_event_json(
- signed.id.clone(),
- signed.raw_json.clone(),
+ signed.id_str().to_owned(),
+ signed.raw_json().to_owned(),
)
.expect("payload"),
RadrootsTransportTargetSet::new(vec![non_nostr_target]).expect("targets"),
@@ -846,8 +851,8 @@ async fn nostr_transport_facade_matches_canonical_equivalent_relay_receipts() {
.deliver(RadrootsTransportDeliveryRequest::new(
"facade-canonical-receipt",
RadrootsTransportPayload::unchecked_signed_event_json(
- signed.id.clone(),
- signed.raw_json.clone(),
+ signed.id_str().to_owned(),
+ signed.raw_json().to_owned(),
)
.expect("payload"),
RadrootsTransportTargetSet::new(vec![target.clone()]).expect("target set"),
@@ -894,8 +899,8 @@ async fn nostr_transport_facade_preserves_scoped_duplicate_target_metadata() {
let request = RadrootsTransportDeliveryRequest::new(
"facade-request-scoped",
RadrootsTransportPayload::unchecked_signed_event_json(
- signed.id.clone(),
- signed.raw_json.clone(),
+ signed.id_str().to_owned(),
+ signed.raw_json().to_owned(),
)
.expect("payload"),
RadrootsTransportTargetSet::new(vec![first.clone(), second.clone()]).expect("targets"),
@@ -932,7 +937,7 @@ async fn publish_receipts_track_terminal_skipped_and_adapter_errors() {
.await
.expect("publish");
- assert_eq!(receipt.event_id, signed.id);
+ assert_eq!(receipt.event_id, signed.id_str());
assert_eq!(receipt.attempted_count, 2);
assert_eq!(receipt.accepted_count, 1);
assert_eq!(receipt.retryable_count, 0);
@@ -1089,11 +1094,11 @@ fn fetch_requests_reject_zero_limits_and_timeouts() {
#[test]
fn fetch_blocking_facade_runs_mock_adapter() {
let signed = signed_post("blocking fetch");
- let accepted_id = signed.id.clone();
+ let accepted_id = signed.id_str().to_owned();
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: signed.raw_json,
+ raw_json: signed.raw_json().to_owned(),
observed_at_ms: 1_090,
},
RadrootsRelayFetchItem::Eose {
@@ -1116,12 +1121,12 @@ async fn fetch_ingests_events_and_records_transport_observations() {
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: signed.raw_json.clone(),
+ raw_json: signed.raw_json().to_owned(),
observed_at_ms: 1_000,
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: signed.raw_json.clone(),
+ raw_json: signed.raw_json().to_owned(),
observed_at_ms: 1_001,
},
RadrootsRelayFetchItem::Event {
@@ -1222,7 +1227,7 @@ async fn fetch_ingests_events_and_records_transport_observations() {
assert!(!receipt.events[4].projection_eligible);
let observations = store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_eq!(observations.len(), 1);
@@ -1245,12 +1250,12 @@ async fn fetch_rejects_out_of_filter_events_before_store_mutation() {
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: wrong_tag.raw_json.clone(),
+ raw_json: wrong_tag.raw_json().to_owned(),
observed_at_ms: 1_005,
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: accepted.raw_json.clone(),
+ raw_json: accepted.raw_json().to_owned(),
observed_at_ms: 1_006,
},
RadrootsRelayFetchItem::Event {
@@ -1289,14 +1294,14 @@ async fn fetch_rejects_out_of_filter_events_before_store_mutation() {
assert!(receipt.events[2].out_of_filter);
assert!(
store
- .get_event(accepted.id.as_str())
+ .get_event(accepted.id_str())
.await
.expect("accepted lookup")
.is_some()
);
assert!(
store
- .get_event(wrong_tag.id.as_str())
+ .get_event(wrong_tag.id_str())
.await
.expect("wrong tag lookup")
.is_none()
@@ -1315,9 +1320,9 @@ async fn fetch_event_cap_counts_accepted_in_filter_events_and_preserves_later_co
let accepted = signed_post("accepted capped event");
let skipped = signed_post("skipped capped event");
let wrong_tag = signed_event_with_kind_and_hashtag("wrong capped tag", KIND_POST, "compost");
- let accepted_id = accepted.id.clone();
- let skipped_id = skipped.id.clone();
- let wrong_tag_id = wrong_tag.id.clone();
+ let accepted_id = accepted.id_str().to_owned();
+ let skipped_id = skipped.id_str().to_owned();
+ let wrong_tag_id = wrong_tag.id_str().to_owned();
let store = RadrootsEventStore::open_memory().await.expect("store");
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
RadrootsRelayFetchItem::Event {
@@ -1327,17 +1332,17 @@ async fn fetch_event_cap_counts_accepted_in_filter_events_and_preserves_later_co
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: wrong_tag.raw_json,
+ raw_json: wrong_tag.raw_json().to_owned(),
observed_at_ms: 1_100,
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: accepted.raw_json.clone(),
+ raw_json: accepted.raw_json().to_owned(),
observed_at_ms: 1_101,
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: skipped.raw_json,
+ raw_json: skipped.raw_json().to_owned(),
observed_at_ms: 1_102,
},
RadrootsRelayFetchItem::Eose {
@@ -1426,7 +1431,7 @@ async fn fetch_relay_events_applies_shared_filter_limit_and_outcome_evidence() {
vec!["soil".to_owned()],
)
.expect("filter");
- let accepted_id = accepted.id.clone();
+ let accepted_id = accepted.id_str().to_owned();
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
@@ -1435,17 +1440,17 @@ async fn fetch_relay_events_applies_shared_filter_limit_and_outcome_evidence() {
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: wrong_tag.raw_json,
+ raw_json: wrong_tag.raw_json().to_owned(),
observed_at_ms: 2_101,
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: accepted.raw_json.clone(),
+ raw_json: accepted.raw_json().to_owned(),
observed_at_ms: 2_102,
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: skipped.raw_json,
+ raw_json: skipped.raw_json().to_owned(),
observed_at_ms: 2_103,
},
RadrootsRelayFetchItem::Eose {
@@ -1496,7 +1501,7 @@ async fn fetch_relay_events_applies_shared_filter_limit_and_outcome_evidence() {
async fn fetch_raw_scan_limit_bounds_noisy_adapter_output() {
let accepted = signed_post("raw scan accepted event");
let wrong_tag = signed_event_with_kind_and_hashtag("raw scan wrong tag", KIND_POST, "compost");
- let accepted_id = accepted.id.clone();
+ let accepted_id = accepted.id_str().to_owned();
let store = RadrootsEventStore::open_memory().await.expect("store");
let adapter = RadrootsMockRelayFetchAdapter::new(vec![
RadrootsRelayFetchItem::Event {
@@ -1506,12 +1511,12 @@ async fn fetch_raw_scan_limit_bounds_noisy_adapter_output() {
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: wrong_tag.raw_json,
+ raw_json: wrong_tag.raw_json().to_owned(),
observed_at_ms: 1_131,
},
RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: accepted.raw_json,
+ raw_json: accepted.raw_json().to_owned(),
observed_at_ms: 1_132,
},
RadrootsRelayFetchItem::Eose {
@@ -1550,7 +1555,7 @@ async fn fetch_subscription_mode_and_store_errors_are_reported() {
let store = RadrootsEventStore::open_memory().await.expect("store");
let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: signed.raw_json.clone(),
+ raw_json: signed.raw_json().to_owned(),
observed_at_ms: 1_200,
}]);
@@ -1565,7 +1570,7 @@ async fn fetch_subscription_mode_and_store_errors_are_reported() {
assert_eq!(receipt.inserted_count, 1);
let observations = store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_eq!(observations.len(), 1);
@@ -1578,7 +1583,7 @@ async fn fetch_subscription_mode_and_store_errors_are_reported() {
closed_store.pool().close().await;
let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event {
relay_url: RELAY_PRIMARY_WSS.to_owned(),
- raw_json: signed.raw_json,
+ raw_json: signed.raw_json().to_owned(),
observed_at_ms: 1_210,
}]);
let receipt =
@@ -1598,7 +1603,7 @@ async fn fetch_ingest_rejects_invalid_observation_endpoint() {
let store = RadrootsEventStore::open_memory().await.expect("store");
let adapter = RadrootsMockRelayFetchAdapter::new(vec![RadrootsRelayFetchItem::Event {
relay_url: " ".to_owned(),
- raw_json: signed.raw_json,
+ raw_json: signed.raw_json().to_owned(),
observed_at_ms: 1_300,
}]);
@@ -1625,10 +1630,10 @@ async fn outbox_publish_persists_partial_success_and_skips_accepted_retry() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -1733,7 +1738,7 @@ async fn outbox_publish_persists_partial_success_and_skips_accepted_retry() {
.await
.expect("retry publish");
- assert_eq!(second.local_ingest.event_id, signed.id);
+ assert_eq!(second.local_ingest.event_id, signed.id_str());
assert_eq!(second.attempted_count, 1);
assert_eq!(retry_adapter.captured_raw_events().len(), 1);
@@ -1751,7 +1756,7 @@ async fn outbox_publish_persists_partial_success_and_skips_accepted_retry() {
assert_eq!(operation.status, RadrootsOutboxOperationStatus::Complete);
let observations = store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_outbox_publish_observations(&observations, 3);
@@ -1765,10 +1770,10 @@ async fn outbox_publish_fans_out_endpoint_receipts_to_scoped_logical_targets() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -1813,8 +1818,8 @@ async fn outbox_publish_fans_out_endpoint_receipts_to_scoped_logical_targets() {
.await
.expect("publish");
- assert_eq!(published.local_ingest.event_id, signed.id);
- assert_eq!(published.event_id, signed.id);
+ assert_eq!(published.local_ingest.event_id, signed.id_str());
+ assert_eq!(published.event_id, signed.id_str());
assert_eq!(published.attempted_count, 2);
assert_eq!(published.accepted_count, 2);
assert_eq!(published.retryable_count, 0);
@@ -1865,7 +1870,7 @@ async fn outbox_publish_fans_out_endpoint_receipts_to_scoped_logical_targets() {
&& target.target_label.as_ref().map(|label| label.as_str()) == Some("East foodshed")
}));
let observations = store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_outbox_publish_observations(&observations, 1);
@@ -1879,10 +1884,10 @@ async fn outbox_publish_required_target_failure_is_not_satisfied_by_optional_suc
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let optional = nostr_target(RELAY_PRIMARY_WSS);
@@ -1982,10 +1987,10 @@ async fn outbox_publish_required_target_success_is_not_blocked_by_optional_retry
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let optional = nostr_target(RELAY_PRIMARY_WSS);
@@ -2049,7 +2054,7 @@ async fn outbox_publish_required_target_success_is_not_blocked_by_optional_retry
.await
.expect("publish");
- assert_eq!(published.local_ingest.event_id, signed.id);
+ assert_eq!(published.local_ingest.event_id, signed.id_str());
assert_eq!(published.attempted_count, 1);
assert_eq!(published.accepted_count, 1);
assert_eq!(published.retryable_count, 0);
@@ -2074,7 +2079,7 @@ async fn outbox_publish_required_target_success_is_not_blocked_by_optional_retry
&& target.status == RadrootsOutboxDeliveryTargetStatus::Accepted
}));
let observations = store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_outbox_publish_observations(&observations, 1);
@@ -2088,10 +2093,10 @@ async fn outbox_publish_required_targets_fan_out_same_endpoint_scoped_receipts()
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let required = scoped_nostr_target(RELAY_PRIMARY_WSS, "foodshed.west", "West foodshed");
@@ -2178,10 +2183,10 @@ async fn outbox_transport_publish_failure_releases_retryable_claim() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2269,10 +2274,10 @@ async fn outbox_publish_marks_published_without_adapter_when_all_relays_already_
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2325,8 +2330,8 @@ async fn outbox_publish_marks_published_without_adapter_when_all_relays_already_
.await
.expect("publish");
- assert_eq!(published.local_ingest.event_id, signed.id);
- assert_eq!(published.event_id, signed.id);
+ assert_eq!(published.local_ingest.event_id, signed.id_str());
+ assert_eq!(published.event_id, signed.id_str());
assert_eq!(published.attempted_count, 0);
assert_eq!(published.accepted_count, 2);
assert_eq!(published.quorum, 0);
@@ -2358,10 +2363,10 @@ async fn outbox_publish_ignores_unknown_adapter_receipts() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2406,7 +2411,7 @@ async fn outbox_publish_ignores_unknown_adapter_receipts() {
.expect("event");
assert_eq!(event.state, RadrootsOutboxEventState::Published);
let observations = store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_outbox_publish_observations(&observations, 1);
@@ -2424,10 +2429,10 @@ async fn outbox_publish_skips_non_nostr_targets() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2498,10 +2503,10 @@ async fn outbox_publish_marks_published_when_delivery_plan_satisfaction_is_met_w
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2589,7 +2594,7 @@ async fn outbox_publish_marks_published_when_delivery_plan_satisfaction_is_met_w
);
let observations = store
- .observations_for_event(signed.id.as_str())
+ .observations_for_event(signed.id_str())
.await
.expect("observations");
assert_outbox_publish_observations(&observations, 2);
@@ -2603,10 +2608,10 @@ async fn outbox_publish_republishes_accepted_relays_when_policy_requests_it() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2654,7 +2659,7 @@ async fn outbox_publish_republishes_accepted_relays_when_policy_requests_it() {
.await
.expect("publish");
- assert_eq!(published.local_ingest.event_id, signed.id);
+ assert_eq!(published.local_ingest.event_id, signed.id_str());
assert_eq!(published.attempted_count, 2);
assert_eq!(published.accepted_count, 2);
assert_eq!(published.quorum, 1);
@@ -2686,10 +2691,10 @@ async fn outbox_publish_republish_policy_keeps_terminal_targets_excluded() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags.clone(),
- signed.content.clone(),
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2766,10 +2771,10 @@ async fn outbox_publish_requires_claimed_signed_event() {
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags,
- signed.content,
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2813,10 +2818,10 @@ async fn outbox_publish_propagates_non_transport_adapter_errors_after_target_fil
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags,
- signed.content,
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2870,10 +2875,10 @@ async fn outbox_publish_rejects_invalid_relay_target_uri_before_adapter_publish(
let draft = RadrootsEventDraft::new(
"radroots.social.post.v1",
KIND_POST,
- signed.created_at,
- signed.tags,
- signed.content,
- signed.pubkey.as_str(),
+ signed.created_at(),
+ signed.tags_as_vec(),
+ signed.content().to_owned(),
+ signed.pubkey_str(),
)
.expect("draft");
let receipt = outbox
@@ -2933,7 +2938,7 @@ async fn smoke_relay_fetch_processes_one_thousand_event_receipts() {
};
items.push(RadrootsRelayFetchItem::Event {
relay_url: relay_url.to_owned(),
- raw_json: signed.raw_json,
+ raw_json: signed.raw_json().to_owned(),
observed_at_ms: 10_000 + index,
});
}