sdk

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

commit 915bc14f3951f99ba853667690776068f991a6af
parent 58c42a7a77cb5f60dd10bb50695d92586232adfd
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 11:01:20 +0000

sdk: migrate signer providers to radroots_signing

- compose SDK providers over the canonical signer SPI
- use the concrete lower local signer adapter
- preserve NIP-46 protocol and relay ownership boundaries
- verify challenge error cancellation and capability behavior

Diffstat:
Mcrates/sdk/src/client.rs | 36++++++++++++++++++++++++++++++++++++
Dcrates/sdk/src/signer_provider.rs | 872-------------------------------------------------------------------------------
Mcrates/sdk/src/signing.rs | 255++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Dcrates/sdk/tests/unit/signer_provider_tests.rs | 1129-------------------------------------------------------------------------------
4 files changed, 290 insertions(+), 2002 deletions(-)

diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -96,6 +96,24 @@ impl ClientBuilder { self } + /// Installs a module-scoped signer provider over the canonical SPI and + /// records its presentation-independent runtime capability. + #[must_use] + pub fn signing(mut self, provider: crate::signing::Provider) -> Self { + let capability = match provider.mode() { + crate::signing::Mode::Local => Some(CapabilityId::LOCAL_SIGNING), + crate::signing::Mode::Nip46 => Some(CapabilityId::NIP46_SIGNING), + crate::signing::Mode::Host => None, + }; + self.signer = Some(provider.into_signer()); + if let Some(capability) = capability { + self.explicitly_configured_capabilities.insert(capability); + self.capability_availability + .insert(capability, Availability::Available); + } + self + } + /// Injects an optional inbound event source. #[must_use] pub fn source(mut self, source: Arc<dyn EventSource>) -> Self { @@ -447,6 +465,24 @@ mod tests { ); } + #[cfg(feature = "local-signing")] + #[test] + fn module_scoped_signing_provider_configures_the_matching_capability() { + let signer = radroots_nostr::signing::LocalSigner::generate().expect("local signer"); + let client = ClientBuilder::memory(generation()) + .sink(Arc::new(TestSink)) + .signing(crate::signing::Provider::local(signer)) + .build() + .expect("client"); + let status = client + .capabilities() + .get(CapabilityId::LOCAL_SIGNING) + .expect("local signing"); + assert!(status.is_compiled()); + assert!(status.is_configured()); + assert_eq!(status.availability(), Availability::Available); + } + #[test] fn close_is_clone_shared_idempotent_and_rejects_later_capability_access() { let client = ClientBuilder::memory(generation()).build().expect("client"); diff --git a/crates/sdk/src/signer_provider.rs b/crates/sdk/src/signer_provider.rs @@ -1,872 +0,0 @@ -use crate::{RadrootsSdkError, workflow_runtime::signing_operation_id}; -use core::fmt; -use nostr::Keys as RadrootsNostrKeys; -use nostr::{JsonUtil, Kind, PublicKey as NostrPublicKey, Tag, Tags, Timestamp, UnsignedEvent}; -use radroots_event::draft::{EventDraft, SignedEvent}; -use radroots_event::envelope::kind::{ - KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, - KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL, -}; -use radroots_event::wire::Nip01EventWire; -use radroots_identity::PublicKey; -use radroots_nostr::event::Event as RadrootsNostrEvent; -use radroots_nostr_connect::{ - Error as NostrConnectError, Method, Permission, Request, Response, - client::{ - CancellationToken, Client, ClientEvent, Completion, Progress, Receive, Target, Transport, - TransportFuture, - }, - message::{RequestId, UnsignedEvent as ConnectUnsignedEvent}, - permission::Permissions, -}; -use radroots_signing::{ - Actor, SignReceipt, SignRequest, Signer, - request::{CancellationPolicy, SignPolicy}, -}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::Mutex as AsyncMutex; -use tokio::time::timeout; -use uuid::Uuid; - -pub type RadrootsSdkLocalSignerCapability = dyn Signer; - -pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 7] = [ - KIND_FARM, - KIND_CLASSIFIED_LISTING, - KIND_TRADE_PROPOSAL, - KIND_TRADE_DECISION, - KIND_TRADE_REVISION_PROPOSAL, - KIND_TRADE_REVISION_DECISION, - KIND_TRADE_CANCELLATION, -]; -pub const RADROOTS_SDK_MYC_NIP46_DEFAULT_REQUEST_TIMEOUT_MS: u64 = 30_000; - -/// Private key transition surface retained through CLI migration Step 271; removed in Step 313. -#[doc(hidden)] -pub struct RadrootsSdkNip46ClientKey { - keys: RadrootsNostrKeys, -} - -#[doc(hidden)] -impl RadrootsSdkNip46ClientKey { - #[must_use] - pub fn generate() -> Self { - Self { - keys: RadrootsNostrKeys::generate(), - } - } - - fn into_keys(self) -> RadrootsNostrKeys { - self.keys - } -} - -impl fmt::Debug for RadrootsSdkNip46ClientKey { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_tuple("RadrootsSdkNip46ClientKey") - .field(&"[redacted]") - .finish() - } -} - -#[doc(hidden)] -pub type RadrootsSdkNip46TransportFuture<'a, T> = TransportFuture<'a, T>; - -/// Private relay-event transition surface retained through CLI migration Step 271; removed in Step 313. -#[doc(hidden)] -pub trait RadrootsSdkNip46Transport: Send + Sync { - fn publish_request_event<'a>( - &'a self, - event: RadrootsNostrEvent, - ) -> RadrootsSdkNip46TransportFuture<'a, ()>; - - fn next_response_event<'a>(&'a self) - -> RadrootsSdkNip46TransportFuture<'a, RadrootsNostrEvent>; -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum RadrootsSdkSignerMode { - #[cfg(feature = "local-signer")] - LocalKey, - MycNip46, -} - -impl RadrootsSdkSignerMode { - pub fn as_str(self) -> &'static str { - match self { - #[cfg(feature = "local-signer")] - Self::LocalKey => "local_key", - Self::MycNip46 => "myc_nip46", - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case")] -#[non_exhaustive] -pub enum RadrootsSdkSignerState { - Ready, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct RadrootsSdkSignerStatus { - pub mode: RadrootsSdkSignerMode, - pub state: RadrootsSdkSignerState, - pub signer_pubkey: String, - pub remote_signer_pubkey: Option<String>, - pub relay_count: usize, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct RadrootsSdkSignerCapability { - pub mode: RadrootsSdkSignerMode, - pub signer_pubkey: String, - pub remote_signer_pubkey: Option<String>, - pub relays: Vec<String>, - pub can_sign_events: bool, - pub nip46_permissions: Vec<String>, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -#[serde(rename_all = "snake_case", tag = "kind")] -pub enum RadrootsSdkSignerProgress { - RequestStarted { - mode: RadrootsSdkSignerMode, - }, - AuthChallenge { - mode: RadrootsSdkSignerMode, - url: String, - }, - RequestCompleted { - mode: RadrootsSdkSignerMode, - }, -} - -pub trait RadrootsSdkSignerProgressSink { - fn on_signer_progress( - &mut self, - progress: RadrootsSdkSignerProgress, - ) -> Result<(), RadrootsSdkError>; -} - -impl<F> RadrootsSdkSignerProgressSink for F -where - F: FnMut(RadrootsSdkSignerProgress) -> Result<(), RadrootsSdkError>, -{ - fn on_signer_progress( - &mut self, - progress: RadrootsSdkSignerProgress, - ) -> Result<(), RadrootsSdkError> { - self(progress) - } -} - -pub struct RadrootsSdkSignRequest<'a> { - pub operation_kind: &'a str, - 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 Actor, frozen_draft: &'a EventDraft) -> Self { - Self { - operation_kind, - actor, - frozen_draft, - progress_sink: None, - } - } - - pub fn with_progress_sink( - mut self, - progress_sink: &'a mut dyn RadrootsSdkSignerProgressSink, - ) -> Self { - self.progress_sink = Some(progress_sink); - self - } - - fn emit_progress( - &mut self, - progress: RadrootsSdkSignerProgress, - ) -> Result<(), RadrootsSdkError> { - match self.progress_sink.as_deref_mut() { - Some(progress_sink) => progress_sink.on_signer_progress(progress), - None => Ok(()), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] -pub struct RadrootsSdkSignReceipt { - pub operation_kind: String, - pub mode: RadrootsSdkSignerMode, - pub signer_pubkey: String, - pub remote_signer_pubkey: Option<String>, - pub signed_event_id: String, - pub signed_event: SignedEvent, -} - -#[derive(Clone)] -pub enum RadrootsSdkSignerProvider { - #[cfg(feature = "local-signer")] - LocalKey(RadrootsSdkLocalKeySigner), - MycNip46(Box<RadrootsSdkMycNip46Signer>), -} - -impl RadrootsSdkSignerProvider { - pub fn mode(&self) -> RadrootsSdkSignerMode { - match self { - #[cfg(feature = "local-signer")] - Self::LocalKey(_) => RadrootsSdkSignerMode::LocalKey, - Self::MycNip46(_) => RadrootsSdkSignerMode::MycNip46, - } - } - - pub fn status(&self) -> RadrootsSdkSignerStatus { - match self { - #[cfg(feature = "local-signer")] - Self::LocalKey(signer) => signer.status(), - Self::MycNip46(signer) => signer.status(), - } - } - - pub fn capability(&self) -> RadrootsSdkSignerCapability { - match self { - #[cfg(feature = "local-signer")] - Self::LocalKey(signer) => signer.capability(), - Self::MycNip46(signer) => signer.capability(), - } - } - - pub async fn sign( - &self, - request: RadrootsSdkSignRequest<'_>, - ) -> Result<RadrootsSdkSignReceipt, RadrootsSdkError> { - match self { - #[cfg(feature = "local-signer")] - Self::LocalKey(signer) => signer.sign(request).await, - Self::MycNip46(signer) => signer.sign(request).await, - } - } -} - -#[cfg(feature = "local-signer")] -#[derive(Clone)] -pub struct RadrootsSdkLocalKeySigner { - signer: Arc<RadrootsSdkLocalSignerCapability>, - signer_pubkey: String, -} - -#[cfg(feature = "local-signer")] -impl RadrootsSdkLocalKeySigner { - pub fn from_signer<S>( - signer: S, - signer_pubkey: impl AsRef<str>, - ) -> Result<Self, RadrootsSdkError> - where - S: Signer + 'static, - { - 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_signer( - signer: Arc<RadrootsSdkLocalSignerCapability>, - signer_pubkey: PublicKey, - ) -> Result<Self, RadrootsSdkError> { - Ok(Self { - signer, - signer_pubkey: signer_pubkey.to_hex(), - }) - } - - pub fn status(&self) -> RadrootsSdkSignerStatus { - RadrootsSdkSignerStatus { - mode: RadrootsSdkSignerMode::LocalKey, - state: RadrootsSdkSignerState::Ready, - signer_pubkey: self.signer_pubkey.clone(), - remote_signer_pubkey: None, - relay_count: 0, - } - } - - pub fn capability(&self) -> RadrootsSdkSignerCapability { - RadrootsSdkSignerCapability { - mode: RadrootsSdkSignerMode::LocalKey, - signer_pubkey: self.signer_pubkey.clone(), - remote_signer_pubkey: None, - relays: Vec::new(), - can_sign_events: true, - nip46_permissions: Vec::new(), - } - } - - pub async fn sign( - &self, - mut request: RadrootsSdkSignRequest<'_>, - ) -> Result<RadrootsSdkSignReceipt, RadrootsSdkError> { - request.emit_progress(RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::LocalKey, - })?; - 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(sdk_sign_receipt( - operation_kind.as_str(), - RadrootsSdkSignerMode::LocalKey, - self.signer_pubkey.clone(), - None, - receipt, - )) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct RadrootsSdkMycNip46RequestPolicy { - request_timeout: Duration, -} - -impl RadrootsSdkMycNip46RequestPolicy { - pub fn new(request_timeout: Duration) -> Result<Self, RadrootsSdkError> { - if request_timeout.is_zero() { - return Err(RadrootsSdkError::SignerUnavailable { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: "myc_nip46 request timeout must be greater than zero".to_owned(), - }); - } - Ok(Self { request_timeout }) - } - - pub fn request_timeout(self) -> Duration { - self.request_timeout - } -} - -impl Default for RadrootsSdkMycNip46RequestPolicy { - fn default() -> Self { - Self { - request_timeout: Duration::from_millis( - RADROOTS_SDK_MYC_NIP46_DEFAULT_REQUEST_TIMEOUT_MS, - ), - } - } -} - -#[derive(Clone)] -pub struct RadrootsSdkMycNip46Signer { - client: Arc<Client>, - user_pubkey: PublicKey, - transport: Arc<AsyncMutex<Box<dyn Transport>>>, - request_policy: RadrootsSdkMycNip46RequestPolicy, - request_id_generator: Arc<dyn RadrootsSdkMycNip46RequestIdGenerator>, -} - -impl RadrootsSdkMycNip46Signer { - #[doc(hidden)] - pub fn new( - client_key: RadrootsSdkNip46ClientKey, - target: Target, - user_pubkey: impl AsRef<str>, - transport: Arc<dyn RadrootsSdkNip46Transport>, - ) -> Result<Self, RadrootsSdkError> { - Self::new_with_request_policy( - client_key, - target, - user_pubkey, - transport, - RadrootsSdkMycNip46RequestPolicy::default(), - ) - } - - #[doc(hidden)] - pub fn new_with_request_policy( - client_key: RadrootsSdkNip46ClientKey, - target: Target, - user_pubkey: impl AsRef<str>, - transport: Arc<dyn RadrootsSdkNip46Transport>, - request_policy: RadrootsSdkMycNip46RequestPolicy, - ) -> Result<Self, RadrootsSdkError> { - let client_secret = client_key.into_keys().secret_key().to_secret_hex(); - let client = Client::from_secret(client_secret.as_str(), target) - .map_err(sdk_error_from_nip46_error)?; - Self::from_client_with_request_policy( - client, - user_pubkey, - RadrootsSdkNip46RelayEventTransport { transport }, - request_policy, - ) - } - - pub fn from_client<T>( - client: Client, - user_pubkey: impl AsRef<str>, - transport: T, - ) -> Result<Self, RadrootsSdkError> - where - T: Transport + 'static, - { - Self::from_client_with_request_policy( - client, - user_pubkey, - transport, - RadrootsSdkMycNip46RequestPolicy::default(), - ) - } - - pub fn from_client_with_request_policy<T>( - client: Client, - user_pubkey: impl AsRef<str>, - transport: T, - request_policy: RadrootsSdkMycNip46RequestPolicy, - ) -> Result<Self, RadrootsSdkError> - where - T: Transport + 'static, - { - Self::new_with_request_id_generator( - client, - user_pubkey, - transport, - request_policy, - Arc::new(RadrootsSdkUuidNip46RequestIdGenerator), - ) - } - - fn new_with_request_id_generator<T>( - client: Client, - user_pubkey: impl AsRef<str>, - transport: T, - request_policy: RadrootsSdkMycNip46RequestPolicy, - request_id_generator: Arc<dyn RadrootsSdkMycNip46RequestIdGenerator>, - ) -> Result<Self, RadrootsSdkError> - where - T: Transport + 'static, - { - RadrootsSdkMycNip46RequestPolicy::new(request_policy.request_timeout())?; - let user_pubkey = PublicKey::from_hex(user_pubkey.as_ref()).map_err(|error| { - RadrootsSdkError::InvalidRequest { - message: format!("myc_nip46 user pubkey is invalid: {error}"), - } - })?; - Ok(Self { - client: Arc::new(client), - user_pubkey, - transport: Arc::new(AsyncMutex::new(Box::new(transport))), - request_policy, - request_id_generator, - }) - } - - pub fn status(&self) -> RadrootsSdkSignerStatus { - RadrootsSdkSignerStatus { - mode: RadrootsSdkSignerMode::MycNip46, - state: RadrootsSdkSignerState::Ready, - signer_pubkey: self.user_pubkey.to_hex(), - remote_signer_pubkey: Some(self.client.target().remote_signer_public_key().to_hex()), - relay_count: self.client.target().relays().len(), - } - } - - pub fn capability(&self) -> RadrootsSdkSignerCapability { - RadrootsSdkSignerCapability { - mode: RadrootsSdkSignerMode::MycNip46, - signer_pubkey: self.user_pubkey.to_hex(), - remote_signer_pubkey: Some(self.client.target().remote_signer_public_key().to_hex()), - relays: self - .client - .target() - .relays() - .iter() - .map(ToString::to_string) - .collect(), - can_sign_events: true, - nip46_permissions: radroots_sdk_myc_nip46_product_permission_strings(), - } - } - - pub async fn sign( - &self, - mut request: RadrootsSdkSignRequest<'_>, - ) -> Result<RadrootsSdkSignReceipt, RadrootsSdkError> { - request.emit_progress(RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::MycNip46, - })?; - 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 = - RequestId::parse(self.next_request_id()).map_err(sdk_error_from_nip46_error)?; - let mut transport = self.transport.lock().await; - let mut adapter = RadrootsSdkNip46TimeoutTransport { - transport: transport.as_mut(), - request_timeout: self.request_policy.request_timeout(), - }; - let mut progress_error = None; - let cancellation = CancellationToken::new(); - let completion = self - .client - .execute( - request_id, - sign_event_request, - &mut adapter, - &cancellation, - |progress| { - let sdk_progress = match progress { - Progress::AuthChallenge { url } => { - RadrootsSdkSignerProgress::AuthChallenge { - mode: RadrootsSdkSignerMode::MycNip46, - url, - } - } - }; - if let Err(error) = request.emit_progress(sdk_progress) { - progress_error = Some(error); - return Err(NostrConnectError::Transport { - reason: "SDK signer progress sink failed".to_owned(), - }); - } - Ok(()) - }, - ) - .await; - if let Some(error) = progress_error { - return Err(error); - } - let response = match completion.map_err(sdk_error_from_nip46_error)? { - Completion::Response(response) => *response, - Completion::Cancelled(phase) => { - return Err(RadrootsSdkError::SignerTransport { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: format!("NIP-46 request cancelled {phase:?}"), - }); - } - }; - 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(sdk_sign_receipt( - operation_kind.as_str(), - RadrootsSdkSignerMode::MycNip46, - self.user_pubkey.to_hex(), - Some(self.client.target().remote_signer_public_key().to_hex()), - receipt, - )) - } - - fn next_request_id(&self) -> String { - self.request_id_generator.next_request_id() - } -} - -trait RadrootsSdkMycNip46RequestIdGenerator: Send + Sync { - fn next_request_id(&self) -> String; -} - -struct RadrootsSdkUuidNip46RequestIdGenerator; - -impl RadrootsSdkMycNip46RequestIdGenerator for RadrootsSdkUuidNip46RequestIdGenerator { - fn next_request_id(&self) -> String { - format!("radroots-sdk-myc-nip46-sign-{}", Uuid::new_v4()) - } -} - -pub fn radroots_sdk_myc_nip46_product_permissions() -> Permissions { - RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS - .iter() - .map(|kind| Permission::with_parameter(Method::SignEvent, kind.to_string())) - .collect::<Vec<_>>() - .into() -} - -pub fn radroots_sdk_myc_nip46_product_permission_strings() -> Vec<String> { - radroots_sdk_myc_nip46_product_permissions() - .as_slice() - .iter() - .map(ToString::to_string) - .collect() -} - -struct RadrootsSdkNip46TimeoutTransport<'a> { - transport: &'a mut dyn Transport, - request_timeout: Duration, -} - -struct RadrootsSdkNip46RelayEventTransport { - transport: Arc<dyn RadrootsSdkNip46Transport>, -} - -impl Transport for RadrootsSdkNip46RelayEventTransport { - fn publish<'a>(&'a mut self, event: ClientEvent) -> TransportFuture<'a, ()> { - let event = match RadrootsNostrEvent::from_json(event.as_json()) { - Ok(event) => event, - Err(error) => { - return Box::pin(async move { - Err(NostrConnectError::Transport { - reason: format!("invalid SDK NIP-46 publication: {error}"), - }) - }); - } - }; - self.transport.publish_request_event(event) - } - - fn receive<'a>( - &'a mut self, - cancellation: &'a CancellationToken, - ) -> TransportFuture<'a, Receive> { - if cancellation.is_cancelled() { - return Box::pin(async { Ok(Receive::Cancelled) }); - } - let next = self.transport.next_response_event(); - Box::pin(async move { - let event = next.await?; - if cancellation.is_cancelled() { - return Ok(Receive::Cancelled); - } - ClientEvent::from_json(event.as_json().as_str()).map(Receive::event) - }) - } -} - -impl Transport for RadrootsSdkNip46TimeoutTransport<'_> { - fn publish<'a>(&'a mut self, event: ClientEvent) -> TransportFuture<'a, ()> { - self.transport.publish(event) - } - - fn receive<'a>( - &'a mut self, - cancellation: &'a CancellationToken, - ) -> TransportFuture<'a, Receive> { - if cancellation.is_cancelled() { - return Box::pin(async { Ok(Receive::Cancelled) }); - } - let next = self.transport.receive(cancellation); - let request_timeout = self.request_timeout; - Box::pin(async move { - match timeout(request_timeout, next).await { - Ok(Ok(receive)) => { - if cancellation.is_cancelled() { - return Ok(Receive::Cancelled); - } - Ok(receive) - } - Ok(Err(error)) => Err(error), - Err(_) => Ok(Receive::TimedOut), - } - }) - } -} - -fn sign_event_request_from_frozen_draft(draft: &EventDraft) -> Result<Request, RadrootsSdkError> { - let public_key = nip46_unsigned_event_pubkey(draft)?; - let kind = nip46_unsigned_event_kind(draft)?; - let tags = nip46_unsigned_event_tags(draft)?; - let unsigned_event = UnsignedEvent { - id: None, - pubkey: public_key, - created_at: Timestamp::from_secs(draft.created_at_u64()), - kind, - tags: Tags::from_list(tags), - content: draft.content().to_owned(), - }; - let unsigned_event = ConnectUnsignedEvent::from_json(&unsigned_event.as_json()) - .map_err(|error| nip46_sign_event_protocol_error(error.to_string()))?; - Ok(Request::SignEvent(unsigned_event)) -} - -fn nip46_unsigned_event_pubkey(draft: &EventDraft) -> Result<NostrPublicKey, RadrootsSdkError> { - NostrPublicKey::from_slice(draft.expected_pubkey().as_bytes()).map_err(|error| { - nip46_sign_event_protocol_error(format!( - "failed to parse frozen draft pubkey for NIP-46 unsigned event: {error}" - )) - }) -} - -fn nip46_unsigned_event_kind(draft: &EventDraft) -> Result<Kind, RadrootsSdkError> { - let kind = u16::try_from(draft.kind_u32()).map_err(|error| { - nip46_sign_event_protocol_error(format!( - "failed to convert frozen draft kind to NIP-46 unsigned event: {error}" - )) - })?; - Ok(Kind::from_u16(kind)) -} - -fn nip46_unsigned_event_tags(draft: &EventDraft) -> Result<Vec<Tag>, RadrootsSdkError> { - let raw_tags = draft.tags_as_vec(); - let mut tags = Vec::with_capacity(raw_tags.len()); - for raw_tag in raw_tags { - let tag = Tag::parse(raw_tag).map_err(|error| { - nip46_sign_event_protocol_error(format!( - "failed to convert frozen draft tags to NIP-46 unsigned event: {error}" - )) - })?; - tags.push(tag); - } - Ok(tags) -} - -fn nip46_sign_event_protocol_error(reason: String) -> RadrootsSdkError { - RadrootsSdkError::SignerProtocol { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason, - } -} - -fn signed_event_from_nip46_response( - operation_kind: &str, - response: Response, -) -> Result<SignedEvent, RadrootsSdkError> { - match response { - Response::SignedEvent(event) => { - let raw_json = event.as_json(); - let wire = Nip01EventWire::parse_json(raw_json.as_str()).map_err(|error| { - RadrootsSdkError::SignerProtocol { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: format!("remote signed event wire is invalid: {error}"), - } - })?; - let signed_event = - SignedEvent::from_wire_verified_id(wire, raw_json).map_err(|error| { - RadrootsSdkError::SignerProtocol { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: format!("remote signed event is invalid: {error}"), - } - })?; - radroots_event_codec::verify::verify_nip01_event(signed_event.envelope().clone()) - .map(|_| signed_event) - .map_err(|error| RadrootsSdkError::SignerProtocol { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: format!("remote signed event signature is invalid: {error}"), - }) - } - Response::Error { error, .. } => Err(RadrootsSdkError::SignerRequestRejected { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: error, - }), - Response::PendingConnection => Err(RadrootsSdkError::SignerAuthChallengePending { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - auth_url: None, - }), - other => Err(RadrootsSdkError::SignerProtocol { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: format!("unexpected NIP-46 response for {operation_kind}: {other:?}"), - }), - } -} - -fn sdk_error_from_nip46_error(error: NostrConnectError) -> RadrootsSdkError { - match error { - NostrConnectError::RequestTimedOut => RadrootsSdkError::SignerRequestTimedOut { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - }, - NostrConnectError::Transport { reason } => RadrootsSdkError::SignerTransport { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason, - }, - NostrConnectError::Encrypt { reason } - | NostrConnectError::Decrypt { reason } - | NostrConnectError::Sign { reason } - | NostrConnectError::Json(reason) - | NostrConnectError::InvalidRequestPayload { reason, .. } - | NostrConnectError::InvalidResponsePayload { reason, .. } => { - RadrootsSdkError::SignerProtocol { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason, - } - } - error => RadrootsSdkError::SignerProtocol { - mode: RadrootsSdkSignerMode::MycNip46.as_str().to_owned(), - reason: error.to_string(), - }, - } -} - -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>, - receipt: SignReceipt, -) -> RadrootsSdkSignReceipt { - let signed_event = receipt.signed_event().clone(); - RadrootsSdkSignReceipt { - operation_kind: operation_kind.to_owned(), - mode, - signer_pubkey, - remote_signer_pubkey, - signed_event_id: signed_event.id_str().to_owned(), - signed_event, - } -} - -#[cfg(test)] -#[path = "../tests/unit/signer_provider_tests.rs"] -mod tests; diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs @@ -1 +1,254 @@ -//! Signing capability composition. +//! Generic signer composition without protocol or relay ownership. + +use std::sync::Arc; + +use radroots_signing::{SignReceipt, SignRequest, Signer, SignerStatus}; + +/// Host-visible signer composition mode. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum Mode { + /// A concrete local Nostr adapter owns opaque key material. + Local, + /// A host-provided signer drives NIP-46 protocol and relay execution. + Nip46, + /// Another host-provided implementation of the generic signer SPI. + Host, +} + +/// Cloneable SDK composition wrapper around the canonical signer SPI. +#[derive(Clone)] +pub struct Provider { + mode: Mode, + signer: Arc<dyn Signer>, +} + +impl Provider { + /// Wraps any host-provided canonical signer implementation. + #[must_use] + pub fn host(signer: Arc<dyn Signer>) -> Self { + Self { + mode: Mode::Host, + signer, + } + } + + /// Wraps the concrete local Nostr adapter without exposing its key. + #[cfg(feature = "local-signing")] + #[must_use] + pub fn local(signer: radroots_nostr::signing::LocalSigner) -> Self { + Self { + mode: Mode::Local, + signer: Arc::new(signer), + } + } + + /// Marks a host-provided canonical signer as a NIP-46 composition. + /// + /// `radroots_nostr_connect` owns protocol state and its transport SPI. The + /// injected implementation owns that client plus explicit relay execution; + /// this wrapper does not contact a relay, persist a session, or start work. + #[cfg(feature = "nip46")] + #[must_use] + pub fn nip46(signer: Arc<dyn Signer>) -> Self { + Self { + mode: Mode::Nip46, + signer, + } + } + + /// Returns the explicit composition mode. + #[must_use] + pub const fn mode(&self) -> Mode { + self.mode + } + + /// Borrows the canonical signer SPI. + #[must_use] + pub fn as_signer(&self) -> &dyn Signer { + self.signer.as_ref() + } + + /// Reports canonical status without initiating a signing request. + pub async fn status(&self) -> Result<SignerStatus, radroots_signing::Error> { + self.signer.status().await + } + + /// Delegates one already-authorized request to the canonical SPI. + pub async fn sign(&self, request: SignRequest) -> Result<SignReceipt, radroots_signing::Error> { + self.signer.sign(request).await + } + + pub(crate) fn into_signer(self) -> Arc<dyn Signer> { + self.signer + } +} + +impl std::fmt::Debug for Provider { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("Provider") + .field("mode", &self.mode) + .field("signer", &"<opaque>") + .finish() + } +} + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }; + + use radroots_event::{EventDraft, contract::AuthorRole}; + use radroots_identity::PublicKey; + use radroots_protocol::runtime::v1::OperationId; + use radroots_signing::{ + Actor, Error, SignReceipt, SignRequest, SignerStatus, + actor::ActorSource, + error::Kind, + request::{CancellationPolicy, SignPolicy}, + signer::BoxFuture, + }; + #[cfg(any(feature = "local-signing", feature = "nip46"))] + use radroots_signing::{capability::SignerKind, status::SignerAvailability}; + #[cfg(feature = "nip46")] + use radroots_signing::{ + capability::{CancellationSupport, SignerCapability}, + status::{AuthChallenge, SignProgress}, + }; + + use super::*; + + const PUBLIC_KEY: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + struct ScriptedSigner { + status: SignerStatus, + result: Kind, + polls: Arc<AtomicUsize>, + } + + impl Signer for ScriptedSigner { + fn status(&self) -> BoxFuture<'_, Result<SignerStatus, Error>> { + let status = self.status.clone(); + Box::pin(async move { Ok(status) }) + } + + fn sign(&self, _request: SignRequest) -> BoxFuture<'_, Result<SignReceipt, Error>> { + let result = self.result; + let polls = Arc::clone(&self.polls); + Box::pin(async move { + polls.fetch_add(1, Ordering::Relaxed); + Err(Error::new(result)) + }) + } + } + + fn request() -> SignRequest { + let actor = Actor::new( + PublicKey::from_hex(PUBLIC_KEY).expect("public key"), + ActorSource::ExplicitPublicKey, + [AuthorRole::Any], + ) + .expect("actor"); + let draft = EventDraft::new( + "radroots.social.geochat.v1", + 20_000, + 1_700_000_000, + Vec::new(), + "frozen-content", + PUBLIC_KEY, + ) + .expect("draft"); + SignRequest::new( + OperationId::SyncPush, + actor, + draft, + SignPolicy::new(1_700_000_100, CancellationPolicy::PreservePublishedRequest) + .expect("policy"), + ) + .expect("request") + } + + #[cfg(feature = "nip46")] + fn remote_status(progress: Option<SignProgress>) -> SignerStatus { + SignerStatus::new( + SignerAvailability::AwaitingAuthentication, + vec![SignerCapability::new( + SignerKind::Remote, + CancellationSupport::BeforeAndAfterPublication, + true, + true, + )], + progress, + ) + } + + #[cfg(feature = "local-signing")] + #[tokio::test] + async fn local_provider_uses_the_concrete_lower_adapter() { + let local = radroots_nostr::signing::LocalSigner::generate().expect("local signer"); + let provider = Provider::local(local); + let status = provider.status().await.expect("status"); + assert_eq!(provider.mode(), Mode::Local); + assert_eq!(status.availability(), SignerAvailability::Ready); + assert_eq!(status.capabilities()[0].kind(), SignerKind::Local); + } + + #[cfg(feature = "nip46")] + #[tokio::test] + async fn nip46_provider_preserves_auth_challenge_and_capabilities() { + let challenge = AuthChallenge::new( + "https://signer.example/approve", + 1_700_000_000, + Some(1_700_000_100), + ) + .expect("challenge"); + let signer = ScriptedSigner { + status: remote_status(Some(SignProgress::authentication(challenge))), + result: Kind::SignerRejected, + polls: Arc::new(AtomicUsize::new(0)), + }; + let provider = Provider::nip46(Arc::new(signer)); + let status = provider.status().await.expect("status"); + assert_eq!(provider.mode(), Mode::Nip46); + assert_eq!( + status.availability(), + SignerAvailability::AwaitingAuthentication + ); + assert!(status.progress().expect("progress").challenge().is_some()); + assert!(status.capabilities()[0].may_require_authentication()); + } + + #[tokio::test] + async fn canonical_errors_preserve_timeout_drift_and_cancellation() { + for expected in [ + Kind::SignerTimeout, + Kind::SignerOutputInvalid, + Kind::SignerCancelled, + ] { + let provider = Provider::host(Arc::new(ScriptedSigner { + status: SignerStatus::unavailable(), + result: expected, + polls: Arc::new(AtomicUsize::new(0)), + })); + assert_eq!( + provider.sign(request()).await.expect_err("failure").kind(), + expected + ); + } + } + + #[test] + fn dropping_unpolled_signing_future_has_no_effect() { + let polls = Arc::new(AtomicUsize::new(0)); + let provider = Provider::host(Arc::new(ScriptedSigner { + status: SignerStatus::unavailable(), + result: Kind::SignerCancelled, + polls: Arc::clone(&polls), + })); + drop(provider.sign(request())); + assert_eq!(polls.load(Ordering::Relaxed), 0); + } +} diff --git a/crates/sdk/tests/unit/signer_provider_tests.rs b/crates/sdk/tests/unit/signer_provider_tests.rs @@ -1,1129 +0,0 @@ -use super::*; -use nostr::Keys as RadrootsNostrKeys; -use nostr::nips::nip44::{self, Version}; -use nostr::{EventBuilder, JsonUtil, Kind, Tag}; -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::signing::sign_frozen_draft; -use radroots_nostr::{event::Event as RadrootsNostrEvent, signing::LocalSigner}; -use radroots_nostr_connect::{ - Error as NostrConnectError, Request, Response, - client::Target, - message::{RPC_KIND, RequestMessage, SignedEvent as ConnectSignedEvent}, - uri::RelayUrl as ConnectRelayUrl, -}; -use radroots_signing::actor::ActorSource; -use std::collections::VecDeque; -use std::future; -use std::sync::{Arc, LazyLock, Mutex}; -use std::time::Duration; -use uuid::Uuid; - -static USER_KEYS: LazyLock<RadrootsNostrKeys> = LazyLock::new(RadrootsNostrKeys::generate); -static USER_PUBLIC_KEY: LazyLock<String> = LazyLock::new(|| USER_KEYS.public_key().to_hex()); -static LOCAL_SIGNER: LazyLock<Arc<LocalSigner>> = - LazyLock::new(|| Arc::new(LocalSigner::generate().expect("generated local signer"))); -static LOCAL_SIGNER_PUBLIC_KEY: LazyLock<String> = - LazyLock::new(|| LOCAL_SIGNER.public_key().to_hex()); -static REMOTE_KEYS: LazyLock<RadrootsNostrKeys> = LazyLock::new(RadrootsNostrKeys::generate); - -fn user_keys() -> RadrootsNostrKeys { - USER_KEYS.clone() -} - -fn connect_signed_event(event: RadrootsNostrEvent) -> ConnectSignedEvent { - ConnectSignedEvent::from_json(&event.as_json()).expect("connect signed event") -} - -fn user_pubkey() -> &'static str { - USER_PUBLIC_KEY.as_str() -} - -fn local_signer_pubkey() -> &'static str { - LOCAL_SIGNER_PUBLIC_KEY.as_str() -} - -fn local_sdk_signer() -> RadrootsSdkLocalKeySigner { - let signer: Arc<RadrootsSdkLocalSignerCapability> = LOCAL_SIGNER.clone(); - RadrootsSdkLocalKeySigner::from_shared_signer(signer, LOCAL_SIGNER.public_key()) - .expect("sdk local signer") -} - -fn remote_keys() -> RadrootsNostrKeys { - REMOTE_KEYS.clone() -} - -fn nip46_target(remote_public_key: nostr::PublicKey, relays: Vec<nostr::RelayUrl>) -> Target { - let remote_public_key = - radroots_nostr::key::public_key_from_nostr(remote_public_key).expect("identity public key"); - let relays = relays - .into_iter() - .map(|relay| ConnectRelayUrl::parse(relay.to_string().as_str()).expect("connect relay")) - .collect(); - Target::try_new(remote_public_key, relays).expect("NIP-46 target") -} - -fn nip46_client(target: Target) -> Client { - Client::generate(target).expect("NIP-46 client") -} - -fn actor() -> Actor { - Actor::from_public_key_hex( - user_pubkey(), - ActorSource::ExplicitPublicKey, - [AuthorRole::Farmer], - ) - .expect("actor") -} - -fn local_actor() -> Actor { - Actor::from_public_key_hex( - local_signer_pubkey(), - ActorSource::ExplicitPublicKey, - [AuthorRole::Farmer], - ) - .expect("local signer actor") -} - -fn frozen_draft() -> EventDraft { - frozen_draft_with( - "radroots.farm.profile.v1", - user_pubkey(), - KIND_FARM, - 1_700_000_000, - vec![vec!["d".to_owned(), "sdk-signer".to_owned()]], - "{}", - ) -} - -fn local_frozen_draft() -> EventDraft { - frozen_draft_with( - "radroots.farm.profile.v1", - local_signer_pubkey(), - KIND_FARM, - 1_700_000_000, - vec![vec!["d".to_owned(), "sdk-local-signer".to_owned()]], - "{}", - ) -} - -fn frozen_draft_with( - contract_id: &str, - pubkey: &str, - kind: u32, - created_at: u32, - tags: Vec<Vec<String>>, - content: &str, -) -> EventDraft { - EventDraft::new( - contract_id, - kind, - u64::from(created_at), - tags, - content, - pubkey, - ) - .expect("frozen draft") -} - -fn sign_event(keys: &RadrootsNostrKeys, draft: &EventDraft) -> RadrootsNostrEvent { - let signed = sign_frozen_draft(keys, draft).expect("signed"); - RadrootsNostrEvent::from_json(signed.raw_json()).expect("event") -} - -fn response_event( - remote_keys: &RadrootsNostrKeys, - client_public_key: nostr::PublicKey, - request_id: &str, - response: Response, -) -> RadrootsNostrEvent { - let envelope = response - .into_envelope(request_id) - .expect("response envelope"); - let payload = serde_json::to_string(&envelope).expect("payload"); - let ciphertext = nip44::encrypt( - remote_keys.secret_key(), - &client_public_key, - payload, - Version::V2, - ) - .expect("ciphertext"); - EventBuilder::new(Kind::Custom(RPC_KIND), ciphertext) - .tag(Tag::public_key(client_public_key)) - .sign_with_keys(remote_keys) - .expect("response event") -} - -fn myc_signer_with_responses( - responses: Vec<MockNip46Response>, -) -> (RadrootsSdkMycNip46Signer, Arc<MockNip46Transport>) { - let remote_keys = remote_keys(); - let transport = Arc::new(MockNip46Transport::new(remote_keys.clone(), responses)); - let target = nip46_target( - remote_keys.public_key(), - vec![nostr::RelayUrl::parse("wss://relay.example.com").expect("relay")], - ); - let signer = RadrootsSdkMycNip46Signer::from_client( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - ) - .expect("signer"); - (signer, transport) -} - -#[derive(Clone)] -struct MockNip46Transport { - remote_keys: RadrootsNostrKeys, - responses: Arc<Mutex<VecDeque<MockNip46Response>>>, - published: Arc<Mutex<Vec<RadrootsNostrEvent>>>, - inbound: Arc<Mutex<VecDeque<RadrootsNostrEvent>>>, -} - -enum MockNip46Response { - Respond(Response), -} - -impl MockNip46Transport { - fn new(remote_keys: RadrootsNostrKeys, responses: Vec<MockNip46Response>) -> Self { - Self { - remote_keys, - responses: Arc::new(Mutex::new(responses.into())), - published: Arc::new(Mutex::new(Vec::new())), - inbound: Arc::new(Mutex::new(VecDeque::new())), - } - } - - fn published(&self) -> Vec<RadrootsNostrEvent> { - self.published.lock().expect("published lock").clone() - } - - fn published_request_messages(&self) -> Vec<RequestMessage> { - self.published() - .iter() - .map(|event| request_message_from_event(&self.remote_keys, event)) - .collect() - } -} - -fn request_message_from_event( - remote_keys: &RadrootsNostrKeys, - event: &RadrootsNostrEvent, -) -> RequestMessage { - let payload = nip44::decrypt(remote_keys.secret_key(), &event.pubkey, &event.content) - .expect("request payload"); - serde_json::from_str(payload.as_str()).expect("request message") -} - -impl Transport for MockNip46Transport { - fn publish<'a>(&'a mut self, event: ClientEvent) -> TransportFuture<'a, ()> { - let event = match RadrootsNostrEvent::from_json(event.as_json()) { - Ok(event) => event, - Err(error) => { - return Box::pin(async move { - Err(NostrConnectError::Transport { - reason: format!("invalid test NIP-46 publication: {error}"), - }) - }); - } - }; - self.published.lock().expect("published lock").push(event); - let response = self.responses.lock().expect("responses lock").pop_front(); - if let Some(MockNip46Response::Respond(response)) = response { - let event = self - .published - .lock() - .expect("published lock") - .last() - .cloned(); - let event = event.expect("published request event"); - let request = request_message_from_event(&self.remote_keys, &event); - let response = response_event(&self.remote_keys, event.pubkey, &request.id, response); - self.inbound - .lock() - .expect("inbound lock") - .push_back(response); - } - Box::pin(async { Ok(()) }) - } - - fn receive<'a>( - &'a mut self, - cancellation: &'a CancellationToken, - ) -> TransportFuture<'a, Receive> { - if cancellation.is_cancelled() { - return Box::pin(async { Ok(Receive::Cancelled) }); - } - let next = self.inbound.lock().expect("inbound lock").pop_front(); - Box::pin(async move { - match next { - Some(event) => ClientEvent::from_json(event.as_json().as_str()).map(Receive::event), - None => Err(NostrConnectError::RequestTimedOut), - } - }) - } -} - -#[derive(Clone)] -struct HangingNip46Transport { - published: Arc<Mutex<Vec<RadrootsNostrEvent>>>, -} - -impl HangingNip46Transport { - fn new() -> Self { - Self { - published: Arc::new(Mutex::new(Vec::new())), - } - } -} - -impl Transport for HangingNip46Transport { - fn publish<'a>(&'a mut self, event: ClientEvent) -> TransportFuture<'a, ()> { - let event = RadrootsNostrEvent::from_json(event.as_json()).expect("valid client event"); - self.published.lock().expect("published lock").push(event); - Box::pin(async { Ok(()) }) - } - - fn receive<'a>( - &'a mut self, - _cancellation: &'a CancellationToken, - ) -> TransportFuture<'a, Receive> { - Box::pin(future::pending::<Result<Receive, NostrConnectError>>()) - } -} - -#[tokio::test] -async fn local_key_provider_signs_authorized_frozen_draft() { - let signer = local_sdk_signer(); - let provider = RadrootsSdkSignerProvider::LocalKey(signer.clone()); - let draft = local_frozen_draft(); - let actor = local_actor(); - let mut progress = Vec::new(); - - let receipt = provider - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - progress.push(event); - Ok(()) - }, - ), - ) - .await - .expect("receipt"); - - assert_eq!(provider.mode(), RadrootsSdkSignerMode::LocalKey); - assert_eq!(provider.status(), signer.status()); - assert!(provider.capability().nip46_permissions.is_empty()); - assert_eq!(receipt.mode, RadrootsSdkSignerMode::LocalKey); - assert_eq!(receipt.signer_pubkey, local_signer_pubkey()); - assert_eq!(receipt.signed_event_id, draft.expected_event_id_hex()); - assert_eq!( - progress, - vec![ - RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::LocalKey - }, - RadrootsSdkSignerProgress::RequestCompleted { - mode: RadrootsSdkSignerMode::LocalKey - } - ] - ); -} - -#[tokio::test] -async fn local_key_provider_returns_progress_sink_errors_without_transport_state() { - let signer = local_sdk_signer(); - let draft = local_frozen_draft(); - let actor = local_actor(); - let wrong_actor = Actor::from_public_key_hex( - &"a".repeat(64), - ActorSource::ExplicitPublicKey, - [AuthorRole::Farmer], - ) - .expect("wrong actor"); - - assert!(matches!( - signer - .sign(RadrootsSdkSignRequest::new( - "farm.publish", - &wrong_actor, - &draft, - )) - .await, - Err(RadrootsSdkError::UnauthorizedActor { .. }) - )); - - let started_error = signer - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - assert!(matches!( - event, - RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::LocalKey - } - )); - Err(RadrootsSdkError::InvalidRequest { - message: "local progress start refused".to_owned(), - }) - }, - ), - ) - .await - .expect_err("progress start error"); - assert!(matches!( - started_error, - RadrootsSdkError::InvalidRequest { ref message } - if message == "local progress start refused" - )); - - let mut observed = Vec::new(); - let completed_error = signer - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - observed.push(event.clone()); - if matches!( - event, - RadrootsSdkSignerProgress::RequestCompleted { - mode: RadrootsSdkSignerMode::LocalKey - } - ) { - return Err(RadrootsSdkError::InvalidRequest { - message: "local progress completion refused".to_owned(), - }); - } - Ok(()) - }, - ), - ) - .await - .expect_err("progress completion error"); - assert!(matches!( - completed_error, - RadrootsSdkError::InvalidRequest { ref message } - if message == "local progress completion refused" - )); - assert_eq!( - observed, - vec![ - RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::LocalKey - }, - RadrootsSdkSignerProgress::RequestCompleted { - mode: RadrootsSdkSignerMode::LocalKey - } - ] - ); -} - -#[test] -fn signer_provider_reports_myc_status_capability_and_constructor_errors() { - assert_eq!(RadrootsSdkSignerMode::LocalKey.as_str(), "local_key"); - assert_eq!(RadrootsSdkSignerMode::MycNip46.as_str(), "myc_nip46"); - - let remote_keys = remote_keys(); - let relays = vec![ - nostr::RelayUrl::parse("wss://relay-a.example.com").expect("relay a"), - nostr::RelayUrl::parse("wss://relay-b.example.com").expect("relay b"), - ]; - let target = nip46_target(remote_keys.public_key(), relays); - let transport = Arc::new(MockNip46Transport::new(remote_keys.clone(), Vec::new())); - let signer = RadrootsSdkMycNip46Signer::from_client( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - ) - .expect("signer"); - let provider = RadrootsSdkSignerProvider::MycNip46(Box::new(signer)); - - assert_eq!(provider.mode(), RadrootsSdkSignerMode::MycNip46); - assert_eq!( - provider.status(), - RadrootsSdkSignerStatus { - mode: RadrootsSdkSignerMode::MycNip46, - state: RadrootsSdkSignerState::Ready, - signer_pubkey: user_pubkey().to_owned(), - remote_signer_pubkey: Some(remote_keys.public_key().to_hex()), - relay_count: 2, - } - ); - assert_eq!( - provider.capability(), - RadrootsSdkSignerCapability { - mode: RadrootsSdkSignerMode::MycNip46, - signer_pubkey: user_pubkey().to_owned(), - remote_signer_pubkey: Some(remote_keys.public_key().to_hex()), - relays: vec![ - "wss://relay-a.example.com".to_owned(), - "wss://relay-b.example.com".to_owned(), - ], - can_sign_events: true, - nip46_permissions: radroots_sdk_myc_nip46_product_permission_strings(), - } - ); - - let target = nip46_target(remote_keys.public_key(), Vec::new()); - let error = match RadrootsSdkMycNip46Signer::from_client( - nip46_client(target), - "not-a-pubkey", - transport.as_ref().clone(), - ) { - Ok(_) => panic!("expected invalid pubkey"), - Err(error) => error, - }; - assert!(matches!( - error, - RadrootsSdkError::InvalidRequest { ref message } - if message.contains("myc_nip46 user pubkey is invalid") - )); -} - -#[test] -fn nip46_private_helpers_map_identity_adapter_and_response_edges() { - assert!(matches!( - signed_event_from_nip46_response( - "farm.publish", - Response::Error { - result: None, - error: "operator rejected".to_owned(), - }, - ), - Err(RadrootsSdkError::SignerRequestRejected { ref mode, ref reason }) - if mode == "myc_nip46" && reason == "operator rejected" - )); - assert!(matches!( - signed_event_from_nip46_response("farm.publish", Response::PendingConnection), - Err(RadrootsSdkError::SignerAuthChallengePending { ref mode, auth_url: None }) - if mode == "myc_nip46" - )); - assert!(matches!( - signed_event_from_nip46_response("farm.publish", Response::Pong), - Err(RadrootsSdkError::SignerProtocol { ref mode, ref reason }) - if mode == "myc_nip46" && reason.contains("farm.publish") - )); - assert!(matches!( - sdk_error_from_nip46_error(NostrConnectError::Transport { - reason: "relay offline".to_owned(), - }), - RadrootsSdkError::SignerTransport { ref mode, ref reason } - if mode == "myc_nip46" && reason == "relay offline" - )); - assert!(matches!( - sdk_error_from_nip46_error(NostrConnectError::Json("bad json".to_owned())), - RadrootsSdkError::SignerProtocol { ref mode, ref reason } - if mode == "myc_nip46" && reason == "bad json" - )); - for error in [ - NostrConnectError::Encrypt { - reason: "encrypt failed".to_owned(), - }, - NostrConnectError::Decrypt { - reason: "decrypt failed".to_owned(), - }, - NostrConnectError::Sign { - reason: "sign failed".to_owned(), - }, - NostrConnectError::InvalidRequestPayload { - method: "sign_event".to_owned(), - reason: "request payload failed".to_owned(), - }, - NostrConnectError::InvalidResponsePayload { - method: "sign_event".to_owned(), - reason: "response payload failed".to_owned(), - }, - ] { - assert!(matches!( - sdk_error_from_nip46_error(error), - RadrootsSdkError::SignerProtocol { ref mode, .. } if mode == "myc_nip46" - )); - } - assert!(matches!( - sdk_error_from_nip46_error(NostrConnectError::InvalidMethod("ping".to_owned())), - RadrootsSdkError::SignerProtocol { ref mode, ref reason } - if mode == "myc_nip46" && reason.contains("invalid NIP-46 method") - )); -} - -#[tokio::test] -async fn nip46_transport_adapter_delegates_publish_and_response_poll() { - let transport = Arc::new(MockNip46Transport::new(remote_keys(), Vec::new())); - let event = sign_event(&user_keys(), &frozen_draft()); - let mut canonical_transport = transport.as_ref().clone(); - let mut adapter = RadrootsSdkNip46TimeoutTransport { - transport: &mut canonical_transport, - request_timeout: Duration::from_millis(10), - }; - - adapter - .publish(ClientEvent::from_json(event.as_json().as_str()).expect("client event")) - .await - .expect("publish request"); - - assert_eq!(transport.published().len(), 1); - assert!(matches!( - adapter.receive(&CancellationToken::new()).await, - Err(NostrConnectError::RequestTimedOut) - )); -} - -#[test] -fn myc_nip46_product_permissions_cover_sdk_write_event_kinds() { - let permissions = radroots_sdk_myc_nip46_product_permissions(); - let rendered = radroots_sdk_myc_nip46_product_permission_strings(); - let mut expected_kinds = vec![KIND_FARM, KIND_CLASSIFIED_LISTING]; - expected_kinds.extend_from_slice(&TRADE_MUTATION_EVENT_KINDS); - - assert_eq!(permissions.as_slice().len(), expected_kinds.len()); - assert_eq!( - RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS.as_slice(), - expected_kinds.as_slice() - ); - assert_eq!(rendered.len(), permissions.as_slice().len()); - for kind in RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS { - assert!(rendered.contains(&format!("sign_event:{kind}"))); - } - for old_trade_write_kind in [3422, 3423, 3432] { - assert!(!rendered.contains(&format!("sign_event:{old_trade_write_kind}"))); - } - assert!(!rendered.contains(&"sign_event:1".to_owned())); -} - -#[tokio::test] -async fn myc_nip46_provider_signs_and_validates_remote_event() { - let remote_keys = remote_keys(); - let user_keys = user_keys(); - let draft = frozen_draft(); - let signed = sign_frozen_draft(&user_keys, &draft).expect("signed"); - let signed_event = RadrootsNostrEvent::from_json(signed.raw_json()).expect("event"); - let transport = Arc::new(MockNip46Transport::new( - remote_keys.clone(), - vec![MockNip46Response::Respond(Response::SignedEvent( - connect_signed_event(signed_event), - ))], - )); - let target = nip46_target( - remote_keys.public_key(), - vec![nostr::RelayUrl::parse("wss://relay.example.com").expect("relay")], - ); - let signer = RadrootsSdkMycNip46Signer::from_client( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - ) - .expect("signer"); - let provider = RadrootsSdkSignerProvider::MycNip46(Box::new(signer)); - assert_eq!( - provider.capability().nip46_permissions, - radroots_sdk_myc_nip46_product_permission_strings() - ); - let actor = actor(); - let mut progress = Vec::new(); - - let receipt = provider - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - progress.push(event); - Ok(()) - }, - ), - ) - .await - .expect("receipt"); - - assert_eq!(receipt.mode, RadrootsSdkSignerMode::MycNip46); - assert_eq!(receipt.signer_pubkey, user_pubkey()); - assert_eq!( - receipt.remote_signer_pubkey, - Some(remote_keys.public_key().to_hex()) - ); - assert_eq!(receipt.signed_event, signed); - assert_eq!(transport.published().len(), 1); - let request_messages = transport.published_request_messages(); - let sign_event_request = match &request_messages[0].request { - Request::SignEvent(unsigned_event) => unsigned_event, - other => panic!("unexpected NIP-46 request: {other:?}"), - }; - let sign_event_request: nostr::UnsignedEvent = - serde_json::from_str(&sign_event_request.as_json()).expect("unsigned event payload"); - let request_tags = sign_event_request - .tags - .clone() - .to_vec() - .into_iter() - .map(Tag::to_vec) - .collect::<Vec<_>>(); - assert_eq!( - sign_event_request.pubkey.to_hex(), - draft.expected_pubkey().to_hex() - ); - assert_eq!( - sign_event_request.created_at.as_secs(), - draft.created_at_u64() - ); - assert_eq!(sign_event_request.kind.as_u16(), draft.kind_u32() as u16); - assert_eq!(request_tags, draft.tags_as_vec()); - assert_eq!(sign_event_request.content, draft.content()); - let request_id = request_messages[0] - .id - .strip_prefix("radroots-sdk-myc-nip46-sign-") - .expect("request id prefix"); - Uuid::parse_str(request_id).expect("uuid request id"); - assert_eq!( - progress, - vec![ - RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::MycNip46 - }, - RadrootsSdkSignerProgress::RequestCompleted { - mode: RadrootsSdkSignerMode::MycNip46 - } - ] - ); -} - -#[tokio::test] -async fn myc_nip46_provider_reports_preflight_and_progress_sink_edges() { - let draft = frozen_draft(); - let actor = actor(); - let (signer, transport) = myc_signer_with_responses(Vec::new()); - - let started_error = signer - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - assert!(matches!( - event, - RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::MycNip46 - } - )); - Err(RadrootsSdkError::InvalidRequest { - message: "myc progress start refused".to_owned(), - }) - }, - ), - ) - .await - .expect_err("progress start error"); - assert!(matches!( - started_error, - RadrootsSdkError::InvalidRequest { ref message } - if message == "myc progress start refused" - )); - assert!(transport.published().is_empty()); - - 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", - &wrong_actor, - &draft, - )) - .await - .expect_err("actor mismatch"); - assert!(matches!( - actor_error, - RadrootsSdkError::UnauthorizedActor { .. } - )); - assert!(transport.published().is_empty()); - - let remote_keys = remote_keys(); - let mismatch_transport = Arc::new(MockNip46Transport::new(remote_keys.clone(), Vec::new())); - let mismatch_target = nip46_target(remote_keys.public_key(), Vec::new()); - let mismatch_signer = RadrootsSdkMycNip46Signer::from_client( - nip46_client(mismatch_target), - remote_keys.public_key().to_hex(), - mismatch_transport.as_ref().clone(), - ) - .expect("mismatch signer"); - let signer_error = mismatch_signer - .sign(RadrootsSdkSignRequest::new("farm.publish", &actor, &draft)) - .await - .expect_err("signer mismatch"); - assert!(matches!( - signer_error, - RadrootsSdkError::UnauthorizedActor { .. } - )); - assert!(mismatch_transport.published().is_empty()); -} - -#[tokio::test] -async fn myc_nip46_provider_returns_completion_progress_errors_after_remote_sign() { - let user_keys = user_keys(); - let draft = frozen_draft(); - let signed = sign_frozen_draft(&user_keys, &draft).expect("signed"); - let signed_event = RadrootsNostrEvent::from_json(signed.raw_json()).expect("event"); - let (signer, transport) = myc_signer_with_responses(vec![MockNip46Response::Respond( - Response::SignedEvent(connect_signed_event(signed_event)), - )]); - let actor = actor(); - let mut observed = Vec::new(); - - let error = signer - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - observed.push(event.clone()); - if matches!( - event, - RadrootsSdkSignerProgress::RequestCompleted { - mode: RadrootsSdkSignerMode::MycNip46 - } - ) { - return Err(RadrootsSdkError::InvalidRequest { - message: "myc progress completion refused".to_owned(), - }); - } - Ok(()) - }, - ), - ) - .await - .expect_err("completion progress error"); - - assert!(matches!( - error, - RadrootsSdkError::InvalidRequest { ref message } - if message == "myc progress completion refused" - )); - assert_eq!(transport.published().len(), 1); - assert_eq!( - observed, - vec![ - RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::MycNip46 - }, - RadrootsSdkSignerProgress::RequestCompleted { - mode: RadrootsSdkSignerMode::MycNip46 - } - ] - ); -} - -#[tokio::test] -async fn myc_nip46_provider_reports_auth_challenge_progress_and_timeout() { - let remote_keys = remote_keys(); - let transport = Arc::new(MockNip46Transport::new( - remote_keys.clone(), - vec![MockNip46Response::Respond(Response::AuthUrl( - "https://auth.example.com/challenge".to_owned(), - ))], - )); - let target = nip46_target(remote_keys.public_key(), Vec::new()); - let signer = RadrootsSdkMycNip46Signer::from_client( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - ) - .expect("signer"); - let mut progress = Vec::new(); - let draft = frozen_draft(); - let actor = actor(); - - let error = signer - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - progress.push(event); - Ok(()) - }, - ), - ) - .await - .expect_err("timeout"); - - assert!(matches!( - error, - RadrootsSdkError::SignerRequestTimedOut { ref mode } if mode == "myc_nip46" - )); - assert_eq!( - progress, - vec![ - RadrootsSdkSignerProgress::RequestStarted { - mode: RadrootsSdkSignerMode::MycNip46 - }, - RadrootsSdkSignerProgress::AuthChallenge { - mode: RadrootsSdkSignerMode::MycNip46, - url: "https://auth.example.com/challenge".to_owned() - } - ] - ); -} - -#[tokio::test] -async fn myc_nip46_provider_returns_progress_sink_errors_from_auth_challenge() { - let remote_keys = remote_keys(); - let transport = Arc::new(MockNip46Transport::new( - remote_keys.clone(), - vec![MockNip46Response::Respond(Response::AuthUrl( - "https://auth.example.com/challenge".to_owned(), - ))], - )); - let target = nip46_target(remote_keys.public_key(), Vec::new()); - let signer = RadrootsSdkMycNip46Signer::from_client( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - ) - .expect("signer"); - let draft = frozen_draft(); - let actor = actor(); - let mut observed = Vec::new(); - - let error = signer - .sign( - RadrootsSdkSignRequest::new("farm.publish", &actor, &draft).with_progress_sink( - &mut |event: RadrootsSdkSignerProgress| { - observed.push(event.clone()); - if matches!( - event, - RadrootsSdkSignerProgress::AuthChallenge { - mode: RadrootsSdkSignerMode::MycNip46, - .. - } - ) { - return Err(RadrootsSdkError::InvalidRequest { - message: "progress sink refused auth challenge".to_owned(), - }); - } - Ok(()) - }, - ), - ) - .await - .expect_err("progress sink error"); - - assert!(matches!( - error, - RadrootsSdkError::InvalidRequest { ref message } - if message == "progress sink refused auth challenge" - )); - assert_eq!(observed.len(), 2); -} - -#[tokio::test] -async fn myc_nip46_provider_rejects_zero_timeout_policy() { - let error = RadrootsSdkMycNip46RequestPolicy::new(Duration::ZERO).expect_err("zero timeout"); - - assert!(matches!( - error, - RadrootsSdkError::SignerUnavailable { ref mode, ref reason } - if mode == "myc_nip46" && reason.contains("timeout") - )); - - let target = nip46_target(remote_keys().public_key(), Vec::new()); - let transport = Arc::new(MockNip46Transport::new(remote_keys(), Vec::new())); - let constructor_error = match RadrootsSdkMycNip46Signer::from_client_with_request_policy( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - RadrootsSdkMycNip46RequestPolicy { - request_timeout: Duration::ZERO, - }, - ) { - Ok(_) => panic!("expected zero timeout constructor error"), - Err(error) => error, - }; - assert!(matches!( - constructor_error, - RadrootsSdkError::SignerUnavailable { ref mode, ref reason } - if mode == "myc_nip46" && reason.contains("timeout") - )); -} - -#[tokio::test] -async fn myc_nip46_provider_times_out_hanging_transport() { - let remote_keys = remote_keys(); - let target = nip46_target(remote_keys.public_key(), Vec::new()); - let transport = Arc::new(HangingNip46Transport::new()); - let policy = RadrootsSdkMycNip46RequestPolicy::new(Duration::from_millis(5)).expect("policy"); - let signer = RadrootsSdkMycNip46Signer::from_client_with_request_policy( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - policy, - ) - .expect("signer"); - let draft = frozen_draft(); - let actor = actor(); - - let error = signer - .sign(RadrootsSdkSignRequest::new("farm.publish", &actor, &draft)) - .await - .expect_err("timeout"); - - assert!(matches!( - error, - RadrootsSdkError::SignerRequestTimedOut { ref mode } if mode == "myc_nip46" - )); -} - -#[tokio::test] -async fn myc_nip46_provider_rejects_returned_event_drift() { - let draft = frozen_draft(); - let wrong_user_keys = remote_keys(); - let wrong_pubkey = wrong_user_keys.public_key().to_hex(); - let cases = vec![ - ( - "pubkey", - wrong_user_keys, - frozen_draft_with( - "radroots.farm.profile.v1", - &wrong_pubkey, - KIND_FARM, - 1_700_000_000, - vec![vec!["d".to_owned(), "sdk-signer".to_owned()]], - "{}", - ), - ), - ( - "id", - user_keys(), - frozen_draft_with( - "radroots.farm.profile.v1", - user_pubkey(), - KIND_FARM, - 1_700_000_000, - vec![vec!["d".to_owned(), "sdk-signer-id-drift".to_owned()]], - "{}", - ), - ), - ( - "created_at", - user_keys(), - frozen_draft_with( - "radroots.farm.profile.v1", - user_pubkey(), - KIND_FARM, - 1_700_000_001, - vec![vec!["d".to_owned(), "sdk-signer".to_owned()]], - "{}", - ), - ), - ( - "kind", - user_keys(), - frozen_draft_with( - "radroots.farm.coop.v1", - user_pubkey(), - KIND_COOP, - 1_700_000_000, - vec![vec!["d".to_owned(), "sdk-signer".to_owned()]], - "{}", - ), - ), - ( - "tags", - user_keys(), - frozen_draft_with( - "radroots.farm.profile.v1", - user_pubkey(), - KIND_FARM, - 1_700_000_000, - vec![vec!["d".to_owned(), "sdk-signer-tags-drift".to_owned()]], - "{}", - ), - ), - ( - "content", - user_keys(), - frozen_draft_with( - "radroots.farm.profile.v1", - user_pubkey(), - KIND_FARM, - 1_700_000_000, - vec![vec!["d".to_owned(), "sdk-signer".to_owned()]], - "{\"drift\":true}", - ), - ), - ]; - - for (drift_kind, signing_keys, drifted_draft) in cases { - let remote_keys = remote_keys(); - let signed_event = sign_event(&signing_keys, &drifted_draft); - let transport = Arc::new(MockNip46Transport::new( - remote_keys.clone(), - vec![MockNip46Response::Respond(Response::SignedEvent( - connect_signed_event(signed_event), - ))], - )); - let target = nip46_target(remote_keys.public_key(), Vec::new()); - let signer = RadrootsSdkMycNip46Signer::from_client( - nip46_client(target), - user_pubkey(), - transport.as_ref().clone(), - ) - .expect("signer"); - let actor = actor(); - - let error = signer - .sign(RadrootsSdkSignRequest::new("farm.publish", &actor, &draft)) - .await - .expect_err(drift_kind); - - assert!(matches!( - error, - RadrootsSdkError::SignerReturnedEventDrift { ref operation, .. } - if operation == "farm.publish" - )); - } -} - -#[tokio::test] -async fn sdk_builder_installs_configured_signer_provider() { - let empty_sdk = crate::RadrootsClient::builder() - .build() - .await - .expect("empty sdk"); - let draft = local_frozen_draft(); - let signer_actor = local_actor(); - let error = empty_sdk - .sign_with_configured_signer(RadrootsSdkSignRequest::new( - "farm.publish", - &signer_actor, - &draft, - )) - .await - .expect_err("missing configured signer"); - assert!(matches!( - error, - RadrootsSdkError::SignerUnavailable { ref mode, ref reason } - if mode == "configured" && reason.contains("no SDK signer provider") - )); - - let signer = local_sdk_signer(); - let sdk = crate::RadrootsClient::builder() - .signer_provider(RadrootsSdkSignerProvider::LocalKey(signer)) - .build() - .await - .expect("sdk"); - - assert!(sdk.configured_signer().is_some()); - assert!(matches!( - sdk.signer_status(), - Some(RadrootsSdkSignerStatus { - mode: RadrootsSdkSignerMode::LocalKey, - .. - }) - )); - let receipt = sdk - .sign_with_configured_signer(RadrootsSdkSignRequest::new( - "farm.publish", - &signer_actor, - &draft, - )) - .await - .expect("receipt"); - assert_eq!(receipt.signed_event_id, draft.expected_event_id_hex()); -}