lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 028b84f7f095e2758c2ee2f7a857c03248a6d833
parent e7aa04171d9f8972c8cce69129df0efec2858ce8
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:
Mcrates/sdk/src/orders_runtime.rs | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/sync_runtime.rs | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/source_boundary.rs | 138+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/unit/orders_runtime_tests.rs | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
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");