use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::logging::{log_error, log_info};
fn default_config_version() -> String {
"0.9.0".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HylixConfig {
#[serde(default = "default_config_version")]
pub version: String,
pub default_backend: BackendType,
pub scaffold_repo: String,
pub devnet: DevnetConfig,
pub build: BuildConfig,
pub bake_profile: String,
pub test: TestConfig,
pub run: RunConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestConfig {
pub print_server_logs: bool,
pub clean_server_data: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunConfig {
pub clean_server_data: bool,
pub server_port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, clap::ValueEnum)]
pub enum BackendType {
Sp1,
Risc0,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DevnetConfig {
pub node_image: String,
pub wallet_server_image: String,
pub wallet_ui_image: String,
pub registry_server_image: String,
pub registry_ui_image: String,
pub node_port: u16,
pub da_port: u16,
pub node_rust_log: String,
pub wallet_api_port: u16,
pub wallet_ws_port: u16,
pub wallet_ui_port: u16,
pub indexer_port: u16,
pub postgres_port: u16,
pub registry_server_port: u16,
pub registry_ui_port: u16,
pub auto_start: bool,
pub container_env: ContainerEnvConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContainerEnvConfig {
pub node: Vec<String>,
pub indexer: Vec<String>,
pub wallet_server: Vec<String>,
pub wallet_ui: Vec<String>,
pub postgres: Vec<String>,
pub registry_server: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct BuildConfig {
pub release: bool,
pub jobs: Option<u32>,
pub extra_flags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BakeProfile {
pub name: String,
pub accounts: Vec<AccountConfig>,
pub funds: Vec<FundConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountConfig {
pub name: String,
pub password: String,
pub invite_code: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FundConfig {
pub from: String,
pub from_password: String,
pub amount: u64,
pub token: String,
pub to: String,
}
impl Default for HylixConfig {
fn default() -> Self {
Self {
version: default_config_version(),
default_backend: BackendType::Risc0,
scaffold_repo: "https://github.com/hyli-org/app-scaffold".to_string(),
devnet: DevnetConfig::default(),
build: BuildConfig::default(),
bake_profile: "bobalice".to_string(),
test: TestConfig::default(),
run: RunConfig::default(),
}
}
}
impl Default for DevnetConfig {
fn default() -> Self {
Self {
node_image: "ghcr.io/hyli-org/hyli:latest".to_string(),
wallet_server_image: "ghcr.io/hyli-org/wallet/wallet-server:main".to_string(),
wallet_ui_image: "ghcr.io/hyli-org/wallet/wallet-ui:main".to_string(),
registry_server_image: "ghcr.io/hyli-org/hyli-registry/zkvm-registry-server:latest"
.to_string(),
registry_ui_image: "ghcr.io/hyli-org/hyli-registry/zkvm-registry-ui:latest".to_string(),
da_port: 4141,
node_rust_log: "info".to_string(),
node_port: 4321,
indexer_port: 4322,
postgres_port: 5432,
wallet_ui_port: 8080,
wallet_api_port: 4000,
wallet_ws_port: 8081,
registry_server_port: 9003,
registry_ui_port: 8082,
auto_start: true,
container_env: ContainerEnvConfig::default(),
}
}
}
impl Default for TestConfig {
fn default() -> Self {
Self {
print_server_logs: false,
clean_server_data: true,
}
}
}
impl Default for RunConfig {
fn default() -> Self {
Self {
clean_server_data: false,
server_port: 9002,
}
}
}
impl HylixConfig {
pub fn load() -> crate::error::HylixResult<Self> {
let config_path = Self::config_path()?;
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let mut toml_value: toml::Value = toml::from_str(&content)
.map_err(crate::error::HylixError::Toml)
.with_context(|| {
format!("Failed to parse TOML from file {}", config_path.display())
})?;
let file_version = toml_value
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("legacy")
.to_string();
let current_version = default_config_version();
if file_version != current_version {
log_info(&format!(
"Upgrading configuration from version '{file_version}' to '{current_version}'"
));
Self::backup()?;
toml_value = Self::migrate_toml(toml_value, file_version)?;
let migrated_content = toml::to_string_pretty(&toml_value)?;
std::fs::write(&config_path, migrated_content)?;
log_info("Configuration successfully upgraded and saved");
}
let config: Self = toml::from_str(&toml::to_string(&toml_value)?)
.map_err(crate::error::HylixError::Toml)
.with_context(|| {
format!(
"Failed to load configuration from file {}",
config_path.display()
)
})?;
Ok(config)
} else {
let config = Self::default();
config.save()?;
log_info(&format!(
"Created default configuration in file {}",
config_path.display()
));
Ok(config)
}
}
fn migrate_toml(
toml_value: toml::Value,
file_version: String,
) -> crate::error::HylixResult<toml::Value> {
let migrations: Vec<Box<dyn ConfigMigration>> =
vec![Box::new(LegacyMigration), Box::new(Migration0_6_0)];
for migration in migrations {
if migration.version() == file_version.as_str() {
return migration.migrate(toml_value);
}
}
log_error(&format!(
"Unsupported configuration version: {file_version}"
));
log_info("Failed to migrate configuration. Please check your configuration file.");
log_info(&format!(
"You can reset to default configuration by running `{}`",
console::style("hy config reset").bold().green()
));
Err(crate::error::HylixError::config(
"Unsupported configuration version".to_string(),
))
}
}
trait ConfigMigration {
fn version(&self) -> &str;
fn migrate(&self, toml_value: toml::Value) -> crate::error::HylixResult<toml::Value>;
}
struct LegacyMigration;
impl ConfigMigration for LegacyMigration {
fn version(&self) -> &str {
"legacy"
}
fn migrate(&self, mut toml_value: toml::Value) -> crate::error::HylixResult<toml::Value> {
log_info("Migrating from legacy configuration");
let current_version = default_config_version();
if let Some(table) = toml_value.as_table_mut() {
table.insert(
"version".to_string(),
toml::Value::String(current_version.clone()),
);
if let Some(devnet) = table.get_mut("devnet") {
if let Some(devnet_table) = devnet.as_table_mut() {
if !devnet_table.contains_key("node_rust_log") {
devnet_table.insert(
"node_rust_log".to_string(),
toml::Value::String("info".to_string()),
);
}
}
} else {
log_error("Devnet section not found in configuration");
log_info("Failed to migrate configuration. Please check your configuration file.");
log_info(&format!(
"You can reset to default configuration by running `{}`",
console::style("hy config reset").bold().green()
));
return Err(crate::error::HylixError::config(
"Devnet section not found in configuration".to_string(),
));
}
}
Ok(toml_value)
}
}
struct Migration0_6_0;
impl ConfigMigration for Migration0_6_0 {
fn version(&self) -> &str {
"0.6.0"
}
fn migrate(&self, mut toml_value: toml::Value) -> crate::error::HylixResult<toml::Value> {
log_info("Migrating from configuration version 0.6.0 to 0.9.0");
let current_version = default_config_version();
if let Some(table) = toml_value.as_table_mut() {
table.insert(
"version".to_string(),
toml::Value::String(current_version.clone()),
);
if let Some(devnet) = table.get_mut("devnet") {
if let Some(devnet_table) = devnet.as_table_mut() {
if !devnet_table.contains_key("registry_server_image") {
devnet_table.insert(
"registry_server_image".to_string(),
toml::Value::String(
"ghcr.io/hyli-org/hyli-registry/zkvm-registry-server:latest"
.to_string(),
),
);
}
if !devnet_table.contains_key("registry_ui_image") {
devnet_table.insert(
"registry_ui_image".to_string(),
toml::Value::String(
"ghcr.io/hyli-org/hyli-registry/zkvm-registry-ui:latest"
.to_string(),
),
);
}
if !devnet_table.contains_key("registry_server_port") {
devnet_table.insert(
"registry_server_port".to_string(),
toml::Value::Integer(9003),
);
}
if !devnet_table.contains_key("registry_ui_port") {
devnet_table
.insert("registry_ui_port".to_string(), toml::Value::Integer(8082));
}
}
}
if let Some(devnet) = table.get_mut("devnet") {
if let Some(devnet_table) = devnet.as_table_mut() {
if let Some(container_env) = devnet_table.get_mut("container_env") {
if let Some(container_env_table) = container_env.as_table_mut() {
if !container_env_table.contains_key("registry_server") {
container_env_table.insert(
"registry_server".to_string(),
toml::Value::Array(vec![]),
);
}
}
}
}
}
}
Ok(toml_value)
}
}
impl HylixConfig {
pub fn save(&self) -> crate::error::HylixResult<()> {
let config_path = Self::config_path()?;
let config_dir = config_path.parent().unwrap();
std::fs::create_dir_all(config_dir)?;
let content = toml::to_string_pretty(self)?;
std::fs::write(&config_path, content)?;
Ok(())
}
pub fn backup() -> crate::error::HylixResult<()> {
let config_path = Self::config_path()?;
let config_dir = config_path.parent().unwrap();
let backup_path = config_dir.join(format!(
"config.toml.{}.backup",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
));
std::fs::copy(&config_path, &backup_path)?;
log_info(&format!(
"Backed up configuration to {}",
backup_path.display()
));
Ok(())
}
fn config_path() -> crate::error::HylixResult<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| crate::error::HylixError::config("Could not find config directory"))?;
Ok(config_dir.join("hylix").join("config.toml"))
}
fn profiles_dir() -> crate::error::HylixResult<PathBuf> {
let config_dir = dirs::config_dir()
.ok_or_else(|| crate::error::HylixError::config("Could not find config directory"))?;
Ok(config_dir.join("hylix").join("profiles"))
}
pub fn load_bake_profile(&self, profile_name: &str) -> crate::error::HylixResult<BakeProfile> {
let profiles_dir = Self::profiles_dir()?;
let profile_path = profiles_dir.join(format!("{profile_name}.toml"));
if !profile_path.exists() {
return Err(crate::error::HylixError::config(format!(
"Profile '{}' not found at {}",
profile_name,
profile_path.display()
)));
}
let content = std::fs::read_to_string(&profile_path)?;
let profile: BakeProfile = toml::from_str(&content)
.map_err(crate::error::HylixError::Toml)
.with_context(|| {
format!(
"Failed to load profile from file {}",
profile_path.display()
)
})?;
log_info(&format!(
"Loaded profile '{}' from {}",
profile_name,
profile_path.display()
));
Ok(profile)
}
pub fn create_default_profile(&self) -> crate::error::HylixResult<()> {
let profiles_dir = Self::profiles_dir()?;
std::fs::create_dir_all(&profiles_dir)?;
let profile_path = profiles_dir.join("bobalice.toml");
if !profile_path.exists() {
let default_profile = BakeProfile {
name: "bobalice".to_string(),
accounts: vec![
AccountConfig {
name: "bob".to_string(),
password: crate::constants::passwords::DEFAULT.to_string(),
invite_code: "vip".to_string(),
},
AccountConfig {
name: "alice".to_string(),
password: crate::constants::passwords::DEFAULT.to_string(),
invite_code: "vip".to_string(),
},
],
funds: vec![
FundConfig {
from: "hyli".to_string(),
from_password: crate::constants::passwords::DEFAULT.to_string(),
amount: 1000,
token: "oranj".to_string(),
to: "bob".to_string(),
},
FundConfig {
from: "hyli".to_string(),
from_password: crate::constants::passwords::DEFAULT.to_string(),
amount: 1000,
token: "oranj".to_string(),
to: "alice".to_string(),
},
FundConfig {
from: "hyli".to_string(),
from_password: crate::constants::passwords::DEFAULT.to_string(),
amount: 500,
token: "oxygen".to_string(),
to: "bob".to_string(),
},
FundConfig {
from: "bob".to_string(),
from_password: crate::constants::passwords::DEFAULT.to_string(),
amount: 50,
token: "oxygen".to_string(),
to: "alice".to_string(),
},
],
};
let content = toml::to_string_pretty(&default_profile)?;
std::fs::write(&profile_path, content)?;
log_info(&format!(
"Created default bobalice profile at {}",
profile_path.display()
));
}
Ok(())
}
}