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
@@ -0,0 +1,710 @@
1
+ """
2
+ Improved error handling functions for htcli using SubstrateInterface built-in error parsing.
3
+ """
4
+
5
+ import os
6
+ import traceback
7
+ from typing import Any
8
+
9
+ from pydantic import ValidationError
10
+ from substrateinterface.exceptions import (
11
+ BlockNotFound,
12
+ ConfigurationError,
13
+ ExtrinsicFailedException,
14
+ StorageFunctionNotFound,
15
+ SubstrateRequestException,
16
+ )
17
+
18
+ from .base import (
19
+ AddressValidationError,
20
+ BalanceError,
21
+ DelegateStakeError,
22
+ HTCLIError,
23
+ InvalidPasswordError,
24
+ KeyDeletionError,
25
+ KeyGenerationError,
26
+ NodeError,
27
+ NodeOperationError,
28
+ NodeRegistrationError,
29
+ StakeError,
30
+ SubnetActivationError,
31
+ SubnetError,
32
+ SubnetRegistrationError,
33
+ TransferError,
34
+ WalletError,
35
+ )
36
+
37
+ DEBUG_ERRORS = os.environ.get("HTCLI_DEBUG", "").lower() in {"1", "true", "yes"}
38
+
39
+
40
+ def _print_debug_traceback(error: Exception) -> None:
41
+ """Print the exception traceback when HTCLI_DEBUG is enabled."""
42
+ if not DEBUG_ERRORS:
43
+ return
44
+ traceback.print_exception(type(error), error, error.__traceback__)
45
+
46
+
47
+ def handle_substrate_error(error: Exception) -> HTCLIError:
48
+ """
49
+ Convert SubstrateInterface errors to user-friendly HTCLI errors.
50
+ Uses SubstrateInterface's built-in error parsing with pattern matching.
51
+ """
52
+ _print_debug_traceback(error)
53
+ error_str = str(error).lower()
54
+
55
+ # Handle SubstrateInterface specific exceptions
56
+ if isinstance(error, ExtrinsicFailedException):
57
+ return _handle_extrinsic_error(error)
58
+ elif isinstance(error, SubstrateRequestException):
59
+ return _handle_request_error(error)
60
+ elif isinstance(error, ConfigurationError):
61
+ return _handle_config_error(error)
62
+ elif isinstance(error, StorageFunctionNotFound):
63
+ return _handle_storage_error(error)
64
+ elif isinstance(error, BlockNotFound):
65
+ return _handle_block_error(error)
66
+
67
+ # Pattern matching for common error types - ORDER MATTERS!
68
+ # Check for config errors first (before "invalid" pattern catches them)
69
+ if "setting" in error_str and (
70
+ "not found" in error_str or "setting '" in error_str
71
+ ):
72
+ return HTCLIError(
73
+ str(error),
74
+ [
75
+ "Check that the configuration key exists",
76
+ "Use 'htcli config show' to view all settings",
77
+ "Use 'htcli config --help' for usage",
78
+ ],
79
+ )
80
+ # Check for stake/delegate errors BEFORE subnet errors (stake errors may mention subnet ID)
81
+ stake_error_patterns = [
82
+ "delegatestake", "delegatestake", "nodedelegatestake", "stake", "staking",
83
+ "minstake", "maxstake", "stakenot", "invalidstake", "stakeamount"
84
+ ]
85
+ if any(pattern in error_str for pattern in stake_error_patterns):
86
+ return _create_stake_error(str(error))
87
+ # Check for specific subnet errors BEFORE generic balance errors
88
+ # This prevents misclassification (e.g., NotSubnetOwner shouldn't become "Insufficient balance")
89
+ # Check for common subnet error patterns (case-insensitive via error_str which is already lowercased)
90
+ subnet_error_patterns = [
91
+ "notsubnetowner", "notpending", "invalidsubnet", "subnetmustbe",
92
+ "subnetnameexist", "subnetrepoexist", "subnetis", "subnetnot",
93
+ "badorigin", "notowner"
94
+ ]
95
+ if any(pattern in error_str for pattern in subnet_error_patterns):
96
+ return _create_subnet_error(str(error))
97
+ # Only match balance errors if they don't contain subnet/owner context
98
+ elif any(pattern in error_str for pattern in ["insufficient", "balance", "funds"]) and not any(pattern in error_str for pattern in ["subnet", "node", "owner", "pending"]):
99
+ return _create_balance_error(str(error))
100
+ elif any(pattern in error_str for pattern in ["wallet", "key", "signature"]):
101
+ return _create_wallet_error(str(error))
102
+ elif any(pattern in error_str for pattern in ["invalid", "address", "account"]) and "typeerror" not in error_str:
103
+ return _create_address_error(str(error))
104
+
105
+ # Generic fallback
106
+ return HTCLIError(
107
+ f"Operation failed: {str(error)}",
108
+ ["Please check your parameters and try again", "Use --help for command usage"],
109
+ )
110
+
111
+
112
+ def _handle_extrinsic_error(error: ExtrinsicFailedException) -> HTCLIError:
113
+ """Handle failed extrinsic errors with specific patterns."""
114
+ error_msg = str(error)
115
+ error_details = getattr(error, "error_message", {})
116
+
117
+ # Parse module errors if available
118
+ if isinstance(error_details, dict) and "Module" in error_details:
119
+ module_info = error_details["Module"]
120
+ return _parse_module_error(module_info, error_msg)
121
+
122
+ # Pattern matching for common extrinsic failures
123
+ if "insufficientbalance" in error_msg.lower():
124
+ return TransferError(
125
+ "Insufficient balance to complete the transaction",
126
+ [
127
+ "Check your balance: htcli wallet balance --wallet <wallet-name>",
128
+ "Ensure you have enough funds for both transfer and fees",
129
+ "Transaction fees are typically 0.001-0.01 TENSOR",
130
+ "Try a smaller amount",
131
+ ],
132
+ )
133
+
134
+ if "invalidaddress" in error_msg.lower():
135
+ return AddressValidationError(
136
+ "Invalid destination address provided",
137
+ [
138
+ "Verify the address format is correct",
139
+ "Ensure the address is for the correct network",
140
+ "Check for any typos in the address",
141
+ ],
142
+ )
143
+
144
+ return HTCLIError(f"Transaction failed: {error_msg}")
145
+
146
+
147
+ def _handle_request_error(error: SubstrateRequestException) -> HTCLIError:
148
+ """Handle RPC request errors."""
149
+ return HTCLIError(
150
+ f"Network request failed: {str(error)}",
151
+ [
152
+ "Check your network connection",
153
+ "Verify the RPC endpoint is accessible",
154
+ "Try again in a moment",
155
+ ],
156
+ )
157
+
158
+
159
+ def _handle_config_error(error: ConfigurationError) -> HTCLIError:
160
+ """Handle configuration errors."""
161
+ return HTCLIError(
162
+ f"Configuration error: {str(error)}",
163
+ [
164
+ "Check your configuration file",
165
+ "Verify network settings",
166
+ "Use htcli config --help for configuration options",
167
+ ],
168
+ )
169
+
170
+
171
+ def _handle_storage_error(error: StorageFunctionNotFound) -> HTCLIError:
172
+ """Handle storage function not found errors."""
173
+ return HTCLIError(
174
+ f"Storage function not found: {str(error)}",
175
+ [
176
+ "This might be a version compatibility issue",
177
+ "Check if you're connected to the correct network",
178
+ "Verify the blockchain runtime version",
179
+ ],
180
+ )
181
+
182
+
183
+ def _handle_block_error(error: BlockNotFound) -> HTCLIError:
184
+ """Handle block not found errors."""
185
+ return HTCLIError(
186
+ f"Block not found: {str(error)}",
187
+ [
188
+ "The requested block may not exist yet",
189
+ "Check if the block number/hash is correct",
190
+ "Try with a different block or use latest",
191
+ ],
192
+ )
193
+
194
+
195
+ def _parse_module_error(module_info: dict, full_error: str) -> HTCLIError:
196
+ """Parse module-specific errors from substrate."""
197
+ error_index = module_info.get("error", 0)
198
+ module_index = module_info.get("index", 0)
199
+
200
+ # Common substrate module error patterns
201
+ balance_errors = [0, 1, 2, 5] # InsufficientBalance, LiquidityRestrictions, etc.
202
+ if error_index in balance_errors:
203
+ return TransferError(
204
+ "Insufficient balance or liquidity restrictions",
205
+ [
206
+ "Check your balance: htcli wallet balance --wallet <wallet-name>",
207
+ "Ensure funds are not locked or reserved",
208
+ "Try a smaller amount",
209
+ "Wait for any pending transactions to complete",
210
+ ],
211
+ )
212
+
213
+ # Generic module error
214
+ return HTCLIError(
215
+ f"Module error (index: {module_index}, error: {error_index}): {full_error}",
216
+ [
217
+ "This is a blockchain-specific error",
218
+ "Check the blockchain documentation for error details",
219
+ "Verify your transaction parameters",
220
+ ],
221
+ )
222
+
223
+
224
+ def _create_balance_error(error_msg: str) -> BalanceError:
225
+ """Create user-friendly balance error."""
226
+ return BalanceError(
227
+ "Insufficient balance to complete the operation",
228
+ [
229
+ "Check your current balance: htcli wallet balance --wallet <wallet-name>",
230
+ "Ensure you have enough funds for the operation plus fees",
231
+ "Transaction fees are typically 0.001-0.01 TENSOR",
232
+ "Try with a smaller amount",
233
+ ],
234
+ )
235
+
236
+
237
+ def _create_address_error(error_msg: str) -> AddressValidationError:
238
+ """Create user-friendly address validation error."""
239
+ return AddressValidationError(
240
+ "Invalid address format or address not found",
241
+ [
242
+ "Verify the address format is correct",
243
+ "Ensure you're using the right network address format",
244
+ "Check for typos in the address",
245
+ "For EVM addresses, use format: 0x1234567890123456789012345678901234567890",
246
+ ],
247
+ )
248
+
249
+
250
+ def _create_subnet_error(error_msg: str) -> SubnetError:
251
+ """Create user-friendly subnet error."""
252
+ if "registration" in error_msg.lower():
253
+ return SubnetRegistrationError(
254
+ "Subnet registration failed",
255
+ [
256
+ "Check if you have sufficient stake for registration",
257
+ "Verify subnet parameters are valid",
258
+ "Ensure you have permission to register subnets",
259
+ "Check subnet requirements: htcli subnet requirements",
260
+ ],
261
+ )
262
+ elif "activation" in error_msg.lower():
263
+ return SubnetActivationError(
264
+ "Subnet activation failed",
265
+ [
266
+ "Check if the subnet meets activation requirements",
267
+ "Verify you own the subnet or have permission",
268
+ "Ensure all activation criteria are met",
269
+ "Check subnet status: htcli subnet list",
270
+ ],
271
+ )
272
+ else:
273
+ return SubnetError(
274
+ "Subnet operation failed",
275
+ [
276
+ "Check subnet status: htcli subnet list",
277
+ "Verify your permissions and balance",
278
+ "Review command help: htcli subnet <command> --help",
279
+ ],
280
+ )
281
+
282
+
283
+ def _create_stake_error(error_msg: str) -> StakeError:
284
+ """Create user-friendly stake error."""
285
+ if "delegate" in error_msg.lower():
286
+ return DelegateStakeError(
287
+ "Delegate stake operation failed",
288
+ [
289
+ "Check your balance: htcli wallet balance --wallet <wallet-name>",
290
+ "Verify subnet ID is valid: htcli subnet info --subnet-id <id>",
291
+ "Ensure you have sufficient funds for the stake amount plus fees",
292
+ "Check stake requirements: htcli stake delegate-add --help",
293
+ ],
294
+ )
295
+ else:
296
+ return StakeError(
297
+ "Stake operation failed",
298
+ [
299
+ "Check your balance: htcli wallet balance --wallet <wallet-name>",
300
+ "Verify subnet/node ID is valid",
301
+ "Ensure you have sufficient funds for the stake amount plus fees",
302
+ "Review command help: htcli stake <command> --help",
303
+ ],
304
+ )
305
+
306
+
307
+ def _create_node_error(error_msg: str) -> NodeError:
308
+ """Create user-friendly node error."""
309
+ error_lower = error_msg.lower()
310
+
311
+ if "registration" in error_lower:
312
+ return NodeRegistrationError(
313
+ "Node registration failed",
314
+ [
315
+ "Check if you have sufficient stake for node registration",
316
+ "Verify node parameters are valid (peer IDs, delegate rate, etc.)",
317
+ "Ensure the subnet exists and is accepting registrations",
318
+ "Check node requirements: htcli node register --help",
319
+ "Verify subnet info: htcli subnet info --subnet-id <id>",
320
+ ],
321
+ )
322
+ elif "overwatch" in error_lower:
323
+ # Handle overwatch-specific errors with appropriate suggestions
324
+ if any(pattern in error_lower for pattern in ["register", "register_overwatch", "qualified", "stake"]):
325
+ return NodeOperationError(
326
+ "Overwatch registration failed",
327
+ [
328
+ "Check qualification status: htcli overwatch check-qualification --coldkey <name>",
329
+ "Ensure you have sufficient balance for minimum stake",
330
+ "Verify the hotkey is not already registered elsewhere",
331
+ "The hotkey must be different from the coldkey",
332
+ "Review command help: htcli overwatch register --help",
333
+ ],
334
+ )
335
+ elif any(pattern in error_lower for pattern in ["commit", "reveal", "epoch"]):
336
+ return NodeOperationError(
337
+ "Overwatch commit/reveal operation failed",
338
+ [
339
+ "Verify the node exists: htcli overwatch list",
340
+ "Check if the epoch number is valid",
341
+ "Ensure the node has overwatch data for the specified epoch",
342
+ "Try: htcli overwatch info --node-id <id>",
343
+ ],
344
+ )
345
+ else:
346
+ return NodeOperationError(
347
+ "Overwatch node operation failed",
348
+ [
349
+ "Check your overwatch node status: htcli overwatch list",
350
+ "Verify you have the correct permissions",
351
+ "Check qualification: htcli overwatch check-qualification --coldkey <name>",
352
+ "Review command help: htcli overwatch --help",
353
+ ],
354
+ )
355
+ elif "not found" in error_lower or "does not exist" in error_lower:
356
+ return NodeOperationError(
357
+ "Node not found",
358
+ [
359
+ "Verify the node ID exists in the specified subnet",
360
+ "Check available nodes: htcli node list --subnet-id <id>",
361
+ "Ensure the subnet ID is correct",
362
+ "Verify subnet exists: htcli subnet list",
363
+ ],
364
+ )
365
+ else:
366
+ return NodeError(
367
+ "Node operation failed",
368
+ [
369
+ "Check node status: htcli node info --subnet-id <id> --node-id <id>",
370
+ "Verify node exists: htcli node list --subnet-id <id>",
371
+ "Ensure you have the correct permissions",
372
+ "Review command help: htcli node <command> --help",
373
+ ],
374
+ )
375
+
376
+
377
+
378
+ def _create_wallet_error(error_msg: str) -> WalletError:
379
+ """Create user-friendly wallet error."""
380
+ if "key" in error_msg.lower():
381
+ if "generation" in error_msg.lower() or "create" in error_msg.lower():
382
+ return KeyGenerationError(
383
+ "Failed to generate wallet keys",
384
+ [
385
+ "Ensure you have write permissions to the wallet directory",
386
+ "Check available disk space",
387
+ "Try with a different wallet name",
388
+ "Verify the key type is supported",
389
+ ],
390
+ )
391
+ elif "deletion" in error_msg.lower() or "remove" in error_msg.lower():
392
+ return KeyDeletionError(
393
+ "Failed to delete wallet keys",
394
+ [
395
+ "Verify the wallet exists: htcli wallet list",
396
+ "Ensure you have write permissions",
397
+ "Check if the wallet is currently in use",
398
+ "Try again with --force flag if appropriate",
399
+ ],
400
+ )
401
+
402
+ # Include the actual error message so users can see what went wrong
403
+ # Only use generic message if error_msg is too generic
404
+ if error_msg.lower() in ["wallet operation failed", "wallet error", "wallet"]:
405
+ display_msg = "Wallet operation failed"
406
+ else:
407
+ display_msg = f"Wallet operation failed: {error_msg}"
408
+
409
+ return WalletError(
410
+ display_msg,
411
+ [
412
+ "Verify the wallet exists: htcli wallet list",
413
+ "Check wallet permissions and password",
414
+ "Ensure wallet files are not corrupted",
415
+ "Try reloading the wallet",
416
+ ],
417
+ )
418
+
419
+
420
+ # Legacy function aliases for backward compatibility
421
+ def handle_blockchain_error(error_msg: str) -> HTCLIError:
422
+ """Legacy function - redirects to new substrate error handler."""
423
+ return handle_substrate_error(Exception(error_msg))
424
+
425
+
426
+ def handle_wallet_error(error: Exception, operation: str = "wallet") -> WalletError:
427
+ """Handle wallet-specific errors."""
428
+ error_str = str(error).lower()
429
+
430
+ # Handle InvalidPasswordError specifically with helpful suggestions
431
+ if isinstance(error, InvalidPasswordError):
432
+ if operation == "update":
433
+ return WalletError(
434
+ "Invalid password provided",
435
+ [
436
+ "The password you entered is incorrect",
437
+ "Double-check that you're using the correct password for this wallet",
438
+ "If you've forgotten your password, you'll need to restore from your mnemonic",
439
+ "Make sure Caps Lock is off and you're entering the password correctly",
440
+ ],
441
+ )
442
+ else:
443
+ return WalletError(
444
+ "Invalid password provided",
445
+ [
446
+ "The password you entered is incorrect",
447
+ "Double-check that you're using the correct password for this wallet",
448
+ "If you've forgotten your password, you'll need to restore from your mnemonic",
449
+ ],
450
+ )
451
+
452
+ # Handle wallet name conflicts specifically
453
+ if ("already exists" in error_str or "already has" in error_str) and ("wallet" in error_str or "name" in error_str or "hotkey" in error_str or "coldkey" in error_str):
454
+ return WalletError(
455
+ str(error),
456
+ [
457
+ "Choose a different wallet name",
458
+ "Use 'htcli wallet list' to see existing wallet names",
459
+ "For hotkeys: each coldkey can only have one hotkey with a given name",
460
+ ],
461
+ )
462
+
463
+ substrate_error = handle_substrate_error(error)
464
+
465
+ if isinstance(substrate_error, WalletError):
466
+ return substrate_error
467
+ else:
468
+ return WalletError(
469
+ f"Wallet {operation} failed: {str(error)}",
470
+ [
471
+ "Verify wallet exists: htcli wallet list",
472
+ "Check wallet permissions and password",
473
+ "Ensure wallet files are accessible",
474
+ f"Review command help: htcli wallet {operation} --help",
475
+ ],
476
+ )
477
+
478
+
479
+ def handle_subnet_error(error_msg: str, operation: str = "subnet") -> SubnetError:
480
+ """Handle subnet-specific errors."""
481
+ error = Exception(error_msg)
482
+ substrate_error = handle_substrate_error(error)
483
+
484
+ if isinstance(substrate_error, SubnetError):
485
+ return substrate_error
486
+ else:
487
+ return SubnetError(
488
+ f"Subnet {operation} failed: {error_msg}",
489
+ [
490
+ "Check subnet status: htcli subnet list",
491
+ "Verify permissions and wallet balance",
492
+ f"Review command help: htcli subnet {operation} --help",
493
+ ],
494
+ )
495
+
496
+
497
+ def format_pydantic_validation_error(error: ValidationError, model_name: str = "Request") -> str:
498
+ """
499
+ Format a Pydantic ValidationError into a user-friendly message.
500
+
501
+ Args:
502
+ error: The Pydantic ValidationError
503
+ model_name: Name of the model that failed validation (e.g., "SubnetRegisterRequest")
504
+
505
+ Returns:
506
+ Formatted error message string
507
+ """
508
+ errors = error.errors()
509
+ if not errors:
510
+ return f"Validation failed for {model_name}"
511
+
512
+ error_lines = [f"[htcli.error]❌ Validation Error in {model_name}[/htcli.error]\n"]
513
+ error_lines.append("[htcli.subtitle]The following fields have invalid values:[/htcli.subtitle]\n")
514
+
515
+ for err in errors:
516
+ field_path = " -> ".join(str(loc) for loc in err["loc"])
517
+ field_name = field_path if field_path != "root" else "request"
518
+ error_type = err["type"]
519
+ error_msg = err.get("msg", "Invalid value")
520
+ input_value = err.get("input")
521
+
522
+ # Format the error message
523
+ error_lines.append(f"[htcli.value]• {field_name}[/htcli.value]")
524
+
525
+ # Extract the actual error message from the context
526
+ if "ctx" in err and "error" in err["ctx"]:
527
+ actual_error = err["ctx"]["error"]
528
+ error_lines.append(f" [htcli.error]{actual_error}[/htcli.error]")
529
+ else:
530
+ # Try to extract meaningful message from error_msg
531
+ if "Value error" in error_msg:
532
+ # Extract the actual error message after "Value error, "
533
+ parts = error_msg.split(", ", 1)
534
+ if len(parts) > 1:
535
+ error_lines.append(f" [htcli.error]{parts[1]}[/htcli.error]")
536
+ else:
537
+ error_lines.append(f" [htcli.error]{error_msg}[/htcli.error]")
538
+ else:
539
+ error_lines.append(f" [htcli.error]{error_msg}[/htcli.error]")
540
+
541
+ # Show the invalid input value if available
542
+ if input_value is not None:
543
+ if isinstance(input_value, (int, float)) and input_value > 1e15:
544
+ # Format large numbers (likely wei values) in a readable way
545
+ formatted_value = f"{input_value / 1e18:.2f} TENSOR (raw: {input_value})"
546
+ else:
547
+ formatted_value = str(input_value)
548
+ error_lines.append(f" [htcli.info]Invalid value: {formatted_value}[/htcli.info]")
549
+
550
+ error_lines.append("") # Empty line between errors
551
+
552
+ return "\n".join(error_lines)
553
+
554
+
555
+ def get_validation_suggestions(error: ValidationError) -> list[str]:
556
+ """
557
+ Generate helpful suggestions based on the validation errors.
558
+
559
+ Args:
560
+ error: The Pydantic ValidationError
561
+
562
+ Returns:
563
+ List of suggestion strings
564
+ """
565
+ suggestions = []
566
+ errors = error.errors()
567
+
568
+ for err in errors:
569
+ field_path = " -> ".join(str(loc) for loc in err["loc"])
570
+ error_type = err["type"]
571
+ error_msg = err.get("msg", "")
572
+
573
+ # Generate suggestions based on field and error type
574
+ if "min_stake" in field_path.lower():
575
+ suggestions.append("min_stake must be between 100 and 250 TENSOR")
576
+ elif "max_stake" in field_path.lower():
577
+ suggestions.append("max_stake must be <= 1000 TENSOR and >= min_stake")
578
+ elif "max_registered_nodes" in field_path.lower():
579
+ suggestions.append("max_registered_nodes must be between 1 and 64")
580
+ elif "delegate_stake_percentage" in field_path.lower():
581
+ suggestions.append("delegate_stake_percentage must be between 5% and 95%")
582
+ elif "required" in error_type.lower() or "missing" in error_msg.lower():
583
+ suggestions.append(f"Field '{field_path}' is required and must be provided")
584
+ elif "greater_than" in error_type.lower() or "less_than" in error_type.lower():
585
+ suggestions.append(f"Check that '{field_path}' is within the allowed range")
586
+
587
+ # Remove duplicates while preserving order
588
+ seen = set()
589
+ unique_suggestions = []
590
+ for suggestion in suggestions:
591
+ if suggestion not in seen:
592
+ seen.add(suggestion)
593
+ unique_suggestions.append(suggestion)
594
+
595
+ return unique_suggestions if unique_suggestions else None
596
+
597
+
598
+ def display_pydantic_validation_error(
599
+ error: ValidationError,
600
+ model_name: str = "Request",
601
+ suggestions: list[str] = None,
602
+ ) -> None:
603
+ """
604
+ Display a Pydantic ValidationError in a user-friendly format.
605
+
606
+ Args:
607
+ error: The Pydantic ValidationError
608
+ model_name: Name of the model that failed validation
609
+ suggestions: Optional list of helpful suggestions
610
+ """
611
+ from ..ui.components import HTCLIPanel
612
+ from ..ui.display import HTCLIConsole
613
+
614
+ console = HTCLIConsole()
615
+
616
+ # Format the error message
617
+ error_content = format_pydantic_validation_error(error, model_name)
618
+
619
+ # Add suggestions if provided
620
+ if suggestions:
621
+ error_content += "\n[htcli.info]💡 How to fix this:[/htcli.info]\n"
622
+ for suggestion in suggestions:
623
+ error_content += f" • {suggestion}\n"
624
+ else:
625
+ # Generate default suggestions based on common validation errors
626
+ error_content += "\n[htcli.info]💡 Tips:[/htcli.info]\n"
627
+ error_content += " • Check that all numeric values are within the allowed ranges\n"
628
+ error_content += " • Ensure required fields are provided\n"
629
+ error_content += " • Verify that string fields meet length requirements\n"
630
+
631
+ # Create and display the error panel
632
+ error_panel = HTCLIPanel(
633
+ error_content,
634
+ title="⚠️ Validation Error",
635
+ border_style="htcli.error",
636
+ highlight=True,
637
+ )
638
+ error_panel.render(console.console)
639
+ console.print("")
640
+
641
+
642
+ def handle_and_display_error(error: Exception, operation: str = "operation") -> None:
643
+ """Handle and display any error with appropriate formatting."""
644
+ # Handle KeyboardInterrupt cleanly
645
+ if isinstance(error, KeyboardInterrupt):
646
+ from ..ui.display import get_console, print_warning
647
+
648
+ get_console().print()
649
+ print_warning("Operation cancelled")
650
+ import sys
651
+ sys.exit(130) # Standard exit code for SIGINT (Ctrl+C)
652
+ return
653
+
654
+ # Handle RuntimeError specifically (connection errors from get_client())
655
+ if isinstance(error, RuntimeError):
656
+ from ..ui.display import print_error
657
+ error_str = str(error).lower()
658
+ if "connect" in error_str or "blockchain" in error_str:
659
+ print_error(f"Blockchain Connection Error: {str(error)}")
660
+ else:
661
+ print_error(f"Error: {str(error)}")
662
+ raise SystemExit(1)
663
+
664
+ # Handle Pydantic ValidationError specifically
665
+ if isinstance(error, ValidationError):
666
+ suggestions = get_validation_suggestions(error)
667
+ display_pydantic_validation_error(
668
+ error,
669
+ model_name=operation.replace("_", " ").title(),
670
+ suggestions=suggestions,
671
+ )
672
+ raise SystemExit(1)
673
+
674
+ # Handle other errors using substrate error handler
675
+ htcli_error = handle_substrate_error(error)
676
+ htcli_error.display()
677
+ raise SystemExit(1)
678
+
679
+
680
+ def handle_and_display_subnet_error(
681
+ error: Exception, operation: str = "subnet"
682
+ ) -> None:
683
+ """Handle and display subnet errors with appropriate formatting."""
684
+ subnet_error = handle_subnet_error(str(error), operation)
685
+ subnet_error.display()
686
+
687
+
688
+ def handle_node_error(error_msg: str, operation: str = "node") -> NodeError:
689
+ """Handle node-specific errors."""
690
+ error = Exception(error_msg)
691
+ substrate_error = handle_substrate_error(error)
692
+
693
+ if isinstance(substrate_error, NodeError):
694
+ return substrate_error
695
+ else:
696
+ return NodeError(
697
+ f"Node {operation} failed: {error_msg}",
698
+ [
699
+ "Check node status: htcli node info --subnet-id <id> --node-id <id>",
700
+ "Verify node exists: htcli node list --subnet-id <id>",
701
+ "Ensure you have the correct permissions",
702
+ f"Review command help: htcli node {operation} --help",
703
+ ],
704
+ )
705
+
706
+
707
+ def handle_and_display_node_error(error: Exception, operation: str = "node") -> None:
708
+ """Handle and display node errors with appropriate formatting."""
709
+ node_error = handle_node_error(str(error), operation)
710
+ node_error.display()