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,487 @@
1
+ """
2
+ Base extrinsic client with retry logic and common functionality.
3
+
4
+ Provides shared functionality for all extrinsic clients including:
5
+ - Retry logic for transient failures
6
+ - Standard extrinsic submission
7
+ - Error handling and extraction
8
+ - Event parsing
9
+ """
10
+
11
+ from typing import Any, Optional
12
+
13
+ from substrateinterface import Keypair, SubstrateInterface
14
+ from substrateinterface.exceptions import SubstrateRequestException
15
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
16
+
17
+ from ...utils.logging import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class BaseExtrinsicClient:
23
+ """Base class for all extrinsic clients with retry logic."""
24
+
25
+ def __init__(self, substrate: Optional[SubstrateInterface] = None):
26
+ """Initialize the base extrinsic client."""
27
+ self.substrate = substrate
28
+
29
+ @retry(
30
+ wait=wait_fixed(7), # Wait 7 seconds between attempts
31
+ stop=stop_after_attempt(4), # Try up to 4 times
32
+ retry=retry_if_exception_type(
33
+ (
34
+ SubstrateRequestException,
35
+ ConnectionError,
36
+ TimeoutError,
37
+ OSError, # Network errors
38
+ )
39
+ ),
40
+ reraise=True, # Re-raise the last exception if all retries fail
41
+ )
42
+ def _submit_extrinsic_with_retry(
43
+ self,
44
+ call: Any,
45
+ keypair: Keypair,
46
+ era_period: int = 64,
47
+ wait_for_inclusion: bool = True,
48
+ wait_for_finalization: bool = True,
49
+ ) -> dict[str, Any]:
50
+ """
51
+ Submit an extrinsic with retry logic for transient failures.
52
+
53
+ Args:
54
+ call: The composed call to submit
55
+ keypair: Keypair for signing the transaction
56
+ era_period: Era period for transaction validity (blocks)
57
+ wait_for_inclusion: Wait for transaction to be included in a block
58
+ wait_for_finalization: Wait for transaction to be finalized (takes precedence over wait_for_inclusion)
59
+
60
+ Returns:
61
+ Dict with success status and transaction details
62
+
63
+ Raises:
64
+ Exception: If all retry attempts fail
65
+ """
66
+ try:
67
+ # Get current nonce
68
+ nonce = self.substrate.get_account_nonce(keypair.ss58_address)
69
+
70
+ # Create signed extrinsic (matching mesh-template pattern - no era parameter)
71
+ extrinsic = self.substrate.create_signed_extrinsic(
72
+ call=call, keypair=keypair, nonce=nonce
73
+ )
74
+
75
+ # Submit with appropriate wait option
76
+ # wait_for_finalization takes precedence if both are True
77
+ if wait_for_finalization:
78
+ response = self.substrate.submit_extrinsic(
79
+ extrinsic, wait_for_finalization=True
80
+ )
81
+ elif wait_for_inclusion:
82
+ response = self.substrate.submit_extrinsic(
83
+ extrinsic, wait_for_inclusion=True
84
+ )
85
+ else:
86
+ # Submit without waiting
87
+ response = self.substrate.submit_extrinsic(extrinsic)
88
+
89
+ if response.is_success:
90
+ # Extract all available fields from response
91
+ # Try direct access first (like NodeExtrinsics does), then fallback to getattr
92
+ try:
93
+ extrinsic_hash = response.extrinsic_hash
94
+ # Convert to hex string if it's bytes
95
+ if isinstance(extrinsic_hash, bytes):
96
+ extrinsic_hash = "0x" + extrinsic_hash.hex()
97
+ elif extrinsic_hash and not isinstance(extrinsic_hash, str):
98
+ extrinsic_hash = str(extrinsic_hash)
99
+ except (AttributeError, Exception):
100
+ extrinsic_hash = getattr(response, "extrinsic_hash", None)
101
+ if extrinsic_hash and isinstance(extrinsic_hash, bytes):
102
+ extrinsic_hash = "0x" + extrinsic_hash.hex()
103
+
104
+ try:
105
+ block_hash = response.block_hash
106
+ # Convert to hex string if it's bytes
107
+ if isinstance(block_hash, bytes):
108
+ block_hash = "0x" + block_hash.hex()
109
+ elif block_hash and not isinstance(block_hash, str):
110
+ block_hash = str(block_hash)
111
+ except (AttributeError, Exception):
112
+ block_hash = getattr(response, "block_hash", None)
113
+ if block_hash and isinstance(block_hash, bytes):
114
+ block_hash = "0x" + block_hash.hex()
115
+
116
+ try:
117
+ block_number = response.block_number
118
+ except (AttributeError, Exception):
119
+ block_number = getattr(response, "block_number", None)
120
+
121
+ # If block_number is not available but we have block_hash, query it
122
+ if block_number is None and block_hash:
123
+ try:
124
+ block_number = self.substrate.get_block_number(block_hash=block_hash)
125
+ except Exception as e:
126
+ logger.debug(f"Could not get block number from block hash: {e}")
127
+ block_number = None
128
+
129
+ try:
130
+ events = response.triggered_events
131
+ except (AttributeError, Exception):
132
+ events = getattr(response, "triggered_events", []) or getattr(response, "events", [])
133
+
134
+ # CRITICAL: Check for ExtrinsicFailed events even if is_success is True
135
+ # Sometimes transactions are included but fail at runtime
136
+ for event in events:
137
+ try:
138
+ event_data = event.value if hasattr(event, "value") else event
139
+ if (
140
+ event_data.get("module_id") == "System"
141
+ and event_data.get("event_id") == "ExtrinsicFailed"
142
+ ):
143
+ # Transaction was included but failed at runtime
144
+ error_msg = self._extract_error_message(response)
145
+ logger.error(f"Extrinsic failed at runtime: {error_msg}")
146
+ return {
147
+ "success": False,
148
+ "error": error_msg,
149
+ "error_details": getattr(response, "error_message", None),
150
+ "extrinsic_hash": extrinsic_hash,
151
+ "block_hash": block_hash,
152
+ "block_number": block_number,
153
+ }
154
+ except Exception:
155
+ continue # Skip malformed events
156
+
157
+ # Validate that we have essential transaction details
158
+ # If we don't have extrinsic_hash, the transaction didn't actually succeed
159
+ if not extrinsic_hash:
160
+ error_msg = "Transaction submitted but no transaction hash returned. This may indicate a failure."
161
+ logger.error(error_msg)
162
+ logger.debug(f"Response type: {type(response)}")
163
+ logger.debug(f"Response has extrinsic_hash attr: {hasattr(response, 'extrinsic_hash')}")
164
+ logger.debug(f"Response has block_hash attr: {hasattr(response, 'block_hash')}")
165
+ if hasattr(response, "extrinsic_hash"):
166
+ logger.debug(f"extrinsic_hash type: {type(response.extrinsic_hash)}, value: {response.extrinsic_hash}")
167
+ if hasattr(response, "block_hash"):
168
+ logger.debug(f"block_hash type: {type(response.block_hash)}, value: {response.block_hash}")
169
+
170
+ # Try to extract error from events as fallback
171
+ error_from_events = self._extract_error_message(response)
172
+ if error_from_events and error_from_events != "Unknown error occurred during extrinsic submission":
173
+ error_msg = error_from_events
174
+
175
+ return {
176
+ "success": False,
177
+ "error": error_msg,
178
+ "error_details": getattr(response, "error_message", None),
179
+ "extrinsic_hash": None,
180
+ "block_hash": block_hash,
181
+ "block_number": block_number,
182
+ }
183
+
184
+ result = {
185
+ "success": True,
186
+ "extrinsic_hash": extrinsic_hash,
187
+ "block_hash": block_hash,
188
+ "block_number": block_number,
189
+ "events": events,
190
+ }
191
+
192
+ # Also set transaction_hash as alias for compatibility
193
+ result["transaction_hash"] = extrinsic_hash
194
+
195
+ logger.info(
196
+ f"Extrinsic submitted successfully: {extrinsic_hash} "
197
+ f"at block {block_number}"
198
+ )
199
+
200
+ return result
201
+ else:
202
+ error_msg = self._extract_error_message(response)
203
+ logger.error(f"Extrinsic failed: {error_msg}")
204
+
205
+ # Return error details for handler to display nicely
206
+ return {
207
+ "success": False,
208
+ "error": error_msg,
209
+ "error_details": getattr(response, "error_message", None),
210
+ "extrinsic_hash": getattr(response, "extrinsic_hash", None),
211
+ }
212
+
213
+ except Exception as e:
214
+ logger.error(f"Error submitting extrinsic: {e}")
215
+ raise # Let retry decorator handle retries
216
+
217
+ def _submit_extrinsic(
218
+ self,
219
+ call: Any,
220
+ keypair: Keypair,
221
+ wait_for_inclusion: bool = True,
222
+ wait_for_finalization: bool = True,
223
+ ) -> dict[str, Any]:
224
+ """
225
+ Submit an extrinsic (wrapper for backward compatibility).
226
+
227
+ Args:
228
+ call: The composed call to submit
229
+ keypair: Keypair for signing the transaction
230
+ wait_for_inclusion: Wait for transaction to be included in a block
231
+ wait_for_finalization: Wait for transaction to be finalized (takes precedence over wait_for_inclusion)
232
+
233
+ Returns:
234
+ Dict with success status and transaction details
235
+ """
236
+ try:
237
+ return self._submit_extrinsic_with_retry(
238
+ call,
239
+ keypair,
240
+ wait_for_inclusion=wait_for_inclusion,
241
+ wait_for_finalization=wait_for_finalization,
242
+ )
243
+ except Exception as e:
244
+ # If retry fails, return error dict instead of raising
245
+ return {
246
+ "success": False,
247
+ "error": str(e),
248
+ "extrinsic_hash": None,
249
+ }
250
+
251
+ def _extract_error_message(self, response: Any) -> str:
252
+ """
253
+ Extract error message from substrate response.
254
+
255
+ Args:
256
+ response: Substrate response object
257
+
258
+ Returns:
259
+ Human-readable error message (preferably just the error name like "NotPendingSubnetOwner")
260
+ """
261
+ try:
262
+ # First, try to get error from error_message attribute
263
+ if hasattr(response, "error_message") and response.error_message:
264
+ error = response.error_message
265
+
266
+ if isinstance(error, dict):
267
+ # Module error format: {"type": "Module", "name": "NotPendingSubnetOwner", "docs": [...]}
268
+ if error.get("type") == "Module":
269
+ module_error = error.get("name", "UnknownError")
270
+ # Return just the error name for easier matching in error handlers
271
+ # The error handlers will provide user-friendly messages
272
+ return module_error
273
+
274
+ # RPC error format: {'code': ..., 'message': ..., 'data': ...}
275
+ if "message" in error:
276
+ message = error["message"]
277
+ data = error.get("data", "")
278
+ if data:
279
+ return f"{message}: {data}"
280
+ return message
281
+
282
+ # Check if error dict has a "name" key directly
283
+ if "name" in error:
284
+ return error["name"]
285
+
286
+ # Other error types
287
+ return str(error)
288
+
289
+ # If error_message is a string, return it
290
+ if isinstance(error, str):
291
+ return error
292
+
293
+ return str(error)
294
+
295
+ # Fallback: Extract error from ExtrinsicFailed events
296
+ if hasattr(response, "triggered_events"):
297
+ for event in response.triggered_events:
298
+ try:
299
+ event_data = event.value if hasattr(event, "value") else event
300
+ if (
301
+ event_data.get("module_id") == "System"
302
+ and event_data.get("event_id") == "ExtrinsicFailed"
303
+ ):
304
+ # Extract the actual error name from event attributes
305
+ attributes = event_data.get("attributes", [])
306
+ if attributes:
307
+ error = attributes[0]
308
+
309
+ # Try multiple ways to extract the error name
310
+ if hasattr(error, "name"):
311
+ return error.name
312
+ elif isinstance(error, dict):
313
+ # Check for nested error structure
314
+ if "name" in error:
315
+ return error["name"]
316
+ elif "error" in error and isinstance(error["error"], dict):
317
+ return error["error"].get("name", str(error))
318
+ # Check for Module error format in attributes
319
+ if error.get("type") == "Module":
320
+ return error.get("name", str(error))
321
+ return str(error)
322
+ elif isinstance(error, (list, tuple)) and len(error) >= 2:
323
+ # Error might be encoded as [module_index, error_index]
324
+ # substrateinterface should decode this, but handle it just in case
325
+ logger.debug(f"Error encoded as indices: {error}")
326
+ return "UnknownError" # Can't decode without metadata
327
+ else:
328
+ # Try to convert to string and check if it looks like an error name
329
+ error_str = str(error)
330
+ # If it's a simple string that looks like an error name, return it
331
+ if error_str and not error_str.startswith("{") and not error_str.startswith("["):
332
+ return error_str
333
+ return error_str
334
+
335
+ # No attributes found
336
+ logger.debug("ExtrinsicFailed event has no attributes")
337
+ return "ExtrinsicFailed"
338
+ except Exception as e:
339
+ logger.debug(f"Error extracting from event: {e}")
340
+ continue # Skip malformed events
341
+
342
+ # Log what we have for debugging
343
+ logger.debug(f"Could not extract error from response. Type: {type(response)}")
344
+ if hasattr(response, "__dict__"):
345
+ logger.debug(f"Response attributes: {list(response.__dict__.keys())}")
346
+
347
+ return "Unknown error occurred during extrinsic submission"
348
+
349
+ except Exception as e:
350
+ logger.warning(f"Could not extract error message: {e}", exc_info=True)
351
+ return "Error occurred during transaction processing"
352
+
353
+ def _extract_event_by_type(
354
+ self, events: list, module_id: str, event_id: str
355
+ ) -> Optional[dict[str, Any]]:
356
+ """
357
+ Extract specific event data from response events.
358
+
359
+ Args:
360
+ events: List of triggered events
361
+ module_id: Module ID to match (e.g., "Network")
362
+ event_id: Event ID to match (e.g., "SubnetRegistered")
363
+
364
+ Returns:
365
+ Event data dict or None if not found
366
+ """
367
+ try:
368
+ for event in events:
369
+ event_data = event.value
370
+ if (
371
+ event_data.get("module_id") == module_id
372
+ and event_data.get("event_id") == event_id
373
+ ):
374
+ return event_data
375
+ except Exception as e:
376
+ logger.warning(f"Could not extract event data: {e}")
377
+
378
+ return None
379
+
380
+ def _extract_attribute_value(
381
+ self, event_data: dict[str, Any], attribute_index: int, default: Any = None
382
+ ) -> Any:
383
+ """
384
+ Extract attribute value from event data by index.
385
+
386
+ Args:
387
+ event_data: Event data dictionary
388
+ attribute_index: Index of attribute to extract
389
+ default: Default value if extraction fails
390
+
391
+ Returns:
392
+ Attribute value or default
393
+ """
394
+ try:
395
+ attributes = event_data.get("attributes", [])
396
+ if attribute_index < len(attributes):
397
+ return attributes[attribute_index]
398
+ except Exception as e:
399
+ logger.warning(f"Could not extract attribute {attribute_index}: {e}")
400
+
401
+ return default
402
+
403
+ def _decode_bytes_attribute(self, value: Any) -> str:
404
+ """
405
+ Decode bytes attribute to string.
406
+
407
+ Args:
408
+ value: Bytes or string value
409
+
410
+ Returns:
411
+ Decoded string
412
+ """
413
+ try:
414
+ if isinstance(value, bytes):
415
+ return value.decode("utf-8", errors="ignore")
416
+ return str(value)
417
+ except Exception:
418
+ return str(value)
419
+
420
+ def _validate_keypair(self, keypair: Keypair) -> None:
421
+ """
422
+ Validate that keypair is properly initialized.
423
+
424
+ Args:
425
+ keypair: Keypair to validate
426
+
427
+ Raises:
428
+ ValueError: If keypair is invalid
429
+ """
430
+ if not keypair:
431
+ raise ValueError("Keypair is required")
432
+
433
+ if not hasattr(keypair, "ss58_address") or not keypair.ss58_address:
434
+ raise ValueError("Keypair must have valid SS58 address")
435
+
436
+ if not hasattr(keypair, "private_key") or not keypair.private_key:
437
+ raise ValueError("Keypair must have valid private key")
438
+
439
+ def _validate_substrate(self) -> None:
440
+ """
441
+ Validate that substrate interface is properly initialized.
442
+
443
+ Raises:
444
+ ValueError: If substrate is invalid
445
+ """
446
+ if not self.substrate:
447
+ raise ValueError("Substrate interface is required")
448
+
449
+ try:
450
+ # Test connection
451
+ self.substrate.get_block_number()
452
+ except Exception as e:
453
+ raise ValueError(f"Substrate interface not connected: {e}") from e
454
+
455
+
456
+ # Common error messages for better user experience
457
+ COMMON_ERROR_MESSAGES = {
458
+ "NotEnoughBalanceToStake": "Insufficient balance to stake the requested amount",
459
+ "NotEnoughStakeToWithdraw": "Insufficient stake shares to withdraw the requested amount",
460
+ "MinStakeNotReached": "Stake amount below minimum required",
461
+ "MaxStakeReached": "Stake amount exceeds maximum allowed",
462
+ "InvalidSubnetId": "Subnet does not exist",
463
+ "InvalidNodeId": "Subnet node does not exist",
464
+ "SubnetAlreadyExists": "Subnet name already exists",
465
+ "NodeAlreadyRegistered": "Node already registered for this subnet",
466
+ "NotSubnetOwner": "Only subnet owner can perform this action",
467
+ "NodeNotActive": "Subnet node is not active",
468
+ "SubnetNotActive": "Subnet is not active",
469
+ "NotKeyOwner": "The selected coldkey does not own the hotkey for this node",
470
+ }
471
+
472
+
473
+ def get_user_friendly_error(error_msg: str) -> str:
474
+ """
475
+ Convert technical error messages to user-friendly ones.
476
+
477
+ Args:
478
+ error_msg: Technical error message
479
+
480
+ Returns:
481
+ User-friendly error message
482
+ """
483
+ for error_type, friendly_msg in COMMON_ERROR_MESSAGES.items():
484
+ if error_type in error_msg:
485
+ return friendly_msg
486
+
487
+ return error_msg
@@ -0,0 +1,79 @@
1
+ """
2
+ Consensus-related extrinsics.
3
+ """
4
+
5
+ from typing import Any, Dict
6
+
7
+ from substrateinterface import Keypair
8
+
9
+ from ...models.requests.consensus import AttestRequest, ProposeAttestationRequest
10
+ from ...utils.logging import get_logger
11
+ from .base import BaseExtrinsicClient
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ class ConsensusExtrinsics(BaseExtrinsicClient):
17
+ """Consensus-related extrinsics."""
18
+
19
+ def propose_attestation(
20
+ self, request: ProposeAttestationRequest, keypair: Keypair
21
+ ) -> Dict[str, Any]:
22
+ """
23
+ Propose an attestation.
24
+
25
+ Args:
26
+ request: Proposal request
27
+ keypair: Keypair for signing
28
+
29
+ Returns:
30
+ Extrinsic receipt
31
+ """
32
+ try:
33
+ logger.info(f"Proposing attestation for subnet {request.subnet_id}")
34
+
35
+ call = self.substrate.compose_call(
36
+ call_module="Network",
37
+ call_function="propose_attestation",
38
+ call_params={
39
+ "subnet_id": request.subnet_id,
40
+ "data": request.data,
41
+ },
42
+ )
43
+
44
+ return self._submit_extrinsic(call, keypair)
45
+
46
+ except Exception as e:
47
+ logger.error(f"Error proposing attestation: {e}")
48
+ return {"success": False, "error": str(e)}
49
+
50
+ def attest(self, request: AttestRequest, keypair: Keypair) -> Dict[str, Any]:
51
+ """
52
+ Attest to a proposal.
53
+
54
+ Args:
55
+ request: Attestation request
56
+ keypair: Keypair for signing
57
+
58
+ Returns:
59
+ Extrinsic receipt
60
+ """
61
+ try:
62
+ logger.info(
63
+ f"Attesting to proposal {request.proposal_hash} on subnet {request.subnet_id}"
64
+ )
65
+
66
+ call = self.substrate.compose_call(
67
+ call_module="Network",
68
+ call_function="attest",
69
+ call_params={
70
+ "subnet_id": request.subnet_id,
71
+ "proposal_hash": request.proposal_hash,
72
+ },
73
+ )
74
+
75
+ return self._submit_extrinsic(call, keypair)
76
+
77
+ except Exception as e:
78
+ logger.error(f"Error attesting: {e}")
79
+ return {"success": False, "error": str(e)}