lib

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

commit d7f482ab97d26dbeb8ef2ccdb3a72e71c67eac09
parent 2c7fbb1291918623ab8880b764ea6771864055d2
Author: triesap <tyson@radroots.org>
Date:   Thu, 30 Jul 2026 12:58:03 +0000

signing: migrate workspace consumers

- replace authority actor and signer imports with the final signing SPI
- route local remote and workflow signing through request-bound receipts
- migrate SDK examples fixtures feature wiring and operation identities
- enforce retired-surface absence across manifests sources and tests

Diffstat:
Mcrates/sdk/Cargo.toml | 25++++++++++++++++---------
Mcrates/sdk/examples/runtime_local.rs | 12++++++++----
Mcrates/sdk/examples/sdk_v1_listing_prepare.rs | 5+++--
Mcrates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs | 12++++++++----
Mcrates/sdk/src/actor_json.rs | 25+++++++++++--------------
Mcrates/sdk/src/error.rs | 58++++++++++++++++++++++++++++++++--------------------------
Mcrates/sdk/src/farms_runtime.rs | 77+++++++++++++++++++++++++++++------------------------------------------------
Mcrates/sdk/src/lib.rs | 2+-
Mcrates/sdk/src/listings_runtime.rs | 37+++++++++++++++----------------------
Mcrates/sdk/src/signer_provider.rs | 149++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mcrates/sdk/src/trade_runtime.rs | 46+++++++++++++++++++++++-----------------------
Mcrates/sdk/src/workflow_runtime.rs | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mcrates/sdk/tests/farms_runtime.rs | 39+++++++++++++++++++++++----------------
Mcrates/sdk/tests/listings_runtime.rs | 32++++++++++++++++++--------------
Mcrates/sdk/tests/source_boundary.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/tests/support/fixture_signer.rs | 38+++++++++++++++++++-------------------
Mcrates/sdk/tests/sync_runtime.rs | 11++++++++---
Mcrates/sdk/tests/unit/actor_json_tests.rs | 31+++++++++++++++++++------------
Mcrates/sdk/tests/unit/error_tests.rs | 18+++++++-----------
Mcrates/sdk/tests/unit/farms_runtime_tests.rs | 49++++++++++++++++++++++++++++++++++++++-----------
Mcrates/sdk/tests/unit/listings_runtime_tests.rs | 24+++++++++++++++++++-----
Mcrates/sdk/tests/unit/signer_provider_tests.rs | 46+++++++++++++++++++++++++---------------------
Mcrates/sdk/tests/unit/sync_runtime_tests.rs | 53+++++++++++++++++++++++++++++++----------------------
Mcrates/sdk/tests/unit/trade_runtime_tests.rs | 28++++++++++++++++++----------
Mcrates/sdk/tests/unit/workflow_runtime_tests.rs | 181+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
25 files changed, 708 insertions(+), 430 deletions(-)

diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -43,7 +43,15 @@ knowledge = [ identity-models = [ "radroots_identity/std", ] -signing = ["dep:nostr", "dep:radroots_nostr", "nostr", "radroots_nostr/std"] +signing = [ + "dep:nostr", + "dep:radroots_nostr", + "dep:radroots_protocol", + "nostr", + "radroots_nostr/std", + "radroots_protocol/std", + "radroots_signing/std", +] transport-nostr-client = ["signing", "std", "serde_json", "radroots_nostr/client"] radrootsd-execution = [ "std", @@ -74,11 +82,11 @@ runtime = [ "dep:tokio", "dep:base64", "dep:hex", - "dep:radroots_authority", "dep:radroots_event_store", "dep:radroots_geocoder", "dep:radroots_outbox", "dep:radroots_protected_store", + "dep:radroots_protocol", "dep:radroots_transport", "dep:radroots_transport_nostr", "dep:radroots_transport_reticulum", @@ -87,19 +95,20 @@ runtime = [ "dep:sha2", "dep:sqlx", "dep:uuid", - "radroots_authority/std", + "radroots_signing/std", "radroots_event_store/sqlite", "radroots_event_store/runtime-tokio", "radroots_outbox/sqlite", "radroots_outbox/runtime-tokio", "radroots_protected_store/std", + "radroots_protocol/std", "radroots_secret_vault/std", "radroots_transport/serde", "radroots_transport_nostr/std", "radroots_transport_nostr/storage", "radroots_transport_nostr/runtime-tokio", ] -local-signer = ["runtime", "radroots_authority/local_signer"] +local-signer = ["runtime", "signing", "radroots_nostr/signing"] transport-nostr-runtime = [ "runtime", "dep:radroots_nostr", @@ -131,7 +140,6 @@ test-fixture-geonames-asset = [ [dependencies] base64 = { workspace = true, optional = true } futures = { workspace = true, optional = true } -radroots_authority = { workspace = true, optional = true, default-features = false } radroots_event_store = { workspace = true, optional = true, default-features = false } radroots_event = { workspace = true, default-features = false } radroots_event_codec = { workspace = true, default-features = false } @@ -144,6 +152,7 @@ radroots_transport_nostr = { workspace = true, optional = true, default-features radroots_transport_reticulum = { workspace = true, optional = true, default-features = false } radroots_runtime_paths = { workspace = true, optional = true, default-features = false } radroots_secret_vault = { workspace = true, optional = true, default-features = false } +radroots_signing = { workspace = true, default-features = false } radroots_trade = { workspace = true, default-features = false } radroots_identity = { workspace = true, default-features = false } radroots_nostr = { workspace = true, optional = true, default-features = false } @@ -201,10 +210,7 @@ nostr = { workspace = true } radroots_core = { workspace = true, default-features = false, features = [ "std", ] } -radroots_authority = { workspace = true, default-features = false, features = [ - "std", - "local_signer", -] } +radroots_signing = { workspace = true, default-features = false, features = ["std"] } radroots_replica_store = { workspace = true, default-features = false, features = [ "native", ] } @@ -214,6 +220,7 @@ radroots_sql_core = { workspace = true, features = ["native"] } radroots_nostr = { workspace = true, default-features = false, features = [ "std", "events", + "signing", ] } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs @@ -1,4 +1,3 @@ -use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner}; use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; use radroots_event::contract::AuthorRole; use radroots_event::farm::FarmRef; @@ -9,11 +8,13 @@ use radroots_event::listing::operational::{ OperationalListingStatus, }; use radroots_nostr::prelude::RadrootsNostrKeys; +use radroots_nostr::signing::LocalSigner; use radroots_sdk::{ ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient, RadrootsSdkError, RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SdkIdempotencyKey, TargetPolicy, }; +use radroots_signing::{Actor, actor::ActorSource}; const RELAY: &str = "wss://relay.example.com"; @@ -21,14 +22,17 @@ const RELAY: &str = "wss://relay.example.com"; async fn main() -> Result<(), Box<dyn std::error::Error>> { let keys = RadrootsNostrKeys::generate(); let seller = keys.public_key().to_hex(); - let signer = - RadrootsSdkLocalKeySigner::from_event_signer(RadrootsLocalEventSigner::new(keys)?)?; + let signer = RadrootsSdkLocalKeySigner::from_signer(LocalSigner::new(keys), seller.as_str())?; let sdk = RadrootsClient::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(), [AuthorRole::Seller])?; + let actor = Actor::from_public_key_hex( + seller.as_str(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + )?; let listing = sample_listing(seller.as_str()); let prepare_request = ListingPreparePublishRequest::new(actor.clone(), listing); let target_policy = TargetPolicy::try_nostr_relays([RELAY], NostrRelayUrlPolicy::Public)?; diff --git a/crates/sdk/examples/sdk_v1_listing_prepare.rs b/crates/sdk/examples/sdk_v1_listing_prepare.rs @@ -1,4 +1,3 @@ -use radroots_authority::RadrootsActorContext; use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; use radroots_event::contract::AuthorRole; use radroots_event::farm::FarmRef; @@ -9,6 +8,7 @@ use radroots_event::listing::operational::{ OperationalListingStatus, }; use radroots_sdk::{ListingPreparePublishRequest, RadrootsClient, RadrootsSdkTimestamp}; +use radroots_signing::{Actor, actor::ActorSource}; const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -18,7 +18,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) .build() .await?; - let actor = RadrootsActorContext::test(SELLER, [AuthorRole::Seller])?; + let actor = + Actor::from_public_key_hex(SELLER, ActorSource::ExplicitPublicKey, [AuthorRole::Seller])?; let request = ListingPreparePublishRequest::new(actor, sample_listing(SELLER)); let plan = sdk.listings().prepare_publish(request)?; diff --git a/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs b/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs @@ -1,4 +1,3 @@ -use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner}; use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; use radroots_event::contract::AuthorRole; use radroots_event::farm::FarmRef; @@ -9,11 +8,13 @@ use radroots_event::listing::operational::{ OperationalListingStatus, }; use radroots_nostr::prelude::RadrootsNostrKeys; +use radroots_nostr::signing::LocalSigner; use radroots_sdk::{ ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient, RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider, RadrootsSdkTimestamp, SdkIdempotencyKey, TargetPolicy, TargetSet, }; +use radroots_signing::{Actor, actor::ActorSource}; use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsNostrTransport}; const LOCAL_RELAY: &str = "ws://localhost:7777"; @@ -22,14 +23,17 @@ 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 = - RadrootsSdkLocalKeySigner::from_event_signer(RadrootsLocalEventSigner::new(keys)?)?; + let signer = RadrootsSdkLocalKeySigner::from_signer(LocalSigner::new(keys), seller.as_str())?; let sdk = RadrootsClient::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(), [AuthorRole::Seller])?; + let actor = Actor::from_public_key_hex( + seller.as_str(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + )?; let targets = TargetSet::nostr_relays([LOCAL_RELAY], NostrRelayUrlPolicy::Localhost)?; let target_policy = TargetPolicy::explicit(targets); diff --git a/crates/sdk/src/actor_json.rs b/crates/sdk/src/actor_json.rs @@ -1,13 +1,10 @@ -use radroots_authority::{RadrootsActorContext, RadrootsActorSource}; use radroots_event::contract::AuthorRole; +use radroots_signing::{Actor, actor::ActorSource}; use serde::{Serialize, ser::SerializeStruct}; -pub(crate) struct SdkActorContextJson<'a>(pub(crate) &'a RadrootsActorContext); +pub(crate) struct SdkActorContextJson<'a>(pub(crate) &'a Actor); -pub(crate) fn serialize_actor_context<S>( - actor: &RadrootsActorContext, - serializer: S, -) -> Result<S::Ok, S::Error> +pub(crate) fn serialize_actor_context<S>(actor: &Actor, serializer: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { @@ -25,8 +22,8 @@ impl serde::Serialize for SdkActorContextJson<'_> { .iter() .map(actor_role_code) .collect::<Vec<_>>(); - let account_id = self.0.account_id().map(|account_id| account_id.as_str()); - let pubkey = self.0.pubkey().to_hex(); + let account_id = self.0.account_id().map(|account_id| account_id.to_hex()); + let pubkey = self.0.public_key().to_hex(); let mut state = serializer.serialize_struct("SdkActorContext", 4)?; state.serialize_field("pubkey", &pubkey)?; state.serialize_field("roles", &roles)?; @@ -50,13 +47,13 @@ fn actor_role_code(role: &AuthorRole) -> &'static str { } } -fn actor_source_code(source: RadrootsActorSource) -> &'static str { +fn actor_source_code(source: ActorSource) -> &'static str { match source { - RadrootsActorSource::LocalAccount => "local_account", - RadrootsActorSource::ExplicitPubkey => "explicit_pubkey", - RadrootsActorSource::RemoteSigner => "remote_signer", - RadrootsActorSource::Service => "service", - RadrootsActorSource::Test => "test", + ActorSource::LocalAccount(_) => "local_account", + ActorSource::ExplicitPublicKey => "explicit_public_key", + ActorSource::RemoteSigner(_) => "remote_signer", + ActorSource::Service(_) => "service", + _ => "unknown", } } diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -1107,36 +1107,42 @@ impl fmt::Display for RadrootsSdkError { impl std::error::Error for RadrootsSdkError {} #[cfg(feature = "runtime")] -impl From<radroots_authority::RadrootsAuthorityError> for RadrootsSdkError { - fn from(error: radroots_authority::RadrootsAuthorityError) -> Self { - match error { - radroots_authority::RadrootsAuthorityError::ActorRoleUnsatisfied { - contract_id, - required_role, - } => Self::UnauthorizedActor { - operation: contract_id, - reason: format!("missing role {required_role:?}"), +impl From<radroots_signing::Error> for RadrootsSdkError { + fn from(error: radroots_signing::Error) -> Self { + use radroots_signing::error::Kind; + + match error.kind() { + Kind::AuthorizationDenied => Self::UnauthorizedActor { + operation: "event signing".to_owned(), + reason: "actor or signer is not authorized for the frozen draft".to_owned(), }, - radroots_authority::RadrootsAuthorityError::ActorPubkeyMismatch { - expected_pubkey, - actor_pubkey, - } => Self::UnauthorizedActor { - operation: "event authorization".to_owned(), - reason: format!( - "actor_pubkey_prefix={} expected_pubkey_prefix={}", - redacted_prefix(actor_pubkey.as_str()), - redacted_prefix(expected_pubkey.as_str()) - ), + Kind::SignerCapabilityMissing => Self::SignerUnavailable { + mode: "configured".to_owned(), + reason: error.to_string(), }, - radroots_authority::RadrootsAuthorityError::SignerPubkeyMismatch { - expected_pubkey, - signer_pubkey, - } => Self::SignerPubkeyMismatch { + Kind::SignerUnavailable => Self::SignerUnavailable { + mode: "configured".to_owned(), + reason: error.to_string(), + }, + Kind::SignerRejected => Self::SignerRequestRejected { + mode: "configured".to_owned(), + reason: error.to_string(), + }, + Kind::SignerTimeout | Kind::DeadlineExceeded => Self::SignerRequestTimedOut { + mode: "configured".to_owned(), + }, + Kind::SignerCancelled => Self::SignerRequestRejected { + mode: "configured".to_owned(), + reason: error.to_string(), + }, + Kind::SignerOutputInvalid => Self::SignerReturnedEventDrift { operation: "event signing".to_owned(), - expected_pubkey_prefix: redacted_prefix(expected_pubkey.as_str()), - signer_pubkey_prefix: redacted_prefix(signer_pubkey.as_str()), + reason: error.to_string(), + }, + Kind::InvalidArgument | Kind::InternalError => Self::Authority { + message: error.to_string(), }, - error => Self::Authority { + _ => Self::Authority { message: error.to_string(), }, } diff --git a/crates/sdk/src/farms_runtime.rs b/crates/sdk/src/farms_runtime.rs @@ -12,8 +12,6 @@ use crate::{ workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow}, }; #[cfg(feature = "runtime")] -use radroots_authority::{RadrootsActorContext, RadrootsEventSigner}; -#[cfg(feature = "runtime")] use radroots_event::{ contract::AuthorRole, draft::EventDraft, @@ -23,6 +21,7 @@ use radroots_event::{ listing::operational::OperationalListingPublicLocation, }; #[cfg(feature = "runtime")] +use radroots_signing::{Actor, Signer}; #[cfg(feature = "runtime")] pub const FARM_PUBLISH_OPERATION_KIND: &str = "farm.publish.v1"; @@ -40,14 +39,14 @@ const GEOHASH_BASE32: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz"; #[non_exhaustive] pub struct FarmPreparePublishRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub farm: Farm, pub created_at: Option<RadrootsSdkTimestamp>, } #[cfg(feature = "runtime")] impl FarmPreparePublishRequest { - pub fn new(actor: RadrootsActorContext, farm: Farm) -> Self { + pub fn new(actor: Actor, farm: Farm) -> Self { Self { actor, farm, @@ -66,7 +65,7 @@ impl FarmPreparePublishRequest { #[non_exhaustive] pub struct FarmEnqueuePublishRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub farm: Farm, pub target_policy: TargetPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, @@ -75,7 +74,7 @@ pub struct FarmEnqueuePublishRequest { #[cfg(feature = "runtime")] impl FarmEnqueuePublishRequest { - pub fn new(actor: RadrootsActorContext, farm: Farm, target_policy: TargetPolicy) -> Self { + pub fn new(actor: Actor, farm: Farm, target_policy: TargetPolicy) -> Self { Self { actor, farm, @@ -213,7 +212,7 @@ impl SdkPublicLocality { #[non_exhaustive] pub struct FarmPrivateLocationUpsertRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub farm_d_tag: String, pub exact_location: SdkExactLocation, pub label: Option<String>, @@ -223,7 +222,7 @@ pub struct FarmPrivateLocationUpsertRequest { #[cfg(feature = "runtime")] impl FarmPrivateLocationUpsertRequest { pub fn new( - actor: RadrootsActorContext, + actor: Actor, farm_d_tag: impl Into<String>, exact_location: SdkExactLocation, ) -> Self { @@ -279,7 +278,7 @@ impl FarmPrivateLocationInput { #[non_exhaustive] pub struct FarmPrivateLocationSetRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub farm_d_tag: String, pub input: FarmPrivateLocationInput, pub label: Option<String>, @@ -289,7 +288,7 @@ pub struct FarmPrivateLocationSetRequest { #[cfg(feature = "runtime")] impl FarmPrivateLocationSetRequest { pub fn new( - actor: RadrootsActorContext, + actor: Actor, farm_d_tag: impl Into<String>, input: FarmPrivateLocationInput, ) -> Self { @@ -303,7 +302,7 @@ impl FarmPrivateLocationSetRequest { } pub fn exact( - actor: RadrootsActorContext, + actor: Actor, farm_d_tag: impl Into<String>, exact_location: SdkExactLocation, ) -> Self { @@ -314,27 +313,15 @@ impl FarmPrivateLocationSetRequest { ) } - pub fn city( - actor: RadrootsActorContext, - farm_d_tag: impl Into<String>, - city: impl Into<String>, - ) -> Self { + pub fn city(actor: Actor, farm_d_tag: impl Into<String>, city: impl Into<String>) -> Self { Self::new(actor, farm_d_tag, FarmPrivateLocationInput::city(city)) } - pub fn query( - actor: RadrootsActorContext, - farm_d_tag: impl Into<String>, - query: impl Into<String>, - ) -> Self { + pub fn query(actor: Actor, farm_d_tag: impl Into<String>, query: impl Into<String>) -> Self { Self::new(actor, farm_d_tag, FarmPrivateLocationInput::query(query)) } - pub fn geonames_id( - actor: RadrootsActorContext, - farm_d_tag: impl Into<String>, - id: i64, - ) -> Self { + pub fn geonames_id(actor: Actor, farm_d_tag: impl Into<String>, id: i64) -> Self { Self::new(actor, farm_d_tag, FarmPrivateLocationInput::geonames_id(id)) } @@ -354,13 +341,13 @@ impl FarmPrivateLocationSetRequest { #[non_exhaustive] pub struct FarmPrivateLocationClearRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub farm_d_tag: String, } #[cfg(feature = "runtime")] impl FarmPrivateLocationClearRequest { - pub fn new(actor: RadrootsActorContext, farm_d_tag: impl Into<String>) -> Self { + pub fn new(actor: Actor, farm_d_tag: impl Into<String>) -> Self { Self { actor, farm_d_tag: farm_d_tag.into(), @@ -455,7 +442,7 @@ impl<'sdk> FarmsClient<'sdk> { pub async fn enqueue_publish_with_explicit_signer( &self, request: FarmEnqueuePublishRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { let FarmEnqueuePublishRequest { actor, @@ -483,7 +470,7 @@ impl<'sdk> FarmsClient<'sdk> { #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_publish( &self, - actor: &RadrootsActorContext, + actor: &Actor, plan: FarmPublishPlan, target_policy: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, @@ -506,11 +493,11 @@ impl<'sdk> FarmsClient<'sdk> { pub async fn enqueue_prepared_publish_with_explicit_signer( &self, - actor: &RadrootsActorContext, + actor: &Actor, plan: FarmPublishPlan, target_policy: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> { let metadata = validate_farm_publish_plan(&plan)?; let enqueue = enqueue_signed_workflow( @@ -596,7 +583,7 @@ impl<'sdk> FarmsClient<'sdk> { let public_locality = public_locality_from_reverse(request.exact_location, &reverse)?; let record = SdkPrivateFarmLocationRecord { farm_addr: farm_addr.clone(), - farm_pubkey: request.actor.pubkey().to_hex(), + farm_pubkey: request.actor.public_key().to_hex(), farm_d_tag: request.farm_d_tag, label, latitude: request.exact_location.latitude, @@ -619,7 +606,7 @@ impl<'sdk> FarmsClient<'sdk> { async fn set_private_location_from_locality( &self, - actor: RadrootsActorContext, + actor: Actor, farm_d_tag: String, locality_query: GeocoderLocalityQuery, label: Option<String>, @@ -641,7 +628,7 @@ impl<'sdk> FarmsClient<'sdk> { }; let record = SdkPrivateFarmLocationRecord { farm_addr: farm_addr.clone(), - farm_pubkey: actor.pubkey().to_hex(), + farm_pubkey: actor.public_key().to_hex(), farm_d_tag, label, latitude: exact_location.latitude, @@ -666,7 +653,7 @@ impl<'sdk> FarmsClient<'sdk> { GeocoderLocalityLookup::NoMatch => Ok(FarmPrivateLocationSetResult::NoMatch( farm_private_location_lookup_receipt( farm_addr, - actor.pubkey().to_hex().as_str(), + actor.public_key().to_hex().as_str(), farm_d_tag, FarmPrivateLocationInput::Locality(locality_query), Vec::new(), @@ -675,7 +662,7 @@ impl<'sdk> FarmsClient<'sdk> { GeocoderLocalityLookup::Ambiguous { candidates } => Ok( FarmPrivateLocationSetResult::Ambiguous(farm_private_location_lookup_receipt( farm_addr, - actor.pubkey().to_hex().as_str(), + actor.public_key().to_hex().as_str(), farm_d_tag, FarmPrivateLocationInput::Locality(locality_query), candidates @@ -800,7 +787,7 @@ fn validate_farm_publish_plan( #[cfg(feature = "runtime")] fn farm_publish_plan( - actor: &RadrootsActorContext, + actor: &Actor, farm_value: Farm, created_at: RadrootsSdkTimestamp, ) -> Result<FarmPublishPlan, RadrootsSdkError> { @@ -818,7 +805,7 @@ fn farm_publish_plan( created_at_nostr.into(), parts.tags, parts.content, - actor.pubkey().to_hex(), + actor.public_key().to_hex(), ) .expect("validated farm publish draft freezes"); let expected_event_id = EventId::parse(frozen_draft.expected_event_id_hex()) @@ -832,10 +819,7 @@ fn farm_publish_plan( } #[cfg(feature = "runtime")] -fn require_farmer_actor( - actor: &RadrootsActorContext, - operation: &'static str, -) -> Result<(), RadrootsSdkError> { +fn require_farmer_actor(actor: &Actor, operation: &'static str) -> Result<(), RadrootsSdkError> { if actor.satisfies(AuthorRole::Farmer) { Ok(()) } else { @@ -847,11 +831,8 @@ fn require_farmer_actor( } #[cfg(feature = "runtime")] -fn farm_addr( - actor: &RadrootsActorContext, - d_tag: &str, -) -> Result<AddressableCoordinate, RadrootsSdkError> { - AddressableCoordinate::parse(format!("{KIND_FARM}:{}:{d_tag}", actor.pubkey())).map_err( +fn farm_addr(actor: &Actor, d_tag: &str) -> Result<AddressableCoordinate, RadrootsSdkError> { + AddressableCoordinate::parse(format!("{KIND_FARM}:{}:{d_tag}", actor.public_key())).map_err( |error| RadrootsSdkError::InvalidRequest { message: format!("farm address is invalid: {error}"), }, diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -18,7 +18,7 @@ mod error; mod farm; #[cfg(feature = "runtime")] mod farms_runtime; -#[cfg(all(test, feature = "runtime"))] +#[cfg(all(test, feature = "runtime", feature = "signer-adapters"))] #[path = "../tests/support/fixture_signer.rs"] pub(crate) mod fixture_signer; #[cfg(feature = "runtime")] diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -7,8 +7,6 @@ use crate::{ workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow}, }; #[cfg(feature = "runtime")] -use radroots_authority::{RadrootsActorContext, RadrootsEventSigner}; -#[cfg(feature = "runtime")] use radroots_event::{ contract::AuthorRole, draft::EventDraft, @@ -19,6 +17,8 @@ use radroots_event::{ #[cfg(feature = "runtime")] use radroots_outbox::RadrootsOutboxEnqueueStatus; #[cfg(feature = "runtime")] +use radroots_signing::{Actor, Signer}; +#[cfg(feature = "runtime")] use radroots_trade::operational_listing::{ RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1, RadrootsOperationalListingMutation, build_operational_listing_mutation_draft, @@ -35,14 +35,14 @@ const OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID: &str = "radroots.operational_li #[non_exhaustive] pub struct ListingPreparePublishRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub document: RadrootsOperationalListingEditDocumentV1, pub created_at: Option<RadrootsSdkTimestamp>, } #[cfg(feature = "runtime")] impl ListingPreparePublishRequest { - pub fn new(actor: RadrootsActorContext, listing: OperationalListing) -> Self { + pub fn new(actor: Actor, listing: OperationalListing) -> Self { Self { actor, document: RadrootsOperationalListingEditDocumentV1::new(listing), @@ -50,10 +50,7 @@ impl ListingPreparePublishRequest { } } - pub fn from_document( - actor: RadrootsActorContext, - document: RadrootsOperationalListingEditDocumentV1, - ) -> Self { + pub fn from_document(actor: Actor, document: RadrootsOperationalListingEditDocumentV1) -> Self { Self { actor, document, @@ -72,7 +69,7 @@ impl ListingPreparePublishRequest { #[non_exhaustive] pub struct ListingEnqueuePublishRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub document: RadrootsOperationalListingEditDocumentV1, pub target_policy: TargetPolicy, pub idempotency_key: Option<SdkIdempotencyKey>, @@ -81,11 +78,7 @@ pub struct ListingEnqueuePublishRequest { #[cfg(feature = "runtime")] impl ListingEnqueuePublishRequest { - pub fn new( - actor: RadrootsActorContext, - listing: OperationalListing, - target_policy: TargetPolicy, - ) -> Self { + pub fn new(actor: Actor, listing: OperationalListing, target_policy: TargetPolicy) -> Self { Self::from_document( actor, RadrootsOperationalListingEditDocumentV1::new(listing), @@ -94,7 +87,7 @@ impl ListingEnqueuePublishRequest { } pub fn from_document( - actor: RadrootsActorContext, + actor: Actor, document: RadrootsOperationalListingEditDocumentV1, target_policy: TargetPolicy, ) -> Self { @@ -234,7 +227,7 @@ impl<'sdk> ListingsClient<'sdk> { pub async fn enqueue_publish_with_explicit_signer( &self, request: ListingEnqueuePublishRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { let ListingEnqueuePublishRequest { actor, @@ -262,7 +255,7 @@ impl<'sdk> ListingsClient<'sdk> { #[cfg(feature = "signer-adapters")] pub async fn enqueue_prepared_publish( &self, - actor: &RadrootsActorContext, + actor: &Actor, plan: ListingPublishPlan, target_policy: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, @@ -285,11 +278,11 @@ impl<'sdk> ListingsClient<'sdk> { pub async fn enqueue_prepared_publish_with_explicit_signer( &self, - actor: &RadrootsActorContext, + actor: &Actor, plan: ListingPublishPlan, target_policy: TargetPolicy, idempotency_key: Option<SdkIdempotencyKey>, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<ListingEnqueueReceipt, RadrootsSdkError> { let metadata = validate_listing_publish_plan(&plan)?; let enqueue = enqueue_signed_workflow( @@ -338,7 +331,7 @@ fn listing_enqueue_receipt( #[cfg(feature = "runtime")] fn canonical_listing_edit( - actor: &RadrootsActorContext, + actor: &Actor, document: RadrootsOperationalListingEditDocumentV1, ) -> Result<RadrootsOperationalListingCanonicalEdit, RadrootsSdkError> { if !actor.satisfies(AuthorRole::Seller) { @@ -347,7 +340,7 @@ fn canonical_listing_edit( reason: "missing role Seller".to_owned(), }); } - canonicalize_operational_listing_edit(*actor.pubkey(), document).map_err(Into::into) + canonicalize_operational_listing_edit(actor.public_key(), document).map_err(Into::into) } #[cfg(feature = "runtime")] @@ -412,7 +405,7 @@ fn validate_listing_publish_plan( #[cfg(feature = "runtime")] fn listing_publish_plan( - actor: &RadrootsActorContext, + actor: &Actor, document: RadrootsOperationalListingEditDocumentV1, created_at: RadrootsSdkTimestamp, ) -> Result<ListingPublishPlan, RadrootsSdkError> { diff --git a/crates/sdk/src/signer_provider.rs b/crates/sdk/src/signer_provider.rs @@ -1,9 +1,5 @@ -use crate::RadrootsSdkError; +use crate::{RadrootsSdkError, workflow_runtime::signing_operation_id}; use nostr::{JsonUtil, Kind, PublicKey as NostrPublicKey, Tag, Tags, Timestamp, UnsignedEvent}; -use radroots_authority::{ - RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, authorize_actor_for_draft, - authorize_signer_for_draft, sign_authorized_draft, validate_signed_event_matches_draft, -}; use radroots_event::draft::{EventDraft, SignedEvent}; use radroots_event::envelope::kind::{ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, @@ -19,13 +15,17 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, RadrootsNostrConnectResponse, execute_request_with_transport, }; +use radroots_signing::{ + Actor, SignReceipt, SignRequest, Signer, + request::{CancellationPolicy, SignPolicy}, +}; use std::sync::Arc; use std::time::Duration; use tokio::time::timeout; use uuid::Uuid; pub type RadrootsSdkNip46TransportFuture<'a, T> = RadrootsNostrConnectClientTransportFuture<'a, T>; -pub type RadrootsSdkLocalSignerCapability = dyn RadrootsEventSigner + Send + Sync; +pub type RadrootsSdkLocalSignerCapability = dyn Signer; pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 7] = [ KIND_FARM, @@ -119,17 +119,13 @@ where pub struct RadrootsSdkSignRequest<'a> { pub operation_kind: &'a str, - pub actor: &'a RadrootsActorContext, + pub actor: &'a Actor, pub frozen_draft: &'a EventDraft, progress_sink: Option<&'a mut dyn RadrootsSdkSignerProgressSink>, } impl<'a> RadrootsSdkSignRequest<'a> { - pub fn new( - operation_kind: &'a str, - actor: &'a RadrootsActorContext, - frozen_draft: &'a EventDraft, - ) -> Self { + pub fn new(operation_kind: &'a str, actor: &'a Actor, frozen_draft: &'a EventDraft) -> Self { Self { operation_kind, actor, @@ -220,20 +216,28 @@ pub struct RadrootsSdkLocalKeySigner { #[cfg(feature = "local-signer")] impl RadrootsSdkLocalKeySigner { - pub fn from_event_signer<S>(signer: S) -> Result<Self, RadrootsSdkError> + pub fn from_signer<S>( + signer: S, + signer_pubkey: impl AsRef<str>, + ) -> Result<Self, RadrootsSdkError> where - S: RadrootsEventSigner + Send + Sync + 'static, + S: Signer + 'static, { - Self::from_shared_event_signer(Arc::new(signer)) + let signer_pubkey = PublicKey::from_hex(signer_pubkey.as_ref()).map_err(|_| { + RadrootsSdkError::InvalidRequest { + message: "local signer public key is invalid".to_owned(), + } + })?; + Self::from_shared_signer(Arc::new(signer), signer_pubkey) } - pub fn from_shared_event_signer( + pub fn from_shared_signer( signer: Arc<RadrootsSdkLocalSignerCapability>, + signer_pubkey: PublicKey, ) -> Result<Self, RadrootsSdkError> { - let signer_pubkey = signer.pubkey().to_hex(); Ok(Self { signer, - signer_pubkey, + signer_pubkey: signer_pubkey.to_hex(), }) } @@ -265,17 +269,22 @@ impl RadrootsSdkLocalKeySigner { request.emit_progress(RadrootsSdkSignerProgress::RequestStarted { mode: RadrootsSdkSignerMode::LocalKey, })?; - let signed_event = - sign_authorized_draft(request.actor, self.signer.as_ref(), request.frozen_draft)?; + let operation_kind = request.operation_kind.to_owned(); + let sign_request = final_sign_request( + &request, + CancellationPolicy::LocalCooperative, + Duration::from_millis(RADROOTS_SDK_MYC_NIP46_DEFAULT_REQUEST_TIMEOUT_MS), + )?; + let receipt = self.signer.sign(sign_request).await?; request.emit_progress(RadrootsSdkSignerProgress::RequestCompleted { mode: RadrootsSdkSignerMode::LocalKey, })?; - Ok(sign_receipt( - request.operation_kind, + Ok(sdk_sign_receipt( + operation_kind.as_str(), RadrootsSdkSignerMode::LocalKey, self.signer_pubkey.clone(), None, - signed_event, + receipt, )) } } @@ -416,12 +425,19 @@ impl RadrootsSdkMycNip46Signer { request.emit_progress(RadrootsSdkSignerProgress::RequestStarted { mode: RadrootsSdkSignerMode::MycNip46, })?; - authorize_actor_for_draft(request.actor, request.frozen_draft)?; - let signer_identity = RadrootsSdkSignerIdentityOnly { - pubkey: self.user_pubkey, - }; - authorize_signer_for_draft(&signer_identity, request.frozen_draft)?; - let sign_event_request = sign_event_request_from_frozen_draft(request.frozen_draft)?; + let operation_kind = request.operation_kind.to_owned(); + let sign_request = final_sign_request( + &request, + CancellationPolicy::PreservePublishedRequest, + self.request_policy.request_timeout(), + )?; + if self.user_pubkey != sign_request.actor().public_key() { + return Err(radroots_signing::Error::new( + radroots_signing::error::Kind::AuthorizationDenied, + ) + .into()); + } + let sign_event_request = sign_event_request_from_frozen_draft(sign_request.draft())?; let request_id = self.next_request_id(); let mut adapter = RadrootsSdkNip46TransportAdapter { transport: self.transport.as_ref(), @@ -458,22 +474,26 @@ impl RadrootsSdkMycNip46Signer { return Err(error); } let response = response.map_err(sdk_error_from_nip46_error)?; - let signed_event = signed_event_from_nip46_response(request.operation_kind, response)?; - validate_signed_event_matches_draft(&signed_event, request.frozen_draft).map_err( - |error| RadrootsSdkError::SignerReturnedEventDrift { - operation: request.operation_kind.to_owned(), - reason: error.to_string(), - }, - )?; + let signed_event = signed_event_from_nip46_response(operation_kind.as_str(), response)?; + let receipt = SignReceipt::from_signed_event(&sign_request, signed_event, unix_time_now()?) + .map_err(|error| match error.kind() { + radroots_signing::error::Kind::SignerOutputInvalid => { + RadrootsSdkError::SignerReturnedEventDrift { + operation: operation_kind.clone(), + reason: error.to_string(), + } + } + _ => error.into(), + })?; request.emit_progress(RadrootsSdkSignerProgress::RequestCompleted { mode: RadrootsSdkSignerMode::MycNip46, })?; - Ok(sign_receipt( - request.operation_kind, + Ok(sdk_sign_receipt( + operation_kind.as_str(), RadrootsSdkSignerMode::MycNip46, self.user_pubkey.to_hex(), Some(self.target.remote_signer_public_key.to_hex()), - signed_event, + receipt, )) } @@ -515,20 +535,6 @@ pub fn radroots_sdk_myc_nip46_product_permission_strings() -> Vec<String> { .collect() } -struct RadrootsSdkSignerIdentityOnly { - pubkey: PublicKey, -} - -impl RadrootsEventSigner for RadrootsSdkSignerIdentityOnly { - fn pubkey(&self) -> &PublicKey { - &self.pubkey - } - - fn sign_frozen_draft(&self, _draft: &EventDraft) -> Result<SignedEvent, RadrootsSignerError> { - Err(RadrootsSignerError::Unavailable) - } -} - struct RadrootsSdkNip46TransportAdapter<'a> { transport: &'a dyn RadrootsSdkNip46Transport, } @@ -676,13 +682,46 @@ fn sdk_error_from_nip46_error(error: RadrootsNostrConnectError) -> RadrootsSdkEr } } -fn sign_receipt( +fn final_sign_request( + request: &RadrootsSdkSignRequest<'_>, + cancellation: CancellationPolicy, + timeout: Duration, +) -> Result<SignRequest, RadrootsSdkError> { + let operation_id = signing_operation_id(request.operation_kind).ok_or_else(|| { + RadrootsSdkError::InvalidRequest { + message: format!("unknown signing operation `{}`", request.operation_kind), + } + })?; + let now = unix_time_now()?; + let timeout_seconds = timeout.as_secs().max(1); + let deadline_unix = now + .checked_add(timeout_seconds) + .ok_or(RadrootsSdkError::TimestampOutOfRange { value: now })?; + let policy = SignPolicy::new(deadline_unix, cancellation)?; + SignRequest::new( + operation_id, + request.actor.clone(), + request.frozen_draft.clone(), + policy, + ) + .map_err(Into::into) +} + +fn unix_time_now() -> Result<u64, RadrootsSdkError> { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .map_err(|_| RadrootsSdkError::ClockBeforeUnixEpoch) +} + +fn sdk_sign_receipt( operation_kind: &str, mode: RadrootsSdkSignerMode, signer_pubkey: String, remote_signer_pubkey: Option<String>, - signed_event: SignedEvent, + receipt: SignReceipt, ) -> RadrootsSdkSignReceipt { + let signed_event = receipt.signed_event().clone(); RadrootsSdkSignReceipt { operation_kind: operation_kind.to_owned(), mode, diff --git a/crates/sdk/src/trade_runtime.rs b/crates/sdk/src/trade_runtime.rs @@ -16,8 +16,6 @@ use crate::{ #[cfg(feature = "runtime")] use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; #[cfg(feature = "runtime")] -use radroots_authority::{RadrootsActorContext, RadrootsEventSigner}; -#[cfg(feature = "runtime")] use radroots_event::{ draft::EventDraft, envelope::kind::TRADE_MUTATION_EVENT_KINDS, @@ -33,6 +31,8 @@ use radroots_event_codec::encode::trade::trade_mutation_event_build; #[cfg(feature = "runtime")] use radroots_event_store::{RadrootsStoredTradeMutation, RadrootsTradeProjectionCheckpoint}; #[cfg(feature = "runtime")] +use radroots_signing::{Actor, Signer}; +#[cfg(feature = "runtime")] use radroots_trade::evidence::{ RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1, RadrootsTradePrivateTermsEvidenceV1, @@ -112,7 +112,7 @@ impl<'client> TradeCommandService<'client> { pub async fn submit_proposal_with_explicit_signer( &self, request: SubmitProposalRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<TradeCommandReceipt, RadrootsSdkError> { let command = TradeCommandRequest::SubmitProposal(request); enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await @@ -130,7 +130,7 @@ impl<'client> TradeCommandService<'client> { pub async fn propose_revision_with_explicit_signer( &self, request: ProposeRevisionRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<TradeCommandReceipt, RadrootsSdkError> { let command = TradeCommandRequest::ProposeRevision(request); enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await @@ -148,7 +148,7 @@ impl<'client> TradeCommandService<'client> { pub async fn decide_candidate_with_explicit_signer( &self, request: DecideCandidateRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<TradeCommandReceipt, RadrootsSdkError> { let command = TradeCommandRequest::DecideCandidate(request); enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await @@ -166,7 +166,7 @@ impl<'client> TradeCommandService<'client> { pub async fn cancel_trade_with_explicit_signer( &self, request: CancelTradeRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<TradeCommandReceipt, RadrootsSdkError> { let command = TradeCommandRequest::CancelTrade(request); enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await @@ -184,7 +184,7 @@ impl<'client> TradeCommandService<'client> { pub async fn resume_operation_with_explicit_signer( &self, request: ResumeOperationRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<TradeCommandReceipt, RadrootsSdkError> { let command = TradeCommandRequest::ResumeOperation(request); enqueue_trade_command_with_explicit_signer(self.sdk, command, signer).await @@ -305,7 +305,7 @@ impl<'client> TradesClient<'client> { #[non_exhaustive] pub struct SubmitProposalRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub envelope: TradeMutationEnvelopeV1, pub target_policy: TargetPolicy, pub satisfaction_policy: SatisfactionPolicy, @@ -315,7 +315,7 @@ pub struct SubmitProposalRequest { #[cfg(feature = "runtime")] impl SubmitProposalRequest { pub fn new( - actor: RadrootsActorContext, + actor: Actor, envelope: TradeMutationEnvelopeV1, target_policy: TargetPolicy, ) -> Self { @@ -352,7 +352,7 @@ impl SubmitProposalRequest { #[non_exhaustive] pub struct ProposeRevisionRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub envelope: TradeMutationEnvelopeV1, pub target_policy: TargetPolicy, pub satisfaction_policy: SatisfactionPolicy, @@ -362,7 +362,7 @@ pub struct ProposeRevisionRequest { #[cfg(feature = "runtime")] impl ProposeRevisionRequest { pub fn new( - actor: RadrootsActorContext, + actor: Actor, envelope: TradeMutationEnvelopeV1, target_policy: TargetPolicy, ) -> Self { @@ -391,7 +391,7 @@ impl ProposeRevisionRequest { #[non_exhaustive] pub struct DecideCandidateRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub envelope: TradeMutationEnvelopeV1, pub target_policy: TargetPolicy, pub satisfaction_policy: SatisfactionPolicy, @@ -402,7 +402,7 @@ pub struct DecideCandidateRequest { #[cfg(feature = "runtime")] impl DecideCandidateRequest { pub fn new( - actor: RadrootsActorContext, + actor: Actor, envelope: TradeMutationEnvelopeV1, target_policy: TargetPolicy, ) -> Self { @@ -437,7 +437,7 @@ impl DecideCandidateRequest { #[non_exhaustive] pub struct CancelTradeRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub envelope: TradeMutationEnvelopeV1, pub target_policy: TargetPolicy, pub satisfaction_policy: SatisfactionPolicy, @@ -447,7 +447,7 @@ pub struct CancelTradeRequest { #[cfg(feature = "runtime")] impl CancelTradeRequest { pub fn new( - actor: RadrootsActorContext, + actor: Actor, envelope: TradeMutationEnvelopeV1, target_policy: TargetPolicy, ) -> Self { @@ -471,7 +471,7 @@ impl CancelTradeRequest { #[non_exhaustive] pub struct ResumeOperationRequest { #[serde(serialize_with = "crate::actor_json::serialize_actor_context")] - pub actor: RadrootsActorContext, + pub actor: Actor, pub envelope: TradeMutationEnvelopeV1, pub operation_kind: &'static str, pub target_policy: TargetPolicy, @@ -483,7 +483,7 @@ pub struct ResumeOperationRequest { #[cfg(feature = "runtime")] impl ResumeOperationRequest { pub fn new( - actor: RadrootsActorContext, + actor: Actor, envelope: TradeMutationEnvelopeV1, operation_kind: &'static str, target_policy: TargetPolicy, @@ -1040,7 +1040,7 @@ pub struct EvidenceView { #[derive(Clone, Debug)] struct TradeCommandPlan { operation_kind: &'static str, - actor: RadrootsActorContext, + actor: Actor, frozen_draft: EventDraft, trade_id: TradeId, mutation_id: MutationId, @@ -1073,7 +1073,7 @@ impl TradeCommandRequest { fn into_parts( self, ) -> ( - RadrootsActorContext, + Actor, TradeMutationEnvelopeV1, TargetPolicy, SatisfactionPolicy, @@ -1163,7 +1163,7 @@ async fn enqueue_configured_trade_command( async fn enqueue_trade_command_with_explicit_signer( sdk: &RadrootsClient, request: TradeCommandRequest, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<TradeCommandReceipt, RadrootsSdkError> { let plan = trade_command_plan(sdk, request).await?; let enqueue = enqueue_signed_workflow(sdk, workflow_request(&plan), signer).await?; @@ -1209,7 +1209,7 @@ async fn trade_command_plan( canonical.authored_at_unix_s, wire.tags, wire.content, - actor.pubkey().to_hex(), + actor.public_key().to_hex(), ) .map_err(|error| { trade_command_error( @@ -1304,10 +1304,10 @@ fn validate_operation_body( #[cfg(feature = "runtime")] fn validate_actor_matches_envelope( operation_kind: &'static str, - actor: &RadrootsActorContext, + actor: &Actor, envelope: &TradeMutationEnvelopeV1, ) -> Result<(), RadrootsSdkError> { - if actor.pubkey() == &envelope.author_pubkey { + if actor.public_key() == envelope.author_pubkey { Ok(()) } else { Err(RadrootsSdkError::UnauthorizedActor { diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs @@ -5,7 +5,6 @@ use crate::{ TargetPolicy, TargetSet, TransportProfile, runtime::{RuntimeRecoveryReceiptWrite, record_runtime_recovery_receipt, sdk_now_ms}, }; -use radroots_authority::{RadrootsActorContext, RadrootsEventSigner, sign_authorized_draft}; use radroots_event::{ draft::{EventDraft, SignedEvent}, envelope::{EventKind, EventKindClass}, @@ -19,6 +18,11 @@ use radroots_outbox::{ RadrootsOutboxDeliveryPlanInput, RadrootsOutboxEnqueueStatus, RadrootsOutboxReticulumBehavior, RadrootsOutboxSignedOperationInput, RadrootsOutboxSignedTradeMutationInput, }; +use radroots_protocol::runtime::v1::OperationId; +use radroots_signing::{ + Actor, SignRequest, Signer, + request::{CancellationPolicy, SignPolicy}, +}; use radroots_transport::{ RADROOTS_RETICULUM_ENDPOINT_URI, RadrootsTransportKind, RadrootsTransportTarget, }; @@ -30,7 +34,7 @@ const SDK_RUNTIME_CONTRACT_VERSION: &str = "1"; pub(crate) struct SdkWorkflowEnqueueRequest<'a> { pub(crate) operation_kind: &'static str, - pub(crate) actor: &'a RadrootsActorContext, + pub(crate) actor: &'a Actor, pub(crate) frozen_draft: &'a EventDraft, pub(crate) target_policy: TargetPolicy, pub(crate) satisfaction_policy: SatisfactionPolicy, @@ -50,7 +54,7 @@ pub(crate) struct SdkWorkflowEnqueueReceipt { pub(crate) async fn enqueue_signed_workflow( sdk: &RadrootsClient, request: SdkWorkflowEnqueueRequest<'_>, - signer: &dyn RadrootsEventSigner, + signer: &dyn Signer, ) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> { ensure_durable_workflow_kind(&request)?; let delivery_plan = @@ -67,8 +71,9 @@ pub(crate) async fn enqueue_signed_workflow( None, ) .await?; - let signed_event = match sign_authorized_draft(request.actor, signer, request.frozen_draft) { - Ok(signed_event) => signed_event, + let sign_request = signing_request(&request)?; + let signed_event = match signer.sign(sign_request).await { + Ok(receipt) => receipt.signed_event().clone(), Err(error) => { let sdk_error: RadrootsSdkError = error.into(); record_runtime_operation_failure(sdk, &request, &prepared.idempotency_key, &sdk_error) @@ -86,6 +91,55 @@ pub(crate) async fn enqueue_signed_workflow( } } +fn signing_request( + request: &SdkWorkflowEnqueueRequest<'_>, +) -> Result<SignRequest, RadrootsSdkError> { + let operation_id = signing_operation_id(request.operation_kind).ok_or_else(|| { + RadrootsSdkError::InvalidRequest { + message: format!("unknown signing operation `{}`", request.operation_kind), + } + })?; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| RadrootsSdkError::ClockBeforeUnixEpoch)? + .as_secs(); + let deadline_unix = now + .checked_add(30) + .ok_or(RadrootsSdkError::TimestampOutOfRange { value: now })?; + let policy = SignPolicy::new(deadline_unix, CancellationPolicy::PreservePublishedRequest)?; + SignRequest::new( + operation_id, + request.actor.clone(), + request.frozen_draft.clone(), + policy, + ) + .map_err(Into::into) +} + +pub(crate) fn signing_operation_id(operation: &str) -> Option<OperationId> { + match operation { + "farm.publish" | "farm.publish.v1" => Some(OperationId::FarmPublish), + "listing.publish" | "listing.publish.v1" => Some(OperationId::ListingPublish), + "trade.proposal.submit" | "trade.submit_proposal.v1" => { + Some(OperationId::TradeProposalSubmit) + } + "trade.revision.propose" | "trade.propose_revision.v1" => { + Some(OperationId::TradeRevisionPropose) + } + "trade.candidate.decide" | "trade.decide_candidate.v1" => { + Some(OperationId::TradeCandidateDecide) + } + "trade.cancellation.submit" | "trade.cancel.v1" => { + Some(OperationId::TradeCancellationSubmit) + } + "trade.operation.resume" | "trade.resume_operation.v1" => { + Some(OperationId::TradeOperationResume) + } + "sync.push" | "sync.push.v1" => Some(OperationId::SyncPush), + _ => OperationId::parse(operation.strip_suffix(".v1").unwrap_or(operation)).ok(), + } +} + #[cfg(feature = "signer-adapters")] pub(crate) async fn enqueue_configured_signed_workflow( sdk: &RadrootsClient, @@ -616,7 +670,7 @@ async fn prepare_runtime_operation_journal( ) .bind(SDK_RUNTIME_CONTRACT_VERSION) .bind(request.operation_kind) - .bind(request.actor.pubkey().to_hex()) + .bind(request.actor.public_key().to_hex()) .bind(idempotency_key.as_str()) .fetch_optional(&mut *tx) .await @@ -633,7 +687,7 @@ async fn prepare_runtime_operation_journal( let new_digest_prefix = digest_prefix(command_hash.as_str()); let error = RadrootsSdkError::IdempotencyConflict { operation_kind: request.operation_kind.to_owned(), - expected_pubkey_prefix: request.actor.pubkey().to_hex().chars().take(12).collect(), + expected_pubkey_prefix: request.actor.public_key().to_hex().chars().take(12).collect(), existing_digest_prefix: existing_digest_prefix.clone(), new_digest_prefix: new_digest_prefix.clone(), }; @@ -642,7 +696,7 @@ async fn prepare_runtime_operation_journal( ) .bind("idempotency_conflict") .bind(request.operation_kind) - .bind(request.actor.pubkey().to_hex()) + .bind(request.actor.public_key().to_hex()) .bind(idempotency_key.as_str()) .bind("retry_operation_with_same_idempotency_key") .bind( @@ -687,7 +741,7 @@ async fn prepare_runtime_operation_journal( .bind(observed_at_ms) .bind(SDK_RUNTIME_CONTRACT_VERSION) .bind(request.operation_kind) - .bind(request.actor.pubkey().to_hex()) + .bind(request.actor.public_key().to_hex()) .bind(idempotency_key.as_str()) .execute(&mut *tx) .await @@ -702,7 +756,7 @@ async fn prepare_runtime_operation_journal( ) .bind(SDK_RUNTIME_CONTRACT_VERSION) .bind(request.operation_kind) - .bind(request.actor.pubkey().to_hex()) + .bind(request.actor.public_key().to_hex()) .bind(idempotency_key.as_str()) .bind(command_hash.as_str()) .bind(frozen_draft_json.as_str()) @@ -754,7 +808,7 @@ async fn mark_runtime_operation_state( .bind(observed_at_ms) .bind(SDK_RUNTIME_CONTRACT_VERSION) .bind(request.operation_kind) - .bind(request.actor.pubkey().to_hex()) + .bind(request.actor.public_key().to_hex()) .bind(idempotency_key.as_str()) .execute(sdk._event_store.pool()) .await @@ -790,7 +844,7 @@ async fn record_runtime_operation_failure( _ => None, }; if let Some((recovery_code, recovery_action)) = recovery { - let actor_pubkey = request.actor.pubkey().to_hex(); + let actor_pubkey = request.actor.public_key().to_hex(); record_runtime_recovery_receipt( sdk._event_store.pool(), RuntimeRecoveryReceiptWrite { @@ -818,7 +872,7 @@ async fn ensure_runtime_operation_can_commit( ) .bind(SDK_RUNTIME_CONTRACT_VERSION) .bind(request.operation_kind) - .bind(request.actor.pubkey().to_hex()) + .bind(request.actor.public_key().to_hex()) .bind(idempotency_key.as_str()) .fetch_one(tx.as_mut()) .await @@ -859,7 +913,7 @@ async fn commit_runtime_operation_journal( .bind(observed_at_ms) .bind(SDK_RUNTIME_CONTRACT_VERSION) .bind(request.operation_kind) - .bind(request.actor.pubkey().to_hex()) + .bind(request.actor.public_key().to_hex()) .bind(idempotency_key.as_str()) .execute(tx.as_mut()) .await @@ -1016,7 +1070,7 @@ fn runtime_request_digest( let digest_document = serde_json::json!({ "contract_version": SDK_RUNTIME_CONTRACT_VERSION, "operation_kind": request.operation_kind, - "actor_pubkey": request.actor.pubkey().to_hex(), + "actor_pubkey": request.actor.public_key().to_hex(), "draft": { "contract_id": request.frozen_draft.contract_id(), "contract_registry_version": request.frozen_draft.contract_registry_version(), diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs @@ -1,6 +1,5 @@ #![cfg(feature = "runtime")] -use radroots_authority::RadrootsActorContext; use radroots_event::{ contract::AuthorRole, envelope::kind::{KIND_FARM, KIND_PROFILE}, @@ -24,6 +23,7 @@ use radroots_sdk::{ SdkIdempotencyKey, SdkMutationState, SdkPublicLocality, StorageStatusRequest, TargetPolicy, TargetSet, TransportProfile, }; +use radroots_signing::{Actor, actor::ActorSource}; use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsNostrTransport}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; @@ -52,12 +52,22 @@ fn other_pubkey() -> &'static str { fixture_bob_pubkey() } -fn farmer_actor() -> RadrootsActorContext { - RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor") +fn farmer_actor() -> Actor { + Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor") } -fn non_farmer_actor() -> RadrootsActorContext { - RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Buyer]).expect("actor") +fn non_farmer_actor() -> Actor { + Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Buyer], + ) + .expect("actor") } fn farm(d_tag: &str, name: &str) -> Farm { @@ -73,8 +83,8 @@ fn farm(d_tag: &str, name: &str) -> Farm { } } -fn farm_addr(actor: &RadrootsActorContext, d_tag: &str) -> AddressableCoordinate { - AddressableCoordinate::parse(format!("{KIND_FARM}:{}:{d_tag}", actor.pubkey())) +fn farm_addr(actor: &Actor, d_tag: &str) -> AddressableCoordinate { + AddressableCoordinate::parse(format!("{KIND_FARM}:{}:{d_tag}", actor.public_key())) .expect("farm addr") } @@ -658,10 +668,7 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation() .expect_err("signer error"); let message = error.to_string(); - assert!(matches!( - error, - RadrootsSdkError::SignerPubkeyMismatch { .. } - )); + assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. })); assert!(!message.contains("raw")); assert!(!message.contains("ffff")); @@ -986,7 +993,7 @@ async fn farm_runtime_dtos_serialize_deterministically() { "pubkey": farmer_pubkey(), "roles": ["farmer"], "account_id": null, - "source": "test" + "source": "explicit_public_key" }, "farm": { "d_tag": FARM_A_D_TAG, @@ -1023,7 +1030,7 @@ async fn farm_runtime_dtos_serialize_deterministically() { "pubkey": farmer_pubkey(), "roles": ["farmer"], "account_id": null, - "source": "test" + "source": "explicit_public_key" }, "farm": { "d_tag": FARM_B_D_TAG, @@ -1094,7 +1101,7 @@ async fn farm_runtime_dtos_serialize_deterministically() { "pubkey": farmer_pubkey(), "roles": ["farmer"], "account_id": null, - "source": "test" + "source": "explicit_public_key" }, "farm_d_tag": FARM_C_D_TAG, "exact_location": { @@ -1121,7 +1128,7 @@ async fn farm_runtime_dtos_serialize_deterministically() { "pubkey": farmer_pubkey(), "roles": ["farmer"], "account_id": null, - "source": "test" + "source": "explicit_public_key" }, "farm_d_tag": FARM_D_D_TAG, "input": { @@ -1180,7 +1187,7 @@ async fn farm_runtime_dtos_serialize_deterministically() { "pubkey": farmer_pubkey(), "roles": ["farmer"], "account_id": null, - "source": "test" + "source": "explicit_public_key" }, "farm_d_tag": FARM_E_D_TAG }) diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -1,6 +1,5 @@ #![cfg(feature = "runtime")] -use radroots_authority::RadrootsActorContext; use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; use radroots_event::{ contract::AuthorRole, @@ -25,6 +24,7 @@ use radroots_sdk::{ RadrootsSdkTimestamp, ReticulumProfile, SdkIdempotencyKey, SdkMutationState, TargetPolicy, TargetSet, TransportProfile, }; +use radroots_signing::{Actor, actor::ActorSource}; use radroots_trade::operational_listing::RadrootsOperationalListingEditDocumentV1; use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsNostrTransport}; use sqlx::Row; @@ -60,12 +60,22 @@ fn other_pubkey() -> &'static str { fixture_bob_pubkey() } -fn actor() -> RadrootsActorContext { - RadrootsActorContext::test(seller_pubkey(), [AuthorRole::Seller]).expect("actor") +fn actor() -> Actor { + Actor::from_public_key_hex( + seller_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + ) + .expect("actor") } -fn non_seller_actor() -> RadrootsActorContext { - RadrootsActorContext::test(seller_pubkey(), [AuthorRole::Buyer]).expect("actor") +fn non_seller_actor() -> Actor { + Actor::from_public_key_hex( + seller_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Buyer], + ) + .expect("actor") } fn listing(d_tag: &str, title: &str) -> OperationalListing { @@ -403,7 +413,7 @@ async fn listing_runtime_dtos_serialize_deterministically() { prepare_json["actor"]["roles"], serde_json::json!(["seller"]) ); - assert_eq!(prepare_json["actor"]["source"], "test"); + assert_eq!(prepare_json["actor"]["source"], "explicit_public_key"); assert_eq!(prepare_json["created_at"], 1_700_000_123); assert_eq!( prepare_json["document"]["listing"]["product"]["title"], @@ -586,10 +596,7 @@ async fn enqueue_prepared_publish_returns_sanitized_signer_errors() { .expect_err("signer error"); let message = error.to_string(); - assert!(matches!( - error, - RadrootsSdkError::SignerPubkeyMismatch { .. } - )); + assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. })); assert!(!message.contains("raw")); assert!(!message.contains("ffff")); } @@ -662,10 +669,7 @@ async fn enqueue_publish_returns_sanitized_signer_errors() { .expect_err("signer error"); let message = error.to_string(); - assert!(matches!( - error, - RadrootsSdkError::SignerPubkeyMismatch { .. } - )); + assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. })); assert!(!message.contains("raw")); assert!(!message.contains("ffff")); } diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -148,3 +148,59 @@ fn sdk_does_not_expose_generic_wire_part_signing() { assert!(!lib.contains("feature = \"signing\",\n")); assert!(!adapters.contains("pub mod signing")); } + +#[test] +fn sdk_consumes_only_the_final_signing_boundary() { + let manifest = manifest_dir(); + let cargo_manifest = read_source(&manifest.join("Cargo.toml")); + let workspace = manifest + .parent() + .and_then(Path::parent) + .expect("SDK crate belongs to the workspace root"); + let workspace_manifest = read_source(&workspace.join("Cargo.toml")); + let cargo_config = read_source(&workspace.join(".cargo/config.toml")); + assert!(cargo_manifest.contains("radroots_signing = { workspace = true")); + assert!(workspace_manifest.contains("radroots_signing = { package = \"radroots_signing\"")); + assert!(cargo_config.contains("radroots_signing = { path = \"../lib/crates/signing\" }")); + for source in [&cargo_manifest, &workspace_manifest, &cargo_config] { + assert!(!source.contains("radroots_authority")); + } + + let retired_signing_surface = [ + "radroots_authority", + "RadrootsActorContext", + "RadrootsEventSigner", + "RadrootsLocalEventSigner", + ]; + + for root in ["src", "tests", "examples"] { + let directory = manifest.join(root); + let mut files = Vec::new(); + if directory.exists() { + fn collect(directory: &Path, files: &mut Vec<PathBuf>) { + for entry in fs::read_dir(directory).expect("read SDK source tree") { + let path = entry.expect("SDK source entry").path(); + if path.is_dir() { + collect(&path, files); + } else if path.extension().is_some_and(|extension| extension == "rs") { + files.push(path); + } + } + } + collect(&directory, &mut files); + } + for path in files { + if path.ends_with("source_boundary.rs") { + continue; + } + let source = read_source(&path); + for retired in retired_signing_surface { + assert!( + !source.contains(retired), + "{} must use radroots_signing instead of retired `{retired}`", + path.display() + ); + } + } + } +} diff --git a/crates/sdk/tests/support/fixture_signer.rs b/crates/sdk/tests/support/fixture_signer.rs @@ -1,7 +1,7 @@ -use radroots_authority::{RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity}; -use radroots_event::draft::{EventDraft, SignedEvent}; -use radroots_identity::PublicKey; use radroots_nostr::prelude::{RadrootsNostrKeys, radroots_nostr_sign_frozen_draft}; +use radroots_signing::{ + Error, SignReceipt, SignRequest, Signer, SignerStatus, error::Kind, signer::BoxFuture, +}; use std::sync::LazyLock; struct FixtureKeyMaterial { @@ -28,9 +28,7 @@ pub(crate) fn fixture_bob_pubkey() -> &'static str { FIXTURE_BOB.pubkey.as_str() } -#[derive(Clone)] pub struct FixtureSigner { - identity: RadrootsSignerIdentity, keys: RadrootsNostrKeys, } @@ -42,27 +40,29 @@ impl FixtureSigner { _ => panic!("unsupported fixture signer public key"), }; Self { - identity: RadrootsSignerIdentity::new(pubkey).expect("identity"), keys: material.keys.clone(), } } + + #[allow(dead_code)] + pub fn sign_frozen_draft( + &self, + draft: &radroots_event::EventDraft, + ) -> Result<radroots_event::SignedEvent, radroots_nostr::error::RadrootsNostrError> { + radroots_nostr_sign_frozen_draft(&self.keys, draft) + } } -impl RadrootsEventSigner for FixtureSigner { - fn pubkey(&self) -> &PublicKey { - self.identity.pubkey() +impl Signer for FixtureSigner { + fn status(&self) -> BoxFuture<'_, Result<SignerStatus, Error>> { + Box::pin(async { Ok(SignerStatus::unavailable()) }) } - fn sign_frozen_draft(&self, draft: &EventDraft) -> Result<SignedEvent, RadrootsSignerError> { - if self.pubkey() != draft.expected_pubkey() { - return Err(RadrootsSignerError::SigningFailed { - message: "wrong fixture signer".to_owned(), - }); - } - radroots_nostr_sign_frozen_draft(&self.keys, draft).map_err(|error| { - RadrootsSignerError::SigningFailed { - message: error.to_string(), - } + fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> { + Box::pin(async move { + let signed_event = radroots_nostr_sign_frozen_draft(&self.keys, request.draft()) + .map_err(|source| Error::with_source(Kind::AuthorizationDenied, source))?; + SignReceipt::from_signed_event(&request, signed_event, 1_700_000_001) }) } } diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -1,7 +1,6 @@ #![cfg(feature = "runtime")] use futures::future::BoxFuture; -use radroots_authority::{RadrootsActorContext, RadrootsEventSigner}; use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit}; use radroots_event::{ contract::AuthorRole, @@ -31,6 +30,7 @@ use radroots_sdk::{ ReticulumTryNowRequest, SdkBackupManifestKind, SdkRelayAuthPolicy, SdkRestoreState, StorageStatusRequest, SyncStatusRequest, SyncStatusSource, TargetPolicy, TransportProfile, }; +use radroots_signing::{Actor, actor::ActorSource}; use radroots_transport::{ RADROOTS_RETICULUM_UNAVAILABLE_MESSAGE, RadrootsTransportMeshScopeId, RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetLabel, @@ -383,8 +383,13 @@ impl RadrootsRelayPublishAdapter for RecordingPublishAdapter { } } -fn actor() -> RadrootsActorContext { - RadrootsActorContext::test(seller_pubkey(), [AuthorRole::Seller]).expect("actor") +fn actor() -> Actor { + Actor::from_public_key_hex( + seller_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + ) + .expect("actor") } fn listing(d_tag: &str, title: &str) -> OperationalListing { diff --git a/crates/sdk/tests/unit/actor_json_tests.rs b/crates/sdk/tests/unit/actor_json_tests.rs @@ -1,6 +1,7 @@ use super::{SdkActorContextJson, actor_role_code, actor_source_code}; -use radroots_authority::{RadrootsActorContext, RadrootsActorSource}; use radroots_event::contract::AuthorRole; +use radroots_identity::AccountId; +use radroots_signing::{Actor, actor::ActorSource}; use crate::serializer_failure::assert_struct_serialize_error_paths; @@ -19,26 +20,28 @@ fn actor_role_and_source_codes_cover_public_actor_taxonomy() { assert_eq!(actor_role_code(&AuthorRole::Service), "service"); assert_eq!( - actor_source_code(RadrootsActorSource::LocalAccount), + actor_source_code(ActorSource::LocalAccount(account_id())), "local_account" ); assert_eq!( - actor_source_code(RadrootsActorSource::ExplicitPubkey), - "explicit_pubkey" + actor_source_code(ActorSource::ExplicitPublicKey), + "explicit_public_key" ); assert_eq!( - actor_source_code(RadrootsActorSource::RemoteSigner), + actor_source_code(ActorSource::RemoteSigner(account_id())), "remote_signer" ); - assert_eq!(actor_source_code(RadrootsActorSource::Service), "service"); - assert_eq!(actor_source_code(RadrootsActorSource::Test), "test"); + assert_eq!( + actor_source_code(ActorSource::Service(account_id())), + "service" + ); } #[test] fn actor_context_json_preserves_source_roles_and_account_id() { - let actor = RadrootsActorContext::local_account( + let actor = Actor::from_public_key_hex( PUBKEY, - "acct-1", + ActorSource::LocalAccount(account_id()), [AuthorRole::Buyer, AuthorRole::Seller], ) .expect("actor"); @@ -50,7 +53,7 @@ fn actor_context_json_preserves_source_roles_and_account_id() { serde_json::json!({ "pubkey": PUBKEY, "roles": ["buyer", "seller"], - "account_id": "acct-1", + "account_id": PUBKEY, "source": "local_account" }) ); @@ -58,12 +61,16 @@ fn actor_context_json_preserves_source_roles_and_account_id() { #[test] fn actor_context_json_reports_serializer_failures() { - let actor = RadrootsActorContext::local_account( + let actor = Actor::from_public_key_hex( PUBKEY, - "acct-1", + ActorSource::LocalAccount(account_id()), [AuthorRole::Buyer, AuthorRole::Seller], ) .expect("actor"); assert_struct_serialize_error_paths(&SdkActorContextJson(&actor), 4); } + +fn account_id() -> AccountId { + AccountId::from_hex(PUBKEY).expect("account ID") +} diff --git a/crates/sdk/tests/unit/error_tests.rs b/crates/sdk/tests/unit/error_tests.rs @@ -4,28 +4,24 @@ use super::{ }; use crate::privacy::{PrivacyPreflightStatus, ProductSensitivityField}; use crate::transport::ReticulumBehavior; -use radroots_authority::RadrootsAuthorityError; use radroots_geocoder::{GeoNamesAssetFetcher, GeoNamesBlockingHttpFetcher, GeocoderError}; +use radroots_signing::{Error as SigningError, error::Kind as SigningErrorKind}; use std::collections::BTreeSet; #[test] -fn authority_error_conversion_redacts_pubkey_mismatches_and_falls_back() { - let actor_error = RadrootsSdkError::from(RadrootsAuthorityError::ActorPubkeyMismatch { - expected_pubkey: "a".repeat(64), - actor_pubkey: "b".repeat(64), - }); +fn signing_error_conversion_preserves_normalized_failures() { + let actor_error = + RadrootsSdkError::from(SigningError::new(SigningErrorKind::AuthorizationDenied)); assert!(matches!( actor_error, RadrootsSdkError::UnauthorizedActor { ref reason, .. } - if reason == "actor_pubkey_prefix=bbbbbbbbbbbb expected_pubkey_prefix=aaaaaaaaaaaa" + if reason == "actor or signer is not authorized for the frozen draft" )); - let fallback = RadrootsSdkError::from(RadrootsAuthorityError::UnknownContract { - contract_id: "contract-x".to_owned(), - }); + let fallback = RadrootsSdkError::from(SigningError::new(SigningErrorKind::InvalidArgument)); assert!(matches!( fallback, - RadrootsSdkError::Authority { ref message } if message.contains("contract-x") + RadrootsSdkError::Authority { ref message } if message == "signing request is invalid" )); } diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs @@ -4,6 +4,7 @@ use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use crate::fixture_signer::{FixtureSigner, fixture_alice_pubkey, fixture_bob_pubkey}; use crate::serializer_failure::assert_struct_serialize_error_paths; +use radroots_signing::actor::ActorSource; const FARM_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; const FARM_B_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ"; @@ -15,8 +16,13 @@ fn farmer_pubkey() -> &'static str { fixture_alice_pubkey() } -fn farmer_actor() -> RadrootsActorContext { - RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor") +fn farmer_actor() -> Actor { + Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor") } fn farm(d_tag: &str, name: &str) -> Farm { @@ -117,8 +123,9 @@ async fn fixture_geocoder(tempdir: &tempfile::TempDir, feature_name: Option<&str #[test] fn farm_publish_plan_rejects_invalid_draft_tags() { - let actor = RadrootsActorContext::test( + let actor = Actor::from_public_key_hex( "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ActorSource::ExplicitPublicKey, [AuthorRole::Farmer], ) .expect("actor"); @@ -748,13 +755,21 @@ async fn farm_configured_local_signer_enqueues_publish_without_explicit_signer() let sdk = crate::RadrootsClient::builder() .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500)) .signer_provider(RadrootsSdkSignerProvider::LocalKey( - RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(farmer_pubkey())) - .expect("signer"), + RadrootsSdkLocalKeySigner::from_signer( + FixtureSigner::new(farmer_pubkey()), + farmer_pubkey(), + ) + .expect("signer"), )) .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let receipt = sdk .farms() @@ -780,13 +795,21 @@ async fn farm_configured_enqueue_reports_prepare_and_signer_errors() { let configured_sdk = crate::RadrootsClient::builder() .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500)) .signer_provider(RadrootsSdkSignerProvider::LocalKey( - RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(farmer_pubkey())) - .expect("signer"), + RadrootsSdkLocalKeySigner::from_signer( + FixtureSigner::new(farmer_pubkey()), + farmer_pubkey(), + ) + .expect("signer"), )) .build() .await .expect("configured sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); assert!(matches!( configured_sdk @@ -918,8 +941,12 @@ async fn farm_private_location_default_client_and_lookup_report_store_edges() { farm_addr(&actor, FARM_B_D_TAG).expect("farm b addr") ); - let non_farmer_actor = - RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Buyer]).expect("buyer actor"); + let non_farmer_actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Buyer], + ) + .expect("buyer actor"); assert!(matches!( sdk.farms() .clear_private_location(FarmPrivateLocationClearRequest::new( diff --git a/crates/sdk/tests/unit/listings_runtime_tests.rs b/crates/sdk/tests/unit/listings_runtime_tests.rs @@ -14,6 +14,7 @@ use radroots_event::{ use crate::fixture_signer::{FixtureSigner, fixture_alice_pubkey, fixture_bob_pubkey}; use crate::serializer_failure::assert_struct_serialize_error_paths; +use radroots_signing::actor::ActorSource; const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; const LISTING_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ"; @@ -26,8 +27,13 @@ fn seller_pubkey() -> &'static str { fixture_alice_pubkey() } -fn actor() -> RadrootsActorContext { - RadrootsActorContext::test(seller_pubkey(), [AuthorRole::Seller]).expect("actor") +fn actor() -> Actor { + Actor::from_public_key_hex( + seller_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + ) + .expect("actor") } fn listing(d_tag: &str, title: &str) -> OperationalListing { @@ -511,13 +517,21 @@ async fn listing_configured_local_signer_enqueues_publish_without_explicit_signe let sdk = crate::RadrootsClient::builder() .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500)) .signer_provider(RadrootsSdkSignerProvider::LocalKey( - RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(seller_pubkey())) - .expect("signer"), + RadrootsSdkLocalKeySigner::from_signer( + FixtureSigner::new(seller_pubkey()), + seller_pubkey(), + ) + .expect("signer"), )) .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(seller_pubkey(), [AuthorRole::Seller]).expect("actor"); + let actor = Actor::from_public_key_hex( + seller_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + ) + .expect("actor"); let receipt = sdk .listings() diff --git a/crates/sdk/tests/unit/signer_provider_tests.rs b/crates/sdk/tests/unit/signer_provider_tests.rs @@ -1,17 +1,17 @@ use super::*; use nostr::nips::nip44::{self, Version}; use nostr::{EventBuilder, JsonUtil, Kind, Tag}; -use radroots_authority::RadrootsLocalEventSigner; use radroots_event::contract::AuthorRole; use radroots_event::draft::EventDraft; use radroots_event::envelope::kind::{ KIND_CLASSIFIED_LISTING, KIND_COOP, KIND_FARM, TRADE_MUTATION_EVENT_KINDS, }; -use radroots_nostr::prelude::RadrootsNostrEvent; +use radroots_nostr::{prelude::RadrootsNostrEvent, signing::LocalSigner}; use radroots_nostr_connect::prelude::{ RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectClientTarget, RadrootsNostrConnectError, RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, }; +use radroots_signing::actor::ActorSource; use std::collections::VecDeque; use std::future; use std::sync::{Arc, LazyLock, Mutex}; @@ -32,10 +32,9 @@ fn user_pubkey() -> &'static str { } fn local_sdk_signer(keys: RadrootsNostrKeys) -> RadrootsSdkLocalKeySigner { - RadrootsSdkLocalKeySigner::from_event_signer( - RadrootsLocalEventSigner::new(keys).expect("local event signer"), - ) - .expect("sdk local signer") + let signer_pubkey = keys.public_key().to_hex(); + RadrootsSdkLocalKeySigner::from_signer(LocalSigner::new(keys), signer_pubkey) + .expect("sdk local signer") } fn remote_keys() -> RadrootsNostrKeys { @@ -46,8 +45,13 @@ fn client_keys() -> RadrootsNostrKeys { CLIENT_KEYS.clone() } -fn actor() -> RadrootsActorContext { - RadrootsActorContext::test(user_pubkey(), [AuthorRole::Farmer]).expect("actor") +fn actor() -> Actor { + Actor::from_public_key_hex( + user_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor") } fn frozen_draft() -> EventDraft { @@ -273,8 +277,12 @@ async fn local_key_provider_returns_progress_sink_errors_without_transport_state let signer = local_sdk_signer(user_keys()); let draft = frozen_draft(); let actor = actor(); - let wrong_actor = - RadrootsActorContext::test("a".repeat(64), [AuthorRole::Farmer]).expect("wrong actor"); + let wrong_actor = Actor::from_public_key_hex( + &"a".repeat(64), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("wrong actor"); assert!(matches!( signer @@ -409,14 +417,6 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() { #[test] fn nip46_private_helpers_map_identity_adapter_and_response_edges() { - let pubkey = user_pubkey().parse().expect("pubkey"); - let identity = RadrootsSdkSignerIdentityOnly { pubkey }; - assert_eq!(identity.pubkey().to_hex(), user_pubkey()); - assert!(matches!( - identity.sign_frozen_draft(&frozen_draft()), - Err(RadrootsSignerError::Unavailable) - )); - assert!(matches!( signed_event_from_nip46_response( "farm.publish", @@ -645,8 +645,12 @@ async fn myc_nip46_provider_reports_preflight_and_progress_sink_edges() { )); assert!(transport.published().is_empty()); - let wrong_actor = - RadrootsActorContext::test("a".repeat(64), [AuthorRole::Farmer]).expect("wrong actor"); + let wrong_actor = Actor::from_public_key_hex( + &"a".repeat(64), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("wrong actor"); let actor_error = signer .sign(RadrootsSdkSignRequest::new( "farm.publish", @@ -678,7 +682,7 @@ async fn myc_nip46_provider_reports_preflight_and_progress_sink_edges() { .expect_err("signer mismatch"); assert!(matches!( signer_error, - RadrootsSdkError::SignerPubkeyMismatch { .. } + RadrootsSdkError::UnauthorizedActor { .. } )); assert!(mismatch_transport.published().is_empty()); } diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs @@ -18,20 +18,14 @@ use crate::adapters::radrootsd::{RadrootsdError, RadrootsdPublishAdapter, Radroo use crate::workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow}; use futures::future::BoxFuture; #[cfg(feature = "radrootsd-execution")] -use radroots_authority::{ - RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity, -}; -#[cfg(feature = "radrootsd-execution")] use radroots_event::contract::AuthorRole; #[cfg(feature = "radrootsd-execution")] -use radroots_event::draft::{EventDraft, SignedEvent}; +use radroots_event::draft::EventDraft; #[cfg(feature = "radrootsd-execution")] use radroots_event::envelope::kind::KIND_FARM; use radroots_event::id::EventId; use radroots_event_store::RadrootsEventStoreStatusSummary; #[cfg(feature = "radrootsd-execution")] -use radroots_identity::PublicKey; -#[cfg(feature = "radrootsd-execution")] use radroots_nostr::prelude::{RadrootsNostrKeys, radroots_nostr_sign_frozen_draft}; #[cfg(feature = "radrootsd-execution")] use radroots_outbox::{ @@ -52,6 +46,11 @@ use radroots_protocol::radrootsd::transport_publish::v5::{ TargetOutcome as TransportPublishTargetOutcome, TargetPolicy as TransportPublishTargetPolicy, TargetSource as TransportPublishTargetSource, }; +#[cfg(feature = "radrootsd-execution")] +use radroots_signing::{ + Actor, Error as SigningError, SignReceipt, SignRequest, Signer, SignerStatus, + actor::ActorSource, error::Kind as SigningErrorKind, signer::BoxFuture as SigningFuture, +}; use radroots_transport::{ RadrootsTransportDeliveryTargetStatus, RadrootsTransportMeshScopeId, RadrootsTransportTarget, RadrootsTransportTargetLabel, @@ -89,7 +88,6 @@ struct UnusedPublishAdapter; #[cfg(feature = "radrootsd-execution")] struct RadrootsdFixtureSigner { - identity: RadrootsSignerIdentity, keys: RadrootsNostrKeys, } @@ -97,24 +95,31 @@ struct RadrootsdFixtureSigner { impl RadrootsdFixtureSigner { fn new() -> Self { Self { - identity: RadrootsSignerIdentity::new(radrootsd_fixture_signer_pubkey()) - .expect("identity"), keys: RADROOTSD_FIXTURE_SIGNER_KEYS.clone(), } } + + fn sign_frozen_draft( + &self, + draft: &EventDraft, + ) -> Result<radroots_event::SignedEvent, radroots_nostr::error::RadrootsNostrError> { + radroots_nostr_sign_frozen_draft(&self.keys, draft) + } } #[cfg(feature = "radrootsd-execution")] -impl RadrootsEventSigner for RadrootsdFixtureSigner { - fn pubkey(&self) -> &PublicKey { - self.identity.pubkey() +impl Signer for RadrootsdFixtureSigner { + fn status(&self) -> SigningFuture<'_, Result<SignerStatus, SigningError>> { + Box::pin(async { Ok(SignerStatus::unavailable()) }) } - fn sign_frozen_draft(&self, draft: &EventDraft) -> Result<SignedEvent, RadrootsSignerError> { - radroots_nostr_sign_frozen_draft(&self.keys, draft).map_err(|error| { - RadrootsSignerError::SigningFailed { - message: error.to_string(), - } + fn sign(&self, request: SignRequest) -> SigningFuture<'_, Result<SignReceipt, SigningError>> { + Box::pin(async move { + let signed_event = radroots_nostr_sign_frozen_draft(&self.keys, request.draft()) + .map_err(|source| { + SigningError::with_source(SigningErrorKind::InternalError, source) + })?; + SignReceipt::from_signed_event(&request, signed_event, 1_700_000_001) }) } } @@ -130,9 +135,13 @@ impl RadrootsRelayPublishAdapter for UnusedPublishAdapter { } #[cfg(feature = "radrootsd-execution")] -fn radrootsd_actor() -> RadrootsActorContext { - RadrootsActorContext::test(radrootsd_fixture_signer_pubkey(), [AuthorRole::Farmer]) - .expect("actor") +fn radrootsd_actor() -> Actor { + Actor::from_public_key_hex( + radrootsd_fixture_signer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor") } #[cfg(feature = "radrootsd-execution")] @@ -164,7 +173,7 @@ async fn claimed_radrootsd_event( enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "sync.radrootsd.unit.v1", + operation_kind: "sync.push.v1", actor: &actor, frozen_draft: &draft, target_policy: crate::TargetPolicy::try_nostr_relays( diff --git a/crates/sdk/tests/unit/trade_runtime_tests.rs b/crates/sdk/tests/unit/trade_runtime_tests.rs @@ -3,7 +3,6 @@ use crate::{ RadrootsClient, RadrootsSdkError, RadrootsSdkTimestamp, RadrootsSdkTradeErrorKind, SatisfactionPolicy, SdkIdempotencyKey, TargetPolicy, }; -use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner}; use radroots_event::{ contract::AuthorRole, envelope::kind::TRADE_MUTATION_EVENT_KINDS, @@ -20,6 +19,8 @@ use radroots_event::{ }; use radroots_identity::PublicKey; use radroots_nostr::prelude::RadrootsNostrKeys; +use radroots_nostr::signing::LocalSigner; +use radroots_signing::{Actor, actor::ActorSource}; use radroots_trade::model::RadrootsTradePrivateTermsStateV1; use radroots_trade::reducer::{RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION}; @@ -35,13 +36,10 @@ fn trade_id() -> TradeId { TradeId::parse("11111111111111111111111111111111").expect("trade id") } -fn local_signer() -> (String, RadrootsLocalEventSigner) { +fn local_signer() -> (String, LocalSigner) { let keys = RadrootsNostrKeys::generate(); let pubkey = keys.public_key().to_hex(); - ( - pubkey, - RadrootsLocalEventSigner::new(keys).expect("local event signer"), - ) + (pubkey, LocalSigner::new(keys)) } #[tokio::test] @@ -105,12 +103,22 @@ async fn trade_capabilities_report_canonical_release_product_surface() { assert!(!capabilities.optional_integrations.reticulum_transport); } -fn buyer_actor(buyer_pubkey: &str) -> RadrootsActorContext { - RadrootsActorContext::test(buyer_pubkey, [AuthorRole::Buyer]).expect("buyer") +fn buyer_actor(buyer_pubkey: &str) -> Actor { + Actor::from_public_key_hex( + buyer_pubkey, + ActorSource::ExplicitPublicKey, + [AuthorRole::Buyer], + ) + .expect("buyer") } -fn seller_actor(seller_pubkey: &str) -> RadrootsActorContext { - RadrootsActorContext::test(seller_pubkey, [AuthorRole::Seller]).expect("seller") +fn seller_actor(seller_pubkey: &str) -> Actor { + Actor::from_public_key_hex( + seller_pubkey, + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], + ) + .expect("seller") } fn candidate(buyer_pubkey: &str, seller_pubkey: &str) -> TradeCandidateTermsV1 { diff --git a/crates/sdk/tests/unit/workflow_runtime_tests.rs b/crates/sdk/tests/unit/workflow_runtime_tests.rs @@ -1,12 +1,14 @@ use super::*; #[cfg(feature = "signer-adapters")] use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider}; -use radroots_authority::{RadrootsSignerError, RadrootsSignerIdentity}; use radroots_event::contract::AuthorRole; use radroots_event::draft::{EventDraft, SignedEvent, SignedEventParts}; use radroots_event::envelope::kind::{KIND_FARM, KIND_GEOCHAT}; -use radroots_identity::PublicKey; use radroots_nostr::prelude::{RadrootsNostrKeys, radroots_nostr_sign_frozen_draft}; +use radroots_signing::{ + Error as SigningError, SignReceipt, SignRequest, SignerStatus, actor::ActorSource, + error::Kind as SigningErrorKind, signer::BoxFuture, +}; use std::sync::LazyLock; struct WorkflowKeyMaterial { @@ -30,37 +32,31 @@ fn workflow_idempotency_key(index: u16) -> SdkIdempotencyKey { } struct WorkflowSigner { - identity: RadrootsSignerIdentity, keys: RadrootsNostrKeys, } impl WorkflowSigner { fn new() -> Self { Self { - identity: RadrootsSignerIdentity::new(farmer_pubkey()).expect("identity"), keys: WORKFLOW_KEY_MATERIAL.keys.clone(), } } } -struct FailIfCalledSigner { - identity: RadrootsSignerIdentity, -} +struct FailIfCalledSigner; impl FailIfCalledSigner { fn new() -> Self { - Self { - identity: RadrootsSignerIdentity::new(farmer_pubkey()).expect("identity"), - } + Self } } -impl RadrootsEventSigner for FailIfCalledSigner { - fn pubkey(&self) -> &PublicKey { - self.identity.pubkey() +impl Signer for FailIfCalledSigner { + fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> { + Box::pin(async { Ok(SignerStatus::unavailable()) }) } - fn sign_frozen_draft(&self, _draft: &EventDraft) -> Result<SignedEvent, RadrootsSignerError> { + fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> { panic!("ephemeral workflow preflight must not invoke the signer") } } @@ -73,35 +69,39 @@ impl InvalidSignatureSigner { } } -impl RadrootsEventSigner for InvalidSignatureSigner { - fn pubkey(&self) -> &PublicKey { - self.0.pubkey() +impl Signer for InvalidSignatureSigner { + fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> { + Box::pin(async { Ok(SignerStatus::unavailable()) }) } - fn sign_frozen_draft(&self, draft: &EventDraft) -> Result<SignedEvent, RadrootsSignerError> { - let signed = self.0.sign_frozen_draft(draft)?; - let mut wire = signed.wire().clone(); - wire.sig = "0".repeat(128); - let raw_json = - serde_json::to_string(&wire).expect("invalid-signature fixture must serialize"); - SignedEvent::from_wire_verified_id(wire, raw_json).map_err(|error| { - RadrootsSignerError::SigningFailed { - message: error.to_string(), - } + fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> { + Box::pin(async move { + let receipt = self.0.sign(request.clone()).await?; + let mut wire = receipt.signed_event().wire().clone(); + wire.sig = "0".repeat(128); + let raw_json = + serde_json::to_string(&wire).expect("invalid-signature fixture must serialize"); + let signed_event = + SignedEvent::from_wire_verified_id(wire, raw_json).map_err(|source| { + SigningError::with_source(SigningErrorKind::InternalError, source) + })?; + SignReceipt::from_signed_event(&request, signed_event, 1_700_000_001) }) } } -impl RadrootsEventSigner for WorkflowSigner { - fn pubkey(&self) -> &PublicKey { - self.identity.pubkey() +impl Signer for WorkflowSigner { + fn status(&self) -> BoxFuture<'_, Result<SignerStatus, SigningError>> { + Box::pin(async { Ok(SignerStatus::unavailable()) }) } - fn sign_frozen_draft(&self, draft: &EventDraft) -> Result<SignedEvent, RadrootsSignerError> { - radroots_nostr_sign_frozen_draft(&self.keys, draft).map_err(|error| { - RadrootsSignerError::SigningFailed { - message: error.to_string(), - } + fn sign(&self, request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, SigningError>> { + Box::pin(async move { + let signed_event = radroots_nostr_sign_frozen_draft(&self.keys, request.draft()) + .map_err(|source| { + SigningError::with_source(SigningErrorKind::InternalError, source) + })?; + SignReceipt::from_signed_event(&request, signed_event, 1_700_000_001) }) } } @@ -208,7 +208,7 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() { let idempotency_key = SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237").expect("idempotency"); let input = signed_outbox_input( - "workflow.test.v1", + "farm.publish.v1", &draft, signed_event(), workflow_delivery_plan(), @@ -216,7 +216,7 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() { true, 1_700_000_000_000, ); - assert_eq!(input.operation_kind, "workflow.test.v1"); + assert_eq!(input.operation_kind, "farm.publish.v1"); assert_eq!( input.delivery_plan.targets[0].uri().as_str(), "wss://relay.example.com" @@ -272,7 +272,12 @@ async fn enqueue_signed_workflow_rejects_ephemeral_event_before_durable_commit() .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = ephemeral_draft_for(farmer_pubkey()); let error = enqueue_signed_workflow( &sdk, @@ -330,9 +335,14 @@ async fn enqueue_signed_workflow_rejects_invalid_signer_signature_without_storag .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-invalid-signature"); - let operation_kind = "workflow.invalid-signature.test.v1"; + let operation_kind = "farm.publish.v1"; let error = enqueue_signed_workflow( &sdk, @@ -397,7 +407,12 @@ async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_comm .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let signer = WorkflowSigner::new(); let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-target-policy"); let first_target_policy = TargetPolicy::try_nostr_relays( @@ -414,7 +429,7 @@ async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_comm let first = enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: first_target_policy.clone(), @@ -428,7 +443,7 @@ async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_comm let replay = enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: first_target_policy.clone(), @@ -442,7 +457,7 @@ async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_comm let conflict = enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: second_target_policy, @@ -511,14 +526,19 @@ async fn enqueue_signed_workflow_maps_no_wait_directly_and_allows_local_only_pro .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let signer = WorkflowSigner::new(); let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-no-wait"); let receipt = enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: TargetPolicy::default_profile(), @@ -584,13 +604,18 @@ async fn enqueue_signed_workflow_rejects_missing_explicit_idempotency_key_withou .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-missing-idempotency"); let error = match enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: TargetPolicy::default_profile(), @@ -634,7 +659,12 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let signer = WorkflowSigner::new(); let first_draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-success"); let idempotency_key = @@ -642,7 +672,7 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con let receipt = enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &first_draft, target_policy: TargetPolicy::default_profile(), @@ -683,7 +713,7 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con let error = match enqueue_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &second_draft, target_policy: TargetPolicy::default_profile(), @@ -703,7 +733,7 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con RadrootsSdkError::IdempotencyConflict { operation_kind, .. - } if operation_kind == "workflow.test.v1" + } if operation_kind == "farm.publish.v1" )); assert_eq!( sdk._event_store @@ -732,19 +762,24 @@ async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() { 1_700_000_011, )) .signer_provider(RadrootsSdkSignerProvider::LocalKey( - RadrootsSdkLocalKeySigner::from_event_signer(WorkflowSigner::new()) + RadrootsSdkLocalKeySigner::from_signer(WorkflowSigner::new(), farmer_pubkey()) .expect("local signer"), )) .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-configured"); let receipt = enqueue_configured_signed_workflow( &sdk, SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: TargetPolicy::default_profile(), @@ -778,10 +813,15 @@ async fn enqueue_signed_workflow_reports_runtime_pool_failure_before_mutation() 0 ); sdk._outbox.pool().close().await; - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = frozen_draft_for(farmer_pubkey()); let request = SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: TargetPolicy::default_profile(), @@ -799,7 +839,12 @@ async fn enqueue_signed_workflow_reports_runtime_pool_failure_before_mutation() #[tokio::test] async fn enqueue_signed_workflow_reports_store_failures() { - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = frozen_draft_for(farmer_pubkey()); let closed_store_sdk = crate::RadrootsClient::builder() .transport_profile(nostr_profile("wss://relay.example.com")) @@ -808,7 +853,7 @@ async fn enqueue_signed_workflow_reports_store_failures() { .expect("sdk"); closed_store_sdk._event_store.pool().close().await; let store_failure_request = SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: TargetPolicy::default_profile(), @@ -834,10 +879,15 @@ async fn enqueue_signed_workflow_reports_clock_failures() { .build() .await .expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = frozen_draft_for(farmer_pubkey()); let request = SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: TargetPolicy::default_profile(), @@ -853,10 +903,15 @@ async fn enqueue_signed_workflow_reports_clock_failures() { #[tokio::test] async fn enqueue_signed_workflow_rejects_transport_profile_targets_without_radrootsd_execution() { let sdk = crate::RadrootsClient::builder().build().await.expect("sdk"); - let actor = RadrootsActorContext::test(farmer_pubkey(), [AuthorRole::Farmer]).expect("actor"); + let actor = Actor::from_public_key_hex( + farmer_pubkey(), + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], + ) + .expect("actor"); let draft = frozen_draft_for(farmer_pubkey()); let request = SdkWorkflowEnqueueRequest { - operation_kind: "workflow.test.v1", + operation_kind: "farm.publish.v1", actor: &actor, frozen_draft: &draft, target_policy: TargetPolicy::DefaultProfile,