lib

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

commit 6cbbe4045b8430bf9235973d8da7f7f8e71de1b5
parent 1e7f3d77a971cd4b77dad63d0488548d5f248494
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Apr 2026 03:04:54 +0000

sdk: enforce listing kind at parse boundaries

Diffstat:
Mcrates/events/src/trade.rs | 6++++++
Mcrates/sdk/src/adapters/radrootsd.rs | 6++++++
Mcrates/sdk/tests/facade.rs | 12++++++++++++
Mcrates/sdk/tests/radrootsd.rs | 17++++++++++++++++-
Mcrates/trade/src/listing/codec.rs | 3++-
Mcrates/trade/src/listing/mod.rs | 29++++++++++++++++++++++++++++-
6 files changed, 70 insertions(+), 3 deletions(-)

diff --git a/crates/events/src/trade.rs b/crates/events/src/trade.rs @@ -16,6 +16,7 @@ pub const RADROOTS_TRADE_ENVELOPE_VERSION: u16 = 1; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsTradeListingParseError { + InvalidKind(u32), MissingTag(String), InvalidTag(String), InvalidNumber(String), @@ -28,6 +29,7 @@ pub enum RadrootsTradeListingParseError { impl core::fmt::Display for RadrootsTradeListingParseError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { + Self::InvalidKind(kind) => write!(f, "invalid listing kind: {kind}"), Self::MissingTag(tag) => write!(f, "missing required tag: {tag}"), Self::InvalidTag(tag) => write!(f, "invalid tag: {tag}"), Self::InvalidNumber(field) => write!(f, "invalid number: {field}"), @@ -722,6 +724,10 @@ mod tests { #[test] fn listing_parse_error_display_variants() { assert_eq!( + RadrootsTradeListingParseError::InvalidKind(KIND_PROFILE).to_string(), + "invalid listing kind: 0" + ); + assert_eq!( RadrootsTradeListingParseError::MissingTag("price".into()).to_string(), "missing required tag: price" ); diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -4,6 +4,7 @@ use crate::RadrootsNostrEvent; use crate::config::RadrootsdAuth; use crate::listing; use crate::listing::RadrootsListing; +use radroots_events::kinds::KIND_LISTING; use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -35,6 +36,11 @@ impl SdkRadrootsdListingPublishRequest { signer_authority: Option<SdkRadrootsdSignerAuthority>, idempotency_key: Option<String>, ) -> Result<Self, listing::RadrootsTradeListingParseError> { + if event.kind != KIND_LISTING { + return Err(listing::RadrootsTradeListingParseError::InvalidKind( + event.kind, + )); + } Ok(Self { listing: listing::parse_event(event)?, kind: Some(event.kind), diff --git a/crates/sdk/tests/facade.rs b/crates/sdk/tests/facade.rs @@ -159,6 +159,18 @@ fn listing_facade_wraps_build_parse_and_validate() { } #[test] +fn listing_parse_rejects_non_listing_kind() { + let listing_value = sample_listing(); + let mut event = listing_event(&listing_value); + event.kind = KIND_PROFILE; + + assert!(matches!( + listing::parse_event(&event), + Err(listing::RadrootsTradeListingParseError::InvalidKind(KIND_PROFILE)) + )); +} + +#[test] fn trade_facade_wraps_build_parse_and_address_ops() { let listing_value = sample_listing(); let listing_addr = format!("{KIND_LISTING}:seller:{}", listing_value.d_tag); diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -4,10 +4,11 @@ use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; +use radroots_events::kinds::KIND_LISTING_DRAFT; use radroots_sdk::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingFarmRef, RadrootsListingLocation, - RadrootsListingProduct, RadrootsListingStatus, + RadrootsListingProduct, RadrootsListingStatus, RadrootsTradeListingParseError, }; use radroots_sdk::{ RadrootsNostrEvent, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, @@ -431,3 +432,17 @@ async fn radrootsd_listing_publish_rejects_relay_transport_mode() -> TestResult< Ok(()) } + +#[test] +fn radrootsd_listing_request_from_event_rejects_listing_draft_kind() -> TestResult<()> { + let draft = radroots_sdk::listing::build_draft(&sample_listing())?; + let mut event = sdk_event("seller", 1_720_000_000, draft); + event.kind = KIND_LISTING_DRAFT; + + assert!(matches!( + SdkRadrootsdListingPublishRequest::from_event(&event, "session-123", None, None), + Err(RadrootsTradeListingParseError::InvalidKind(KIND_LISTING_DRAFT)) + )); + + Ok(()) +} diff --git a/crates/trade/src/listing/codec.rs b/crates/trade/src/listing/codec.rs @@ -154,7 +154,7 @@ fn map_listing_tags_error(err: EventEncodeError) -> TradeListingParseError { TradeListingParseError::InvalidTag(field.to_string()) } EventEncodeError::Json => TradeListingParseError::InvalidJson("discount".to_string()), - EventEncodeError::InvalidKind(_) => TradeListingParseError::InvalidTag("kind".to_string()), + EventEncodeError::InvalidKind(kind) => TradeListingParseError::InvalidKind(kind), } } @@ -698,6 +698,7 @@ mod tests { fn parse_error_tag(error: TradeListingParseError) -> String { match error { + TradeListingParseError::InvalidKind(_) => "kind".to_string(), TradeListingParseError::MissingTag(tag) => tag, TradeListingParseError::InvalidTag(tag) => tag, TradeListingParseError::InvalidNumber(field) => field, diff --git a/crates/trade/src/listing/mod.rs b/crates/trade/src/listing/mod.rs @@ -7,7 +7,7 @@ pub mod projection; pub mod publish; pub mod validation; -use radroots_events::{RadrootsNostrEvent, listing::RadrootsListing}; +use radroots_events::{RadrootsNostrEvent, kinds::is_listing_kind, listing::RadrootsListing}; pub(crate) use self::contract as dvm; #[allow(unused_imports)] @@ -18,5 +18,32 @@ pub use radroots_events::trade::RadrootsTradeListingParseError as TradeListingPa pub fn parse_listing_event( event: &RadrootsNostrEvent, ) -> Result<RadrootsListing, TradeListingParseError> { + if !is_listing_kind(event.kind) { + return Err(TradeListingParseError::InvalidKind(event.kind)); + } self::codec::listing_from_event_parts(&event.tags, &event.content) } + +#[cfg(test)] +mod tests { + use super::parse_listing_event; + use radroots_events::{RadrootsNostrEvent, kinds::KIND_PROFILE, trade::RadrootsTradeListingParseError}; + + #[test] + fn parse_listing_event_rejects_non_listing_kind() { + let event = RadrootsNostrEvent { + id: "event-1".into(), + author: "seller".into(), + created_at: 1, + kind: KIND_PROFILE, + tags: vec![], + content: String::new(), + sig: String::new(), + }; + + assert!(matches!( + parse_listing_event(&event), + Err(RadrootsTradeListingParseError::InvalidKind(KIND_PROFILE)) + )); + } +}