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,680 @@
1
+ """
2
+ RPC client for overwatch-related queries using scalecodec for decoding.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from substrateinterface import SubstrateInterface
8
+
9
+ from ...models.responses import OverwatchNodeInfo
10
+ from ...utils.logging import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class OverwatchRpcClient:
16
+ """RPC client for overwatch-related queries."""
17
+
18
+ def __init__(self, substrate: SubstrateInterface):
19
+ """Initialize the overwatch RPC client."""
20
+ self.substrate = substrate
21
+
22
+ def get_overwatch_commits_for_epoch_and_node(
23
+ self, epoch: int, overwatch_node_id: int
24
+ ) -> list[dict[str, Any]]:
25
+ """
26
+ Get overwatch commits for a specific epoch and node using custom RPC method.
27
+
28
+ Args:
29
+ epoch: The epoch number
30
+ overwatch_node_id: The overwatch node ID
31
+
32
+ Returns:
33
+ list of commit data (subnet_id, commit_hash pairs)
34
+ """
35
+ try:
36
+ # Use custom RPC method
37
+ result = self.substrate.rpc_request(
38
+ method="network_getOverwatchCommitsForEpochAndNode",
39
+ params=[epoch, overwatch_node_id],
40
+ )
41
+
42
+ if not result or not result.get("result"):
43
+ return []
44
+
45
+ # Decode the SCALE bytes using centralized type registry
46
+ scale_data = result["result"]
47
+ if isinstance(scale_data, str) and scale_data.startswith("0x"):
48
+ scale_data = bytes.fromhex(scale_data[2:])
49
+ elif isinstance(scale_data, (list, tuple)):
50
+ scale_data = bytes(scale_data)
51
+
52
+ # The result should be a Vec<(u32, Hash)> - (subnet_id, commit_hash) pairs
53
+ from scalecodec import ScaleBytes
54
+ from ...utils.blockchain.type_registry import get_rpc_runtime_config
55
+
56
+ # Use centralized type registry configuration
57
+ runtime_config = get_rpc_runtime_config()
58
+
59
+ # Decode the vector of tuples
60
+ obj = runtime_config.create_scale_object(
61
+ "Vec<(u32, Hash)>", ScaleBytes(scale_data)
62
+ )
63
+ decoded = obj.decode()
64
+
65
+ # Convert to list of dictionaries
66
+ commits = []
67
+ for item in decoded:
68
+ commit_hash = item[1].hex() if hasattr(item[1], "hex") else bytes(item[1]).hex()
69
+ commits.append({"subnet_id": item[0], "commit_hash": commit_hash})
70
+
71
+ return commits
72
+
73
+ except Exception as e:
74
+ logger.error(f"Error getting overwatch commits: {e}")
75
+ return []
76
+
77
+ def get_overwatch_reveals_for_epoch_and_node(
78
+ self, epoch: int, overwatch_node_id: int
79
+ ) -> list[dict[str, Any]]:
80
+ """
81
+ Get overwatch reveals for a specific epoch and node using custom RPC method.
82
+
83
+ Args:
84
+ epoch: The epoch number
85
+ overwatch_node_id: The overwatch node ID
86
+
87
+ Returns:
88
+ list of reveal data (subnet_id, weight pairs)
89
+ """
90
+ try:
91
+ # Use custom RPC method
92
+ result = self.substrate.rpc_request(
93
+ method="network_getOverwatchRevealsForEpochAndNode",
94
+ params=[epoch, overwatch_node_id],
95
+ )
96
+
97
+ if not result or not result.get("result"):
98
+ return []
99
+
100
+ # Decode the SCALE bytes using centralized type registry
101
+ scale_data = result["result"]
102
+ if isinstance(scale_data, str) and scale_data.startswith("0x"):
103
+ scale_data = bytes.fromhex(scale_data[2:])
104
+ elif isinstance(scale_data, (list, tuple)):
105
+ scale_data = bytes(scale_data)
106
+
107
+ # The result should be a Vec<(u32, u128)> - (subnet_id, weight) pairs
108
+ from scalecodec import ScaleBytes
109
+ from ...utils.blockchain.type_registry import get_rpc_runtime_config
110
+
111
+ # Use centralized type registry configuration
112
+ runtime_config = get_rpc_runtime_config()
113
+
114
+ # Decode the vector of tuples
115
+ obj = runtime_config.create_scale_object(
116
+ "Vec<(u32, u128)>", ScaleBytes(scale_data)
117
+ )
118
+ decoded = obj.decode()
119
+
120
+ # Convert to list of dictionaries
121
+ reveals = []
122
+ for item in decoded:
123
+ reveals.append({"subnet_id": item[0], "weight": item[1]})
124
+
125
+ return reveals
126
+
127
+ except Exception as e:
128
+ logger.error(f"Error getting overwatch reveals: {e}")
129
+ return []
130
+
131
+ def get_overwatch_node_info(
132
+ self, overwatch_node_id: int
133
+ ) -> Optional[OverwatchNodeInfo]:
134
+ """
135
+ Get overwatch node information using storage queries.
136
+ Note: This is not a custom RPC method, but uses storage queries.
137
+
138
+ Args:
139
+ overwatch_node_id: The overwatch node ID
140
+
141
+ Returns:
142
+ OverwatchNodeInfo if found, None otherwise
143
+ """
144
+ try:
145
+ # Query overwatch node data from storage
146
+ # Storage: OverwatchNodes<T: Config> = StorageMap<_, Identity, u32, OverwatchNode<T::AccountId>, OptionQuery>
147
+ node_data = self.substrate.query(
148
+ module="Network",
149
+ storage_function="OverwatchNodes",
150
+ params=[overwatch_node_id],
151
+ )
152
+
153
+ if not node_data or not node_data.value:
154
+ logger.debug(f"No overwatch node found with ID {overwatch_node_id}")
155
+ return None
156
+
157
+ # Extract hotkey from node data
158
+ # OverwatchNode struct has: id (u32), hotkey (AccountId)
159
+ hotkey = node_data.value.get("hotkey") if isinstance(node_data.value, dict) else None
160
+ if not hotkey:
161
+ logger.warning(f"No hotkey found in overwatch node {overwatch_node_id}")
162
+ return None
163
+
164
+ # Get coldkey from HotkeyOwner storage
165
+ coldkey = self.substrate.query(
166
+ module="Network",
167
+ storage_function="HotkeyOwner",
168
+ params=[hotkey],
169
+ ).value
170
+
171
+ # Query peer IDs from OverwatchNodeIndex (maps node_id -> {subnet_id -> peer_id})
172
+ peer_ids_data = (
173
+ self.substrate.query(
174
+ module="Network",
175
+ storage_function="OverwatchNodeIndex",
176
+ params=[overwatch_node_id],
177
+ ).value
178
+ or {}
179
+ )
180
+
181
+ # Handle BTreeMap decoding (might be list of tuples or dict)
182
+ if isinstance(peer_ids_data, list):
183
+ # Handle [(subnet_id, peer_id), ...]
184
+ peer_ids = {int(k): v for k, v in peer_ids_data}
185
+ elif isinstance(peer_ids_data, dict):
186
+ # Handle {subnet_id: peer_id}
187
+ peer_ids = {int(k): v for k, v in peer_ids_data.items()}
188
+ else:
189
+ peer_ids = {}
190
+
191
+ # Query reputation from ColdkeyReputation (if we have coldkey)
192
+ reputation = None
193
+ if coldkey:
194
+ reputation = self.substrate.query(
195
+ module="Network",
196
+ storage_function="ColdkeyReputation",
197
+ params=[coldkey],
198
+ ).value
199
+
200
+ # Query stake balance from AccountOverwatchStake (keyed by hotkey)
201
+ stake_balance = 0
202
+ if hotkey:
203
+ stake_result = self.substrate.query(
204
+ module="Network",
205
+ storage_function="AccountOverwatchStake",
206
+ params=[hotkey],
207
+ )
208
+ stake_balance = stake_result.value if stake_result and stake_result.value else 0
209
+
210
+ return OverwatchNodeInfo(
211
+ overwatch_node_id=overwatch_node_id,
212
+ coldkey=coldkey,
213
+ hotkey=hotkey,
214
+ peer_ids=peer_ids,
215
+ penalties=0, # Note: OverwatchNodePenalties may not exist
216
+ stake_balance=stake_balance,
217
+ reputation=reputation,
218
+ )
219
+
220
+ except Exception as e:
221
+ logger.error(f"Error getting overwatch node info: {e}")
222
+ return None
223
+
224
+
225
+ def list_overwatch_nodes(self) -> list[OverwatchNodeInfo]:
226
+ """
227
+ list all overwatch nodes using storage queries.
228
+ Note: This is not a custom RPC method, but uses storage queries.
229
+
230
+ Returns:
231
+ list of OverwatchNodeInfo objects
232
+ """
233
+ try:
234
+ nodes = []
235
+
236
+ # Get total overwatch node count
237
+ total_nodes = (
238
+ self.substrate.query(
239
+ module="Network", storage_function="TotalOverwatchNodes"
240
+ ).value
241
+ or 0
242
+ )
243
+
244
+ # Query each node
245
+ for node_id in range(1, total_nodes + 1):
246
+ node_info = self.get_overwatch_node_info(node_id)
247
+ if node_info:
248
+ nodes.append(node_info)
249
+
250
+ return nodes
251
+
252
+ except Exception as e:
253
+ logger.error(f"Error listing overwatch nodes: {e}")
254
+ return []
255
+
256
+ def check_overwatch_qualification(
257
+ self, coldkey: str, hotkey: Optional[str] = None
258
+ ) -> dict[str, Any]:
259
+ """
260
+ Check if a coldkey meets overwatch node registration requirements.
261
+
262
+ This mirrors the logic in the EVM codebase:
263
+ - hypertensor-evm/pallets/network/src/overwatch_nodes/registering.rs
264
+ - do_register_overwatch_node() - lines 21-93: Registration checks
265
+ - is_overwatch_node_qualified() - lines 132-200: Qualification logic
266
+
267
+ Requirements checked:
268
+ 1. Overwatch epochs have started (epoch > 0)
269
+ 2. Network not at max capacity (TotalOverwatchNodes < MaxOverwatchNodes)
270
+ 3. Coldkey not blacklisted (OverwatchNodeBlacklist)
271
+ 4. Hotkey != coldkey (if hotkey provided)
272
+ 5. Hotkey has no existing owner (if hotkey provided)
273
+ 6. Hotkey not already in coldkey's hotkeys (if hotkey provided)
274
+ 7. Network age sufficient (current_epoch > min_age)
275
+ 8. Coldkey age >= OverwatchMinAge epochs
276
+ 9. Reputation score >= OverwatchMinRepScore
277
+ 10. Diversification ratio >= OverwatchMinDiversificationRatio
278
+ 11. Average attestation >= OverwatchMinAvgAttestationRatio
279
+ 12. Balance >= OverwatchMinStakeBalance
280
+
281
+ Args:
282
+ coldkey: Coldkey address (hex string starting with 0x)
283
+ hotkey: Optional hotkey address to validate (hex string starting with 0x)
284
+
285
+ Returns:
286
+ Dictionary with qualification status and detailed breakdown
287
+ """
288
+ try:
289
+ if not self.substrate:
290
+ raise Exception("Not connected to blockchain")
291
+
292
+ # Normalize coldkey address
293
+ if coldkey.startswith("0x"):
294
+ coldkey = coldkey.lower()
295
+
296
+ # Normalize hotkey address if provided
297
+ if hotkey and hotkey.startswith("0x"):
298
+ hotkey = hotkey.lower()
299
+
300
+ requirements_met = []
301
+ requirements_failed = []
302
+
303
+ # ========================================
304
+ # 1. Check overwatch epoch > 0
305
+ # Source: era.rs line 51-56: get_current_overwatch_epoch_as_u32()
306
+ # Overwatch epoch = current_block / (epoch_length * multiplier)
307
+ # ========================================
308
+ # Get current block number (requires block hash)
309
+ latest_hash = self.substrate.get_block_hash()
310
+ current_block = self.substrate.get_block_number(block_hash=latest_hash) if latest_hash else 0
311
+ if not current_block:
312
+ current_block = 0
313
+
314
+
315
+ # Get EpochLength constant
316
+ epoch_length_result = self.substrate.get_constant("Network", "EpochLength")
317
+ epoch_length = epoch_length_result.value if epoch_length_result else 100
318
+
319
+ # Get OverwatchEpochLengthMultiplier storage
320
+ multiplier_result = self.substrate.query(
321
+ module="Network", storage_function="OverwatchEpochLengthMultiplier"
322
+ )
323
+ multiplier = multiplier_result.value if multiplier_result and multiplier_result.value else 1
324
+
325
+ # Calculate overwatch epoch: current_block / (epoch_length * multiplier)
326
+ overwatch_epoch_length = epoch_length * multiplier
327
+ current_overwatch_epoch = current_block // overwatch_epoch_length if overwatch_epoch_length > 0 else 0
328
+
329
+ # Calculate current epoch: current_block / epoch_length
330
+ current_epoch = current_block // epoch_length if epoch_length > 0 else 0
331
+
332
+ if current_overwatch_epoch > 0:
333
+ requirements_met.append(
334
+ f"Overwatch epochs started (current epoch: {current_overwatch_epoch})"
335
+ )
336
+ else:
337
+ requirements_failed.append(
338
+ "Overwatch epochs have not started yet (epoch is 0)"
339
+ )
340
+
341
+ # ========================================
342
+ # 2. Check network capacity
343
+ # Source: registering.rs line 38-43
344
+ # ========================================
345
+ total_overwatch_nodes = self.substrate.query(
346
+ module="Network", storage_function="TotalOverwatchNodes"
347
+ ).value or 0
348
+
349
+ max_overwatch_nodes = self.substrate.query(
350
+ module="Network", storage_function="MaxOverwatchNodes"
351
+ ).value or 0
352
+
353
+ if total_overwatch_nodes < max_overwatch_nodes:
354
+ requirements_met.append(
355
+ f"Network capacity available ({total_overwatch_nodes}/{max_overwatch_nodes} nodes registered)"
356
+ )
357
+ else:
358
+ requirements_failed.append(
359
+ f"Network at max capacity ({total_overwatch_nodes}/{max_overwatch_nodes} nodes)"
360
+ )
361
+
362
+ # ========================================
363
+ # 3. Check blacklist
364
+ # Source: registering.rs line 28-31
365
+ # ========================================
366
+ is_blacklisted = self.substrate.query(
367
+ module="Network",
368
+ storage_function="OverwatchNodeBlacklist",
369
+ params=[coldkey],
370
+ ).value or False
371
+
372
+ if not is_blacklisted:
373
+ requirements_met.append("Coldkey not blacklisted")
374
+ else:
375
+ requirements_failed.append("Coldkey is blacklisted from overwatch registration")
376
+
377
+ # ========================================
378
+ # 3a. Hotkey-specific checks (if hotkey provided)
379
+ # Source: registering.rs lines 45, 48-58
380
+ # ========================================
381
+ if hotkey:
382
+ # Check hotkey != coldkey (line 45)
383
+ if hotkey.lower() == coldkey.lower():
384
+ requirements_failed.append(
385
+ "Hotkey cannot be the same as coldkey"
386
+ )
387
+ else:
388
+ requirements_met.append("Hotkey is different from coldkey")
389
+
390
+ # Check hotkey doesn't already have an owner (line 48-51)
391
+ try:
392
+ hotkey_owner_result = self.substrate.query(
393
+ module="Network",
394
+ storage_function="HotkeyOwner",
395
+ params=[hotkey],
396
+ )
397
+ hotkey_owner = hotkey_owner_result.value if hotkey_owner_result else None
398
+
399
+ # Check if owner is a zero/null address (treat as no owner)
400
+ is_zero_address = False
401
+ if hotkey_owner:
402
+ owner_str = str(hotkey_owner).lower()
403
+ # Check for zero address patterns
404
+ is_zero_address = (
405
+ owner_str == "0x0000000000000000000000000000000000000000" or
406
+ owner_str.replace("0x", "").replace("0", "") == "" or
407
+ owner_str.startswith("0x00000000")
408
+ )
409
+
410
+ if hotkey_owner and not is_zero_address:
411
+ # Format the address properly with color markup
412
+ from ...utils.wallet.crypto import format_address_display
413
+ formatted_owner = format_address_display(str(hotkey_owner))
414
+ requirements_failed.append(
415
+ f"Hotkey already has an owner (owned by [htcli.address]{formatted_owner}[/htcli.address])"
416
+ )
417
+ else:
418
+ requirements_met.append("Hotkey is fresh (no existing owner)")
419
+ except Exception as e:
420
+ logger.warning(f"Could not check HotkeyOwner: {e}")
421
+ requirements_met.append("Hotkey owner check skipped (storage query failed)")
422
+
423
+ # Check hotkey not already in coldkey's hotkeys (line 53-58)
424
+ try:
425
+ coldkey_hotkeys_result = self.substrate.query(
426
+ module="Network",
427
+ storage_function="ColdkeyHotkeys",
428
+ params=[coldkey],
429
+ )
430
+ coldkey_hotkeys = coldkey_hotkeys_result.value if coldkey_hotkeys_result else None
431
+
432
+ # Handle different return formats (set, list, etc.)
433
+ if coldkey_hotkeys:
434
+ if isinstance(coldkey_hotkeys, (set, list)):
435
+ hotkey_in_set = hotkey in coldkey_hotkeys or hotkey.lower() in [h.lower() if isinstance(h, str) else str(h).lower() for h in coldkey_hotkeys]
436
+ else:
437
+ hotkey_in_set = False
438
+
439
+ if hotkey_in_set:
440
+ requirements_failed.append(
441
+ "Hotkey already registered to this coldkey"
442
+ )
443
+ else:
444
+ requirements_met.append("Hotkey not already registered to coldkey")
445
+ else:
446
+ requirements_met.append("Hotkey not already registered to coldkey")
447
+ except Exception as e:
448
+ logger.warning(f"Could not check ColdkeyHotkeys: {e}")
449
+ requirements_met.append("Coldkey hotkeys check skipped (storage query failed)")
450
+
451
+ # ========================================
452
+ # 4. Get coldkey reputation for age, score, attestation checks
453
+ # Source: registering.rs line 64-70, 132-200
454
+ # ========================================
455
+ reputation_data = self.substrate.query(
456
+ module="Network",
457
+ storage_function="ColdkeyReputation",
458
+ params=[coldkey],
459
+ ).value
460
+
461
+ # Get minimum requirements
462
+ min_age = self.substrate.query(
463
+ module="Network", storage_function="OverwatchMinAge"
464
+ ).value or 0
465
+
466
+ min_rep_score = self.substrate.query(
467
+ module="Network", storage_function="OverwatchMinRepScore"
468
+ ).value or 0
469
+
470
+ min_diversification = self.substrate.query(
471
+ module="Network", storage_function="OverwatchMinDiversificationRatio"
472
+ ).value or 0
473
+
474
+ min_avg_attestation = self.substrate.query(
475
+ module="Network", storage_function="OverwatchMinAvgAttestationRatio"
476
+ ).value or 0
477
+
478
+ min_stake_balance = self.substrate.query(
479
+ module="Network", storage_function="OverwatchMinStakeBalance"
480
+ ).value or 0
481
+
482
+
483
+ # Process reputation data
484
+ if reputation_data:
485
+ start_epoch = reputation_data.get("start_epoch", 0)
486
+ rep_score = reputation_data.get("score", 0)
487
+ avg_attestation = reputation_data.get("average_attestation", 0)
488
+
489
+ # ========================================
490
+ # 4a. Check network age (no one can qualify if network too young)
491
+ # Source: registering.rs line 145-147
492
+ # ========================================
493
+ if current_epoch <= min_age:
494
+ requirements_failed.append(
495
+ f"Network too young: no one can qualify until epoch {min_age + 1} "
496
+ f"(current epoch: {current_epoch})"
497
+ )
498
+ else:
499
+ requirements_met.append(
500
+ f"Network age sufficient (epoch {current_epoch} > min_age {min_age})"
501
+ )
502
+
503
+ # ========================================
504
+ # 4b. Check coldkey age
505
+ # Source: registering.rs line 149-153
506
+ # ========================================
507
+ age = current_epoch - start_epoch if current_epoch >= start_epoch else 0
508
+
509
+ if age >= min_age:
510
+ requirements_met.append(
511
+ f"Coldkey age requirement met ({age}/{min_age} epochs)"
512
+ )
513
+ else:
514
+ requirements_failed.append(
515
+ f"Coldkey age not met ({age}/{min_age} epochs, need {min_age - age} more)"
516
+ )
517
+
518
+ # ========================================
519
+ # 4b. Check reputation score
520
+ # Source: registering.rs line 155-157
521
+ # ========================================
522
+ if rep_score >= min_rep_score:
523
+ requirements_met.append(
524
+ f"Reputation score met ({rep_score}/{min_rep_score})"
525
+ )
526
+ else:
527
+ requirements_failed.append(
528
+ f"Reputation score not met ({rep_score}/{min_rep_score})"
529
+ )
530
+
531
+ # ========================================
532
+ # 4c. Check average attestation
533
+ # Source: registering.rs line 195-197
534
+ # ========================================
535
+ # Convert to percentage for display (values are in 1e18 format)
536
+ avg_attestation_pct = avg_attestation / 1e16 if avg_attestation else 0
537
+ min_attestation_pct = min_avg_attestation / 1e16 if min_avg_attestation else 0
538
+
539
+ if avg_attestation >= min_avg_attestation:
540
+ requirements_met.append(
541
+ f"Average attestation met ({avg_attestation_pct:.2f}% >= {min_attestation_pct:.2f}%)"
542
+ )
543
+ else:
544
+ requirements_failed.append(
545
+ f"Average attestation not met ({avg_attestation_pct:.2f}% < {min_attestation_pct:.2f}%)"
546
+ )
547
+ else:
548
+ # No reputation history
549
+ requirements_failed.append(
550
+ f"No reputation history found for coldkey (need {min_age} epochs of activity)"
551
+ )
552
+ requirements_failed.append(
553
+ f"Reputation score not available (need {min_rep_score})"
554
+ )
555
+ requirements_failed.append(
556
+ f"Average attestation not available"
557
+ )
558
+
559
+ # ========================================
560
+ # 5. Check diversification ratio
561
+ # Source: registering.rs line 159-193
562
+ # ========================================
563
+ coldkey_subnet_nodes = self.substrate.query(
564
+ module="Network",
565
+ storage_function="ColdkeySubnetNodes",
566
+ params=[coldkey],
567
+ ).value or {}
568
+
569
+ total_active_subnets = self.substrate.query(
570
+ module="Network", storage_function="TotalActiveSubnets"
571
+ ).value or 0
572
+
573
+ # Count unique subnets with active nodes
574
+ active_unique_subnets = len(coldkey_subnet_nodes) if coldkey_subnet_nodes else 0
575
+
576
+ # Calculate diversification (simplified - actual calculation is more complex)
577
+ # Source: registering.rs line 183-189
578
+ if total_active_subnets > 0:
579
+ diversification = (active_unique_subnets * 100 * 1e16) / total_active_subnets
580
+ else:
581
+ diversification = 100 * 1e16 if active_unique_subnets > 0 else 0
582
+
583
+ diversification_pct = diversification / 1e16 if diversification else 0
584
+ min_diversification_pct = min_diversification / 1e16 if min_diversification else 0
585
+
586
+ if diversification >= min_diversification:
587
+ requirements_met.append(
588
+ f"Diversification met ({active_unique_subnets} subnets, {diversification_pct:.2f}% >= {min_diversification_pct:.2f}%)"
589
+ )
590
+ else:
591
+ requirements_failed.append(
592
+ f"Diversification not met ({active_unique_subnets} subnets, {diversification_pct:.2f}% < {min_diversification_pct:.2f}%)"
593
+ )
594
+
595
+ # ========================================
596
+ # 6. Check balance for minimum stake
597
+ # Source: registering.rs line 72-74 (calls do_add_overwatch_stake)
598
+ # ========================================
599
+ try:
600
+ balance_result = self.substrate.query(
601
+ module="System",
602
+ storage_function="Account",
603
+ params=[coldkey],
604
+ )
605
+ free_balance = balance_result.value.get("data", {}).get("free", 0) if balance_result.value else 0
606
+ except Exception:
607
+ free_balance = 0
608
+
609
+ # Debug logging to diagnose potential value mismatches
610
+ logger.debug(f"Balance check - coldkey: {coldkey}")
611
+ logger.debug(f"Balance check - min_stake_balance (raw): {min_stake_balance} (type: {type(min_stake_balance)})")
612
+ logger.debug(f"Balance check - free_balance (raw): {free_balance} (type: {type(free_balance)})")
613
+
614
+ min_stake_tensor = min_stake_balance / 1e18 if isinstance(min_stake_balance, (int, float)) else 0
615
+ balance_tensor = free_balance / 1e18 if isinstance(free_balance, (int, float)) else 0
616
+
617
+ logger.debug(f"Balance check - min_stake_tensor: {min_stake_tensor}, balance_tensor: {balance_tensor}")
618
+
619
+ # Ensure both values are numeric for comparison
620
+ try:
621
+ min_stake_numeric = int(min_stake_balance) if min_stake_balance else 0
622
+ free_balance_numeric = int(free_balance) if free_balance else 0
623
+ except (ValueError, TypeError):
624
+ min_stake_numeric = 0
625
+ free_balance_numeric = 0
626
+
627
+ if free_balance_numeric >= min_stake_numeric:
628
+ requirements_met.append(
629
+ f"Sufficient balance for min stake ({balance_tensor:.2f} TENSOR >= {min_stake_tensor:.2f} TENSOR)"
630
+ )
631
+ else:
632
+ requirements_failed.append(
633
+ f"Insufficient balance ({balance_tensor:.2f} TENSOR < {min_stake_tensor:.2f} TENSOR required)"
634
+ )
635
+
636
+ # ========================================
637
+ # Determine overall qualification
638
+ # ========================================
639
+ can_register = len(requirements_failed) == 0
640
+
641
+ return {
642
+ "success": True,
643
+ "data": {
644
+ "coldkey": coldkey,
645
+ "hotkey": hotkey,
646
+ "can_register": can_register,
647
+ "requirements_met": requirements_met,
648
+ "requirements_failed": requirements_failed,
649
+ "details": {
650
+ "current_overwatch_epoch": current_overwatch_epoch,
651
+ "current_epoch": current_epoch,
652
+ "total_overwatch_nodes": total_overwatch_nodes,
653
+ "max_overwatch_nodes": max_overwatch_nodes,
654
+ "is_blacklisted": is_blacklisted,
655
+ "min_age_epochs": min_age,
656
+ "min_rep_score": min_rep_score,
657
+ "min_diversification_pct": min_diversification_pct,
658
+ "min_avg_attestation_pct": min_avg_attestation / 1e16 if min_avg_attestation else 0,
659
+ "min_stake_tensor": min_stake_tensor,
660
+ "coldkey_subnets": active_unique_subnets,
661
+ "total_active_subnets": total_active_subnets,
662
+ "balance_tensor": balance_tensor,
663
+ "reputation": reputation_data,
664
+ },
665
+ },
666
+ }
667
+
668
+ except Exception as e:
669
+ logger.error(f"Error checking overwatch qualification: {e}")
670
+ return {
671
+ "success": False,
672
+ "error": str(e),
673
+ "data": {
674
+ "coldkey": coldkey,
675
+ "can_register": False,
676
+ "requirements_met": [],
677
+ "requirements_failed": [f"Error checking qualification: {str(e)}"],
678
+ },
679
+ }
680
+