commit fab7ed497fd658df947cd8794fac9b62e80ebb4c
parent 52e6cdecf8690e395ec9f38ec65f3f3a0123bcf1
Author: triesap <tyson@radroots.org>
Date: Mon, 13 Apr 2026 15:10:20 +0000
sdk: validate public trade publish requests
Diffstat:
4 files changed, 559 insertions(+), 30 deletions(-)
diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs
@@ -170,6 +170,128 @@ pub struct SdkRadrootsdPublicTradePublishRequest {
pub payload: trade::RadrootsTradeMessagePayload,
}
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SdkRadrootsdPublicTradeRoute {
+ listing_addr: String,
+ order_id: String,
+ counterparty_pubkey: String,
+}
+
+impl SdkRadrootsdPublicTradeRoute {
+ pub fn new(
+ listing_addr: impl Into<String>,
+ order_id: impl Into<String>,
+ counterparty_pubkey: impl Into<String>,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ let listing_addr = normalize_required_string(listing_addr, "listing_addr")?;
+ let order_id = normalize_required_string(order_id, "order_id")?;
+ let counterparty_pubkey =
+ normalize_required_string(counterparty_pubkey, "counterparty_pubkey")?;
+ Ok(Self {
+ listing_addr,
+ order_id,
+ counterparty_pubkey,
+ })
+ }
+
+ fn listing_addr(&self) -> &str {
+ self.listing_addr.as_str()
+ }
+
+ fn order_id(&self) -> &str {
+ self.order_id.as_str()
+ }
+
+ fn counterparty_pubkey(&self) -> &str {
+ self.counterparty_pubkey.as_str()
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SdkRadrootsdTradeChain {
+ root_event_id: String,
+ prev_event_id: String,
+}
+
+impl SdkRadrootsdTradeChain {
+ pub fn new(
+ root_event_id: impl Into<String>,
+ prev_event_id: impl Into<String>,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ let root_event_id = normalize_required_string(root_event_id, "root_event_id")?;
+ let prev_event_id = normalize_required_string(prev_event_id, "prev_event_id")?;
+ Ok(Self {
+ root_event_id,
+ prev_event_id,
+ })
+ }
+
+ fn root_event_id(&self) -> &str {
+ self.root_event_id.as_str()
+ }
+
+ fn prev_event_id(&self) -> &str {
+ self.prev_event_id.as_str()
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum SdkRadrootsdPublicTradePublishValidationError {
+ UnsupportedPayload(trade::RadrootsTradeMessageType),
+ MissingListingSnapshot(trade::RadrootsTradeMessageType),
+ ListingSnapshotRelaysEmpty,
+ MissingTradeChain(trade::RadrootsTradeMessageType),
+ InvalidOrderRevisionAcceptPayload,
+ InvalidOrderRevisionDeclinePayload,
+ InvalidDiscountAcceptPayload,
+ InvalidDiscountDeclinePayload,
+ EmptyField(&'static str),
+}
+
+impl fmt::Display for SdkRadrootsdPublicTradePublishValidationError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::UnsupportedPayload(message_type) => match message_type {
+ trade::RadrootsTradeMessageType::OrderRequest => f.write_str(
+ "trade public publish does not support payload `OrderRequest`; use trade.publish_order_request_via_radrootsd for order requests",
+ ),
+ trade::RadrootsTradeMessageType::ListingValidateRequest
+ | trade::RadrootsTradeMessageType::ListingValidateResult => f.write_str(
+ "trade public publish does not support listing validation payloads; use trade.validate_listing_event for listing validation",
+ ),
+ _ => write!(
+ f,
+ "trade public publish does not support payload `{message_type:?}`",
+ ),
+ },
+ Self::MissingListingSnapshot(message_type) => write!(
+ f,
+ "trade public publish requires listing_event for `{message_type:?}`"
+ ),
+ Self::ListingSnapshotRelaysEmpty => {
+ f.write_str("trade public publish listing_event.relays must not be empty")
+ }
+ Self::MissingTradeChain(message_type) => write!(
+ f,
+ "trade public publish requires root_event_id and prev_event_id for `{message_type:?}`"
+ ),
+ Self::InvalidOrderRevisionAcceptPayload => f.write_str(
+ "trade public publish order revision accept payload must set accepted = true",
+ ),
+ Self::InvalidOrderRevisionDeclinePayload => f.write_str(
+ "trade public publish order revision decline payload must set accepted = false",
+ ),
+ Self::InvalidDiscountAcceptPayload => f.write_str(
+ "trade public publish discount accept payload must be an accept decision",
+ ),
+ Self::InvalidDiscountDeclinePayload => f.write_str(
+ "trade public publish discount decline payload must be a decline decision",
+ ),
+ Self::EmptyField(field) => write!(f, "trade public publish field `{field}` must not be empty"),
+ }
+ }
+}
+
impl SdkRadrootsdPublicTradePublishRequest {
pub fn new(
listing_addr: impl Into<String>,
@@ -249,6 +371,283 @@ impl SdkRadrootsdPublicTradePublishRequest {
}
}
}
+
+ pub fn order_response(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeOrderResponse,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::OrderResponse(payload),
+ )
+ }
+
+ pub fn order_revision(
+ route: &SdkRadrootsdPublicTradeRoute,
+ listing_event: RadrootsNostrEventPtr,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeOrderRevision,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ Some(listing_event),
+ trade::RadrootsTradeMessagePayload::OrderRevision(payload),
+ )
+ }
+
+ pub fn order_revision_accept(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeOrderRevisionResponse,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::OrderRevisionAccept(payload),
+ )
+ }
+
+ pub fn order_revision_decline(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeOrderRevisionResponse,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::OrderRevisionDecline(payload),
+ )
+ }
+
+ pub fn question(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeQuestion,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::Question(payload),
+ )
+ }
+
+ pub fn answer(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeAnswer,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::Answer(payload),
+ )
+ }
+
+ pub fn discount_request(
+ route: &SdkRadrootsdPublicTradeRoute,
+ listing_event: RadrootsNostrEventPtr,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeDiscountRequest,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ Some(listing_event),
+ trade::RadrootsTradeMessagePayload::DiscountRequest(payload),
+ )
+ }
+
+ pub fn discount_offer(
+ route: &SdkRadrootsdPublicTradeRoute,
+ listing_event: RadrootsNostrEventPtr,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeDiscountOffer,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ Some(listing_event),
+ trade::RadrootsTradeMessagePayload::DiscountOffer(payload),
+ )
+ }
+
+ pub fn discount_accept(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeDiscountDecision,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::DiscountAccept(payload),
+ )
+ }
+
+ pub fn discount_decline(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeDiscountDecision,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::DiscountDecline(payload),
+ )
+ }
+
+ pub fn cancel(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeListingCancel,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::Cancel(payload),
+ )
+ }
+
+ pub fn fulfillment_update(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeFulfillmentUpdate,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::FulfillmentUpdate(payload),
+ )
+ }
+
+ pub fn receipt(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: &SdkRadrootsdTradeChain,
+ payload: trade::RadrootsTradeReceipt,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ Self::from_components(
+ route,
+ Some(chain),
+ None,
+ trade::RadrootsTradeMessagePayload::Receipt(payload),
+ )
+ }
+
+ pub fn validate_for_publish(
+ &self,
+ ) -> Result<trade::RadrootsTradeMessageType, SdkRadrootsdPublicTradePublishValidationError>
+ {
+ normalize_required_string(self.listing_addr.as_str(), "listing_addr")?;
+ normalize_required_string(self.order_id.as_str(), "order_id")?;
+ normalize_required_string(self.counterparty_pubkey.as_str(), "counterparty_pubkey")?;
+
+ let message_type = self.payload.message_type();
+ if !message_type.is_public()
+ || matches!(message_type, trade::RadrootsTradeMessageType::OrderRequest)
+ {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::UnsupportedPayload(message_type),
+ );
+ }
+
+ if message_type.requires_listing_snapshot() {
+ let Some(listing_event) = self.listing_event.as_ref() else {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::MissingListingSnapshot(
+ message_type,
+ ),
+ );
+ };
+ if listing_event
+ .relays
+ .as_ref()
+ .is_some_and(|relay| relay.trim().is_empty())
+ {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::ListingSnapshotRelaysEmpty,
+ );
+ }
+ }
+
+ if message_type.requires_trade_chain() {
+ let Some(root_event_id) = self.root_event_id.as_deref() else {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::MissingTradeChain(message_type),
+ );
+ };
+ let Some(prev_event_id) = self.prev_event_id.as_deref() else {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::MissingTradeChain(message_type),
+ );
+ };
+ normalize_required_string(root_event_id, "root_event_id")?;
+ normalize_required_string(prev_event_id, "prev_event_id")?;
+ }
+
+ match &self.payload {
+ trade::RadrootsTradeMessagePayload::OrderRevisionAccept(response)
+ if !response.accepted =>
+ {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::InvalidOrderRevisionAcceptPayload,
+ )
+ }
+ trade::RadrootsTradeMessagePayload::OrderRevisionDecline(response)
+ if response.accepted =>
+ {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::InvalidOrderRevisionDeclinePayload,
+ )
+ }
+ trade::RadrootsTradeMessagePayload::DiscountAccept(
+ trade::RadrootsTradeDiscountDecision::Decline { .. },
+ ) => {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::InvalidDiscountAcceptPayload,
+ )
+ }
+ trade::RadrootsTradeMessagePayload::DiscountDecline(
+ trade::RadrootsTradeDiscountDecision::Accept { .. },
+ ) => {
+ return Err(
+ SdkRadrootsdPublicTradePublishValidationError::InvalidDiscountDeclinePayload,
+ )
+ }
+ _ => {}
+ }
+
+ Ok(message_type)
+ }
+
+ fn from_components(
+ route: &SdkRadrootsdPublicTradeRoute,
+ chain: Option<&SdkRadrootsdTradeChain>,
+ listing_event: Option<RadrootsNostrEventPtr>,
+ payload: trade::RadrootsTradeMessagePayload,
+ ) -> Result<Self, SdkRadrootsdPublicTradePublishValidationError> {
+ let request = Self {
+ listing_addr: route.listing_addr().to_owned(),
+ order_id: route.order_id().to_owned(),
+ counterparty_pubkey: route.counterparty_pubkey().to_owned(),
+ listing_event,
+ root_event_id: chain.map(|chain| chain.root_event_id().to_owned()),
+ prev_event_id: chain.map(|chain| chain.prev_event_id().to_owned()),
+ payload,
+ };
+ request.validate_for_publish()?;
+ Ok(request)
+ }
}
impl fmt::Debug for SdkRadrootsdPublicTradePublishRequest {
@@ -265,6 +664,19 @@ impl fmt::Debug for SdkRadrootsdPublicTradePublishRequest {
}
}
+fn normalize_required_string(
+ value: impl Into<String>,
+ field: &'static str,
+) -> Result<String, SdkRadrootsdPublicTradePublishValidationError> {
+ let value = value.into().trim().to_owned();
+ if value.is_empty() {
+ return Err(SdkRadrootsdPublicTradePublishValidationError::EmptyField(
+ field,
+ ));
+ }
+ Ok(value)
+}
+
impl SdkRadrootsdListingPublishRequest {
pub fn from_event(
event: &RadrootsNostrEvent,
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -1743,20 +1743,16 @@ impl<'a> TradeClient<'a> {
request: &radrootsd::SdkRadrootsdPublicTradePublishRequest,
options: &SdkRadrootsdPublicTradePublishOptions,
) -> Result<SdkPublishReceipt, SdkPublishError> {
- match request.message_type() {
- Some(_) => {
- self.client
- .publish_public_trade_via_radrootsd(
- request,
- options.session(),
- options.idempotency_key(),
- )
- .await
- }
- None => Err(SdkPublishError::Encode(
- "trade.publish_public_message_via_radrootsd requires a bridge.order.* public trade payload; use trade.publish_order_request_via_radrootsd for order requests".to_owned(),
- )),
- }
+ request
+ .validate_for_publish()
+ .map_err(|err| SdkPublishError::Encode(err.to_string()))?;
+ self.client
+ .publish_public_trade_via_radrootsd(
+ request,
+ options.session(),
+ options.idempotency_key(),
+ )
+ .await
}
}
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -29,8 +29,9 @@ pub mod trade;
pub use crate::adapters::radrootsd::{
SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeJobStatus,
SdkRadrootsdBridgeRelayPublishResult, SdkRadrootsdPublicTradePublishRequest,
+ SdkRadrootsdPublicTradePublishValidationError, SdkRadrootsdPublicTradeRoute,
SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest,
- SdkRadrootsdSignerSessionMode, SdkRadrootsdSignerSessionRole,
+ SdkRadrootsdSignerSessionMode, SdkRadrootsdSignerSessionRole, SdkRadrootsdTradeChain,
};
pub use crate::client::{
FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError,
diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs
@@ -16,17 +16,19 @@ use radroots_sdk::listing::{
RadrootsListingProduct, RadrootsListingStatus, RadrootsTradeListingParseError,
};
use radroots_sdk::trade::{
- RadrootsTradeMessagePayload, RadrootsTradeMessageType, RadrootsTradeOrder,
- RadrootsTradeOrderItem, RadrootsTradeOrderResponse,
+ RadrootsTradeDiscountDecision, RadrootsTradeMessagePayload, RadrootsTradeMessageType,
+ RadrootsTradeOrder, RadrootsTradeOrderItem, RadrootsTradeOrderResponse,
+ RadrootsTradeOrderRevision, RadrootsTradeOrderRevisionResponse,
};
use radroots_sdk::{
- RadrootsNostrEvent, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig,
- SdkConfigError, SdkEnvironment, SdkPublishError, SdkRadrootsdBridgeDeliveryPolicy,
- SdkRadrootsdBridgeError, SdkRadrootsdBridgeJobStatus, SdkRadrootsdListingPublishOptions,
- SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdPublicTradePublishOptions,
- SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle,
- SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView, SdkTransportMode,
- SdkTransportReceipt, SignerConfig,
+ RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth,
+ RadrootsdConfig, SdkConfigError, SdkEnvironment, SdkPublishError,
+ SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeError, SdkRadrootsdBridgeJobStatus,
+ SdkRadrootsdListingPublishOptions, SdkRadrootsdOrderRequestPublishOptions,
+ SdkRadrootsdPublicTradePublishOptions, SdkRadrootsdPublicTradePublishValidationError,
+ SdkRadrootsdPublicTradeRoute, SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError,
+ SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionRole, SdkRadrootsdSignerSessionView,
+ SdkRadrootsdTradeChain, SdkTransportMode, SdkTransportReceipt, SignerConfig,
};
use serde_json::{Value, json};
use std::collections::VecDeque;
@@ -390,16 +392,36 @@ fn sample_trade_order() -> RadrootsTradeOrder {
}
fn sample_public_trade_request() -> SdkRadrootsdPublicTradePublishRequest {
- SdkRadrootsdPublicTradePublishRequest::new(
+ SdkRadrootsdPublicTradePublishRequest::order_response(
+ &sample_public_trade_route(),
+ &sample_trade_chain(),
+ RadrootsTradeOrderResponse {
+ accepted: true,
+ reason: None,
+ },
+ )
+ .expect("sample order response request should be valid")
+}
+
+fn sample_public_trade_route() -> SdkRadrootsdPublicTradeRoute {
+ SdkRadrootsdPublicTradeRoute::new(
format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"),
"order-1",
"buyer",
- RadrootsTradeMessagePayload::OrderResponse(RadrootsTradeOrderResponse {
- accepted: true,
- reason: None,
- }),
)
- .with_trade_chain("root-event-1", "prev-event-1")
+ .expect("sample public trade route should be valid")
+}
+
+fn sample_trade_chain() -> SdkRadrootsdTradeChain {
+ SdkRadrootsdTradeChain::new("root-event-1", "prev-event-1")
+ .expect("sample trade chain should be valid")
+}
+
+fn listing_event_ptr_with_relays(relays: Option<&str>) -> RadrootsNostrEventPtr {
+ RadrootsNostrEventPtr {
+ id: "listing-event-1".to_owned(),
+ relays: relays.map(str::to_owned),
+ }
}
fn sdk_event(
@@ -1473,6 +1495,104 @@ async fn radrootsd_trade_public_message_publish_rejects_order_request_payload()
Ok(())
}
+#[test]
+fn public_trade_request_validation_requires_listing_snapshot_for_order_revision() {
+ let error = SdkRadrootsdPublicTradePublishRequest::new(
+ format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"),
+ "order-1",
+ "buyer",
+ RadrootsTradeMessagePayload::OrderRevision(RadrootsTradeOrderRevision {
+ revision_id: "revision-1".to_owned(),
+ changes: Vec::new(),
+ }),
+ )
+ .validate_for_publish()
+ .expect_err("order revision without listing snapshot should be rejected");
+
+ assert_eq!(
+ error,
+ SdkRadrootsdPublicTradePublishValidationError::MissingListingSnapshot(
+ RadrootsTradeMessageType::OrderRevision,
+ )
+ );
+}
+
+#[test]
+fn public_trade_request_validation_requires_trade_chain_for_order_response() {
+ let error = SdkRadrootsdPublicTradePublishRequest::new(
+ format!("{KIND_LISTING}:seller:AAAAAAAAAAAAAAAAAAAAAg"),
+ "order-1",
+ "buyer",
+ RadrootsTradeMessagePayload::OrderResponse(RadrootsTradeOrderResponse {
+ accepted: true,
+ reason: None,
+ }),
+ )
+ .validate_for_publish()
+ .expect_err("order response without trade chain should be rejected");
+
+ assert_eq!(
+ error,
+ SdkRadrootsdPublicTradePublishValidationError::MissingTradeChain(
+ RadrootsTradeMessageType::OrderResponse,
+ )
+ );
+}
+
+#[test]
+fn public_trade_request_validation_rejects_blank_listing_snapshot_relays() {
+ let error = SdkRadrootsdPublicTradePublishRequest::order_revision(
+ &sample_public_trade_route(),
+ listing_event_ptr_with_relays(Some(" ")),
+ &sample_trade_chain(),
+ RadrootsTradeOrderRevision {
+ revision_id: "revision-1".to_owned(),
+ changes: Vec::new(),
+ },
+ )
+ .expect_err("blank listing_event relays should be rejected");
+
+ assert_eq!(
+ error,
+ SdkRadrootsdPublicTradePublishValidationError::ListingSnapshotRelaysEmpty
+ );
+}
+
+#[test]
+fn public_trade_request_validation_rejects_invalid_order_revision_accept_payload() {
+ let error = SdkRadrootsdPublicTradePublishRequest::order_revision_accept(
+ &sample_public_trade_route(),
+ &sample_trade_chain(),
+ RadrootsTradeOrderRevisionResponse {
+ accepted: false,
+ reason: Some("not accepted".to_owned()),
+ },
+ )
+ .expect_err("order revision accept must require accepted = true");
+
+ assert_eq!(
+ error,
+ SdkRadrootsdPublicTradePublishValidationError::InvalidOrderRevisionAcceptPayload
+ );
+}
+
+#[test]
+fn public_trade_request_validation_rejects_invalid_discount_accept_payload() {
+ let error = SdkRadrootsdPublicTradePublishRequest::discount_accept(
+ &sample_public_trade_route(),
+ &sample_trade_chain(),
+ RadrootsTradeDiscountDecision::Decline {
+ reason: Some("declined".to_owned()),
+ },
+ )
+ .expect_err("discount accept must use an accept decision");
+
+ assert_eq!(
+ error,
+ SdkRadrootsdPublicTradePublishValidationError::InvalidDiscountAcceptPayload
+ );
+}
+
#[tokio::test]
async fn radrootsd_sdk_workflow_chains_session_listing_trade_and_bridge_job() -> TestResult<()> {
let (server, mut request_rx) = JsonRpcSequenceServer::spawn(