lib

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

commit ab1eb2941dee184bf7a8c2d01abf01f3f51bcd5e
parent 6ab7fe71a96aa8741f95eaf0c8b577622f0091c7
Author: triesap <tyson@radroots.org>
Date:   Mon,  6 Jul 2026 08:16:26 +0000

validation: close final runtime gates

- Remove clippy-only allocations from event contract tag validation tests.
- Build frozen listing mutation drafts through codec-owned JSON wire parts.
- Align authority and event-ref validation fixtures with strict draft contracts.
- Keep release-preflight coverage gates green for final closeout.

Diffstat:
Mcrates/authority/src/authorization.rs | 10+++++++++-
Mcrates/events/src/contract.rs | 8++++----
Mcrates/events_codec/src/event_ref.rs | 23+++++++++++------------
Mcrates/events_codec/src/listing/encode.rs | 17+++++++++++++++++
Mcrates/events_codec/tests/listing.rs | 36+++++++++++++++++++++++++++++++++++-
Mcrates/trade/src/listing/mutation.rs | 10++++++++--
6 files changed, 84 insertions(+), 20 deletions(-)

diff --git a/crates/authority/src/authorization.rs b/crates/authority/src/authorization.rs @@ -607,7 +607,15 @@ mod tests { "radroots.order.request.v1", KIND_ORDER_REQUEST, 1_700_000_000, - Vec::new(), + vec![ + vec!["d".to_owned(), "order-1".to_owned()], + vec!["p".to_owned(), pubkey.clone()], + vec![ + "a".to_owned(), + format!("30402:{}:listing-a", pubkey.as_str()), + ], + vec!["listing_event".to_owned(), hex_64('b')], + ], "{}", pubkey.as_str(), ) diff --git a/crates/events/src/contract.rs b/crates/events/src/contract.rs @@ -4344,7 +4344,7 @@ mod tests { ); assert_eq!( validate_contract_tags_parts( - &vec![vec!["test_many".to_owned(), "one".to_owned()]], + &[vec!["test_many".to_owned(), "one".to_owned()]], &required_many ), Ok(()) @@ -4355,7 +4355,7 @@ mod tests { assert_eq!(validate_contract_tags_parts(&[], &optional_one), Ok(())); assert_eq!( validate_contract_tags_parts( - &vec![ + &[ vec!["test_optional".to_owned(), "one".to_owned()], vec!["test_optional".to_owned(), "two".to_owned()], ], @@ -4373,7 +4373,7 @@ mod tests { ); assert_eq!( validate_contract_tags_parts( - &vec![ + &[ vec!["test_required".to_owned(), "one".to_owned()], vec!["test_required".to_owned(), "two".to_owned()], ], @@ -4388,7 +4388,7 @@ mod tests { ); assert_eq!( validate_contract_tags_parts( - &vec![ + &[ vec!["test_optional".to_owned(), "one".to_owned()], vec!["test_optional".to_owned(), "two".to_owned()], ], diff --git a/crates/events_codec/src/event_ref.rs b/crates/events_codec/src/event_ref.rs @@ -36,25 +36,24 @@ pub fn parse_event_ref_tag( if tag.len() < 5 { return Err(EventParseError::InvalidTag(tag_name)); } - let id = tag.get(1).ok_or(EventParseError::InvalidTag(tag_name))?; + let id = &tag[1]; RadrootsEventId::parse(id).map_err(|_| EventParseError::InvalidTag(tag_name))?; - let author = tag.get(2).ok_or(EventParseError::InvalidTag(tag_name))?; + let author = &tag[2]; RadrootsPublicKey::parse(author).map_err(|_| EventParseError::InvalidTag(tag_name))?; - let kind_s = tag.get(3).ok_or(EventParseError::InvalidTag(tag_name))?; + let kind_s = &tag[3]; let kind: u32 = kind_s .parse() .map_err(|e| EventParseError::InvalidNumber(tag_name, e))?; - let d_tag = match tag.get(4) { - Some(value) if value.is_empty() => None, - Some(value) => { - if RadrootsRelayUrl::parse(value).is_ok() { - return Err(EventParseError::InvalidTag(tag_name)); - } - RadrootsDTag::parse(value).map_err(|_| EventParseError::InvalidTag(tag_name))?; - Some(value.clone()) + let d_tag_value = &tag[4]; + let d_tag = if d_tag_value.is_empty() { + None + } else { + if RadrootsRelayUrl::parse(d_tag_value).is_ok() { + return Err(EventParseError::InvalidTag(tag_name)); } - None => return Err(EventParseError::InvalidTag(tag_name)), + RadrootsDTag::parse(d_tag_value).map_err(|_| EventParseError::InvalidTag(tag_name))?; + Some(d_tag_value.clone()) }; let relays = if tag.len() > 5 { diff --git a/crates/events_codec/src/listing/encode.rs b/crates/events_codec/src/listing/encode.rs @@ -46,6 +46,23 @@ pub fn to_wire_parts_with_kind( } #[cfg(feature = "serde_json")] +pub fn to_json_wire_parts_with_kind( + listing: &RadrootsListing, + kind: u32, +) -> Result<WireEventParts, EventEncodeError> { + if !is_listing_kind(kind) { + return Err(EventEncodeError::InvalidKind(kind)); + } + let tags = listing_tags_full(listing)?; + let content = serde_json::to_string(listing).map_err(|_| EventEncodeError::Json)?; + Ok(WireEventParts { + kind, + content, + tags, + }) +} + +#[cfg(feature = "serde_json")] fn listing_markdown_content(listing: &RadrootsListing) -> String { let title = listing.product.title.trim(); let summary = listing diff --git a/crates/events_codec/tests/listing.rs b/crates/events_codec/tests/listing.rs @@ -27,7 +27,7 @@ use radroots_events_codec::listing::decode::{ parsed_from_nostr_event, }; use radroots_events_codec::listing::encode::{ - listing_build_tags, to_wire_parts, to_wire_parts_with_kind, + listing_build_tags, to_json_wire_parts_with_kind, to_wire_parts, to_wire_parts_with_kind, }; use radroots_events_codec::listing::tags::{ ListingTagOptions, listing_tags_full, listing_tags_with_options, @@ -219,6 +219,40 @@ fn listing_roundtrip_from_event() { } #[test] +fn listing_json_wire_parts_with_kind_serializes_listing_object() { + let listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg"); + let parts = to_json_wire_parts_with_kind(&listing, KIND_LISTING_DRAFT).unwrap(); + + assert_eq!(parts.kind, KIND_LISTING_DRAFT); + let decoded: RadrootsListing = serde_json::from_str(&parts.content).unwrap(); + assert_eq!(decoded.d_tag, listing.d_tag); + assert_eq!(decoded.product.title, listing.product.title); + assert_eq!(decoded.primary_bin_id, listing.primary_bin_id); +} + +#[test] +fn listing_json_wire_parts_with_kind_rejects_wrong_kind() { + let err = to_json_wire_parts_with_kind(&sample_listing("AAAAAAAAAAAAAAAAAAAAAg"), KIND_POST) + .unwrap_err(); + assert!(matches!(err, EventEncodeError::InvalidKind(KIND_POST))); +} + +#[test] +fn listing_json_wire_parts_with_kind_reports_tag_errors() { + let mut listing = sample_listing("AAAAAAAAAAAAAAAAAAAAAg"); + listing.resource_area = Some(RadrootsResourceAreaRef { + pubkey: "resource_pubkey".to_string(), + d_tag: "bad d".to_string(), + }); + + let err = to_json_wire_parts_with_kind(&listing, KIND_LISTING).unwrap_err(); + assert!(matches!( + err, + EventEncodeError::InvalidField("resource_area.d_tag") + )); +} + +#[test] fn listing_from_event_reconstructs_from_tags_with_markdown_content() { let listing = sample_listing_full("FAAAAAAAAAAAAAAAAAAAAA"); let tags = listing_build_tags(&listing).unwrap(); diff --git a/crates/trade/src/listing/mutation.rs b/crates/trade/src/listing/mutation.rs @@ -21,7 +21,7 @@ use radroots_events::{ kinds::{KIND_LISTING, KIND_LISTING_DRAFT}, }; #[cfg(feature = "serde_json")] -use radroots_events_codec::{listing::encode::to_wire_parts_with_kind, wire::to_frozen_draft}; +use radroots_events_codec::{listing::encode::to_json_wire_parts_with_kind, wire::to_frozen_draft}; use thiserror::Error; use crate::listing::draft::RadrootsCanonicalListingDraft; @@ -133,7 +133,7 @@ pub fn build_listing_mutation_draft( return Err(RadrootsListingMutationError::UnsupportedMutation); } }; - let parts = to_wire_parts_with_kind(draft.listing(), kind) + let parts = to_json_wire_parts_with_kind(draft.listing(), kind) .map_err(|error| RadrootsListingMutationError::EncodeListing(error.to_string()))?; to_frozen_draft( parts, @@ -368,6 +368,9 @@ mod tests { 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); + let published_content: RadrootsListing = + 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); @@ -383,6 +386,9 @@ mod tests { assert_eq!(draft.contract_id, LISTING_DRAFT_CONTRACT_ID); assert_eq!(draft.expected_pubkey, SELLER); assert_eq!(draft.created_at, 1_700_000_000); + let draft_content: RadrootsListing = + serde_json::from_str(&draft.content).expect("listing json"); + assert_eq!(draft_content.d_tag.as_str(), "AAAAAAAAAAAAAAAAAAAAAg"); } #[test]