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.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- 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)}
|