sdk

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

commit 2397de02f145089dd8d335fd6d91daf61ebddcc9
parent b5c7b0d62c8477fc2621a549bd21e762dc25af3f
Author: triesap <tyson@radroots.org>
Date:   Mon, 15 Jun 2026 22:32:35 -0700

sdk: add prepared listing enqueue

Diffstat:
Mcrates/sdk/src/listings_runtime.rs | 35++++++++++++++++++++++++++++++-----
Mcrates/sdk/tests/listings_runtime.rs | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 178 insertions(+), 5 deletions(-)

diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -191,11 +191,36 @@ impl<'sdk> ListingsClient<'sdk> { where S: RadrootsEventSigner + ?Sized, { - let target_relays = self.resolved_target_relays(&request.target_relays)?; - let idempotency_key = request.idempotency_key.clone(); - 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 ListingEnqueuePublishRequest { + actor, + document, + target_relays, + idempotency_key, + created_at, + } = request; + let prepare_request = ListingPreparePublishRequest { + actor: actor.clone(), + document, + created_at, + }; + let plan = self.prepare_publish(prepare_request)?; + self.enqueue_prepared_publish(&actor, plan, target_relays, idempotency_key, signer) + .await + } + + pub async fn enqueue_prepared_publish<S>( + &self, + actor: &RadrootsActorContext, + plan: ListingPublishPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, + signer: &S, + ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> + where + S: RadrootsEventSigner + ?Sized, + { + let target_relays = self.resolved_target_relays(&target_relays)?; + let signed_event = sign_authorized_draft(actor, signer, &plan.frozen_draft)?; let idempotency_key = match idempotency_key { Some(idempotency_key) => idempotency_key, None => SdkIdempotencyKey::derive( diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -32,6 +32,10 @@ const LISTING_C_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAw"; const LISTING_D_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABA"; const LISTING_E_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABQ"; const LISTING_F_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABg"; +const LISTING_G_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABw"; +const LISTING_H_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACA"; +const LISTING_I_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACQ"; +const LISTING_J_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACg"; const RELAY: &str = "wss://relay.example.com"; const RELAY_B: &str = "wss://relay-b.example.com"; @@ -274,6 +278,150 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish() } #[tokio::test] +async fn prepare_then_enqueue_prepared_uses_same_event_id() { + let (_tempdir, sdk) = directory_sdk().await; + let actor = actor(); + let prepared = sdk + .listings() + .prepare_publish(ListingPreparePublishRequest::new( + actor.clone(), + listing(LISTING_G_D_TAG, "Coffee"), + )) + .expect("prepared"); + let receipt = sdk + .listings() + .enqueue_prepared_publish( + &actor, + prepared.clone(), + SdkRelayTargetPolicy::UseConfiguredRelays, + None, + &FixtureSigner::new(SELLER), + ) + .await + .expect("prepared enqueue"); + + assert_eq!(receipt.expected_event_id, prepared.expected_event_id); + assert_eq!(receipt.signed_event_id, prepared.expected_event_id); + + let paths = sdk.storage_paths().expect("paths"); + let event_store = RadrootsEventStore::open_file(&paths.event_store_path) + .await + .expect("event store"); + assert!( + event_store + .get_event(prepared.expected_event_id.as_str()) + .await + .expect("event lookup") + .is_some() + ); + + let outbox = RadrootsOutbox::open_file(&paths.outbox_path) + .await + .expect("outbox"); + let outbox_event = outbox + .get_event(receipt.outbox_event_id) + .await + .expect("outbox event") + .expect("outbox event"); + assert_eq!(outbox_event.event_id, prepared.expected_event_id.as_str()); +} + +#[tokio::test] +async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() { + let (_prepared_tempdir, prepared_sdk) = directory_sdk().await; + let prepared_actor = actor(); + let prepared_plan = prepared_sdk + .listings() + .prepare_publish(ListingPreparePublishRequest::new( + prepared_actor.clone(), + listing(LISTING_H_D_TAG, "Coffee"), + )) + .expect("prepared plan"); + let prepared_receipt = prepared_sdk + .listings() + .enqueue_prepared_publish( + &prepared_actor, + prepared_plan, + SdkRelayTargetPolicy::UseConfiguredRelays, + None, + &FixtureSigner::new(SELLER), + ) + .await + .expect("prepared enqueue"); + + let (_convenience_tempdir, convenience_sdk) = directory_sdk().await; + let convenience_request = ListingEnqueuePublishRequest::new( + actor(), + listing(LISTING_H_D_TAG, "Coffee"), + SdkRelayTargetPolicy::UseConfiguredRelays, + ); + let convenience_receipt = convenience_sdk + .listings() + .enqueue_publish(convenience_request, &FixtureSigner::new(SELLER)) + .await + .expect("convenience enqueue"); + + assert_eq!(convenience_receipt, prepared_receipt); +} + +#[tokio::test] +async fn enqueue_prepared_publish_returns_structured_actor_errors() { + let (_tempdir, sdk) = directory_sdk().await; + let prepared = sdk + .listings() + .prepare_publish(ListingPreparePublishRequest::new( + actor(), + listing(LISTING_I_D_TAG, "Coffee"), + )) + .expect("prepared"); + let error = sdk + .listings() + .enqueue_prepared_publish( + &non_seller_actor(), + prepared, + SdkRelayTargetPolicy::UseConfiguredRelays, + None, + &FixtureSigner::new(SELLER), + ) + .await + .expect_err("actor error"); + + assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. })); +} + +#[tokio::test] +async fn enqueue_prepared_publish_returns_sanitized_signer_errors() { + let (_tempdir, sdk) = directory_sdk().await; + let actor = actor(); + let prepared = sdk + .listings() + .prepare_publish(ListingPreparePublishRequest::new( + actor.clone(), + listing(LISTING_J_D_TAG, "Coffee"), + )) + .expect("prepared"); + let error = sdk + .listings() + .enqueue_prepared_publish( + &actor, + prepared, + SdkRelayTargetPolicy::UseConfiguredRelays, + None, + &FixtureSigner::new(OTHER), + ) + .await + .expect_err("signer error"); + let message = error.to_string(); + + assert!(matches!( + error, + RadrootsSdkError::SignerPubkeyMismatch { .. } + )); + assert!(!message.contains("raw")); + assert!(!message.contains("ffff")); +} + +#[tokio::test] async fn enqueue_publish_returns_sanitized_signer_errors() { let (_tempdir, sdk) = directory_sdk().await; let request = ListingEnqueuePublishRequest::new(