1use anyhow::Context;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5use crate::logging::{log_error, log_info};
6
7fn default_config_version() -> String {
9 "0.9.0".to_string()
10}
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct HylixConfig {
15 #[serde(default = "default_config_version")]
17 pub version: String,
18 pub default_backend: BackendType,
20 pub scaffold_repo: String,
22 pub devnet: DevnetConfig,
24 pub build: BuildConfig,
26 pub bake_profile: String,
28 pub test: TestConfig,
30 pub run: RunConfig,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TestConfig {
37 pub print_server_logs: bool,
39 pub clean_server_data: bool,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct RunConfig {
46 pub clean_server_data: bool,
48 pub server_port: u16,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, clap::ValueEnum)]
54pub enum BackendType {
55 Sp1,
56 Risc0,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct DevnetConfig {
62 pub node_image: String,
64 pub wallet_server_image: String,
66 pub wallet_ui_image: String,
68 pub registry_server_image: String,
70 pub registry_ui_image: String,
72 pub node_port: u16,
74 pub da_port: u16,
76 pub node_rust_log: String,
78 pub wallet_api_port: u16,
80 pub wallet_ws_port: u16,
82 pub wallet_ui_port: u16,
84 pub indexer_port: u16,
86 pub postgres_port: u16,
88 pub registry_server_port: u16,
90 pub registry_ui_port: u16,
92 pub auto_start: bool,
94 pub container_env: ContainerEnvConfig,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize, Default)]
100pub struct ContainerEnvConfig {
101 pub node: Vec<String>,
103 pub indexer: Vec<String>,
105 pub wallet_server: Vec<String>,
107 pub wallet_ui: Vec<String>,
109 pub postgres: Vec<String>,
111 pub registry_server: Vec<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117pub struct BuildConfig {
118 pub release: bool,
120 pub jobs: Option<u32>,
122 pub extra_flags: Vec<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct BakeProfile {
129 pub name: String,
131 pub accounts: Vec<AccountConfig>,
133 pub funds: Vec<FundConfig>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AccountConfig {
140 pub name: String,
142 pub password: String,
144 pub invite_code: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct FundConfig {
151 pub from: String,
153 pub from_password: String,
155 pub amount: u64,
157 pub token: String,
159 pub to: String,
161}
162
163impl Default for HylixConfig {
164 fn default() -> Self {
165 Self {
166 version: default_config_version(),
167 default_backend: BackendType::Risc0,
168 scaffold_repo: "https://github.com/hyli-org/app-scaffold".to_string(),
169 devnet: DevnetConfig::default(),
170 build: BuildConfig::default(),
171 bake_profile: "bobalice".to_string(),
172 test: TestConfig::default(),
173 run: RunConfig::default(),
174 }
175 }
176}
177
178impl Default for DevnetConfig {
179 fn default() -> Self {
180 Self {
181 node_image: "ghcr.io/hyli-org/hyli:latest".to_string(),
182 wallet_server_image: "ghcr.io/hyli-org/wallet/wallet-server:main".to_string(),
183 wallet_ui_image: "ghcr.io/hyli-org/wallet/wallet-ui:main".to_string(),
184 registry_server_image: "ghcr.io/hyli-org/hyli-registry/zkvm-registry-server:latest"
185 .to_string(),
186 registry_ui_image: "ghcr.io/hyli-org/hyli-registry/zkvm-registry-ui:latest".to_string(),
187 da_port: 4141,
188 node_rust_log: "info".to_string(),
189 node_port: 4321,
190 indexer_port: 4322,
191 postgres_port: 5432,
192 wallet_ui_port: 8080,
193 wallet_api_port: 4000,
194 wallet_ws_port: 8081,
195 registry_server_port: 9003,
196 registry_ui_port: 8082,
197 auto_start: true,
198 container_env: ContainerEnvConfig::default(),
199 }
200 }
201}
202
203impl Default for TestConfig {
204 fn default() -> Self {
205 Self {
206 print_server_logs: false,
207 clean_server_data: true,
208 }
209 }
210}
211
212impl Default for RunConfig {
213 fn default() -> Self {
214 Self {
215 clean_server_data: false,
216 server_port: 9002,
217 }
218 }
219}
220
221impl HylixConfig {
222 pub fn load() -> crate::error::HylixResult<Self> {
224 let config_path = Self::config_path()?;
225
226 if config_path.exists() {
227 let content = std::fs::read_to_string(&config_path)?;
228
229 let mut toml_value: toml::Value = toml::from_str(&content)
231 .map_err(crate::error::HylixError::Toml)
232 .with_context(|| {
233 format!("Failed to parse TOML from file {}", config_path.display())
234 })?;
235
236 let file_version = toml_value
238 .get("version")
239 .and_then(|v| v.as_str())
240 .unwrap_or("legacy")
241 .to_string();
242
243 let current_version = default_config_version();
244
245 if file_version != current_version {
246 log_info(&format!(
247 "Upgrading configuration from version '{file_version}' to '{current_version}'"
248 ));
249
250 Self::backup()?;
252
253 toml_value = Self::migrate_toml(toml_value, file_version)?;
255
256 let migrated_content = toml::to_string_pretty(&toml_value)?;
258 std::fs::write(&config_path, migrated_content)?;
259
260 log_info("Configuration successfully upgraded and saved");
261 }
262
263 let config: Self = toml::from_str(&toml::to_string(&toml_value)?)
265 .map_err(crate::error::HylixError::Toml)
266 .with_context(|| {
267 format!(
268 "Failed to load configuration from file {}",
269 config_path.display()
270 )
271 })?;
272
273 Ok(config)
274 } else {
275 let config = Self::default();
276 config.save()?;
277 log_info(&format!(
278 "Created default configuration in file {}",
279 config_path.display()
280 ));
281 Ok(config)
282 }
283 }
284
285 fn migrate_toml(
287 toml_value: toml::Value,
288 file_version: String,
289 ) -> crate::error::HylixResult<toml::Value> {
290 let migrations: Vec<Box<dyn ConfigMigration>> =
291 vec![Box::new(LegacyMigration), Box::new(Migration0_6_0)];
292
293 for migration in migrations {
294 if migration.version() == file_version.as_str() {
295 return migration.migrate(toml_value);
296 }
297 }
298
299 log_error(&format!(
300 "Unsupported configuration version: {file_version}"
301 ));
302 log_info("Failed to migrate configuration. Please check your configuration file.");
303 log_info(&format!(
304 "You can reset to default configuration by running `{}`",
305 console::style("hy config reset").bold().green()
306 ));
307 Err(crate::error::HylixError::config(
308 "Unsupported configuration version".to_string(),
309 ))
310 }
311}
312
313trait ConfigMigration {
315 fn version(&self) -> &str;
316 fn migrate(&self, toml_value: toml::Value) -> crate::error::HylixResult<toml::Value>;
317}
318
319struct LegacyMigration;
321
322impl ConfigMigration for LegacyMigration {
323 fn version(&self) -> &str {
324 "legacy"
325 }
326
327 fn migrate(&self, mut toml_value: toml::Value) -> crate::error::HylixResult<toml::Value> {
328 log_info("Migrating from legacy configuration");
329 let current_version = default_config_version();
330
331 if let Some(table) = toml_value.as_table_mut() {
332 table.insert(
334 "version".to_string(),
335 toml::Value::String(current_version.clone()),
336 );
337
338 if let Some(devnet) = table.get_mut("devnet") {
340 if let Some(devnet_table) = devnet.as_table_mut() {
341 if !devnet_table.contains_key("node_rust_log") {
342 devnet_table.insert(
343 "node_rust_log".to_string(),
344 toml::Value::String("info".to_string()),
345 );
346 }
347 }
348 } else {
349 log_error("Devnet section not found in configuration");
350 log_info("Failed to migrate configuration. Please check your configuration file.");
351 log_info(&format!(
352 "You can reset to default configuration by running `{}`",
353 console::style("hy config reset").bold().green()
354 ));
355 return Err(crate::error::HylixError::config(
356 "Devnet section not found in configuration".to_string(),
357 ));
358 }
359 }
360
361 Ok(toml_value)
362 }
363}
364
365struct Migration0_6_0;
367
368impl ConfigMigration for Migration0_6_0 {
369 fn version(&self) -> &str {
370 "0.6.0"
371 }
372
373 fn migrate(&self, mut toml_value: toml::Value) -> crate::error::HylixResult<toml::Value> {
374 log_info("Migrating from configuration version 0.6.0 to 0.9.0");
375 let current_version = default_config_version();
376
377 if let Some(table) = toml_value.as_table_mut() {
378 table.insert(
380 "version".to_string(),
381 toml::Value::String(current_version.clone()),
382 );
383
384 if let Some(devnet) = table.get_mut("devnet") {
386 if let Some(devnet_table) = devnet.as_table_mut() {
387 if !devnet_table.contains_key("registry_server_image") {
389 devnet_table.insert(
390 "registry_server_image".to_string(),
391 toml::Value::String(
392 "ghcr.io/hyli-org/hyli-registry/zkvm-registry-server:latest"
393 .to_string(),
394 ),
395 );
396 }
397 if !devnet_table.contains_key("registry_ui_image") {
399 devnet_table.insert(
400 "registry_ui_image".to_string(),
401 toml::Value::String(
402 "ghcr.io/hyli-org/hyli-registry/zkvm-registry-ui:latest"
403 .to_string(),
404 ),
405 );
406 }
407 if !devnet_table.contains_key("registry_server_port") {
409 devnet_table.insert(
410 "registry_server_port".to_string(),
411 toml::Value::Integer(9003),
412 );
413 }
414 if !devnet_table.contains_key("registry_ui_port") {
416 devnet_table
417 .insert("registry_ui_port".to_string(), toml::Value::Integer(8082));
418 }
419 }
420 }
421
422 if let Some(devnet) = table.get_mut("devnet") {
424 if let Some(devnet_table) = devnet.as_table_mut() {
425 if let Some(container_env) = devnet_table.get_mut("container_env") {
426 if let Some(container_env_table) = container_env.as_table_mut() {
427 if !container_env_table.contains_key("registry_server") {
428 container_env_table.insert(
429 "registry_server".to_string(),
430 toml::Value::Array(vec![]),
431 );
432 }
433 }
434 }
435 }
436 }
437 }
438
439 Ok(toml_value)
440 }
441}
442
443impl HylixConfig {
444 pub fn save(&self) -> crate::error::HylixResult<()> {
446 let config_path = Self::config_path()?;
447 let config_dir = config_path.parent().unwrap();
448
449 std::fs::create_dir_all(config_dir)?;
450
451 let content = toml::to_string_pretty(self)?;
452 std::fs::write(&config_path, content)?;
453
454 Ok(())
455 }
456
457 pub fn backup() -> crate::error::HylixResult<()> {
459 let config_path = Self::config_path()?;
460 let config_dir = config_path.parent().unwrap();
461 let backup_path = config_dir.join(format!(
462 "config.toml.{}.backup",
463 std::time::SystemTime::now()
464 .duration_since(std::time::UNIX_EPOCH)
465 .unwrap()
466 .as_secs()
467 ));
468 std::fs::copy(&config_path, &backup_path)?;
469 log_info(&format!(
470 "Backed up configuration to {}",
471 backup_path.display()
472 ));
473 Ok(())
474 }
475
476 fn config_path() -> crate::error::HylixResult<PathBuf> {
478 let config_dir = dirs::config_dir()
479 .ok_or_else(|| crate::error::HylixError::config("Could not find config directory"))?;
480
481 Ok(config_dir.join("hylix").join("config.toml"))
482 }
483
484 fn profiles_dir() -> crate::error::HylixResult<PathBuf> {
486 let config_dir = dirs::config_dir()
487 .ok_or_else(|| crate::error::HylixError::config("Could not find config directory"))?;
488
489 Ok(config_dir.join("hylix").join("profiles"))
490 }
491
492 pub fn load_bake_profile(&self, profile_name: &str) -> crate::error::HylixResult<BakeProfile> {
494 let profiles_dir = Self::profiles_dir()?;
495 let profile_path = profiles_dir.join(format!("{profile_name}.toml"));
496
497 if !profile_path.exists() {
498 return Err(crate::error::HylixError::config(format!(
499 "Profile '{}' not found at {}",
500 profile_name,
501 profile_path.display()
502 )));
503 }
504
505 let content = std::fs::read_to_string(&profile_path)?;
506 let profile: BakeProfile = toml::from_str(&content)
507 .map_err(crate::error::HylixError::Toml)
508 .with_context(|| {
509 format!(
510 "Failed to load profile from file {}",
511 profile_path.display()
512 )
513 })?;
514
515 log_info(&format!(
516 "Loaded profile '{}' from {}",
517 profile_name,
518 profile_path.display()
519 ));
520
521 Ok(profile)
522 }
523
524 pub fn create_default_profile(&self) -> crate::error::HylixResult<()> {
526 let profiles_dir = Self::profiles_dir()?;
527 std::fs::create_dir_all(&profiles_dir)?;
528
529 let profile_path = profiles_dir.join("bobalice.toml");
530
531 if !profile_path.exists() {
532 let default_profile = BakeProfile {
533 name: "bobalice".to_string(),
534 accounts: vec![
535 AccountConfig {
536 name: "bob".to_string(),
537 password: crate::constants::passwords::DEFAULT.to_string(),
538 invite_code: "vip".to_string(),
539 },
540 AccountConfig {
541 name: "alice".to_string(),
542 password: crate::constants::passwords::DEFAULT.to_string(),
543 invite_code: "vip".to_string(),
544 },
545 ],
546 funds: vec![
547 FundConfig {
548 from: "hyli".to_string(),
549 from_password: crate::constants::passwords::DEFAULT.to_string(),
550 amount: 1000,
551 token: "oranj".to_string(),
552 to: "bob".to_string(),
553 },
554 FundConfig {
555 from: "hyli".to_string(),
556 from_password: crate::constants::passwords::DEFAULT.to_string(),
557 amount: 1000,
558 token: "oranj".to_string(),
559 to: "alice".to_string(),
560 },
561 FundConfig {
562 from: "hyli".to_string(),
563 from_password: crate::constants::passwords::DEFAULT.to_string(),
564 amount: 500,
565 token: "oxygen".to_string(),
566 to: "bob".to_string(),
567 },
568 FundConfig {
569 from: "bob".to_string(),
570 from_password: crate::constants::passwords::DEFAULT.to_string(),
571 amount: 50,
572 token: "oxygen".to_string(),
573 to: "alice".to_string(),
574 },
575 ],
576 };
577
578 let content = toml::to_string_pretty(&default_profile)?;
579 std::fs::write(&profile_path, content)?;
580
581 log_info(&format!(
582 "Created default bobalice profile at {}",
583 profile_path.display()
584 ));
585 }
586
587 Ok(())
588 }
589}