Skip to main content

hylix/
config.rs

1use anyhow::Context;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5use crate::logging::{log_error, log_info};
6
7/// Default configuration version
8fn default_config_version() -> String {
9    "0.9.0".to_string()
10}
11
12/// Hylix configuration
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct HylixConfig {
15    /// Configuration schema version
16    #[serde(default = "default_config_version")]
17    pub version: String,
18    /// Default backend type for new projects
19    pub default_backend: BackendType,
20    /// Default scaffold repository URL
21    pub scaffold_repo: String,
22    /// Local devnet configuration
23    pub devnet: DevnetConfig,
24    /// Build configuration
25    pub build: BuildConfig,
26    /// Bake profile configuration
27    pub bake_profile: String,
28    /// Testing configuration
29    pub test: TestConfig,
30    /// Run configuration
31    pub run: RunConfig,
32}
33
34/// Testing configuration
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct TestConfig {
37    /// Print logs to console
38    pub print_server_logs: bool,
39    /// Clean data directory before running tests
40    pub clean_server_data: bool,
41}
42
43/// Run configuration
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct RunConfig {
46    /// Clean data directory before running
47    pub clean_server_data: bool,
48    /// Server port
49    pub server_port: u16,
50}
51
52/// Backend type enumeration
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, clap::ValueEnum)]
54pub enum BackendType {
55    Sp1,
56    Risc0,
57}
58
59/// Devnet configuration
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct DevnetConfig {
62    /// Custom image for the Hyli node and indexer
63    pub node_image: String,
64    /// Custom image for the wallet server
65    pub wallet_server_image: String,
66    /// Custom image for the wallet UI
67    pub wallet_ui_image: String,
68    /// Custom image for the registry server
69    pub registry_server_image: String,
70    /// Custom image for the registry UI
71    pub registry_ui_image: String,
72    /// Default port for the local node
73    pub node_port: u16,
74    /// Default port for the DA server
75    pub da_port: u16,
76    /// Default value for node'sRUST_LOG environment variable
77    pub node_rust_log: String,
78    /// Default port for the wallet app
79    pub wallet_api_port: u16,
80    /// Default port for the wallet WS
81    pub wallet_ws_port: u16,
82    /// Default port for the wallet UI
83    pub wallet_ui_port: u16,
84    /// Default port for the indexer
85    pub indexer_port: u16,
86    /// Default port for the postgres server
87    pub postgres_port: u16,
88    /// Default port for the registry server
89    pub registry_server_port: u16,
90    /// Default port for the registry UI
91    pub registry_ui_port: u16,
92    /// Auto-start devnet on test command
93    pub auto_start: bool,
94    /// Custom environment variables for containers
95    pub container_env: ContainerEnvConfig,
96}
97
98/// Container environment variables configuration
99#[derive(Debug, Clone, Serialize, Deserialize, Default)]
100pub struct ContainerEnvConfig {
101    /// Custom environment variables for the node container
102    pub node: Vec<String>,
103    /// Custom environment variables for the indexer container
104    pub indexer: Vec<String>,
105    /// Custom environment variables for the wallet server container
106    pub wallet_server: Vec<String>,
107    /// Custom environment variables for the wallet UI container
108    pub wallet_ui: Vec<String>,
109    /// Custom environment variables for the postgres container
110    pub postgres: Vec<String>,
111    /// Custom environment variables for the registry server container
112    pub registry_server: Vec<String>,
113}
114
115/// Build configuration
116#[derive(Debug, Clone, Serialize, Deserialize, Default)]
117pub struct BuildConfig {
118    /// Build in release mode by default
119    pub release: bool,
120    /// Number of parallel build jobs
121    pub jobs: Option<u32>,
122    /// Additional cargo build flags
123    pub extra_flags: Vec<String>,
124}
125
126/// Bake profile configuration
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct BakeProfile {
129    /// Name of the profile
130    pub name: String,
131    /// Accounts to create
132    pub accounts: Vec<AccountConfig>,
133    /// Funds to send to accounts
134    pub funds: Vec<FundConfig>,
135}
136
137/// Account configuration for baking
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct AccountConfig {
140    /// Account name
141    pub name: String,
142    /// Account password
143    pub password: String,
144    /// Account type (e.g., "vip")
145    pub invite_code: String,
146}
147
148/// Fund configuration for baking
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct FundConfig {
151    /// Source account name
152    pub from: String,
153    /// Source account password
154    pub from_password: String,
155    /// Amount to send
156    pub amount: u64,
157    /// Token type (e.g., "oranj", "oxygen")
158    pub token: String,
159    /// Destination account name
160    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    /// Load configuration from file or create default
223    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            // Parse as TOML value to check version
230            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            // Check version and migrate if needed
237            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                // Backup the old config before migration
251                Self::backup()?;
252
253                // Migrate the TOML value
254                toml_value = Self::migrate_toml(toml_value, file_version)?;
255
256                // Write the migrated config back to file
257                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            // Now parse the (possibly migrated) config
264            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    /// Migrate TOML configuration from previous versions
286    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
313/// Strategy pattern for config migrations
314trait ConfigMigration {
315    fn version(&self) -> &str;
316    fn migrate(&self, toml_value: toml::Value) -> crate::error::HylixResult<toml::Value>;
317}
318
319/// Migration from legacy configuration (no version field)
320struct 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            // Add version field
333            table.insert(
334                "version".to_string(),
335                toml::Value::String(current_version.clone()),
336            );
337
338            // Add node_rust_log field if devnet section exists
339            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
365/// Migration from version 0.6.0 to 0.9.0
366struct 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            // Update version field
379            table.insert(
380                "version".to_string(),
381                toml::Value::String(current_version.clone()),
382            );
383
384            // Add registry fields to devnet section
385            if let Some(devnet) = table.get_mut("devnet") {
386                if let Some(devnet_table) = devnet.as_table_mut() {
387                    // Add registry_server_image if missing
388                    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                    // Add registry_ui_image if missing
398                    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                    // Add registry_server_port if missing
408                    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                    // Add registry_ui_port if missing
415                    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            // Add registry_server to container_env if it exists
423            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    /// Save configuration to file
445    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    /// Backup configuration to file
458    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    /// Get the configuration file path
477    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    /// Get the profiles directory path
485    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    /// Load a bake profile by name
493    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    /// Create default bobalice profile if it doesn't exist
525    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}