use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum BittensorError {
#[error("Transaction submission failed: {message}")]
TxSubmissionError { message: String },
#[error("Transaction timeout after {timeout:?}: {message}")]
TxTimeoutError { message: String, timeout: Duration },
#[error("Transaction fees insufficient: required {required}, available {available}")]
InsufficientTxFees { required: u64, available: u64 },
#[error("Transaction nonce invalid: expected {expected}, got {actual}")]
InvalidNonce { expected: u64, actual: u64 },
#[error("Transaction finalization failed: {reason}")]
TxFinalizationError { reason: String },
#[error("Transaction dropped from pool: {reason}")]
TxDroppedError { reason: String },
#[error("RPC connection error: {message}")]
RpcConnectionError { message: String },
#[error("RPC method error: {method} - {message}")]
RpcMethodError { method: String, message: String },
#[error("RPC timeout after {timeout:?}: {message}")]
RpcTimeoutError { message: String, timeout: Duration },
#[error("Network connectivity issue: {message}")]
NetworkConnectivityError { message: String },
#[error("Chain synchronization error: {message}")]
ChainSyncError { message: String },
#[error("Websocket connection error: {message}")]
WebsocketError { message: String },
#[error("Chain metadata error: {message}")]
MetadataError { message: String },
#[error("Runtime version mismatch: expected {expected}, got {actual}")]
RuntimeVersionMismatch { expected: String, actual: String },
#[error("Storage query failed: {key} - {message}")]
StorageQueryError { key: String, message: String },
#[error("Block hash not found: {hash}")]
BlockNotFound { hash: String },
#[error("Invalid block number: {number}")]
InvalidBlockNumber { number: u64 },
#[error("Wallet loading error: {message}")]
WalletLoadingError { message: String },
#[error("Key derivation error: {message}")]
KeyDerivationError { message: String },
#[error("Signature verification failed: {message}")]
SignatureError { message: String },
#[error("Invalid hotkey format: {hotkey}")]
InvalidHotkey { hotkey: String },
#[error("Hotkey not registered on subnet {netuid}: {hotkey}")]
HotkeyNotRegistered { hotkey: String, netuid: u16 },
#[error("Neuron not found: uid {uid} on subnet {netuid}")]
NeuronNotFound { uid: u16, netuid: u16 },
#[error("Subnet not found: {netuid}")]
SubnetNotFound { netuid: u16 },
#[error("Insufficient stake: {available} TAO < {required} TAO")]
InsufficientStake { available: u64, required: u64 },
#[error("Weight setting failed on subnet {netuid}: {reason}")]
WeightSettingFailed { netuid: u16, reason: String },
#[error("Invalid weight vector: {reason}")]
InvalidWeights { reason: String },
#[error("Registration failed on subnet {netuid}: {reason}")]
RegistrationFailed { netuid: u16, reason: String },
#[error("Serialization error: {message}")]
SerializationError { message: String },
#[error("Configuration error: {field} - {message}")]
ConfigError { field: String, message: String },
#[error("Operation timeout after {timeout:?}: {operation}")]
OperationTimeout {
operation: String,
timeout: Duration,
},
#[error("Rate limit exceeded: {message}")]
RateLimitExceeded { message: String },
#[error("Service unavailable: {message}")]
ServiceUnavailable { message: String },
#[error("Maximum retry attempts exceeded: {attempts} attempts failed")]
MaxRetriesExceeded { attempts: u32 },
#[error("Backoff timeout reached: operation abandoned after {duration:?}")]
BackoffTimeoutReached { duration: Duration },
#[error("Non-retryable error: {message}")]
NonRetryable { message: String },
#[error("RPC error: {message}")]
RpcError { message: String },
#[error("Network error: {message}")]
NetworkError { message: String },
#[error("Chain error: {message}")]
ChainError { message: String },
#[error("Wallet error: {message}")]
WalletError { message: String },
#[error("Timeout error: {message}")]
TimeoutError { message: String },
#[error("Authentication error: {message}")]
AuthError { message: String },
#[error("Insufficient balance: {available} < {required}")]
InsufficientBalance { available: u64, required: u64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Transient,
RateLimit,
Auth,
Config,
Network,
Permanent,
}
#[derive(Debug, Clone)]
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_delay: Duration,
pub max_delay: Duration,
pub backoff_multiplier: f64,
pub jitter: bool,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_millis(100),
max_delay: Duration::from_secs(30),
backoff_multiplier: 2.0,
jitter: true,
}
}
}
impl RetryConfig {
pub fn transient() -> Self {
Self {
max_attempts: 5,
initial_delay: Duration::from_millis(200),
max_delay: Duration::from_secs(10),
backoff_multiplier: 1.5,
jitter: true,
}
}
pub fn rate_limit() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
backoff_multiplier: 2.0,
jitter: false,
}
}
pub fn network() -> Self {
Self {
max_attempts: 4,
initial_delay: Duration::from_millis(500),
max_delay: Duration::from_secs(30),
backoff_multiplier: 2.0,
jitter: true,
}
}
pub fn auth() -> Self {
Self {
max_attempts: 2,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(5),
backoff_multiplier: 1.0,
jitter: false,
}
}
}
impl From<anyhow::Error> for BittensorError {
fn from(err: anyhow::Error) -> Self {
BittensorError::ChainError {
message: err.to_string(),
}
}
}
impl From<subxt::Error> for BittensorError {
fn from(err: subxt::Error) -> Self {
let err_str = err.to_string().to_lowercase();
match err {
subxt::Error::Rpc(rpc_err) => {
let rpc_msg = rpc_err.to_string();
let rpc_lower = rpc_msg.to_lowercase();
if rpc_lower.contains("timeout") {
BittensorError::RpcTimeoutError {
message: rpc_msg,
timeout: Duration::from_secs(30), }
} else if rpc_lower.contains("connection") || rpc_lower.contains("network") {
BittensorError::RpcConnectionError { message: rpc_msg }
} else if rpc_lower.contains("rate") || rpc_lower.contains("limit") {
BittensorError::RateLimitExceeded { message: rpc_msg }
} else {
BittensorError::RpcMethodError {
method: "unknown".to_string(),
message: rpc_msg,
}
}
}
subxt::Error::Metadata(meta_err) => {
let meta_msg = meta_err.to_string();
if meta_msg.to_lowercase().contains("version") {
BittensorError::RuntimeVersionMismatch {
expected: "unknown".to_string(),
actual: "unknown".to_string(),
}
} else {
BittensorError::MetadataError { message: meta_msg }
}
}
subxt::Error::Codec(codec_err) => BittensorError::SerializationError {
message: codec_err.to_string(),
},
subxt::Error::Transaction(tx_err) => {
let tx_msg = tx_err.to_string();
let tx_lower = tx_msg.to_lowercase();
if tx_lower.contains("timeout") {
BittensorError::TxTimeoutError {
message: tx_msg,
timeout: Duration::from_secs(60),
}
} else if tx_lower.contains("fee") || tx_lower.contains("balance") {
BittensorError::InsufficientTxFees {
required: 0,
available: 0,
}
} else if tx_lower.contains("nonce") {
BittensorError::InvalidNonce {
expected: 0,
actual: 0,
}
} else if tx_lower.contains("dropped") || tx_lower.contains("pool") {
BittensorError::TxDroppedError { reason: tx_msg }
} else if tx_lower.contains("finalization") || tx_lower.contains("finalized") {
BittensorError::TxFinalizationError { reason: tx_msg }
} else {
BittensorError::TxSubmissionError { message: tx_msg }
}
}
subxt::Error::Block(block_err) => {
let block_msg = format!("Block error: {block_err}");
if err_str.contains("not found") {
BittensorError::BlockNotFound {
hash: "unknown".to_string(),
}
} else {
BittensorError::ChainError { message: block_msg }
}
}
subxt::Error::Runtime(runtime_err) => {
let runtime_msg = format!("Runtime error: {runtime_err}");
if err_str.contains("version") {
BittensorError::RuntimeVersionMismatch {
expected: "unknown".to_string(),
actual: "unknown".to_string(),
}
} else {
BittensorError::ChainError {
message: runtime_msg,
}
}
}
subxt::Error::Other(other_err) => {
if err_str.contains("websocket") || err_str.contains("ws") {
BittensorError::WebsocketError { message: other_err }
} else if err_str.contains("network") || err_str.contains("connection") {
BittensorError::NetworkConnectivityError { message: other_err }
} else {
BittensorError::ChainError { message: other_err }
}
}
_ => {
if err_str.contains("timeout") {
BittensorError::OperationTimeout {
operation: "subxt_operation".to_string(),
timeout: Duration::from_secs(30),
}
} else if err_str.contains("network") || err_str.contains("connection") {
BittensorError::NetworkConnectivityError {
message: err.to_string(),
}
} else {
BittensorError::ChainError {
message: err.to_string(),
}
}
}
}
}
}
impl From<std::io::Error> for BittensorError {
fn from(err: std::io::Error) -> Self {
let err_msg = err.to_string();
let err_lower = err_msg.to_lowercase();
if err_lower.contains("file") || err_lower.contains("path") || err_lower.contains("io") {
BittensorError::WalletLoadingError {
message: format!("Wallet file access failed: {err}"),
}
} else if err_lower.contains("key") || err_lower.contains("derivation") {
BittensorError::KeyDerivationError {
message: format!("Key derivation failed: {err}"),
}
} else if err_lower.contains("format") || err_lower.contains("invalid") {
BittensorError::InvalidHotkey {
hotkey: "unknown".to_string(),
}
} else {
BittensorError::WalletLoadingError {
message: format!("Account loading failed: {err}"),
}
}
}
}
impl From<sp_core::crypto::SecretStringError> for BittensorError {
fn from(err: sp_core::crypto::SecretStringError) -> Self {
BittensorError::KeyDerivationError {
message: format!("Key derivation failed: {err}"),
}
}
}
impl BittensorError {
pub fn category(&self) -> ErrorCategory {
match self {
BittensorError::RpcConnectionError { .. }
| BittensorError::RpcTimeoutError { .. }
| BittensorError::TxTimeoutError { .. }
| BittensorError::WebsocketError { .. }
| BittensorError::ChainSyncError { .. }
| BittensorError::ServiceUnavailable { .. }
| BittensorError::OperationTimeout { .. }
| BittensorError::TxDroppedError { .. } => ErrorCategory::Transient,
BittensorError::NetworkConnectivityError { .. }
| BittensorError::NetworkError { .. } => ErrorCategory::Network,
BittensorError::RateLimitExceeded { .. } => ErrorCategory::RateLimit,
BittensorError::SignatureError { .. }
| BittensorError::AuthError { .. }
| BittensorError::HotkeyNotRegistered { .. } => ErrorCategory::Auth,
BittensorError::ConfigError { .. }
| BittensorError::InvalidHotkey { .. }
| BittensorError::InvalidWeights { .. }
| BittensorError::InvalidNonce { .. }
| BittensorError::RuntimeVersionMismatch { .. }
| BittensorError::SerializationError { .. } => ErrorCategory::Config,
BittensorError::NeuronNotFound { .. }
| BittensorError::SubnetNotFound { .. }
| BittensorError::InsufficientStake { .. }
| BittensorError::InsufficientTxFees { .. }
| BittensorError::InsufficientBalance { .. }
| BittensorError::NonRetryable { .. }
| BittensorError::MaxRetriesExceeded { .. }
| BittensorError::BackoffTimeoutReached { .. }
| BittensorError::BlockNotFound { .. }
| BittensorError::InvalidBlockNumber { .. } => ErrorCategory::Permanent,
BittensorError::RpcError { message }
| BittensorError::ChainError { message }
| BittensorError::TimeoutError { message } => {
if message.to_lowercase().contains("timeout")
|| message.to_lowercase().contains("connection")
{
ErrorCategory::Transient
} else {
ErrorCategory::Permanent
}
}
BittensorError::WalletError { message } => {
if message.to_lowercase().contains("loading")
|| message.to_lowercase().contains("file")
{
ErrorCategory::Config
} else {
ErrorCategory::Auth
}
}
BittensorError::TxSubmissionError { .. }
| BittensorError::TxFinalizationError { .. }
| BittensorError::RpcMethodError { .. }
| BittensorError::MetadataError { .. }
| BittensorError::StorageQueryError { .. }
| BittensorError::WalletLoadingError { .. }
| BittensorError::KeyDerivationError { .. }
| BittensorError::WeightSettingFailed { .. }
| BittensorError::RegistrationFailed { .. } => ErrorCategory::Transient,
}
}
pub fn retry_config(&self) -> Option<RetryConfig> {
match self.category() {
ErrorCategory::Transient => Some(RetryConfig::transient()),
ErrorCategory::RateLimit => Some(RetryConfig::rate_limit()),
ErrorCategory::Network => Some(RetryConfig::network()),
ErrorCategory::Auth => Some(RetryConfig::auth()),
ErrorCategory::Config | ErrorCategory::Permanent => None,
}
}
pub fn is_retryable(&self) -> bool {
!matches!(
self.category(),
ErrorCategory::Config | ErrorCategory::Permanent
)
}
pub fn max_retries_exceeded(attempts: u32) -> Self {
BittensorError::MaxRetriesExceeded { attempts }
}
pub fn backoff_timeout(duration: Duration) -> Self {
BittensorError::BackoffTimeoutReached { duration }
}
}