1use anyhow::{bail, Context, Result};
2use serde::{Deserialize, Serialize};
3use std::env;
4use url::Url;
5
6const DEFAULT_DMS_BASE_URL: &str = "https://dms.auki.network/v1";
7const DEFAULT_DDS_BASE_URL: &str = "https://dds.auki.network";
8const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 60;
9const DEFAULT_REGISTER_INTERVAL_SECS: u64 = 120;
10const DEFAULT_REGISTER_MAX_RETRY: i32 = -1;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14#[serde(rename_all = "lowercase")]
15pub enum LogFormat {
16 #[default]
17 Json,
18 Text,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23pub struct NodeConfig {
24 pub dms_base_url: Url,
26 pub node_version: String,
27 pub request_timeout_secs: u64,
28
29 pub dds_base_url: Option<Url>,
31 pub reg_secret: Option<String>,
32 pub secp256k1_privhex: Option<String>,
33
34 pub heartbeat_jitter_ms: u64,
36 pub poll_backoff_ms_min: u64,
37 pub poll_backoff_ms_max: u64,
38 pub token_safety_ratio: f32,
39 pub token_reauth_max_retries: u32,
40 pub token_reauth_jitter_ms: u64,
41 pub register_interval_secs: Option<u64>,
42 pub register_max_retry: Option<i32>,
43 pub max_concurrency: u32,
44 pub log_format: LogFormat,
45 pub enable_noop: bool,
46 pub noop_sleep_secs: u64,
47}
48
49impl NodeConfig {
50 pub fn from_env() -> Result<Self> {
52 let dms_base_url = parse_url_default("DMS_BASE_URL", DEFAULT_DMS_BASE_URL)?;
54 let request_timeout_secs =
55 parse_u64_default("REQUEST_TIMEOUT_SECS", DEFAULT_REQUEST_TIMEOUT_SECS)?;
56 let node_version = env::var("NODE_VERSION")
57 .ok()
58 .filter(|s| !s.trim().is_empty())
59 .unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
60
61 let dds_base_url = parse_url_default("DDS_BASE_URL", DEFAULT_DDS_BASE_URL)?;
63 let reg_secret = env::var("REG_SECRET")
64 .with_context(|| "REG_SECRET required for DDS SIWE authentication")?
65 .trim()
66 .to_string();
67 if reg_secret.is_empty() {
68 bail!("REG_SECRET required for DDS SIWE authentication");
69 }
70 let secp256k1_privhex = env::var("SECP256K1_PRIVHEX")
71 .with_context(|| "SECP256K1_PRIVHEX required for DDS SIWE authentication")?
72 .trim()
73 .to_string();
74 if secp256k1_privhex.is_empty() {
75 bail!("SECP256K1_PRIVHEX required for DDS SIWE authentication");
76 }
77
78 let heartbeat_jitter_ms = parse_u64_opt("HEARTBEAT_JITTER_MS", 250)?;
80 let poll_backoff_ms_min = parse_u64_opt("POLL_BACKOFF_MS_MIN", 1000)?;
81 let poll_backoff_ms_max = parse_u64_opt("POLL_BACKOFF_MS_MAX", 30000)?;
82 let token_safety_ratio = parse_f32_opt("TOKEN_SAFETY_RATIO", 0.75)?;
83 let token_reauth_max_retries = parse_u32_opt("TOKEN_REAUTH_MAX_RETRIES", 3)?;
84 let token_reauth_jitter_ms = parse_u64_opt("TOKEN_REAUTH_JITTER_MS", 500)?;
85 let register_interval_secs = Some(parse_u64_default(
86 "REGISTER_INTERVAL_SECS",
87 DEFAULT_REGISTER_INTERVAL_SECS,
88 )?);
89 let register_max_retry = Some(parse_i32_default(
90 "REGISTER_MAX_RETRY",
91 DEFAULT_REGISTER_MAX_RETRY,
92 )?);
93 let max_concurrency = parse_u32_opt("MAX_CONCURRENCY", 1)?;
94 let log_format = parse_log_format("LOG_FORMAT").unwrap_or_default();
95 let enable_noop = parse_bool_opt("ENABLE_NOOP", false)?;
96 let noop_sleep_secs = parse_u64_opt("NOOP_SLEEP_SECS", 5)?;
97
98 Ok(Self {
99 dms_base_url,
100 node_version,
101 request_timeout_secs,
102 dds_base_url: Some(dds_base_url),
103 reg_secret: Some(reg_secret),
104 secp256k1_privhex: Some(secp256k1_privhex),
105 heartbeat_jitter_ms,
106 poll_backoff_ms_min,
107 poll_backoff_ms_max,
108 token_safety_ratio,
109 token_reauth_max_retries,
110 token_reauth_jitter_ms,
111 register_interval_secs,
112 register_max_retry,
113 max_concurrency,
114 log_format,
115 enable_noop,
116 noop_sleep_secs,
117 })
118 }
119}
120
121fn env_var_trimmed(key: &str) -> Option<String> {
122 env::var(key).ok().and_then(|value| {
123 let trimmed = value.trim();
124 if trimmed.is_empty() {
125 None
126 } else {
127 Some(trimmed.to_string())
128 }
129 })
130}
131
132fn parse_url_default(key: &str, default: &str) -> Result<Url> {
133 let raw = env_var_trimmed(key).unwrap_or_else(|| default.to_string());
134 Url::parse(&raw).with_context(|| format!("invalid URL in {key}"))
135}
136
137fn parse_u64_default(key: &str, default: u64) -> Result<u64> {
138 match env_var_trimmed(key) {
139 Some(value) => value
140 .parse()
141 .with_context(|| format!("invalid integer in {key}")),
142 None => Ok(default),
143 }
144}
145
146fn parse_u64_opt(key: &str, default: u64) -> Result<u64> {
147 match env::var(key) {
148 Ok(v) => v
149 .parse()
150 .with_context(|| format!("invalid integer in {key}")),
151 Err(_) => Ok(default),
152 }
153}
154
155fn parse_u32_opt(key: &str, default: u32) -> Result<u32> {
156 match env::var(key) {
157 Ok(v) => v
158 .parse()
159 .with_context(|| format!("invalid integer in {key}")),
160 Err(_) => Ok(default),
161 }
162}
163
164fn parse_i32_default(key: &str, default: i32) -> Result<i32> {
165 match env_var_trimmed(key) {
166 Some(value) => {
167 let parsed: i32 = value
168 .parse()
169 .with_context(|| format!("invalid integer in {key}"))?;
170 if parsed < -1 {
171 bail!("{key} must be -1 or a non-negative integer, got {parsed}");
172 }
173 Ok(parsed)
174 }
175 None => Ok(default),
176 }
177}
178
179fn parse_f32_opt(key: &str, default: f32) -> Result<f32> {
180 match env::var(key) {
181 Ok(v) => v.parse().with_context(|| format!("invalid float in {key}")),
182 Err(_) => Ok(default),
183 }
184}
185
186fn parse_bool_opt(key: &str, default: bool) -> Result<bool> {
187 match env::var(key) {
188 Ok(v) => v
189 .parse::<bool>()
190 .with_context(|| format!("invalid bool in {key}; expected true/false")),
191 Err(_) => Ok(default),
192 }
193}
194
195fn parse_log_format(key: &str) -> Option<LogFormat> {
196 match env::var(key).ok()?.to_lowercase().as_str() {
197 "json" => Some(LogFormat::Json),
198 "text" => Some(LogFormat::Text),
199 _ => None,
200 }
201}