lib

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

commit a528f0fee4606c62ebaa15698dcbc6b5c9ca41a3
parent 683b7e5d5359fa39348f79496326ac14b2d68170
Author: triesap <tyson@radroots.org>
Date:   Mon,  6 Jul 2026 22:27:56 +0000

sdk: add transport profile API

- replace relay-target builder state with transport profile configuration

- expose SDK transport target and profile models

- construct typed outbox delivery plans for workflow enqueue

- update runtime tests and README for target-state transport APIs

Diffstat:
Mcrates/sdk/Cargo.toml | 21+++++++++++++--------
Mcrates/sdk/README | 29+++++++++++++++--------------
Mcrates/sdk/examples/runtime_local.rs | 8++++----
Mcrates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs | 12++++++------
Mcrates/sdk/src/adapters/radrootsd.rs | 23++++++++++++-----------
Mcrates/sdk/src/dvm_runtime.rs | 18++++++++++--------
Mcrates/sdk/src/error.rs | 40+++++++++++++++++++++++++++-------------
Mcrates/sdk/src/farms_runtime.rs | 18++++++++++--------
Mcrates/sdk/src/lib.rs | 23+++++++++++++----------
Mcrates/sdk/src/listings_runtime.rs | 20+++++++++++---------
Mcrates/sdk/src/orders_runtime.rs | 213++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Dcrates/sdk/src/relay_targets.rs | 280-------------------------------------------------------------------------------
Mcrates/sdk/src/runtime.rs | 72++++++++++++------------------------------------------------------------
Mcrates/sdk/src/sync_runtime.rs | 258+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Acrates/sdk/src/transport.rs | 494+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/workflow_runtime.rs | 144+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mcrates/sdk/tests/dvm_runtime.rs | 15+++++++--------
Mcrates/sdk/tests/farms_runtime.rs | 69+++++++++++++++++++++++++++++++++++++++++++--------------------------
Mcrates/sdk/tests/listings_runtime.rs | 85+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/sdk/tests/market_runtime.rs | 13++++++++-----
Mcrates/sdk/tests/orders_runtime.rs | 382++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mcrates/sdk/tests/runtime_foundation.rs | 138++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/sdk/tests/source_boundary.rs | 4++--
Mcrates/sdk/tests/sync_runtime.rs | 102+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/sdk/tests/trade_product_publish_runtime.rs | 24++++++++++--------------
Mcrates/sdk/tests/unit/adapters_radrootsd_tests.rs | 26+++++++++++++++++++-------
Mcrates/sdk/tests/unit/dvm_runtime_tests.rs | 16++++++++--------
Mcrates/sdk/tests/unit/error_tests.rs | 26+++++++++++++-------------
Mcrates/sdk/tests/unit/farms_runtime_tests.rs | 24++++++++++++------------
Mcrates/sdk/tests/unit/listings_runtime_tests.rs | 20++++++++++----------
Mcrates/sdk/tests/unit/orders_runtime_tests.rs | 207++++++++++++++++++++++++++++++++++++++++---------------------------------------
Dcrates/sdk/tests/unit/relay_targets_tests.rs | 201-------------------------------------------------------------------------------
Mcrates/sdk/tests/unit/runtime_tests.rs | 42++++++++++++++++++++++++++++--------------
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 51+++++++++++++++++++++++++++++++++++----------------
Acrates/sdk/tests/unit/transport_tests.rs | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/unit/workflow_runtime_tests.rs | 61++++++++++++++++++++++++++++++++++++++++++++-----------------
36 files changed, 1997 insertions(+), 1395 deletions(-)

diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -43,11 +43,11 @@ radrootsd-proxy = [ "serde_json", "dep:futures", "dep:radroots_publish_proxy_protocol", - "dep:radroots_relay_transport", + "dep:radroots_transport_nostr", "dep:reqwest", "radroots_publish_proxy_protocol/serde", "radroots_publish_proxy_protocol/std", - "radroots_relay_transport/std", + "radroots_transport_nostr/std", ] signer-adapters = [ "identity-models", @@ -68,7 +68,9 @@ runtime = [ "dep:radroots_event_store", "dep:radroots_geocoder", "dep:radroots_outbox", - "dep:radroots_relay_transport", + "dep:radroots_transport", + "dep:radroots_transport_nostr", + "dep:radroots_transport_reticulum", "dep:radroots_runtime_paths", "dep:sha2", "dep:sqlx", @@ -78,9 +80,10 @@ runtime = [ "radroots_event_store/runtime-tokio", "radroots_outbox/sqlite", "radroots_outbox/runtime-tokio", - "radroots_relay_transport/std", - "radroots_relay_transport/storage", - "radroots_relay_transport/runtime-tokio", + "radroots_transport/serde", + "radroots_transport_nostr/std", + "radroots_transport_nostr/storage", + "radroots_transport_nostr/runtime-tokio", "radroots_trade/event_store", ] local-signer = ["runtime", "radroots_authority/local_signer"] @@ -88,7 +91,7 @@ relay-runtime = [ "runtime", "dep:radroots_nostr", "radroots_nostr/client", - "radroots_relay_transport/client", + "radroots_transport_nostr/client", ] local-runtime = [ "std", @@ -121,7 +124,9 @@ radroots_events_codec = { workspace = true, default-features = false } radroots_geocoder = { workspace = true, optional = true } radroots_outbox = { workspace = true, optional = true, default-features = false } radroots_publish_proxy_protocol = { workspace = true, optional = true, default-features = false } -radroots_relay_transport = { workspace = true, optional = true, default-features = false } +radroots_transport = { workspace = true, optional = true, default-features = false } +radroots_transport_nostr = { workspace = true, optional = true, default-features = false } +radroots_transport_reticulum = { workspace = true, optional = true, default-features = false } radroots_runtime_paths = { workspace = true, optional = true, default-features = false } radroots_trade = { workspace = true, default-features = false } radroots_identity = { workspace = true, optional = true, default-features = false } diff --git a/crates/sdk/README b/crates/sdk/README @@ -5,10 +5,11 @@ Curated Radroots Rust SDK for local-first Radroots product workflows. The SDK v1 product runtime is centered on `RadrootsClient::builder()`, `sdk.farms()`, `sdk.listings()`, `sdk.trades()`, `sdk.market()`, and `sdk.sync()`. -`RadrootsClient::builder()` defaults to memory storage, the system clock, no relay URLs, and no -production network publishing. Directory storage is opt-in and creates `event_store.sqlite` and -`outbox.sqlite` in the selected directory. Configured relay URLs are opt-in enqueue defaults used -when a publish request chooses `SdkRelayTargetPolicy::UseConfiguredRelays`. +`RadrootsClient::builder()` defaults to memory storage, the system clock, the `LocalOnly` transport +profile, and no production network publishing. Directory storage is opt-in and creates +`event_store.sqlite` and `outbox.sqlite` in the selected directory. Configured Nostr relay URLs live +inside `TransportProfile::Nostr` or the Nostr side of `TransportProfile::Hybrid`, and product +enqueue requests choose them through `TargetPolicy::UseConfiguredProfile`. When `signer-adapters` is enabled, `RadrootsClient::builder()` accepts a configured `RadrootsSdkSignerProvider`. The production signing modes are `local_key` and `myc_nip46`. Product @@ -21,8 +22,8 @@ farm, listing, or trade write call. `signer_status()`, `configured_signer()`, an that prepared plan, ingests it into the local event store, queues signed outbox work, and returns a typed `ListingEnqueueReceipt`. `sdk.listings().enqueue_publish(...)` is the convenience path that prepares once and delegates to `enqueue_prepared_publish(...)`. Farm and trade write methods follow -the same configured signer pattern. The enqueue path uses typed relay target and idempotency inputs; -omitted idempotency keys are derived deterministically. +the same configured signer pattern. The enqueue path uses typed transport target and idempotency +inputs; omitted idempotency keys are derived deterministically. Explicit signer injection remains available under `*_with_explicit_signer` method names for controlled adapter-level tests and advanced integration checks. Those methods are not the primary @@ -34,8 +35,8 @@ clock at enqueue time. Listing enqueue receipts report mutation state with the p `StoredAndQueued` and `AlreadyQueued`. `sdk.sync().push_outbox(...)` is the product sync entrypoint. It publishes queued signed outbox work -when `relay-runtime` is enabled. Push time uses the relay targets already stored on each queued -outbox event, so already queued work does not require configured builder relays. Direct relay +when `relay-runtime` is enabled. Push time uses the delivery targets already stored on each queued +outbox event, so already queued work does not require a configured builder profile. Nostr transport publishing and the `radrootsd-proxy` Publish Proxy transport consume signed outbox events; neither transport owns signing. `push_outbox_with_adapter(...)` remains available for tests and controlled adapter-level substrate checks. `radrootsd-proxy` adds daemon-resolved publishing through @@ -59,8 +60,8 @@ DTOs. DVM proof requests and validation receipt ingestion use `sdk.dvm()`. The `local-runtime` feature is the curated feature bundle for local product runtime consumers. It enables `std`, `serde`, `serde_json`, `runtime`, `signer-adapters`, `relay-runtime`, and `relay-client`. `signer-adapters` contains the SDK `local_key` and `myc_nip46` signing surface. -`relay-client` supplies the relay WebSocket publish adapter used when signed outbox work targets -configured relay URLs. `local-runtime-radrootsd-proxy` uses daemon-resolved publishing through +`relay-client` supplies the Nostr relay WebSocket publish adapter used when signed outbox work +targets Nostr relay URLs. `local-runtime-radrootsd-proxy` uses daemon-resolved publishing through `publish.event` and the same configured signer provider API. `radroots_sdk_myc_nip46_product_permissions()` and @@ -69,7 +70,7 @@ configured relay URLs. `local-runtime-radrootsd-proxy` uses daemon-resolved publ listing, and trade event kinds the SDK can write. Relay URL policy is explicit. Public relay URLs must use `wss://`. Local development `ws://` relay -URLs are accepted only under `SdkRelayUrlPolicy::Localhost` and only for `localhost`, `127.0.0.1`, +URLs are accepted only under `NostrRelayUrlPolicy::Localhost` and only for `localhost`, `127.0.0.1`, or `[::1]`. Non-local insecure `ws://` targets, including private LAN addresses, are rejected. Low-level event-contract, wire-codec, reducer, and DVM contract ownership lives in the shared @@ -92,7 +93,7 @@ cargo check -p radroots_sdk --example sdk_v1_myc_nip46_signer_setup --features r `sdk_v1_knowledge_prepare` shows `radroots_sdk::knowledge`, typed event builders, frozen draft preparation, signed-event verification, decoded event matching, and manifest hash access without local runtime storage. -`sdk_v1_local_enqueue_and_mock_sync` shows localhost relay target selection, configured local-key +`sdk_v1_local_enqueue_and_mock_sync` shows localhost Nostr target selection, configured local-key signing, prepared listing enqueue, `push_outbox_with_adapter(...)` with a mock relay adapter, and `TradeStatusRequest`. `sdk_v1_myc_nip46_signer_setup` shows Myc NIP-46 signer provider setup and product permission derivation at the SDK boundary. The examples stay on product APIs and do not use @@ -126,8 +127,8 @@ Runtime request DTOs are constructor-led and marked non-exhaustive where they ca `parse`, `default`, and `with_*` methods rather than struct literals. Runtime enums that may gain variants are non-exhaustive. This includes storage, clock, -relay-target, mutation-state, trade-status, sync-status, relay-auth, and push-outbox state or -outcome enums. +target-policy, transport-profile, mutation-state, trade-status, sync-status, relay-auth, and +push-outbox state or outcome enums. Runtime receipts and status records expose stable serialized public fields for CLI and local-runtime reporting. Future additive reporting must add fields without changing existing serialized field diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs @@ -9,9 +9,9 @@ use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}; use radroots_nostr::prelude::RadrootsNostrKeys; use radroots_sdk::{ - ListingPreparePublishRequest, PushOutboxRequest, RadrootsClient, RadrootsSdkError, - RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SdkIdempotencyKey, - SdkRelayTargetPolicy, SdkRelayUrlPolicy, TradeStatusRequest, + ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient, + RadrootsSdkError, RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, + SdkIdempotencyKey, TargetPolicy, TradeStatusRequest, }; const RELAY: &str = "wss://relay.example.com"; @@ -29,7 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { let actor = RadrootsActorContext::test(seller.as_str(), [RadrootsActorRole::Seller])?; let listing = sample_listing(seller.as_str()); let prepare_request = ListingPreparePublishRequest::new(actor.clone(), listing); - let target_relays = SdkRelayTargetPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public)?; + let target_relays = TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public)?; let idempotency_key = SdkIdempotencyKey::new("example-1")?; let prepared = sdk.listings().prepare_publish(prepare_request)?; diff --git a/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs b/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs @@ -8,12 +8,12 @@ use radroots_events::farm::RadrootsFarmRef; use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}; use radroots_nostr::prelude::RadrootsNostrKeys; -use radroots_relay_transport::RadrootsMockRelayPublishAdapter; use radroots_sdk::{ - ListingPreparePublishRequest, PushOutboxRequest, RadrootsClient, RadrootsSdkLocalKeySigner, - RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkRelayTargetPolicy, - SdkRelayTargetSet, SdkRelayUrlPolicy, TradeStatusRequest, + ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient, + RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SdkIdempotencyKey, + TargetPolicy, TargetSet, TradeStatusRequest, }; +use radroots_transport_nostr::RadrootsMockRelayPublishAdapter; const LOCAL_RELAY: &str = "ws://localhost:7777"; @@ -28,8 +28,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { .build() .await?; let actor = RadrootsActorContext::test(seller.as_str(), [RadrootsActorRole::Seller])?; - let targets = SdkRelayTargetSet::new([LOCAL_RELAY], SdkRelayUrlPolicy::Localhost)?; - let target_policy = SdkRelayTargetPolicy::explicit(targets); + let targets = TargetSet::new([LOCAL_RELAY], NostrRelayUrlPolicy::Localhost)?; + let target_policy = TargetPolicy::explicit(targets); let prepared = sdk .listings() diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -6,7 +6,8 @@ use radroots_publish_proxy_protocol::{ METHOD_EVENT, PublishDeliveryPolicy, PublishEventRequest, PublishEventResponse, PublishProxyProtocolError, PublishRelayOutcomeKind, PublishRelayPolicy, SignedNostrEventWire, }; -use radroots_relay_transport::{ +use radroots_transport::RadrootsTransportSatisfactionPolicy; +use radroots_transport_nostr::{ RadrootsRelayOutcome, RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter, RadrootsRelayPublishReceipt, RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTransportError, @@ -120,8 +121,8 @@ impl RadrootsRelayPublishAdapter for RadrootsdProxyPublishAdapter { let request = RadrootsdProxyPublishRequest { delivery_policy: delivery_policy_from_relay_request( request.targets.len(), - request.accepted_quorum, - ), + &request.satisfaction_policy, + )?, signed_event: request.signed_event, relays: request.targets.relay_strings(), idempotency_key: None, @@ -353,17 +354,17 @@ fn signed_event_wire(event: &RadrootsSignedNostrEvent) -> SignedNostrEventWire { fn delivery_policy_from_relay_request( target_count: usize, - accepted_quorum: usize, -) -> PublishDeliveryPolicy { - if accepted_quorum >= target_count { + satisfaction_policy: &RadrootsTransportSatisfactionPolicy, +) -> Result<PublishDeliveryPolicy, RadrootsRelayTransportError> { + let required = satisfaction_policy.required_target_count(target_count)?; + let delivery_policy = if required >= target_count { PublishDeliveryPolicy::All - } else if accepted_quorum <= 1 { + } else if required <= 1 { PublishDeliveryPolicy::Any } else { - PublishDeliveryPolicy::Quorum { - quorum: accepted_quorum, - } - } + PublishDeliveryPolicy::Quorum { quorum: required } + }; + Ok(delivery_policy) } fn proxy_receipt_from_response( diff --git a/crates/sdk/src/dvm_runtime.rs b/crates/sdk/src/dvm_runtime.rs @@ -2,9 +2,9 @@ use crate::workflow_runtime::enqueue_configured_signed_workflow; #[cfg(feature = "runtime")] use crate::{ - DvmClient, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkMutationState, - SdkRelayTargetPolicy, SdkRelayUrlPolicy, SyncProjectionRefreshReceipt, - SyncProjectionRefreshRequest, + DvmClient, NostrRelayUrlPolicy, RadrootsSdkError, RadrootsSdkTimestamp, SatisfactionPolicy, + SdkIdempotencyKey, SdkMutationState, SyncProjectionRefreshReceipt, + SyncProjectionRefreshRequest, TargetPolicy, runtime::sdk_now_ms, sync_runtime::refresh_product_projections_for_sdk, workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow}, @@ -174,7 +174,7 @@ impl DvmTradeTransitionProofPrepareRequest { pub struct DvmTradeTransitionProofEnqueueRequest { #[serde(flatten)] pub prepare: DvmTradeTransitionProofPrepareRequest, - pub target_relays: SdkRelayTargetPolicy, + pub target_relays: TargetPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, } @@ -188,7 +188,7 @@ impl DvmTradeTransitionProofEnqueueRequest { request_event_id: RadrootsEventId, decision_event_id: RadrootsEventId, inventory_bins: Vec<RadrootsTradeInventoryBinWitnessDto>, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, ) -> Self { Self::from_prepare( DvmTradeTransitionProofPrepareRequest::new( @@ -206,7 +206,7 @@ impl DvmTradeTransitionProofEnqueueRequest { pub fn from_prepare( prepare: DvmTradeTransitionProofPrepareRequest, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, ) -> Self { Self { prepare, @@ -218,13 +218,13 @@ impl DvmTradeTransitionProofEnqueueRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -430,6 +430,7 @@ impl<'sdk> DvmClient<'sdk> { actor: &actor, frozen_draft: &plan.frozen_draft, target_relays, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key, }, ) @@ -453,6 +454,7 @@ impl<'sdk> DvmClient<'sdk> { actor: &actor, frozen_draft: &plan.frozen_draft, target_relays, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key, }, signer, diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -823,7 +823,7 @@ impl From<radroots_trade::projection::RadrootsTradeProjectionError> for Radroots impl From<radroots_outbox::RadrootsOutboxError> for RadrootsSdkError { fn from(error: radroots_outbox::RadrootsOutboxError) -> Self { match error { - radroots_outbox::RadrootsOutboxError::EmptyTargetRelays => { + radroots_outbox::RadrootsOutboxError::EmptyDeliveryTargets => { Self::empty_target_relays("outbox enqueue") } radroots_outbox::RadrootsOutboxError::IdempotencyConflict { @@ -846,34 +846,48 @@ impl From<radroots_outbox::RadrootsOutboxError> for RadrootsSdkError { } #[cfg(feature = "runtime")] -impl From<radroots_relay_transport::RadrootsRelayTransportError> for RadrootsSdkError { - fn from(error: radroots_relay_transport::RadrootsRelayTransportError) -> Self { +impl From<radroots_transport::RadrootsTransportError> for RadrootsSdkError { + fn from(error: radroots_transport::RadrootsTransportError) -> Self { match error { - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlParse { + radroots_transport::RadrootsTransportError::EmptyTargetSet => { + Self::empty_target_relays("transport target set") + } + error => Self::RelayTransport { + message: error.to_string(), + }, + } + } +} + +#[cfg(feature = "runtime")] +impl From<radroots_transport_nostr::RadrootsRelayTransportError> for RadrootsSdkError { + fn from(error: radroots_transport_nostr::RadrootsRelayTransportError) -> Self { + match error { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlParse { url, reason, } => Self::invalid_relay_url(url, reason), - radroots_relay_transport::RadrootsRelayTransportError::WsRequiresLocalhostPolicy { + radroots_transport_nostr::RadrootsRelayTransportError::WsRequiresLocalhostPolicy { url, } => Self::invalid_relay_url(url, "ws relay URL requires localhost policy"), - radroots_relay_transport::RadrootsRelayTransportError::UnsupportedRelayScheme { + radroots_transport_nostr::RadrootsRelayTransportError::UnsupportedRelayScheme { url, scheme, } => Self::invalid_relay_url(url, format!("unsupported scheme `{scheme}`")), - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlUserinfo { url } => { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlUserinfo { url } => { Self::invalid_relay_url(url, "relay URL must not include userinfo") } - radroots_relay_transport::RadrootsRelayTransportError::EmptyRelayHost { url } => { + radroots_transport_nostr::RadrootsRelayTransportError::EmptyRelayHost { url } => { Self::invalid_relay_url(url, "relay URL must include a host") } - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlQueryOrFragment { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlQueryOrFragment { url, } => Self::invalid_relay_url(url, "relay URL must not include query or fragment"), - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlForbiddenDestination { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlForbiddenDestination { url, reason, } => Self::invalid_relay_url(url, reason), - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination { url, address, reason, @@ -881,11 +895,11 @@ impl From<radroots_relay_transport::RadrootsRelayTransportError> for RadrootsSdk url, format!("relay URL resolved to forbidden address `{address}`: {reason}"), ), - radroots_relay_transport::RadrootsRelayTransportError::EmptyTargetSet => { + radroots_transport_nostr::RadrootsRelayTransportError::EmptyTargetSet => { Self::empty_target_relays("relay publish") } #[cfg(feature = "runtime")] - radroots_relay_transport::RadrootsRelayTransportError::Outbox(error) => error.into(), + radroots_transport_nostr::RadrootsRelayTransportError::Outbox(error) => error.into(), error => Self::RelayTransport { message: error.to_string(), }, diff --git a/crates/sdk/src/farms_runtime.rs b/crates/sdk/src/farms_runtime.rs @@ -2,8 +2,8 @@ use crate::workflow_runtime::enqueue_configured_signed_workflow; #[cfg(feature = "runtime")] use crate::{ - FarmsClient, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkMutationState, - SdkRelayTargetPolicy, SdkRelayUrlPolicy, farm, + FarmsClient, NostrRelayUrlPolicy, RadrootsSdkError, RadrootsSdkTimestamp, SatisfactionPolicy, + SdkIdempotencyKey, SdkMutationState, TargetPolicy, farm, geonames::{ Geocoder, GeocoderLocalityCandidate, GeocoderLocalityLookup, GeocoderLocalityQuery, GeocoderPoint, GeocoderReverseOptions, GeocoderReverseResult, @@ -69,7 +69,7 @@ pub struct FarmEnqueuePublishRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] pub actor: RadrootsActorContext, pub farm: RadrootsFarm, - pub target_relays: SdkRelayTargetPolicy, + pub target_relays: TargetPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, } @@ -79,7 +79,7 @@ impl FarmEnqueuePublishRequest { pub fn new( actor: RadrootsActorContext, farm: RadrootsFarm, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, ) -> Self { Self { actor, @@ -93,13 +93,13 @@ impl FarmEnqueuePublishRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -471,7 +471,7 @@ impl<'sdk> FarmsClient<'sdk> { &self, actor: &RadrootsActorContext, plan: FarmPublishPlan, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { let enqueue = enqueue_configured_signed_workflow( @@ -481,6 +481,7 @@ impl<'sdk> FarmsClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key, }, ) @@ -492,7 +493,7 @@ impl<'sdk> FarmsClient<'sdk> { &self, actor: &RadrootsActorContext, plan: FarmPublishPlan, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { @@ -503,6 +504,7 @@ impl<'sdk> FarmsClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key, }, signer, diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -44,8 +44,6 @@ mod private_store; #[cfg(feature = "runtime")] mod product_clients; #[cfg(feature = "runtime")] -mod relay_targets; -#[cfg(feature = "runtime")] mod runtime; #[cfg(all(feature = "runtime", feature = "signer-adapters"))] mod signer_provider; @@ -54,6 +52,8 @@ mod sync_runtime; #[cfg(feature = "runtime")] mod trade_storage; #[cfg(feature = "runtime")] +pub mod transport; +#[cfg(feature = "runtime")] mod workflow_runtime; #[cfg(feature = "runtime")] @@ -193,19 +193,14 @@ pub use crate::product_clients::{ TradeResyncClient, TradeSellerClient, TradeValidationReceiptsClient, TradesClient, }; #[cfg(feature = "runtime")] -pub use crate::relay_targets::{ - AckPolicy, PublishMode, RelayResolutionPolicy, SDK_RELAY_TARGET_MAX_COUNT, - SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy, -}; -#[cfg(feature = "runtime")] pub use crate::runtime::{ BackupReceipt, BackupRequest, IntegrityReceipt, IntegrityRequest, RadrootsClient, RadrootsClientBuilder, RadrootsSdkClock, RadrootsSdkStorageConfig, RadrootsSdkStoragePaths, RadrootsSdkTimestamp, RestoreArchive, RestoreReceipt, RestoreRequest, SdkBackupManifest, SdkBackupManifestKind, SdkBackupState, SdkBackupVerification, SdkEventStoreStorageStatus, - SdkOutboxStorageStatus, SdkPrivateStoreStorageStatus, SdkPublishTransport, SdkRestoreState, - SdkSqliteStoreStatus, SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, - StorageCheckpointReceipt, StorageCheckpointRequest, StorageStatusReceipt, StorageStatusRequest, + SdkOutboxStorageStatus, SdkPrivateStoreStorageStatus, SdkRestoreState, SdkSqliteStoreStatus, + SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, StorageCheckpointReceipt, + StorageCheckpointRequest, StorageStatusReceipt, StorageStatusRequest, }; #[cfg(all(feature = "runtime", feature = "signer-adapters"))] pub use crate::signer_provider::{ @@ -234,6 +229,14 @@ pub use crate::trade_storage::{ SdkTradeProjectionCacheRecord, }; #[cfg(feature = "runtime")] +pub use crate::transport::{ + HybridProfile, NostrProfile, NostrRelayUrlPolicy, ProxyProfile, PublishMode, + ReticulumPreviewBehavior, ReticulumPreviewProfile, SDK_TRANSPORT_TARGET_MAX_COUNT, + SatisfactionPolicy, TargetPolicy, TargetSet, TransportDeliveryReceipt, + TransportDeliveryTargetStatus, TransportKind, TransportOutcome, TransportProfile, + TransportReceipt, TransportTargetReceipt, +}; +#[cfg(feature = "runtime")] pub use radroots_trade::dvm::RadrootsTradeInventoryBinWitnessDto; #[cfg(feature = "runtime")] pub use radroots_trade::validation_receipt::{ diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -2,8 +2,8 @@ use crate::workflow_runtime::enqueue_configured_signed_workflow; #[cfg(feature = "runtime")] use crate::{ - ListingsClient, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, - SdkRelayTargetPolicy, SdkRelayUrlPolicy, + ListingsClient, NostrRelayUrlPolicy, RadrootsSdkError, RadrootsSdkTimestamp, + SatisfactionPolicy, SdkIdempotencyKey, TargetPolicy, workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow}, }; #[cfg(feature = "runtime")] @@ -68,7 +68,7 @@ pub struct ListingEnqueuePublishRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] pub actor: RadrootsActorContext, pub document: RadrootsListingDraftDocumentV1, - pub target_relays: SdkRelayTargetPolicy, + pub target_relays: TargetPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, } @@ -78,7 +78,7 @@ impl ListingEnqueuePublishRequest { pub fn new( actor: RadrootsActorContext, listing: RadrootsListing, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, ) -> Self { Self::from_document( actor, @@ -90,7 +90,7 @@ impl ListingEnqueuePublishRequest { pub fn from_document( actor: RadrootsActorContext, document: RadrootsListingDraftDocumentV1, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, ) -> Self { Self { actor, @@ -104,13 +104,13 @@ impl ListingEnqueuePublishRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -241,7 +241,7 @@ impl<'sdk> ListingsClient<'sdk> { &self, actor: &RadrootsActorContext, plan: ListingPublishPlan, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { let enqueue = enqueue_configured_signed_workflow( @@ -251,6 +251,7 @@ impl<'sdk> ListingsClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key, }, ) @@ -262,7 +263,7 @@ impl<'sdk> ListingsClient<'sdk> { &self, actor: &RadrootsActorContext, plan: ListingPublishPlan, - target_relays: SdkRelayTargetPolicy, + target_relays: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { @@ -273,6 +274,7 @@ impl<'sdk> ListingsClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key, }, signer, diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs @@ -8,20 +8,19 @@ use crate::sync_runtime::SyncProjectionRefreshReceipt; use crate::sync_runtime::{SyncProjectionRefreshRequest, refresh_product_projections_for_sdk}; #[cfg(feature = "signer-adapters")] use crate::workflow_runtime::enqueue_configured_signed_workflow; +#[cfg(all(feature = "runtime", test))] +use crate::{NostrRelayUrlPolicy, workflow_runtime::enqueue_signed_workflow}; #[cfg(any(feature = "signer-adapters", test))] use crate::{ - AckPolicy, PrivacyPreflightConfirmation, PrivacyPreflightReceipt, ProductSensitivityField, - PublishMode, PushOutboxReceipt, PushOutboxRequest, RadrootsSdkRecoveryAction, - RelayResolutionPolicy, SdkIdempotencyKey, SdkMutationState, - workflow_runtime::SdkWorkflowEnqueueRequest, + PrivacyPreflightConfirmation, PrivacyPreflightReceipt, ProductSensitivityField, PublishMode, + PushOutboxReceipt, PushOutboxRequest, RadrootsSdkRecoveryAction, SatisfactionPolicy, + SdkIdempotencyKey, SdkMutationState, TargetPolicy, workflow_runtime::SdkWorkflowEnqueueRequest, }; #[cfg(feature = "runtime")] use crate::{ RadrootsSdkError, RadrootsSdkTimestamp, TradeResyncClient, TradeSellerClient, TradeValidationReceiptsClient, TradesClient, order, }; -#[cfg(all(feature = "runtime", test))] -use crate::{SdkRelayUrlPolicy, workflow_runtime::enqueue_signed_workflow}; #[cfg(feature = "runtime")] use radroots_authority::RadrootsActorContext; #[cfg(all(feature = "runtime", test))] @@ -75,12 +74,6 @@ use radroots_nostr::prelude::{ RadrootsNostrEventId, RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrPublicKey, radroots_nostr_filter_tag, }; -#[cfg(all(feature = "runtime", feature = "relay-runtime"))] -use radroots_relay_transport::{ - RadrootsNostrClientFetchAdapter, RadrootsRelayFetchAdapter, RadrootsRelayFetchEventReceipt, - RadrootsRelayFetchOutcomeKind, RadrootsRelayFetchReceipt, RadrootsRelayFetchRelayOutcome, - RadrootsRelayFetchRequest, RadrootsRelayOutcomeKind, fetch_and_ingest_relay_events, -}; #[cfg(feature = "runtime")] use radroots_trade::dvm::RADROOTS_DVM_TAG_VALIDATION_RECEIPT; #[cfg(feature = "runtime")] @@ -112,6 +105,12 @@ use radroots_trade::validation_receipt::{ }; #[cfg(feature = "runtime")] use radroots_trade::workflow::RadrootsTradeWorkflowState; +#[cfg(all(feature = "runtime", feature = "relay-runtime"))] +use radroots_transport_nostr::{ + RadrootsNostrClientFetchAdapter, RadrootsRelayFetchAdapter, RadrootsRelayFetchEventReceipt, + RadrootsRelayFetchOutcomeKind, RadrootsRelayFetchReceipt, RadrootsRelayFetchRelayOutcome, + RadrootsRelayFetchRequest, RadrootsRelayOutcomeKind, fetch_and_ingest_relay_events, +}; #[cfg(feature = "runtime")] use serde::Deserialize; #[cfg(feature = "runtime")] @@ -281,9 +280,9 @@ pub struct TradeSubmitEnqueueRequest { pub actor: RadrootsActorContext, pub listing_event: RadrootsNostrEventPtr, pub order: RadrootsOrderRequest, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, } @@ -295,9 +294,9 @@ impl TradeSubmitEnqueueRequest { actor: RadrootsActorContext, listing_event: RadrootsNostrEventPtr, order: RadrootsOrderRequest, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Self { Self { actor, @@ -314,13 +313,13 @@ impl TradeSubmitEnqueueRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = RelayResolutionPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -487,9 +486,9 @@ pub struct TradeDecisionEnqueueRequest { pub actor: RadrootsActorContext, pub request_event: RadrootsNostrEventPtr, pub decision: RadrootsOrderDecision, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, } @@ -501,9 +500,9 @@ impl TradeDecisionEnqueueRequest { actor: RadrootsActorContext, request_event: RadrootsNostrEventPtr, decision: RadrootsOrderDecision, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Self { Self { actor, @@ -520,13 +519,13 @@ impl TradeDecisionEnqueueRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = RelayResolutionPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -628,9 +627,9 @@ pub struct TradeRevisionProposalEnqueueRequest { pub root_event: RadrootsNostrEventPtr, pub previous_event: RadrootsNostrEventPtr, pub proposal: RadrootsOrderRevisionProposal, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, } @@ -643,9 +642,9 @@ impl TradeRevisionProposalEnqueueRequest { root_event: RadrootsNostrEventPtr, previous_event: RadrootsNostrEventPtr, proposal: RadrootsOrderRevisionProposal, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Self { Self { actor, @@ -663,13 +662,13 @@ impl TradeRevisionProposalEnqueueRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = RelayResolutionPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -773,9 +772,9 @@ pub struct TradeRevisionDecisionEnqueueRequest { pub root_event: RadrootsNostrEventPtr, pub previous_event: RadrootsNostrEventPtr, pub decision: RadrootsOrderRevisionDecision, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, } @@ -788,9 +787,9 @@ impl TradeRevisionDecisionEnqueueRequest { root_event: RadrootsNostrEventPtr, previous_event: RadrootsNostrEventPtr, decision: RadrootsOrderRevisionDecision, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Self { Self { actor, @@ -808,13 +807,13 @@ impl TradeRevisionDecisionEnqueueRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = RelayResolutionPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -918,9 +917,9 @@ pub struct TradeCancellationEnqueueRequest { pub root_event: RadrootsNostrEventPtr, pub previous_event: RadrootsNostrEventPtr, pub cancellation: RadrootsOrderCancellation, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, } @@ -933,9 +932,9 @@ impl TradeCancellationEnqueueRequest { root_event: RadrootsNostrEventPtr, previous_event: RadrootsNostrEventPtr, cancellation: RadrootsOrderCancellation, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Self { Self { actor, @@ -953,13 +952,13 @@ impl TradeCancellationEnqueueRequest { pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayUrlPolicy, + policy: NostrRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = RelayResolutionPolicy::try_explicit(target_relays, policy)?; + self.target_relays = TargetPolicy::try_nostr_relays(target_relays, policy)?; Ok(self) } @@ -1069,9 +1068,9 @@ pub struct TradeProposeRequest { pub items: Vec<RadrootsOrderItem>, pub economics: RadrootsOrderEconomics, pub public_note: Option<String>, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub privacy_confirmation: PrivacyPreflightConfirmation, pub idempotency_key: Option<SdkIdempotencyKey>, pub created_at: Option<RadrootsSdkTimestamp>, @@ -1087,9 +1086,9 @@ impl TradeProposeRequest { seller_pubkey: RadrootsPublicKey, items: Vec<RadrootsOrderItem>, economics: RadrootsOrderEconomics, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Self { Self { actor, @@ -1149,9 +1148,9 @@ pub struct TradeAcceptRequest { pub actor: RadrootsActorContext, pub locator: RadrootsTradeLocator, pub inventory_commitments: Vec<RadrootsOrderInventoryCommitment>, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub evidence_mode: TradeEvidenceMode, pub privacy_confirmation: PrivacyPreflightConfirmation, pub idempotency_key: Option<SdkIdempotencyKey>, @@ -1164,9 +1163,9 @@ impl TradeAcceptRequest { actor: RadrootsActorContext, locator: RadrootsTradeLocator, inventory_commitments: Vec<RadrootsOrderInventoryCommitment>, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, evidence_mode: TradeEvidenceMode, ) -> Self { Self { @@ -1213,9 +1212,9 @@ pub struct TradeDeclineRequest { pub actor: RadrootsActorContext, pub locator: RadrootsTradeLocator, pub reason: String, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub evidence_mode: TradeEvidenceMode, pub privacy_confirmation: PrivacyPreflightConfirmation, pub idempotency_key: Option<SdkIdempotencyKey>, @@ -1228,9 +1227,9 @@ impl TradeDeclineRequest { actor: RadrootsActorContext, locator: RadrootsTradeLocator, reason: impl Into<String>, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, evidence_mode: TradeEvidenceMode, ) -> Self { Self { @@ -1277,9 +1276,9 @@ pub struct TradeCancelRequest { pub actor: RadrootsActorContext, pub locator: RadrootsTradeLocator, pub reason: String, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub evidence_mode: TradeEvidenceMode, pub privacy_confirmation: PrivacyPreflightConfirmation, pub idempotency_key: Option<SdkIdempotencyKey>, @@ -1292,9 +1291,9 @@ impl TradeCancelRequest { actor: RadrootsActorContext, locator: RadrootsTradeLocator, reason: impl Into<String>, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, evidence_mode: TradeEvidenceMode, ) -> Self { Self { @@ -1344,9 +1343,9 @@ pub struct TradeRevisionProposalRequest { pub items: Vec<RadrootsOrderItem>, pub economics: RadrootsOrderEconomics, pub reason: String, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub evidence_mode: TradeEvidenceMode, pub privacy_confirmation: PrivacyPreflightConfirmation, pub idempotency_key: Option<SdkIdempotencyKey>, @@ -1362,9 +1361,9 @@ impl TradeRevisionProposalRequest { items: Vec<RadrootsOrderItem>, economics: RadrootsOrderEconomics, reason: impl Into<String>, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, evidence_mode: TradeEvidenceMode, ) -> Self { Self { @@ -1415,9 +1414,9 @@ pub struct TradeRevisionDecisionRequest { pub locator: RadrootsTradeLocator, pub revision_id: RadrootsOrderRevisionId, pub decision: RadrootsOrderRevisionOutcome, - pub target_relays: RelayResolutionPolicy, + pub target_relays: TargetPolicy, pub publish_mode: PublishMode, - pub ack_policy: AckPolicy, + pub ack_policy: SatisfactionPolicy, pub evidence_mode: TradeEvidenceMode, pub privacy_confirmation: PrivacyPreflightConfirmation, pub idempotency_key: Option<SdkIdempotencyKey>, @@ -1431,9 +1430,9 @@ impl TradeRevisionDecisionRequest { locator: RadrootsTradeLocator, revision_id: RadrootsOrderRevisionId, decision: RadrootsOrderRevisionOutcome, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, evidence_mode: TradeEvidenceMode, ) -> Self { Self { @@ -2639,9 +2638,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeSubmitPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, ) -> Result<TradeSubmitReceipt, RadrootsSdkError> { validate_trade_enqueue_policy(publish_mode, ack_policy)?; @@ -2652,6 +2651,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, ) @@ -2664,9 +2664,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeSubmitPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<TradeSubmitReceipt, RadrootsSdkError> { @@ -2678,6 +2678,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, signer, @@ -2776,9 +2777,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeDecisionPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, ) -> Result<TradeDecisionReceipt, RadrootsSdkError> { validate_trade_enqueue_policy(publish_mode, ack_policy)?; @@ -2795,6 +2796,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, ) @@ -2807,9 +2809,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeDecisionPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<TradeDecisionReceipt, RadrootsSdkError> { @@ -2827,6 +2829,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, signer, @@ -2930,9 +2933,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeRevisionProposalPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, ) -> Result<TradeRevisionProposalReceipt, RadrootsSdkError> { validate_trade_enqueue_policy(publish_mode, ack_policy)?; @@ -2949,6 +2952,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, ) @@ -2961,9 +2965,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeRevisionProposalPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<TradeRevisionProposalReceipt, RadrootsSdkError> { @@ -2981,6 +2985,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, signer, @@ -3084,9 +3089,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeRevisionDecisionPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, ) -> Result<TradeRevisionDecisionReceipt, RadrootsSdkError> { validate_trade_enqueue_policy(publish_mode, ack_policy)?; @@ -3103,6 +3108,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, ) @@ -3115,9 +3121,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeRevisionDecisionPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<TradeRevisionDecisionReceipt, RadrootsSdkError> { @@ -3135,6 +3141,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, signer, @@ -3238,9 +3245,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeCancellationPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, ) -> Result<TradeCancellationReceipt, RadrootsSdkError> { validate_trade_enqueue_policy(publish_mode, ack_policy)?; @@ -3257,6 +3264,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, ) @@ -3269,9 +3277,9 @@ impl<'sdk> TradesClient<'sdk> { &self, actor: &RadrootsActorContext, plan: TradeCancellationPlan, - target_relays: RelayResolutionPolicy, + target_relays: TargetPolicy, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<TradeCancellationReceipt, RadrootsSdkError> { @@ -3289,6 +3297,7 @@ impl<'sdk> TradesClient<'sdk> { actor, frozen_draft: &plan.frozen_draft, target_relays: target_relays.workflow_target_policy(), + satisfaction_policy: ack_policy, idempotency_key, }, signer, @@ -3607,7 +3616,7 @@ async fn execute_trade_resync_with_fetch_adapter<A>( where A: RadrootsRelayFetchAdapter, { - let relay_targets = sdk.relay_urls().to_vec(); + let relay_targets = sdk.configured_nostr_relay_urls().to_vec(); if relay_targets.is_empty() { return Err(RadrootsSdkError::empty_target_relays(operation)); } @@ -4289,7 +4298,7 @@ fn validation_receipt_relay_targets( sdk: &crate::RadrootsClient, operation: impl Into<String>, ) -> Result<Vec<String>, RadrootsSdkError> { - let relay_targets = sdk.relay_urls().to_vec(); + let relay_targets = sdk.configured_nostr_relay_urls().to_vec(); if relay_targets.is_empty() { return Err(RadrootsSdkError::empty_target_relays(operation)); } @@ -6111,7 +6120,7 @@ fn event_ptr(event_id: &RadrootsEventId) -> RadrootsNostrEventPtr { async fn trade_product_post_enqueue_outcome<Plan, Receipt>( sdk: &crate::RadrootsClient, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, outbox_event_id: i64, receipt: Receipt, ) -> Result<TradeMutationOutcome<Plan, Receipt>, RadrootsSdkError> { @@ -6132,33 +6141,37 @@ async fn trade_product_post_enqueue_outcome<Plan, Receipt>( #[cfg(feature = "signer-adapters")] fn push_request_for_ack_policy( - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, outbox_event_id: i64, ) -> Result<PushOutboxRequest, RadrootsSdkError> { let request = PushOutboxRequest::new().with_outbox_event_id(outbox_event_id); match ack_policy { - AckPolicy::NoWait => Err(RadrootsSdkError::InvalidRequest { + SatisfactionPolicy::NoWait => Err(RadrootsSdkError::InvalidRequest { message: "trade enqueue-and-publish requires a relay acknowledgement policy".to_owned(), }), - AckPolicy::AtLeastOneRelay => Ok(request.with_accepted_quorum(1)), - AckPolicy::AllRelays => Ok(request), - AckPolicy::Quorum { required } => Ok(request.with_accepted_quorum(usize::from(required))), + SatisfactionPolicy::AtLeastOneTarget => Ok(request.with_accepted_quorum(1)), + SatisfactionPolicy::AllTargets => Ok(request), + SatisfactionPolicy::AtLeast { required } => { + Ok(request.with_accepted_quorum(usize::from(required))) + } } } #[cfg(feature = "signer-adapters")] fn validate_trade_product_publish_policy( publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Result<(), RadrootsSdkError> { match publish_mode { - PublishMode::DryRun | PublishMode::EnqueueOnly if ack_policy != AckPolicy::NoWait => { + PublishMode::DryRun | PublishMode::EnqueueOnly + if ack_policy != SatisfactionPolicy::NoWait => + { Err(RadrootsSdkError::InvalidRequest { message: "trade dry-run and enqueue-only modes require no-wait acknowledgement" .to_owned(), }) } - PublishMode::EnqueueAndPublish if ack_policy == AckPolicy::NoWait => { + PublishMode::EnqueueAndPublish if ack_policy == SatisfactionPolicy::NoWait => { Err(RadrootsSdkError::InvalidRequest { message: "trade enqueue-and-publish requires a relay acknowledgement policy" .to_owned(), @@ -6264,20 +6277,20 @@ fn trade_reason_contains_private_coordination(reason: &str) -> bool { #[cfg(any(feature = "signer-adapters", test))] fn validate_trade_enqueue_policy( publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> Result<(), RadrootsSdkError> { if publish_mode == PublishMode::DryRun { return Err(RadrootsSdkError::InvalidRequest { message: "trade dry-run publish mode must use a prepare request".to_owned(), }); } - if publish_mode == PublishMode::EnqueueOnly && ack_policy != AckPolicy::NoWait { + if publish_mode == PublishMode::EnqueueOnly && ack_policy != SatisfactionPolicy::NoWait { return Err(RadrootsSdkError::InvalidRequest { message: "trade enqueue-only publish mode only supports no-wait acknowledgement" .to_owned(), }); } - if publish_mode == PublishMode::EnqueueAndPublish && ack_policy == AckPolicy::NoWait { + if publish_mode == PublishMode::EnqueueAndPublish && ack_policy == SatisfactionPolicy::NoWait { return Err(RadrootsSdkError::InvalidRequest { message: "trade enqueue-and-publish requires a relay acknowledgement policy".to_owned(), }); diff --git a/crates/sdk/src/relay_targets.rs b/crates/sdk/src/relay_targets.rs @@ -1,280 +0,0 @@ -use crate::RadrootsSdkError; -use radroots_relay_transport::{RadrootsRelayUrl, RadrootsRelayUrlPolicy}; -use serde::ser::SerializeStruct; -use std::collections::BTreeSet; - -pub const SDK_RELAY_TARGET_MAX_COUNT: usize = 20; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum PublishMode { - DryRun, - EnqueueOnly, - EnqueueAndPublish, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum AckPolicy { - NoWait, - AtLeastOneRelay, - AllRelays, - Quorum { required: u16 }, -} - -impl AckPolicy { - pub fn quorum(required: u16) -> Result<Self, RadrootsSdkError> { - if required == 0 { - return Err(RadrootsSdkError::InvalidRequest { - message: "ack policy quorum must require at least one relay".to_owned(), - }); - } - Ok(Self::Quorum { required }) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum RelayResolutionPolicy { - ConfiguredRelays, - Explicit(SdkRelayTargetSet), -} - -impl RelayResolutionPolicy { - pub fn configured_relays() -> Self { - Self::ConfiguredRelays - } - - pub fn explicit(targets: SdkRelayTargetSet) -> Self { - Self::Explicit(targets) - } - - pub fn try_explicit<I, S>( - relays: I, - url_policy: SdkRelayUrlPolicy, - ) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - Ok(Self::Explicit(SdkRelayTargetSet::new(relays, url_policy)?)) - } - - #[cfg(any(feature = "signer-adapters", test))] - pub(crate) fn workflow_target_policy(self) -> SdkRelayTargetPolicy { - match self { - Self::ConfiguredRelays => SdkRelayTargetPolicy::UseConfiguredRelays, - Self::Explicit(targets) => SdkRelayTargetPolicy::Explicit(targets), - } - } -} - -impl serde::Serialize for RelayResolutionPolicy { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - match self { - Self::ConfiguredRelays => { - let mut state = serializer.serialize_struct("RelayResolutionPolicy", 1)?; - state.serialize_field("kind", "configured_relays")?; - state.end() - } - Self::Explicit(targets) => { - let mut state = serializer.serialize_struct("RelayResolutionPolicy", 3)?; - state.serialize_field("kind", "explicit")?; - state.serialize_field("relays", targets.relays())?; - state.serialize_field("canonical_relays", targets.canonical_relays())?; - state.end() - } - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum SdkRelayUrlPolicy { - Public, - Localhost, -} - -impl SdkRelayUrlPolicy { - pub(crate) fn relay_transport_policy(self) -> RadrootsRelayUrlPolicy { - match self { - Self::Public => RadrootsRelayUrlPolicy::Public, - Self::Localhost => RadrootsRelayUrlPolicy::Localhost, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum SdkRelayTargetPolicy { - Explicit(SdkRelayTargetSet), - UseConfiguredRelays, - UsePublishTransport, -} - -impl SdkRelayTargetPolicy { - pub fn explicit(targets: SdkRelayTargetSet) -> Self { - Self::Explicit(targets) - } - - pub fn try_explicit<I, S>( - relays: I, - url_policy: SdkRelayUrlPolicy, - ) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - Ok(Self::Explicit(SdkRelayTargetSet::new(relays, url_policy)?)) - } - - pub fn use_publish_transport() -> Self { - Self::UsePublishTransport - } -} - -impl serde::Serialize for SdkRelayTargetPolicy { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - match self { - Self::Explicit(targets) => { - let mut state = serializer.serialize_struct("SdkRelayTargetPolicy", 3)?; - state.serialize_field("kind", "explicit")?; - state.serialize_field("relays", targets.relays())?; - state.serialize_field("canonical_relays", targets.canonical_relays())?; - state.end() - } - Self::UseConfiguredRelays => { - let mut state = serializer.serialize_struct("SdkRelayTargetPolicy", 1)?; - state.serialize_field("kind", "use_configured_relays")?; - state.end() - } - Self::UsePublishTransport => { - let mut state = serializer.serialize_struct("SdkRelayTargetPolicy", 1)?; - state.serialize_field("kind", "use_publish_transport")?; - state.end() - } - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRelayTargetSet { - relays: Vec<String>, - canonical_relays: Vec<String>, -} - -impl SdkRelayTargetSet { - pub fn new<I, S>(relays: I, policy: SdkRelayUrlPolicy) -> Result<Self, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - let mut ordered_relays = Vec::new(); - let mut seen = BTreeSet::new(); - for relay in relays { - let normalized = normalized_relay_url(relay.as_ref(), policy)?; - if seen.insert(normalized.clone()) { - ordered_relays.push(normalized); - } - } - Self::from_normalized_ordered(ordered_relays) - } - - pub fn relays(&self) -> &[String] { - self.relays.as_slice() - } - - pub fn canonical_relays(&self) -> &[String] { - self.canonical_relays.as_slice() - } - - pub fn into_vec(self) -> Vec<String> { - self.relays - } - - pub fn len(&self) -> usize { - self.relays.len() - } - - pub fn is_empty(&self) -> bool { - self.relays.is_empty() - } - - pub(crate) fn from_configured_relays<I, S>( - relays: I, - policy: SdkRelayUrlPolicy, - ) -> Result<Vec<String>, RadrootsSdkError> - where - I: IntoIterator<Item = S>, - S: AsRef<str>, - { - let relays = relays.into_iter().collect::<Vec<_>>(); - if relays.is_empty() { - return Ok(Vec::new()); - } - Ok(Self::new(relays, policy)?.into_vec()) - } - - pub(crate) fn from_normalized_relays(relays: Vec<String>) -> Result<Self, RadrootsSdkError> { - let mut ordered = Vec::new(); - let mut seen = BTreeSet::new(); - for relay in relays { - if seen.insert(relay.clone()) { - ordered.push(relay); - } - } - Self::from_normalized_ordered(ordered) - } - - fn from_normalized_ordered(relays: Vec<String>) -> Result<Self, RadrootsSdkError> { - if relays.is_empty() { - return Err(RadrootsSdkError::empty_target_relays( - "sdk relay target set", - )); - } - if relays.len() > SDK_RELAY_TARGET_MAX_COUNT { - return Err(RadrootsSdkError::relay_target_limit_exceeded( - SDK_RELAY_TARGET_MAX_COUNT, - relays.len(), - )); - } - let canonical_relays = relays.iter().cloned().collect::<BTreeSet<_>>(); - Ok(Self { - relays, - canonical_relays: canonical_relays.into_iter().collect(), - }) - } -} - -impl serde::Serialize for SdkRelayTargetSet { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("SdkRelayTargetSet", 2)?; - state.serialize_field("relays", self.relays())?; - state.serialize_field("canonical_relays", self.canonical_relays())?; - state.end() - } -} - -fn normalized_relay_url( - value: &str, - policy: SdkRelayUrlPolicy, -) -> Result<String, RadrootsSdkError> { - let relay = RadrootsRelayUrl::parse(value, policy.relay_transport_policy())?; - Ok(relay.into_string()) -} - -#[cfg(test)] -#[path = "../tests/unit/relay_targets_tests.rs"] -mod tests; diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs @@ -3,7 +3,7 @@ use crate::private_store::{SDK_PRIVATE_STORE_SCHEMA_VERSION, SdkPrivateStore}; #[cfg(feature = "runtime")] use crate::{ DvmClient, FarmsClient, GeoNamesClient, ListingsClient, MarketClient, RadrootsGeoNamesConfig, - RadrootsSdkError, SdkRelayTargetSet, SdkRelayUrlPolicy, SyncClient, TradesClient, + RadrootsSdkError, SyncClient, TradesClient, transport::TransportProfile, }; #[cfg(all(feature = "runtime", feature = "signer-adapters"))] use crate::{ @@ -24,9 +24,6 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] -use crate::adapters::radrootsd::RadrootsdProxyConfig; - #[cfg(feature = "runtime")] const SDK_STORAGE_MANIFEST_VERSION: u16 = 1; #[cfg(feature = "runtime")] @@ -395,33 +392,6 @@ pub enum SdkRestoreState { } #[cfg(feature = "runtime")] -#[derive(Clone, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum SdkPublishTransport { - DirectNostrRelay, - #[cfg(feature = "radrootsd-proxy")] - RadrootsdProxy(RadrootsdProxyConfig), -} - -#[cfg(feature = "runtime")] -impl Default for SdkPublishTransport { - fn default() -> Self { - Self::DirectNostrRelay - } -} - -#[cfg(feature = "runtime")] -impl SdkPublishTransport { - pub(crate) fn supports_delegated_relay_resolution(&self) -> bool { - match self { - Self::DirectNostrRelay => false, - #[cfg(feature = "radrootsd-proxy")] - Self::RadrootsdProxy(_) => true, - } - } -} - -#[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct RestoreArchive { pub source: PathBuf, @@ -455,9 +425,7 @@ pub struct RadrootsClientBuilder { storage: RadrootsSdkStorageConfig, geonames: Option<RadrootsGeoNamesConfig>, clock: RadrootsSdkClock, - relay_urls: Vec<String>, - relay_url_policy: SdkRelayUrlPolicy, - publish_transport: SdkPublishTransport, + transport_profile: TransportProfile, #[cfg(feature = "signer-adapters")] signer_provider: Option<RadrootsSdkSignerProvider>, } @@ -469,9 +437,7 @@ impl Default for RadrootsClientBuilder { storage: RadrootsSdkStorageConfig::Memory, geonames: None, clock: RadrootsSdkClock::System, - relay_urls: Vec::new(), - relay_url_policy: SdkRelayUrlPolicy::Public, - publish_transport: SdkPublishTransport::DirectNostrRelay, + transport_profile: TransportProfile::default(), #[cfg(feature = "signer-adapters")] signer_provider: None, } @@ -510,18 +476,8 @@ impl RadrootsClientBuilder { self } - pub fn relay_url(mut self, relay_url: impl Into<String>) -> Self { - self.relay_urls.push(relay_url.into()); - self - } - - pub fn relay_url_policy(mut self, policy: SdkRelayUrlPolicy) -> Self { - self.relay_url_policy = policy; - self - } - - pub fn publish_transport(mut self, transport: SdkPublishTransport) -> Self { - self.publish_transport = transport; + pub fn transport_profile(mut self, profile: TransportProfile) -> Self { + self.transport_profile = profile; self } @@ -533,8 +489,6 @@ impl RadrootsClientBuilder { pub async fn build(self) -> Result<RadrootsClient, RadrootsSdkError> { let storage = open_storage(&self.storage).await?; - let relay_urls = - SdkRelayTargetSet::from_configured_relays(&self.relay_urls, self.relay_url_policy)?; Ok(RadrootsClient { _event_store: storage.event_store, _outbox: storage.outbox, @@ -542,8 +496,7 @@ impl RadrootsClientBuilder { storage_paths: storage.paths, geonames: self.geonames, clock: self.clock, - relay_urls, - publish_transport: self.publish_transport, + transport_profile: self.transport_profile, #[cfg(feature = "signer-adapters")] signer_provider: self.signer_provider, }) @@ -559,8 +512,7 @@ pub struct RadrootsClient { storage_paths: Option<RadrootsSdkStoragePaths>, geonames: Option<RadrootsGeoNamesConfig>, clock: RadrootsSdkClock, - relay_urls: Vec<String>, - publish_transport: SdkPublishTransport, + transport_profile: TransportProfile, #[cfg(feature = "signer-adapters")] signer_provider: Option<RadrootsSdkSignerProvider>, } @@ -603,12 +555,12 @@ impl RadrootsClient { self.clock.now() } - pub fn relay_urls(&self) -> &[String] { - &self.relay_urls + pub fn transport_profile(&self) -> &TransportProfile { + &self.transport_profile } - pub fn publish_transport(&self) -> &SdkPublishTransport { - &self.publish_transport + pub fn configured_nostr_relay_urls(&self) -> Vec<String> { + self.transport_profile.configured_nostr_relay_urls() } #[cfg(feature = "signer-adapters")] @@ -664,7 +616,7 @@ impl RadrootsClient { store: event_store_status, total_events: event_summary.total_events, projection_eligible_events: event_summary.projection_eligible_events, - relay_observations: event_summary.relay_observations, + relay_observations: event_summary.transport_observations, last_event_seq: event_summary.last_event_seq, last_event_updated_at_ms: event_summary.last_event_updated_at_ms, }, diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -1,11 +1,13 @@ #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] use crate::adapters::radrootsd::{ - RadrootsdError, RadrootsdProxyPublishAdapter, RadrootsdProxyPublishRequest, + RadrootsdError, RadrootsdProxyConfig, RadrootsdProxyPublishAdapter, + RadrootsdProxyPublishRequest, }; #[cfg(feature = "runtime")] use crate::{ - RadrootsSdkError, SdkRelayUrlPolicy, SyncClient, + NostrRelayUrlPolicy, RadrootsSdkError, SyncClient, runtime::{RadrootsClient, sdk_now_ms}, + transport::TransportProfile, }; #[cfg(feature = "runtime")] use radroots_event_store::{RADROOTS_EVENT_STORE_QUERY_LIMIT_MAX, RadrootsEventStoreStatusSummary}; @@ -14,24 +16,29 @@ use radroots_events::ids::RadrootsEventId; #[cfg(all(feature = "runtime", feature = "relay-runtime"))] use radroots_nostr::prelude::RadrootsNostrClient; #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] -use radroots_outbox::RadrootsOutboxClaimedEvent; +use radroots_outbox::{ + RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryTargetRecord, + RadrootsOutboxDeliveryTargetStatus, +}; #[cfg(feature = "runtime")] use radroots_outbox::{RadrootsOutboxEventState, RadrootsOutboxStatusSummary}; #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] use radroots_publish_proxy_protocol::PublishDeliveryPolicy; -#[cfg(all(feature = "runtime", feature = "relay-runtime"))] -use radroots_relay_transport::RadrootsNostrClientPublishAdapter; -#[cfg(feature = "runtime")] -use radroots_relay_transport::{ - RadrootsOutboxPublishPolicy, RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter, - RadrootsRelayPublishReceipt, RadrootsRelayPublishRelayReceipt, publish_claimed_outbox_event, -}; #[cfg(feature = "runtime")] use radroots_trade::projection::{ RADROOTS_PRODUCT_PROJECTION_ID, RADROOTS_PRODUCT_PROJECTION_VERSION, RadrootsProjectionRefreshReceipt, RadrootsProjectionRefreshRequest, refresh_product_projections, }; +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +use radroots_transport::{RadrootsTransportKind, RadrootsTransportSatisfactionPolicy}; +#[cfg(all(feature = "runtime", feature = "relay-runtime"))] +use radroots_transport_nostr::RadrootsNostrClientPublishAdapter; +#[cfg(feature = "runtime")] +use radroots_transport_nostr::{ + RadrootsOutboxPublishPolicy, RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter, + RadrootsRelayPublishReceipt, RadrootsRelayPublishRelayReceipt, publish_claimed_outbox_event, +}; #[cfg(feature = "runtime")] pub const PUSH_OUTBOX_DEFAULT_LIMIT: usize = 20; @@ -95,7 +102,7 @@ impl From<RadrootsEventStoreStatusSummary> for SyncEventStoreStatus { Self { total_events: summary.total_events, projection_eligible_events: summary.projection_eligible_events, - relay_observations: summary.relay_observations, + relay_observations: summary.transport_observations, last_event_seq: summary.last_event_seq, last_event_updated_at_ms: summary.last_event_updated_at_ms, } @@ -193,7 +200,7 @@ impl SyncProjectionRefreshReceipt { listing_upserts: receipt.listing_upserts, trade_upserts: receipt.trade_upserts, validation_receipts: receipt.validation_receipts, - relay_observations: receipt.relay_observations, + relay_observations: receipt.transport_observations, last_event_seq: receipt.last_event_seq, } } @@ -222,7 +229,7 @@ pub struct PushOutboxRequest { pub outbox_event_id: Option<i64>, pub republish_accepted_relays: bool, pub accepted_quorum: Option<usize>, - pub relay_url_policy: SdkRelayUrlPolicy, + pub relay_url_policy: NostrRelayUrlPolicy, pub auth_policy: SdkRelayAuthPolicy, pub claim_ttl_ms: i64, pub next_attempt_delay_ms: i64, @@ -236,7 +243,7 @@ impl Default for PushOutboxRequest { outbox_event_id: None, republish_accepted_relays: false, accepted_quorum: None, - relay_url_policy: SdkRelayUrlPolicy::Public, + relay_url_policy: NostrRelayUrlPolicy::Public, auth_policy: SdkRelayAuthPolicy::DetectOnly, claim_ttl_ms: PUSH_OUTBOX_DEFAULT_CLAIM_TTL_MS, next_attempt_delay_ms: PUSH_OUTBOX_DEFAULT_NEXT_ATTEMPT_DELAY_MS, @@ -271,7 +278,7 @@ impl PushOutboxRequest { self } - pub fn with_relay_url_policy(mut self, policy: SdkRelayUrlPolicy) -> Self { + pub fn with_relay_url_policy(mut self, policy: NostrRelayUrlPolicy) -> Self { self.relay_url_policy = policy; self } @@ -480,8 +487,8 @@ impl<'sdk> SyncClient<'sdk> { event_store: event_store.into(), outbox: outbox.into(), relay_targets: SyncRelayTargetSummary { - configured_count: self.sdk.relay_urls().len(), - configured_relays: self.sdk.relay_urls().to_vec(), + configured_count: self.sdk.configured_nostr_relay_urls().len(), + configured_relays: self.sdk.configured_nostr_relay_urls(), }, }) } @@ -490,8 +497,8 @@ impl<'sdk> SyncClient<'sdk> { &self, request: PushOutboxRequest, ) -> Result<PushOutboxReceipt, RadrootsSdkError> { - match self.sdk.publish_transport() { - crate::runtime::SdkPublishTransport::DirectNostrRelay => { + match self.sdk.transport_profile() { + TransportProfile::Nostr { .. } | TransportProfile::Hybrid { .. } => { #[cfg(feature = "relay-runtime")] { let adapter = RadrootsNostrClientPublishAdapter::new( @@ -510,13 +517,34 @@ impl<'sdk> SyncClient<'sdk> { } } #[cfg(feature = "radrootsd-proxy")] - crate::runtime::SdkPublishTransport::RadrootsdProxy(config) => { - let adapter = RadrootsdProxyPublishAdapter::new(config.clone()); + TransportProfile::Proxy { profile } => { + let adapter = RadrootsdProxyPublishAdapter::new(RadrootsdProxyConfig::new( + profile.endpoint_url().to_owned(), + )); self.push_outbox_with_proxy_adapter(&adapter, request).await } + TransportProfile::LocalOnly | TransportProfile::ReticulumPreview { .. } => { + if self.push_outbox_has_no_ready_signed_work(&request).await? { + return Ok(PushOutboxReceipt::default()); + } + Err(RadrootsSdkError::ProductSyncUnsupported { + operation: "sync.push_outbox", + required_feature: "transport profile with Nostr delivery", + }) + } } } + async fn push_outbox_has_no_ready_signed_work( + &self, + request: &PushOutboxRequest, + ) -> Result<bool, RadrootsSdkError> { + request.validate()?; + let now_ms = sdk_now_ms(self.sdk)?; + let summary = self.sdk._outbox.status_summary(now_ms).await?; + Ok(summary.ready_signed_events == 0) + } + pub async fn push_outbox_with_adapter<A>( &self, adapter: &A, @@ -545,11 +573,7 @@ impl<'sdk> SyncClient<'sdk> { publish_now_ms.saturating_add(request.next_attempt_delay_ms), ) .republish_accepted_relays(request.republish_accepted_relays) - .relay_url_policy(request.relay_url_policy.relay_transport_policy()); - let policy = match request.accepted_quorum { - Some(accepted_quorum) => policy.with_accepted_quorum(accepted_quorum), - None => policy, - }; + .relay_url_policy(request.relay_url_policy.nostr_transport_policy()); let publish = publish_claimed_outbox_event( &self.sdk._outbox, &self.sdk._event_store, @@ -594,7 +618,6 @@ impl<'sdk> SyncClient<'sdk> { self, adapter, &claimed, - request.accepted_quorum, request.next_attempt_delay_ms, publish_now_ms, ) @@ -663,12 +686,11 @@ async fn push_proxy_claimed_outbox_event( sync: &SyncClient<'_>, adapter: &RadrootsdProxyPublishAdapter, claimed: &RadrootsOutboxClaimedEvent, - accepted_quorum: Option<usize>, next_attempt_delay_ms: i64, now_ms: i64, ) -> Result<RadrootsRelayPublishReceipt, RadrootsSdkError> { let signed_event = claimed.signed_event.clone().ok_or( - radroots_relay_transport::RadrootsRelayTransportError::MissingSignedOutboxEvent( + radroots_transport_nostr::RadrootsRelayTransportError::MissingSignedOutboxEvent( claimed.outbox_event_id, ), )?; @@ -681,10 +703,14 @@ async fn push_proxy_claimed_outbox_event( now_ms, ) .await?; + let relays = proxy_nostr_relay_targets(claimed) + .iter() + .map(|target| target.endpoint_uri.as_str().to_owned()) + .collect::<Vec<_>>(); let request = RadrootsdProxyPublishRequest { signed_event: signed_event.clone(), - relays: claimed.target_relays.clone(), - delivery_policy: proxy_delivery_policy(claimed.target_relays.len(), accepted_quorum), + delivery_policy: proxy_delivery_policy(sync, claimed, relays.len()).await?, + relays, idempotency_key: Some(proxy_outbox_idempotency_key( claimed.outbox_event_id, claimed.attempt_count, @@ -714,18 +740,50 @@ async fn push_proxy_claimed_outbox_event( } #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] -fn proxy_delivery_policy( +async fn proxy_delivery_policy( + sync: &SyncClient<'_>, + claimed: &RadrootsOutboxClaimedEvent, target_count: usize, - accepted_quorum: Option<usize>, -) -> PublishDeliveryPolicy { - if let Some(quorum) = accepted_quorum { - return PublishDeliveryPolicy::Quorum { quorum }; - } +) -> Result<PublishDeliveryPolicy, RadrootsSdkError> { + let plans = sync + .sdk + ._outbox + .delivery_plans(claimed.outbox_event_id) + .await?; + let plan = plans + .iter() + .find(|plan| { + claimed + .delivery_targets + .iter() + .any(|target| target.delivery_plan_id == plan.delivery_plan_id) + }) + .or_else(|| plans.first()) + .ok_or_else(|| RadrootsSdkError::InvalidRequest { + message: format!( + "outbox event {} has no delivery plan for proxy publish", + claimed.outbox_event_id + ), + })?; + proxy_delivery_policy_from_satisfaction(target_count, &plan.satisfaction_policy) +} + +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +fn proxy_delivery_policy_from_satisfaction( + target_count: usize, + satisfaction_policy: &RadrootsTransportSatisfactionPolicy, +) -> Result<PublishDeliveryPolicy, RadrootsSdkError> { if target_count == 0 { - PublishDeliveryPolicy::Any - } else { - PublishDeliveryPolicy::All - } + return Ok(PublishDeliveryPolicy::Any); + } + let required = satisfaction_policy.required_target_count(target_count)?; + Ok(match satisfaction_policy { + RadrootsTransportSatisfactionPolicy::AnyTarget => PublishDeliveryPolicy::Any, + RadrootsTransportSatisfactionPolicy::AllTargets => PublishDeliveryPolicy::All, + RadrootsTransportSatisfactionPolicy::AtLeast(_) => { + PublishDeliveryPolicy::Quorum { quorum: required } + } + }) } #[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] @@ -745,44 +803,140 @@ async fn complete_proxy_publish_attempt( next_attempt_delay_ms: i64, now_ms: i64, ) -> Result<(), RadrootsSdkError> { - if publish.quorum_met { + let mut completed_target_ids = std::collections::BTreeSet::new(); + for relay in &publish.relays { + if let Some(target) = proxy_nostr_relay_targets(claimed) + .into_iter() + .find(|target| target.endpoint_uri.as_str() == relay.relay_url.as_str()) + { + complete_proxy_delivery_target(sync, claimed, target, relay, now_ms).await?; + completed_target_ids.insert(target.delivery_target_id); + } + } + for target in claimed + .delivery_targets + .iter() + .filter(|target| target.status.is_ready_for_attempt()) + .filter(|target| !completed_target_ids.contains(&target.delivery_target_id)) + { + complete_unmatched_proxy_delivery_target(sync, claimed, target, publish, now_ms).await?; + } + sync.sdk + ._outbox + .complete_publish_attempt( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + "radrootsd proxy publish incomplete", + "radrootsd proxy publish terminal", + now_ms.saturating_add(next_attempt_delay_ms), + now_ms, + ) + .await?; + Ok(()) +} + +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +fn proxy_nostr_relay_targets( + claimed: &RadrootsOutboxClaimedEvent, +) -> Vec<&RadrootsOutboxDeliveryTargetRecord> { + claimed + .delivery_targets + .iter() + .filter(|target| target.transport_kind == RadrootsTransportKind::Nostr) + .filter(|target| target.status.is_ready_for_attempt()) + .collect() +} + +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +async fn complete_proxy_delivery_target( + sync: &SyncClient<'_>, + claimed: &RadrootsOutboxClaimedEvent, + target: &RadrootsOutboxDeliveryTargetRecord, + relay: &RadrootsRelayPublishRelayReceipt, + now_ms: i64, +) -> Result<(), RadrootsSdkError> { + if relay.outcome.counts_toward_quorum() { sync.sdk ._outbox - .set_publish_quorum( + .mark_delivery_target_accepted( claimed.outbox_event_id, claimed.claim_token.as_str(), - 0, + target.delivery_target_id, now_ms, ) .await?; + } else if relay.outcome.is_retryable() { sync.sdk ._outbox - .complete_publish_attempt( + .mark_delivery_target_failed_retryable( claimed.outbox_event_id, claimed.claim_token.as_str(), - "radrootsd proxy publish incomplete", - "radrootsd proxy publish terminal", - now_ms.saturating_add(next_attempt_delay_ms), + target.delivery_target_id, + relay + .outcome + .message + .as_deref() + .unwrap_or("radrootsd proxy publish retryable"), now_ms, ) .await?; - } else if publish.retryable_count > 0 { + } else { + sync.sdk + ._outbox + .mark_delivery_target_failed_terminal( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + target.delivery_target_id, + relay + .outcome + .message + .as_deref() + .unwrap_or("radrootsd proxy publish terminal"), + now_ms, + ) + .await?; + } + Ok(()) +} + +#[cfg(all(feature = "runtime", feature = "radrootsd-proxy"))] +async fn complete_unmatched_proxy_delivery_target( + sync: &SyncClient<'_>, + claimed: &RadrootsOutboxClaimedEvent, + target: &RadrootsOutboxDeliveryTargetRecord, + publish: &RadrootsRelayPublishReceipt, + now_ms: i64, +) -> Result<(), RadrootsSdkError> { + if publish.quorum_met { + sync.sdk + ._outbox + .mark_delivery_target_accepted( + claimed.outbox_event_id, + claimed.claim_token.as_str(), + target.delivery_target_id, + now_ms, + ) + .await?; + } else if publish.retryable_count > 0 + || target.status == RadrootsOutboxDeliveryTargetStatus::FailedRetryable + { sync.sdk ._outbox - .mark_publish_retryable( + .mark_delivery_target_failed_retryable( claimed.outbox_event_id, claimed.claim_token.as_str(), + target.delivery_target_id, "radrootsd proxy publish incomplete", - now_ms.saturating_add(next_attempt_delay_ms), now_ms, ) .await?; } else { sync.sdk ._outbox - .mark_publish_failed_terminal( + .mark_delivery_target_failed_terminal( claimed.outbox_event_id, claimed.claim_token.as_str(), + target.delivery_target_id, "radrootsd proxy publish terminal", now_ms, ) diff --git a/crates/sdk/src/transport.rs b/crates/sdk/src/transport.rs @@ -0,0 +1,494 @@ +use crate::RadrootsSdkError; +use radroots_transport::{ + RadrootsTransportDeliveryReceipt, RadrootsTransportKind, RadrootsTransportTarget, + RadrootsTransportTargetFingerprint, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, +}; +use radroots_transport_nostr::{RadrootsRelayUrl, RadrootsRelayUrlPolicy}; +use serde::ser::SerializeStruct; +use std::collections::BTreeSet; + +pub use radroots_transport::{ + RadrootsTransportDeliveryReceipt as TransportDeliveryReceipt, + RadrootsTransportDeliveryTargetStatus as TransportDeliveryTargetStatus, + RadrootsTransportKind as TransportKind, RadrootsTransportOutcome as TransportOutcome, + RadrootsTransportTargetReceipt as TransportTargetReceipt, +}; + +pub const SDK_TRANSPORT_TARGET_MAX_COUNT: usize = 20; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum PublishMode { + DryRun, + EnqueueOnly, + EnqueueAndPublish, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum SatisfactionPolicy { + NoWait, + AtLeastOneTarget, + AllTargets, + AtLeast { required: u16 }, +} + +impl SatisfactionPolicy { + pub fn at_least(required: u16) -> Result<Self, RadrootsSdkError> { + if required == 0 { + return Err(RadrootsSdkError::InvalidRequest { + message: "satisfaction policy must require at least one target".to_owned(), + }); + } + Ok(Self::AtLeast { required }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum NostrRelayUrlPolicy { + Public, + Localhost, +} + +impl NostrRelayUrlPolicy { + pub(crate) fn nostr_transport_policy(self) -> RadrootsRelayUrlPolicy { + match self { + Self::Public => RadrootsRelayUrlPolicy::Public, + Self::Localhost => RadrootsRelayUrlPolicy::Localhost, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum TargetPolicy { + UseConfiguredProfile, + Explicit(TargetSet), + UseTransportProfile, +} + +impl TargetPolicy { + pub fn configured_profile() -> Self { + Self::UseConfiguredProfile + } + + pub fn explicit(targets: TargetSet) -> Self { + Self::Explicit(targets) + } + + pub fn try_nostr_relays<I, S>( + relays: I, + url_policy: NostrRelayUrlPolicy, + ) -> Result<Self, RadrootsSdkError> + where + I: IntoIterator<Item = S>, + S: AsRef<str>, + { + Ok(Self::Explicit(TargetSet::nostr_relays(relays, url_policy)?)) + } + + pub fn use_transport_profile() -> Self { + Self::UseTransportProfile + } + + #[cfg(any(feature = "signer-adapters", test))] + pub(crate) fn workflow_target_policy(self) -> Self { + self + } +} + +impl serde::Serialize for TargetPolicy { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + match self { + Self::UseConfiguredProfile => { + let mut state = serializer.serialize_struct("TargetPolicy", 1)?; + state.serialize_field("kind", "use_configured_profile")?; + state.end() + } + Self::Explicit(targets) => { + let mut state = serializer.serialize_struct("TargetPolicy", 3)?; + state.serialize_field("kind", "explicit")?; + state.serialize_field("targets", targets.targets())?; + state.serialize_field("canonical_targets", targets.canonical_targets())?; + state.end() + } + Self::UseTransportProfile => { + let mut state = serializer.serialize_struct("TargetPolicy", 1)?; + state.serialize_field("kind", "use_transport_profile")?; + state.end() + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TargetSet { + targets: Vec<RadrootsTransportTarget>, + canonical_targets: Vec<String>, +} + +impl TargetSet { + pub fn new<I, S>(relays: I, policy: NostrRelayUrlPolicy) -> Result<Self, RadrootsSdkError> + where + I: IntoIterator<Item = S>, + S: AsRef<str>, + { + let mut targets = Vec::new(); + let mut seen = BTreeSet::new(); + for relay in relays { + let normalized = normalized_nostr_relay_url(relay.as_ref(), policy)?; + if seen.insert(normalized.clone()) { + targets.push(RadrootsTransportTarget::new( + RadrootsTransportKind::Nostr, + normalized, + )?); + } + } + Self::from_transport_targets(targets) + } + + pub fn nostr_relays<I, S>( + relays: I, + policy: NostrRelayUrlPolicy, + ) -> Result<Self, RadrootsSdkError> + where + I: IntoIterator<Item = S>, + S: AsRef<str>, + { + Self::new(relays, policy) + } + + pub fn transport_targets( + targets: Vec<RadrootsTransportTarget>, + ) -> Result<Self, RadrootsSdkError> { + Self::from_transport_targets(targets) + } + + pub fn targets(&self) -> &[RadrootsTransportTarget] { + self.targets.as_slice() + } + + pub fn canonical_targets(&self) -> &[String] { + self.canonical_targets.as_slice() + } + + pub fn transport_target_set(&self) -> Result<RadrootsTransportTargetSet, RadrootsSdkError> { + Ok(RadrootsTransportTargetSet::new(self.targets.clone())?) + } + + pub fn nostr_relay_urls(&self) -> Vec<String> { + self.targets + .iter() + .filter(|target| target.kind == RadrootsTransportKind::Nostr) + .map(|target| target.uri.as_str().to_owned()) + .collect() + } + + pub fn into_targets(self) -> Vec<RadrootsTransportTarget> { + self.targets + } + + pub fn len(&self) -> usize { + self.targets.len() + } + + pub fn is_empty(&self) -> bool { + self.targets.is_empty() + } + + pub(crate) fn from_normalized_nostr_relays( + relays: Vec<String>, + ) -> Result<Self, RadrootsSdkError> { + let mut targets = Vec::new(); + let mut seen = BTreeSet::new(); + for relay in relays { + if seen.insert(relay.clone()) { + targets.push(RadrootsTransportTarget::new( + RadrootsTransportKind::Nostr, + relay, + )?); + } + } + Self::from_transport_targets(targets) + } + + fn from_transport_targets( + targets: Vec<RadrootsTransportTarget>, + ) -> Result<Self, RadrootsSdkError> { + if targets.is_empty() { + return Err(RadrootsSdkError::empty_target_relays( + "sdk transport target set", + )); + } + if targets.len() > SDK_TRANSPORT_TARGET_MAX_COUNT { + return Err(RadrootsSdkError::relay_target_limit_exceeded( + SDK_TRANSPORT_TARGET_MAX_COUNT, + targets.len(), + )); + } + let canonical_targets = targets + .iter() + .map(|target| { + RadrootsTransportTargetFingerprint::from_target(&target.kind, &target.uri) + .to_string() + }) + .collect::<BTreeSet<_>>() + .into_iter() + .collect(); + Ok(Self { + targets, + canonical_targets, + }) + } +} + +impl serde::Serialize for TargetSet { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("TargetSet", 2)?; + state.serialize_field("targets", self.targets())?; + state.serialize_field("canonical_targets", self.canonical_targets())?; + state.end() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct NostrProfile { + target_set: TargetSet, +} + +impl NostrProfile { + pub fn new<I, S>(relays: I, policy: NostrRelayUrlPolicy) -> Result<Self, RadrootsSdkError> + where + I: IntoIterator<Item = S>, + S: AsRef<str>, + { + Ok(Self { + target_set: TargetSet::nostr_relays(relays, policy)?, + }) + } + + pub fn target_set(&self) -> &TargetSet { + &self.target_set + } + + pub fn relay_urls(&self) -> Vec<String> { + self.target_set.nostr_relay_urls() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct ReticulumPreviewProfile { + endpoint_uri: String, + behavior: ReticulumPreviewBehavior, +} + +impl ReticulumPreviewProfile { + pub fn preview_unavailable() -> Self { + Self { + endpoint_uri: "reticulum:preview".to_owned(), + behavior: ReticulumPreviewBehavior::RejectDeliveryAttempts, + } + } + + pub fn with_behavior(mut self, behavior: ReticulumPreviewBehavior) -> Self { + self.behavior = behavior; + self + } + + pub fn endpoint_uri(&self) -> &str { + self.endpoint_uri.as_str() + } + + pub fn behavior(&self) -> ReticulumPreviewBehavior { + self.behavior + } + + pub fn target_set(&self) -> Result<TargetSet, RadrootsSdkError> { + TargetSet::transport_targets(vec![RadrootsTransportTarget::new( + RadrootsTransportKind::Reticulum, + self.endpoint_uri.as_str(), + )?]) + } +} + +impl Default for ReticulumPreviewProfile { + fn default() -> Self { + Self::preview_unavailable() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum ReticulumPreviewBehavior { + RejectDeliveryAttempts, + DeferDeliveryPlans, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct HybridProfile { + nostr: NostrProfile, + reticulum_preview: ReticulumPreviewProfile, +} + +impl HybridProfile { + pub fn new(nostr: NostrProfile, reticulum_preview: ReticulumPreviewProfile) -> Self { + Self { + nostr, + reticulum_preview, + } + } + + pub fn nostr(&self) -> &NostrProfile { + &self.nostr + } + + pub fn reticulum_preview(&self) -> &ReticulumPreviewProfile { + &self.reticulum_preview + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct ProxyProfile { + endpoint_url: String, +} + +impl ProxyProfile { + pub fn new(endpoint_url: impl Into<String>) -> Self { + Self { + endpoint_url: endpoint_url.into(), + } + } + + pub fn endpoint_url(&self) -> &str { + self.endpoint_url.as_str() + } + + pub(crate) fn target_set(&self) -> Result<TargetSet, RadrootsSdkError> { + TargetSet::transport_targets(vec![RadrootsTransportTarget::new( + RadrootsTransportKind::custom("radrootsd_proxy")?, + self.endpoint_url.as_str(), + )?]) + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case", tag = "kind")] +#[non_exhaustive] +pub enum TransportProfile { + LocalOnly, + Nostr { profile: NostrProfile }, + ReticulumPreview { profile: ReticulumPreviewProfile }, + Hybrid { profile: HybridProfile }, + Proxy { profile: ProxyProfile }, +} + +impl Default for TransportProfile { + fn default() -> Self { + Self::LocalOnly + } +} + +impl TransportProfile { + pub fn local_only() -> Self { + Self::LocalOnly + } + + pub fn nostr(profile: NostrProfile) -> Self { + Self::Nostr { profile } + } + + pub fn reticulum_preview(profile: ReticulumPreviewProfile) -> Self { + Self::ReticulumPreview { profile } + } + + pub fn hybrid(profile: HybridProfile) -> Self { + Self::Hybrid { profile } + } + + pub fn proxy(profile: ProxyProfile) -> Self { + Self::Proxy { profile } + } + + pub fn supports_delegated_target_resolution(&self) -> bool { + matches!(self, Self::Proxy { .. }) + } + + pub(crate) fn transport_profile_id(&self) -> &'static str { + match self { + Self::LocalOnly => "local_only", + Self::Nostr { .. } => "nostr", + Self::ReticulumPreview { .. } => "reticulum_preview", + Self::Hybrid { .. } => "hybrid", + Self::Proxy { .. } => "proxy", + } + } + + pub(crate) fn target_set(&self) -> Result<Option<TargetSet>, RadrootsSdkError> { + match self { + Self::LocalOnly => Ok(None), + Self::Nostr { profile } => Ok(Some(profile.target_set().clone())), + Self::ReticulumPreview { profile } => Ok(Some(profile.target_set()?)), + Self::Hybrid { profile } => { + let mut targets = profile.nostr().target_set().targets().to_vec(); + targets.extend(profile.reticulum_preview().target_set()?.into_targets()); + Ok(Some(TargetSet::transport_targets(targets)?)) + } + Self::Proxy { profile } => Ok(Some(profile.target_set()?)), + } + } + + pub(crate) fn configured_nostr_relay_urls(&self) -> Vec<String> { + match self { + Self::Nostr { profile } => profile.relay_urls(), + Self::Hybrid { profile } => profile.nostr().relay_urls(), + Self::LocalOnly | Self::ReticulumPreview { .. } | Self::Proxy { .. } => Vec::new(), + } + } +} + +impl From<RadrootsTransportDeliveryReceipt> for TransportReceipt { + fn from(value: RadrootsTransportDeliveryReceipt) -> Self { + Self { + request_id: value.request_id, + target_receipts: value.target_receipts, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +pub struct TransportReceipt { + pub request_id: String, + pub target_receipts: Vec<RadrootsTransportTargetReceipt>, +} + +impl TransportReceipt { + pub fn satisfied_target_count(&self) -> usize { + self.target_receipts + .iter() + .filter(|receipt| receipt.status.counts_as_satisfied()) + .count() + } +} + +fn normalized_nostr_relay_url( + value: &str, + policy: NostrRelayUrlPolicy, +) -> Result<String, RadrootsSdkError> { + let relay = RadrootsRelayUrl::parse(value, policy.nostr_transport_policy())?; + Ok(relay.into_string()) +} + +#[cfg(test)] +#[path = "../tests/unit/transport_tests.rs"] +mod tests; diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs @@ -1,8 +1,8 @@ #[cfg(feature = "signer-adapters")] use crate::RadrootsSdkSignRequest; use crate::{ - RadrootsClient, RadrootsSdkError, SdkIdempotencyKey, SdkRelayTargetPolicy, SdkRelayTargetSet, - runtime::sdk_now_ms, + RadrootsClient, RadrootsSdkError, SatisfactionPolicy, SdkIdempotencyKey, TargetPolicy, + TargetSet, runtime::sdk_now_ms, }; use radroots_authority::{RadrootsActorContext, RadrootsEventSigner, sign_authorized_draft}; use radroots_event_store::RadrootsEventIngest; @@ -11,7 +11,11 @@ use radroots_events::{ draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent}, ids::RadrootsEventId, }; -use radroots_outbox::{RadrootsOutboxEnqueueStatus, RadrootsOutboxSignedOperationInput}; +use radroots_outbox::{ + RadrootsOutboxDeliveryPlanInput, RadrootsOutboxEnqueueStatus, + RadrootsOutboxSignedOperationInput, +}; +use radroots_transport::RadrootsTransportSatisfactionPolicy; #[cfg(test)] use sha2::{Digest, Sha256}; @@ -19,7 +23,8 @@ pub(crate) struct SdkWorkflowEnqueueRequest<'a> { pub(crate) operation_kind: &'static str, pub(crate) actor: &'a RadrootsActorContext, pub(crate) frozen_draft: &'a RadrootsFrozenEventDraft, - pub(crate) target_relays: SdkRelayTargetPolicy, + pub(crate) target_relays: TargetPolicy, + pub(crate) satisfaction_policy: SatisfactionPolicy, pub(crate) idempotency_key: Option<SdkIdempotencyKey>, } @@ -37,9 +42,10 @@ pub(crate) async fn enqueue_signed_workflow( request: SdkWorkflowEnqueueRequest<'_>, signer: &dyn RadrootsEventSigner, ) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> { - let target_relays = resolved_target_relays(sdk, &request.target_relays)?; + let delivery_plan = + resolved_delivery_plan(sdk, &request.target_relays, request.satisfaction_policy)?; let signed_event = sign_authorized_draft(request.actor, signer, request.frozen_draft)?; - enqueue_signed_workflow_event(sdk, request, signed_event, target_relays).await + enqueue_signed_workflow_event(sdk, request, signed_event, delivery_plan).await } #[cfg(feature = "signer-adapters")] @@ -47,7 +53,8 @@ pub(crate) async fn enqueue_configured_signed_workflow( sdk: &RadrootsClient, request: SdkWorkflowEnqueueRequest<'_>, ) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> { - let target_relays = resolved_target_relays(sdk, &request.target_relays)?; + let delivery_plan = + resolved_delivery_plan(sdk, &request.target_relays, request.satisfaction_policy)?; let signed_event = sdk .sign_with_configured_signer(RadrootsSdkSignRequest::new( request.operation_kind, @@ -56,14 +63,14 @@ pub(crate) async fn enqueue_configured_signed_workflow( )) .await? .signed_event; - enqueue_signed_workflow_event(sdk, request, signed_event, target_relays).await + enqueue_signed_workflow_event(sdk, request, signed_event, delivery_plan).await } async fn enqueue_signed_workflow_event( sdk: &RadrootsClient, request: SdkWorkflowEnqueueRequest<'_>, signed_event: RadrootsSignedNostrEvent, - target_relays: SdkResolvedRelayTargets, + delivery_plan: SdkResolvedDeliveryPlan, ) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> { let idempotency_key = match request.idempotency_key { Some(idempotency_key) => idempotency_key, @@ -71,22 +78,20 @@ async fn enqueue_signed_workflow_event( request.operation_kind, request.frozen_draft.expected_event_id.as_str(), request.frozen_draft.expected_pubkey.as_str(), - target_relays.canonical_relays.as_slice(), + delivery_plan.canonical_targets.as_slice(), ), }; let observed_at_ms = sdk_now_ms(sdk)?; let signed_event_id = RadrootsEventId::parse(request.frozen_draft.expected_event_id.as_str()) .expect("frozen workflow draft has a valid expected event id"); - let allow_empty_target_relays = target_relays.allow_empty_target_relays; - let target_relay_values = target_relays.relays; + let delivery_plan_value = delivery_plan.delivery_plan; let idempotency_key_for_enqueue = idempotency_key.clone(); let preflight_input = signed_outbox_input( request.operation_kind, request.frozen_draft, signed_event.clone(), - target_relay_values.clone(), + delivery_plan_value.clone(), idempotency_key, - allow_empty_target_relays, false, observed_at_ms, ); @@ -94,7 +99,8 @@ async fn enqueue_signed_workflow_event( ._outbox .preflight_signed_operation_idempotency(&preflight_input) .await?; - let partial_failure_digest_prefix = digest_prefix(preflight.idempotency_digest.as_str()); + let partial_failure_digest_prefix = + digest_prefix(preflight.operation_idempotency_digest.as_str()); let event = event_from_signed(&signed_event); let ingest = RadrootsEventIngest::new(event, observed_at_ms) .with_raw_json(signed_event.raw_json.clone()); @@ -103,9 +109,8 @@ async fn enqueue_signed_workflow_event( request.operation_kind, request.frozen_draft, signed_event, - target_relay_values, + delivery_plan_value, idempotency_key_for_enqueue, - allow_empty_target_relays, ingest_receipt.inserted, observed_at_ms, ); @@ -131,7 +136,8 @@ async fn enqueue_signed_workflow_event( ) } })?; - let idempotency_digest_prefix = digest_prefix(outbox_receipt.idempotency_digest.as_str()); + let idempotency_digest_prefix = + digest_prefix(outbox_receipt.operation_idempotency_digest.as_str()); Ok(SdkWorkflowEnqueueReceipt { signed_event_id, local_event_seq: ingest_receipt.seq, @@ -142,46 +148,66 @@ async fn enqueue_signed_workflow_event( }) } -struct SdkResolvedRelayTargets { - relays: Vec<String>, - canonical_relays: Vec<String>, - allow_empty_target_relays: bool, +struct SdkResolvedDeliveryPlan { + delivery_plan: RadrootsOutboxDeliveryPlanInput, + canonical_targets: Vec<String>, } -fn resolved_target_relays( +fn resolved_delivery_plan( sdk: &RadrootsClient, - target_relays: &SdkRelayTargetPolicy, -) -> Result<SdkResolvedRelayTargets, RadrootsSdkError> { + target_relays: &TargetPolicy, + satisfaction_policy: SatisfactionPolicy, +) -> Result<SdkResolvedDeliveryPlan, RadrootsSdkError> { match target_relays { - SdkRelayTargetPolicy::Explicit(target_relays) => Ok(SdkResolvedRelayTargets { - relays: target_relays.relays().to_vec(), - canonical_relays: target_relays.canonical_relays().to_vec(), - allow_empty_target_relays: false, - }), - SdkRelayTargetPolicy::UseConfiguredRelays => { + TargetPolicy::Explicit(target_relays) => { + delivery_plan_from_target_set("explicit", target_relays.clone(), satisfaction_policy) + } + TargetPolicy::UseConfiguredProfile => { let target_relays = - SdkRelayTargetSet::from_normalized_relays(sdk.relay_urls().to_vec())?; - Ok(SdkResolvedRelayTargets { - relays: target_relays.relays().to_vec(), - canonical_relays: target_relays.canonical_relays().to_vec(), - allow_empty_target_relays: false, - }) + TargetSet::from_normalized_nostr_relays(sdk.configured_nostr_relay_urls())?; + delivery_plan_from_target_set("configured_profile", target_relays, satisfaction_policy) } - SdkRelayTargetPolicy::UsePublishTransport => { - if sdk - .publish_transport() - .supports_delegated_relay_resolution() - { - Ok(SdkResolvedRelayTargets { - relays: Vec::new(), - canonical_relays: Vec::new(), - allow_empty_target_relays: true, - }) - } else { - Err(RadrootsSdkError::empty_target_relays( - "publish transport relay resolution", - )) - } + TargetPolicy::UseTransportProfile => { + let target_set = sdk.transport_profile().target_set()?.ok_or_else(|| { + RadrootsSdkError::empty_target_relays("publish transport profile") + })?; + delivery_plan_from_target_set( + sdk.transport_profile().transport_profile_id(), + target_set, + satisfaction_policy, + ) + } + } +} + +fn delivery_plan_from_target_set( + transport_profile_id: impl Into<String>, + target_set: TargetSet, + satisfaction_policy: SatisfactionPolicy, +) -> Result<SdkResolvedDeliveryPlan, RadrootsSdkError> { + let canonical_targets = target_set.canonical_targets().to_vec(); + let delivery_plan = RadrootsOutboxDeliveryPlanInput::new( + transport_profile_id, + 1, + transport_satisfaction_policy(satisfaction_policy), + target_set.into_targets(), + ); + Ok(SdkResolvedDeliveryPlan { + delivery_plan, + canonical_targets, + }) +} + +fn transport_satisfaction_policy( + satisfaction_policy: SatisfactionPolicy, +) -> RadrootsTransportSatisfactionPolicy { + match satisfaction_policy { + SatisfactionPolicy::NoWait | SatisfactionPolicy::AllTargets => { + RadrootsTransportSatisfactionPolicy::AllTargets + } + SatisfactionPolicy::AtLeastOneTarget => RadrootsTransportSatisfactionPolicy::AnyTarget, + SatisfactionPolicy::AtLeast { required } => { + RadrootsTransportSatisfactionPolicy::AtLeast(required) } } } @@ -226,27 +252,21 @@ fn signed_outbox_input( operation_kind: &'static str, frozen_draft: &RadrootsFrozenEventDraft, signed_event: RadrootsSignedNostrEvent, - target_relays: Vec<String>, + delivery_plan: RadrootsOutboxDeliveryPlanInput, idempotency_key: SdkIdempotencyKey, - allow_empty_target_relays: bool, event_store_inserted: bool, observed_at_ms: i64, ) -> RadrootsOutboxSignedOperationInput { - let input = RadrootsOutboxSignedOperationInput::new( + RadrootsOutboxSignedOperationInput::new( operation_kind, frozen_draft.clone(), signed_event, - target_relays, + delivery_plan, event_store_inserted, observed_at_ms, observed_at_ms, ) - .with_idempotency_key(idempotency_key.into_string()); - if allow_empty_target_relays { - input.allow_empty_target_relays() - } else { - input - } + .with_idempotency_key(idempotency_key.into_string()) } fn event_from_signed(signed_event: &RadrootsSignedNostrEvent) -> RadrootsNostrEvent { diff --git a/crates/sdk/tests/dvm_runtime.rs b/crates/sdk/tests/dvm_runtime.rs @@ -29,11 +29,10 @@ use radroots_nostr::prelude::{ use radroots_outbox::RadrootsOutbox; use radroots_sdk::{ DVM_TRADE_TRANSITION_PROOF_REQUEST_OPERATION_KIND, DvmTradeTransitionProofEnqueueRequest, - DvmTradeTransitionProofPrepareRequest, DvmValidationReceiptIngestRequest, RadrootsClient, - RadrootsSdkError, RadrootsSdkStorageConfig, RadrootsSdkTimestamp, + DvmTradeTransitionProofPrepareRequest, DvmValidationReceiptIngestRequest, NostrRelayUrlPolicy, + RadrootsClient, RadrootsSdkError, RadrootsSdkStorageConfig, RadrootsSdkTimestamp, RadrootsTradeInventoryBinWitnessDto, RadrootsTradeValidationTrustState, SdkMutationState, - SdkRelayTargetPolicy, SdkRelayUrlPolicy, TradeStatusKind, TradeStatusNextActionKind, - TradeStatusRequest, + TargetPolicy, TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest, }; #[cfg(feature = "signer-adapters")] use radroots_sdk::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider}; @@ -235,7 +234,7 @@ async fn dvm_configured_enqueue_reports_prepare_and_target_errors_without_mutati deterministic_event_id("request-event"), deterministic_event_id("decision-event"), inventory_bins(), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ); let error = sdk .dvm() @@ -412,7 +411,7 @@ async fn dvm_trade_transition_proof_request_reports_prepare_and_target_errors_wi deterministic_event_id("request-event"), deterministic_event_id("decision-event"), inventory_bins(), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ); let error = sdk .dvm() @@ -664,8 +663,8 @@ fn service_actor(signer: &FixtureSigner) -> RadrootsActorContext { .expect("service actor") } -fn explicit_relays() -> SdkRelayTargetPolicy { - SdkRelayTargetPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public).expect("relay targets") +fn explicit_relays() -> TargetPolicy { + TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("relay targets") } fn inventory_bins() -> Vec<RadrootsTradeInventoryBinWitnessDto> { diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs @@ -12,18 +12,18 @@ use radroots_events::{ kinds::{KIND_FARM, KIND_PROFILE}, }; use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState}; -use radroots_relay_transport::RadrootsMockRelayPublishAdapter; use radroots_sdk::{ FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmPreparePublishRequest, FarmPrivateLocationClearRequest, FarmPrivateLocationInput, FarmPrivateLocationLookupCandidate, FarmPrivateLocationLookupReceipt, FarmPrivateLocationReceipt, FarmPrivateLocationSetRequest, FarmPrivateLocationSetResult, FarmPrivateLocationUpsertRequest, Geocoder, - GeocoderLocalityQuery, PushOutboxEventState, PushOutboxRelayOutcomeKind, PushOutboxRequest, - RadrootsClient, RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, - RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, SdkExactLocation, SdkIdempotencyKey, - SdkMutationState, SdkPublicLocality, SdkRelayTargetPolicy, SdkRelayTargetSet, - SdkRelayUrlPolicy, StorageStatusRequest, + GeocoderLocalityQuery, NostrProfile, NostrRelayUrlPolicy, PushOutboxEventState, + PushOutboxRelayOutcomeKind, PushOutboxRequest, RadrootsClient, RadrootsSdkError, + RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, RadrootsSdkRecoveryAction, + RadrootsSdkTimestamp, SdkExactLocation, SdkIdempotencyKey, SdkMutationState, SdkPublicLocality, + StorageStatusRequest, TargetPolicy, TargetSet, TransportProfile, }; +use radroots_transport_nostr::RadrootsMockRelayPublishAdapter; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; #[path = "support/serializer_failure.rs"] @@ -138,8 +138,11 @@ async fn directory_sdk_with_relays(relays: &[&str]) -> (tempfile::TempDir, Radro let mut builder = RadrootsClient::builder() .directory_storage(tempdir.path().join("sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)); - for relay in relays { - builder = builder.relay_url(*relay); + if !relays.is_empty() { + builder = builder.transport_profile(TransportProfile::nostr( + NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + )); } let sdk = builder.build().await.expect("sdk"); (tempdir, sdk) @@ -606,7 +609,7 @@ async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_prof let request = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_B_D_TAG, "North Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("farm-idem-b") .expect("idempotency key"); @@ -672,7 +675,7 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation() let request = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_C_D_TAG, "North Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ); let error = sdk .farms() @@ -718,15 +721,15 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() { let first = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_D_D_TAG, "North Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY_B, RELAY, RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY_B, RELAY, RELAY], NostrRelayUrlPolicy::Public) .expect("first target relays"); let second = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_D_D_TAG, "North Farm"), - SdkRelayTargetPolicy::explicit( - SdkRelayTargetSet::new([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + TargetPolicy::explicit( + TargetSet::new([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("second target relays"), ), ); @@ -757,11 +760,11 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() { .await .expect("outbox"); let relay_urls = outbox - .relay_statuses(first_receipt.outbox_event_id) + .delivery_targets(first_receipt.outbox_event_id) .await - .expect("relay statuses") + .expect("delivery targets") .into_iter() - .map(|status| status.relay_url) + .map(|target| target.endpoint_uri.to_string()) .collect::<Vec<_>>(); assert_eq!(relay_urls, vec![RELAY_B.to_owned(), RELAY.to_owned()]); } @@ -772,9 +775,9 @@ async fn farm_enqueue_publish_pushes_queued_event_with_mock_relay_sync() { let enqueue_request = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_D_D_TAG, "Sync Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let enqueue_receipt = sdk .farms() @@ -829,7 +832,7 @@ async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mut let first = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_E_D_TAG, "North Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("farm-idem-e") .expect("idempotency key"); @@ -864,7 +867,7 @@ async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mut let second = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_F_D_TAG, "Changed Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("farm-idem-e") .expect("idempotency key"); @@ -938,9 +941,9 @@ async fn farm_runtime_dtos_serialize_deterministically() { let enqueue_request = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_B_D_TAG, "Queued Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("relay targets") .with_idempotency_key( SdkIdempotencyKey::new("farm-serialized-idempotency").expect("idempotency"), @@ -970,8 +973,22 @@ async fn farm_runtime_dtos_serialize_deterministically() { }, "target_relays": { "kind": "explicit", - "relays": [RELAY, RELAY_B], - "canonical_relays": [RELAY_B, RELAY] + "targets": [ + { + "kind": "Nostr", + "uri": RELAY, + "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + }, + { + "kind": "Nostr", + "uri": RELAY_B, + "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05" + } + ], + "canonical_targets": [ + "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05", + "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + ] }, "idempotency_key": { "value": "<redacted>", "len": 27 }, "created_at": 1_700_000_123 @@ -986,7 +1003,7 @@ async fn farm_runtime_dtos_serialize_deterministically() { let try_key_request = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_C_D_TAG, "Queued Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("farm-serialized-try-key") .expect("try idempotency key"); diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -19,9 +19,9 @@ use radroots_events::{ use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState}; use radroots_sdk::{ LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest, ListingPreparePublishRequest, - RadrootsClient, RadrootsSdkError, RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, - SdkIdempotencyKey, SdkMutationState, SdkRelayTargetPolicy, SdkRelayTargetSet, - SdkRelayUrlPolicy, + NostrProfile, NostrRelayUrlPolicy, RadrootsClient, RadrootsSdkError, RadrootsSdkRecoveryAction, + RadrootsSdkTimestamp, SdkIdempotencyKey, SdkMutationState, TargetPolicy, TargetSet, + TransportProfile, }; use radroots_trade::listing::RadrootsListingDraftDocumentV1; @@ -171,8 +171,11 @@ async fn directory_sdk_with_relays(relays: &[&str]) -> (tempfile::TempDir, Radro let mut builder = RadrootsClient::builder() .directory_storage(tempdir.path().join("sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)); - for relay in relays { - builder = builder.relay_url(*relay); + if !relays.is_empty() { + builder = builder.transport_profile(TransportProfile::nostr( + NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + )); } let sdk = builder.build().await.expect("sdk"); (tempdir, sdk) @@ -238,7 +241,7 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish() let request = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_B_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("idem-b") .expect("idempotency key"); @@ -290,24 +293,24 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish() } #[tokio::test] -async fn enqueue_publish_use_configured_relays_rejects_empty_builder_relays() { +async fn enqueue_publish_use_configured_profile_rejects_empty_transport_targets() { let (_tempdir, sdk) = directory_sdk_with_relays(&[]).await; let request = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ); let error = sdk .listings() .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER)) .await - .expect_err("empty configured relays"); + .expect_err("empty configured profile"); assert!(matches!( error, RadrootsSdkError::EmptyTargetRelays { operation } - if operation == "sdk relay target set" + if operation == "sdk transport target set" )); } @@ -327,7 +330,7 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() { .enqueue_prepared_publish_with_explicit_signer( &actor, prepared.clone(), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, None, &FixtureSigner::new(SELLER), ) @@ -366,7 +369,7 @@ async fn enqueue_receipt_debug_omits_signed_event_payload_material() { let request = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("debug-secret-idempotency") .expect("idempotency key"); @@ -425,9 +428,9 @@ async fn listing_runtime_dtos_serialize_deterministically() { let enqueue_request = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_B_D_TAG, "Queued Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("relay targets") .with_idempotency_key(SdkIdempotencyKey::new("serialized-idempotency").expect("idempotency")) .with_created_at(created_at); @@ -436,12 +439,26 @@ async fn listing_runtime_dtos_serialize_deterministically() { assert_eq!(enqueue_json["target_relays"]["kind"], "explicit"); assert_eq!( - enqueue_json["target_relays"]["relays"], - serde_json::json!([RELAY, RELAY_B]) + enqueue_json["target_relays"]["targets"], + serde_json::json!([ + { + "kind": "Nostr", + "uri": RELAY, + "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + }, + { + "kind": "Nostr", + "uri": RELAY_B, + "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05" + } + ]) ); assert_eq!( - enqueue_json["target_relays"]["canonical_relays"], - serde_json::json!([RELAY_B, RELAY]) + enqueue_json["target_relays"]["canonical_targets"], + serde_json::json!([ + "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05", + "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + ]) ); assert_eq!( enqueue_json["idempotency_key"], @@ -452,7 +469,7 @@ async fn listing_runtime_dtos_serialize_deterministically() { let try_key_request = ListingEnqueuePublishRequest::from_document( actor(), RadrootsListingDraftDocumentV1::new(listing(LISTING_C_D_TAG, "Queued Coffee")), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("listing-serialized-try-key") .expect("try idempotency key"); @@ -489,7 +506,7 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() { .enqueue_prepared_publish_with_explicit_signer( &prepared_actor, prepared_plan, - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, None, &FixtureSigner::new(SELLER), ) @@ -500,7 +517,7 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() { let convenience_request = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_H_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ); let convenience_receipt = convenience_sdk .listings() @@ -526,7 +543,7 @@ async fn enqueue_prepared_publish_returns_structured_actor_errors() { .enqueue_prepared_publish_with_explicit_signer( &non_seller_actor(), prepared, - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, None, &FixtureSigner::new(SELLER), ) @@ -552,7 +569,7 @@ async fn enqueue_prepared_publish_returns_sanitized_signer_errors() { .enqueue_prepared_publish_with_explicit_signer( &actor, prepared, - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, None, &FixtureSigner::new(OTHER), ) @@ -576,7 +593,7 @@ async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() { let request = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_K_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .with_created_at(created_at); @@ -622,7 +639,7 @@ async fn enqueue_publish_returns_sanitized_signer_errors() { let request = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_C_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ); let error = sdk .listings() @@ -645,7 +662,7 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation let first = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_D_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("idem-d") .expect("idempotency key"); @@ -680,7 +697,7 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation let second = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_E_D_TAG, "Changed"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("idem-d") .expect("idempotency key"); @@ -724,15 +741,15 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() { let first = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_F_D_TAG, "Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY_B, RELAY, RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY_B, RELAY, RELAY], NostrRelayUrlPolicy::Public) .expect("first target relays"); let second = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_F_D_TAG, "Coffee"), - SdkRelayTargetPolicy::explicit( - SdkRelayTargetSet::new([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + TargetPolicy::explicit( + TargetSet::new([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("second target relays"), ), ); @@ -763,11 +780,11 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() { .await .expect("outbox"); let relay_urls = outbox - .relay_statuses(first_receipt.outbox_event_id) + .delivery_targets(first_receipt.outbox_event_id) .await - .expect("relay statuses") + .expect("delivery targets") .into_iter() - .map(|status| status.relay_url) + .map(|target| target.endpoint_uri.to_string()) .collect::<Vec<_>>(); assert_eq!(relay_urls, vec![RELAY_B.to_owned(), RELAY.to_owned()]); } diff --git a/crates/sdk/tests/market_runtime.rs b/crates/sdk/tests/market_runtime.rs @@ -22,8 +22,9 @@ use radroots_nostr::prelude::{ RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft, }; use radroots_sdk::{ - ListingEnqueuePublishRequest, MarketSearchRequest, RadrootsClient, RadrootsSdkError, - RadrootsSdkTimestamp, SdkRelayTargetPolicy, SdkRelayUrlPolicy, SyncProjectionRefreshRequest, + ListingEnqueuePublishRequest, MarketSearchRequest, NostrProfile, NostrRelayUrlPolicy, + RadrootsClient, RadrootsSdkError, RadrootsSdkTimestamp, SyncProjectionRefreshRequest, + TargetPolicy, TransportProfile, }; const SELLER_SECRET_KEY_HEX: &str = @@ -134,7 +135,9 @@ async fn directory_sdk() -> (tempfile::TempDir, RadrootsClient) { let sdk = RadrootsClient::builder() .directory_storage(tempdir.path().join("sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) - .relay_url(RELAY) + .transport_profile(TransportProfile::nostr( + NostrProfile::new([RELAY], NostrRelayUrlPolicy::Public).expect("Nostr profile"), + )) .build() .await .expect("sdk"); @@ -147,9 +150,9 @@ async fn market_search_refreshes_local_projection_and_reads_fts() { let publish = ListingEnqueuePublishRequest::new( seller_actor(), listing("Blueberries"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let receipt = sdk .listings() diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -39,29 +39,24 @@ use radroots_nostr::prelude::{ }; use radroots_outbox::RadrootsOutbox; #[cfg(feature = "relay-runtime")] -use radroots_relay_transport::{ - RadrootsMockRelayFetchAdapter, RadrootsRelayFetchAdapter, RadrootsRelayFetchItem, - RadrootsRelayFetchRequest, RadrootsRelayTransportError, -}; -use radroots_sdk::{ - AckPolicy, PublishMode, RadrootsClient, RadrootsSdkError, RadrootsSdkRecoveryAction, - RadrootsSdkTimestamp, RadrootsTradeValidationTrustPolicy, RadrootsTradeValidationTrustState, - RelayResolutionPolicy, SdkMutationState, SdkRelayTargetSet, SdkRelayUrlPolicy, - SdkTradeStatusIssue, SdkTradeStatusIssueKind, SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT, - TRADE_STATUS_MAX_LIMIT, TRADE_STATUS_WATCH_MAX_CAPACITY, TRADE_SUBMIT_OPERATION_KIND, - TradeAcceptRequest, TradeCancelRequest, TradeDeclineRequest, TradeEvidenceIngestRequest, - TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest, - TradeRequestEvidenceIngestRequest, TradeRevisionDecisionRequest, TradeRevisionProposalRequest, - TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest, TradeStatusWatchCancelState, - TradeStatusWatchRequest, -}; -#[cfg(feature = "relay-runtime")] use radroots_sdk::{ DvmValidationReceiptIngestRequest, TradeEvidenceQueryBranchKind, TradeResyncRelayOutcomeKind, TradeResyncRelayTransportOutcomeKind, TradeResyncRequest, TradeSellerInboxRequest, TradeValidationReceiptInspectRequest, TradeValidationReceiptListRequest, TradeValidationReceiptVerifyRequest, }; +use radroots_sdk::{ + NostrProfile, NostrRelayUrlPolicy, PublishMode, RadrootsClient, RadrootsSdkError, + RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, RadrootsTradeValidationTrustPolicy, + RadrootsTradeValidationTrustState, SatisfactionPolicy, SdkMutationState, SdkTradeStatusIssue, + SdkTradeStatusIssueKind, SdkTradeStatusSource, TRADE_STATUS_DEFAULT_LIMIT, + TRADE_STATUS_MAX_LIMIT, TRADE_STATUS_WATCH_MAX_CAPACITY, TRADE_SUBMIT_OPERATION_KIND, + TargetPolicy, TargetSet, TradeAcceptRequest, TradeCancelRequest, TradeDeclineRequest, + TradeEvidenceIngestRequest, TradeEvidenceMode, TradeMutationOutcome, TradeProposeRequest, + TradeRequestEvidenceIngestRequest, TradeRevisionDecisionRequest, TradeRevisionProposalRequest, + TradeStatusKind, TradeStatusNextActionKind, TradeStatusRequest, TradeStatusWatchCancelState, + TradeStatusWatchRequest, TransportProfile, +}; use radroots_sdk::{PrivacyPreflightConfirmation, PrivacyPreflightStatus, ProductSensitivityField}; #[cfg(all(feature = "signer-adapters", feature = "local-signer"))] use radroots_sdk::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider}; @@ -74,6 +69,11 @@ use radroots_trade::validation_receipt::{ RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, validation_receipt_event_build, validation_receipt_public_values_hash_hex, }; +#[cfg(feature = "relay-runtime")] +use radroots_transport_nostr::{ + RadrootsMockRelayFetchAdapter, RadrootsRelayFetchAdapter, RadrootsRelayFetchItem, + RadrootsRelayFetchRequest, RadrootsRelayTransportError, +}; use serde::Serialize; use serde::ser::{self, SerializeStruct}; @@ -470,8 +470,11 @@ async fn directory_sdk_and_store_with_relays( let mut builder = RadrootsClient::builder() .directory_storage(tempdir.path().join("sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)); - for relay in relays { - builder = builder.relay_url(*relay); + if !relays.is_empty() { + builder = builder.transport_profile(TransportProfile::nostr( + NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + )); } let sdk = builder.build().await.expect("sdk"); let store = @@ -500,8 +503,11 @@ async fn directory_sdk_with_signer_and_relays( .signer_provider(RadrootsSdkSignerProvider::LocalKey( RadrootsSdkLocalKeySigner::new(signer_keys).expect("local signer"), )); - for relay in relays { - builder = builder.relay_url(*relay); + if !relays.is_empty() { + builder = builder.transport_profile(TransportProfile::nostr( + NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + )); } builder.build().await.expect("sdk") } @@ -556,9 +562,9 @@ fn listing_event_ptr() -> RadrootsNostrEventPtr { } } -fn explicit_trade_relays() -> RelayResolutionPolicy { - RelayResolutionPolicy::explicit( - SdkRelayTargetSet::new([RELAY], SdkRelayUrlPolicy::Public).expect("target relays"), +fn explicit_trade_relays() -> TargetPolicy { + TargetPolicy::explicit( + TargetSet::new([RELAY], NostrRelayUrlPolicy::Public).expect("target relays"), ) } @@ -641,7 +647,7 @@ fn order_request(raw_order_id: &str) -> RadrootsOrderRequest { fn trade_propose_request( raw_order_id: &str, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> TradeProposeRequest { let order = order_request(raw_order_id); TradeProposeRequest::new( @@ -843,11 +849,11 @@ async fn order_submit_enqueue_stores_event_queues_outbox_and_status_sees_request buyer_actor(), listing_event_ptr(), order, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays") .try_with_idempotency_key("order-submit-enqueue-idempotency") .expect("idempotency key"); @@ -978,7 +984,7 @@ async fn trade_product_clients_propose_inbox_accept_status_and_resync() { trade_propose_request( "trade-product-facade-flow", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-facade-propose") .expect("propose idempotency"), @@ -1049,7 +1055,7 @@ async fn trade_product_clients_propose_inbox_accept_status_and_resync() { }], explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .try_with_idempotency_key("trade-product-facade-accept") @@ -1136,7 +1142,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() { trade_propose_request( "trade-product-committed-resync", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-committed-resync-propose") .expect("propose idempotency"), @@ -1185,7 +1191,7 @@ async fn trade_product_clients_resync_committed_after_rhi_validation_receipt() { }], explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .try_with_idempotency_key("trade-product-committed-resync-accept") @@ -1444,7 +1450,7 @@ async fn trade_product_accept_resync_before_mutation_imports_relay_visible_reque trade_propose_request( "trade-product-resync-before-accept", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-resync-before-accept-propose") .expect("propose idempotency"), @@ -1479,7 +1485,7 @@ async fn trade_product_accept_resync_before_mutation_imports_relay_visible_reque }], explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::ResyncBeforeMutation, ) .try_with_idempotency_key("trade-product-resync-before-accept") @@ -1566,7 +1572,7 @@ async fn trade_product_revision_status_resync_imports_pending_revision_proposal( trade_propose_request( "trade-product-resync-before-revision-decision", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-resync-before-revision-decision-propose") .expect("propose idempotency"), @@ -1612,7 +1618,7 @@ async fn trade_product_revision_status_resync_imports_pending_revision_proposal( "increase quantity", explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .with_privacy_confirmation(public_note_confirmation()) @@ -1694,7 +1700,7 @@ async fn trade_product_accept_local_only_does_not_fetch_relay_evidence() { trade_propose_request( "trade-product-local-only-no-fetch", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-local-only-no-fetch-propose") .expect("propose idempotency"), @@ -1716,7 +1722,7 @@ async fn trade_product_accept_local_only_does_not_fetch_relay_evidence() { }], explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ), &adapter, @@ -1763,7 +1769,7 @@ async fn trade_product_accept_require_explicit_evidence_ingests_supplied_request trade_propose_request( "trade-product-explicit-accept", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-explicit-accept-propose") .expect("propose idempotency"), @@ -1786,7 +1792,7 @@ async fn trade_product_accept_require_explicit_evidence_ingests_supplied_request }], explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::require_explicit_evidence([ TradeEvidenceIngestRequest::new(request_event), ]), @@ -1839,7 +1845,7 @@ async fn trade_product_accept_require_explicit_evidence_rejects_empty_evidence() trade_propose_request( "trade-product-empty-explicit-accept", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-empty-explicit-accept-propose") .expect("propose idempotency"), @@ -1859,7 +1865,7 @@ async fn trade_product_accept_require_explicit_evidence_rejects_empty_evidence() }], explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::require_explicit_evidence(Vec::<TradeEvidenceIngestRequest>::new()), )) .await @@ -2707,7 +2713,7 @@ async fn trade_product_propose_idempotency_replays_same_payload_and_conflicts_di let request = trade_propose_request( "trade-product-idempotent", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-idempotent-key") .expect("idempotency"); @@ -2762,7 +2768,7 @@ async fn trade_product_propose_idempotency_replays_same_payload_and_conflicts_di trade_propose_request( "trade-product-idempotent-conflict", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-idempotent-key") .expect("conflict idempotency"), @@ -2810,7 +2816,7 @@ async fn trade_product_propose_requires_public_note_privacy_confirmation() { trade_propose_request( "trade-product-propose-public-note", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_public_note("please leave at the community table"), ) @@ -2866,7 +2872,7 @@ async fn trade_product_propose_publishes_public_note_after_confirmation() { trade_propose_request( "trade-product-propose-public-note-confirmed", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_public_note("please leave at the community table") .with_privacy_confirmation(public_note_confirmation()) @@ -2909,7 +2915,7 @@ async fn trade_product_propose_blocks_sensitive_fulfillment_note_even_when_confi trade_propose_request( "trade-product-propose-sensitive-note", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_public_note("pickup address is 123 Farm Lane") .with_privacy_confirmation(public_note_confirmation()), @@ -2956,7 +2962,7 @@ async fn trade_product_decline_requires_public_reason_privacy_confirmation() { trade_propose_request( "trade-product-privacy-decline", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-privacy-decline-propose") .expect("propose idempotency"), @@ -2975,7 +2981,7 @@ async fn trade_product_decline_requires_public_reason_privacy_confirmation() { "sold elsewhere", explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, )) .await @@ -3024,7 +3030,7 @@ async fn trade_product_decline_requires_public_reason_privacy_confirmation() { "sold elsewhere", explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .with_privacy_confirmation(public_note_confirmation()) @@ -3060,7 +3066,7 @@ async fn trade_product_cancel_blocks_sensitive_fulfillment_reason_before_mutatio trade_propose_request( "trade-product-privacy-cancel", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-privacy-cancel-propose") .expect("propose idempotency"), @@ -3079,7 +3085,7 @@ async fn trade_product_cancel_blocks_sensitive_fulfillment_reason_before_mutatio "pickup address is 123 Farm Lane", explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .with_privacy_confirmation(public_note_confirmation()), @@ -3126,7 +3132,7 @@ async fn trade_product_cancel_enqueues_with_locator_and_updates_status() { trade_propose_request( "trade-product-cancel", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-cancel-propose") .expect("propose idempotency"), @@ -3146,7 +3152,7 @@ async fn trade_product_cancel_enqueues_with_locator_and_updates_status() { "changed plan", explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .with_privacy_confirmation(public_note_confirmation()) @@ -3191,7 +3197,7 @@ async fn trade_product_revision_lifecycle_uses_locator_and_updates_status() { trade_propose_request( "trade-product-revision", PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trade-product-revision-propose") .expect("propose idempotency"), @@ -3218,7 +3224,7 @@ async fn trade_product_revision_lifecycle_uses_locator_and_updates_status() { "increase quantity", explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .with_privacy_confirmation(public_note_confirmation()) @@ -3252,7 +3258,7 @@ async fn trade_product_revision_lifecycle_uses_locator_and_updates_status() { RadrootsOrderRevisionOutcome::Accepted, explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, ) .try_with_idempotency_key("trade-product-revision-decision") @@ -3285,7 +3291,7 @@ async fn trade_product_propose_dry_run_returns_plan_without_local_side_effects() .propose_trade(trade_propose_request( "trade-product-dry-run", PublishMode::DryRun, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await .expect("dry-run proposal"); @@ -3326,11 +3332,11 @@ async fn order_submit_enqueue_returns_sanitized_signer_errors_before_mutation() buyer_actor(), listing_event_ptr(), order_request("order-submit-wrong-signer"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let error = sdk @@ -3376,22 +3382,22 @@ async fn order_submit_enqueue_derives_order_independent_idempotency_key() { buyer_actor(), listing_event_ptr(), order_request("order-submit-idempotent"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY_B, RELAY, RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY_B, RELAY, RELAY], NostrRelayUrlPolicy::Public) .expect("first target relays"); let second = TradeSubmitEnqueueRequest::new( buyer_actor(), listing_event_ptr(), order_request("order-submit-idempotent"), - RelayResolutionPolicy::explicit( - SdkRelayTargetSet::new([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + TargetPolicy::explicit( + TargetSet::new([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("second target relays"), ), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ); let first_receipt = sdk @@ -3446,11 +3452,11 @@ async fn order_submit_enqueue_derives_order_independent_idempotency_key() { .await .expect("outbox"); let relay_urls = outbox - .relay_statuses(first_receipt.outbox_event_id) + .delivery_targets(first_receipt.outbox_event_id) .await - .expect("relay statuses") + .expect("delivery targets") .into_iter() - .map(|status| status.relay_url) + .map(|target| target.endpoint_uri.to_string()) .collect::<Vec<_>>(); assert_eq!(relay_urls, vec![RELAY_B.to_owned(), RELAY.to_owned()]); } @@ -3463,11 +3469,11 @@ async fn order_submit_enqueue_pushes_queued_event_with_mock_relay_sync() { buyer_actor(), listing_event_ptr(), order_request("order-submit-sync"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let enqueue_receipt = sdk .trades() @@ -3527,11 +3533,11 @@ async fn order_submit_enqueue_reports_partial_local_mutation_after_outbox_confli buyer_actor(), listing_event_ptr(), order_request("order-submit-conflict-a"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("first target relays") .try_with_idempotency_key("order-submit-conflict-idempotency") .expect("first idempotency key"); @@ -3544,11 +3550,11 @@ async fn order_submit_enqueue_reports_partial_local_mutation_after_outbox_confli buyer_actor(), listing_event_ptr(), order_request("order-submit-conflict-b"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("second target relays") .try_with_idempotency_key("order-submit-conflict-idempotency") .expect("second idempotency key"); @@ -3636,11 +3642,11 @@ async fn order_submit_runtime_dtos_serialize_deterministically() { buyer_actor(), listing_event_ptr(), order_request("order-submit-serialized-enqueue"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("relay targets") .with_idempotency_key( SdkIdempotencyKey::new("order-serialized-idempotency").expect("idempotency"), @@ -3653,8 +3659,22 @@ async fn order_submit_runtime_dtos_serialize_deterministically() { enqueue_json["target_relays"], serde_json::json!({ "kind": "explicit", - "relays": [RELAY, RELAY_B], - "canonical_relays": [RELAY_B, RELAY] + "targets": [ + { + "kind": "Nostr", + "uri": RELAY, + "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + }, + { + "kind": "Nostr", + "uri": RELAY_B, + "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05" + } + ], + "canonical_targets": [ + "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05", + "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + ] }) ); assert_eq!( @@ -3672,9 +3692,9 @@ async fn order_submit_runtime_dtos_serialize_deterministically() { buyer_actor(), listing_event_ptr(), order_request("order-submit-try-idempotency"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("order-submit-try-key") .expect("try idempotency key"); @@ -4300,10 +4320,10 @@ async fn order_request_evidence_ingest_stores_request_and_enables_decision_enque .enqueue_prepared_decision_with_explicit_signer( &actor, plan, - RelayResolutionPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -4643,11 +4663,11 @@ async fn order_decision_runtime_dtos_serialize_deterministically() { seller_actor(), request_event_ptr(&request_event), order_decision("order-decision-serialized-enqueue"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("target relays") .with_idempotency_key( SdkIdempotencyKey::new("order-decision-serialized-idempotency").expect("idempotency"), @@ -4660,8 +4680,22 @@ async fn order_decision_runtime_dtos_serialize_deterministically() { enqueue_json["target_relays"], serde_json::json!({ "kind": "explicit", - "relays": [RELAY, RELAY_B], - "canonical_relays": [RELAY_B, RELAY] + "targets": [ + { + "kind": "Nostr", + "uri": RELAY, + "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + }, + { + "kind": "Nostr", + "uri": RELAY_B, + "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05" + } + ], + "canonical_targets": [ + "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05", + "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + ] }) ); assert_eq!( @@ -4679,9 +4713,9 @@ async fn order_decision_runtime_dtos_serialize_deterministically() { seller_actor(), request_event_ptr(&request_event), order_decision("order-decision-try-idempotency"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("order-decision-try-key") .expect("try idempotency key"); @@ -4832,11 +4866,11 @@ async fn order_revision_and_cancellation_dtos_serialize_deterministically() { root_event.clone(), previous_event.clone(), proposal.clone(), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("proposal relays") .with_idempotency_key(SdkIdempotencyKey::new("order-revision-proposal-dto").expect("key")) .with_created_at(created_at); @@ -4847,8 +4881,22 @@ async fn order_revision_and_cancellation_dtos_serialize_deterministically() { proposal_enqueue_json["target_relays"], serde_json::json!({ "kind": "explicit", - "relays": [RELAY, RELAY_B], - "canonical_relays": [RELAY_B, RELAY] + "targets": [ + { + "kind": "Nostr", + "uri": RELAY, + "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + }, + { + "kind": "Nostr", + "uri": RELAY_B, + "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05" + } + ], + "canonical_targets": [ + "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05", + "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af" + ] }) ); assert_eq!( @@ -4862,9 +4910,9 @@ async fn order_revision_and_cancellation_dtos_serialize_deterministically() { root_event.clone(), previous_event.clone(), proposal.clone(), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("order-revision-proposal-try") .expect("proposal try key"); @@ -4905,11 +4953,11 @@ async fn order_revision_and_cancellation_dtos_serialize_deterministically() { root_event.clone(), previous_event.clone(), revision_decision, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("decision relays") .with_idempotency_key( SdkIdempotencyKey::new("order-revision-decision-dto").expect("decision idempotency"), @@ -4934,9 +4982,9 @@ async fn order_revision_and_cancellation_dtos_serialize_deterministically() { &previous_event_id, RadrootsOrderRevisionOutcome::Accepted, ), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("order-revision-decision-try") .expect("decision try key"); @@ -4966,11 +5014,11 @@ async fn order_revision_and_cancellation_dtos_serialize_deterministically() { root_event.clone(), previous_event.clone(), cancellation, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public) .expect("cancellation relays") .with_idempotency_key( SdkIdempotencyKey::new("order-cancellation-dto").expect("cancellation idempotency"), @@ -4995,9 +5043,9 @@ async fn order_revision_and_cancellation_dtos_serialize_deterministically() { root_event.clone(), previous_event.clone(), order_cancellation("order-revision-dto"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("order-cancellation-try") .expect("cancellation try key"); @@ -5037,11 +5085,11 @@ async fn order_decision_enqueue_accept_stores_event_queues_outbox_and_updates_st seller_actor(), request_event_ptr(&request_event), order_decision("order-decision-accept"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays") .try_with_idempotency_key("order-decision-accept-idempotency") .expect("idempotency"); @@ -5144,11 +5192,11 @@ async fn order_decision_enqueue_decline_stores_event_and_status_sees_declined() seller_actor(), request_event_ptr(&request_event), decision, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let receipt = sdk @@ -5188,11 +5236,11 @@ async fn order_decision_enqueue_rejects_missing_request_evidence_before_mutation seller_actor(), missing_request, order_decision("order-decision-missing-request"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let error = sdk @@ -5236,11 +5284,11 @@ async fn order_decision_enqueue_returns_sanitized_signer_errors_before_decision_ seller_actor(), request_event_ptr(&request_event), order_decision("order-decision-wrong-signer"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let error = sdk @@ -5291,11 +5339,11 @@ async fn order_decision_enqueue_rejects_existing_decision_state_before_mutation( seller_actor(), request_event_ptr(&request_event), decline, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("target relays"); let error = sdk @@ -5359,10 +5407,10 @@ async fn order_revision_lifecycle_accepts_proposal_and_waits_for_rhi() { .enqueue_prepared_revision_proposal_with_explicit_signer( &proposal_actor, proposal_plan, - RelayResolutionPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("proposal target relays"), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, Some( SdkIdempotencyKey::new("order-lifecycle-revision-proposal") .expect("proposal idempotency"), @@ -5410,10 +5458,10 @@ async fn order_revision_lifecycle_accepts_proposal_and_waits_for_rhi() { .enqueue_prepared_revision_decision_with_explicit_signer( &revision_decision_actor, revision_decision_plan, - RelayResolutionPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("revision decision target relays"), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &FixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -5501,11 +5549,11 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif request_event_ptr(&request_event), request_event_ptr(&request_event), proposal, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("proposal target relays"), &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -5544,11 +5592,11 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif seller_actor(), request_event_ptr(&request_event), order_decision("order-lifecycle-pending-revision"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("decision target relays"), &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -5572,11 +5620,11 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif request_event_ptr(&request_event), order_event_ptr(&proposal_receipt.signed_event_id), blocked_proposal, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("blocked proposal target relays"), &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -5619,11 +5667,11 @@ async fn order_declined_revision_finalizes_declined_negotiation() { request_event_ptr(&request_event), request_event_ptr(&request_event), proposal.clone(), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("proposal target relays"), &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -5644,11 +5692,11 @@ async fn order_declined_revision_finalizes_declined_negotiation() { request_event_ptr(&request_event), order_event_ptr(&proposal_receipt.signed_event_id), declined_revision, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("declined revision target relays"), &FixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -5694,11 +5742,11 @@ async fn order_declined_revision_finalizes_declined_negotiation() { request_event_ptr(&request_event), order_event_ptr(&proposal_receipt.signed_event_id), second_decision, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("second decision target relays"), &FixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -5743,10 +5791,10 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { .enqueue_prepared_cancellation_with_explicit_signer( &cancellation_actor, cancellation_plan, - RelayResolutionPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("cancellation target relays"), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, Some( SdkIdempotencyKey::new("order-lifecycle-cancel").expect("cancellation idempotency"), ), @@ -5778,11 +5826,11 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { request_event_ptr(&request_event), request_event_ptr(&request_event), order_cancellation("order-lifecycle-cancel"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("replay target relays") .try_with_idempotency_key("order-lifecycle-cancel") .expect("replay idempotency"), @@ -5834,11 +5882,11 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { &request_event_id, &request_event_id, ), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("missing target relays"), &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -5865,11 +5913,11 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { seller_actor(), request_event_ptr(&request_event), order_decision("order-lifecycle-invalid"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("decision target relays"), &FixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -5892,11 +5940,11 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { request_event_ptr(&request_event), order_event_ptr(&decision_receipt.signed_event_id), revision_without_proposal, - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("revision decision target relays"), &FixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -5915,11 +5963,11 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { request_event_ptr(&request_event), order_event_ptr(&decision_receipt.signed_event_id), order_cancellation("order-lifecycle-invalid"), - RelayResolutionPolicy::ConfiguredRelays, + TargetPolicy::UseConfiguredProfile, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("cancellation target relays"), &FixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -6771,7 +6819,7 @@ async fn trade_product_mutation_returns_structured_ambiguity() { }], explicit_trade_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, TradeEvidenceMode::LocalOnly, )) .await diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -2,26 +2,38 @@ use radroots_events::ids::RadrootsOrderId; use radroots_sdk::{ - BackupRequest, IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, RadrootsClient, - RadrootsSdkClock, RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, - RadrootsSdkRecoveryAction, RadrootsSdkStorageConfig, RadrootsSdkTimestamp, RestoreRequest, - SDK_IDEMPOTENCY_KEY_MAX_LEN, SDK_RELAY_TARGET_MAX_COUNT, SdkBackupState, SdkBackupVerification, + BackupRequest, IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, NostrProfile, + NostrRelayUrlPolicy, RadrootsClient, RadrootsSdkClock, RadrootsSdkError, RadrootsSdkErrorClass, + RadrootsSdkGeoNamesErrorKind, RadrootsSdkRecoveryAction, RadrootsSdkStorageConfig, + RadrootsSdkTimestamp, RestoreRequest, SDK_IDEMPOTENCY_KEY_MAX_LEN, + SDK_TRANSPORT_TARGET_MAX_COUNT, SdkBackupState, SdkBackupVerification, SdkEventStoreStorageStatus, SdkIdempotencyKey, SdkOutboxStorageStatus, - SdkPrivateStoreStorageStatus, SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy, - SdkRestoreState, SdkSqliteStoreStatus, SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, - SdkStorageKind, StorageCheckpointReceipt, StorageCheckpointRequest, StorageStatusReceipt, - StorageStatusRequest, + SdkPrivateStoreStorageStatus, SdkRestoreState, SdkSqliteStoreStatus, + SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, StorageCheckpointReceipt, + StorageCheckpointRequest, StorageStatusReceipt, StorageStatusRequest, TargetPolicy, TargetSet, + TransportProfile, }; use radroots_trade::identity::RadrootsTradeLocator; use sqlx::Row; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use std::path::{Path, PathBuf}; +fn nostr_profile<I, S>( + relays: I, + policy: NostrRelayUrlPolicy, +) -> Result<TransportProfile, RadrootsSdkError> +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + Ok(TransportProfile::nostr(NostrProfile::new(relays, policy)?)) +} + #[tokio::test] async fn sdk_builder_defaults_to_memory_storage_and_no_relays() { let sdk = RadrootsClient::builder().build().await.expect("sdk"); - assert!(sdk.relay_urls().is_empty()); + assert!(sdk.configured_nostr_relay_urls().is_empty()); assert!(sdk.storage_paths().is_none()); let _listings = sdk.listings(); let _market = sdk.market(); @@ -34,15 +46,23 @@ async fn sdk_builder_defaults_to_memory_storage_and_no_relays() { #[tokio::test] async fn sdk_builder_validates_configured_relay_targets() { let sdk = RadrootsClient::builder() - .relay_url(" wss://relay-b.example.com/ ") - .relay_url("wss://relay-a.example.com") - .relay_url("wss://relay-a.example.com") + .transport_profile( + nostr_profile( + [ + " wss://relay-b.example.com/ ", + "wss://relay-a.example.com", + "wss://relay-a.example.com", + ], + NostrRelayUrlPolicy::Public, + ) + .expect("profile"), + ) .build() .await .expect("sdk"); assert_eq!( - sdk.relay_urls(), + sdk.configured_nostr_relay_urls(), &[ "wss://relay-b.example.com".to_owned(), "wss://relay-a.example.com".to_owned() @@ -52,23 +72,20 @@ async fn sdk_builder_validates_configured_relay_targets() { #[tokio::test] async fn sdk_builder_rejects_ws_relay_without_localhost_policy() { - let result = RadrootsClient::builder() - .relay_url("ws://127.0.0.1:8080") - .build() - .await; + let result = nostr_profile(["ws://127.0.0.1:8080"], NostrRelayUrlPolicy::Public); match result { Err(RadrootsSdkError::InvalidRelayUrl { .. }) => {} - Err(error) => panic!("unexpected builder error: {error}"), - Ok(_) => panic!("builder accepted ws relay without localhost policy"), + Err(error) => panic!("unexpected profile error: {error}"), + Ok(_) => panic!("profile accepted ws relay without localhost policy"), } } #[test] fn invalid_relay_url_errors_redact_userinfo() { - let error = SdkRelayTargetSet::new( + let error = TargetSet::new( ["wss://user:password@relay.example.com/path?token=secret#frag"], - SdkRelayUrlPolicy::Public, + NostrRelayUrlPolicy::Public, ) .expect_err("invalid relay"); let message = error.to_string(); @@ -98,32 +115,31 @@ fn invalid_relay_url_errors_redact_userinfo() { #[tokio::test] async fn sdk_builder_allows_only_local_ws_targets_with_localhost_policy() { let sdk = RadrootsClient::builder() - .relay_url_policy(SdkRelayUrlPolicy::Localhost) - .relay_url("ws://localhost:8080") - .relay_url("ws://127.0.0.1:8081") - .relay_url("ws://[::1]:8082") + .transport_profile( + nostr_profile( + [ + "ws://localhost:8080", + "ws://127.0.0.1:8081", + "ws://[::1]:8082", + ], + NostrRelayUrlPolicy::Localhost, + ) + .expect("profile"), + ) .build() .await .expect("sdk"); - assert_eq!(sdk.relay_urls().len(), 3); + assert_eq!(sdk.configured_nostr_relay_urls().len(), 3); - let result = RadrootsClient::builder() - .relay_url_policy(SdkRelayUrlPolicy::Localhost) - .relay_url("ws://relay.example.com") - .build() - .await; + let result = nostr_profile(["ws://relay.example.com"], NostrRelayUrlPolicy::Localhost); assert!(matches!( result, Err(RadrootsSdkError::InvalidRelayUrl { .. }) )); - let result = RadrootsClient::builder() - .relay_url_policy(SdkRelayUrlPolicy::Localhost) - .relay_url("ws://192.168.1.10:8080") - .build() - .await; + let result = nostr_profile(["ws://192.168.1.10:8080"], NostrRelayUrlPolicy::Localhost); assert!(matches!( result, @@ -396,7 +412,7 @@ fn sdk_error_contract_methods_cover_all_variants() { vec![RadrootsSdkRecoveryAction::ConfigureRelayTargets], ), ( - SdkRelayTargetSet::new(["wss://u:p@relay.example.com"], SdkRelayUrlPolicy::Public) + TargetSet::new(["wss://u:p@relay.example.com"], NostrRelayUrlPolicy::Public) .expect_err("invalid relay"), "invalid_relay_url", RadrootsSdkErrorClass::Configuration, @@ -588,54 +604,68 @@ fn sdk_error_contract_methods_cover_all_variants() { #[test] fn relay_target_set_validates_normalizes_dedupes_preserves_order_and_caps() { - let targets = SdkRelayTargetSet::new( + let targets = TargetSet::new( [ " wss://relay-b.example.com/ ", "wss://relay-a.example.com", "wss://relay-a.example.com", ], - SdkRelayUrlPolicy::Public, + NostrRelayUrlPolicy::Public, ) .expect("targets"); assert_eq!( - targets.relays(), + targets.nostr_relay_urls(), &[ "wss://relay-b.example.com".to_owned(), "wss://relay-a.example.com".to_owned() ] ); assert_eq!( - targets.canonical_relays(), + targets.canonical_targets(), &[ - "wss://relay-a.example.com".to_owned(), - "wss://relay-b.example.com".to_owned() + "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05".to_owned(), + "fc957b234632cc52e2be19cba88bc85c69966ee5a2df61742b5875ff717fd6fa".to_owned() ] ); assert_eq!( - serde_json::to_value(SdkRelayTargetPolicy::explicit(targets.clone())) + serde_json::to_value(TargetPolicy::explicit(targets.clone())) .expect("relay target policy json"), serde_json::json!({ "kind": "explicit", - "relays": ["wss://relay-b.example.com", "wss://relay-a.example.com"], - "canonical_relays": ["wss://relay-a.example.com", "wss://relay-b.example.com"] + "targets": [ + { + "kind": "Nostr", + "uri": "wss://relay-b.example.com", + "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05" + }, + { + "kind": "Nostr", + "uri": "wss://relay-a.example.com", + "fingerprint": "fc957b234632cc52e2be19cba88bc85c69966ee5a2df61742b5875ff717fd6fa" + } + ], + "canonical_targets": [ + "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05", + "fc957b234632cc52e2be19cba88bc85c69966ee5a2df61742b5875ff717fd6fa" + ] }) ); assert!(matches!( - SdkRelayTargetSet::new(Vec::<String>::new(), SdkRelayUrlPolicy::Public), + TargetSet::new(Vec::<String>::new(), NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::EmptyTargetRelays { .. }) )); - let too_many = (0..=SDK_RELAY_TARGET_MAX_COUNT) + let too_many = (0..=SDK_TRANSPORT_TARGET_MAX_COUNT) .map(|index| format!("wss://relay-{index}.example.com")) .collect::<Vec<_>>(); assert!(matches!( - SdkRelayTargetSet::new(too_many, SdkRelayUrlPolicy::Public), + TargetSet::new(too_many, NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::RelayTargetLimitExceeded { - max: SDK_RELAY_TARGET_MAX_COUNT, + max: SDK_TRANSPORT_TARGET_MAX_COUNT, actual - }) if actual == SDK_RELAY_TARGET_MAX_COUNT + 1 + }) if actual == SDK_TRANSPORT_TARGET_MAX_COUNT + 1 )); } @@ -995,9 +1025,9 @@ fn sdk_examples_stay_on_product_api_boundary() { let local_enqueue = include_str!("../examples/sdk_v1_local_enqueue_and_mock_sync.rs"); assert!(local_enqueue.contains("RadrootsClient::builder()")); assert!(local_enqueue.contains("ListingPreparePublishRequest")); - assert!(local_enqueue.contains("SdkRelayTargetPolicy")); - assert!(local_enqueue.contains("SdkRelayTargetSet")); - assert!(local_enqueue.contains("SdkRelayUrlPolicy::Localhost")); + assert!(local_enqueue.contains("TargetPolicy")); + assert!(local_enqueue.contains("TargetSet")); + assert!(local_enqueue.contains("NostrRelayUrlPolicy::Localhost")); assert!(local_enqueue.contains("RadrootsSdkLocalKeySigner")); assert!(local_enqueue.contains("RadrootsSdkSignerProvider::LocalKey")); assert!(local_enqueue.contains("enqueue_prepared_publish")); diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -239,9 +239,9 @@ const REQUIRED_IDENTITY_STORAGE_EXPORTS: &[&str] = &[ ]; const REQUIRED_TRADE_POLICY_EXPORTS: &[&str] = &[ - "AckPolicy", + "SatisfactionPolicy", "PublishMode", - "RelayResolutionPolicy", + "TargetPolicy", "SdkTradeIdempotencyRecord", "PrivacyPreflightConfirmation", "PrivacyPreflightReceipt", diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -17,21 +17,22 @@ use radroots_events::{ listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct}, }; use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState, RadrootsOutboxOperationInput}; -use radroots_relay_transport::{ - RadrootsMockRelayPublishAdapter, RadrootsRelayOutcome, RadrootsRelayPublishAdapter, - RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTransportError, -}; +#[cfg(feature = "radrootsd-proxy")] +use radroots_sdk::ProxyProfile; use radroots_sdk::{ BackupRequest, IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest, - ListingPreparePublishRequest, PUSH_OUTBOX_DEFAULT_CLAIM_TTL_MS, PUSH_OUTBOX_DEFAULT_LIMIT, + ListingPreparePublishRequest, NostrProfile, NostrRelayUrlPolicy, + PUSH_OUTBOX_DEFAULT_CLAIM_TTL_MS, PUSH_OUTBOX_DEFAULT_LIMIT, PUSH_OUTBOX_DEFAULT_NEXT_ATTEMPT_DELAY_MS, PUSH_OUTBOX_MAX_LIMIT, PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRelayOutcomeKind, PushOutboxRelayReceipt, PushOutboxRequest, RadrootsClient, RadrootsSdkError, RadrootsSdkTimestamp, RestoreRequest, - SdkBackupManifestKind, SdkRelayAuthPolicy, SdkRelayTargetPolicy, SdkRelayUrlPolicy, - SdkRestoreState, StorageStatusRequest, SyncStatusRequest, SyncStatusSource, + SdkBackupManifestKind, SdkRelayAuthPolicy, SdkRestoreState, StorageStatusRequest, + SyncStatusRequest, SyncStatusSource, TargetPolicy, TransportProfile, +}; +use radroots_transport_nostr::{ + RadrootsMockRelayPublishAdapter, RadrootsRelayOutcome, RadrootsRelayPublishAdapter, + RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTransportError, }; -#[cfg(feature = "radrootsd-proxy")] -use radroots_sdk::{SdkPublishTransport, adapters::radrootsd::RadrootsdProxyConfig}; #[cfg(feature = "radrootsd-proxy")] use std::io::{Read, Write}; #[cfg(feature = "radrootsd-proxy")] @@ -436,8 +437,11 @@ async fn directory_sdk(relays: &[&str]) -> (tempfile::TempDir, RadrootsClient) { let mut builder = RadrootsClient::builder() .directory_storage(tempdir.path().join("sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)); - for relay in relays { - builder = builder.relay_url(*relay); + if !relays.is_empty() { + builder = builder.transport_profile(TransportProfile::nostr( + NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + )); } let sdk = builder.build().await.expect("sdk"); (tempdir, sdk) @@ -446,15 +450,35 @@ async fn directory_sdk(relays: &[&str]) -> (tempfile::TempDir, RadrootsClient) { async fn system_clock_directory_sdk(relays: &[&str]) -> (tempfile::TempDir, RadrootsClient) { let tempdir = tempfile::tempdir().expect("tempdir"); let mut builder = RadrootsClient::builder().directory_storage(tempdir.path().join("sdk")); - for relay in relays { - builder = builder.relay_url(*relay); + if !relays.is_empty() { + builder = builder.transport_profile(TransportProfile::nostr( + NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + )); } let sdk = builder.build().await.expect("sdk"); (tempdir, sdk) } async fn enqueue_listing(sdk: &RadrootsClient, d_tag: &str, title: &str, relays: &[&str]) -> i64 { - enqueue_listing_with_policy(sdk, d_tag, title, relays, SdkRelayUrlPolicy::Public).await + enqueue_listing_with_policy(sdk, d_tag, title, relays, NostrRelayUrlPolicy::Public).await +} + +fn delivery_plan_for_relays<I, S>( + relays: I, + policy: NostrRelayUrlPolicy, +) -> radroots_outbox::RadrootsOutboxDeliveryPlanInput +where + I: IntoIterator<Item = S>, + S: AsRef<str>, +{ + let target_set = radroots_sdk::TargetSet::nostr_relays(relays, policy).expect("target set"); + radroots_outbox::RadrootsOutboxDeliveryPlanInput::new( + "explicit", + 1, + radroots_transport::RadrootsTransportSatisfactionPolicy::AllTargets, + target_set.into_targets(), + ) } async fn backup_source(sdk: &RadrootsClient, root: &Path, name: &str) -> PathBuf { @@ -483,14 +507,14 @@ async fn enqueue_listing_with_policy( d_tag: &str, title: &str, relays: &[&str], - url_policy: SdkRelayUrlPolicy, + url_policy: NostrRelayUrlPolicy, ) -> i64 { sdk.listings() .enqueue_publish_with_explicit_signer( ListingEnqueuePublishRequest::new( actor(), listing(d_tag, title), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_target_relays(relays, url_policy) .expect("relay targets"), @@ -1425,9 +1449,7 @@ async fn product_push_outbox_uses_radrootsd_proxy_transport_with_daemon_resolved let sdk = RadrootsClient::builder() .directory_storage(tempdir.path().join("sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) - .publish_transport(SdkPublishTransport::RadrootsdProxy( - RadrootsdProxyConfig::new(endpoint), - )) + .transport_profile(TransportProfile::proxy(ProxyProfile::new(endpoint))) .build() .await .expect("sdk"); @@ -1438,7 +1460,7 @@ async fn product_push_outbox_uses_radrootsd_proxy_transport_with_daemon_resolved ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Proxy Coffee"), - SdkRelayTargetPolicy::use_publish_transport(), + TargetPolicy::use_transport_profile(), ), &FixtureSigner::new(SELLER), ) @@ -1499,11 +1521,11 @@ async fn product_push_outbox_radrootsd_proxy_idempotency_is_attempt_scoped() { ]); let tempdir = tempfile::tempdir().expect("tempdir"); let storage = tempdir.path().join("sdk"); - let transport = SdkPublishTransport::RadrootsdProxy(RadrootsdProxyConfig::new(endpoint)); + let transport = TransportProfile::proxy(ProxyProfile::new(endpoint)); let sdk = RadrootsClient::builder() .directory_storage(storage.clone()) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) - .publish_transport(transport.clone()) + .transport_profile(transport.clone()) .build() .await .expect("sdk"); @@ -1514,7 +1536,7 @@ async fn product_push_outbox_radrootsd_proxy_idempotency_is_attempt_scoped() { ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Retry Coffee"), - SdkRelayTargetPolicy::use_publish_transport(), + TargetPolicy::use_transport_profile(), ), &FixtureSigner::new(SELLER), ) @@ -1543,7 +1565,7 @@ async fn product_push_outbox_radrootsd_proxy_idempotency_is_attempt_scoped() { let sdk = RadrootsClient::builder() .directory_storage(storage) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_001)) - .publish_transport(transport) + .transport_profile(transport) .build() .await .expect("reopened sdk"); @@ -1598,9 +1620,7 @@ async fn product_push_outbox_radrootsd_proxy_error_and_terminal_paths_update_out let retryable_sdk = RadrootsClient::builder() .directory_storage(tempdir.path().join("retryable-sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) - .publish_transport(SdkPublishTransport::RadrootsdProxy( - RadrootsdProxyConfig::new(closed_endpoint).with_timeout(Duration::from_millis(50)), - )) + .transport_profile(TransportProfile::proxy(ProxyProfile::new(closed_endpoint))) .build() .await .expect("retryable sdk"); @@ -1610,7 +1630,7 @@ async fn product_push_outbox_radrootsd_proxy_error_and_terminal_paths_update_out ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Proxy Error Coffee"), - SdkRelayTargetPolicy::use_publish_transport(), + TargetPolicy::use_transport_profile(), ), &FixtureSigner::new(SELLER), ) @@ -1651,9 +1671,9 @@ async fn product_push_outbox_radrootsd_proxy_error_and_terminal_paths_update_out let terminal_sdk = RadrootsClient::builder() .directory_storage(tempdir.path().join("terminal-sdk")) .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) - .publish_transport(SdkPublishTransport::RadrootsdProxy( - RadrootsdProxyConfig::new(terminal_endpoint), - )) + .transport_profile(TransportProfile::proxy(ProxyProfile::new( + terminal_endpoint, + ))) .build() .await .expect("terminal sdk"); @@ -1663,7 +1683,7 @@ async fn product_push_outbox_radrootsd_proxy_error_and_terminal_paths_update_out ListingEnqueuePublishRequest::new( actor(), listing(LISTING_B_D_TAG, "Terminal Coffee"), - SdkRelayTargetPolicy::use_publish_transport(), + TargetPolicy::use_transport_profile(), ), &FixtureSigner::new(SELLER), ) @@ -1702,7 +1722,7 @@ fn push_outbox_contract_dtos_serialize_deterministically() { .with_outbox_event_id(7) .republish_accepted_relays(true) .with_accepted_quorum(1) - .with_relay_url_policy(SdkRelayUrlPolicy::Localhost) + .with_relay_url_policy(NostrRelayUrlPolicy::Localhost) .with_auth_policy(SdkRelayAuthPolicy::DetectOnly) .with_claim_ttl_ms(1_000) .with_next_attempt_delay_ms(2_000); @@ -1939,7 +1959,7 @@ async fn push_outbox_default_public_policy_rejects_queued_localhost_ws_targets() LISTING_A_D_TAG, "Local Coffee", &[LOCAL_RELAY_A], - SdkRelayUrlPolicy::Localhost, + NostrRelayUrlPolicy::Localhost, ) .await; let adapter = RadrootsMockRelayPublishAdapter::new(); @@ -1962,7 +1982,7 @@ async fn push_outbox_with_adapter_accepts_explicit_queued_localhost_ws_targets() LISTING_A_D_TAG, "Local Coffee", &[LOCAL_RELAY_A, LOCAL_RELAY_B, LOCAL_RELAY_C], - SdkRelayUrlPolicy::Localhost, + NostrRelayUrlPolicy::Localhost, ) .await; let adapter = RadrootsMockRelayPublishAdapter::new(); @@ -1973,7 +1993,7 @@ async fn push_outbox_with_adapter_accepts_explicit_queued_localhost_ws_targets() &adapter, PushOutboxRequest::new() .with_limit(1) - .with_relay_url_policy(SdkRelayUrlPolicy::Localhost), + .with_relay_url_policy(NostrRelayUrlPolicy::Localhost), ) .await .expect("push"); @@ -2016,9 +2036,9 @@ fn enqueue_publish_rejects_nonlocal_ws_relay_targets() { let error = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_C_D_TAG, "Nonlocal Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([NONLOCAL_WS_RELAY], SdkRelayUrlPolicy::Localhost) + .try_with_target_relays([NONLOCAL_WS_RELAY], NostrRelayUrlPolicy::Localhost) .expect_err("nonlocal ws relay target"); assert!(matches!(error, RadrootsSdkError::InvalidRelayUrl { .. })); @@ -2026,9 +2046,9 @@ fn enqueue_publish_rejects_nonlocal_ws_relay_targets() { let error = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_C_D_TAG, "Private LAN Coffee"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([PRIVATE_LAN_WS_RELAY], SdkRelayUrlPolicy::Localhost) + .try_with_target_relays([PRIVATE_LAN_WS_RELAY], NostrRelayUrlPolicy::Localhost) .expect_err("private LAN ws relay target"); assert!(matches!(error, RadrootsSdkError::InvalidRelayUrl { .. })); @@ -2260,7 +2280,7 @@ async fn push_outbox_does_not_claim_unsigned_outbox_work() { .enqueue_operation(RadrootsOutboxOperationInput::new( LISTING_PUBLISH_OPERATION_KIND, prepared.frozen_draft, - vec![RELAY_A.to_owned()], + delivery_plan_for_relays([RELAY_A], NostrRelayUrlPolicy::Public), 1_700_000_000_000, )) .await diff --git a/crates/sdk/tests/trade_product_publish_runtime.rs b/crates/sdk/tests/trade_product_publish_runtime.rs @@ -21,10 +21,9 @@ use radroots_events::{ }; use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey}; use radroots_sdk::{ - AckPolicy, PublishMode, PushOutboxRelayOutcomeKind, RadrootsClient, RadrootsSdkLocalKeySigner, - RadrootsSdkSignerProvider, RadrootsSdkTimestamp, RelayResolutionPolicy, SdkPublishTransport, - SdkRelayTargetSet, SdkRelayUrlPolicy, TradeMutationOutcome, TradeProposeRequest, - adapters::radrootsd::RadrootsdProxyConfig, + NostrRelayUrlPolicy, ProxyProfile, PublishMode, PushOutboxRelayOutcomeKind, RadrootsClient, + RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SatisfactionPolicy, + TargetPolicy, TargetSet, TradeMutationOutcome, TradeProposeRequest, TransportProfile, }; use std::{ io::{Read, Write}, @@ -207,7 +206,7 @@ fn order_request(raw_order_id: &str) -> RadrootsOrderRequest { fn trade_propose_request( raw_order_id: &str, publish_mode: PublishMode, - ack_policy: AckPolicy, + ack_policy: SatisfactionPolicy, ) -> TradeProposeRequest { let order = order_request(raw_order_id); TradeProposeRequest::new( @@ -224,9 +223,9 @@ fn trade_propose_request( ) } -fn explicit_trade_relays() -> RelayResolutionPolicy { - RelayResolutionPolicy::explicit( - SdkRelayTargetSet::new([RELAY], SdkRelayUrlPolicy::Public).expect("target relays"), +fn explicit_trade_relays() -> TargetPolicy { + TargetPolicy::explicit( + TargetSet::new([RELAY], NostrRelayUrlPolicy::Public).expect("target relays"), ) } @@ -242,9 +241,7 @@ async fn trade_product_propose_enqueue_and_publish_uses_ack_policy() { .signer_provider(RadrootsSdkSignerProvider::LocalKey( RadrootsSdkLocalKeySigner::new(signer_keys).expect("local signer"), )) - .publish_transport(SdkPublishTransport::RadrootsdProxy( - RadrootsdProxyConfig::new(endpoint), - )) + .transport_profile(TransportProfile::proxy(ProxyProfile::new(endpoint))) .build() .await .expect("sdk"); @@ -256,7 +253,7 @@ async fn trade_product_propose_enqueue_and_publish_uses_ack_policy() { trade_propose_request( "trade-product-publish", PublishMode::EnqueueAndPublish, - AckPolicy::AtLeastOneRelay, + SatisfactionPolicy::AtLeastOneTarget, ) .try_with_idempotency_key("trade-product-publish") .expect("idempotency"), @@ -284,7 +281,6 @@ async fn trade_product_propose_enqueue_and_publish_uses_ack_policy() { let recorded = handle.join().expect("proxy request"); let body: serde_json::Value = serde_json::from_str(recorded.body.as_str()).expect("body"); assert_eq!(body["method"], "publish.event"); - assert_eq!(body["params"]["delivery_policy"]["mode"], "quorum"); - assert_eq!(body["params"]["delivery_policy"]["quorum"], 1); + assert_eq!(body["params"]["delivery_policy"]["mode"], "any"); assert_eq!(body["params"]["relays"], serde_json::json!([RELAY])); } diff --git a/crates/sdk/tests/unit/adapters_radrootsd_tests.rs b/crates/sdk/tests/unit/adapters_radrootsd_tests.rs @@ -3,7 +3,7 @@ use radroots_publish_proxy_protocol::{ PublishJobStatus, PublishJobView, PublishRelayOutcome, PublishRelayOutcomeKind, PublishRelaySource, }; -use radroots_relay_transport::{ +use radroots_transport_nostr::{ RadrootsRelayPublishRequest, RadrootsRelayTargetSet, RadrootsRelayUrlPolicy, }; use std::io::{Read, Write}; @@ -508,10 +508,22 @@ async fn publish_event_empty_http_error_reports_empty_body() { #[tokio::test] async fn relay_publish_adapter_derives_delivery_policy_and_timeout() { - for (target_count, quorum, expected_policy) in [ - (2, 2, PublishDeliveryPolicy::All), - (2, 1, PublishDeliveryPolicy::Any), - (3, 2, PublishDeliveryPolicy::Quorum { quorum: 2 }), + for (target_count, satisfaction_policy, expected_policy) in [ + ( + 2, + radroots_transport::RadrootsTransportSatisfactionPolicy::AllTargets, + PublishDeliveryPolicy::All, + ), + ( + 2, + radroots_transport::RadrootsTransportSatisfactionPolicy::AnyTarget, + PublishDeliveryPolicy::Any, + ), + ( + 3, + radroots_transport::RadrootsTransportSatisfactionPolicy::AtLeast(2), + PublishDeliveryPolicy::Quorum { quorum: 2 }, + ), ] { let response_body = publish_response_json(); let (endpoint, handle) = spawn_http_server("200 OK", response_body.as_str()); @@ -527,7 +539,7 @@ async fn relay_publish_adapter_derives_delivery_policy_and_timeout() { let receipts = adapter .publish( RadrootsRelayPublishRequest::new(signed_event(), targets, 10) - .with_accepted_quorum(quorum), + .with_satisfaction_policy(satisfaction_policy), ) .await .expect("adapter publish"); @@ -569,7 +581,7 @@ async fn relay_publish_adapter_maps_proxy_errors_to_transport_errors() { assert!(matches!( error, - radroots_relay_transport::RadrootsRelayTransportError::Transport(message) + radroots_transport_nostr::RadrootsRelayTransportError::Transport(message) if message.contains("radrootsd") )); } diff --git a/crates/sdk/tests/unit/dvm_runtime_tests.rs b/crates/sdk/tests/unit/dvm_runtime_tests.rs @@ -4,8 +4,8 @@ use super::{ dvm_trade_transition_proof_plan, sdk_timestamp_ms, }; use crate::{ - RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkRelayTargetPolicy, - SdkRelayUrlPolicy, SyncProjectionRefreshRequest, + NostrRelayUrlPolicy, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, + SyncProjectionRefreshRequest, TargetPolicy, }; use radroots_authority::RadrootsActorContext; use radroots_events::{ @@ -230,9 +230,9 @@ fn enqueue_request_builders_and_ingest_request_builders_are_deterministic() { let idempotency_len = idempotency.as_str().len(); let request = DvmTradeTransitionProofEnqueueRequest::from_prepare( prepare.clone(), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY], NostrRelayUrlPolicy::Public) .expect("relays") .with_idempotency_key(idempotency) .with_inventory_sequence(11) @@ -254,7 +254,7 @@ fn enqueue_request_builders_and_ingest_request_builders_are_deterministic() { event_id('2'), event_id('3'), inventory_bins(), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("dvm-proof-request-2") .expect("idempotency") @@ -267,15 +267,15 @@ fn enqueue_request_builders_and_ingest_request_builders_are_deterministic() { assert!(matches!( DvmTradeTransitionProofEnqueueRequest::from_prepare( prepare, - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays(["ws://relay.example.com"], SdkRelayUrlPolicy::Public), + .try_with_target_relays(["ws://relay.example.com"], NostrRelayUrlPolicy::Public), Err(RadrootsSdkError::InvalidRelayUrl { .. }) )); assert!(matches!( DvmTradeTransitionProofEnqueueRequest::from_prepare( proof_request(service_actor()), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key(""), Err(RadrootsSdkError::InvalidRequest { .. }) diff --git a/crates/sdk/tests/unit/error_tests.rs b/crates/sdk/tests/unit/error_tests.rs @@ -131,7 +131,7 @@ fn listing_and_store_errors_convert_to_sdk_error_classes() { #[test] fn outbox_error_conversion_handles_empty_targets_and_fallbacks() { assert!(matches!( - RadrootsSdkError::from(radroots_outbox::RadrootsOutboxError::EmptyTargetRelays), + RadrootsSdkError::from(radroots_outbox::RadrootsOutboxError::EmptyDeliveryTargets), RadrootsSdkError::EmptyTargetRelays { ref operation } if operation == "outbox enqueue" )); @@ -162,7 +162,7 @@ fn outbox_error_conversion_handles_empty_targets_and_fallbacks() { #[test] fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { let unsupported = RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::UnsupportedRelayScheme { + radroots_transport_nostr::RadrootsRelayTransportError::UnsupportedRelayScheme { url: "ftp://user:secret@relay.example.com/path?token=secret".to_owned(), scheme: "ftp".to_owned(), }, @@ -176,7 +176,7 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::EmptyRelayHost { + radroots_transport_nostr::RadrootsRelayTransportError::EmptyRelayHost { url: "wss://".to_owned(), }, ), @@ -185,7 +185,7 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { )); assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlQueryOrFragment { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlQueryOrFragment { url: "wss://relay.example.com?token=secret".to_owned(), }, ), @@ -194,7 +194,7 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { )); assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlUserinfo { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlUserinfo { url: "wss://user:secret@relay.example.com".to_owned(), }, ), @@ -204,7 +204,7 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { )); assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::WsRequiresLocalhostPolicy { + radroots_transport_nostr::RadrootsRelayTransportError::WsRequiresLocalhostPolicy { url: "ws://relay.example.com".to_owned(), }, ), @@ -213,7 +213,7 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { )); assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlForbiddenDestination { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlForbiddenDestination { url: "ws://127.0.0.1:9000".to_owned(), reason: "localhost disabled".to_owned(), }, @@ -223,7 +223,7 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { )); assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination { url: "ws://relay.example.com".to_owned(), address: "127.0.0.1".to_owned(), reason: "loopback disabled".to_owned(), @@ -234,13 +234,13 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { )); assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::EmptyTargetSet + radroots_transport_nostr::RadrootsRelayTransportError::EmptyTargetSet ), RadrootsSdkError::EmptyTargetRelays { ref operation } if operation == "relay publish" )); assert!(matches!( RadrootsSdkError::from( - radroots_relay_transport::RadrootsRelayTransportError::RelayUrlParse { + radroots_transport_nostr::RadrootsRelayTransportError::RelayUrlParse { url: "wss://user:secret@relay.example.com/path?token=secret".to_owned(), reason: "bad relay URL".to_owned(), }, @@ -250,13 +250,13 @@ fn relay_transport_error_conversion_redacts_and_classifies_url_errors() { && reason == "bad relay URL" )); assert!(matches!( - RadrootsSdkError::from(radroots_relay_transport::RadrootsRelayTransportError::Outbox( - radroots_outbox::RadrootsOutboxError::EmptyTargetRelays, + RadrootsSdkError::from(radroots_transport_nostr::RadrootsRelayTransportError::Outbox( + radroots_outbox::RadrootsOutboxError::EmptyDeliveryTargets, )), RadrootsSdkError::EmptyTargetRelays { ref operation } if operation == "outbox enqueue" )); assert!(matches!( - RadrootsSdkError::from(radroots_relay_transport::RadrootsRelayTransportError::Transport( + RadrootsSdkError::from(radroots_transport_nostr::RadrootsRelayTransportError::Transport( "offline".to_owned(), )), RadrootsSdkError::RelayTransport { ref message } if message == "Relay transport error: offline" diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs @@ -167,9 +167,9 @@ fn farm_runtime_request_builders_and_serializers_cover_success_paths() { let enqueue = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_B_D_TAG, "Queued Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY_A, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY_A, RELAY_B], NostrRelayUrlPolicy::Public) .expect("relay targets") .with_idempotency_key(SdkIdempotencyKey::new("farm-unit-key").expect("key")) .with_created_at(created_at); @@ -182,7 +182,7 @@ fn farm_runtime_request_builders_and_serializers_cover_success_paths() { let try_key = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_C_D_TAG, "Try Key Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("farm-unit-try-key") .expect("try key"); @@ -357,15 +357,15 @@ fn farm_request_builders_reject_invalid_options_and_timestamp_bounds() { let invalid_relays = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_A_D_TAG, "Invalid Relay Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays(["http://relay.radroots.test"], SdkRelayUrlPolicy::Public); + .try_with_target_relays(["http://relay.radroots.test"], NostrRelayUrlPolicy::Public); assert!(invalid_relays.is_err()); let invalid_key = FarmEnqueuePublishRequest::new( farmer_actor(), farm(FARM_B_D_TAG, "Invalid Key Farm"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key(""); assert!(invalid_key.is_err()); @@ -531,7 +531,7 @@ async fn farm_enqueue_publish_reports_prepare_errors_before_signing() { FarmEnqueuePublishRequest::new( farmer_actor(), farm("AAAAAAAAAAAAAAAAAAAAA!", "Invalid Enqueue Farm"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), ), &FixtureSigner::new(FARMER), @@ -556,7 +556,7 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() { FarmEnqueuePublishRequest::new( actor.clone(), farm(FARM_A_D_TAG, "Enqueued Farm"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), ) .try_with_idempotency_key("farm-source-attached-enqueue") @@ -580,7 +580,7 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() { .enqueue_prepared_publish_with_explicit_signer( &actor, plan, - SdkRelayTargetPolicy::try_explicit([RELAY_B], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_B], NostrRelayUrlPolicy::Public) .expect("prepared target relays"), None, &signer, @@ -612,7 +612,7 @@ async fn farm_configured_local_signer_enqueues_publish_without_explicit_signer() FarmEnqueuePublishRequest::new( actor, farm(FARM_C_D_TAG, "Configured Farm"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), ) .try_with_idempotency_key("farm-configured-local") @@ -646,7 +646,7 @@ async fn farm_configured_enqueue_reports_prepare_and_signer_errors() { .enqueue_publish(FarmEnqueuePublishRequest::new( actor.clone(), farm("AAAAAAAAAAAAAAAAAAAAA!", "Invalid Configured Farm"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), )) .await, @@ -671,7 +671,7 @@ async fn farm_configured_enqueue_reports_prepare_and_signer_errors() { .enqueue_prepared_publish( &actor, plan, - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), None, ) diff --git a/crates/sdk/tests/unit/listings_runtime_tests.rs b/crates/sdk/tests/unit/listings_runtime_tests.rs @@ -104,9 +104,9 @@ fn listing_runtime_request_builders_and_serializers_cover_success_paths() { let enqueue = ListingEnqueuePublishRequest::from_document( actor(), RadrootsListingDraftDocumentV1::new(listing(LISTING_B_D_TAG, "Queued Greens")), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) - .try_with_target_relays([RELAY_A, RELAY_B], SdkRelayUrlPolicy::Public) + .try_with_target_relays([RELAY_A, RELAY_B], NostrRelayUrlPolicy::Public) .expect("relay targets") .with_idempotency_key(SdkIdempotencyKey::new("listing-unit-key").expect("key")) .with_created_at(created_at); @@ -119,7 +119,7 @@ fn listing_runtime_request_builders_and_serializers_cover_success_paths() { let try_key = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_C_D_TAG, "Try Key Greens"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key("listing-unit-try-key") .expect("try key"); @@ -134,7 +134,7 @@ fn listing_request_builders_reject_invalid_options_and_timestamp_bounds() { let invalid_key = ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Invalid Key Greens"), - SdkRelayTargetPolicy::UseConfiguredRelays, + TargetPolicy::UseConfiguredProfile, ) .try_with_idempotency_key(""); assert!(invalid_key.is_err()); @@ -235,7 +235,7 @@ async fn listing_enqueue_publish_reports_prepare_errors_before_signing() { ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Future Enqueue Greens"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), ) .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)), @@ -264,7 +264,7 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() { ListingEnqueuePublishRequest::new( actor.clone(), listing(LISTING_A_D_TAG, "Enqueued Greens"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), ) .try_with_idempotency_key("listing-source-attached-enqueue") @@ -288,7 +288,7 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() { .enqueue_prepared_publish_with_explicit_signer( &actor, plan, - SdkRelayTargetPolicy::try_explicit([RELAY_B], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_B], NostrRelayUrlPolicy::Public) .expect("prepared target relays"), None, &signer, @@ -320,7 +320,7 @@ async fn listing_configured_local_signer_enqueues_publish_without_explicit_signe ListingEnqueuePublishRequest::new( actor, listing_for_seller(seller.as_str(), LISTING_C_D_TAG, "Configured Greens"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), ) .try_with_idempotency_key("listing-configured-local") @@ -347,7 +347,7 @@ async fn listing_configured_enqueue_reports_missing_signer_after_prepare() { ListingEnqueuePublishRequest::new( actor.clone(), listing(LISTING_A_D_TAG, "Configured Prepare Error Greens"), - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), ) .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)), @@ -368,7 +368,7 @@ async fn listing_configured_enqueue_reports_missing_signer_after_prepare() { .enqueue_prepared_publish( &actor, plan, - SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public) .expect("target relays"), None, ) diff --git a/crates/sdk/tests/unit/orders_runtime_tests.rs b/crates/sdk/tests/unit/orders_runtime_tests.rs @@ -411,8 +411,8 @@ fn fixture_cancellation(raw_order_id: &str) -> RadrootsOrderCancellation { } } -fn fixture_target_relays() -> RelayResolutionPolicy { - RelayResolutionPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public).expect("target relays") +fn fixture_target_relays() -> TargetPolicy { + TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public).expect("target relays") } async fn prepared_order_sdk() -> RadrootsClient { @@ -446,7 +446,7 @@ async fn order_configured_local_signer_enqueues_submit_without_explicit_signer() fixture_order_request("order-configured-local-1"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await .expect("enqueue submit"); @@ -467,7 +467,7 @@ async fn order_configured_local_signer_enqueues_lifecycle_wrappers_without_expli fixture_order_decision("order-configured-decision"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await .expect("configured decision"); @@ -489,7 +489,7 @@ async fn order_configured_local_signer_enqueues_lifecycle_wrappers_without_expli proposal_payload, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await .expect("configured revision proposal"); @@ -514,7 +514,7 @@ async fn order_configured_local_signer_enqueues_lifecycle_wrappers_without_expli revision_proposal_payload.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -533,7 +533,7 @@ async fn order_configured_local_signer_enqueues_lifecycle_wrappers_without_expli revision_decision_payload, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await .expect("configured revision decision"); @@ -557,7 +557,7 @@ async fn order_configured_local_signer_enqueues_lifecycle_wrappers_without_expli fixture_cancellation("order-configured-cancel"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await .expect("configured cancellation"); @@ -585,7 +585,7 @@ async fn enqueue_fixture_submit(sdk: &RadrootsClient, raw_order_id: &str) -> Tra plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2075,7 +2075,7 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { RadrootsOrderRevisionOutcome::Accepted, ); let cancellation_payload = cancellation_payload(); - let policy = RelayResolutionPolicy::ConfiguredRelays; + let policy = TargetPolicy::UseConfiguredProfile; assert_error_display( TradeSubmitEnqueueRequest::new( @@ -2084,9 +2084,9 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { submit_payload.clone(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(Vec::<String>::new(), SdkRelayUrlPolicy::Public), + .try_with_target_relays(Vec::<String>::new(), NostrRelayUrlPolicy::Public), "target relays", ); assert_error_display( @@ -2096,7 +2096,7 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { submit_payload, policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key(""), "idempotency key", @@ -2109,9 +2109,9 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { decision_payload.clone(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(Vec::<String>::new(), SdkRelayUrlPolicy::Public), + .try_with_target_relays(Vec::<String>::new(), NostrRelayUrlPolicy::Public), "target relays", ); assert_error_display( @@ -2121,7 +2121,7 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { decision_payload, policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key(" leading"), "idempotency key", @@ -2135,9 +2135,9 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { proposal_payload.clone(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(Vec::<String>::new(), SdkRelayUrlPolicy::Public), + .try_with_target_relays(Vec::<String>::new(), NostrRelayUrlPolicy::Public), "target relays", ); assert_error_display( @@ -2148,7 +2148,7 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { proposal_payload.clone(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("trailing "), "idempotency key", @@ -2162,9 +2162,9 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { revision_decision_payload.clone(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(Vec::<String>::new(), SdkRelayUrlPolicy::Public), + .try_with_target_relays(Vec::<String>::new(), NostrRelayUrlPolicy::Public), "target relays", ); assert_error_display( @@ -2175,7 +2175,7 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { revision_decision_payload, policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key("invalid\nkey"), "idempotency key", @@ -2189,9 +2189,9 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { cancellation_payload.clone(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(Vec::<String>::new(), SdkRelayUrlPolicy::Public), + .try_with_target_relays(Vec::<String>::new(), NostrRelayUrlPolicy::Public), "target relays", ); assert_error_display( @@ -2202,7 +2202,7 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { cancellation_payload, policy, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .try_with_idempotency_key(""), "idempotency key", @@ -2212,24 +2212,29 @@ fn order_enqueue_request_mutators_reject_invalid_relays_and_idempotency_keys() { #[test] fn trade_enqueue_policy_rejects_publish_modes_without_matching_side_effects() { assert!(matches!( - validate_trade_enqueue_policy(PublishMode::DryRun, AckPolicy::NoWait), + validate_trade_enqueue_policy(PublishMode::DryRun, SatisfactionPolicy::NoWait), Err(RadrootsSdkError::InvalidRequest { ref message }) if message == "trade dry-run publish mode must use a prepare request" )); assert!(matches!( - validate_trade_enqueue_policy(PublishMode::EnqueueOnly, AckPolicy::AtLeastOneRelay), + validate_trade_enqueue_policy(PublishMode::EnqueueOnly, SatisfactionPolicy::AtLeastOneTarget), Err(RadrootsSdkError::InvalidRequest { ref message }) if message == "trade enqueue-only publish mode only supports no-wait acknowledgement" )); assert!(matches!( - validate_trade_enqueue_policy(PublishMode::EnqueueAndPublish, AckPolicy::NoWait), + validate_trade_enqueue_policy(PublishMode::EnqueueAndPublish, SatisfactionPolicy::NoWait), Err(RadrootsSdkError::InvalidRequest { ref message }) if message == "trade enqueue-and-publish requires a relay acknowledgement policy" )); - assert!(validate_trade_enqueue_policy(PublishMode::EnqueueOnly, AckPolicy::NoWait).is_ok()); assert!( - validate_trade_enqueue_policy(PublishMode::EnqueueAndPublish, AckPolicy::AtLeastOneRelay) - .is_ok() + validate_trade_enqueue_policy(PublishMode::EnqueueOnly, SatisfactionPolicy::NoWait).is_ok() + ); + assert!( + validate_trade_enqueue_policy( + PublishMode::EnqueueAndPublish, + SatisfactionPolicy::AtLeastOneTarget + ) + .is_ok() ); } @@ -2347,7 +2352,7 @@ async fn prepared_submit_and_decision_enqueue_cover_source_attached_success_path decision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, Some(SdkIdempotencyKey::new("prepared-decision").expect("idempotency")), &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2385,7 +2390,7 @@ async fn prepared_revision_lifecycle_enqueue_cover_source_attached_success_paths proposal_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, Some(SdkIdempotencyKey::new("prepared-proposal").expect("idempotency")), &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2415,7 +2420,7 @@ async fn prepared_revision_lifecycle_enqueue_cover_source_attached_success_paths revision_decision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2449,7 +2454,7 @@ async fn prepared_cancellation_enqueue_covers_source_attached_success_path() { cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, Some(SdkIdempotencyKey::new("prepared-cancellation").expect("idempotency")), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2474,7 +2479,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { fixture_order_request("order-wrapper-decision"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2489,7 +2494,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { fixture_order_decision("order-wrapper-decision"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2506,7 +2511,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { fixture_order_request("order-wrapper-revision"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2527,7 +2532,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { proposal_payload.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2543,7 +2548,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { fixture_revision_decision(&proposal_payload, &proposal.signed_event_id), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2560,7 +2565,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { fixture_order_request("order-wrapper-cancellation"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2576,7 +2581,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { fixture_cancellation("order-wrapper-cancellation"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2615,7 +2620,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( decision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2642,7 +2647,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( proposal_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2669,7 +2674,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( revision_decision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2696,7 +2701,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2760,7 +2765,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( closed_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2774,7 +2779,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( closed_proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -2791,7 +2796,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( closed_revision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2808,7 +2813,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( closed_cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -2850,7 +2855,7 @@ async fn configured_prepared_lifecycle_enqueues_run_preflight_guards() { decision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -2876,7 +2881,7 @@ async fn configured_prepared_lifecycle_enqueues_run_preflight_guards() { proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -2903,7 +2908,7 @@ async fn configured_prepared_lifecycle_enqueues_run_preflight_guards() { revision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -2929,7 +2934,7 @@ async fn configured_prepared_lifecycle_enqueues_run_preflight_guards() { cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -2981,7 +2986,7 @@ async fn configured_prepared_lifecycle_enqueues_report_existing_event_lookup_err decision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -2995,7 +3000,7 @@ async fn configured_prepared_lifecycle_enqueues_report_existing_event_lookup_err proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await, @@ -3030,7 +3035,7 @@ async fn configured_prepared_lifecycle_enqueues_report_existing_event_lookup_err revision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -3044,7 +3049,7 @@ async fn configured_prepared_lifecycle_enqueues_report_existing_event_lookup_err cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -3073,7 +3078,7 @@ async fn configured_enqueue_wrappers_report_prepare_errors_before_signing() { fixture_order_request("order-configured-prepare-submit"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await, Err(RadrootsSdkError::UnauthorizedActor { .. }) @@ -3087,7 +3092,7 @@ async fn configured_enqueue_wrappers_report_prepare_errors_before_signing() { fixture_order_decision("order-configured-prepare-decision"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await, Err(RadrootsSdkError::UnauthorizedActor { .. }) @@ -3102,7 +3107,7 @@ async fn configured_enqueue_wrappers_report_prepare_errors_before_signing() { proposal.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await, Err(RadrootsSdkError::UnauthorizedActor { .. }) @@ -3117,7 +3122,7 @@ async fn configured_enqueue_wrappers_report_prepare_errors_before_signing() { fixture_revision_decision(&proposal, &previous_event_id), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await, Err(RadrootsSdkError::UnauthorizedActor { .. }) @@ -3132,7 +3137,7 @@ async fn configured_enqueue_wrappers_report_prepare_errors_before_signing() { fixture_cancellation("order-configured-prepare-cancel"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, )) .await, Err(RadrootsSdkError::UnauthorizedActor { .. }) @@ -3160,7 +3165,7 @@ async fn configured_prepared_methods_report_missing_configured_signer_after_pref submit_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -3183,7 +3188,7 @@ async fn configured_prepared_methods_report_missing_configured_signer_after_pref decision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -3212,7 +3217,7 @@ async fn configured_prepared_methods_report_missing_configured_signer_after_pref proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -3241,7 +3246,7 @@ async fn configured_prepared_methods_report_missing_configured_signer_after_pref revision_proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3266,7 +3271,7 @@ async fn configured_prepared_methods_report_missing_configured_signer_after_pref revision_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -3290,7 +3295,7 @@ async fn configured_prepared_methods_report_missing_configured_signer_after_pref cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None ) .await, @@ -3400,7 +3405,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3436,7 +3441,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3461,7 +3466,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { revision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -3494,7 +3499,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -3527,7 +3532,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { decision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3540,7 +3545,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { decision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3563,7 +3568,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { decision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -3602,7 +3607,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { proposal_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3615,7 +3620,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { proposal_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3638,7 +3643,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { proposal_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -3677,7 +3682,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { proposal_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), ) @@ -3700,7 +3705,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { revision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -3713,7 +3718,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { revision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -3736,7 +3741,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { revision_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -3771,7 +3776,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { cancellation_plan.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), ) @@ -3784,7 +3789,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { cancellation_plan, fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, None, ) .await @@ -3834,7 +3839,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { fixture_order_request("order-wrapper-submit-error"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_created_at(out_of_range), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), @@ -3851,7 +3856,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { fixture_order_decision("order-wrapper-decision-error"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_created_at(out_of_range), &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), @@ -3877,7 +3882,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { proposal_payload.clone(), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_created_at(out_of_range), &OrderFixtureSigner::new(SELLER_SECRET_KEY_HEX), @@ -3895,7 +3900,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { fixture_revision_decision(&proposal_payload, &previous_event_id), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_created_at(out_of_range), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), @@ -3913,7 +3918,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { fixture_cancellation("order-wrapper-cancellation-error"), fixture_target_relays(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) .with_created_at(out_of_range), &OrderFixtureSigner::new(BUYER_SECRET_KEY_HEX), @@ -4024,7 +4029,7 @@ fn order_runtime_request_builders_and_serializers_cover_source_attached_paths() reason: "not workable".to_owned(), }, ); - let policy = RelayResolutionPolicy::ConfiguredRelays; + let policy = TargetPolicy::UseConfiguredProfile; let submit_prepare = TradeSubmitPrepareRequest::new(buyer_actor(), root_event.clone(), order_request_payload()) @@ -4041,9 +4046,9 @@ fn order_runtime_request_builders_and_serializers_cover_source_attached_paths() order_request_payload(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(["wss://relay-a.radroots.test"], SdkRelayUrlPolicy::Public) + .try_with_target_relays(["wss://relay-a.radroots.test"], NostrRelayUrlPolicy::Public) .expect("submit relays") .with_idempotency_key(SdkIdempotencyKey::new("submit-unit-key").expect("key")) .with_created_at(created_at); @@ -4070,9 +4075,9 @@ fn order_runtime_request_builders_and_serializers_cover_source_attached_paths() order_decision_payload(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(["wss://relay-b.radroots.test"], SdkRelayUrlPolicy::Public) + .try_with_target_relays(["wss://relay-b.radroots.test"], NostrRelayUrlPolicy::Public) .expect("decision relays") .with_idempotency_key(SdkIdempotencyKey::new("decision-unit-key").expect("key")) .with_created_at(created_at); @@ -4094,9 +4099,9 @@ fn order_runtime_request_builders_and_serializers_cover_source_attached_paths() proposal.clone(), policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(["wss://relay-c.radroots.test"], SdkRelayUrlPolicy::Public) + .try_with_target_relays(["wss://relay-c.radroots.test"], NostrRelayUrlPolicy::Public) .expect("proposal relays") .with_idempotency_key(SdkIdempotencyKey::new("proposal-unit-key").expect("key")) .with_created_at(created_at); @@ -4118,9 +4123,9 @@ fn order_runtime_request_builders_and_serializers_cover_source_attached_paths() revision_decision, policy.clone(), PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(["wss://relay-d.radroots.test"], SdkRelayUrlPolicy::Public) + .try_with_target_relays(["wss://relay-d.radroots.test"], NostrRelayUrlPolicy::Public) .expect("revision decision relays") .with_idempotency_key(SdkIdempotencyKey::new("revision-decision-unit-key").expect("key")) .with_created_at(created_at); @@ -4142,9 +4147,9 @@ fn order_runtime_request_builders_and_serializers_cover_source_attached_paths() cancellation_payload(), policy, PublishMode::EnqueueOnly, - AckPolicy::NoWait, + SatisfactionPolicy::NoWait, ) - .try_with_target_relays(["wss://relay-e.radroots.test"], SdkRelayUrlPolicy::Public) + .try_with_target_relays(["wss://relay-e.radroots.test"], NostrRelayUrlPolicy::Public) .expect("cancellation relays") .with_idempotency_key(SdkIdempotencyKey::new("cancellation-unit-key").expect("key")) .with_created_at(created_at); diff --git a/crates/sdk/tests/unit/relay_targets_tests.rs b/crates/sdk/tests/unit/relay_targets_tests.rs @@ -1,201 +0,0 @@ -use super::{ - AckPolicy, PublishMode, RelayResolutionPolicy, SdkRelayTargetPolicy, SdkRelayTargetSet, - SdkRelayUrlPolicy, -}; -use crate::{RadrootsSdkError, SDK_RELAY_TARGET_MAX_COUNT}; - -#[path = "../support/serializer_failure.rs"] -mod serializer_failure; - -use serializer_failure::assert_struct_serialize_error_paths; - -#[test] -fn publish_mode_and_ack_policy_serialize_explicit_product_contracts() { - assert_eq!( - serde_json::to_value(PublishMode::DryRun).expect("json"), - serde_json::json!("dry_run") - ); - assert_eq!( - serde_json::to_value(PublishMode::EnqueueOnly).expect("json"), - serde_json::json!("enqueue_only") - ); - assert_eq!( - serde_json::to_value(PublishMode::EnqueueAndPublish).expect("json"), - serde_json::json!("enqueue_and_publish") - ); - assert_eq!( - serde_json::to_value(AckPolicy::NoWait).expect("json"), - serde_json::json!("no_wait") - ); - assert_eq!( - serde_json::to_value(AckPolicy::AtLeastOneRelay).expect("json"), - serde_json::json!("at_least_one_relay") - ); - assert_eq!( - serde_json::to_value(AckPolicy::AllRelays).expect("json"), - serde_json::json!("all_relays") - ); - assert_eq!( - serde_json::to_value(AckPolicy::quorum(2).expect("quorum")).expect("json"), - serde_json::json!({ "quorum": { "required": 2 } }) - ); - assert!(matches!( - AckPolicy::quorum(0), - Err(RadrootsSdkError::InvalidRequest { ref message }) - if message == "ack policy quorum must require at least one relay" - )); -} - -fn is_local_ws_relay(value: &str) -> bool { - let Some(rest) = value.strip_prefix("ws://") else { - return false; - }; - let authority = rest - .split_once('/') - .map(|(authority, _)| authority) - .unwrap_or(rest); - let host = relay_authority_host(authority); - matches!(host.as_deref(), Some("localhost" | "127.0.0.1" | "[::1]")) -} - -fn relay_authority_host(authority: &str) -> Option<String> { - if let Some(after_open) = authority.strip_prefix('[') { - let close_index = after_open.find(']')?; - return Some(format!("[{}]", &after_open[..close_index])); - } - Some( - authority - .split_once(':') - .map(|(host, _)| host) - .unwrap_or(authority) - .to_owned(), - ) -} - -#[test] -fn use_configured_policy_serializes_as_kind_only() { - let trade_policy = RelayResolutionPolicy::ConfiguredRelays; - assert_eq!( - serde_json::to_value(&trade_policy).expect("json"), - serde_json::json!({ "kind": "configured_relays" }) - ); - assert_struct_serialize_error_paths(&trade_policy, 1); - - let policy = SdkRelayTargetPolicy::UseConfiguredRelays; - assert_eq!( - serde_json::to_value(&policy).expect("json"), - serde_json::json!({ "kind": "use_configured_relays" }) - ); - assert_struct_serialize_error_paths(&policy, 1); - - let publish_transport_policy = SdkRelayTargetPolicy::use_publish_transport(); - assert_eq!( - serde_json::to_value(&publish_transport_policy).expect("json"), - serde_json::json!({ "kind": "use_publish_transport" }) - ); - assert_struct_serialize_error_paths(&publish_transport_policy, 1); -} - -#[test] -fn target_set_accessors_and_configured_relays_cover_empty_and_dedupe_paths() { - assert_eq!( - SdkRelayTargetSet::from_configured_relays(Vec::<String>::new(), SdkRelayUrlPolicy::Public) - .expect("empty configured"), - Vec::<String>::new() - ); - - let targets = SdkRelayTargetSet::from_normalized_relays(vec![ - "wss://relay-a.example.com".to_owned(), - "wss://relay-a.example.com".to_owned(), - "wss://relay-b.example.com".to_owned(), - ]) - .expect("targets"); - - assert_eq!(targets.len(), 2); - assert!(!targets.is_empty()); - assert_struct_serialize_error_paths(&targets, 2); - assert_struct_serialize_error_paths(&SdkRelayTargetPolicy::explicit(targets.clone()), 3); - assert_eq!( - serde_json::to_value(&targets).expect("targets json"), - serde_json::json!({ - "relays": ["wss://relay-a.example.com", "wss://relay-b.example.com"], - "canonical_relays": ["wss://relay-a.example.com", "wss://relay-b.example.com"] - }) - ); - assert_eq!( - targets.into_vec(), - vec![ - "wss://relay-a.example.com".to_owned(), - "wss://relay-b.example.com".to_owned() - ] - ); - - assert_eq!( - SdkRelayTargetPolicy::try_explicit( - vec!["wss://relay-c.example.com".to_owned()], - SdkRelayUrlPolicy::Public, - ) - .expect("explicit policy"), - SdkRelayTargetPolicy::Explicit( - SdkRelayTargetSet::new(["wss://relay-c.example.com"], SdkRelayUrlPolicy::Public) - .expect("target set"), - ) - ); - assert_eq!( - serde_json::to_value( - RelayResolutionPolicy::try_explicit( - vec!["wss://relay-c.example.com".to_owned()], - SdkRelayUrlPolicy::Public, - ) - .expect("trade explicit policy") - ) - .expect("trade policy json"), - serde_json::json!({ - "kind": "explicit", - "relays": ["wss://relay-c.example.com"], - "canonical_relays": ["wss://relay-c.example.com"] - }) - ); -} - -#[test] -fn normalized_relays_reject_empty_and_over_limit_sets() { - assert!(matches!( - SdkRelayTargetSet::from_normalized_relays(Vec::new()), - Err(RadrootsSdkError::EmptyTargetRelays { .. }) - )); - - let too_many = (0..=SDK_RELAY_TARGET_MAX_COUNT) - .map(|index| format!("wss://relay-{index}.example.com")) - .collect::<Vec<_>>(); - assert!(matches!( - SdkRelayTargetSet::from_normalized_relays(too_many), - Err(RadrootsSdkError::RelayTargetLimitExceeded { actual, .. }) - if actual == SDK_RELAY_TARGET_MAX_COUNT + 1 - )); -} - -#[test] -fn local_ws_authority_parser_handles_ipv6_ports_and_non_ws_values() { - assert!(is_local_ws_relay("ws://localhost:8080/path")); - assert!(is_local_ws_relay("ws://127.0.0.1:8080")); - assert!(is_local_ws_relay("ws://[::1]:8080")); - assert!(!is_local_ws_relay("wss://relay.example.com")); - assert!(!is_local_ws_relay("ws://relay.example.com")); - assert!(matches!( - SdkRelayTargetSet::new(["ws://relay.example.com"], SdkRelayUrlPolicy::Localhost), - Err(RadrootsSdkError::InvalidRelayUrl { reason, .. }) - if reason.contains("localhost") - )); - assert!(matches!( - SdkRelayTargetSet::new(["ws://relay.example.com"], SdkRelayUrlPolicy::Public), - Err(RadrootsSdkError::InvalidRelayUrl { reason, .. }) - if reason.contains("localhost") - )); - assert_eq!(relay_authority_host("[::1]:8080"), Some("[::1]".to_owned())); - assert_eq!( - relay_authority_host("relay.example.com:443"), - Some("relay.example.com".to_owned()) - ); - assert_eq!(relay_authority_host("[::1"), None); -} diff --git a/crates/sdk/tests/unit/runtime_tests.rs b/crates/sdk/tests/unit/runtime_tests.rs @@ -71,6 +71,13 @@ fn assert_wal_checkpoint_complete(receipt: &SdkSqliteWalCheckpointReceipt) { assert!(receipt.checkpoint_complete); } +fn nostr_profile( + relays: impl IntoIterator<Item = &'static str>, + policy: crate::NostrRelayUrlPolicy, +) -> crate::TransportProfile { + crate::TransportProfile::nostr(crate::NostrProfile::new(relays, policy).expect("Nostr profile")) +} + fn storage_status() -> StorageStatusReceipt { StorageStatusReceipt { storage: SdkStorageKind::Memory, @@ -258,16 +265,20 @@ async fn private_store_validates_location_rows_and_round_trips_valid_records() { #[test] fn publish_transport_defaults_and_delegated_resolution_are_explicit() { - let direct = SdkPublishTransport::default(); - assert_eq!(direct, SdkPublishTransport::DirectNostrRelay); - assert!(!direct.supports_delegated_relay_resolution()); + let local = TransportProfile::default(); + assert_eq!(local, TransportProfile::LocalOnly); + assert!(!local.supports_delegated_target_resolution()); + + let nostr = nostr_profile( + ["wss://relay.example.com"], + crate::NostrRelayUrlPolicy::Public, + ); + assert!(!nostr.supports_delegated_target_resolution()); #[cfg(feature = "radrootsd-proxy")] { - let proxy = SdkPublishTransport::RadrootsdProxy(RadrootsdProxyConfig::new( - "http://127.0.0.1:9/rpc", - )); - assert!(proxy.supports_delegated_relay_resolution()); + let proxy = TransportProfile::proxy(crate::ProxyProfile::new("http://127.0.0.1:9/rpc")); + assert!(proxy.supports_delegated_target_resolution()); } } @@ -284,8 +295,7 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures( storage_paths: None, geonames: None, clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)), - relay_urls: Vec::new(), - publish_transport: SdkPublishTransport::DirectNostrRelay, + transport_profile: TransportProfile::local_only(), #[cfg(feature = "signer-adapters")] signer_provider: None, }; @@ -307,8 +317,7 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures( storage_paths: Some(directory_paths), geonames: None, clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)), - relay_urls: Vec::new(), - publish_transport: SdkPublishTransport::DirectNostrRelay, + transport_profile: TransportProfile::local_only(), #[cfg(feature = "signer-adapters")] signer_provider: None, }; @@ -354,8 +363,10 @@ async fn runtime_public_surface_covers_builders_status_integrity_backup_and_rest .clock(RadrootsSdkClock::Fixed( RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000), )) - .relay_url_policy(SdkRelayUrlPolicy::Localhost) - .relay_url("ws://127.0.0.1:7777") + .transport_profile(nostr_profile( + ["ws://127.0.0.1:7777"], + crate::NostrRelayUrlPolicy::Localhost, + )) .build() .await .expect("memory sdk"); @@ -363,7 +374,10 @@ async fn runtime_public_surface_covers_builders_status_integrity_backup_and_rest memory_sdk.now().expect("fixed now").unix_seconds(), 1_700_000_000 ); - assert_eq!(memory_sdk.relay_urls(), ["ws://127.0.0.1:7777"]); + assert_eq!( + memory_sdk.configured_nostr_relay_urls(), + ["ws://127.0.0.1:7777"] + ); assert!(memory_sdk.storage_paths().is_none()); let _ = memory_sdk.farms(); let _ = memory_sdk.listings(); diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -1,7 +1,8 @@ #[cfg(feature = "radrootsd-proxy")] use super::{ - CLAIM_OWNER, complete_proxy_publish_attempt, proxy_delivery_policy, proxy_error_message, - proxy_outbox_idempotency_key, proxy_transport_error_receipt, push_proxy_claimed_outbox_event, + CLAIM_OWNER, complete_proxy_publish_attempt, proxy_delivery_policy_from_satisfaction, + proxy_error_message, proxy_outbox_idempotency_key, proxy_transport_error_receipt, + push_proxy_claimed_outbox_event, }; use super::{ PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRelayOutcomeKind, @@ -39,7 +40,9 @@ use radroots_outbox::RadrootsOutboxClaimedEvent; use radroots_outbox::{RadrootsOutboxEventState, RadrootsOutboxStatusSummary}; #[cfg(feature = "radrootsd-proxy")] use radroots_publish_proxy_protocol::PublishDeliveryPolicy; -use radroots_relay_transport::{ +#[cfg(feature = "radrootsd-proxy")] +use radroots_transport::RadrootsTransportSatisfactionPolicy; +use radroots_transport_nostr::{ RadrootsRelayOutcomeKind, RadrootsRelayPublishAdapter, RadrootsRelayPublishReceipt, RadrootsRelayPublishRelayReceipt, RadrootsRelayPublishRequest, RadrootsRelayTransportError, }; @@ -139,11 +142,12 @@ async fn claimed_proxy_event(d_tag: &str) -> (crate::RadrootsClient, RadrootsOut operation_kind: "sync.proxy.unit.v1", actor: &actor, frozen_draft: &draft, - target_relays: crate::SdkRelayTargetPolicy::try_explicit( + target_relays: crate::TargetPolicy::try_nostr_relays( ["wss://relay.example.com"], - crate::SdkRelayUrlPolicy::Public, + crate::NostrRelayUrlPolicy::Public, ) .expect("target relays"), + satisfaction_policy: crate::SatisfactionPolicy::AllTargets, idempotency_key: None, }, &ProxyFixtureSigner::new(), @@ -308,7 +312,7 @@ fn sync_status_summary_conversions_preserve_all_fields() { let event_summary = RadrootsEventStoreStatusSummary { total_events: 11, projection_eligible_events: 7, - relay_observations: 3, + transport_observations: 3, last_event_seq: Some(9), last_event_updated_at_ms: Some(1_700_000_000_000), }; @@ -352,7 +356,7 @@ fn push_outbox_request_builders_validate_all_bounds() { .with_outbox_event_id(9) .republish_accepted_relays(true) .with_accepted_quorum(2) - .with_relay_url_policy(crate::SdkRelayUrlPolicy::Localhost) + .with_relay_url_policy(crate::NostrRelayUrlPolicy::Localhost) .with_auth_policy(SdkRelayAuthPolicy::DetectOnly) .with_claim_ttl_ms(7) .with_next_attempt_delay_ms(11); @@ -506,11 +510,26 @@ async fn proxy_push_empty_queue_and_private_helpers_are_deterministic() { .expect("empty proxy push"); assert_eq!(receipt.attempted_events, 0); - assert_eq!(proxy_delivery_policy(0, None), PublishDeliveryPolicy::Any); - assert_eq!(proxy_delivery_policy(2, None), PublishDeliveryPolicy::All); assert_eq!( - proxy_delivery_policy(3, Some(2)), - PublishDeliveryPolicy::Quorum { quorum: 2 } + proxy_delivery_policy_from_satisfaction( + 0, + &RadrootsTransportSatisfactionPolicy::AllTargets + ) + .expect("zero-target proxy policy"), + PublishDeliveryPolicy::Any + ); + assert_eq!( + proxy_delivery_policy_from_satisfaction( + 2, + &RadrootsTransportSatisfactionPolicy::AllTargets + ) + .expect("all-target proxy policy"), + PublishDeliveryPolicy::All + ); + assert_eq!( + proxy_delivery_policy_from_satisfaction(2, &RadrootsTransportSatisfactionPolicy::AnyTarget) + .expect("any-target proxy policy"), + PublishDeliveryPolicy::Any ); assert_eq!( proxy_outbox_idempotency_key(7, 3, "event-id"), @@ -594,11 +613,11 @@ async fn proxy_push_reports_missing_signed_claim_before_daemon_publish() { expected_event_id: "b".repeat(64), }, signed_event: None, - target_relays: vec!["wss://relay.example.com".to_owned()], + delivery_targets: Vec::new(), }; assert!(matches!( - push_proxy_claimed_outbox_event(&sync, &adapter, &claimed, None, 60_000, 1_700_000_000_000) + push_proxy_claimed_outbox_event(&sync, &adapter, &claimed, 60_000, 1_700_000_000_000) .await, Err(RadrootsSdkError::RelayTransport { message }) if message.contains("Outbox claim 41 does not contain a signed event") @@ -613,7 +632,7 @@ async fn proxy_claim_publish_marks_retryable_transport_errors() { let adapter = RadrootsdProxyPublishAdapter::new(RadrootsdProxyConfig::new("http://127.0.0.1:9/rpc")); let receipt = - push_proxy_claimed_outbox_event(&sync, &adapter, &claimed, None, 60_000, 1_700_000_000_000) + push_proxy_claimed_outbox_event(&sync, &adapter, &claimed, 60_000, 1_700_000_000_000) .await .expect("transport error receipt"); @@ -706,9 +725,9 @@ trait RelayReceiptFixture { impl RelayReceiptFixture for RadrootsRelayPublishReceipt { fn with_relay(mut self) -> Self { self.relays.push( - radroots_relay_transport::RadrootsRelayPublishRelayReceipt::attempted( + radroots_transport_nostr::RadrootsRelayPublishRelayReceipt::attempted( "wss://relay.example.com", - radroots_relay_transport::RadrootsRelayOutcome::accepted(), + radroots_transport_nostr::RadrootsRelayOutcome::accepted(), ), ); self diff --git a/crates/sdk/tests/unit/transport_tests.rs b/crates/sdk/tests/unit/transport_tests.rs @@ -0,0 +1,213 @@ +use super::{ + NostrProfile, NostrRelayUrlPolicy, PublishMode, SatisfactionPolicy, TargetPolicy, TargetSet, + TransportProfile, +}; +use crate::{RadrootsSdkError, SDK_TRANSPORT_TARGET_MAX_COUNT}; + +#[path = "../support/serializer_failure.rs"] +mod serializer_failure; + +use serializer_failure::assert_struct_serialize_error_paths; + +#[test] +fn publish_mode_and_ack_policy_serialize_explicit_product_contracts() { + assert_eq!( + serde_json::to_value(PublishMode::DryRun).expect("json"), + serde_json::json!("dry_run") + ); + assert_eq!( + serde_json::to_value(PublishMode::EnqueueOnly).expect("json"), + serde_json::json!("enqueue_only") + ); + assert_eq!( + serde_json::to_value(PublishMode::EnqueueAndPublish).expect("json"), + serde_json::json!("enqueue_and_publish") + ); + assert_eq!( + serde_json::to_value(SatisfactionPolicy::NoWait).expect("json"), + serde_json::json!("no_wait") + ); + assert_eq!( + serde_json::to_value(SatisfactionPolicy::AtLeastOneTarget).expect("json"), + serde_json::json!("at_least_one_target") + ); + assert_eq!( + serde_json::to_value(SatisfactionPolicy::AllTargets).expect("json"), + serde_json::json!("all_targets") + ); + assert_eq!( + serde_json::to_value(SatisfactionPolicy::at_least(2).expect("satisfaction policy")) + .expect("json"), + serde_json::json!({ "at_least": { "required": 2 } }) + ); + assert!(matches!( + SatisfactionPolicy::at_least(0), + Err(RadrootsSdkError::InvalidRequest { ref message }) + if message == "satisfaction policy must require at least one target" + )); +} + +fn is_local_ws_relay(value: &str) -> bool { + let Some(rest) = value.strip_prefix("ws://") else { + return false; + }; + let authority = rest + .split_once('/') + .map(|(authority, _)| authority) + .unwrap_or(rest); + let host = relay_authority_host(authority); + matches!(host.as_deref(), Some("localhost" | "127.0.0.1" | "[::1]")) +} + +fn relay_authority_host(authority: &str) -> Option<String> { + if let Some(after_open) = authority.strip_prefix('[') { + let close_index = after_open.find(']')?; + return Some(format!("[{}]", &after_open[..close_index])); + } + Some( + authority + .split_once(':') + .map(|(host, _)| host) + .unwrap_or(authority) + .to_owned(), + ) +} + +#[test] +fn use_configured_policy_serializes_as_kind_only() { + let policy = TargetPolicy::UseConfiguredProfile; + assert_eq!( + serde_json::to_value(&policy).expect("json"), + serde_json::json!({ "kind": "use_configured_profile" }) + ); + assert_struct_serialize_error_paths(&policy, 1); + + let publish_transport_policy = TargetPolicy::use_transport_profile(); + assert_eq!( + serde_json::to_value(&publish_transport_policy).expect("json"), + serde_json::json!({ "kind": "use_transport_profile" }) + ); + assert_struct_serialize_error_paths(&publish_transport_policy, 1); +} + +#[test] +fn target_set_accessors_and_configured_relays_cover_empty_and_dedupe_paths() { + assert!( + TransportProfile::local_only() + .configured_nostr_relay_urls() + .is_empty() + ); + + let targets = TargetSet::from_normalized_nostr_relays(vec![ + "wss://relay-a.example.com".to_owned(), + "wss://relay-a.example.com".to_owned(), + "wss://relay-b.example.com".to_owned(), + ]) + .expect("targets"); + + assert_eq!(targets.len(), 2); + assert!(!targets.is_empty()); + assert_struct_serialize_error_paths(&targets, 2); + assert_struct_serialize_error_paths(&TargetPolicy::explicit(targets.clone()), 3); + let targets_json = serde_json::to_value(&targets).expect("targets json"); + assert_eq!( + targets_json["targets"].as_array().expect("targets").len(), + 2 + ); + assert_eq!( + targets_json["canonical_targets"] + .as_array() + .expect("canonical targets") + .len(), + 2 + ); + assert_eq!( + targets.nostr_relay_urls(), + vec![ + "wss://relay-a.example.com".to_owned(), + "wss://relay-b.example.com".to_owned() + ] + ); + + assert_eq!( + TargetPolicy::try_nostr_relays( + vec!["wss://relay-c.example.com".to_owned()], + NostrRelayUrlPolicy::Public, + ) + .expect("explicit policy"), + TargetPolicy::Explicit( + TargetSet::new(["wss://relay-c.example.com"], NostrRelayUrlPolicy::Public) + .expect("target set"), + ) + ); + assert_eq!( + serde_json::to_value( + TargetPolicy::try_nostr_relays( + vec!["wss://relay-c.example.com".to_owned()], + NostrRelayUrlPolicy::Public, + ) + .expect("trade explicit policy") + ) + .expect("trade policy json"), + serde_json::json!({ + "kind": "explicit", + "targets": [{ + "kind": "Nostr", + "uri": "wss://relay-c.example.com", + "fingerprint": "ec4b5005dd1fcf0d949045e3d5524f9a6a95209ecc888f582ae2e9bf69e5b8e6" + }], + "canonical_targets": ["ec4b5005dd1fcf0d949045e3d5524f9a6a95209ecc888f582ae2e9bf69e5b8e6"] + }) + ); + + let nostr_profile = TransportProfile::nostr( + NostrProfile::new(["wss://relay-d.example.com"], NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + ); + assert_eq!( + nostr_profile.configured_nostr_relay_urls(), + vec!["wss://relay-d.example.com".to_owned()] + ); +} + +#[test] +fn normalized_relays_reject_empty_and_over_limit_sets() { + assert!(matches!( + TargetSet::from_normalized_nostr_relays(Vec::new()), + Err(RadrootsSdkError::EmptyTargetRelays { .. }) + )); + + let too_many = (0..=SDK_TRANSPORT_TARGET_MAX_COUNT) + .map(|index| format!("wss://relay-{index}.example.com")) + .collect::<Vec<_>>(); + assert!(matches!( + TargetSet::from_normalized_nostr_relays(too_many), + Err(RadrootsSdkError::RelayTargetLimitExceeded { actual, .. }) + if actual == SDK_TRANSPORT_TARGET_MAX_COUNT + 1 + )); +} + +#[test] +fn local_ws_authority_parser_handles_ipv6_ports_and_non_ws_values() { + assert!(is_local_ws_relay("ws://localhost:8080/path")); + assert!(is_local_ws_relay("ws://127.0.0.1:8080")); + assert!(is_local_ws_relay("ws://[::1]:8080")); + assert!(!is_local_ws_relay("wss://relay.example.com")); + assert!(!is_local_ws_relay("ws://relay.example.com")); + assert!(matches!( + TargetSet::new(["ws://relay.example.com"], NostrRelayUrlPolicy::Localhost), + Err(RadrootsSdkError::InvalidRelayUrl { reason, .. }) + if reason.contains("localhost") + )); + assert!(matches!( + TargetSet::new(["ws://relay.example.com"], NostrRelayUrlPolicy::Public), + Err(RadrootsSdkError::InvalidRelayUrl { reason, .. }) + if reason.contains("localhost") + )); + assert_eq!(relay_authority_host("[::1]:8080"), Some("[::1]".to_owned())); + assert_eq!( + relay_authority_host("relay.example.com:443"), + Some("relay.example.com".to_owned()) + ); + assert_eq!(relay_authority_host("[::1"), None); +} diff --git a/crates/sdk/tests/unit/workflow_runtime_tests.rs b/crates/sdk/tests/unit/workflow_runtime_tests.rs @@ -84,6 +84,27 @@ fn signed_event() -> RadrootsSignedNostrEvent { } } +fn nostr_profile(relay: &'static str) -> crate::TransportProfile { + crate::TransportProfile::nostr( + crate::NostrProfile::new([relay], crate::NostrRelayUrlPolicy::Public) + .expect("Nostr profile"), + ) +} + +fn workflow_delivery_plan() -> radroots_outbox::RadrootsOutboxDeliveryPlanInput { + let target_set = TargetSet::nostr_relays( + ["wss://relay.example.com"], + crate::NostrRelayUrlPolicy::Public, + ) + .expect("target set"); + radroots_outbox::RadrootsOutboxDeliveryPlanInput::new( + "explicit", + 1, + radroots_transport::RadrootsTransportSatisfactionPolicy::AllTargets, + target_set.into_targets(), + ) +} + #[test] fn workflow_digest_and_event_helpers_cover_error_and_input_paths() { assert_eq!(digest_prefix("abcdef1234567890"), "abcdef123456"); @@ -115,16 +136,15 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() { "workflow.test.v1", &draft, signed_event(), - vec!["wss://relay.example.com".to_owned()], + workflow_delivery_plan(), idempotency_key, - false, true, 1_700_000_000_000, ); assert_eq!(input.operation_kind, "workflow.test.v1"); assert_eq!( - input.target_relays, - vec!["wss://relay.example.com".to_owned()] + input.delivery_plan.targets[0].uri.as_str(), + "wss://relay.example.com" ); assert!(input.event_store_inserted); } @@ -132,7 +152,7 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() { #[tokio::test] async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_conflicts() { let sdk = crate::RadrootsClient::builder() - .relay_url("wss://relay.example.com") + .transport_profile(nostr_profile("wss://relay.example.com")) .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds( 1_700_000_010, )) @@ -150,7 +170,8 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con operation_kind: "workflow.test.v1", actor: &actor, frozen_draft: &first_draft, - target_relays: SdkRelayTargetPolicy::UseConfiguredRelays, + target_relays: TargetPolicy::UseConfiguredProfile, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key: Some(idempotency_key.clone()), }, &signer, @@ -190,7 +211,8 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con operation_kind: "workflow.test.v1", actor: &actor, frozen_draft: &second_draft, - target_relays: SdkRelayTargetPolicy::UseConfiguredRelays, + target_relays: TargetPolicy::UseConfiguredProfile, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key: Some(idempotency_key), }, &signer, @@ -232,7 +254,7 @@ async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() { let secret_key = RadrootsNostrSecretKey::from_hex(FARMER_SECRET_KEY_HEX).expect("secret key"); let keys = RadrootsNostrKeys::new(secret_key); let sdk = crate::RadrootsClient::builder() - .relay_url("wss://relay.example.com") + .transport_profile(nostr_profile("wss://relay.example.com")) .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds( 1_700_000_011, )) @@ -252,7 +274,8 @@ async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() { operation_kind: "workflow.test.v1", actor: &actor, frozen_draft: &draft, - target_relays: SdkRelayTargetPolicy::UseConfiguredRelays, + target_relays: TargetPolicy::UseConfiguredProfile, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key: None, }, ) @@ -266,7 +289,7 @@ async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() { #[tokio::test] async fn enqueue_signed_workflow_reports_outbox_preflight_failure_without_mutation() { let sdk = crate::RadrootsClient::builder() - .relay_url("wss://relay.example.com") + .transport_profile(nostr_profile("wss://relay.example.com")) .build() .await .expect("sdk"); @@ -286,7 +309,8 @@ async fn enqueue_signed_workflow_reports_outbox_preflight_failure_without_mutati operation_kind: "workflow.test.v1", actor: &actor, frozen_draft: &draft, - target_relays: SdkRelayTargetPolicy::UseConfiguredRelays, + target_relays: TargetPolicy::UseConfiguredProfile, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key: None, }; @@ -312,7 +336,7 @@ async fn enqueue_signed_workflow_reports_store_failures() { .expect("actor"); let draft = frozen_draft_for(FARMER_PUBLIC_KEY_HEX); let closed_store_sdk = crate::RadrootsClient::builder() - .relay_url("wss://relay.example.com") + .transport_profile(nostr_profile("wss://relay.example.com")) .build() .await .expect("sdk"); @@ -321,7 +345,8 @@ async fn enqueue_signed_workflow_reports_store_failures() { operation_kind: "workflow.test.v1", actor: &actor, frozen_draft: &draft, - target_relays: SdkRelayTargetPolicy::UseConfiguredRelays, + target_relays: TargetPolicy::UseConfiguredProfile, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key: None, }; assert!(matches!( @@ -339,7 +364,7 @@ async fn enqueue_signed_workflow_reports_store_failures() { async fn enqueue_signed_workflow_reports_clock_failures() { let sdk = crate::RadrootsClient::builder() .clock(crate::RadrootsSdkClock::BeforeUnixEpoch) - .relay_url("wss://relay.example.com") + .transport_profile(nostr_profile("wss://relay.example.com")) .build() .await .expect("sdk"); @@ -350,7 +375,8 @@ async fn enqueue_signed_workflow_reports_clock_failures() { operation_kind: "workflow.test.v1", actor: &actor, frozen_draft: &draft, - target_relays: SdkRelayTargetPolicy::UseConfiguredRelays, + target_relays: TargetPolicy::UseConfiguredProfile, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key: None, }; assert!(matches!( @@ -369,13 +395,14 @@ async fn enqueue_signed_workflow_rejects_publish_transport_targets_without_proxy operation_kind: "workflow.test.v1", actor: &actor, frozen_draft: &draft, - target_relays: SdkRelayTargetPolicy::UsePublishTransport, + target_relays: TargetPolicy::UseTransportProfile, + satisfaction_policy: SatisfactionPolicy::AllTargets, idempotency_key: None, }; assert!(matches!( enqueue_signed_workflow(&sdk, request, &WorkflowSigner::new()).await, Err(RadrootsSdkError::EmptyTargetRelays { operation }) - if operation == "publish transport relay resolution" + if operation == "publish transport profile" )); }