commit 9a43937b5c956970c07155ed845a499c942574e0
parent 650aac1745fbadb4aa342677229a4193c6e15153
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:
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());
-}