htcli 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. htcli-1.1.0.dist-info/METADATA +509 -0
  2. htcli-1.1.0.dist-info/RECORD +140 -0
  3. htcli-1.1.0.dist-info/WHEEL +4 -0
  4. htcli-1.1.0.dist-info/entry_points.txt +2 -0
  5. htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
  6. src/__init__.py +0 -0
  7. src/htcli/__init__.py +5 -0
  8. src/htcli/client/__init__.py +338 -0
  9. src/htcli/client/extrinsics/__init__.py +26 -0
  10. src/htcli/client/extrinsics/base.py +487 -0
  11. src/htcli/client/extrinsics/consensus.py +79 -0
  12. src/htcli/client/extrinsics/governance.py +714 -0
  13. src/htcli/client/extrinsics/identity.py +490 -0
  14. src/htcli/client/extrinsics/node.py +1054 -0
  15. src/htcli/client/extrinsics/overwatch.py +401 -0
  16. src/htcli/client/extrinsics/staking.py +1504 -0
  17. src/htcli/client/extrinsics/subnet.py +2218 -0
  18. src/htcli/client/extrinsics/validator.py +203 -0
  19. src/htcli/client/extrinsics/wallet.py +323 -0
  20. src/htcli/client/offchain/__init__.py +10 -0
  21. src/htcli/client/offchain/backup.py +385 -0
  22. src/htcli/client/offchain/config.py +541 -0
  23. src/htcli/client/offchain/wallet.py +839 -0
  24. src/htcli/client/rpc/__init__.py +20 -0
  25. src/htcli/client/rpc/chain.py +568 -0
  26. src/htcli/client/rpc/node.py +783 -0
  27. src/htcli/client/rpc/overwatch.py +680 -0
  28. src/htcli/client/rpc/staking.py +216 -0
  29. src/htcli/client/rpc/subnet.py +2104 -0
  30. src/htcli/client/rpc/wallet.py +912 -0
  31. src/htcli/commands/__init__.py +31 -0
  32. src/htcli/commands/chain/__init__.py +66 -0
  33. src/htcli/commands/chain/display.py +204 -0
  34. src/htcli/commands/chain/handlers.py +260 -0
  35. src/htcli/commands/config/__init__.py +158 -0
  36. src/htcli/commands/config/display.py +353 -0
  37. src/htcli/commands/config/handlers.py +347 -0
  38. src/htcli/commands/config/prompts.py +357 -0
  39. src/htcli/commands/consensus/__init__.py +61 -0
  40. src/htcli/commands/consensus/handlers.py +100 -0
  41. src/htcli/commands/governance/__init__.py +49 -0
  42. src/htcli/commands/governance/handlers.py +81 -0
  43. src/htcli/commands/node/__init__.py +304 -0
  44. src/htcli/commands/node/display.py +749 -0
  45. src/htcli/commands/node/error_handling.py +470 -0
  46. src/htcli/commands/node/handlers.py +844 -0
  47. src/htcli/commands/node/prompts.py +346 -0
  48. src/htcli/commands/overwatch/__init__.py +219 -0
  49. src/htcli/commands/overwatch/display.py +396 -0
  50. src/htcli/commands/overwatch/error_handling.py +276 -0
  51. src/htcli/commands/overwatch/handlers.py +443 -0
  52. src/htcli/commands/overwatch/prompts.py +359 -0
  53. src/htcli/commands/stake/__init__.py +736 -0
  54. src/htcli/commands/stake/display.py +1103 -0
  55. src/htcli/commands/stake/error_handling.py +425 -0
  56. src/htcli/commands/stake/handlers.py +1902 -0
  57. src/htcli/commands/stake/prompts.py +1080 -0
  58. src/htcli/commands/subnet/__init__.py +639 -0
  59. src/htcli/commands/subnet/display.py +801 -0
  60. src/htcli/commands/subnet/error_handling.py +524 -0
  61. src/htcli/commands/subnet/handlers.py +2855 -0
  62. src/htcli/commands/subnet/prompts.py +1225 -0
  63. src/htcli/commands/validator/__init__.py +192 -0
  64. src/htcli/commands/validator/display.py +54 -0
  65. src/htcli/commands/validator/handlers.py +340 -0
  66. src/htcli/commands/wallet/__init__.py +546 -0
  67. src/htcli/commands/wallet/display.py +806 -0
  68. src/htcli/commands/wallet/error_handling.py +210 -0
  69. src/htcli/commands/wallet/handlers.py +3040 -0
  70. src/htcli/commands/wallet/prompts.py +1518 -0
  71. src/htcli/config.py +184 -0
  72. src/htcli/dependencies.py +186 -0
  73. src/htcli/errors/__init__.py +63 -0
  74. src/htcli/errors/base.py +141 -0
  75. src/htcli/errors/display.py +20 -0
  76. src/htcli/errors/handlers.py +710 -0
  77. src/htcli/main.py +343 -0
  78. src/htcli/models/__init__.py +21 -0
  79. src/htcli/models/enums/enum_types.py +35 -0
  80. src/htcli/models/errors.py +103 -0
  81. src/htcli/models/requests/__init__.py +197 -0
  82. src/htcli/models/requests/config.py +70 -0
  83. src/htcli/models/requests/consensus.py +19 -0
  84. src/htcli/models/requests/governance.py +38 -0
  85. src/htcli/models/requests/identity.py +51 -0
  86. src/htcli/models/requests/key.py +22 -0
  87. src/htcli/models/requests/node.py +91 -0
  88. src/htcli/models/requests/overwatch.py +64 -0
  89. src/htcli/models/requests/staking.py +580 -0
  90. src/htcli/models/requests/subnet.py +195 -0
  91. src/htcli/models/requests/validator.py +139 -0
  92. src/htcli/models/requests/wallet.py +118 -0
  93. src/htcli/models/responses/__init__.py +147 -0
  94. src/htcli/models/responses/base.py +18 -0
  95. src/htcli/models/responses/chain.py +39 -0
  96. src/htcli/models/responses/config.py +58 -0
  97. src/htcli/models/responses/identity.py +102 -0
  98. src/htcli/models/responses/overwatch.py +51 -0
  99. src/htcli/models/responses/staking.py +502 -0
  100. src/htcli/models/responses/subnet.py +856 -0
  101. src/htcli/models/responses/wallet.py +185 -0
  102. src/htcli/ui/__init__.py +87 -0
  103. src/htcli/ui/colors.py +309 -0
  104. src/htcli/ui/components/__init__.py +60 -0
  105. src/htcli/ui/components/panels.py +174 -0
  106. src/htcli/ui/components/progress.py +166 -0
  107. src/htcli/ui/components/spinners.py +92 -0
  108. src/htcli/ui/components/tables.py +809 -0
  109. src/htcli/ui/components/trees.py +721 -0
  110. src/htcli/ui/display.py +336 -0
  111. src/htcli/ui/prompts.py +870 -0
  112. src/htcli/utils/__init__.py +76 -0
  113. src/htcli/utils/blockchain/__init__.py +75 -0
  114. src/htcli/utils/blockchain/formatting.py +368 -0
  115. src/htcli/utils/blockchain/patches.py +286 -0
  116. src/htcli/utils/blockchain/peer_id.py +186 -0
  117. src/htcli/utils/blockchain/staking.py +448 -0
  118. src/htcli/utils/blockchain/type_registry.py +1373 -0
  119. src/htcli/utils/blockchain/validation.py +179 -0
  120. src/htcli/utils/cache.py +613 -0
  121. src/htcli/utils/constants.py +38 -0
  122. src/htcli/utils/legacy/__init__.py +12 -0
  123. src/htcli/utils/legacy/colors.py +311 -0
  124. src/htcli/utils/legacy/crypto.py +1176 -0
  125. src/htcli/utils/legacy/formatting.py +452 -0
  126. src/htcli/utils/legacy/interactive.py +306 -0
  127. src/htcli/utils/legacy/subnet_manifest.py +265 -0
  128. src/htcli/utils/legacy/validation.py +488 -0
  129. src/htcli/utils/logging.py +183 -0
  130. src/htcli/utils/network/__init__.py +20 -0
  131. src/htcli/utils/network/subnet.py +344 -0
  132. src/htcli/utils/prompts.py +27 -0
  133. src/htcli/utils/scale_codec.py +155 -0
  134. src/htcli/utils/validation/__init__.py +57 -0
  135. src/htcli/utils/validation/prompt_validators.py +267 -0
  136. src/htcli/utils/wallet/__init__.py +65 -0
  137. src/htcli/utils/wallet/auth.py +151 -0
  138. src/htcli/utils/wallet/core.py +1069 -0
  139. src/htcli/utils/wallet/crypto.py +1615 -0
  140. src/htcli/utils/wallet/migration.py +159 -0
