lib

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

commit 35c1c179234f92c3ecd16a933797c2c89264fd56
parent 131c2c558406037e1cf9f011d296c78cb8b3005a
Author: triesap <tyson@radroots.org>
Date:   Thu, 28 Aug 2025 09:57:56 +0000

net-core: add `radroots-net` crate

Diffstat:
M.gitignore | 2++
MCargo.lock | 16++++++++++++++++
MCargo.toml | 14+++++++++++++-
Mcrates/identity/src/error.rs | 1-
Mcrates/identity/src/lib.rs | 3+--
Mcrates/identity/src/spec.rs | 9---------
Acrates/net-core/Cargo.toml | 17+++++++++++++++++
Acrates/net-core/build.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/net-core/src/builder.rs | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/net-core/src/config.rs | 4++++
Acrates/net-core/src/error.rs | 22++++++++++++++++++++++
Acrates/net-core/src/lib.rs | 8++++++++
Acrates/net-core/src/net.rs | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/net/Cargo.toml | 16++++++++++++++++
Acrates/net/src/lib.rs | 1+
15 files changed, 352 insertions(+), 13 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -26,6 +26,8 @@ Thumbs.db *.crt *.key +# Testing +test*.json # Editors .vscode/ .idea/ diff --git a/Cargo.lock b/Cargo.lock @@ -1635,6 +1635,22 @@ dependencies = [ ] [[package]] +name = "radroots-net" +version = "0.1.0" +dependencies = [ + "radroots-net-core", +] + +[[package]] +name = "radroots-net-core" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror 1.0.69", + "tokio", +] + +[[package]] name = "radroots-nostr" version = "0.1.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -18,18 +18,29 @@ radroots-events-indexed = { path = "crates/events-indexed", version = "0.1.0", d radroots-identity = { path = "crates/identity", version = "0.1.0", default-features = false } radroots-nostr = { path = "crates/nostr", version = "0.1.0", default-features = false } radroots-runtime = { path = "crates/runtime", version = "0.1.0", default-features = false } +radroots-net = { path = "crates/net", version = "0.1.0", default-features = false } +radroots-net-core = { path = "crates/net-core", version = "0.1.0", default-features = false } radroots-trade = { path = "crates/trade", version = "0.1.0", default-features = false } anyhow = { version = "1" } +cfg-if = { version = "1" } +chrono = { version = "0.4" } clap = { version = "4" } config = { version = "0.14" } -nostr-sdk = { version = "0.43.0" } +directories = { version = "6" } +futures = { version = "0.3" } +hex = { version = "0.4" } nostr = { version = "0.43.0" } +nostr-relay-pool = { version = "0.43.0" } +nostr-sdk = { version = "0.43.0" } +num_cpus = { version = "1.17.0" } +secrecy = { version = "0.10.3" } serde = { version = "1", default-features = false } serde_json = { version = "1", default-features = false } reqwest = { version = "0.12", default-features = false } rust_decimal = { version = "1", default-features = false } rust_decimal_macros = { version = "1" } +sled = { version = "0.34" } tempfile = { version = "3" } thiserror = { version = "1" } tokio = { version = "1" } @@ -38,4 +49,5 @@ tracing = { version = "0.1" } tracing-subscriber = { version = "0.3" } tracing-appender = { version = "0.2" } typeshare = { version = "1" } +url = { version = "2" } uuid = { version = "1.16.0" } diff --git a/crates/identity/src/error.rs b/crates/identity/src/error.rs @@ -2,7 +2,6 @@ use radroots_runtime::RuntimeJsonError; use std::path::PathBuf; use thiserror::Error; -/// Errors when loading or generating an identity. #[derive(Debug, Error)] pub enum IdentityError { #[error(transparent)] diff --git a/crates/identity/src/lib.rs b/crates/identity/src/lib.rs @@ -2,7 +2,6 @@ pub mod error; pub mod spec; pub use error::IdentityError; -pub use spec::{to_keys, load_or_generate, IdentitySpec, MinimalIdentity, ExtendedIdentity}; +pub use spec::{ExtendedIdentity, IdentitySpec, MinimalIdentity, load_or_generate, to_keys}; -/// The canonical default identity file path. pub const DEFAULT_IDENTITY_PATH: &str = "identity.json"; diff --git a/crates/identity/src/spec.rs b/crates/identity/src/spec.rs @@ -7,29 +7,21 @@ use std::{ }; use uuid::Uuid; -/// Trait that identity file types must implement. pub trait IdentitySpec: Serialize + DeserializeOwned + Sized { - /// The runtime key material type (e.g. `nostr::Keys`). type Keys; - /// Error type when parsing stored material into keys. type ParseError: std::error::Error + Send + Sync + 'static; - /// Create a brand new identity value if the file does not exist. fn generate_new() -> Self; - /// Turn this identity into runtime key material. fn to_keys(&self) -> Result<Self::Keys, Self::ParseError>; } -/// Convert an identity into its keys, mapped into the shared error type. pub fn to_keys<I: IdentitySpec>(id: &I) -> Result<I::Keys, IdentityError> { id.to_keys() .map_err(|e| IdentityError::Invalid(Box::new(e))) } -/// Load an identity file, or generate a new one if allowed. -/// Defaults to [`DEFAULT_IDENTITY_PATH`] if no path is provided. pub fn load_or_generate<I, P>( path: Option<P>, allow_generate: bool, @@ -55,7 +47,6 @@ where Ok(store) } -/// A minimal identity: just a secret key string. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MinimalIdentity { pub key: String, diff --git a/crates/net-core/Cargo.toml b/crates/net-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "radroots-net-core" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true + +[features] +default = ["std"] +std = [] +rt = ["std", "dep:tokio"] + +[dependencies] +serde = { workspace = true, features = ["derive"] } +thiserror = { workspace = true } +tokio = { workspace = true, optional = true, features = ["rt-multi-thread"] } diff --git a/crates/net-core/build.rs b/crates/net-core/build.rs @@ -0,0 +1,81 @@ +use std::env; +use std::path::PathBuf; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH"); + + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()); + + let mut dir = PathBuf::from(&manifest_dir); + let git_dir = loop { + if dir.join(".git").exists() { + break dir.join(".git"); + } + if !dir.pop() { + break PathBuf::from(".git"); + } + }; + + if git_dir.exists() { + println!("cargo:rerun-if-changed={}", git_dir.join("HEAD").display()); + println!( + "cargo:rerun-if-changed={}", + git_dir.join("refs/heads").display() + ); + println!("cargo:rerun-if-changed={}", git_dir.join("index").display()); + } + + let build_time_unix = env::var("SOURCE_DATE_EPOCH") + .ok() + .and_then(|v| v.parse::<u64>().ok()) + .unwrap_or_else(|| { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) + }); + println!("cargo:rustc-env=BUILD_TIME_UNIX={}", build_time_unix); + + let rustc_bin = env::var("RUSTC").unwrap_or_else(|_| "rustc".into()); + if let Ok(out) = Command::new(rustc_bin).arg("--version").output() { + if out.status.success() { + if let Ok(ver) = String::from_utf8(out.stdout) { + println!("cargo:rustc-env=RUSTC_VERSION={}", ver.trim()); + } + } + } + + let git_hash = Command::new("git") + .args(["rev-parse", "--short=12", "HEAD"]) + .output() + .ok() + .and_then(|o| { + if o.status.success() { + String::from_utf8(o.stdout).ok() + } else { + None + } + }) + .map(|s| s.trim().to_string()); + + let dirty = Command::new("git") + .args(["status", "--porcelain"]) + .output() + .ok() + .map(|o| o.status.success() && !o.stdout.is_empty()) + .unwrap_or(false); + + if let Some(mut h) = git_hash { + if dirty { + h.push_str("-dirty"); + } + println!("cargo:rustc-env=GIT_HASH={}", h); + } + + if let Ok(profile) = env::var("PROFILE") { + println!("cargo:rustc-env=PROFILE={}", profile); + } +} diff --git a/crates/net-core/src/builder.rs b/crates/net-core/src/builder.rs @@ -0,0 +1,50 @@ +use crate::config::NetConfig; +use crate::error::Result; +use crate::{Net, NetHandle}; + +#[derive(Debug, Clone)] +pub struct NetBuilder { + config: NetConfig, + manage_runtime: bool, +} + +impl Default for NetBuilder { + fn default() -> Self { + Self { + config: NetConfig::default(), + manage_runtime: false, + } + } +} + +impl NetBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn config(mut self, cfg: NetConfig) -> Self { + self.config = cfg; + self + } + + pub fn manage_runtime(mut self, yes: bool) -> Self { + self.manage_runtime = yes; + self + } + + #[allow(unreachable_code)] + pub fn build(self) -> Result<NetHandle> { + let net = Net::new(self.config.clone()); + + #[cfg(feature = "rt")] + { + let mut net = net; + if self.manage_runtime { + net.init_managed_runtime(None)?; + } + return Ok(NetHandle::from_inner(net)); + } + + Ok(NetHandle::from_inner(net)) + } +} diff --git a/crates/net-core/src/config.rs b/crates/net-core/src/config.rs @@ -0,0 +1,4 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct NetConfig {} diff --git a/crates/net-core/src/error.rs b/crates/net-core/src/error.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("{0}")] + Msg(String), + + #[error("mutex lock poisoned!")] + Poisoned, + + #[cfg(feature = "std")] + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), +} + +impl Error { + pub fn msg<M: Into<String>>(msg: M) -> Self { + Error::Msg(msg.into()) + } +} + +pub type Result<T> = core::result::Result<T, Error>; diff --git a/crates/net-core/src/lib.rs b/crates/net-core/src/lib.rs @@ -0,0 +1,8 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod builder; +pub mod config; +pub mod error; +pub mod net; + +pub use net::{Net, NetHandle}; diff --git a/crates/net-core/src/net.rs b/crates/net-core/src/net.rs @@ -0,0 +1,121 @@ +use serde::Serialize; +use std::sync::{Arc, Mutex, MutexGuard}; + +use crate::error::{Error, Result}; + +#[derive(Debug, Clone, Serialize)] +pub struct BuildInfo { + pub crate_name: &'static str, + pub crate_version: &'static str, + #[serde(skip_serializing_if = "Option::is_none")] + pub rustc: Option<&'static str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub profile: Option<&'static str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub git_sha: Option<&'static str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub build_time_unix: Option<u64>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct NetInfo { + pub build: BuildInfo, +} + +pub struct Net { + pub info: NetInfo, + pub config: crate::config::NetConfig, + + #[cfg(feature = "rt")] + pub rt: Option<tokio::runtime::Runtime>, +} + +impl Net { + pub fn new(cfg: crate::config::NetConfig) -> Self { + Self { + info: NetInfo { + build: BuildInfo { + crate_name: env!("CARGO_PKG_NAME"), + crate_version: env!("CARGO_PKG_VERSION"), + rustc: option_env!("RUSTC_VERSION"), + profile: option_env!("PROFILE"), + git_sha: option_env!("GIT_HASH"), + build_time_unix: option_env!("BUILD_TIME_UNIX").and_then(|s| s.parse().ok()), + }, + }, + config: cfg, + #[cfg(feature = "rt")] + rt: None, + } + } + + #[cfg(feature = "rt")] + pub fn init_managed_runtime(&mut self, worker_threads: Option<usize>) -> Result<()> { + if self.rt.is_some() { + return Ok(()); + } + + let threads = worker_threads.unwrap_or_else(|| { + std::thread::available_parallelism() + .map(|n| n.get()) + .unwrap_or(1) + .max(1) + }); + + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(threads) + .enable_all() + .build() + .map_err(|e| Error::msg(format!("failed to build tokio runtime: {e}")))?; + + self.rt = Some(rt); + Ok(()) + } +} + +#[derive(Clone)] +pub struct NetHandle(Arc<Mutex<Net>>); + +impl NetHandle { + pub fn from_inner(inner: Net) -> Self { + Self(Arc::new(Mutex::new(inner))) + } + + pub fn lock(&self) -> Result<MutexGuard<'_, Net>> { + self.0.lock().map_err(|_| Error::Poisoned) + } +} + +#[cfg(test)] +mod tests { + use crate::builder::NetBuilder; + + #[test] + fn builds_minimal() { + let cfg = crate::config::NetConfig::default(); + let handle = NetBuilder::new().config(cfg).build(); + assert!(handle.is_ok()); + } + + #[test] + fn lock_is_ok() { + let cfg = crate::config::NetConfig::default(); + let handle = NetBuilder::new().config(cfg).build().unwrap(); + let guard = handle.lock(); + assert!(guard.is_ok()); + } + + #[cfg(feature = "rt")] + #[test] + fn builds_with_managed_rt() { + let cfg = crate::config::NetConfig::default(); + let handle = crate::builder::NetBuilder::new() + .config(cfg) + .manage_runtime(true) + .build() + .expect("build with runtime"); + + let rt_present = handle.lock().unwrap().rt.is_some(); + assert!(rt_present); + } +} diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "radroots-net" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true + +[features] +default = ["std"] +std = ["radroots-net-core/std"] +rt = ["radroots-net-core/rt"] +all = ["std", "rt"] + +[dependencies] +radroots-net-core = { workspace = true, optional = false } diff --git a/crates/net/src/lib.rs b/crates/net/src/lib.rs @@ -0,0 +1 @@ +pub use radroots_net_core as core;