1use std::time::Duration;
7use thiserror::Error;
8
9#[derive(Error, Debug, Clone)]
11pub enum BittensorError {
12 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum ErrorCategory {
156 Transient,
158 RateLimit,
160 Auth,
162 Config,
164 Network,
166 Permanent,
168}
169
170#[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 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 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 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 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
246impl 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), }
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
365impl 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
391impl 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
400impl BittensorError {
403 pub fn category(&self) -> ErrorCategory {
405 match self {
406 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 BittensorError::NetworkConnectivityError { .. }
418 | BittensorError::NetworkError { .. } => ErrorCategory::Network,
419
420 BittensorError::RateLimitExceeded { .. } => ErrorCategory::RateLimit,
422
423 BittensorError::SignatureError { .. }
425 | BittensorError::AuthError { .. }
426 | BittensorError::HotkeyNotRegistered { .. } => ErrorCategory::Auth,
427
428 BittensorError::ConfigError { .. }
430 | BittensorError::InvalidHotkey { .. }
431 | BittensorError::InvalidWeights { .. }
432 | BittensorError::InvalidNonce { .. }
433 | BittensorError::RuntimeVersionMismatch { .. }
434 | BittensorError::SerializationError { .. } => ErrorCategory::Config,
435
436 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 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 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 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 pub fn is_retryable(&self) -> bool {
497 !matches!(
498 self.category(),
499 ErrorCategory::Config | ErrorCategory::Permanent
500 )
501 }
502
503 pub fn max_retries_exceeded(attempts: u32) -> Self {
505 BittensorError::MaxRetriesExceeded { attempts }
506 }
507
508 pub fn backoff_timeout(duration: Duration) -> Self {
510 BittensorError::BackoffTimeoutReached { duration }
511 }
512}