bittensor_rs/
config.rs

1//! # Bittensor Configuration
2//!
3//! Configuration types for the Bittensor SDK, including network settings,
4//! wallet configuration, and connection pool settings.
5
6use serde::{Deserialize, Serialize};
7use std::time::Duration;
8
9/// Bittensor network configuration
10///
11/// # Example
12///
13/// ```
14/// use bittensor_rs::config::BittensorConfig;
15///
16/// let config = BittensorConfig::default();
17/// assert_eq!(config.network, "finney");
18/// ```
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct BittensorConfig {
21    /// Wallet name for operations
22    pub wallet_name: String,
23
24    /// Hotkey name for the neuron
25    pub hotkey_name: String,
26
27    /// Network to connect to ("finney", "test", or "local")
28    pub network: String,
29
30    /// Subnet netuid
31    pub netuid: u16,
32
33    /// Optional chain endpoint override
34    pub chain_endpoint: Option<String>,
35
36    /// Optional fallback chain endpoints for failover
37    #[serde(default)]
38    pub fallback_endpoints: Vec<String>,
39
40    /// Weight setting interval in seconds
41    pub weight_interval_secs: u64,
42
43    /// Read-only mode (default: false)
44    /// When true, wallet is only used for querying metagraph, not signing transactions
45    #[serde(default)]
46    pub read_only: bool,
47
48    /// Connection pool size (default: 3)
49    #[serde(default)]
50    pub connection_pool_size: Option<usize>,
51
52    /// Health check interval (default: 60 seconds)
53    #[serde(default, with = "optional_duration_serde")]
54    pub health_check_interval: Option<Duration>,
55
56    /// Circuit breaker failure threshold (default: 5)
57    #[serde(default)]
58    pub circuit_breaker_threshold: Option<u32>,
59
60    /// Circuit breaker recovery timeout (default: 60 seconds)
61    #[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, // 5 minutes
75            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    /// Create a new configuration for the finney network
86    ///
87    /// # Example
88    ///
89    /// ```
90    /// use bittensor_rs::config::BittensorConfig;
91    ///
92    /// let config = BittensorConfig::finney("my_wallet", "my_hotkey", 1);
93    /// assert_eq!(config.network, "finney");
94    /// ```
95    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    /// Create a new configuration for the test network
106    ///
107    /// # Example
108    ///
109    /// ```
110    /// use bittensor_rs::config::BittensorConfig;
111    ///
112    /// let config = BittensorConfig::testnet("my_wallet", "my_hotkey", 1);
113    /// assert_eq!(config.network, "test");
114    /// ```
115    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    /// Create a new configuration for a local network
126    ///
127    /// # Example
128    ///
129    /// ```
130    /// use bittensor_rs::config::BittensorConfig;
131    ///
132    /// let config = BittensorConfig::local("my_wallet", "my_hotkey", 1);
133    /// assert_eq!(config.network, "local");
134    /// ```
135    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    /// Get the chain endpoint, auto-detecting based on network if not explicitly configured
146    ///
147    /// # Panics
148    ///
149    /// Panics if the network is not one of: finney, test, local
150    ///
151    /// # Example
152    ///
153    /// ```
154    /// use bittensor_rs::config::BittensorConfig;
155    ///
156    /// let config = BittensorConfig::default();
157    /// let endpoint = config.get_chain_endpoint();
158    /// assert!(endpoint.starts_with("wss://"));
159    /// ```
160    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    /// Get all chain endpoints including fallbacks
175    ///
176    /// Returns the primary endpoint followed by any configured fallback endpoints.
177    /// If no fallbacks are configured, network-specific defaults are added.
178    ///
179    /// # Example
180    ///
181    /// ```
182    /// use bittensor_rs::config::BittensorConfig;
183    ///
184    /// let config = BittensorConfig::default();
185    /// let endpoints = config.get_chain_endpoints();
186    /// assert!(!endpoints.is_empty());
187    /// ```
188    pub fn get_chain_endpoints(&self) -> Vec<String> {
189        let mut endpoints = vec![self.get_chain_endpoint()];
190
191        // Add configured fallback endpoints
192        endpoints.extend(self.fallback_endpoints.clone());
193
194        // Add network-specific default fallbacks if not already configured
195        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        // Deduplicate endpoints while preserving order
208        let mut seen = std::collections::HashSet::new();
209        endpoints.retain(|endpoint| seen.insert(endpoint.clone()));
210
211        endpoints
212    }
213
214    /// Validate the configuration
215    ///
216    /// # Returns
217    ///
218    /// * `Ok(())` if the configuration is valid
219    /// * `Err(String)` with a description of the validation error
220    ///
221    /// # Example
222    ///
223    /// ```
224    /// use bittensor_rs::config::BittensorConfig;
225    ///
226    /// let config = BittensorConfig::default();
227    /// assert!(config.validate().is_ok());
228    /// ```
229    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    /// Set a custom chain endpoint
256    pub fn with_endpoint(mut self, endpoint: &str) -> Self {
257        self.chain_endpoint = Some(endpoint.to_string());
258        self
259    }
260
261    /// Set fallback endpoints
262    pub fn with_fallback_endpoints(mut self, endpoints: Vec<String>) -> Self {
263        self.fallback_endpoints = endpoints;
264        self
265    }
266
267    /// Set connection pool size
268    pub fn with_pool_size(mut self, size: usize) -> Self {
269        self.connection_pool_size = Some(size);
270        self
271    }
272
273    /// Set read-only mode
274    pub fn with_read_only(mut self, read_only: bool) -> Self {
275        self.read_only = read_only;
276        self
277    }
278}
279
280/// Serde helper for optional Duration fields
281mod 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        // Primary endpoint should be first
370        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}