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