cli

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

commit 6da69db2f79f812657ce33b6b335aa06ecd0a85b
parent a43f499a5be5655176377b9cc174a83bc9b076fd
Author: triesap <tyson@radroots.org>
Date:   Thu,  9 Jul 2026 21:55:22 +0000

transport: align CLI public target surfaces

- rename CLI publish and trade JSON transport fields to target-oriented endpoint and failure names
- consume SDK Nostr-scoped validation evidence names and current satisfaction policy variants
- expose target scope, label, and typed transport outcome metadata on failure views
- update terminal renderers and tests for target-oriented transport wording

Diffstat:
Msrc/ops/exec/farm.rs | 4++--
Msrc/ops/exec/listing.rs | 8++++----
Msrc/ops/exec/trade.rs | 93+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/out/terminal/renderer.rs | 4++--
Msrc/out/terminal/renderers/farm.rs | 16++++++++--------
Msrc/out/terminal/renderers/listing.rs | 14+++++++-------
Msrc/out/terminal/renderers/trade.rs | 12++++++------
Msrc/out/terminal/renderers/validation.rs | 6+++---
Msrc/runtime/farm.rs | 60++++++++++++++++++++++++++++++++++++------------------------
Msrc/runtime/listing.rs | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/runtime/order.rs | 287+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/runtime/order/sdk_status.rs | 6+++---
Msrc/runtime/sdk.rs | 33++++++++++++++++++++++++++-------
Msrc/runtime/sync.rs | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/runtime/transport.rs | 15++++++++++++++-
Msrc/runtime/validation_receipt.rs | 157+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/view/runtime.rs | 84+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mtests/signer_runtime_modes.rs | 54++++++++++++++++++++++++++++--------------------------
Mtests/support/mod.rs | 12++++++------
Mtests/target_cli.rs | 34++++++++++++++++++++--------------
20 files changed, 659 insertions(+), 440 deletions(-)

diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs @@ -350,8 +350,8 @@ fn farm_private_location_set_result( fn farm_publish_relay_unavailable(view: &FarmPublishView) -> bool { view.state == "partial" - || !view.profile.failed_relays.is_empty() - || !view.farm.failed_relays.is_empty() + || !view.profile.failed_transport_targets.is_empty() + || !view.farm.failed_transport_targets.is_empty() } fn require_relay_target<P>( diff --git a/src/ops/exec/listing.rs b/src/ops/exec/listing.rs @@ -339,10 +339,10 @@ fn listing_relay_unavailable(view: &ListingMutationView) -> bool { ) && (view.reason.as_deref().is_some_and(|reason| { reason.contains("configured Nostr relay") || reason.contains("Nostr transport connection failed") - || reason.contains("SDK relay publish") - }) || !view.target_relays.is_empty() - || !view.connected_relays.is_empty() - || !view.failed_relays.is_empty()) + || reason.contains("SDK transport publish") + }) || !view.target_transport_endpoints.is_empty() + || !view.attempted_transport_endpoints.is_empty() + || !view.failed_transport_targets.is_empty()) } fn listing_app_record_export_result<R>( diff --git a/src/ops/exec/trade.rs b/src/ops/exec/trade.rs @@ -28,7 +28,7 @@ use crate::view::runtime::{ OrderSubmitView, }; -const TRADE_EVENT_WATCH_DEFERRED_REASON: &str = "relay-backed trade event watch is not implemented"; +const TRADE_EVENT_WATCH_DEFERRED_REASON: &str = "Nostr-backed trade event watch is not implemented"; pub struct TradeOperationService<'a> { config: &'a RuntimeConfig, @@ -502,7 +502,9 @@ where .unwrap_or_else(|| format!("order decision finished with state `{}`", view.state)); if disposition == CommandDisposition::ExternalUnavailable { let detail = order_decision_error_detail(view); - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { + if !view.failed_transport_targets.is_empty() + && view.attempted_transport_endpoints.is_empty() + { Err(OperationAdapterError::network_unavailable_with_detail( operation_id, message, @@ -555,10 +557,10 @@ fn order_decision_error_detail(view: &OrderDecisionView) -> Value { "seller_pubkey": &view.seller_pubkey, "decision": &view.decision, "dry_run": view.dry_run, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "acknowledged_relays": &view.acknowledged_relays, - "failed_relays": &view.failed_relays, + "target_transport_endpoints": &view.target_transport_endpoints, + "attempted_transport_endpoints": &view.attempted_transport_endpoints, + "accepted_transport_endpoints": &view.accepted_transport_endpoints, + "failed_transport_targets": &view.failed_transport_targets, "fetched_count": view.fetched_count, "decoded_count": view.decoded_count, "skipped_count": view.skipped_count, @@ -595,7 +597,9 @@ where .unwrap_or_else(|| format!("order cancel finished with state `{}`", view.state)); if disposition == CommandDisposition::ExternalUnavailable { let detail = order_cancellation_error_detail(view); - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { + if !view.failed_transport_targets.is_empty() + && view.attempted_transport_endpoints.is_empty() + { Err(OperationAdapterError::network_unavailable_with_detail( operation_id, message, @@ -641,10 +645,10 @@ fn order_cancellation_error_detail(view: &OrderCancellationView) -> Value { "seller_pubkey": &view.seller_pubkey, "cancellation_reason": &view.cancellation_reason, "dry_run": view.dry_run, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "acknowledged_relays": &view.acknowledged_relays, - "failed_relays": &view.failed_relays, + "target_transport_endpoints": &view.target_transport_endpoints, + "attempted_transport_endpoints": &view.attempted_transport_endpoints, + "accepted_transport_endpoints": &view.accepted_transport_endpoints, + "failed_transport_targets": &view.failed_transport_targets, "fetched_count": view.fetched_count, "decoded_count": view.decoded_count, "skipped_count": view.skipped_count, @@ -686,7 +690,9 @@ where }); if disposition == CommandDisposition::ExternalUnavailable { let detail = order_revision_proposal_error_detail(view); - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { + if !view.failed_transport_targets.is_empty() + && view.attempted_transport_endpoints.is_empty() + { Err(OperationAdapterError::network_unavailable_with_detail( operation_id, message, @@ -735,10 +741,10 @@ fn order_revision_proposal_error_detail(view: &OrderRevisionProposalView) -> Val "buyer_pubkey": &view.buyer_pubkey, "seller_pubkey": &view.seller_pubkey, "dry_run": view.dry_run, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "acknowledged_relays": &view.acknowledged_relays, - "failed_relays": &view.failed_relays, + "target_transport_endpoints": &view.target_transport_endpoints, + "attempted_transport_endpoints": &view.attempted_transport_endpoints, + "accepted_transport_endpoints": &view.accepted_transport_endpoints, + "failed_transport_targets": &view.failed_transport_targets, "fetched_count": view.fetched_count, "decoded_count": view.decoded_count, "skipped_count": view.skipped_count, @@ -782,7 +788,9 @@ where }); if disposition == CommandDisposition::ExternalUnavailable { let detail = order_revision_decision_error_detail(view); - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { + if !view.failed_transport_targets.is_empty() + && view.attempted_transport_endpoints.is_empty() + { Err(OperationAdapterError::network_unavailable_with_detail( operation_id, message, @@ -832,10 +840,10 @@ fn order_revision_decision_error_detail(view: &OrderRevisionDecisionView) -> Val "buyer_pubkey": &view.buyer_pubkey, "seller_pubkey": &view.seller_pubkey, "dry_run": view.dry_run, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "acknowledged_relays": &view.acknowledged_relays, - "failed_relays": &view.failed_relays, + "target_transport_endpoints": &view.target_transport_endpoints, + "attempted_transport_endpoints": &view.attempted_transport_endpoints, + "accepted_transport_endpoints": &view.accepted_transport_endpoints, + "failed_transport_targets": &view.failed_transport_targets, "fetched_count": view.fetched_count, "decoded_count": view.decoded_count, "skipped_count": view.skipped_count, @@ -862,7 +870,9 @@ where .unwrap_or_else(|| format!("order status finished with state `{}`", view.state)); if disposition == CommandDisposition::ExternalUnavailable { let detail = order_status_error_detail(view); - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { + if !view.failed_transport_targets.is_empty() + && view.attempted_transport_endpoints.is_empty() + { Err(OperationAdapterError::network_unavailable_with_detail( operation_id, message, @@ -910,9 +920,9 @@ fn order_status_error_detail(view: &OrderStatusView) -> Value { "lifecycle": &view.lifecycle, "sdk_receipt": &view.sdk_receipt, "reducer_issues": &view.reducer_issues, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "failed_relays": &view.failed_relays, + "target_transport_endpoints": &view.target_transport_endpoints, + "attempted_transport_endpoints": &view.attempted_transport_endpoints, + "failed_transport_targets": &view.failed_transport_targets, "fetched_count": view.fetched_count, "decoded_count": view.decoded_count, "skipped_count": view.skipped_count, @@ -964,7 +974,9 @@ where )) } CommandDisposition::ExternalUnavailable => { - if !view.failed_relays.is_empty() && view.connected_relays.is_empty() { + if !view.failed_transport_targets.is_empty() + && view.attempted_transport_endpoints.is_empty() + { Err(OperationAdapterError::network_unavailable_with_detail( operation_id, message, @@ -1108,10 +1120,10 @@ fn order_submit_error_detail(view: &OrderSubmitView) -> Value { "event_kind": view.event_kind, "dry_run": view.dry_run, "deduplicated": view.deduplicated, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "acknowledged_relays": &view.acknowledged_relays, - "failed_relays": &view.failed_relays, + "target_transport_endpoints": &view.target_transport_endpoints, + "attempted_transport_endpoints": &view.attempted_transport_endpoints, + "accepted_transport_endpoints": &view.accepted_transport_endpoints, + "failed_transport_targets": &view.failed_transport_targets, "idempotency_key": &view.idempotency_key, "signer_mode": &view.signer_mode, "issues": &view.issues, @@ -1160,9 +1172,9 @@ fn order_event_list_error_detail(view: &crate::view::runtime::OrderEventListView json!({ "state": &view.state, "seller_pubkey": &view.seller_pubkey, - "target_relays": &view.target_relays, - "connected_relays": &view.connected_relays, - "failed_relays": &view.failed_relays, + "target_transport_endpoints": &view.target_transport_endpoints, + "attempted_transport_endpoints": &view.attempted_transport_endpoints, + "failed_transport_targets": &view.failed_transport_targets, "fetched_count": view.fetched_count, "decoded_count": view.decoded_count, "skipped_count": view.skipped_count, @@ -1397,7 +1409,12 @@ mod tests { assert_eq!(detail["state"], "unconfigured"); assert_eq!(detail["trade_id"], "ord_pending"); assert_eq!(detail["decision"], "accepted"); - assert!(detail["target_relays"].as_array().unwrap().is_empty()); + assert!( + detail["target_transport_endpoints"] + .as_array() + .unwrap() + .is_empty() + ); } #[test] @@ -1868,10 +1885,10 @@ mod tests { event_kind: Some(3423), inventory: None, dry_run: false, - target_relays: vec!["ws://relay.test".to_owned()], - connected_relays: vec!["ws://relay.test".to_owned()], - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: vec!["ws://relay.test".to_owned()], + attempted_transport_endpoints: vec!["ws://relay.test".to_owned()], + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), fetched_count: 2, decoded_count: 2, skipped_count: 0, diff --git a/src/out/terminal/renderer.rs b/src/out/terminal/renderer.rs @@ -211,7 +211,7 @@ mod tests { )) .with_field(TerminalField::new("Listing", "AAAAAAAAAAAAAAAAAAAAAg")) .with_field(TerminalField::new("Transport", "nostr")) - .with_field(TerminalField::new("Relays", "2 acknowledged · 0 failed")) + .with_field(TerminalField::new("Targets", "2 acknowledged · 0 failed")) .with_field(TerminalField::new("Event", "9f3a…c12")) .with_next(TerminalAction::command( "radroots listing get AAAAAAAAAAAAAAAAAAAAAg", @@ -219,7 +219,7 @@ mod tests { assert_eq!( render_terminal_document(&document, &TerminalRenderContext::default()), - "✓ Listing published\n\n Listing AAAAAAAAAAAAAAAAAAAAAg\n Transport nostr\n Relays 2 acknowledged · 0 failed\n Event 9f3a…c12\n\nNext\n radroots listing get AAAAAAAAAAAAAAAAAAAAAg" + "✓ Listing published\n\n Listing AAAAAAAAAAAAAAAAAAAAAg\n Transport nostr\n Targets 2 acknowledged · 0 failed\n Event 9f3a…c12\n\nNext\n radroots listing get AAAAAAAAAAAAAAAAAAAAAg" ); } diff --git a/src/out/terminal/renderers/farm.rs b/src/out/terminal/renderers/farm.rs @@ -172,14 +172,14 @@ fn farm_publish_document(envelope: &OutputEnvelope, result: &Value) -> TerminalD common::push_path_field(&mut document, "Farm publish", result, &["farm", "state"]); push_publish_component_fields( &mut document, - "Profile relays", + "Profile targets", "Profile event", result, &["profile"], ); push_publish_component_fields( &mut document, - "Farm relays", + "Farm targets", "Farm event", result, &["farm"], @@ -289,7 +289,7 @@ fn push_missing_section(document: &mut TerminalDocument, result: &Value) { fn push_publish_component_fields( document: &mut TerminalDocument, - relay_label: &str, + target_label: &str, event_label: &str, result: &Value, path: &[&str], @@ -298,7 +298,7 @@ fn push_publish_component_fields( return; }; if let Some(summary) = relay_component_summary(component) { - common::push_field(document, relay_label, summary); + common::push_field(document, target_label, summary); } common::push_path_field(document, event_label, component, &["event_id"]); common::push_path_field(document, "Job", component, &["job_id"]); @@ -324,7 +324,7 @@ fn push_publish_components_section(document: &mut TerminalDocument, result: &Val vec![ TerminalTableColumn::new("Part", 4, 8), TerminalTableColumn::new("State", 5, 16), - TerminalTableColumn::new("Relays", 8, 28), + TerminalTableColumn::new("Targets", 8, 28), ], rows, "No publish components", @@ -332,13 +332,13 @@ fn push_publish_components_section(document: &mut TerminalDocument, result: &Val } fn relay_component_summary(value: &Value) -> Option<String> { - let acknowledged = common::array(value, &["acknowledged_relays"]) + let acknowledged = common::array(value, &["accepted_transport_endpoints"]) .map(Vec::len) .unwrap_or(0); - let failed = common::array(value, &["failed_relays"]) + let failed = common::array(value, &["failed_transport_targets"]) .map(Vec::len) .unwrap_or(0); - let target = common::array(value, &["target_relays"]) + let target = common::array(value, &["target_transport_endpoints"]) .map(Vec::len) .unwrap_or(0); (acknowledged > 0 || failed > 0 || target > 0) diff --git a/src/out/terminal/renderers/listing.rs b/src/out/terminal/renderers/listing.rs @@ -277,12 +277,12 @@ fn push_issues_section(document: &mut TerminalDocument, result: &Value) { fn push_relay_field(document: &mut TerminalDocument, result: &Value) { if let Some(summary) = relay_field_summary(result) { - common::push_field(document, "Relays", summary); + common::push_field(document, "Targets", summary); } } fn relay_field_summary(result: &Value) -> Option<String> { - let acknowledged = common::array(result, &["acknowledged_relays"]) + let acknowledged = common::array(result, &["accepted_transport_endpoints"]) .map(Vec::len) .or_else(|| { common::number_path(result, &["publish", "acknowledged_count"]) @@ -293,7 +293,7 @@ fn relay_field_summary(result: &Value) -> Option<String> { .and_then(|value| usize::try_from(value).ok()) }) .unwrap_or(0); - let failed = common::array(result, &["failed_relays"]) + let failed = common::array(result, &["failed_transport_targets"]) .map(Vec::len) .or_else(|| { common::number_path(result, &["publish", "failed_count"]) @@ -304,7 +304,7 @@ fn relay_field_summary(result: &Value) -> Option<String> { .and_then(|value| usize::try_from(value).ok()) }) .unwrap_or(0); - let target = common::array(result, &["target_relays"]) + let target = common::array(result, &["target_transport_endpoints"]) .map(Vec::len) .unwrap_or(0); (acknowledged > 0 || failed > 0 || target > 0) @@ -339,8 +339,8 @@ mod tests { "state": "published", "listing_id": "AAAAAAAAAAAAAAAAAAAAAg", "source": "SDK listing publish · configured signer", - "acknowledged_relays": ["wss://relay.example"], - "failed_relays": [], + "accepted_transport_endpoints": ["wss://relay.example"], + "failed_transport_targets": [], "event_id": "9f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac129f3ac12", "actions": ["radroots listing get AAAAAAAAAAAAAAAAAAAAAg"] }), @@ -351,7 +351,7 @@ mod tests { assert!(rendered.contains("✓ Listing published")); assert!(rendered.contains("Listing")); - assert!(rendered.contains("Relays")); + assert!(rendered.contains("Targets")); assert!(rendered.contains("Event")); assert!(!rendered.contains("Proof")); assert!(!rendered.contains("verified")); diff --git a/src/out/terminal/renderers/trade.rs b/src/out/terminal/renderers/trade.rs @@ -466,19 +466,19 @@ fn push_trade_identity_fields(document: &mut TerminalDocument, result: &Value) { } fn push_relay_field(document: &mut TerminalDocument, result: &Value) { - let acknowledged = array_len(result, &["acknowledged_relays"]); - let connected = array_len(result, &["connected_relays"]); - let failed = array_len(result, &["failed_relays"]); + let acknowledged = array_len(result, &["accepted_transport_endpoints"]); + let connected = array_len(result, &["attempted_transport_endpoints"]); + let failed = array_len(result, &["failed_transport_targets"]); if acknowledged > 0 || failed > 0 { common::push_field( document, - "Relays", + "Targets", relay_summary(acknowledged, failed, "acknowledged"), ); } else if connected > 0 || failed > 0 { common::push_field( document, - "Relays", + "Targets", relay_summary(connected, failed, "connected"), ); } @@ -694,7 +694,7 @@ mod tests { json!({ "state": "not_implemented", "trade_id": "trade_test", - "reason": "relay-backed trade event watch is not implemented", + "reason": "Nostr-backed trade event watch is not implemented", "actions": ["radroots trade status get trade_test"] }), ); diff --git a/src/out/terminal/renderers/validation.rs b/src/out/terminal/renderers/validation.rs @@ -161,12 +161,12 @@ fn push_invalid_receipts_section(document: &mut TerminalDocument, result: &Value } fn push_relay_field(document: &mut TerminalDocument, result: &Value) { - let connected = array_len(result, &["connected_relays"]); - let failed = array_len(result, &["failed_relays"]); + let connected = array_len(result, &["attempted_transport_endpoints"]); + let failed = array_len(result, &["failed_transport_targets"]); if connected > 0 || failed > 0 { common::push_field( document, - "Relays", + "Targets", relay_summary(connected, failed, "connected"), ); } diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -35,7 +35,7 @@ use crate::runtime::farm_config::{ use crate::runtime::local_events::append_local_work; use crate::runtime::sdk::{ CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_policy, - validate_configured_signer_for_actor, + sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, }; use crate::runtime::signer::ActorWriteBindingError; use crate::view::runtime::{ @@ -43,7 +43,7 @@ use crate::view::runtime::{ FarmPrivateExactLocationView, FarmPrivateLocationCandidateView, FarmPrivateLocationView, FarmPrivatePublicLocalityView, FarmPublicationView, FarmPublishComponentView, FarmPublishEventView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView, - FarmSetupView, FarmStatusView, RelayFailureView, + FarmSetupView, FarmStatusView, TransportTargetFailureView, }; const FARM_CONFIG_SOURCE: &str = "farm config · local first"; @@ -998,10 +998,10 @@ fn preview_component( rpc_method: rpc_method.to_owned(), event_kind, deduplicated: false, - target_relays: Vec::new(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), job_id: None, job_status: None, signer_mode: None, @@ -1107,7 +1107,7 @@ fn sdk_prepared_publish_view( ), FarmPublishComponentView { state: "not_submitted".to_owned(), - reason: Some("dry run requested; SDK enqueue and relay push skipped".to_owned()), + reason: Some("dry run requested; SDK enqueue and transport push skipped".to_owned()), signer_mode: Some(config.signer.backend.as_str().to_owned()), event_id: Some(plan.expected_event_id.as_str().to_owned()), event_addr: Some(plan.farm_addr.as_str().to_owned()), @@ -1120,7 +1120,7 @@ fn sdk_prepared_publish_view( None, ) }, - Some("dry run requested; SDK enqueue and relay push skipped".to_owned()), + Some("dry run requested; SDK enqueue and transport push skipped".to_owned()), vec!["radroots farm publish".to_owned()], ) } @@ -1154,16 +1154,18 @@ fn sdk_enqueued_publish_view( FarmPublishComponentView { state, deduplicated: matches!(enqueue.state, SdkMutationState::AlreadyQueued), - target_relays: push_event - .map(sdk_push_target_relays) + target_transport_endpoints: push_event + .map(sdk_push_target_transport_endpoints) .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()), - connected_relays: push_event - .map(sdk_push_connected_relays) + attempted_transport_endpoints: push_event + .map(sdk_push_attempted_transport_endpoints) .unwrap_or_default(), - acknowledged_relays: push_event - .map(sdk_push_acknowledged_relays) + accepted_transport_endpoints: push_event + .map(sdk_push_accepted_transport_endpoints) + .unwrap_or_default(), + failed_transport_targets: push_event + .map(sdk_push_failed_transport_targets) .unwrap_or_default(), - failed_relays: push_event.map(sdk_push_failed_relays).unwrap_or_default(), signer_mode: Some(config.signer.backend.as_str().to_owned()), event_id: Some(enqueue.signed_event_id.as_str().to_owned()), event_addr: Some(enqueue.farm_addr.as_str().to_owned()), @@ -1211,13 +1213,15 @@ fn sdk_publish_reason(push_event: Option<&PushOutboxEventReceipt>) -> Option<Str match push_event.map(|event| event.final_state) { Some(PushOutboxEventState::Published) => None, Some(PushOutboxEventState::PublishRetryable) => Some( - "SDK relay publish did not reach accepted quorum; outbox event remains retryable" + "SDK transport publish did not reach accepted quorum; outbox event remains retryable" .to_owned(), ), Some(PushOutboxEventState::FailedTerminal) => { - Some("SDK relay publish failed terminally".to_owned()) + Some("SDK transport publish failed terminally".to_owned()) } - Some(state) => Some(format!("SDK relay push left event in state `{state:?}`")), + Some(state) => Some(format!( + "SDK transport push left event in state `{state:?}`" + )), None => Some( "farm publish queued in SDK outbox; no ready SDK outbox event was pushed".to_owned(), ), @@ -1234,7 +1238,7 @@ fn sdk_publish_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<Strin Vec::new() } -fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_target_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -1242,7 +1246,7 @@ fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_attempted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -1251,7 +1255,7 @@ fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_accepted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -1266,7 +1270,9 @@ fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> { +fn sdk_push_failed_transport_targets( + event: &PushOutboxEventReceipt, +) -> Vec<TransportTargetFailureView> { event .targets .iter() @@ -1277,8 +1283,14 @@ fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureVie | PushOutboxTargetOutcomeKind::DuplicateAccepted ) }) - .map(|target| RelayFailureView { - relay: target.endpoint_uri.clone(), + .map(|target| TransportTargetFailureView { + transport_kind: target.transport_kind.clone(), + endpoint_uri: target.endpoint_uri.clone(), + target_scope: target.target_scope.clone(), + target_label: target.target_label.clone(), + transport_outcome_kind: target + .transport_outcome_kind + .map(sdk_transport_outcome_kind_label), reason: target .message .clone() diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -49,7 +49,7 @@ use crate::runtime::local_events::{ }; use crate::runtime::sdk::{ CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_policy, - validate_configured_signer_for_actor, + sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, }; use crate::runtime::sync::{ RelayIngestScope, freshness_for_scope_from_executor, market_refresh, missing_freshness, @@ -59,7 +59,7 @@ use crate::view::runtime::{ ListingAppRecordListView, ListingAppRecordSummaryView, ListingGetView, ListingListView, ListingMutationEventView, ListingMutationView, ListingNewView, ListingRebindView, ListingSummaryView, ListingValidateView, ListingValidationIssueView, MarketReadinessView, - RelayFailureView, + TransportTargetFailureView, }; const DRAFT_KIND: &str = "listing_draft_v1"; @@ -1869,10 +1869,10 @@ fn sdk_prepared_publish_view( event_kind: KIND_LISTING, dry_run: true, deduplicated: false, - target_relays: Vec::new(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), job_id: None, job_status: None, signer_mode: Some(config.signer.backend.as_str().to_owned()), @@ -1880,7 +1880,7 @@ fn sdk_prepared_publish_view( event_addr: Some(listing_addr), idempotency_key: args.idempotency_key.clone(), local_replica: None, - reason: Some("dry run requested; SDK enqueue and relay push skipped".to_owned()), + reason: Some("dry run requested; SDK enqueue and transport push skipped".to_owned()), job: None, event: args.print_event.then_some(event), actions: vec![format!("radroots listing publish {}", args.file.display())], @@ -1900,16 +1900,18 @@ fn sdk_enqueued_publish_view( .and_then(|receipt| sdk_push_event_for_listing(&enqueue, receipt)); let state = sdk_publish_state(args, push_event); let reason = sdk_publish_reason(args, push_event); - let target_relays = push_event - .map(sdk_push_target_relays) + let target_transport_endpoints = push_event + .map(sdk_push_target_transport_endpoints) .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()); - let connected_relays = push_event - .map(sdk_push_connected_relays) + let attempted_transport_endpoints = push_event + .map(sdk_push_attempted_transport_endpoints) .unwrap_or_default(); - let acknowledged_relays = push_event - .map(sdk_push_acknowledged_relays) + let accepted_transport_endpoints = push_event + .map(sdk_push_accepted_transport_endpoints) + .unwrap_or_default(); + let failed_transport_targets = push_event + .map(sdk_push_failed_transport_targets) .unwrap_or_default(); - let failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default(); let event_id = enqueue.signed_event_id.as_str().to_owned(); let listing_addr = enqueue.public_listing_addr.as_str().to_owned(); ListingMutationView { @@ -1925,10 +1927,10 @@ fn sdk_enqueued_publish_view( event_kind: KIND_LISTING, dry_run: false, deduplicated: matches!(enqueue.state, SdkMutationState::AlreadyQueued), - target_relays, - connected_relays, - acknowledged_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + accepted_transport_endpoints, + failed_transport_targets, job_id: None, job_status: None, signer_mode: Some(config.signer.backend.as_str().to_owned()), @@ -1987,15 +1989,18 @@ fn sdk_publish_reason( match push_event.map(|event| event.final_state) { Some(PushOutboxEventState::Published) => None, Some(PushOutboxEventState::PublishRetryable) => Some( - "SDK relay publish did not reach accepted quorum; outbox event remains retryable" + "SDK transport publish did not reach accepted quorum; outbox event remains retryable" .to_owned(), ), Some(PushOutboxEventState::FailedTerminal) => { - Some("SDK relay publish failed terminally".to_owned()) + Some("SDK transport publish failed terminally".to_owned()) } - Some(state) => Some(format!("SDK relay push left event in state `{state:?}`")), + Some(state) => Some(format!( + "SDK transport push left event in state `{state:?}`" + )), None if args.offline => Some( - "listing publish queued in SDK outbox; relay push skipped for offline mode".to_owned(), + "listing publish queued in SDK outbox; transport push skipped for offline mode" + .to_owned(), ), None => Some( "listing publish queued in SDK outbox; no ready SDK outbox event was pushed".to_owned(), @@ -2018,7 +2023,7 @@ fn sdk_publish_actions( Vec::new() } -fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_target_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -2026,7 +2031,7 @@ fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_attempted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -2035,7 +2040,7 @@ fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_accepted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -2050,7 +2055,9 @@ fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> { +fn sdk_push_failed_transport_targets( + event: &PushOutboxEventReceipt, +) -> Vec<TransportTargetFailureView> { event .targets .iter() @@ -2061,8 +2068,14 @@ fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureVie | PushOutboxTargetOutcomeKind::DuplicateAccepted ) }) - .map(|target| RelayFailureView { - relay: target.endpoint_uri.clone(), + .map(|target| TransportTargetFailureView { + transport_kind: target.transport_kind.clone(), + endpoint_uri: target.endpoint_uri.clone(), + target_scope: target.target_scope.clone(), + target_label: target.target_label.clone(), + transport_outcome_kind: target + .transport_outcome_kind + .map(sdk_transport_outcome_kind_label), reason: target .message .clone() @@ -3142,15 +3155,15 @@ fn encode_base64url_no_pad(bytes: [u8; 16]) -> String { mod tests { use super::{ DRAFT_KIND, ListingDraftDocument, encode_base64url_no_pad, generate_d_tag, - sdk_publish_actions, sdk_publish_reason, sdk_publish_state, sdk_push_acknowledged_relays, - sdk_push_failed_relays, + sdk_publish_actions, sdk_publish_reason, sdk_publish_state, + sdk_push_accepted_transport_endpoints, sdk_push_failed_transport_targets, }; use crate::cli::global::ListingMutationArgs; use radroots_events::ids::RadrootsEventId; use radroots_events_codec::d_tag::is_d_tag_base64url; use radroots_sdk::{ PushOutboxEventReceipt, PushOutboxEventState, PushOutboxTargetOutcomeKind, - PushOutboxTargetReceipt, + PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind, }; #[test] @@ -3179,17 +3192,17 @@ mod tests { assert!(sdk_publish_reason(&args, Some(&accepted)).is_none()); assert!(sdk_publish_actions(&args, Some(&accepted)).is_empty()); assert_eq!( - sdk_push_acknowledged_relays(&accepted), + sdk_push_accepted_transport_endpoints(&accepted), vec!["ws://127.0.0.1:19000".to_owned()] ); - assert!(sdk_push_failed_relays(&accepted).is_empty()); + assert!(sdk_push_failed_transport_targets(&accepted).is_empty()); let auth_required = sdk_push_event( PushOutboxEventState::PublishRetryable, PushOutboxTargetOutcomeKind::AuthRequired, Some("auth required".to_owned()), ); - let failed = sdk_push_failed_relays(&auth_required); + let failed = sdk_push_failed_transport_targets(&auth_required); assert_eq!( sdk_publish_state(&args, Some(&auth_required)), @@ -3201,7 +3214,7 @@ mod tests { .contains("accepted quorum") ); assert_eq!(failed.len(), 1); - assert_eq!(failed[0].relay, "ws://127.0.0.1:19000"); + assert_eq!(failed[0].endpoint_uri, "ws://127.0.0.1:19000"); assert_eq!(failed[0].reason, "auth required"); assert_eq!( sdk_publish_actions(&args, Some(&auth_required)), @@ -3376,13 +3389,47 @@ mod tests { targets: vec![PushOutboxTargetReceipt { transport_kind: "nostr".to_owned(), endpoint_uri: "ws://127.0.0.1:19000".to_owned(), + target_scope: None, + target_label: None, outcome_kind, + transport_outcome_kind: test_transport_outcome_kind(outcome_kind), attempted: true, message, }], } } + fn test_transport_outcome_kind( + kind: PushOutboxTargetOutcomeKind, + ) -> Option<PushOutboxTransportOutcomeKind> { + Some(match kind { + PushOutboxTargetOutcomeKind::Accepted => PushOutboxTransportOutcomeKind::Accepted, + PushOutboxTargetOutcomeKind::DuplicateAccepted => { + PushOutboxTransportOutcomeKind::DuplicateAccepted + } + PushOutboxTargetOutcomeKind::Timeout => PushOutboxTransportOutcomeKind::Timeout, + PushOutboxTargetOutcomeKind::ConnectionFailed => { + PushOutboxTransportOutcomeKind::ConnectionFailed + } + PushOutboxTargetOutcomeKind::AuthRequired + | PushOutboxTargetOutcomeKind::Blocked + | PushOutboxTargetOutcomeKind::RateLimited + | PushOutboxTargetOutcomeKind::Invalid + | PushOutboxTargetOutcomeKind::PowRequired + | PushOutboxTargetOutcomeKind::Restricted + | PushOutboxTargetOutcomeKind::Muted + | PushOutboxTargetOutcomeKind::Unsupported + | PushOutboxTargetOutcomeKind::PaymentRequired + | PushOutboxTargetOutcomeKind::Error + | PushOutboxTargetOutcomeKind::TargetUriRejected + | PushOutboxTargetOutcomeKind::SkippedAlreadyAccepted + | PushOutboxTargetOutcomeKind::DeferredUntilImplemented + | PushOutboxTargetOutcomeKind::PreviewUnavailable + | PushOutboxTargetOutcomeKind::Unknown => PushOutboxTransportOutcomeKind::Rejected, + _ => PushOutboxTransportOutcomeKind::Rejected, + }) + } + fn listing_mutation_args(offline: bool) -> ListingMutationArgs { ListingMutationArgs { file: "listing.toml".into(), diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -83,6 +83,7 @@ use crate::runtime::local_events::{ }; use crate::runtime::sdk::{ CliSdkAdapterError, CliSdkSession, fetch_relay_events_via_shared_transport, + sdk_transport_outcome_kind_label, }; use crate::runtime::sync::{RelayIngestScope, relay_provenance_relays_for_scope}; use crate::view::runtime::{ @@ -90,7 +91,7 @@ use crate::view::runtime::{ OrderCancellationView, OrderDecisionView, OrderDraftItemView, OrderEventListEntryView, OrderEventListView, OrderGetView, OrderIssueView, OrderListView, OrderNewView, OrderRebindView, OrderRevisionDecisionView, OrderRevisionProposalView, OrderStatusView, OrderSubmitView, - OrderSummaryView, OrderTradeLocatorView, RelayFailureView, + OrderSummaryView, OrderTradeLocatorView, TransportTargetFailureView, }; use self::sdk_status::sdk_order_status_view; @@ -103,7 +104,7 @@ const ORDER_DECISION_SOURCE: &str = "SDK trade decision · local key"; const ORDER_REVISION_PROPOSAL_SOURCE: &str = "SDK trade revision proposal · local key"; const ORDER_REVISION_DECISION_SOURCE: &str = "SDK trade revision decision · local key"; const ORDER_CANCELLATION_SOURCE: &str = "SDK trade cancellation · local key"; -const ORDER_EVENT_LIST_SOURCE: &str = "shared relay transport fetch · selected seller identity"; +const ORDER_EVENT_LIST_SOURCE: &str = "shared Nostr transport fetch · selected seller identity"; const ORDER_STATUS_SDK_SOURCE: &str = "SDK local trade projection"; const ORDER_EVENT_LIST_RELAY_ACTION: &str = "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com"; @@ -165,7 +166,7 @@ fn trade_publish_mode(config: &RuntimeConfig) -> PublishMode { fn trade_satisfaction_policy(mode: PublishMode) -> Result<SatisfactionPolicy, RuntimeError> { Ok(match mode { PublishMode::DryRun | PublishMode::EnqueueOnly => SatisfactionPolicy::NoWait, - PublishMode::EnqueueAndPublish => SatisfactionPolicy::AtLeastOneTarget, + PublishMode::EnqueueAndPublish => SatisfactionPolicy::AnyAccepted, _ => { return Err(RuntimeError::Config( "unsupported SDK publish mode for CLI trade workflow".to_owned(), @@ -897,10 +898,10 @@ pub fn submit( event_kind: None, dry_run: config.output.dry_run, deduplicated: false, - target_relays: Vec::new(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), idempotency_key: args.idempotency_key.clone(), signer_mode: None, reason: Some(reason), @@ -933,10 +934,10 @@ pub fn submit( event_kind: None, dry_run: config.output.dry_run, deduplicated: false, - target_relays: Vec::new(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), idempotency_key: args.idempotency_key.clone(), signer_mode: None, reason: Some(format!("trade draft `{}` was not found", args.key)), @@ -980,10 +981,10 @@ pub fn submit( event_kind: None, dry_run: config.output.dry_run, deduplicated: false, - target_relays: Vec::new(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), idempotency_key: args.idempotency_key.clone(), signer_mode: None, reason: Some("trade draft is not ready for submit".to_owned()), @@ -1592,7 +1593,7 @@ fn sdk_trade_decision_outcome_view( view.prev_event_id = Some(plan.request_event_id.to_string()); view.event_id = Some(plan.expected_event_id.to_string()); view.event_kind = Some(KIND_ORDER_DECISION); - view.target_relays = config.transport.nostr_relay_urls.clone(); + view.target_transport_endpoints = config.transport.nostr_relay_urls.clone(); view.reason = Some(format!( "dry run requested; seller trade {} publication skipped", args.decision.command() @@ -1634,7 +1635,7 @@ fn sdk_trade_revision_outcome_view( }) .collect(); view.economics = Some(revision.economics); - view.target_relays = config.transport.nostr_relay_urls.clone(); + view.target_transport_endpoints = config.transport.nostr_relay_urls.clone(); view.reason = Some("dry run requested; seller revision proposal publication skipped".to_owned()); view.actions = vec![format!("radroots trade status get {}", status.order_id)]; @@ -1669,7 +1670,7 @@ fn sdk_trade_revision_decision_outcome_view( view.prev_event_id = Some(plan.previous_event_id.to_string()); view.event_id = Some(plan.expected_event_id.to_string()); view.event_kind = Some(KIND_ORDER_REVISION_DECISION); - view.target_relays = config.transport.nostr_relay_urls.clone(); + view.target_transport_endpoints = config.transport.nostr_relay_urls.clone(); view.reason = Some(format!( "dry run requested; buyer revision {} publication skipped", args.decision.command() @@ -1700,7 +1701,7 @@ fn sdk_trade_cancellation_outcome_view( view.prev_event_id = Some(plan.previous_event_id.to_string()); view.event_id = Some(plan.expected_event_id.to_string()); view.event_kind = Some(KIND_ORDER_CANCELLATION); - view.target_relays = config.transport.nostr_relay_urls.clone(); + view.target_transport_endpoints = config.transport.nostr_relay_urls.clone(); view.reason = Some("dry run requested; buyer trade cancellation publication skipped".to_owned()); view.actions = vec![format!("radroots trade status get {}", status.order_id)]; @@ -1789,7 +1790,7 @@ fn order_event_list_unconfigured( seller_pubkey: Option<String>, actor_context_source: &'static str, reason: String, - target_relays: Vec<String>, + target_transport_endpoints: Vec<String>, actions: Vec<String>, ) -> OrderEventListView { OrderEventListView { @@ -1797,9 +1798,9 @@ fn order_event_list_unconfigured( source: ORDER_EVENT_LIST_SOURCE.to_owned(), actor_context_source: actor_context_source.to_owned(), seller_pubkey, - target_relays, - connected_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints, + attempted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), fetched_count: 0, decoded_count: 0, skipped_count: 0, @@ -1814,22 +1815,22 @@ fn order_event_list_unavailable( seller_pubkey: String, actor_context_source: &'static str, reason: String, - target_relays: Vec<String>, - failed_relays: Vec<RadrootsRelayFetchFailure>, + target_transport_endpoints: Vec<String>, + failed_transport_targets: Vec<RadrootsRelayFetchFailure>, ) -> OrderEventListView { OrderEventListView { state: "unavailable".to_owned(), source: ORDER_EVENT_LIST_SOURCE.to_owned(), actor_context_source: actor_context_source.to_owned(), seller_pubkey: Some(seller_pubkey), - target_relays, - connected_relays: Vec::new(), - failed_relays: relay_failures(failed_relays), + target_transport_endpoints, + attempted_transport_endpoints: Vec::new(), + failed_transport_targets: relay_failures(failed_transport_targets), fetched_count: 0, decoded_count: 0, skipped_count: 0, count: 0, - reason: Some(format!("relay transport fetch failed: {reason}")), + reason: Some(format!("Nostr transport fetch failed: {reason}")), orders: Vec::new(), actions: Vec::new(), } @@ -1875,9 +1876,9 @@ fn order_event_list_from_receipt( let reason = if orders.is_empty() { Some(match order_id { Some(order_id) => { - format!("no relay-backed order request events matched `{order_id}`") + format!("no Nostr-backed order request events matched `{order_id}`") } - None => "no relay-backed order request events matched the selected seller".to_owned(), + None => "no Nostr-backed order request events matched the selected seller".to_owned(), }) } else { None @@ -1888,9 +1889,9 @@ fn order_event_list_from_receipt( source: ORDER_EVENT_LIST_SOURCE.to_owned(), actor_context_source: actor_context_source.to_owned(), seller_pubkey: Some(seller_pubkey), - target_relays, - connected_relays, - failed_relays: relay_failures(failed_relays), + target_transport_endpoints: target_relays, + attempted_transport_endpoints: connected_relays, + failed_transport_targets: relay_failures(failed_relays), fetched_count, decoded_count, skipped_count, @@ -1924,10 +1925,10 @@ fn order_decision_base_view( event_kind: None, inventory: None, dry_run, - target_relays: config.transport.nostr_relay_urls.clone(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: config.transport.nostr_relay_urls.clone(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), fetched_count: 0, decoded_count: 0, skipped_count: 0, @@ -1964,10 +1965,10 @@ fn order_revision_base_view( economics: None, inventory: None, dry_run, - target_relays: config.transport.nostr_relay_urls.clone(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: config.transport.nostr_relay_urls.clone(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), fetched_count: 0, decoded_count: 0, skipped_count: 0, @@ -2005,10 +2006,10 @@ fn order_revision_decision_base_view( economics: None, inventory: None, dry_run, - target_relays: config.transport.nostr_relay_urls.clone(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: config.transport.nostr_relay_urls.clone(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), fetched_count: 0, decoded_count: 0, skipped_count: 0, @@ -2042,10 +2043,10 @@ fn order_cancellation_base_view( event_kind: None, cancellation_reason: Some(args.reason.clone()), dry_run, - target_relays: config.transport.nostr_relay_urls.clone(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: config.transport.nostr_relay_urls.clone(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), fetched_count: 0, decoded_count: 0, skipped_count: 0, @@ -2067,9 +2068,9 @@ fn apply_order_cancellation_status(view: &mut OrderCancellationView, status: &Or view.decision_event_id = status.decision_event_id.clone(); view.root_event_id = status.request_event_id.clone(); view.prev_event_id = order_cancellation_prev_event_id(status); - view.target_relays = status.target_relays.clone(); - view.connected_relays = status.connected_relays.clone(); - view.failed_relays = status.failed_relays.clone(); + view.target_transport_endpoints = status.target_transport_endpoints.clone(); + view.attempted_transport_endpoints = status.attempted_transport_endpoints.clone(); + view.failed_transport_targets = status.failed_transport_targets.clone(); view.fetched_count = status.fetched_count; view.decoded_count = status.decoded_count; view.skipped_count = status.skipped_count; @@ -2097,9 +2098,9 @@ fn apply_order_decision_status(view: &mut OrderDecisionView, status: &OrderStatu view.listing_event_id = status.listing_event_id.clone(); view.root_event_id = status.request_event_id.clone(); view.prev_event_id = status.last_event_id.clone(); - view.target_relays = status.target_relays.clone(); - view.connected_relays = status.connected_relays.clone(); - view.failed_relays = status.failed_relays.clone(); + view.target_transport_endpoints = status.target_transport_endpoints.clone(); + view.attempted_transport_endpoints = status.attempted_transport_endpoints.clone(); + view.failed_transport_targets = status.failed_transport_targets.clone(); view.fetched_count = status.fetched_count; view.decoded_count = status.decoded_count; view.skipped_count = status.skipped_count; @@ -2119,9 +2120,9 @@ fn apply_order_revision_status(view: &mut OrderRevisionProposalView, status: &Or view.prev_event_id = status.last_event_id.clone(); view.economics = status.economics.clone(); view.inventory = status.inventory.clone(); - view.target_relays = status.target_relays.clone(); - view.connected_relays = status.connected_relays.clone(); - view.failed_relays = status.failed_relays.clone(); + view.target_transport_endpoints = status.target_transport_endpoints.clone(); + view.attempted_transport_endpoints = status.attempted_transport_endpoints.clone(); + view.failed_transport_targets = status.failed_transport_targets.clone(); view.fetched_count = status.fetched_count; view.decoded_count = status.decoded_count; view.skipped_count = status.skipped_count; @@ -2144,9 +2145,9 @@ fn apply_order_revision_decision_status( view.prev_event_id = status.last_event_id.clone(); view.economics = status.economics.clone(); view.inventory = status.inventory.clone(); - view.target_relays = status.target_relays.clone(); - view.connected_relays = status.connected_relays.clone(); - view.failed_relays = status.failed_relays.clone(); + view.target_transport_endpoints = status.target_transport_endpoints.clone(); + view.attempted_transport_endpoints = status.attempted_transport_endpoints.clone(); + view.failed_transport_targets = status.failed_transport_targets.clone(); view.fetched_count = status.fetched_count; view.decoded_count = status.decoded_count; view.skipped_count = status.skipped_count; @@ -2408,16 +2409,18 @@ fn sdk_enqueued_order_decision_view( view.prev_event_id = Some(enqueue.request_event_id.to_string()); view.event_id = Some(enqueue.signed_event_id.as_str().to_owned()); view.event_kind = Some(KIND_ORDER_DECISION); - view.target_relays = push_event - .map(sdk_push_target_relays) + view.target_transport_endpoints = push_event + .map(sdk_push_target_transport_endpoints) .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()); - view.connected_relays = push_event - .map(sdk_push_connected_relays) + view.attempted_transport_endpoints = push_event + .map(sdk_push_attempted_transport_endpoints) + .unwrap_or_default(); + view.accepted_transport_endpoints = push_event + .map(sdk_push_accepted_transport_endpoints) .unwrap_or_default(); - view.acknowledged_relays = push_event - .map(sdk_push_acknowledged_relays) + view.failed_transport_targets = push_event + .map(sdk_push_failed_transport_targets) .unwrap_or_default(); - view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default(); view.reason = sdk_order_decision_reason(&enqueue.workflow, push_event); view.actions = sdk_order_decision_actions(push_event); view @@ -2453,17 +2456,17 @@ fn sdk_order_decision_reason( match push_event.map(|event| event.final_state) { Some(PushOutboxEventState::Published) => None, Some(PushOutboxEventState::PublishRetryable) => Some(format!( - "{}; SDK relay publish did not reach accepted quorum; outbox event remains retryable; {}", + "{}; SDK transport publish did not reach accepted quorum; outbox event remains retryable; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), Some(PushOutboxEventState::FailedTerminal) => Some(format!( - "{}; SDK relay publish failed terminally; {}", + "{}; SDK transport publish failed terminally; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), Some(state) => Some(format!( - "{}; SDK relay push left event in state `{state:?}`; {}", + "{}; SDK transport push left event in state `{state:?}`; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), @@ -2517,16 +2520,18 @@ fn sdk_enqueued_order_revision_view( view.economics = Some(revision.economics); view.event_id = Some(enqueue.signed_event_id.as_str().to_owned()); view.event_kind = Some(KIND_ORDER_REVISION_PROPOSAL); - view.target_relays = push_event - .map(sdk_push_target_relays) + view.target_transport_endpoints = push_event + .map(sdk_push_target_transport_endpoints) .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()); - view.connected_relays = push_event - .map(sdk_push_connected_relays) + view.attempted_transport_endpoints = push_event + .map(sdk_push_attempted_transport_endpoints) .unwrap_or_default(); - view.acknowledged_relays = push_event - .map(sdk_push_acknowledged_relays) + view.accepted_transport_endpoints = push_event + .map(sdk_push_accepted_transport_endpoints) + .unwrap_or_default(); + view.failed_transport_targets = push_event + .map(sdk_push_failed_transport_targets) .unwrap_or_default(); - view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default(); view.reason = sdk_order_lifecycle_reason("trade revision proposal", &enqueue.workflow, push_event); view.actions = sdk_order_lifecycle_actions(push_event); @@ -2560,16 +2565,18 @@ fn sdk_enqueued_order_revision_decision_view( if args.decision == TradeRevisionDecisionArg::Accept { view.agreement_event_id = Some(enqueue.signed_event_id.as_str().to_owned()); } - view.target_relays = push_event - .map(sdk_push_target_relays) + view.target_transport_endpoints = push_event + .map(sdk_push_target_transport_endpoints) .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()); - view.connected_relays = push_event - .map(sdk_push_connected_relays) + view.attempted_transport_endpoints = push_event + .map(sdk_push_attempted_transport_endpoints) + .unwrap_or_default(); + view.accepted_transport_endpoints = push_event + .map(sdk_push_accepted_transport_endpoints) .unwrap_or_default(); - view.acknowledged_relays = push_event - .map(sdk_push_acknowledged_relays) + view.failed_transport_targets = push_event + .map(sdk_push_failed_transport_targets) .unwrap_or_default(); - view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default(); view.reason = sdk_order_lifecycle_reason("trade revision decision", &enqueue.workflow, push_event); view.actions = sdk_order_lifecycle_actions(push_event); @@ -2597,16 +2604,18 @@ fn sdk_enqueued_order_cancellation_view( view.prev_event_id = Some(enqueue.previous_event_id.to_string()); view.event_id = Some(enqueue.signed_event_id.as_str().to_owned()); view.event_kind = Some(KIND_ORDER_CANCELLATION); - view.target_relays = push_event - .map(sdk_push_target_relays) + view.target_transport_endpoints = push_event + .map(sdk_push_target_transport_endpoints) .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()); - view.connected_relays = push_event - .map(sdk_push_connected_relays) + view.attempted_transport_endpoints = push_event + .map(sdk_push_attempted_transport_endpoints) + .unwrap_or_default(); + view.accepted_transport_endpoints = push_event + .map(sdk_push_accepted_transport_endpoints) .unwrap_or_default(); - view.acknowledged_relays = push_event - .map(sdk_push_acknowledged_relays) + view.failed_transport_targets = push_event + .map(sdk_push_failed_transport_targets) .unwrap_or_default(); - view.failed_relays = push_event.map(sdk_push_failed_relays).unwrap_or_default(); view.reason = sdk_order_lifecycle_reason("trade cancellation", &enqueue.workflow, push_event); view.actions = sdk_order_lifecycle_actions(push_event); view @@ -2641,17 +2650,17 @@ fn sdk_order_lifecycle_reason( match push_event.map(|event| event.final_state) { Some(PushOutboxEventState::Published) => None, Some(PushOutboxEventState::PublishRetryable) => Some(format!( - "{}; SDK relay publish for {workflow} did not reach accepted quorum; outbox event remains retryable; {}", + "{}; SDK transport publish for {workflow} did not reach accepted quorum; outbox event remains retryable; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), Some(PushOutboxEventState::FailedTerminal) => Some(format!( - "{}; SDK relay publish for {workflow} failed terminally; {}", + "{}; SDK transport publish for {workflow} failed terminally; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), Some(state) => Some(format!( - "{}; SDK relay push for {workflow} left event in state `{state:?}`; {}", + "{}; SDK transport push for {workflow} left event in state `{state:?}`; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), @@ -4642,10 +4651,10 @@ fn order_submit_app_signed_evidence_view( event_kind: Some(KIND_ORDER_REQUEST), dry_run: config.output.dry_run, deduplicated: true, - target_relays: Vec::new(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), idempotency_key: args.idempotency_key.clone(), signer_mode: None, reason: Some( @@ -4678,10 +4687,10 @@ fn order_submit_app_signed_evidence_view( event_kind: Some(KIND_ORDER_REQUEST), dry_run: config.output.dry_run, deduplicated: false, - target_relays: Vec::new(), - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), idempotency_key: args.idempotency_key.clone(), signer_mode: None, reason: Some( @@ -4758,7 +4767,7 @@ fn order_submit_dry_run_view( loaded: &LoadedOrderDraft, args: &TradeSubmitArgs, plan: TradeSubmitPlan, - target_relays: Vec<String>, + target_transport_endpoints: Vec<String>, ) -> OrderSubmitView { OrderSubmitView { state: "dry_run".to_owned(), @@ -4786,13 +4795,13 @@ fn order_submit_dry_run_view( event_kind: Some(KIND_ORDER_REQUEST), dry_run: true, deduplicated: false, - target_relays, - connected_relays: Vec::new(), - acknowledged_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints, + attempted_transport_endpoints: Vec::new(), + accepted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), idempotency_key: args.idempotency_key.clone(), signer_mode: Some(config.signer.backend.as_str().to_owned()), - reason: Some("dry run requested; SDK enqueue and relay push skipped".to_owned()), + reason: Some("dry run requested; SDK enqueue and transport push skipped".to_owned()), job: None, issues: Vec::new(), actions: vec![format!( @@ -4953,16 +4962,18 @@ fn sdk_enqueued_order_submit_view( event_kind: Some(KIND_ORDER_REQUEST), dry_run: false, deduplicated: matches!(enqueue.state, SdkMutationState::AlreadyQueued), - target_relays: push_event - .map(sdk_push_target_relays) + target_transport_endpoints: push_event + .map(sdk_push_target_transport_endpoints) .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()), - connected_relays: push_event - .map(sdk_push_connected_relays) + attempted_transport_endpoints: push_event + .map(sdk_push_attempted_transport_endpoints) + .unwrap_or_default(), + accepted_transport_endpoints: push_event + .map(sdk_push_accepted_transport_endpoints) .unwrap_or_default(), - acknowledged_relays: push_event - .map(sdk_push_acknowledged_relays) + failed_transport_targets: push_event + .map(sdk_push_failed_transport_targets) .unwrap_or_default(), - failed_relays: push_event.map(sdk_push_failed_relays).unwrap_or_default(), idempotency_key: args.idempotency_key.clone(), signer_mode: Some(config.signer.backend.as_str().to_owned()), reason: sdk_order_submit_reason(&enqueue.workflow, push_event), @@ -4999,17 +5010,17 @@ fn sdk_order_submit_reason( match push_event.map(|event| event.final_state) { Some(PushOutboxEventState::Published) => None, Some(PushOutboxEventState::PublishRetryable) => Some(format!( - "{}; SDK relay publish did not reach accepted quorum; outbox event remains retryable; {}", + "{}; SDK transport publish did not reach accepted quorum; outbox event remains retryable; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), Some(PushOutboxEventState::FailedTerminal) => Some(format!( - "{}; SDK relay publish failed terminally; {}", + "{}; SDK transport publish failed terminally; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), Some(state) => Some(format!( - "{}; SDK relay push left event in state `{state:?}`; {}", + "{}; SDK transport push left event in state `{state:?}`; {}", sdk_order_enqueue_summary(enqueue), sdk_order_enqueue_retry_summary(enqueue) )), @@ -5031,7 +5042,7 @@ fn sdk_order_submit_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec< Vec::new() } -fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_target_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -5039,7 +5050,7 @@ fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_attempted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -5048,7 +5059,7 @@ fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> { +fn sdk_push_accepted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { event .targets .iter() @@ -5063,7 +5074,9 @@ fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> { .collect() } -fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> { +fn sdk_push_failed_transport_targets( + event: &PushOutboxEventReceipt, +) -> Vec<TransportTargetFailureView> { event .targets .iter() @@ -5074,8 +5087,14 @@ fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureVie | PushOutboxTargetOutcomeKind::DuplicateAccepted ) }) - .map(|target| RelayFailureView { - relay: target.endpoint_uri.clone(), + .map(|target| TransportTargetFailureView { + transport_kind: target.transport_kind.clone(), + endpoint_uri: target.endpoint_uri.clone(), + target_scope: target.target_scope.clone(), + target_label: target.target_label.clone(), + transport_outcome_kind: target + .transport_outcome_kind + .map(sdk_transport_outcome_kind_label), reason: target .message .clone() @@ -5243,22 +5262,26 @@ fn order_relay_fetch_error(error: RadrootsRelayTransportError) -> RuntimeError { RuntimeError::Network(error.to_string()) } -fn relay_fetch_failure_reason(failed_relays: &[RadrootsRelayFetchFailure]) -> String { - if failed_relays.is_empty() { +fn relay_fetch_failure_reason(failed_transport_targets: &[RadrootsRelayFetchFailure]) -> String { + if failed_transport_targets.is_empty() { return "no relay acknowledged the fetch".to_owned(); } - failed_relays + failed_transport_targets .iter() .map(|failure| format!("{}: {}", failure.relay_url, failure.reason)) .collect::<Vec<_>>() .join("; ") } -fn relay_failures(failures: Vec<RadrootsRelayFetchFailure>) -> Vec<RelayFailureView> { +fn relay_failures(failures: Vec<RadrootsRelayFetchFailure>) -> Vec<TransportTargetFailureView> { failures .into_iter() - .map(|failure| RelayFailureView { - relay: failure.relay_url, + .map(|failure| TransportTargetFailureView { + transport_kind: "nostr".to_owned(), + endpoint_uri: failure.relay_url, + target_scope: None, + target_label: None, + transport_outcome_kind: None, reason: failure.reason, }) .collect() diff --git a/src/runtime/order/sdk_status.rs b/src/runtime/order/sdk_status.rs @@ -58,9 +58,9 @@ pub(super) fn sdk_order_status_view(receipt: TradeStatusReceipt) -> OrderStatusV sdk_receipt, ambiguity_candidates, reducer_issues, - target_relays: Vec::new(), - connected_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), fetched_count: 0, decoded_count: receipt.event_count, skipped_count: 0, diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -16,12 +16,13 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectClientTarget, RadrootsNostrConnectError, RadrootsNostrConnectUri, }; use radroots_sdk::{ - HybridProfile, MeshScopeId, NostrProfile, NostrRelayUrlPolicy, ProxyProfile, RadrootsClient, - RadrootsClientBuilder, RadrootsSdkError, RadrootsSdkLocalKeySigner, - RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, - RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, RadrootsSdkStorageConfig, - ReticulumPreviewAgentEndpoint, ReticulumPreviewBehavior as SdkReticulumPreviewBehavior, - ReticulumPreviewProfile, TargetPolicy, TransportProfile, + HybridProfile, MeshScopeId, NostrProfile, NostrRelayUrlPolicy, ProxyProfile, + PushOutboxTransportOutcomeKind, RadrootsClient, RadrootsClientBuilder, RadrootsSdkError, + RadrootsSdkLocalKeySigner, RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, + RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, + RadrootsSdkStorageConfig, ReticulumPreviewAgentEndpoint, + ReticulumPreviewBehavior as SdkReticulumPreviewBehavior, ReticulumPreviewProfile, TargetPolicy, + TradeValidationReceiptNostrRelayTransportOutcomeKind, TransportProfile, }; use radroots_transport_nostr::{ RadrootsNostrClientFetchAdapter, RadrootsRelayFetchRequest, RadrootsRelayFetchedEventsReceipt, @@ -51,6 +52,24 @@ pub enum CliSdkAdapterError { Sdk(#[from] RadrootsSdkError), } +pub fn sdk_transport_outcome_kind_label(kind: PushOutboxTransportOutcomeKind) -> String { + sdk_enum_label(kind) +} + +pub fn sdk_validation_transport_outcome_kind_label( + kind: TradeValidationReceiptNostrRelayTransportOutcomeKind, +) -> String { + sdk_enum_label(kind) +} + +fn sdk_enum_label<T: serde::Serialize>(value: T) -> String { + match serde_json::to_value(value) { + Ok(serde_json::Value::String(value)) => value, + Ok(value) => panic!("SDK enum serialized to non-string JSON value `{value}`"), + Err(error) => panic!("SDK enum serialization failed: {error}"), + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct CliSdkConfig { pub storage_root: PathBuf, @@ -1160,7 +1179,7 @@ mod tests { "ReplicaSql::new", "SqliteExecutor::open(&config.local.replica_db_path)", "outbox_idempotency_digest", - "canonical_target_relays", + "canonical_target_transport_endpoints", "radroots_sdk::protocol::order", "build_order_request_draft", "build_order_decision_draft", diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -37,7 +37,7 @@ use crate::runtime::RuntimeError; use crate::runtime::config::{RuntimeConfig, TransportProfileKind}; use crate::runtime::sdk::{ CliSdkAdapterError, CliSdkSession, fetch_relay_events_via_shared_transport, - sdk_nostr_relay_url_policy, + sdk_nostr_relay_url_policy, sdk_transport_outcome_kind_label, }; use crate::view::runtime::{ SyncActionView, SyncFreshnessView, SyncQueueView, SyncRunFreshnessView, SyncStatusView, @@ -54,7 +54,7 @@ const SYNC_PULL_ACTION: &str = "radroots sync pull"; const SYNC_PUSH_ACTION: &str = "radroots sync push"; const SYNC_READY_ACTION: &str = "radroots market product search eggs"; const MARKET_READY_ACTION: &str = "radroots market product search eggs"; -const INGEST_SOURCE: &str = "shared relay transport fetch · local replica ingest"; +const INGEST_SOURCE: &str = "shared Nostr transport fetch · local replica ingest"; const RELAY_FETCH_LIMIT: usize = 1_000; const RELAY_FETCH_MAX_PAGES: usize = 5; const MARKET_FRESHNESS_STALE_AFTER_SECONDS: u64 = 15 * 60; @@ -236,7 +236,7 @@ where if config.output.dry_run { let mut view = empty_action_from_snapshot(snapshot, "pull"); view.state = "ready".to_owned(); - view.reason = Some("dry run requested; relay fetch skipped".to_owned()); + view.reason = Some("dry run requested; Nostr fetch skipped".to_owned()); view.target_transport_endpoints = config.transport.nostr_relay_urls.clone(); view.fetched_count = Some(0); view.ingested_count = Some(0); @@ -256,7 +256,7 @@ where let target_transport_endpoints = receipt.target_relays; let failed_transport_targets = relay_failures(receipt.failed_relays); let reason = relay_failure_reason(&failed_transport_targets); - let failure_reason = format!("relay transport fetch failed: {reason}"); + let failure_reason = format!("Nostr transport fetch failed: {reason}"); let executor = SqliteExecutor::open(&config.local.replica_db_path)?; migrations::run_all_up(&executor)?; record_sync_run( @@ -273,7 +273,7 @@ where let mut view = empty_action_from_snapshot(snapshot, "pull"); view.state = "unavailable".to_owned(); view.reason = Some(failure_reason); - view.reason_code = Some("relay_fetch_failed".to_owned()); + view.reason_code = Some("nostr_fetch_failed".to_owned()); view.target_transport_endpoints = target_transport_endpoints; view.failed_transport_targets = failed_transport_targets; view.freshness = freshness_for_scope_from_executor(config, &executor, scope)?; @@ -298,7 +298,7 @@ where let mut view = empty_action_from_snapshot(snapshot, "pull"); view.state = "unavailable".to_owned(); view.reason = Some(failure_reason); - view.reason_code = Some("relay_fetch_failed".to_owned()); + view.reason_code = Some("nostr_fetch_failed".to_owned()); view.target_transport_endpoints = config.transport.nostr_relay_urls.clone(); view.freshness = freshness_for_scope_from_executor(config, &executor, scope)?; return Ok(view); @@ -588,6 +588,8 @@ fn sdk_transport_targets(receipt: &SyncStatusReceipt) -> Vec<SyncTransportTarget transport_kind: target.transport_kind.clone(), endpoint_uri: target.endpoint_uri.clone(), endpoint_fingerprint: target.endpoint_fingerprint.clone(), + target_scope: target.target_scope.clone(), + target_label: target.target_label.clone(), }) .collect() } @@ -834,6 +836,11 @@ fn sdk_push_failed_transport_targets( .map(|target| TransportTargetFailureView { transport_kind: target.transport_kind.clone(), endpoint_uri: target.endpoint_uri.clone(), + target_scope: target.target_scope.clone(), + target_label: target.target_label.clone(), + transport_outcome_kind: target + .transport_outcome_kind + .map(sdk_transport_outcome_kind_label), reason: target .message .clone() @@ -935,6 +942,8 @@ fn sync_transport_target_view( transport_kind: target.kind.canonical_label(), endpoint_uri: target.uri.as_str().to_owned(), endpoint_fingerprint: target.fingerprint.as_str().to_owned(), + target_scope: target.scope.as_ref().map(|scope| scope.as_str().to_owned()), + target_label: target.label.as_ref().map(|label| label.as_str().to_owned()), }) } @@ -1554,7 +1563,7 @@ fn relay_ingest_reason_code( ) -> Option<&'static str> { ingest .reason_code() - .or_else(|| (!failed_transport_targets.is_empty()).then_some("relay_fetch_partial")) + .or_else(|| (!failed_transport_targets.is_empty()).then_some("nostr_fetch_partial")) } fn relay_ingest_reason( @@ -1688,6 +1697,9 @@ fn relay_failures(failures: Vec<RadrootsRelayFetchFailure>) -> Vec<TransportTarg .map(|failure| TransportTargetFailureView { transport_kind: "nostr".to_owned(), endpoint_uri: failure.relay_url, + target_scope: None, + target_label: None, + transport_outcome_kind: None, reason: failure.reason, }) .collect() @@ -1781,9 +1793,9 @@ mod tests { }; use radroots_sdk::{ PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, - PushOutboxTargetOutcomeKind, PushOutboxTargetReceipt, SyncEventStoreStatus, - SyncOutboxStatus, SyncStatusReceipt, SyncStatusSource, SyncTransportProfileSummary, - SyncTransportStatusSummary, SyncTransportTargetSummary, + PushOutboxTargetOutcomeKind, PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind, + SyncEventStoreStatus, SyncOutboxStatus, SyncStatusReceipt, SyncStatusSource, + SyncTransportProfileSummary, SyncTransportStatusSummary, SyncTransportTargetSummary, }; use radroots_secret_vault::RadrootsSecretBackend; use radroots_transport::{ @@ -2087,11 +2099,15 @@ mod tests { transport_kind: "nostr".to_owned(), endpoint_uri: "wss://relay.example.com".to_owned(), endpoint_fingerprint: "0".repeat(64), + target_scope: None, + target_label: None, }, SyncTransportTargetSummary { transport_kind: "reticulum".to_owned(), endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), endpoint_fingerprint: "1".repeat(64), + target_scope: Some("local_preview".to_owned()), + target_label: None, }, ], transport_statuses: vec![ @@ -2476,6 +2492,8 @@ mod tests { transport_kind: "nostr".to_owned(), endpoint_uri: (*relay).to_owned(), endpoint_fingerprint: format!("test-fingerprint-{index}"), + target_scope: None, + target_label: None, }) .collect(), transport_statuses: vec![SyncTransportStatusSummary { @@ -2525,6 +2543,8 @@ mod tests { transport_kind: "reticulum".to_owned(), endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), endpoint_fingerprint: "1".repeat(64), + target_scope: Some("local_preview".to_owned()), + target_label: None, }], transport_statuses: vec![SyncTransportStatusSummary { transport: "reticulum".to_owned(), @@ -2582,7 +2602,10 @@ mod tests { targets: vec![PushOutboxTargetReceipt { transport_kind: "nostr".to_owned(), endpoint_uri: relay_url.to_owned(), + target_scope: None, + target_label: None, outcome_kind, + transport_outcome_kind: test_transport_outcome_kind(outcome_kind), attempted: true, message, }], @@ -2606,13 +2629,51 @@ mod tests { targets: vec![PushOutboxTargetReceipt { transport_kind: "reticulum".to_owned(), endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + target_scope: Some("local_preview".to_owned()), + target_label: None, outcome_kind, + transport_outcome_kind: test_transport_outcome_kind(outcome_kind), attempted: false, message: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()), }], } } + fn test_transport_outcome_kind( + kind: PushOutboxTargetOutcomeKind, + ) -> Option<PushOutboxTransportOutcomeKind> { + Some(match kind { + PushOutboxTargetOutcomeKind::Accepted => PushOutboxTransportOutcomeKind::Accepted, + PushOutboxTargetOutcomeKind::DuplicateAccepted => { + PushOutboxTransportOutcomeKind::DuplicateAccepted + } + PushOutboxTargetOutcomeKind::DeferredUntilImplemented => { + PushOutboxTransportOutcomeKind::DeferredUntilImplemented + } + PushOutboxTargetOutcomeKind::Timeout => PushOutboxTransportOutcomeKind::Timeout, + PushOutboxTargetOutcomeKind::ConnectionFailed => { + PushOutboxTransportOutcomeKind::ConnectionFailed + } + PushOutboxTargetOutcomeKind::PreviewUnavailable => { + PushOutboxTransportOutcomeKind::TransportUnavailable + } + PushOutboxTargetOutcomeKind::AuthRequired + | PushOutboxTargetOutcomeKind::Blocked + | PushOutboxTargetOutcomeKind::RateLimited + | PushOutboxTargetOutcomeKind::Invalid + | PushOutboxTargetOutcomeKind::PowRequired + | PushOutboxTargetOutcomeKind::Restricted + | PushOutboxTargetOutcomeKind::Muted + | PushOutboxTargetOutcomeKind::Unsupported + | PushOutboxTargetOutcomeKind::PaymentRequired + | PushOutboxTargetOutcomeKind::Error + | PushOutboxTargetOutcomeKind::TargetUriRejected + | PushOutboxTargetOutcomeKind::SkippedAlreadyAccepted + | PushOutboxTargetOutcomeKind::Unknown => PushOutboxTransportOutcomeKind::Rejected, + _ => PushOutboxTransportOutcomeKind::Rejected, + }) + } + #[test] fn sync_pull_ingests_relay_events_and_market_reads_without_daemon() { let dir = tempdir().expect("tempdir"); @@ -2760,7 +2821,7 @@ mod tests { ); assert_eq!(view.failed_transport_targets.len(), 1); assert_eq!(view.failed_count, Some(1)); - assert_eq!(view.reason_code.as_deref(), Some("relay_fetch_partial")); + assert_eq!(view.reason_code.as_deref(), Some("nostr_fetch_partial")); assert!( view.reason .as_deref() diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs @@ -715,7 +715,9 @@ fn string_array_input(input: &OperationData, key: &str) -> Vec<String> { mod tests { use super::*; use radroots_events::ids::RadrootsEventId; - use radroots_sdk::{PushOutboxEventReceipt, PushOutboxTargetReceipt}; + use radroots_sdk::{ + PushOutboxEventReceipt, PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind, + }; #[test] fn transport_outbox_push_reports_reticulum_preview_without_attempts() { @@ -767,7 +769,18 @@ mod tests { targets: vec![PushOutboxTargetReceipt { transport_kind: "reticulum".to_owned(), endpoint_uri: RADROOTS_RETICULUM_PREVIEW_ENDPOINT_URI.to_owned(), + target_scope: Some("local_preview".to_owned()), + target_label: None, outcome_kind, + transport_outcome_kind: Some(match outcome_kind { + PushOutboxTargetOutcomeKind::DeferredUntilImplemented => { + PushOutboxTransportOutcomeKind::DeferredUntilImplemented + } + PushOutboxTargetOutcomeKind::PreviewUnavailable => { + PushOutboxTransportOutcomeKind::TransportUnavailable + } + _ => PushOutboxTransportOutcomeKind::TransportUnavailable, + }), attempted: false, message: Some(RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE.to_owned()), }], diff --git a/src/runtime/validation_receipt.rs b/src/runtime/validation_receipt.rs @@ -2,7 +2,7 @@ use radroots_sdk::{ RadrootsSdkError, TradeValidationReceiptEvent, TradeValidationReceiptInspectReceipt, TradeValidationReceiptInspectRequest, TradeValidationReceiptInvalidCandidate, TradeValidationReceiptListReceipt, TradeValidationReceiptListRequest, - TradeValidationReceiptRelayOutcomeKind, TradeValidationReceiptRelayOutcomeReceipt, + TradeValidationReceiptNostrRelayOutcomeKind, TradeValidationReceiptNostrRelayOutcomeReceipt, TradeValidationReceiptTags, TradeValidationReceiptVerifyRequest, TradeValidationReceiptWorkerEvidence, TradeValidationReceiptWorkerEvidenceSelection as SdkWorkerEvidenceSelection, @@ -18,8 +18,10 @@ use serde::Serialize; use serde_json::Value; use crate::runtime::config::RuntimeConfig; -use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession}; -use crate::view::runtime::{CommandDisposition, RelayFailureView}; +use crate::runtime::sdk::{ + CliSdkAdapterError, CliSdkSession, sdk_validation_transport_outcome_kind_label, +}; +use crate::view::runtime::{CommandDisposition, TransportTargetFailureView}; #[derive(Debug, Clone)] pub struct ValidationReceiptEventArgs { @@ -42,9 +44,9 @@ pub struct ValidationReceiptInspectionView { pub receipt: Option<RadrootsTradeValidationReceipt>, pub receipt_tags: Option<ValidationReceiptTagsView>, pub event: Option<ValidationReceiptEventView>, - pub target_relays: Vec<String>, - pub connected_relays: Vec<String>, - pub failed_relays: Vec<RelayFailureView>, + pub target_transport_endpoints: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, pub reason_code: Option<String>, pub reason: Option<String>, pub sdk_error: Option<Value>, @@ -73,9 +75,9 @@ pub struct ValidationReceiptListView { pub invalid_count: usize, pub receipts: Vec<ValidationReceiptSummaryView>, pub invalid_receipts: Vec<ValidationReceiptInvalidCandidateView>, - pub target_relays: Vec<String>, - pub connected_relays: Vec<String>, - pub failed_relays: Vec<RelayFailureView>, + pub target_transport_endpoints: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, pub reason_code: Option<String>, pub reason: Option<String>, pub sdk_error: Option<Value>, @@ -334,16 +336,19 @@ fn inspection_from_sdk_receipt( intent: ValidationReceiptCommandIntent, sdk_receipt: TradeValidationReceiptInspectReceipt, ) -> ValidationReceiptInspectionView { - let target_relays = sdk_receipt.relay_targets; - let connected_relays = connected_relays(&sdk_receipt.relay_evidence.relays); - let failed_relays = sdk_relay_failures(&sdk_receipt.relay_evidence.relays); - let reason_code = (!failed_relays.is_empty()).then_some("relay_fetch_partial".to_owned()); + let target_transport_endpoints = sdk_receipt.nostr_relay_urls; + let attempted_transport_endpoints = + attempted_transport_endpoints(&sdk_receipt.nostr_evidence.nostr_relay_outcomes); + let failed_transport_targets = + sdk_relay_failures(&sdk_receipt.nostr_evidence.nostr_relay_outcomes); + let reason_code = + (!failed_transport_targets.is_empty()).then_some("nostr_fetch_partial".to_owned()); if let Some(invalid) = sdk_receipt.invalid_receipt { return invalid_inspected_event_view( invalid, - target_relays, - connected_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + failed_transport_targets, ); } let Some(receipt) = sdk_receipt.receipt else { @@ -357,9 +362,9 @@ fn inspection_from_sdk_receipt( receipt: None, receipt_tags: None, event: None, - target_relays, - connected_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + failed_transport_targets, reason_code: Some("validation_receipt_not_found".to_owned()), reason: Some(format!( "validation receipt event `{receipt_event_id}` was not found on configured Nostr relays" @@ -372,9 +377,9 @@ fn inspection_from_sdk_receipt( receipt, success_state, intent, - target_relays, - connected_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + failed_transport_targets, reason_code, ) } @@ -383,9 +388,9 @@ fn inspected_event_view( sdk_receipt: TradeValidationReceiptEvent, success_state: &str, intent: ValidationReceiptCommandIntent, - target_relays: Vec<String>, - connected_relays: Vec<String>, - failed_relays: Vec<RelayFailureView>, + target_transport_endpoints: Vec<String>, + attempted_transport_endpoints: Vec<String>, + failed_transport_targets: Vec<TransportTargetFailureView>, relay_reason_code: Option<String>, ) -> ValidationReceiptInspectionView { let event_id = sdk_receipt.event.id.clone(); @@ -414,9 +419,9 @@ fn inspected_event_view( receipt: Some(sdk_receipt.receipt), receipt_tags: Some(tags_view(&sdk_receipt.tags)), event: Some(event_view(sdk_receipt.event)), - target_relays, - connected_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + failed_transport_targets, reason_code: proof_verification.reason_code.clone(), reason: proof_verification.reason.clone(), sdk_error: None, @@ -433,9 +438,9 @@ fn inspected_event_view( receipt: Some(sdk_receipt.receipt), receipt_tags: Some(tags_view(&sdk_receipt.tags)), event: Some(event_view(sdk_receipt.event)), - target_relays, - connected_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + failed_transport_targets, reason_code: relay_reason_code, reason: None, sdk_error: None, @@ -445,9 +450,9 @@ fn inspected_event_view( fn invalid_inspected_event_view( invalid: TradeValidationReceiptInvalidCandidate, - target_relays: Vec<String>, - connected_relays: Vec<String>, - failed_relays: Vec<RelayFailureView>, + target_transport_endpoints: Vec<String>, + attempted_transport_endpoints: Vec<String>, + failed_transport_targets: Vec<TransportTargetFailureView>, ) -> ValidationReceiptInspectionView { ValidationReceiptInspectionView { state: "invalid".to_owned(), @@ -459,9 +464,9 @@ fn invalid_inspected_event_view( receipt: None, receipt_tags: None, event: Some(event_view(invalid.event)), - target_relays, - connected_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + failed_transport_targets, reason_code: Some(invalid.reason_code), reason: Some(invalid.reason), sdk_error: None, @@ -472,9 +477,11 @@ fn invalid_inspected_event_view( fn list_from_sdk_receipt( sdk_receipt: TradeValidationReceiptListReceipt, ) -> ValidationReceiptListView { - let target_relays = sdk_receipt.relay_targets; - let connected_relays = connected_relays(&sdk_receipt.relay_evidence.relays); - let failed_relays = sdk_relay_failures(&sdk_receipt.relay_evidence.relays); + let target_transport_endpoints = sdk_receipt.nostr_relay_urls; + let attempted_transport_endpoints = + attempted_transport_endpoints(&sdk_receipt.nostr_evidence.nostr_relay_outcomes); + let failed_transport_targets = + sdk_relay_failures(&sdk_receipt.nostr_evidence.nostr_relay_outcomes); let mut invalid_receipts = sdk_receipt .invalid_receipts .into_iter() @@ -522,8 +529,8 @@ fn list_from_sdk_receipt( }; let reason_code = if invalid_count > 0 { Some("validation_receipt_candidates_invalid".to_owned()) - } else if !failed_relays.is_empty() { - Some("relay_fetch_partial".to_owned()) + } else if !failed_transport_targets.is_empty() { + Some("nostr_fetch_partial".to_owned()) } else { None }; @@ -545,9 +552,9 @@ fn list_from_sdk_receipt( invalid_count, receipts, invalid_receipts, - target_relays, - connected_relays, - failed_relays, + target_transport_endpoints, + attempted_transport_endpoints, + failed_transport_targets, reason_code, reason, sdk_error: None, @@ -570,9 +577,9 @@ fn inspection_sdk_error_view( receipt: None, receipt_tags: None, event: None, - target_relays: Vec::new(), - connected_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), reason_code: Some(mapped.reason_code), reason: Some(mapped.reason), sdk_error: mapped.sdk_error, @@ -590,9 +597,9 @@ fn list_sdk_error_view(order_id: &str, error: CliSdkAdapterError) -> ValidationR invalid_count: 0, receipts: Vec::new(), invalid_receipts: Vec::new(), - target_relays: Vec::new(), - connected_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), reason_code: Some(mapped.reason_code), reason: Some(mapped.reason), sdk_error: mapped.sdk_error, @@ -623,7 +630,7 @@ fn validation_receipt_sdk_error_parts(error: CliSdkAdapterError) -> ValidationRe fn sdk_error_parts(error: RadrootsSdkError) -> ValidationReceiptSdkErrorParts { let state = match error.code() { - "empty_target_relays" => "unconfigured", + "empty_transport_targets" => "unconfigured", _ => match error.class() { radroots_sdk::RadrootsSdkErrorClass::Configuration | radroots_sdk::RadrootsSdkErrorClass::Unsupported => "unconfigured", @@ -636,7 +643,7 @@ fn sdk_error_parts(error: RadrootsSdkError) -> ValidationReceiptSdkErrorParts { _ => "network_unavailable", }, }; - let actions = if error.code() == "empty_target_relays" { + let actions = if error.code() == "empty_transport_targets" { vec![ "radroots transport profile set --kind nostr --nostr-relay wss://relay.example.com" .to_owned(), @@ -668,9 +675,9 @@ fn invalid_inspection_view( receipt: None, receipt_tags: None, event: None, - target_relays: Vec::new(), - connected_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), reason_code: Some(reason_code.to_owned()), reason: Some(reason.into()), sdk_error: None, @@ -691,9 +698,9 @@ fn invalid_list_view( invalid_count: 0, receipts: Vec::new(), invalid_receipts: Vec::new(), - target_relays: Vec::new(), - connected_relays: Vec::new(), - failed_relays: Vec::new(), + target_transport_endpoints: Vec::new(), + attempted_transport_endpoints: Vec::new(), + failed_transport_targets: Vec::new(), reason_code: Some(reason_code.to_owned()), reason: Some(reason.into()), sdk_error: None, @@ -1222,22 +1229,30 @@ fn worker_evidence_view( } } -fn connected_relays(relays: &[TradeValidationReceiptRelayOutcomeReceipt]) -> Vec<String> { +fn attempted_transport_endpoints( + relays: &[TradeValidationReceiptNostrRelayOutcomeReceipt], +) -> Vec<String> { relays .iter() - .filter(|relay| relay.outcome_kind == TradeValidationReceiptRelayOutcomeKind::Eose) - .map(|relay| relay.relay_url.clone()) + .filter(|relay| relay.outcome_kind == TradeValidationReceiptNostrRelayOutcomeKind::Eose) + .map(|relay| relay.nostr_relay_url.clone()) .collect() } fn sdk_relay_failures( - relays: &[TradeValidationReceiptRelayOutcomeReceipt], -) -> Vec<RelayFailureView> { + relays: &[TradeValidationReceiptNostrRelayOutcomeReceipt], +) -> Vec<TransportTargetFailureView> { relays .iter() - .filter(|relay| relay.outcome_kind != TradeValidationReceiptRelayOutcomeKind::Eose) - .map(|relay| RelayFailureView { - relay: relay.relay_url.clone(), + .filter(|relay| relay.outcome_kind != TradeValidationReceiptNostrRelayOutcomeKind::Eose) + .map(|relay| TransportTargetFailureView { + transport_kind: "nostr".to_owned(), + endpoint_uri: relay.nostr_relay_url.clone(), + target_scope: None, + target_label: None, + transport_outcome_kind: relay + .transport_outcome_kind + .map(sdk_validation_transport_outcome_kind_label), reason: relay .message .clone() @@ -1246,11 +1261,11 @@ fn sdk_relay_failures( .collect() } -fn sdk_relay_outcome_kind(kind: TradeValidationReceiptRelayOutcomeKind) -> &'static str { +fn sdk_relay_outcome_kind(kind: TradeValidationReceiptNostrRelayOutcomeKind) -> &'static str { match kind { - TradeValidationReceiptRelayOutcomeKind::Eose => "eose", - TradeValidationReceiptRelayOutcomeKind::Closed => "closed", - TradeValidationReceiptRelayOutcomeKind::Notice => "notice", + TradeValidationReceiptNostrRelayOutcomeKind::Eose => "eose", + TradeValidationReceiptNostrRelayOutcomeKind::Closed => "closed", + TradeValidationReceiptNostrRelayOutcomeKind::Notice => "notice", _ => "unknown", } } diff --git a/src/view/runtime.rs b/src/view/runtime.rs @@ -995,13 +995,13 @@ pub struct FarmPublishComponentView { pub event_kind: u32, pub deduplicated: bool, #[serde(default)] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default)] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default)] - pub acknowledged_relays: Vec<String>, + pub accepted_transport_endpoints: Vec<String>, #[serde(default)] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(skip_serializing_if = "Option::is_none")] pub job_id: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] @@ -1064,12 +1064,6 @@ pub struct FarmPublishEventView { } #[derive(Debug, Clone, Serialize)] -pub struct RelayFailureView { - pub relay: String, - pub reason: String, -} - -#[derive(Debug, Clone, Serialize)] pub struct FarmConfigSummaryView { pub scope: String, pub path: String, @@ -1771,13 +1765,13 @@ pub struct OrderSubmitView { #[serde(default)] pub deduplicated: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub acknowledged_relays: Vec<String>, + pub accepted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(skip_serializing_if = "Option::is_none")] pub idempotency_key: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] @@ -1879,13 +1873,13 @@ pub struct OrderDecisionView { #[serde(default)] pub dry_run: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub acknowledged_relays: Vec<String>, + pub accepted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(default)] pub fetched_count: usize, #[serde(default)] @@ -1947,13 +1941,13 @@ pub struct OrderCancellationView { #[serde(default)] pub dry_run: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub acknowledged_relays: Vec<String>, + pub accepted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(default)] pub fetched_count: usize, #[serde(default)] @@ -2023,13 +2017,13 @@ pub struct OrderRevisionProposalView { #[serde(default)] pub dry_run: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub acknowledged_relays: Vec<String>, + pub accepted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(default)] pub fetched_count: usize, #[serde(default)] @@ -2100,13 +2094,13 @@ pub struct OrderRevisionDecisionView { #[serde(default)] pub dry_run: bool, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub acknowledged_relays: Vec<String>, + pub accepted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(default)] pub fetched_count: usize, #[serde(default)] @@ -2178,11 +2172,11 @@ pub struct OrderStatusView { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub reducer_issues: Vec<OrderIssueView>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(default)] pub fetched_count: usize, #[serde(default)] @@ -2364,11 +2358,11 @@ pub struct OrderEventListView { #[serde(skip_serializing_if = "Option::is_none")] pub seller_pubkey: Option<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(default)] pub fetched_count: usize, #[serde(default)] @@ -2919,13 +2913,13 @@ pub struct ListingMutationView { #[serde(default)] pub deduplicated: bool, #[serde(default)] - pub target_relays: Vec<String>, + pub target_transport_endpoints: Vec<String>, #[serde(default)] - pub connected_relays: Vec<String>, + pub attempted_transport_endpoints: Vec<String>, #[serde(default)] - pub acknowledged_relays: Vec<String>, + pub accepted_transport_endpoints: Vec<String>, #[serde(default)] - pub failed_relays: Vec<RelayFailureView>, + pub failed_transport_targets: Vec<TransportTargetFailureView>, #[serde(skip_serializing_if = "Option::is_none")] pub job_id: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] @@ -3551,6 +3545,10 @@ pub struct SyncTransportTargetView { pub transport_kind: String, pub endpoint_uri: String, pub endpoint_fingerprint: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub target_scope: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub target_label: Option<String>, } #[derive(Debug, Clone, Serialize)] @@ -3570,6 +3568,12 @@ pub struct SyncTransportStatusView { pub struct TransportTargetFailureView { pub transport_kind: String, pub endpoint_uri: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub target_scope: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub target_label: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub transport_outcome_kind: Option<String>, pub reason: String, } diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs @@ -1447,7 +1447,7 @@ fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() { assert_eq!(value["errors"][0]["detail"]["class"], "network"); assert_contains( &value["errors"][0]["message"], - "SDK relay publish did not reach accepted quorum", + "SDK transport publish did not reach accepted quorum", ); let detail = &value["errors"][0]["detail"]; assert_eq!(detail["source"], "SDK farm publish · configured signer"); @@ -1465,8 +1465,11 @@ fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() { assert_eq!(detail["profile"]["idempotency_key"], "farm_partial:profile"); assert_eq!(detail["farm"]["idempotency_key"], "farm_partial:farm"); assert_eq!(detail["actions"][0], "radroots sync push"); - assert_eq!(detail["farm"]["target_relays"][0], relay_url); - assert_relay_url(&detail["farm"]["failed_relays"][0]["relay"], relay_url); + assert_eq!(detail["farm"]["target_transport_endpoints"][0], relay_url); + assert_relay_url( + &detail["farm"]["failed_transport_targets"][0]["endpoint_uri"], + relay_url, + ); assert_no_removed_command_reference(&value, &["farm", "publish"]); assert_no_daemon_runtime_reference(&value, &["farm", "publish"]); @@ -2167,18 +2170,18 @@ fn local_seller_publish_commands_attempt_configured_relay() { assert_eq!(farm_value["errors"][0]["detail"]["class"], "network"); assert_contains( &farm_value["errors"][0]["message"], - "SDK relay publish did not reach accepted quorum", + "SDK transport publish did not reach accepted quorum", ); assert_eq!( farm_value["errors"][0]["detail"]["source"], "SDK farm publish · configured signer" ); assert_eq!( - farm_value["errors"][0]["detail"]["farm"]["target_relays"][0], + farm_value["errors"][0]["detail"]["farm"]["target_transport_endpoints"][0], relay ); assert_eq!( - farm_value["errors"][0]["detail"]["farm"]["failed_relays"][0]["relay"], + farm_value["errors"][0]["detail"]["farm"]["failed_transport_targets"][0]["endpoint_uri"], relay ); assert_no_removed_command_reference(&farm_value, &["farm", "publish"]); @@ -2204,23 +2207,23 @@ fn local_seller_publish_commands_attempt_configured_relay() { assert_eq!(publish_value["errors"][0]["detail"]["class"], "network"); assert_contains( &publish_value["errors"][0]["message"], - "SDK relay publish did not reach accepted quorum", + "SDK transport publish did not reach accepted quorum", ); assert_no_removed_command_reference(&publish_value, &["listing", "publish"]); assert_no_daemon_runtime_reference(&publish_value, &["listing", "publish"]); assert_eq!( - publish_value["errors"][0]["detail"]["target_relays"][0], + publish_value["errors"][0]["detail"]["target_transport_endpoints"][0], relay ); assert_eq!( - publish_value["errors"][0]["detail"]["connected_relays"] + publish_value["errors"][0]["detail"]["attempted_transport_endpoints"] .as_array() .expect("connected relays") .len(), 1 ); assert_eq!( - publish_value["errors"][0]["detail"]["failed_relays"] + publish_value["errors"][0]["detail"]["failed_transport_targets"] .as_array() .expect("failed relays") .len(), @@ -2243,23 +2246,23 @@ fn local_seller_publish_commands_attempt_configured_relay() { assert_eq!(archive_value["errors"][0]["detail"]["class"], "network"); assert_contains( &archive_value["errors"][0]["message"], - "SDK relay publish did not reach accepted quorum", + "SDK transport publish did not reach accepted quorum", ); assert_no_removed_command_reference(&archive_value, &["listing", "archive"]); assert_no_daemon_runtime_reference(&archive_value, &["listing", "archive"]); assert_eq!( - archive_value["errors"][0]["detail"]["target_relays"][0], + archive_value["errors"][0]["detail"]["target_transport_endpoints"][0], relay ); assert_eq!( - archive_value["errors"][0]["detail"]["connected_relays"] + archive_value["errors"][0]["detail"]["attempted_transport_endpoints"] .as_array() .expect("connected relays") .len(), 1 ); assert_eq!( - archive_value["errors"][0]["detail"]["failed_relays"] + archive_value["errors"][0]["detail"]["failed_transport_targets"] .as_array() .expect("failed relays") .len(), @@ -2324,7 +2327,7 @@ fn local_seller_publish_commands_attempt_configured_relay() { } #[test] -fn local_order_event_list_attempts_configured_shared_relay_transport() { +fn local_order_event_list_attempts_configured_shared_nostr_transport() { let sandbox = RadrootsCliSandbox::new(); sandbox.json_success(&["--format", "json", "account", "create"]); let relay = "ws://127.0.0.1:9"; @@ -2333,25 +2336,28 @@ fn local_order_event_list_attempts_configured_shared_relay_transport() { let (output, value) = sandbox.json_output(&["--format", "json", "trade", "event", "list"]); assert!(!output.status.success()); - assert_relay_transport_fetch_failure(&value, "trade.event.list", &["trade", "event", "list"]); + assert_transport_fetch_failure(&value, "trade.event.list", &["trade", "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"] + value["errors"][0]["detail"]["target_transport_endpoints"][0], + relay + ); + assert_eq!( + value["errors"][0]["detail"]["attempted_transport_endpoints"] .as_array() .expect("connected relays") .len(), 0 ); assert_eq!( - value["errors"][0]["detail"]["failed_relays"] + value["errors"][0]["detail"]["failed_transport_targets"] .as_array() .expect("failed relays") .len(), 1 ); assert_contains( - &value["errors"][0]["detail"]["failed_relays"][0]["relay"], + &value["errors"][0]["detail"]["failed_transport_targets"][0]["endpoint_uri"], "127.0.0.1:9", ); assert_eq!(value["errors"][0]["detail"]["fetched_count"], 0); @@ -2662,11 +2668,7 @@ fn configure_myc_mode(sandbox: &RadrootsCliSandbox, executable: &Path) { )); } -fn assert_relay_transport_fetch_failure( - value: &serde_json::Value, - operation_id: &str, - args: &[&str], -) { +fn assert_transport_fetch_failure(value: &serde_json::Value, operation_id: &str, args: &[&str]) { assert_eq!(value["operation_id"], operation_id); assert_eq!(value["result"], serde_json::Value::Null); assert_eq!(value["errors"][0]["code"], "network_unavailable"); @@ -2674,7 +2676,7 @@ fn assert_relay_transport_fetch_failure( assert_eq!(value["errors"][0]["detail"]["class"], "network"); assert_contains( &value["errors"][0]["message"], - "relay transport fetch failed", + "Nostr transport fetch failed", ); assert_no_removed_command_reference(value, args); assert_no_daemon_runtime_reference(value, args); diff --git a/tests/support/mod.rs b/tests/support/mod.rs @@ -341,9 +341,9 @@ pub fn seed_market_refresh_provenance(sandbox: &RadrootsCliSandbox, relay_urls: id INTEGER PRIMARY KEY AUTOINCREMENT, scope TEXT NOT NULL, relay_set_fingerprint TEXT NOT NULL, - target_relays_json TEXT NOT NULL, - connected_relays_json TEXT NOT NULL, - failed_relays_json TEXT NOT NULL, + target_transport_endpoints_json TEXT NOT NULL, + attempted_transport_endpoints_json TEXT NOT NULL, + failed_transport_targets_json TEXT NOT NULL, started_at INTEGER NOT NULL, completed_at INTEGER, state TEXT NOT NULL, @@ -368,9 +368,9 @@ pub fn seed_market_refresh_provenance(sandbox: &RadrootsCliSandbox, relay_urls: "INSERT INTO radroots_cli_sync_run ( scope, relay_set_fingerprint, - target_relays_json, - connected_relays_json, - failed_relays_json, + target_transport_endpoints_json, + attempted_transport_endpoints_json, + failed_transport_targets_json, started_at, completed_at, state, diff --git a/tests/target_cli.rs b/tests/target_cli.rs @@ -3156,7 +3156,7 @@ fn proxy_listing_publish_update_and_archive_dry_run_without_direct_publish_relay ); assert_eq!(value["result"]["dry_run"], true); assert_eq!( - value["result"]["target_relays"] + value["result"]["target_transport_endpoints"] .as_array() .expect("relays") .len(), @@ -3164,7 +3164,7 @@ fn proxy_listing_publish_update_and_archive_dry_run_without_direct_publish_relay ); assert_contains( &value["result"]["reason"], - "SDK enqueue and relay push skipped", + "SDK enqueue and transport push skipped", ); } } @@ -3395,7 +3395,10 @@ relay_urls = ["{}"] value["result"]["source"], "SDK listing publish · configured signer" ); - assert_eq!(value["result"]["target_relays"][0], relay_endpoint); + assert_eq!( + value["result"]["target_transport_endpoints"][0], + relay_endpoint + ); assert!(report.connection_count >= 1); assert!(report.req_count >= 1); assert_eq!(report.sign_request_count, 1); @@ -3583,7 +3586,7 @@ fn listing_update_publish_attempts_direct_publish_with_approval() { assert_eq!(value["errors"][0]["detail"]["class"], "network"); assert_contains( &value["errors"][0]["message"], - "SDK relay publish did not reach accepted quorum", + "SDK transport publish did not reach accepted quorum", ); assert!( !value["errors"][0]["message"] @@ -4117,7 +4120,7 @@ fn quiet_terminal_failure_keeps_reason_and_suppresses_optional_body() { let stderr = String::from_utf8(output.stderr).expect("utf8 stderr"); assert!(stderr.starts_with("✕ Command failed\n")); - assert!(stderr.contains("Reason relay-backed trade event watch is not implemented")); + assert!(stderr.contains("Reason Nostr-backed trade event watch is not implemented")); assert!(!stderr.contains("State not implemented")); assert!(!stderr.contains("Next\n")); assert!(serde_json::from_str::<Value>(&stderr).is_err()); @@ -4310,7 +4313,7 @@ fn terminal_failure_output_renders_structured_error_detail() { let stderr = String::from_utf8(output.stderr).expect("utf8 stderr"); assert!(stderr.starts_with("✕ Command failed\n")); - assert!(stderr.contains("Reason relay-backed trade event watch is not implemented")); + assert!(stderr.contains("Reason Nostr-backed trade event watch is not implemented")); assert!(stderr.contains("State not implemented")); assert!(stderr.contains("Next\n radroots trade status get ord_missing")); assert!(serde_json::from_str::<Value>(&stderr).is_err()); @@ -4651,7 +4654,7 @@ fn offline_listing_publish_enqueues_sdk_outbox_without_direct_publish_push() { publish["result"]["source"], "SDK listing publish · configured signer" ); - assert_eq!(publish["result"]["target_relays"][0], relay); + assert_eq!(publish["result"]["target_transport_endpoints"][0], relay); assert_eq!(publish["result"]["actions"][0], "radroots sync push"); assert_eq!( publish["result"]["event_id"] @@ -6489,7 +6492,7 @@ fn order_app_records_list_export_get_and_submit_supported_app_order() { assert_eq!(submit["result"]["state"], "dry_run"); assert_eq!(submit["result"]["source"], "SDK trade submit · local key"); assert_eq!(submit["result"]["event_kind"], 3422); - assert!(submit["result"]["target_relays"].is_null()); + assert!(submit["result"]["target_transport_endpoints"].is_null()); assert_eq!( submit["result"]["event_id"] .as_str() @@ -7104,8 +7107,11 @@ fn farm_publish_uses_sdk_outbox_without_legacy_signed_event_records() { assert_eq!(detail["profile"]["state"], "not_submitted"); assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null); assert_eq!(detail["farm"]["state"], "unavailable"); - assert_eq!(detail["farm"]["target_relays"][0], relay_url); - assert_eq!(detail["farm"]["failed_relays"][0]["relay"], relay_url); + assert_eq!(detail["farm"]["target_transport_endpoints"][0], relay_url); + assert_eq!( + detail["farm"]["failed_transport_targets"][0]["endpoint_uri"], + relay_url + ); assert_eq!( detail["farm"]["event_id"] .as_str() @@ -7173,11 +7179,11 @@ fn listing_publish_failure_uses_sdk_outbox_without_legacy_local_event_record() { ); assert_eq!(publish["errors"][0]["detail"]["state"], "unavailable"); assert_eq!( - publish["errors"][0]["detail"]["target_relays"][0], + publish["errors"][0]["detail"]["target_transport_endpoints"][0], relay_url ); assert_eq!( - publish["errors"][0]["detail"]["failed_relays"][0]["relay"], + publish["errors"][0]["detail"]["failed_transport_targets"][0]["endpoint_uri"], relay_url ); assert_eq!( @@ -8093,7 +8099,7 @@ fn buyer_target_flow_acceptance_uses_target_operations() { assert_eq!(submit["result"]["source"], "SDK trade submit · local key"); assert_eq!(submit["result"]["event_kind"], 3422); assert_eq!( - submit["result"]["target_relays"][0], + submit["result"]["target_transport_endpoints"][0], ORDERABLE_LISTING_RELAY ); assert_eq!( @@ -9290,7 +9296,7 @@ fn ready_order_submit_dry_run_validates_local_buyer_authority() { assert_eq!(dry_run["result"]["source"], "SDK trade submit · local key"); assert_eq!(dry_run["result"]["event_kind"], 3422); assert_eq!( - dry_run["result"]["target_relays"][0], + dry_run["result"]["target_transport_endpoints"][0], ORDERABLE_LISTING_RELAY ); assert_no_daemon_runtime_reference(&dry_run, &["trade", "submit", "--dry-run"]);