sdk

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

commit 96d078828c89240fe458662deb3625cbde3bd300
parent c56df8b75d8c2728555a463e13778a61533abaae
Author: triesap <tyson@radroots.org>
Date:   Fri, 19 Jun 2026 03:19:25 -0700

sdk: remove legacy client config facade

Remove old public client and config modules.

Delete tests for retired direct transport facade behavior.

Keep radrootsd auth scoped to the adapter boundary.

Guard the canonical runtime public surface with source-boundary tests.

Diffstat:
Mcrates/sdk/src/adapters/radrootsd.rs | 359++++---------------------------------------------------------------------------
Dcrates/sdk/src/client.rs | 2263-------------------------------------------------------------------------------
Dcrates/sdk/src/config.rs | 388-------------------------------------------------------------------------------
Mcrates/sdk/src/lib.rs | 25-------------------------
Dcrates/sdk/tests/client.rs | 383-------------------------------------------------------------------------------
Dcrates/sdk/tests/config.rs | 562-------------------------------------------------------------------------------
Dcrates/sdk/tests/radrootsd.rs | 1955-------------------------------------------------------------------------------
Dcrates/sdk/tests/relay_direct.rs | 551-------------------------------------------------------------------------------
Mcrates/sdk/tests/source_boundary.rs | 74+++++++++++++++++++++++++++++++++++---------------------------------------
9 files changed, 52 insertions(+), 6508 deletions(-)

