cli

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

commit 8061cf6cf2668d77081011d5e359af2f772ab14f
parent 46c04272f416f13d5440481fd047d63aa962bcaa
Author: triesap <tyson@radroots.org>
Date:   Tue, 28 Apr 2026 14:05:34 +0000

cli: preserve order event relay failure detail

Diffstat:
Msrc/operation_adapter.rs | 15+++++++++++++++
Msrc/operation_order.rs | 32++++++++++++++++++++++++++------
Msrc/runtime/direct_relay.rs | 28++++++++++++++++++++++------
Msrc/runtime/order.rs | 43++++++++++++++++++++++++++++++++++++++++---
Mtests/signer_runtime_modes.rs | 23+++++++++++++++++++++++
5 files changed, 126 insertions(+), 15 deletions(-)

diff --git a/src/operation_adapter.rs b/src/operation_adapter.rs @@ -446,6 +446,21 @@ impl OperationAdapterError { } } + pub fn network_unavailable_with_detail( + operation_id: &str, + message: String, + detail: Value, + ) -> Self { + Self::DetailedFailure { + operation_id: operation_id.to_owned(), + code: "network_unavailable".to_owned(), + class: "network".to_owned(), + message, + exit_code: CliExitCode::SyncOrNetworkFailure, + detail_json: detail.to_string(), + } + } + pub fn unavailable(operation_id: &str, message: String) -> Self { classify_runtime_failure( operation_id, diff --git a/src/operation_order.rs b/src/operation_order.rs @@ -175,13 +175,33 @@ where { match view.disposition() { CommandDisposition::Success => serialized_target_result::<R, _>(view), - disposition => Err(OperationAdapterError::from_command_disposition( - operation_id, - disposition, - view.reason.clone().unwrap_or_else(|| { + disposition => { + let message = view.reason.clone().unwrap_or_else(|| { format!("order event list finished with state `{}`", view.state) - }), - )), + }); + if disposition == CommandDisposition::ExternalUnavailable { + Err(OperationAdapterError::network_unavailable_with_detail( + operation_id, + message, + json!({ + "state": &view.state, + "seller_pubkey": &view.seller_pubkey, + "target_relays": &view.target_relays, + "connected_relays": &view.connected_relays, + "failed_relays": &view.failed_relays, + "fetched_count": view.fetched_count, + "decoded_count": view.decoded_count, + "skipped_count": view.skipped_count, + }), + )) + } else { + Err(OperationAdapterError::from_command_disposition( + operation_id, + disposition, + message, + )) + } + } } } diff --git a/src/runtime/direct_relay.rs b/src/runtime/direct_relay.rs @@ -68,8 +68,12 @@ pub enum DirectRelayFetchError { #[source] source: RadrootsNostrError, }, - #[error("direct relay connection failed: {0}")] - Connect(String), + #[error("direct relay connection failed: {reason}")] + Connect { + reason: String, + target_relays: Vec<String>, + failed_relays: Vec<DirectRelayFailure>, + }, #[error("direct relay fetch failed: {0}")] Fetch(#[source] RadrootsNostrError), } @@ -142,9 +146,11 @@ async fn fetch_events_from_relays_async( let connection_output = client.try_connect(connect_timeout).await; let failed_relays = relay_failures_from_output(&connection_output); if connection_output.success.is_empty() { - return Err(DirectRelayFetchError::Connect(summarize_failures( - &failed_relays, - ))); + return Err(DirectRelayFetchError::Connect { + reason: summarize_failures(&failed_relays), + target_relays: relay_urls.to_vec(), + failed_relays, + }); } let events = client @@ -319,6 +325,16 @@ mod tests { .await .expect_err("connection failure"); - assert!(matches!(err, DirectRelayFetchError::Connect(_))); + match err { + DirectRelayFetchError::Connect { + target_relays, + failed_relays, + .. + } => { + assert_eq!(target_relays, vec!["ws://127.0.0.1:9"]); + assert_eq!(failed_relays.len(), 1); + } + _ => panic!("expected connection failure"), + } } } diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -35,7 +35,7 @@ use crate::runtime::RuntimeError; use crate::runtime::accounts; use crate::runtime::config::{RuntimeConfig, SignerBackend}; use crate::runtime::direct_relay::{ - DirectRelayFailure, DirectRelayFetchReceipt, DirectRelayPublishReceipt, + DirectRelayFailure, DirectRelayFetchError, DirectRelayFetchReceipt, DirectRelayPublishReceipt, fetch_events_from_relays, publish_parts_with_identity, }; use crate::runtime::signer::ActorWriteBindingError; @@ -615,8 +615,22 @@ pub fn history( }; let seller_pubkey = seller.record.public_identity.public_key_hex; let filter = order_request_filter(seller_pubkey.as_str(), order_id)?; - let receipt = fetch_events_from_relays(&config.relay.urls, filter) - .map_err(|error| RuntimeError::Network(error.to_string()))?; + let receipt = match fetch_events_from_relays(&config.relay.urls, filter) { + Ok(receipt) => receipt, + Err(DirectRelayFetchError::Connect { + reason, + target_relays, + failed_relays, + }) => { + return Ok(order_history_unavailable( + seller_pubkey, + reason, + target_relays, + failed_relays, + )); + } + Err(error) => return Err(RuntimeError::Network(error.to_string())), + }; Ok(order_history_from_receipt(seller_pubkey, order_id, receipt)) } @@ -689,6 +703,29 @@ fn order_history_unconfigured( } } +fn order_history_unavailable( + seller_pubkey: String, + reason: String, + target_relays: Vec<String>, + failed_relays: Vec<DirectRelayFailure>, +) -> OrderHistoryView { + OrderHistoryView { + state: "unavailable".to_owned(), + source: ORDER_EVENT_LIST_SOURCE.to_owned(), + seller_pubkey: Some(seller_pubkey), + target_relays, + connected_relays: Vec::new(), + failed_relays: relay_failures(failed_relays), + fetched_count: 0, + decoded_count: 0, + skipped_count: 0, + count: 0, + reason: Some(format!("direct relay connection failed: {reason}")), + orders: Vec::new(), + actions: Vec::new(), + } +} + fn order_history_from_receipt( seller_pubkey: String, order_id: Option<&str>, diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs @@ -774,6 +774,29 @@ fn local_order_event_list_attempts_configured_direct_relay() { assert!(!output.status.success()); assert_direct_relay_connection_failure(&value, "order.event.list", &["order", "event", "list"]); + assert_eq!(value["errors"][0]["detail"]["state"], "unavailable"); + assert_eq!(value["errors"][0]["detail"]["target_relays"][0], relay); + assert_eq!( + value["errors"][0]["detail"]["connected_relays"] + .as_array() + .expect("connected relays") + .len(), + 0 + ); + assert_eq!( + value["errors"][0]["detail"]["failed_relays"] + .as_array() + .expect("failed relays") + .len(), + 1 + ); + assert_contains( + &value["errors"][0]["detail"]["failed_relays"][0]["relay"], + "127.0.0.1:9", + ); + assert_eq!(value["errors"][0]["detail"]["fetched_count"], 0); + assert_eq!(value["errors"][0]["detail"]["decoded_count"], 0); + assert_eq!(value["errors"][0]["detail"]["skipped_count"], 0); } #[test]