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,1054 @@
1
+ """
2
+ Subnet node management extrinsics for HTCLI.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from substrateinterface import Keypair, SubstrateInterface
8
+
9
+ from ...models.requests.node import (
10
+ SubnetNodeActivateRequest,
11
+ SubnetNodeBootnodePeerIdUpdateRequest,
12
+ SubnetNodeBootnodeUpdateRequest,
13
+ SubnetNodeClientPeerIdUpdateRequest,
14
+ SubnetNodePauseRequest,
15
+ SubnetNodePeerIdUpdateRequest,
16
+ SubnetNodeReactivateRequest,
17
+ SubnetNodeRegisterRequest,
18
+ SubnetNodeRemoveRequest,
19
+ SubnetNodeUpdateRequest,
20
+ )
21
+ from ...models.responses.subnet import SubnetNodeInfo
22
+ from ...utils.blockchain import validate_ethereum_address
23
+ from ...utils.logging import get_logger
24
+ from .base import BaseExtrinsicClient
25
+ from .subnet import _encode_multiaddr
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ # Simple response wrapper for methods that don't have specific response models
31
+ class SimpleExtrinsicResponse:
32
+ """Simple wrapper for extrinsic responses."""
33
+
34
+ def __init__(self, response_dict: dict):
35
+ self.success = response_dict.get("success", False)
36
+ self.error = response_dict.get("error")
37
+ self.transaction_hash = response_dict.get("extrinsic_hash")
38
+ self.block_number = response_dict.get("block_number")
39
+ self.block_hash = response_dict.get("block_hash")
40
+ self.message = response_dict.get("message")
41
+ self.subnet_id = response_dict.get("subnet_id")
42
+ self.validator_id = response_dict.get("validator_id")
43
+ self.subnet_node_id = response_dict.get("subnet_node_id")
44
+ self.hotkey = response_dict.get("hotkey")
45
+ self.stake_balance = response_dict.get("stake_balance")
46
+
47
+
48
+ class NodeExtrinsics(BaseExtrinsicClient):
49
+ """Client for subnet node-related extrinsics."""
50
+
51
+ def __init__(self, substrate: Optional[SubstrateInterface] = None):
52
+ """Initialize the subnet node client."""
53
+ super().__init__(substrate)
54
+
55
+ # ============================================================================
56
+ # SUBNET NODE REGISTRATION
57
+ # ============================================================================
58
+
59
+ def register_subnet_node(
60
+ self, request: SubnetNodeRegisterRequest, keypair: Keypair
61
+ ) -> SimpleExtrinsicResponse:
62
+ """
63
+ Register a new subnet node.
64
+
65
+ Args:
66
+ request: Subnet node registration request
67
+ keypair: Keypair for signing the transaction
68
+
69
+ Returns:
70
+ SimpleExtrinsicResponse with operation result
71
+ """
72
+ try:
73
+ logger.info(f"Registering subnet node for subnet {request.subnet_id}")
74
+
75
+ # Validate inputs
76
+ self._validate_node_registration_request(request)
77
+
78
+ def peer_info(peer_id: str, multiaddr: Optional[str]) -> dict[str, Any]:
79
+ encoded_multiaddr = None
80
+ if multiaddr:
81
+ encoded_multiaddr = _encode_multiaddr(
82
+ f"{multiaddr.rstrip('/')}/p2p/{peer_id}"
83
+ )
84
+ return {
85
+ "peer_id": peer_id,
86
+ "multiaddr": encoded_multiaddr,
87
+ }
88
+
89
+ # Prepare call parameters using the live validator-runtime metadata names.
90
+ call_params = {
91
+ "validator_id": request.validator_id,
92
+ "subnet_id": request.subnet_id,
93
+ "hotkey": request.hotkey,
94
+ "peer_info": peer_info(request.peer_id, request.bootnode),
95
+ "bootnode_peer_info": peer_info(
96
+ request.bootnode_peer_id, request.bootnode
97
+ ),
98
+ "client_peer_info": peer_info(request.client_peer_id, None),
99
+ "stake_to_be_added": request.stake_to_be_added,
100
+ "unique": request.unique,
101
+ "non_unique": request.non_unique,
102
+ "max_burn_amount": request.max_burn_amount,
103
+ }
104
+
105
+ call = self.substrate.compose_call(
106
+ call_module="Network",
107
+ call_function="register_subnet_node",
108
+ call_params=call_params,
109
+ )
110
+
111
+ result = self._submit_extrinsic(call, keypair)
112
+
113
+ if result["success"]:
114
+ # Extract node ID from events
115
+ for event in result.get("events", []):
116
+ if hasattr(event, "value"):
117
+ module_id = event.value.get("module_id", "")
118
+ event_id = event.value.get("event_id", "")
119
+
120
+ # Node registration with stake triggers SubnetNodeActivated event
121
+ if module_id == "Network" and event_id in [
122
+ "SubnetNodeRegistered",
123
+ "SubnetNodeActivated",
124
+ ]:
125
+ attributes = event.value.get("attributes", {})
126
+
127
+ # Handle both dict and list formats
128
+ if isinstance(attributes, dict):
129
+ result["subnet_node_id"] = attributes.get(
130
+ "subnet_node_id"
131
+ )
132
+ result["subnet_id"] = attributes.get(
133
+ "subnet_id", request.subnet_id
134
+ )
135
+ result["validator_id"] = attributes.get(
136
+ "validator_id", request.validator_id
137
+ )
138
+ result["hotkey"] = attributes.get("hotkey")
139
+ elif (
140
+ isinstance(attributes, (list, tuple))
141
+ and len(attributes) >= 2
142
+ ):
143
+ result["subnet_id"] = attributes[0]
144
+ result["subnet_node_id"] = attributes[1]
145
+ if len(attributes) > 2:
146
+ result["hotkey"] = attributes[2]
147
+ break
148
+
149
+ # Add stake balance and hotkey from request if not found in events
150
+ if "validator_id" not in result:
151
+ result["validator_id"] = request.validator_id
152
+ if "stake_balance" not in result:
153
+ result["stake_balance"] = request.stake_to_be_added
154
+ if "hotkey" not in result or not result["hotkey"]:
155
+ result["hotkey"] = request.hotkey
156
+
157
+ result["message"] = (
158
+ f"Subnet node registered successfully for subnet {request.subnet_id}"
159
+ )
160
+
161
+ return SimpleExtrinsicResponse(result)
162
+
163
+ except Exception as e:
164
+ logger.error(f"Error registering subnet node: {e}")
165
+ return SimpleExtrinsicResponse({"success": False, "error": str(e)})
166
+
167
+ # ============================================================================
168
+ # SUBNET NODE ACTIVATION
169
+ # ============================================================================
170
+
171
+ def activate_subnet_node(
172
+ self, request: SubnetNodeActivateRequest, keypair: Keypair
173
+ ) -> SimpleExtrinsicResponse:
174
+ """
175
+ Activate a subnet node.
176
+
177
+ Args:
178
+ request: Subnet node activation request
179
+ keypair: Keypair for signing the transaction
180
+
181
+ Returns:
182
+ Dictionary with operation result
183
+ """
184
+ try:
185
+ logger.info(
186
+ f"Activating subnet {request.subnet_id} node {request.subnet_node_id}"
187
+ )
188
+
189
+ call = self.substrate.compose_call(
190
+ call_module="Network",
191
+ call_function="activate_subnet_node",
192
+ call_params={
193
+ "subnet_id": request.subnet_id,
194
+ "subnet_node_id": request.subnet_node_id,
195
+ },
196
+ )
197
+
198
+ result = self._submit_extrinsic(call, keypair)
199
+
200
+ if result["success"]:
201
+ result["message"] = (
202
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} activated successfully"
203
+ )
204
+ result["subnet_id"] = request.subnet_id
205
+ result["subnet_node_id"] = request.subnet_node_id
206
+
207
+ return SimpleExtrinsicResponse(result)
208
+
209
+ except Exception as e:
210
+ logger.error(f"Error activating subnet node: {e}")
211
+ return SimpleExtrinsicResponse({"success": False, "error": str(e)})
212
+
213
+ # ============================================================================
214
+ # SUBNET NODE PAUSE/REACTIVATE
215
+ # ============================================================================
216
+
217
+ def pause_subnet_node(
218
+ self, request: SubnetNodePauseRequest, keypair: Keypair
219
+ ) -> dict[str, Any]:
220
+ """
221
+ Pause a subnet node.
222
+
223
+ Args:
224
+ request: Subnet node pause request
225
+ keypair: Keypair for signing the transaction
226
+
227
+ Returns:
228
+ Dictionary with operation result
229
+ """
230
+ try:
231
+ logger.info(
232
+ f"Pausing subnet {request.subnet_id} node {request.subnet_node_id}"
233
+ )
234
+
235
+ call = self.substrate.compose_call(
236
+ call_module="Network",
237
+ call_function="pause_subnet_node",
238
+ call_params={
239
+ "subnet_id": request.subnet_id,
240
+ "subnet_node_id": request.subnet_node_id,
241
+ },
242
+ )
243
+
244
+ response = self._submit_extrinsic(call, keypair)
245
+
246
+ if response["success"]:
247
+ response["message"] = (
248
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} paused successfully"
249
+ )
250
+
251
+ return response
252
+
253
+ except Exception as e:
254
+ logger.error(f"Error pausing subnet node: {e}")
255
+ return {"success": False, "error": str(e)}
256
+
257
+ def reactivate_subnet_node(
258
+ self, request: SubnetNodeReactivateRequest, keypair: Keypair
259
+ ) -> dict[str, Any]:
260
+ """
261
+ Reactivate a subnet node.
262
+
263
+ Args:
264
+ request: Subnet node reactivation request
265
+ keypair: Keypair for signing the transaction
266
+
267
+ Returns:
268
+ Dictionary with operation result
269
+ """
270
+ try:
271
+ logger.info(
272
+ f"Reactivating subnet {request.subnet_id} node {request.subnet_node_id}"
273
+ )
274
+
275
+ call = self.substrate.compose_call(
276
+ call_module="Network",
277
+ call_function="reactivate_subnet_node",
278
+ call_params={
279
+ "subnet_id": request.subnet_id,
280
+ "subnet_node_id": request.subnet_node_id,
281
+ },
282
+ )
283
+
284
+ response = self._submit_extrinsic(call, keypair)
285
+
286
+ if response["success"]:
287
+ response["message"] = (
288
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} reactivated successfully"
289
+ )
290
+
291
+ return response
292
+
293
+ except Exception as e:
294
+ logger.error(f"Error reactivating subnet node: {e}")
295
+ return {"success": False, "error": str(e)}
296
+
297
+ # ============================================================================
298
+ # SUBNET NODE REMOVAL
299
+ # ============================================================================
300
+
301
+ def remove_subnet_node(
302
+ self, request: SubnetNodeRemoveRequest, keypair: Keypair
303
+ ) -> dict[str, Any]:
304
+ """
305
+ Remove a subnet node.
306
+
307
+ Args:
308
+ request: Subnet node removal request
309
+ keypair: Keypair for signing the transaction
310
+
311
+ Returns:
312
+ Dictionary with operation result
313
+ """
314
+ try:
315
+ logger.info(
316
+ f"Removing subnet {request.subnet_id} node {request.subnet_node_id}"
317
+ )
318
+
319
+ call = self.substrate.compose_call(
320
+ call_module="Network",
321
+ call_function="remove_subnet_node",
322
+ call_params={
323
+ "subnet_id": request.subnet_id,
324
+ "subnet_node_id": request.subnet_node_id,
325
+ },
326
+ )
327
+
328
+ response = self._submit_extrinsic(call, keypair)
329
+
330
+ if response["success"]:
331
+ response["message"] = (
332
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} removed successfully"
333
+ )
334
+
335
+ return response
336
+
337
+ except Exception as e:
338
+ logger.error(f"Error removing subnet node: {e}")
339
+ return {"success": False, "error": str(e)}
340
+
341
+ # ============================================================================
342
+ # SUBNET NODE PARAMETER UPDATES
343
+ # ============================================================================
344
+
345
+ def update_subnet_node_parameters(
346
+ self,
347
+ subnet_id: int,
348
+ subnet_node_id: int,
349
+ *,
350
+ peer_id: Optional[str] = None,
351
+ bootnode: Optional[str] = None,
352
+ bootnode_peer_id: Optional[str] = None,
353
+ client_peer_id: Optional[str] = None,
354
+ delegate_reward_rate: Optional[int] = None,
355
+ unique: Optional[str] = None,
356
+ non_unique: Optional[str] = None,
357
+ keypair: Keypair,
358
+ ) -> list[dict[str, Any]]:
359
+ """
360
+ Perform one or more subnet node parameter updates in sequence.
361
+
362
+ Returns a list of result dicts, each tagged with the logical field name.
363
+ """
364
+ results: list[dict[str, Any]] = []
365
+
366
+ # Main peer ID
367
+ if peer_id is not None:
368
+ try:
369
+ logger.info(
370
+ f"Updating peer ID for subnet {subnet_id} node {subnet_node_id}"
371
+ )
372
+ request = SubnetNodePeerIdUpdateRequest(
373
+ subnet_id=subnet_id,
374
+ subnet_node_id=subnet_node_id,
375
+ new_peer_id=peer_id,
376
+ )
377
+ result = self.update_subnet_node_peer_id(request, keypair)
378
+ except Exception as e:
379
+ logger.error(f"Error updating subnet node peer ID: {e}")
380
+ result = {"success": False, "error": str(e)}
381
+ results.append({"field": "peer_id", "result": result})
382
+
383
+ # Bootnode multiaddress
384
+ if bootnode is not None:
385
+ try:
386
+ logger.info(
387
+ f"Updating bootnode for subnet {subnet_id} node {subnet_node_id}"
388
+ )
389
+ request = SubnetNodeBootnodeUpdateRequest(
390
+ subnet_id=subnet_id,
391
+ subnet_node_id=subnet_node_id,
392
+ new_bootnode=bootnode,
393
+ )
394
+ result = self.update_subnet_node_bootnode(request, keypair)
395
+ except Exception as e:
396
+ logger.error(f"Error updating subnet node bootnode: {e}")
397
+ result = {"success": False, "error": str(e)}
398
+ results.append({"field": "bootnode", "result": result})
399
+
400
+ # Bootnode peer ID
401
+ if bootnode_peer_id is not None:
402
+ try:
403
+ logger.info(
404
+ f"Updating bootnode peer ID for subnet {subnet_id} node {subnet_node_id}"
405
+ )
406
+ request = SubnetNodeBootnodePeerIdUpdateRequest(
407
+ subnet_id=subnet_id,
408
+ subnet_node_id=subnet_node_id,
409
+ new_bootnode_peer_id=bootnode_peer_id,
410
+ )
411
+ result = self.update_subnet_node_bootnode_peer_id(request, keypair)
412
+ except Exception as e:
413
+ logger.error(f"Error updating subnet node bootnode peer ID: {e}")
414
+ result = {"success": False, "error": str(e)}
415
+ results.append({"field": "bootnode_peer_id", "result": result})
416
+
417
+ # Client peer ID
418
+ if client_peer_id is not None:
419
+ try:
420
+ logger.info(
421
+ f"Updating client peer ID for subnet {subnet_id} node {subnet_node_id}"
422
+ )
423
+ request = SubnetNodeClientPeerIdUpdateRequest(
424
+ subnet_id=subnet_id,
425
+ subnet_node_id=subnet_node_id,
426
+ new_client_peer_id=client_peer_id,
427
+ )
428
+ result = self.update_subnet_node_client_peer_id(request, keypair)
429
+ except Exception as e:
430
+ logger.error(f"Error updating subnet node client peer ID: {e}")
431
+ result = {"success": False, "error": str(e)}
432
+ results.append({"field": "client_peer_id", "result": result})
433
+
434
+ # Delegate reward rate
435
+ if delegate_reward_rate is not None:
436
+ try:
437
+ logger.info(
438
+ f"Updating delegate reward rate for subnet {subnet_id} node {subnet_node_id}"
439
+ )
440
+ request = SubnetNodeUpdateRequest(
441
+ subnet_id=subnet_id,
442
+ subnet_node_id=subnet_node_id,
443
+ value=str(delegate_reward_rate),
444
+ )
445
+ result = self.update_subnet_node_delegate_reward_rate(request, keypair)
446
+ except Exception as e:
447
+ logger.error(f"Error updating subnet node delegate reward rate: {e}")
448
+ result = {"success": False, "error": str(e)}
449
+ results.append({"field": "delegate_reward_rate", "result": result})
450
+
451
+ # Unique metadata
452
+ if unique is not None:
453
+ try:
454
+ logger.info(
455
+ f"Updating unique metadata for subnet {subnet_id} node {subnet_node_id}"
456
+ )
457
+ request = SubnetNodeUpdateRequest(
458
+ subnet_id=subnet_id,
459
+ subnet_node_id=subnet_node_id,
460
+ value=unique,
461
+ )
462
+ result = self.update_subnet_node_unique(request, keypair)
463
+ except Exception as e:
464
+ logger.error(f"Error updating subnet node unique metadata: {e}")
465
+ result = {"success": False, "error": str(e)}
466
+ results.append({"field": "unique", "result": result})
467
+
468
+ # Non-unique metadata
469
+ if non_unique is not None:
470
+ try:
471
+ logger.info(
472
+ f"Updating non-unique metadata for subnet {subnet_id} node {subnet_node_id}"
473
+ )
474
+ request = SubnetNodeUpdateRequest(
475
+ subnet_id=subnet_id,
476
+ subnet_node_id=subnet_node_id,
477
+ value=non_unique,
478
+ )
479
+ result = self.update_subnet_node_non_unique(request, keypair)
480
+ except Exception as e:
481
+ logger.error(f"Error updating subnet node non-unique metadata: {e}")
482
+ result = {"success": False, "error": str(e)}
483
+ results.append({"field": "non_unique", "result": result})
484
+
485
+ return results
486
+
487
+ def update_subnet_node_unique(
488
+ self, request: SubnetNodeUpdateRequest, keypair: Keypair
489
+ ) -> dict[str, Any]:
490
+ """
491
+ Update subnet node unique parameter.
492
+
493
+ Args:
494
+ request: Subnet node update request
495
+ keypair: Keypair for signing the transaction
496
+
497
+ Returns:
498
+ Dictionary with operation result
499
+ """
500
+ try:
501
+ logger.info(
502
+ f"Updating subnet {request.subnet_id} node {request.subnet_node_id} unique parameter"
503
+ )
504
+
505
+ call = self.substrate.compose_call(
506
+ call_module="Network",
507
+ call_function="update_subnet_node_unique",
508
+ call_params={
509
+ "subnet_id": request.subnet_id,
510
+ "subnet_node_id": request.subnet_node_id,
511
+ "value": request.value.encode("utf-8") if request.value else None,
512
+ },
513
+ )
514
+
515
+ response = self._submit_extrinsic(call, keypair)
516
+
517
+ if response["success"]:
518
+ response["message"] = (
519
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} unique parameter updated successfully"
520
+ )
521
+
522
+ return response
523
+
524
+ except Exception as e:
525
+ logger.error(f"Error updating subnet node unique parameter: {e}")
526
+ return {"success": False, "error": str(e)}
527
+
528
+ def update_subnet_node_non_unique(
529
+ self, request: SubnetNodeUpdateRequest, keypair: Keypair
530
+ ) -> dict[str, Any]:
531
+ """
532
+ Update subnet node non-unique parameter.
533
+
534
+ Args:
535
+ request: Subnet node update request
536
+ keypair: Keypair for signing the transaction
537
+
538
+ Returns:
539
+ Dictionary with operation result
540
+ """
541
+ try:
542
+ logger.info(
543
+ f"Updating subnet {request.subnet_id} node {request.subnet_node_id} non-unique parameter"
544
+ )
545
+
546
+ call = self.substrate.compose_call(
547
+ call_module="Network",
548
+ call_function="update_subnet_node_non_unique",
549
+ call_params={
550
+ "subnet_id": request.subnet_id,
551
+ "subnet_node_id": request.subnet_node_id,
552
+ "value": request.value.encode("utf-8") if request.value else None,
553
+ },
554
+ )
555
+
556
+ response = self._submit_extrinsic(call, keypair)
557
+
558
+ if response["success"]:
559
+ response["message"] = (
560
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} non-unique parameter updated successfully"
561
+ )
562
+
563
+ return response
564
+
565
+ except Exception as e:
566
+ logger.error(f"Error updating subnet node non-unique parameter: {e}")
567
+ return {"success": False, "error": str(e)}
568
+
569
+ def update_subnet_node_peer_id(
570
+ self, request: SubnetNodePeerIdUpdateRequest, keypair: Keypair
571
+ ) -> dict[str, Any]:
572
+ """
573
+ Update subnet node peer ID.
574
+
575
+ Args:
576
+ request: Subnet node peer ID update request
577
+ keypair: Keypair for signing the transaction
578
+
579
+ Returns:
580
+ Dictionary with operation result
581
+ """
582
+ try:
583
+ logger.info(
584
+ f"Updating subnet {request.subnet_id} node {request.subnet_node_id} peer ID"
585
+ )
586
+
587
+ # peer_id passed as string, substrate-interface handles encoding
588
+ call = self.substrate.compose_call(
589
+ call_module="Network",
590
+ call_function="update_subnet_node_peer_id",
591
+ call_params={
592
+ "subnet_id": request.subnet_id,
593
+ "subnet_node_id": request.subnet_node_id,
594
+ "new_peer_id": request.new_peer_id, # String, not encoded
595
+ },
596
+ )
597
+
598
+ response = self._submit_extrinsic(call, keypair)
599
+
600
+ if response["success"]:
601
+ response["message"] = (
602
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} peer ID updated successfully"
603
+ )
604
+
605
+ return response
606
+
607
+ except Exception as e:
608
+ logger.error(f"Error updating subnet node peer ID: {e}")
609
+ return {"success": False, "error": str(e)}
610
+
611
+ def update_subnet_node_bootnode(
612
+ self, request: SubnetNodeBootnodeUpdateRequest, keypair: Keypair
613
+ ) -> dict[str, Any]:
614
+ """
615
+ Update subnet node bootnode.
616
+
617
+ Args:
618
+ request: Subnet node bootnode update request
619
+ keypair: Keypair for signing the transaction
620
+
621
+ Returns:
622
+ Dictionary with operation result
623
+ """
624
+ try:
625
+ logger.info(
626
+ f"Updating subnet {request.subnet_id} node {request.subnet_node_id} bootnode"
627
+ )
628
+
629
+ call = self.substrate.compose_call(
630
+ call_module="Network",
631
+ call_function="update_subnet_node_bootnode",
632
+ call_params={
633
+ "subnet_id": request.subnet_id,
634
+ "subnet_node_id": request.subnet_node_id,
635
+ "new_bootnode": (
636
+ request.new_bootnode.encode("utf-8")
637
+ if request.new_bootnode
638
+ else None
639
+ ),
640
+ },
641
+ )
642
+
643
+ response = self._submit_extrinsic(call, keypair)
644
+
645
+ if response["success"]:
646
+ response["message"] = (
647
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} bootnode updated successfully"
648
+ )
649
+
650
+ return response
651
+
652
+ except Exception as e:
653
+ logger.error(f"Error updating subnet node bootnode: {e}")
654
+ return {"success": False, "error": str(e)}
655
+
656
+ def update_subnet_node_bootnode_peer_id(
657
+ self, request: SubnetNodeBootnodePeerIdUpdateRequest, keypair: Keypair
658
+ ) -> dict[str, Any]:
659
+ """
660
+ Update subnet node bootnode peer ID.
661
+
662
+ Args:
663
+ request: Subnet node bootnode peer ID update request
664
+ keypair: Keypair for signing the transaction
665
+
666
+ Returns:
667
+ Dictionary with operation result
668
+ """
669
+ try:
670
+ logger.info(
671
+ f"Updating subnet {request.subnet_id} node {request.subnet_node_id} bootnode peer ID"
672
+ )
673
+
674
+ # peer_id passed as string, substrate-interface handles encoding
675
+ call = self.substrate.compose_call(
676
+ call_module="Network",
677
+ call_function="update_bootnode_peer_id",
678
+ call_params={
679
+ "subnet_id": request.subnet_id,
680
+ "subnet_node_id": request.subnet_node_id,
681
+ "new_bootnode_peer_id": request.new_bootnode_peer_id, # String, not encoded
682
+ },
683
+ )
684
+
685
+ response = self._submit_extrinsic(call, keypair)
686
+
687
+ if response["success"]:
688
+ response["message"] = (
689
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} bootnode peer ID updated successfully"
690
+ )
691
+
692
+ return response
693
+
694
+ except Exception as e:
695
+ logger.error(f"Error updating subnet node bootnode peer ID: {e}")
696
+ return {"success": False, "error": str(e)}
697
+
698
+ def update_subnet_node_client_peer_id(
699
+ self, request: SubnetNodeClientPeerIdUpdateRequest, keypair: Keypair
700
+ ) -> dict[str, Any]:
701
+ """
702
+ Update subnet node client peer ID.
703
+
704
+ Args:
705
+ request: Subnet node client peer ID update request
706
+ keypair: Keypair for signing the transaction
707
+
708
+ Returns:
709
+ Dictionary with operation result
710
+ """
711
+ try:
712
+ logger.info(
713
+ f"Updating subnet {request.subnet_id} node {request.subnet_node_id} client peer ID"
714
+ )
715
+
716
+ # peer_id passed as string, substrate-interface handles encoding
717
+ call = self.substrate.compose_call(
718
+ call_module="Network",
719
+ call_function="update_client_peer_id",
720
+ call_params={
721
+ "subnet_id": request.subnet_id,
722
+ "subnet_node_id": request.subnet_node_id,
723
+ "new_client_peer_id": request.new_client_peer_id, # String, not encoded
724
+ },
725
+ )
726
+
727
+ response = self._submit_extrinsic(call, keypair)
728
+
729
+ if response["success"]:
730
+ response["message"] = (
731
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} client peer ID updated successfully"
732
+ )
733
+
734
+ return response
735
+
736
+ except Exception as e:
737
+ logger.error(f"Error updating subnet node client peer ID: {e}")
738
+ return {"success": False, "error": str(e)}
739
+
740
+ def update_subnet_node_delegate_reward_rate(
741
+ self, request: SubnetNodeUpdateRequest, keypair: Keypair
742
+ ) -> dict[str, Any]:
743
+ """
744
+ Update subnet node delegate reward rate.
745
+
746
+ Args:
747
+ request: Subnet node update request (value should be the new reward rate)
748
+ keypair: Keypair for signing the transaction
749
+
750
+ Returns:
751
+ Dictionary with operation result
752
+ """
753
+ try:
754
+ logger.info(
755
+ f"Updating subnet {request.subnet_id} node {request.subnet_node_id} delegate reward rate"
756
+ )
757
+
758
+ # Convert string value to int for reward rate
759
+ reward_rate = int(request.value) if request.value else 0
760
+
761
+ call = self.substrate.compose_call(
762
+ call_module="Network",
763
+ call_function="update_subnet_node_delegate_reward_rate",
764
+ call_params={
765
+ "subnet_id": request.subnet_id,
766
+ "subnet_node_id": request.subnet_node_id,
767
+ "value": reward_rate,
768
+ },
769
+ )
770
+
771
+ response = self._submit_extrinsic(call, keypair)
772
+
773
+ if response["success"]:
774
+ response["message"] = (
775
+ f"Subnet {request.subnet_id} node {request.subnet_node_id} delegate reward rate updated successfully"
776
+ )
777
+
778
+ return response
779
+
780
+ except Exception as e:
781
+ logger.error(f"Error updating subnet node delegate reward rate: {e}")
782
+ return {"success": False, "error": str(e)}
783
+
784
+ # ============================================================================
785
+ # QUERY METHODS
786
+ # ============================================================================
787
+
788
+ def get_subnet_node_info(
789
+ self, subnet_id: int, subnet_node_id: int
790
+ ) -> Optional[SubnetNodeInfo]:
791
+ """
792
+ Get information about a specific subnet node.
793
+
794
+ Args:
795
+ subnet_id: The subnet ID
796
+ subnet_node_id: The subnet node ID
797
+
798
+ Returns:
799
+ SubnetNodeInfo if found, None otherwise
800
+ """
801
+ try:
802
+ # Query subnet node data
803
+ node_data = self.substrate.query(
804
+ module="Network",
805
+ storage_function="SubnetNodesData",
806
+ params=[subnet_id, subnet_node_id],
807
+ )
808
+
809
+ if not node_data.value:
810
+ return None
811
+
812
+ # Query additional information
813
+ stake_balance = (
814
+ self.substrate.query(
815
+ module="Network",
816
+ storage_function="SubnetNodeStake",
817
+ params=[subnet_id, subnet_node_id],
818
+ ).value
819
+ or 0
820
+ )
821
+
822
+ node_delegate_stake_balance = (
823
+ self.substrate.query(
824
+ module="Network",
825
+ storage_function="SubnetNodeDelegateStake",
826
+ params=[subnet_id, subnet_node_id],
827
+ ).value
828
+ or 0
829
+ )
830
+
831
+ penalties = (
832
+ self.substrate.query(
833
+ module="Network",
834
+ storage_function="SubnetNodePenalties",
835
+ params=[subnet_id, subnet_node_id],
836
+ ).value
837
+ or 0
838
+ )
839
+
840
+ # Query coldkey
841
+ coldkey = self.substrate.query(
842
+ module="Network",
843
+ storage_function="SubnetNodeColdkey",
844
+ params=[subnet_id, subnet_node_id],
845
+ ).value
846
+
847
+ # Query identity data
848
+ identity = self.substrate.query(
849
+ module="Network", storage_function="Identity", params=[coldkey]
850
+ ).value
851
+
852
+ # Query reputation
853
+ reputation = self.substrate.query(
854
+ module="Network", storage_function="Reputation", params=[coldkey]
855
+ ).value
856
+
857
+ return SubnetNodeInfo(
858
+ subnet_id=subnet_id,
859
+ subnet_node_id=subnet_node_id,
860
+ coldkey=coldkey,
861
+ hotkey=node_data.value["hotkey"],
862
+ peer_id=node_data.value["peer_id"].decode("utf-8"),
863
+ bootnode_peer_id=node_data.value["bootnode_peer_id"].decode("utf-8"),
864
+ client_peer_id=node_data.value["client_peer_id"].decode("utf-8"),
865
+ bootnode=(
866
+ node_data.value.get("bootnode", {}).decode("utf-8")
867
+ if node_data.value.get("bootnode")
868
+ else None
869
+ ),
870
+ identity=identity,
871
+ classification=node_data.value["classification"].name,
872
+ delegate_reward_rate=node_data.value.get("delegate_reward_rate", 0),
873
+ last_delegate_reward_rate_update=node_data.value.get(
874
+ "last_delegate_reward_rate_update", 0
875
+ ),
876
+ unique=(
877
+ node_data.value.get("unique", {}).decode("utf-8")
878
+ if node_data.value.get("unique")
879
+ else None
880
+ ),
881
+ non_unique=(
882
+ node_data.value.get("non_unique", {}).decode("utf-8")
883
+ if node_data.value.get("non_unique")
884
+ else None
885
+ ),
886
+ stake_balance=stake_balance,
887
+ node_delegate_stake_balance=node_delegate_stake_balance,
888
+ penalties=penalties,
889
+ reputation=reputation,
890
+ )
891
+
892
+ except Exception as e:
893
+ logger.error(f"Error getting subnet node info: {e}")
894
+ return None
895
+
896
+ def list_subnet_nodes(self, subnet_id: int) -> list[SubnetNodeInfo]:
897
+ """
898
+ List all nodes for a specific subnet.
899
+
900
+ Args:
901
+ subnet_id: The subnet ID
902
+
903
+ Returns:
904
+ List of SubnetNodeInfo objects
905
+ """
906
+ try:
907
+ nodes = []
908
+
909
+ # Get total node count for this subnet
910
+ total_nodes = (
911
+ self.substrate.query(
912
+ module="Network",
913
+ storage_function="TotalSubnetNodes",
914
+ params=[subnet_id],
915
+ ).value
916
+ or 0
917
+ )
918
+
919
+ # Query each node
920
+ for node_id in range(1, total_nodes + 1):
921
+ node_info = self.get_subnet_node_info(subnet_id, node_id)
922
+ if node_info:
923
+ nodes.append(node_info)
924
+
925
+ return nodes
926
+
927
+ except Exception as e:
928
+ logger.error(f"Error listing subnet nodes: {e}")
929
+ return []
930
+
931
+ # ============================================================================
932
+ # PRIVATE HELPER METHODS
933
+ # ============================================================================
934
+
935
+ def _validate_node_registration_request(self, request: SubnetNodeRegisterRequest):
936
+ """Validate the node registration request."""
937
+ if request.validator_id < 0:
938
+ raise ValueError("Validator ID must be non-negative")
939
+
940
+ # Validate account IDs
941
+ validate_ethereum_address(request.hotkey)
942
+
943
+ # Validate peer IDs
944
+ if not request.peer_id or not request.peer_id.strip():
945
+ raise ValueError("Peer ID cannot be empty")
946
+ if not request.bootnode_peer_id or not request.bootnode_peer_id.strip():
947
+ raise ValueError("Bootnode peer ID cannot be empty")
948
+ if not request.client_peer_id or not request.client_peer_id.strip():
949
+ raise ValueError("Client peer ID cannot be empty")
950
+
951
+ # Validate stake amount
952
+ if request.stake_to_be_added <= 0:
953
+ raise ValueError("Stake amount must be positive")
954
+
955
+ def _submit_extrinsic(self, call, keypair: Keypair) -> dict[str, Any]:
956
+ """Submit an extrinsic and return the result."""
957
+ try:
958
+ extrinsic = self.substrate.create_signed_extrinsic(
959
+ call=call,
960
+ keypair=keypair,
961
+ era={"period": 64}, # 64 block era
962
+ )
963
+
964
+ response = self.substrate.submit_extrinsic(
965
+ extrinsic, wait_for_inclusion=True, wait_for_finalization=True
966
+ )
967
+
968
+ if response.is_success:
969
+ # Extract extrinsic_hash with defensive handling
970
+ try:
971
+ extrinsic_hash = response.extrinsic_hash
972
+ if isinstance(extrinsic_hash, bytes):
973
+ extrinsic_hash = "0x" + extrinsic_hash.hex()
974
+ elif extrinsic_hash and not isinstance(extrinsic_hash, str):
975
+ extrinsic_hash = str(extrinsic_hash)
976
+ except (AttributeError, Exception):
977
+ extrinsic_hash = getattr(response, "extrinsic_hash", None)
978
+ if extrinsic_hash and isinstance(extrinsic_hash, bytes):
979
+ extrinsic_hash = "0x" + extrinsic_hash.hex()
980
+
981
+ # Extract block_hash with defensive handling
982
+ try:
983
+ block_hash = response.block_hash
984
+ if isinstance(block_hash, bytes):
985
+ block_hash = "0x" + block_hash.hex()
986
+ elif block_hash and not isinstance(block_hash, str):
987
+ block_hash = str(block_hash)
988
+ except (AttributeError, Exception):
989
+ block_hash = getattr(response, "block_hash", None)
990
+ if block_hash and isinstance(block_hash, bytes):
991
+ block_hash = "0x" + block_hash.hex()
992
+
993
+ # Extract block_number with defensive handling
994
+ try:
995
+ block_number = response.block_number
996
+ except (AttributeError, Exception):
997
+ block_number = getattr(response, "block_number", None)
998
+
999
+ # If block_number is not available but we have block_hash, query it
1000
+ if block_number is None and block_hash:
1001
+ try:
1002
+ block_number = self.substrate.get_block_number(
1003
+ block_hash=block_hash
1004
+ )
1005
+ except Exception as e:
1006
+ logger.debug(f"Could not get block number from block hash: {e}")
1007
+ block_number = None
1008
+
1009
+ # Extract events
1010
+ try:
1011
+ events = response.triggered_events
1012
+ except (AttributeError, Exception):
1013
+ events = getattr(response, "triggered_events", []) or getattr(
1014
+ response, "events", []
1015
+ )
1016
+
1017
+ return {
1018
+ "success": True,
1019
+ "extrinsic_hash": extrinsic_hash,
1020
+ "transaction_hash": extrinsic_hash, # Alias for compatibility
1021
+ "block_hash": block_hash,
1022
+ "block_number": block_number,
1023
+ "events": events,
1024
+ }
1025
+ else:
1026
+ error_msg = self._extract_error_message(response)
1027
+ return {"success": False, "error": error_msg}
1028
+
1029
+ except Exception as e:
1030
+ logger.error(f"Error submitting extrinsic: {e}")
1031
+ return {"success": False, "error": str(e)}
1032
+
1033
+ def _extract_error_message(self, response) -> str:
1034
+ """Extract error message from failed response."""
1035
+ try:
1036
+ if response.error_message:
1037
+ error_msg = response.error_message
1038
+ if isinstance(error_msg, dict):
1039
+ return error_msg.get("name", str(error_msg))
1040
+ return str(error_msg)
1041
+
1042
+ for event in response.triggered_events:
1043
+ if (
1044
+ event.value["module_id"] == "System"
1045
+ and event.value["event_id"] == "ExtrinsicFailed"
1046
+ ):
1047
+ error = event.value["attributes"][0]
1048
+ if hasattr(error, "name"):
1049
+ return error.name
1050
+ return str(error)
1051
+ except Exception as e:
1052
+ logger.warning(f"Could not extract error message: {e}")
1053
+
1054
+ return "Unknown error occurred"