commit c180b47d48b1e8efc16ff340be76491588452792
parent 5e41f8a06adcf359a88b21b9c5c8bf4a7e7c2745
Author: triesap <tyson@radroots.org>
Date: Thu, 7 May 2026 05:44:39 +0000
listing: preserve relay failure event ids
- carry direct relay publish event ids into failure views
- expose signed event ids in structured machine output
- update listing update README mutation posture
- cover relay failure and approval documentation paths
Diffstat:
1 file changed, 85 insertions(+), 30 deletions(-)
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -1821,24 +1821,74 @@ fn direct_relay_error_view(
operation: ListingMutationOperation,
canonical: &CanonicalListingDraft,
listing_addr: String,
- event_preview: ListingMutationEventView,
+ mut event_preview: ListingMutationEventView,
error: DirectRelayPublishError,
) -> ListingMutationView {
- let (reason, target_relays, connected_relays, failed_relays) = match error {
+ let parts = direct_relay_error_view_parts(config.relay.urls.as_slice(), error);
+ if let Some(event_id) = parts.event_id.as_ref() {
+ event_preview.event_id = Some(event_id.clone());
+ }
+
+ ListingMutationView {
+ state: "unavailable".to_owned(),
+ operation: operation.as_str().to_owned(),
+ source: listing_write_source(config).to_owned(),
+ file: args.file.display().to_string(),
+ listing_id: canonical.listing_id.clone(),
+ listing_addr: listing_addr.clone(),
+ seller_pubkey: canonical.seller_pubkey.clone(),
+ event_kind: KIND_LISTING,
+ dry_run: false,
+ deduplicated: false,
+ target_relays: parts.target_relays,
+ connected_relays: parts.connected_relays,
+ acknowledged_relays: Vec::new(),
+ failed_relays: parts.failed_relays,
+ job_id: None,
+ job_status: None,
+ signer_mode: Some(config.signer.backend.as_str().to_owned()),
+ event_id: parts.event_id,
+ event_addr: Some(listing_addr),
+ idempotency_key: args.idempotency_key.clone(),
+ signer_session_id: None,
+ requested_signer_session_id: args.signer_session_id.clone(),
+ reason: Some(parts.reason),
+ job: None,
+ event: args.print_event.then_some(event_preview),
+ actions: Vec::new(),
+ }
+}
+
+#[derive(Debug, Clone)]
+struct DirectRelayErrorViewParts {
+ reason: String,
+ target_relays: Vec<String>,
+ connected_relays: Vec<String>,
+ failed_relays: Vec<RelayFailureView>,
+ event_id: Option<String>,
+}
+
+fn direct_relay_error_view_parts(
+ configured_relays: &[String],
+ error: DirectRelayPublishError,
+) -> DirectRelayErrorViewParts {
+ let (reason, target_relays, connected_relays, failed_relays, event_id) = match error {
DirectRelayPublishError::MissingRelays => (
"direct relay publish requires at least one configured relay".to_owned(),
- config.relay.urls.clone(),
+ configured_relays.to_vec(),
Vec::new(),
Vec::new(),
+ None,
),
DirectRelayPublishError::RelayConfig { relay, source } => (
format!("failed to configure relay `{relay}` for direct relay publish: {source}"),
- config.relay.urls.clone(),
+ configured_relays.to_vec(),
Vec::new(),
vec![RelayFailureView {
relay,
reason: source.to_string(),
}],
+ None,
),
DirectRelayPublishError::Connect {
reason,
@@ -1850,6 +1900,7 @@ fn direct_relay_error_view(
target_relays,
connected_relays,
relay_failures(failed_relays),
+ None,
),
DirectRelayPublishError::Publish {
event_id,
@@ -1862,39 +1913,18 @@ fn direct_relay_error_view(
target_relays,
connected_relays,
relay_failures(failed_relays),
+ Some(event_id),
),
DirectRelayPublishError::Runtime(_)
| DirectRelayPublishError::Build(_)
| DirectRelayPublishError::Sign(_) => unreachable!(),
};
-
- ListingMutationView {
- state: "unavailable".to_owned(),
- operation: operation.as_str().to_owned(),
- source: listing_write_source(config).to_owned(),
- file: args.file.display().to_string(),
- listing_id: canonical.listing_id.clone(),
- listing_addr: listing_addr.clone(),
- seller_pubkey: canonical.seller_pubkey.clone(),
- event_kind: KIND_LISTING,
- dry_run: false,
- deduplicated: false,
+ DirectRelayErrorViewParts {
+ reason,
target_relays,
connected_relays,
- acknowledged_relays: Vec::new(),
failed_relays,
- job_id: None,
- job_status: None,
- signer_mode: Some(config.signer.backend.as_str().to_owned()),
- event_id: None,
- event_addr: Some(listing_addr),
- idempotency_key: args.idempotency_key.clone(),
- signer_session_id: None,
- requested_signer_session_id: args.signer_session_id.clone(),
- reason: Some(reason),
- job: None,
- event: args.print_event.then_some(event_preview),
- actions: Vec::new(),
+ event_id,
}
}
@@ -2356,7 +2386,11 @@ fn encode_base64url_no_pad(bytes: [u8; 16]) -> String {
#[cfg(test)]
mod tests {
- use super::{DRAFT_KIND, ListingDraftDocument, encode_base64url_no_pad, generate_d_tag};
+ use super::{
+ DRAFT_KIND, ListingDraftDocument, direct_relay_error_view_parts, encode_base64url_no_pad,
+ generate_d_tag,
+ };
+ use crate::runtime::direct_relay::{DirectRelayFailure, DirectRelayPublishError};
use radroots_events_codec::d_tag::is_d_tag_base64url;
#[test]
@@ -2373,6 +2407,27 @@ mod tests {
}
#[test]
+ fn direct_relay_publish_error_parts_preserve_event_id() {
+ let parts = direct_relay_error_view_parts(
+ &["ws://127.0.0.1:19000".to_owned()],
+ DirectRelayPublishError::Publish {
+ event_id: "e".repeat(64),
+ reason: "relay rejected event".to_owned(),
+ target_relays: vec!["ws://127.0.0.1:19000".to_owned()],
+ connected_relays: vec!["ws://127.0.0.1:19000".to_owned()],
+ failed_relays: vec![DirectRelayFailure {
+ relay: "ws://127.0.0.1:19000".to_owned(),
+ reason: "relay rejected event".to_owned(),
+ }],
+ },
+ );
+
+ assert_eq!(parts.event_id, Some("e".repeat(64)));
+ assert!(parts.reason.contains("direct relay publish failed"));
+ assert_eq!(parts.failed_relays.len(), 1);
+ }
+
+ #[test]
fn listing_draft_kind_constant_is_stable() {
let document = ListingDraftDocument {
version: 1,