sdk

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

commit 57dbb2e0d0ab3a08ec9adfc2059bdeb6647aab0d
parent d7609bf452d600f169cdfe70029df17ab91befd2
Author: triesap <tyson@radroots.org>
Date:   Tue, 23 Jun 2026 23:52:07 +0000

sdk: wire configured signer workflows

- add configured signer enqueue paths for farm listing and order workflows
- keep explicit signer entrypoints under advanced method names
- expose Radroots product NIP-46 permission sets for Myc signing
- update SDK feature bundles docs examples and workflow coverage

Diffstat:
Mcrates/sdk/Cargo.toml | 13++++++++-----
Mcrates/sdk/README | 66+++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/sdk/examples/runtime_local.rs | 75++++++++++++---------------------------------------------------------------
Mcrates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs | 10+++++-----
Acrates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/farms_runtime.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/sdk/src/lib.rs | 11++++++-----
Mcrates/sdk/src/listings_runtime.rs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/sdk/src/orders_runtime.rs | 528+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcrates/sdk/src/signer_provider.rs | 41++++++++++++++++++++++++++++++++++++++++-
Mcrates/sdk/src/workflow_runtime.rs | 28++++++++++++++++++++++++++++
Mcrates/sdk/tests/farms_runtime.rs | 16++++++++--------
Mcrates/sdk/tests/listings_runtime.rs | 30+++++++++++++++---------------
Mcrates/sdk/tests/orders_runtime.rs | 67++++++++++++++++++++++++++++++++++++++-----------------------------
Mcrates/sdk/tests/runtime_foundation.rs | 12++++++++++++
Mcrates/sdk/tests/source_boundary.rs | 20++++++++++++++++++++
Mcrates/sdk/tests/sync_runtime.rs | 6+++---
Mcrates/sdk/tests/unit/farms_runtime_tests.rs | 42+++++++++++++++++++++++++++++++++++++++---
Mcrates/sdk/tests/unit/listings_runtime_tests.rs | 48++++++++++++++++++++++++++++++++++++++++++++----
Mcrates/sdk/tests/unit/orders_runtime_tests.rs | 108++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mcrates/sdk/tests/unit/signer_provider_tests.rs | 21+++++++++++++++++++++
21 files changed, 1036 insertions(+), 332 deletions(-)

diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -85,9 +85,8 @@ local-runtime = [ "serde", "serde_json", "runtime", - "local-signer", + "signer-adapters", "relay-runtime", - "signing", "relay-client", ] local-runtime-radrootsd-proxy = [ @@ -95,8 +94,7 @@ local-runtime-radrootsd-proxy = [ "serde", "serde_json", "runtime", - "local-signer", - "signing", + "signer-adapters", "radrootsd-proxy", ] @@ -141,7 +139,12 @@ required-features = ["runtime"] [[example]] name = "sdk_v1_local_enqueue_and_mock_sync" path = "examples/sdk_v1_local_enqueue_and_mock_sync.rs" -required-features = ["runtime", "local-signer"] +required-features = ["runtime", "signer-adapters"] + +[[example]] +name = "sdk_v1_myc_nip46_signer_setup" +path = "examples/sdk_v1_myc_nip46_signer_setup.rs" +required-features = ["runtime", "signer-adapters"] [[example]] name = "runtime_local" diff --git a/crates/sdk/README b/crates/sdk/README @@ -3,7 +3,7 @@ Curated Rad Roots Rust SDK for local-first Rad Roots product workflows. The SDK v1 product runtime is centered on `RadrootsSdk::builder()`, -`sdk.listings()`, `sdk.sync()`, and `sdk.orders()`. +`sdk.farms()`, `sdk.listings()`, `sdk.orders()`, and `sdk.sync()`. `RadrootsSdk::builder()` defaults to memory storage, the system clock, no relay URLs, and no production network publishing. Directory storage is opt-in and @@ -11,14 +11,27 @@ creates `event_store.sqlite` and `outbox.sqlite` in the selected directory. Configured relay URLs are also opt-in enqueue defaults used when a publish request chooses `SdkRelayTargetPolicy::UseConfiguredRelays`. +When `signer-adapters` is enabled, `RadrootsSdk::builder()` accepts a configured +`RadrootsSdkSignerProvider`. The production signing modes are `local_key` and +`myc_nip46`. Product enqueue methods use that configured provider, so callers do +not pass signing internals through every farm, listing, or order write call. +`signer_status()`, `configured_signer()`, and +`sign_with_configured_signer(...)` expose the typed SDK signer boundary. + `sdk.listings().prepare_publish(...)` is side-effect-free and returns a typed -`ListingPublishPlan`. `sdk.listings().enqueue_prepared_publish(...)` signs 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(...)`. The enqueue path uses typed relay target and +`ListingPublishPlan`. With a configured signer, +`sdk.listings().enqueue_prepared_publish(...)` signs 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 order write methods follow the same +configured signer pattern. The enqueue path uses typed relay 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 product API. + Event `created_at` and local observation time are separate contracts. The event timestamp remains the authored Nostr event timestamp, while event-store and outbox mutation timestamps use the SDK clock at enqueue time. Listing enqueue @@ -28,19 +41,25 @@ receipts report mutation state with the product names `StoredAndQueued` and `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. `push_outbox_with_adapter(...)` remains -available for tests and controlled adapter-level substrate checks. -`radrootsd-proxy` adds the Radrootsd Publish Proxy transport for daemon-resolved -publishing through `publish.event`. +not require configured builder relays. Direct relay 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 `publish.event`. The `local-runtime` feature is the curated feature bundle for local product runtime consumers. It enables `std`, `serde`, `serde_json`, `runtime`, -`local-signer`, `relay-runtime`, `signing`, and `relay-client`. `relay-client` -is retained in this bundle only for current direct relay publish callers that -have not migrated to the product runtime yet; it is classified for removal once -those callers are SDK-runtime backed. `local-runtime` does not enable -`radrootsd-proxy`; use `local-runtime-radrootsd-proxy` when the runtime should -publish through Radrootsd instead of a direct relay transport. +`signer-adapters`, `relay-runtime`, and `relay-client`. `signer-adapters` +contains the SDK `local_key` and `myc_nip46` signing surface. `relay-client` is +retained in this bundle only for direct relay publish callers. `local-runtime` +does not enable `radrootsd-proxy`; use `local-runtime-radrootsd-proxy` when the +runtime should publish through Radrootsd instead of a direct relay transport. +Both bundles use the same configured signer provider API. + +`radroots_sdk_myc_nip46_product_permissions()` and +`radroots_sdk_myc_nip46_product_permission_strings()` expose the SDK product +NIP-46 `sign_event:<kind>` permission set for Myc connection setup. The set is +derived from the farm, listing, and order 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` @@ -61,8 +80,6 @@ explicit modules: - `protocol::listing` - `protocol::order` - `protocol::identity` when `identity-models` is enabled -- `client` -- `config` - `adapters` Product runtime callers do not need `WireEventParts`. Wire helpers are exposed @@ -74,15 +91,18 @@ Compile the SDK v1 product examples with: ```bash cargo check -p radroots_sdk --example sdk_v1_listing_prepare --features runtime -cargo check -p radroots_sdk --example sdk_v1_local_enqueue_and_mock_sync --features runtime,local-signer +cargo check -p radroots_sdk --example sdk_v1_local_enqueue_and_mock_sync --features runtime,signer-adapters +cargo check -p radroots_sdk --example sdk_v1_myc_nip46_signer_setup --features runtime,signer-adapters ``` `sdk_v1_listing_prepare` shows `RadrootsSdk::builder()`, `ListingPreparePublishRequest`, and `ListingPublishPlan`. `sdk_v1_local_enqueue_and_mock_sync` shows localhost relay target selection, -prepared listing enqueue, `push_outbox_with_adapter(...)` with a mock relay -adapter, and `OrderStatusRequest`. The examples stay on product APIs and do not -use `WireEventParts`. +configured local-key signing, prepared listing enqueue, +`push_outbox_with_adapter(...)` with a mock relay adapter, and +`OrderStatusRequest`. `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 `WireEventParts`. SDK v1 does not claim strict NIP-99 Markdown-content interoperability. It emits and consumes Rad Roots v1 listing and trade event contracts. @@ -93,7 +113,7 @@ Optional advanced substrate is explicitly feature-scoped: - `identity-storage`: encrypted identity-file helpers - `signing`: Nostr builder and local signing adapters - `relay-client`: relay client and publish adapters -- `signer-adapters`: NIP-46 and signer-session primitives +- `signer-adapters`: SDK local-key and Myc NIP-46 signer providers The crate is licensed as `MIT OR Apache-2.0`. Its manifest is configured for a future crates.io release, but a public release still requires the full SDK check diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs @@ -1,85 +1,35 @@ -use radroots_authority::{ - RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity, -}; +use radroots_authority::RadrootsActorContext; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::contract::RadrootsActorRole; -use radroots_events::draft::{ - RadrootsFrozenEventDraft, RadrootsSignedNostrEvent, RadrootsSignedNostrEventParts, -}; use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; +use radroots_nostr::prelude::RadrootsNostrKeys; use radroots_sdk::protocol::farm::RadrootsFarmRef; use radroots_sdk::protocol::listing::{ RadrootsListing, RadrootsListingBin, RadrootsListingProduct, }; use radroots_sdk::{ ListingPreparePublishRequest, OrderStatusRequest, PushOutboxRequest, RadrootsSdk, - RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkRelayTargetPolicy, - SdkRelayUrlPolicy, + RadrootsSdkError, RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, + SdkIdempotencyKey, SdkRelayTargetPolicy, SdkRelayUrlPolicy, }; -const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; const RELAY: &str = "wss://relay.example.com"; -#[derive(Clone)] -struct FixtureSigner { - identity: RadrootsSignerIdentity, -} - -impl FixtureSigner { - fn new(pubkey: &str) -> Self { - Self { - identity: RadrootsSignerIdentity::new(pubkey).expect("identity"), - } - } -} - -impl RadrootsEventSigner for FixtureSigner { - fn pubkey(&self) -> &radroots_events::ids::RadrootsPublicKey { - self.identity.pubkey() - } - - fn sign_frozen_draft( - &self, - draft: &RadrootsFrozenEventDraft, - ) -> Result<RadrootsSignedNostrEvent, RadrootsSignerError> { - let sig = "f".repeat(128); - let raw_json = serde_json::json!({ - "id": draft.expected_event_id, - "pubkey": self.pubkey().as_str(), - "created_at": draft.created_at, - "kind": draft.kind, - "tags": draft.tags, - "content": draft.content, - "sig": sig, - }) - .to_string(); - RadrootsSignedNostrEvent::new(RadrootsSignedNostrEventParts { - id: draft.expected_event_id.clone(), - pubkey: self.pubkey().as_str().to_owned(), - created_at: draft.created_at, - kind: draft.kind, - tags: draft.tags.clone(), - content: draft.content.clone(), - sig, - raw_json, - }) - .map_err(|error| RadrootsSignerError::SigningFailed { - message: error.to_string(), - }) - } -} - #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { + let keys = RadrootsNostrKeys::generate(); + let seller = keys.public_key().to_hex(); + let signer = RadrootsSdkLocalKeySigner::new(keys)?; let sdk = RadrootsSdk::builder() .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) + .signer_provider(RadrootsSdkSignerProvider::LocalKey(signer)) .build() .await?; - let actor = RadrootsActorContext::test(SELLER, [RadrootsActorRole::Seller])?; - let listing = sample_listing(); + 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 idempotency_key = SdkIdempotencyKey::new("example-1")?; @@ -92,7 +42,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { prepared.clone(), target_relays, Some(idempotency_key), - &FixtureSigner::new(SELLER), ) .await?; let push = sdk @@ -122,12 +71,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { Ok(()) } -fn sample_listing() -> RadrootsListing { +fn sample_listing(seller: &str) -> RadrootsListing { RadrootsListing { d_tag: RadrootsDTag::parse("AAAAAAAAAAAAAAAAAAAAAQ").expect("d tag"), published_at: None, farm: RadrootsFarmRef { - pubkey: SELLER.to_owned(), + pubkey: seller.to_owned(), d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), }, product: RadrootsListingProduct { 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 @@ -1,4 +1,4 @@ -use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner}; +use radroots_authority::RadrootsActorContext; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, @@ -13,8 +13,8 @@ use radroots_sdk::protocol::listing::{ }; use radroots_sdk::{ ListingPreparePublishRequest, OrderStatusRequest, PushOutboxRequest, RadrootsSdk, - RadrootsSdkTimestamp, SdkIdempotencyKey, SdkRelayTargetPolicy, SdkRelayTargetSet, - SdkRelayUrlPolicy, + RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SdkIdempotencyKey, + SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy, }; const LOCAL_RELAY: &str = "ws://localhost:7777"; @@ -23,9 +23,10 @@ const LOCAL_RELAY: &str = "ws://localhost:7777"; async fn main() -> Result<(), Box<dyn std::error::Error>> { let keys = RadrootsNostrKeys::generate(); let seller = keys.public_key().to_hex(); - let signer = RadrootsLocalEventSigner::new(keys)?; + let signer = RadrootsSdkLocalKeySigner::new(keys)?; let sdk = RadrootsSdk::builder() .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) + .signer_provider(RadrootsSdkSignerProvider::LocalKey(signer)) .build() .await?; let actor = RadrootsActorContext::test(seller.as_str(), [RadrootsActorRole::Seller])?; @@ -45,7 +46,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { prepared, target_policy, Some(SdkIdempotencyKey::new("sdk-v1-local-example")?), - &signer, ) .await?; let adapter = RadrootsMockRelayPublishAdapter::new(); diff --git a/crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs b/crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs @@ -0,0 +1,56 @@ +use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrKeys}; +use radroots_nostr_connect::prelude::{ + RadrootsNostrConnectClientTarget, RadrootsNostrConnectError, +}; +use radroots_sdk::{ + RadrootsSdk, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, + RadrootsSdkNip46TransportFuture, RadrootsSdkSignerMode, RadrootsSdkSignerProvider, + radroots_sdk_myc_nip46_product_permission_strings, +}; +use std::sync::Arc; + +struct ExampleNip46Transport; + +impl RadrootsSdkNip46Transport for ExampleNip46Transport { + fn publish_request_event<'a>( + &'a self, + _event: RadrootsNostrEvent, + ) -> RadrootsSdkNip46TransportFuture<'a, ()> { + Box::pin(async { Ok(()) }) + } + + fn next_response_event<'a>( + &'a self, + ) -> RadrootsSdkNip46TransportFuture<'a, RadrootsNostrEvent> { + Box::pin(async { Err(RadrootsNostrConnectError::RequestTimedOut) }) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error>> { + let client_keys = RadrootsNostrKeys::generate(); + let remote_signer_keys = RadrootsNostrKeys::generate(); + let user_keys = RadrootsNostrKeys::generate(); + let target = RadrootsNostrConnectClientTarget::new( + remote_signer_keys.public_key(), + vec![nostr::RelayUrl::parse("wss://relay.example.com")?], + ); + let signer = RadrootsSdkMycNip46Signer::new( + client_keys, + target, + user_keys.public_key().to_hex(), + Arc::new(ExampleNip46Transport), + )?; + let sdk = RadrootsSdk::builder() + .signer_provider(RadrootsSdkSignerProvider::MycNip46(signer)) + .build() + .await?; + let status = sdk.signer_status().expect("configured signer status"); + let permissions = radroots_sdk_myc_nip46_product_permission_strings(); + + assert_eq!(status.mode, RadrootsSdkSignerMode::MycNip46); + assert!(permissions.iter().any(|value| value == "sign_event:30340")); + println!("configured signer mode: {}", status.mode.as_str()); + println!("requested permissions: {}", permissions.join(",")); + Ok(()) +} diff --git a/crates/sdk/src/farms_runtime.rs b/crates/sdk/src/farms_runtime.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "signer-adapters")] +use crate::workflow_runtime::enqueue_configured_signed_workflow; #[cfg(feature = "runtime")] use crate::{ FarmsClient, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, SdkMutationState, @@ -140,10 +142,10 @@ impl<'sdk> FarmsClient<'sdk> { farm_publish_plan(&request.actor, request.farm, created_at) } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_publish( &self, request: FarmEnqueuePublishRequest, - signer: &dyn RadrootsEventSigner, ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { let FarmEnqueuePublishRequest { actor, @@ -158,16 +160,66 @@ impl<'sdk> FarmsClient<'sdk> { created_at, }; let plan = self.prepare_publish(prepare_request)?; - self.enqueue_prepared_publish(&actor, plan, target_relays, idempotency_key, signer) + self.enqueue_prepared_publish(&actor, plan, target_relays, idempotency_key) .await } + pub async fn enqueue_publish_with_explicit_signer( + &self, + request: FarmEnqueuePublishRequest, + signer: &dyn RadrootsEventSigner, + ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { + let FarmEnqueuePublishRequest { + actor, + farm, + target_relays, + idempotency_key, + created_at, + } = request; + let prepare_request = FarmPreparePublishRequest { + actor: actor.clone(), + farm, + created_at, + }; + let plan = self.prepare_publish(prepare_request)?; + self.enqueue_prepared_publish_with_explicit_signer( + &actor, + plan, + target_relays, + idempotency_key, + signer, + ) + .await + } + + #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_publish( &self, actor: &RadrootsActorContext, plan: FarmPublishPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, + ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { + let enqueue = enqueue_configured_signed_workflow( + self.sdk, + SdkWorkflowEnqueueRequest { + operation_kind: FARM_PUBLISH_OPERATION_KIND, + actor, + frozen_draft: &plan.frozen_draft, + target_relays, + idempotency_key, + }, + ) + .await?; + Ok(farm_enqueue_receipt(plan, enqueue)) + } + + pub async fn enqueue_prepared_publish_with_explicit_signer( + &self, + actor: &RadrootsActorContext, + plan: FarmPublishPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { let enqueue = enqueue_signed_workflow( @@ -182,16 +234,7 @@ impl<'sdk> FarmsClient<'sdk> { signer, ) .await?; - Ok(FarmEnqueueReceipt { - farm_addr: plan.farm_addr, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) + Ok(farm_enqueue_receipt(plan, enqueue)) } fn resolved_created_at( @@ -206,6 +249,23 @@ impl<'sdk> FarmsClient<'sdk> { } #[cfg(feature = "runtime")] +fn farm_enqueue_receipt( + plan: FarmPublishPlan, + enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt, +) -> FarmEnqueueReceipt { + FarmEnqueueReceipt { + farm_addr: plan.farm_addr, + expected_event_id: plan.expected_event_id, + signed_event_id: enqueue.signed_event_id, + local_event_seq: enqueue.local_event_seq, + outbox_operation_id: enqueue.outbox_operation_id, + outbox_event_id: enqueue.outbox_event_id, + state: enqueue.state.into(), + idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), + } +} + +#[cfg(feature = "runtime")] fn farm_publish_plan( actor: &RadrootsActorContext, farm_value: RadrootsFarm, diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -96,11 +96,12 @@ pub use crate::runtime::{ }; #[cfg(all(feature = "runtime", feature = "signer-adapters"))] pub use crate::signer_provider::{ - RadrootsSdkLocalKeySigner, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, - RadrootsSdkNip46TransportFuture, RadrootsSdkSignReceipt, RadrootsSdkSignRequest, - RadrootsSdkSignerCapability, RadrootsSdkSignerMode, RadrootsSdkSignerProgress, - RadrootsSdkSignerProgressSink, RadrootsSdkSignerProvider, RadrootsSdkSignerState, - RadrootsSdkSignerStatus, + RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS, RadrootsSdkLocalKeySigner, + RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture, + RadrootsSdkSignReceipt, RadrootsSdkSignRequest, RadrootsSdkSignerCapability, + RadrootsSdkSignerMode, RadrootsSdkSignerProgress, RadrootsSdkSignerProgressSink, + RadrootsSdkSignerProvider, RadrootsSdkSignerState, RadrootsSdkSignerStatus, + radroots_sdk_myc_nip46_product_permission_strings, radroots_sdk_myc_nip46_product_permissions, }; #[cfg(feature = "runtime")] pub use crate::sync_runtime::{ diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "signer-adapters")] +use crate::workflow_runtime::enqueue_configured_signed_workflow; #[cfg(feature = "runtime")] use crate::{ ListingsClient, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, @@ -184,10 +186,10 @@ impl<'sdk> ListingsClient<'sdk> { listing_publish_plan(&request.actor, request.document, created_at) } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_publish( &self, request: ListingEnqueuePublishRequest, - signer: &dyn RadrootsEventSigner, ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { let ListingEnqueuePublishRequest { actor, @@ -202,16 +204,66 @@ impl<'sdk> ListingsClient<'sdk> { created_at, }; let plan = self.prepare_publish(prepare_request)?; - self.enqueue_prepared_publish(&actor, plan, target_relays, idempotency_key, signer) + self.enqueue_prepared_publish(&actor, plan, target_relays, idempotency_key) .await } + pub async fn enqueue_publish_with_explicit_signer( + &self, + request: ListingEnqueuePublishRequest, + signer: &dyn RadrootsEventSigner, + ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { + 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_with_explicit_signer( + &actor, + plan, + target_relays, + idempotency_key, + signer, + ) + .await + } + + #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_publish( &self, actor: &RadrootsActorContext, plan: ListingPublishPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, + ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { + let enqueue = enqueue_configured_signed_workflow( + self.sdk, + SdkWorkflowEnqueueRequest { + operation_kind: LISTING_PUBLISH_OPERATION_KIND, + actor, + frozen_draft: &plan.frozen_draft, + target_relays, + idempotency_key, + }, + ) + .await?; + Ok(listing_enqueue_receipt(plan, enqueue)) + } + + pub async fn enqueue_prepared_publish_with_explicit_signer( + &self, + actor: &RadrootsActorContext, + plan: ListingPublishPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { let enqueue = enqueue_signed_workflow( @@ -226,17 +278,7 @@ impl<'sdk> ListingsClient<'sdk> { signer, ) .await?; - Ok(ListingEnqueueReceipt { - public_listing_addr: plan.public_listing_addr, - draft_listing_addr: plan.draft_listing_addr, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) + Ok(listing_enqueue_receipt(plan, enqueue)) } fn resolved_created_at( @@ -251,6 +293,24 @@ impl<'sdk> ListingsClient<'sdk> { } #[cfg(feature = "runtime")] +fn listing_enqueue_receipt( + plan: ListingPublishPlan, + enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt, +) -> ListingEnqueueReceipt { + ListingEnqueueReceipt { + public_listing_addr: plan.public_listing_addr, + draft_listing_addr: plan.draft_listing_addr, + expected_event_id: plan.expected_event_id, + signed_event_id: enqueue.signed_event_id, + local_event_seq: enqueue.local_event_seq, + outbox_operation_id: enqueue.outbox_operation_id, + outbox_event_id: enqueue.outbox_event_id, + state: enqueue.state.into(), + idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), + } +} + +#[cfg(feature = "runtime")] fn canonical_listing_draft( actor: &RadrootsActorContext, document: RadrootsListingDraftDocumentV1, diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "signer-adapters")] +use crate::workflow_runtime::enqueue_configured_signed_workflow; #[cfg(feature = "runtime")] use crate::{ OrdersClient, RadrootsSdkError, RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, @@ -1159,10 +1161,10 @@ impl<'sdk> OrdersClient<'sdk> { ) } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_submit( &self, request: OrderSubmitEnqueueRequest, - signer: &dyn RadrootsEventSigner, ) -> Result<OrderSubmitReceipt, RadrootsSdkError> { let OrderSubmitEnqueueRequest { actor, @@ -1179,16 +1181,68 @@ impl<'sdk> OrdersClient<'sdk> { created_at, }; let plan = self.prepare_submit(prepare_request)?; - self.enqueue_prepared_submit(&actor, plan, target_relays, idempotency_key, signer) + self.enqueue_prepared_submit(&actor, plan, target_relays, idempotency_key) .await } + pub async fn enqueue_submit_with_explicit_signer( + &self, + request: OrderSubmitEnqueueRequest, + signer: &dyn RadrootsEventSigner, + ) -> Result<OrderSubmitReceipt, RadrootsSdkError> { + let OrderSubmitEnqueueRequest { + actor, + listing_event, + order, + target_relays, + idempotency_key, + created_at, + } = request; + let prepare_request = OrderSubmitPrepareRequest { + actor: actor.clone(), + listing_event, + order, + created_at, + }; + let plan = self.prepare_submit(prepare_request)?; + self.enqueue_prepared_submit_with_explicit_signer( + &actor, + plan, + target_relays, + idempotency_key, + signer, + ) + .await + } + + #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_submit( &self, actor: &RadrootsActorContext, plan: OrderSubmitPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, + ) -> Result<OrderSubmitReceipt, RadrootsSdkError> { + let enqueue = enqueue_configured_signed_workflow( + self.sdk, + SdkWorkflowEnqueueRequest { + operation_kind: OrderWorkflowKind::Submit.operation_kind(), + actor, + frozen_draft: &plan.frozen_draft, + target_relays, + idempotency_key, + }, + ) + .await?; + Ok(order_submit_receipt(plan, enqueue)) + } + + pub async fn enqueue_prepared_submit_with_explicit_signer( + &self, + actor: &RadrootsActorContext, + plan: OrderSubmitPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<OrderSubmitReceipt, RadrootsSdkError> { let enqueue = enqueue_signed_workflow( @@ -1203,23 +1257,7 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderSubmitReceipt { - workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::Submit, - plan.expected_event_id.clone(), - &enqueue, - ), - order_id: plan.order_id, - listing_addr: plan.listing_addr, - listing_event_id: plan.listing_event_id, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) + Ok(order_submit_receipt(plan, enqueue)) } pub fn prepare_decision( @@ -1235,10 +1273,10 @@ impl<'sdk> OrdersClient<'sdk> { ) } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_decision( &self, request: OrderDecisionEnqueueRequest, - signer: &dyn RadrootsEventSigner, ) -> Result<OrderDecisionReceipt, RadrootsSdkError> { let OrderDecisionEnqueueRequest { actor, @@ -1255,16 +1293,74 @@ impl<'sdk> OrdersClient<'sdk> { created_at, }; let plan = self.prepare_decision(prepare_request)?; - self.enqueue_prepared_decision(&actor, plan, target_relays, idempotency_key, signer) + self.enqueue_prepared_decision(&actor, plan, target_relays, idempotency_key) .await } + pub async fn enqueue_decision_with_explicit_signer( + &self, + request: OrderDecisionEnqueueRequest, + signer: &dyn RadrootsEventSigner, + ) -> Result<OrderDecisionReceipt, RadrootsSdkError> { + let OrderDecisionEnqueueRequest { + actor, + request_event, + decision, + target_relays, + idempotency_key, + created_at, + } = request; + let prepare_request = OrderDecisionPrepareRequest { + actor: actor.clone(), + request_event, + decision, + created_at, + }; + let plan = self.prepare_decision(prepare_request)?; + self.enqueue_prepared_decision_with_explicit_signer( + &actor, + plan, + target_relays, + idempotency_key, + signer, + ) + .await + } + + #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_decision( &self, actor: &RadrootsActorContext, plan: OrderDecisionPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, + ) -> Result<OrderDecisionReceipt, RadrootsSdkError> { + if !self + .prepared_order_event_exists(&plan.expected_event_id) + .await? + { + self.require_decision_preflight(&plan).await?; + } + let enqueue = enqueue_configured_signed_workflow( + self.sdk, + SdkWorkflowEnqueueRequest { + operation_kind: OrderWorkflowKind::Decision.operation_kind(), + actor, + frozen_draft: &plan.frozen_draft, + target_relays, + idempotency_key, + }, + ) + .await?; + Ok(order_decision_receipt(plan, enqueue)) + } + + pub async fn enqueue_prepared_decision_with_explicit_signer( + &self, + actor: &RadrootsActorContext, + plan: OrderDecisionPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<OrderDecisionReceipt, RadrootsSdkError> { if !self @@ -1285,25 +1381,7 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderDecisionReceipt { - workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::Decision, - plan.expected_event_id.clone(), - &enqueue, - ), - order_id: plan.order_id, - listing_addr: plan.listing_addr, - buyer_pubkey: plan.buyer_pubkey, - seller_pubkey: plan.seller_pubkey, - request_event_id: plan.request_event_id, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) + Ok(order_decision_receipt(plan, enqueue)) } pub fn prepare_revision_proposal( @@ -1320,9 +1398,35 @@ impl<'sdk> OrdersClient<'sdk> { ) } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_revision_proposal( &self, request: OrderRevisionProposalEnqueueRequest, + ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> { + let OrderRevisionProposalEnqueueRequest { + actor, + root_event, + previous_event, + proposal, + target_relays, + idempotency_key, + created_at, + } = request; + let prepare_request = OrderRevisionProposalPrepareRequest { + actor: actor.clone(), + root_event, + previous_event, + proposal, + created_at, + }; + let plan = self.prepare_revision_proposal(prepare_request)?; + self.enqueue_prepared_revision_proposal(&actor, plan, target_relays, idempotency_key) + .await + } + + pub async fn enqueue_revision_proposal_with_explicit_signer( + &self, + request: OrderRevisionProposalEnqueueRequest, signer: &dyn RadrootsEventSigner, ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> { let OrderRevisionProposalEnqueueRequest { @@ -1342,7 +1446,7 @@ impl<'sdk> OrdersClient<'sdk> { created_at, }; let plan = self.prepare_revision_proposal(prepare_request)?; - self.enqueue_prepared_revision_proposal( + self.enqueue_prepared_revision_proposal_with_explicit_signer( &actor, plan, target_relays, @@ -1352,12 +1456,40 @@ impl<'sdk> OrdersClient<'sdk> { .await } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_revision_proposal( &self, actor: &RadrootsActorContext, plan: OrderRevisionProposalPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, + ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> { + if !self + .prepared_order_event_exists(&plan.expected_event_id) + .await? + { + self.require_revision_proposal_preflight(&plan).await?; + } + let enqueue = enqueue_configured_signed_workflow( + self.sdk, + SdkWorkflowEnqueueRequest { + operation_kind: OrderWorkflowKind::RevisionProposal.operation_kind(), + actor, + frozen_draft: &plan.frozen_draft, + target_relays, + idempotency_key, + }, + ) + .await?; + Ok(order_revision_proposal_receipt(plan, enqueue)) + } + + pub async fn enqueue_prepared_revision_proposal_with_explicit_signer( + &self, + actor: &RadrootsActorContext, + plan: OrderRevisionProposalPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<OrderRevisionProposalReceipt, RadrootsSdkError> { if !self @@ -1378,26 +1510,7 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderRevisionProposalReceipt { - workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::RevisionProposal, - plan.expected_event_id.clone(), - &enqueue, - ), - order_id: plan.order_id, - listing_addr: plan.listing_addr, - buyer_pubkey: plan.buyer_pubkey, - seller_pubkey: plan.seller_pubkey, - root_event_id: plan.root_event_id, - previous_event_id: plan.previous_event_id, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) + Ok(order_revision_proposal_receipt(plan, enqueue)) } pub fn prepare_revision_decision( @@ -1414,9 +1527,35 @@ impl<'sdk> OrdersClient<'sdk> { ) } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_revision_decision( &self, request: OrderRevisionDecisionEnqueueRequest, + ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> { + let OrderRevisionDecisionEnqueueRequest { + actor, + root_event, + previous_event, + decision, + target_relays, + idempotency_key, + created_at, + } = request; + let prepare_request = OrderRevisionDecisionPrepareRequest { + actor: actor.clone(), + root_event, + previous_event, + decision, + created_at, + }; + let plan = self.prepare_revision_decision(prepare_request)?; + self.enqueue_prepared_revision_decision(&actor, plan, target_relays, idempotency_key) + .await + } + + pub async fn enqueue_revision_decision_with_explicit_signer( + &self, + request: OrderRevisionDecisionEnqueueRequest, signer: &dyn RadrootsEventSigner, ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> { let OrderRevisionDecisionEnqueueRequest { @@ -1436,7 +1575,7 @@ impl<'sdk> OrdersClient<'sdk> { created_at, }; let plan = self.prepare_revision_decision(prepare_request)?; - self.enqueue_prepared_revision_decision( + self.enqueue_prepared_revision_decision_with_explicit_signer( &actor, plan, target_relays, @@ -1446,12 +1585,40 @@ impl<'sdk> OrdersClient<'sdk> { .await } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_revision_decision( &self, actor: &RadrootsActorContext, plan: OrderRevisionDecisionPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, + ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> { + if !self + .prepared_order_event_exists(&plan.expected_event_id) + .await? + { + self.require_revision_decision_preflight(&plan).await?; + } + let enqueue = enqueue_configured_signed_workflow( + self.sdk, + SdkWorkflowEnqueueRequest { + operation_kind: OrderWorkflowKind::RevisionDecision.operation_kind(), + actor, + frozen_draft: &plan.frozen_draft, + target_relays, + idempotency_key, + }, + ) + .await?; + Ok(order_revision_decision_receipt(plan, enqueue)) + } + + pub async fn enqueue_prepared_revision_decision_with_explicit_signer( + &self, + actor: &RadrootsActorContext, + plan: OrderRevisionDecisionPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<OrderRevisionDecisionReceipt, RadrootsSdkError> { if !self @@ -1472,26 +1639,7 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderRevisionDecisionReceipt { - workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::RevisionDecision, - plan.expected_event_id.clone(), - &enqueue, - ), - order_id: plan.order_id, - listing_addr: plan.listing_addr, - buyer_pubkey: plan.buyer_pubkey, - seller_pubkey: plan.seller_pubkey, - root_event_id: plan.root_event_id, - previous_event_id: plan.previous_event_id, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) + Ok(order_revision_decision_receipt(plan, enqueue)) } pub fn prepare_cancellation( @@ -1508,10 +1656,10 @@ impl<'sdk> OrdersClient<'sdk> { ) } + #[cfg(feature = "signer-adapters")] pub async fn enqueue_cancellation( &self, request: OrderCancellationEnqueueRequest, - signer: &dyn RadrootsEventSigner, ) -> Result<OrderCancellationReceipt, RadrootsSdkError> { let OrderCancellationEnqueueRequest { actor, @@ -1530,16 +1678,76 @@ impl<'sdk> OrdersClient<'sdk> { created_at, }; let plan = self.prepare_cancellation(prepare_request)?; - self.enqueue_prepared_cancellation(&actor, plan, target_relays, idempotency_key, signer) + self.enqueue_prepared_cancellation(&actor, plan, target_relays, idempotency_key) .await } + pub async fn enqueue_cancellation_with_explicit_signer( + &self, + request: OrderCancellationEnqueueRequest, + signer: &dyn RadrootsEventSigner, + ) -> Result<OrderCancellationReceipt, RadrootsSdkError> { + let OrderCancellationEnqueueRequest { + actor, + root_event, + previous_event, + cancellation, + target_relays, + idempotency_key, + created_at, + } = request; + let prepare_request = OrderCancellationPrepareRequest { + actor: actor.clone(), + root_event, + previous_event, + cancellation, + created_at, + }; + let plan = self.prepare_cancellation(prepare_request)?; + self.enqueue_prepared_cancellation_with_explicit_signer( + &actor, + plan, + target_relays, + idempotency_key, + signer, + ) + .await + } + + #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_cancellation( &self, actor: &RadrootsActorContext, plan: OrderCancellationPlan, target_relays: SdkRelayTargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, + ) -> Result<OrderCancellationReceipt, RadrootsSdkError> { + if !self + .prepared_order_event_exists(&plan.expected_event_id) + .await? + { + self.require_cancellation_preflight(&plan).await?; + } + let enqueue = enqueue_configured_signed_workflow( + self.sdk, + SdkWorkflowEnqueueRequest { + operation_kind: OrderWorkflowKind::Cancellation.operation_kind(), + actor, + frozen_draft: &plan.frozen_draft, + target_relays, + idempotency_key, + }, + ) + .await?; + Ok(order_cancellation_receipt(plan, enqueue)) + } + + pub async fn enqueue_prepared_cancellation_with_explicit_signer( + &self, + actor: &RadrootsActorContext, + plan: OrderCancellationPlan, + target_relays: SdkRelayTargetPolicy, + idempotency_key: Option<SdkIdempotencyKey>, signer: &dyn RadrootsEventSigner, ) -> Result<OrderCancellationReceipt, RadrootsSdkError> { if !self @@ -1560,26 +1768,7 @@ impl<'sdk> OrdersClient<'sdk> { signer, ) .await?; - Ok(OrderCancellationReceipt { - workflow: order_workflow_enqueue_receipt( - OrderWorkflowKind::Cancellation, - plan.expected_event_id.clone(), - &enqueue, - ), - order_id: plan.order_id, - listing_addr: plan.listing_addr, - buyer_pubkey: plan.buyer_pubkey, - seller_pubkey: plan.seller_pubkey, - root_event_id: plan.root_event_id, - previous_event_id: plan.previous_event_id, - expected_event_id: plan.expected_event_id, - signed_event_id: enqueue.signed_event_id, - local_event_seq: enqueue.local_event_seq, - outbox_operation_id: enqueue.outbox_operation_id, - outbox_event_id: enqueue.outbox_event_id, - state: enqueue.state.into(), - idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), - }) + Ok(order_cancellation_receipt(plan, enqueue)) } pub async fn status( @@ -2124,6 +2313,137 @@ fn order_workflow_plan( } #[cfg(feature = "runtime")] +fn order_submit_receipt( + plan: OrderSubmitPlan, + enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt, +) -> OrderSubmitReceipt { + OrderSubmitReceipt { + workflow: order_workflow_enqueue_receipt( + OrderWorkflowKind::Submit, + plan.expected_event_id.clone(), + &enqueue, + ), + order_id: plan.order_id, + listing_addr: plan.listing_addr, + listing_event_id: plan.listing_event_id, + expected_event_id: plan.expected_event_id, + signed_event_id: enqueue.signed_event_id, + local_event_seq: enqueue.local_event_seq, + outbox_operation_id: enqueue.outbox_operation_id, + outbox_event_id: enqueue.outbox_event_id, + state: enqueue.state.into(), + idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), + } +} + +#[cfg(feature = "runtime")] +fn order_decision_receipt( + plan: OrderDecisionPlan, + enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt, +) -> OrderDecisionReceipt { + OrderDecisionReceipt { + workflow: order_workflow_enqueue_receipt( + OrderWorkflowKind::Decision, + plan.expected_event_id.clone(), + &enqueue, + ), + order_id: plan.order_id, + listing_addr: plan.listing_addr, + buyer_pubkey: plan.buyer_pubkey, + seller_pubkey: plan.seller_pubkey, + request_event_id: plan.request_event_id, + expected_event_id: plan.expected_event_id, + signed_event_id: enqueue.signed_event_id, + local_event_seq: enqueue.local_event_seq, + outbox_operation_id: enqueue.outbox_operation_id, + outbox_event_id: enqueue.outbox_event_id, + state: enqueue.state.into(), + idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), + } +} + +#[cfg(feature = "runtime")] +fn order_revision_proposal_receipt( + plan: OrderRevisionProposalPlan, + enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt, +) -> OrderRevisionProposalReceipt { + OrderRevisionProposalReceipt { + workflow: order_workflow_enqueue_receipt( + OrderWorkflowKind::RevisionProposal, + plan.expected_event_id.clone(), + &enqueue, + ), + order_id: plan.order_id, + listing_addr: plan.listing_addr, + buyer_pubkey: plan.buyer_pubkey, + seller_pubkey: plan.seller_pubkey, + root_event_id: plan.root_event_id, + previous_event_id: plan.previous_event_id, + expected_event_id: plan.expected_event_id, + signed_event_id: enqueue.signed_event_id, + local_event_seq: enqueue.local_event_seq, + outbox_operation_id: enqueue.outbox_operation_id, + outbox_event_id: enqueue.outbox_event_id, + state: enqueue.state.into(), + idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), + } +} + +#[cfg(feature = "runtime")] +fn order_revision_decision_receipt( + plan: OrderRevisionDecisionPlan, + enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt, +) -> OrderRevisionDecisionReceipt { + OrderRevisionDecisionReceipt { + workflow: order_workflow_enqueue_receipt( + OrderWorkflowKind::RevisionDecision, + plan.expected_event_id.clone(), + &enqueue, + ), + order_id: plan.order_id, + listing_addr: plan.listing_addr, + buyer_pubkey: plan.buyer_pubkey, + seller_pubkey: plan.seller_pubkey, + root_event_id: plan.root_event_id, + previous_event_id: plan.previous_event_id, + expected_event_id: plan.expected_event_id, + signed_event_id: enqueue.signed_event_id, + local_event_seq: enqueue.local_event_seq, + outbox_operation_id: enqueue.outbox_operation_id, + outbox_event_id: enqueue.outbox_event_id, + state: enqueue.state.into(), + idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), + } +} + +#[cfg(feature = "runtime")] +fn order_cancellation_receipt( + plan: OrderCancellationPlan, + enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt, +) -> OrderCancellationReceipt { + OrderCancellationReceipt { + workflow: order_workflow_enqueue_receipt( + OrderWorkflowKind::Cancellation, + plan.expected_event_id.clone(), + &enqueue, + ), + order_id: plan.order_id, + listing_addr: plan.listing_addr, + buyer_pubkey: plan.buyer_pubkey, + seller_pubkey: plan.seller_pubkey, + root_event_id: plan.root_event_id, + previous_event_id: plan.previous_event_id, + expected_event_id: plan.expected_event_id, + signed_event_id: enqueue.signed_event_id, + local_event_seq: enqueue.local_event_seq, + outbox_operation_id: enqueue.outbox_operation_id, + outbox_event_id: enqueue.outbox_event_id, + state: enqueue.state.into(), + idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix), + } +} + +#[cfg(feature = "runtime")] fn order_workflow_enqueue_receipt( kind: OrderWorkflowKind, expected_event_id: RadrootsEventId, diff --git a/crates/sdk/src/signer_provider.rs b/crates/sdk/src/signer_provider.rs @@ -7,11 +7,16 @@ use radroots_authority::{ }; use radroots_events::draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent}; use radroots_events::ids::RadrootsPublicKey; +use radroots_events::kinds::{ + KIND_FARM, KIND_LISTING, KIND_ORDER_CANCELLATION, KIND_ORDER_DECISION, KIND_ORDER_REQUEST, + KIND_ORDER_REVISION_DECISION, KIND_ORDER_REVISION_PROPOSAL, +}; use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrKeys, radroots_event_from_nostr}; use radroots_nostr_connect::prelude::{ RadrootsNostrConnectClientRequest, RadrootsNostrConnectClientTarget, RadrootsNostrConnectClientTransport, RadrootsNostrConnectClientTransportFuture, - RadrootsNostrConnectError, RadrootsNostrConnectRequest, RadrootsNostrConnectResponse, + RadrootsNostrConnectError, RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, + RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, RadrootsNostrConnectResponse, execute_request_with_transport, }; use serde_json::json; @@ -22,6 +27,16 @@ use std::sync::{ pub type RadrootsSdkNip46TransportFuture<'a, T> = RadrootsNostrConnectClientTransportFuture<'a, T>; +pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 7] = [ + KIND_FARM, + KIND_LISTING, + KIND_ORDER_REQUEST, + KIND_ORDER_DECISION, + KIND_ORDER_REVISION_PROPOSAL, + KIND_ORDER_REVISION_DECISION, + KIND_ORDER_CANCELLATION, +]; + #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "snake_case")] #[non_exhaustive] @@ -64,6 +79,7 @@ pub struct RadrootsSdkSignerCapability { pub remote_signer_pubkey: Option<String>, pub relays: Vec<String>, pub can_sign_events: bool, + pub nip46_permissions: Vec<String>, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] @@ -229,6 +245,7 @@ impl RadrootsSdkLocalKeySigner { remote_signer_pubkey: None, relays: Vec::new(), can_sign_events: true, + nip46_permissions: Vec::new(), } } @@ -311,6 +328,7 @@ impl RadrootsSdkMycNip46Signer { remote_signer_pubkey: Some(self.target.remote_signer_public_key.to_hex()), relays: self.target.relays.iter().map(ToString::to_string).collect(), can_sign_events: true, + nip46_permissions: radroots_sdk_myc_nip46_product_permission_strings(), } } @@ -388,6 +406,27 @@ impl RadrootsSdkMycNip46Signer { } } +pub fn radroots_sdk_myc_nip46_product_permissions() -> RadrootsNostrConnectPermissions { + RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS + .iter() + .map(|kind| { + RadrootsNostrConnectPermission::with_parameter( + RadrootsNostrConnectMethod::SignEvent, + kind.to_string(), + ) + }) + .collect::<Vec<_>>() + .into() +} + +pub fn radroots_sdk_myc_nip46_product_permission_strings() -> Vec<String> { + radroots_sdk_myc_nip46_product_permissions() + .as_slice() + .iter() + .map(ToString::to_string) + .collect() +} + struct RadrootsSdkSignerIdentityOnly { pubkey: RadrootsPublicKey, } diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "signer-adapters")] +use crate::RadrootsSdkSignRequest; use crate::{ RadrootsSdk, RadrootsSdkError, SdkIdempotencyKey, SdkRelayTargetPolicy, SdkRelayTargetSet, runtime::sdk_now_ms, @@ -36,6 +38,32 @@ pub(crate) async fn enqueue_signed_workflow( ) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> { let target_relays = resolved_target_relays(sdk, &request.target_relays)?; let signed_event = sign_authorized_draft(request.actor, signer, request.frozen_draft)?; + enqueue_signed_workflow_event(sdk, request, signed_event, target_relays).await +} + +#[cfg(feature = "signer-adapters")] +pub(crate) async fn enqueue_configured_signed_workflow( + sdk: &RadrootsSdk, + request: SdkWorkflowEnqueueRequest<'_>, +) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> { + let target_relays = resolved_target_relays(sdk, &request.target_relays)?; + let signed_event = sdk + .sign_with_configured_signer(RadrootsSdkSignRequest::new( + request.operation_kind, + request.actor, + request.frozen_draft, + )) + .await? + .signed_event; + enqueue_signed_workflow_event(sdk, request, signed_event, target_relays).await +} + +async fn enqueue_signed_workflow_event( + sdk: &RadrootsSdk, + request: SdkWorkflowEnqueueRequest<'_>, + signed_event: RadrootsSignedNostrEvent, + target_relays: SdkResolvedRelayTargets, +) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> { let idempotency_key = match request.idempotency_key { Some(idempotency_key) => idempotency_key, None => SdkIdempotencyKey::derive( diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs @@ -208,7 +208,7 @@ async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_prof .expect("prepared"); let receipt = sdk .farms() - .enqueue_publish(request, &FixtureSigner::new(FARMER)) + .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(FARMER)) .await .expect("enqueue"); @@ -265,7 +265,7 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation() ); let error = sdk .farms() - .enqueue_publish(request, &FixtureSigner::new(OTHER)) + .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(OTHER)) .await .expect_err("signer error"); let message = error.to_string(); @@ -322,12 +322,12 @@ async fn farm_enqueue_publish_derives_order_independent_idempotency_key() { let first_receipt = sdk .farms() - .enqueue_publish(first, &FixtureSigner::new(FARMER)) + .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(FARMER)) .await .expect("first enqueue"); let second_receipt = sdk .farms() - .enqueue_publish(second, &FixtureSigner::new(FARMER)) + .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(FARMER)) .await .expect("second enqueue"); @@ -367,7 +367,7 @@ async fn farm_enqueue_publish_pushes_queued_event_with_mock_relay_sync() { .expect("target relays"); let enqueue_receipt = sdk .farms() - .enqueue_publish(enqueue_request, &FixtureSigner::new(FARMER)) + .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(FARMER)) .await .expect("enqueue"); let adapter = RadrootsMockRelayPublishAdapter::new(); @@ -423,7 +423,7 @@ async fn farm_enqueue_publish_reports_partial_local_mutation_after_outbox_confli .try_with_idempotency_key("farm-idem-e") .expect("idempotency key"); sdk.farms() - .enqueue_publish(first, &FixtureSigner::new(FARMER)) + .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(FARMER)) .await .expect("first enqueue"); @@ -436,7 +436,7 @@ async fn farm_enqueue_publish_reports_partial_local_mutation_after_outbox_confli .expect("idempotency key"); let error = sdk .farms() - .enqueue_publish(second, &FixtureSigner::new(FARMER)) + .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(FARMER)) .await .expect_err("partial"); @@ -549,7 +549,7 @@ async fn farm_runtime_dtos_serialize_deterministically() { let receipt = sdk .farms() - .enqueue_publish(enqueue_request, &FixtureSigner::new(FARMER)) + .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(FARMER)) .await .expect("enqueue"); let receipt_json = serde_json::to_value(&receipt).expect("receipt json"); diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -251,7 +251,7 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish() .expect("prepared"); let receipt = sdk .listings() - .enqueue_publish(request, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER)) .await .expect("enqueue"); @@ -300,7 +300,7 @@ async fn enqueue_publish_use_configured_relays_rejects_empty_builder_relays() { let error = sdk .listings() - .enqueue_publish(request, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER)) .await .expect_err("empty configured relays"); @@ -324,7 +324,7 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() { .expect("prepared"); let receipt = sdk .listings() - .enqueue_prepared_publish( + .enqueue_prepared_publish_with_explicit_signer( &actor, prepared.clone(), SdkRelayTargetPolicy::UseConfiguredRelays, @@ -372,7 +372,7 @@ async fn enqueue_receipt_debug_omits_signed_event_payload_material() { .expect("idempotency key"); let receipt = sdk .listings() - .enqueue_publish(request, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER)) .await .expect("enqueue"); let debug = format!("{receipt:?}"); @@ -463,7 +463,7 @@ async fn listing_runtime_dtos_serialize_deterministically() { let receipt = sdk .listings() - .enqueue_publish(enqueue_request, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(SELLER)) .await .expect("enqueue"); let receipt_json = serde_json::to_value(&receipt).expect("receipt json"); @@ -486,7 +486,7 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() { .expect("prepared plan"); let prepared_receipt = prepared_sdk .listings() - .enqueue_prepared_publish( + .enqueue_prepared_publish_with_explicit_signer( &prepared_actor, prepared_plan, SdkRelayTargetPolicy::UseConfiguredRelays, @@ -504,7 +504,7 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() { ); let convenience_receipt = convenience_sdk .listings() - .enqueue_publish(convenience_request, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(convenience_request, &FixtureSigner::new(SELLER)) .await .expect("convenience enqueue"); @@ -523,7 +523,7 @@ async fn enqueue_prepared_publish_returns_structured_actor_errors() { .expect("prepared"); let error = sdk .listings() - .enqueue_prepared_publish( + .enqueue_prepared_publish_with_explicit_signer( &non_seller_actor(), prepared, SdkRelayTargetPolicy::UseConfiguredRelays, @@ -549,7 +549,7 @@ async fn enqueue_prepared_publish_returns_sanitized_signer_errors() { .expect("prepared"); let error = sdk .listings() - .enqueue_prepared_publish( + .enqueue_prepared_publish_with_explicit_signer( &actor, prepared, SdkRelayTargetPolicy::UseConfiguredRelays, @@ -582,7 +582,7 @@ async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() { let receipt = sdk .listings() - .enqueue_publish(request, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER)) .await .expect("enqueue"); @@ -626,7 +626,7 @@ async fn enqueue_publish_returns_sanitized_signer_errors() { ); let error = sdk .listings() - .enqueue_publish(request, &FixtureSigner::new(OTHER)) + .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(OTHER)) .await .expect_err("signer error"); let message = error.to_string(); @@ -650,7 +650,7 @@ async fn enqueue_publish_reports_partial_local_mutation_after_outbox_conflict() .try_with_idempotency_key("idem-d") .expect("idempotency key"); sdk.listings() - .enqueue_publish(first, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(SELLER)) .await .expect("first enqueue"); @@ -663,7 +663,7 @@ async fn enqueue_publish_reports_partial_local_mutation_after_outbox_conflict() .expect("idempotency key"); let error = sdk .listings() - .enqueue_publish(second, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(SELLER)) .await .expect_err("partial"); @@ -702,12 +702,12 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() { let first_receipt = sdk .listings() - .enqueue_publish(first, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(SELLER)) .await .expect("first enqueue"); let second_receipt = sdk .listings() - .enqueue_publish(second, &FixtureSigner::new(SELLER)) + .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(SELLER)) .await .expect("second enqueue"); diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -685,7 +685,7 @@ async fn order_submit_enqueue_stores_event_queues_outbox_and_status_sees_request let receipt = sdk .orders() - .enqueue_submit(request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer(request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) .await .expect("enqueue"); @@ -800,7 +800,7 @@ async fn order_submit_enqueue_returns_sanitized_signer_errors_before_mutation() let error = sdk .orders() - .enqueue_submit(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) .await .expect_err("signer error"); let message = error.to_string(); @@ -856,12 +856,12 @@ async fn order_submit_enqueue_derives_order_independent_idempotency_key() { let first_receipt = sdk .orders() - .enqueue_submit(first, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer(first, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) .await .expect("first enqueue"); let second_receipt = sdk .orders() - .enqueue_submit(second, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer(second, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) .await .expect("second enqueue"); @@ -928,7 +928,10 @@ async fn order_submit_enqueue_pushes_queued_event_with_mock_relay_sync() { .expect("target relays"); let enqueue_receipt = sdk .orders() - .enqueue_submit(enqueue_request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer( + enqueue_request, + &FixtureSigner::new(BUYER_SECRET_KEY_HEX), + ) .await .expect("enqueue"); let adapter = RadrootsMockRelayPublishAdapter::new(); @@ -987,7 +990,7 @@ async fn order_submit_enqueue_reports_partial_local_mutation_after_outbox_confli .try_with_idempotency_key("order-submit-conflict-idempotency") .expect("first idempotency key"); sdk.orders() - .enqueue_submit(first, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer(first, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) .await .expect("first enqueue"); @@ -1003,7 +1006,7 @@ async fn order_submit_enqueue_reports_partial_local_mutation_after_outbox_confli .expect("second idempotency key"); let error = sdk .orders() - .enqueue_submit(second, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer(second, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) .await .expect_err("partial"); @@ -1129,7 +1132,10 @@ async fn order_submit_runtime_dtos_serialize_deterministically() { let receipt = sdk .orders() - .enqueue_submit(enqueue_request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_submit_with_explicit_signer( + enqueue_request, + &FixtureSigner::new(BUYER_SECRET_KEY_HEX), + ) .await .expect("enqueue"); let receipt_json = serde_json::to_value(&receipt).expect("receipt json"); @@ -1372,7 +1378,7 @@ async fn order_request_evidence_ingest_stores_request_and_enables_decision_enque .expect("prepare decision"); let receipt = sdk .orders() - .enqueue_prepared_decision( + .enqueue_prepared_decision_with_explicit_signer( &actor, plan, SdkRelayTargetPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) @@ -1758,7 +1764,10 @@ async fn order_decision_runtime_dtos_serialize_deterministically() { let receipt = sdk .orders() - .enqueue_decision(enqueue_request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) + .enqueue_decision_with_explicit_signer( + enqueue_request, + &FixtureSigner::new(SELLER_SECRET_KEY_HEX), + ) .await .expect("enqueue"); assert_eq!(receipt.workflow.kind, OrderWorkflowKind::Decision); @@ -2088,7 +2097,7 @@ async fn order_decision_enqueue_accept_stores_event_queues_outbox_and_updates_st let receipt = sdk .orders() - .enqueue_decision(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) + .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) .await .expect("enqueue"); @@ -2190,7 +2199,7 @@ async fn order_decision_enqueue_decline_stores_event_and_status_sees_declined() let receipt = sdk .orders() - .enqueue_decision(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) + .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) .await .expect("enqueue"); @@ -2231,7 +2240,7 @@ async fn order_decision_enqueue_rejects_missing_request_evidence_before_mutation let error = sdk .orders() - .enqueue_decision(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) + .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) .await .expect_err("missing request evidence"); @@ -2276,7 +2285,7 @@ async fn order_decision_enqueue_returns_sanitized_signer_errors_before_decision_ let error = sdk .orders() - .enqueue_decision(request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) + .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(BUYER_SECRET_KEY_HEX)) .await .expect_err("signer error"); let message = error.to_string(); @@ -2328,7 +2337,7 @@ async fn order_decision_enqueue_rejects_existing_decision_state_before_mutation( let error = sdk .orders() - .enqueue_decision(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) + .enqueue_decision_with_explicit_signer(request, &FixtureSigner::new(SELLER_SECRET_KEY_HEX)) .await .expect_err("existing decision"); @@ -2383,7 +2392,7 @@ async fn order_revision_lifecycle_accepts_proposal_and_finalizes_agreement() { .expect("prepare revision proposal"); let proposal_receipt = sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &proposal_actor, proposal_plan, SdkRelayTargetPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) @@ -2432,7 +2441,7 @@ async fn order_revision_lifecycle_accepts_proposal_and_finalizes_agreement() { .expect("prepare revision decision"); let revision_decision_receipt = sdk .orders() - .enqueue_prepared_revision_decision( + .enqueue_prepared_revision_decision_with_explicit_signer( &revision_decision_actor, revision_decision_plan, SdkRelayTargetPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) @@ -2514,7 +2523,7 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif ); let proposal_receipt = sdk .orders() - .enqueue_revision_proposal( + .enqueue_revision_proposal_with_explicit_signer( OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), @@ -2556,7 +2565,7 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif let decision_error = sdk .orders() - .enqueue_decision( + .enqueue_decision_with_explicit_signer( OrderDecisionEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), @@ -2581,7 +2590,7 @@ async fn order_revision_proposal_status_exposes_pending_and_blocks_follow_on_lif ); let proposal_error = sdk .orders() - .enqueue_revision_proposal( + .enqueue_revision_proposal_with_explicit_signer( OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), @@ -2625,7 +2634,7 @@ async fn order_declined_revision_finalizes_declined_negotiation() { ); let proposal_receipt = sdk .orders() - .enqueue_revision_proposal( + .enqueue_revision_proposal_with_explicit_signer( OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), @@ -2648,7 +2657,7 @@ async fn order_declined_revision_finalizes_declined_negotiation() { ); let declined_revision_receipt = sdk .orders() - .enqueue_revision_decision( + .enqueue_revision_decision_with_explicit_signer( OrderRevisionDecisionEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), @@ -2696,7 +2705,7 @@ async fn order_declined_revision_finalizes_declined_negotiation() { ); let second_decision_error = sdk .orders() - .enqueue_revision_decision( + .enqueue_revision_decision_with_explicit_signer( OrderRevisionDecisionEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), @@ -2745,7 +2754,7 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { .expect("prepare cancellation"); let cancellation = sdk .orders() - .enqueue_prepared_cancellation( + .enqueue_prepared_cancellation_with_explicit_signer( &cancellation_actor, cancellation_plan, SdkRelayTargetPolicy::try_explicit([RELAY], SdkRelayUrlPolicy::Public) @@ -2775,7 +2784,7 @@ async fn order_cancel_lifecycle_enqueue_updates_status() { ); let replay = sdk .orders() - .enqueue_cancellation( + .enqueue_cancellation_with_explicit_signer( OrderCancellationEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), @@ -2824,7 +2833,7 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id"); let missing = sdk .orders() - .enqueue_revision_proposal( + .enqueue_revision_proposal_with_explicit_signer( OrderRevisionProposalEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), @@ -2858,7 +2867,7 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { .expect("ingest request"); let decision_receipt = sdk .orders() - .enqueue_decision( + .enqueue_decision_with_explicit_signer( OrderDecisionEnqueueRequest::new( seller_actor(), request_event_ptr(&request_event), @@ -2882,7 +2891,7 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { ); let revision_error = sdk .orders() - .enqueue_revision_decision( + .enqueue_revision_decision_with_explicit_signer( OrderRevisionDecisionEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), @@ -2903,7 +2912,7 @@ async fn order_lifecycle_enqueue_rejects_invalid_state_before_mutation() { let cancellation_error = sdk .orders() - .enqueue_cancellation( + .enqueue_cancellation_with_explicit_signer( OrderCancellationEnqueueRequest::new( buyer_actor(), request_event_ptr(&request_event), diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -779,6 +779,10 @@ fn sdk_examples_stay_on_product_api_boundary() { "sdk_v1_local_enqueue_and_mock_sync", include_str!("../examples/sdk_v1_local_enqueue_and_mock_sync.rs"), ), + ( + "sdk_v1_myc_nip46_signer_setup", + include_str!("../examples/sdk_v1_myc_nip46_signer_setup.rs"), + ), ]; for (name, example) in examples { @@ -800,7 +804,15 @@ fn sdk_examples_stay_on_product_api_boundary() { assert!(local_enqueue.contains("SdkRelayTargetPolicy")); assert!(local_enqueue.contains("SdkRelayTargetSet")); assert!(local_enqueue.contains("SdkRelayUrlPolicy::Localhost")); + assert!(local_enqueue.contains("RadrootsSdkLocalKeySigner")); + assert!(local_enqueue.contains("RadrootsSdkSignerProvider::LocalKey")); assert!(local_enqueue.contains("enqueue_prepared_publish")); + assert!(!local_enqueue.contains("enqueue_prepared_publish_with_explicit_signer")); assert!(local_enqueue.contains("push_outbox_with_adapter")); assert!(local_enqueue.contains("OrderStatusRequest")); + + let myc_setup = include_str!("../examples/sdk_v1_myc_nip46_signer_setup.rs"); + assert!(myc_setup.contains("RadrootsSdkMycNip46Signer")); + assert!(myc_setup.contains("RadrootsSdkSignerProvider::MycNip46")); + assert!(myc_setup.contains("radroots_sdk_myc_nip46_product_permission_strings")); } diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -108,6 +108,19 @@ const REQUIRED_ORDERS_CLIENT_METHODS: &[&str] = &[ "pub async fn status(", ]; +const REQUIRED_ORDERS_CLIENT_ADVANCED_SIGNER_METHODS: &[&str] = &[ + "pub async fn enqueue_submit_with_explicit_signer(", + "pub async fn enqueue_prepared_submit_with_explicit_signer(", + "pub async fn enqueue_decision_with_explicit_signer(", + "pub async fn enqueue_prepared_decision_with_explicit_signer(", + "pub async fn enqueue_revision_proposal_with_explicit_signer(", + "pub async fn enqueue_prepared_revision_proposal_with_explicit_signer(", + "pub async fn enqueue_revision_decision_with_explicit_signer(", + "pub async fn enqueue_prepared_revision_decision_with_explicit_signer(", + "pub async fn enqueue_cancellation_with_explicit_signer(", + "pub async fn enqueue_prepared_cancellation_with_explicit_signer(", +]; + const FORBIDDEN_ORDER_RUNTIME_PUBLIC_EXPORTS: &[&str] = &[ "CheckoutClient", "EscrowClient", @@ -289,6 +302,13 @@ fn orders_client_surface_is_inventory_guarded() { "OrdersClient must expose inventory-guarded method `{method}`" ); } + + for method in REQUIRED_ORDERS_CLIENT_ADVANCED_SIGNER_METHODS { + assert!( + source.contains(method), + "OrdersClient must expose explicit-signer advanced method `{method}`" + ); + } } #[test] diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -461,7 +461,7 @@ async fn enqueue_listing_with_policy( url_policy: SdkRelayUrlPolicy, ) -> i64 { sdk.listings() - .enqueue_publish( + .enqueue_publish_with_explicit_signer( ListingEnqueuePublishRequest::new( actor(), listing(d_tag, title), @@ -1372,7 +1372,7 @@ async fn product_push_outbox_uses_radrootsd_proxy_transport_with_daemon_resolved let enqueue = sdk .listings() - .enqueue_publish( + .enqueue_publish_with_explicit_signer( ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Proxy Coffee"), @@ -1448,7 +1448,7 @@ async fn product_push_outbox_radrootsd_proxy_idempotency_is_attempt_scoped() { let enqueue = sdk .listings() - .enqueue_publish( + .enqueue_publish_with_explicit_signer( ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Retry Coffee"), diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs @@ -1,4 +1,6 @@ use super::*; +use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider}; +use radroots_nostr::prelude::RadrootsNostrKeys; #[path = "../support/fixture_signer.rs"] mod fixture_signer; @@ -198,7 +200,7 @@ async fn farm_enqueue_publish_reports_prepare_errors_before_signing() { .expect("sdk"); let error = sdk .farms() - .enqueue_publish( + .enqueue_publish_with_explicit_signer( FarmEnqueuePublishRequest::new( farmer_actor(), farm("AAAAAAAAAAAAAAAAAAAAA!", "Invalid Enqueue Farm"), @@ -223,7 +225,7 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() { let actor = farmer_actor(); let receipt = sdk .farms() - .enqueue_publish( + .enqueue_publish_with_explicit_signer( FarmEnqueuePublishRequest::new( actor.clone(), farm(FARM_A_D_TAG, "Enqueued Farm"), @@ -248,7 +250,7 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() { .expect("prepared farm"); let prepared = sdk .farms() - .enqueue_prepared_publish( + .enqueue_prepared_publish_with_explicit_signer( &actor, plan, SdkRelayTargetPolicy::try_explicit([RELAY_B], SdkRelayUrlPolicy::Public) @@ -261,3 +263,37 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() { assert_eq!(prepared.signed_event_id, prepared.expected_event_id); assert_eq!(prepared.local_event_seq, 2); } + +#[tokio::test] +async fn farm_configured_local_signer_enqueues_publish_without_explicit_signer() { + let keys = RadrootsNostrKeys::generate(); + let farmer = keys.public_key().to_hex(); + let sdk = crate::RadrootsSdk::builder() + .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500)) + .signer_provider(RadrootsSdkSignerProvider::LocalKey( + RadrootsSdkLocalKeySigner::new(keys).expect("signer"), + )) + .build() + .await + .expect("sdk"); + let actor = + RadrootsActorContext::test(farmer.as_str(), [RadrootsActorRole::Farmer]).expect("actor"); + + let receipt = sdk + .farms() + .enqueue_publish( + FarmEnqueuePublishRequest::new( + actor, + farm(FARM_C_D_TAG, "Configured Farm"), + SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + .expect("target relays"), + ) + .try_with_idempotency_key("farm-configured-local") + .expect("idempotency"), + ) + .await + .expect("enqueue farm"); + + assert_eq!(receipt.signed_event_id, receipt.expected_event_id); + assert_eq!(receipt.state, SdkMutationState::StoredAndQueued); +} diff --git a/crates/sdk/tests/unit/listings_runtime_tests.rs b/crates/sdk/tests/unit/listings_runtime_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider}; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, @@ -10,6 +11,7 @@ use radroots_events::{ listing::{RadrootsListingBin, RadrootsListingProduct}, resource_area::RadrootsResourceAreaRef, }; +use radroots_nostr::prelude::RadrootsNostrKeys; #[path = "../support/fixture_signer.rs"] mod fixture_signer; @@ -32,11 +34,15 @@ fn actor() -> RadrootsActorContext { } fn listing(d_tag: &str, title: &str) -> RadrootsListing { + listing_for_seller(SELLER, d_tag, title) +} + +fn listing_for_seller(seller: &str, d_tag: &str, title: &str) -> RadrootsListing { RadrootsListing { d_tag: RadrootsDTag::parse(d_tag).expect("d tag"), published_at: None, farm: RadrootsFarmRef { - pubkey: SELLER.to_owned(), + pubkey: seller.to_owned(), d_tag: FARM_D_TAG.to_owned(), }, product: RadrootsListingProduct { @@ -225,7 +231,7 @@ async fn listing_enqueue_publish_reports_prepare_errors_before_signing() { .expect("sdk"); let error = sdk .listings() - .enqueue_publish( + .enqueue_publish_with_explicit_signer( ListingEnqueuePublishRequest::new( actor(), listing(LISTING_A_D_TAG, "Future Enqueue Greens"), @@ -254,7 +260,7 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() { let actor = actor(); let receipt = sdk .listings() - .enqueue_publish( + .enqueue_publish_with_explicit_signer( ListingEnqueuePublishRequest::new( actor.clone(), listing(LISTING_A_D_TAG, "Enqueued Greens"), @@ -279,7 +285,7 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() { .expect("prepared listing"); let prepared = sdk .listings() - .enqueue_prepared_publish( + .enqueue_prepared_publish_with_explicit_signer( &actor, plan, SdkRelayTargetPolicy::try_explicit([RELAY_B], SdkRelayUrlPolicy::Public) @@ -292,3 +298,37 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() { assert_eq!(prepared.signed_event_id, prepared.expected_event_id); assert_eq!(prepared.local_event_seq, 2); } + +#[tokio::test] +async fn listing_configured_local_signer_enqueues_publish_without_explicit_signer() { + let keys = RadrootsNostrKeys::generate(); + let seller = keys.public_key().to_hex(); + let sdk = crate::RadrootsSdk::builder() + .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500)) + .signer_provider(RadrootsSdkSignerProvider::LocalKey( + RadrootsSdkLocalKeySigner::new(keys).expect("signer"), + )) + .build() + .await + .expect("sdk"); + let actor = + RadrootsActorContext::test(seller.as_str(), [RadrootsActorRole::Seller]).expect("actor"); + + let receipt = sdk + .listings() + .enqueue_publish( + ListingEnqueuePublishRequest::new( + actor, + listing_for_seller(seller.as_str(), LISTING_C_D_TAG, "Configured Greens"), + SdkRelayTargetPolicy::try_explicit([RELAY_A], SdkRelayUrlPolicy::Public) + .expect("target relays"), + ) + .try_with_idempotency_key("listing-configured-local") + .expect("idempotency"), + ) + .await + .expect("enqueue listing"); + + assert_eq!(receipt.signed_event_id, receipt.expected_event_id); + assert_eq!(receipt.state, SdkMutationState::StoredAndQueued); +} diff --git a/crates/sdk/tests/unit/orders_runtime_tests.rs b/crates/sdk/tests/unit/orders_runtime_tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::RadrootsSdk; +use crate::{RadrootsSdk, RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider}; use radroots_authority::{RadrootsSignerError, RadrootsSignerIdentity}; use radroots_core::{ RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, @@ -260,8 +260,7 @@ struct OrderFixtureSigner { impl OrderFixtureSigner { fn new(secret_key_hex: &str) -> Self { - let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key"); - let keys = RadrootsNostrKeys::new(secret_key); + let keys = keys_from_secret(secret_key_hex); let pubkey = keys.public_key().to_hex(); Self { identity: RadrootsSignerIdentity::new(pubkey).expect("identity"), @@ -270,6 +269,11 @@ impl OrderFixtureSigner { } } +fn keys_from_secret(secret_key_hex: &str) -> RadrootsNostrKeys { + let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key"); + RadrootsNostrKeys::new(secret_key) +} + impl RadrootsEventSigner for OrderFixtureSigner { fn pubkey(&self) -> &RadrootsPublicKey { self.identity.pubkey() @@ -411,6 +415,32 @@ async fn prepared_order_sdk() -> RadrootsSdk { .expect("sdk") } +#[tokio::test] +async fn order_configured_local_signer_enqueues_submit_without_explicit_signer() { + let sdk = RadrootsSdk::builder() + .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) + .signer_provider(RadrootsSdkSignerProvider::LocalKey( + RadrootsSdkLocalKeySigner::new(keys_from_secret(BUYER_SECRET_KEY_HEX)).expect("signer"), + )) + .build() + .await + .expect("sdk"); + + let receipt = sdk + .orders() + .enqueue_submit(OrderSubmitEnqueueRequest::new( + fixture_buyer_actor(), + fixture_event_ptr('a'), + fixture_order_request("order-configured-local-1"), + fixture_target_relays(), + )) + .await + .expect("enqueue submit"); + + assert_eq!(receipt.signed_event_id, receipt.expected_event_id); + assert_eq!(receipt.state, SdkMutationState::StoredAndQueued); +} + async fn enqueue_fixture_submit(sdk: &RadrootsSdk, raw_order_id: &str) -> OrderSubmitReceipt { let buyer = fixture_buyer_actor(); let plan = sdk @@ -422,7 +452,7 @@ async fn enqueue_fixture_submit(sdk: &RadrootsSdk, raw_order_id: &str) -> OrderS )) .expect("submit plan"); sdk.orders() - .enqueue_prepared_submit( + .enqueue_prepared_submit_with_explicit_signer( &buyer, plan, fixture_target_relays(), @@ -2069,7 +2099,7 @@ async fn prepared_submit_and_decision_enqueue_cover_source_attached_success_path .expect("decision plan"); let decision = sdk .orders() - .enqueue_prepared_decision( + .enqueue_prepared_decision_with_explicit_signer( &seller, decision_plan, fixture_target_relays(), @@ -2105,7 +2135,7 @@ async fn prepared_revision_lifecycle_enqueue_cover_source_attached_success_paths .expect("proposal plan"); let proposal = sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, proposal_plan, fixture_target_relays(), @@ -2133,7 +2163,7 @@ async fn prepared_revision_lifecycle_enqueue_cover_source_attached_success_paths .expect("revision decision plan"); let revision = sdk .orders() - .enqueue_prepared_revision_decision( + .enqueue_prepared_revision_decision_with_explicit_signer( &buyer, revision_decision_plan, fixture_target_relays(), @@ -2165,7 +2195,7 @@ async fn prepared_cancellation_enqueue_covers_source_attached_success_path() { .expect("cancellation plan"); let cancellation = sdk .orders() - .enqueue_prepared_cancellation( + .enqueue_prepared_cancellation_with_explicit_signer( &buyer, cancellation_plan, fixture_target_relays(), @@ -2186,7 +2216,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { let sdk = prepared_order_sdk().await; let decision_submit = sdk .orders() - .enqueue_submit( + .enqueue_submit_with_explicit_signer( OrderSubmitEnqueueRequest::new( fixture_buyer_actor(), fixture_event_ptr('b'), @@ -2199,7 +2229,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { .expect("enqueue submit"); let decision = sdk .orders() - .enqueue_decision( + .enqueue_decision_with_explicit_signer( OrderDecisionEnqueueRequest::new( fixture_seller_actor(), fixture_order_event_ptr(&decision_submit.signed_event_id), @@ -2214,7 +2244,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { let revision_submit = sdk .orders() - .enqueue_submit( + .enqueue_submit_with_explicit_signer( OrderSubmitEnqueueRequest::new( fixture_buyer_actor(), fixture_event_ptr('c'), @@ -2232,7 +2262,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { ); let proposal = sdk .orders() - .enqueue_revision_proposal( + .enqueue_revision_proposal_with_explicit_signer( OrderRevisionProposalEnqueueRequest::new( fixture_seller_actor(), fixture_order_event_ptr(&revision_submit.signed_event_id), @@ -2246,7 +2276,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { .expect("enqueue proposal"); let revision = sdk .orders() - .enqueue_revision_decision( + .enqueue_revision_decision_with_explicit_signer( OrderRevisionDecisionEnqueueRequest::new( fixture_buyer_actor(), fixture_order_event_ptr(&revision_submit.signed_event_id), @@ -2262,7 +2292,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { let cancellation_submit = sdk .orders() - .enqueue_submit( + .enqueue_submit_with_explicit_signer( OrderSubmitEnqueueRequest::new( fixture_buyer_actor(), fixture_event_ptr('d'), @@ -2275,7 +2305,7 @@ async fn convenience_order_enqueue_methods_cover_source_attached_wrappers() { .expect("enqueue cancellation submit"); let cancellation = sdk .orders() - .enqueue_cancellation( + .enqueue_cancellation_with_explicit_signer( OrderCancellationEnqueueRequest::new( fixture_buyer_actor(), fixture_order_event_ptr(&cancellation_submit.signed_event_id), @@ -2315,7 +2345,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( .expect("decision plan"); let decision_missing = sdk .orders() - .enqueue_prepared_decision( + .enqueue_prepared_decision_with_explicit_signer( &seller, decision_plan, fixture_target_relays(), @@ -2340,7 +2370,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( .expect("proposal plan"); let proposal_missing = sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, proposal_plan, fixture_target_relays(), @@ -2365,7 +2395,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( .expect("revision decision plan"); let revision_missing = sdk .orders() - .enqueue_prepared_revision_decision( + .enqueue_prepared_revision_decision_with_explicit_signer( &buyer, revision_decision_plan, fixture_target_relays(), @@ -2390,7 +2420,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( .expect("cancellation plan"); let cancellation_missing = sdk .orders() - .enqueue_prepared_cancellation( + .enqueue_prepared_cancellation_with_explicit_signer( &buyer, cancellation_plan, fixture_target_relays(), @@ -2452,7 +2482,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( closed_sdk._event_store.pool().close().await; let closed_error = closed_sdk .orders() - .enqueue_prepared_decision( + .enqueue_prepared_decision_with_explicit_signer( &seller, closed_plan, fixture_target_relays(), @@ -2464,7 +2494,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( assert!(matches!(closed_error, RadrootsSdkError::EventStore { .. })); let closed_proposal_error = closed_sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, closed_proposal_plan, fixture_target_relays(), @@ -2479,7 +2509,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( )); let closed_revision_error = closed_sdk .orders() - .enqueue_prepared_revision_decision( + .enqueue_prepared_revision_decision_with_explicit_signer( &buyer, closed_revision_plan, fixture_target_relays(), @@ -2494,7 +2524,7 @@ async fn prepared_lifecycle_enqueues_report_missing_and_closed_preflight_errors( )); let closed_cancellation_error = closed_sdk .orders() - .enqueue_prepared_cancellation( + .enqueue_prepared_cancellation_with_explicit_signer( &buyer, closed_cancellation_plan, fixture_target_relays(), @@ -2605,7 +2635,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { proposal_sdk._outbox.pool().close().await; let proposal_error = proposal_sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, proposal_plan, fixture_target_relays(), @@ -2635,7 +2665,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { .expect("revision proposal plan"); let proposal = revision_sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, proposal_plan, fixture_target_relays(), @@ -2657,7 +2687,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { revision_sdk._outbox.pool().close().await; let revision_error = revision_sdk .orders() - .enqueue_prepared_revision_decision( + .enqueue_prepared_revision_decision_with_explicit_signer( &buyer, revision_plan, fixture_target_relays(), @@ -2683,7 +2713,7 @@ async fn prepared_lifecycle_enqueues_report_closed_outbox_after_preflight() { cancellation_sdk._outbox.pool().close().await; let cancellation_error = cancellation_sdk .orders() - .enqueue_prepared_cancellation( + .enqueue_prepared_cancellation_with_explicit_signer( &buyer, cancellation_plan, fixture_target_relays(), @@ -2710,7 +2740,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { .expect("decision plan"); decision_sdk .orders() - .enqueue_prepared_decision( + .enqueue_prepared_decision_with_explicit_signer( &seller, decision_plan.clone(), fixture_target_relays(), @@ -2721,7 +2751,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { .expect("enqueue decision"); let decision_repeat = decision_sdk .orders() - .enqueue_prepared_decision( + .enqueue_prepared_decision_with_explicit_signer( &seller, decision_plan, fixture_target_relays(), @@ -2759,7 +2789,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { .expect("proposal plan"); proposal_sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, proposal_plan.clone(), fixture_target_relays(), @@ -2770,7 +2800,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { .expect("enqueue proposal"); let proposal_repeat = proposal_sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, proposal_plan, fixture_target_relays(), @@ -2808,7 +2838,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { .expect("revision proposal plan"); let proposal = revision_sdk .orders() - .enqueue_prepared_revision_proposal( + .enqueue_prepared_revision_proposal_with_explicit_signer( &seller, proposal_plan, fixture_target_relays(), @@ -2829,7 +2859,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { .expect("revision plan"); revision_sdk .orders() - .enqueue_prepared_revision_decision( + .enqueue_prepared_revision_decision_with_explicit_signer( &buyer, revision_plan.clone(), fixture_target_relays(), @@ -2840,7 +2870,7 @@ async fn prepared_lifecycle_enqueues_skip_preflight_for_existing_events() { .expect("enqueue revision"); let revision_repeat = revision_sdk .orders() - .enqueue_prepared_revision_decision( + .enqueue_prepared_revision_decision_with_explicit_signer( &buyer, revision_plan, fixture_target_relays(), @@ -2887,7 +2917,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { )); assert!(matches!( sdk.orders() - .enqueue_submit( + .enqueue_submit_with_explicit_signer( OrderSubmitEnqueueRequest::new( buyer.clone(), fixture_event_ptr('a'), @@ -2902,7 +2932,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { )); assert!(matches!( sdk.orders() - .enqueue_decision( + .enqueue_decision_with_explicit_signer( OrderDecisionEnqueueRequest::new( seller.clone(), fixture_event_ptr('b'), @@ -2925,7 +2955,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { ); assert!(matches!( sdk.orders() - .enqueue_revision_proposal( + .enqueue_revision_proposal_with_explicit_signer( OrderRevisionProposalEnqueueRequest::new( seller.clone(), ptr(root_event_id.as_str().to_owned()), @@ -2941,7 +2971,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { )); assert!(matches!( sdk.orders() - .enqueue_revision_decision( + .enqueue_revision_decision_with_explicit_signer( OrderRevisionDecisionEnqueueRequest::new( buyer.clone(), ptr(root_event_id.as_str().to_owned()), @@ -2957,7 +2987,7 @@ async fn order_ingest_and_enqueue_wrappers_report_prepare_timestamp_errors() { )); assert!(matches!( sdk.orders() - .enqueue_cancellation( + .enqueue_cancellation_with_explicit_signer( OrderCancellationEnqueueRequest::new( buyer, ptr(root_event_id.as_str().to_owned()), diff --git a/crates/sdk/tests/unit/signer_provider_tests.rs b/crates/sdk/tests/unit/signer_provider_tests.rs @@ -160,6 +160,7 @@ async fn local_key_provider_signs_authorized_frozen_draft() { assert_eq!(provider.mode(), RadrootsSdkSignerMode::LocalKey); assert_eq!(provider.status(), signer.status()); + assert!(provider.capability().nip46_permissions.is_empty()); assert_eq!(receipt.mode, RadrootsSdkSignerMode::LocalKey); assert_eq!(receipt.signer_pubkey, USER_PUBLIC_KEY_HEX); assert_eq!(receipt.signed_event_id, draft.expected_event_id); @@ -176,6 +177,22 @@ async fn local_key_provider_signs_authorized_frozen_draft() { ); } +#[test] +fn myc_nip46_product_permissions_cover_sdk_write_event_kinds() { + let permissions = radroots_sdk_myc_nip46_product_permissions(); + let rendered = radroots_sdk_myc_nip46_product_permission_strings(); + + assert_eq!( + permissions.as_slice().len(), + RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS.len() + ); + assert_eq!(rendered.len(), permissions.as_slice().len()); + for kind in RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS { + assert!(rendered.contains(&format!("sign_event:{kind}"))); + } + assert!(!rendered.contains(&"sign_event:1".to_owned())); +} + #[tokio::test] async fn myc_nip46_provider_signs_and_validates_remote_event() { let client_keys = client_keys(); @@ -200,6 +217,10 @@ async fn myc_nip46_provider_signs_and_validates_remote_event() { RadrootsSdkMycNip46Signer::new(client_keys, target, USER_PUBLIC_KEY_HEX, transport.clone()) .expect("signer"); let provider = RadrootsSdkSignerProvider::MycNip46(signer); + assert_eq!( + provider.capability().nip46_permissions, + radroots_sdk_myc_nip46_product_permission_strings() + ); let actor = actor(); let mut progress = Vec::new();