lib

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

commit c13a36cc86b80877a9b286cb056e698b96eb0df5
parent c489bb75228ae1ffd4ae5f9a5c6c404b038a1af4
Author: triesap <tyson@radroots.org>
Date:   Mon, 13 Apr 2026 00:51:39 +0000

sdk: add config defaults

Diffstat:
Acrates/sdk/src/config.rs | 260+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/lib.rs | 27+++++++++++++++++----------
Acrates/sdk/tests/config.rs | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 430 insertions(+), 10 deletions(-)

diff --git a/crates/sdk/src/config.rs b/crates/sdk/src/config.rs @@ -0,0 +1,260 @@ +#[cfg(not(feature = "std"))] +use alloc::{string::String, vec::Vec}; +use core::fmt; +#[cfg(feature = "std")] +use std::{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; + +#[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() { + 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 => 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(Debug, Clone, PartialEq, Eq, Default)] +pub enum RadrootsdAuth { + #[default] + None, + BearerToken(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum SignerConfig { + #[default] + DraftOnly, + LocalIdentity, + Nip46, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkConfig { + pub timeout_ms: u64, + pub retry_policy: RetryPolicy, +} + +impl Default for NetworkConfig { + fn default() -> Self { + Self { + timeout_ms: RADROOTS_SDK_DEFAULT_TIMEOUT_MS, + retry_policy: RetryPolicy::default(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub enum RetryPolicy { + #[default] + None, + Fixed { + max_attempts: u32, + backoff_ms: u64, + }, +} + +#[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 {} + +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); + } + if !(trimmed.starts_with("ws://") || trimmed.starts_with("wss://")) { + return Err(SdkConfigError::InvalidRelayUrl(trimmed.to_owned())); + } + Ok(trimmed.to_owned()) +} + +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()) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -4,24 +4,32 @@ #[cfg(not(feature = "std"))] extern crate alloc; -#[cfg(feature = "std")] -use std::{string::String, vec::Vec}; #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; +#[cfg(feature = "std")] +use std::{string::String, vec::Vec}; -pub mod farm; -#[cfg(feature = "identity-models")] -pub mod identity; -pub mod listing; -pub mod profile; -pub mod trade; #[cfg(any( feature = "signing", feature = "relay-client", feature = "signer-adapters" ))] pub mod adapters; +pub mod config; +pub mod farm; +#[cfg(feature = "identity-models")] +pub mod identity; +pub mod listing; +pub mod profile; +pub mod trade; +pub use crate::config::{ + NetworkConfig, RADROOTS_SDK_LOCAL_RADROOTSD_ENDPOINT, RADROOTS_SDK_LOCAL_RELAY_URL, + RADROOTS_SDK_PRODUCTION_RADROOTSD_ENDPOINT, RADROOTS_SDK_PRODUCTION_RELAY_URL, + RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT, RADROOTS_SDK_STAGING_RELAY_URL, RadrootsSdkConfig, + RadrootsdAuth, RadrootsdConfig, RelayConfig, RetryPolicy, SdkConfigError, SdkEnvironment, + SdkTransportMode, SignerConfig, +}; pub use radroots_events::{ RadrootsNostrEvent, RadrootsNostrEventPtr, RadrootsNostrEventRef, farm::RadrootsFarm, @@ -31,8 +39,7 @@ pub use radroots_events::{ }; #[cfg(feature = "serde_json")] pub use radroots_events_codec::trade::{ - RadrootsTradeEnvelopeParseError, RadrootsTradeListingAddress, - RadrootsTradeListingAddressError, + RadrootsTradeEnvelopeParseError, RadrootsTradeListingAddress, RadrootsTradeListingAddressError, }; pub use radroots_events_codec::wire::{EventDraft as UnsignedEventDraft, WireEventParts}; pub use radroots_trade::listing::validation::RadrootsTradeListing as TradeListingValidateResult; diff --git a/crates/sdk/tests/config.rs b/crates/sdk/tests/config.rs @@ -0,0 +1,153 @@ +use radroots_sdk::{ + 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, RetryPolicy, SdkConfigError, SdkEnvironment, SdkTransportMode, SignerConfig, +}; + +#[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() { + 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 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 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 retry_policy_is_explicit_and_non_ambient() { + let config = RadrootsSdkConfig::default(); + + assert_eq!(config.network.retry_policy, RetryPolicy::None); +}