1use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct BittensorConfig {
21 pub wallet_name: String,
23
24 pub hotkey_name: String,
26
27 pub network: String,
29
30 pub netuid: u16,
32
33 pub chain_endpoint: Option<String>,
35
36 #[serde(default)]
38 pub fallback_endpoints: Vec<String>,
39
40 pub weight_interval_secs: u64,
42
43 #[serde(default)]
46 pub read_only: bool,
47
48 #[serde(default)]
50 pub connection_pool_size: Option<usize>,
51
52 #[serde(default, with = "optional_duration_serde")]
54 pub health_check_interval: Option<Duration>,
55
56 #[serde(default)]
58 pub circuit_breaker_threshold: Option<u32>,
59
60 #[serde(default, with = "optional_duration_serde")]
62 pub circuit_breaker_recovery: Option<Duration>,
63}
64
65impl Default for BittensorConfig {
66 fn default() -> Self {
67 Self {
68 wallet_name: "default".to_string(),
69 hotkey_name: "default".to_string(),
70 network: "finney".to_string(),
71 netuid: 1,
72 chain_endpoint: None,
73 fallback_endpoints: Vec::new(),
74 weight_interval_secs: 300, read_only: false,
76 connection_pool_size: Some(3),
77 health_check_interval: Some(Duration::from_secs(60)),
78 circuit_breaker_threshold: Some(5),
79 circuit_breaker_recovery: Some(Duration::from_secs(60)),
80 }
81 }
82}
83
84impl BittensorConfig {
85 pub fn finney(wallet_name: &str, hotkey_name: &str, netuid: u16) -> Self {
96 Self {
97 wallet_name: wallet_name.to_string(),
98 hotkey_name: hotkey_name.to_string(),
99 network: "finney".to_string(),
100 netuid,
101 ..Default::default()
102 }
103 }
104
105 pub fn testnet(wallet_name: &str, hotkey_name: &str, netuid: u16) -> Self {
116 Self {
117 wallet_name: wallet_name.to_string(),
118 hotkey_name: hotkey_name.to_string(),
119 network: "test".to_string(),
120 netuid,
121 ..Default::default()
122 }
123 }
124
125 pub fn local(wallet_name: &str, hotkey_name: &str, netuid: u16) -> Self {
136 Self {
137 wallet_name: wallet_name.to_string(),
138 hotkey_name: hotkey_name.to_string(),
139 network: "local".to_string(),
140 netuid,
141 ..Default::default()
142 }
143 }
144
145 pub fn get_chain_endpoint(&self) -> String {
161 self.chain_endpoint
162 .clone()
163 .unwrap_or_else(|| match self.network.as_str() {
164 "local" => "ws://127.0.0.1:9944".to_string(),
165 "finney" => "wss://entrypoint-finney.opentensor.ai:443".to_string(),
166 "test" => "wss://test.finney.opentensor.ai:443".to_string(),
167 _ => panic!(
168 "Unknown network: {}. Valid networks are: finney, test, local",
169 self.network
170 ),
171 })
172 }
173
174 pub fn get_chain_endpoints(&self) -> Vec<String> {
189 let mut endpoints = vec![self.get_chain_endpoint()];
190
191 endpoints.extend(self.fallback_endpoints.clone());
193
194 if self.fallback_endpoints.is_empty() {
196 match self.network.as_str() {
197 "finney" => {
198 endpoints.push("wss://entrypoint-finney.opentensor.ai:443".to_string());
199 }
200 "test" => {
201 endpoints.push("wss://test.finney.opentensor.ai:443".to_string());
202 }
203 _ => {}
204 }
205 }
206
207 let mut seen = std::collections::HashSet::new();
209 endpoints.retain(|endpoint| seen.insert(endpoint.clone()));
210
211 endpoints
212 }
213
214 pub fn validate(&self) -> Result<(), String> {
230 if self.wallet_name.is_empty() {
231 return Err("Wallet name cannot be empty".to_string());
232 }
233
234 if self.hotkey_name.is_empty() {
235 return Err("Hotkey name cannot be empty".to_string());
236 }
237
238 if self.netuid == 0 {
239 return Err("Netuid must be greater than 0".to_string());
240 }
241
242 if self.weight_interval_secs == 0 {
243 return Err("Weight interval must be greater than 0 seconds".to_string());
244 }
245
246 match self.network.as_str() {
247 "finney" | "test" | "local" => Ok(()),
248 _ => Err(format!(
249 "Unknown network: {}. Valid networks are: finney, test, local",
250 self.network
251 )),
252 }
253 }
254
255 pub fn with_endpoint(mut self, endpoint: &str) -> Self {
257 self.chain_endpoint = Some(endpoint.to_string());
258 self
259 }
260
261 pub fn with_fallback_endpoints(mut self, endpoints: Vec<String>) -> Self {
263 self.fallback_endpoints = endpoints;
264 self
265 }
266
267 pub fn with_pool_size(mut self, size: usize) -> Self {
269 self.connection_pool_size = Some(size);
270 self
271 }
272
273 pub fn with_read_only(mut self, read_only: bool) -> Self {
275 self.read_only = read_only;
276 self
277 }
278}
279
280mod optional_duration_serde {
282 use serde::{Deserialize, Deserializer, Serialize, Serializer};
283 use std::time::Duration;
284
285 pub fn serialize<S>(value: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
286 where
287 S: Serializer,
288 {
289 match value {
290 Some(duration) => duration.as_secs().serialize(serializer),
291 None => serializer.serialize_none(),
292 }
293 }
294
295 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
296 where
297 D: Deserializer<'de>,
298 {
299 let opt: Option<u64> = Option::deserialize(deserializer)?;
300 Ok(opt.map(Duration::from_secs))
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
309 fn test_default_config() {
310 let config = BittensorConfig::default();
311 assert_eq!(config.wallet_name, "default");
312 assert_eq!(config.hotkey_name, "default");
313 assert_eq!(config.network, "finney");
314 assert_eq!(config.netuid, 1);
315 assert!(!config.read_only);
316 }
317
318 #[test]
319 fn test_finney_config() {
320 let config = BittensorConfig::finney("test_wallet", "test_hotkey", 42);
321 assert_eq!(config.wallet_name, "test_wallet");
322 assert_eq!(config.hotkey_name, "test_hotkey");
323 assert_eq!(config.network, "finney");
324 assert_eq!(config.netuid, 42);
325 }
326
327 #[test]
328 fn test_testnet_config() {
329 let config = BittensorConfig::testnet("wallet", "hotkey", 1);
330 assert_eq!(config.network, "test");
331 }
332
333 #[test]
334 fn test_local_config() {
335 let config = BittensorConfig::local("wallet", "hotkey", 1);
336 assert_eq!(config.network, "local");
337 assert_eq!(config.get_chain_endpoint(), "ws://127.0.0.1:9944");
338 }
339
340 #[test]
341 fn test_endpoint_resolution() {
342 let finney = BittensorConfig::finney("w", "h", 1);
343 assert_eq!(
344 finney.get_chain_endpoint(),
345 "wss://entrypoint-finney.opentensor.ai:443"
346 );
347
348 let test = BittensorConfig::testnet("w", "h", 1);
349 assert_eq!(
350 test.get_chain_endpoint(),
351 "wss://test.finney.opentensor.ai:443"
352 );
353
354 let local = BittensorConfig::local("w", "h", 1);
355 assert_eq!(local.get_chain_endpoint(), "ws://127.0.0.1:9944");
356 }
357
358 #[test]
359 fn test_custom_endpoint() {
360 let config = BittensorConfig::default().with_endpoint("wss://custom.endpoint:443");
361 assert_eq!(config.get_chain_endpoint(), "wss://custom.endpoint:443");
362 }
363
364 #[test]
365 fn test_fallback_endpoints() {
366 let config = BittensorConfig::default();
367 let endpoints = config.get_chain_endpoints();
368 assert!(!endpoints.is_empty());
369 assert_eq!(endpoints[0], config.get_chain_endpoint());
371 }
372
373 #[test]
374 fn test_validation_success() {
375 let config = BittensorConfig::default();
376 assert!(config.validate().is_ok());
377 }
378
379 #[test]
380 fn test_validation_empty_wallet() {
381 let config = BittensorConfig {
382 wallet_name: String::new(),
383 ..Default::default()
384 };
385 assert!(config.validate().is_err());
386 }
387
388 #[test]
389 fn test_validation_empty_hotkey() {
390 let config = BittensorConfig {
391 hotkey_name: String::new(),
392 ..Default::default()
393 };
394 assert!(config.validate().is_err());
395 }
396
397 #[test]
398 fn test_validation_zero_netuid() {
399 let config = BittensorConfig {
400 netuid: 0,
401 ..Default::default()
402 };
403 assert!(config.validate().is_err());
404 }
405
406 #[test]
407 fn test_validation_invalid_network() {
408 let config = BittensorConfig {
409 network: "invalid".to_string(),
410 ..Default::default()
411 };
412 assert!(config.validate().is_err());
413 }
414
415 #[test]
416 #[should_panic(expected = "Unknown network")]
417 fn test_invalid_network_endpoint() {
418 let config = BittensorConfig {
419 network: "invalid".to_string(),
420 ..Default::default()
421 };
422 config.get_chain_endpoint();
423 }
424
425 #[test]
426 fn test_builder_pattern() {
427 let config = BittensorConfig::finney("w", "h", 1)
428 .with_endpoint("wss://custom:443")
429 .with_pool_size(5)
430 .with_read_only(true);
431
432 assert_eq!(config.get_chain_endpoint(), "wss://custom:443");
433 assert_eq!(config.connection_pool_size, Some(5));
434 assert!(config.read_only);
435 }
436
437 #[test]
438 fn test_serialization() {
439 let config = BittensorConfig::default();
440 let json = serde_json::to_string(&config).unwrap();
441 let deserialized: BittensorConfig = serde_json::from_str(&json).unwrap();
442 assert_eq!(config.wallet_name, deserialized.wallet_name);
443 assert_eq!(config.network, deserialized.network);
444 }
445}