Skip to main content

empoorio_sdk/
config.rs

1use serde::{Serialize, Deserialize};
2use std::{collections::HashMap, env, fs, path::Path};
3
4use anyhow::{Result, anyhow};
5use log::warn;
6
7const ENV_RPC_URL: &str = "EMPOORIO_RPC_URL";
8const ENV_WS_URL: &str = "EMPOORIO_WS_URL";
9const ENV_CHAIN_ID: &str = "EMPOORIO_CHAIN_ID";
10const ENV_AI_DATA_TYPE: &str = "EMPOORIO_AI_DATA_TYPE";
11const ENV_AI_DATA_SOURCES: &str = "EMPOORIO_AI_DATA_SOURCES";
12const ENV_AI_MAX_RESPONSES: &str = "EMPOORIO_AI_MAX_RESPONSES";
13const ENV_AI_REQUIRED_CONFIDENCE: &str = "EMPOORIO_AI_REQUIRED_CONFIDENCE";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct SdkConfig {
17    pub rpc_url: String,
18    pub ws_url: String,
19    pub chain_id: u32,
20    pub ai_default_data_type: String,
21    pub ai_default_sources: Vec<String>,
22    pub ai_default_max_responses: u32,
23    pub ai_default_required_confidence: u8,
24}
25
26pub const TESTNET_WS: &str = "ws://217.160.88.153:9944";
27pub const TESTNET_RPC: &str = "http://217.160.88.153:9944";
28pub const TESTNET_CHAIN_ID: u32 = 1001; 
29
30pub const MAINNET_WS: &str = "wss://mainnet.empooriochain.org/ws";
31pub const MAINNET_RPC: &str = "https://mainnet.empooriochain.org/rpc";
32pub const MAINNET_CHAIN_ID: u32 = 2026;
33
34impl Default for SdkConfig {
35    fn default() -> Self {
36        // En producción, el SDK debe apuntar a la Testnet oficial por defecto si no hay ENV configurado
37        Self {
38            rpc_url: env::var(ENV_RPC_URL).unwrap_or_else(|_| TESTNET_RPC.to_string()),
39            ws_url: env::var(ENV_WS_URL).unwrap_or_else(|_| TESTNET_WS.to_string()),
40            chain_id: env::var(ENV_CHAIN_ID)
41                .unwrap_or_else(|_| TESTNET_CHAIN_ID.to_string())
42                .parse()
43                .unwrap_or(TESTNET_CHAIN_ID),
44            ai_default_data_type: env::var(ENV_AI_DATA_TYPE).unwrap_or_else(|_| "AI_PROMPT".to_string()),
45            ai_default_sources: env::var(ENV_AI_DATA_SOURCES)
46                .map(|v| parse_csv(&v))
47                .unwrap_or_else(|_| vec!["Ailoos".to_string()]),
48            ai_default_max_responses: env::var(ENV_AI_MAX_RESPONSES)
49                .ok()
50                .and_then(|v| v.parse().ok())
51                .unwrap_or(5),
52            ai_default_required_confidence: env::var(ENV_AI_REQUIRED_CONFIDENCE)
53                .ok()
54                .and_then(|v| v.parse().ok())
55                .unwrap_or(80),
56        }
57    }
58}
59
60impl SdkConfig {
61    pub fn new(rpc_url: &str, ws_url: &str, chain_id: u32) -> Self {
62        let mut r_url = rpc_url.to_string();
63        let mut w_url = ws_url.to_string();
64
65        // Enforce RPC protocol (http/https)
66        if !r_url.starts_with("http") {
67            r_url = format!("https://{}", r_url); // Default to secure
68        }
69
70        // Enforce WS protocol (ws/wss)
71        if !w_url.starts_with("ws") {
72            if w_url.starts_with("http") {
73                w_url = w_url.replace("http", "ws");
74                if !w_url.starts_with("wss") && w_url.contains("empooriochain.org") {
75                    w_url = w_url.replace("ws", "wss");
76                }
77            } else {
78                w_url = format!("wss://{}", w_url); // Default to secure
79            }
80        }
81
82        Self {
83            rpc_url: r_url,
84            ws_url: w_url,
85            chain_id,
86            ai_default_data_type: "AI_PROMPT".to_string(),
87            ai_default_sources: vec!["Ailoos".to_string()],
88            ai_default_max_responses: 5,
89            ai_default_required_confidence: 80,
90        }
91    }
92
93    /// Crea una configuración predefinida para una red específica (mainnet, testnet).
94    pub fn for_network(network: &str) -> Self {
95        match network.to_lowercase().as_str() {
96            "mainnet" => Self::new(MAINNET_RPC, MAINNET_WS, MAINNET_CHAIN_ID),
97            "testnet" => Self::new(TESTNET_RPC, TESTNET_WS, TESTNET_CHAIN_ID),
98            _ => Self::default(),
99        }
100    }
101
102    /// Load configuration from a .env-style file, optionally overridden by env vars.
103    /// Env vars take precedence over file values.
104    pub fn from_env_or_file<P: AsRef<Path>>(path: P) -> Result<Self> {
105        let path = path.as_ref();
106        let file_map = if path.exists() {
107            check_protected_file(path)?;
108            let contents = fs::read_to_string(path)?;
109            parse_env_file(&contents)
110        } else {
111            HashMap::new()
112        };
113
114        let mut cfg = SdkConfig::default();
115        cfg.apply_kv_map(&file_map);
116        cfg.apply_env_overrides();
117        Ok(cfg)
118    }
119
120    /// Load configuration from env only (no file I/O).
121    pub fn from_env() -> Self {
122        SdkConfig::default()
123    }
124
125    fn apply_env_overrides(&mut self) {
126        if let Ok(v) = env::var(ENV_RPC_URL) { self.rpc_url = v; }
127        if let Ok(v) = env::var(ENV_WS_URL) { self.ws_url = v; }
128        if let Ok(v) = env::var(ENV_CHAIN_ID) {
129            self.chain_id = v.parse().unwrap_or(self.chain_id);
130        }
131        if let Ok(v) = env::var(ENV_AI_DATA_TYPE) { self.ai_default_data_type = v; }
132        if let Ok(v) = env::var(ENV_AI_DATA_SOURCES) { self.ai_default_sources = parse_csv(&v); }
133        if let Ok(v) = env::var(ENV_AI_MAX_RESPONSES) {
134            if let Ok(n) = v.parse() { self.ai_default_max_responses = n; }
135        }
136        if let Ok(v) = env::var(ENV_AI_REQUIRED_CONFIDENCE) {
137            if let Ok(n) = v.parse() { self.ai_default_required_confidence = n; }
138        }
139    }
140
141    fn apply_kv_map(&mut self, map: &HashMap<String, String>) {
142        if let Some(v) = map.get(ENV_RPC_URL) { self.rpc_url = v.clone(); }
143        if let Some(v) = map.get(ENV_WS_URL) { self.ws_url = v.clone(); }
144        if let Some(v) = map.get(ENV_CHAIN_ID) {
145            self.chain_id = v.parse().unwrap_or(self.chain_id);
146        }
147        if let Some(v) = map.get(ENV_AI_DATA_TYPE) { self.ai_default_data_type = v.clone(); }
148        if let Some(v) = map.get(ENV_AI_DATA_SOURCES) { self.ai_default_sources = parse_csv(v); }
149        if let Some(v) = map.get(ENV_AI_MAX_RESPONSES) {
150            if let Ok(n) = v.parse() { self.ai_default_max_responses = n; }
151        }
152        if let Some(v) = map.get(ENV_AI_REQUIRED_CONFIDENCE) {
153            if let Ok(n) = v.parse() { self.ai_default_required_confidence = n; }
154        }
155    }
156}
157
158fn parse_csv(s: &str) -> Vec<String> {
159    s.split(',')
160        .map(|v| v.trim())
161        .filter(|v| !v.is_empty())
162        .map(|v| v.to_string())
163        .collect()
164}
165
166fn parse_env_file(contents: &str) -> HashMap<String, String> {
167    let mut map = HashMap::new();
168    for line in contents.lines() {
169        let line = line.trim();
170        if line.is_empty() || line.starts_with('#') {
171            continue;
172        }
173        let line = line.strip_prefix("export ").unwrap_or(line);
174        let mut parts = line.splitn(2, '=');
175        let key = parts.next().unwrap_or("").trim();
176        let mut val = parts.next().unwrap_or("").trim().to_string();
177        if key.is_empty() {
178            continue;
179        }
180        if (val.starts_with('"') && val.ends_with('"')) || (val.starts_with('\'') && val.ends_with('\'')) {
181            val = val[1..val.len() - 1].to_string();
182        }
183        map.insert(key.to_string(), val);
184    }
185    map
186}
187
188fn check_protected_file(path: &Path) -> Result<()> {
189    #[cfg(unix)]
190    {
191        use std::os::unix::fs::MetadataExt;
192        let meta = fs::metadata(path)?;
193        let mode = meta.mode() & 0o777;
194        let group_other = mode & 0o077;
195        if group_other != 0 {
196            warn!(
197                "Config file {} is readable/writable by group/others (mode {:o}). Consider chmod 600.",
198                path.display(),
199                mode
200            );
201        }
202    }
203    if !path.is_file() {
204        return Err(anyhow!("Config path is not a file: {}", path.display()));
205    }
206    Ok(())
207}