Contractless/src/config.rs

215 lines
7.2 KiB
Rust
Raw Normal View History

2026-05-24 17:56:57 +00:00
use crate::env;
use crate::tilde;
use crate::Ini;
use crate::Path;
use crate::PathBuf;
lazy_static::lazy_static! {
pub static ref SETTINGS: Settings = Settings::load().expect("Failed to load settings");
}
#[derive(Debug)]
pub struct Settings {
pub block_path: String,
pub torrent_path: String,
pub db_path: String,
pub wallet_path: String,
pub wallet_name: String,
pub balance_sheet: String,
pub log_path: String,
pub log_level: String,
pub public_ip: String,
pub listen_ip: String,
pub rpc_port: String,
pub testnet_rpc_port: String,
2026-05-26 18:14:52 +00:00
pub validator: bool,
2026-05-24 17:56:57 +00:00
pub outgoing_connections: u8,
pub incoming_connections: u8,
pub threads: u16,
pub piggybacks: Vec<String>,
pub pg_host: String,
pub pg_port: u16,
pub pg_user: String,
pub pg_password: Option<String>,
pub pg_dbname: String,
}
impl Settings {
fn expand(path: &str, base_dir: &Path) -> String {
let expanded = PathBuf::from(tilde(path).as_ref());
if expanded.is_relative() {
base_dir.join(expanded).to_string_lossy().into_owned()
} else {
expanded.to_string_lossy().into_owned()
}
}
fn get_config_path() -> PathBuf {
// Priority: CLI arg > ENV var > ./settings.ini > executable directory > platform fallback
let mut args = env::args().skip(1); // Skip the executable name
while let Some(arg) = args.next() {
if arg == "--config" {
if let Some(path) = args.next() {
return PathBuf::from(path);
} else {
eprintln!("Error: --config flag requires a path");
std::process::exit(1);
}
}
}
if let Ok(path) = env::var("SETTINGS_PATH") {
return PathBuf::from(path);
}
let local_settings = env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("settings.ini");
if local_settings.exists() {
return local_settings;
}
if let Ok(exe_path) = env::current_exe() {
if let Some(exe_dir) = exe_path.parent() {
let exe_settings = exe_dir.join("settings.ini");
if exe_settings.exists() {
return exe_settings;
}
}
}
#[cfg(unix)]
{
PathBuf::from("/etc/contractless/settings.ini")
}
#[cfg(not(unix))]
{
env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("settings.ini")
}
}
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
let config_file_path = Self::get_config_path();
let config_dir = config_file_path.parent().unwrap_or_else(|| Path::new("."));
let conf = Ini::load_from_file(&config_file_path)?;
let section = conf
.section(Some("Piggyback"))
.ok_or("Piggyback section not found")?;
#[cfg(feature = "mainnet")]
let pg_section = conf
.section(Some("Postgres"))
.ok_or("Postgres section not found")?;
#[cfg(feature = "testnet")]
let pg_section = conf
.section(Some("Postgres-Testnet"))
.ok_or("Postgres-Testnet section not found")?;
let mut piggybacks = Vec::new();
for (key, value) in section.iter() {
if key.to_uppercase().starts_with("PIGGYBACK_") {
piggybacks.push(value.to_string());
}
}
let threads = conf
.get_from(Some("Settings"), "THREADS")
.unwrap_or("1")
.parse::<u16>()?;
if threads != 1 && threads != 2 && threads % 4 != 0 {
return Err("THREADS must be 1, 2, or a multiple of 4".into());
}
if threads == 0 || threads > 256 {
return Err("THREADS must be between 1 and 256".into());
}
Ok(Settings {
block_path: Self::expand(
conf.get_from(Some("Paths"), "BLOCK_PATH")
.ok_or("BLOCK_PATH not found")?,
config_dir,
),
torrent_path: Self::expand(
conf.get_from(Some("Paths"), "TORRENT_PATH")
.ok_or("TORRENT_PATH not found")?,
config_dir,
),
db_path: Self::expand(
conf.get_from(Some("Paths"), "DB_PATH")
.ok_or("DB_PATH not found")?,
config_dir,
),
wallet_path: Self::expand(
conf.get_from(Some("Paths"), "WALLET_PATH")
.ok_or("WALLET_PATH not found")?,
config_dir,
),
wallet_name: conf
.get_from(Some("Paths"), "WALLET_NAME")
.ok_or("WALLET_NAME not found")?
.to_string(),
balance_sheet: Self::expand(
conf.get_from(Some("Paths"), "BALANCE_SHEET")
.ok_or("BALANCE_SHEET not found")?,
config_dir,
),
log_path: Self::expand(
2026-05-26 06:24:57 +00:00
conf.get_from(Some("Paths"), "LOG_PATH").unwrap_or("./logs"),
2026-05-24 17:56:57 +00:00
config_dir,
),
log_level: conf
.get_from(Some("Settings"), "LOG_LEVEL")
.unwrap_or("info")
.to_string(),
public_ip: conf
.get_from(Some("Settings"), "PUBLIC_IP")
.or_else(|| conf.get_from(Some("Settings"), "IP"))
.ok_or("PUBLIC_IP not found")?
.to_string(),
listen_ip: conf
.get_from(Some("Settings"), "LISTEN_IP")
.unwrap_or("0.0.0.0")
.to_string(),
rpc_port: conf
.get_from(Some("Settings"), "RPC_PORT")
.ok_or("RPC_PORT not found")?
.to_string(),
testnet_rpc_port: conf
.get_from(Some("Settings"), "TESTNET_RPC_PORT")
.ok_or("TESTNET_RPC_PORT not found")?
.to_string(),
2026-05-26 18:14:52 +00:00
validator: conf
.get_from(Some("Settings"), "VALIDATOR")
.unwrap_or("false")
.parse::<bool>()?,
2026-05-24 17:56:57 +00:00
outgoing_connections: conf
.get_from(Some("Settings"), "OUTGOING_CONNECTIONS")
.ok_or("OUTGOING_CONNECTIONS not found")?
.parse::<u8>()?,
incoming_connections: conf
.get_from(Some("Settings"), "INCOMING_CONNECTIONS")
.ok_or("INCOMING_CONNECTIONS not found")?
.parse::<u8>()?,
threads,
pg_host: pg_section.get("host").unwrap_or("127.0.0.1").to_string(),
pg_port: pg_section.get("port").unwrap_or("5432").parse::<u16>()?,
pg_user: pg_section
.get("user")
.ok_or("postgres user not set")?
.to_string(),
pg_password: pg_section.get("password").map(|s| s.to_string()),
pg_dbname: pg_section
.get("dbname")
.ok_or("postgres dbname not set")?
.to_string(),
piggybacks,
})
}
}