net.rs (7715B)
1 use serde::Serialize; 2 use std::sync::{Arc, Mutex, MutexGuard}; 3 4 use crate::error::{NetError, Result}; 5 #[cfg(feature = "nostr-client")] 6 use crate::nostr_client::{NostrClientManager, NostrConnectionSnapshot}; 7 #[cfg(feature = "nostr-client")] 8 use radroots_nostr_accounts::prelude::RadrootsNostrAccountsManager; 9 #[cfg(feature = "nostr-client")] 10 use radroots_nostr_signer::prelude::RadrootsNostrSignerCapability; 11 12 #[derive(Debug, Clone, Serialize)] 13 pub struct BuildInfo { 14 pub crate_name: &'static str, 15 pub crate_version: &'static str, 16 #[serde(skip_serializing_if = "Option::is_none")] 17 pub rustc: Option<&'static str>, 18 #[serde(skip_serializing_if = "Option::is_none")] 19 pub profile: Option<&'static str>, 20 #[serde(skip_serializing_if = "Option::is_none")] 21 pub git_sha: Option<&'static str>, 22 #[serde(skip_serializing_if = "Option::is_none")] 23 pub build_time_unix: Option<u64>, 24 } 25 26 #[derive(Debug, Clone, Serialize)] 27 pub struct NetInfo { 28 pub build: BuildInfo, 29 } 30 31 pub struct Net { 32 pub info: NetInfo, 33 pub config: crate::config::NetConfig, 34 35 #[cfg(feature = "nostr-client")] 36 pub accounts: RadrootsNostrAccountsManager, 37 38 #[cfg(feature = "nostr-client")] 39 pub signer: Option<RadrootsNostrSignerCapability>, 40 41 #[cfg(feature = "nostr-client")] 42 pub nostr: Option<NostrClientManager>, 43 44 #[cfg(feature = "rt")] 45 pub rt: Option<tokio::runtime::Runtime>, 46 } 47 48 impl Net { 49 pub fn new(cfg: crate::config::NetConfig) -> Self { 50 Self { 51 info: NetInfo { 52 build: BuildInfo { 53 crate_name: env!("CARGO_PKG_NAME"), 54 crate_version: env!("CARGO_PKG_VERSION"), 55 rustc: option_env!("RUSTC_VERSION"), 56 profile: option_env!("PROFILE"), 57 git_sha: option_env!("GIT_HASH"), 58 build_time_unix: option_env!("BUILD_TIME_UNIX").and_then(|s| s.parse().ok()), 59 }, 60 }, 61 config: cfg, 62 #[cfg(feature = "nostr-client")] 63 accounts: RadrootsNostrAccountsManager::new_in_memory(), 64 #[cfg(feature = "nostr-client")] 65 signer: None, 66 #[cfg(feature = "nostr-client")] 67 nostr: None, 68 #[cfg(feature = "rt")] 69 rt: None, 70 } 71 } 72 73 #[cfg(feature = "rt")] 74 pub fn init_managed_runtime(&mut self, worker_threads: Option<usize>) -> Result<()> { 75 if self.rt.is_some() { 76 return Ok(()); 77 } 78 79 let threads = worker_threads.unwrap_or_else(|| { 80 std::thread::available_parallelism() 81 .map(|n| n.get()) 82 .unwrap_or(1) 83 .max(1) 84 }); 85 86 let rt = tokio::runtime::Builder::new_multi_thread() 87 .worker_threads(threads) 88 .enable_all() 89 .build() 90 .map_err(|e| NetError::msg(format!("failed to build tokio runtime: {e}")))?; 91 92 self.rt = Some(rt); 93 Ok(()) 94 } 95 96 #[cfg(feature = "nostr-client")] 97 pub fn nostr_set_default_relays(&mut self, urls: &[String]) -> Result<()> { 98 if self.nostr.is_none() { 99 let keys = self.selected_nostr_keys().ok_or(NetError::MissingKey)?; 100 let rt = self 101 .rt 102 .as_ref() 103 .ok_or_else(|| NetError::msg("tokio runtime missing"))?; 104 self.nostr = Some(NostrClientManager::new(keys, rt.handle().clone())); 105 } 106 if let Some(n) = &self.nostr { 107 n.set_relays(urls); 108 } 109 Ok(()) 110 } 111 112 #[cfg(feature = "nostr-client")] 113 pub fn nostr_connect_if_key_present(&mut self) -> Result<()> { 114 let Some(keys) = self.selected_nostr_keys() else { 115 return Ok(()); 116 }; 117 let rt = self 118 .rt 119 .as_ref() 120 .ok_or_else(|| NetError::msg("tokio runtime missing"))?; 121 if self.nostr.is_none() { 122 self.nostr = Some(NostrClientManager::new(keys, rt.handle().clone())); 123 } 124 if let Some(n) = &self.nostr { 125 n.connect()?; 126 } 127 Ok(()) 128 } 129 130 #[cfg(feature = "nostr-client")] 131 pub fn nostr_connection_snapshot(&self) -> Option<NostrConnectionSnapshot> { 132 self.nostr.as_ref().map(|n| n.snapshot()) 133 } 134 135 #[cfg(feature = "nostr-client")] 136 pub fn set_nostr_signer(&mut self, signer: Option<RadrootsNostrSignerCapability>) { 137 self.signer = signer; 138 } 139 140 #[cfg(feature = "nostr-client")] 141 pub fn selected_nostr_signer(&self) -> Option<RadrootsNostrSignerCapability> { 142 self.signer 143 .clone() 144 .or_else(|| self.accounts.default_signer_capability().ok().flatten()) 145 } 146 147 #[cfg(feature = "nostr-client")] 148 pub fn selected_nostr_keys(&self) -> Option<radroots_nostr::prelude::RadrootsNostrKeys> { 149 let signer = self.selected_nostr_signer()?; 150 self.accounts 151 .resolve_signing_identity_for_signer(&signer) 152 .ok() 153 .flatten() 154 .map(|identity| identity.into_keys()) 155 } 156 } 157 158 #[derive(Clone)] 159 pub struct NetHandle(Arc<Mutex<Net>>); 160 161 impl NetHandle { 162 pub fn from_inner(inner: Net) -> Self { 163 Self(Arc::new(Mutex::new(inner))) 164 } 165 166 pub fn lock(&self) -> Result<MutexGuard<'_, Net>> { 167 self.0.lock().map_err(|_| NetError::Poisoned) 168 } 169 } 170 171 #[cfg(test)] 172 mod tests { 173 use crate::builder::NetBuilder; 174 #[cfg(feature = "nostr-client")] 175 use radroots_identity::RadrootsIdentity; 176 #[cfg(feature = "nostr-client")] 177 use radroots_nostr_signer::prelude::{ 178 RadrootsNostrRemoteSessionSignerCapability, RadrootsNostrSignerCapability, 179 RadrootsNostrSignerConnectionId, 180 }; 181 182 #[test] 183 fn builds_minimal() { 184 let cfg = crate::config::NetConfig::default(); 185 let handle = NetBuilder::new().config(cfg).build(); 186 assert!(handle.is_ok()); 187 } 188 189 #[test] 190 fn lock_is_ok() { 191 let cfg = crate::config::NetConfig::default(); 192 let handle = NetBuilder::new().config(cfg).build().unwrap(); 193 let guard = handle.lock(); 194 assert!(guard.is_ok()); 195 } 196 197 #[cfg(feature = "rt")] 198 #[test] 199 fn builds_with_managed_rt() { 200 let cfg = crate::config::NetConfig::default(); 201 let handle = crate::builder::NetBuilder::new() 202 .config(cfg) 203 .manage_runtime(true) 204 .build() 205 .expect("build with runtime"); 206 207 let rt_present = handle.lock().unwrap().rt.is_some(); 208 assert!(rt_present); 209 } 210 211 #[cfg(feature = "nostr-client")] 212 #[test] 213 fn selected_nostr_keys_reflects_selected_signing_account() { 214 let cfg = crate::config::NetConfig::default(); 215 let mut net = crate::Net::new(cfg); 216 assert!(net.selected_nostr_keys().is_none()); 217 assert!(net.selected_nostr_signer().is_none()); 218 219 net.accounts 220 .generate_identity(Some("primary".into()), true) 221 .expect("generate account"); 222 assert!(net.selected_nostr_keys().is_some()); 223 assert!(net.selected_nostr_signer().is_some()); 224 225 let remote = RadrootsNostrSignerCapability::RemoteSession(Box::new( 226 RadrootsNostrRemoteSessionSignerCapability::new( 227 RadrootsNostrSignerConnectionId::new_v7(), 228 RadrootsIdentity::generate().to_public(), 229 RadrootsIdentity::generate().to_public(), 230 ), 231 )); 232 net.set_nostr_signer(Some(remote.clone())); 233 assert_eq!(net.selected_nostr_signer(), Some(remote)); 234 assert!(net.selected_nostr_keys().is_none()); 235 236 net.set_nostr_signer(None); 237 assert!(net.selected_nostr_keys().is_some()); 238 } 239 }