cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 30c47396a172af443694a6e174944ee4ee02d337
parent 6246a9ad2e8b5c62195a288eeeb89a6a91a0ea96
Author: triesap <tyson@radroots.org>
Date:   Sun,  5 Jul 2026 02:54:30 +0000

status: render trade ambiguity candidates

Diffstat:
Msrc/out/terminal/renderers/trade.rs | 28++++++++++++++++++++++++++++
Msrc/runtime/order/sdk_status.rs | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/runtime/sdk.rs | 4++++
Msrc/view/runtime.rs | 9+++++++++
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>,