commit 6df2858a4101aa78008a3fe64a100c548f4f17b6
parent ba8d92e530c5576d6c2ad3ca0ec43a92a16fbe7e
Author: triesap <tyson@radroots.org>
Date: Tue, 14 Apr 2026 04:52:09 +0000
config: prefer root env for local sdk defaults
Diffstat:
2 files changed, 125 insertions(+), 5 deletions(-)
diff --git a/crates/sdk/src/config.rs b/crates/sdk/src/config.rs
@@ -2,7 +2,7 @@
use alloc::{string::String, vec::Vec};
use core::fmt;
#[cfg(feature = "std")]
-use std::{string::String, vec::Vec};
+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";
@@ -15,6 +15,19 @@ 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 = "NOSTR_RS_RELAY_PUBLIC_SCHEME";
+#[cfg(feature = "std")]
+const LOCAL_RELAY_HOST_ENV: &str = "NOSTR_RS_RELAY_PUBLIC_HOST";
+#[cfg(feature = "std")]
+const LOCAL_RELAY_PORT_ENV: &str = "NOSTR_RS_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,
@@ -113,6 +126,12 @@ impl RelayConfig {
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);
@@ -132,10 +151,19 @@ 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),
+ 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)
+ }
}
}
}
@@ -265,3 +293,32 @@ fn normalize_radrootsd_endpoint(value: &str) -> Result<String, SdkConfigError> {
}
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/tests/config.rs b/crates/sdk/tests/config.rs
@@ -4,6 +4,43 @@ use radroots_sdk::{
RADROOTS_SDK_STAGING_RADROOTSD_ENDPOINT, RADROOTS_SDK_STAGING_RELAY_URL, RadrootsSdkConfig,
RadrootsdAuth, SdkConfigError, SdkEnvironment, SdkTransportMode, SignerConfig,
};
+use std::sync::{Mutex, OnceLock};
+
+fn sdk_env_lock() -> &'static Mutex<()> {
+ static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+ LOCK.get_or_init(|| Mutex::new(()))
+}
+
+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 saved = pairs
+ .iter()
+ .map(|(key, _)| (key.to_string(), std::env::var(key).ok()))
+ .collect::<Vec<_>>();
+
+ for (key, value) in pairs {
+ // The global lock keeps env mutation single-threaded for this test file.
+ unsafe {
+ std::env::set_var(key, value);
+ }
+ }
+
+ test();
+
+ for (key, original) in saved {
+ match original {
+ Some(value) => unsafe {
+ std::env::set_var(&key, value);
+ },
+ None => unsafe {
+ std::env::remove_var(&key);
+ },
+ }
+ }
+}
#[test]
fn default_config_uses_production_relay_direct_draft_only() {
@@ -65,6 +102,32 @@ fn local_environment_resolves_localhost_defaults() {
}
#[test]
+fn local_environment_prefers_root_env_contract_when_present() {
+ with_local_sdk_env(
+ &[
+ ("NOSTR_RS_RELAY_PUBLIC_SCHEME", "ws"),
+ ("NOSTR_RS_RELAY_PUBLIC_HOST", "127.0.0.1"),
+ ("NOSTR_RS_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 explicit_coordinates_override_environment_defaults_exactly() {
let mut config = RadrootsSdkConfig::production();
config.relay.urls = vec![