commit bc9de9e176233b58a46261de2be9b66b1109fda7
parent 011e4137dc62321e76ad03e842c6463ede1071fa
Author: triesap <tyson@radroots.org>
Date: Sun, 5 Jul 2026 02:54:22 +0000
status: add root-specific trade selectors
Diffstat:
3 files changed, 120 insertions(+), 10 deletions(-)
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -104,12 +104,12 @@ pub use crate::market_runtime::{
#[cfg(feature = "runtime")]
pub use crate::orders_runtime::{
SdkTradeStatusIssue, SdkTradeStatusIssueKind, SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT,
- TRADE_STATUS_MAX_LIMIT, TradeEvidenceBranchReceipt, TradeEvidenceIngestReceipt,
- TradeEvidenceIngestRequest, TradeEvidenceQueryBranch, TradeEvidenceQueryBranchKind,
- TradeEvidenceQueryPlan, TradeEvidenceRelayFilter, TradeEvidenceRelayTagFilter,
- TradeRequestEvidenceIngestReceipt, TradeRequestEvidenceIngestRequest,
- TradeResyncEventImportReceipt, TradeResyncEvidenceReceipt, TradeResyncReceipt,
- TradeResyncRelayOutcomeKind, TradeResyncRelayOutcomeReceipt,
+ TRADE_STATUS_MAX_LIMIT, TRADE_STATUS_ROOT_SELECTOR_SEPARATOR, TradeEvidenceBranchReceipt,
+ TradeEvidenceIngestReceipt, TradeEvidenceIngestRequest, TradeEvidenceQueryBranch,
+ TradeEvidenceQueryBranchKind, TradeEvidenceQueryPlan, TradeEvidenceRelayFilter,
+ TradeEvidenceRelayTagFilter, TradeRequestEvidenceIngestReceipt,
+ TradeRequestEvidenceIngestRequest, TradeResyncEventImportReceipt, TradeResyncEvidenceReceipt,
+ TradeResyncReceipt, TradeResyncRelayOutcomeKind, TradeResyncRelayOutcomeReceipt,
TradeResyncRelayTransportOutcomeKind, TradeResyncRequest, TradeSellerInboxReceipt,
TradeSellerInboxRequest, TradeStatusAmbiguityCandidate, TradeStatusEligibility,
TradeStatusEvidenceSummary, TradeStatusKind, TradeStatusNextActionKind, TradeStatusReceipt,
diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs
@@ -114,6 +114,8 @@ use std::collections::{BTreeMap, BTreeSet};
pub const TRADE_STATUS_DEFAULT_LIMIT: u32 = 500;
#[cfg(feature = "runtime")]
pub const TRADE_STATUS_MAX_LIMIT: u32 = 1_000;
+#[cfg(feature = "runtime")]
+pub const TRADE_STATUS_ROOT_SELECTOR_SEPARATOR: char = '@';
#[cfg(any(feature = "signer-adapters", test))]
pub const TRADE_SUBMIT_OPERATION_KIND: &str = "trade.submit.v1";
#[cfg(any(feature = "signer-adapters", test))]
@@ -1965,11 +1967,35 @@ impl TradeStatusRequest {
}
}
- pub fn parse(order_id: &str) -> Result<Self, RadrootsSdkError> {
- RadrootsOrderId::parse(order_id)
+ pub fn parse(selector: &str) -> Result<Self, RadrootsSdkError> {
+ let (trade_id, root_event_id) = trade_status_selector_parts(selector)?;
+ let locator = RadrootsOrderId::parse(trade_id)
.map(RadrootsTradeLocator::from_order_id)
- .map(Self::new)
- .map_err(|error| RadrootsSdkError::invalid_trade_id(order_id, error.to_string()))
+ .map_err(|error| RadrootsSdkError::invalid_trade_id(trade_id, error.to_string()))?;
+ let locator = match root_event_id {
+ Some(root_event_id) => {
+ locator.with_root_event_id(root_event_id.parse().map_err(|error| {
+ RadrootsSdkError::InvalidRequest {
+ message: format!(
+ "invalid trade status root selector `{selector}`: {error}"
+ ),
+ }
+ })?)
+ }
+ None => locator,
+ };
+ Ok(Self::new(locator))
+ }
+
+ pub fn locator_selector(locator: &RadrootsTradeLocator) -> String {
+ match locator.root_event_id.as_ref() {
+ Some(root_event_id) => format!(
+ "{}{TRADE_STATUS_ROOT_SELECTOR_SEPARATOR}{}",
+ locator.trade_id.as_str(),
+ root_event_id.as_str()
+ ),
+ None => locator.trade_id.as_str().to_owned(),
+ }
}
pub fn with_limit(mut self, limit: u32) -> Self {
@@ -2015,6 +2041,30 @@ impl TradeStatusRequest {
}
#[cfg(feature = "runtime")]
+fn trade_status_selector_parts(selector: &str) -> Result<(&str, Option<&str>), RadrootsSdkError> {
+ let selector = selector.trim();
+ if selector.is_empty() {
+ return Err(RadrootsSdkError::invalid_trade_id(
+ selector,
+ "empty trade id",
+ ));
+ }
+ let Some((trade_id, root_event_id)) = selector.split_once(TRADE_STATUS_ROOT_SELECTOR_SEPARATOR)
+ else {
+ return Ok((selector, None));
+ };
+ if trade_id.trim().is_empty()
+ || root_event_id.trim().is_empty()
+ || root_event_id.contains(TRADE_STATUS_ROOT_SELECTOR_SEPARATOR)
+ {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: format!("invalid trade status selector `{selector}`"),
+ });
+ }
+ Ok((trade_id, Some(root_event_id)))
+}
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct TradeStatusReceipt {
pub locator: RadrootsTradeLocator,
diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs
@@ -6031,6 +6031,35 @@ fn order_status_parse_rejects_invalid_order_ids() {
assert!(matches!(error, RadrootsSdkError::InvalidTradeId { .. }));
}
+#[test]
+fn order_status_parse_accepts_root_specific_selectors() {
+ let root_event_id = deterministic_event_id("order-status-root-selector");
+ let request = TradeStatusRequest::parse(&format!("order-1@{}", root_event_id.as_str()))
+ .expect("root-specific status request");
+
+ assert_eq!(request.locator.order_id().as_str(), "order-1");
+ assert_eq!(
+ request
+ .locator
+ .root_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
+ Some(root_event_id.as_str())
+ );
+ assert_eq!(
+ TradeStatusRequest::locator_selector(&request.locator),
+ format!("order-1@{}", root_event_id.as_str())
+ );
+}
+
+#[test]
+fn order_status_parse_rejects_malformed_root_selectors() {
+ for selector in ["order-1@", "@aaaaaaaa", "order-1@aaaaaaaa@bbbbbbbb"] {
+ let error = TradeStatusRequest::parse(selector).expect_err("malformed root selector");
+ assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
+ }
+}
+
#[tokio::test]
async fn order_status_contract_dtos_serialize_deterministically() {
let (_tempdir, sdk, _store) = directory_sdk_and_store().await;
@@ -6662,6 +6691,37 @@ async fn order_status_reports_root_ambiguity_for_reused_trade_ids() {
second_request_event.id.as_str()
]
);
+ assert_eq!(
+ receipt
+ .ambiguity_candidates
+ .iter()
+ .map(|candidate| TradeStatusRequest::locator_selector(&candidate.locator))
+ .collect::<Vec<_>>(),
+ vec![
+ format!("order-1@{}", first_request_event.id.as_str()),
+ format!("order-1@{}", second_request_event.id.as_str())
+ ]
+ );
+
+ let root_specific = sdk
+ .trades()
+ .status(
+ TradeStatusRequest::parse(&format!("order-1@{}", second_request_event.id.as_str()))
+ .expect("root status request"),
+ )
+ .await
+ .expect("root-specific status");
+
+ assert!(root_specific.found);
+ assert_eq!(root_specific.status, TradeStatusKind::Requested);
+ assert_eq!(
+ root_specific
+ .request_event_id
+ .as_ref()
+ .map(RadrootsEventId::as_str),
+ Some(second_request_event.id.as_str())
+ );
+ assert!(root_specific.ambiguity_candidates.is_empty());
}
#[cfg(feature = "signer-adapters")]