bittensor_rs/
service.rs

1//! # Bittensor Service
2//!
3//! Central service for all Bittensor chain interactions with connection pooling,
4//! automatic failover, and circuit breaker protection.
5//!
6//! The [`Service`] struct is the main entry point for interacting with the Bittensor
7//! blockchain. It manages:
8//!
9//! - **Connection pooling**: Multiple connections with automatic health checks
10//! - **Retry logic**: Exponential backoff for transient failures
11//! - **Circuit breaker**: Prevents cascade failures during outages
12//! - **Transaction signing**: Uses the configured wallet hotkey
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use bittensor_rs::{config::BittensorConfig, Service};
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
21//!     let config = BittensorConfig::finney("my_wallet", "my_hotkey", 1);
22//!     let service = Service::new(config).await?;
23//!     
24//!     // Query metagraph
25//!     let metagraph = service.get_metagraph(1).await?;
26//!     println!("Neurons: {}", metagraph.hotkeys.len());
27//!     
28//!     // Set weights
29//!     service.set_weights(1, vec![(0, 100), (1, 200)]).await?;
30//!     
31//!     // Graceful shutdown
32//!     service.shutdown().await;
33//!     Ok(())
34//! }
35//! ```
36//!
37//! # Connection Monitoring
38//!
39//! Monitor connection health and metrics:
40//!
41//! ```rust,no_run
42//! # use bittensor_rs::{config::BittensorConfig, Service};
43//! # async fn example(service: Service) -> Result<(), Box<dyn std::error::Error>> {
44//! let metrics = service.connection_metrics().await;
45//! println!("Healthy: {}/{}", metrics.healthy_connections, metrics.total_connections);
46//!
47//! // Force reconnect if needed
48//! service.force_reconnect().await?;
49//! # Ok(())
50//! # }
51//! ```
52
53use crate::config::BittensorConfig;
54use crate::connect::{CircuitBreaker, HealthChecker, RetryConfig, RetryNode};
55use crate::connect::{ConnectionManager, ConnectionPool, ConnectionPoolBuilder};
56use crate::error::BittensorError;
57use crate::utils::{set_weights_payload, NormalizedWeight};
58use crate::AccountId;
59use anyhow::Result;
60
61// Wallet utilities - we'll implement these
62use std::path::PathBuf;
63
64// Always use our own generated API module
65use crate::api::api;
66use std::net::SocketAddr;
67use std::sync::Arc;
68use std::time::Duration;
69use tracing::{debug, info, warn};
70
71// Use subxt directly with our generated API
72use subxt_signer::sr25519::Keypair;
73
74// Type alias for our chain client removed; Service uses pooled clients via connect::pool
75
76// Wallet helper functions
77fn home_hotkey_location(wallet_name: &str, hotkey_name: &str) -> Option<PathBuf> {
78    home::home_dir().map(|home| {
79        home.join(".bittensor")
80            .join("wallets")
81            .join(wallet_name)
82            .join("hotkeys")
83            .join(hotkey_name)
84    })
85}
86
87fn load_key_seed(path: &PathBuf) -> Result<String, Box<dyn std::error::Error>> {
88    let content = std::fs::read_to_string(path)?;
89
90    // Try to parse as JSON first (new format)
91    if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&content) {
92        if let Some(secret_phrase) = json_value.get("secretPhrase").and_then(|v| v.as_str()) {
93            return Ok(secret_phrase.to_string());
94        }
95        // Fall back to secretSeed if secretPhrase is not available
96        if let Some(secret_seed) = json_value.get("secretSeed").and_then(|v| v.as_str()) {
97            return Ok(secret_seed.to_string());
98        }
99        return Err("JSON wallet file missing secretPhrase or secretSeed".into());
100    }
101
102    // If not JSON, assume it's a raw seed phrase (old format)
103    Ok(content.trim().to_string())
104}
105
106fn signer_from_seed(seed: &str) -> Result<Keypair, Box<dyn std::error::Error + Send + Sync>> {
107    use subxt_signer::SecretUri;
108    
109    // Parse the seed as a SecretUri (handles mnemonic, hex seeds, etc.)
110    let uri: SecretUri = seed.parse()?;
111    let keypair = Keypair::from_uri(&uri)?;
112    Ok(keypair)
113}
114
115// Import the metagraph types
116use crate::{Metagraph, SelectiveMetagraph};
117
118/// Central service for Bittensor chain interactions with connection pooling and retry mechanisms
119pub struct Service {
120    config: BittensorConfig,
121    connection_pool: Arc<ConnectionPool>,
122    connection_manager: Arc<ConnectionManager>,
123    signer: Keypair,
124    retry_node: RetryNode,
125    circuit_breaker: Arc<tokio::sync::Mutex<CircuitBreaker>>,
126    health_monitor_handle: Option<tokio::task::JoinHandle<()>>,
127}
128
129impl Service {
130    /// Creates a new Service instance with the provided configuration.
131    ///
132    /// # Arguments
133    ///
134    /// * `config` - The Bittensor configuration containing network and wallet settings
135    ///
136    /// # Returns
137    ///
138    /// * `Result<Self, BittensorError>` - A Result containing either the initialized Service or an error
139    ///
140    /// # Errors
141    ///
142    /// * `NetworkError` - If connection to the client network fails
143    /// * `WalletError` - If wallet key loading or signer creation fails
144    pub async fn new(config: BittensorConfig) -> Result<Self, BittensorError> {
145        info!(
146            "Initializing enhanced Bittensor service for network: {}",
147            config.network
148        );
149
150        // Build connection pool with configuration
151        let pool = Arc::new(
152            ConnectionPoolBuilder::new(config.get_chain_endpoints())
153                .max_connections(config.connection_pool_size.unwrap_or(3))
154                .retry_config(RetryConfig::network())
155                .build(),
156        );
157
158        // Initialize pool with retry logic
159        let retry_node = RetryNode::new().with_timeout(Duration::from_secs(120));
160
161        retry_node
162            .execute_with_config(
163                || async {
164                    pool.initialize()
165                        .await
166                        .map_err(|e| BittensorError::NetworkError {
167                            message: format!("Pool initialization failed: {}", e),
168                        })
169                },
170                RetryConfig::network(),
171            )
172            .await?;
173
174        info!(
175            "Connection pool initialized with {} healthy connections",
176            pool.healthy_connection_count().await
177        );
178
179        // Create connection manager for state tracking
180        let connection_manager = Arc::new(ConnectionManager::new(config.clone()));
181
182        // Start health monitoring
183        let health_checker = Arc::new(
184            HealthChecker::new()
185                .with_interval(
186                    config
187                        .health_check_interval
188                        .unwrap_or(Duration::from_secs(60)),
189                )
190                .with_timeout(Duration::from_secs(5))
191                .with_failure_threshold(3),
192        );
193
194        let monitor_handle = health_checker.start_monitoring(Arc::clone(&pool));
195
196        // Load wallet signer
197        let hotkey_path = home_hotkey_location(&config.wallet_name, &config.hotkey_name)
198            .ok_or_else(|| BittensorError::WalletError {
199                message: "Failed to find home directory".to_string(),
200            })?;
201
202        if config.read_only {
203            info!(
204                "Loading hotkey from path: {:?} in READ-ONLY mode (wallet: {}, hotkey: {})",
205                hotkey_path, config.wallet_name, config.hotkey_name
206            );
207            info!("Read-only mode: wallet is used for metagraph queries only, not for signing transactions");
208        } else {
209            info!(
210                "Loading hotkey from path: {:?} (wallet: {}, hotkey: {})",
211                hotkey_path, config.wallet_name, config.hotkey_name
212            );
213        }
214
215        let seed = load_key_seed(&hotkey_path).map_err(|e| BittensorError::WalletError {
216            message: format!("Failed to load hotkey from {hotkey_path:?}: {e}"),
217        })?;
218
219        let signer = signer_from_seed(&seed).map_err(|e| BittensorError::WalletError {
220            message: format!("Failed to create signer from seed: {e}"),
221        })?;
222
223        // Initialize circuit breaker for cascade failure prevention
224        let circuit_breaker = Arc::new(tokio::sync::Mutex::new(CircuitBreaker::new(
225            config.circuit_breaker_threshold.unwrap_or(5),
226            config
227                .circuit_breaker_recovery
228                .unwrap_or(Duration::from_secs(60)),
229        )));
230
231        let service = Self {
232            config,
233            connection_pool: pool,
234            connection_manager,
235            signer,
236            retry_node,
237            circuit_breaker,
238            health_monitor_handle: Some(monitor_handle),
239        };
240
241        info!("Enhanced Bittensor service initialized with connection pooling");
242        Ok(service)
243    }
244
245    /// Serves an axon on the Bittensor network with retry logic.
246    ///
247    /// # Arguments
248    ///
249    /// * `netuid` - The subnet UID to serve the axon on
250    /// * `axon_addr` - The socket address where the axon will be served
251    ///
252    /// # Returns
253    ///
254    /// * `Result<(), BittensorError>` - A Result indicating success or failure
255    ///
256    /// # Errors
257    ///
258    /// * `TxSubmissionError` - If the serve_axon transaction fails to submit
259    /// * `MaxRetriesExceeded` - If all retry attempts are exhausted
260    pub async fn serve_axon(
261        &self,
262        netuid: u16,
263        axon_addr: SocketAddr,
264    ) -> Result<(), BittensorError> {
265        info!(
266            "Serving axon for netuid {} at {} with retry logic",
267            netuid, axon_addr
268        );
269
270        let operation = || {
271            // Create serve_axon payload using our generated API
272            let (ip, ip_type): (u128, u8) = match axon_addr.ip() {
273                std::net::IpAddr::V4(ipv4) => (u32::from(ipv4) as u128, 4),
274                std::net::IpAddr::V6(ipv6) => (u128::from(ipv6), 6),
275            };
276            let port = axon_addr.port();
277            let protocol = 0; // TCP = 0, UDP = 1
278
279            let payload = api::tx().subtensor_module().serve_axon(
280                netuid, 0, // version (u32)
281                ip, port, ip_type, protocol, 0, // placeholder1
282                0, // placeholder2
283            );
284
285            let signer = &self.signer;
286
287            async move {
288                // Get a healthy client from the pool
289                let client = self
290                    .connection_pool
291                    .get_healthy_client()
292                    .await
293                    .map_err(|e| BittensorError::NetworkError {
294                        message: format!("Failed to get healthy client: {}", e),
295                    })?;
296
297                client
298                    .tx()
299                    .sign_and_submit_then_watch_default(&payload, signer)
300                    .await
301                    .map_err(|e| {
302                        let err_msg = e.to_string();
303                        let err_lower = err_msg.to_lowercase();
304
305                        if err_lower.contains("timeout") {
306                            BittensorError::TxTimeoutError {
307                                message: format!("serve_axon transaction timeout: {err_msg}"),
308                                timeout: Duration::from_secs(60),
309                            }
310                        } else if err_lower.contains("fee") || err_lower.contains("balance") {
311                            BittensorError::InsufficientTxFees {
312                                required: 0,
313                                available: 0,
314                            }
315                        } else if err_lower.contains("nonce") {
316                            BittensorError::InvalidNonce {
317                                expected: 0,
318                                actual: 0,
319                            }
320                        } else {
321                            BittensorError::TxSubmissionError {
322                                message: format!("Failed to submit serve_axon: {err_msg}"),
323                            }
324                        }
325                    })?;
326                Ok(())
327            }
328        };
329
330        self.retry_node.execute(operation).await?;
331        info!("Axon served successfully");
332        Ok(())
333    }
334
335    /// Sets weights for neurons in the subnet with retry logic.
336    ///
337    /// # Arguments
338    ///
339    /// * `netuid` - The subnet UID to set weights for
340    /// * `weights` - Vector of (uid, weight) pairs representing neuron weights
341    ///
342    /// # Returns
343    ///
344    /// * `Result<(), BittensorError>` - A Result indicating success or failure
345    ///
346    /// # Errors
347    ///
348    /// * `TxSubmissionError` - If the set_weights transaction fails to submit
349    /// * `InvalidWeights` - If the weight vector is invalid
350    /// * `MaxRetriesExceeded` - If all retry attempts are exhausted
351    pub async fn set_weights(
352        &self,
353        netuid: u16,
354        weights: Vec<(u16, u16)>,
355    ) -> Result<(), BittensorError> {
356        info!(
357            "Setting weights for netuid {} with {} weights using retry logic",
358            netuid,
359            weights.len()
360        );
361
362        // Validate weights before attempting transaction
363        if weights.is_empty() {
364            return Err(BittensorError::InvalidWeights {
365                reason: "Weight vector cannot be empty".to_string(),
366            });
367        }
368
369        // Check for duplicate UIDs
370        let mut seen_uids = std::collections::HashSet::new();
371        for (uid, _) in &weights {
372            if !seen_uids.insert(*uid) {
373                return Err(BittensorError::InvalidWeights {
374                    reason: format!("Duplicate UID found: {uid}"),
375                });
376            }
377        }
378
379        let operation = || {
380            // Convert to NormalizedWeight format
381            let normalized_weights: Vec<NormalizedWeight> = weights
382                .iter()
383                .map(|(uid, weight)| NormalizedWeight {
384                    uid: *uid,
385                    weight: *weight,
386                })
387                .collect();
388
389            // Create set_weights payload with version_key = 0
390            let payload = set_weights_payload(netuid, normalized_weights, 0);
391            let signer = &self.signer;
392
393            async move {
394                // Get a healthy client from the pool
395                let client = self
396                    .connection_pool
397                    .get_healthy_client()
398                    .await
399                    .map_err(|e| BittensorError::NetworkError {
400                        message: format!("Failed to get healthy client: {}", e),
401                    })?;
402
403                client
404                    .tx()
405                    .sign_and_submit_then_watch_default(&payload, signer)
406                    .await
407                    .map_err(|e| {
408                        let err_msg = e.to_string();
409                        let err_lower = err_msg.to_lowercase();
410
411                        if err_lower.contains("timeout") {
412                            BittensorError::TxTimeoutError {
413                                message: format!("set_weights transaction timeout: {err_msg}"),
414                                timeout: Duration::from_secs(120),
415                            }
416                        } else if err_lower.contains("weight") || err_lower.contains("invalid") {
417                            BittensorError::WeightSettingFailed {
418                                netuid,
419                                reason: format!("Weight validation failed: {err_msg}"),
420                            }
421                        } else if err_lower.contains("fee") || err_lower.contains("balance") {
422                            BittensorError::InsufficientTxFees {
423                                required: 0,
424                                available: 0,
425                            }
426                        } else if err_lower.contains("nonce") {
427                            BittensorError::InvalidNonce {
428                                expected: 0,
429                                actual: 0,
430                            }
431                        } else {
432                            BittensorError::TxSubmissionError {
433                                message: format!("Failed to submit set_weights: {err_msg}"),
434                            }
435                        }
436                    })?;
437                Ok(())
438            }
439        };
440
441        self.retry_node.execute(operation).await?;
442        info!("Weights set successfully for netuid {}", netuid);
443        Ok(())
444    }
445
446    /// Gets neuron information for a specific UID in the subnet.
447    ///
448    /// # Arguments
449    ///
450    /// * `netuid` - The subnet UID
451    /// * `uid` - The neuron UID to get information for
452    ///
453    /// # Returns
454    ///
455    /// * `Result<Option<NeuronInfo>, BittensorError>` - A Result containing either the neuron info or an error
456    ///
457    /// # Errors
458    ///
459    /// * `RpcError` - If the RPC call fails
460    pub async fn get_neuron(
461        &self,
462        netuid: u16,
463        uid: u16,
464    ) -> Result<Option<crate::NeuronInfo>, BittensorError> {
465        debug!("Getting neuron info for UID: {} on netuid: {}", uid, netuid);
466
467        // Get a healthy client from the pool
468        let client = self
469            .connection_pool
470            .get_healthy_client()
471            .await
472            .map_err(|e| BittensorError::NetworkError {
473                message: format!("Failed to get healthy client: {}", e),
474            })?;
475
476        let runtime_api =
477            client
478                .runtime_api()
479                .at_latest()
480                .await
481                .map_err(|e| BittensorError::RpcError {
482                    message: format!("Failed to get runtime API: {e}"),
483                })?;
484
485        let neuron_info = runtime_api
486            .call(
487                api::runtime_apis::neuron_info_runtime_api::NeuronInfoRuntimeApi
488                    .get_neuron(netuid, uid),
489            )
490            .await
491            .map_err(|e| BittensorError::RpcError {
492                message: format!("Failed to call get_neuron: {e}"),
493            })?;
494
495        Ok(neuron_info)
496    }
497
498    /// Gets the complete metagraph for a subnet with circuit breaker protection.
499    ///
500    /// # Arguments
501    ///
502    /// * `netuid` - The subnet UID to get the metagraph for
503    ///
504    /// # Returns
505    ///
506    /// * `Result<Metagraph, BittensorError>` - A Result containing either the metagraph or an error
507    ///
508    /// # Errors
509    ///
510    /// * `RpcError` - If the RPC call fails
511    /// * `SubnetNotFound` - If the subnet doesn't exist
512    /// * `ServiceUnavailable` - If circuit breaker is open
513    pub async fn get_metagraph(&self, netuid: u16) -> Result<Metagraph, BittensorError> {
514        info!(
515            "Fetching metagraph for netuid: {} with circuit breaker protection",
516            netuid
517        );
518
519        let operation = || {
520            async move {
521                // Get a healthy client from the pool
522                let client = self
523                    .connection_pool
524                    .get_healthy_client()
525                    .await
526                    .map_err(|e| BittensorError::NetworkError {
527                        message: format!("Failed to get healthy client: {}", e),
528                    })?;
529
530                let runtime_api = client.runtime_api().at_latest().await.map_err(|e| {
531                    let err_msg = e.to_string();
532                    let err_lower = err_msg.to_lowercase();
533
534                    if err_lower.contains("timeout") {
535                        BittensorError::RpcTimeoutError {
536                            message: format!("Runtime API timeout: {err_msg}"),
537                            timeout: Duration::from_secs(30),
538                        }
539                    } else if err_lower.contains("connection") {
540                        BittensorError::RpcConnectionError {
541                            message: format!("Runtime API connection failed: {err_msg}"),
542                        }
543                    } else {
544                        BittensorError::RpcMethodError {
545                            method: "runtime_api".to_string(),
546                            message: err_msg,
547                        }
548                    }
549                })?;
550
551                let metagraph = runtime_api
552                    .call(
553                        api::runtime_apis::subnet_info_runtime_api::SubnetInfoRuntimeApi
554                            .get_metagraph(netuid),
555                    )
556                    .await
557                    .map_err(|e| {
558                        let err_msg = e.to_string();
559                        if err_msg.to_lowercase().contains("timeout") {
560                            BittensorError::RpcTimeoutError {
561                                message: format!("get_metagraph call timeout: {err_msg}"),
562                                timeout: Duration::from_secs(30),
563                            }
564                        } else {
565                            BittensorError::RpcMethodError {
566                                method: "get_metagraph".to_string(),
567                                message: err_msg,
568                            }
569                        }
570                    })?
571                    .ok_or(BittensorError::SubnetNotFound { netuid })?;
572
573                Ok(metagraph)
574            }
575        };
576
577        // Use circuit breaker for RPC calls
578        // Clone the circuit breaker to avoid holding the lock across await
579        let mut circuit_breaker = {
580            let cb = self.circuit_breaker.lock().await;
581            cb.clone()
582        };
583        let result = circuit_breaker.execute(operation).await;
584
585        // Update the original circuit breaker with the new state
586        {
587            let mut original_cb = self.circuit_breaker.lock().await;
588            *original_cb = circuit_breaker;
589        }
590
591        match &result {
592            Ok(_) => info!("Metagraph fetched successfully for netuid: {}", netuid),
593            Err(e) => warn!("Failed to fetch metagraph for netuid {}: {}", netuid, e),
594        }
595
596        result
597    }
598
599    /// Retrieves a selective metagraph for a specific subnet, containing only the requested fields.
600    ///
601    /// # Arguments
602    ///
603    /// * `netuid` - The subnet UID to get the selective metagraph for
604    /// * `fields` - Vector of field indices to include in the selective metagraph
605    ///
606    /// # Returns
607    ///
608    /// * `Result<SelectiveMetagraph, BittensorError>` - A Result containing either the selective metagraph or an error
609    ///
610    /// # Errors
611    ///
612    /// * `RpcError` - If connection to the runtime API fails
613    /// * `RpcError` - If the selective metagraph call fails
614    /// * `RpcError` - If no selective metagraph is found for the subnet
615    ///
616    /// # Example
617    ///
618    /// ```rust,no_run
619    /// # use bittensor_rs::Service;
620    /// # use bittensor_rs::config::BittensorConfig;
621    /// # #[tokio::main]
622    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
623    /// # let config = BittensorConfig::default();
624    /// # let service = Service::new(config).await?;
625    /// let fields = vec![0, 1, 2]; // Include first three fields
626    /// let selective_metagraph = service.get_selective_metagraph(1, fields).await?;
627    /// # Ok(())
628    /// # }
629    /// ```
630    pub async fn get_selective_metagraph(
631        &self,
632        netuid: u16,
633        fields: Vec<u16>,
634    ) -> Result<SelectiveMetagraph, BittensorError> {
635        info!(
636            "Fetching selective metagraph for netuid: {} with {} fields",
637            netuid,
638            fields.len()
639        );
640
641        // Get a healthy client from the pool
642        let client = self
643            .connection_pool
644            .get_healthy_client()
645            .await
646            .map_err(|e| BittensorError::NetworkError {
647                message: format!("Failed to get healthy client: {}", e),
648            })?;
649
650        let runtime_api =
651            client
652                .runtime_api()
653                .at_latest()
654                .await
655                .map_err(|e| BittensorError::RpcError {
656                    message: format!("Failed to get runtime API: {e}"),
657                })?;
658
659        let selective_metagraph = runtime_api
660            .call(
661                api::runtime_apis::subnet_info_runtime_api::SubnetInfoRuntimeApi
662                    .get_selective_metagraph(netuid, fields),
663            )
664            .await
665            .map_err(|e| BittensorError::RpcError {
666                message: format!("Failed to call get_selective_metagraph: {e}"),
667            })?
668            .ok_or_else(|| BittensorError::RpcError {
669                message: format!("Selective metagraph not found for subnet {netuid}"),
670            })?;
671
672        Ok(selective_metagraph)
673    }
674
675    /// Retrieves the current block number from the Bittensor network.
676    ///
677    /// # Returns
678    ///
679    /// * `Result<u64, BittensorError>` - A Result containing either the current block number or an error
680    ///
681    /// # Errors
682    ///
683    /// * `RpcError` - If connection to the latest block fails
684    ///
685    /// # Example
686    ///
687    /// ```rust,no_run
688    /// # use bittensor_rs::Service;
689    /// # use bittensor_rs::config::BittensorConfig;
690    /// # #[tokio::main]
691    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
692    /// # let config = BittensorConfig::default();
693    /// # let service = Service::new(config).await?;
694    /// let block_number = service.get_block_number().await?;
695    /// println!("Current block: {}", block_number);
696    /// # Ok(())
697    /// # }
698    /// ```
699    pub async fn get_block_number(&self) -> Result<u64, BittensorError> {
700        // Get a healthy client from the pool
701        let client = self
702            .connection_pool
703            .get_healthy_client()
704            .await
705            .map_err(|e| BittensorError::NetworkError {
706                message: format!("Failed to get healthy client: {}", e),
707            })?;
708
709        let latest_block =
710            client
711                .blocks()
712                .at_latest()
713                .await
714                .map_err(|e| BittensorError::RpcError {
715                    message: format!("Failed to get latest block: {e}"),
716                })?;
717
718        Ok(latest_block.number().into())
719    }
720
721    /// Returns the account ID associated with the service's signer.
722    ///
723    /// # Returns
724    ///
725    /// * `&AccountId` - Reference to the signer's account ID
726    ///
727    /// # Example
728    ///
729    /// ```rust,no_run
730    /// # use bittensor_rs::Service;
731    /// # use bittensor_rs::config::BittensorConfig;
732    /// # #[tokio::main]
733    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
734    /// # let config = BittensorConfig::default();
735    /// # let service = Service::new(config).await?;
736    /// let account_id = service.get_account_id();
737    /// println!("Account ID: {}", account_id);
738    /// # Ok(())
739    /// # }
740    /// ```
741    pub fn get_account_id(&self) -> AccountId {
742        subxt::config::polkadot::AccountId32::from(self.signer.public_key().0)
743    }
744
745    /// Get current block number (alias for get_block_number)
746    pub async fn get_current_block(&self) -> Result<u64, BittensorError> {
747        self.get_block_number().await
748    }
749
750    /// Submit an extrinsic (transaction) to the chain
751    pub async fn submit_extrinsic<T>(&self, payload: T) -> Result<(), BittensorError>
752    where
753        T: subxt::tx::Payload,
754    {
755        // Get a healthy client from the pool
756        let client = self
757            .connection_pool
758            .get_healthy_client()
759            .await
760            .map_err(|e| BittensorError::NetworkError {
761                message: format!("Failed to get healthy client: {}", e),
762            })?;
763
764        let tx_result = client
765            .tx()
766            .sign_and_submit_default(&payload, &self.signer)
767            .await
768            .map_err(|e| BittensorError::TxSubmissionError {
769                message: format!("Failed to submit extrinsic: {e}"),
770            })?;
771
772        info!("Transaction submitted with hash: {:?}", tx_result);
773        Ok(())
774    }
775
776    /// Returns the configured network name for this service instance.
777    ///
778    /// # Returns
779    ///
780    /// * `&str` - Reference to the network name
781    ///
782    /// # Example
783    ///
784    /// ```rust,no_run
785    /// # use bittensor_rs::Service;
786    /// # use bittensor_rs::config::BittensorConfig;
787    /// # #[tokio::main]
788    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
789    /// # let config = BittensorConfig::default();
790    /// # let service = Service::new(config).await?;
791    /// let network = service.get_network();
792    /// println!("Connected to network: {}", network);
793    /// # Ok(())
794    /// # }
795    /// ```
796    pub fn get_network(&self) -> &str {
797        &self.config.network
798    }
799
800    /// Returns the configured subnet UID for this service instance.
801    ///
802    /// # Returns
803    ///
804    /// * `u16` - The subnet UID
805    ///
806    /// # Example
807    ///
808    /// ```rust,no_run
809    /// # use bittensor_rs::Service;
810    /// # use bittensor_rs::config::BittensorConfig;
811    /// # #[tokio::main]
812    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
813    /// # let config = BittensorConfig::default();
814    /// # let service = Service::new(config).await?;
815    /// let netuid = service.get_netuid();
816    /// println!("Subnet UID: {}", netuid);
817    /// # Ok(())
818    /// # }
819    /// ```
820    pub fn get_netuid(&self) -> u16 {
821        self.config.netuid
822    }
823
824    /// Sign data with the service's signer (hotkey)
825    ///
826    /// This method signs arbitrary data with the validator/miner's hotkey.
827    /// The signature can be verified using `verify_bittensor_signature`.
828    ///
829    /// # Arguments
830    ///
831    /// * `data` - The data to sign
832    ///
833    /// # Returns
834    ///
835    /// * `Result<String, BittensorError>` - Hex-encoded signature string
836    ///
837    /// # Example
838    ///
839    /// ```rust,no_run
840    /// # use bittensor_rs::Service;
841    /// # use bittensor_rs::config::BittensorConfig;
842    /// # #[tokio::main]
843    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
844    /// # let config = BittensorConfig::default();
845    /// # let service = Service::new(config).await?;
846    /// let nonce = "test-nonce-123";
847    /// let signature = service.sign_data(nonce.as_bytes())?;
848    /// println!("Signature: {}", signature);
849    /// # Ok(())
850    /// # }
851    /// ```
852    pub fn sign_data(&self, data: &[u8]) -> Result<String, BittensorError> {
853        // Sign the data with our signer
854        let signature = self.signer.sign(data);
855        Ok(hex::encode(signature.0))
856    }
857}
858
859impl Service {
860    /// Gets retry statistics for monitoring
861    pub async fn get_retry_stats(&self) -> RetryStats {
862        RetryStats {
863            circuit_breaker_state: {
864                let cb = self.circuit_breaker.lock().await;
865                format!("{cb:?}")
866            },
867        }
868    }
869
870    /// Resets the circuit breaker state (for recovery operations)
871    pub async fn reset_circuit_breaker(&self) {
872        let mut cb = self.circuit_breaker.lock().await;
873        *cb = CircuitBreaker::new(5, Duration::from_secs(60));
874        info!("Circuit breaker reset");
875    }
876
877    /// Get connection pool metrics for monitoring
878    pub async fn connection_metrics(&self) -> ConnectionPoolMetrics {
879        ConnectionPoolMetrics {
880            total_connections: self.connection_pool.total_connections().await,
881            healthy_connections: self.connection_pool.healthy_connection_count().await,
882            connection_state: self.connection_manager.get_state().await.status_message(),
883            metrics: self.connection_manager.metrics(),
884        }
885    }
886
887    /// Force reconnection of all connections
888    pub async fn force_reconnect(&self) -> Result<(), BittensorError> {
889        warn!("Forcing reconnection of all connections");
890        self.connection_pool.refresh_connections().await?;
891        self.connection_manager.force_reconnect().await?;
892        Ok(())
893    }
894
895    /// Gracefully shutdown the service
896    pub async fn shutdown(mut self) {
897        info!("Shutting down enhanced Bittensor service");
898
899        // Stop health monitoring
900        if let Some(handle) = self.health_monitor_handle.take() {
901            handle.abort();
902        }
903
904        // Connection pool will be dropped automatically
905    }
906}
907
908/// Statistics for retry mechanisms
909#[derive(Debug, Clone)]
910pub struct RetryStats {
911    pub circuit_breaker_state: String,
912}
913
914/// Connection pool metrics for monitoring
915#[derive(Debug, Clone)]
916pub struct ConnectionPoolMetrics {
917    pub total_connections: usize,
918    pub healthy_connections: usize,
919    pub connection_state: String,
920    pub metrics: crate::connect::ConnectionMetricsSnapshot,
921}
922
923// Implement Drop to ensure cleanup
924impl Drop for Service {
925    fn drop(&mut self) {
926        if let Some(handle) = self.health_monitor_handle.take() {
927            handle.abort();
928        }
929    }
930}