src/htcli/config.py ADDED
@@ -0,0 +1,184 @@
1
+ """
2
+ Configuration management for the Hypertensor CLI.
3
+ """
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class NetworkConfig(BaseModel):
13
+ """Network configuration."""
14
+
15
+ endpoint: str = Field("wss://rpc.htcli.io/", description="RPC endpoint")
16
+ ws_endpoint: str = Field("wss://rpc.htcli.io/", description="WebSocket endpoint")
17
+ timeout: int = Field(30, description="Connection timeout in seconds")
18
+ retry_attempts: int = Field(3, description="Number of retry attempts")
19
+ network_type: str = Field(
20
+ "mainnet", description="Network type (mainnet/testnet/devnet)"
21
+ )
22
+ evm_compatible: bool = Field(
23
+ True, description="Whether network supports EVM compatibility"
24
+ )
25
+
26
+
27
+ NETWORK_PROFILES: dict[str, dict[str, object]] = {
28
+ "mainnet": {
29
+ "endpoint": "wss://rpc.htcli.io/",
30
+ "ws_endpoint": "wss://rpc.htcli.io/",
31
+ "network_type": "mainnet",
32
+ "evm_compatible": True,
33
+ },
34
+ "testnet": {
35
+ "endpoint": "wss://testnet-rpc.hypertensor.io",
36
+ "ws_endpoint": "wss://testnet-ws.hypertensor.io",
37
+ "network_type": "testnet",
38
+ "evm_compatible": False,
39
+ },
40
+ "local": {
41
+ "endpoint": "ws://127.0.0.1:9944",
42
+ "ws_endpoint": "ws://127.0.0.1:9944",
43
+ "network_type": "devnet",
44
+ "evm_compatible": False,
45
+ },
46
+ }
47
+
48
+ NETWORK_ALIASES = {
49
+ "main": "mainnet",
50
+ "mainnet": "mainnet",
51
+ "test": "testnet",
52
+ "testnet": "testnet",
53
+ "local": "local",
54
+ "dev": "local",
55
+ "devnet": "local",
56
+ }
57
+
58
+
59
+ def apply_network_profile(config: "Config", network_name: str) -> "Config":
60
+ """Apply a named network profile to the active config."""
61
+ normalized = NETWORK_ALIASES.get(network_name.strip().lower())
62
+ if normalized is None:
63
+ valid = ", ".join(sorted(NETWORK_ALIASES))
64
+ raise ValueError(f"Unknown network '{network_name}'. Expected one of: {valid}")
65
+
66
+ profile = NETWORK_PROFILES[normalized]
67
+ config.network.endpoint = str(profile["endpoint"])
68
+ config.network.ws_endpoint = str(profile["ws_endpoint"])
69
+ config.network.network_type = str(profile["network_type"])
70
+ config.network.evm_compatible = bool(profile["evm_compatible"])
71
+ return config
72
+
73
+
74
+ class OutputConfig(BaseModel):
75
+ """Output configuration."""
76
+
77
+ format: str = Field("table", description="Output format (table/json/csv)")
78
+ verbose: bool = Field(False, description="Verbose output")
79
+ color: bool = Field(True, description="Enable colored output")
80
+ color_scheme: str = Field(
81
+ "default",
82
+ description="Color scheme (default/dark/light/high_contrast/soft)",
83
+ )
84
+
85
+
86
+ class FilterConfig(BaseModel):
87
+ """Filter configuration."""
88
+
89
+ mine: bool = Field(False, description="Filter results to show only user assets")
90
+
91
+
92
+ class WalletConfig(BaseModel):
93
+ """Wallet configuration."""
94
+
95
+ path: str = Field("~/.htcli/wallets", description="Wallet storage path")
96
+ default_name: str = Field("default", description="Default wallet name")
97
+ encryption_enabled: bool = Field(True, description="Enable wallet encryption")
98
+ default_key_type: str = Field(
99
+ "ecdsa",
100
+ description="Default key type for new wallets (ecdsa/sr25519/ed25519)",
101
+ )
102
+ mainnet_key_type: str = Field(
103
+ "ecdsa", description="Required key type for mainnet (EVM compatibility)"
104
+ )
105
+ testnet_key_type: str = Field("sr25519", description="Default key type for testnet")
106
+
107
+
108
+ class Config(BaseModel):
109
+ """Main configuration."""
110
+
111
+ network: NetworkConfig = Field(default_factory=NetworkConfig)
112
+ output: OutputConfig = Field(default_factory=OutputConfig)
113
+ wallet: WalletConfig = Field(default_factory=WalletConfig)
114
+ filter: FilterConfig = Field(default_factory=FilterConfig)
115
+
116
+
117
+ def load_config(config_file: Optional[Path] = None) -> Config:
118
+ """Load configuration from file or use defaults."""
119
+ if config_file and config_file.exists():
120
+ # Load from file
121
+ import yaml
122
+
123
+ with open(config_file) as f:
124
+ config_data = yaml.safe_load(f)
125
+ return Config(**config_data)
126
+ else:
127
+ # Try to load from YAML config file created by config commands
128
+ yaml_config_path = Path.home() / ".htcli" / "config.yaml"
129
+ if yaml_config_path.exists():
130
+ try:
131
+ import yaml
132
+
133
+ with open(yaml_config_path) as f:
134
+ yaml_config_data = yaml.safe_load(f)
135
+ # Load directly as YAML already matches Pydantic Config structure
136
+ return Config(**yaml_config_data)
137
+ except Exception:
138
+ # If YAML config is invalid or can't be read, fall through to env vars/defaults
139
+ pass
140
+
141
+ # Use environment variables or defaults
142
+ network_config = NetworkConfig(
143
+ endpoint=os.getenv("HTCLI_NETWORK_ENDPOINT", "wss://rpc.htcli.io/"),
144
+ ws_endpoint=os.getenv("HTCLI_NETWORK_WS_ENDPOINT", "wss://rpc.htcli.io/"),
145
+ timeout=int(os.getenv("HTCLI_NETWORK_TIMEOUT", "30")),
146
+ retry_attempts=int(os.getenv("HTCLI_NETWORK_RETRY_ATTEMPTS", "3")),
147
+ network_type=os.getenv("HTCLI_NETWORK_TYPE", "mainnet"),
148
+ evm_compatible=os.getenv("HTCLI_NETWORK_EVM_COMPATIBLE", "true").lower()
149
+ == "true",
150
+ )
151
+
152
+ output_config = OutputConfig(
153
+ format=os.getenv("HTCLI_OUTPUT_FORMAT", "table"),
154
+ verbose=os.getenv("HTCLI_OUTPUT_VERBOSE", "false").lower() == "true",
155
+ color=os.getenv("HTCLI_OUTPUT_COLOR", "true").lower() == "true",
156
+ color_scheme=os.getenv("HTCLI_OUTPUT_COLOR_SCHEME", "default"),
157
+ )
158
+
159
+ wallet_config = WalletConfig(
160
+ path=os.getenv("HTCLI_WALLET_PATH", "~/.htcli/wallets"),
161
+ default_name=os.getenv("HTCLI_WALLET_DEFAULT_NAME", "default"),
162
+ encryption_enabled=os.getenv(
163
+ "HTCLI_WALLET_ENCRYPTION_ENABLED", "true"
164
+ ).lower()
165
+ == "true",
166
+ default_key_type=os.getenv("HTCLI_WALLET_DEFAULT_KEY_TYPE", "ecdsa"),
167
+ mainnet_key_type=os.getenv("HTCLI_WALLET_MAINNET_KEY_TYPE", "ecdsa"),
168
+ testnet_key_type=os.getenv("HTCLI_WALLET_TESTNET_KEY_TYPE", "sr25519"),
169
+ )
170
+
171
+ filter_config = FilterConfig(
172
+ mine=os.getenv("HTCLI_FILTER_MINE", "false").lower() == "true"
173
+ )
174
+
175
+ return Config(
176
+ network=network_config,
177
+ output=output_config,
178
+ wallet=wallet_config,
179
+ filter=filter_config,
180
+ )
181
+
182
+
183
+ # Global configuration instance
184
+ config_instance = load_config()
@@ -0,0 +1,186 @@
1
+ """
2
+ Dependencies for the Hypertensor CLI.
3
+ """
4
+
5
+ import time
6
+ from typing import Optional
7
+
8
+ from .client import HypertensorClient
9
+ from .config import Config
10
+ from .utils.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ # Global client instance and config
15
+ _client: Optional[HypertensorClient] = None
16
+ _config: Optional[Config] = None
17
+
18
+
19
+ def _retry_delay_seconds(attempt: int) -> int:
20
+ """Return bounded exponential backoff delay for a retry attempt."""
21
+ return min(2 ** attempt, 5)
22
+
23
+
24
+ def _is_closed_websocket_error(error: Exception) -> bool:
25
+ """Classify common closed WebSocket failures without depending on one package."""
26
+ error_name = error.__class__.__name__
27
+ error_message = str(error).lower()
28
+ return (
29
+ error_name == "WebSocketConnectionClosedException"
30
+ or "websocket" in error_message
31
+ and ("closed" in error_message or "not connected" in error_message)
32
+ )
33
+
34
+
35
+ def _websocket_is_connected(substrate) -> bool:
36
+ """Best-effort connection-state check that avoids network calls."""
37
+ websocket = getattr(substrate, "websocket", None)
38
+ if not websocket:
39
+ return True
40
+
41
+ if hasattr(websocket, "connected"):
42
+ return bool(websocket.connected)
43
+
44
+ sock = getattr(websocket, "sock", None)
45
+ if sock is None:
46
+ return False
47
+
48
+ if hasattr(sock, "connected"):
49
+ return bool(sock.connected)
50
+
51
+ return True
52
+
53
+
54
+ def _client_needs_reconnect(client: HypertensorClient) -> bool:
55
+ """Check whether the global client should reconnect before use."""
56
+ if not client.substrate or not client.rpc or not client.extrinsics:
57
+ logger.debug("Connection layers missing, reconnecting...")
58
+ return True
59
+
60
+ try:
61
+ if not _websocket_is_connected(client.substrate):
62
+ logger.debug("WebSocket connection appears closed, reconnecting...")
63
+ return True
64
+ except Exception as check_error:
65
+ logger.debug(
66
+ f"Lightweight connection check failed: {check_error}, will reconnect"
67
+ )
68
+ return True
69
+
70
+ return False
71
+
72
+
73
+ def _connect_with_retries(
74
+ client: HypertensorClient,
75
+ max_retries: int,
76
+ *,
77
+ force_reconnect: bool = False,
78
+ operation: str = "Connection",
79
+ ) -> tuple[bool, Optional[Exception]]:
80
+ """Connect or reconnect with bounded exponential backoff."""
81
+ retry_count = 0
82
+ last_error = None
83
+
84
+ while retry_count < max_retries:
85
+ try:
86
+ if client.connect(force_reconnect=force_reconnect):
87
+ logger.debug(
88
+ f"{operation} succeeded (attempt {retry_count + 1})"
89
+ )
90
+ return True, None
91
+
92
+ retry_count += 1
93
+ if retry_count < max_retries:
94
+ logger.warning(
95
+ f"{operation} attempt {retry_count} failed, retrying..."
96
+ )
97
+ time.sleep(_retry_delay_seconds(retry_count))
98
+ except Exception as error:
99
+ last_error = error
100
+ retry_count += 1
101
+ if retry_count < max_retries:
102
+ if _is_closed_websocket_error(error):
103
+ logger.warning(
104
+ f"{operation} attempt {retry_count} hit a closed WebSocket, retrying..."
105
+ )
106
+ else:
107
+ logger.warning(
108
+ f"{operation} attempt {retry_count} failed: {error}, retrying..."
109
+ )
110
+ time.sleep(_retry_delay_seconds(retry_count))
111
+ else:
112
+ logger.error(f"All {max_retries} {operation.lower()} attempts failed")
113
+
114
+ return False, last_error
115
+
116
+
117
+ def set_client(client: HypertensorClient):
118
+ """Set the global client instance."""
119
+ global _client
120
+ _client = client
121
+
122
+
123
+ def set_config(config: Config):
124
+ """Set the global config instance for lazy client initialization."""
125
+ global _config
126
+ _config = config
127
+
128
+
129
+ def get_client() -> HypertensorClient:
130
+ """Get the global client instance, initializing it lazily if needed."""
131
+ global _client, _config
132
+
133
+ if _client is None:
134
+ if _config is None:
135
+ raise RuntimeError(
136
+ "Configuration not set. Please ensure config is loaded before using client."
137
+ )
138
+
139
+ # Initialize client only when first requested
140
+ _client = HypertensorClient(_config)
141
+
142
+ # Connect to blockchain when client is first accessed with retry logic
143
+ max_retries = _config.network.retry_attempts or 3
144
+ _, last_error = _connect_with_retries(
145
+ _client,
146
+ max_retries,
147
+ operation="Connection",
148
+ )
149
+
150
+ if not _client.is_connected():
151
+ error_msg = str(last_error) if last_error else "Connection failed after retries"
152
+ raise RuntimeError(
153
+ f"Failed to connect to blockchain after {max_retries} attempts. "
154
+ f"Please check your network connection and configuration. Last error: {error_msg}"
155
+ )
156
+ else:
157
+ needs_reconnect = _client_needs_reconnect(_client)
158
+
159
+ # Only reconnect if lightweight check indicates we need to
160
+ if needs_reconnect:
161
+ logger.debug("Connection appears lost, attempting to reconnect...")
162
+ # Try to reconnect with force flag to close old connection
163
+ max_retries = _config.network.retry_attempts or 3
164
+ _, last_error = _connect_with_retries(
165
+ _client,
166
+ max_retries,
167
+ force_reconnect=True,
168
+ operation="Reconnection",
169
+ )
170
+
171
+ # Final verification: do a lightweight check, not expensive query
172
+ if _client_needs_reconnect(_client):
173
+ error_msg = str(last_error) if last_error else "Reconnection failed after retries"
174
+ # Reset client to allow fresh connection on next call
175
+ _client = None
176
+ raise RuntimeError(
177
+ f"Blockchain connection lost. Failed to reconnect after {max_retries} attempts. "
178
+ f"Please check your network connection. Last error: {error_msg}"
179
+ )
180
+
181
+ return _client
182
+
183
+
184
+ def get_config() -> Optional[Config]:
185
+ """Get the global config instance."""
186
+ return _config
@@ -0,0 +1,63 @@
1
+ """
2
+ Error handling module for htcli.
3
+ """
4
+
5
+ from .base import (
6
+ AddressValidationError,
7
+ BalanceError,
8
+ HTCLIError,
9
+ KeyDeletionError,
10
+ KeyGenerationError,
11
+ NodeError,
12
+ NodeOperationError,
13
+ NodeRegistrationError,
14
+ SubnetActivationError,
15
+ SubnetError,
16
+ SubnetRegistrationError,
17
+ SubnetValidationError,
18
+ TransferError,
19
+ WalletError,
20
+ )
21
+ from .display import display_balance_info
22
+ from .handlers import (
23
+ display_pydantic_validation_error,
24
+ format_pydantic_validation_error,
25
+ get_validation_suggestions,
26
+ handle_and_display_error,
27
+ handle_and_display_node_error,
28
+ handle_and_display_subnet_error,
29
+ handle_blockchain_error,
30
+ handle_node_error,
31
+ handle_subnet_error,
32
+ handle_substrate_error,
33
+ handle_wallet_error,
34
+ )
35
+
36
+ __all__ = [
37
+ "HTCLIError",
38
+ "WalletError",
39
+ "TransferError",
40
+ "BalanceError",
41
+ "KeyGenerationError",
42
+ "KeyDeletionError",
43
+ "NodeError",
44
+ "NodeOperationError",
45
+ "NodeRegistrationError",
46
+ "SubnetError",
47
+ "SubnetRegistrationError",
48
+ "SubnetActivationError",
49
+ "SubnetValidationError",
50
+ "AddressValidationError",
51
+ "handle_blockchain_error",
52
+ "handle_wallet_error",
53
+ "handle_and_display_error",
54
+ "handle_node_error",
55
+ "handle_and_display_node_error",
56
+ "handle_subnet_error",
57
+ "handle_and_display_subnet_error",
58
+ "handle_substrate_error",
59
+ "display_balance_info",
60
+ "display_pydantic_validation_error",
61
+ "format_pydantic_validation_error",
62
+ "get_validation_suggestions",
63
+ ]
@@ -0,0 +1,141 @@
1
+ """
2
+ Base error classes for htcli.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from rich.console import Console
8
+
9
+ console = Console()
10
+
11
+
12
+ class HTCLIError(Exception):
13
+ """Base exception class for htcli errors."""
14
+
15
+ def __init__(self, message: str, suggestions: Optional[list[str]] = None):
16
+ self.message = message
17
+ self.suggestions = suggestions or []
18
+ super().__init__(self.message)
19
+
20
+ def display(self):
21
+ """Display the error with suggestions."""
22
+ console.print(f"❌ {self.message}")
23
+
24
+ if self.suggestions:
25
+ console.print("\n[bold yellow]💡 Helpful Suggestions:[/bold yellow]")
26
+ for suggestion in self.suggestions:
27
+ console.print(f"• {suggestion}")
28
+
29
+
30
+ class WalletError(HTCLIError):
31
+ """Base class for wallet-related errors."""
32
+
33
+ pass
34
+
35
+
36
+ class TransferError(HTCLIError):
37
+ """Base class for transfer-related errors."""
38
+
39
+ pass
40
+
41
+
42
+ class BalanceError(HTCLIError):
43
+ """Base class for balance-related errors."""
44
+
45
+ pass
46
+
47
+
48
+ class KeyGenerationError(HTCLIError):
49
+ """Base class for key generation errors."""
50
+
51
+ pass
52
+
53
+
54
+ class KeyDeletionError(HTCLIError):
55
+ """Base class for key deletion errors."""
56
+
57
+ pass
58
+
59
+
60
+ class SubnetError(HTCLIError):
61
+ """Base class for subnet-related errors."""
62
+
63
+ pass
64
+
65
+
66
+ class SubnetRegistrationError(HTCLIError):
67
+ """Custom exception for subnet registration errors."""
68
+
69
+ pass
70
+
71
+
72
+ class InvalidPasswordError(HTCLIError):
73
+ """Custom exception for invalid wallet password."""
74
+
75
+
76
+ class SubnetActivationError(SubnetError):
77
+ """Error during subnet activation."""
78
+
79
+ pass
80
+
81
+
82
+ class SubnetValidationError(SubnetError):
83
+ """Error during subnet input validation."""
84
+
85
+ pass
86
+
87
+
88
+ class AddressValidationError(HTCLIError):
89
+ """Error during address validation."""
90
+
91
+ pass
92
+
93
+
94
+ class NodeError(HTCLIError):
95
+ """Base class for node-related errors."""
96
+
97
+ pass
98
+
99
+
100
+ class NodeRegistrationError(NodeError):
101
+ """Custom exception for node registration errors."""
102
+
103
+ pass
104
+
105
+
106
+ class NodeOperationError(NodeError):
107
+ """Error during node operations."""
108
+
109
+ pass
110
+
111
+
112
+ class StakeError(HTCLIError):
113
+ """Base class for stake-related errors."""
114
+
115
+ pass
116
+
117
+
118
+ class DelegateStakeError(StakeError):
119
+ """Error during delegate stake operations."""
120
+
121
+ pass
122
+
123
+
124
+ # Add the custom exception to errors/base.py
125
+ class AmbiguousWalletError(HTCLIError):
126
+ """Raised when multiple wallets match the given criteria."""
127
+ def __init__(self, name: str, matches: list[dict], is_hotkey: bool = False):
128
+ self.name = name
129
+ self.matches = matches
130
+ self.is_hotkey = is_hotkey
131
+ error_msg = (
132
+ f"Wallet '{name}' is ambiguous. Found {len(matches)} hotkey wallets "
133
+ f"with this name. Please specify owner_address or owner_coldkey_name to disambiguate:\n"
134
+ )
135
+ for match in matches:
136
+ owner_addr = match.get("owner_address", "Unknown")
137
+ owner_ck = match.get("owner_coldkey_name", "Unknown")
138
+ error_msg += (
139
+ f" - Owner coldkey: {owner_ck} (address: {owner_addr})\n"
140
+ )
141
+ super().__init__(error_msg)
@@ -0,0 +1,20 @@
1
+ """
2
+ Display utility functions for error handling.
3
+ """
4
+
5
+ from rich.console import Console
6
+
7
+ console = Console()
8
+
9
+
10
+ def display_balance_info(address: str, balance: float = 0):
11
+ """Display helpful information when balance is 0."""
12
+ if balance == 0:
13
+ console.print(
14
+ "\n[bold yellow]💡 Note:[/bold yellow] This wallet has no balance."
15
+ )
16
+ console.print(f"• To receive funds, share this address: [bold]{address}[/bold]")
17
+ console.print(
18
+ "• You can transfer funds from another wallet using: [bold]htcli wallet transfer[/bold]"
19
+ )
20
+ console.print("• Transaction fees are typically around 0.001-0.01 TENSOR")