commit 6eb503f43415039b5c70bbd97e0f64f6cfe19329
parent d4ffe3cac0fdcd95830098427b42d3c89df30f1f
Author: triesap <tyson@radroots.org>
Date: Sat, 13 Jun 2026 04:13:19 -0700
events: type listing event pointers
- add an EventPointer tag value type for listing_event contract metadata
- persist listing_event tag metadata as event_pointer in the event store
- validate order listing_event pointers with typed event id and relay hint checks
- cover contract, codec, and store metadata behavior with focused tests
Diffstat:
5 files changed, 146 insertions(+), 39 deletions(-)
diff --git a/crates/event_store/src/model.rs b/crates/event_store/src/model.rs
@@ -321,6 +321,7 @@ pub fn tag_value_type_name(value: RadrootsTagValueType) -> &'static str {
RadrootsTagValueType::AddressableCoordinate => "addressable_coordinate",
RadrootsTagValueType::DTag => "d_tag",
RadrootsTagValueType::EventId => "event_id",
+ RadrootsTagValueType::EventPointer => "event_pointer",
RadrootsTagValueType::Kind => "kind",
RadrootsTagValueType::PublicKey => "public_key",
RadrootsTagValueType::RelayUrl => "relay_url",
diff --git a/crates/event_store/src/store.rs b/crates/event_store/src/store.rs
@@ -716,7 +716,7 @@ fn bool_i64(value: bool) -> i64 {
mod tests {
use super::*;
use radroots_events::event_head::event_head_candidate_for_event;
- use radroots_events::kinds::{KIND_POST, KIND_PROFILE};
+ use radroots_events::kinds::{KIND_LISTING, KIND_ORDER_REQUEST, KIND_POST, KIND_PROFILE};
use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp,
radroots_event_from_nostr, radroots_nostr_build_event,
@@ -733,6 +733,10 @@ mod tests {
RadrootsNostrKeys::new(secret_key)
}
+ fn event_id(character: char) -> String {
+ core::iter::repeat_n(character, 64).collect()
+ }
+
fn signed_event(
kind: u32,
created_at: u32,
@@ -968,6 +972,57 @@ mod tests {
}
#[tokio::test]
+ async fn listing_event_tag_persists_event_pointer_contract_metadata() {
+ let store = RadrootsEventStore::open_memory().await.expect("open");
+ let listing_event_id = event_id('f');
+ let event = signed_event(
+ KIND_ORDER_REQUEST,
+ 16,
+ vec![
+ vec!["d".to_owned(), "order-1".to_owned()],
+ vec!["p".to_owned(), FIXTURE_ALICE_PUBLIC_KEY_HEX.to_owned()],
+ vec![
+ "a".to_owned(),
+ format!(
+ "{KIND_LISTING}:{}:AAAAAAAAAAAAAAAAAAAAAg",
+ FIXTURE_ALICE_PUBLIC_KEY_HEX
+ ),
+ ],
+ vec![
+ "listing_event".to_owned(),
+ listing_event_id.clone(),
+ "wss://relay.example.com".to_owned(),
+ ],
+ ],
+ "{}",
+ );
+
+ store
+ .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 listing_tag = tags
+ .iter()
+ .find(|tag| tag.tag_name == "listing_event")
+ .expect("listing event tag");
+
+ assert_eq!(
+ listing_tag.tag_value.as_deref(),
+ Some(listing_event_id.as_str())
+ );
+ assert_eq!(
+ listing_tag.contract_semantic.as_deref(),
+ Some("listing_snapshot")
+ );
+ assert_eq!(
+ listing_tag.contract_value_type.as_deref(),
+ Some("event_pointer")
+ );
+ assert!(!listing_tag.relay_indexed);
+ }
+
+ #[tokio::test]
async fn relay_observations_upsert_separately_from_event_identity() {
let store = RadrootsEventStore::open_memory().await.expect("open");
let event = signed_event(KIND_POST, 15, Vec::new(), "hello");
diff --git a/crates/events/src/contract.rs b/crates/events/src/contract.rs
@@ -130,6 +130,7 @@ pub enum RadrootsTagValueType {
AddressableCoordinate,
DTag,
EventId,
+ EventPointer,
Kind,
PublicKey,
RelayUrl,
@@ -351,7 +352,7 @@ const TAG_LISTING_EVENT: RadrootsTagContract = tag(
"listing_event",
RadrootsTagCardinality::RequiredOne,
RadrootsTagSemantic::ListingSnapshot,
- RadrootsTagValueType::EventId,
+ RadrootsTagValueType::EventPointer,
false,
);
const TAG_SERVICE_INPUT: RadrootsTagContract = tag(
@@ -2677,6 +2678,20 @@ mod tests {
}
#[test]
+ fn order_request_listing_event_contract_is_event_pointer() {
+ let contract = event_contract("radroots.order.request.v1").expect("order request");
+ let tag = contract
+ .tags
+ .iter()
+ .find(|tag| tag.name == "listing_event")
+ .expect("listing event tag");
+
+ assert_eq!(tag.semantic, RadrootsTagSemantic::ListingSnapshot);
+ assert_eq!(tag.value_type, RadrootsTagValueType::EventPointer);
+ assert!(!tag.relay_indexed);
+ }
+
+ #[test]
fn covers_public_kind_arrays() {
for kind in COMMERCIAL_EVENT_KINDS
.iter()
diff --git a/crates/events_codec/src/order/decode.rs b/crates/events_codec/src/order/decode.rs
@@ -842,9 +842,13 @@ mod tests {
}
}
+ fn event_id(character: char) -> String {
+ core::iter::repeat_n(character, 64).collect()
+ }
+
fn listing_event_ptr() -> RadrootsNostrEventPtr {
RadrootsNostrEventPtr {
- id: "listing-snapshot".into(),
+ id: event_id('a'),
relays: Some("wss://relay.example.com".into()),
}
}
diff --git a/crates/events_codec/src/order/tags.rs b/crates/events_codec/src/order/tags.rs
@@ -3,6 +3,7 @@ use alloc::{borrow::ToOwned, string::String, vec::Vec};
use radroots_events::{
RadrootsNostrEventPtr,
+ ids::{RadrootsEventId, RadrootsNostrEventPointer},
tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT},
};
@@ -29,13 +30,17 @@ fn build_event_ptr_tag(
if ptr.id.trim().is_empty() {
return Err(EventEncodeError::EmptyRequiredField(field_prefix));
}
+ let event_id = RadrootsEventId::parse(ptr.id.as_str())
+ .map_err(|_| EventEncodeError::InvalidField(field_prefix))?;
let mut tag = Vec::with_capacity(3);
tag.push(name.to_owned());
- tag.push(ptr.id.clone());
+ tag.push(event_id.as_str().to_owned());
if let Some(relay) = &ptr.relays {
if relay.trim().is_empty() {
return Err(EventEncodeError::EmptyRequiredField("listing_event.relays"));
}
+ RadrootsNostrEventPointer::new(event_id, [relay.as_str()])
+ .map_err(|_| EventEncodeError::InvalidField("listing_event.relays"))?;
tag.push(relay.clone());
}
Ok(tag)
@@ -55,13 +60,19 @@ fn parse_event_ptr_tag(
if id.trim().is_empty() {
return Err(EventParseError::InvalidTag(name));
}
+ let event_id =
+ RadrootsEventId::parse(id.as_str()).map_err(|_| EventParseError::InvalidTag(name))?;
let relay = match tag.get(2) {
Some(value) if value.trim().is_empty() => return Err(EventParseError::InvalidTag(name)),
Some(value) => Some(value.clone()),
None => None,
};
+ if let Some(relay) = relay.as_ref() {
+ RadrootsNostrEventPointer::new(event_id.clone(), [relay.as_str()])
+ .map_err(|_| EventParseError::InvalidTag(name))?;
+ }
Ok(Some(RadrootsNostrEventPtr {
- id: id.clone(),
+ id: event_id.as_str().to_owned(),
relays: relay,
}))
}
@@ -250,6 +261,10 @@ mod tests {
tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT},
};
+ fn event_id(character: char) -> String {
+ core::iter::repeat_n(character, 64).collect()
+ }
+
#[test]
fn order_envelope_tags_build_expected_tags() {
let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg");
@@ -271,28 +286,27 @@ mod tests {
}
#[test]
- fn order_envelope_tags_include_snapshot_and_chain_refs() {
+ fn order_envelope_tags_include_listing_event_pointer_and_chain_refs() {
let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg");
+ let listing_event_id = event_id('a');
let tags = order_envelope_tags(
"buyer",
listing_addr.as_str(),
Some("order-1"),
Some(&RadrootsNostrEventPtr {
- id: "listing-snapshot".into(),
+ id: listing_event_id.clone(),
relays: Some("wss://relay.example".into()),
}),
Some("root-event"),
Some("prev-event"),
)
.expect("trade tags");
- assert!(tags.iter().any(|tag| {
- tag.as_slice()
- == [
- TAG_LISTING_EVENT.to_string(),
- "listing-snapshot".to_string(),
- "wss://relay.example".to_string(),
- ]
- }));
+ let expected_listing_event = vec![
+ TAG_LISTING_EVENT.to_string(),
+ listing_event_id,
+ "wss://relay.example".to_string(),
+ ];
+ assert!(tags.iter().any(|tag| tag == &expected_listing_event));
assert!(
tags.iter().any(|tag| {
tag.as_slice() == [TAG_E_ROOT.to_string(), "root-event".to_string()]
@@ -306,14 +320,15 @@ mod tests {
}
#[test]
- fn order_envelope_tags_support_snapshot_without_relay() {
+ fn order_envelope_tags_support_listing_event_without_relay() {
let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg");
+ let listing_event_id = event_id('b');
let tags = order_envelope_tags(
"buyer",
listing_addr.as_str(),
None::<&str>,
Some(&RadrootsNostrEventPtr {
- id: "listing-snapshot".into(),
+ id: listing_event_id.clone(),
relays: None,
}),
Some("root-event"),
@@ -325,10 +340,7 @@ mod tests {
vec![
vec![String::from("p"), String::from("buyer")],
vec![String::from("a"), listing_addr],
- vec![
- String::from(TAG_LISTING_EVENT),
- String::from("listing-snapshot"),
- ],
+ vec![String::from(TAG_LISTING_EVENT), listing_event_id],
vec![String::from(TAG_E_ROOT), String::from("root-event")],
]
);
@@ -359,14 +371,15 @@ mod tests {
}
#[test]
- fn order_envelope_tags_accept_str_listing_address_with_snapshot_only() {
+ fn order_envelope_tags_accept_str_listing_address_with_listing_event_only() {
let listing_addr = format!("{KIND_LISTING}:pubkey:AAAAAAAAAAAAAAAAAAAAAg");
+ let listing_event_id = event_id('c');
let tags = order_envelope_tags(
"buyer",
listing_addr.as_str(),
None::<&str>,
Some(&RadrootsNostrEventPtr {
- id: "listing-snapshot".into(),
+ id: listing_event_id.clone(),
relays: None,
}),
None,
@@ -378,10 +391,7 @@ mod tests {
vec![
vec![String::from("p"), String::from("buyer")],
vec![String::from("a"), listing_addr],
- vec![
- String::from(TAG_LISTING_EVENT),
- String::from("listing-snapshot"),
- ],
+ vec![String::from(TAG_LISTING_EVENT), listing_event_id],
]
);
}
@@ -433,7 +443,24 @@ mod tests {
listing_addr.as_str(),
None::<&str>,
Some(&RadrootsNostrEventPtr {
- id: "listing-snapshot".into(),
+ id: "not-an-event-id".into(),
+ relays: None,
+ }),
+ None,
+ None,
+ )
+ .expect_err("invalid listing snapshot id");
+ assert!(matches!(
+ err,
+ EventEncodeError::InvalidField("listing_event.id")
+ ));
+
+ let err = order_envelope_tags(
+ "buyer",
+ listing_addr.as_str(),
+ None::<&str>,
+ Some(&RadrootsNostrEventPtr {
+ id: event_id('d'),
relays: Some(" ".into()),
}),
None,
@@ -475,12 +502,13 @@ mod tests {
}
#[test]
- fn order_envelope_tag_parsers_cover_public_context() {
+ fn order_envelope_tag_parsers_cover_listing_event_public_context() {
+ let listing_event_id = event_id('e');
let tags = vec![
vec!["p".into(), "counterparty".into()],
vec![
TAG_LISTING_EVENT.into(),
- "snapshot".into(),
+ listing_event_id.clone(),
"wss://relay".into(),
],
vec![TAG_E_ROOT.into(), "root".into()],
@@ -493,7 +521,7 @@ mod tests {
assert_eq!(
parse_order_listing_event_tag(&tags).expect("snapshot"),
Some(RadrootsNostrEventPtr {
- id: "snapshot".into(),
+ id: listing_event_id,
relays: Some("wss://relay".into()),
})
);
@@ -508,7 +536,7 @@ mod tests {
}
#[test]
- fn order_envelope_tag_parsers_cover_missing_and_invalid_context() {
+ fn order_envelope_tag_parsers_reject_invalid_listing_event_context() {
assert_eq!(
parse_order_listing_event_tag(&[]).expect("no snapshot"),
None
@@ -543,19 +571,23 @@ mod tests {
assert!(matches!(
parse_order_listing_event_tag(&[vec![
String::from(TAG_LISTING_EVENT),
- String::from("snapshot"),
- String::from(" "),
+ String::from("not-an-event-id"),
]]),
Err(EventParseError::InvalidTag(TAG_LISTING_EVENT))
));
- assert_eq!(
+ assert!(matches!(
parse_order_listing_event_tag(&[vec![
String::from(TAG_LISTING_EVENT),
- String::from("snapshot"),
- ]])
- .expect("snapshot without relay"),
+ event_id('f'),
+ String::from(" "),
+ ]]),
+ Err(EventParseError::InvalidTag(TAG_LISTING_EVENT))
+ ));
+ assert_eq!(
+ parse_order_listing_event_tag(&[vec![String::from(TAG_LISTING_EVENT), event_id('a'),]])
+ .expect("snapshot without relay"),
Some(RadrootsNostrEventPtr {
- id: "snapshot".into(),
+ id: event_id('a'),
relays: None,
})
);