bittensor_rs/
error.rs

1//! # Bittensor Error Types
2//!
3//! Comprehensive error handling for Bittensor chain interactions with detailed
4//! error categorization and retry support.
5
6use std::time::Duration;
7use thiserror::Error;
8
9/// Errors that can occur during Bittensor operations
10#[derive(Error, Debug, Clone)]
11pub enum BittensorError {
12    // === Transaction Errors ===
13    #[error("Transaction submission failed: {message}")]
14    TxSubmissionError { message: String },
15
16    #[error("Transaction timeout after {timeout:?}: {message}")]
17    TxTimeoutError { message: String, timeout: Duration },
18
19    #[error("Transaction fees insufficient: required {required}, available {available}")]
20    InsufficientTxFees { required: u64, available: u64 },
21
22    #[error("Transaction nonce invalid: expected {expected}, got {actual}")]
23    InvalidNonce { expected: u64, actual: u64 },
24
25    #[error("Transaction finalization failed: {reason}")]
26    TxFinalizationError { reason: String },
27
28    #[error("Transaction dropped from pool: {reason}")]
29    TxDroppedError { reason: String },
30
31    // === RPC and Network Errors ===
32    #[error("RPC connection error: {message}")]
33    RpcConnectionError { message: String },
34
35    #[error("RPC method error: {method} - {message}")]
36    RpcMethodError { method: String, message: String },
37
38    #[error("RPC timeout after {timeout:?}: {message}")]
39    RpcTimeoutError { message: String, timeout: Duration },
40
41    #[error("Network connectivity issue: {message}")]
42    NetworkConnectivityError { message: String },
43
44    #[error("Chain synchronization error: {message}")]
45    ChainSyncError { message: String },
46
47    #[error("Websocket connection error: {message}")]
48    WebsocketError { message: String },
49
50    // === Chain State Errors ===
51    #[error("Chain metadata error: {message}")]
52    MetadataError { message: String },
53
54    #[error("Runtime version mismatch: expected {expected}, got {actual}")]
55    RuntimeVersionMismatch { expected: String, actual: String },
56
57    #[error("Storage query failed: {key} - {message}")]
58    StorageQueryError { key: String, message: String },
59
60    #[error("Block hash not found: {hash}")]
61    BlockNotFound { hash: String },
62
63    #[error("Invalid block number: {number}")]
64    InvalidBlockNumber { number: u64 },
65
66    // === Wallet and Authentication Errors ===
67    #[error("Wallet loading error: {message}")]
68    WalletLoadingError { message: String },
69
70    #[error("Key derivation error: {message}")]
71    KeyDerivationError { message: String },
72
73    #[error("Signature verification failed: {message}")]
74    SignatureError { message: String },
75
76    #[error("Invalid hotkey format: {hotkey}")]
77    InvalidHotkey { hotkey: String },
78
79    #[error("Hotkey not registered on subnet {netuid}: {hotkey}")]
80    HotkeyNotRegistered { hotkey: String, netuid: u16 },
81
82    // === Neuron and Subnet Errors ===
83    #[error("Neuron not found: uid {uid} on subnet {netuid}")]
84    NeuronNotFound { uid: u16, netuid: u16 },
85
86    #[error("Subnet not found: {netuid}")]
87    SubnetNotFound { netuid: u16 },
88
89    #[error("Insufficient stake: {available} TAO < {required} TAO")]
90    InsufficientStake { available: u64, required: u64 },
91
92    #[error("Weight setting failed on subnet {netuid}: {reason}")]
93    WeightSettingFailed { netuid: u16, reason: String },
94
95    #[error("Invalid weight vector: {reason}")]
96    InvalidWeights { reason: String },
97
98    #[error("Registration failed on subnet {netuid}: {reason}")]
99    RegistrationFailed { netuid: u16, reason: String },
100
101    // === Operational Errors ===
102    #[error("Serialization error: {message}")]
103    SerializationError { message: String },
104
105    #[error("Configuration error: {field} - {message}")]
106    ConfigError { field: String, message: String },
107
108    #[error("Operation timeout after {timeout:?}: {operation}")]
109    OperationTimeout {
110        operation: String,
111        timeout: Duration,
112    },
113
114    #[error("Rate limit exceeded: {message}")]
115    RateLimitExceeded { message: String },
116
117    #[error("Service unavailable: {message}")]
118    ServiceUnavailable { message: String },
119
120    // === Retry and Recovery Errors ===
121    #[error("Maximum retry attempts exceeded: {attempts} attempts failed")]
122    MaxRetriesExceeded { attempts: u32 },
123
124    #[error("Backoff timeout reached: operation abandoned after {duration:?}")]
125    BackoffTimeoutReached { duration: Duration },
126
127    #[error("Non-retryable error: {message}")]
128    NonRetryable { message: String },
129
130    // === Legacy Error Types (for backwards compatibility) ===
131    #[error("RPC error: {message}")]
132    RpcError { message: String },
133
134    #[error("Network error: {message}")]
135    NetworkError { message: String },
136
137    #[error("Chain error: {message}")]
138    ChainError { message: String },
139
140    #[error("Wallet error: {message}")]
141    WalletError { message: String },
142
143    #[error("Timeout error: {message}")]
144    TimeoutError { message: String },
145
146    #[error("Authentication error: {message}")]
147    AuthError { message: String },
148
149    #[error("Insufficient balance: {available} < {required}")]
150    InsufficientBalance { available: u64, required: u64 },
151}
152
153/// Classification of errors for retry logic
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum ErrorCategory {
156    /// Transient errors that can be retried with exponential backoff
157    Transient,
158    /// Rate limiting errors that require specific backoff strategies
159    RateLimit,
160    /// Authentication/authorization errors that may be retryable
161    Auth,
162    /// Configuration or input validation errors (not retryable)
163    Config,
164    /// Network connectivity issues (retryable with longer backoff)
165    Network,
166    /// Permanent errors that should not be retried
167    Permanent,
168}
169
170/// Retry configuration for different error categories
171#[derive(Debug, Clone)]
172pub struct RetryConfig {
173    pub max_attempts: u32,
174    pub initial_delay: Duration,
175    pub max_delay: Duration,
176    pub backoff_multiplier: f64,
177    pub jitter: bool,
178}
179
180impl Default for RetryConfig {
181    fn default() -> Self {
182        Self {
183            max_attempts: 3,
184            initial_delay: Duration::from_millis(100),
185            max_delay: Duration::from_secs(30),
186            backoff_multiplier: 2.0,
187            jitter: true,
188        }
189    }
190}
191
192impl RetryConfig {
193    /// Configuration for transient errors
194    pub fn transient() -> Self {
195        Self {
196            max_attempts: 5,
197            initial_delay: Duration::from_millis(200),
198            max_delay: Duration::from_secs(10),
199            backoff_multiplier: 1.5,
200            jitter: true,
201        }
202    }
203
204    /// Configuration for rate limit errors
205    pub fn rate_limit() -> Self {
206        Self {
207            max_attempts: 3,
208            initial_delay: Duration::from_secs(1),
209            max_delay: Duration::from_secs(60),
210            backoff_multiplier: 2.0,
211            jitter: false,
212        }
213    }
214
215    /// Configuration for network errors
216    pub fn network() -> Self {
217        Self {
218            max_attempts: 4,
219            initial_delay: Duration::from_millis(500),
220            max_delay: Duration::from_secs(30),
221            backoff_multiplier: 2.0,
222            jitter: true,
223        }
224    }
225
226    /// Configuration for authentication errors
227    pub fn auth() -> Self {
228        Self {
229            max_attempts: 2,
230            initial_delay: Duration::from_secs(1),
231            max_delay: Duration::from_secs(5),
232            backoff_multiplier: 1.0,
233            jitter: false,
234        }
235    }
236}
237
238impl From<anyhow::Error> for BittensorError {
239    fn from(err: anyhow::Error) -> Self {
240        BittensorError::ChainError {
241            message: err.to_string(),
242        }
243    }
244}
245
246// Enhanced conversions from subxt errors with detailed error mapping
247impl From<subxt::Error> for BittensorError {
248    fn from(err: subxt::Error) -> Self {
249        let err_str = err.to_string().to_lowercase();
250
251        match err {
252            subxt::Error::Rpc(rpc_err) => {
253                let rpc_msg = rpc_err.to_string();
254                let rpc_lower = rpc_msg.to_lowercase();
255
256                if rpc_lower.contains("timeout") {
257                    BittensorError::RpcTimeoutError {
258                        message: rpc_msg,
259                        timeout: Duration::from_secs(30), // Default timeout
260                    }
261                } else if rpc_lower.contains("connection") || rpc_lower.contains("network") {
262                    BittensorError::RpcConnectionError { message: rpc_msg }
263                } else if rpc_lower.contains("rate") || rpc_lower.contains("limit") {
264                    BittensorError::RateLimitExceeded { message: rpc_msg }
265                } else {
266                    BittensorError::RpcMethodError {
267                        method: "unknown".to_string(),
268                        message: rpc_msg,
269                    }
270                }
271            }
272            subxt::Error::Metadata(meta_err) => {
273                let meta_msg = meta_err.to_string();
274                if meta_msg.to_lowercase().contains("version") {
275                    BittensorError::RuntimeVersionMismatch {
276                        expected: "unknown".to_string(),
277                        actual: "unknown".to_string(),
278                    }
279                } else {
280                    BittensorError::MetadataError { message: meta_msg }
281                }
282            }
283            subxt::Error::Codec(codec_err) => BittensorError::SerializationError {
284                message: codec_err.to_string(),
285            },
286            subxt::Error::Transaction(tx_err) => {
287                let tx_msg = tx_err.to_string();
288                let tx_lower = tx_msg.to_lowercase();
289
290                if tx_lower.contains("timeout") {
291                    BittensorError::TxTimeoutError {
292                        message: tx_msg,
293                        timeout: Duration::from_secs(60),
294                    }
295                } else if tx_lower.contains("fee") || tx_lower.contains("balance") {
296                    BittensorError::InsufficientTxFees {
297                        required: 0,
298                        available: 0,
299                    }
300                } else if tx_lower.contains("nonce") {
301                    BittensorError::InvalidNonce {
302                        expected: 0,
303                        actual: 0,
304                    }
305                } else if tx_lower.contains("dropped") || tx_lower.contains("pool") {
306                    BittensorError::TxDroppedError { reason: tx_msg }
307                } else if tx_lower.contains("finalization") || tx_lower.contains("finalized") {
308                    BittensorError::TxFinalizationError { reason: tx_msg }
309                } else {
310                    BittensorError::TxSubmissionError { message: tx_msg }
311                }
312            }
313            subxt::Error::Block(block_err) => {
314                let block_msg = format!("Block error: {block_err}");
315                if err_str.contains("not found") {
316                    BittensorError::BlockNotFound {
317                        hash: "unknown".to_string(),
318                    }
319                } else {
320                    BittensorError::ChainError { message: block_msg }
321                }
322            }
323            subxt::Error::Runtime(runtime_err) => {
324                let runtime_msg = format!("Runtime error: {runtime_err}");
325                if err_str.contains("version") {
326                    BittensorError::RuntimeVersionMismatch {
327                        expected: "unknown".to_string(),
328                        actual: "unknown".to_string(),
329                    }
330                } else {
331                    BittensorError::ChainError {
332                        message: runtime_msg,
333                    }
334                }
335            }
336            subxt::Error::Other(other_err) => {
337                if err_str.contains("websocket") || err_str.contains("ws") {
338                    BittensorError::WebsocketError { message: other_err }
339                } else if err_str.contains("network") || err_str.contains("connection") {
340                    BittensorError::NetworkConnectivityError { message: other_err }
341                } else {
342                    BittensorError::ChainError { message: other_err }
343                }
344            }
345            _ => {
346                if err_str.contains("timeout") {
347                    BittensorError::OperationTimeout {
348                        operation: "subxt_operation".to_string(),
349                        timeout: Duration::from_secs(30),
350                    }
351                } else if err_str.contains("network") || err_str.contains("connection") {
352                    BittensorError::NetworkConnectivityError {
353                        message: err.to_string(),
354                    }
355                } else {
356                    BittensorError::ChainError {
357                        message: err.to_string(),
358                    }
359                }
360            }
361        }
362    }
363}
364
365// Enhanced conversions from wallet errors
366impl From<std::io::Error> for BittensorError {
367    fn from(err: std::io::Error) -> Self {
368        let err_msg = err.to_string();
369        let err_lower = err_msg.to_lowercase();
370
371        if err_lower.contains("file") || err_lower.contains("path") || err_lower.contains("io") {
372            BittensorError::WalletLoadingError {
373                message: format!("Wallet file access failed: {err}"),
374            }
375        } else if err_lower.contains("key") || err_lower.contains("derivation") {
376            BittensorError::KeyDerivationError {
377                message: format!("Key derivation failed: {err}"),
378            }
379        } else if err_lower.contains("format") || err_lower.contains("invalid") {
380            BittensorError::InvalidHotkey {
381                hotkey: "unknown".to_string(),
382            }
383        } else {
384            BittensorError::WalletLoadingError {
385                message: format!("Account loading failed: {err}"),
386            }
387        }
388    }
389}
390
391// Enhanced conversions from sp_core errors (used by crabtensor for keys)
392impl From<sp_core::crypto::SecretStringError> for BittensorError {
393    fn from(err: sp_core::crypto::SecretStringError) -> Self {
394        BittensorError::KeyDerivationError {
395            message: format!("Key derivation failed: {err}"),
396        }
397    }
398}
399
400// Remove duplicate - already implemented above
401
402impl BittensorError {
403    /// Gets the error category for retry logic
404    pub fn category(&self) -> ErrorCategory {
405        match self {
406            // Transient errors - can be retried
407            BittensorError::RpcConnectionError { .. }
408            | BittensorError::RpcTimeoutError { .. }
409            | BittensorError::TxTimeoutError { .. }
410            | BittensorError::WebsocketError { .. }
411            | BittensorError::ChainSyncError { .. }
412            | BittensorError::ServiceUnavailable { .. }
413            | BittensorError::OperationTimeout { .. }
414            | BittensorError::TxDroppedError { .. } => ErrorCategory::Transient,
415
416            // Network errors - retryable with longer backoff
417            BittensorError::NetworkConnectivityError { .. }
418            | BittensorError::NetworkError { .. } => ErrorCategory::Network,
419
420            // Rate limiting errors
421            BittensorError::RateLimitExceeded { .. } => ErrorCategory::RateLimit,
422
423            // Authentication errors - may be retryable
424            BittensorError::SignatureError { .. }
425            | BittensorError::AuthError { .. }
426            | BittensorError::HotkeyNotRegistered { .. } => ErrorCategory::Auth,
427
428            // Configuration errors - not retryable
429            BittensorError::ConfigError { .. }
430            | BittensorError::InvalidHotkey { .. }
431            | BittensorError::InvalidWeights { .. }
432            | BittensorError::InvalidNonce { .. }
433            | BittensorError::RuntimeVersionMismatch { .. }
434            | BittensorError::SerializationError { .. } => ErrorCategory::Config,
435
436            // Permanent errors - not retryable
437            BittensorError::NeuronNotFound { .. }
438            | BittensorError::SubnetNotFound { .. }
439            | BittensorError::InsufficientStake { .. }
440            | BittensorError::InsufficientTxFees { .. }
441            | BittensorError::InsufficientBalance { .. }
442            | BittensorError::NonRetryable { .. }
443            | BittensorError::MaxRetriesExceeded { .. }
444            | BittensorError::BackoffTimeoutReached { .. }
445            | BittensorError::BlockNotFound { .. }
446            | BittensorError::InvalidBlockNumber { .. } => ErrorCategory::Permanent,
447
448            // Legacy errors - categorize based on content
449            BittensorError::RpcError { message }
450            | BittensorError::ChainError { message }
451            | BittensorError::TimeoutError { message } => {
452                if message.to_lowercase().contains("timeout")
453                    || message.to_lowercase().contains("connection")
454                {
455                    ErrorCategory::Transient
456                } else {
457                    ErrorCategory::Permanent
458                }
459            }
460
461            BittensorError::WalletError { message } => {
462                if message.to_lowercase().contains("loading")
463                    || message.to_lowercase().contains("file")
464                {
465                    ErrorCategory::Config
466                } else {
467                    ErrorCategory::Auth
468                }
469            }
470
471            // Default categorization for remaining errors
472            BittensorError::TxSubmissionError { .. }
473            | BittensorError::TxFinalizationError { .. }
474            | BittensorError::RpcMethodError { .. }
475            | BittensorError::MetadataError { .. }
476            | BittensorError::StorageQueryError { .. }
477            | BittensorError::WalletLoadingError { .. }
478            | BittensorError::KeyDerivationError { .. }
479            | BittensorError::WeightSettingFailed { .. }
480            | BittensorError::RegistrationFailed { .. } => ErrorCategory::Transient,
481        }
482    }
483
484    /// Gets the appropriate retry configuration for this error
485    pub fn retry_config(&self) -> Option<RetryConfig> {
486        match self.category() {
487            ErrorCategory::Transient => Some(RetryConfig::transient()),
488            ErrorCategory::RateLimit => Some(RetryConfig::rate_limit()),
489            ErrorCategory::Network => Some(RetryConfig::network()),
490            ErrorCategory::Auth => Some(RetryConfig::auth()),
491            ErrorCategory::Config | ErrorCategory::Permanent => None,
492        }
493    }
494
495    /// Checks if this error is retryable
496    pub fn is_retryable(&self) -> bool {
497        !matches!(
498            self.category(),
499            ErrorCategory::Config | ErrorCategory::Permanent
500        )
501    }
502
503    /// Creates a retry exhausted error
504    pub fn max_retries_exceeded(attempts: u32) -> Self {
505        BittensorError::MaxRetriesExceeded { attempts }
506    }
507
508    /// Creates a backoff timeout error
509    pub fn backoff_timeout(duration: Duration) -> Self {
510        BittensorError::BackoffTimeoutReached { duration }
511    }
512}