sdk

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

commit 267e76547d08db145df6133dc2e8ffbf25b48e50
parent d19a688128087c9c62b767b9fdd9e571a3e9add9
Author: triesap <tyson@radroots.org>
Date:   Sat,  1 Aug 2026 07:15:00 +0000

sdk: quarantine NIP-46 transition surface

- Store the canonical Nostr Connect transport behind the SDK signer boundary.
- Add final client-based constructors and keep only hidden CLI transition adapters.
- Update examples and tests to exercise the final package-owned state machine.
- Verify package checks, tests, clippy, architecture policy, and source boundaries.

Diffstat:
Mcontracts/releases/api_boundaries.toml | 12++++++------
Mcrates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs | 35+++++++++++++++--------------------
Mcrates/sdk/src/signer_provider.rs | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/sdk/tests/source_boundary.rs | 18++++++++++++++++--
Mcrates/sdk/tests/unit/signer_provider_tests.rs | 179+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mdocs/implementation/COMPATIBILITY_SHIMS.md | 1+
6 files changed, 242 insertions(+), 156 deletions(-)

diff --git a/contracts/releases/api_boundaries.toml b/contracts/releases/api_boundaries.toml @@ -117,8 +117,8 @@ observed_paths = [ "nostr::RelayUrl", ] adr = "docs/decisions/0001-public-api-leakage-migration-baseline.md" -removal_step = 140 -rationale = "Legacy client state-machine signatures retain exact protocol types until the Nostr Connect transport split and conformance gate." +removal_step = 313 +rationale = "Hidden, publish-frozen client signatures remain only for CLI, Myc, and residual first-party cutovers in Steps 271, 288, and 293." [[exception]] id = "RCRV1-API-007" @@ -138,8 +138,8 @@ observed_paths = [ "nostr::UnsignedEvent", ] adr = "docs/decisions/0001-public-api-leakage-migration-baseline.md" -removal_step = 140 -rationale = "Legacy request and response envelopes retain exact Nostr values until canonical package-owned envelopes are complete." +removal_step = 313 +rationale = "Hidden, publish-frozen aliases remain only for CLI, Myc, and residual first-party cutovers in Steps 271, 288, and 293." [[exception]] id = "RCRV1-API-008" @@ -152,5 +152,5 @@ items = [ ] observed_paths = ["nostr::PublicKey", "nostr::RelayUrl"] adr = "docs/decisions/0001-public-api-leakage-migration-baseline.md" -removal_step = 140 -rationale = "Legacy URI wrappers retain exact upstream value types until URI normalization and the package conformance gate." +removal_step = 313 +rationale = "Hidden, publish-frozen URI aliases remain only for CLI, Myc, and residual first-party cutovers in Steps 271, 288, and 293." diff --git a/crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs b/crates/sdk/examples/sdk_v1_myc_nip46_signer_setup.rs @@ -1,36 +1,31 @@ use nostr::Keys as RadrootsNostrKeys; use radroots_event::envelope::kind::KIND_TRADE_PROPOSAL; -use radroots_nostr::event::Event as RadrootsNostrEvent; use radroots_nostr_connect::{ - Error as NostrConnectError, client::Target, uri::RelayUrl as ConnectRelayUrl, + client::{CancellationToken, Client, ClientEvent, Receive, Target, Transport, TransportFuture}, + uri::RelayUrl as ConnectRelayUrl, }; use radroots_sdk::{ - RadrootsClient, RadrootsSdkMycNip46Signer, RadrootsSdkNip46ClientKey, - RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture, RadrootsSdkSignerMode, - RadrootsSdkSignerProvider, radroots_sdk_myc_nip46_product_permission_strings, + RadrootsClient, RadrootsSdkMycNip46Signer, RadrootsSdkSignerMode, RadrootsSdkSignerProvider, + radroots_sdk_myc_nip46_product_permission_strings, }; -use std::sync::Arc; struct ExampleNip46Transport; -impl RadrootsSdkNip46Transport for ExampleNip46Transport { - fn publish_request_event<'a>( - &'a self, - _event: RadrootsNostrEvent, - ) -> RadrootsSdkNip46TransportFuture<'a, ()> { +impl Transport for ExampleNip46Transport { + fn publish<'a>(&'a mut self, _event: ClientEvent) -> TransportFuture<'a, ()> { Box::pin(async { Ok(()) }) } - fn next_response_event<'a>( - &'a self, - ) -> RadrootsSdkNip46TransportFuture<'a, RadrootsNostrEvent> { - Box::pin(async { Err(NostrConnectError::RequestTimedOut) }) + fn receive<'a>( + &'a mut self, + _cancellation: &'a CancellationToken, + ) -> TransportFuture<'a, Receive> { + Box::pin(async { Ok(Receive::TimedOut) }) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { - let client_key = RadrootsSdkNip46ClientKey::generate(); let remote_signer_keys = RadrootsNostrKeys::generate(); let user_keys = RadrootsNostrKeys::generate(); let remote_signer_public_key = @@ -39,11 +34,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { remote_signer_public_key, vec![ConnectRelayUrl::parse("wss://relay.example.com")?], )?; - let signer = RadrootsSdkMycNip46Signer::new( - client_key, - target, + let client = Client::generate(target)?; + let signer = RadrootsSdkMycNip46Signer::from_client( + client, user_keys.public_key().to_hex(), - Arc::new(ExampleNip46Transport), + ExampleNip46Transport, )?; let sdk = RadrootsClient::builder() .signer_provider(RadrootsSdkSignerProvider::MycNip46(Box::new(signer))) diff --git a/crates/sdk/src/signer_provider.rs b/crates/sdk/src/signer_provider.rs @@ -25,10 +25,10 @@ use radroots_signing::{ }; use std::sync::Arc; use std::time::Duration; +use tokio::sync::Mutex as AsyncMutex; use tokio::time::timeout; use uuid::Uuid; -pub type RadrootsSdkNip46TransportFuture<'a, T> = TransportFuture<'a, T>; pub type RadrootsSdkLocalSignerCapability = dyn Signer; pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 7] = [ @@ -42,32 +42,14 @@ pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 7] = [ ]; pub const RADROOTS_SDK_MYC_NIP46_DEFAULT_REQUEST_TIMEOUT_MS: u64 = 30_000; -/// Opaque client key used only to authenticate and encrypt one NIP-46 session. -/// -/// The key is single-owner, cannot be serialized, and never exposes its -/// secret representation. Generate it explicitly and move it into -/// [`RadrootsSdkMycNip46Signer::new`]. -/// -/// ```compile_fail -/// use radroots_sdk::RadrootsSdkNip46ClientKey; -/// -/// let key = RadrootsSdkNip46ClientKey::generate(); -/// let _duplicate = key.clone(); -/// ``` -/// -/// ```compile_fail -/// use radroots_sdk::RadrootsSdkNip46ClientKey; -/// -/// let key = RadrootsSdkNip46ClientKey::generate(); -/// let _json = serde_json::to_string(&key)?; -/// # Ok::<(), serde_json::Error>(()) -/// ``` +/// 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 { - /// Generates fresh client key material for one NIP-46 signer session. #[must_use] pub fn generate() -> Self { Self { @@ -89,6 +71,21 @@ impl fmt::Debug for RadrootsSdkNip46ClientKey { } } +#[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] @@ -340,16 +337,6 @@ impl RadrootsSdkLocalKeySigner { } } -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)] pub struct RadrootsSdkMycNip46RequestPolicy { request_timeout: Duration, @@ -385,12 +372,13 @@ impl Default for RadrootsSdkMycNip46RequestPolicy { pub struct RadrootsSdkMycNip46Signer { client: Arc<Client>, user_pubkey: PublicKey, - transport: Arc<dyn RadrootsSdkNip46Transport>, + 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, @@ -406,6 +394,7 @@ impl RadrootsSdkMycNip46Signer { ) } + #[doc(hidden)] pub fn new_with_request_policy( client_key: RadrootsSdkNip46ClientKey, target: Target, @@ -413,9 +402,44 @@ impl RadrootsSdkMycNip46Signer { 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_key, - target, + client, user_pubkey, transport, request_policy, @@ -423,18 +447,17 @@ impl RadrootsSdkMycNip46Signer { ) } - fn new_with_request_id_generator( - client_key: RadrootsSdkNip46ClientKey, - target: Target, + fn new_with_request_id_generator<T>( + client: Client, user_pubkey: impl AsRef<str>, - transport: Arc<dyn RadrootsSdkNip46Transport>, + transport: T, request_policy: RadrootsSdkMycNip46RequestPolicy, request_id_generator: Arc<dyn RadrootsSdkMycNip46RequestIdGenerator>, - ) -> Result<Self, RadrootsSdkError> { + ) -> Result<Self, RadrootsSdkError> + where + T: Transport + 'static, + { RadrootsSdkMycNip46RequestPolicy::new(request_policy.request_timeout())?; - 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)?; let user_pubkey = PublicKey::from_hex(user_pubkey.as_ref()).map_err(|error| { RadrootsSdkError::InvalidRequest { message: format!("myc_nip46 user pubkey is invalid: {error}"), @@ -443,7 +466,7 @@ impl RadrootsSdkMycNip46Signer { Ok(Self { client: Arc::new(client), user_pubkey, - transport, + transport: Arc::new(AsyncMutex::new(Box::new(transport))), request_policy, request_id_generator, }) @@ -498,8 +521,9 @@ impl RadrootsSdkMycNip46Signer { 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 adapter = RadrootsSdkNip46TransportAdapter { - transport: self.transport.as_ref(), + 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; @@ -598,12 +622,16 @@ pub fn radroots_sdk_myc_nip46_product_permission_strings() -> Vec<String> { .collect() } -struct RadrootsSdkNip46TransportAdapter<'a> { - transport: &'a dyn RadrootsSdkNip46Transport, +struct RadrootsSdkNip46TimeoutTransport<'a> { + transport: &'a mut dyn Transport, request_timeout: Duration, } -impl Transport for RadrootsSdkNip46TransportAdapter<'_> { +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, @@ -626,14 +654,37 @@ impl Transport for RadrootsSdkNip46TransportAdapter<'_> { 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(event)) => { + Ok(Ok(receive)) => { if cancellation.is_cancelled() { return Ok(Receive::Cancelled); } - ClientEvent::from_json(event.as_json().as_str()).map(Receive::event) + Ok(receive) } Ok(Err(error)) => Err(error), Err(_) => Ok(Receive::TimedOut), diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -285,7 +285,6 @@ fn signer_transition_surface_is_private_hidden_and_scheduled_for_removal() { fn signer_consumers_use_the_final_nostr_connect_state_machine() { let manifest = manifest_dir(); for relative in [ - "src/signer_provider.rs", "src/adapters/signer.rs", "examples/sdk_v1_myc_nip46_signer_setup.rs", ] { @@ -298,6 +297,8 @@ fn signer_consumers_use_the_final_nostr_connect_state_machine() { "RadrootsNostrConnectRequest", "RadrootsNostrConnectResponse", "RADROOTS_NOSTR_CONNECT_", + "RadrootsSdkNip46ClientKey", + "RadrootsSdkNip46Transport", ] { assert!( !source.contains(retired), @@ -307,6 +308,19 @@ fn signer_consumers_use_the_final_nostr_connect_state_machine() { } let provider = read_source(&manifest.join("src/signer_provider.rs")); - assert!(provider.contains("impl Transport for RadrootsSdkNip46TransportAdapter")); + assert!(provider.contains("transport: Arc<AsyncMutex<Box<dyn Transport>>>")); + assert!(provider.contains("impl Transport for RadrootsSdkNip46TimeoutTransport")); assert!(provider.contains(".client\n .execute(")); + assert!(provider.contains("pub fn from_client<T>(")); + assert!(provider.contains("CLI migration Step 271; removed in Step 313")); + for shim in [ + "pub struct RadrootsSdkNip46ClientKey", + "pub type RadrootsSdkNip46TransportFuture", + "pub trait RadrootsSdkNip46Transport", + ] { + let position = provider + .find(shim) + .unwrap_or_else(|| panic!("missing compatibility shim `{shim}`")); + assert!(provider[..position].ends_with("#[doc(hidden)]\n")); + } } diff --git a/crates/sdk/tests/unit/signer_provider_tests.rs b/crates/sdk/tests/unit/signer_provider_tests.rs @@ -1,4 +1,5 @@ use super::*; +use nostr::Keys as RadrootsNostrKeys; use nostr::nips::nip44::{self, Version}; use nostr::{EventBuilder, JsonUtil, Kind, Tag}; use radroots_event::contract::AuthorRole; @@ -65,16 +66,8 @@ fn nip46_target(remote_public_key: nostr::PublicKey, relays: Vec<nostr::RelayUrl Target::try_new(remote_public_key, relays).expect("NIP-46 target") } -fn client_keys() -> RadrootsSdkNip46ClientKey { - RadrootsSdkNip46ClientKey::generate() -} - -#[test] -fn nip46_client_key_debug_output_is_always_redacted() { - assert_eq!( - format!("{:?}", RadrootsSdkNip46ClientKey::generate()), - "RadrootsSdkNip46ClientKey(\"[redacted]\")" - ); +fn nip46_client(target: Target) -> Client { + Client::generate(target).expect("NIP-46 client") } fn actor() -> Actor { @@ -173,17 +166,21 @@ fn myc_signer_with_responses( remote_keys.public_key(), vec![nostr::RelayUrl::parse("wss://relay.example.com").expect("relay")], ); - let signer = - RadrootsSdkMycNip46Signer::new(client_keys(), target, user_pubkey(), transport.clone()) - .expect("signer"); + 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: Mutex<VecDeque<MockNip46Response>>, - published: Mutex<Vec<RadrootsNostrEvent>>, - inbound: Mutex<VecDeque<RadrootsNostrEvent>>, + responses: Arc<Mutex<VecDeque<MockNip46Response>>>, + published: Arc<Mutex<Vec<RadrootsNostrEvent>>>, + inbound: Arc<Mutex<VecDeque<RadrootsNostrEvent>>>, } enum MockNip46Response { @@ -194,9 +191,9 @@ impl MockNip46Transport { fn new(remote_keys: RadrootsNostrKeys, responses: Vec<MockNip46Response>) -> Self { Self { remote_keys, - responses: Mutex::new(responses.into()), - published: Mutex::new(Vec::new()), - inbound: Mutex::new(VecDeque::new()), + responses: Arc::new(Mutex::new(responses.into())), + published: Arc::new(Mutex::new(Vec::new())), + inbound: Arc::new(Mutex::new(VecDeque::new())), } } @@ -221,11 +218,18 @@ fn request_message_from_event( serde_json::from_str(payload.as_str()).expect("request message") } -impl RadrootsSdkNip46Transport for MockNip46Transport { - fn publish_request_event<'a>( - &'a self, - event: RadrootsNostrEvent, - ) -> RadrootsSdkNip46TransportFuture<'a, ()> { +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 { @@ -246,41 +250,48 @@ impl RadrootsSdkNip46Transport for MockNip46Transport { Box::pin(async { Ok(()) }) } - fn next_response_event<'a>( - &'a self, - ) -> RadrootsSdkNip46TransportFuture<'a, RadrootsNostrEvent> { + 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 { next.ok_or(NostrConnectError::RequestTimedOut) }) + 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: Mutex<Vec<RadrootsNostrEvent>>, + published: Arc<Mutex<Vec<RadrootsNostrEvent>>>, } impl HangingNip46Transport { fn new() -> Self { Self { - published: Mutex::new(Vec::new()), + published: Arc::new(Mutex::new(Vec::new())), } } } -impl RadrootsSdkNip46Transport for HangingNip46Transport { - fn publish_request_event<'a>( - &'a self, - event: RadrootsNostrEvent, - ) -> RadrootsSdkNip46TransportFuture<'a, ()> { +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 next_response_event<'a>( - &'a self, - ) -> RadrootsSdkNip46TransportFuture<'a, RadrootsNostrEvent> { - Box::pin(future::pending::< - Result<RadrootsNostrEvent, NostrConnectError>, - >()) + fn receive<'a>( + &'a mut self, + _cancellation: &'a CancellationToken, + ) -> TransportFuture<'a, Receive> { + Box::pin(future::pending::<Result<Receive, NostrConnectError>>()) } } @@ -422,9 +433,12 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() { ]; let target = nip46_target(remote_keys.public_key(), relays); let transport = Arc::new(MockNip46Transport::new(remote_keys.clone(), Vec::new())); - let signer = - RadrootsSdkMycNip46Signer::new(client_keys(), target, user_pubkey(), transport.clone()) - .expect("signer"); + 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); @@ -454,11 +468,14 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() { ); let target = nip46_target(remote_keys.public_key(), Vec::new()); - let error = - match RadrootsSdkMycNip46Signer::new(client_keys(), target, "not-a-pubkey", transport) { - Ok(_) => panic!("expected invalid pubkey"), - Err(error) => error, - }; + 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 } @@ -536,8 +553,9 @@ fn nip46_private_helpers_map_identity_adapter_and_response_edges() { 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 adapter = RadrootsSdkNip46TransportAdapter { - transport: transport.as_ref(), + let mut canonical_transport = transport.as_ref().clone(); + let mut adapter = RadrootsSdkNip46TimeoutTransport { + transport: &mut canonical_transport, request_timeout: Duration::from_millis(10), }; @@ -577,7 +595,6 @@ fn myc_nip46_product_permissions_cover_sdk_write_event_kinds() { #[tokio::test] async fn myc_nip46_provider_signs_and_validates_remote_event() { - let client_keys = client_keys(); let remote_keys = remote_keys(); let user_keys = user_keys(); let draft = frozen_draft(); @@ -593,9 +610,12 @@ async fn myc_nip46_provider_signs_and_validates_remote_event() { remote_keys.public_key(), vec![nostr::RelayUrl::parse("wss://relay.example.com").expect("relay")], ); - let signer = - RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport.clone()) - .expect("signer"); + 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, @@ -721,11 +741,10 @@ async fn myc_nip46_provider_reports_preflight_and_progress_sink_edges() { 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::new( - client_keys(), - mismatch_target, + let mismatch_signer = RadrootsSdkMycNip46Signer::from_client( + nip46_client(mismatch_target), remote_keys.public_key().to_hex(), - mismatch_transport.clone(), + mismatch_transport.as_ref().clone(), ) .expect("mismatch signer"); let signer_error = mismatch_signer @@ -794,7 +813,6 @@ async fn myc_nip46_provider_returns_completion_progress_errors_after_remote_sign #[tokio::test] async fn myc_nip46_provider_reports_auth_challenge_progress_and_timeout() { - let client_keys = client_keys(); let remote_keys = remote_keys(); let transport = Arc::new(MockNip46Transport::new( remote_keys.clone(), @@ -803,8 +821,12 @@ async fn myc_nip46_provider_reports_auth_challenge_progress_and_timeout() { ))], )); let target = nip46_target(remote_keys.public_key(), Vec::new()); - let signer = RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport) - .expect("signer"); + 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(); @@ -841,7 +863,6 @@ async fn myc_nip46_provider_reports_auth_challenge_progress_and_timeout() { #[tokio::test] async fn myc_nip46_provider_returns_progress_sink_errors_from_auth_challenge() { - let client_keys = client_keys(); let remote_keys = remote_keys(); let transport = Arc::new(MockNip46Transport::new( remote_keys.clone(), @@ -850,8 +871,12 @@ async fn myc_nip46_provider_returns_progress_sink_errors_from_auth_challenge() { ))], )); let target = nip46_target(remote_keys.public_key(), Vec::new()); - let signer = RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport) - .expect("signer"); + 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(); @@ -899,11 +924,10 @@ async fn myc_nip46_provider_rejects_zero_timeout_policy() { 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::new_with_request_policy( - client_keys(), - target, + let constructor_error = match RadrootsSdkMycNip46Signer::from_client_with_request_policy( + nip46_client(target), user_pubkey(), - transport, + transport.as_ref().clone(), RadrootsSdkMycNip46RequestPolicy { request_timeout: Duration::ZERO, }, @@ -920,16 +944,14 @@ async fn myc_nip46_provider_rejects_zero_timeout_policy() { #[tokio::test] async fn myc_nip46_provider_times_out_hanging_transport() { - let client_keys = client_keys(); 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::new_with_request_policy( - client_keys, - target, + let signer = RadrootsSdkMycNip46Signer::from_client_with_request_policy( + nip46_client(target), user_pubkey(), - transport, + transport.as_ref().clone(), policy, ) .expect("signer"); @@ -1028,7 +1050,6 @@ async fn myc_nip46_provider_rejects_returned_event_drift() { ]; for (drift_kind, signing_keys, drifted_draft) in cases { - let client_keys = client_keys(); let remote_keys = remote_keys(); let signed_event = sign_event(&signing_keys, &drifted_draft); let transport = Arc::new(MockNip46Transport::new( @@ -1038,8 +1059,12 @@ async fn myc_nip46_provider_rejects_returned_event_drift() { ))], )); let target = nip46_target(remote_keys.public_key(), Vec::new()); - let signer = RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport) - .expect("signer"); + let signer = RadrootsSdkMycNip46Signer::from_client( + nip46_client(target), + user_pubkey(), + transport.as_ref().clone(), + ) + .expect("signer"); let actor = actor(); let error = signer diff --git a/docs/implementation/COMPATIBILITY_SHIMS.md b/docs/implementation/COMPATIBILITY_SHIMS.md @@ -8,6 +8,7 @@ second protocol authority. | --- | --- | --- | --- | | `radroots_runtime_contract_v1` | `radroots_protocol::runtime::v1` | SDK CLI-host generator; standalone `oss/cli` runtime registry and command code | Step 270 | | SDK signer provider façade and `adapters::signer` | `radroots_signing` plus host-owned `radroots_nostr_connect` adapters | SDK runtime/examples/tests; standalone `oss/cli` and `oss/studio_app` | Step 313, after SDK Step 248, downstream Steps 269-293, and matrix Step 294 | +| hidden `RadrootsSdkNip46ClientKey`, `RadrootsSdkNip46Transport`, and legacy constructors | package-owned `radroots_nostr_connect::client::{Client, Transport}` consumed by `RadrootsSdkMycNip46Signer::from_client` | standalone `oss/cli` | Step 313, after CLI NIP-46 cutover Step 271 and matrix Step 294 | The `radroots_sdk` library no longer depends on or reexports the runtime shim. Its radrootsd execution path also consumes