rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 110++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
MCargo.toml | 28+++++++++++++++++++++++++---
Mconfig.toml | 31-------------------------------
Mcrates/rhi/Cargo.toml | 42+++++++++++++++++++++---------------------
Acrates/rhi/src/cli.rs | 35+++++++++++++++++++++++++++++++++++
Acrates/rhi/src/config.rs | 13+++++++++++++
Dcrates/rhi/src/config/mod.rs | 54------------------------------------------------------
Dcrates/rhi/src/identity/keys.rs | 204-------------------------------------------------------------------------------
Mcrates/rhi/src/infra/mod.rs | 1-
Dcrates/rhi/src/infra/telemetry.rs | 23-----------------------
Acrates/rhi/src/key_profile.rs | 29+++++++++++++++++++++++++++++
Mcrates/rhi/src/lib.rs | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mcrates/rhi/src/main.rs | 16+++++++---------
Acrates/rhi/src/rhi.rs | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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), + } +}