cli

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

commit 0267db462bacbfcd331f0dc346448453b3662b97
parent 5cbc48ce50d4f4d6939fffa0375c5b8142c3fe63
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Apr 2026 19:52:33 +0000

cli: invalidate poisoned order requests

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

diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -132,6 +132,7 @@ struct SellerOrderRequestResolution { decoded_count: usize, skipped_count: usize, requests: Vec<ResolvedSellerOrderRequest>, + candidate_issues: Vec<OrderIssueView>, } pub fn scaffold( @@ -719,6 +720,14 @@ pub fn decide( args.key.as_str(), receipt, )?; + if !resolution.candidate_issues.is_empty() { + return Ok(order_decision_view_from_resolution( + config, + args, + seller_pubkey, + resolution, + )); + } if resolution.requests.len() == 1 { let request = resolution.requests[0].clone(); let signing = match resolve_local_order_decision_signing_identity( @@ -1168,6 +1177,7 @@ fn order_decision_view_from_resolution( decoded_count, skipped_count, requests, + candidate_issues, } = resolution; let mut view = order_decision_base_view(config, args, "missing", config.output.dry_run); view.seller_pubkey = Some(seller_pubkey); @@ -1177,7 +1187,17 @@ fn order_decision_view_from_resolution( view.fetched_count = fetched_count; view.decoded_count = decoded_count; view.skipped_count = skipped_count; + view.issues = candidate_issues; + if !view.issues.is_empty() { + view.state = "invalid".to_owned(); + view.reason = Some(format!( + "seller order request preflight found invalid request candidates for `{}`", + args.key + )); + view.actions = vec![format!("radroots order status get {}", args.key)]; + return view; + } match requests.as_slice() { [] => { view.reason = Some(format!( @@ -1187,18 +1207,24 @@ fn order_decision_view_from_resolution( view } _ => { - view.state = "unavailable".to_owned(); + let event_ids = requests + .iter() + .map(|request| request.request_event_id.clone()) + .collect::<Vec<_>>(); + view.state = "invalid".to_owned(); view.reason = Some(format!( "multiple seller-targeted order request events matched `{}`; refusing to choose an order root", args.key )); view.issues = vec![issue( - "order_id", + "request_event_id", format!( - "matched {} request events for the same order id", - requests.len() + "matched {} request events for the same order id: {}", + requests.len(), + event_ids.join(", ") ), )]; + view.actions = vec![format!("radroots order status get {}", args.key)]; view } } @@ -1327,18 +1353,37 @@ fn seller_order_request_resolution_from_receipt( let mut skipped_count = 0usize; let mut decoded_count = 0usize; let mut requests = Vec::new(); + let mut candidate_issues = Vec::new(); for event in events { + if event_kind_u32(&event) != KIND_TRADE_ORDER_REQUEST { + skipped_count += 1; + continue; + } + if !event_matches_tag_value(&event, "d", order_id) + || !event_matches_tag_value(&event, "p", seller_pubkey) + { + skipped_count += 1; + continue; + } + let event_id = event.id.to_string(); match seller_order_request_from_event(&event, seller_pubkey, order_id) { Ok(request) => { decoded_count += 1; requests.push(request); } - Err(_) => skipped_count += 1, + Err(error) => { + skipped_count += 1; + candidate_issues.push(issue( + "request_event_id", + format!("request event `{event_id}` failed seller decision preflight: {error}"), + )); + } } } requests.sort_by(|left, right| left.request_event_id.cmp(&right.request_event_id)); + candidate_issues.sort_by(|left, right| left.message.cmp(&right.message)); Ok(SellerOrderRequestResolution { target_relays, @@ -1348,6 +1393,15 @@ fn seller_order_request_resolution_from_receipt( decoded_count, skipped_count, requests, + candidate_issues, + }) +} + +fn event_matches_tag_value(event: &RadrootsNostrEvent, key: &str, value: &str) -> bool { + event.tags.iter().any(|tag| { + let values = tag.as_slice(); + values.first().map(String::as_str) == Some(key) + && values.get(1).map(String::as_str) == Some(value) }) }