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,783 @@
1
+ """
2
+ RPC client for subnet node-related queries using scalecodec for decoding.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from substrateinterface import SubstrateInterface
8
+
9
+ from ...models.responses import SubnetNodeInfo
10
+ from ...utils.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class SubnetNodeRpcClient:
16
+ """RPC client for subnet node-related queries."""
17
+
18
+ def __init__(self, substrate: Optional[SubstrateInterface] = None):
19
+ """Initialize the subnet node RPC client."""
20
+ self.substrate = substrate
21
+
22
+ def get_subnet_node_info(
23
+ self, subnet_id: int, subnet_node_id: int
24
+ ) -> Optional[SubnetNodeInfo]:
25
+ """
26
+ Get subnet node information using custom RPC method with full SCALE decoding.
27
+
28
+ Args:
29
+ subnet_id: The subnet ID
30
+ subnet_node_id: The subnet node ID
31
+
32
+ Returns:
33
+ SubnetNodeInfo with all fields decoded, or None if not found
34
+
35
+ Reference:
36
+ - mesh-template chain_functions.py lines 1843-1864, 2090-2107
37
+ - hypertensor-evm/pallets/network/src/lib.rs lines 1184-1203
38
+ """
39
+ # Check if substrate connection exists
40
+ if self.substrate is None:
41
+ logger.error("Cannot get subnet node info: not connected to blockchain")
42
+ raise RuntimeError("Not connected to blockchain")
43
+
44
+ try:
45
+ # Call RPC method
46
+ result = self.substrate.rpc_request(
47
+ method="network_getSubnetNodeInfo", params=[subnet_id, subnet_node_id]
48
+ )
49
+
50
+ if not result or not result.get("result"):
51
+ return None
52
+
53
+ result_data = result["result"]
54
+
55
+ # Decode Option<SubnetNodeInfo> using custom type registry
56
+ from ...utils.blockchain.type_registry import decode_option_subnet_node_info
57
+
58
+ decoded = decode_option_subnet_node_info(result_data)
59
+
60
+ if decoded is None:
61
+ return None
62
+
63
+ # Convert decoded dict to SubnetNodeInfo model
64
+ def to_bytes(val):
65
+ """Convert decoded value to bytes for peer IDs and other byte fields."""
66
+ if isinstance(val, bytes):
67
+ return val
68
+ if isinstance(val, list):
69
+ # Vec<u8> from SCALE decoding is a list of integers (0-255)
70
+ # Convert to bytes
71
+ try:
72
+ return bytes(val)
73
+ except (ValueError, TypeError):
74
+ # If conversion fails, try to convert each element
75
+ return bytes(int(b) for b in val if 0 <= int(b) <= 255)
76
+ if isinstance(val, str):
77
+ # String peer IDs should be encoded to UTF-8 bytes
78
+ return val.encode("utf-8")
79
+ # Default: return empty bytes
80
+ return b""
81
+
82
+ def to_address(val):
83
+ from ...utils.blockchain.formatting import to_checksum_address
84
+
85
+ if isinstance(val, str):
86
+ raw_addr = val if val.startswith("0x") else "0x" + val
87
+ else:
88
+ raw_addr = "0x" + (
89
+ bytes(val).hex() if isinstance(val, (list, bytes)) else ""
90
+ )
91
+ return to_checksum_address(raw_addr)
92
+
93
+ # Extract classification (it's a dict with node_class and start_epoch)
94
+ classification_data = decoded.get("classification", {})
95
+
96
+ return SubnetNodeInfo(
97
+ subnet_id=decoded["subnet_id"],
98
+ subnet_node_id=decoded["subnet_node_id"],
99
+ coldkey=to_address(decoded["coldkey"]),
100
+ hotkey=to_address(decoded["hotkey"]),
101
+ peer_id=to_bytes(decoded.get("peer_id", b"")),
102
+ bootnode_peer_id=to_bytes(decoded.get("bootnode_peer_id", b"")),
103
+ client_peer_id=to_bytes(decoded.get("client_peer_id", b"")),
104
+ bootnode=(
105
+ to_bytes(decoded.get("bootnode"))
106
+ if decoded.get("bootnode")
107
+ else None
108
+ ),
109
+ identity=decoded.get("identity"),
110
+ classification=classification_data, # Pass dict as-is
111
+ delegate_reward_rate=decoded.get("delegate_reward_rate", 0),
112
+ last_delegate_reward_rate_update=decoded.get(
113
+ "last_delegate_reward_rate_update", 0
114
+ ),
115
+ unique=(
116
+ to_bytes(decoded.get("unique")) if decoded.get("unique") else None
117
+ ),
118
+ non_unique=(
119
+ to_bytes(decoded.get("non_unique"))
120
+ if decoded.get("non_unique")
121
+ else None
122
+ ),
123
+ stake_balance=decoded.get("stake_balance", 0),
124
+ total_node_delegate_stake_shares=decoded.get(
125
+ "total_node_delegate_stake_shares", 0
126
+ ),
127
+ node_delegate_stake_balance=decoded.get(
128
+ "node_delegate_stake_balance", 0
129
+ ),
130
+ coldkey_reputation=decoded.get("coldkey_reputation"),
131
+ subnet_node_reputation=decoded.get("subnet_node_reputation", 0),
132
+ node_slot_index=decoded.get("node_slot_index"),
133
+ consecutive_idle_epochs=decoded.get("consecutive_idle_epochs", 0),
134
+ consecutive_included_epochs=decoded.get(
135
+ "consecutive_included_epochs", 0
136
+ ),
137
+ )
138
+
139
+ except Exception as e:
140
+ logger.error(f"Error getting subnet node info: {e}")
141
+ return None
142
+
143
+ def get_subnet_nodes_info(self, subnet_id: int) -> list[SubnetNodeInfo]:
144
+ """
145
+ Get information about all nodes in a subnet using custom RPC method with full SCALE decoding.
146
+
147
+ Args:
148
+ subnet_id: The subnet ID
149
+
150
+ Returns:
151
+ List of SubnetNodeInfo objects with all fields decoded
152
+
153
+ Reference:
154
+ - mesh-template chain_functions.py lines 1619-1634, 2109-2124
155
+ - hypertensor-evm/pallets/network/src/rpc_info/info.rs lines 148+
156
+ """
157
+ # Check if substrate connection exists
158
+ if self.substrate is None:
159
+ logger.error("Cannot get subnet nodes info: not connected to blockchain")
160
+ raise RuntimeError("Not connected to blockchain")
161
+
162
+ try:
163
+ # Call RPC method
164
+ result = self.substrate.rpc_request(
165
+ method="network_getSubnetNodesInfo", params=[subnet_id]
166
+ )
167
+
168
+ if not result or not result.get("result"):
169
+ return []
170
+
171
+ result_data = result["result"]
172
+
173
+ # Decode Vec<SubnetNodeInfo> using custom type registry
174
+ from ...utils.blockchain.type_registry import decode_vec_subnet_node_info
175
+
176
+ decoded_list = decode_vec_subnet_node_info(result_data)
177
+
178
+ if not decoded_list:
179
+ # Try fallback method when Vec decode returns nothing
180
+ fallback_nodes = self._get_subnet_nodes_info_fallback(subnet_id, [])
181
+ if fallback_nodes:
182
+ return fallback_nodes
183
+ return []
184
+
185
+ # Convert to list of SubnetNodeInfo objects
186
+ def to_bytes(val):
187
+ """Convert decoded value to bytes for peer IDs and other byte fields."""
188
+ if isinstance(val, bytes):
189
+ return val
190
+ if isinstance(val, list):
191
+ # Vec<u8> from SCALE decoding is a list of integers (0-255)
192
+ # Convert to bytes
193
+ try:
194
+ return bytes(val)
195
+ except (ValueError, TypeError):
196
+ # If conversion fails, try to convert each element
197
+ return bytes(int(b) for b in val if 0 <= int(b) <= 255)
198
+ if isinstance(val, str):
199
+ # String peer IDs should be encoded to UTF-8 bytes
200
+ return val.encode("utf-8")
201
+ # Default: return empty bytes
202
+ return b""
203
+
204
+ def to_address(val):
205
+ from ...utils.blockchain.formatting import to_checksum_address
206
+
207
+ if isinstance(val, str):
208
+ raw_addr = val if val.startswith("0x") else "0x" + val
209
+ else:
210
+ raw_addr = "0x" + (
211
+ bytes(val).hex() if isinstance(val, (list, bytes)) else ""
212
+ )
213
+ return to_checksum_address(raw_addr)
214
+
215
+ nodes = []
216
+ for idx, decoded in enumerate(decoded_list):
217
+ try:
218
+ classification_data = decoded.get("classification", {})
219
+
220
+ node_info = SubnetNodeInfo(
221
+ subnet_id=decoded["subnet_id"],
222
+ subnet_node_id=decoded["subnet_node_id"],
223
+ coldkey=to_address(decoded["coldkey"]),
224
+ hotkey=to_address(decoded["hotkey"]),
225
+ peer_id=to_bytes(decoded["peer_id"]),
226
+ bootnode_peer_id=to_bytes(decoded["bootnode_peer_id"]),
227
+ client_peer_id=to_bytes(decoded["client_peer_id"]),
228
+ bootnode=(
229
+ to_bytes(decoded.get("bootnode"))
230
+ if decoded.get("bootnode")
231
+ else None
232
+ ),
233
+ identity=decoded.get("identity"),
234
+ classification=classification_data, # Pass dict as-is
235
+ delegate_reward_rate=decoded.get("delegate_reward_rate", 0),
236
+ last_delegate_reward_rate_update=decoded.get(
237
+ "last_delegate_reward_rate_update", 0
238
+ ),
239
+ unique=(
240
+ to_bytes(decoded.get("unique"))
241
+ if decoded.get("unique")
242
+ else None
243
+ ),
244
+ non_unique=(
245
+ to_bytes(decoded.get("non_unique"))
246
+ if decoded.get("non_unique")
247
+ else None
248
+ ),
249
+ stake_balance=decoded.get("stake_balance", 0),
250
+ node_delegate_stake_balance=decoded.get(
251
+ "node_delegate_stake_balance", 0
252
+ ),
253
+ total_node_delegate_stake_shares=decoded.get(
254
+ "total_node_delegate_stake_shares", 0
255
+ ),
256
+ coldkey_reputation=decoded.get("coldkey_reputation"),
257
+ subnet_node_reputation=decoded.get("subnet_node_reputation", 0),
258
+ node_slot_index=decoded.get("node_slot_index"),
259
+ consecutive_idle_epochs=decoded.get(
260
+ "consecutive_idle_epochs", 0
261
+ ),
262
+ consecutive_included_epochs=decoded.get(
263
+ "consecutive_included_epochs", 0
264
+ ),
265
+ )
266
+ nodes.append(node_info)
267
+ except Exception as e:
268
+ logger.warning(
269
+ f"Failed to convert node info (idx={idx}, subnet_id={decoded.get('subnet_id')}, node_id={decoded.get('subnet_node_id')}): {e}"
270
+ )
271
+ continue
272
+
273
+ # Fallback: If we got fewer nodes than expected (or Vec decode seems incomplete),
274
+ # try querying individual nodes sequentially
275
+ # Check data size to see if there might be more nodes
276
+ data_size = len(result_data) if hasattr(result_data, "__len__") else 0
277
+ if len(nodes) <= 2 and data_size > 100:
278
+ fallback_nodes = self._get_subnet_nodes_info_fallback(subnet_id, nodes)
279
+ if fallback_nodes and len(fallback_nodes) > len(nodes):
280
+ return fallback_nodes
281
+
282
+ return nodes
283
+
284
+ except Exception as e:
285
+ logger.error(f"Error getting subnet nodes info: {e}")
286
+ import traceback
287
+
288
+ logger.debug(traceback.format_exc())
289
+ # Try fallback method even on exception
290
+ try:
291
+ fallback_nodes = self._get_subnet_nodes_info_fallback(subnet_id, [])
292
+ if fallback_nodes:
293
+ return fallback_nodes
294
+ except Exception as fallback_error:
295
+ logger.debug(f"Fallback method also failed: {fallback_error}")
296
+ return []
297
+
298
+ def _get_subnet_nodes_info_fallback(
299
+ self, subnet_id: int, existing_nodes: list[SubnetNodeInfo]
300
+ ) -> list[SubnetNodeInfo]:
301
+ """
302
+ Fallback method: Query individual nodes one by one starting from node_id=1
303
+ until we hit a non-existent node.
304
+
305
+ Args:
306
+ subnet_id: The subnet ID
307
+ existing_nodes: Already found nodes from Vec decode (to avoid duplicates)
308
+
309
+ Returns:
310
+ List of all SubnetNodeInfo objects found
311
+ """
312
+ nodes = []
313
+ existing_node_ids = {node.subnet_node_id for node in existing_nodes}
314
+
315
+ # Start from node_id=1 and increment until we hit a non-existent node
316
+ node_id = 1
317
+ consecutive_not_found = 0
318
+ max_consecutive_not_found = 5 # Stop after 5 consecutive non-existent nodes
319
+ max_nodes = 1000 # Safety limit to prevent infinite loops
320
+
321
+ while (
322
+ node_id <= max_nodes and consecutive_not_found < max_consecutive_not_found
323
+ ):
324
+ # Skip if we already have this node from Vec decode
325
+ if node_id in existing_node_ids:
326
+ node_id += 1
327
+ consecutive_not_found = 0 # Reset counter when we find an existing one
328
+ continue
329
+
330
+ try:
331
+ node_info = self.get_subnet_node_info(subnet_id, node_id)
332
+ if node_info:
333
+ nodes.append(node_info)
334
+ consecutive_not_found = 0 # Reset counter on success
335
+ else:
336
+ consecutive_not_found += 1
337
+ if consecutive_not_found >= max_consecutive_not_found:
338
+ break
339
+ except Exception:
340
+ consecutive_not_found += 1
341
+ if consecutive_not_found >= max_consecutive_not_found:
342
+ break
343
+
344
+ node_id += 1
345
+
346
+ # Combine with existing nodes, removing duplicates
347
+ all_nodes = existing_nodes + nodes
348
+ # Remove duplicates by subnet_node_id (keep first occurrence)
349
+ seen_ids = set()
350
+ unique_nodes = []
351
+ for node in all_nodes:
352
+ if node.subnet_node_id not in seen_ids:
353
+ seen_ids.add(node.subnet_node_id)
354
+ unique_nodes.append(node)
355
+
356
+ # Sort by node_id for consistency
357
+ unique_nodes.sort(key=lambda x: x.subnet_node_id)
358
+
359
+ return unique_nodes
360
+
361
+ def get_all_subnet_nodes_info(self) -> list[SubnetNodeInfo]:
362
+ """
363
+ Get information about all subnet nodes across all subnets using custom RPC method with full SCALE decoding.
364
+
365
+ Returns:
366
+ List of SubnetNodeInfo objects with all fields decoded
367
+
368
+ Reference:
369
+ - mesh-template chain_functions.py lines 1636-1650, 2126-2141
370
+ """
371
+ # Check if substrate connection exists
372
+ if self.substrate is None:
373
+ logger.error(
374
+ "Cannot get all subnet nodes info: not connected to blockchain"
375
+ )
376
+ raise RuntimeError("Not connected to blockchain")
377
+
378
+ try:
379
+ # Call RPC method
380
+ result = self.substrate.rpc_request(
381
+ method="network_getAllSubnetNodesInfo", params=[]
382
+ )
383
+
384
+ if not result or not result.get("result"):
385
+ logger.warning(
386
+ "RPC request returned no result for getAllSubnetNodesInfo"
387
+ )
388
+ return []
389
+
390
+ result_data = result["result"]
391
+
392
+ # Decode Vec<SubnetNodeInfo> using custom type registry
393
+ from ...utils.blockchain.type_registry import decode_vec_subnet_node_info
394
+
395
+ decoded_list = decode_vec_subnet_node_info(result_data)
396
+
397
+ if not decoded_list:
398
+ logger.info("No nodes found across all subnets")
399
+ return []
400
+
401
+ # Convert to list of SubnetNodeInfo objects (same logic as get_subnet_nodes_info)
402
+ def to_bytes(val):
403
+ """Convert decoded value to bytes for peer IDs and other byte fields."""
404
+ if isinstance(val, bytes):
405
+ return val
406
+ if isinstance(val, list):
407
+ # Vec<u8> from SCALE decoding is a list of integers (0-255)
408
+ # Convert to bytes
409
+ try:
410
+ return bytes(val)
411
+ except (ValueError, TypeError):
412
+ # If conversion fails, try to convert each element
413
+ return bytes(int(b) for b in val if 0 <= int(b) <= 255)
414
+ if isinstance(val, str):
415
+ # String peer IDs should be encoded to UTF-8 bytes
416
+ return val.encode("utf-8")
417
+ # Default: return empty bytes
418
+ return b""
419
+
420
+ def to_address(val):
421
+ from ...utils.blockchain.formatting import to_checksum_address
422
+
423
+ if isinstance(val, str):
424
+ raw_addr = val if val.startswith("0x") else "0x" + val
425
+ else:
426
+ raw_addr = "0x" + (
427
+ bytes(val).hex() if isinstance(val, (list, bytes)) else ""
428
+ )
429
+ return to_checksum_address(raw_addr)
430
+
431
+ nodes = []
432
+ for decoded in decoded_list:
433
+ try:
434
+ classification_data = decoded.get("classification", {})
435
+
436
+ node_info = SubnetNodeInfo(
437
+ subnet_id=decoded["subnet_id"],
438
+ subnet_node_id=decoded["subnet_node_id"],
439
+ coldkey=to_address(decoded["coldkey"]),
440
+ hotkey=to_address(decoded["hotkey"]),
441
+ peer_id=to_bytes(decoded["peer_id"]),
442
+ bootnode_peer_id=to_bytes(decoded["bootnode_peer_id"]),
443
+ client_peer_id=to_bytes(decoded["client_peer_id"]),
444
+ bootnode=(
445
+ to_bytes(decoded.get("bootnode"))
446
+ if decoded.get("bootnode")
447
+ else None
448
+ ),
449
+ identity=decoded.get("identity"),
450
+ classification=classification_data, # Pass dict as-is
451
+ delegate_reward_rate=decoded.get("delegate_reward_rate", 0),
452
+ last_delegate_reward_rate_update=decoded.get(
453
+ "last_delegate_reward_rate_update", 0
454
+ ),
455
+ unique=(
456
+ to_bytes(decoded.get("unique"))
457
+ if decoded.get("unique")
458
+ else None
459
+ ),
460
+ non_unique=(
461
+ to_bytes(decoded.get("non_unique"))
462
+ if decoded.get("non_unique")
463
+ else None
464
+ ),
465
+ stake_balance=decoded.get("stake_balance", 0),
466
+ node_delegate_stake_balance=decoded.get(
467
+ "node_delegate_stake_balance", 0
468
+ ),
469
+ total_node_delegate_stake_shares=decoded.get(
470
+ "total_node_delegate_stake_shares", 0
471
+ ),
472
+ coldkey_reputation=decoded.get("coldkey_reputation"),
473
+ subnet_node_reputation=decoded.get("subnet_node_reputation", 0),
474
+ node_slot_index=decoded.get("node_slot_index"),
475
+ consecutive_idle_epochs=decoded.get(
476
+ "consecutive_idle_epochs", 0
477
+ ),
478
+ consecutive_included_epochs=decoded.get(
479
+ "consecutive_included_epochs", 0
480
+ ),
481
+ )
482
+ nodes.append(node_info)
483
+ except Exception as e:
484
+ logger.warning(f"Failed to convert node info: {e}")
485
+ continue
486
+
487
+ return nodes
488
+
489
+ except Exception as e:
490
+ logger.error(f"Error getting all subnet nodes info: {e}")
491
+ logger.error(f"Error type: {type(e).__name__}")
492
+ raise # Re-raise the exception so handler can catch it
493
+
494
+ def get_validator_subnet_nodes_info(
495
+ self, validator_id: int
496
+ ) -> list[SubnetNodeInfo]:
497
+ """
498
+ Get subnet node information for a specific validator.
499
+
500
+ Args:
501
+ validator_id: Validator ID
502
+
503
+ Returns:
504
+ List of SubnetNodeInfo objects with all fields decoded
505
+ """
506
+ try:
507
+ result = self.substrate.rpc_request(
508
+ method="network_getValidatorSubnetNodesInfo", params=[validator_id]
509
+ )
510
+
511
+ if not result or not result.get("result"):
512
+ return []
513
+
514
+ result_data = result["result"]
515
+
516
+ from ...utils.blockchain.type_registry import decode_vec_subnet_node_info
517
+
518
+ decoded_list = decode_vec_subnet_node_info(result_data)
519
+
520
+ if not decoded_list:
521
+ return []
522
+
523
+ def to_bytes(val):
524
+ """Convert decoded value to bytes for peer IDs and byte fields."""
525
+ if isinstance(val, bytes):
526
+ return val
527
+ if isinstance(val, list):
528
+ try:
529
+ return bytes(val)
530
+ except (ValueError, TypeError):
531
+ return bytes(int(b) for b in val if 0 <= int(b) <= 255)
532
+ if isinstance(val, str):
533
+ return val.encode("utf-8")
534
+ return b""
535
+
536
+ def to_address(val):
537
+ from ...utils.blockchain.formatting import to_checksum_address
538
+
539
+ if isinstance(val, str):
540
+ raw_addr = val if val.startswith("0x") else "0x" + val
541
+ else:
542
+ raw_addr = "0x" + (
543
+ bytes(val).hex() if isinstance(val, (list, bytes)) else ""
544
+ )
545
+ return to_checksum_address(raw_addr)
546
+
547
+ nodes = []
548
+ for decoded in decoded_list:
549
+ try:
550
+ classification_data = decoded.get("classification", {})
551
+
552
+ node_info = SubnetNodeInfo(
553
+ subnet_id=decoded["subnet_id"],
554
+ subnet_node_id=decoded["subnet_node_id"],
555
+ coldkey=to_address(decoded["coldkey"]),
556
+ hotkey=to_address(decoded["hotkey"]),
557
+ peer_id=to_bytes(decoded["peer_id"]),
558
+ bootnode_peer_id=to_bytes(decoded["bootnode_peer_id"]),
559
+ client_peer_id=to_bytes(decoded["client_peer_id"]),
560
+ bootnode=(
561
+ to_bytes(decoded.get("bootnode"))
562
+ if decoded.get("bootnode")
563
+ else None
564
+ ),
565
+ identity=decoded.get("identity"),
566
+ classification=classification_data,
567
+ delegate_reward_rate=decoded.get("delegate_reward_rate", 0),
568
+ last_delegate_reward_rate_update=decoded.get(
569
+ "last_delegate_reward_rate_update", 0
570
+ ),
571
+ unique=(
572
+ to_bytes(decoded.get("unique"))
573
+ if decoded.get("unique")
574
+ else None
575
+ ),
576
+ non_unique=(
577
+ to_bytes(decoded.get("non_unique"))
578
+ if decoded.get("non_unique")
579
+ else None
580
+ ),
581
+ stake_balance=decoded.get("stake_balance", 0),
582
+ node_delegate_stake_balance=decoded.get(
583
+ "node_delegate_stake_balance", 0
584
+ ),
585
+ total_node_delegate_stake_shares=decoded.get(
586
+ "total_node_delegate_stake_shares", 0
587
+ ),
588
+ coldkey_reputation=decoded.get("coldkey_reputation"),
589
+ subnet_node_reputation=decoded.get("subnet_node_reputation", 0),
590
+ node_slot_index=decoded.get("node_slot_index"),
591
+ consecutive_idle_epochs=decoded.get(
592
+ "consecutive_idle_epochs", 0
593
+ ),
594
+ consecutive_included_epochs=decoded.get(
595
+ "consecutive_included_epochs", 0
596
+ ),
597
+ )
598
+ nodes.append(node_info)
599
+ except Exception as e:
600
+ logger.warning(
601
+ "Failed to convert validator node info "
602
+ f"(validator_id={validator_id}, "
603
+ f"subnet_id={decoded.get('subnet_id')}, "
604
+ f"node_id={decoded.get('subnet_node_id')}): {e}"
605
+ )
606
+ continue
607
+
608
+ return nodes
609
+
610
+ except Exception as e:
611
+ logger.error(f"Error getting validator subnet nodes info: {e}")
612
+ import traceback
613
+
614
+ logger.debug(traceback.format_exc())
615
+ return []
616
+
617
+ def get_coldkey_subnet_nodes_info(self, coldkey: str) -> list[SubnetNodeInfo]:
618
+ """
619
+ Get subnet node information for a specific coldkey using custom RPC method with full SCALE decoding.
620
+
621
+ Args:
622
+ coldkey: The coldkey account ID (0x... format)
623
+
624
+ Returns:
625
+ List of SubnetNodeInfo objects with all fields decoded
626
+
627
+ Reference:
628
+ - mesh-template chain_functions.py lines 1676-1688, 2160-2177
629
+ - hypertensor-evm/pallets/network/rpc/src/lib.rs lines 44-49
630
+ """
631
+ try:
632
+ # Normalize address format for RPC call (same as wallet.py)
633
+ if coldkey.startswith("0x"):
634
+ # EVM address format - use hex string without 0x prefix, normalize to lowercase
635
+ coldkey_param = coldkey[2:].lower()
636
+ else:
637
+ # SS58 address format - try to decode to hex
638
+ from substrateinterface.utils.ss58 import ss58_decode
639
+
640
+ try:
641
+ coldkey_bytes = ss58_decode(coldkey)
642
+ coldkey_param = coldkey_bytes.hex()
643
+ except Exception:
644
+ # If SS58 decode fails, use address as-is
645
+ coldkey_param = coldkey
646
+
647
+ # Call RPC method
648
+ result = self.substrate.rpc_request(
649
+ method="network_getColdkeySubnetNodesInfo", params=[coldkey_param]
650
+ )
651
+
652
+ if not result or not result.get("result"):
653
+ return []
654
+
655
+ result_data = result["result"]
656
+
657
+ # Decode Vec<SubnetNodeInfo> using custom type registry
658
+ from ...utils.blockchain.type_registry import decode_vec_subnet_node_info
659
+
660
+ decoded_list = decode_vec_subnet_node_info(result_data)
661
+
662
+ if not decoded_list:
663
+ return []
664
+ for idx, decoded in enumerate(decoded_list):
665
+ logger.info(
666
+ f" Node {idx}: subnet_id={decoded.get('subnet_id')}, node_id={decoded.get('subnet_node_id')}, coldkey={decoded.get('coldkey')}, hotkey={decoded.get('hotkey')}"
667
+ )
668
+
669
+ # Convert to list of SubnetNodeInfo objects (same logic as get_subnet_nodes_info)
670
+ def to_bytes(val):
671
+ """Convert decoded value to bytes for peer IDs and other byte fields."""
672
+ if isinstance(val, bytes):
673
+ return val
674
+ if isinstance(val, list):
675
+ # Vec<u8> from SCALE decoding is a list of integers (0-255)
676
+ # Convert to bytes
677
+ try:
678
+ return bytes(val)
679
+ except (ValueError, TypeError):
680
+ # If conversion fails, try to convert each element
681
+ return bytes(int(b) for b in val if 0 <= int(b) <= 255)
682
+ if isinstance(val, str):
683
+ # String peer IDs should be encoded to UTF-8 bytes
684
+ return val.encode("utf-8")
685
+ # Default: return empty bytes
686
+ return b""
687
+
688
+ def to_address(val):
689
+ from ...utils.blockchain.formatting import to_checksum_address
690
+
691
+ if isinstance(val, str):
692
+ raw_addr = val if val.startswith("0x") else "0x" + val
693
+ else:
694
+ raw_addr = "0x" + (
695
+ bytes(val).hex() if isinstance(val, (list, bytes)) else ""
696
+ )
697
+ return to_checksum_address(raw_addr)
698
+
699
+ nodes = []
700
+ for decoded in decoded_list:
701
+ try:
702
+ classification_data = decoded.get("classification", {})
703
+
704
+ # Log each node being processed
705
+ node_subnet_id = decoded.get("subnet_id")
706
+ node_id = decoded.get("subnet_node_id")
707
+ node_coldkey = to_address(decoded["coldkey"])
708
+ logger.debug(
709
+ f"Processing node {node_id} in subnet {node_subnet_id}, coldkey: {node_coldkey}"
710
+ )
711
+
712
+ node_info = SubnetNodeInfo(
713
+ subnet_id=node_subnet_id,
714
+ subnet_node_id=node_id,
715
+ coldkey=node_coldkey,
716
+ hotkey=to_address(decoded["hotkey"]),
717
+ peer_id=to_bytes(decoded["peer_id"]),
718
+ bootnode_peer_id=to_bytes(decoded["bootnode_peer_id"]),
719
+ client_peer_id=to_bytes(decoded["client_peer_id"]),
720
+ bootnode=(
721
+ to_bytes(decoded.get("bootnode"))
722
+ if decoded.get("bootnode")
723
+ else None
724
+ ),
725
+ identity=decoded.get("identity"),
726
+ classification=classification_data, # Pass dict as-is
727
+ delegate_reward_rate=decoded.get("delegate_reward_rate", 0),
728
+ last_delegate_reward_rate_update=decoded.get(
729
+ "last_delegate_reward_rate_update", 0
730
+ ),
731
+ unique=(
732
+ to_bytes(decoded.get("unique"))
733
+ if decoded.get("unique")
734
+ else None
735
+ ),
736
+ non_unique=(
737
+ to_bytes(decoded.get("non_unique"))
738
+ if decoded.get("non_unique")
739
+ else None
740
+ ),
741
+ stake_balance=decoded.get("stake_balance", 0),
742
+ node_delegate_stake_balance=decoded.get(
743
+ "node_delegate_stake_balance", 0
744
+ ),
745
+ total_node_delegate_stake_shares=decoded.get(
746
+ "total_node_delegate_stake_shares", 0
747
+ ),
748
+ coldkey_reputation=decoded.get("coldkey_reputation"),
749
+ subnet_node_reputation=decoded.get("subnet_node_reputation", 0),
750
+ node_slot_index=decoded.get("node_slot_index"),
751
+ consecutive_idle_epochs=decoded.get(
752
+ "consecutive_idle_epochs", 0
753
+ ),
754
+ consecutive_included_epochs=decoded.get(
755
+ "consecutive_included_epochs", 0
756
+ ),
757
+ )
758
+ nodes.append(node_info)
759
+ logger.debug(f"Successfully added node {node_id} to list")
760
+ except Exception as e:
761
+ logger.warning(
762
+ f"Failed to convert node info (subnet_id={decoded.get('subnet_id')}, node_id={decoded.get('subnet_node_id')}): {e}"
763
+ )
764
+ import traceback
765
+
766
+ logger.debug(traceback.format_exc())
767
+ continue
768
+
769
+ logger.info(
770
+ f"🔍 DEBUG: Successfully converted {len(nodes)} nodes for coldkey {coldkey}"
771
+ )
772
+ for node in nodes:
773
+ logger.info(
774
+ f" Final node: subnet_id={node.subnet_id}, node_id={node.subnet_node_id}, coldkey={node.coldkey}"
775
+ )
776
+ return nodes
777
+
778
+ except Exception as e:
779
+ logger.error(f"Error getting coldkey subnet nodes info: {e}")
780
+ import traceback
781
+
782
+ logger.debug(traceback.format_exc())
783
+ return []