sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit a91221f03664e4d45556268b4d5014c0be421e92
parent 0a33a01d2ba94c67d19e2677819b1f29882ec679
Author: triesap <tyson@radroots.org>
Date:   Mon, 15 Jun 2026 16:48:45 -0700

sdk: split listing publish runtime requests

- replace the broad listing publish request with prepare and enqueue contracts
- return typed listing publish plans with event and address identifiers
- separate relay target selection from relay URL validation policy
- update runtime tests and examples for the product listing API

Diffstat:
Mcrates/sdk/examples/runtime_local.rs | 22++++++++++++++++------
Mcrates/sdk/src/lib.rs | 5+++--
Mcrates/sdk/src/listings_runtime.rs | 182++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mcrates/sdk/src/runtime.rs | 12++++++------
Mcrates/sdk/src/runtime_targets.rs | 33++++++++++++++++++++++++++++-----
Mcrates/sdk/tests/listings_runtime.rs | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/sdk/tests/runtime_foundation.rs | 12++++++------
Mcrates/sdk/tests/sync_runtime.rs | 27+++++++++++++++------------
8 files changed, 291 insertions(+), 119 deletions(-)

diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs @@ -16,7 +16,8 @@ use radroots_sdk::protocol::listing::{ RadrootsListing, RadrootsListingBin, RadrootsListingProduct, }; use radroots_sdk::{ - ListingPublishRequest, OrderStatusRequest, PushOutboxRequest, RadrootsSdk, RadrootsSdkTimestamp, + ListingEnqueuePublishRequest, ListingPreparePublishRequest, OrderStatusRequest, + PushOutboxRequest, RadrootsSdk, RadrootsSdkTimestamp, SdkRelayTargetPolicy, }; const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -79,13 +80,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { .build() .await?; let actor = RadrootsActorContext::test(SELLER, [RadrootsActorRole::Seller])?; - let request = - ListingPublishRequest::new(sample_listing()).try_with_idempotency_key("example-1")?; + let listing = sample_listing(); + let prepare_request = ListingPreparePublishRequest::new(actor.clone(), listing.clone()); + let enqueue_request = ListingEnqueuePublishRequest::new( + actor, + listing, + SdkRelayTargetPolicy::UseConfiguredRelays, + ) + .try_with_idempotency_key("example-1")?; - let prepared = sdk.listings().prepare_publish(&actor, request.clone())?; + let prepared = sdk.listings().prepare_publish(prepare_request)?; let enqueue = sdk .listings() - .enqueue_publish(&actor, &FixtureSigner::new(SELLER), request) + .enqueue_publish(enqueue_request, &FixtureSigner::new(SELLER)) .await?; let push = sdk .sync() @@ -99,7 +106,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { .status(OrderStatusRequest::new("example-order-1")) .await?; - assert_eq!(prepared.listing_address, enqueue.listing_address); + assert_eq!( + prepared.public_listing_addr.as_str(), + enqueue.listing_address.as_str() + ); assert_eq!(push.attempted_events, 1); assert!(!order_status.found); Ok(()) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -55,7 +55,8 @@ pub use crate::error::{ }; #[cfg(feature = "runtime")] pub use crate::listings_runtime::{ - ListingEnqueueReceipt, ListingPublishRequest, PreparedListingPublish, + ListingEnqueuePublishRequest, ListingEnqueueReceipt, ListingPreparePublishRequest, + ListingPublishPlan, }; #[cfg(feature = "runtime")] pub use crate::orders_runtime::{ @@ -75,7 +76,7 @@ pub use crate::runtime::{ #[cfg(feature = "runtime")] pub use crate::runtime_targets::{ SDK_IDEMPOTENCY_KEY_MAX_LEN, SDK_RELAY_TARGET_MAX_COUNT, SdkIdempotencyKey, - SdkRelayTargetPolicy, SdkRelayTargetSet, + SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy, }; #[cfg(feature = "runtime")] pub use crate::sync_runtime::{ diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -2,7 +2,7 @@ use crate::{ ListingsClient, RadrootsSdkError, RadrootsSdkEventReference, RadrootsSdkLocalMutationReceipt, RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkRelayTargetPolicy, - SdkRelayTargetSet, + SdkRelayTargetSet, SdkRelayUrlPolicy, }; #[cfg(feature = "runtime")] use radroots_authority::{RadrootsActorContext, RadrootsEventSigner, sign_authorized_draft}; @@ -12,6 +12,7 @@ use radroots_event_store::RadrootsEventIngest; use radroots_events::{ RadrootsNostrEvent, draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent}, + ids::{RadrootsEventId, RadrootsListingAddress}, listing::RadrootsListing, }; #[cfg(feature = "runtime")] @@ -27,37 +28,87 @@ const LISTING_PUBLISH_OPERATION_KIND: &str = "listing.publish.v1"; #[cfg(feature = "runtime")] #[derive(Clone, Debug)] -pub struct ListingPublishRequest { - pub listing: RadrootsListing, - pub target_relays: Option<SdkRelayTargetSet>, - pub idempotency_key: Option<SdkIdempotencyKey>, +pub struct ListingPreparePublishRequest { + pub actor: RadrootsActorContext, + pub document: RadrootsListingDraftDocumentV1, + pub created_at: Option<RadrootsSdkTimestamp>, } #[cfg(feature = "runtime")] -impl ListingPublishRequest { - pub fn new(listing: RadrootsListing) -> Self { +impl ListingPreparePublishRequest { + pub fn new(actor: RadrootsActorContext, listing: RadrootsListing) -> Self { Self { - listing, - target_relays: None, - idempotency_key: None, + actor, + document: RadrootsListingDraftDocumentV1::new(listing), + created_at: None, } } - pub fn with_target_relays(mut self, target_relays: SdkRelayTargetSet) -> Self { - self.target_relays = Some(target_relays); + pub fn from_document( + actor: RadrootsActorContext, + document: RadrootsListingDraftDocumentV1, + ) -> Self { + Self { + actor, + document, + created_at: None, + } + } + + pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self { + self.created_at = Some(created_at); self } +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Debug)] +pub struct ListingEnqueuePublishRequest { + pub actor: RadrootsActorContext, + pub document: RadrootsListingDraftDocumentV1, + pub target_relays: SdkRelayTargetPolicy, + pub idempotency_key: Option<SdkIdempotencyKey>, + pub created_at: Option<RadrootsSdkTimestamp>, +} + +#[cfg(feature = "runtime")] +impl ListingEnqueuePublishRequest { + pub fn new( + actor: RadrootsActorContext, + listing: RadrootsListing, + target_relays: SdkRelayTargetPolicy, + ) -> Self { + Self::from_document( + actor, + RadrootsListingDraftDocumentV1::new(listing), + target_relays, + ) + } + + pub fn from_document( + actor: RadrootsActorContext, + document: RadrootsListingDraftDocumentV1, + target_relays: SdkRelayTargetPolicy, + ) -> Self { + Self { + actor, + document, + target_relays, + idempotency_key: None, + created_at: None, + } + } pub fn try_with_target_relays<I, S>( mut self, target_relays: I, - policy: SdkRelayTargetPolicy, + policy: SdkRelayUrlPolicy, ) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, { - self.target_relays = Some(SdkRelayTargetSet::new(target_relays, policy)?); + self.target_relays = SdkRelayTargetPolicy::try_explicit(target_relays, policy)?; Ok(self) } @@ -73,13 +124,20 @@ impl ListingPublishRequest { self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?); Ok(self) } + + pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self { + self.created_at = Some(created_at); + self + } } #[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct PreparedListingPublish { - pub draft: RadrootsFrozenEventDraft, - pub listing_address: String, +pub struct ListingPublishPlan { + pub public_listing_addr: RadrootsListingAddress, + pub draft_listing_addr: RadrootsListingAddress, + pub expected_event_id: RadrootsEventId, + pub frozen_draft: RadrootsFrozenEventDraft, pub created_at: RadrootsSdkTimestamp, } @@ -94,51 +152,41 @@ pub struct ListingEnqueueReceipt { impl<'sdk> ListingsClient<'sdk> { pub fn prepare_publish( &self, - actor: &RadrootsActorContext, - request: ListingPublishRequest, - ) -> Result<PreparedListingPublish, RadrootsSdkError> { - let created_at = self.sdk.now()?; - let created_at_nostr = created_at.try_into_nostr_created_at()?; - let canonical = canonical_listing_draft(actor, request.listing)?; - let mutation = RadrootsListingMutation::publish(canonical); - let listing_address = mutation.listing_addr()?.as_str().to_owned(); - let draft = build_listing_mutation_draft(&mutation, created_at_nostr)?; - Ok(PreparedListingPublish { - draft, - listing_address, - created_at, - }) + request: ListingPreparePublishRequest, + ) -> Result<ListingPublishPlan, RadrootsSdkError> { + let created_at = self.resolved_created_at(request.created_at)?; + listing_publish_plan(&request.actor, request.document, created_at) } pub async fn enqueue_publish<S>( &self, - actor: &RadrootsActorContext, + request: ListingEnqueuePublishRequest, signer: &S, - request: ListingPublishRequest, ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> where S: RadrootsEventSigner + ?Sized, { - let target_relays = self.resolved_target_relays(&request)?; + let target_relays = self.resolved_target_relays(&request.target_relays)?; let idempotency_key = request.idempotency_key.clone(); - let prepared = self.prepare_publish(actor, request)?; - let signed_event = sign_authorized_draft(actor, signer, &prepared.draft)?; + let created_at = self.resolved_created_at(request.created_at)?; + let plan = listing_publish_plan(&request.actor, request.document, created_at)?; + let signed_event = sign_authorized_draft(&request.actor, signer, &plan.frozen_draft)?; let idempotency_key = match idempotency_key { Some(idempotency_key) => idempotency_key, None => SdkIdempotencyKey::derive( LISTING_PUBLISH_OPERATION_KIND, - prepared.draft.expected_event_id.as_str(), - prepared.draft.expected_pubkey.as_str(), + plan.frozen_draft.expected_event_id.as_str(), + plan.frozen_draft.expected_pubkey.as_str(), target_relays.relays(), )?, }; - let observed_at_ms = i64::from(prepared.draft.created_at) * 1_000; + let observed_at_ms = i64::from(plan.frozen_draft.created_at) * 1_000; let event = event_from_signed(&signed_event); let ingest = RadrootsEventIngest::new(event, observed_at_ms) .with_raw_json(signed_event.raw_json.clone()); let ingest_receipt = self.sdk._event_store.ingest_event(ingest).await?; let outbox_input = signed_outbox_input( - &prepared, + &plan, signed_event.clone(), target_relays.into_vec(), idempotency_key, @@ -158,7 +206,7 @@ impl<'sdk> ListingsClient<'sdk> { ) })?; Ok(ListingEnqueueReceipt { - listing_address: prepared.listing_address, + listing_address: plan.public_listing_addr.into_string(), local: RadrootsSdkLocalMutationReceipt { event: RadrootsSdkEventReference { event_id: signed_event.id, @@ -178,11 +226,23 @@ impl<'sdk> ListingsClient<'sdk> { fn resolved_target_relays( &self, - request: &ListingPublishRequest, + target_relays: &SdkRelayTargetPolicy, ) -> Result<SdkRelayTargetSet, RadrootsSdkError> { - match request.target_relays.as_ref() { - Some(target_relays) => Ok(target_relays.clone()), - None => SdkRelayTargetSet::from_normalized_relays(self.sdk.relay_urls().to_vec()), + match target_relays { + SdkRelayTargetPolicy::Explicit(target_relays) => Ok(target_relays.clone()), + SdkRelayTargetPolicy::UseConfiguredRelays => { + SdkRelayTargetSet::from_normalized_relays(self.sdk.relay_urls().to_vec()) + } + } + } + + fn resolved_created_at( + &self, + created_at: Option<RadrootsSdkTimestamp>, + ) -> Result<RadrootsSdkTimestamp, RadrootsSdkError> { + match created_at { + Some(created_at) => Ok(created_at), + None => self.sdk.now(), } } } @@ -190,15 +250,39 @@ impl<'sdk> ListingsClient<'sdk> { #[cfg(feature = "runtime")] fn canonical_listing_draft( actor: &RadrootsActorContext, - listing: RadrootsListing, + document: RadrootsListingDraftDocumentV1, ) -> Result<RadrootsCanonicalListingDraft, RadrootsSdkError> { - let document = RadrootsListingDraftDocumentV1::new(listing); canonicalize_listing_draft(actor, document).map_err(Into::into) } #[cfg(feature = "runtime")] +fn listing_publish_plan( + actor: &RadrootsActorContext, + document: RadrootsListingDraftDocumentV1, + created_at: RadrootsSdkTimestamp, +) -> Result<ListingPublishPlan, RadrootsSdkError> { + let created_at_nostr = created_at.try_into_nostr_created_at()?; + let canonical = canonical_listing_draft(actor, document)?; + let public_listing_addr = canonical.public_listing_addr().clone(); + let draft_listing_addr = canonical.draft_listing_addr().clone(); + let mutation = RadrootsListingMutation::publish(canonical); + let frozen_draft = build_listing_mutation_draft(&mutation, created_at_nostr)?; + let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id.as_str()) + .map_err(|error| RadrootsSdkError::InvalidRequest { + message: format!("listing publish draft produced invalid event id: {error}"), + })?; + Ok(ListingPublishPlan { + public_listing_addr, + draft_listing_addr, + expected_event_id, + frozen_draft, + created_at, + }) +} + +#[cfg(feature = "runtime")] fn signed_outbox_input( - prepared: &PreparedListingPublish, + plan: &ListingPublishPlan, signed_event: RadrootsSignedNostrEvent, target_relays: Vec<String>, idempotency_key: SdkIdempotencyKey, @@ -207,7 +291,7 @@ fn signed_outbox_input( ) -> RadrootsOutboxSignedOperationInput { RadrootsOutboxSignedOperationInput::new( LISTING_PUBLISH_OPERATION_KIND, - prepared.draft.clone(), + plan.frozen_draft.clone(), signed_event, target_relays, event_store_inserted, diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs @@ -1,6 +1,6 @@ #[cfg(feature = "runtime")] use crate::{ - ListingsClient, OrdersClient, RadrootsSdkError, SdkRelayTargetPolicy, SdkRelayTargetSet, + ListingsClient, OrdersClient, RadrootsSdkError, SdkRelayTargetSet, SdkRelayUrlPolicy, SyncClient, error::RadrootsSdkRecoveryAction, }; #[cfg(feature = "runtime")] @@ -89,7 +89,7 @@ pub struct RadrootsSdkBuilder { storage: RadrootsSdkStorageConfig, clock: RadrootsSdkClock, relay_urls: Vec<String>, - relay_target_policy: SdkRelayTargetPolicy, + relay_url_policy: SdkRelayUrlPolicy, } #[cfg(feature = "runtime")] @@ -99,7 +99,7 @@ impl Default for RadrootsSdkBuilder { storage: RadrootsSdkStorageConfig::Memory, clock: RadrootsSdkClock::System, relay_urls: Vec::new(), - relay_target_policy: SdkRelayTargetPolicy::Public, + relay_url_policy: SdkRelayUrlPolicy::Public, } } } @@ -131,15 +131,15 @@ impl RadrootsSdkBuilder { self } - pub fn relay_target_policy(mut self, policy: SdkRelayTargetPolicy) -> Self { - self.relay_target_policy = policy; + pub fn relay_url_policy(mut self, policy: SdkRelayUrlPolicy) -> Self { + self.relay_url_policy = policy; self } pub async fn build(self) -> Result<RadrootsSdk, RadrootsSdkError> { let storage = open_storage(&self.storage).await?; let relay_urls = - SdkRelayTargetSet::from_configured_relays(&self.relay_urls, self.relay_target_policy)?; + SdkRelayTargetSet::from_configured_relays(&self.relay_urls, self.relay_url_policy)?; Ok(RadrootsSdk { _event_store: storage.event_store, _outbox: storage.outbox, diff --git a/crates/sdk/src/runtime_targets.rs b/crates/sdk/src/runtime_targets.rs @@ -8,12 +8,12 @@ pub const SDK_RELAY_TARGET_MAX_COUNT: usize = 20; pub const SDK_IDEMPOTENCY_KEY_MAX_LEN: usize = 256; #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SdkRelayTargetPolicy { +pub enum SdkRelayUrlPolicy { Public, Localhost, } -impl SdkRelayTargetPolicy { +impl SdkRelayUrlPolicy { fn relay_transport_policy(self) -> RadrootsRelayUrlPolicy { match self { Self::Public => RadrootsRelayUrlPolicy::Public, @@ -23,12 +23,35 @@ impl SdkRelayTargetPolicy { } #[derive(Clone, Debug, PartialEq, Eq)] +pub enum SdkRelayTargetPolicy { + Explicit(SdkRelayTargetSet), + UseConfiguredRelays, +} + +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)?)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SdkRelayTargetSet { relays: Vec<String>, } impl SdkRelayTargetSet { - pub fn new<I, S>(relays: I, policy: SdkRelayTargetPolicy) -> Result<Self, RadrootsSdkError> + pub fn new<I, S>(relays: I, policy: SdkRelayUrlPolicy) -> Result<Self, RadrootsSdkError> where I: IntoIterator<Item = S>, S: AsRef<str>, @@ -58,7 +81,7 @@ impl SdkRelayTargetSet { pub(crate) fn from_configured_relays<I, S>( relays: I, - policy: SdkRelayTargetPolicy, + policy: SdkRelayUrlPolicy, ) -> Result<Vec<String>, RadrootsSdkError> where I: IntoIterator<Item = S>, @@ -161,7 +184,7 @@ struct SdkIdempotencyDerivationInput<'a> { fn normalized_relay_url( value: &str, - policy: SdkRelayTargetPolicy, + policy: SdkRelayUrlPolicy, ) -> Result<String, RadrootsSdkError> { let relay = RadrootsRelayUrl::parse(value, policy.relay_transport_policy()) .map_err(|error| invalid_request(format!("invalid relay target: {error}")))?; diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -18,8 +18,9 @@ use radroots_events::{ }; use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState}; use radroots_sdk::{ - ListingPublishRequest, RadrootsSdk, RadrootsSdkError, RadrootsSdkRecoveryAction, - RadrootsSdkTimestamp, SdkRelayTargetPolicy, SdkRelayTargetSet, + ListingEnqueuePublishRequest, ListingPreparePublishRequest, RadrootsSdk, RadrootsSdkError, + RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, SdkRelayTargetPolicy, SdkRelayTargetSet, + SdkRelayUrlPolicy, }; const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -92,6 +93,10 @@ fn actor() -> RadrootsActorContext { RadrootsActorContext::test(SELLER, [RadrootsActorRole::Seller]).expect("actor") } +fn non_seller_actor() -> RadrootsActorContext { + RadrootsActorContext::test(SELLER, [RadrootsActorRole::Buyer]).expect("actor") +} + fn listing(d_tag: &str, title: &str) -> RadrootsListing { RadrootsListing { d_tag: RadrootsDTag::parse(d_tag).expect("d tag"), @@ -160,14 +165,19 @@ async fn directory_sdk() -> (tempfile::TempDir, RadrootsSdk) { #[tokio::test] async fn prepare_publish_is_side_effect_free() { let (_tempdir, sdk) = directory_sdk().await; - let request = ListingPublishRequest::new(listing(LISTING_A_D_TAG, "Coffee")); - let prepared = sdk - .listings() - .prepare_publish(&actor(), request) - .expect("prepared"); + let request = ListingPreparePublishRequest::new(actor(), listing(LISTING_A_D_TAG, "Coffee")); + let prepared = sdk.listings().prepare_publish(request).expect("prepared"); - assert_eq!(prepared.draft.kind, KIND_LISTING); + assert_eq!(prepared.frozen_draft.kind, KIND_LISTING); assert_eq!(prepared.created_at.unix_seconds(), 1_700_000_000); + assert_eq!( + prepared.expected_event_id, + prepared.frozen_draft.expected_event_id + ); + assert_eq!( + prepared.public_listing_addr.as_str(), + format!("{KIND_LISTING}:{SELLER}:{LISTING_A_D_TAG}") + ); let paths = sdk.storage_paths().expect("paths"); let event_store = RadrootsEventStore::open_file(&paths.event_store_path) @@ -175,7 +185,7 @@ async fn prepare_publish_is_side_effect_free() { .expect("event store"); assert!( event_store - .get_event(prepared.draft.expected_event_id.as_str()) + .get_event(prepared.expected_event_id.as_str()) .await .expect("event lookup") .is_none() @@ -193,24 +203,45 @@ async fn prepare_publish_is_side_effect_free() { } #[tokio::test] +async fn prepare_publish_rejects_non_seller_actor() { + let (_tempdir, sdk) = directory_sdk().await; + let request = + ListingPreparePublishRequest::new(non_seller_actor(), listing(LISTING_B_D_TAG, "Coffee")); + + let error = sdk + .listings() + .prepare_publish(request) + .expect_err("non seller"); + + assert!(matches!(error, RadrootsSdkError::ListingDraft { .. })); +} + +#[tokio::test] async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish() { let (_tempdir, sdk) = directory_sdk().await; - let request = ListingPublishRequest::new(listing(LISTING_B_D_TAG, "Coffee")) - .try_with_idempotency_key("idem-b") - .expect("idempotency key"); + let request = ListingEnqueuePublishRequest::new( + actor(), + listing(LISTING_B_D_TAG, "Coffee"), + SdkRelayTargetPolicy::UseConfiguredRelays, + ) + .try_with_idempotency_key("idem-b") + .expect("idempotency key"); let prepared = sdk .listings() - .prepare_publish(&actor(), request.clone()) + .prepare_publish(ListingPreparePublishRequest::new( + actor(), + listing(LISTING_B_D_TAG, "Coffee"), + )) .expect("prepared"); let receipt = sdk .listings() - .enqueue_publish(&actor(), &FixtureSigner::new(SELLER), request) + .enqueue_publish(request, &FixtureSigner::new(SELLER)) .await .expect("enqueue"); assert_eq!( receipt.local.event.event_id, - prepared.draft.expected_event_id + prepared.expected_event_id.as_str() ); assert_eq!(receipt.local.event.kind, KIND_LISTING); assert!(receipt.local.stored); @@ -244,10 +275,14 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish() #[tokio::test] async fn enqueue_publish_returns_sanitized_signer_errors() { let (_tempdir, sdk) = directory_sdk().await; - let request = ListingPublishRequest::new(listing(LISTING_C_D_TAG, "Coffee")); + let request = ListingEnqueuePublishRequest::new( + actor(), + listing(LISTING_C_D_TAG, "Coffee"), + SdkRelayTargetPolicy::UseConfiguredRelays, + ); let error = sdk .listings() - .enqueue_publish(&actor(), &FixtureSigner::new(OTHER), request) + .enqueue_publish(request, &FixtureSigner::new(OTHER)) .await .expect_err("signer error"); let message = error.to_string(); @@ -260,20 +295,28 @@ async fn enqueue_publish_returns_sanitized_signer_errors() { #[tokio::test] async fn enqueue_publish_reports_partial_local_mutation_after_outbox_conflict() { let (_tempdir, sdk) = directory_sdk().await; - let first = ListingPublishRequest::new(listing(LISTING_D_D_TAG, "Coffee")) - .try_with_idempotency_key("idem-d") - .expect("idempotency key"); + let first = ListingEnqueuePublishRequest::new( + actor(), + listing(LISTING_D_D_TAG, "Coffee"), + SdkRelayTargetPolicy::UseConfiguredRelays, + ) + .try_with_idempotency_key("idem-d") + .expect("idempotency key"); sdk.listings() - .enqueue_publish(&actor(), &FixtureSigner::new(SELLER), first) + .enqueue_publish(first, &FixtureSigner::new(SELLER)) .await .expect("first enqueue"); - let second = ListingPublishRequest::new(listing(LISTING_E_D_TAG, "Changed")) - .try_with_idempotency_key("idem-d") - .expect("idempotency key"); + let second = ListingEnqueuePublishRequest::new( + actor(), + listing(LISTING_E_D_TAG, "Changed"), + SdkRelayTargetPolicy::UseConfiguredRelays, + ) + .try_with_idempotency_key("idem-d") + .expect("idempotency key"); let error = sdk .listings() - .enqueue_publish(&actor(), &FixtureSigner::new(SELLER), second) + .enqueue_publish(second, &FixtureSigner::new(SELLER)) .await .expect_err("partial"); @@ -290,22 +333,30 @@ async fn enqueue_publish_reports_partial_local_mutation_after_outbox_conflict() #[tokio::test] async fn enqueue_publish_derives_order_independent_idempotency_key() { let (_tempdir, sdk) = directory_sdk().await; - let first = ListingPublishRequest::new(listing(LISTING_F_D_TAG, "Coffee")) - .try_with_target_relays([RELAY_B, RELAY, RELAY], SdkRelayTargetPolicy::Public) - .expect("first target relays"); - let second = ListingPublishRequest::new(listing(LISTING_F_D_TAG, "Coffee")).with_target_relays( - SdkRelayTargetSet::new([RELAY, RELAY_B], SdkRelayTargetPolicy::Public) - .expect("second target relays"), + let first = ListingEnqueuePublishRequest::new( + actor(), + listing(LISTING_F_D_TAG, "Coffee"), + SdkRelayTargetPolicy::UseConfiguredRelays, + ) + .try_with_target_relays([RELAY_B, RELAY, RELAY], SdkRelayUrlPolicy::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) + .expect("second target relays"), + ), ); let first_receipt = sdk .listings() - .enqueue_publish(&actor(), &FixtureSigner::new(SELLER), first) + .enqueue_publish(first, &FixtureSigner::new(SELLER)) .await .expect("first enqueue"); let second_receipt = sdk .listings() - .enqueue_publish(&actor(), &FixtureSigner::new(SELLER), second) + .enqueue_publish(second, &FixtureSigner::new(SELLER)) .await .expect("second enqueue"); diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -3,7 +3,7 @@ use radroots_sdk::{ RadrootsSdk, RadrootsSdkClock, RadrootsSdkError, RadrootsSdkRecoveryAction, RadrootsSdkStorageConfig, RadrootsSdkTimestamp, SDK_IDEMPOTENCY_KEY_MAX_LEN, - SDK_RELAY_TARGET_MAX_COUNT, SdkIdempotencyKey, SdkRelayTargetPolicy, SdkRelayTargetSet, + SDK_RELAY_TARGET_MAX_COUNT, SdkIdempotencyKey, SdkRelayTargetSet, SdkRelayUrlPolicy, }; #[tokio::test] @@ -52,7 +52,7 @@ async fn sdk_builder_rejects_ws_relay_without_localhost_policy() { #[tokio::test] async fn sdk_builder_allows_only_local_ws_targets_with_localhost_policy() { let sdk = RadrootsSdk::builder() - .relay_target_policy(SdkRelayTargetPolicy::Localhost) + .relay_url_policy(SdkRelayUrlPolicy::Localhost) .relay_url("ws://localhost:8080") .relay_url("ws://127.0.0.1:8081") .relay_url("ws://[::1]:8082") @@ -63,7 +63,7 @@ async fn sdk_builder_allows_only_local_ws_targets_with_localhost_policy() { assert_eq!(sdk.relay_urls().len(), 3); let result = RadrootsSdk::builder() - .relay_target_policy(SdkRelayTargetPolicy::Localhost) + .relay_url_policy(SdkRelayUrlPolicy::Localhost) .relay_url("ws://relay.example.com") .build() .await; @@ -149,7 +149,7 @@ fn relay_target_set_validates_normalizes_dedupes_sorts_and_caps() { "wss://relay-a.example.com", "wss://relay-a.example.com", ], - SdkRelayTargetPolicy::Public, + SdkRelayUrlPolicy::Public, ) .expect("targets"); @@ -162,7 +162,7 @@ fn relay_target_set_validates_normalizes_dedupes_sorts_and_caps() { ); assert!(matches!( - SdkRelayTargetSet::new(Vec::<String>::new(), SdkRelayTargetPolicy::Public), + SdkRelayTargetSet::new(Vec::<String>::new(), SdkRelayUrlPolicy::Public), Err(RadrootsSdkError::InvalidRequest { .. }) )); @@ -170,7 +170,7 @@ fn relay_target_set_validates_normalizes_dedupes_sorts_and_caps() { .map(|index| format!("wss://relay-{index}.example.com")) .collect::<Vec<_>>(); assert!(matches!( - SdkRelayTargetSet::new(too_many, SdkRelayTargetPolicy::Public), + SdkRelayTargetSet::new(too_many, SdkRelayUrlPolicy::Public), Err(RadrootsSdkError::InvalidRequest { .. }) )); } diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -17,9 +17,9 @@ use radroots_events::{ use radroots_outbox::{RadrootsOutbox, RadrootsOutboxEventState, RadrootsOutboxOperationInput}; use radroots_relay_transport::{RadrootsMockRelayPublishAdapter, RadrootsRelayOutcome}; use radroots_sdk::{ - ListingPublishRequest, PUSH_OUTBOX_DEFAULT_LIMIT, PUSH_OUTBOX_MAX_LIMIT, PushOutboxEventState, - PushOutboxRelayOutcomeKind, PushOutboxRequest, RadrootsSdk, RadrootsSdkError, - RadrootsSdkTimestamp, SdkRelayTargetPolicy, + ListingEnqueuePublishRequest, ListingPreparePublishRequest, PUSH_OUTBOX_DEFAULT_LIMIT, + PUSH_OUTBOX_MAX_LIMIT, PushOutboxEventState, PushOutboxRelayOutcomeKind, PushOutboxRequest, + RadrootsSdk, RadrootsSdkError, RadrootsSdkTimestamp, SdkRelayTargetPolicy, SdkRelayUrlPolicy, }; const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -157,11 +157,14 @@ async fn directory_sdk(relays: &[&str]) -> (tempfile::TempDir, RadrootsSdk) { async fn enqueue_listing(sdk: &RadrootsSdk, d_tag: &str, title: &str, relays: &[&str]) -> i64 { sdk.listings() .enqueue_publish( - &actor(), + ListingEnqueuePublishRequest::new( + actor(), + listing(d_tag, title), + SdkRelayTargetPolicy::UseConfiguredRelays, + ) + .try_with_target_relays(relays, SdkRelayUrlPolicy::Public) + .expect("relay targets"), &FixtureSigner::new(SELLER), - ListingPublishRequest::new(listing(d_tag, title)) - .try_with_target_relays(relays, SdkRelayTargetPolicy::Public) - .expect("relay targets"), ) .await .expect("enqueue") @@ -331,10 +334,10 @@ async fn push_outbox_does_not_claim_unsigned_outbox_work() { let (_tempdir, sdk) = directory_sdk(&[RELAY_A]).await; let prepared = sdk .listings() - .prepare_publish( - &actor(), - ListingPublishRequest::new(listing(LISTING_C_D_TAG, "Unsigned")), - ) + .prepare_publish(ListingPreparePublishRequest::new( + actor(), + listing(LISTING_C_D_TAG, "Unsigned"), + )) .expect("prepared"); let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").outbox_path) .await @@ -342,7 +345,7 @@ async fn push_outbox_does_not_claim_unsigned_outbox_work() { let unsigned = outbox .enqueue_operation(RadrootsOutboxOperationInput::new( "listing.publish.v1", - prepared.draft, + prepared.frozen_draft, vec![RELAY_A.to_owned()], 1_700_000_000_000, ))