commit 60143307860335d7a824ee5e0d9d52194d73b11a
parent 5e246db9e4cc1de1aaffa8847bcbfdaa670de045
Author: triesap <137732411+triesap@users.noreply.github.com>
Date: Sun, 24 Aug 2025 23:10:04 +0000
Update runtime configuration.
Diffstat:
14 files changed, 360 insertions(+), 412 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -39,6 +39,18 @@ dependencies = [
]
[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -48,6 +60,12 @@ dependencies = [
]
[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -427,20 +445,20 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "config"
-version = "0.15.11"
+version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80"
+checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf"
dependencies = [
"async-trait",
"convert_case",
"json5",
+ "nom",
"pathdiff",
"ron",
"rust-ini",
"serde",
"serde_json",
"toml",
- "winnow",
"yaml-rust2",
]
@@ -610,12 +628,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
-name = "foldhash"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
-
-[[package]]
name = "form_urlencoded"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -771,23 +783,24 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
-dependencies = [
- "foldhash",
-]
[[package]]
name = "hashlink"
-version = "0.10.0"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
- "hashbrown 0.15.2",
+ "hashbrown 0.14.5",
]
[[package]]
@@ -1127,6 +1140,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
name = "miniz_oxide"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1153,6 +1172,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0efe882e02d206d8d279c20eb40e03baf7cb5136a1476dc084a324fbc3ec42d"
[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
name = "nostr"
version = "0.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1471,6 +1500,36 @@ dependencies = [
]
[[package]]
+name = "radroots-identity"
+version = "0.1.0"
+dependencies = [
+ "nostr",
+ "radroots-runtime",
+ "serde",
+ "thiserror 1.0.69",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "radroots-runtime"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "config",
+ "serde",
+ "serde_json",
+ "tempfile",
+ "thiserror 1.0.69",
+ "tokio",
+ "toml",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "radroots-trade"
version = "0.1.0"
dependencies = [
@@ -1600,13 +1659,14 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
- "config",
"futures",
"nostr",
"nostr-sdk",
"radroots-core",
"radroots-events",
"radroots-events-codec",
+ "radroots-identity",
+ "radroots-runtime",
"radroots-trade",
"serde",
"serde_json",
@@ -1614,8 +1674,6 @@ dependencies = [
"thiserror 1.0.69",
"tokio",
"tracing",
- "tracing-appender",
- "tracing-subscriber",
"uuid",
]
@@ -1647,13 +1705,12 @@ dependencies = [
[[package]]
name = "rust-ini"
-version = "0.21.1"
+version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
+checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a"
dependencies = [
"cfg-if",
"ordered-multimap",
- "trim-in-place",
]
[[package]]
@@ -2241,12 +2298,6 @@ dependencies = [
]
[[package]]
-name = "trim-in-place"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
-
-[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2379,6 +2430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
+ "serde",
]
[[package]]
@@ -2693,9 +2745,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "yaml-rust2"
-version = "0.10.1"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "818913695e83ece1f8d2a1c52d54484b7b46d0f9c06beeb2649b9da50d9b512d"
+checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8"
dependencies = [
"arraydeque",
"encoding_rs",
diff --git a/Cargo.toml b/Cargo.toml
@@ -7,6 +7,28 @@ resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2024"
-authors = ["rad roots <admin@radroots.dev>"]
-license = "AGPLv3"
-description = "rhizome is a Nostr data vending machine (NIP-90)"
+rust-version = "1.86.0"
+license = "AGPL-3.0"
+
+[workspace.dependencies]
+radroots-core = { path = "../../crates/crates/core" }
+radroots-events = { path = "../../crates/crates/events" }
+radroots-events-codec = { path = "../../crates/crates/events-codec" }
+radroots-identity = { path = "../../crates/crates/identity" }
+radroots-nostr = { path = "../../crates/crates/nostr" }
+radroots-runtime = { path = "../../crates/crates/runtime" }
+radroots-trade = { path = "../../crates/crates/trade" }
+
+anyhow = { version = "1" }
+clap = { version = "4" }
+config = { version = "0.15" }
+futures = { version = "0.3" }
+nostr = { version = "0.43.0", features = ["nip04"] }
+nostr-sdk = { version = "0.43.0" }
+serde = { version = "1", default-features = false }
+serde_json = { version = "1", default-features = false }
+tempfile = { version = "3.19.1" }
+thiserror = { version = "1" }
+tokio = { version = "1" }
+tracing = { version = "0.1" }
+uuid = { version = "1.16.0" }
diff --git a/config.toml b/config.toml
@@ -1,47 +1,16 @@
-# rhizome Nostr data vending machine configuration
-
[metadata]
-# The name shown on the profile
name = "rhi"
-
-# A full display name that can include emojis or symbols
# display_name = ""
-
-# A profile biography or description
# about = ""
-
-# URL of profile picture
# picture = ""
-
-# URL of banner image
# banner = ""
-
-# URL of website
# banner = ""
-
-# Profile mapping to DNS-based internet identifier
# nip05 = ""
-
-# Lightning address LNURL format
# lud06 = ""
-
-# Lightning address internet identifiers format
# lud16 = ""
[config]
-# Where to write logs/rotations.
logs_dir = "logs"
-
-# Path to the keys profile JSON.
-keys_path = "keys.json"
-
-# Generate a new key profile if none is found.
-generate_keys = true
-
-# Optional NIP-89 application handler identifier.
-identifier = ""
-
-# Relays to connect to for publishing/subscribing.
relays = [
"ws://127.0.0.1:8080"
]
\ No newline at end of file
diff --git a/crates/rhi/Cargo.toml b/crates/rhi/Cargo.toml
@@ -2,28 +2,28 @@
name = "rhi"
version.workspace = true
edition.workspace = true
-authors.workspace = true
+authors = ["Radroots Authors"]
+rust-version.workspace = true
license.workspace = true
-description.workspace = true
+description = "Rhizome Nostr data vending machine (NIP-90)"
[dependencies]
-radroots-core = { path = "../../../../../crates/radroots-common/crates/core" }
-radroots-events = { path = "../../../../../crates/radroots-common/crates/events" }
-radroots-events-codec = { path = "../../../../../crates/radroots-common/crates/events-codec" }
-radroots-trade = { path = "../../../../../crates/radroots-common/crates/trade" }
+radroots-core = { workspace = true, features = ["std", "serde", "typeshare"] }
+radroots-events = { workspace = true, features = ["serde"] }
+radroots-events-codec = { workspace = true, features = ["serde"] }
+radroots-identity = { workspace = true }
+radroots-runtime = { workspace = true, features = ["cli"] }
+radroots-trade = { workspace = true }
-anyhow = "1.0"
-clap = { version = "4", features = ["derive"] }
-config = "0.15"
-futures = "0.3"
-nostr = { version = "0.43.0", features = ["nip04"] }
-nostr-sdk = "0.43.0"
-serde = "1.0"
-serde_json = "1.0"
-tempfile = "3.19.1"
-thiserror = "1.0"
-tokio = { version = "1", features = ["full"] }
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
-tracing-appender = "0.2"
-uuid = { version = "1.16.0", features = ["v4"] }
+anyhow = { workspace = true }
+clap = { workspace = true, features = ["derive"]}
+futures = { workspace = true }
+nostr = { workspace = true }
+nostr-sdk = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+tempfile = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true, features = ["full"] }
+tracing = { workspace = true }
+uuid = { workspace = true, features = ["v4"] }
diff --git a/crates/rhi/src/cli.rs b/crates/rhi/src/cli.rs
@@ -0,0 +1,35 @@
+use std::path::PathBuf;
+
+use clap::{Parser, ValueHint, command};
+
+#[derive(Parser, Debug, Clone)]
+#[command(
+ about = env!("CARGO_PKG_DESCRIPTION"),
+ author = env!("CARGO_PKG_AUTHORS"),
+ version = env!("CARGO_PKG_VERSION")
+)]
+pub struct Args {
+ #[arg(
+ long,
+ value_name = "PATH",
+ value_hint = ValueHint::FilePath,
+ default_value = "config.toml",
+ help = "Path to the daemon configuration file (defaults to config.toml)"
+ )]
+ pub config: PathBuf,
+
+ #[arg(
+ long,
+ value_name = "PATH",
+ value_hint = ValueHint::FilePath,
+ help = "Path to the daemon identity JSON file (defaults to identity.json)",
+ )]
+ pub identity: Option<PathBuf>,
+
+ #[arg(
+ long,
+ action = clap::ArgAction::SetTrue,
+ help = "Allow generating a new identity file if missing; if not set and identity file is absent, the daemon will fail"
+ )]
+ pub allow_generate_identity: bool,
+}
diff --git a/crates/rhi/src/config.rs b/crates/rhi/src/config.rs
@@ -0,0 +1,13 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Configuration {
+ pub logs_dir: String,
+ pub relays: Vec<String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Settings {
+ pub metadata: nostr::Metadata,
+ pub config: Configuration,
+}
diff --git a/crates/rhi/src/config/mod.rs b/crates/rhi/src/config/mod.rs
@@ -1,54 +0,0 @@
-use config::{Config, ConfigError, File};
-use nostr::Metadata;
-use serde::{Deserialize, Serialize};
-use std::path::Path;
-use thiserror::Error;
-use tracing::error;
-
-#[derive(Debug, Error)]
-pub enum SettingsError {
- #[error("Configuration loading failed: {0}")]
- Load(#[from] ConfigError),
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct Configuration {
- pub logs_dir: String,
- pub keys_path: String,
- pub generate_keys: bool,
- pub identifier: Option<String>,
- pub relays: Vec<String>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Settings {
- pub metadata: Metadata,
- pub config: Configuration,
-}
-
-impl Settings {
- pub fn load(config_path: &Option<std::path::PathBuf>) -> Result<Self, SettingsError> {
- let path: &Path = config_path
- .as_deref()
- .unwrap_or_else(|| Path::new("config.toml"));
-
- let builder = Config::builder().add_source(File::from(path).required(true));
-
- match builder.build() {
- Ok(cfg) => match cfg.try_deserialize::<Settings>() {
- Ok(settings) => Ok(settings),
- Err(err) => {
- error!("❌ Failed to deserialize configuration: {err}");
- Err(SettingsError::Load(err))
- }
- },
- Err(err) => {
- error!(
- "❌ Failed to load configuration from '{}': {err}",
- path.display()
- );
- Err(SettingsError::Load(err))
- }
- }
- }
-}
diff --git a/crates/rhi/src/identity/keys.rs b/crates/rhi/src/identity/keys.rs
@@ -1,204 +0,0 @@
-use anyhow::Result;
-use nostr::{
- Event, Keys,
- event::{EventBuilder, Kind, Tag, TagKind},
- nips::nip01::Metadata,
-};
-use radroots_events::kinds::{KIND_APPLICATION_HANDLER, KIND_JOB_REQUEST_MIN};
-use serde::{Deserialize, Serialize};
-use std::{
- fs::{self, File},
- io::{BufReader, Write},
- path::{Path, PathBuf},
- str::FromStr,
-};
-use tempfile::NamedTempFile;
-use thiserror::Error;
-use tracing::{error, warn};
-use uuid::Uuid;
-
-#[cfg(unix)]
-use std::os::unix::fs::PermissionsExt;
-
-#[derive(Error, Debug)]
-pub enum KeyProfileError {
- #[error("Keys file does not exist at {0}")]
- NotFound(PathBuf),
-
- #[error("Failed to open keys file at {0}: {1}")]
- FileOpen(PathBuf, #[source] std::io::Error),
-
- #[error("Keys file already exists at {0}")]
- AlreadyExists(PathBuf),
-
- #[error("Failed to parse keys file at {0}: {1}")]
- FileParse(PathBuf, #[source] serde_json::Error),
-
- #[error("Failed to serialize keys: {0}")]
- Serialization(#[from] serde_json::Error),
-
- #[error("IO error during key write: {0}")]
- Io(#[from] std::io::Error),
-
- #[error("Failed to persist keys to disk: {0}")]
- Persist(#[from] tempfile::PersistError),
-
- #[error("Failed to build or sign nostr event: {0}")]
- NostrBuilder(#[from] nostr::event::builder::Error),
-
- #[error("Invalid secret key for identifier: {0}")]
- InvalidSecretKey(String),
-
- #[error("Kind 0 metadata must be initialized before building kind {0} application handler")]
- MissingMetadata(u32),
-}
-
-#[derive(Debug, Clone, Deserialize, Serialize)]
-pub struct KeyProfile {
- key: String,
- identifier: String,
- pub metadata: Option<Event>,
- pub application_handler: Option<Event>,
-
- #[serde(skip)]
- path: Option<PathBuf>,
-}
-
-impl KeyProfile {
- pub fn init<P: AsRef<str>>(
- path_str: P,
- generate: bool,
- identifier_tag: Option<String>,
- ) -> Result<Self, KeyProfileError> {
- let path = PathBuf::from(path_str.as_ref());
-
- if path.exists() {
- let file = File::open(&path).map_err(|e| KeyProfileError::FileOpen(path.clone(), e))?;
- let reader = BufReader::new(file);
- let mut profile: KeyProfile = serde_json::from_reader(reader)
- .map_err(|e| KeyProfileError::FileParse(path.clone(), e))?;
- profile.path = Some(path.clone());
-
- if !profile.identifier.trim().is_empty() {
- if let Some(new_id) = identifier_tag {
- warn!(
- "Provided identifier '{}' is being ignored because the keys file already contains identifier '{}'.",
- new_id, profile.identifier
- );
- }
- } else {
- profile.identifier = identifier_tag.unwrap_or_else(|| {
- warn!(
- "Missing NIP-89 application handler identifier in key file, generating UUID."
- );
- Uuid::new_v4().to_string()
- });
- profile.persist()?;
- }
-
- Ok(profile)
- } else if generate {
- let keys = Keys::generate();
- let secret = keys.secret_key();
- let identifier = match identifier_tag {
- Some(identifier) => identifier,
- None => {
- warn!(
- "Missing NIP-89 application handler identifier in key file, generating UUID."
- );
- Uuid::new_v4().to_string()
- }
- };
-
- let profile = KeyProfile {
- key: secret.to_secret_hex(),
- identifier,
- metadata: None,
- application_handler: None,
- path: Some(path.clone()),
- };
-
- profile.atomic_write(&path)?;
- Ok(profile)
- } else {
- Err(KeyProfileError::NotFound(path))
- }
- }
-
- pub fn keys(&self) -> Result<Keys, KeyProfileError> {
- Keys::from_str(&self.key)
- .map_err(|_| KeyProfileError::InvalidSecretKey(self.identifier.clone()))
- }
-
- fn atomic_write<P: AsRef<Path>>(&self, path: P) -> Result<(), KeyProfileError> {
- let json = serde_json::to_string(self)?;
-
- let dir = path.as_ref().parent().unwrap_or_else(|| Path::new("."));
- let mut temp_file = NamedTempFile::new_in(dir)?;
-
- temp_file.write_all(json.as_bytes())?;
- temp_file.as_file_mut().sync_all()?;
-
- #[cfg(unix)]
- {
- fs::set_permissions(temp_file.path(), fs::Permissions::from_mode(0o600))?;
- }
-
- temp_file.persist(path)?;
- Ok(())
- }
-
- fn persist(&self) -> Result<(), KeyProfileError> {
- match &self.path {
- Some(p) => self.atomic_write(p),
- None => Err(KeyProfileError::NotFound(PathBuf::from("[unknown path]"))),
- }
- }
-
- pub async fn build_metadata(
- &mut self,
- metadata: &Metadata,
- ) -> Result<Option<Event>, KeyProfileError> {
- if self.metadata.is_none() {
- let keys = self.keys()?;
- let event = EventBuilder::metadata(metadata).sign(&keys).await?;
- self.metadata = Some(event.clone());
- self.persist()?;
- Ok(Some(event))
- } else {
- Ok(None)
- }
- }
-
- pub async fn build_application_handler(&mut self) -> Result<Option<Event>, KeyProfileError> {
- if self.application_handler.is_none() {
- let keys = self.keys()?;
- let kind = KIND_APPLICATION_HANDLER;
-
- let kind_0_content = if let Some(m) = &self.metadata {
- m.content.clone()
- } else {
- return Err(KeyProfileError::MissingMetadata(kind));
- };
-
- let tags: Vec<Tag> = vec![
- Tag::custom(
- TagKind::Custom("k".into()),
- [KIND_JOB_REQUEST_MIN.to_string()],
- ),
- Tag::identifier(self.identifier.to_string()),
- ];
-
- let event = EventBuilder::new(Kind::Custom(kind as u16), kind_0_content)
- .tags(tags)
- .sign(&keys)
- .await?;
-
- self.application_handler = Some(event.clone());
- self.persist()?;
- Ok(Some(event))
- } else {
- Ok(None)
- }
- }
-}
diff --git a/crates/rhi/src/infra/mod.rs b/crates/rhi/src/infra/mod.rs
@@ -1,2 +1 @@
pub mod nostr;
-pub mod telemetry;
diff --git a/crates/rhi/src/infra/telemetry.rs b/crates/rhi/src/infra/telemetry.rs
@@ -1,23 +0,0 @@
-use std::path::Path;
-use tracing_appender::rolling;
-use tracing_subscriber::{EnvFilter, Registry, fmt, prelude::*};
-
-pub fn init(logs_dir: impl AsRef<Path>) {
- let file_appender = rolling::daily(&logs_dir, concat!(env!("CARGO_PKG_NAME"), ".log"));
- let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
- std::mem::forget(guard);
-
- let stdout_layer = fmt::layer().with_writer(std::io::stdout).with_target(false);
-
- let file_layer = fmt::layer()
- .with_writer(file_writer)
- .with_ansi(false)
- .with_target(false);
-
- let subscriber = Registry::default()
- .with(EnvFilter::from_default_env())
- .with(stdout_layer)
- .with(file_layer);
-
- subscriber.init();
-}
diff --git a/crates/rhi/src/key_profile.rs b/crates/rhi/src/key_profile.rs
@@ -0,0 +1,29 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct KeyProfile {
+ pub key: String,
+ pub identifier: String,
+ pub metadata: Option<nostr::Event>,
+ pub application_handler: Option<nostr::Event>,
+}
+
+impl radroots_identity::IdentitySpec for KeyProfile {
+ type Keys = nostr::Keys;
+ type ParseError = nostr::key::Error;
+
+ fn generate_new() -> Self {
+ let keys = nostr::Keys::generate();
+ Self {
+ key: keys.secret_key().to_secret_hex(),
+ identifier: uuid::Uuid::new_v4().to_string(),
+ metadata: None,
+ application_handler: None,
+ }
+ }
+
+ fn to_keys(&self) -> Result<Self::Keys, Self::ParseError> {
+ use std::str::FromStr;
+ nostr::Keys::from_str(&self.key)
+ }
+}
diff --git a/crates/rhi/src/lib.rs b/crates/rhi/src/lib.rs
@@ -1,60 +1,101 @@
pub mod adapters;
+pub mod cli;
pub mod config;
pub mod infra;
+pub mod rhi;
pub mod features {
pub mod trade_listing;
}
-pub mod identity {
- pub mod keys;
-}
+pub mod key_profile;
+
+pub use cli::Args as cli_args;
use anyhow::Result;
-use nostr::event::Event;
-use nostr_sdk::Client;
-use tokio::signal::unix::{SignalKind, signal};
-use tracing::{error, info};
-
-use crate::{config::Settings, features::trade_listing, identity::keys::KeyProfile};
-
-use std::path::PathBuf;
-
-use clap::Parser;
-
-#[derive(Parser, Debug, Clone)]
-#[command(
- about = env!("CARGO_PKG_DESCRIPTION"),
- author = env!("CARGO_PKG_AUTHORS"),
- version = env!("CARGO_PKG_VERSION")
-)]
-pub struct Args {
- #[arg(
- long,
- value_name = "PATH",
- value_parser = clap::value_parser!(PathBuf),
- help = "(Optional) Path to config file; default is 'config.toml'"
- )]
- pub config: Option<PathBuf>,
-}
-pub async fn run(settings: Settings) -> Result<()> {
- let mut key_profile = KeyProfile::init(
- &settings.config.keys_path,
- settings.config.generate_keys,
- settings.config.identifier.clone(),
+use crate::{
+ key_profile::KeyProfile,
+ rhi::{Rhi, start_subscriber},
+};
+
+pub async fn run_rhi(settings: &config::Settings, args: &cli_args) -> Result<()> {
+ let identity = radroots_identity::load_or_generate::<KeyProfile, _>(
+ args.identity.as_ref(),
+ args.allow_generate_identity,
+ )?;
+ let keys = radroots_identity::to_keys(&identity.value)?;
+
+ let rhi = Rhi::new(keys.clone());
+
+ for relay in settings.config.relays.iter() {
+ rhi.client.add_relay(relay).await?;
+ }
+
+ if !settings.config.relays.is_empty() {
+ let client = rhi.client.clone();
+ let md = settings.metadata.clone();
+ let has_metadata = serde_json::to_value(&md)
+ .ok()
+ .and_then(|v| v.as_object().cloned())
+ .map(|o| !o.is_empty())
+ .unwrap_or(false);
+
+ tokio::spawn(async move {
+ client.connect().await;
+ if has_metadata {
+ if let Err(e) = client.set_metadata(&md).await {
+ tracing::warn!("Failed to publish metadata on startup: {e}");
+ } else {
+ tracing::info!("Published metadata on startup");
+ }
+ }
+ });
+ }
+
+ let keys_sub = keys.clone();
+ let relays_sub = settings.config.relays.clone();
+ tokio::spawn(async move {
+ loop {
+ if let Err(e) = crate::features::trade_listing::subscriber::subscriber(
+ keys_sub.clone(),
+ relays_sub.clone(),
+ )
+ .await
+ {
+ tracing::error!("Error on job request subscription: {e}");
+ }
+ }
+ });
+
+ let handle = start_subscriber(keys.clone(), settings.config.relays.clone()).await;
+
+ let stop_handle = handle.clone();
+
+ tokio::select! {
+ _ = radroots_runtime::shutdown_signal() => {
+ tracing::info!("Shutting down…");
+ stop_handle.stop();
+ }
+ _ = handle.stopped() => {}
+ }
+
+ /*
+ let identity = radroots_identity::load_or_generate::<KeyProfile, _>(
+ args.identity.as_ref(),
+ args.allow_generate_identity,
)?;
+ let keys = radroots_identity::to_keys(&identity.value)?;
- let keys = key_profile.keys()?;
let metadata = settings.metadata.clone();
let mut events_to_send: Vec<Event> = vec![];
- if let Some(event) = key_profile.build_metadata(&metadata).await? {
+ if let Some(event) = identity.value.metadata.clone() {
events_to_send.push(event);
}
- if let Some(event) = key_profile.build_application_handler().await? {
+ if let Some(event) = identity.value.application_handler.clone() {
events_to_send.push(event);
}
@@ -95,6 +136,7 @@ pub async fn run(settings: Settings) -> Result<()> {
info!("Received SIGINT. Shutting down...");
}
}
+ */
Ok(())
}
diff --git a/crates/rhi/src/main.rs b/crates/rhi/src/main.rs
@@ -1,24 +1,22 @@
use anyhow::Result;
-use clap::Parser;
-use tracing::{error, info};
-
-use rhi::{Args, config::Settings, infra::telemetry, run};
+use rhi::{cli_args, config, run_rhi};
+use tracing::info;
#[tokio::main]
async fn main() {
if let Err(err) = setup().await {
- error!("Fatal error: {err:#?}");
+ eprintln!("Fatal error: {err:#?}");
std::process::exit(1);
}
}
async fn setup() -> Result<()> {
- let args = Args::parse();
+ let (args, settings): (cli_args, config::Settings) =
+ radroots_runtime::parse_and_load_path(|a: &cli_args| Some(a.config.as_path()))?;
- let settings = Settings::load(&args.config)?;
+ radroots_runtime::init_with(&settings.config.logs_dir, None)?;
- telemetry::init(&settings.config.logs_dir);
info!("Starting");
- run(settings).await
+ run_rhi(&settings, &args).await
}
diff --git a/crates/rhi/src/rhi.rs b/crates/rhi/src/rhi.rs
@@ -0,0 +1,70 @@
+use nostr_sdk::Client;
+use std::time::Instant;
+
+pub struct Rhi {
+ pub(crate) _started: Instant,
+ pub client: Client,
+}
+
+impl Rhi {
+ pub fn new(keys: nostr::Keys) -> Self {
+ let client = Client::new(keys);
+ Self {
+ _started: Instant::now(),
+ client,
+ }
+ }
+}
+
+use std::sync::Arc;
+use tokio::sync::Mutex;
+
+pub struct RhiHandle {
+ stop_tx: Arc<Mutex<Option<tokio::sync::oneshot::Sender<()>>>>,
+ join: Option<tokio::task::JoinHandle<()>>,
+}
+
+impl Clone for RhiHandle {
+ fn clone(&self) -> Self {
+ Self {
+ stop_tx: Arc::clone(&self.stop_tx),
+ join: None, // don’t clone the JoinHandle!
+ }
+ }
+}
+
+impl RhiHandle {
+ pub fn stop(&self) {
+ if let Some(tx) = self.stop_tx.try_lock().ok().and_then(|mut opt| opt.take()) {
+ let _ = tx.send(());
+ }
+ }
+
+ pub async fn stopped(mut self) {
+ if let Some(join) = self.join.take() {
+ let _ = join.await;
+ }
+ }
+}
+
+pub async fn start_subscriber(keys: nostr::Keys, relays: Vec<String>) -> RhiHandle {
+ let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel();
+
+ let join = tokio::spawn(async move {
+ loop {
+ tokio::select! {
+ _ = &mut stop_rx => break,
+ res = crate::features::trade_listing::subscriber::subscriber(keys.clone(), relays.clone()) => {
+ if let Err(e) = res {
+ tracing::error!("Error on job request subscription: {e}");
+ }
+ }
+ }
+ }
+ });
+
+ RhiHandle {
+ stop_tx: Arc::new(Mutex::new(Some(stop_tx))),
+ join: Some(join),
+ }
+}