commit 30c47396a172af443694a6e174944ee4ee02d337
parent 6246a9ad2e8b5c62195a288eeeb89a6a91a0ea96
Author: triesap <tyson@radroots.org>
Date: Sun, 5 Jul 2026 02:54:30 +0000
status: render trade ambiguity candidates
Diffstat:
4 files changed, 160 insertions(+), 29 deletions(-)
diff --git a/src/out/terminal/renderers/trade.rs b/src/out/terminal/renderers/trade.rs
@@ -341,6 +341,7 @@ fn trade_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalD
common::push_path_field(&mut document, "Revision", result, &["revision", "state"]);
push_relay_field(&mut document, result);
push_inventory_section(&mut document, result);
+ push_ambiguity_candidates_section(&mut document, result);
push_issue_sections(&mut document, result);
push_nested_issues_section(
&mut document,
@@ -352,6 +353,33 @@ fn trade_status_document(envelope: &OutputEnvelope, result: &Value) -> TerminalD
document
}
+fn push_ambiguity_candidates_section(document: &mut TerminalDocument, result: &Value) {
+ let rows = common::array(result, &["ambiguity_candidates"])
+ .into_iter()
+ .flatten()
+ .map(|candidate| {
+ TerminalTableRow::new(vec![
+ common::string(candidate, &["status_selector"]).unwrap_or_default(),
+ common::string(candidate, &["locator", "root_event_id"]).unwrap_or_default(),
+ common::string(candidate, &["status_command"]).unwrap_or_default(),
+ ])
+ })
+ .collect::<Vec<_>>();
+ if rows.is_empty() {
+ return;
+ }
+ document.sections.push(common::table_section(
+ "Ambiguity candidates",
+ vec![
+ TerminalTableColumn::new("Selector", 8, 32),
+ TerminalTableColumn::new("Root", 4, 22),
+ TerminalTableColumn::new("Command", 7, 48),
+ ],
+ rows,
+ "No ambiguity candidates",
+ ));
+}
+
fn trade_event_list_document(envelope: &OutputEnvelope, result: &Value) -> TerminalDocument {
let mut document = common::document_with_title(envelope, "Trade events");
common::push_path_field(&mut document, "State", result, &["state"]);
diff --git a/src/runtime/order/sdk_status.rs b/src/runtime/order/sdk_status.rs
@@ -1,13 +1,18 @@
use radroots_events::ids::RadrootsEventId;
+#[cfg(test)]
+use radroots_events::ids::RadrootsOrderId;
use radroots_sdk::{
- SdkTradeStatusIssue, TradeStatusEligibility, TradeStatusEvidenceSummary, TradeStatusKind,
- TradeStatusNextActionKind, TradeStatusReceipt, TradeValidationTrustDecision,
+ SdkTradeStatusIssue, TradeStatusAmbiguityCandidate, TradeStatusEligibility,
+ TradeStatusEvidenceSummary, TradeStatusKind, TradeStatusNextActionKind, TradeStatusReceipt,
+ TradeStatusRequest, TradeValidationTrustDecision,
};
+use radroots_trade::identity::RadrootsTradeLocator;
use crate::view::runtime::{
- OrderIssueView, OrderStatusEligibilityView, OrderStatusEvidenceSummaryView,
- OrderStatusLifecycleCancellationView, OrderStatusLifecycleView, OrderStatusSdkReceiptView,
- OrderStatusValidationTrustView, OrderStatusView, OrderTradeLocatorView,
+ OrderIssueView, OrderStatusAmbiguityCandidateView, OrderStatusEligibilityView,
+ OrderStatusEvidenceSummaryView, OrderStatusLifecycleCancellationView, OrderStatusLifecycleView,
+ OrderStatusSdkReceiptView, OrderStatusValidationTrustView, OrderStatusView,
+ OrderTradeLocatorView,
};
use super::{ORDER_ACTOR_CONTEXT_SDK_LOCAL, ORDER_STATUS_SDK_SOURCE};
@@ -22,12 +27,21 @@ pub(super) fn sdk_order_status_view(receipt: TradeStatusReceipt) -> OrderStatusV
let reason = sdk_order_status_reason(receipt.status, receipt.order_id.as_str());
let lifecycle = sdk_order_status_lifecycle_view(&receipt, reducer_issues.as_slice());
let sdk_receipt = Some(sdk_order_status_receipt_view(&receipt));
+ let ambiguity_candidates = receipt
+ .ambiguity_candidates
+ .iter()
+ .map(sdk_status_ambiguity_candidate_view)
+ .collect::<Vec<_>>();
+ let actions = ambiguity_candidates
+ .iter()
+ .map(|candidate| candidate.status_command.clone())
+ .collect::<Vec<_>>();
OrderStatusView {
state,
source: ORDER_STATUS_SDK_SOURCE.to_owned(),
order_id: receipt.order_id.to_string(),
- locator: sdk_trade_locator_view(&receipt),
+ locator: sdk_trade_locator_view(&receipt.locator),
actor_context_source: ORDER_ACTOR_CONTEXT_SDK_LOCAL.to_owned(),
request_event_id: sdk_event_id_string(receipt.request_event_id.as_ref()),
decision_event_id: sdk_event_id_string(receipt.decision_event_id.as_ref()),
@@ -42,6 +56,7 @@ pub(super) fn sdk_order_status_view(receipt: TradeStatusReceipt) -> OrderStatusV
inventory: None,
lifecycle: Some(lifecycle),
sdk_receipt,
+ ambiguity_candidates,
reducer_issues,
target_relays: Vec::new(),
connected_relays: Vec::new(),
@@ -50,33 +65,28 @@ pub(super) fn sdk_order_status_view(receipt: TradeStatusReceipt) -> OrderStatusV
decoded_count: receipt.event_count,
skipped_count: 0,
reason,
- actions: Vec::new(),
+ actions,
}
}
-fn sdk_trade_locator_view(receipt: &TradeStatusReceipt) -> OrderTradeLocatorView {
+fn sdk_trade_locator_view(locator: &RadrootsTradeLocator) -> OrderTradeLocatorView {
OrderTradeLocatorView {
- trade_id: receipt.locator.trade_id.as_str().to_owned(),
- root_event_id: receipt
- .locator
- .root_event_id
- .as_ref()
- .map(ToString::to_string),
- listing_addr: receipt
- .locator
- .listing_addr
- .as_ref()
- .map(ToString::to_string),
- buyer_pubkey: receipt
- .locator
- .buyer_pubkey
- .as_ref()
- .map(ToString::to_string),
- seller_pubkey: receipt
- .locator
- .seller_pubkey
- .as_ref()
- .map(ToString::to_string),
+ trade_id: locator.trade_id.as_str().to_owned(),
+ root_event_id: locator.root_event_id.as_ref().map(ToString::to_string),
+ listing_addr: locator.listing_addr.as_ref().map(ToString::to_string),
+ buyer_pubkey: locator.buyer_pubkey.as_ref().map(ToString::to_string),
+ seller_pubkey: locator.seller_pubkey.as_ref().map(ToString::to_string),
+ }
+}
+
+fn sdk_status_ambiguity_candidate_view(
+ candidate: &TradeStatusAmbiguityCandidate,
+) -> OrderStatusAmbiguityCandidateView {
+ let status_selector = TradeStatusRequest::locator_selector(&candidate.locator);
+ OrderStatusAmbiguityCandidateView {
+ locator: sdk_trade_locator_view(&candidate.locator),
+ status_command: format!("radroots trade status get {status_selector}"),
+ status_selector,
}
}
@@ -243,3 +253,83 @@ fn sdk_order_status_issue_view(issue: &SdkTradeStatusIssue) -> OrderIssueView {
fn sdk_event_id_string(event_id: Option<&RadrootsEventId>) -> Option<String> {
event_id.map(RadrootsEventId::to_string)
}
+
+#[cfg(test)]
+mod tests {
+ use serde_json::json;
+
+ use super::*;
+
+ #[test]
+ fn sdk_order_status_view_exposes_ambiguity_candidates() {
+ let order_id = RadrootsOrderId::parse("order-1").expect("order id");
+ let root_event_id = RadrootsEventId::parse(
+ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ )
+ .expect("root event id");
+ let receipt = TradeStatusReceipt {
+ locator: RadrootsTradeLocator::from_order_id(order_id.clone()),
+ order_id,
+ root_event_id: None,
+ ambiguity_candidates: vec![TradeStatusAmbiguityCandidate {
+ locator: RadrootsTradeLocator::from_order_id(
+ RadrootsOrderId::parse("order-1").expect("candidate order id"),
+ )
+ .with_root_event_id(root_event_id.clone()),
+ }],
+ source: radroots_sdk::SdkTradeStatusSource::LocalOnly,
+ found: false,
+ event_count: 2,
+ limit_applied: 500,
+ status: TradeStatusKind::Ambiguous,
+ lifecycle_terminal: false,
+ listing_addr: None,
+ buyer_pubkey: None,
+ seller_pubkey: None,
+ economics: None,
+ evidence: TradeStatusEvidenceSummary {
+ event_count: 2,
+ limit_applied: 500,
+ has_request: true,
+ has_decision: false,
+ has_agreement: false,
+ has_validation_receipt: false,
+ has_pending_revision: false,
+ has_cancellation: false,
+ has_issues: false,
+ },
+ validation_trust: None,
+ online_evidence: None,
+ eligibility: TradeStatusEligibility {
+ can_decide: false,
+ can_propose_revision: false,
+ can_decide_revision: false,
+ can_cancel: false,
+ },
+ next_action: TradeStatusNextActionKind::InspectEvidenceIssues,
+ event_ids: vec![root_event_id.clone()],
+ request_event_id: None,
+ decision_event_id: None,
+ agreement_event_id: None,
+ rhi_receipt_event_id: None,
+ pending_revision_event_id: None,
+ cancellation_event_id: None,
+ last_event_id: None,
+ issues: Vec::new(),
+ };
+
+ let view_json = serde_json::to_value(sdk_order_status_view(receipt)).expect("view json");
+
+ assert_eq!(
+ view_json["ambiguity_candidates"][0]["status_selector"],
+ json!(format!("order-1@{}", root_event_id.as_str()))
+ );
+ assert_eq!(
+ view_json["actions"][0],
+ json!(format!(
+ "radroots trade status get order-1@{}",
+ root_event_id.as_str()
+ ))
+ );
+ }
+}
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -967,6 +967,9 @@ mod tests {
"TradeValidationTrustDecision",
"validation_trust",
"has_validation_receipt",
+ "ambiguity_candidates",
+ "TradeStatusAmbiguityCandidate",
+ "TradeStatusRequest::locator_selector",
],
},
MigratedCliPathGuard {
@@ -1085,6 +1088,7 @@ mod tests {
const MIGRATED_PATH_DISALLOWED_TOKENS: &[&str] = &[
"fetch_events_from_relays",
+ "fetch_relay_events_via_shared_transport",
"publish_parts_with_identity",
"publish_via_direct_relay",
"mutate_via_direct_relay",
diff --git a/src/view/runtime.rs b/src/view/runtime.rs
@@ -1726,6 +1726,13 @@ pub struct OrderTradeLocatorView {
}
#[derive(Debug, Clone, Serialize)]
+pub struct OrderStatusAmbiguityCandidateView {
+ pub locator: OrderTradeLocatorView,
+ pub status_selector: String,
+ pub status_command: String,
+}
+
+#[derive(Debug, Clone, Serialize)]
pub struct OrderSubmitView {
pub state: String,
pub source: String,
@@ -2165,6 +2172,8 @@ pub struct OrderStatusView {
#[serde(skip_serializing_if = "Option::is_none")]
pub sdk_receipt: Option<OrderStatusSdkReceiptView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub ambiguity_candidates: Vec<OrderStatusAmbiguityCandidateView>,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reducer_issues: Vec<OrderIssueView>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_relays: Vec<String>,