commit 01e989020cdb3632400b4a22e4702494146033f1
parent 627b6823c9f3f44f0e1bd256ba019c92a046e22d
Author: triesap <tyson@radroots.org>
Date: Fri, 10 Jul 2026 02:08:40 +0000
sdk: add public outcome label helpers
Add explicit SDK-owned label helpers for push outbox, trade resync,
and validation receipt outcome enums.
Cover every current public outcome variant in unit tests and source
boundary guards so new variants cannot silently degrade through serde,
panic conversion, or wildcard unknown paths.
Regenerated SDK TypeScript and package metadata gates were run and did
not leave generated diffs.
Diffstat:
5 files changed, 517 insertions(+), 2 deletions(-)
diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs
@@ -1685,6 +1685,17 @@ pub enum TradeResyncNostrRelayOutcomeKind {
}
#[cfg(feature = "runtime")]
+impl TradeResyncNostrRelayOutcomeKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Eose => "eose",
+ Self::Closed => "closed",
+ Self::Notice => "notice",
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
@@ -1709,6 +1720,31 @@ pub enum TradeResyncNostrRelayTransportOutcomeKind {
}
#[cfg(feature = "runtime")]
+impl TradeResyncNostrRelayTransportOutcomeKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Accepted => "accepted",
+ Self::DuplicateAccepted => "duplicate_accepted",
+ Self::Blocked => "blocked",
+ Self::RateLimited => "rate_limited",
+ Self::Invalid => "invalid",
+ Self::PowRequired => "pow_required",
+ Self::Restricted => "restricted",
+ Self::AuthRequired => "auth_required",
+ Self::Muted => "muted",
+ Self::Unsupported => "unsupported",
+ Self::PaymentRequired => "payment_required",
+ Self::Error => "error",
+ Self::Timeout => "timeout",
+ Self::ConnectionFailed => "connection_failed",
+ Self::RelayUrlRejected => "relay_url_rejected",
+ Self::SkippedAlreadyAccepted => "skipped_already_accepted",
+ Self::Unknown => "unknown",
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub struct TradeValidationReceiptListRequest {
@@ -1948,6 +1984,17 @@ pub enum TradeValidationReceiptNostrRelayOutcomeKind {
}
#[cfg(feature = "runtime")]
+impl TradeValidationReceiptNostrRelayOutcomeKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Eose => "eose",
+ Self::Closed => "closed",
+ Self::Notice => "notice",
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
@@ -1972,6 +2019,31 @@ pub enum TradeValidationReceiptNostrRelayTransportOutcomeKind {
}
#[cfg(feature = "runtime")]
+impl TradeValidationReceiptNostrRelayTransportOutcomeKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Accepted => "accepted",
+ Self::DuplicateAccepted => "duplicate_accepted",
+ Self::Blocked => "blocked",
+ Self::RateLimited => "rate_limited",
+ Self::Invalid => "invalid",
+ Self::PowRequired => "pow_required",
+ Self::Restricted => "restricted",
+ Self::AuthRequired => "auth_required",
+ Self::Muted => "muted",
+ Self::Unsupported => "unsupported",
+ Self::PaymentRequired => "payment_required",
+ Self::Error => "error",
+ Self::Timeout => "timeout",
+ Self::ConnectionFailed => "connection_failed",
+ Self::RelayUrlRejected => "relay_url_rejected",
+ Self::SkippedAlreadyAccepted => "skipped_already_accepted",
+ Self::Unknown => "unknown",
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
#[non_exhaustive]
pub struct TradeStatusRequest {
diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs
@@ -554,6 +554,33 @@ pub enum PushOutboxTargetOutcomeKind {
}
#[cfg(feature = "runtime")]
+impl PushOutboxTargetOutcomeKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Accepted => "accepted",
+ Self::DuplicateAccepted => "duplicate_accepted",
+ Self::Blocked => "blocked",
+ Self::RateLimited => "rate_limited",
+ Self::Invalid => "invalid",
+ Self::PowRequired => "pow_required",
+ Self::Restricted => "restricted",
+ Self::AuthRequired => "auth_required",
+ Self::Muted => "muted",
+ Self::Unsupported => "unsupported",
+ Self::PaymentRequired => "payment_required",
+ Self::Error => "error",
+ Self::Timeout => "timeout",
+ Self::ConnectionFailed => "connection_failed",
+ Self::TargetUriRejected => "target_uri_rejected",
+ Self::SkippedAlreadyAccepted => "skipped_already_accepted",
+ Self::DeferredUntilImplemented => "deferred_until_implemented",
+ Self::PreviewUnavailable => "preview_unavailable",
+ Self::Unknown => "unknown",
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
@@ -575,6 +602,28 @@ pub enum PushOutboxTransportOutcomeKind {
}
#[cfg(feature = "runtime")]
+impl PushOutboxTransportOutcomeKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Accepted => "accepted",
+ Self::DuplicateAccepted => "duplicate_accepted",
+ Self::Delivered => "delivered",
+ Self::Forwarded => "forwarded",
+ Self::StoredByGateway => "stored_by_gateway",
+ Self::Seen => "seen",
+ Self::DeferredUntilImplemented => "deferred_until_implemented",
+ Self::Rejected => "rejected",
+ Self::RouteUnavailable => "route_unavailable",
+ Self::PayloadTooLarge => "payload_too_large",
+ Self::PolicyDenied => "policy_denied",
+ Self::Timeout => "timeout",
+ Self::ConnectionFailed => "connection_failed",
+ Self::TransportUnavailable => "transport_unavailable",
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
impl From<RadrootsTransportOutcomeKind> for PushOutboxTransportOutcomeKind {
fn from(kind: RadrootsTransportOutcomeKind) -> Self {
match kind {
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -1556,6 +1556,144 @@ fn sdk_proxy_surfaces_reject_removed_daemon_publish_proxy_identifiers() {
}
#[test]
+fn sdk_public_outcome_label_contracts_are_explicit() {
+ let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
+ let sync_runtime_source = read_source(manifest_dir.join("src/sync_runtime.rs").as_path());
+ let orders_runtime_source = read_source(manifest_dir.join("src/orders_runtime.rs").as_path());
+
+ for (source, enum_name, required_labels) in [
+ (
+ sync_runtime_source.as_str(),
+ "PushOutboxTargetOutcomeKind",
+ &[
+ r#"Self::Accepted => "accepted""#,
+ r#"Self::DuplicateAccepted => "duplicate_accepted""#,
+ r#"Self::Blocked => "blocked""#,
+ r#"Self::RateLimited => "rate_limited""#,
+ r#"Self::Invalid => "invalid""#,
+ r#"Self::PowRequired => "pow_required""#,
+ r#"Self::Restricted => "restricted""#,
+ r#"Self::AuthRequired => "auth_required""#,
+ r#"Self::Muted => "muted""#,
+ r#"Self::Unsupported => "unsupported""#,
+ r#"Self::PaymentRequired => "payment_required""#,
+ r#"Self::Error => "error""#,
+ r#"Self::Timeout => "timeout""#,
+ r#"Self::ConnectionFailed => "connection_failed""#,
+ r#"Self::TargetUriRejected => "target_uri_rejected""#,
+ r#"Self::SkippedAlreadyAccepted => "skipped_already_accepted""#,
+ r#"Self::DeferredUntilImplemented => "deferred_until_implemented""#,
+ r#"Self::PreviewUnavailable => "preview_unavailable""#,
+ r#"Self::Unknown => "unknown""#,
+ ][..],
+ ),
+ (
+ sync_runtime_source.as_str(),
+ "PushOutboxTransportOutcomeKind",
+ &[
+ r#"Self::Accepted => "accepted""#,
+ r#"Self::DuplicateAccepted => "duplicate_accepted""#,
+ r#"Self::Delivered => "delivered""#,
+ r#"Self::Forwarded => "forwarded""#,
+ r#"Self::StoredByGateway => "stored_by_gateway""#,
+ r#"Self::Seen => "seen""#,
+ r#"Self::DeferredUntilImplemented => "deferred_until_implemented""#,
+ r#"Self::Rejected => "rejected""#,
+ r#"Self::RouteUnavailable => "route_unavailable""#,
+ r#"Self::PayloadTooLarge => "payload_too_large""#,
+ r#"Self::PolicyDenied => "policy_denied""#,
+ r#"Self::Timeout => "timeout""#,
+ r#"Self::ConnectionFailed => "connection_failed""#,
+ r#"Self::TransportUnavailable => "transport_unavailable""#,
+ ][..],
+ ),
+ (
+ orders_runtime_source.as_str(),
+ "TradeResyncNostrRelayOutcomeKind",
+ &[
+ r#"Self::Eose => "eose""#,
+ r#"Self::Closed => "closed""#,
+ r#"Self::Notice => "notice""#,
+ ][..],
+ ),
+ (
+ orders_runtime_source.as_str(),
+ "TradeResyncNostrRelayTransportOutcomeKind",
+ &[
+ r#"Self::Accepted => "accepted""#,
+ r#"Self::DuplicateAccepted => "duplicate_accepted""#,
+ r#"Self::Blocked => "blocked""#,
+ r#"Self::RateLimited => "rate_limited""#,
+ r#"Self::Invalid => "invalid""#,
+ r#"Self::PowRequired => "pow_required""#,
+ r#"Self::Restricted => "restricted""#,
+ r#"Self::AuthRequired => "auth_required""#,
+ r#"Self::Muted => "muted""#,
+ r#"Self::Unsupported => "unsupported""#,
+ r#"Self::PaymentRequired => "payment_required""#,
+ r#"Self::Error => "error""#,
+ r#"Self::Timeout => "timeout""#,
+ r#"Self::ConnectionFailed => "connection_failed""#,
+ r#"Self::RelayUrlRejected => "relay_url_rejected""#,
+ r#"Self::SkippedAlreadyAccepted => "skipped_already_accepted""#,
+ r#"Self::Unknown => "unknown""#,
+ ][..],
+ ),
+ (
+ orders_runtime_source.as_str(),
+ "TradeValidationReceiptNostrRelayOutcomeKind",
+ &[
+ r#"Self::Eose => "eose""#,
+ r#"Self::Closed => "closed""#,
+ r#"Self::Notice => "notice""#,
+ ][..],
+ ),
+ (
+ orders_runtime_source.as_str(),
+ "TradeValidationReceiptNostrRelayTransportOutcomeKind",
+ &[
+ r#"Self::Accepted => "accepted""#,
+ r#"Self::DuplicateAccepted => "duplicate_accepted""#,
+ r#"Self::Blocked => "blocked""#,
+ r#"Self::RateLimited => "rate_limited""#,
+ r#"Self::Invalid => "invalid""#,
+ r#"Self::PowRequired => "pow_required""#,
+ r#"Self::Restricted => "restricted""#,
+ r#"Self::AuthRequired => "auth_required""#,
+ r#"Self::Muted => "muted""#,
+ r#"Self::Unsupported => "unsupported""#,
+ r#"Self::PaymentRequired => "payment_required""#,
+ r#"Self::Error => "error""#,
+ r#"Self::Timeout => "timeout""#,
+ r#"Self::ConnectionFailed => "connection_failed""#,
+ r#"Self::RelayUrlRejected => "relay_url_rejected""#,
+ r#"Self::SkippedAlreadyAccepted => "skipped_already_accepted""#,
+ r#"Self::Unknown => "unknown""#,
+ ][..],
+ ),
+ ] {
+ let marker = format!("impl {enum_name} {{");
+ let block = impl_block(source, marker.as_str());
+ assert!(
+ block.contains("pub fn as_str(self) -> &'static str"),
+ "{enum_name} must expose an SDK-owned public label helper"
+ );
+ for required in required_labels {
+ assert!(
+ block.contains(required),
+ "{enum_name} label helper must explicitly cover `{required}`"
+ );
+ }
+ for forbidden in ["serde_json::to_value", "panic!(", "_ =>"] {
+ assert!(
+ !block.contains(forbidden),
+ "{enum_name} label helper must not contain forbidden conversion token `{forbidden}`"
+ );
+ }
+ }
+}
+
+#[test]
fn sdk_transport_policy_sources_reject_configured_profile_and_proxy_relay_bridge() {
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
for relative_path in [
diff --git a/crates/sdk/tests/unit/orders_runtime_tests.rs b/crates/sdk/tests/unit/orders_runtime_tests.rs
@@ -4171,6 +4171,171 @@ fn order_runtime_request_builders_and_serializers_cover_source_attached_paths()
assert_struct_serialize_error_paths(&issue, 3);
}
+#[test]
+fn relay_outcome_labels_cover_resync_and_validation_receipt_variants() {
+ for (kind, label) in [
+ (TradeResyncNostrRelayOutcomeKind::Eose, "eose"),
+ (TradeResyncNostrRelayOutcomeKind::Closed, "closed"),
+ (TradeResyncNostrRelayOutcomeKind::Notice, "notice"),
+ ] {
+ assert_eq!(kind.as_str(), label);
+ }
+
+ for (kind, label) in [
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::Accepted,
+ "accepted",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::DuplicateAccepted,
+ "duplicate_accepted",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::Blocked,
+ "blocked",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::RateLimited,
+ "rate_limited",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::Invalid,
+ "invalid",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::PowRequired,
+ "pow_required",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::Restricted,
+ "restricted",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::AuthRequired,
+ "auth_required",
+ ),
+ (TradeResyncNostrRelayTransportOutcomeKind::Muted, "muted"),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::Unsupported,
+ "unsupported",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::PaymentRequired,
+ "payment_required",
+ ),
+ (TradeResyncNostrRelayTransportOutcomeKind::Error, "error"),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::Timeout,
+ "timeout",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::ConnectionFailed,
+ "connection_failed",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::RelayUrlRejected,
+ "relay_url_rejected",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::SkippedAlreadyAccepted,
+ "skipped_already_accepted",
+ ),
+ (
+ TradeResyncNostrRelayTransportOutcomeKind::Unknown,
+ "unknown",
+ ),
+ ] {
+ assert_eq!(kind.as_str(), label);
+ }
+
+ for (kind, label) in [
+ (TradeValidationReceiptNostrRelayOutcomeKind::Eose, "eose"),
+ (
+ TradeValidationReceiptNostrRelayOutcomeKind::Closed,
+ "closed",
+ ),
+ (
+ TradeValidationReceiptNostrRelayOutcomeKind::Notice,
+ "notice",
+ ),
+ ] {
+ assert_eq!(kind.as_str(), label);
+ }
+
+ for (kind, label) in [
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Accepted,
+ "accepted",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::DuplicateAccepted,
+ "duplicate_accepted",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Blocked,
+ "blocked",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::RateLimited,
+ "rate_limited",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Invalid,
+ "invalid",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::PowRequired,
+ "pow_required",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Restricted,
+ "restricted",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::AuthRequired,
+ "auth_required",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Muted,
+ "muted",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Unsupported,
+ "unsupported",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::PaymentRequired,
+ "payment_required",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Error,
+ "error",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Timeout,
+ "timeout",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::ConnectionFailed,
+ "connection_failed",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::RelayUrlRejected,
+ "relay_url_rejected",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::SkippedAlreadyAccepted,
+ "skipped_already_accepted",
+ ),
+ (
+ TradeValidationReceiptNostrRelayTransportOutcomeKind::Unknown,
+ "unknown",
+ ),
+ ] {
+ assert_eq!(kind.as_str(), label);
+ }
+}
+
#[tokio::test]
async fn closed_event_store_errors_are_mapped_for_ingest_and_prepared_lookup() {
let sdk = crate::RadrootsClient::builder().build().await.expect("sdk");
diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs
@@ -7,8 +7,8 @@ use super::{
};
use super::{
PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxTargetOutcomeKind,
- SdkRelayAuthPolicy, SyncEventStoreStatus, SyncOutboxStatus, push_event_final_state,
- push_event_receipt, push_outbox_claim_token,
+ PushOutboxTransportOutcomeKind, SdkRelayAuthPolicy, SyncEventStoreStatus, SyncOutboxStatus,
+ push_event_final_state, push_event_receipt, push_outbox_claim_token,
};
use crate::RadrootsSdkError;
#[cfg(feature = "radrootsd-proxy")]
@@ -653,6 +653,97 @@ fn relay_outcome_kind_mapping_covers_all_transport_outcomes() {
);
}
+#[test]
+fn push_outbox_outcome_kind_labels_cover_all_public_variants() {
+ for (kind, label) in [
+ (PushOutboxTargetOutcomeKind::Accepted, "accepted"),
+ (
+ PushOutboxTargetOutcomeKind::DuplicateAccepted,
+ "duplicate_accepted",
+ ),
+ (PushOutboxTargetOutcomeKind::Blocked, "blocked"),
+ (PushOutboxTargetOutcomeKind::RateLimited, "rate_limited"),
+ (PushOutboxTargetOutcomeKind::Invalid, "invalid"),
+ (PushOutboxTargetOutcomeKind::PowRequired, "pow_required"),
+ (PushOutboxTargetOutcomeKind::Restricted, "restricted"),
+ (PushOutboxTargetOutcomeKind::AuthRequired, "auth_required"),
+ (PushOutboxTargetOutcomeKind::Muted, "muted"),
+ (PushOutboxTargetOutcomeKind::Unsupported, "unsupported"),
+ (
+ PushOutboxTargetOutcomeKind::PaymentRequired,
+ "payment_required",
+ ),
+ (PushOutboxTargetOutcomeKind::Error, "error"),
+ (PushOutboxTargetOutcomeKind::Timeout, "timeout"),
+ (
+ PushOutboxTargetOutcomeKind::ConnectionFailed,
+ "connection_failed",
+ ),
+ (
+ PushOutboxTargetOutcomeKind::TargetUriRejected,
+ "target_uri_rejected",
+ ),
+ (
+ PushOutboxTargetOutcomeKind::SkippedAlreadyAccepted,
+ "skipped_already_accepted",
+ ),
+ (
+ PushOutboxTargetOutcomeKind::DeferredUntilImplemented,
+ "deferred_until_implemented",
+ ),
+ (
+ PushOutboxTargetOutcomeKind::PreviewUnavailable,
+ "preview_unavailable",
+ ),
+ (PushOutboxTargetOutcomeKind::Unknown, "unknown"),
+ ] {
+ assert_eq!(kind.as_str(), label);
+ }
+
+ for (kind, label) in [
+ (PushOutboxTransportOutcomeKind::Accepted, "accepted"),
+ (
+ PushOutboxTransportOutcomeKind::DuplicateAccepted,
+ "duplicate_accepted",
+ ),
+ (PushOutboxTransportOutcomeKind::Delivered, "delivered"),
+ (PushOutboxTransportOutcomeKind::Forwarded, "forwarded"),
+ (
+ PushOutboxTransportOutcomeKind::StoredByGateway,
+ "stored_by_gateway",
+ ),
+ (PushOutboxTransportOutcomeKind::Seen, "seen"),
+ (
+ PushOutboxTransportOutcomeKind::DeferredUntilImplemented,
+ "deferred_until_implemented",
+ ),
+ (PushOutboxTransportOutcomeKind::Rejected, "rejected"),
+ (
+ PushOutboxTransportOutcomeKind::RouteUnavailable,
+ "route_unavailable",
+ ),
+ (
+ PushOutboxTransportOutcomeKind::PayloadTooLarge,
+ "payload_too_large",
+ ),
+ (
+ PushOutboxTransportOutcomeKind::PolicyDenied,
+ "policy_denied",
+ ),
+ (PushOutboxTransportOutcomeKind::Timeout, "timeout"),
+ (
+ PushOutboxTransportOutcomeKind::ConnectionFailed,
+ "connection_failed",
+ ),
+ (
+ PushOutboxTransportOutcomeKind::TransportUnavailable,
+ "transport_unavailable",
+ ),
+ ] {
+ assert_eq!(kind.as_str(), label);
+ }
+}
+
#[tokio::test]
async fn sync_status_maps_closed_store_errors() {
let event_store_closed = crate::RadrootsClient::builder().build().await.expect("sdk");