cli

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

commit 7fe415d87616db3338d797c4133fc586710eb1a4
parent b789900994f6c1a9f98a3133a4fc45d4fc1e7a36
Author: triesap <tyson@radroots.org>
Date:   Wed, 29 Apr 2026 16:06:02 +0000

cli: align order status preflight

- classify malformed same-order request candidates during status reads
- validate active request tag context before reducer projection
- preserve deterministic invalid status issue output
- cover malformed and duplicate request status cases

Diffstat:
Msrc/runtime/order.rs | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 119 insertions(+), 5 deletions(-)

diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -863,6 +863,7 @@ fn order_status_from_receipt(order_id: &str, receipt: DirectRelayFetchReceipt) - let mut skipped_count = 0usize; let mut requests = Vec::new(); let mut decisions = Vec::new(); + let mut candidate_issues = Vec::new(); for event in events { match order_status_record_from_event(&event) { @@ -874,18 +875,43 @@ fn order_status_from_receipt(order_id: &str, receipt: DirectRelayFetchReceipt) - decoded_count += 1; decisions.push(record); } - Err(_) => skipped_count += 1, + Err(error) => { + skipped_count += 1; + if order_status_request_candidate(&event, order_id) { + let event_id = event.id.to_string(); + candidate_issues.push(issue_with_events( + "invalid_request_candidate", + "request_event_id", + format!( + "request event `{event_id}` failed order status validation: {error}" + ), + vec![event_id], + )); + } + } } } + candidate_issues.sort_by(|left, right| { + left.event_ids + .cmp(&right.event_ids) + .then_with(|| left.message.cmp(&right.message)) + }); let projection = reduce_active_order_events(order_id, requests, decisions); - let state = active_order_status_state(&projection.status).to_owned(); - let reason = active_order_status_reason(&projection.status, order_id); - let reducer_issues = projection + let mut state = active_order_status_state(&projection.status).to_owned(); + let mut reason = active_order_status_reason(&projection.status, order_id); + let mut reducer_issues = projection .issues .into_iter() .map(active_order_reducer_issue_view) - .collect(); + .collect::<Vec<_>>(); + if !candidate_issues.is_empty() { + state = "invalid".to_owned(); + reason = Some(format!( + "active order request candidates for `{order_id}` failed status validation" + )); + reducer_issues.extend(candidate_issues); + } OrderStatusView { state, @@ -909,6 +935,11 @@ fn order_status_from_receipt(order_id: &str, receipt: DirectRelayFetchReceipt) - } } +fn order_status_request_candidate(event: &RadrootsNostrEvent, order_id: &str) -> bool { + event_kind_u32(event) == KIND_TRADE_ORDER_REQUEST + && event_matches_tag_value(event, "d", order_id) +} + fn order_status_record_from_event( event: &RadrootsNostrEvent, ) -> Result<OrderStatusRecord, RuntimeError> { @@ -924,6 +955,13 @@ fn order_status_record_from_event( "active order request event used the wrong message type".to_owned(), )); } + active_trade_event_context_from_tags( + RadrootsActiveTradeMessageType::TradeOrderRequested, + &event.tags, + ) + .map_err(|error| { + RuntimeError::Config(format!("decode active order request tags: {error}")) + })?; Ok(OrderStatusRecord::Request( RadrootsActiveOrderRequestRecord { event_id: event.id, @@ -3601,6 +3639,82 @@ mod tests { } #[test] + fn order_status_from_receipt_reports_invalid_same_order_request_candidate() { + let fixture = order_status_fixture(); + let invalid_event = signed_malformed_order_request_event( + &fixture.buyer, + fixture.order_id.as_str(), + fixture.listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + fixture.seller_pubkey.as_str(), + "2".repeat(64).as_str(), + ); + let invalid_event_id = invalid_event.id.to_string(); + let receipt = DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![fixture.request_event.clone(), invalid_event], + }; + + let view = order_status_from_receipt(fixture.order_id.as_str(), receipt); + + assert_eq!(view.state, "invalid"); + assert_eq!(view.decoded_count, 1); + assert_eq!(view.skipped_count, 1); + assert_eq!( + view.reason.as_deref(), + Some( + "active order request candidates for `ord_AAAAAAAAAAAAAAAAAAAAAg` failed status validation" + ) + ); + let issue = view + .reducer_issues + .iter() + .find(|issue| issue.code == "invalid_request_candidate") + .expect("invalid request candidate issue"); + assert_eq!(issue.field, "request_event_id"); + assert_eq!(issue.event_ids, vec![invalid_event_id]); + } + + #[test] + fn order_status_from_receipt_reports_multiple_request_candidates_invalid() { + let fixture = order_status_fixture(); + let second_request_event = signed_order_request_event( + &fixture.buyer, + fixture.order_id.as_str(), + fixture.listing_addr.as_str(), + fixture.buyer_pubkey.as_str(), + fixture.seller_pubkey.as_str(), + "2".repeat(64).as_str(), + ); + let mut expected_event_ids = vec![ + fixture.request_event.id.to_string(), + second_request_event.id.to_string(), + ]; + expected_event_ids.sort(); + let receipt = DirectRelayFetchReceipt { + target_relays: vec!["ws://relay.test".to_owned()], + connected_relays: vec!["ws://relay.test".to_owned()], + failed_relays: Vec::new(), + events: vec![fixture.request_event.clone(), second_request_event], + }; + + let view = order_status_from_receipt(fixture.order_id.as_str(), receipt); + + assert_eq!(view.state, "invalid"); + assert_eq!(view.decoded_count, 2); + assert_eq!(view.skipped_count, 0); + let issue = view + .reducer_issues + .iter() + .find(|issue| issue.code == "multiple_requests") + .expect("multiple request issue"); + assert_eq!(issue.field, "request_event_id"); + assert_eq!(issue.event_ids, expected_event_ids); + } + + #[test] fn seller_order_request_resolution_skips_wrong_seller_request() { let selected_seller = RadrootsIdentity::generate(); let other_seller = RadrootsIdentity::generate();