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:
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))
+ ));
+ }
+}