commit 6538de41d7992251e06205f1450f6348b7a55cc1
parent edefb4d696ae3588f1f6503f9817e296a40d3d1f
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 15:53:20 +0000
app-rt: delegate runtime behavior to radroots sdk
- Replace the parallel net runtime with one shared SDK client
- Keep executor lifecycle and presentation mapping in the mobile host
- Remove duplicate account relay listing and lower-package engines
- Verify registry-resolved native FFI and WASM workspace targets
Diffstat:
14 files changed, 259 insertions(+), 1793 deletions(-)
diff --git a/crates/mobile_core/Cargo.toml b/crates/mobile_core/Cargo.toml
@@ -17,28 +17,12 @@ crate-type = ["rlib"]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
[features]
-default = ["rt", "nostr-client"]
-rt = ["radroots_net_core/rt"]
-nostr-client = [
- "radroots_net_core/nostr-client",
- "dep:radroots_core",
- "dep:radroots_event_codec",
- "dep:radroots_trade",
- "dep:radroots_identity",
- "dep:radroots_nostr",
-]
-directories = ["radroots_net_core/directories"]
-fs-persistence = ["radroots_net_core/fs-persistence"]
+default = ["rt"]
+rt = []
[dependencies]
radroots_log = { workspace = true }
-radroots_event = { workspace = true }
-radroots_net_core = { workspace = true, features = ["std"] }
-radroots_core = { workspace = true, optional = true }
-radroots_identity = { workspace = true, optional = true, default-features = false, features = ["std"] }
-radroots_event_codec = { workspace = true, features = ["serde_json"], optional = true }
-radroots_trade = { workspace = true, features = ["std", "serde", "serde_json"], optional = true }
-radroots_nostr = { workspace = true, features = ["events"], optional = true }
+radroots_sdk = { workspace = true }
chrono = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
diff --git a/crates/mobile_core/src/error.rs b/crates/mobile_core/src/error.rs
@@ -19,6 +19,15 @@ pub enum RadrootsAppError {
}
impl RadrootsAppError {
+ pub(crate) fn from_sdk(error: radroots_sdk::Error) -> Self {
+ let report = error.to_report();
+ Self::Runtime(format!(
+ "{}: {}",
+ report.code().as_str(),
+ report.message().as_str()
+ ))
+ }
+
pub fn initialization(message: impl Into<String>) -> Self {
Self::Initialization(message.into())
}
diff --git a/crates/mobile_core/src/lib.rs b/crates/mobile_core/src/lib.rs
@@ -5,6 +5,4 @@ pub mod logging;
pub mod runtime;
pub use error::RadrootsAppError;
-pub use radroots_net_core::net::{BuildInfo, NetInfo};
-pub use radroots_net_core::{Net, NetHandle};
pub use runtime::RadrootsRuntime;
diff --git a/crates/mobile_core/src/runtime/builder.rs b/crates/mobile_core/src/runtime/builder.rs
@@ -1,61 +1,16 @@
-use radroots_net_core::NetHandle;
-use radroots_net_core::builder::NetBuilder;
-use radroots_net_core::config::NetConfig;
+use crate::{RadrootsAppError, RadrootsRuntime};
-use crate::RadrootsAppError;
-
-pub struct RuntimeBuilder {
- config: NetConfig,
- manage_runtime: bool,
-}
-
-impl Default for RuntimeBuilder {
- fn default() -> Self {
- Self::new()
- }
-}
+/// Host-owned construction boundary for the shared SDK-backed runtime.
+#[derive(Default)]
+pub struct RuntimeBuilder;
impl RuntimeBuilder {
- pub fn new() -> Self {
- Self {
- config: NetConfig::default(),
- manage_runtime: true,
- }
+ #[must_use]
+ pub const fn new() -> Self {
+ Self
}
- pub fn with_config(mut self, config: NetConfig) -> Self {
- self.config = config;
- self
- }
-
- pub fn manage_runtime(mut self, manage: bool) -> Self {
- self.manage_runtime = manage;
- self
- }
-
- pub fn build(self) -> Result<NetHandle, RadrootsAppError> {
- #[cfg(feature = "rt")]
- {
- match NetBuilder::new()
- .config(self.config)
- .manage_runtime(self.manage_runtime)
- .build()
- {
- Ok(handle) => Ok(handle),
- Err(err) => Err(RadrootsAppError::initialization(format!(
- "net build failed: {err}"
- ))),
- }
- }
-
- #[cfg(not(feature = "rt"))]
- {
- let handle = NetBuilder::new()
- .config(self.config)
- .manage_runtime(self.manage_runtime)
- .build()
- .expect("net build must succeed when rt feature is disabled");
- Ok(handle)
- }
+ pub fn build(self) -> Result<RadrootsRuntime, RadrootsAppError> {
+ RadrootsRuntime::new()
}
}
diff --git a/crates/mobile_core/src/runtime/info.rs b/crates/mobile_core/src/runtime/info.rs
@@ -1,10 +1,9 @@
use super::RadrootsRuntime;
use chrono::Utc;
-use radroots_net_core::net;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, Default, uniffi::Record)]
-pub struct NetBuildInfo {
+pub struct RuntimeBuildInfo {
pub crate_name: String,
pub crate_version: String,
pub rustc: Option<String>,
@@ -13,22 +12,9 @@ pub struct NetBuildInfo {
pub build_time_unix: Option<u64>,
}
-impl From<&net::BuildInfo> for NetBuildInfo {
- fn from(b: &net::BuildInfo) -> Self {
- Self {
- crate_name: b.crate_name.to_string(),
- crate_version: b.crate_version.to_string(),
- rustc: b.rustc.map(|s| s.to_string()),
- profile: b.profile.map(|s| s.to_string()),
- git_sha: b.git_sha.map(|s| s.to_string()),
- build_time_unix: b.build_time_unix,
- }
- }
-}
-
#[derive(Debug, Clone, Serialize, uniffi::Record)]
pub struct AppInfo {
- pub build: NetBuildInfo,
+ pub build: RuntimeBuildInfo,
pub started_unix_ms: i64,
pub uptime_millis: i64,
pub shutting_down: bool,
@@ -38,82 +24,56 @@ pub struct AppInfo {
#[derive(Debug, Clone, Serialize, uniffi::Record)]
pub struct RuntimeInfo {
pub app: AppInfo,
- pub net: NetBuildInfo,
+ pub sdk: RuntimeBuildInfo,
+ pub sdk_closed: bool,
}
pub fn gather_runtime_info(runtime: &RadrootsRuntime) -> RuntimeInfo {
let now_ms = Utc::now().timestamp_millis();
- let app_info = AppInfo {
- build: app_build_info(),
- started_unix_ms: runtime.started_unix_ms,
- uptime_millis: now_ms - runtime.started_unix_ms,
- shutting_down: runtime
- .shutting_down
- .load(std::sync::atomic::Ordering::SeqCst),
- platform: runtime.platform_app.read().ok().and_then(|g| (*g).clone()),
- };
-
- let net_info = match runtime.net.lock() {
- Ok(guard) => NetBuildInfo::from(&guard.info.build),
- Err(_) => NetBuildInfo::default(),
- };
-
RuntimeInfo {
- app: app_info,
- net: net_info,
+ app: AppInfo {
+ build: app_build_info(),
+ started_unix_ms: runtime.started_unix_ms,
+ uptime_millis: now_ms - runtime.started_unix_ms,
+ shutting_down: runtime
+ .shutting_down
+ .load(std::sync::atomic::Ordering::SeqCst),
+ platform: runtime
+ .platform_app
+ .read()
+ .ok()
+ .and_then(|value| (*value).clone()),
+ },
+ sdk: RuntimeBuildInfo {
+ crate_name: "radroots_sdk".to_owned(),
+ crate_version: "0.1.0-alpha".to_owned(),
+ ..RuntimeBuildInfo::default()
+ },
+ sdk_closed: runtime.client.is_closed(),
}
}
-pub fn app_build_info() -> NetBuildInfo {
- NetBuildInfo {
- crate_name: env!("CARGO_PKG_NAME").to_string(),
- crate_version: env!("CARGO_PKG_VERSION").to_string(),
- rustc: env_opt_to_owned(option_env!("RUSTC_VERSION")),
- profile: env_opt_to_owned(option_env!("PROFILE")),
- git_sha: env_opt_to_owned(option_env!("GIT_HASH")),
- build_time_unix: env_opt_to_u64(option_env!("BUILD_TIME_UNIX")),
+pub fn app_build_info() -> RuntimeBuildInfo {
+ RuntimeBuildInfo {
+ crate_name: env!("CARGO_PKG_NAME").to_owned(),
+ crate_version: env!("CARGO_PKG_VERSION").to_owned(),
+ rustc: option_env!("RUSTC_VERSION").map(str::to_owned),
+ profile: option_env!("PROFILE").map(str::to_owned),
+ git_sha: option_env!("GIT_HASH").map(str::to_owned),
+ build_time_unix: option_env!("BUILD_TIME_UNIX").and_then(|value| value.parse().ok()),
}
}
-fn env_opt_to_owned(value: Option<&str>) -> Option<String> {
- value.map(str::to_owned)
-}
-
-fn env_opt_to_u64(value: Option<&str>) -> Option<u64> {
- value.map(str::parse::<u64>).and_then(Result::ok)
-}
-
#[cfg(test)]
mod tests {
- use super::NetBuildInfo;
- use radroots_net_core::net;
-
- #[test]
- fn net_build_info_from_copies_optional_fields() {
- let source = net::BuildInfo {
- crate_name: "radroots-net-core",
- crate_version: "1.2.3",
- rustc: Some("rustc 1.97.0"),
- profile: Some("debug"),
- git_sha: Some("abc123"),
- build_time_unix: Some(1_700_000_000),
- };
-
- let out = NetBuildInfo::from(&source);
- assert_eq!(out.crate_name, "radroots-net-core");
- assert_eq!(out.crate_version, "1.2.3");
- assert_eq!(out.rustc.as_deref(), Some("rustc 1.97.0"));
- assert_eq!(out.profile.as_deref(), Some("debug"));
- assert_eq!(out.git_sha.as_deref(), Some("abc123"));
- assert_eq!(out.build_time_unix, Some(1_700_000_000));
- }
-
#[test]
- fn env_opt_helpers_cover_some_none_and_parse_failure() {
- assert_eq!(super::env_opt_to_owned(Some("abc")).as_deref(), Some("abc"));
- assert_eq!(super::env_opt_to_owned(None), None);
- assert_eq!(super::env_opt_to_u64(Some("123")), Some(123));
- assert_eq!(super::env_opt_to_u64(Some("abc")), None);
- assert_eq!(super::env_opt_to_u64(None), None);
+ fn build_info_uses_sdk_identity_without_lower_runtime_metadata() {
+ let runtime = super::RadrootsRuntime::new().expect("runtime");
+ let info = runtime.info();
+ assert_eq!(info.sdk.crate_name, "radroots_sdk");
+ assert_eq!(info.sdk.crate_version, "0.1.0-alpha");
+ assert!(!info.sdk_closed);
+ runtime.stop();
+ assert!(runtime.info().sdk_closed);
}
}
diff --git a/crates/mobile_core/src/runtime/key_management.rs b/crates/mobile_core/src/runtime/key_management.rs
@@ -1,365 +0,0 @@
-use super::RadrootsRuntime;
-use crate::RadrootsAppError;
-#[cfg(feature = "nostr-client")]
-use radroots_identity::{RadrootsIdentity, RadrootsIdentityId};
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrIdentityRecord {
- pub id: String,
- pub public_key_hex: String,
- pub public_key_npub: String,
- pub label: Option<String>,
- pub is_selected: bool,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrIdentitySnapshot {
- pub has_selected_signing_identity: bool,
- pub selected_identity_id: Option<String>,
- pub selected_npub: Option<String>,
- pub identities: Vec<NostrIdentityRecord>,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrHostCustodyIdentity {
- pub id: String,
- pub public_key_hex: String,
- pub public_key_npub: String,
-}
-
-#[cfg(feature = "nostr-client")]
-fn account_record(
- net: &radroots_net_core::Net,
- account_id: &RadrootsIdentityId,
-) -> Result<NostrIdentityRecord, RadrootsAppError> {
- let selected_identity_id = net
- .accounts
- .default_account_id()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- let account = net
- .accounts
- .list_accounts()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?
- .into_iter()
- .find(|account| &account.account_id == account_id)
- .ok_or_else(|| RadrootsAppError::identity(format!("identity not found: {account_id}")))?;
- let is_selected = selected_identity_id
- .as_ref()
- .map(|selected| selected == &account.account_id)
- .unwrap_or(false);
-
- Ok(NostrIdentityRecord {
- id: account.account_id.to_string(),
- public_key_hex: account.public_identity.public_key_hex,
- public_key_npub: account.public_identity.public_key_npub,
- label: account.label,
- is_selected,
- })
-}
-
-#[cfg(feature = "nostr-client")]
-fn invalidate_nostr_runtime(net: &mut radroots_net_core::Net) {
- net.set_nostr_signer(None);
- net.nostr = None;
-}
-
-#[cfg(feature = "nostr-client")]
-fn identity_from_secret(secret_key: &str) -> Result<RadrootsIdentity, RadrootsAppError> {
- RadrootsIdentity::from_secret_key_str(secret_key)
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))
-}
-
-#[cfg(feature = "nostr-client")]
-fn host_custody_identity_from_secret(
- secret_key: &str,
-) -> Result<(RadrootsIdentity, NostrHostCustodyIdentity), RadrootsAppError> {
- let identity = identity_from_secret(secret_key)?;
- let record = NostrHostCustodyIdentity {
- id: identity.id().to_string(),
- public_key_hex: identity.public_key_hex(),
- public_key_npub: identity.public_key_npub(),
- };
- Ok((identity, record))
-}
-
-#[cfg(feature = "nostr-client")]
-fn restore_host_custody_identity(
- net: &mut radroots_net_core::Net,
- identity: &RadrootsIdentity,
- label: Option<String>,
- make_selected: bool,
-) -> Result<NostrIdentityRecord, RadrootsAppError> {
- let account_id = net
- .accounts
- .upsert_identity(identity, label, make_selected)
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- invalidate_nostr_runtime(net);
- account_record(net, &account_id)
-}
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-impl RadrootsRuntime {
- pub fn nostr_identity_has_selected_signing_identity(&self) -> bool {
- #[cfg(feature = "nostr-client")]
- {
- if let Ok(guard) = self.net.lock() {
- return guard
- .accounts
- .default_signing_identity()
- .ok()
- .flatten()
- .is_some();
- }
- }
-
- #[cfg(not(feature = "nostr-client"))]
- {
- false
- }
-
- #[cfg(feature = "nostr-client")]
- false
- }
-
- pub fn nostr_identity_selected_npub(&self) -> Option<String> {
- #[cfg(feature = "nostr-client")]
- {
- if let Ok(guard) = self.net.lock() {
- return guard
- .accounts
- .default_public_identity()
- .ok()
- .flatten()
- .map(|identity| identity.public_key_npub);
- }
- }
-
- #[cfg(not(feature = "nostr-client"))]
- {
- None
- }
-
- #[cfg(feature = "nostr-client")]
- None
- }
-
- pub fn nostr_identity_list(&self) -> Result<Vec<NostrIdentityRecord>, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- let selected_identity_id = guard
- .accounts
- .default_account_id()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- let accounts = guard
- .accounts
- .list_accounts()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- Ok(accounts
- .into_iter()
- .map(|account| {
- let is_selected = selected_identity_id
- .as_ref()
- .map(|selected| selected == &account.account_id)
- .unwrap_or(false);
- NostrIdentityRecord {
- id: account.account_id.to_string(),
- public_key_hex: account.public_identity.public_key_hex,
- public_key_npub: account.public_identity.public_key_npub,
- label: account.label,
- is_selected,
- }
- })
- .collect())
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_identity_list_ids(&self) -> Result<Vec<String>, RadrootsAppError> {
- Ok(self
- .nostr_identity_list()?
- .into_iter()
- .map(|identity| identity.id)
- .collect())
- }
-
- pub fn nostr_identity_snapshot(&self) -> Result<NostrIdentitySnapshot, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- let selected_identity_id = guard
- .accounts
- .default_account_id()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- let selected_npub = guard
- .accounts
- .default_public_identity()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?
- .map(|identity| identity.public_key_npub);
- let has_selected_signing_identity = guard
- .accounts
- .default_signing_identity()
- .ok()
- .flatten()
- .is_some();
- let identities = guard
- .accounts
- .list_accounts()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?
- .into_iter()
- .map(|account| {
- let is_selected = selected_identity_id
- .as_ref()
- .map(|selected| selected == &account.account_id)
- .unwrap_or(false);
- NostrIdentityRecord {
- id: account.account_id.to_string(),
- public_key_hex: account.public_identity.public_key_hex,
- public_key_npub: account.public_identity.public_key_npub,
- label: account.label,
- is_selected,
- }
- })
- .collect();
- Ok(NostrIdentitySnapshot {
- has_selected_signing_identity,
- selected_identity_id: selected_identity_id.map(|id| id.to_string()),
- selected_npub,
- identities,
- })
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_identity_validate_host_custody_secret(
- &self,
- secret_key: String,
- ) -> Result<NostrHostCustodyIdentity, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- host_custody_identity_from_secret(secret_key.as_str()).map(|(_, identity)| identity)
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = secret_key;
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_identity_restore_host_custody_secret(
- &self,
- secret_key: String,
- label: Option<String>,
- make_selected: bool,
- ) -> Result<NostrIdentityRecord, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mut guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- let (identity, _) = host_custody_identity_from_secret(secret_key.as_str())?;
- restore_host_custody_identity(&mut guard, &identity, label, make_selected)
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = (secret_key, label, make_selected);
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_identity_select(&self, identity_id: String) -> Result<(), RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mut guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- let account_id = RadrootsIdentityId::parse(identity_id.as_str())
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- guard
- .accounts
- .set_default_account(&account_id)
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- invalidate_nostr_runtime(&mut guard);
- Ok(())
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = identity_id;
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_identity_remove(&self, identity_id: String) -> Result<(), RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mut guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- let account_id = RadrootsIdentityId::parse(identity_id.as_str())
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- guard
- .accounts
- .remove_account(&account_id)
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- invalidate_nostr_runtime(&mut guard);
- Ok(())
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = identity_id;
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_identity_lock_host_custody_runtime(&self) -> Result<(), RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mut guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- let accounts = guard
- .accounts
- .list_accounts()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- for account in accounts {
- guard
- .accounts
- .remove_account(&account.account_id)
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- }
- guard
- .accounts
- .clear_default_account()
- .map_err(|e| RadrootsAppError::identity(format!("{e}")))?;
- invalidate_nostr_runtime(&mut guard);
- if let Ok(mut rx_guard) = self.post_events_rx.lock() {
- *rx_guard = None;
- }
- Ok(())
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_identity_reset_host_custody_runtime(&self) -> Result<(), RadrootsAppError> {
- self.nostr_identity_lock_host_custody_runtime()
- }
-}
diff --git a/crates/mobile_core/src/runtime/mod.rs b/crates/mobile_core/src/runtime/mod.rs
@@ -1,22 +1,15 @@
pub mod app_info;
pub mod builder;
pub mod info;
-pub mod key_management;
-pub mod nostr;
pub mod product_surface;
-#[cfg(feature = "nostr-client")]
-pub mod trade_listing;
+pub mod sdk;
use chrono::Utc;
-use radroots_net_core::{NetHandle, builder::NetBuilder};
-#[cfg(feature = "nostr-client")]
-use std::sync::Mutex;
+use radroots_sdk::{Client, ClientBuilder};
use std::sync::{
- RwLock,
+ Mutex, RwLock,
atomic::{AtomicBool, Ordering},
};
-#[cfg(feature = "nostr-client")]
-use tokio::sync::broadcast::Receiver;
use self::{
app_info::AppInfoPlatform,
@@ -26,72 +19,60 @@ use crate::RadrootsAppError;
#[derive(uniffi::Object)]
pub struct RadrootsRuntime {
- pub(crate) net: NetHandle,
+ pub(crate) client: Client,
+ #[cfg(feature = "rt")]
+ executor: Mutex<Option<tokio::runtime::Runtime>>,
pub(crate) started_unix_ms: i64,
pub(crate) shutting_down: AtomicBool,
pub(crate) platform_app: RwLock<Option<AppInfoPlatform>>,
- #[cfg(feature = "nostr-client")]
- pub(crate) post_events_rx: Mutex<
- Option<
- Receiver<
- radroots_event_codec::parsed::RadrootsParsedData<
- radroots_event::post::RadrootsPost,
- >,
- >,
- >,
- >,
}
#[cfg_attr(not(coverage_nightly), uniffi::export)]
impl RadrootsRuntime {
#[cfg_attr(not(coverage_nightly), uniffi::constructor)]
pub fn new() -> Result<Self, RadrootsAppError> {
- let cfg = radroots_net_core::config::NetConfig::default();
+ let client = ClientBuilder::memory_default()
+ .build()
+ .map_err(RadrootsAppError::from_sdk)?;
#[cfg(feature = "rt")]
- let handle = match NetBuilder::new().config(cfg).manage_runtime(true).build() {
- Ok(handle) => handle,
- Err(err) => {
- return Err(RadrootsAppError::initialization(format!(
- "net build failed: {err}"
- )));
- }
- };
- #[cfg(not(feature = "rt"))]
- let handle = NetBuilder::new()
- .config(cfg)
- .manage_runtime(true)
+ let executor = tokio::runtime::Builder::new_multi_thread()
+ .thread_name("radroots-app-sdk")
+ .enable_all()
.build()
- .expect("net build must succeed when rt feature is disabled");
+ .map_err(|error| RadrootsAppError::initialization(error.to_string()))?;
Ok(Self {
- net: handle,
+ client,
+ #[cfg(feature = "rt")]
+ executor: Mutex::new(Some(executor)),
started_unix_ms: Utc::now().timestamp_millis(),
shutting_down: AtomicBool::new(false),
platform_app: RwLock::new(None),
- #[cfg(feature = "nostr-client")]
- post_events_rx: Mutex::new(None),
})
}
pub fn stop(&self) {
if self.shutting_down.swap(true, Ordering::SeqCst) {
let _ = crate::logging::log_info(
- "Runtime stop already in progress or completed.".to_string(),
+ "Runtime stop already in progress or completed.".to_owned(),
);
return;
}
#[cfg(feature = "rt")]
- {
- if let Ok(mut net) = self.net.lock() {
- if let Some(_rt) = net.rt.take() {
- let _ = crate::logging::log_info("Runtime stopped gracefully.".to_string());
- } else {
- let _ = crate::logging::log_info("No runtime was active at stop.".to_string());
+ match self.executor.lock() {
+ Ok(mut executor) => {
+ if let Some(executor) = executor.take() {
+ if let Err(error) = executor.block_on(self.client.close()) {
+ let _ = crate::logging::log_error(format!(
+ "SDK runtime close failed safely: {error}"
+ ));
+ }
}
- } else {
- let _ = crate::logging::log_info(
- "Failed to acquire runtime lock during stop.".to_string(),
+ }
+ Err(_) => {
+ let _ = crate::logging::log_error(
+ "SDK executor lock was unavailable during shutdown.".to_owned(),
);
}
}
@@ -99,7 +80,7 @@ impl RadrootsRuntime {
#[cfg(not(feature = "rt"))]
{
let _ = crate::logging::log_info(
- "No managed runtime is available for this build.".to_string(),
+ "Host must complete asynchronous SDK close for this runtime build.".to_owned(),
);
}
}
@@ -113,17 +94,8 @@ impl RadrootsRuntime {
}
pub fn info_json(&self) -> String {
- #[cfg(feature = "rt")]
- {
- match serde_json::to_string_pretty(&self.info()) {
- Ok(json) => json,
- Err(err) => format!(r#"{{"error":"serialize RuntimeInfo: {err}"}}"#),
- }
- }
- #[cfg(not(feature = "rt"))]
- {
- serde_json::to_string_pretty(&self.info()).unwrap_or_default()
- }
+ serde_json::to_string_pretty(&self.info())
+ .unwrap_or_else(|error| format!(r#"{{"error":"serialize RuntimeInfo: {error}"}}"#))
}
pub fn set_app_info_platform(
@@ -145,23 +117,9 @@ impl RadrootsRuntime {
#[cfg(test)]
mod tests {
use super::RadrootsRuntime;
+ use radroots_sdk::capability::{Availability, CapabilityId};
use std::panic::{AssertUnwindSafe, catch_unwind};
- fn init_info_logging() {
- let _ = tracing_subscriber::fmt()
- .with_test_writer()
- .with_max_level(tracing::Level::INFO)
- .try_init();
- }
-
- fn poison_net_lock(runtime: &RadrootsRuntime) {
- let handle = runtime.net.clone();
- let _ = catch_unwind(AssertUnwindSafe(|| {
- let _guard = handle.lock().expect("lock net");
- panic!("poison net lock");
- }));
- }
-
fn poison_platform_lock(runtime: &RadrootsRuntime) {
let _ = catch_unwind(AssertUnwindSafe(|| {
let _guard = runtime.platform_app.write().expect("lock platform");
@@ -170,47 +128,42 @@ mod tests {
}
#[test]
- fn runtime_info_uses_default_net_info_when_lock_is_poisoned() {
- init_info_logging();
+ fn runtime_owns_one_sdk_client_and_closes_idempotently() {
let runtime = RadrootsRuntime::new().expect("runtime");
+ let storage = runtime
+ .client
+ .capabilities()
+ .get(CapabilityId::CANONICAL_STORAGE)
+ .expect("storage capability");
+ assert_eq!(storage.availability(), Availability::Available);
+ assert!(!runtime.client.is_closed());
- let healthy = runtime.info();
- assert!(!healthy.net.crate_name.is_empty());
- poison_net_lock(&runtime);
-
- let _ = runtime.uptime_millis();
- let info = runtime.info();
- assert_eq!(info.net.crate_name, String::new());
- assert_eq!(info.net.crate_version, String::new());
- let json = runtime.info_json();
- assert!(json.contains("\"net\""));
runtime.stop();
+ assert!(runtime.client.is_closed());
runtime.stop();
}
#[test]
fn set_platform_info_handles_poisoned_lock() {
- init_info_logging();
let runtime = RadrootsRuntime::new().expect("runtime");
runtime.set_app_info_platform(
- Some("ios".to_string()),
- Some("org.radroots.app".to_string()),
- Some("1.0.0".to_string()),
- Some("100".to_string()),
- Some("abc123".to_string()),
+ Some("ios".to_owned()),
+ Some("org.radroots.app".to_owned()),
+ Some("1.0.0".to_owned()),
+ Some("100".to_owned()),
+ Some("abc123".to_owned()),
);
- let info = runtime.info();
assert_eq!(
- info.app.platform.as_ref().and_then(|v| v.platform.clone()),
- Some("ios".to_string())
+ runtime
+ .info()
+ .app
+ .platform
+ .as_ref()
+ .and_then(|value| value.platform.clone()),
+ Some("ios".to_owned())
);
poison_platform_lock(&runtime);
- runtime.set_app_info_platform(
- Some("ios".to_string()),
- Some("org.radroots.app".to_string()),
- Some("1.0.0".to_string()),
- Some("100".to_string()),
- Some("abc123".to_string()),
- );
+ runtime.set_app_info_platform(None, None, None, None, None);
+ runtime.stop();
}
}
diff --git a/crates/mobile_core/src/runtime/nostr.rs b/crates/mobile_core/src/runtime/nostr.rs
@@ -1,375 +0,0 @@
-use super::RadrootsRuntime;
-use crate::RadrootsAppError;
-#[cfg(feature = "nostr-client")]
-use tokio::sync::broadcast::error::TryRecvError;
-
-#[cfg(feature = "nostr-client")]
-fn nostr_manager(
- runtime: &RadrootsRuntime,
-) -> Result<radroots_net_core::nostr_client::NostrClientManager, RadrootsAppError> {
- let guard = runtime
- .net
- .lock()
- .map_err(|err| RadrootsAppError::runtime(format!("{err}")))?;
- guard
- .nostr
- .clone()
- .ok_or_else(|| RadrootsAppError::relay("nostr not initialized"))
-}
-
-#[derive(uniffi::Enum, Debug, Clone, Copy, PartialEq, Eq)]
-pub enum NostrLight {
- Red,
- Yellow,
- Green,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrConnectionStatus {
- pub light: NostrLight,
- pub connected: u32,
- pub connecting: u32,
- pub last_error: Option<String>,
-}
-
-#[derive(uniffi::Record, Debug, Clone, Default)]
-pub struct NostrProfile {
- pub name: Option<String>,
- pub display_name: Option<String>,
- pub nip05: Option<String>,
- pub about: Option<String>,
- pub website: Option<String>,
- pub picture: Option<String>,
- pub banner: Option<String>,
- pub lud06: Option<String>,
- pub lud16: Option<String>,
- pub bot: Option<String>,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrProfileEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u64,
- pub profile: NostrProfile,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrEvent {
- pub id: String,
- pub author: String,
- pub created_at: u64,
- pub kind: u32,
- pub content: String,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrPost {
- pub content: String,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct NostrPostEventMetadata {
- pub id: String,
- pub author: String,
- pub published_at: u64,
- pub post: NostrPost,
-}
-
-#[cfg(feature = "nostr-client")]
-fn map_post_event_metadata(
- event: radroots_event_codec::parsed::RadrootsParsedData<radroots_event::post::RadrootsPost>,
-) -> NostrPostEventMetadata {
- NostrPostEventMetadata {
- id: event.id,
- author: event.author,
- published_at: event.published_at,
- post: NostrPost {
- content: event.data.content,
- },
- }
-}
-
-#[cfg(feature = "nostr-client")]
-fn map_profile_event_metadata(
- event: radroots_event_codec::parsed::RadrootsParsedData<
- radroots_event_codec::profile::RadrootsProfileData,
- >,
-) -> NostrProfileEventMetadata {
- NostrProfileEventMetadata {
- id: event.id,
- author: event.author,
- published_at: event.published_at,
- profile: NostrProfile {
- name: event.data.profile.name.into(),
- display_name: event.data.profile.display_name,
- nip05: event.data.profile.nip05,
- about: event.data.profile.about,
- website: event.data.profile.website,
- picture: event.data.profile.picture,
- banner: event.data.profile.banner,
- lud06: event.data.profile.lud06,
- lud16: event.data.profile.lud16,
- bot: event.data.profile.bot,
- },
- }
-}
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-impl RadrootsRuntime {
- pub fn nostr_set_default_relays(&self, relays: Vec<String>) -> Result<(), RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mut guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- guard
- .nostr_set_default_relays(&relays)
- .map_err(|e| RadrootsAppError::relay(format!("{e}")))
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = relays;
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_connect_if_key_present(&self) -> Result<(), RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mut guard = match self.net.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- guard
- .nostr_connect_if_key_present()
- .map_err(|e| RadrootsAppError::relay(format!("{e}")))
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_connection_status(&self) -> NostrConnectionStatus {
- #[cfg(feature = "nostr-client")]
- {
- let guard = self.net.lock();
- if let Ok(g) = guard
- && let Some(s) = g.nostr_connection_snapshot()
- {
- let light = match s.light {
- radroots_net_core::nostr_client::Light::Green => NostrLight::Green,
- radroots_net_core::nostr_client::Light::Yellow => NostrLight::Yellow,
- radroots_net_core::nostr_client::Light::Red => NostrLight::Red,
- };
- return NostrConnectionStatus {
- light,
- connected: s.connected as u32,
- connecting: s.connecting as u32,
- last_error: s.last_error,
- };
- }
- NostrConnectionStatus {
- light: NostrLight::Red,
- connected: 0,
- connecting: 0,
- last_error: None,
- }
- }
-
- #[cfg(not(feature = "nostr-client"))]
- {
- NostrConnectionStatus {
- light: NostrLight::Red,
- connected: 0,
- connecting: 0,
- last_error: None,
- }
- }
- }
-
- pub fn nostr_profile_for_self(
- &self,
- ) -> Result<Option<NostrProfileEventMetadata>, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let (pk, mgr) = {
- let guard = self
- .net
- .lock()
- .map_err(|err| RadrootsAppError::runtime(format!("{err}")))?;
- let keys = guard.selected_nostr_keys().ok_or_else(|| {
- RadrootsAppError::identity("selected signing identity is not configured")
- })?;
- let pk = keys.public_key();
- let mgr = guard
- .nostr
- .clone()
- .ok_or_else(|| RadrootsAppError::relay("nostr not initialized"))?;
- (pk, mgr)
- };
- let out = mgr
- .fetch_profile_event_blocking(pk)
- .map_err(|error| RadrootsAppError::relay(error.to_string()))?;
- Ok(out.map(map_profile_event_metadata))
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_post_profile(
- &self,
- name: Option<String>,
- display_name: Option<String>,
- nip05: Option<String>,
- about: Option<String>,
- ) -> Result<String, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mgr = nostr_manager(self)?;
- mgr.publish_profile_event_blocking(name, display_name, nip05, about)
- .map_err(|e| RadrootsAppError::relay(e.to_string()))
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = (name, display_name, nip05, about);
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_post_text_note(&self, content: String) -> Result<String, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mgr = nostr_manager(self)?;
- mgr.publish_post_event_blocking(content)
- .map_err(|e| RadrootsAppError::relay(e.to_string()))
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = content;
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_fetch_text_notes(
- &self,
- limit: u16,
- since_unix: Option<u64>,
- ) -> Result<Vec<NostrPostEventMetadata>, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mgr = nostr_manager(self)?;
- let items = mgr
- .fetch_post_events_blocking(limit, since_unix)
- .map_err(|e| RadrootsAppError::relay(e.to_string()))?;
- Ok(items.into_iter().map(map_post_event_metadata).collect())
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = (limit, since_unix);
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_post_reply(
- &self,
- parent_event_id_hex: String,
- parent_author_hex: String,
- content: String,
- root_event_id_hex: Option<String>,
- ) -> Result<String, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mgr = nostr_manager(self)?;
- mgr.publish_post_reply_event_blocking(
- parent_event_id_hex,
- parent_author_hex,
- content,
- root_event_id_hex,
- )
- .map_err(|e| RadrootsAppError::relay(e.to_string()))
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = (
- parent_event_id_hex,
- parent_author_hex,
- content,
- root_event_id_hex,
- );
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_start_post_event_stream(
- &self,
- since_unix: Option<u64>,
- ) -> Result<(), RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mgr = nostr_manager(self)?;
- mgr.start_post_event_stream(since_unix);
- if let Ok(mut rx_guard) = self.post_events_rx.lock()
- && rx_guard.is_none()
- {
- *rx_guard = Some(mgr.subscribe_post_events());
- }
- Ok(())
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = since_unix;
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_next_post_event(
- &self,
- ) -> Result<Option<NostrPostEventMetadata>, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mut rx_guard = match self.post_events_rx.lock() {
- Ok(guard) => guard,
- Err(err) => return Err(RadrootsAppError::runtime(format!("{err}"))),
- };
- let Some(rx) = rx_guard.as_mut() else {
- return Ok(None);
- };
- match rx.try_recv() {
- Ok(event) => Ok(Some(map_post_event_metadata(event))),
- Err(TryRecvError::Empty) => Ok(None),
- Err(TryRecvError::Lagged(count)) => Err(RadrootsAppError::relay(format!(
- "post event stream lagged by {count} events"
- ))),
- Err(TryRecvError::Closed) => {
- *rx_guard = None;
- Err(RadrootsAppError::relay("post event stream closed"))
- }
- }
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn nostr_stop_post_event_stream(&self) -> Result<(), RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mgr = nostr_manager(self)?;
- mgr.stop_post_event_stream();
- if let Ok(mut rx_guard) = self.post_events_rx.lock() {
- *rx_guard = None;
- }
- Ok(())
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-}
diff --git a/crates/mobile_core/src/runtime/sdk.rs b/crates/mobile_core/src/runtime/sdk.rs
@@ -0,0 +1,105 @@
+use radroots_sdk::capability::{Availability, Maturity};
+
+use super::RadrootsRuntime;
+use crate::RadrootsAppError;
+
+#[derive(Clone, Debug, uniffi::Record)]
+pub struct SdkCapabilityRecord {
+ pub id: String,
+ pub compiled: bool,
+ pub configured: bool,
+ pub availability: String,
+ pub maturity: String,
+}
+
+#[derive(Clone, Debug, uniffi::Record)]
+pub struct SdkStorageStatusRecord {
+ pub backend: String,
+ pub open_mode: String,
+ pub shutdown: String,
+ pub integrity: String,
+}
+
+#[cfg_attr(not(coverage_nightly), uniffi::export)]
+impl RadrootsRuntime {
+ pub fn sdk_capabilities(&self) -> Vec<SdkCapabilityRecord> {
+ self.client
+ .capabilities()
+ .iter()
+ .map(|status| SdkCapabilityRecord {
+ id: status.id().as_str().to_owned(),
+ compiled: status.is_compiled(),
+ configured: status.is_configured(),
+ availability: availability_label(status.availability()).to_owned(),
+ maturity: maturity_label(status.maturity()).to_owned(),
+ })
+ .collect()
+ }
+
+ pub fn sdk_storage_status(&self) -> Result<SdkStorageStatusRecord, RadrootsAppError> {
+ #[cfg(feature = "rt")]
+ {
+ let executor = self
+ .executor
+ .lock()
+ .map_err(|_| RadrootsAppError::runtime("SDK executor lock is unavailable"))?;
+ let executor = executor
+ .as_ref()
+ .ok_or_else(|| RadrootsAppError::runtime("SDK runtime is closed"))?;
+ let status = executor
+ .block_on(self.client.storage_status())
+ .map_err(RadrootsAppError::from_sdk)?;
+ Ok(SdkStorageStatusRecord {
+ backend: format!("{:?}", status.backend()).to_ascii_lowercase(),
+ open_mode: format!("{:?}", status.open_mode()).to_ascii_lowercase(),
+ shutdown: format!("{:?}", status.shutdown()).to_ascii_lowercase(),
+ integrity: format!("{:?}", status.integrity().health()).to_ascii_lowercase(),
+ })
+ }
+ #[cfg(not(feature = "rt"))]
+ {
+ Err(RadrootsAppError::unsupported(
+ "SDK storage status requires a host async executor",
+ ))
+ }
+ }
+}
+
+const fn availability_label(value: Availability) -> &'static str {
+ match value {
+ Availability::Available => "available",
+ Availability::Degraded => "degraded",
+ Availability::Unavailable => "unavailable",
+ Availability::Unsupported => "unsupported",
+ }
+}
+
+const fn maturity_label(value: Maturity) -> &'static str {
+ match value {
+ Maturity::Stable => "stable",
+ Maturity::Preview => "preview",
+ Maturity::Experimental => "experimental",
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::RadrootsRuntime;
+
+ #[test]
+ fn sdk_records_are_stable_and_storage_is_memory_backed() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+ let capabilities = runtime.sdk_capabilities();
+ assert!(capabilities.iter().any(|capability| {
+ capability.id == "storage.canonical"
+ && capability.configured
+ && capability.availability == "available"
+ && capability.maturity == "stable"
+ }));
+ let status = runtime.sdk_storage_status().expect("storage status");
+ assert_eq!(status.backend, "memory");
+ assert_eq!(status.integrity, "healthy");
+ runtime.stop();
+ assert!(runtime.sdk_storage_status().is_err());
+ }
+}
diff --git a/crates/mobile_core/src/runtime/trade_listing.rs b/crates/mobile_core/src/runtime/trade_listing.rs
@@ -1,382 +0,0 @@
-#![forbid(unsafe_code)]
-
-use core::str::FromStr;
-
-use radroots_core::{
- RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
- RadrootsCoreQuantityPrice, RadrootsCoreUnit,
-};
-use radroots_event::{
- RadrootsEventEnvelope,
- farm::RadrootsFarmRef,
- ids::{RadrootsDTag, RadrootsInventoryBinId},
- kinds::KIND_LISTING,
- listing::{
- RadrootsListing, RadrootsListingAvailability, RadrootsListingBin,
- RadrootsListingDeliveryMethod, RadrootsListingProduct, RadrootsListingPublicLocation,
- RadrootsListingStatus,
- },
-};
-use radroots_event_codec::listing::encode::to_wire_parts as listing_to_wire_parts;
-use radroots_nostr::prelude::{
- RadrootsNostrFilter, RadrootsNostrKind, RadrootsNostrTimestamp, radroots_event_from_nostr,
-};
-use radroots_trade::listing::validation::validate_listing_event;
-
-use super::RadrootsRuntime;
-use crate::RadrootsAppError;
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct TradeListingDraft {
- pub listing_id: Option<String>,
- pub farm_pubkey: String,
- pub farm_d_tag: String,
- pub title: String,
- pub description: String,
- pub category: String,
- pub bin_display_amount: String,
- pub bin_display_unit: String,
- pub unit_price: String,
- pub currency: String,
- pub bin_label: Option<String>,
- pub bin_id: Option<String>,
- pub inventory: String,
- pub delivery_method: String,
- pub location_primary: String,
- pub location_city: Option<String>,
- pub location_region: Option<String>,
- pub location_country: Option<String>,
- pub location_geohash: String,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct TradeListingSummary {
- pub event_id: String,
- pub seller_pubkey: String,
- pub published_at: u64,
- pub listing_id: String,
- pub listing_addr: String,
- pub title: String,
- pub description: String,
- pub product_type: String,
- pub primary_bin_id: String,
- pub unit_price_amount: String,
- pub unit_price_currency: String,
- pub unit_price_unit: String,
- pub bin_display_amount: String,
- pub bin_display_unit: String,
- pub bin_display_label: Option<String>,
- pub inventory_available: String,
- pub availability: String,
- pub location: String,
- pub delivery_method: String,
-}
-
-#[derive(uniffi::Record, Debug, Clone)]
-pub struct TradeListingEventParts {
- pub kind: u32,
- pub content: String,
- pub tags_json: String,
-}
-
-#[cfg_attr(not(coverage_nightly), uniffi::export)]
-impl RadrootsRuntime {
- pub fn trade_listing_build_event_parts(
- &self,
- draft: TradeListingDraft,
- ) -> Result<TradeListingEventParts, RadrootsAppError> {
- let listing = listing_from_draft(&draft)?;
- let parts = listing_to_wire_parts(&listing).map_err(|error| {
- RadrootsAppError::runtime(format!("listing encode failed: {error}"))
- })?;
- let tags_json = serde_json::to_string(&parts.tags).map_err(|error| {
- RadrootsAppError::runtime(format!("listing tags encode failed: {error}"))
- })?;
- Ok(TradeListingEventParts {
- kind: parts.kind,
- content: parts.content,
- tags_json,
- })
- }
-
- pub fn trade_listing_publish(
- &self,
- draft: TradeListingDraft,
- ) -> Result<String, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let (current_pubkey, mgr) = {
- let guard = self
- .net
- .lock()
- .map_err(|error| RadrootsAppError::runtime(format!("{error}")))?;
- let current_pubkey = guard
- .accounts
- .default_public_identity()
- .map_err(|error| RadrootsAppError::identity(format!("{error}")))?
- .ok_or_else(|| RadrootsAppError::identity("default account is not configured"))?
- .public_key_hex;
- let mgr = guard
- .nostr
- .clone()
- .ok_or_else(|| RadrootsAppError::relay("nostr not initialized"))?;
- (current_pubkey, mgr)
- };
- let listing = listing_from_draft(&draft)?;
- if listing.farm.pubkey != current_pubkey {
- return Err(RadrootsAppError::runtime(
- "farm_pubkey must match the default account public key",
- ));
- }
- let parts = listing_to_wire_parts(&listing).map_err(|error| {
- RadrootsAppError::runtime(format!("listing encode failed: {error}"))
- })?;
- mgr.send_custom_event_blocking(parts.kind, parts.content, parts.tags)
- .map_err(|error| RadrootsAppError::relay(error.to_string()))
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = draft;
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-
- pub fn trade_listings_fetch(
- &self,
- limit: u16,
- since_unix: Option<u64>,
- ) -> Result<Vec<TradeListingSummary>, RadrootsAppError> {
- #[cfg(feature = "nostr-client")]
- {
- let mgr = {
- let guard = self
- .net
- .lock()
- .map_err(|error| RadrootsAppError::runtime(format!("{error}")))?;
- guard
- .nostr
- .clone()
- .ok_or_else(|| RadrootsAppError::relay("nostr not initialized"))?
- };
- let mut filter =
- RadrootsNostrFilter::new().kind(RadrootsNostrKind::Custom(KIND_LISTING as u16));
- filter = filter.limit(limit.into());
- if let Some(since) = since_unix {
- filter = filter.since(RadrootsNostrTimestamp::from(since));
- }
-
- let events = mgr
- .fetch_events_blocking(filter, core::time::Duration::from_secs(10))
- .map_err(|error| RadrootsAppError::relay(error.to_string()))?;
- let mut out = Vec::new();
- for event in events {
- let event = radroots_event_from_nostr(&event);
- if let Ok(listing) = validate_listing_event(&event) {
- out.push(listing_summary_from_trade(listing, &event));
- }
- }
- out.sort_by_key(|listing| std::cmp::Reverse(listing.published_at));
- Ok(out)
- }
- #[cfg(not(feature = "nostr-client"))]
- {
- let _ = (limit, since_unix);
- Err(RadrootsAppError::unsupported("nostr disabled"))
- }
- }
-}
-
-fn listing_from_draft(draft: &TradeListingDraft) -> Result<RadrootsListing, RadrootsAppError> {
- let listing_id = non_empty(
- draft
- .listing_id
- .clone()
- .unwrap_or_else(|| format!("listing-{}", chrono::Utc::now().timestamp_millis())),
- "listing_id",
- )?;
- let farm_pubkey = non_empty(draft.farm_pubkey.clone(), "farm_pubkey")?;
- let farm_d_tag = non_empty(draft.farm_d_tag.clone(), "farm_d_tag")?;
- let title = non_empty(draft.title.clone(), "title")?;
- let description = non_empty(draft.description.clone(), "description")?;
- let category = non_empty(draft.category.clone(), "category")?;
- let bin_id = non_empty(
- draft.bin_id.clone().unwrap_or_else(|| "bin-1".to_string()),
- "bin_id",
- )?;
- let listing_id = RadrootsDTag::parse(listing_id)
- .map_err(|error| RadrootsAppError::runtime(format!("invalid listing_id: {error}")))?;
- let bin_id = RadrootsInventoryBinId::parse(bin_id)
- .map_err(|error| RadrootsAppError::runtime(format!("invalid bin_id: {error}")))?;
- let amount = parse_decimal(&draft.bin_display_amount, "bin_display_amount")?;
- let unit = parse_unit(&draft.bin_display_unit)?;
- let canonical_unit = unit.canonical_unit();
- let currency = parse_currency(&draft.currency)?;
- let unit_price = parse_decimal(&draft.unit_price, "unit_price")?;
- let inventory = parse_decimal(&draft.inventory, "inventory")?;
- let location_primary = non_empty(draft.location_primary.clone(), "location_primary")?;
- let location_geohash = non_empty(draft.location_geohash.clone(), "location_geohash")?;
-
- Ok(RadrootsListing {
- d_tag: listing_id,
- published_at: None,
- farm: RadrootsFarmRef {
- pubkey: farm_pubkey,
- d_tag: farm_d_tag,
- },
- product: RadrootsListingProduct {
- key: category.clone(),
- title,
- category,
- summary: Some(description),
- process: None,
- lot: None,
- location: None,
- profile: None,
- year: None,
- },
- primary_bin_id: bin_id.clone(),
- bins: vec![RadrootsListingBin {
- bin_id,
- quantity: RadrootsCoreQuantity::new(amount, canonical_unit),
- price_per_canonical_unit: RadrootsCoreQuantityPrice::new(
- RadrootsCoreMoney::new(unit_price, currency),
- RadrootsCoreQuantity::new(RadrootsCoreDecimal::ONE, canonical_unit),
- ),
- display_amount: Some(amount),
- display_unit: Some(unit),
- display_label: draft.bin_label.clone(),
- display_price: Some(RadrootsCoreMoney::new(unit_price, currency)),
- display_price_unit: Some(unit),
- }],
- resource_area: None,
- plot: None,
- discounts: None,
- inventory_available: Some(inventory),
- availability: Some(RadrootsListingAvailability::Status {
- status: RadrootsListingStatus::Active,
- }),
- delivery_method: Some(parse_delivery_method(&draft.delivery_method)),
- location: Some(RadrootsListingPublicLocation {
- primary: location_primary,
- city: blank_to_none(draft.location_city.clone()),
- region: blank_to_none(draft.location_region.clone()),
- country: blank_to_none(draft.location_country.clone()),
- geohash: location_geohash,
- }),
- images: None,
- })
-}
-
-fn listing_summary_from_trade(
- listing: radroots_trade::listing::validation::RadrootsTradeListing,
- event: &RadrootsEventEnvelope,
-) -> TradeListingSummary {
- let primary_bin = listing
- .listing
- .bins
- .iter()
- .find(|bin| bin.bin_id == listing.primary_bin_id);
- TradeListingSummary {
- event_id: event.id_str().to_owned(),
- seller_pubkey: listing.seller_pubkey,
- published_at: event.created_at_u64(),
- listing_id: listing.listing_id,
- listing_addr: listing.listing_addr,
- title: listing.title,
- description: listing.description,
- product_type: listing.product_type,
- primary_bin_id: listing.primary_bin_id,
- unit_price_amount: listing.unit_price.amount.to_string(),
- unit_price_currency: listing.unit_price.currency.to_string(),
- unit_price_unit: listing.unit.to_string(),
- bin_display_amount: primary_bin
- .and_then(|bin| bin.display_amount)
- .unwrap_or(listing.bin_quantity.amount)
- .to_string(),
- bin_display_unit: primary_bin
- .and_then(|bin| bin.display_unit)
- .unwrap_or(listing.unit)
- .to_string(),
- bin_display_label: primary_bin.and_then(|bin| bin.display_label.clone()),
- inventory_available: listing.inventory_available.to_string(),
- availability: availability_label(&listing.availability),
- location: location_label(&listing.location),
- delivery_method: delivery_method_label(&listing.delivery_method),
- }
-}
-
-fn non_empty(value: String, field: &str) -> Result<String, RadrootsAppError> {
- let value = value.trim().to_string();
- if value.is_empty() {
- return Err(RadrootsAppError::runtime(format!("{field} is required")));
- }
- Ok(value)
-}
-
-fn blank_to_none(value: Option<String>) -> Option<String> {
- value
- .map(|value| value.trim().to_string())
- .filter(|value| !value.is_empty())
-}
-
-fn parse_decimal(value: &str, field: &str) -> Result<RadrootsCoreDecimal, RadrootsAppError> {
- RadrootsCoreDecimal::from_str(value.trim())
- .map_err(|error| RadrootsAppError::runtime(format!("{field} is invalid: {error}")))
-}
-
-fn parse_currency(value: &str) -> Result<RadrootsCoreCurrency, RadrootsAppError> {
- RadrootsCoreCurrency::from_str(value.trim())
- .map_err(|error| RadrootsAppError::runtime(format!("currency is invalid: {error}")))
-}
-
-fn parse_unit(value: &str) -> Result<RadrootsCoreUnit, RadrootsAppError> {
- RadrootsCoreUnit::from_str(value.trim())
- .map_err(|error| RadrootsAppError::runtime(format!("unit is invalid: {error}")))
-}
-
-fn parse_delivery_method(value: &str) -> RadrootsListingDeliveryMethod {
- match value.trim().to_ascii_lowercase().as_str() {
- "pickup" => RadrootsListingDeliveryMethod::Pickup,
- "local_delivery" | "local delivery" => RadrootsListingDeliveryMethod::LocalDelivery,
- "shipping" => RadrootsListingDeliveryMethod::Shipping,
- other => RadrootsListingDeliveryMethod::Other {
- method: other.to_string(),
- },
- }
-}
-
-fn availability_label(value: &RadrootsListingAvailability) -> String {
- match value {
- RadrootsListingAvailability::Window { start, end } => {
- format!("window:{start:?}:{end:?}")
- }
- RadrootsListingAvailability::Status { status } => match status {
- RadrootsListingStatus::Active => "active".to_string(),
- RadrootsListingStatus::Sold => "sold".to_string(),
- RadrootsListingStatus::Other { value } => value.clone(),
- },
- }
-}
-
-fn delivery_method_label(value: &RadrootsListingDeliveryMethod) -> String {
- match value {
- RadrootsListingDeliveryMethod::Pickup => "pickup".to_string(),
- RadrootsListingDeliveryMethod::LocalDelivery => "local_delivery".to_string(),
- RadrootsListingDeliveryMethod::Shipping => "shipping".to_string(),
- RadrootsListingDeliveryMethod::Other { method } => method.clone(),
- }
-}
-
-fn location_label(value: &RadrootsListingPublicLocation) -> String {
- [
- Some(value.primary.as_str()),
- value.city.as_deref(),
- value.region.as_deref(),
- value.country.as_deref(),
- ]
- .into_iter()
- .flatten()
- .filter(|part| !part.trim().is_empty())
- .collect::<Vec<_>>()
- .join(", ")
-}
diff --git a/crates/mobile_core/tests/error_mapping.rs b/crates/mobile_core/tests/error_mapping.rs
@@ -1,127 +0,0 @@
-#![cfg(feature = "nostr-client")]
-
-use std::{sync::mpsc, time::Duration};
-
-use radroots_app_core::runtime::trade_listing::TradeListingDraft;
-use radroots_app_core::{RadrootsAppError, RadrootsRuntime};
-
-#[test]
-fn invalid_host_custody_secret_maps_to_identity_error() {
- let runtime = RadrootsRuntime::new().expect("runtime");
-
- let err = runtime
- .nostr_identity_validate_host_custody_secret("not-a-secret".to_string())
- .expect_err("invalid secret should fail");
-
- assert!(matches!(err, RadrootsAppError::Identity(_)));
-}
-
-#[test]
-fn uninitialized_nostr_publish_maps_to_relay_error() {
- let runtime = RadrootsRuntime::new().expect("runtime");
-
- let err = runtime
- .nostr_post_text_note("hello".to_string())
- .expect_err("uninitialized nostr should fail");
-
- assert!(matches!(
- err,
- RadrootsAppError::Relay(message) if message == "nostr not initialized"
- ));
-}
-
-#[test]
-fn profile_read_without_identity_maps_to_identity_error() {
- let runtime = RadrootsRuntime::new().expect("runtime");
-
- let err = runtime
- .nostr_profile_for_self()
- .expect_err("missing identity should fail");
-
- assert!(matches!(err, RadrootsAppError::Identity(_)));
-}
-
-#[test]
-fn profile_read_without_initialized_nostr_maps_to_relay_error() {
- let runtime = RadrootsRuntime::new().expect("runtime");
- let identity = radroots_identity::RadrootsIdentity::generate();
- runtime
- .nostr_identity_restore_host_custody_secret(
- identity.secret_key_hex(),
- Some("field".to_string()),
- true,
- )
- .expect("restore identity");
-
- let err = runtime
- .nostr_profile_for_self()
- .expect_err("uninitialized nostr should fail");
-
- assert!(matches!(
- err,
- RadrootsAppError::Relay(message) if message == "nostr not initialized"
- ));
-}
-
-#[test]
-fn post_stream_read_without_started_stream_returns_no_data() {
- let runtime = RadrootsRuntime::new().expect("runtime");
-
- let event = runtime
- .nostr_next_post_event()
- .expect("missing stream should be a no-data state");
-
- assert!(event.is_none());
-}
-
-#[test]
-fn trade_listing_publish_with_initialized_nostr_does_not_relock_runtime() {
- let runtime = RadrootsRuntime::new().expect("runtime");
- let identity = radroots_identity::RadrootsIdentity::generate();
- let public_key_hex = identity.public_key_hex();
- runtime
- .nostr_identity_restore_host_custody_secret(
- identity.secret_key_hex(),
- Some("field".to_string()),
- true,
- )
- .expect("restore identity");
- runtime
- .nostr_set_default_relays(Vec::new())
- .expect("initialize nostr manager");
- let draft = TradeListingDraft {
- listing_id: Some("AAAAAAAAAAAAAAAAAAAAAg".to_string()),
- farm_pubkey: public_key_hex,
- farm_d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
- title: "Carrots".to_string(),
- description: "Fresh carrots".to_string(),
- category: "produce".to_string(),
- bin_display_amount: "1".to_string(),
- bin_display_unit: "lb".to_string(),
- unit_price: "3.50".to_string(),
- currency: "USD".to_string(),
- bin_label: None,
- bin_id: Some("bin-1".to_string()),
- inventory: "10".to_string(),
- delivery_method: "pickup".to_string(),
- location_primary: "farm stand".to_string(),
- location_city: Some("Asheville".to_string()),
- location_region: None,
- location_country: None,
- location_geohash: "9q8yy".to_string(),
- };
- let (tx, rx) = mpsc::channel();
- std::thread::spawn(move || {
- let result = runtime.trade_listing_publish(draft);
- let _ = tx.send(result);
- });
-
- let result = rx
- .recv_timeout(Duration::from_secs(3))
- .expect("publish must return instead of re-locking the runtime");
-
- match result {
- Ok(_) | Err(RadrootsAppError::Relay(_)) => {}
- other => panic!("unexpected publish result: {other:?}"),
- }
-}
diff --git a/crates/mobile_core/tests/key_management.rs b/crates/mobile_core/tests/key_management.rs
@@ -1,137 +0,0 @@
-#![cfg(feature = "nostr-client")]
-
-use radroots_app_core::RadrootsRuntime;
-
-#[test]
-fn identity_reset_all_removes_selected_and_unselected_identities() {
- let runtime = RadrootsRuntime::new().expect("runtime");
- let selected_identity = radroots_identity::RadrootsIdentity::generate();
- let other_identity = radroots_identity::RadrootsIdentity::generate();
-
- let selected = runtime
- .nostr_identity_restore_host_custody_secret(
- selected_identity.secret_key_hex(),
- Some("selected".to_string()),
- true,
- )
- .expect("selected identity");
- let other = runtime
- .nostr_identity_restore_host_custody_secret(
- other_identity.secret_key_hex(),
- Some("other".to_string()),
- false,
- )
- .expect("other identity");
-
- let snapshot = runtime.nostr_identity_snapshot().expect("snapshot");
- assert!(snapshot.has_selected_signing_identity);
- assert_eq!(
- snapshot.selected_identity_id.as_deref(),
- Some(selected.id.as_str())
- );
- assert!(selected.is_selected);
- assert!(!other.is_selected);
- assert_eq!(snapshot.identities.len(), 2);
-
- runtime
- .nostr_identity_reset_host_custody_runtime()
- .expect("reset all");
-
- let snapshot = runtime.nostr_identity_snapshot().expect("reset snapshot");
- assert!(!snapshot.has_selected_signing_identity);
- assert_eq!(snapshot.selected_identity_id, None);
- assert_eq!(snapshot.selected_npub, None);
- assert!(snapshot.identities.is_empty());
- assert!(runtime.nostr_identity_list().expect("list").is_empty());
-
- assert!(runtime.nostr_identity_remove(other.id).is_err());
-}
-
-#[test]
-fn host_custody_secret_validation_derives_public_identity_without_runtime_mutation() {
- let runtime = RadrootsRuntime::new().expect("runtime");
- let host_identity = radroots_identity::RadrootsIdentity::generate();
-
- let validated = runtime
- .nostr_identity_validate_host_custody_secret(host_identity.secret_key_hex())
- .expect("validate host custody secret");
-
- assert_eq!(validated.id, host_identity.id().to_string());
- assert_eq!(validated.public_key_hex, host_identity.public_key_hex());
- assert_eq!(validated.public_key_npub, host_identity.public_key_npub());
- assert!(!runtime.nostr_identity_has_selected_signing_identity());
- assert!(
- runtime
- .nostr_identity_snapshot()
- .expect("snapshot")
- .identities
- .is_empty()
- );
-}
-
-#[test]
-fn host_custody_secret_restore_recreates_runtime_signing_identity_after_lock() {
- let runtime = RadrootsRuntime::new().expect("runtime");
- let host_identity = radroots_identity::RadrootsIdentity::generate();
- let secret_key = host_identity.secret_key_hex();
-
- let restored = runtime
- .nostr_identity_restore_host_custody_secret(
- secret_key.clone(),
- Some("local custody".to_string()),
- true,
- )
- .expect("restore host secret");
- assert_eq!(restored.public_key_hex, host_identity.public_key_hex());
- assert!(restored.is_selected);
- assert!(runtime.nostr_identity_has_selected_signing_identity());
-
- runtime
- .nostr_identity_lock_host_custody_runtime()
- .expect("clear runtime state");
- let locked = runtime.nostr_identity_snapshot().expect("locked snapshot");
- assert!(!locked.has_selected_signing_identity);
- assert!(locked.identities.is_empty());
- assert!(!runtime.nostr_identity_has_selected_signing_identity());
-
- let restored_again = runtime
- .nostr_identity_restore_host_custody_secret(
- secret_key,
- Some("local custody".to_string()),
- true,
- )
- .expect("restore host secret again");
- assert_eq!(
- restored_again.public_key_hex,
- host_identity.public_key_hex()
- );
- assert!(runtime.nostr_identity_has_selected_signing_identity());
-}
-
-#[test]
-fn invalid_host_custody_secret_is_rejected_before_runtime_mutation() {
- let runtime = RadrootsRuntime::new().expect("runtime");
-
- assert!(
- runtime
- .nostr_identity_validate_host_custody_secret("not-a-secret".to_string())
- .is_err()
- );
- assert!(
- runtime
- .nostr_identity_restore_host_custody_secret(
- "not-a-secret".to_string(),
- Some("bad".to_string()),
- true,
- )
- .is_err()
- );
- assert!(!runtime.nostr_identity_has_selected_signing_identity());
- assert!(
- runtime
- .nostr_identity_snapshot()
- .expect("snapshot")
- .identities
- .is_empty()
- );
-}
diff --git a/crates/mobile_core/tests/no_nostr_runtime.rs b/crates/mobile_core/tests/no_nostr_runtime.rs
@@ -1,150 +0,0 @@
-#![cfg(not(feature = "nostr-client"))]
-
-use radroots_app_core::logging;
-use radroots_app_core::runtime::builder::RuntimeBuilder;
-use radroots_app_core::runtime::nostr::{
- NostrConnectionStatus, NostrEvent, NostrLight, NostrPost, NostrPostEventMetadata, NostrProfile,
- NostrProfileEventMetadata,
-};
-use radroots_app_core::{RadrootsAppError, RadrootsRuntime};
-use radroots_net_core::config::NetConfig;
-
-fn expect_disabled<T>(result: Result<T, RadrootsAppError>) {
- match result {
- Err(RadrootsAppError::Unsupported(message)) => assert_eq!(message, "nostr disabled"),
- _ => panic!("expected nostr disabled error"),
- }
-}
-
-#[test]
-fn runtime_info_and_platform_paths_are_exercised() {
- let runtime = RadrootsRuntime::new().expect("runtime");
- assert!(runtime.uptime_millis() >= 0);
-
- runtime.stop();
- runtime.stop();
-}
-
-#[test]
-fn key_management_disabled_paths_are_exercised() {
- let runtime = RadrootsRuntime::new().expect("runtime");
-
- assert!(!runtime.nostr_identity_has_selected_signing_identity());
- assert_eq!(runtime.nostr_identity_selected_npub(), None);
- expect_disabled(runtime.nostr_identity_list());
- expect_disabled(runtime.nostr_identity_list_ids());
- expect_disabled(runtime.nostr_identity_snapshot());
- expect_disabled(runtime.nostr_identity_validate_host_custody_secret("deadbeef".to_string()));
- expect_disabled(runtime.nostr_identity_restore_host_custody_secret(
- "deadbeef".to_string(),
- Some("alpha".to_string()),
- true,
- ));
- expect_disabled(runtime.nostr_identity_select("account-1".to_string()));
- expect_disabled(runtime.nostr_identity_remove("account-1".to_string()));
- expect_disabled(runtime.nostr_identity_lock_host_custody_runtime());
- expect_disabled(runtime.nostr_identity_reset_host_custody_runtime());
-}
-
-#[test]
-fn nostr_disabled_paths_are_exercised() {
- let runtime = RadrootsRuntime::new().expect("runtime");
-
- let status = runtime.nostr_connection_status();
- assert_eq!(status.connected, 0);
- assert_eq!(status.connecting, 0);
- assert!(status.last_error.is_none());
-
- expect_disabled(runtime.nostr_profile_for_self());
- expect_disabled(runtime.nostr_next_post_event());
- expect_disabled(runtime.nostr_set_default_relays(vec!["wss://relay.example.com".to_string()]));
- expect_disabled(runtime.nostr_connect_if_key_present());
- expect_disabled(runtime.nostr_post_profile(None, None, None, None));
- expect_disabled(runtime.nostr_post_text_note("hello".to_string()));
- expect_disabled(runtime.nostr_fetch_text_notes(25, Some(0)));
- expect_disabled(runtime.nostr_post_reply(
- "event-id".to_string(),
- "author".to_string(),
- "reply".to_string(),
- None,
- ));
- expect_disabled(runtime.nostr_start_post_event_stream(None));
- expect_disabled(runtime.nostr_stop_post_event_stream());
-}
-
-#[test]
-fn runtime_builder_and_logging_paths_are_exercised() {
- let handle = RuntimeBuilder::new()
- .with_config(NetConfig::default())
- .manage_runtime(false)
- .build()
- .expect("build net handle");
- drop(handle);
- let default_handle = RuntimeBuilder::new()
- .build()
- .expect("build default net handle");
- drop(default_handle);
-
- let err = logging::init_logging(Some("/dev/null/file.log".to_string()), None, Some(false));
- assert!(matches!(err, Err(RadrootsAppError::Initialization(_))));
- let _ = logging::init_logging(None, None, None);
- let _ = logging::init_logging(None, Some("app.log".to_string()), Some(false));
- let _ = logging::init_logging_stdout();
-
- assert!(logging::log_info("info".to_string()).is_ok());
- assert!(logging::log_error("error".to_string()).is_ok());
- assert!(logging::log_debug("debug".to_string()).is_ok());
-}
-
-#[test]
-fn nostr_records_and_enums_are_exercised() {
- let status = NostrConnectionStatus {
- light: NostrLight::Yellow,
- connected: 1,
- connecting: 2,
- last_error: Some("err".to_string()),
- };
- let _status_debug = format!("{status:?}");
- let _status_clone = status.clone();
-
- let profile = NostrProfile::default();
- let _profile_debug = format!("{profile:?}");
- let _profile_clone = profile.clone();
-
- let profile_event = NostrProfileEventMetadata {
- id: "id".to_string(),
- author: "author".to_string(),
- published_at: 1,
- profile,
- };
- let _profile_event_debug = format!("{profile_event:?}");
- let _profile_event_clone = profile_event.clone();
-
- let event = NostrEvent {
- id: "event-id".to_string(),
- author: "event-author".to_string(),
- created_at: 2,
- kind: 1,
- content: "content".to_string(),
- };
- let _event_debug = format!("{event:?}");
- let _event_clone = event.clone();
-
- let post = NostrPost {
- content: "post".to_string(),
- };
- let _post_debug = format!("{post:?}");
- let _post_clone = post.clone();
-
- let post_event = NostrPostEventMetadata {
- id: "id".to_string(),
- author: "author".to_string(),
- published_at: 3,
- post,
- };
- let _post_event_debug = format!("{post_event:?}");
- let _post_event_clone = post_event.clone();
-
- assert!(matches!(NostrLight::Red, NostrLight::Red));
- assert!(matches!(NostrLight::Green, NostrLight::Green));
-}
diff --git a/crates/mobile_core/tests/sdk_runtime.rs b/crates/mobile_core/tests/sdk_runtime.rs
@@ -0,0 +1,38 @@
+use std::sync::Arc;
+
+use radroots_app_core::{RadrootsAppError, RadrootsRuntime};
+
+#[test]
+fn runtime_is_send_sync_and_shares_one_sdk_lifecycle() {
+ fn require_send_sync<T: Send + Sync>() {}
+ require_send_sync::<RadrootsRuntime>();
+
+ let runtime = Arc::new(RadrootsRuntime::new().expect("runtime"));
+ let worker = {
+ let runtime = Arc::clone(&runtime);
+ std::thread::spawn(move || runtime.sdk_capabilities())
+ };
+ let capabilities = worker.join().expect("worker");
+ assert!(
+ capabilities
+ .iter()
+ .any(|capability| capability.id == "storage.canonical")
+ );
+ runtime.stop();
+ runtime.stop();
+ assert!(runtime.info().sdk_closed);
+}
+
+#[test]
+fn operations_fail_safely_after_explicit_close() {
+ let runtime = RadrootsRuntime::new().expect("runtime");
+ assert_eq!(
+ runtime.sdk_storage_status().expect("status").backend,
+ "memory"
+ );
+ runtime.stop();
+ assert!(matches!(
+ runtime.sdk_storage_status(),
+ Err(RadrootsAppError::Runtime(_))
+ ));
+}