lib

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

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:
Mcrates/event_store/src/model.rs | 1+
Mcrates/event_store/src/store.rs | 57++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mcrates/events/src/contract.rs | 17++++++++++++++++-
Mcrates/events_codec/src/order/decode.rs | 6+++++-
Mcrates/events_codec/src/order/tags.rs | 104++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
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, }) );