use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
pub use pocx_protocol::{BasicAuthConfig, RpcAuth, RpcServerAuth, RpcTransport, SubmissionMode};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub server: ServerConfig,
pub upstream: UpstreamConfig,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub database: DatabaseConfig,
#[serde(default)]
pub dashboard: Option<DashboardConfig>,
#[serde(default)]
pub logging: LoggingConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
#[serde(default = "default_listen_address")]
pub listen_address: String,
#[serde(default)]
pub auth: RpcServerAuth,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
listen_address: default_listen_address(),
auth: RpcServerAuth::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpstreamConfig {
pub name: String,
#[serde(default)]
pub rpc_transport: RpcTransport,
#[serde(default = "default_rpc_host")]
pub rpc_host: String,
#[serde(default = "default_rpc_port")]
pub rpc_port: u16,
#[serde(default)]
pub rpc_auth: RpcAuth,
#[serde(default)]
pub submission_mode: SubmissionMode,
#[serde(default = "default_block_time")]
pub block_time_secs: u64,
}
impl UpstreamConfig {
pub fn build_url(&self) -> Option<String> {
match self.rpc_transport {
RpcTransport::Http => Some(format!("http://{}:{}", self.rpc_host, self.rpc_port)),
RpcTransport::Https => Some(format!("https://{}:{}", self.rpc_host, self.rpc_port)),
}
}
pub fn endpoint(&self) -> String {
match self.rpc_transport {
RpcTransport::Http => format!("http://{}:{}", self.rpc_host, self.rpc_port),
RpcTransport::Https => format!("https://{}:{}", self.rpc_host, self.rpc_port),
}
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(Error::Config("Upstream name cannot be empty".to_string()));
}
if self.rpc_host.is_empty() {
return Err(Error::Config("rpc_host cannot be empty".to_string()));
}
if self.rpc_port == 0 {
return Err(Error::Config("rpc_port cannot be 0".to_string()));
}
let url_str = self.build_url().unwrap();
if let Err(e) = url::Url::parse(&url_str) {
return Err(Error::Config(format!(
"Upstream has invalid URL '{}': {}",
url_str, e
)));
}
Ok(())
}
pub fn get_auth_token_or_exit(&self) -> std::result::Result<Option<String>, String> {
self.rpc_auth.get_token_or_exit(&self.name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
#[serde(default = "default_mining_info_ttl")]
pub mining_info_ttl_secs: u64,
#[serde(default = "default_pool_timeout")]
pub pool_timeout_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
#[serde(default = "default_database_path")]
pub path: String,
#[serde(default = "default_db_retention_days")]
pub retention_days: u64,
}
impl Default for DatabaseConfig {
fn default() -> Self {
Self {
path: default_database_path(),
retention_days: default_db_retention_days(),
}
}
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
mining_info_ttl_secs: default_mining_info_ttl(),
pool_timeout_secs: default_pool_timeout(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_dashboard_address")]
pub listen_address: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default = "default_log_file")]
pub file: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: default_log_level(),
file: default_log_file(),
}
}
}
impl Config {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let contents = std::fs::read_to_string(path)?;
let config: Config = serde_yaml::from_str(&contents)?;
config.validate()?;
Ok(config)
}
pub fn genesis_base_target(&self) -> u64 {
2u64.pow(42) / self.upstream.block_time_secs
}
pub fn retention_blocks(&self) -> u64 {
if self.database.retention_days == 0 {
return 0;
}
self.database.retention_days * 86400 / self.upstream.block_time_secs
}
fn validate(&self) -> Result<()> {
self.upstream.validate()?;
if self.cache.mining_info_ttl_secs == 0 {
return Err(Error::Config(
"mining_info_ttl_secs must be greater than 0".to_string(),
));
}
if self.cache.pool_timeout_secs == 0 {
return Err(Error::Config(
"pool_timeout_secs must be greater than 0".to_string(),
));
}
if self.server.auth.enabled && self.server.auth.basic_auth.is_none() {
return Err(Error::Config(
"Server auth is enabled but no credentials configured".to_string(),
));
}
Ok(())
}
}
fn default_listen_address() -> String {
"0.0.0.0:8080".to_string()
}
fn default_rpc_host() -> String {
"127.0.0.1".to_string()
}
fn default_rpc_port() -> u16 {
8080
}
fn default_mining_info_ttl() -> u64 {
5
}
fn default_pool_timeout() -> u64 {
30
}
fn default_block_time() -> u64 {
120 }
fn default_database_path() -> String {
"aggregator.db".to_string()
}
fn default_db_retention_days() -> u64 {
7 }
fn default_true() -> bool {
true
}
fn default_dashboard_address() -> String {
"0.0.0.0:8081".to_string()
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_log_file() -> String {
"aggregator.log".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_validation_empty_name() {
let upstream = UpstreamConfig {
name: "".to_string(),
rpc_transport: RpcTransport::Http,
rpc_host: "localhost".to_string(),
rpc_port: 8080,
rpc_auth: RpcAuth::None,
submission_mode: SubmissionMode::Pool,
block_time_secs: 120,
};
assert!(upstream.validate().is_err());
}
#[test]
fn test_valid_http_upstream() {
let upstream = UpstreamConfig {
name: "test".to_string(),
rpc_transport: RpcTransport::Http,
rpc_host: "localhost".to_string(),
rpc_port: 8080,
rpc_auth: RpcAuth::None,
submission_mode: SubmissionMode::Pool,
block_time_secs: 120,
};
assert!(upstream.validate().is_ok());
assert_eq!(upstream.endpoint(), "http://localhost:8080");
}
#[test]
fn test_invalid_upstream_empty_host() {
let upstream = UpstreamConfig {
name: "test".to_string(),
rpc_transport: RpcTransport::Http,
rpc_host: "".to_string(),
rpc_port: 8080,
rpc_auth: RpcAuth::None,
submission_mode: SubmissionMode::Pool,
block_time_secs: 120,
};
assert!(upstream.validate().is_err());
}
#[test]
fn test_full_config_validation() {
let config = Config {
server: ServerConfig::default(),
upstream: UpstreamConfig {
name: "test".to_string(),
rpc_transport: RpcTransport::Http,
rpc_host: "localhost".to_string(),
rpc_port: 8080,
rpc_auth: RpcAuth::None,
submission_mode: SubmissionMode::Pool,
block_time_secs: 120,
},
cache: CacheConfig::default(),
database: DatabaseConfig::default(),
dashboard: None,
logging: LoggingConfig::default(),
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_with_enabled_auth_but_no_credentials() {
let config = Config {
server: ServerConfig {
listen_address: "0.0.0.0:8080".to_string(),
auth: RpcServerAuth {
enabled: true,
basic_auth: None, },
},
upstream: UpstreamConfig {
name: "test".to_string(),
rpc_transport: RpcTransport::Http,
rpc_host: "localhost".to_string(),
rpc_port: 8080,
rpc_auth: RpcAuth::None,
submission_mode: SubmissionMode::Pool,
block_time_secs: 120,
},
cache: CacheConfig::default(),
database: DatabaseConfig::default(),
dashboard: None,
logging: LoggingConfig::default(),
};
assert!(config.validate().is_err());
}
}