use evmlib::Network as EvmNetwork;
use serde::{Deserialize, Serialize};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::path::{Path, PathBuf};
pub const NODE_IDENTITY_FILENAME: &str = "node_identity.key";
pub const NODES_SUBDIR: &str = "nodes";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UpgradeChannel {
#[default]
Stable,
Beta,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NetworkMode {
#[default]
Production,
Testnet,
Development,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestnetConfig {
#[serde(default = "default_testnet_max_per_ip")]
pub max_per_ip: Option<usize>,
#[serde(default = "default_testnet_max_per_subnet")]
pub max_per_subnet: Option<usize>,
}
impl Default for TestnetConfig {
fn default() -> Self {
Self {
max_per_ip: default_testnet_max_per_ip(),
max_per_subnet: default_testnet_max_per_subnet(),
}
}
}
#[allow(clippy::unnecessary_wraps)]
const fn default_testnet_max_per_ip() -> Option<usize> {
Some(usize::MAX)
}
#[allow(clippy::unnecessary_wraps)]
const fn default_testnet_max_per_subnet() -> Option<usize> {
Some(usize::MAX)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeConfig {
#[serde(default = "default_root_dir")]
pub root_dir: PathBuf,
#[serde(default)]
pub port: u16,
#[serde(default)]
pub ipv4_only: bool,
#[serde(default)]
pub bootstrap: Vec<SocketAddr>,
#[serde(default)]
pub network_mode: NetworkMode,
#[serde(default)]
pub testnet: TestnetConfig,
#[serde(default)]
pub upgrade: UpgradeConfig,
#[serde(default)]
pub payment: PaymentConfig,
#[serde(default)]
pub bootstrap_cache: BootstrapCacheConfig,
#[serde(default)]
pub storage: StorageConfig,
#[serde(default)]
pub close_group_cache_dir: Option<PathBuf>,
#[serde(default = "default_max_message_size")]
pub max_message_size: usize,
#[serde(default = "default_log_level")]
pub log_level: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpgradeConfig {
#[serde(default)]
pub channel: UpgradeChannel,
#[serde(default = "default_check_interval")]
pub check_interval_hours: u64,
#[serde(default = "default_github_repo")]
pub github_repo: String,
#[serde(default = "default_staged_rollout_hours")]
pub staged_rollout_hours: u64,
#[serde(default)]
pub stop_on_upgrade: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "type")]
pub enum EvmNetworkConfig {
#[default]
ArbitrumOne,
ArbitrumSepolia,
Custom {
rpc_url: String,
payment_token_address: String,
payment_vault_address: String,
},
}
impl EvmNetworkConfig {
#[must_use]
pub fn into_evm_network(self) -> EvmNetwork {
match self {
Self::ArbitrumOne => EvmNetwork::ArbitrumOne,
Self::ArbitrumSepolia => EvmNetwork::ArbitrumSepoliaTest,
Self::Custom {
rpc_url,
payment_token_address,
payment_vault_address,
} => EvmNetwork::new_custom(&rpc_url, &payment_token_address, &payment_vault_address),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentConfig {
#[serde(default = "default_cache_capacity")]
pub cache_capacity: usize,
#[serde(default)]
pub rewards_address: Option<String>,
#[serde(default)]
pub evm_network: EvmNetworkConfig,
#[serde(default = "default_metrics_port")]
pub metrics_port: u16,
}
impl Default for PaymentConfig {
fn default() -> Self {
Self {
cache_capacity: default_cache_capacity(),
rewards_address: None,
evm_network: EvmNetworkConfig::default(),
metrics_port: default_metrics_port(),
}
}
}
const fn default_metrics_port() -> u16 {
9100
}
const fn default_cache_capacity() -> usize {
100_000
}
impl Default for NodeConfig {
fn default() -> Self {
Self {
root_dir: default_root_dir(),
port: 0,
ipv4_only: false,
bootstrap: Vec::new(),
network_mode: NetworkMode::default(),
testnet: TestnetConfig::default(),
upgrade: UpgradeConfig::default(),
payment: PaymentConfig::default(),
bootstrap_cache: BootstrapCacheConfig::default(),
storage: StorageConfig::default(),
close_group_cache_dir: None,
max_message_size: default_max_message_size(),
log_level: default_log_level(),
}
}
}
impl NodeConfig {
#[must_use]
pub fn testnet() -> Self {
Self {
network_mode: NetworkMode::Testnet,
testnet: TestnetConfig::default(),
bootstrap: default_testnet_bootstrap(),
..Self::default()
}
}
#[must_use]
pub fn development() -> Self {
Self {
network_mode: NetworkMode::Development,
testnet: TestnetConfig {
max_per_ip: Some(usize::MAX),
max_per_subnet: Some(usize::MAX),
},
..Self::default()
}
}
#[must_use]
pub fn is_relaxed(&self) -> bool {
!matches!(self.network_mode, NetworkMode::Production)
}
pub fn from_file(path: &std::path::Path) -> crate::Result<Self> {
let content = std::fs::read_to_string(path)?;
toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
}
pub fn to_file(&self, path: &std::path::Path) -> crate::Result<()> {
let content =
toml::to_string_pretty(self).map_err(|e| crate::Error::Config(e.to_string()))?;
std::fs::write(path, content)?;
Ok(())
}
}
impl Default for UpgradeConfig {
fn default() -> Self {
Self {
channel: UpgradeChannel::default(),
check_interval_hours: default_check_interval(),
github_repo: default_github_repo(),
staged_rollout_hours: default_staged_rollout_hours(),
stop_on_upgrade: false,
}
}
}
fn default_github_repo() -> String {
"WithAutonomi/ant-node".to_string()
}
#[must_use]
pub fn default_root_dir() -> PathBuf {
directories::ProjectDirs::from("", "", "ant").map_or_else(
|| PathBuf::from(".ant"),
|dirs| dirs.data_dir().to_path_buf(),
)
}
#[must_use]
pub fn default_nodes_dir() -> PathBuf {
default_root_dir().join(NODES_SUBDIR)
}
fn default_max_message_size() -> usize {
crate::replication::config::MAX_REPLICATION_MESSAGE_SIZE
.max(crate::ant_protocol::MAX_WIRE_MESSAGE_SIZE)
}
fn default_log_level() -> String {
"info".to_string()
}
const fn default_check_interval() -> u64 {
1 }
const fn default_staged_rollout_hours() -> u64 {
24 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootstrapCacheConfig {
#[serde(default = "default_bootstrap_cache_enabled")]
pub enabled: bool,
#[serde(default)]
pub cache_dir: Option<PathBuf>,
#[serde(default = "default_bootstrap_max_contacts")]
pub max_contacts: usize,
#[serde(default = "default_bootstrap_stale_days")]
pub stale_threshold_days: u64,
}
impl Default for BootstrapCacheConfig {
fn default() -> Self {
Self {
enabled: default_bootstrap_cache_enabled(),
cache_dir: None,
max_contacts: default_bootstrap_max_contacts(),
stale_threshold_days: default_bootstrap_stale_days(),
}
}
}
const fn default_bootstrap_cache_enabled() -> bool {
true
}
const fn default_bootstrap_max_contacts() -> usize {
10_000
}
const fn default_bootstrap_stale_days() -> u64 {
7
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
#[serde(default = "default_storage_enabled")]
pub enabled: bool,
#[serde(default = "default_storage_verify_on_read")]
pub verify_on_read: bool,
#[serde(default)]
pub db_size_gb: usize,
#[serde(default = "default_disk_reserve_mb")]
pub disk_reserve_mb: u64,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
enabled: default_storage_enabled(),
verify_on_read: default_storage_verify_on_read(),
db_size_gb: 0,
disk_reserve_mb: default_disk_reserve_mb(),
}
}
}
const fn default_disk_reserve_mb() -> u64 {
500
}
const fn default_storage_enabled() -> bool {
true
}
const fn default_storage_verify_on_read() -> bool {
true
}
pub const BOOTSTRAP_PEERS_FILENAME: &str = "bootstrap_peers.toml";
pub const BOOTSTRAP_PEERS_ENV: &str = "ANT_BOOTSTRAP_PEERS_PATH";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootstrapPeersConfig {
#[serde(default)]
pub peers: Vec<SocketAddr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BootstrapSource {
Cli,
ConfigFile,
AutoDiscovered(PathBuf),
None,
}
impl BootstrapPeersConfig {
pub fn from_file(path: &Path) -> crate::Result<Self> {
let content = std::fs::read_to_string(path)?;
toml::from_str(&content).map_err(|e| crate::Error::Config(e.to_string()))
}
#[must_use]
pub fn discover() -> Option<(Self, PathBuf)> {
let candidates = Self::search_paths();
for path in candidates {
if path.is_file() {
match Self::from_file(&path) {
Ok(config) if !config.peers.is_empty() => return Some((config, path)),
Ok(_) => {}
Err(err) => {
crate::logging::warn!(
"Failed to load bootstrap peers from {}: {err}",
path.display(),
);
}
}
}
}
None
}
fn search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(env_path) = std::env::var(BOOTSTRAP_PEERS_ENV) {
paths.push(PathBuf::from(env_path));
}
if let Ok(exe) = std::env::current_exe() {
if let Some(exe_dir) = exe.parent() {
paths.push(exe_dir.join(BOOTSTRAP_PEERS_FILENAME));
}
}
if let Some(proj_dirs) = directories::ProjectDirs::from("", "", "ant") {
paths.push(proj_dirs.config_dir().join(BOOTSTRAP_PEERS_FILENAME));
}
#[cfg(unix)]
{
paths.push(PathBuf::from("/etc/ant").join(BOOTSTRAP_PEERS_FILENAME));
}
paths
}
}
fn default_testnet_bootstrap() -> Vec<SocketAddr> {
vec![
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(165, 22, 4, 178), 12000)),
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(164, 92, 111, 156), 12000)),
]
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
fn test_default_config_has_cache_capacity() {
let config = PaymentConfig::default();
assert!(config.cache_capacity > 0, "Cache capacity must be positive");
}
#[test]
fn test_default_evm_network() {
use crate::payment::EvmVerifierConfig;
let _config = EvmVerifierConfig::default();
}
#[test]
fn test_bootstrap_peers_parse_valid_toml() {
let toml_str = r#"
peers = [
"127.0.0.1:10000",
"192.168.1.1:10001",
]
"#;
let config: BootstrapPeersConfig =
toml::from_str(toml_str).expect("valid TOML should parse");
assert_eq!(config.peers.len(), 2);
assert_eq!(config.peers[0].port(), 10000);
assert_eq!(config.peers[1].port(), 10001);
}
#[test]
fn test_bootstrap_peers_parse_empty_peers() {
let toml_str = r"peers = []";
let config: BootstrapPeersConfig =
toml::from_str(toml_str).expect("empty peers should parse");
assert!(config.peers.is_empty());
}
#[test]
fn test_bootstrap_peers_parse_missing_peers_field() {
let toml_str = "";
let config: BootstrapPeersConfig =
toml::from_str(toml_str).expect("missing field should use default");
assert!(config.peers.is_empty());
}
#[test]
fn test_bootstrap_peers_from_file() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("bootstrap_peers.toml");
std::fs::write(&path, r#"peers = ["10.0.0.1:10000", "10.0.0.2:10000"]"#)
.expect("write file");
let config = BootstrapPeersConfig::from_file(&path).expect("load from file");
assert_eq!(config.peers.len(), 2);
}
#[test]
fn test_bootstrap_peers_from_file_invalid_toml() {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("bootstrap_peers.toml");
std::fs::write(&path, "not valid toml [[[").expect("write file");
assert!(BootstrapPeersConfig::from_file(&path).is_err());
}
#[test]
#[serial]
fn test_bootstrap_peers_discover_env_var() {
{
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("bootstrap_peers.toml");
std::fs::write(&path, r#"peers = ["10.0.0.1:10000"]"#).expect("write file");
std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
let result = BootstrapPeersConfig::discover();
std::env::remove_var(BOOTSTRAP_PEERS_ENV);
let (config, discovered_path) = result.expect("should discover from env var");
assert_eq!(config.peers.len(), 1);
assert_eq!(discovered_path, path);
}
{
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().join("bootstrap_peers.toml");
std::fs::write(&path, r"peers = []").expect("write file");
std::env::set_var(BOOTSTRAP_PEERS_ENV, &path);
let result = BootstrapPeersConfig::discover();
std::env::remove_var(BOOTSTRAP_PEERS_ENV);
assert!(result.is_none(), "empty peers file should be skipped");
}
}
#[test]
fn test_bootstrap_peers_search_paths_contains_exe_dir() {
let paths = BootstrapPeersConfig::search_paths();
assert!(
paths
.iter()
.any(|p| p.file_name().is_some_and(|f| f == BOOTSTRAP_PEERS_FILENAME)),
"search paths should include a candidate with the bootstrap peers filename"
);
}
}