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}