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 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 if !r_url.starts_with("http") {
67 r_url = format!("https://{}", r_url); }
69
70 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); }
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 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 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 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}