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:
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]