lib

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

commit 89a9a563a7d1a6e7a2e72fcc64d2fff9fed2c1cc
parent 233f9fc50f5571d517d07be8564a282faeb959ee
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Apr 2026 07:48:53 +0000

sdk: add radrootsd signer session handles

Diffstat:
Mcrates/sdk/src/adapters/radrootsd.rs | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mcrates/sdk/src/client.rs | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/lib.rs | 10+++++++++-
Mcrates/sdk/tests/client.rs | 4++++
Mcrates/sdk/tests/radrootsd.rs | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
5 files changed, 481 insertions(+), 44 deletions(-)

diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -7,7 +7,7 @@ use crate::listing; use crate::listing::RadrootsListing; use radroots_events::kinds::KIND_LISTING; use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -34,6 +34,59 @@ impl fmt::Debug for SdkRadrootsdSignerAuthority { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum SdkRadrootsdSignerSessionMode { + #[serde(alias = "bunker")] + Bunker, + #[serde(alias = "nostrconnect")] + Nostrconnect, +} + +#[derive(Clone, PartialEq, Eq, Serialize)] +pub struct SdkRadrootsdSignerSessionConnectRequest { + pub url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub client_secret_key: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub signer_authority: Option<SdkRadrootsdSignerAuthority>, +} + +impl SdkRadrootsdSignerSessionConnectRequest { + pub fn bunker(url: impl Into<String>) -> Self { + Self { + url: url.into(), + client_secret_key: None, + signer_authority: None, + } + } + + pub fn nostrconnect(url: impl Into<String>, client_secret_key: impl Into<String>) -> Self { + Self { + url: url.into(), + client_secret_key: Some(client_secret_key.into()), + signer_authority: None, + } + } + + pub fn with_signer_authority(mut self, signer_authority: SdkRadrootsdSignerAuthority) -> Self { + self.signer_authority = Some(signer_authority); + self + } +} + +impl fmt::Debug for SdkRadrootsdSignerSessionConnectRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdSignerSessionConnectRequest"); + debug.field("url", &self.url); + debug.field( + "client_secret_key", + &self.client_secret_key.as_ref().map(|_| "<redacted>"), + ); + debug.field("signer_authority", &self.signer_authority); + debug.finish() + } +} + #[derive(Clone, Serialize)] pub struct SdkRadrootsdListingPublishRequest { pub listing: RadrootsListing, @@ -80,6 +133,15 @@ impl SdkRadrootsdListingPublishRequest { } } +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] +pub(crate) struct SdkRadrootsdSignerSessionConnectResponse { + pub session_id: String, + pub mode: SdkRadrootsdSignerSessionMode, + pub remote_signer_pubkey: String, + pub client_pubkey: String, + pub relays: Vec<String>, +} + #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct SdkRadrootsdBridgePublishResponse { pub deduplicated: bool, @@ -168,6 +230,69 @@ pub async fn publish_listing( request: &SdkRadrootsdListingPublishRequest, timeout: Duration, ) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-listing-publish", + "bridge.listing.publish", + request, + timeout, + ) + .await +} + +pub(crate) async fn connect_signer_session( + endpoint: &str, + auth: &RadrootsdAuth, + request: &SdkRadrootsdSignerSessionConnectRequest, + timeout: Duration, +) -> Result<SdkRadrootsdSignerSessionConnectResponse, RadrootsdError> { + jsonrpc_call( + endpoint, + auth, + "radroots-sdk-nip46-connect", + "nip46.connect", + request, + timeout, + ) + .await +} + +fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { + let mut headers = HeaderMap::new(); + match auth { + RadrootsdAuth::None => Ok(headers), + RadrootsdAuth::BearerToken(token) => { + let value = HeaderValue::from_str(format!("Bearer {token}").as_str()) + .map_err(|err| RadrootsdError::InvalidAuthHeader(err.to_string()))?; + headers.insert(AUTHORIZATION, value); + Ok(headers) + } + } +} + +pub fn bridge_listing_publish_request_json( + request: &SdkRadrootsdListingPublishRequest, +) -> Result<Value, RadrootsdError> { + serde_json::to_value(request).map_err(|err| { + RadrootsdError::MalformedResponse(format!( + "serialize radrootsd listing publish request: {err}" + )) + }) +} + +async fn jsonrpc_call<P, R>( + endpoint: &str, + auth: &RadrootsdAuth, + request_id: &str, + method: &str, + params: &P, + timeout: Duration, +) -> Result<R, RadrootsdError> +where + P: Serialize + ?Sized, + R: DeserializeOwned, +{ let client = reqwest::Client::builder() .timeout(timeout) .build() @@ -177,16 +302,17 @@ pub async fn publish_listing( .headers(auth_headers(auth)?) .json(&json!({ "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "method": "bridge.listing.publish", - "params": request, + "id": request_id, + "method": method, + "params": params, })); request_builder = request_builder.header(CONTENT_TYPE, "application/json"); - let response = request_builder.send().await.map_err(|err| { - RadrootsdError::Http(format!("send radrootsd listing publish request: {err}")) - })?; + let response = request_builder + .send() + .await + .map_err(|err| RadrootsdError::Http(format!("send radrootsd {method} request: {err}")))?; let status = response.status(); let body = response .text() @@ -201,47 +327,21 @@ pub async fn publish_listing( ))); } - let envelope: JsonRpcEnvelope<SdkRadrootsdBridgePublishResponse> = - serde_json::from_str(body.as_str()).map_err(|err| { - RadrootsdError::MalformedResponse(format!( - "decode radrootsd bridge.listing.publish response: {err}" - )) - })?; + let envelope: JsonRpcEnvelope<R> = serde_json::from_str(body.as_str()).map_err(|err| { + RadrootsdError::MalformedResponse(format!("decode radrootsd {method} response: {err}")) + })?; match (envelope.result, envelope.error) { (Some(result), None) => Ok(result), (None, Some(error)) => Err(RadrootsdError::JsonRpc(format!( - "radrootsd bridge.listing.publish failed {}: {}", + "radrootsd {method} failed {}: {}", error.code, error.message ))), (Some(_), Some(error)) => Err(RadrootsdError::MalformedResponse(format!( - "radrootsd bridge.listing.publish returned result and error: {} {}", + "radrootsd {method} returned result and error: {} {}", error.code, error.message ))), - (None, None) => Err(RadrootsdError::MalformedResponse( - "radrootsd bridge.listing.publish returned neither result nor error".to_owned(), - )), - } -} - -fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { - let mut headers = HeaderMap::new(); - match auth { - RadrootsdAuth::None => Ok(headers), - RadrootsdAuth::BearerToken(token) => { - let value = HeaderValue::from_str(format!("Bearer {token}").as_str()) - .map_err(|err| RadrootsdError::InvalidAuthHeader(err.to_string()))?; - headers.insert(AUTHORIZATION, value); - Ok(headers) - } + (None, None) => Err(RadrootsdError::MalformedResponse(format!( + "radrootsd {method} returned neither result nor error" + ))), } } - -pub fn bridge_listing_publish_request_json( - request: &SdkRadrootsdListingPublishRequest, -) -> Result<Value, RadrootsdError> { - serde_json::to_value(request).map_err(|err| { - RadrootsdError::MalformedResponse(format!( - "serialize radrootsd listing publish request: {err}" - )) - }) -} diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -177,6 +177,105 @@ impl core::fmt::Display for SdkPublishError { impl std::error::Error for SdkPublishError {} #[cfg(feature = "radrootsd-client")] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SdkRadrootsdSessionError { + Config(SdkConfigError), + UnsupportedTransport { + transport: SdkTransportMode, + operation: &'static str, + }, + Radrootsd(String), +} + +#[cfg(feature = "radrootsd-client")] +impl From<SdkConfigError> for SdkRadrootsdSessionError { + fn from(value: SdkConfigError) -> Self { + Self::Config(value) + } +} + +#[cfg(feature = "radrootsd-client")] +impl fmt::Display for SdkRadrootsdSessionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Config(err) => write!(f, "{err}"), + Self::UnsupportedTransport { + transport, + operation, + } => { + write!( + f, + "{operation} requires a different sdk transport mode than {transport:?}" + ) + } + Self::Radrootsd(message) => write!(f, "{message}"), + } + } +} + +#[cfg(all(feature = "radrootsd-client", feature = "std"))] +impl std::error::Error for SdkRadrootsdSessionError {} + +#[cfg(feature = "radrootsd-client")] +#[derive(Clone, PartialEq, Eq)] +pub struct SdkRadrootsdSignerSessionHandle { + session_id: String, + mode: radrootsd::SdkRadrootsdSignerSessionMode, + remote_signer_pubkey: String, + client_pubkey: String, + relays: Vec<String>, +} + +#[cfg(feature = "radrootsd-client")] +impl fmt::Debug for SdkRadrootsdSignerSessionHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("SdkRadrootsdSignerSessionHandle"); + debug.field("session_id", &"<redacted>"); + debug.field("mode", &self.mode); + debug.field("remote_signer_pubkey", &self.remote_signer_pubkey); + debug.field("client_pubkey", &self.client_pubkey); + debug.field("relays", &self.relays); + debug.finish() + } +} + +#[cfg(feature = "radrootsd-client")] +impl SdkRadrootsdSignerSessionHandle { + pub fn mode(&self) -> radrootsd::SdkRadrootsdSignerSessionMode { + self.mode + } + + pub fn remote_signer_pubkey(&self) -> &str { + self.remote_signer_pubkey.as_str() + } + + pub fn client_pubkey(&self) -> &str { + self.client_pubkey.as_str() + } + + pub fn relays(&self) -> &[String] { + self.relays.as_slice() + } + + pub(crate) fn session_id(&self) -> &str { + self.session_id.as_str() + } +} + +#[cfg(feature = "radrootsd-client")] +impl From<radrootsd::SdkRadrootsdSignerSessionConnectResponse> for SdkRadrootsdSignerSessionHandle { + fn from(value: radrootsd::SdkRadrootsdSignerSessionConnectResponse) -> Self { + Self { + session_id: value.session_id, + mode: value.mode, + remote_signer_pubkey: value.remote_signer_pubkey, + client_pubkey: value.client_pubkey, + relays: value.relays, + } + } +} + +#[cfg(feature = "radrootsd-client")] #[derive(Clone, PartialEq, Eq)] pub struct SdkRadrootsdListingPublishOptions { pub signer_session_id: String, @@ -191,6 +290,10 @@ impl SdkRadrootsdListingPublishOptions { idempotency_key: None, } } + + pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { + Self::new(session.session_id()) + } } #[cfg(feature = "radrootsd-client")] @@ -263,6 +366,11 @@ impl RadrootsSdkClient { TradeClient { client: self } } + #[cfg(feature = "radrootsd-client")] + pub fn radrootsd(&self) -> RadrootsdClient<'_> { + RadrootsdClient { client: self } + } + #[cfg(any( feature = "radrootsd-client", all( @@ -364,6 +472,113 @@ impl RadrootsSdkClient { response, )) } + + #[cfg(feature = "radrootsd-client")] + async fn connect_radrootsd_signer_session( + &self, + request: &radrootsd::SdkRadrootsdSignerSessionConnectRequest, + ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { + if self.transport() != SdkTransportMode::Radrootsd { + return Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.signer_sessions.connect", + }); + } + + let endpoint = match &self.resolved_transport_target { + SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), + SdkResolvedTransportTarget::RelayDirect { .. } => { + return Err(SdkRadrootsdSessionError::UnsupportedTransport { + transport: self.transport(), + operation: "radrootsd.signer_sessions.connect", + }); + } + }; + let response = radrootsd::connect_signer_session( + endpoint, + &self.config.radrootsd.auth, + request, + Duration::from_millis(self.config.network.timeout_ms), + ) + .await + .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; + Ok(response.into()) + } +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Debug, Clone, Copy)] +pub struct RadrootsdClient<'a> { + client: &'a RadrootsSdkClient, +} + +#[cfg(feature = "radrootsd-client")] +impl<'a> RadrootsdClient<'a> { + pub fn sdk(&self) -> &'a RadrootsSdkClient { + self.client + } + + pub fn transport(&self) -> SdkTransportMode { + self.client.transport() + } + + pub fn signer(&self) -> SignerConfig { + self.client.signer() + } + + pub fn signer_sessions(&self) -> RadrootsdSignerSessionClient<'a> { + RadrootsdSignerSessionClient { + client: self.client, + } + } +} + +#[cfg(feature = "radrootsd-client")] +#[derive(Debug, Clone, Copy)] +pub struct RadrootsdSignerSessionClient<'a> { + client: &'a RadrootsSdkClient, +} + +#[cfg(feature = "radrootsd-client")] +impl<'a> RadrootsdSignerSessionClient<'a> { + pub fn sdk(&self) -> &'a RadrootsSdkClient { + self.client + } + + pub fn transport(&self) -> SdkTransportMode { + self.client.transport() + } + + pub fn signer(&self) -> SignerConfig { + self.client.signer() + } + + pub async fn connect( + &self, + request: &radrootsd::SdkRadrootsdSignerSessionConnectRequest, + ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { + self.client.connect_radrootsd_signer_session(request).await + } + + pub async fn connect_bunker( + &self, + url: impl Into<String>, + ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { + let request = radrootsd::SdkRadrootsdSignerSessionConnectRequest::bunker(url); + self.connect(&request).await + } + + pub async fn connect_nostrconnect( + &self, + url: impl Into<String>, + client_secret_key: impl Into<String>, + ) -> Result<SdkRadrootsdSignerSessionHandle, SdkRadrootsdSessionError> { + let request = radrootsd::SdkRadrootsdSignerSessionConnectRequest::nostrconnect( + url, + client_secret_key, + ); + self.connect(&request).await + } } #[derive(Debug, Clone, Copy)] diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -26,12 +26,20 @@ pub mod profile; pub mod trade; #[cfg(feature = "radrootsd-client")] -pub use crate::client::SdkRadrootsdListingPublishOptions; +pub use crate::adapters::radrootsd::{ + SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, + SdkRadrootsdSignerSessionMode, +}; pub use crate::client::{ FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError, SdkPublishReceipt, SdkRadrootsdPublishReceipt, SdkRelayFailure, SdkRelayPublishReceipt, SdkResolvedTransportTarget, SdkTransportReceipt, TradeClient, }; +#[cfg(feature = "radrootsd-client")] +pub use crate::client::{ + RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdListingPublishOptions, + SdkRadrootsdSessionError, SdkRadrootsdSignerSessionHandle, +}; pub use crate::config::{ NetworkConfig, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, RADROOTS_SDK_LOCAL_RELAY_URL, RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT, RADROOTS_SDK_PRODUCTION_RELAY_URL, diff --git a/crates/sdk/tests/client.rs b/crates/sdk/tests/client.rs @@ -165,11 +165,15 @@ fn namespace_clients_reflect_explicit_transport_mode() { assert_eq!(client.farm().transport(), SdkTransportMode::Radrootsd); assert_eq!(client.listing().transport(), SdkTransportMode::Radrootsd); assert_eq!(client.trade().transport(), SdkTransportMode::Radrootsd); + #[cfg(feature = "radrootsd-client")] + assert_eq!(client.radrootsd().transport(), SdkTransportMode::Radrootsd); assert_eq!(client.signer(), SignerConfig::LocalIdentity); assert_eq!(client.profile().signer(), SignerConfig::LocalIdentity); assert_eq!(client.farm().signer(), SignerConfig::LocalIdentity); assert_eq!(client.listing().signer(), SignerConfig::LocalIdentity); assert_eq!(client.trade().signer(), SignerConfig::LocalIdentity); + #[cfg(feature = "radrootsd-client")] + assert_eq!(client.radrootsd().signer(), SignerConfig::LocalIdentity); } #[test] diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -7,7 +7,8 @@ use radroots_core::{ use radroots_events::kinds::KIND_LISTING_DRAFT; use radroots_sdk::adapters::radrootsd::{ SdkRadrootsdBridgeJob, SdkRadrootsdBridgePublishResponse, SdkRadrootsdListingPublishRequest, - SdkRadrootsdSignerAuthority, + SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, + SdkRadrootsdSignerSessionMode, }; use radroots_sdk::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, @@ -17,7 +18,7 @@ use radroots_sdk::listing::{ use radroots_sdk::{ RadrootsNostrEvent, RadrootsSdkClient, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, SdkEnvironment, SdkPublishError, SdkRadrootsdListingPublishOptions, SdkRadrootsdPublishReceipt, - SdkTransportMode, SdkTransportReceipt, SignerConfig, + SdkRadrootsdSessionError, SdkTransportMode, SdkTransportReceipt, SignerConfig, }; use serde_json::{Value, json}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -325,6 +326,115 @@ fn radrootsd_debug_redacts_signer_session_values() { let options_debug = format!("{options:?}"); assert!(!options_debug.contains("session-123")); assert!(options_debug.contains("<redacted>")); + + let connect_request = SdkRadrootsdSignerSessionConnectRequest::nostrconnect( + "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + "client-secret-key", + ) + .with_signer_authority(SdkRadrootsdSignerAuthority { + provider_runtime_id: "runtime-1".to_owned(), + account_identity_id: "identity-1".to_owned(), + provider_signer_session_id: Some("provider-session-123".to_owned()), + }); + let connect_request_debug = format!("{connect_request:?}"); + assert!(!connect_request_debug.contains("client-secret-key")); + assert!(!connect_request_debug.contains("provider-session-123")); + assert!(connect_request_debug.contains("<redacted>")); +} + +#[tokio::test] +async fn radrootsd_signer_session_connect_returns_opaque_handle() -> TestResult<()> { + let (server, request_rx) = JsonRpcServer::spawn( + Some("Bearer sdk-secret"), + json!({ + "jsonrpc": "2.0", + "id": "radroots-sdk-nip46-connect", + "result": { + "session_id": "session-123", + "mode": "Nostrconnect", + "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "relays": ["wss://radroots.org"] + } + }), + ) + .await?; + + let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); + config.transport = SdkTransportMode::Radrootsd; + config.signer = SignerConfig::Nip46; + config.radrootsd = RadrootsdConfig { + endpoint: Some(server.endpoint().to_owned()), + auth: RadrootsdAuth::BearerToken("sdk-secret".to_owned()), + }; + let client = RadrootsSdkClient::from_config(config)?; + let request = SdkRadrootsdSignerSessionConnectRequest::nostrconnect( + "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + "client-secret-key", + ); + + let handle = client + .radrootsd() + .signer_sessions() + .connect(&request) + .await?; + let request_json = request_rx.await?; + + assert_eq!(request_json["method"], "nip46.connect"); + assert_eq!( + request_json["params"]["url"], + "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret" + ); + assert_eq!( + request_json["params"]["client_secret_key"], + "client-secret-key" + ); + assert_eq!(handle.mode(), SdkRadrootsdSignerSessionMode::Nostrconnect); + assert_eq!( + handle.remote_signer_pubkey(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); + assert_eq!( + handle.client_pubkey(), + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + ); + assert_eq!(handle.relays(), &["wss://radroots.org".to_owned()]); + + let handle_debug = format!("{handle:?}"); + assert!(!handle_debug.contains("session-123")); + assert!(handle_debug.contains("<redacted>")); + + let options = SdkRadrootsdListingPublishOptions::from_signer_session(&handle); + let options_debug = format!("{options:?}"); + assert!(!options_debug.contains("session-123")); + assert!(options_debug.contains("<redacted>")); + + Ok(()) +} + +#[tokio::test] +async fn radrootsd_signer_session_connect_rejects_relay_transport_mode() -> TestResult<()> { + let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; + let request = SdkRadrootsdSignerSessionConnectRequest::bunker( + "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", + ); + + let error = client + .radrootsd() + .signer_sessions() + .connect(&request) + .await + .expect_err("unsupported transport"); + + assert!(matches!( + error, + SdkRadrootsdSessionError::UnsupportedTransport { + transport: SdkTransportMode::RelayDirect, + operation: "radrootsd.signer_sessions.connect", + } + )); + + Ok(()) } #[tokio::test]