diff --git a/crates/sdk/src/adapters/radrootsd.rs b/crates/sdk/src/adapters/radrootsd.rs @@ -1,18 +1,32 @@ use core::fmt; use core::time::Duration; -use crate::config::RadrootsdAuth; use crate::farm::RadrootsFarm; use crate::listing; use crate::listing::RadrootsListing; -use crate::order; use crate::profile::{RadrootsProfile, RadrootsProfileType}; +use radroots_events::RadrootsNostrEvent; use radroots_events::kinds::KIND_LISTING; -use radroots_events::{RadrootsNostrEvent, RadrootsNostrEventPtr}; use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde_json::{Value, json}; +#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum RadrootsdAuth { + #[default] + None, + BearerToken(String), +} + +impl fmt::Debug for RadrootsdAuth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => f.write_str("None"), + Self::BearerToken(_) => f.write_str("BearerToken(<redacted>)"), + } + } +} + #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SdkRadrootsdSignerAuthority { pub provider_runtime_id: String, @@ -185,29 +199,6 @@ impl fmt::Debug for SdkRadrootsdListingPublishRequest { } } -#[derive(Clone, PartialEq, Eq, Serialize)] -pub(crate) struct SdkRadrootsdOrderRequestPublishRequest { - pub order: order::RadrootsOrderRequest, - pub listing_event: RadrootsNostrEventPtr, - pub signer_session_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub idempotency_key: Option<String>, -} - -impl fmt::Debug for SdkRadrootsdOrderRequestPublishRequest { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdOrderRequestPublishRequest"); - debug.field("order", &self.order); - debug.field("listing_event", &self.listing_event); - debug.field("signer_session_id", &"<redacted>"); - debug.field("signer_authority", &self.signer_authority); - debug.field("idempotency_key", &self.idempotency_key); - debug.finish() - } -} - impl SdkRadrootsdListingPublishRequest { pub fn from_event( event: &RadrootsNostrEvent, @@ -228,84 +219,6 @@ 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(Clone, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionViewResponse { - pub session_id: String, - pub role: SdkRadrootsdSignerSessionRole, - pub client_pubkey: String, - pub signer_pubkey: String, - #[serde(default)] - pub user_pubkey: Option<String>, - pub relays: Vec<String>, - pub permissions: Vec<String>, - #[serde(default)] - pub name: Option<String>, - #[serde(default)] - pub url: Option<String>, - #[serde(default)] - pub image: Option<String>, - pub auth_required: bool, - pub authorized: bool, - #[serde(default)] - pub auth_url: Option<String>, - #[serde(default)] - pub expires_in_secs: Option<u64>, - #[serde(default)] - pub signer_authority: Option<SdkRadrootsdSignerAuthority>, -} - -impl fmt::Debug for SdkRadrootsdSignerSessionViewResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdSignerSessionViewResponse"); - debug.field("session_id", &"<redacted>"); - debug.field("role", &self.role); - debug.field("client_pubkey", &self.client_pubkey); - debug.field("signer_pubkey", &self.signer_pubkey); - debug.field("user_pubkey", &self.user_pubkey); - debug.field("relays", &self.relays); - debug.field("permissions", &self.permissions); - debug.field("name", &self.name); - debug.field("url", &self.url); - debug.field("image", &self.image); - debug.field("auth_required", &self.auth_required); - debug.field("authorized", &self.authorized); - debug.field("auth_url", &self.auth_url); - debug.field("expires_in_secs", &self.expires_in_secs); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionAuthorizeResponse { - pub authorized: bool, - pub replayed: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionPublicKeyResponse { - pub pubkey: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionRequireAuthResponse { - pub required: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] -pub(crate) struct SdkRadrootsdSignerSessionCloseResponse { - pub closed: bool, -} - #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct SdkRadrootsdBridgePublishResponse { pub deduplicated: bool, @@ -491,22 +404,6 @@ struct JsonRpcError { message: String, } -#[derive(Debug, Serialize)] -struct SdkRadrootsdSignerSessionParams<'a> { - session_id: &'a str, -} - -#[derive(Debug, Serialize)] -struct SdkRadrootsdSignerSessionRequireAuthParams<'a> { - session_id: &'a str, - auth_url: &'a str, -} - -#[derive(Debug, Serialize)] -struct SdkRadrootsdBridgeJobParams<'a> { - job_id: &'a str, -} - pub async fn publish_listing( endpoint: &str, auth: &RadrootsdAuth, @@ -524,228 +421,6 @@ pub async fn publish_listing( .await } -pub(crate) async fn publish_profile( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdProfilePublishRequest, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-profile-publish", - "bridge.profile.publish", - request, - timeout, - ) - .await -} - -pub(crate) async fn publish_farm( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdFarmPublishRequest, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-farm-publish", - "bridge.farm.publish", - request, - timeout, - ) - .await -} - -pub(crate) async fn publish_order_request( - endpoint: &str, - auth: &RadrootsdAuth, - request: &SdkRadrootsdOrderRequestPublishRequest, - timeout: Duration, -) -> Result<SdkRadrootsdBridgePublishResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-order-request-publish", - "bridge.order.request", - 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 -} - -pub(crate) async fn signer_session_status( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionViewResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-status", - "nip46.session.status", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn list_signer_sessions( - endpoint: &str, - auth: &RadrootsdAuth, - timeout: Duration, -) -> Result<Vec<SdkRadrootsdSignerSessionViewResponse>, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-list", - "nip46.session.list", - &json!({}), - timeout, - ) - .await -} - -pub(crate) async fn authorize_signer_session( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionAuthorizeResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-authorize", - "nip46.session.authorize", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn get_signer_session_public_key( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionPublicKeyResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-get-public-key", - "nip46.get_public_key", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn require_signer_session_auth( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - auth_url: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionRequireAuthResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-require-auth", - "nip46.session.require_auth", - &SdkRadrootsdSignerSessionRequireAuthParams { - session_id, - auth_url, - }, - timeout, - ) - .await -} - -pub(crate) async fn close_signer_session( - endpoint: &str, - auth: &RadrootsdAuth, - session_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdSignerSessionCloseResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-nip46-session-close", - "nip46.session.close", - &SdkRadrootsdSignerSessionParams { session_id }, - timeout, - ) - .await -} - -pub(crate) async fn bridge_status( - endpoint: &str, - auth: &RadrootsdAuth, - timeout: Duration, -) -> Result<SdkRadrootsdBridgeStatusResponse, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-bridge-status", - "bridge.status", - &json!({}), - timeout, - ) - .await -} - -pub(crate) async fn bridge_job_status( - endpoint: &str, - auth: &RadrootsdAuth, - job_id: &str, - timeout: Duration, -) -> Result<SdkRadrootsdBridgeJobView, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-bridge-job-status", - "bridge.job.status", - &SdkRadrootsdBridgeJobParams { job_id }, - timeout, - ) - .await -} - -pub(crate) async fn list_bridge_jobs( - endpoint: &str, - auth: &RadrootsdAuth, - timeout: Duration, -) -> Result<Vec<SdkRadrootsdBridgeJobView>, RadrootsdError> { - jsonrpc_call( - endpoint, - auth, - "radroots-sdk-bridge-job-list", - "bridge.job.list", - &json!({}), - timeout, - ) - .await -} - fn auth_headers(auth: &RadrootsdAuth) -> Result<HeaderMap, RadrootsdError> { let mut headers = HeaderMap::new(); match auth { diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -1,2263 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{format, string::String, vec::Vec}; -use core::fmt; -#[cfg(feature = "std")] -use std::{string::String, vec::Vec}; - -#[cfg(feature = "radrootsd-client")] -use crate::adapters::radrootsd; -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -use crate::adapters::{relay, signing}; -use crate::config::SignerConfig; -use crate::config::{RadrootsSdkConfig, SdkConfigError, SdkTransportMode}; -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -use crate::identity::RadrootsIdentity; -use crate::listing; -#[cfg(feature = "radrootsd-client")] -use crate::order; -#[cfg(feature = "serde_json")] -use crate::{farm, profile}; -#[cfg(any( - feature = "radrootsd-client", - all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ) -))] -use core::time::Duration; -use radroots_events::RadrootsNostrEvent; -#[cfg(feature = "radrootsd-client")] -use radroots_events::RadrootsNostrEventPtr; -#[cfg(feature = "radrootsd-client")] -use radroots_events::kinds::{KIND_FARM, KIND_LISTING}; -#[cfg(feature = "serde_json")] -use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -#[cfg(feature = "serde_json")] -use radroots_events_codec::wire::WireEventParts; - -type NostrTags = Vec<Vec<String>>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SdkPublishReceipt { - pub transport: SdkTransportMode, - pub event_kind: Option<u32>, - pub event_id: Option<String>, - pub transport_receipt: SdkTransportReceipt, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkTransportReceipt { - RelayDirect(SdkRelayPublishReceipt), - Radrootsd(SdkRadrootsdPublishReceipt), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SdkRelayPublishReceipt { - pub event: RadrootsNostrEvent, - pub event_id: String, - pub event_kind: u32, - pub created_at: u32, - pub signature: String, - pub target_relays: Vec<String>, - pub connected_relays: Vec<String>, - pub acknowledged_relays: Vec<String>, - pub failed_relays: Vec<SdkRelayFailure>, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SdkRelayFailure { - pub relay_url: String, - pub error: String, -} - -#[derive(Clone, PartialEq, Eq, Default)] -pub struct SdkRadrootsdPublishReceipt { - pub accepted: bool, - pub deduplicated: bool, - pub job_id: Option<String>, - pub status: Option<String>, - pub signer_mode: Option<String>, - pub signer_session_id: Option<String>, - pub event_addr: Option<String>, - pub relay_count: Option<usize>, - pub acknowledged_relay_count: Option<usize>, -} - -impl fmt::Debug for SdkRadrootsdPublishReceipt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdPublishReceipt"); - debug.field("accepted", &self.accepted); - debug.field("deduplicated", &self.deduplicated); - debug.field("job_id", &self.job_id); - debug.field("status", &self.status); - debug.field( - "signer_mode", - &self.signer_mode.as_ref().map(|_| "<redacted>"), - ); - debug.field( - "signer_session_id", - &self.signer_session_id.as_ref().map(|_| "<redacted>"), - ); - debug.field("event_addr", &self.event_addr); - debug.field("relay_count", &self.relay_count); - debug.field("acknowledged_relay_count", &self.acknowledged_relay_count); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdPublishReceipt { - pub fn job(&self) -> Option<SdkRadrootsdBridgeJobRef> { - self.job_id - .as_ref() - .map(|job_id| SdkRadrootsdBridgeJobRef::new(job_id.clone())) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkPublishError { - Config(SdkConfigError), - Encode(String), - UnsupportedTransport { - transport: SdkTransportMode, - operation: &'static str, - }, - UnsupportedSignerMode { - transport: SdkTransportMode, - signer: SignerConfig, - required: SignerConfig, - operation: &'static str, - }, - Relay(String), - RelaySetup { - transport: SdkTransportMode, - operation: &'static str, - target_relays: Vec<String>, - error: String, - }, - RelayNotAcknowledged { - transport: SdkTransportMode, - failed_relays: Vec<SdkRelayFailure>, - }, - Radrootsd(String), -} - -impl From<SdkConfigError> for SdkPublishError { - fn from(value: SdkConfigError) -> Self { - Self::Config(value) - } -} - -impl core::fmt::Display for SdkPublishError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Config(err) => write!(f, "{err}"), - Self::Encode(message) => write!(f, "{message}"), - Self::UnsupportedTransport { - transport, - operation, - } => { - write!( - f, - "{operation} requires a different sdk transport mode than {transport:?}" - ) - } - Self::UnsupportedSignerMode { - transport, - signer, - required, - operation, - } => write!( - f, - "{operation} requires signer mode `{required}` for {transport:?} transport, got `{signer}`" - ), - Self::Relay(message) => write!(f, "{message}"), - Self::RelaySetup { - transport, - operation, - target_relays, - error, - } => { - if target_relays.is_empty() { - write!( - f, - "{operation} failed to prepare {transport:?} relay publish: {error}" - ) - } else { - let relays = target_relays.join(", "); - write!( - f, - "{operation} failed to prepare {transport:?} relay publish for {relays}: {error}" - ) - } - } - Self::RelayNotAcknowledged { - transport, - failed_relays, - } => { - if failed_relays.is_empty() { - write!(f, "{transport:?} publish was not acknowledged by any relay") - } else { - let summary = failed_relays - .iter() - .map(|failure| format!("{}: {}", failure.relay_url, failure.error)) - .collect::<Vec<_>>() - .join(", "); - write!( - f, - "{transport:?} publish was not acknowledged by any relay: {summary}" - ) - } - } - Self::Radrootsd(message) => write!(f, "{message}"), - } - } -} - -#[cfg(feature = "std")] -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(Debug, Clone, PartialEq, Eq)] -pub enum SdkRadrootsdBridgeError { - Config(SdkConfigError), - UnsupportedTransport { - transport: SdkTransportMode, - operation: &'static str, - }, - Radrootsd(String), -} - -#[cfg(feature = "radrootsd-client")] -impl From<SdkConfigError> for SdkRadrootsdBridgeError { - fn from(value: SdkConfigError) -> Self { - Self::Config(value) - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Display for SdkRadrootsdBridgeError { - 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 SdkRadrootsdBridgeError {} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionRef { - session_id: String, -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdSignerSessionRef { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SdkRadrootsdSignerSessionRef") - .field("session_id", &"<redacted>") - .finish() - } -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdSignerSessionRef { - pub fn from_session_id(session_id: impl Into<String>) -> Self { - Self { - session_id: session_id.into(), - } - } - - pub fn session_id(&self) -> &str { - self.session_id.as_str() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdBridgeJobRef { - job_id: String, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdBridgeJobRef { - pub fn new(job_id: impl Into<String>) -> Self { - Self { - job_id: job_id.into(), - } - } - - pub fn job_id(&self) -> &str { - self.job_id.as_str() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdBridgeStatus { - pub enabled: bool, - pub ready: bool, - pub auth_mode: String, - pub signer_mode: String, - pub default_signer_mode: String, - pub supported_signer_modes: Vec<String>, - pub available_nip46_signer_sessions: usize, - pub relay_count: usize, - pub delivery_policy: radrootsd::SdkRadrootsdBridgeDeliveryPolicy, - pub delivery_quorum: Option<usize>, - pub publish_max_attempts: usize, - pub publish_initial_backoff_millis: u64, - pub publish_max_backoff_millis: u64, - pub job_status_retention: usize, - pub retained_jobs: usize, - pub retained_idempotency_keys: usize, - pub accepted_jobs: usize, - pub published_jobs: usize, - pub failed_jobs: usize, - pub recovered_failed_jobs: usize, - pub methods: Vec<String>, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdBridgeJobView { - job: SdkRadrootsdBridgeJobRef, - pub command: String, - pub idempotency_key: Option<String>, - pub status: radrootsd::SdkRadrootsdBridgeJobStatus, - pub terminal: bool, - pub recovered_after_restart: bool, - pub requested_at_unix: u64, - pub completed_at_unix: Option<u64>, - pub signer_mode: String, - pub signer_session_id: Option<String>, - pub event_kind: u32, - pub event_id: Option<String>, - pub event_addr: Option<String>, - pub delivery_policy: radrootsd::SdkRadrootsdBridgeDeliveryPolicy, - pub delivery_quorum: Option<usize>, - pub relay_count: usize, - pub acknowledged_relay_count: usize, - pub required_acknowledged_relay_count: usize, - pub attempt_count: usize, - pub attempt_summaries: Vec<String>, - pub relay_results: Vec<radrootsd::SdkRadrootsdBridgeRelayPublishResult>, - pub relay_outcome_summary: String, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdBridgeJobView { - pub fn job(&self) -> &SdkRadrootsdBridgeJobRef { - &self.job - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdBridgeStatusResponse> for SdkRadrootsdBridgeStatus { - fn from(value: radrootsd::SdkRadrootsdBridgeStatusResponse) -> Self { - Self { - enabled: value.enabled, - ready: value.ready, - auth_mode: value.auth_mode, - signer_mode: value.signer_mode, - default_signer_mode: value.default_signer_mode, - supported_signer_modes: value.supported_signer_modes, - available_nip46_signer_sessions: value.available_nip46_signer_sessions, - relay_count: value.relay_count, - delivery_policy: value.delivery_policy, - delivery_quorum: value.delivery_quorum, - publish_max_attempts: value.publish_max_attempts, - publish_initial_backoff_millis: value.publish_initial_backoff_millis, - publish_max_backoff_millis: value.publish_max_backoff_millis, - job_status_retention: value.job_status_retention, - retained_jobs: value.retained_jobs, - retained_idempotency_keys: value.retained_idempotency_keys, - accepted_jobs: value.accepted_jobs, - published_jobs: value.published_jobs, - failed_jobs: value.failed_jobs, - recovered_failed_jobs: value.recovered_failed_jobs, - methods: value.methods, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdBridgeJobView> for SdkRadrootsdBridgeJobView { - fn from(value: radrootsd::SdkRadrootsdBridgeJobView) -> Self { - Self { - job: SdkRadrootsdBridgeJobRef::new(value.job_id), - command: value.command, - idempotency_key: value.idempotency_key, - status: value.status, - terminal: value.terminal, - recovered_after_restart: value.recovered_after_restart, - requested_at_unix: value.requested_at_unix, - completed_at_unix: value.completed_at_unix, - signer_mode: value.signer_mode, - signer_session_id: value.signer_session_id, - event_kind: value.event_kind, - event_id: value.event_id, - event_addr: value.event_addr, - delivery_policy: value.delivery_policy, - delivery_quorum: value.delivery_quorum, - relay_count: value.relay_count, - acknowledged_relay_count: value.acknowledged_relay_count, - required_acknowledged_relay_count: value.required_acknowledged_relay_count, - attempt_count: value.attempt_count, - attempt_summaries: value.attempt_summaries, - relay_results: value.relay_results, - relay_outcome_summary: value.relay_outcome_summary, - } - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionHandle { - session: SdkRadrootsdSignerSessionRef, - mode: radrootsd::SdkRadrootsdSignerSessionMode, - remote_signer_pubkey: String, - client_pubkey: String, - relays: Vec<String>, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionView { - session: SdkRadrootsdSignerSessionRef, - pub role: radrootsd::SdkRadrootsdSignerSessionRole, - pub client_pubkey: String, - pub signer_pubkey: String, - pub user_pubkey: Option<String>, - pub relays: Vec<String>, - pub permissions: Vec<String>, - pub name: Option<String>, - pub url: Option<String>, - pub image: Option<String>, - pub auth_required: bool, - pub authorized: bool, - pub auth_url: Option<String>, - pub expires_in_secs: Option<u64>, - pub signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdSignerSessionView { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdSignerSessionView"); - debug.field("session", &self.session); - debug.field("role", &self.role); - debug.field("client_pubkey", &self.client_pubkey); - debug.field("signer_pubkey", &self.signer_pubkey); - debug.field("user_pubkey", &self.user_pubkey); - debug.field("relays", &self.relays); - debug.field("permissions", &self.permissions); - debug.field("name", &self.name); - debug.field("url", &self.url); - debug.field("image", &self.image); - debug.field("auth_required", &self.auth_required); - debug.field("authorized", &self.authorized); - debug.field("auth_url", &self.auth_url); - debug.field("expires_in_secs", &self.expires_in_secs); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdSignerSessionView { - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionAuthorizeResult { - pub authorized: bool, - pub replayed: bool, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionPublicKeyResult { - pub pubkey: String, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionRequireAuthResult { - pub required: bool, -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SdkRadrootsdSignerSessionCloseResult { - pub closed: bool, -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionViewResponse> for SdkRadrootsdSignerSessionView { - fn from(value: radrootsd::SdkRadrootsdSignerSessionViewResponse) -> Self { - Self { - session: SdkRadrootsdSignerSessionRef { - session_id: value.session_id, - }, - role: value.role, - client_pubkey: value.client_pubkey, - signer_pubkey: value.signer_pubkey, - user_pubkey: value.user_pubkey, - relays: value.relays, - permissions: value.permissions, - name: value.name, - url: value.url, - image: value.image, - auth_required: value.auth_required, - authorized: value.authorized, - auth_url: value.auth_url, - expires_in_secs: value.expires_in_secs, - signer_authority: value.signer_authority, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionAuthorizeResponse> - for SdkRadrootsdSignerSessionAuthorizeResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionAuthorizeResponse) -> Self { - Self { - authorized: value.authorized, - replayed: value.replayed, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionPublicKeyResponse> - for SdkRadrootsdSignerSessionPublicKeyResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionPublicKeyResponse) -> Self { - Self { - pubkey: value.pubkey, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionRequireAuthResponse> - for SdkRadrootsdSignerSessionRequireAuthResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionRequireAuthResponse) -> Self { - Self { - required: value.required, - } - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionCloseResponse> - for SdkRadrootsdSignerSessionCloseResult -{ - fn from(value: radrootsd::SdkRadrootsdSignerSessionCloseResponse) -> Self { - Self { - closed: value.closed, - } - } -} - -#[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", &self.session); - 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 session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - 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() - } -} - -#[cfg(feature = "radrootsd-client")] -impl From<radrootsd::SdkRadrootsdSignerSessionConnectResponse> for SdkRadrootsdSignerSessionHandle { - fn from(value: radrootsd::SdkRadrootsdSignerSessionConnectResponse) -> Self { - Self { - session: SdkRadrootsdSignerSessionRef { - 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 SdkRadrootsdProfilePublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdProfilePublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdProfilePublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdProfilePublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdFarmPublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdFarmPublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdFarmPublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdFarmPublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdListingPublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdListingPublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdListingPublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdListingPublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Clone, PartialEq, Eq)] -pub struct SdkRadrootsdOrderRequestPublishOptions { - session: SdkRadrootsdSignerSessionRef, - idempotency_key: Option<String>, - signer_authority: Option<radrootsd::SdkRadrootsdSignerAuthority>, -} - -#[cfg(feature = "radrootsd-client")] -impl SdkRadrootsdOrderRequestPublishOptions { - pub fn from_signer_session(session: &SdkRadrootsdSignerSessionHandle) -> Self { - Self { - session: session.session().clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn from_signer_session_ref(session: &SdkRadrootsdSignerSessionRef) -> Self { - Self { - session: session.clone(), - idempotency_key: None, - signer_authority: None, - } - } - - pub fn with_idempotency_key(mut self, idempotency_key: impl Into<String>) -> Self { - self.idempotency_key = Some(idempotency_key.into()); - self - } - - pub fn with_signer_authority( - mut self, - signer_authority: radrootsd::SdkRadrootsdSignerAuthority, - ) -> Self { - self.signer_authority = Some(signer_authority); - self - } - - pub fn session(&self) -> &SdkRadrootsdSignerSessionRef { - &self.session - } - - pub fn idempotency_key(&self) -> Option<&str> { - self.idempotency_key.as_deref() - } - - pub fn signer_authority(&self) -> Option<&radrootsd::SdkRadrootsdSignerAuthority> { - self.signer_authority.as_ref() - } -} - -#[cfg(feature = "radrootsd-client")] -impl fmt::Debug for SdkRadrootsdOrderRequestPublishOptions { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut debug = f.debug_struct("SdkRadrootsdOrderRequestPublishOptions"); - debug.field("session", &self.session); - debug.field("idempotency_key", &self.idempotency_key); - debug.field("signer_authority", &self.signer_authority); - debug.finish() - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsSdkClient { - config: RadrootsSdkConfig, - resolved_transport_target: SdkResolvedTransportTarget, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkResolvedTransportTarget { - RelayDirect { relay_urls: Vec<String> }, - Radrootsd { endpoint: String }, -} - -impl RadrootsSdkClient { - pub fn from_config(config: RadrootsSdkConfig) -> Result<Self, SdkConfigError> { - let resolved_transport_target = match config.transport { - SdkTransportMode::RelayDirect => SdkResolvedTransportTarget::RelayDirect { - relay_urls: config.resolved_relay_urls()?, - }, - SdkTransportMode::Radrootsd => SdkResolvedTransportTarget::Radrootsd { - endpoint: config.resolved_radrootsd_endpoint()?, - }, - }; - Ok(Self { - config, - resolved_transport_target, - }) - } - - pub fn config(&self) -> &RadrootsSdkConfig { - &self.config - } - - pub fn transport(&self) -> SdkTransportMode { - self.config.transport - } - - pub fn signer(&self) -> SignerConfig { - self.config.signer - } - - pub fn resolved_transport_target(&self) -> &SdkResolvedTransportTarget { - &self.resolved_transport_target - } - - pub fn profile(&self) -> ProfileClient<'_> { - ProfileClient { client: self } - } - - pub fn farm(&self) -> FarmClient<'_> { - FarmClient { client: self } - } - - pub fn listing(&self) -> ListingClient<'_> { - ListingClient { client: self } - } - - pub fn order(&self) -> TradeClient<'_> { - TradeClient { client: self } - } - - #[cfg(feature = "radrootsd-client")] - pub fn radrootsd(&self) -> RadrootsdClient<'_> { - RadrootsdClient { client: self } - } - - #[cfg(any( - feature = "radrootsd-client", - all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ) - ))] - fn require_signer_mode( - &self, - required: SignerConfig, - operation: &'static str, - ) -> Result<(), SdkPublishError> { - let signer = self.signer(); - if signer == required { - return Ok(()); - } - Err(SdkPublishError::UnsupportedSignerMode { - transport: self.transport(), - signer, - required, - operation, - }) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - async fn publish_parts_via_relay_with_identity( - &self, - identity: &RadrootsIdentity, - parts: WireEventParts, - operation: &'static str, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::RelayDirect { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation, - }); - } - self.require_signer_mode(SignerConfig::LocalIdentity, operation)?; - - let relay_urls = match &self.resolved_transport_target { - SdkResolvedTransportTarget::RelayDirect { relay_urls } => relay_urls.clone(), - SdkResolvedTransportTarget::Radrootsd { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation, - }); - } - }; - let client = relay::connected_client_from_identity( - identity, - &relay_urls, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation, - target_relays: relay_urls.clone(), - error: err.to_string(), - })?; - let connected_relays = relay::connected_relay_urls(&client).await; - if connected_relays.is_empty() { - return Err(SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation, - target_relays: relay_urls, - error: "no relay connection was established".to_owned(), - }); - } - let signed_event = signing::sign_parts_with_identity(identity, parts) - .map_err(|err| SdkPublishError::Relay(err.to_string()))?; - let output = relay::publish_signed_event(&client, &signed_event) - .await - .map_err(|err| SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation, - target_relays: relay_urls.clone(), - error: err.to_string(), - })?; - sdk_publish_receipt_from_relay_output(signed_event, relay_urls, connected_relays, output) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_listing_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdListingPublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "listing.publish_via_radrootsd", - }); - } - self.require_signer_mode(SignerConfig::Nip46, "listing.publish_via_radrootsd")?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "listing.publish_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_listing( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_profile_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdProfilePublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "profile.publish_via_radrootsd", - }); - } - self.require_signer_mode(SignerConfig::Nip46, "profile.publish_via_radrootsd")?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "profile.publish_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_profile( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_farm_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdFarmPublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "farm.publish_via_radrootsd", - }); - } - self.require_signer_mode(SignerConfig::Nip46, "farm.publish_via_radrootsd")?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "farm.publish_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_farm( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(response)) - } - - #[cfg(feature = "radrootsd-client")] - async fn publish_order_request_via_radrootsd( - &self, - request: &radrootsd::SdkRadrootsdOrderRequestPublishRequest, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "order.publish_order_request_via_radrootsd", - }); - } - self.require_signer_mode( - SignerConfig::Nip46, - "order.publish_order_request_via_radrootsd", - )?; - - let endpoint = match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => endpoint.as_str(), - SdkResolvedTransportTarget::RelayDirect { .. } => { - return Err(SdkPublishError::UnsupportedTransport { - transport: self.transport(), - operation: "order.publish_order_request_via_radrootsd", - }); - } - }; - let response = radrootsd::publish_order_request( - endpoint, - &self.config.radrootsd.auth, - request, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkPublishError::Radrootsd(err.to_string()))?; - Ok(sdk_publish_receipt_from_radrootsd_bridge_response(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 = self.require_radrootsd_endpoint("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")] - fn require_radrootsd_endpoint( - &self, - operation: &'static str, - ) -> Result<&str, SdkRadrootsdSessionError> { - match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => Ok(endpoint.as_str()), - SdkResolvedTransportTarget::RelayDirect { .. } => { - Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation, - }) - } - } - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_signer_session_status( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionView, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.status", - }); - } - - let response = radrootsd::signer_session_status( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.status")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_list_signer_sessions( - &self, - ) -> Result<Vec<SdkRadrootsdSignerSessionView>, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.list", - }); - } - - let response = radrootsd::list_signer_sessions( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.list")?, - &self.config.radrootsd.auth, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into_iter().map(Into::into).collect()) - } - - #[cfg(feature = "radrootsd-client")] - async fn authorize_radrootsd_signer_session( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.authorize", - }); - } - - let response = radrootsd::authorize_signer_session( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.authorize")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn get_radrootsd_signer_session_public_key( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionPublicKeyResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.get_public_key", - }); - } - - let response = radrootsd::get_signer_session_public_key( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.get_public_key")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn require_radrootsd_signer_session_auth( - &self, - session: &SdkRadrootsdSignerSessionRef, - auth_url: &str, - ) -> Result<SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.require_auth", - }); - } - - let response = radrootsd::require_signer_session_auth( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.require_auth")?, - &self.config.radrootsd.auth, - session.session_id(), - auth_url, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn close_radrootsd_signer_session( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSessionError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdSessionError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.signer_sessions.close", - }); - } - - let response = radrootsd::close_signer_session( - self.require_radrootsd_endpoint("radrootsd.signer_sessions.close")?, - &self.config.radrootsd.auth, - session.session_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdSessionError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - fn require_radrootsd_bridge_endpoint( - &self, - operation: &'static str, - ) -> Result<&str, SdkRadrootsdBridgeError> { - match &self.resolved_transport_target { - SdkResolvedTransportTarget::Radrootsd { endpoint } => Ok(endpoint.as_str()), - SdkResolvedTransportTarget::RelayDirect { .. } => { - Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation, - }) - } - } - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_bridge_status( - &self, - ) -> Result<SdkRadrootsdBridgeStatus, SdkRadrootsdBridgeError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.bridge.status", - }); - } - - let response = radrootsd::bridge_status( - self.require_radrootsd_bridge_endpoint("radrootsd.bridge.status")?, - &self.config.radrootsd.auth, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_bridge_job_status( - &self, - job: &SdkRadrootsdBridgeJobRef, - ) -> Result<SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.bridge.job", - }); - } - - let response = radrootsd::bridge_job_status( - self.require_radrootsd_bridge_endpoint("radrootsd.bridge.job")?, - &self.config.radrootsd.auth, - job.job_id(), - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; - Ok(response.into()) - } - - #[cfg(feature = "radrootsd-client")] - async fn radrootsd_bridge_jobs( - &self, - ) -> Result<Vec<SdkRadrootsdBridgeJobView>, SdkRadrootsdBridgeError> { - if self.transport() != SdkTransportMode::Radrootsd { - return Err(SdkRadrootsdBridgeError::UnsupportedTransport { - transport: self.transport(), - operation: "radrootsd.bridge.jobs", - }); - } - - let response = radrootsd::list_bridge_jobs( - self.require_radrootsd_bridge_endpoint("radrootsd.bridge.jobs")?, - &self.config.radrootsd.auth, - Duration::from_millis(self.config.network.timeout_ms), - ) - .await - .map_err(|err| SdkRadrootsdBridgeError::Radrootsd(err.to_string()))?; - Ok(response.into_iter().map(Into::into).collect()) - } -} - -#[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, - } - } - - pub fn bridge(&self) -> RadrootsdBridgeClient<'a> { - RadrootsdBridgeClient { - 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 - } - - pub async fn status( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionView, SdkRadrootsdSessionError> { - self.client.radrootsd_signer_session_status(session).await - } - - pub async fn list( - &self, - ) -> Result<Vec<SdkRadrootsdSignerSessionView>, SdkRadrootsdSessionError> { - self.client.radrootsd_list_signer_sessions().await - } - - pub async fn authorize( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionAuthorizeResult, SdkRadrootsdSessionError> { - self.client - .authorize_radrootsd_signer_session(session) - .await - } - - pub async fn get_public_key( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionPublicKeyResult, SdkRadrootsdSessionError> { - self.client - .get_radrootsd_signer_session_public_key(session) - .await - } - - pub async fn require_auth( - &self, - session: &SdkRadrootsdSignerSessionRef, - auth_url: impl AsRef<str>, - ) -> Result<SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSessionError> { - self.client - .require_radrootsd_signer_session_auth(session, auth_url.as_ref()) - .await - } - - pub async fn close( - &self, - session: &SdkRadrootsdSignerSessionRef, - ) -> Result<SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSessionError> { - self.client.close_radrootsd_signer_session(session).await - } -} - -#[cfg(feature = "radrootsd-client")] -#[derive(Debug, Clone, Copy)] -pub struct RadrootsdBridgeClient<'a> { - client: &'a RadrootsSdkClient, -} - -#[cfg(feature = "radrootsd-client")] -impl<'a> RadrootsdBridgeClient<'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 status(&self) -> Result<SdkRadrootsdBridgeStatus, SdkRadrootsdBridgeError> { - self.client.radrootsd_bridge_status().await - } - - pub async fn job( - &self, - job: &SdkRadrootsdBridgeJobRef, - ) -> Result<SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeError> { - self.client.radrootsd_bridge_job_status(job).await - } - - pub async fn jobs(&self) -> Result<Vec<SdkRadrootsdBridgeJobView>, SdkRadrootsdBridgeError> { - self.client.radrootsd_bridge_jobs().await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ProfileClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> ProfileClient<'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() - } - - #[cfg(feature = "serde_json")] - pub fn build_draft( - &self, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - ) -> Result<WireEventParts, profile::ProfileEncodeError> { - profile::build_draft(profile_value, profile_type) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_with_identity( - &self, - identity: &RadrootsIdentity, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = profile::build_draft(profile_value, profile_type) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity(identity, parts, "profile.publish_with_identity") - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: WireEventParts, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft, - "profile.publish_draft_with_identity", - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_profile_via_radrootsd( - &self, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_profile_via_radrootsd_with_options( - profile_value, - profile_type, - &SdkRadrootsdProfilePublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_profile_via_radrootsd_with_options( - &self, - profile_value: &RadrootsProfile, - profile_type: Option<RadrootsProfileType>, - options: &SdkRadrootsdProfilePublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdProfilePublishRequest { - profile: profile_value.clone(), - profile_type, - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client.publish_profile_via_radrootsd(&request).await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct FarmClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> FarmClient<'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() - } - - #[cfg(feature = "serde_json")] - pub fn build_draft( - &self, - farm_value: &farm::RadrootsFarm, - ) -> Result<WireEventParts, farm::EventEncodeError> { - farm::build_draft(farm_value) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_with_identity( - &self, - identity: &RadrootsIdentity, - farm_value: &farm::RadrootsFarm, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = farm::build_draft(farm_value) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client - .publish_parts_via_relay_with_identity(identity, parts, "farm.publish_with_identity") - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: WireEventParts, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft, - "farm.publish_draft_with_identity", - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_farm_via_radrootsd( - &self, - farm_value: &farm::RadrootsFarm, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_farm_via_radrootsd_with_options( - farm_value, - &SdkRadrootsdFarmPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_farm_via_radrootsd_with_options( - &self, - farm_value: &farm::RadrootsFarm, - options: &SdkRadrootsdFarmPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdFarmPublishRequest { - farm: farm_value.clone(), - kind: Some(KIND_FARM), - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client.publish_farm_via_radrootsd(&request).await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct ListingClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> ListingClient<'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 build_tags( - &self, - listing_value: &listing::RadrootsListing, - ) -> Result<NostrTags, listing::EventEncodeError> { - listing::build_tags(listing_value) - } - - #[cfg(feature = "serde_json")] - pub fn build_draft( - &self, - listing_value: &listing::RadrootsListing, - ) -> Result<listing::RadrootsListingDraft, listing::EventEncodeError> { - listing::build_draft(listing_value) - } - - #[cfg(feature = "serde_json")] - pub fn parse_event( - &self, - event: &RadrootsNostrEvent, - ) -> Result<listing::RadrootsListing, listing::RadrootsListingParseError> { - listing::parse_event(event) - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_with_identity( - &self, - identity: &RadrootsIdentity, - listing_value: &listing::RadrootsListing, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = listing::build_draft(listing_value) - .map_err(|err| SdkPublishError::Encode(err.to_string()))? - .into_wire_parts(); - self.client - .publish_parts_via_relay_with_identity(identity, parts, "listing.publish_with_identity") - .await - } - - #[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" - ))] - pub async fn publish_draft_with_identity( - &self, - identity: &RadrootsIdentity, - draft: listing::RadrootsListingDraft, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.client - .publish_parts_via_relay_with_identity( - identity, - draft.into_wire_parts(), - "listing.publish_draft_with_identity", - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_listing_via_radrootsd( - &self, - listing_value: &listing::RadrootsListing, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_listing_via_radrootsd_with_options( - listing_value, - &SdkRadrootsdListingPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_listing_via_radrootsd_with_options( - &self, - listing_value: &listing::RadrootsListing, - options: &SdkRadrootsdListingPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdListingPublishRequest { - listing: listing_value.clone(), - kind: Some(KIND_LISTING), - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client.publish_listing_via_radrootsd(&request).await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_draft_via_radrootsd( - &self, - draft: listing::RadrootsListingDraft, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_draft_via_radrootsd_with_options( - draft, - &SdkRadrootsdListingPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_draft_via_radrootsd_with_options( - &self, - draft: listing::RadrootsListingDraft, - options: &SdkRadrootsdListingPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let parts = draft.into_wire_parts(); - let event = RadrootsNostrEvent { - id: String::new(), - author: String::new(), - created_at: 0, - kind: parts.kind, - tags: parts.tags, - content: parts.content, - sig: String::new(), - }; - let request = radrootsd::SdkRadrootsdListingPublishRequest::from_event( - &event, - options.session().session_id().to_owned(), - options.signer_authority().cloned(), - options.idempotency_key().map(str::to_owned), - ) - .map_err(|err| SdkPublishError::Encode(err.to_string()))?; - self.client.publish_listing_via_radrootsd(&request).await - } -} - -#[derive(Debug, Clone, Copy)] -pub struct TradeClient<'a> { - client: &'a RadrootsSdkClient, -} - -impl<'a> TradeClient<'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() - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_order_request_via_radrootsd( - &self, - order: &order::RadrootsOrderRequest, - listing_event: &RadrootsNostrEventPtr, - session: &SdkRadrootsdSignerSessionHandle, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - self.publish_order_request_via_radrootsd_with_options( - order, - listing_event, - &SdkRadrootsdOrderRequestPublishOptions::from_signer_session(session), - ) - .await - } - - #[cfg(feature = "radrootsd-client")] - pub async fn publish_order_request_via_radrootsd_with_options( - &self, - order: &order::RadrootsOrderRequest, - listing_event: &RadrootsNostrEventPtr, - options: &SdkRadrootsdOrderRequestPublishOptions, - ) -> Result<SdkPublishReceipt, SdkPublishError> { - let request = radrootsd::SdkRadrootsdOrderRequestPublishRequest { - order: order.clone(), - listing_event: listing_event.clone(), - signer_session_id: options.session().session_id().to_owned(), - signer_authority: options.signer_authority().cloned(), - idempotency_key: options.idempotency_key().map(str::to_owned), - }; - self.client - .publish_order_request_via_radrootsd(&request) - .await - } -} - -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -fn sdk_publish_receipt_from_relay_output( - signed_event: signing::SignedNostrEvent, - target_relays: Vec<String>, - connected_relays: Vec<String>, - output: relay::RelayOutput<relay::RelayEventId>, -) -> Result<SdkPublishReceipt, SdkPublishError> { - let event = sdk_event_from_signed_event(&signed_event); - let event_id = event.id.clone(); - let event_kind = event.kind; - let created_at = event.created_at; - let signature = event.sig.clone(); - let target_relays = sorted_unique_strings(target_relays); - let connected_relays = sorted_unique_strings(connected_relays); - let mut acknowledged_relays = output - .success - .into_iter() - .map(|relay| relay.to_string()) - .collect::<Vec<_>>(); - acknowledged_relays = sorted_unique_strings(acknowledged_relays); - - let mut failed_relays = output - .failed - .into_iter() - .map(|(relay_url, error)| SdkRelayFailure { - relay_url: relay_url.to_string(), - error, - }) - .collect::<Vec<_>>(); - failed_relays.sort_by(|left, right| left.relay_url.cmp(&right.relay_url)); - - if acknowledged_relays.is_empty() { - return Err(SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays, - }); - } - - Ok(SdkPublishReceipt { - transport: SdkTransportMode::RelayDirect, - event_kind: Some(event_kind), - event_id: Some(event_id.clone()), - transport_receipt: SdkTransportReceipt::RelayDirect(SdkRelayPublishReceipt { - event, - event_id, - event_kind, - created_at, - signature, - target_relays, - connected_relays, - acknowledged_relays, - failed_relays, - }), - }) -} - -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -fn sdk_event_from_signed_event(event: &signing::SignedNostrEvent) -> RadrootsNostrEvent { - RadrootsNostrEvent { - id: event.id.to_string(), - author: event.pubkey.to_string(), - created_at: u32::try_from(event.created_at.as_secs()).unwrap_or(u32::MAX), - kind: event.kind.as_u16() as u32, - tags: event - .tags - .iter() - .map(|tag| tag.as_slice().to_vec()) - .collect(), - content: event.content.clone(), - sig: event.sig.to_string(), - } -} - -#[cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -fn sorted_unique_strings(mut values: Vec<String>) -> Vec<String> { - values.sort(); - values.dedup(); - values -} - -#[cfg(feature = "radrootsd-client")] -fn sdk_publish_receipt_from_radrootsd_bridge_response( - response: radrootsd::SdkRadrootsdBridgePublishResponse, -) -> SdkPublishReceipt { - let job = response.job; - SdkPublishReceipt { - transport: SdkTransportMode::Radrootsd, - event_kind: Some(job.event_kind), - event_id: job.event_id.clone(), - transport_receipt: SdkTransportReceipt::Radrootsd(SdkRadrootsdPublishReceipt { - accepted: true, - deduplicated: response.deduplicated, - job_id: Some(job.job_id), - status: Some(job.status), - signer_mode: Some(job.signer_mode), - signer_session_id: job.signer_session_id, - event_addr: job.event_addr, - relay_count: Some(job.relay_count), - acknowledged_relay_count: Some(job.acknowledged_relay_count), - }), - } -} - -#[cfg(all( - test, - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] -mod tests { - use super::{ - SdkPublishError, SdkRelayFailure, SdkTransportMode, sdk_publish_receipt_from_relay_output, - }; - use crate::adapters::relay::RelayOutput; - use crate::adapters::signing::sign_parts_with_identity; - use crate::identity::RadrootsIdentity; - use radroots_events_codec::wire::WireEventParts; - use radroots_nostr::prelude::RadrootsNostrEventId; - use std::collections::{HashMap, HashSet}; - - #[test] - fn relay_output_maps_to_normalized_publish_receipt() { - let identity = RadrootsIdentity::generate(); - let signed_event = sign_parts_with_identity( - &identity, - WireEventParts { - kind: 30402, - content: "listing".to_owned(), - tags: vec![vec!["d".to_owned(), "AAAAAAAAAAAAAAAAAAAAAg".to_owned()]], - }, - ) - .expect("signed event"); - let event_id = signed_event.id.to_string(); - let event_created_at = u32::try_from(signed_event.created_at.as_secs()).unwrap(); - let event_signature = signed_event.sig.to_string(); - let output = RelayOutput { - val: RadrootsNostrEventId::parse(event_id.as_str()).expect("event id"), - success: HashSet::from([ - nostr::RelayUrl::parse("ws://127.0.0.1:8080").expect("relay a"), - nostr::RelayUrl::parse("ws://127.0.0.1:8081").expect("relay b"), - ]), - failed: HashMap::from([( - nostr::RelayUrl::parse("ws://127.0.0.1:8082").expect("relay c"), - "timeout".to_owned(), - )]), - }; - - let receipt = sdk_publish_receipt_from_relay_output( - signed_event, - vec![ - "ws://127.0.0.1:8081".to_owned(), - "ws://127.0.0.1:8080".to_owned(), - ], - vec!["ws://127.0.0.1:8080".to_owned()], - output, - ) - .expect("receipt"); - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(30402)); - assert_eq!(receipt.event_id, Some(event_id.clone())); - let relay_receipt = match receipt.transport_receipt { - super::SdkTransportReceipt::RelayDirect(relay_receipt) => relay_receipt, - super::SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - }; - assert_eq!(relay_receipt.event.id, event_id); - assert_eq!(relay_receipt.event_id, relay_receipt.event.id); - assert_eq!(relay_receipt.event_kind, 30402); - assert_eq!(relay_receipt.created_at, event_created_at); - assert_eq!(relay_receipt.signature, event_signature); - assert_eq!( - relay_receipt.target_relays, - vec![ - "ws://127.0.0.1:8080".to_owned(), - "ws://127.0.0.1:8081".to_owned(), - ] - ); - assert_eq!( - relay_receipt.connected_relays, - vec!["ws://127.0.0.1:8080".to_owned()] - ); - } - - #[test] - fn relay_output_without_acknowledgement_is_rejected() { - let identity = RadrootsIdentity::generate(); - let signed_event = sign_parts_with_identity( - &identity, - WireEventParts { - kind: 30402, - content: "listing".to_owned(), - tags: vec![], - }, - ) - .expect("signed event"); - let output = RelayOutput { - val: RadrootsNostrEventId::parse(signed_event.id.to_string().as_str()) - .expect("event id"), - success: HashSet::new(), - failed: HashMap::from([( - nostr::RelayUrl::parse("ws://127.0.0.1:8082").expect("relay c"), - "blocked".to_owned(), - )]), - }; - - let error = sdk_publish_receipt_from_relay_output(signed_event, vec![], vec![], output) - .expect_err("error"); - - assert_eq!( - error, - SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays: vec![SdkRelayFailure { - relay_url: "ws://127.0.0.1:8082".to_owned(), - error: "blocked".to_owned(), - }], - } - ); - } -} diff --git a/crates/sdk/src/config.rs b/crates/sdk/src/config.rs @@ -1,388 +0,0 @@ -#[cfg(not(feature = "std"))] -use alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; -use core::fmt; -#[cfg(feature = "std")] -use std::{env, string::String, vec::Vec}; - -pub const RADROOTS_SDK_PRODUCTION_RELAY_URL: &str = "wss://radroots.org"; -pub const RADROOTS_SDK_STAGING_RELAY_URL: &str = "wss://staging.radroots.org"; -pub const RADROOTS_SDK_LOCAL_RELAY_URL: &str = "ws://127.0.0.1:8080"; - -pub const RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT: &str = "https://rpc.radroots.org/jsonrpc"; -pub const RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT: &str = - "https://rpc.staging.radroots.org/jsonrpc"; -pub const RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT: &str = "http://127.0.0.1:7070"; - -pub const RADROOTS_SDK_DEFAULT_TIMEOUT_MS: u64 = 10_000; - -#[cfg(feature = "std")] -const LOCAL_RELAY_SCHEME_ENV: &str = "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_SCHEME"; -#[cfg(feature = "std")] -const LOCAL_RELAY_HOST_ENV: &str = "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST"; -#[cfg(feature = "std")] -const LOCAL_RELAY_PORT_ENV: &str = "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_PORT"; -#[cfg(feature = "std")] -const LOCAL_RADROOTSD_ENDPOINT_ENV: &str = "RADROOTSD_RPC_URL"; -#[cfg(feature = "std")] -const LOCAL_RADROOTSD_HOST_ENV: &str = "RADROOTSD_RPC_HOST"; -#[cfg(feature = "std")] -const LOCAL_RADROOTSD_PORT_ENV: &str = "RADROOTSD_RPC_PORT"; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsSdkConfig { - pub environment: SdkEnvironment, - pub transport: SdkTransportMode, - pub relay: RelayConfig, - pub radrootsd: RadrootsdConfig, - pub signer: SignerConfig, - pub network: NetworkConfig, -} - -impl RadrootsSdkConfig { - pub fn production() -> Self { - Self::for_environment(SdkEnvironment::Production) - } - - pub fn staging() -> Self { - Self::for_environment(SdkEnvironment::Staging) - } - - pub fn local() -> Self { - Self::for_environment(SdkEnvironment::Local) - } - - pub fn custom() -> Self { - Self::for_environment(SdkEnvironment::Custom) - } - - pub fn for_environment(environment: SdkEnvironment) -> Self { - Self { - environment, - transport: SdkTransportMode::RelayDirect, - relay: RelayConfig::default(), - radrootsd: RadrootsdConfig::default(), - signer: SignerConfig::default(), - network: NetworkConfig::default(), - } - } - - pub fn resolved_relay_urls(&self) -> Result<Vec<String>, SdkConfigError> { - self.relay.resolved_urls(self.environment) - } - - pub fn resolved_radrootsd_endpoint(&self) -> Result<String, SdkConfigError> { - self.radrootsd.resolved_endpoint(self.environment) - } -} - -impl Default for RadrootsSdkConfig { - fn default() -> Self { - Self::production() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SdkEnvironment { - Production, - Staging, - Local, - Custom, -} - -impl SdkEnvironment { - pub fn default_relay_urls(self) -> Option<Vec<String>> { - match self { - Self::Production => Some(vec![RADROOTS_SDK_PRODUCTION_RELAY_URL.to_owned()]), - Self::Staging => Some(vec![RADROOTS_SDK_STAGING_RELAY_URL.to_owned()]), - Self::Local => Some(vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()]), - Self::Custom => None, - } - } - - pub fn default_radrootsd_endpoint(self) -> Option<&'static str> { - match self { - Self::Production => Some(RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT), - Self::Staging => Some(RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT), - Self::Local => Some(RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT), - Self::Custom => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SdkTransportMode { - RelayDirect, - Radrootsd, -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct RelayConfig { - pub urls: Vec<String>, -} - -impl RelayConfig { - pub fn resolved_urls( - &self, - environment: SdkEnvironment, - ) -> Result<Vec<String>, SdkConfigError> { - if self.urls.is_empty() { - if environment == SdkEnvironment::Local { - #[cfg(feature = "std")] - if let Some(local_url) = resolve_local_relay_url_from_env() { - return Ok(vec![normalize_relay_url(local_url.as_str())?]); - } - } - return environment - .default_relay_urls() - .ok_or(SdkConfigError::MissingCustomRelayUrls); - } - - normalize_relay_urls(&self.urls) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RadrootsdConfig { - pub endpoint: Option<String>, - pub auth: RadrootsdAuth, -} - -impl RadrootsdConfig { - pub fn resolved_endpoint(&self, environment: SdkEnvironment) -> Result<String, SdkConfigError> { - match self.endpoint.as_deref() { - Some(endpoint) => normalize_radrootsd_endpoint(endpoint), - None => { - if environment == SdkEnvironment::Local { - #[cfg(feature = "std")] - if let Some(endpoint) = resolve_local_radrootsd_endpoint_from_env() { - return normalize_radrootsd_endpoint(endpoint.as_str()); - } - } - - environment - .default_radrootsd_endpoint() - .map(str::to_owned) - .ok_or(SdkConfigError::MissingCustomRadrootsdEndpoint) - } - } - } -} - -impl Default for RadrootsdConfig { - fn default() -> Self { - Self { - endpoint: None, - auth: RadrootsdAuth::default(), - } - } -} - -#[derive(Clone, PartialEq, Eq, Default)] -pub enum RadrootsdAuth { - #[default] - None, - BearerToken(String), -} - -impl fmt::Debug for RadrootsdAuth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::None => f.write_str("None"), - Self::BearerToken(_) => f.write_str("BearerToken(\"<redacted>\")"), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum SignerConfig { - #[default] - DraftOnly, - LocalIdentity, - Nip46, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NetworkConfig { - pub timeout_ms: u64, -} - -impl Default for NetworkConfig { - fn default() -> Self { - Self { - timeout_ms: RADROOTS_SDK_DEFAULT_TIMEOUT_MS, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SdkConfigError { - MissingCustomRelayUrls, - MissingCustomRadrootsdEndpoint, - EmptyRelayUrl, - InvalidRelayUrl(String), - EmptyRadrootsdEndpoint, - InvalidRadrootsdEndpoint(String), -} - -impl fmt::Display for SdkConfigError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::MissingCustomRelayUrls => { - f.write_str("custom sdk environment requires explicit relay urls") - } - Self::MissingCustomRadrootsdEndpoint => { - f.write_str("custom sdk environment requires an explicit radrootsd endpoint") - } - Self::EmptyRelayUrl => f.write_str("relay url must not be empty"), - Self::InvalidRelayUrl(value) => { - write!(f, "relay url must use ws or wss, got `{value}`") - } - Self::EmptyRadrootsdEndpoint => f.write_str("radrootsd endpoint must not be empty"), - Self::InvalidRadrootsdEndpoint(value) => { - write!( - f, - "radrootsd endpoint must use http or https, got `{value}`" - ) - } - } - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SdkConfigError {} - -impl fmt::Display for SignerConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DraftOnly => f.write_str("draft_only"), - Self::LocalIdentity => f.write_str("local_identity"), - Self::Nip46 => f.write_str("nip46"), - } - } -} - -fn normalize_relay_urls(values: &[String]) -> Result<Vec<String>, SdkConfigError> { - let mut normalized = Vec::new(); - for value in values { - let relay = normalize_relay_url(value.as_str())?; - if !normalized.iter().any(|existing| existing == &relay) { - normalized.push(relay); - } - } - Ok(normalized) -} - -fn normalize_relay_url(value: &str) -> Result<String, SdkConfigError> { - let trimmed = value.trim(); - if trimmed.is_empty() { - return Err(SdkConfigError::EmptyRelayUrl); - } - - let rest = if let Some(rest) = trimmed.strip_prefix("ws://") { - rest - } else if let Some(rest) = trimmed.strip_prefix("wss://") { - rest - } else { - return Err(SdkConfigError::InvalidRelayUrl(trimmed.to_owned())); - }; - - if relay_authority_is_invalid(rest) { - return Err(SdkConfigError::InvalidRelayUrl(trimmed.to_owned())); - } - - Ok(trimmed.to_owned()) -} - -fn relay_authority_is_invalid(rest: &str) -> bool { - let authority_end = rest - .char_indices() - .find(|(_, ch)| matches!(ch, '/' | '?' | '#')) - .map(|(index, _)| index) - .unwrap_or(rest.len()); - let authority = &rest[..authority_end]; - - if authority.is_empty() || authority.chars().any(char::is_whitespace) { - return true; - } - if authority.contains('@') { - return true; - } - - if let Some(after_open) = authority.strip_prefix('[') { - let Some(close_index) = after_open.find(']') else { - return true; - }; - let host = &after_open[..close_index]; - let after_host = &after_open[close_index + 1..]; - if host.is_empty() { - return true; - } - return relay_port_suffix_is_invalid(after_host); - } - - let colon_count = authority.bytes().filter(|byte| *byte == b':').count(); - match colon_count { - 0 => false, - 1 => { - let (host, port) = authority - .split_once(':') - .expect("one colon in relay authority"); - host.is_empty() || relay_port_is_invalid(port) - } - _ => true, - } -} - -fn relay_port_suffix_is_invalid(after_host: &str) -> bool { - if after_host.is_empty() { - return false; - } - let Some(port) = after_host.strip_prefix(':') else { - return true; - }; - relay_port_is_invalid(port) -} - -fn relay_port_is_invalid(port: &str) -> bool { - port.is_empty() || !port.bytes().all(|byte| byte.is_ascii_digit()) -} - -fn normalize_radrootsd_endpoint(value: &str) -> Result<String, SdkConfigError> { - let trimmed = value.trim(); - if trimmed.is_empty() { - return Err(SdkConfigError::EmptyRadrootsdEndpoint); - } - if !(trimmed.starts_with("http://") || trimmed.starts_with("https://")) { - return Err(SdkConfigError::InvalidRadrootsdEndpoint(trimmed.to_owned())); - } - Ok(trimmed.to_owned()) -} - -#[cfg(feature = "std")] -fn resolve_local_relay_url_from_env() -> Option<String> { - let scheme = read_trimmed_env(LOCAL_RELAY_SCHEME_ENV)?; - let host = read_trimmed_env(LOCAL_RELAY_HOST_ENV)?; - let port = read_trimmed_env(LOCAL_RELAY_PORT_ENV)?; - Some(format!("{scheme}://{host}:{port}")) -} - -#[cfg(feature = "std")] -fn resolve_local_radrootsd_endpoint_from_env() -> Option<String> { - if let Some(endpoint) = read_trimmed_env(LOCAL_RADROOTSD_ENDPOINT_ENV) { - return Some(endpoint); - } - - let host = read_trimmed_env(LOCAL_RADROOTSD_HOST_ENV)?; - let port = read_trimmed_env(LOCAL_RADROOTSD_PORT_ENV)?; - Some(format!("http://{host}:{port}")) -} - -#[cfg(feature = "std")] -fn read_trimmed_env(key: &str) -> Option<String> { - let value = env::var(key).ok()?; - let trimmed = value.trim(); - if trimmed.is_empty() { - return None; - } - Some(trimmed.to_owned()) -} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -13,8 +13,6 @@ mod actor_json; feature = "signer-adapters" ))] pub mod adapters; -pub mod client; -pub mod config; #[cfg(feature = "runtime")] mod error; mod farm; @@ -43,29 +41,6 @@ mod sync_runtime; #[cfg(feature = "runtime")] mod workflow_runtime; -pub use crate::client::{ - FarmClient, ListingClient, ProfileClient, RadrootsSdkClient, SdkPublishError, - SdkPublishReceipt, SdkRadrootsdPublishReceipt, SdkRelayFailure, SdkRelayPublishReceipt, - SdkResolvedTransportTarget, SdkTransportReceipt, -}; -#[cfg(feature = "radrootsd-client")] -pub use crate::client::{ - RadrootsdBridgeClient, RadrootsdClient, RadrootsdSignerSessionClient, SdkRadrootsdBridgeError, - SdkRadrootsdBridgeJobRef, SdkRadrootsdBridgeJobView, SdkRadrootsdBridgeStatus, - SdkRadrootsdFarmPublishOptions, SdkRadrootsdListingPublishOptions, - SdkRadrootsdOrderRequestPublishOptions, SdkRadrootsdProfilePublishOptions, - SdkRadrootsdSessionError, SdkRadrootsdSignerSessionAuthorizeResult, - SdkRadrootsdSignerSessionCloseResult, SdkRadrootsdSignerSessionHandle, - SdkRadrootsdSignerSessionPublicKeyResult, SdkRadrootsdSignerSessionRef, - SdkRadrootsdSignerSessionRequireAuthResult, SdkRadrootsdSignerSessionView, -}; -pub use crate::config::{ - NetworkConfig, RADROOTS_SDK_DEFAULT_TIMEOUT_MS, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, - RADROOTS_SDK_LOCAL_RELAY_URL, RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT, - RADROOTS_SDK_PRODUCTION_RELAY_URL, RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT, - RADROOTS_SDK_STAGING_RELAY_URL, RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, RelayConfig, - SdkConfigError, SdkEnvironment, SdkTransportMode, SignerConfig, -}; #[cfg(feature = "runtime")] pub use crate::error::{ RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkPartialLocalMutationError, diff --git a/crates/sdk/tests/client.rs b/crates/sdk/tests/client.rs @@ -1,383 +0,0 @@ -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::farm::{RadrootsFarm, RadrootsFarmRef}; -use radroots_events::kinds::{KIND_FARM, KIND_LISTING, KIND_PROFILE}; -use radroots_events::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, -}; -use radroots_events::profile::{RadrootsProfile, RadrootsProfileType}; -use radroots_sdk::client::{ - RadrootsSdkClient, SdkPublishError, SdkRadrootsdPublishReceipt, SdkRelayFailure, - SdkResolvedTransportTarget, -}; -use radroots_sdk::config::{ - RADROOTS_SDK_PRODUCTION_RELAY_URL, RadrootsSdkConfig, RelayConfig, SdkConfigError, - SdkEnvironment, SdkTransportMode, SignerConfig, -}; -use radroots_sdk::protocol::events::RadrootsNostrEvent; - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Organic coffee".into()), - website: None, - picture: None, - banner: None, - location: None, - tags: Some(vec!["coffee".into()]), - } -} - -fn sample_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "a".repeat(64), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("Single origin coffee".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice { - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(20u32), - RadrootsCoreCurrency::USD, - ), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - }, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(5u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "North Farm".into(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), - images: None, - } -} - -fn sample_profile() -> RadrootsProfile { - RadrootsProfile { - name: "north-farm".into(), - display_name: Some("North Farm".into()), - nip05: None, - about: Some("Farm profile".into()), - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - } -} - -#[test] -fn client_default_config_uses_production_relay_direct() { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::default()).expect("sdk client"); - - assert_eq!(client.transport(), SdkTransportMode::RelayDirect); - assert_eq!( - client.resolved_transport_target(), - &SdkResolvedTransportTarget::RelayDirect { - relay_urls: vec![RADROOTS_SDK_PRODUCTION_RELAY_URL.to_string()], - } - ); -} - -#[test] -fn client_rejects_invalid_config_on_construction() { - let mut config = RadrootsSdkConfig::custom(); - config.transport = SdkTransportMode::RelayDirect; - config.relay = RelayConfig { - urls: vec!["https://radroots.org".into()], - }; - - let error = RadrootsSdkClient::from_config(config).expect_err("invalid config"); - assert_eq!( - error, - SdkConfigError::InvalidRelayUrl("https://radroots.org".into()) - ); -} - -#[test] -fn client_rejects_invalid_radrootsd_config_on_construction() { - let mut missing = RadrootsSdkConfig::custom(); - missing.transport = SdkTransportMode::Radrootsd; - - assert_eq!( - RadrootsSdkClient::from_config(missing).expect_err("missing radrootsd endpoint"), - SdkConfigError::MissingCustomRadrootsdEndpoint - ); - - let mut invalid = RadrootsSdkConfig::custom(); - invalid.transport = SdkTransportMode::Radrootsd; - invalid.radrootsd.endpoint = Some("wss://rpc.bad".into()); - - assert_eq!( - RadrootsSdkClient::from_config(invalid).expect_err("invalid radrootsd endpoint"), - SdkConfigError::InvalidRadrootsdEndpoint("wss://rpc.bad".into()) - ); -} - -#[test] -fn client_allows_custom_relay_without_radrootsd_endpoint() { - let mut config = RadrootsSdkConfig::custom(); - config.transport = SdkTransportMode::RelayDirect; - config.relay = RelayConfig { - urls: vec!["wss://radroots.org".into()], - }; - - let client = RadrootsSdkClient::from_config(config).expect("relay-only sdk client"); - assert_eq!( - client.resolved_transport_target(), - &SdkResolvedTransportTarget::RelayDirect { - relay_urls: vec!["wss://radroots.org".to_string()], - } - ); -} - -#[test] -fn client_allows_custom_radrootsd_without_relay_urls() { - let endpoint = "https://custom.radroots.org/jsonrpc"; - let mut config = RadrootsSdkConfig::custom(); - config.transport = SdkTransportMode::Radrootsd; - config.radrootsd.endpoint = Some(endpoint.into()); - - let client = RadrootsSdkClient::from_config(config).expect("radrootsd-only sdk client"); - assert_eq!( - client.resolved_transport_target(), - &SdkResolvedTransportTarget::Radrootsd { - endpoint: endpoint.to_string(), - } - ); -} - -#[test] -fn namespace_clients_reflect_explicit_transport_mode() { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - - let client = RadrootsSdkClient::from_config(config).expect("sdk client"); - - assert_eq!(client.transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.profile().transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.farm().transport(), SdkTransportMode::Radrootsd); - assert_eq!(client.listing().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); - #[cfg(feature = "radrootsd-client")] - assert_eq!(client.radrootsd().signer(), SignerConfig::LocalIdentity); -} - -#[test] -fn namespace_clients_expose_parent_sdk_and_draft_facades() { - let client = - RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let profile = client.profile(); - let farm = client.farm(); - let listing = client.listing(); - - assert_eq!(client.config().environment, SdkEnvironment::Production); - assert!(std::ptr::eq(profile.sdk(), &client)); - assert!(std::ptr::eq(farm.sdk(), &client)); - assert!(std::ptr::eq(listing.sdk(), &client)); - - let profile_draft = profile - .build_draft(&sample_profile(), Some(RadrootsProfileType::Farm)) - .expect("profile draft"); - assert_eq!(profile_draft.kind, KIND_PROFILE); - - let farm_draft = farm.build_draft(&sample_farm()).expect("farm draft"); - assert_eq!(farm_draft.kind, KIND_FARM); - - let listing_draft = listing - .build_draft(&sample_listing()) - .expect("listing draft"); - assert_eq!(listing_draft.as_wire_parts().kind, KIND_LISTING); - assert_eq!(listing_draft.into_wire_parts().kind, KIND_LISTING); - - let mut invalid_listing = sample_listing(); - invalid_listing.farm.pubkey.clear(); - assert!(listing.build_draft(&invalid_listing).is_err()); -} - -#[test] -fn listing_client_wraps_existing_sdk_facade() { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::local()).expect("sdk client"); - let listing_value = sample_listing(); - - let tags = client - .listing() - .build_tags(&listing_value) - .expect("listing tags"); - assert!(!tags.is_empty()); - - let draft = client - .listing() - .build_draft(&listing_value) - .expect("listing draft"); - assert_eq!(draft.as_wire_parts().kind, KIND_LISTING); - - let event = RadrootsNostrEvent { - id: "listing-1".into(), - author: listing_value.farm.pubkey.clone(), - created_at: 1, - kind: draft.as_wire_parts().kind, - tags: draft.as_wire_parts().tags.clone(), - content: draft.as_wire_parts().content.clone(), - sig: String::new(), - }; - let parsed = client - .listing() - .parse_event(&event) - .expect("parsed listing"); - assert_eq!(parsed.d_tag, listing_value.d_tag); -} - -#[test] -fn publish_receipts_and_errors_format_public_details() { - let receipt = SdkRadrootsdPublishReceipt { - accepted: true, - deduplicated: true, - job_id: Some("job-1".into()), - status: Some("accepted".into()), - signer_mode: Some("secret-mode".into()), - signer_session_id: Some("secret-session".into()), - event_addr: Some("3432:pubkey:order-1".into()), - relay_count: Some(2), - acknowledged_relay_count: Some(1), - }; - let debug = format!("{receipt:?}"); - - assert!(debug.contains("SdkRadrootsdPublishReceipt")); - assert!(debug.contains("<redacted>")); - assert!(!debug.contains("secret-mode")); - assert!(!debug.contains("secret-session")); - - let relay_failure = SdkRelayFailure { - relay_url: "wss://relay.example".into(), - error: "closed".into(), - }; - let formatted = [ - SdkPublishError::from(SdkConfigError::EmptyRelayUrl).to_string(), - SdkPublishError::Encode("encode failed".into()).to_string(), - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "order.publish", - } - .to_string(), - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "order.publish", - } - .to_string(), - SdkPublishError::Relay("relay failed".into()).to_string(), - SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation: "order.publish", - target_relays: Vec::new(), - error: "setup failed".into(), - } - .to_string(), - SdkPublishError::RelaySetup { - transport: SdkTransportMode::RelayDirect, - operation: "order.publish", - target_relays: vec!["wss://relay.example".into()], - error: "setup failed".into(), - } - .to_string(), - SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays: Vec::new(), - } - .to_string(), - SdkPublishError::RelayNotAcknowledged { - transport: SdkTransportMode::RelayDirect, - failed_relays: vec![relay_failure], - } - .to_string(), - SdkPublishError::Radrootsd("radrootsd failed".into()).to_string(), - ]; - - assert!( - formatted - .iter() - .any(|message| message == "relay url must not be empty") - ); - assert!(formatted.iter().any(|message| message == "encode failed")); - assert!( - formatted - .iter() - .any(|message| message.contains("requires signer mode `local_identity`")) - ); - assert!(formatted.iter().any(|message| { - message.contains("failed to prepare RelayDirect relay publish for wss://relay.example") - })); - assert!( - formatted - .iter() - .any(|message| message.contains("wss://relay.example: closed")) - ); - assert!( - formatted - .iter() - .any(|message| message == "radrootsd failed") - ); -} - -#[test] -fn farm_client_wraps_existing_farm_facade() { - let client = - RadrootsSdkClient::from_config(RadrootsSdkConfig::production()).expect("sdk client"); - let farm = sample_farm(); - - let draft = client.farm().build_draft(&farm).expect("farm draft"); - assert!(!draft.tags.is_empty()); -} diff --git a/crates/sdk/tests/config.rs b/crates/sdk/tests/config.rs @@ -1,562 +0,0 @@ -use radroots_sdk::config::{ - NetworkConfig, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, RADROOTS_SDK_LOCAL_RELAY_URL, - RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT, RADROOTS_SDK_PRODUCTION_RELAY_URL, - RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT, RADROOTS_SDK_STAGING_RELAY_URL, RadrootsSdkConfig, - RadrootsdAuth, RelayConfig, SdkConfigError, SdkEnvironment, SdkTransportMode, SignerConfig, -}; -use std::{ - ffi::OsString, - sync::{Mutex, OnceLock}, -}; - -const LOCAL_SDK_ENV_KEYS: &[&str] = &[ - "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_SCHEME", - "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST", - "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_PORT", - "RADROOTSD_RPC_URL", - "RADROOTSD_RPC_HOST", - "RADROOTSD_RPC_PORT", -]; - -fn sdk_env_lock() -> &'static Mutex<()> { - static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) -} - -struct LocalSdkEnvRestore { - saved: Vec<(&'static str, Option<OsString>)>, -} - -impl LocalSdkEnvRestore { - fn apply(pairs: &[(&str, &str)]) -> Self { - let saved = LOCAL_SDK_ENV_KEYS - .iter() - .map(|key| (*key, std::env::var_os(key))) - .collect::<Vec<_>>(); - - for key in LOCAL_SDK_ENV_KEYS { - unsafe { - std::env::remove_var(key); - } - } - for (key, value) in pairs { - assert!( - LOCAL_SDK_ENV_KEYS.contains(key), - "unexpected local sdk env key `{key}`" - ); - unsafe { - std::env::set_var(key, value); - } - } - - Self { saved } - } -} - -impl Drop for LocalSdkEnvRestore { - fn drop(&mut self) { - for (key, original) in self.saved.drain(..) { - match original { - Some(value) => unsafe { - std::env::set_var(key, value); - }, - None => unsafe { - std::env::remove_var(key); - }, - } - } - } -} - -struct EnvKeyRestore { - key: &'static str, - saved: Option<OsString>, -} - -impl EnvKeyRestore { - fn capture(key: &'static str) -> Self { - Self { - key, - saved: std::env::var_os(key), - } - } -} - -impl Drop for EnvKeyRestore { - fn drop(&mut self) { - match &self.saved { - Some(value) => unsafe { - std::env::set_var(self.key, value); - }, - None => unsafe { - std::env::remove_var(self.key); - }, - } - } -} - -fn with_local_sdk_env<F>(pairs: &[(&str, &str)], test: F) -where - F: FnOnce(), -{ - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let _env_restore = LocalSdkEnvRestore::apply(pairs); - - test(); -} - -#[test] -fn local_sdk_env_restore_preserves_original_os_string_values() { - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let key = "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST"; - let _restore_key = EnvKeyRestore::capture(key); - let original = OsString::from("relay.before.example"); - - unsafe { - std::env::set_var(key, &original); - } - - { - let _env_restore = LocalSdkEnvRestore::apply(&[("RADROOTSD_RPC_PORT", "18080")]); - - assert_eq!(std::env::var_os(key), None); - } - - assert_eq!(std::env::var_os(key), Some(original)); -} - -#[test] -fn env_key_restore_restores_existing_value() { - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let key = "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST"; - let _restore_outer = EnvKeyRestore::capture(key); - let original = OsString::from("relay.before.example"); - let changed = OsString::from("relay.changed.example"); - - unsafe { - std::env::set_var(key, &original); - } - - { - let _restore_inner = EnvKeyRestore::capture(key); - - unsafe { - std::env::set_var(key, &changed); - } - } - - assert_eq!(std::env::var_os(key), Some(original)); -} - -#[cfg(unix)] -#[test] -fn local_sdk_env_restore_preserves_non_unicode_original_values() { - use std::os::unix::ffi::OsStringExt; - - let _guard = sdk_env_lock().lock().expect("sdk env lock"); - let key = "RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST"; - let _restore_key = EnvKeyRestore::capture(key); - let original = OsString::from_vec(vec![b'r', b'e', b'l', b'a', b'y', 0x80]); - - unsafe { - std::env::set_var(key, &original); - } - - { - let _env_restore = LocalSdkEnvRestore::apply(&[("RADROOTSD_RPC_PORT", "18080")]); - - assert_eq!(std::env::var_os(key), None); - } - - assert_eq!(std::env::var_os(key), Some(original)); -} - -#[test] -fn default_config_uses_production_relay_direct_draft_only() { - let config = RadrootsSdkConfig::default(); - - assert_eq!(config.environment, SdkEnvironment::Production); - assert_eq!(config.transport, SdkTransportMode::RelayDirect); - assert_eq!(config.signer, SignerConfig::DraftOnly); - assert_eq!(config.network, NetworkConfig::default()); - assert_eq!(config.radrootsd.auth, RadrootsdAuth::None); -} - -#[test] -fn production_environment_resolves_radroots_org_defaults() { - let config = RadrootsSdkConfig::production(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_PRODUCTION_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT - ); -} - -#[test] -fn staging_environment_resolves_staging_defaults() { - let config = RadrootsSdkConfig::staging(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_STAGING_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT - ); -} - -#[test] -fn local_environment_resolves_localhost_defaults() { - with_local_sdk_env(&[], || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT - ); - }); -} - -#[test] -fn local_environment_prefers_root_env_contract_when_present() { - with_local_sdk_env( - &[ - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_SCHEME", "ws"), - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST", "127.0.0.1"), - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_PORT", "18080"), - ("RADROOTSD_RPC_URL", "http://127.0.0.1:17070/jsonrpc"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec!["ws://127.0.0.1:18080".to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - "http://127.0.0.1:17070/jsonrpc" - ); - }, - ); -} - -#[test] -fn local_environment_ignores_partial_or_blank_env_contracts() { - with_local_sdk_env( - &[ - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_SCHEME", "ws"), - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST", " "), - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_PORT", "18080"), - ("RADROOTSD_RPC_HOST", "127.0.0.1"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("radrootsd endpoint"), - RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT - ); - }, - ); -} - -#[test] -fn local_environment_handles_invalid_and_missing_relay_port_env() { - with_local_sdk_env( - &[ - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_SCHEME", "http"), - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST", "127.0.0.1"), - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_PORT", "18080"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect_err("invalid relay env"), - SdkConfigError::InvalidRelayUrl("http://127.0.0.1:18080".to_owned()) - ); - }, - ); - - with_local_sdk_env( - &[ - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_SCHEME", "ws"), - ("RADROOTS_LOCAL_NOSTR_RELAY_PUBLIC_HOST", "127.0.0.1"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config.resolved_relay_urls().expect("relay defaults"), - vec![RADROOTS_SDK_LOCAL_RELAY_URL.to_owned()] - ); - }, - ); -} - -#[test] -fn local_environment_builds_radrootsd_endpoint_from_host_port_env() { - with_local_sdk_env( - &[ - ("RADROOTSD_RPC_HOST", "127.0.0.1"), - ("RADROOTSD_RPC_PORT", "17070"), - ], - || { - let config = RadrootsSdkConfig::local(); - - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("host port endpoint"), - "http://127.0.0.1:17070" - ); - }, - ); -} - -#[test] -fn explicit_coordinates_override_environment_defaults_exactly() { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![ - " wss://relay.custom.one ".to_owned(), - "wss://relay.custom.one".to_owned(), - "ws://relay.custom.two".to_owned(), - ]; - config.radrootsd.endpoint = Some(" https://rpc.custom.radroots.org ".to_owned()); - - assert_eq!( - config.resolved_relay_urls().expect("relay overrides"), - vec![ - "wss://relay.custom.one".to_owned(), - "ws://relay.custom.two".to_owned(), - ] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("endpoint override"), - "https://rpc.custom.radroots.org" - ); -} - -#[test] -fn custom_environment_requires_explicit_coordinates() { - let config = RadrootsSdkConfig::custom(); - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("custom relay error"), - SdkConfigError::MissingCustomRelayUrls - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect_err("custom radrootsd error"), - SdkConfigError::MissingCustomRadrootsdEndpoint - ); -} - -#[test] -fn custom_environment_accepts_explicit_coordinates() { - let mut config = RadrootsSdkConfig::custom(); - config.relay.urls = vec!["wss://relay.custom.radroots.org".to_owned()]; - config.radrootsd.endpoint = Some("https://rpc.custom.radroots.org".to_owned()); - - assert_eq!( - config.resolved_relay_urls().expect("custom relay"), - vec!["wss://relay.custom.radroots.org".to_owned()] - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect("custom endpoint"), - "https://rpc.custom.radroots.org" - ); -} - -#[test] -fn empty_coordinate_values_fail_loudly() { - let mut config = RadrootsSdkConfig::production(); - config.relay = RelayConfig { - urls: vec![" ".to_owned()], - }; - config.radrootsd.endpoint = Some(" ".to_owned()); - - assert_eq!( - config.resolved_relay_urls().expect_err("empty relay"), - SdkConfigError::EmptyRelayUrl - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect_err("empty radrootsd endpoint"), - SdkConfigError::EmptyRadrootsdEndpoint - ); -} - -#[test] -fn invalid_coordinate_schemes_fail_loudly() { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec!["https://relay.bad".to_owned()]; - config.radrootsd.endpoint = Some("wss://rpc.bad".to_owned()); - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("relay scheme error"), - SdkConfigError::InvalidRelayUrl("https://relay.bad".to_owned()) - ); - assert_eq!( - config - .resolved_radrootsd_endpoint() - .expect_err("endpoint scheme error"), - SdkConfigError::InvalidRadrootsdEndpoint("wss://rpc.bad".to_owned()) - ); -} - -#[test] -fn invalid_relay_authorities_fail_loudly() { - let invalid_relays = [ - "wss://", - "wss:///relay", - "ws://:8080", - "wss://relay.example:", - "wss://relay example", - "wss://user@relay.example", - "wss://relay.example:abc", - "wss://2001:db8::1", - ]; - - for relay_url in invalid_relays { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![relay_url.to_owned()]; - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("relay authority error"), - SdkConfigError::InvalidRelayUrl(relay_url.to_owned()) - ); - } -} - -#[test] -fn invalid_bracketed_relay_authorities_fail_loudly() { - let invalid_relays = [ - "wss://[2001:db8::1", - "wss://[]:443", - "wss://[2001:db8::1]suffix", - "wss://[2001:db8::1]:abc", - ]; - - for relay_url in invalid_relays { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![relay_url.to_owned()]; - - assert_eq!( - config - .resolved_relay_urls() - .expect_err("bracketed relay authority error"), - SdkConfigError::InvalidRelayUrl(relay_url.to_owned()) - ); - } -} - -#[test] -fn valid_relay_authorities_still_resolve() { - let mut config = RadrootsSdkConfig::production(); - config.relay.urls = vec![ - " wss://relay.example/nostr ".to_owned(), - "ws://127.0.0.1:8080".to_owned(), - "wss://[2001:db8::1]/relay".to_owned(), - "wss://[2001:db8::1]:443/relay".to_owned(), - ]; - - assert_eq!( - config.resolved_relay_urls().expect("valid relays"), - vec![ - "wss://relay.example/nostr".to_owned(), - "ws://127.0.0.1:8080".to_owned(), - "wss://[2001:db8::1]/relay".to_owned(), - "wss://[2001:db8::1]:443/relay".to_owned() - ] - ); -} - -#[test] -fn signer_modes_format_as_config_tokens() { - assert_eq!(SignerConfig::DraftOnly.to_string(), "draft_only"); - assert_eq!(SignerConfig::LocalIdentity.to_string(), "local_identity"); - assert_eq!(SignerConfig::Nip46.to_string(), "nip46"); -} - -#[test] -fn config_errors_format_operator_facing_messages() { - let formatted = [ - SdkConfigError::MissingCustomRelayUrls.to_string(), - SdkConfigError::MissingCustomRadrootsdEndpoint.to_string(), - SdkConfigError::EmptyRelayUrl.to_string(), - SdkConfigError::InvalidRelayUrl("http://relay.example".into()).to_string(), - SdkConfigError::EmptyRadrootsdEndpoint.to_string(), - SdkConfigError::InvalidRadrootsdEndpoint("ws://rpc.example".into()).to_string(), - ]; - - assert_eq!( - formatted, - [ - "custom sdk environment requires explicit relay urls", - "custom sdk environment requires an explicit radrootsd endpoint", - "relay url must not be empty", - "relay url must use ws or wss, got `http://relay.example`", - "radrootsd endpoint must not be empty", - "radrootsd endpoint must use http or https, got `ws://rpc.example`", - ] - ); -} - -#[test] -fn radrootsd_auth_debug_formats_none_and_redacts_bearer_tokens() { - assert_eq!(format!("{:?}", RadrootsdAuth::None), "None"); - - let bearer = RadrootsdAuth::BearerToken("sdk-secret-token".to_owned()); - let debug = format!("{bearer:?}"); - - assert!(!debug.contains("sdk-secret-token")); - assert_eq!(debug, "BearerToken(\"<redacted>\")"); -} - -#[test] -fn sdk_config_debug_redacts_bearer_tokens() { - let mut config = RadrootsSdkConfig::production(); - config.radrootsd.auth = RadrootsdAuth::BearerToken("sdk-secret-token".to_owned()); - - let debug = format!("{config:?}"); - - assert!(!debug.contains("sdk-secret-token")); - assert!(debug.contains("BearerToken(\"<redacted>\")")); -} diff --git a/crates/sdk/tests/radrootsd.rs b/crates/sdk/tests/radrootsd.rs @@ -1,1955 +0,0 @@ -#![cfg(feature = "radrootsd-client")] - -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_events::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; -use radroots_events::ids::RadrootsPublicKey; -use radroots_events::kinds::{ - KIND_FARM, KIND_LISTING, KIND_LISTING_DRAFT, KIND_ORDER_REQUEST, KIND_PROFILE, -}; -use radroots_sdk::adapters::radrootsd::{ - SdkRadrootsdBridgeDeliveryPolicy, SdkRadrootsdBridgeJob, SdkRadrootsdBridgeJobStatus, - SdkRadrootsdBridgePublishResponse, SdkRadrootsdListingPublishRequest, - SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest, - SdkRadrootsdSignerSessionMode, SdkRadrootsdSignerSessionRole, -}; -use radroots_sdk::client::{ - RadrootsSdkClient, SdkPublishError, SdkRadrootsdBridgeError, SdkRadrootsdFarmPublishOptions, - SdkRadrootsdListingPublishOptions, SdkRadrootsdOrderRequestPublishOptions, - SdkRadrootsdProfilePublishOptions, SdkRadrootsdPublishReceipt, SdkRadrootsdSessionError, - SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionView, SdkTransportReceipt, -}; -use radroots_sdk::config::{ - RadrootsSdkConfig, RadrootsdAuth, RadrootsdConfig, SdkConfigError, SdkEnvironment, - SdkTransportMode, SignerConfig, -}; -use radroots_sdk::protocol::events::{RadrootsNostrEvent, RadrootsNostrEventPtr}; -use radroots_sdk::protocol::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingParseError, - RadrootsListingProduct, RadrootsListingStatus, -}; -use radroots_sdk::protocol::order::{ - RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, - RadrootsOrderItem, RadrootsOrderPricingBasis, RadrootsOrderRequest, -}; -use radroots_sdk::protocol::profile::{RadrootsProfile, RadrootsProfileType}; -use serde_json::{Value, json}; -use std::collections::VecDeque; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpListener; -use tokio::sync::{mpsc, oneshot}; - -type TestResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>; - -struct JsonRpcServer { - endpoint: String, - shutdown_tx: Option<oneshot::Sender<()>>, -} - -impl JsonRpcServer { - async fn spawn( - expected_auth: Option<&str>, - response_body: Value, - ) -> TestResult<(Self, oneshot::Receiver<Value>)> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let addr = listener.local_addr()?; - let endpoint = format!("http://{addr}/jsonrpc"); - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - let (request_tx, request_rx) = oneshot::channel(); - let expected_auth = expected_auth.map(str::to_owned); - let response_text = response_body.to_string(); - - tokio::spawn(async move { - loop { - tokio::select! { - _ = &mut shutdown_rx => break, - accept = listener.accept() => { - let Ok((mut stream, _)) = accept else { - break; - }; - let mut buffer = Vec::new(); - let mut chunk = [0_u8; 4096]; - let header_end = loop { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - return; - } - buffer.extend_from_slice(&chunk[..read]); - if let Some(index) = find_headers_end(&buffer) { - break index; - } - }; - - let headers = String::from_utf8_lossy(&buffer[..header_end]).into_owned(); - let content_length = parse_content_length(headers.as_str()).unwrap_or(0); - let body_start = header_end + 4; - while buffer.len().saturating_sub(body_start) < content_length { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - break; - } - buffer.extend_from_slice(&chunk[..read]); - } - - if let Some(expected_auth) = expected_auth.as_deref() { - let actual_auth = parse_authorization(headers.as_str()); - if actual_auth.as_deref() != Some(expected_auth) { - let _ = write_http_response( - &mut stream, - 401, - json!({ - "jsonrpc": "2.0", - "id": "sdk-test", - "error": { - "code": -32001, - "message": format!( - "unexpected authorization header: {:?}", - actual_auth - ), - } - }) - .to_string() - .as_str(), - ) - .await; - return; - } - } - - let body = &buffer[body_start..body_start + content_length]; - let Ok(request_json) = serde_json::from_slice::<Value>(body) else { - return; - }; - let _ = request_tx.send(request_json); - let _ = write_http_response(&mut stream, 200, response_text.as_str()).await; - break; - } - } - } - }); - - Ok(( - Self { - endpoint, - shutdown_tx: Some(shutdown_tx), - }, - request_rx, - )) - } - - fn endpoint(&self) -> &str { - self.endpoint.as_str() - } -} - -struct JsonRpcSequenceServer { - endpoint: String, - shutdown_tx: Option<oneshot::Sender<()>>, -} - -impl JsonRpcSequenceServer { - async fn spawn( - expected_auth: Option<&str>, - response_bodies: Vec<Value>, - ) -> TestResult<(Self, mpsc::UnboundedReceiver<Value>)> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let addr = listener.local_addr()?; - let endpoint = format!("http://{addr}/jsonrpc"); - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - let (request_tx, request_rx) = mpsc::unbounded_channel(); - let expected_auth = expected_auth.map(str::to_owned); - let mut response_texts = response_bodies - .into_iter() - .map(|value| value.to_string()) - .collect::<VecDeque<_>>(); - - tokio::spawn(async move { - loop { - if response_texts.is_empty() { - break; - } - - tokio::select! { - _ = &mut shutdown_rx => break, - accept = listener.accept() => { - let Ok((mut stream, _)) = accept else { - break; - }; - let mut buffer = Vec::new(); - let mut chunk = [0_u8; 4096]; - let header_end = loop { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - return; - } - buffer.extend_from_slice(&chunk[..read]); - if let Some(index) = find_headers_end(&buffer) { - break index; - } - }; - - let headers = String::from_utf8_lossy(&buffer[..header_end]).into_owned(); - let content_length = parse_content_length(headers.as_str()).unwrap_or(0); - let body_start = header_end + 4; - while buffer.len().saturating_sub(body_start) < content_length { - let Ok(read) = stream.read(&mut chunk).await else { - return; - }; - if read == 0 { - break; - } - buffer.extend_from_slice(&chunk[..read]); - } - - if let Some(expected_auth) = expected_auth.as_deref() { - let actual_auth = parse_authorization(headers.as_str()); - if actual_auth.as_deref() != Some(expected_auth) { - let _ = write_http_response( - &mut stream, - 401, - json!({ - "jsonrpc": "2.0", - "id": "sdk-test", - "error": { - "code": -32001, - "message": format!( - "unexpected authorization header: {:?}", - actual_auth - ), - } - }) - .to_string() - .as_str(), - ) - .await; - return; - } - } - - let body = &buffer[body_start..body_start + content_length]; - let Ok(request_json) = serde_json::from_slice::<Value>(body) else { - return; - }; - let _ = request_tx.send(request_json); - let Some(response_text) = response_texts.pop_front() else { - return; - }; - let _ = write_http_response(&mut stream, 200, response_text.as_str()).await; - } - } - } - }); - - Ok(( - Self { - endpoint, - shutdown_tx: Some(shutdown_tx), - }, - request_rx, - )) - } - - fn endpoint(&self) -> &str { - self.endpoint.as_str() - } -} - -impl Drop for JsonRpcSequenceServer { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - } -} - -impl Drop for JsonRpcServer { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - } -} - -fn find_headers_end(buffer: &[u8]) -> Option<usize> { - buffer.windows(4).position(|window| window == b"\r\n\r\n") -} - -fn parse_content_length(headers: &str) -> Option<usize> { - headers.lines().find_map(|line| { - let (name, value) = line.split_once(':')?; - if !name.eq_ignore_ascii_case("content-length") { - return None; - } - value.trim().parse().ok() - }) -} - -fn parse_authorization(headers: &str) -> Option<String> { - headers.lines().find_map(|line| { - let (name, value) = line.split_once(':')?; - if !name.eq_ignore_ascii_case("authorization") { - return None; - } - Some(value.trim().to_owned()) - }) -} - -async fn write_http_response( - stream: &mut tokio::net::TcpStream, - status: u16, - body: &str, -) -> Result<(), std::io::Error> { - let status_text = match status { - 200 => "OK", - 401 => "Unauthorized", - _ => "Internal Server Error", - }; - let response = format!( - "HTTP/1.1 {status} {status_text}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", - body.len(), - body - ); - stream.write_all(response.as_bytes()).await -} - -fn sample_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "seller".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("Single origin coffee".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice { - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(20u32), - RadrootsCoreCurrency::USD, - ), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - }, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(5u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "North Farm".into(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), - images: None, - } -} - -fn sample_profile() -> RadrootsProfile { - RadrootsProfile { - name: "North Farm".into(), - display_name: Some("North Farm".into()), - nip05: None, - about: Some("Coffee farm".into()), - website: Some("https://example.invalid/north-farm".into()), - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - } -} - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Coffee farm".into()), - website: Some("https://example.invalid/north-farm".into()), - picture: None, - banner: None, - location: Some(RadrootsFarmLocation { - primary: Some("North Farm".into()), - city: Some("San Francisco".into()), - region: Some("CA".into()), - country: Some("US".into()), - gcs: None, - }), - tags: Some(vec!["coffee".into()]), - } -} - -fn sample_order_request_economics() -> RadrootsOrderEconomics { - RadrootsOrderEconomics { - quote_id: "quote-1".parse().expect("quote id"), - quote_version: 1, - pricing_basis: RadrootsOrderPricingBasis::ListingEvent, - currency: RadrootsCoreCurrency::USD, - items: vec![RadrootsOrderEconomicItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - quantity_amount: RadrootsCoreDecimal::from(1u32), - quantity_unit: RadrootsCoreUnit::MassG, - unit_price_amount: RadrootsCoreDecimal::from(20u32), - unit_price_currency: RadrootsCoreCurrency::USD, - line_subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(40u32), - RadrootsCoreCurrency::USD, - ), - }], - discounts: Vec::<RadrootsOrderEconomicLine>::new(), - adjustments: Vec::<RadrootsOrderEconomicLine>::new(), - subtotal: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(40u32), - RadrootsCoreCurrency::USD, - ), - discount_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - adjustment_total: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(0u32), - RadrootsCoreCurrency::USD, - ), - total: RadrootsCoreMoney::new(RadrootsCoreDecimal::from(40u32), RadrootsCoreCurrency::USD), - } -} - -fn sample_order_request() -> RadrootsOrderRequest { - let seller_pubkey: RadrootsPublicKey = "a".repeat(64).parse().expect("seller public key"); - - RadrootsOrderRequest { - order_id: "order-1".parse().expect("order id"), - listing_addr: format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg") - .parse() - .expect("listing address"), - buyer_pubkey: "b".repeat(64).parse().expect("buyer public key"), - seller_pubkey, - items: vec![RadrootsOrderItem { - bin_id: "bin-1".parse().expect("bin id"), - bin_count: 2, - }], - economics: sample_order_request_economics(), - } -} - -fn listing_event_ptr_with_relays(relays: Option<&str>) -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: "a".repeat(64), - relays: relays.map(str::to_owned), - } -} - -fn sdk_event( - author: &str, - created_at: u32, - draft: radroots_sdk::protocol::listing::RadrootsListingDraft, -) -> RadrootsNostrEvent { - let parts = draft.into_wire_parts(); - RadrootsNostrEvent { - id: "event-1".to_owned(), - author: author.to_owned(), - created_at, - kind: parts.kind, - tags: parts.tags, - content: parts.content, - sig: "f".repeat(128), - } -} - -fn radrootsd_test_client(endpoint: &str) -> Result<RadrootsSdkClient, SdkConfigError> { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::Nip46; - config.radrootsd = RadrootsdConfig { - endpoint: Some(endpoint.to_owned()), - auth: RadrootsdAuth::BearerToken("sdk-secret".to_owned()), - }; - RadrootsSdkClient::from_config(config) -} - -fn sample_session_view_json(session_id: &str) -> Value { - json!({ - "session_id": session_id, - "role": "outbound_remote_signer", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "user_pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "relays": ["wss://radroots.org"], - "permissions": ["sign_event:30402"], - "name": "Radroots Signer", - "url": "https://radroots.org/signers/demo", - "image": "https://radroots.org/signers/demo.png", - "auth_required": false, - "authorized": true, - "auth_url": null, - "expires_in_secs": 120, - "signer_authority": { - "provider_runtime_id": "runtime-1", - "account_identity_id": "identity-1", - "provider_signer_session_id": "provider-session-123" - } - }) -} - -fn sample_bridge_status_json() -> Value { - json!({ - "enabled": true, - "ready": true, - "auth_mode": "bearer_token", - "signer_mode": "selectable_per_request", - "default_signer_mode": "embedded_service_identity", - "supported_signer_modes": ["embedded_service_identity", "nip46_session"], - "available_nip46_signer_sessions": 2, - "relay_count": 1, - "delivery_policy": "quorum", - "delivery_quorum": 1, - "publish_max_attempts": 3, - "publish_initial_backoff_millis": 250, - "publish_max_backoff_millis": 4000, - "job_status_retention": 64, - "retained_jobs": 4, - "retained_idempotency_keys": 2, - "accepted_jobs": 1, - "published_jobs": 2, - "failed_jobs": 1, - "recovered_failed_jobs": 0, - "methods": ["bridge.status", "bridge.job.status", "bridge.job.list", "bridge.listing.publish"] - }) -} - -fn sample_bridge_job_json(job_id: &str) -> Value { - sample_bridge_job_json_for(job_id, "bridge.listing.publish", 30402) -} - -fn sample_bridge_job_json_for(job_id: &str, command: &str, event_kind: u32) -> Value { - json!({ - "job_id": job_id, - "command": command, - "idempotency_key": "idem-bridge-1", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "requested_at_unix": 1720000000u64, - "completed_at_unix": 1720000001u64, - "signer_mode": "nip46_session", - "signer_session_id": "session-123", - "event_kind": event_kind, - "event_id": "event-bridge-1", - "event_addr": "30402:seller:listing-bridge-1", - "delivery_policy": "quorum", - "delivery_quorum": 1, - "relay_count": 2, - "acknowledged_relay_count": 1, - "required_acknowledged_relay_count": 1, - "attempt_count": 1, - "attempt_summaries": ["attempt 1: 1/2 relays acknowledged"], - "relay_results": [ - { - "relay_url": "wss://radroots.org", - "acknowledged": true, - "detail": null - }, - { - "relay_url": "wss://backup.radroots.org", - "acknowledged": false, - "detail": "timeout" - } - ], - "relay_outcome_summary": "quorum satisfied with 1/2 relay acknowledgements" - }) -} - -async fn connected_bunker_session_handle( - session_id: &str, -) -> TestResult<SdkRadrootsdSignerSessionHandle> { - let (server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": session_id, - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await - .map_err(Into::into) -} - -#[test] -fn radrootsd_debug_redacts_signer_session_values() { - let 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 request = SdkRadrootsdListingPublishRequest { - listing: sample_listing(), - kind: Some(30402), - signer_session_id: "session-123".to_owned(), - signer_authority: Some(signer_authority), - idempotency_key: Some("idem-1".to_owned()), - }; - let job = SdkRadrootsdBridgeJob { - job_id: "job-1".to_owned(), - command: "bridge.listing.publish".to_owned(), - status: "published".to_owned(), - terminal: true, - recovered_after_restart: false, - signer_mode: "nip46_session:session-123".to_owned(), - signer_session_id: Some("session-123".to_owned()), - event_kind: 30402, - event_id: Some("event-1".to_owned()), - event_addr: Some("30402:seller:listing-1".to_owned()), - relay_count: 1, - acknowledged_relay_count: 1, - }; - let response = SdkRadrootsdBridgePublishResponse { - deduplicated: false, - job, - }; - let receipt = SdkRadrootsdPublishReceipt { - accepted: true, - deduplicated: false, - job_id: Some("job-1".to_owned()), - status: Some("published".to_owned()), - signer_mode: Some("nip46_session:session-123".to_owned()), - signer_session_id: Some("session-123".to_owned()), - event_addr: Some("30402:seller:listing-1".to_owned()), - relay_count: Some(1), - acknowledged_relay_count: Some(1), - }; - - let request_debug = format!("{request:?}"); - let response_debug = format!("{response:?}"); - let receipt_debug = format!("{receipt:?}"); - - assert!(!request_debug.contains("session-123")); - assert!(!request_debug.contains("provider-session-123")); - assert!(request_debug.contains("<redacted>")); - - assert!(!response_debug.contains("session-123")); - assert!(response_debug.contains("<redacted>")); - - assert!(!receipt_debug.contains("session-123")); - assert!(receipt_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: SdkRadrootsdSignerSessionHandle = 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_bunker_supports_bunker_mode() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-bunker", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - - let client = radrootsd_test_client(server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.connect"); - assert_eq!( - request_json["params"]["url"], - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret" - ); - assert!(request_json["params"]["client_secret_key"].is_null()); - assert_eq!(handle.mode(), SdkRadrootsdSignerSessionMode::Bunker); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_status_returns_typed_view() -> TestResult<()> { - let (connect_server, _) = 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 connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_nostrconnect( - "nostrconnect://bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - "client-secret-key", - ) - .await?; - - let (status_server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-status", - "result": sample_session_view_json("session-123") - }), - ) - .await?; - let status_client = radrootsd_test_client(status_server.endpoint())?; - let session: SdkRadrootsdSignerSessionView = status_client - .radrootsd() - .signer_sessions() - .status(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.status"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert_eq!(session.session(), handle.session()); - assert_eq!( - session.role, - SdkRadrootsdSignerSessionRole::OutboundRemoteSigner - ); - assert_eq!( - session.client_pubkey, - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ); - assert_eq!( - session.signer_pubkey, - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ); - assert_eq!( - session.user_pubkey.as_deref(), - Some("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") - ); - assert_eq!(session.relays, vec!["wss://radroots.org".to_owned()]); - assert_eq!(session.permissions, vec!["sign_event:30402".to_owned()]); - assert_eq!(session.name.as_deref(), Some("Radroots Signer")); - assert_eq!( - session.url.as_deref(), - Some("https://radroots.org/signers/demo") - ); - assert_eq!( - session.image.as_deref(), - Some("https://radroots.org/signers/demo.png") - ); - assert!(session.authorized); - assert!(!session.auth_required); - assert_eq!(session.expires_in_secs, Some(120)); - assert_eq!( - session - .signer_authority - .as_ref() - .map(|value| value.provider_runtime_id.as_str()), - Some("runtime-1") - ); - - let debug = format!("{session:?}"); - assert!(!debug.contains("session-123")); - assert!(debug.contains("<redacted>")); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_list_returns_typed_views() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-list", - "result": [ - sample_session_view_json("session-123"), - sample_session_view_json("session-456") - ] - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let sessions: Vec<SdkRadrootsdSignerSessionView> = - client.radrootsd().signer_sessions().list().await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.list"); - assert_eq!(sessions.len(), 2); - assert_eq!( - sessions[0].role, - SdkRadrootsdSignerSessionRole::OutboundRemoteSigner - ); - let debug = format!("{:?}", sessions[0].session()); - assert!(!debug.contains("session-123")); - assert!(debug.contains("<redacted>")); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_authorize_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-authorize", - "result": { - "authorized": true, - "replayed": true - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .authorize(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.authorize"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert!(result.authorized); - assert!(result.replayed); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_get_public_key_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-get-public-key", - "result": { - "pubkey": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .get_public_key(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.get_public_key"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert_eq!( - result.pubkey, - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" - ); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_require_auth_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-require-auth", - "result": { - "required": true - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .require_auth(handle.session(), "https://radroots.org/auth") - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.require_auth"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert_eq!( - request_json["params"]["auth_url"], - "https://radroots.org/auth" - ); - assert!(result.required); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_signer_session_close_returns_typed_result() -> TestResult<()> { - let (connect_server, _) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-123", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - ) - .await?; - let connect_client = radrootsd_test_client(connect_server.endpoint())?; - let handle: SdkRadrootsdSignerSessionHandle = connect_client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-session-close", - "result": { - "closed": true - } - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let result = client - .radrootsd() - .signer_sessions() - .close(handle.session()) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "nip46.session.close"); - assert_eq!(request_json["params"]["session_id"], "session-123"); - assert!(result.closed); - - 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] -async fn radrootsd_listing_publish_accepts_sdk_built_draft() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-1", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-123", - "signer_session_id": "session-123", - "event_kind": 30402, - "event_id": "event-1", - "event_addr": "30402:seller:listing-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-123").await?; - let client = radrootsd_test_client(server.endpoint())?; - let draft = client.listing().build_draft(&sample_listing())?; - let options = SdkRadrootsdListingPublishOptions::from_signer_session(&handle) - .with_idempotency_key("idem-1"); - - let receipt = client - .listing() - .publish_draft_via_radrootsd_with_options(draft, &options) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.listing.publish"); - assert_eq!(request_json["params"]["signer_session_id"], "session-123"); - assert_eq!(request_json["params"]["idempotency_key"], "idem-1"); - assert_eq!(request_json["params"]["kind"], 30402); - assert_eq!( - request_json["params"]["listing"]["d_tag"], - "AAAAAAAAAAAAAAAAAAAAAg" - ); - - assert_eq!(receipt.transport, SdkTransportMode::Radrootsd); - assert_eq!(receipt.event_kind, Some(30402)); - assert_eq!(receipt.event_id, Some("event-1".to_owned())); - match receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(rpc_receipt) => { - assert!(rpc_receipt.accepted); - assert!(!rpc_receipt.deduplicated); - assert_eq!(rpc_receipt.job_id.as_deref(), Some("job-1")); - assert_eq!(rpc_receipt.status.as_deref(), Some("published")); - assert_eq!( - rpc_receipt.signer_session_id.as_deref(), - Some("session-123") - ); - assert_eq!( - rpc_receipt.event_addr.as_deref(), - Some("30402:seller:listing-1") - ); - assert_eq!(rpc_receipt.relay_count, Some(1)); - assert_eq!(rpc_receipt.acknowledged_relay_count, Some(1)); - } - SdkTransportReceipt::RelayDirect(_) => panic!("unexpected relay receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_accepts_typed_listing_value() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-2", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-456", - "signer_session_id": "session-456", - "event_kind": 30402, - "event_id": "event-2", - "event_addr": "30402:seller:listing-2", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-456").await?; - let client = radrootsd_test_client(server.endpoint())?; - - let receipt = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.listing.publish"); - assert_eq!(request_json["params"]["signer_session_id"], "session-456"); - assert!(request_json["params"]["idempotency_key"].is_null()); - assert_eq!(request_json["params"]["kind"], 30402); - assert_eq!( - request_json["params"]["listing"]["d_tag"], - "AAAAAAAAAAAAAAAAAAAAAg" - ); - - assert_eq!(receipt.transport, SdkTransportMode::Radrootsd); - assert_eq!(receipt.event_kind, Some(30402)); - assert_eq!(receipt.event_id, Some("event-2".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_profile_publish_accepts_typed_profile_value() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-profile-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-profile-1", - "command": "bridge.profile.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-profile-1", - "signer_session_id": "session-profile-1", - "event_kind": 0, - "event_id": "event-profile-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-profile-1").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdProfilePublishOptions::from_signer_session(&handle) - .with_idempotency_key("profile-idem-1") - .with_signer_authority(SdkRadrootsdSignerAuthority { - provider_runtime_id: "runtime-profile".to_owned(), - account_identity_id: "identity-profile".to_owned(), - provider_signer_session_id: Some("provider-session-profile".to_owned()), - }); - - let receipt = client - .profile() - .publish_profile_via_radrootsd_with_options( - &sample_profile(), - Some(RadrootsProfileType::Farm), - &options, - ) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.profile.publish"); - assert_eq!( - request_json["params"]["signer_session_id"], - "session-profile-1" - ); - assert_eq!(request_json["params"]["profile_type"], "farm"); - assert_eq!(request_json["params"]["profile"]["name"], "North Farm"); - assert_eq!(request_json["params"]["idempotency_key"], "profile-idem-1"); - assert_eq!( - request_json["params"]["signer_authority"]["provider_runtime_id"], - "runtime-profile" - ); - assert_eq!(receipt.event_kind, Some(KIND_PROFILE)); - assert_eq!(receipt.event_id, Some("event-profile-1".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_farm_publish_accepts_typed_farm_value() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-farm-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-farm-1", - "command": "bridge.farm.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-farm-1", - "signer_session_id": "session-farm-1", - "event_kind": 30340, - "event_id": "event-farm-1", - "event_addr": "30340:seller:AAAAAAAAAAAAAAAAAAAAAA", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-farm-1").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdFarmPublishOptions::from_signer_session(&handle) - .with_idempotency_key("farm-idem-1"); - - let receipt = client - .farm() - .publish_farm_via_radrootsd_with_options(&sample_farm(), &options) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.farm.publish"); - assert_eq!( - request_json["params"]["signer_session_id"], - "session-farm-1" - ); - assert_eq!(request_json["params"]["kind"], KIND_FARM); - assert_eq!( - request_json["params"]["farm"]["d_tag"], - "AAAAAAAAAAAAAAAAAAAAAA" - ); - assert_eq!(request_json["params"]["idempotency_key"], "farm-idem-1"); - assert_eq!(receipt.event_kind, Some(KIND_FARM)); - assert_eq!(receipt.event_id, Some("event-farm-1".to_owned())); - match receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(receipt) => { - assert_eq!( - receipt.event_addr, - Some("30340:seller:AAAAAAAAAAAAAAAAAAAAAA".to_owned()) - ); - } - SdkTransportReceipt::RelayDirect(_) => panic!("unexpected relay receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_with_options_forwards_typed_continuity_metadata() --> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-3", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-789", - "signer_session_id": "session-789", - "event_kind": 30402, - "event_id": "event-3", - "event_addr": "30402:seller:listing-3", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-789").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdListingPublishOptions::from_signer_session(&handle) - .with_idempotency_key("idem-3") - .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 receipt = client - .listing() - .publish_listing_via_radrootsd_with_options(&sample_listing(), &options) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.listing.publish"); - assert_eq!(request_json["params"]["signer_session_id"], "session-789"); - assert_eq!(request_json["params"]["idempotency_key"], "idem-3"); - assert_eq!( - request_json["params"]["signer_authority"]["provider_runtime_id"], - "runtime-1" - ); - assert_eq!( - request_json["params"]["signer_authority"]["account_identity_id"], - "identity-1" - ); - assert_eq!( - request_json["params"]["signer_authority"]["provider_signer_session_id"], - "provider-session-123" - ); - assert_eq!(receipt.event_id, Some("event-3".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::DraftOnly; - let client = RadrootsSdkClient::from_config(config)?; - let handle = connected_bunker_session_handle("session-123").await?; - - let error = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::Radrootsd, - signer: SignerConfig::DraftOnly, - required: SignerConfig::Nip46, - operation: "listing.publish_via_radrootsd", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_rejects_local_identity_signer_mode() -> TestResult<()> { - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Production); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - let handle = connected_bunker_session_handle("session-123").await?; - - let error = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::Radrootsd, - signer: SignerConfig::LocalIdentity, - required: SignerConfig::Nip46, - operation: "listing.publish_via_radrootsd", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_listing_publish_rejects_relay_transport_mode() -> TestResult<()> { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; - let handle = connected_bunker_session_handle("session-123").await?; - - let error = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::RelayDirect, - operation: "listing.publish_via_radrootsd", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_order_request_publish_accepts_session_handle() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-order-request-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-order-1", - "command": "bridge.order.request", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-order-1", - "signer_session_id": "session-order-1", - "event_kind": KIND_ORDER_REQUEST, - "event_id": "event-order-1", - "event_addr": format!("{KIND_LISTING}:{}:AAAAAAAAAAAAAAAAAAAAAg", "a".repeat(64)), - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - - let handle = connected_bunker_session_handle("session-order-1").await?; - let client = radrootsd_test_client(server.endpoint())?; - let options = SdkRadrootsdOrderRequestPublishOptions::from_signer_session(&handle) - .with_idempotency_key("idem-order-1") - .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-order-1".to_owned()), - }); - - let receipt = client - .order() - .publish_order_request_via_radrootsd_with_options( - &sample_order_request(), - &listing_event_ptr_with_relays(Some("wss://radroots.org")), - &options, - ) - .await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.order.request"); - assert_eq!( - request_json["params"]["signer_session_id"], - "session-order-1" - ); - assert_eq!(request_json["params"]["idempotency_key"], "idem-order-1"); - assert_eq!(request_json["params"]["order"]["order_id"], "order-1"); - assert_eq!( - request_json["params"]["listing_event"]["id"], - "a".repeat(64) - ); - assert_eq!( - request_json["params"]["listing_event"]["relays"], - "wss://radroots.org" - ); - assert_eq!( - request_json["params"]["signer_authority"]["provider_runtime_id"], - "runtime-1" - ); - assert_eq!( - request_json["params"]["signer_authority"]["provider_signer_session_id"], - "provider-session-order-1" - ); - assert_eq!(receipt.event_kind, Some(KIND_ORDER_REQUEST)); - assert_eq!(receipt.event_id, Some("event-order-1".to_owned())); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_sdk_workflow_chains_session_listing_order_and_bridge_job() -> TestResult<()> { - let (server, mut request_rx) = JsonRpcSequenceServer::spawn( - Some("Bearer sdk-secret"), - vec![ - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-nip46-connect", - "result": { - "session_id": "session-workflow-1", - "mode": "Bunker", - "remote_signer_pubkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "client_pubkey": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "relays": ["wss://radroots.org"] - } - }), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-workflow-listing", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-workflow-1", - "signer_session_id": "session-workflow-1", - "event_kind": 30402, - "event_id": "event-workflow-listing", - "event_addr": "30402:seller:listing-workflow-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-order-request-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-workflow-order", - "command": "bridge.order.request", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-workflow-1", - "signer_session_id": "session-workflow-1", - "event_kind": KIND_ORDER_REQUEST, - "event_id": "event-workflow-order", - "event_addr": format!("{KIND_LISTING}:{}:AAAAAAAAAAAAAAAAAAAAAg", "a".repeat(64)), - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-job-status", - "result": sample_bridge_job_json_for( - "job-workflow-order", - "bridge.order.request", - KIND_ORDER_REQUEST, - ) - }), - ], - ) - .await?; - - let client = radrootsd_test_client(server.endpoint())?; - let handle = client - .radrootsd() - .signer_sessions() - .connect_bunker( - "bunker://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?relay=wss%3A%2F%2Fradroots.org&secret=shared-secret", - ) - .await?; - assert_eq!(handle.mode(), SdkRadrootsdSignerSessionMode::Bunker); - - let connect_request = request_rx.recv().await.expect("connect request"); - assert_eq!(connect_request["method"], "nip46.connect"); - - let listing_receipt = client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await?; - let listing_request = request_rx.recv().await.expect("listing publish request"); - assert_eq!(listing_request["method"], "bridge.listing.publish"); - assert_eq!( - listing_request["params"]["signer_session_id"], - "session-workflow-1" - ); - - let order_receipt = client - .order() - .publish_order_request_via_radrootsd( - &sample_order_request(), - &listing_event_ptr_with_relays(Some("wss://radroots.org")), - &handle, - ) - .await?; - let order_request = request_rx.recv().await.expect("order publish request"); - assert_eq!(order_request["method"], "bridge.order.request"); - assert_eq!( - order_request["params"]["signer_session_id"], - "session-workflow-1" - ); - assert_eq!(order_request["params"]["order"]["order_id"], "order-1"); - assert_eq!( - order_request["params"]["listing_event"]["id"], - "a".repeat(64) - ); - - let order_job = match &order_receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(receipt) => receipt.job(), - SdkTransportReceipt::RelayDirect(_) => None, - } - .expect("order publish receipt should expose a bridge job ref"); - - let job_view = client.radrootsd().bridge().job(&order_job).await?; - let job_request = request_rx.recv().await.expect("bridge job request"); - assert_eq!(job_request["method"], "bridge.job.status"); - assert_eq!(job_request["params"]["job_id"], "job-workflow-order"); - - assert_eq!(listing_receipt.event_kind, Some(30402)); - assert_eq!(order_receipt.event_kind, Some(KIND_ORDER_REQUEST)); - assert_eq!(job_view.job().job_id(), "job-workflow-order"); - assert_eq!(job_view.command, "bridge.order.request"); - assert_eq!(job_view.status, SdkRadrootsdBridgeJobStatus::Published); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_status_returns_typed_status() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-status", - "result": sample_bridge_status_json() - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let status = client.radrootsd().bridge().status().await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.status"); - assert!(status.enabled); - assert!(status.ready); - assert_eq!( - status.delivery_policy, - SdkRadrootsdBridgeDeliveryPolicy::Quorum - ); - assert_eq!(status.delivery_quorum, Some(1)); - assert_eq!(status.available_nip46_signer_sessions, 2); - assert!( - status - .methods - .contains(&"bridge.listing.publish".to_owned()) - ); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_job_status_accepts_typed_job_ref_from_publish_receipt() -> TestResult<()> -{ - let (publish_server, publish_request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-listing-publish", - "result": { - "deduplicated": false, - "job": { - "job_id": "job-bridge-1", - "command": "bridge.listing.publish", - "status": "published", - "terminal": true, - "recovered_after_restart": false, - "signer_mode": "nip46_session:session-123", - "signer_session_id": "session-123", - "event_kind": 30402, - "event_id": "event-bridge-1", - "event_addr": "30402:seller:listing-bridge-1", - "relay_count": 1, - "acknowledged_relay_count": 1 - } - } - }), - ) - .await?; - let handle = connected_bunker_session_handle("session-123").await?; - let publish_client = radrootsd_test_client(publish_server.endpoint())?; - let publish_receipt = publish_client - .listing() - .publish_listing_via_radrootsd(&sample_listing(), &handle) - .await?; - let publish_request_json = publish_request_rx.await?; - assert_eq!(publish_request_json["method"], "bridge.listing.publish"); - - let job = match &publish_receipt.transport_receipt { - SdkTransportReceipt::Radrootsd(receipt) => receipt.job(), - SdkTransportReceipt::RelayDirect(_) => None, - } - .expect("publish receipt should expose a bridge job ref"); - - let (job_server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-job-status", - "result": sample_bridge_job_json("job-bridge-1") - }), - ) - .await?; - let job_client = radrootsd_test_client(job_server.endpoint())?; - let job_view = job_client.radrootsd().bridge().job(&job).await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.job.status"); - assert_eq!(request_json["params"]["job_id"], "job-bridge-1"); - assert_eq!(job_view.job().job_id(), "job-bridge-1"); - assert_eq!(job_view.status, SdkRadrootsdBridgeJobStatus::Published); - assert_eq!( - job_view.delivery_policy, - SdkRadrootsdBridgeDeliveryPolicy::Quorum - ); - assert_eq!(job_view.attempt_count, 1); - assert_eq!(job_view.relay_results.len(), 2); - assert_eq!(job_view.relay_results[0].relay_url, "wss://radroots.org"); - assert!(job_view.relay_results[0].acknowledged); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_job_list_returns_typed_views() -> TestResult<()> { - let (server, request_rx) = JsonRpcServer::spawn( - Some("Bearer sdk-secret"), - json!({ - "jsonrpc": "2.0", - "id": "radroots-sdk-bridge-job-list", - "result": [ - sample_bridge_job_json("job-bridge-1"), - sample_bridge_job_json("job-bridge-2") - ] - }), - ) - .await?; - let client = radrootsd_test_client(server.endpoint())?; - let jobs = client.radrootsd().bridge().jobs().await?; - let request_json = request_rx.await?; - - assert_eq!(request_json["method"], "bridge.job.list"); - assert_eq!(jobs.len(), 2); - assert_eq!(jobs[0].job().job_id(), "job-bridge-1"); - assert_eq!(jobs[1].job().job_id(), "job-bridge-2"); - assert_eq!(jobs[0].status, SdkRadrootsdBridgeJobStatus::Published); - - Ok(()) -} - -#[tokio::test] -async fn radrootsd_bridge_status_rejects_relay_transport_mode() -> TestResult<()> { - let client = RadrootsSdkClient::from_config(RadrootsSdkConfig::production())?; - let error = client - .radrootsd() - .bridge() - .status() - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkRadrootsdBridgeError::UnsupportedTransport { - transport: SdkTransportMode::RelayDirect, - operation: "radrootsd.bridge.status", - } - )); - - Ok(()) -} - -#[test] -fn radrootsd_listing_request_from_event_rejects_listing_draft_kind() -> TestResult<()> { - let draft = radroots_sdk::protocol::listing::build_draft(&sample_listing())?; - let mut event = sdk_event("seller", 1_720_000_000, draft); - event.kind = KIND_LISTING_DRAFT; - - assert!(matches!( - SdkRadrootsdListingPublishRequest::from_event(&event, "session-123", None, None), - Err(RadrootsListingParseError::InvalidKind(KIND_LISTING_DRAFT)) - )); - - Ok(()) -} diff --git a/crates/sdk/tests/relay_direct.rs b/crates/sdk/tests/relay_direct.rs @@ -1,551 +0,0 @@ -#![cfg(all( - feature = "identity-models", - feature = "relay-client", - feature = "signing" -))] - -use futures::{SinkExt, StreamExt}; -use nostr::{ClientMessage, JsonUtil, RelayMessage}; -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, - RadrootsCoreQuantityPrice, RadrootsCoreUnit, -}; -use radroots_sdk::client::{RadrootsSdkClient, SdkPublishError, SdkTransportReceipt}; -use radroots_sdk::config::{ - RadrootsSdkConfig, RelayConfig, SdkEnvironment, SdkTransportMode, SignerConfig, -}; -use radroots_sdk::protocol::farm::{RadrootsFarm, RadrootsFarmLocation, RadrootsFarmRef}; -use radroots_sdk::protocol::identity::RadrootsIdentity; -use radroots_sdk::protocol::listing::{ - RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, - RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, - RadrootsListingStatus, -}; -use radroots_sdk::protocol::profile::{RadrootsProfile, RadrootsProfileType}; -use tokio::net::TcpListener; -use tokio::sync::oneshot; -use tokio_tungstenite::tungstenite::Message; - -type TestResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync>>; - -struct AckRelay { - url: String, - shutdown_tx: Option<oneshot::Sender<()>>, -} - -impl AckRelay { - async fn spawn() -> TestResult<Self> { - let listener = TcpListener::bind("127.0.0.1:0").await?; - let addr = listener.local_addr()?; - let url = format!("ws://{addr}"); - let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); - - tokio::spawn(async move { - loop { - tokio::select! { - _ = &mut shutdown_rx => break, - accept = listener.accept() => { - let Ok((stream, _)) = accept else { - break; - }; - tokio::spawn(async move { - let Ok(websocket) = tokio_tungstenite::accept_async(stream).await else { - return; - }; - let (mut writer, mut reader) = websocket.split(); - while let Some(message) = reader.next().await { - let Ok(message) = message else { - break; - }; - let Message::Text(text) = message else { - continue; - }; - let Ok(client_message) = ClientMessage::from_json(text.as_str()) else { - continue; - }; - if let ClientMessage::Event(event) = client_message { - let relay_message = - RelayMessage::ok(event.id, true, "").as_json(); - if writer - .send(Message::Text(relay_message.into())) - .await - .is_err() - { - break; - } - } - } - }); - } - } - } - }); - - Ok(Self { - url, - shutdown_tx: Some(shutdown_tx), - }) - } - - fn url(&self) -> &str { - self.url.as_str() - } -} - -impl Drop for AckRelay { - fn drop(&mut self) { - if let Some(shutdown_tx) = self.shutdown_tx.take() { - let _ = shutdown_tx.send(()); - } - } -} - -fn sample_listing() -> RadrootsListing { - RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"), - published_at: None, - farm: RadrootsFarmRef { - pubkey: "seller".into(), - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - }, - product: RadrootsListingProduct { - key: "coffee".into(), - title: "Coffee".into(), - category: "coffee".into(), - summary: Some("Single origin coffee".into()), - process: None, - lot: None, - location: None, - profile: None, - year: None, - }, - primary_bin_id: "bin-1".parse().expect("primary bin id"), - bins: vec![RadrootsListingBin { - bin_id: "bin-1".parse().expect("bin id"), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1000u32), - RadrootsCoreUnit::MassG, - ), - price_per_canonical_unit: RadrootsCoreQuantityPrice { - amount: RadrootsCoreMoney::new( - RadrootsCoreDecimal::from(20u32), - RadrootsCoreCurrency::USD, - ), - quantity: RadrootsCoreQuantity::new( - RadrootsCoreDecimal::from(1u32), - RadrootsCoreUnit::MassG, - ), - }, - display_amount: None, - display_unit: None, - display_label: None, - display_price: None, - display_price_unit: None, - }], - resource_area: None, - plot: None, - discounts: None, - inventory_available: Some(RadrootsCoreDecimal::from(5u32)), - availability: Some(RadrootsListingAvailability::Status { - status: RadrootsListingStatus::Active, - }), - delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), - location: Some(RadrootsListingLocation { - primary: "North Farm".into(), - city: None, - region: None, - country: None, - lat: None, - lng: None, - geohash: None, - }), - images: None, - } -} - -fn sample_profile() -> RadrootsProfile { - RadrootsProfile { - name: "north-farm".into(), - display_name: Some("North Farm".into()), - nip05: None, - about: Some("Farm profile".into()), - website: None, - picture: None, - banner: None, - lud06: None, - lud16: None, - bot: None, - } -} - -fn sample_farm() -> RadrootsFarm { - RadrootsFarm { - d_tag: "AAAAAAAAAAAAAAAAAAAAAA".into(), - name: "North Farm".into(), - about: Some("Vegetable farm".into()), - website: None, - picture: None, - banner: None, - location: Some(RadrootsFarmLocation { - primary: Some("North Road".into()), - city: None, - region: None, - country: Some("US".into()), - gcs: None, - }), - tags: Some(vec!["vegetables".into()]), - } -} - -#[tokio::test] -async fn relay_direct_farm_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = client.farm().build_draft(&sample_farm())?; - - let receipt = client - .farm() - .publish_draft_with_identity(&identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(30340)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(relay_receipt.event.kind, 30340); - assert_eq!(relay_receipt.event.author, identity.public_key_hex()); - assert_eq!( - relay_receipt.event.tags, - vec![ - vec!["d".to_owned(), "AAAAAAAAAAAAAAAAAAAAAA".to_owned()], - vec!["t".to_owned(), "vegetables".to_owned()] - ] - ); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_farm_publish_rejects_radrootsd_transport_mode() -> TestResult<()> { - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::production(); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .farm() - .publish_with_identity(&identity, &sample_farm()) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "farm.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_farm_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::DraftOnly; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .farm() - .publish_with_identity(&identity, &sample_farm()) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "farm.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_profile_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = client - .profile() - .build_draft(&sample_profile(), Some(RadrootsProfileType::Farm))?; - - let receipt = client - .profile() - .publish_draft_with_identity(&identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(0)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(relay_receipt.event.kind, 0); - assert_eq!(relay_receipt.event.author, identity.public_key_hex()); - assert_eq!( - relay_receipt.event.tags, - vec![vec!["t".to_owned(), "radroots:type:farm".to_owned()]] - ); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_profile_publish_rejects_radrootsd_transport_mode() -> TestResult<()> { - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::production(); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .profile() - .publish_with_identity( - &identity, - &sample_profile(), - Some(RadrootsProfileType::Farm), - ) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "profile.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_profile_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::DraftOnly; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .profile() - .publish_with_identity( - &identity, - &sample_profile(), - Some(RadrootsProfileType::Farm), - ) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "profile.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_listing_publish_accepts_sdk_built_draft() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::LocalIdentity; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - let draft = client.listing().build_draft(&sample_listing())?; - - let receipt = client - .listing() - .publish_draft_with_identity(&identity, draft) - .await?; - - assert_eq!(receipt.transport, SdkTransportMode::RelayDirect); - assert_eq!(receipt.event_kind, Some(30402)); - assert!(receipt.event_id.is_some()); - match receipt.transport_receipt { - SdkTransportReceipt::RelayDirect(relay_receipt) => { - assert_eq!( - receipt.event_id.as_deref(), - Some(relay_receipt.event_id.as_str()) - ); - assert_eq!(receipt.event_kind, Some(relay_receipt.event_kind)); - assert_eq!(relay_receipt.event.kind, 30402); - assert_eq!(relay_receipt.event_id, relay_receipt.event.id); - assert_eq!(relay_receipt.signature, relay_receipt.event.sig); - assert_eq!(relay_receipt.created_at, relay_receipt.event.created_at); - assert_eq!(relay_receipt.event.author, identity.public_key_hex()); - assert_eq!(relay_receipt.target_relays, vec![relay.url().to_owned()]); - assert_eq!(relay_receipt.connected_relays, vec![relay.url().to_owned()]); - assert_eq!( - relay_receipt.acknowledged_relays, - vec![relay.url().to_owned()] - ); - assert!(relay_receipt.failed_relays.is_empty()); - } - SdkTransportReceipt::Radrootsd(_) => panic!("unexpected radrootsd receipt"), - } - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_publish_rejects_radrootsd_transport_mode() -> TestResult<()> { - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::production(); - config.transport = SdkTransportMode::Radrootsd; - config.signer = SignerConfig::LocalIdentity; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .listing() - .publish_with_identity(&identity, &sample_listing()) - .await - .expect_err("unsupported transport"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedTransport { - transport: SdkTransportMode::Radrootsd, - operation: "listing.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_publish_rejects_draft_only_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::DraftOnly; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .listing() - .publish_with_identity(&identity, &sample_listing()) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::DraftOnly, - required: SignerConfig::LocalIdentity, - operation: "listing.publish_with_identity", - } - )); - - Ok(()) -} - -#[tokio::test] -async fn relay_direct_publish_rejects_nip46_signer_mode() -> TestResult<()> { - let relay = AckRelay::spawn().await?; - let identity = RadrootsIdentity::generate(); - let mut config = RadrootsSdkConfig::for_environment(SdkEnvironment::Custom); - config.transport = SdkTransportMode::RelayDirect; - config.signer = SignerConfig::Nip46; - config.relay = RelayConfig { - urls: vec![relay.url().to_owned()], - }; - let client = RadrootsSdkClient::from_config(config)?; - - let error = client - .listing() - .publish_with_identity(&identity, &sample_listing()) - .await - .expect_err("unsupported signer mode"); - - assert!(matches!( - error, - SdkPublishError::UnsupportedSignerMode { - transport: SdkTransportMode::RelayDirect, - signer: SignerConfig::Nip46, - required: SignerConfig::LocalIdentity, - operation: "listing.publish_with_identity", - } - )); - - Ok(()) -} diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs @@ -86,47 +86,14 @@ fn migrated_runtime_tests_stay_on_product_runtime_boundary() { } #[test] -fn legacy_order_direct_publish_facades_are_removed_from_sdk_client() { - let source = read_source( - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("src/client.rs") - .as_path(), - ); +fn legacy_client_and_config_modules_are_removed() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - for forbidden in [ - "publish_order_request_with_identity", - "publish_order_decision_with_identity", - "publish_order_revision_proposal_with_identity", - "publish_order_revision_decision_with_identity", - "publish_order_cancellation_with_identity", - "publish_fulfillment_update_with_identity", - "publish_buyer_receipt_with_identity", - "publish_order_request_draft_with_identity", - "publish_order_decision_draft_with_identity", - "publish_order_revision_proposal_draft_with_identity", - "publish_order_revision_decision_draft_with_identity", - "publish_order_cancellation_draft_with_identity", - "publish_fulfillment_update_draft_with_identity", - "publish_buyer_receipt_draft_with_identity", - "build_order_request_draft", - "build_order_decision_draft", - "build_order_revision_proposal_draft", - "build_order_revision_decision_draft", - "build_order_cancellation_draft", - "build_fulfillment_update_draft", - "build_buyer_receipt_draft", - "parse_order_request", - "parse_order_decision", - "parse_order_revision_proposal", - "parse_order_revision_decision", - "parse_order_cancellation", - "parse_fulfillment_update", - "parse_buyer_receipt", - "validate_listing_event", - ] { + for relative_path in ["src/client.rs", "src/config.rs"] { + let path = manifest_dir.join(relative_path); assert!( - !source.contains(forbidden), - "src/client.rs must not expose legacy order direct facade `{forbidden}`" + !path.exists(), + "{relative_path} must not exist after SDK runtime surface closure" ); } } @@ -145,6 +112,35 @@ fn legacy_trade_client_root_export_is_removed() { ); } +#[test] +fn legacy_client_config_modules_are_not_public() { + let source = read_source( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("src/lib.rs") + .as_path(), + ); + + for forbidden in [ + "pub mod client;", + "pub mod config;", + "pub use crate::client", + "pub use crate::config", + "RadrootsSdkClient", + "RadrootsSdkConfig", + "SdkTransportMode", + "ProfileClient", + "FarmClient", + "ListingClient", + "SdkPublishReceipt", + "SdkTransportReceipt", + ] { + assert!( + !source.contains(forbidden), + "src/lib.rs must not expose legacy SDK client/config concept `{forbidden}`" + ); + } +} + fn product_runtime_file_stays_on_boundary(relative_path: &str) { let source = read_source( Path::new(env!("CARGO_MANIFEST_DIR"))