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:
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;