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,2104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RPC client for subnet-related queries using scalecodec for decoding.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from substrateinterface import SubstrateInterface
|
|
8
|
+
|
|
9
|
+
from ...models.enums.enum_types import KeyType, SubnetState
|
|
10
|
+
from ...models.responses import SubnetData, SubnetInfo, SubnetNodeInfo
|
|
11
|
+
|
|
12
|
+
from ...utils.logging import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SubnetRpcClient:
|
|
18
|
+
"""RPC client for subnet-related queries."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, substrate: Optional[SubstrateInterface] = None):
|
|
21
|
+
"""Initialize the subnet RPC client."""
|
|
22
|
+
self.substrate = substrate
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _to_bytes(value) -> bytes:
|
|
26
|
+
"""Convert scalecodec values into raw bytes."""
|
|
27
|
+
if value is None:
|
|
28
|
+
return b""
|
|
29
|
+
if isinstance(value, bytes):
|
|
30
|
+
return value
|
|
31
|
+
if isinstance(value, str):
|
|
32
|
+
return value.encode("utf-8")
|
|
33
|
+
if isinstance(value, (list, tuple)):
|
|
34
|
+
return bytes(value)
|
|
35
|
+
return bytes(str(value), "utf-8")
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _to_address(value) -> Optional[str]:
|
|
39
|
+
"""Convert SCALE AccountId representations to checksummed hex strings.
|
|
40
|
+
|
|
41
|
+
Handles Option<AccountId> types - if value is a dict with 'Some' key,
|
|
42
|
+
extracts the inner value. Also handles empty lists/bytes as None.
|
|
43
|
+
|
|
44
|
+
Returns EIP-55 checksummed addresses for proper display.
|
|
45
|
+
"""
|
|
46
|
+
from ...utils.blockchain.formatting import to_checksum_address
|
|
47
|
+
|
|
48
|
+
if value is None:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# Handle Option<AccountId> - scalecodec might decode as dict with 'Some'/'None' keys
|
|
52
|
+
if isinstance(value, dict):
|
|
53
|
+
if "Some" in value:
|
|
54
|
+
value = value["Some"]
|
|
55
|
+
elif "None" in value or value == {}:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
# Handle empty collections as None
|
|
59
|
+
if isinstance(value, (list, tuple)) and len(value) == 0:
|
|
60
|
+
return None
|
|
61
|
+
if isinstance(value, bytes) and len(value) == 0:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Handle string representation
|
|
65
|
+
if isinstance(value, str):
|
|
66
|
+
# Check if it's an empty string or "None"
|
|
67
|
+
if not value or value.lower() == "none":
|
|
68
|
+
return None
|
|
69
|
+
raw_addr = value if value.startswith("0x") else f"0x{value}"
|
|
70
|
+
return to_checksum_address(raw_addr)
|
|
71
|
+
|
|
72
|
+
# Handle bytes/bytearray
|
|
73
|
+
if isinstance(value, (bytes, bytearray)):
|
|
74
|
+
# Check for zero address (all zeros)
|
|
75
|
+
if all(b == 0 for b in value):
|
|
76
|
+
return None
|
|
77
|
+
raw_addr = "0x" + value.hex()
|
|
78
|
+
return to_checksum_address(raw_addr)
|
|
79
|
+
|
|
80
|
+
# Handle list/tuple of bytes/ints
|
|
81
|
+
if isinstance(value, (list, tuple)):
|
|
82
|
+
# Check for empty or all-zero list
|
|
83
|
+
if len(value) == 0:
|
|
84
|
+
return None
|
|
85
|
+
if all(v == 0 for v in value):
|
|
86
|
+
return None
|
|
87
|
+
data = bytes(value)
|
|
88
|
+
raw_addr = "0x" + data.hex()
|
|
89
|
+
return to_checksum_address(raw_addr)
|
|
90
|
+
|
|
91
|
+
return str(value)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _decode_initial_coldkeys(raw) -> Optional[dict[str, int]]:
|
|
96
|
+
if not raw:
|
|
97
|
+
return None
|
|
98
|
+
mapping: dict[str, int] = {}
|
|
99
|
+
for entry in raw:
|
|
100
|
+
if not isinstance(entry, (list, tuple)) or len(entry) != 2:
|
|
101
|
+
continue
|
|
102
|
+
addr = SubnetRpcClient._to_address(entry[0])
|
|
103
|
+
max_regs = entry[1]
|
|
104
|
+
if addr:
|
|
105
|
+
mapping[addr] = max_regs
|
|
106
|
+
return mapping or None
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _decode_key_types(raw) -> set:
|
|
110
|
+
"""Decode BTreeSet<KeyType> from raw decoded data.
|
|
111
|
+
|
|
112
|
+
Handles multiple formats:
|
|
113
|
+
- List of integers: [0, 1, 2, 3] (RSA=0, ED25519=1, SECP256K1=2, ECDSA=3)
|
|
114
|
+
- List of strings: ["RSA", "ECDSA", "ED25519", "SECP256K1"]
|
|
115
|
+
- List of dicts: [{"type": "RSA"}, ...]
|
|
116
|
+
- Already decoded KeyType enums
|
|
117
|
+
|
|
118
|
+
Note: scalecodec decodes BTreeSet as list, so we convert list to set.
|
|
119
|
+
"""
|
|
120
|
+
if raw is None:
|
|
121
|
+
return set()
|
|
122
|
+
|
|
123
|
+
key_types = set()
|
|
124
|
+
|
|
125
|
+
# Handle empty collections (ValueQuery returns [] for empty sets)
|
|
126
|
+
if isinstance(raw, (list, tuple)) and len(raw) == 0:
|
|
127
|
+
return set()
|
|
128
|
+
if isinstance(raw, set) and len(raw) == 0:
|
|
129
|
+
return set()
|
|
130
|
+
|
|
131
|
+
# Handle if already a set - convert to list for processing
|
|
132
|
+
if isinstance(raw, set):
|
|
133
|
+
raw = list(raw)
|
|
134
|
+
|
|
135
|
+
# Process list/tuple
|
|
136
|
+
if isinstance(raw, (list, tuple)):
|
|
137
|
+
for item in raw:
|
|
138
|
+
decoded = None
|
|
139
|
+
|
|
140
|
+
# Strategy 1: Already a KeyType enum
|
|
141
|
+
if isinstance(item, KeyType):
|
|
142
|
+
decoded = item
|
|
143
|
+
|
|
144
|
+
# Strategy 2: Integer value (0=RSA, 1=ED25519, 2=SECP256K1, 3=ECDSA)
|
|
145
|
+
elif isinstance(item, int):
|
|
146
|
+
try:
|
|
147
|
+
decoded = KeyType(item)
|
|
148
|
+
except (ValueError, KeyError):
|
|
149
|
+
logger.debug(f"Invalid KeyType integer value: {item}")
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Strategy 3: String name (case-insensitive)
|
|
153
|
+
elif isinstance(item, str):
|
|
154
|
+
item_upper = item.upper()
|
|
155
|
+
# Handle Rust naming (lowercase) and Python naming (uppercase)
|
|
156
|
+
# Map common variations to KeyType enum
|
|
157
|
+
name_mapping = {
|
|
158
|
+
"RSA": KeyType.RSA,
|
|
159
|
+
"ED25519": KeyType.ED25519,
|
|
160
|
+
"SECP256K1": KeyType.SECP256K1,
|
|
161
|
+
"ECDSA": KeyType.ECDSA,
|
|
162
|
+
}
|
|
163
|
+
if item_upper in name_mapping:
|
|
164
|
+
decoded = name_mapping[item_upper]
|
|
165
|
+
else:
|
|
166
|
+
try:
|
|
167
|
+
# Try direct enum lookup
|
|
168
|
+
decoded = KeyType[item_upper]
|
|
169
|
+
except (KeyError, AttributeError):
|
|
170
|
+
try:
|
|
171
|
+
# Try as integer string
|
|
172
|
+
decoded = KeyType(int(item))
|
|
173
|
+
except (ValueError, KeyError):
|
|
174
|
+
logger.debug(f"Invalid KeyType string: {item}")
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
# Strategy 4: Dict with "type" or "name" key
|
|
178
|
+
elif isinstance(item, dict):
|
|
179
|
+
type_name = item.get("type") or item.get("name")
|
|
180
|
+
if type_name:
|
|
181
|
+
try:
|
|
182
|
+
decoded = KeyType[type_name.upper()]
|
|
183
|
+
except (KeyError, AttributeError):
|
|
184
|
+
try:
|
|
185
|
+
decoded = KeyType(int(type_name))
|
|
186
|
+
except (ValueError, KeyError):
|
|
187
|
+
logger.debug(f"Invalid KeyType dict value: {type_name}")
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
# Strategy 5: Object with .name or .value attribute
|
|
191
|
+
elif hasattr(item, "name"):
|
|
192
|
+
try:
|
|
193
|
+
decoded = KeyType[item.name.upper()]
|
|
194
|
+
except (KeyError, AttributeError):
|
|
195
|
+
try:
|
|
196
|
+
decoded = KeyType(item.name)
|
|
197
|
+
except (ValueError, KeyError):
|
|
198
|
+
logger.debug(f"Invalid KeyType object name: {item.name}")
|
|
199
|
+
continue
|
|
200
|
+
elif hasattr(item, "value"):
|
|
201
|
+
try:
|
|
202
|
+
decoded = KeyType(item.value)
|
|
203
|
+
except (ValueError, KeyError):
|
|
204
|
+
logger.debug(f"Invalid KeyType object value: {item.value}")
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
if decoded is not None:
|
|
208
|
+
key_types.add(decoded)
|
|
209
|
+
|
|
210
|
+
return key_types
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _decode_bootnode_access(raw) -> set[str]:
|
|
214
|
+
"""Decode BTreeSet<AccountId> from raw decoded data.
|
|
215
|
+
|
|
216
|
+
AccountId is 20 bytes, needs to be converted to hex string (0x...).
|
|
217
|
+
|
|
218
|
+
Handles multiple formats:
|
|
219
|
+
- List of byte arrays: [[20 bytes], [20 bytes], ...]
|
|
220
|
+
- List of integers: [list of 20 integers]
|
|
221
|
+
- Already decoded addresses
|
|
222
|
+
|
|
223
|
+
Note: scalecodec decodes BTreeSet as list, so we convert list to set.
|
|
224
|
+
"""
|
|
225
|
+
if raw is None:
|
|
226
|
+
return set()
|
|
227
|
+
|
|
228
|
+
result = set()
|
|
229
|
+
|
|
230
|
+
# Handle empty collections (ValueQuery returns [] for empty sets)
|
|
231
|
+
if isinstance(raw, (list, tuple)) and len(raw) == 0:
|
|
232
|
+
return set()
|
|
233
|
+
if isinstance(raw, set) and len(raw) == 0:
|
|
234
|
+
return set()
|
|
235
|
+
|
|
236
|
+
# Handle if already a set - convert to list for processing
|
|
237
|
+
if isinstance(raw, set):
|
|
238
|
+
raw = list(raw)
|
|
239
|
+
|
|
240
|
+
# Process list/tuple
|
|
241
|
+
if isinstance(raw, (list, tuple)):
|
|
242
|
+
for item in raw:
|
|
243
|
+
# Convert AccountId to address string
|
|
244
|
+
addr = SubnetRpcClient._to_address(item)
|
|
245
|
+
# Only add valid non-zero addresses
|
|
246
|
+
if addr and addr != "0x0000000000000000000000000000000000000000":
|
|
247
|
+
result.add(addr)
|
|
248
|
+
|
|
249
|
+
return result
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def _decode_bootnodes(raw) -> set[bytes]:
|
|
253
|
+
"""Decode BTreeSet<BoundedVec<u8>> from raw decoded data.
|
|
254
|
+
|
|
255
|
+
Note: scalecodec decodes BTreeSet as list, so we convert list to set.
|
|
256
|
+
"""
|
|
257
|
+
if raw is None:
|
|
258
|
+
return set()
|
|
259
|
+
|
|
260
|
+
# Handle if it's already a set
|
|
261
|
+
if isinstance(raw, set):
|
|
262
|
+
return {SubnetRpcClient._to_bytes(item) for item in raw}
|
|
263
|
+
|
|
264
|
+
# Handle list/tuple (scalecodec decodes BTreeSet as list)
|
|
265
|
+
if isinstance(raw, (list, tuple)):
|
|
266
|
+
return {SubnetRpcClient._to_bytes(item) for item in raw}
|
|
267
|
+
|
|
268
|
+
return set()
|
|
269
|
+
|
|
270
|
+
def _query_subnet_storage_items(self, subnet_id: int) -> dict[str, Any]:
|
|
271
|
+
"""Query all storage items for a subnet and return as a dict.
|
|
272
|
+
|
|
273
|
+
This queries all 44+ storage items that compose SubnetInfo.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
subnet_id: The subnet ID to query
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Dict mapping field names to storage values
|
|
280
|
+
"""
|
|
281
|
+
if not self.substrate:
|
|
282
|
+
return {}
|
|
283
|
+
|
|
284
|
+
storage_data = {}
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
# Main SubnetData
|
|
288
|
+
subnet_data_result = self.substrate.query(
|
|
289
|
+
module="Network",
|
|
290
|
+
storage_function="SubnetsData",
|
|
291
|
+
params=[subnet_id],
|
|
292
|
+
)
|
|
293
|
+
if subnet_data_result and subnet_data_result.value:
|
|
294
|
+
storage_data['subnet_data'] = subnet_data_result.value
|
|
295
|
+
|
|
296
|
+
# All other storage items - map storage function name to field name
|
|
297
|
+
storage_items = [
|
|
298
|
+
("SubnetIdFriendlyUid", "friendly_id", [subnet_id]),
|
|
299
|
+
("ChurnLimit", "churn_limit", [subnet_id]),
|
|
300
|
+
("ChurnLimitMultiplier", "churn_limit_multiplier", [subnet_id]),
|
|
301
|
+
("SubnetMinStakeBalance", "min_stake", [subnet_id]),
|
|
302
|
+
("SubnetMaxStakeBalance", "max_stake", [subnet_id]),
|
|
303
|
+
("QueueImmunityEpochs", "queue_immunity_epochs", [subnet_id]),
|
|
304
|
+
("TargetNodeRegistrationsPerEpoch", "target_node_registrations_per_epoch", [subnet_id]),
|
|
305
|
+
("NodeRegistrationsThisEpoch", "node_registrations_this_epoch", [subnet_id]),
|
|
306
|
+
("SubnetNodeQueueEpochs", "subnet_node_queue_epochs", [subnet_id]),
|
|
307
|
+
("IdleClassificationEpochs", "idle_classification_epochs", [subnet_id]),
|
|
308
|
+
("IncludedClassificationEpochs", "included_classification_epochs", [subnet_id]),
|
|
309
|
+
("SubnetDelegateStakeRewardsPercentage", "delegate_stake_percentage", [subnet_id]),
|
|
310
|
+
("LastSubnetDelegateStakeRewardsUpdate", "last_delegate_stake_rewards_update", [subnet_id]),
|
|
311
|
+
("NodeBurnRateAlpha", "node_burn_rate_alpha", [subnet_id]),
|
|
312
|
+
("CurrentNodeBurnRate", "current_node_burn_rate", [subnet_id]),
|
|
313
|
+
("SubnetRegistrationInitialColdkeys", "initial_coldkeys", [subnet_id]),
|
|
314
|
+
("InitialColdkeyData", "initial_coldkey_data", [subnet_id]),
|
|
315
|
+
("MaxRegisteredNodes", "max_registered_nodes", [subnet_id]),
|
|
316
|
+
("SubnetOwner", "owner", [subnet_id]),
|
|
317
|
+
("PendingSubnetOwner", "pending_owner", [subnet_id]),
|
|
318
|
+
("SubnetRegistrationEpoch", "registration_epoch", [subnet_id]),
|
|
319
|
+
("PreviousSubnetPauseEpoch", "prev_pause_epoch", [subnet_id]),
|
|
320
|
+
("SubnetKeyTypes", "key_types", [subnet_id]),
|
|
321
|
+
("SubnetSlot", "slot_index", [subnet_id]),
|
|
322
|
+
("SlotAssignment", "slot_assignment", [subnet_id]),
|
|
323
|
+
("SubnetNodeMinWeightDecreaseReputationThreshold", "subnet_node_min_weight_decrease_reputation_threshold", [subnet_id]),
|
|
324
|
+
("SubnetReputation", "reputation", [subnet_id]),
|
|
325
|
+
("MinSubnetNodeReputation", "min_subnet_node_reputation", [subnet_id]),
|
|
326
|
+
("AbsentDecreaseReputationFactor", "absent_decrease_reputation_factor", [subnet_id]),
|
|
327
|
+
("IncludedIncreaseReputationFactor", "included_increase_reputation_factor", [subnet_id]),
|
|
328
|
+
("BelowMinWeightDecreaseReputationFactor", "below_min_weight_decrease_reputation_factor", [subnet_id]),
|
|
329
|
+
("NonAttestorDecreaseReputationFactor", "non_attestor_decrease_reputation_factor", [subnet_id]),
|
|
330
|
+
("NonConsensusAttestorDecreaseReputationFactor", "non_consensus_attestor_decrease_reputation_factor", [subnet_id]),
|
|
331
|
+
("ValidatorAbsentSubnetNodeReputationFactor", "validator_absent_subnet_node_reputation_factor", [subnet_id]),
|
|
332
|
+
("ValidatorNonConsensusSubnetNodeReputationFactor", "validator_non_consensus_subnet_node_reputation_factor", [subnet_id]),
|
|
333
|
+
("SubnetBootnodeAccess", "bootnode_access", [subnet_id]),
|
|
334
|
+
("SubnetBootnodes", "bootnodes", [subnet_id]),
|
|
335
|
+
("TotalSubnetNodes", "total_nodes", [subnet_id]),
|
|
336
|
+
("TotalActiveSubnetNodes", "total_active_nodes", [subnet_id]),
|
|
337
|
+
("TotalSubnetElectableNodes", "total_electable_nodes", [subnet_id]),
|
|
338
|
+
("TotalSubnetStake", "total_subnet_stake", [subnet_id]),
|
|
339
|
+
("TotalSubnetDelegateStakeShares", "total_subnet_delegate_stake_shares", [subnet_id]),
|
|
340
|
+
("TotalSubnetDelegateStakeBalance", "total_subnet_delegate_stake_balance", [subnet_id]),
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
for storage_name, field_name, params in storage_items:
|
|
344
|
+
try:
|
|
345
|
+
result = self.substrate.query(
|
|
346
|
+
module="Network",
|
|
347
|
+
storage_function=storage_name,
|
|
348
|
+
params=params,
|
|
349
|
+
)
|
|
350
|
+
if result and result.value is not None:
|
|
351
|
+
storage_data[field_name] = result.value
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.debug(f"Failed to query {storage_name} for subnet {subnet_id}: {e}")
|
|
354
|
+
# Continue with other queries
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
logger.error(f"Error querying storage for subnet {subnet_id}: {e}")
|
|
358
|
+
|
|
359
|
+
return storage_data
|
|
360
|
+
|
|
361
|
+
def _get_min_subnet_delegate_stake_balance(self, subnet_id: int) -> int:
|
|
362
|
+
"""Get minimum subnet delegate stake balance.
|
|
363
|
+
|
|
364
|
+
This implements the logic from info.rs line 83.
|
|
365
|
+
Requires querying additional storage items to compute the value.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
subnet_id: The subnet ID
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Minimum delegate stake balance
|
|
372
|
+
"""
|
|
373
|
+
if not self.substrate:
|
|
374
|
+
return 0
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
# Query TotalSubnetDelegateStakeShares and TotalSubnetDelegateStakeBalance
|
|
378
|
+
shares_result = self.substrate.query(
|
|
379
|
+
module="Network",
|
|
380
|
+
storage_function="TotalSubnetDelegateStakeShares",
|
|
381
|
+
params=[subnet_id],
|
|
382
|
+
)
|
|
383
|
+
balance_result = self.substrate.query(
|
|
384
|
+
module="Network",
|
|
385
|
+
storage_function="TotalSubnetDelegateStakeBalance",
|
|
386
|
+
params=[subnet_id],
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
shares = shares_result.value if shares_result and shares_result.value else 0
|
|
390
|
+
balance = balance_result.value if balance_result and balance_result.value else 0
|
|
391
|
+
|
|
392
|
+
# Compute minimum: if shares > 0, return balance / shares, else 0
|
|
393
|
+
if shares > 0:
|
|
394
|
+
return balance // shares
|
|
395
|
+
return 0
|
|
396
|
+
except Exception as e:
|
|
397
|
+
logger.debug(f"Error computing min delegate stake for subnet {subnet_id}: {e}")
|
|
398
|
+
return 0
|
|
399
|
+
|
|
400
|
+
def _build_subnet_info_from_storage(self, subnet_id: int, storage_data: dict[str, Any]) -> Optional[SubnetInfo]:
|
|
401
|
+
"""Build SubnetInfo object from storage data.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
subnet_id: The subnet ID
|
|
405
|
+
storage_data: Dict of storage field names to values
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
SubnetInfo object or None if subnet_data is missing
|
|
409
|
+
"""
|
|
410
|
+
subnet_data = storage_data.get('subnet_data')
|
|
411
|
+
if not subnet_data:
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
# Extract values from subnet_data
|
|
415
|
+
decoded = {
|
|
416
|
+
"id": subnet_data.get("id", subnet_id),
|
|
417
|
+
"friendly_id": storage_data.get("friendly_id"),
|
|
418
|
+
"name": subnet_data.get("name", b""),
|
|
419
|
+
"repo": subnet_data.get("repo", b""),
|
|
420
|
+
"description": subnet_data.get("description", b""),
|
|
421
|
+
"misc": subnet_data.get("misc", b""),
|
|
422
|
+
"state": subnet_data.get("state"), # Will be decoded by _build_subnet_info
|
|
423
|
+
"start_epoch": subnet_data.get("start_epoch", 0),
|
|
424
|
+
"churn_limit": storage_data.get("churn_limit", 0),
|
|
425
|
+
"churn_limit_multiplier": storage_data.get("churn_limit_multiplier", 1),
|
|
426
|
+
"min_stake": storage_data.get("min_stake", 0),
|
|
427
|
+
"max_stake": storage_data.get("max_stake", 0),
|
|
428
|
+
"queue_immunity_epochs": storage_data.get("queue_immunity_epochs", 0),
|
|
429
|
+
"target_node_registrations_per_epoch": storage_data.get("target_node_registrations_per_epoch", 0),
|
|
430
|
+
"node_registrations_this_epoch": storage_data.get("node_registrations_this_epoch", 0),
|
|
431
|
+
"subnet_node_queue_epochs": storage_data.get("subnet_node_queue_epochs", 0),
|
|
432
|
+
"idle_classification_epochs": storage_data.get("idle_classification_epochs", 0),
|
|
433
|
+
"included_classification_epochs": storage_data.get("included_classification_epochs", 0),
|
|
434
|
+
"delegate_stake_percentage": storage_data.get("delegate_stake_percentage", 0),
|
|
435
|
+
"last_delegate_stake_rewards_update": storage_data.get("last_delegate_stake_rewards_update", 0),
|
|
436
|
+
"node_burn_rate_alpha": storage_data.get("node_burn_rate_alpha", 0),
|
|
437
|
+
"current_node_burn_rate": storage_data.get("current_node_burn_rate", 0),
|
|
438
|
+
"initial_coldkeys": storage_data.get("initial_coldkeys"),
|
|
439
|
+
"initial_coldkey_data": storage_data.get("initial_coldkey_data"),
|
|
440
|
+
"max_registered_nodes": storage_data.get("max_registered_nodes", 0),
|
|
441
|
+
"owner": storage_data.get("owner"),
|
|
442
|
+
"pending_owner": storage_data.get("pending_owner"),
|
|
443
|
+
"registration_epoch": storage_data.get("registration_epoch"),
|
|
444
|
+
"prev_pause_epoch": storage_data.get("prev_pause_epoch", 0),
|
|
445
|
+
"key_types": storage_data.get("key_types"),
|
|
446
|
+
"slot_index": storage_data.get("slot_index"),
|
|
447
|
+
"slot_assignment": storage_data.get("slot_assignment"),
|
|
448
|
+
"subnet_node_min_weight_decrease_reputation_threshold": storage_data.get("subnet_node_min_weight_decrease_reputation_threshold", 0),
|
|
449
|
+
"reputation": storage_data.get("reputation", 0),
|
|
450
|
+
"min_subnet_node_reputation": storage_data.get("min_subnet_node_reputation", 0),
|
|
451
|
+
"absent_decrease_reputation_factor": storage_data.get("absent_decrease_reputation_factor", 0),
|
|
452
|
+
"included_increase_reputation_factor": storage_data.get("included_increase_reputation_factor", 0),
|
|
453
|
+
"below_min_weight_decrease_reputation_factor": storage_data.get("below_min_weight_decrease_reputation_factor", 0),
|
|
454
|
+
"non_attestor_decrease_reputation_factor": storage_data.get("non_attestor_decrease_reputation_factor", 0),
|
|
455
|
+
"non_consensus_attestor_decrease_reputation_factor": storage_data.get("non_consensus_attestor_decrease_reputation_factor", 0),
|
|
456
|
+
"validator_absent_subnet_node_reputation_factor": storage_data.get("validator_absent_subnet_node_reputation_factor", 0),
|
|
457
|
+
"validator_non_consensus_subnet_node_reputation_factor": storage_data.get("validator_non_consensus_subnet_node_reputation_factor", 0),
|
|
458
|
+
"bootnode_access": storage_data.get("bootnode_access"),
|
|
459
|
+
"bootnodes": storage_data.get("bootnodes"),
|
|
460
|
+
"total_nodes": storage_data.get("total_nodes", 0),
|
|
461
|
+
"total_active_nodes": storage_data.get("total_active_nodes", 0),
|
|
462
|
+
"total_electable_nodes": storage_data.get("total_electable_nodes", 0),
|
|
463
|
+
"current_min_delegate_stake": self._get_min_subnet_delegate_stake_balance(subnet_id),
|
|
464
|
+
"total_subnet_stake": storage_data.get("total_subnet_stake", 0),
|
|
465
|
+
"total_subnet_delegate_stake_shares": storage_data.get("total_subnet_delegate_stake_shares", 0),
|
|
466
|
+
"total_subnet_delegate_stake_balance": storage_data.get("total_subnet_delegate_stake_balance", 0),
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
# Build SubnetInfo using existing _build_subnet_info method
|
|
470
|
+
return self._build_subnet_info(decoded)
|
|
471
|
+
|
|
472
|
+
def _get_subnet_info_from_storage(self, subnet_id: int) -> Optional[SubnetInfo]:
|
|
473
|
+
"""Get SubnetInfo by querying storage directly.
|
|
474
|
+
|
|
475
|
+
This replaces the RPC-based approach with storage queries.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
subnet_id: The subnet ID
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
SubnetInfo object or None if subnet not found
|
|
482
|
+
"""
|
|
483
|
+
if not self.substrate:
|
|
484
|
+
logger.error("Cannot get subnet info from storage: not connected to blockchain")
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
# Query all storage items
|
|
489
|
+
storage_data = self._query_subnet_storage_items(subnet_id)
|
|
490
|
+
|
|
491
|
+
# Build SubnetInfo from storage data
|
|
492
|
+
subnet_info = self._build_subnet_info_from_storage(subnet_id, storage_data)
|
|
493
|
+
|
|
494
|
+
return subnet_info
|
|
495
|
+
|
|
496
|
+
except Exception as e:
|
|
497
|
+
logger.error(f"Error getting subnet info from storage for {subnet_id}: {e}")
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
def _query_storage_for_missing_fields(
|
|
501
|
+
self, subnet_id: int, subnet_info: SubnetInfo
|
|
502
|
+
) -> SubnetInfo:
|
|
503
|
+
"""Query storage for missing fields that RPC might not return correctly.
|
|
504
|
+
|
|
505
|
+
This queries storage directly for:
|
|
506
|
+
- owner (SubnetOwner)
|
|
507
|
+
- pending_owner (PendingSubnetOwner)
|
|
508
|
+
- key_types (SubnetKeyTypes)
|
|
509
|
+
- bootnodes (SubnetBootnodes)
|
|
510
|
+
- bootnode_access (SubnetBootnodeAccess)
|
|
511
|
+
- initial_coldkeys (SubnetRegistrationInitialColdkeys)
|
|
512
|
+
- registration_epoch (SubnetRegistrationEpoch)
|
|
513
|
+
- slot_index (SubnetSlot)
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
subnet_id: The subnet ID
|
|
517
|
+
subnet_info: The SubnetInfo object to update
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
Updated SubnetInfo with storage-queried fields
|
|
521
|
+
"""
|
|
522
|
+
if not self.substrate:
|
|
523
|
+
return subnet_info
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
# Query owner if missing
|
|
527
|
+
if not subnet_info.owner:
|
|
528
|
+
try:
|
|
529
|
+
owner_result = self.substrate.query(
|
|
530
|
+
module="Network",
|
|
531
|
+
storage_function="SubnetOwner",
|
|
532
|
+
params=[subnet_id],
|
|
533
|
+
)
|
|
534
|
+
if owner_result and owner_result.value is not None:
|
|
535
|
+
owner_address = self._to_address(owner_result.value)
|
|
536
|
+
if (
|
|
537
|
+
owner_address
|
|
538
|
+
and owner_address
|
|
539
|
+
!= "0x0000000000000000000000000000000000000000"
|
|
540
|
+
):
|
|
541
|
+
subnet_info.owner = owner_address
|
|
542
|
+
except Exception as e:
|
|
543
|
+
logger.debug(
|
|
544
|
+
f"Failed to query SubnetOwner storage for subnet {subnet_id}: {e}"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Always query pending_owner from storage (RPC might not return it correctly)
|
|
548
|
+
# OptionQuery returns None if not set
|
|
549
|
+
try:
|
|
550
|
+
pending_owner_result = self.substrate.query(
|
|
551
|
+
module="Network",
|
|
552
|
+
storage_function="PendingSubnetOwner",
|
|
553
|
+
params=[subnet_id],
|
|
554
|
+
)
|
|
555
|
+
if pending_owner_result and pending_owner_result.value is not None:
|
|
556
|
+
pending_owner_address = self._to_address(
|
|
557
|
+
pending_owner_result.value
|
|
558
|
+
)
|
|
559
|
+
if (
|
|
560
|
+
pending_owner_address
|
|
561
|
+
and pending_owner_address
|
|
562
|
+
!= "0x0000000000000000000000000000000000000000"
|
|
563
|
+
):
|
|
564
|
+
subnet_info.pending_owner = pending_owner_address
|
|
565
|
+
else:
|
|
566
|
+
# Explicitly set to None if zero address
|
|
567
|
+
subnet_info.pending_owner = None
|
|
568
|
+
else:
|
|
569
|
+
# No pending owner in storage
|
|
570
|
+
subnet_info.pending_owner = None
|
|
571
|
+
except Exception as e:
|
|
572
|
+
logger.debug(
|
|
573
|
+
f"Failed to query PendingSubnetOwner storage for subnet {subnet_id}: {e}"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Always query key_types from storage (RPC might not return it correctly)
|
|
577
|
+
# ValueQuery returns [] (empty list) if not set, not None
|
|
578
|
+
try:
|
|
579
|
+
key_types_result = self.substrate.query(
|
|
580
|
+
module="Network",
|
|
581
|
+
storage_function="SubnetKeyTypes",
|
|
582
|
+
params=[subnet_id],
|
|
583
|
+
)
|
|
584
|
+
if key_types_result and hasattr(key_types_result, "value"):
|
|
585
|
+
raw_value = key_types_result.value
|
|
586
|
+
# ValueQuery always returns a value (empty list [] if not set)
|
|
587
|
+
decoded_key_types = self._decode_key_types(raw_value)
|
|
588
|
+
# Always update, even if empty set (to ensure we have the latest from storage)
|
|
589
|
+
subnet_info.key_types = decoded_key_types
|
|
590
|
+
logger.debug(
|
|
591
|
+
f"Subnet {subnet_id} key_types from storage: {decoded_key_types}"
|
|
592
|
+
)
|
|
593
|
+
else:
|
|
594
|
+
# No result - set empty set
|
|
595
|
+
subnet_info.key_types = set()
|
|
596
|
+
except Exception as e:
|
|
597
|
+
logger.warning(
|
|
598
|
+
f"Failed to query SubnetKeyTypes storage for subnet {subnet_id}: {e}"
|
|
599
|
+
)
|
|
600
|
+
# Set empty set on error to ensure field is initialized
|
|
601
|
+
subnet_info.key_types = set()
|
|
602
|
+
|
|
603
|
+
# Always query bootnodes from storage (RPC might not return it correctly)
|
|
604
|
+
# ValueQuery returns [] (empty list) if not set, not None
|
|
605
|
+
try:
|
|
606
|
+
bootnodes_result = self.substrate.query(
|
|
607
|
+
module="Network",
|
|
608
|
+
storage_function="SubnetBootnodes",
|
|
609
|
+
params=[subnet_id],
|
|
610
|
+
)
|
|
611
|
+
if bootnodes_result and hasattr(bootnodes_result, "value"):
|
|
612
|
+
raw_value = bootnodes_result.value
|
|
613
|
+
# ValueQuery always returns a value (empty list [] if not set)
|
|
614
|
+
decoded_bootnodes = self._decode_bootnodes(raw_value)
|
|
615
|
+
# Always update, even if empty set (to ensure we have the latest from storage)
|
|
616
|
+
subnet_info.bootnodes = decoded_bootnodes
|
|
617
|
+
logger.debug(
|
|
618
|
+
f"Subnet {subnet_id} bootnodes from storage: {len(decoded_bootnodes)} bootnodes"
|
|
619
|
+
)
|
|
620
|
+
else:
|
|
621
|
+
# No result - set empty set
|
|
622
|
+
subnet_info.bootnodes = set()
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.warning(
|
|
625
|
+
f"Failed to query SubnetBootnodes storage for subnet {subnet_id}: {e}"
|
|
626
|
+
)
|
|
627
|
+
# Set empty set on error to ensure field is initialized
|
|
628
|
+
subnet_info.bootnodes = set()
|
|
629
|
+
|
|
630
|
+
# Always query bootnode_access from storage (RPC might not return it correctly)
|
|
631
|
+
# ValueQuery returns [] (empty list) if not set, not None
|
|
632
|
+
try:
|
|
633
|
+
bootnode_access_result = self.substrate.query(
|
|
634
|
+
module="Network",
|
|
635
|
+
storage_function="SubnetBootnodeAccess",
|
|
636
|
+
params=[subnet_id],
|
|
637
|
+
)
|
|
638
|
+
if bootnode_access_result and hasattr(bootnode_access_result, "value"):
|
|
639
|
+
raw_value = bootnode_access_result.value
|
|
640
|
+
# ValueQuery always returns a value (empty list [] if not set)
|
|
641
|
+
decoded_bootnode_access = self._decode_bootnode_access(raw_value)
|
|
642
|
+
# Always update, even if empty set (to ensure we have the latest from storage)
|
|
643
|
+
subnet_info.bootnode_access = decoded_bootnode_access
|
|
644
|
+
logger.debug(
|
|
645
|
+
f"Subnet {subnet_id} bootnode_access from storage: {len(decoded_bootnode_access)} addresses"
|
|
646
|
+
)
|
|
647
|
+
else:
|
|
648
|
+
# No result - set empty set
|
|
649
|
+
subnet_info.bootnode_access = set()
|
|
650
|
+
except Exception as e:
|
|
651
|
+
logger.warning(
|
|
652
|
+
f"Failed to query SubnetBootnodeAccess storage for subnet {subnet_id}: {e}"
|
|
653
|
+
)
|
|
654
|
+
# Set empty set on error to ensure field is initialized
|
|
655
|
+
subnet_info.bootnode_access = set()
|
|
656
|
+
|
|
657
|
+
# Query initial_coldkeys if missing
|
|
658
|
+
if not subnet_info.initial_coldkeys:
|
|
659
|
+
try:
|
|
660
|
+
initial_coldkeys_result = self.substrate.query(
|
|
661
|
+
module="Network",
|
|
662
|
+
storage_function="SubnetRegistrationInitialColdkeys",
|
|
663
|
+
params=[subnet_id],
|
|
664
|
+
)
|
|
665
|
+
if (
|
|
666
|
+
initial_coldkeys_result
|
|
667
|
+
and initial_coldkeys_result.value is not None
|
|
668
|
+
):
|
|
669
|
+
raw_value = initial_coldkeys_result.value
|
|
670
|
+
decoded_initial_coldkeys = self._decode_initial_coldkeys(
|
|
671
|
+
raw_value
|
|
672
|
+
)
|
|
673
|
+
if decoded_initial_coldkeys:
|
|
674
|
+
subnet_info.initial_coldkeys = decoded_initial_coldkeys
|
|
675
|
+
except Exception as e:
|
|
676
|
+
logger.debug(
|
|
677
|
+
f"Failed to query SubnetRegistrationInitialColdkeys storage for subnet {subnet_id}: {e}"
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Query registration_epoch if missing
|
|
681
|
+
if subnet_info.registration_epoch is None:
|
|
682
|
+
try:
|
|
683
|
+
registration_epoch_result = self.substrate.query(
|
|
684
|
+
module="Network",
|
|
685
|
+
storage_function="SubnetRegistrationEpoch",
|
|
686
|
+
params=[subnet_id],
|
|
687
|
+
)
|
|
688
|
+
if (
|
|
689
|
+
registration_epoch_result
|
|
690
|
+
and registration_epoch_result.value is not None
|
|
691
|
+
):
|
|
692
|
+
subnet_info.registration_epoch = registration_epoch_result.value
|
|
693
|
+
except Exception as e:
|
|
694
|
+
logger.debug(
|
|
695
|
+
f"Failed to query SubnetRegistrationEpoch storage for subnet {subnet_id}: {e}"
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# Query slot_index if missing
|
|
699
|
+
if subnet_info.slot_index is None:
|
|
700
|
+
try:
|
|
701
|
+
slot_index_result = self.substrate.query(
|
|
702
|
+
module="Network",
|
|
703
|
+
storage_function="SubnetSlot",
|
|
704
|
+
params=[subnet_id],
|
|
705
|
+
)
|
|
706
|
+
if slot_index_result and slot_index_result.value is not None:
|
|
707
|
+
subnet_info.slot_index = slot_index_result.value
|
|
708
|
+
except Exception as e:
|
|
709
|
+
logger.debug(
|
|
710
|
+
f"Failed to query SubnetSlot storage for subnet {subnet_id}: {e}"
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
except Exception as e:
|
|
714
|
+
logger.warning(
|
|
715
|
+
f"Error querying storage for missing fields for subnet {subnet_id}: {e}"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
return subnet_info
|
|
719
|
+
|
|
720
|
+
@staticmethod
|
|
721
|
+
def _decode_state(value) -> SubnetState:
|
|
722
|
+
if value is None:
|
|
723
|
+
return SubnetState.Registered
|
|
724
|
+
if isinstance(value, SubnetState):
|
|
725
|
+
return value
|
|
726
|
+
if hasattr(value, "name"):
|
|
727
|
+
return SubnetState(value.name)
|
|
728
|
+
if isinstance(value, str):
|
|
729
|
+
try:
|
|
730
|
+
return SubnetState(value)
|
|
731
|
+
except ValueError:
|
|
732
|
+
return SubnetState[value]
|
|
733
|
+
if isinstance(value, dict) and value:
|
|
734
|
+
key = next(iter(value))
|
|
735
|
+
return SubnetState(key)
|
|
736
|
+
return SubnetState.Registered
|
|
737
|
+
|
|
738
|
+
@classmethod
|
|
739
|
+
def _build_subnet_info(cls, decoded: dict) -> SubnetInfo:
|
|
740
|
+
friendly_id = None
|
|
741
|
+
if "friendly_id" in decoded or "friendlyId" in decoded:
|
|
742
|
+
friendly_id = decoded.get("friendly_id", decoded.get("friendlyId"))
|
|
743
|
+
else:
|
|
744
|
+
friendly_id = decoded.get("id")
|
|
745
|
+
|
|
746
|
+
# Decode owner - handle Option<AccountId> properly
|
|
747
|
+
raw_owner = decoded.get("owner")
|
|
748
|
+
owner = cls._to_address(raw_owner)
|
|
749
|
+
|
|
750
|
+
# Decode pending_owner
|
|
751
|
+
raw_pending_owner = decoded.get("pending_owner")
|
|
752
|
+
pending_owner = cls._to_address(raw_pending_owner)
|
|
753
|
+
|
|
754
|
+
# Extract raw values for decoding
|
|
755
|
+
raw_key_types = decoded.get("key_types")
|
|
756
|
+
raw_bootnode_access = decoded.get("bootnode_access")
|
|
757
|
+
raw_bootnodes = decoded.get("bootnodes")
|
|
758
|
+
|
|
759
|
+
return SubnetInfo(
|
|
760
|
+
id=decoded["id"],
|
|
761
|
+
friendly_id=friendly_id,
|
|
762
|
+
name=cls._to_bytes(decoded.get("name")),
|
|
763
|
+
repo=cls._to_bytes(decoded.get("repo")),
|
|
764
|
+
description=cls._to_bytes(decoded.get("description")),
|
|
765
|
+
misc=cls._to_bytes(decoded.get("misc")),
|
|
766
|
+
state=cls._decode_state(decoded.get("state")),
|
|
767
|
+
start_epoch=decoded.get("start_epoch", 0),
|
|
768
|
+
churn_limit=decoded.get("churn_limit", 0),
|
|
769
|
+
churn_limit_multiplier=decoded.get("churn_limit_multiplier", 1),
|
|
770
|
+
min_stake=decoded.get("min_stake", 0),
|
|
771
|
+
max_stake=decoded.get("max_stake", 0),
|
|
772
|
+
queue_immunity_epochs=decoded.get("queue_immunity_epochs", 0),
|
|
773
|
+
target_node_registrations_per_epoch=decoded.get(
|
|
774
|
+
"target_node_registrations_per_epoch", 0
|
|
775
|
+
),
|
|
776
|
+
node_registrations_this_epoch=decoded.get("node_registrations_this_epoch", 0),
|
|
777
|
+
subnet_node_queue_epochs=decoded.get("subnet_node_queue_epochs", 0),
|
|
778
|
+
idle_classification_epochs=decoded.get("idle_classification_epochs", 0),
|
|
779
|
+
included_classification_epochs=decoded.get(
|
|
780
|
+
"included_classification_epochs", 0
|
|
781
|
+
),
|
|
782
|
+
delegate_stake_percentage=decoded.get("delegate_stake_percentage", 0),
|
|
783
|
+
last_delegate_stake_rewards_update=decoded.get("last_delegate_stake_rewards_update", 0),
|
|
784
|
+
node_burn_rate_alpha=decoded.get("node_burn_rate_alpha", 0),
|
|
785
|
+
current_node_burn_rate=decoded.get("current_node_burn_rate", 0),
|
|
786
|
+
initial_coldkeys=cls._decode_initial_coldkeys(
|
|
787
|
+
decoded.get("initial_coldkeys")
|
|
788
|
+
),
|
|
789
|
+
initial_coldkey_data=cls._decode_initial_coldkeys(
|
|
790
|
+
decoded.get("initial_coldkey_data")
|
|
791
|
+
),
|
|
792
|
+
max_registered_nodes=decoded.get("max_registered_nodes", 0),
|
|
793
|
+
owner=owner,
|
|
794
|
+
pending_owner=pending_owner,
|
|
795
|
+
registration_epoch=decoded.get("registration_epoch"),
|
|
796
|
+
prev_pause_epoch=decoded.get("prev_pause_epoch", 0),
|
|
797
|
+
key_types=cls._decode_key_types(raw_key_types),
|
|
798
|
+
slot_index=decoded.get("slot_index"),
|
|
799
|
+
slot_assignment=decoded.get("slot_assignment"),
|
|
800
|
+
subnet_node_min_weight_decrease_reputation_threshold=decoded.get(
|
|
801
|
+
"subnet_node_min_weight_decrease_reputation_threshold", 0
|
|
802
|
+
),
|
|
803
|
+
reputation=decoded.get("reputation", 0),
|
|
804
|
+
min_subnet_node_reputation=decoded.get("min_subnet_node_reputation", 0),
|
|
805
|
+
absent_decrease_reputation_factor=decoded.get(
|
|
806
|
+
"absent_decrease_reputation_factor", 0
|
|
807
|
+
),
|
|
808
|
+
included_increase_reputation_factor=decoded.get(
|
|
809
|
+
"included_increase_reputation_factor", 0
|
|
810
|
+
),
|
|
811
|
+
below_min_weight_decrease_reputation_factor=decoded.get(
|
|
812
|
+
"below_min_weight_decrease_reputation_factor", 0
|
|
813
|
+
),
|
|
814
|
+
non_attestor_decrease_reputation_factor=decoded.get(
|
|
815
|
+
"non_attestor_decrease_reputation_factor", 0
|
|
816
|
+
),
|
|
817
|
+
non_consensus_attestor_decrease_reputation_factor=decoded.get(
|
|
818
|
+
"non_consensus_attestor_decrease_reputation_factor", 0
|
|
819
|
+
),
|
|
820
|
+
validator_absent_subnet_node_reputation_factor=decoded.get(
|
|
821
|
+
"validator_absent_subnet_node_reputation_factor", 0
|
|
822
|
+
),
|
|
823
|
+
validator_non_consensus_subnet_node_reputation_factor=decoded.get(
|
|
824
|
+
"validator_non_consensus_subnet_node_reputation_factor", 0
|
|
825
|
+
),
|
|
826
|
+
bootnode_access=cls._decode_bootnode_access(raw_bootnode_access),
|
|
827
|
+
bootnodes=cls._decode_bootnodes(raw_bootnodes),
|
|
828
|
+
total_nodes=decoded.get("total_nodes", 0),
|
|
829
|
+
total_active_nodes=decoded.get("total_active_nodes", 0),
|
|
830
|
+
total_electable_nodes=decoded.get("total_electable_nodes", 0),
|
|
831
|
+
current_min_delegate_stake=decoded.get("current_min_delegate_stake", 0),
|
|
832
|
+
total_subnet_stake=decoded.get("total_subnet_stake", 0),
|
|
833
|
+
total_subnet_delegate_stake_shares=decoded.get("total_subnet_delegate_stake_shares", 0),
|
|
834
|
+
total_subnet_delegate_stake_balance=decoded.get("total_subnet_delegate_stake_balance", 0),
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
def get_subnet_info(self, subnet_id: int) -> Optional[SubnetInfo]:
|
|
838
|
+
"""
|
|
839
|
+
Get complete subnet information via RPC, with storage fallback.
|
|
840
|
+
|
|
841
|
+
This uses the network_getSubnetInfo RPC method which returns SubnetInfo
|
|
842
|
+
serialized as SCALE bytes. Falls back to storage queries if RPC fails.
|
|
843
|
+
"""
|
|
844
|
+
if self.substrate is None:
|
|
845
|
+
logger.error("Cannot get subnet info: not connected to blockchain")
|
|
846
|
+
return None
|
|
847
|
+
|
|
848
|
+
try:
|
|
849
|
+
# Try RPC method first (most efficient)
|
|
850
|
+
logger.debug(f"Attempting network_getSubnetInfo RPC for subnet {subnet_id}")
|
|
851
|
+
result = self.substrate.rpc_request(
|
|
852
|
+
method="network_getSubnetInfo", params=[subnet_id]
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
if result and result.get("result"):
|
|
856
|
+
from ...utils.blockchain.type_registry import decode_option_subnet_info
|
|
857
|
+
|
|
858
|
+
result_data = result["result"]
|
|
859
|
+
# Handle different data formats
|
|
860
|
+
if isinstance(result_data, list):
|
|
861
|
+
data_bytes = bytes(result_data)
|
|
862
|
+
elif isinstance(result_data, str):
|
|
863
|
+
hex_data = result_data[2:] if result_data.startswith("0x") else result_data
|
|
864
|
+
data_bytes = bytes.fromhex(hex_data)
|
|
865
|
+
else:
|
|
866
|
+
data_bytes = result_data
|
|
867
|
+
|
|
868
|
+
decoded = decode_option_subnet_info(data_bytes)
|
|
869
|
+
|
|
870
|
+
if decoded:
|
|
871
|
+
logger.info(f"Successfully decoded subnet {subnet_id} via RPC")
|
|
872
|
+
subnet_info = self._build_subnet_info(decoded)
|
|
873
|
+
# Query storage for any fields that might be missing from RPC
|
|
874
|
+
subnet_info = self._query_storage_for_missing_fields(subnet_id, subnet_info)
|
|
875
|
+
return subnet_info
|
|
876
|
+
else:
|
|
877
|
+
logger.debug(f"Subnet {subnet_id} not found via RPC (Option::None)")
|
|
878
|
+
return None
|
|
879
|
+
|
|
880
|
+
except Exception as e:
|
|
881
|
+
logger.debug(f"RPC method failed for subnet {subnet_id}, falling back to storage: {e}")
|
|
882
|
+
|
|
883
|
+
# Fallback: Use storage-based query
|
|
884
|
+
try:
|
|
885
|
+
logger.debug(f"Falling back to storage query for subnet {subnet_id}")
|
|
886
|
+
subnet_info = self._get_subnet_info_from_storage(subnet_id)
|
|
887
|
+
return subnet_info
|
|
888
|
+
|
|
889
|
+
except Exception as e:
|
|
890
|
+
logger.error(f"Error getting subnet info from storage for {subnet_id}: {e}")
|
|
891
|
+
return None
|
|
892
|
+
|
|
893
|
+
def get_all_subnets_info(self) -> list[SubnetInfo]:
|
|
894
|
+
"""
|
|
895
|
+
Get information about all subnets by querying storage directly.
|
|
896
|
+
|
|
897
|
+
This method queries SubnetsData storage to find all existing subnets.
|
|
898
|
+
It handles gaps in subnet IDs (e.g., if subnet 1 is removed, we still find 2, 3, 4, 5).
|
|
899
|
+
|
|
900
|
+
This method includes ALL subnets regardless of their state:
|
|
901
|
+
- Active subnets (state = Active)
|
|
902
|
+
- Inactive/Registered subnets (state = Registered)
|
|
903
|
+
- Paused subnets (state = Paused)
|
|
904
|
+
- Subnets whose status has changed (current state is read from storage)
|
|
905
|
+
|
|
906
|
+
Only subnets that don't exist in SubnetsData storage are excluded.
|
|
907
|
+
|
|
908
|
+
Strategy:
|
|
909
|
+
1. First tries RPC method network_getAllSubnetsInfo (most efficient, uses SubnetsData::iter())
|
|
910
|
+
2. Falls back to storage discovery method that scans SubnetsData to find all existing subnet IDs
|
|
911
|
+
"""
|
|
912
|
+
if self.substrate is None:
|
|
913
|
+
logger.error("Cannot get subnets info: not connected to blockchain")
|
|
914
|
+
return []
|
|
915
|
+
|
|
916
|
+
try:
|
|
917
|
+
# First, try to use the RPC method if available (most efficient)
|
|
918
|
+
# This uses SubnetsData::iter() internally, which finds all existing subnets
|
|
919
|
+
try:
|
|
920
|
+
result = self.substrate.rpc_request(
|
|
921
|
+
method="network_getAllSubnetsInfo", params=[]
|
|
922
|
+
)
|
|
923
|
+
if result and result.get("result"):
|
|
924
|
+
logger.debug("Using RPC method network_getAllSubnetsInfo")
|
|
925
|
+
return self._get_all_subnets_info_legacy_rpc()
|
|
926
|
+
except Exception as e:
|
|
927
|
+
logger.debug(f"RPC method not available, falling back to storage discovery: {e}")
|
|
928
|
+
|
|
929
|
+
# Fallback: Use storage discovery method that scans SubnetsData storage
|
|
930
|
+
# This properly handles gaps (e.g., if subnet 1 was removed)
|
|
931
|
+
logger.debug("Using storage discovery method to find all subnet IDs")
|
|
932
|
+
return self._get_all_subnets_info_individual()
|
|
933
|
+
|
|
934
|
+
except Exception as e:
|
|
935
|
+
logger.error(f"Error getting all subnets info from storage: {e}")
|
|
936
|
+
return []
|
|
937
|
+
|
|
938
|
+
def _get_all_subnets_info_legacy_rpc(self) -> list[SubnetInfo]:
|
|
939
|
+
"""
|
|
940
|
+
Legacy RPC-based method (kept for reference, not used).
|
|
941
|
+
|
|
942
|
+
Get information about all subnets using the network_getAllSubnetsInfo RPC method.
|
|
943
|
+
|
|
944
|
+
This uses the proper RPC method that returns Vec<SubnetInfo>, which is more efficient
|
|
945
|
+
than querying subnets individually. Falls back to individual queries if Vec decoding fails.
|
|
946
|
+
"""
|
|
947
|
+
if self.substrate is None:
|
|
948
|
+
logger.error("Cannot get subnets info: not connected to blockchain")
|
|
949
|
+
return []
|
|
950
|
+
|
|
951
|
+
try:
|
|
952
|
+
# Try using the proper RPC method first
|
|
953
|
+
logger.debug(
|
|
954
|
+
"Attempting to get all subnets via network_getAllSubnetsInfo RPC..."
|
|
955
|
+
)
|
|
956
|
+
result = self.substrate.rpc_request(
|
|
957
|
+
method="network_getAllSubnetsInfo", params=[]
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
if result and result.get("result"):
|
|
961
|
+
result_data = result["result"]
|
|
962
|
+
|
|
963
|
+
from ...utils.blockchain.type_registry import decode_vec_subnet_info
|
|
964
|
+
|
|
965
|
+
decoded_list = decode_vec_subnet_info(result_data)
|
|
966
|
+
|
|
967
|
+
logger.info(
|
|
968
|
+
f"Decoded {len(decoded_list) if decoded_list else 0} subnets from RPC response"
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
if decoded_list:
|
|
972
|
+
# Successfully decoded Vec<SubnetInfo> - convert to SubnetInfo models
|
|
973
|
+
subnets: list[SubnetInfo] = []
|
|
974
|
+
for decoded in decoded_list:
|
|
975
|
+
subnet_id = decoded.get("id", "?")
|
|
976
|
+
logger.debug(f"Processing decoded subnet ID: {subnet_id}")
|
|
977
|
+
try:
|
|
978
|
+
subnet_info = self._build_subnet_info(decoded)
|
|
979
|
+
|
|
980
|
+
# Query storage for missing fields (owner, pending_owner, key_types, bootnodes, bootnode_access)
|
|
981
|
+
subnet_id_val = decoded.get("id", subnet_id)
|
|
982
|
+
subnet_info = self._query_storage_for_missing_fields(
|
|
983
|
+
subnet_id_val, subnet_info
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
subnets.append(subnet_info)
|
|
987
|
+
except ValueError as validation_error:
|
|
988
|
+
# Validation error - log and try to fix or skip
|
|
989
|
+
error_msg = str(validation_error)
|
|
990
|
+
|
|
991
|
+
# If it's a node count validation issue, try to fix it
|
|
992
|
+
if (
|
|
993
|
+
"total_active_nodes" in error_msg
|
|
994
|
+
or "total_electable_nodes" in error_msg
|
|
995
|
+
):
|
|
996
|
+
logger.warning(
|
|
997
|
+
f"Subnet {subnet_id} has invalid node counts - attempting to fix: {error_msg}"
|
|
998
|
+
)
|
|
999
|
+
# Try to fix by ensuring node counts are logically consistent
|
|
1000
|
+
total_nodes = decoded.get("total_nodes", 0)
|
|
1001
|
+
total_active = decoded.get("total_active_nodes", 0)
|
|
1002
|
+
total_electable = decoded.get(
|
|
1003
|
+
"total_electable_nodes", 0
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
# Clamp values to be valid
|
|
1007
|
+
if total_active > total_nodes:
|
|
1008
|
+
logger.warning(
|
|
1009
|
+
f"Subnet {subnet_id}: Fixing total_active_nodes ({total_active}) > total_nodes ({total_nodes})"
|
|
1010
|
+
)
|
|
1011
|
+
decoded["total_active_nodes"] = min(
|
|
1012
|
+
total_active, total_nodes
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
if total_electable > decoded.get(
|
|
1016
|
+
"total_active_nodes", 0
|
|
1017
|
+
):
|
|
1018
|
+
logger.warning(
|
|
1019
|
+
f"Subnet {subnet_id}: Fixing total_electable_nodes ({total_electable}) > total_active_nodes"
|
|
1020
|
+
)
|
|
1021
|
+
decoded["total_electable_nodes"] = min(
|
|
1022
|
+
total_electable,
|
|
1023
|
+
decoded.get("total_active_nodes", 0),
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
# Try again with fixed values
|
|
1027
|
+
try:
|
|
1028
|
+
subnet_info = self._build_subnet_info(decoded)
|
|
1029
|
+
# Query storage for missing fields after fixing node counts
|
|
1030
|
+
subnet_info = (
|
|
1031
|
+
self._query_storage_for_missing_fields(
|
|
1032
|
+
subnet_id, subnet_info
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
subnets.append(subnet_info)
|
|
1036
|
+
except Exception as retry_error:
|
|
1037
|
+
logger.error(
|
|
1038
|
+
f"Failed to convert subnet {subnet_id} even after fixing node counts: {retry_error}"
|
|
1039
|
+
)
|
|
1040
|
+
continue
|
|
1041
|
+
else:
|
|
1042
|
+
logger.error(
|
|
1043
|
+
f"Failed to convert subnet {subnet_id}: {validation_error}. Skipping."
|
|
1044
|
+
)
|
|
1045
|
+
continue
|
|
1046
|
+
except Exception as conversion_error:
|
|
1047
|
+
logger.error(
|
|
1048
|
+
f"Failed to convert subnet {decoded.get('id', '?')}: {conversion_error}"
|
|
1049
|
+
)
|
|
1050
|
+
continue
|
|
1051
|
+
|
|
1052
|
+
logger.info(
|
|
1053
|
+
f"Successfully retrieved {len(subnets)} subnet(s) via RPC Vec<SubnetInfo>"
|
|
1054
|
+
)
|
|
1055
|
+
return subnets
|
|
1056
|
+
else:
|
|
1057
|
+
logger.warning(
|
|
1058
|
+
"RPC returned data but Vec decoding failed, falling back to individual queries"
|
|
1059
|
+
)
|
|
1060
|
+
else:
|
|
1061
|
+
logger.warning(
|
|
1062
|
+
"RPC method returned no result, falling back to individual queries"
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
except Exception as e:
|
|
1066
|
+
logger.warning(
|
|
1067
|
+
f"Error using network_getAllSubnetsInfo RPC: {e}, falling back to individual queries"
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
# Fallback: query subnets individually
|
|
1071
|
+
logger.debug("Falling back to individual subnet queries...")
|
|
1072
|
+
return self._get_all_subnets_info_individual()
|
|
1073
|
+
|
|
1074
|
+
def _get_all_subnets_info_individual(self) -> list[SubnetInfo]:
|
|
1075
|
+
"""
|
|
1076
|
+
Fallback method: Get information about all subnets by discovering subnet IDs from storage
|
|
1077
|
+
and then querying each individually using storage queries.
|
|
1078
|
+
|
|
1079
|
+
This method:
|
|
1080
|
+
1. Queries TotalSubnetUids to get the maximum possible subnet ID
|
|
1081
|
+
2. Scans SubnetsData storage to discover all actual subnet IDs (handles non-sequential IDs)
|
|
1082
|
+
3. Queries each discovered subnet via storage queries
|
|
1083
|
+
|
|
1084
|
+
This handles cases where subnets don't start at ID 1.
|
|
1085
|
+
"""
|
|
1086
|
+
try:
|
|
1087
|
+
logger.debug("Discovering subnet IDs from storage...")
|
|
1088
|
+
|
|
1089
|
+
# Step 1: Get TotalSubnetUids to know the maximum possible subnet ID
|
|
1090
|
+
total_subnets_result = self.substrate.query(
|
|
1091
|
+
module="Network", storage_function="TotalSubnetUids"
|
|
1092
|
+
)
|
|
1093
|
+
total_subnet_uids = (
|
|
1094
|
+
total_subnets_result.value if total_subnets_result else 0
|
|
1095
|
+
)
|
|
1096
|
+
logger.debug(f"TotalSubnetUids: {total_subnet_uids}")
|
|
1097
|
+
|
|
1098
|
+
# Step 2: Discover all actual subnet IDs by querying SubnetsData storage
|
|
1099
|
+
# This handles non-sequential subnet IDs (e.g., if subnets start at ID 21)
|
|
1100
|
+
subnet_ids = set()
|
|
1101
|
+
max_subnet_id = max(
|
|
1102
|
+
total_subnet_uids, 50
|
|
1103
|
+
) # Check at least up to TotalSubnetUids or 50
|
|
1104
|
+
|
|
1105
|
+
logger.debug(
|
|
1106
|
+
f"Scanning SubnetsData storage from ID 1 to {max_subnet_id}..."
|
|
1107
|
+
)
|
|
1108
|
+
for test_id in range(1, max_subnet_id + 1):
|
|
1109
|
+
try:
|
|
1110
|
+
result = self.substrate.query(
|
|
1111
|
+
module="Network",
|
|
1112
|
+
storage_function="SubnetsData",
|
|
1113
|
+
params=[test_id],
|
|
1114
|
+
)
|
|
1115
|
+
if result and result.value is not None:
|
|
1116
|
+
subnet_ids.add(test_id)
|
|
1117
|
+
logger.debug(f"Found subnet ID in storage: {test_id}")
|
|
1118
|
+
except Exception:
|
|
1119
|
+
# Subnet doesn't exist, continue silently
|
|
1120
|
+
pass
|
|
1121
|
+
|
|
1122
|
+
# If we didn't find any, try a broader scan
|
|
1123
|
+
if not subnet_ids:
|
|
1124
|
+
logger.warning(
|
|
1125
|
+
"No subnets found in initial scan, trying broader range (1-200)..."
|
|
1126
|
+
)
|
|
1127
|
+
for test_id in range(1, 201):
|
|
1128
|
+
try:
|
|
1129
|
+
result = self.substrate.query(
|
|
1130
|
+
module="Network",
|
|
1131
|
+
storage_function="SubnetsData",
|
|
1132
|
+
params=[test_id],
|
|
1133
|
+
)
|
|
1134
|
+
if result and result.value is not None:
|
|
1135
|
+
subnet_ids.add(test_id)
|
|
1136
|
+
logger.debug(f"Found subnet ID in storage: {test_id}")
|
|
1137
|
+
except Exception:
|
|
1138
|
+
pass
|
|
1139
|
+
|
|
1140
|
+
if not subnet_ids:
|
|
1141
|
+
logger.warning(
|
|
1142
|
+
"No subnets found in storage, cannot retrieve subnet info"
|
|
1143
|
+
)
|
|
1144
|
+
return []
|
|
1145
|
+
|
|
1146
|
+
logger.info(
|
|
1147
|
+
f"Discovered {len(subnet_ids)} subnet ID(s) from storage: {sorted(subnet_ids)}"
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
# Step 3: Query each discovered subnet ID using storage queries
|
|
1151
|
+
subnets: list[SubnetInfo] = []
|
|
1152
|
+
|
|
1153
|
+
for subnet_id in sorted(subnet_ids):
|
|
1154
|
+
logger.debug(f"Querying subnet {subnet_id} via storage...")
|
|
1155
|
+
|
|
1156
|
+
# Query individual subnet using storage queries
|
|
1157
|
+
try:
|
|
1158
|
+
subnet_info = self._get_subnet_info_from_storage(subnet_id)
|
|
1159
|
+
if subnet_info:
|
|
1160
|
+
subnets.append(subnet_info)
|
|
1161
|
+
logger.debug(f"Successfully retrieved subnet {subnet_id}")
|
|
1162
|
+
else:
|
|
1163
|
+
logger.warning(f"No subnet info returned for subnet {subnet_id}, skipping")
|
|
1164
|
+
except Exception as e:
|
|
1165
|
+
logger.warning(f"Error querying subnet {subnet_id} from storage: {e}, skipping")
|
|
1166
|
+
continue
|
|
1167
|
+
|
|
1168
|
+
logger.info(
|
|
1169
|
+
f"Retrieved {len(subnets)} subnet(s) by querying individually after storage discovery"
|
|
1170
|
+
)
|
|
1171
|
+
return subnets
|
|
1172
|
+
|
|
1173
|
+
except Exception as e:
|
|
1174
|
+
logger.error(f"Error getting all subnets info individually: {e}")
|
|
1175
|
+
return []
|
|
1176
|
+
|
|
1177
|
+
def get_subnet_data(self, subnet_id: int) -> Optional[SubnetData]:
|
|
1178
|
+
"""
|
|
1179
|
+
Get basic subnet data using storage query.
|
|
1180
|
+
|
|
1181
|
+
Args:
|
|
1182
|
+
subnet_id: The subnet ID to query
|
|
1183
|
+
|
|
1184
|
+
Returns:
|
|
1185
|
+
SubnetData if found, None otherwise
|
|
1186
|
+
"""
|
|
1187
|
+
try:
|
|
1188
|
+
# Query subnet data from storage
|
|
1189
|
+
result = self.substrate.query(
|
|
1190
|
+
module="Network", storage_function="SubnetsData", params=[subnet_id]
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
if not result.value:
|
|
1194
|
+
return None
|
|
1195
|
+
|
|
1196
|
+
# Convert to SubnetData using scalecodec
|
|
1197
|
+
# The result.value is already decoded by substrate-interface
|
|
1198
|
+
# but we can re-encode and decode with scalecodec for consistency
|
|
1199
|
+
return SubnetData(
|
|
1200
|
+
id=result.value["id"],
|
|
1201
|
+
friendly_id=(
|
|
1202
|
+
result.value["friendly_id"]
|
|
1203
|
+
if "friendly_id" in result.value
|
|
1204
|
+
else result.value["id"]
|
|
1205
|
+
),
|
|
1206
|
+
name=result.value["name"],
|
|
1207
|
+
repo=result.value["repo"],
|
|
1208
|
+
description=result.value["description"],
|
|
1209
|
+
misc=result.value["misc"],
|
|
1210
|
+
state=result.value["state"],
|
|
1211
|
+
start_epoch=result.value["start_epoch"],
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
except Exception as e:
|
|
1215
|
+
logger.error(f"Error getting subnet data: {e}")
|
|
1216
|
+
return None
|
|
1217
|
+
|
|
1218
|
+
def get_subnet_bootnodes(self, subnet_id: int) -> dict[str, list[bytes]]:
|
|
1219
|
+
"""
|
|
1220
|
+
Get subnet bootnodes using custom RPC method.
|
|
1221
|
+
|
|
1222
|
+
Args:
|
|
1223
|
+
subnet_id: The subnet ID
|
|
1224
|
+
|
|
1225
|
+
Returns:
|
|
1226
|
+
Dictionary containing 'bootnodes' and 'node_bootnodes' lists
|
|
1227
|
+
"""
|
|
1228
|
+
try:
|
|
1229
|
+
# Use custom RPC method
|
|
1230
|
+
result = self.substrate.rpc_request(
|
|
1231
|
+
method="network_getBootnodes", params=[subnet_id]
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
if not result or not result.get("result"):
|
|
1235
|
+
return {"bootnodes": [], "node_bootnodes": []}
|
|
1236
|
+
|
|
1237
|
+
# Decode the SCALE bytes using centralized type registry
|
|
1238
|
+
scale_data = result["result"]
|
|
1239
|
+
if isinstance(scale_data, str) and scale_data.startswith("0x"):
|
|
1240
|
+
scale_data = bytes.fromhex(scale_data[2:])
|
|
1241
|
+
elif isinstance(scale_data, (list, tuple)):
|
|
1242
|
+
scale_data = bytes(scale_data)
|
|
1243
|
+
|
|
1244
|
+
from scalecodec import ScaleBytes
|
|
1245
|
+
from ...utils.blockchain.type_registry import get_rpc_runtime_config
|
|
1246
|
+
|
|
1247
|
+
# Use centralized type registry configuration
|
|
1248
|
+
runtime_config = get_rpc_runtime_config()
|
|
1249
|
+
|
|
1250
|
+
# Decode using AllSubnetBootnodes struct
|
|
1251
|
+
# This contains both official bootnodes and node bootnodes
|
|
1252
|
+
obj = runtime_config.create_scale_object(
|
|
1253
|
+
"AllSubnetBootnodes", ScaleBytes(scale_data)
|
|
1254
|
+
)
|
|
1255
|
+
decoded = obj.decode()
|
|
1256
|
+
|
|
1257
|
+
# Helper to convert byte arrays/lists to bytes
|
|
1258
|
+
def ensure_bytes(item):
|
|
1259
|
+
if isinstance(item, list):
|
|
1260
|
+
return bytes(item)
|
|
1261
|
+
return item
|
|
1262
|
+
|
|
1263
|
+
# Process the decoded dictionary
|
|
1264
|
+
bootnodes = []
|
|
1265
|
+
node_bootnodes = []
|
|
1266
|
+
|
|
1267
|
+
if isinstance(decoded, dict):
|
|
1268
|
+
# Official bootnodes
|
|
1269
|
+
raw_bootnodes = decoded.get("bootnodes", [])
|
|
1270
|
+
bootnodes = [ensure_bytes(item) for item in raw_bootnodes]
|
|
1271
|
+
|
|
1272
|
+
# Node bootnodes
|
|
1273
|
+
raw_node_bootnodes = decoded.get("node_bootnodes", [])
|
|
1274
|
+
node_bootnodes = [ensure_bytes(item) for item in raw_node_bootnodes]
|
|
1275
|
+
|
|
1276
|
+
return {
|
|
1277
|
+
"bootnodes": bootnodes,
|
|
1278
|
+
"node_bootnodes": node_bootnodes
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
except Exception as e:
|
|
1282
|
+
logger.error(f"Error getting subnet bootnodes: {e}")
|
|
1283
|
+
return {"bootnodes": [], "node_bootnodes": []}
|
|
1284
|
+
|
|
1285
|
+
def proof_of_stake(
|
|
1286
|
+
self, subnet_id: int, peer_id: bytes, min_class: int = 0
|
|
1287
|
+
) -> bool:
|
|
1288
|
+
"""
|
|
1289
|
+
Check proof of stake for a peer ID in a subnet.
|
|
1290
|
+
|
|
1291
|
+
Args:
|
|
1292
|
+
subnet_id: The subnet ID
|
|
1293
|
+
peer_id: The peer ID to check
|
|
1294
|
+
min_class: Minimum class requirement
|
|
1295
|
+
|
|
1296
|
+
Returns:
|
|
1297
|
+
True if proof of stake is valid, False otherwise
|
|
1298
|
+
"""
|
|
1299
|
+
try:
|
|
1300
|
+
# Use custom RPC method
|
|
1301
|
+
result = self.substrate.rpc_request(
|
|
1302
|
+
method="network_proofOfStake",
|
|
1303
|
+
params=[subnet_id, peer_id.hex(), min_class],
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
if not result or not result.get("result"):
|
|
1307
|
+
return False
|
|
1308
|
+
|
|
1309
|
+
return result["result"]
|
|
1310
|
+
|
|
1311
|
+
except Exception as e:
|
|
1312
|
+
logger.error(f"Error checking proof of stake: {e}")
|
|
1313
|
+
return False
|
|
1314
|
+
|
|
1315
|
+
def get_elected_validator_info(
|
|
1316
|
+
self, subnet_id: int, subnet_epoch: int
|
|
1317
|
+
) -> Optional[SubnetNodeInfo]:
|
|
1318
|
+
"""
|
|
1319
|
+
Get elected validator information for a subnet and epoch using custom RPC method.
|
|
1320
|
+
|
|
1321
|
+
Returns Option<SubnetNodeInfo> - the elected validator node information.
|
|
1322
|
+
|
|
1323
|
+
Args:
|
|
1324
|
+
subnet_id: The subnet ID
|
|
1325
|
+
subnet_epoch: The subnet epoch number
|
|
1326
|
+
|
|
1327
|
+
Returns:
|
|
1328
|
+
SubnetNodeInfo for the elected validator, or None if not found or error
|
|
1329
|
+
|
|
1330
|
+
"""
|
|
1331
|
+
if self.substrate is None:
|
|
1332
|
+
logger.error(
|
|
1333
|
+
"Cannot get elected validator info: not connected to blockchain"
|
|
1334
|
+
)
|
|
1335
|
+
return None
|
|
1336
|
+
|
|
1337
|
+
try:
|
|
1338
|
+
# Call RPC method
|
|
1339
|
+
result = self.substrate.rpc_request(
|
|
1340
|
+
method="network_getElectedValidatorInfo",
|
|
1341
|
+
params=[subnet_id, subnet_epoch],
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
if not result or not result.get("result"):
|
|
1345
|
+
return None
|
|
1346
|
+
|
|
1347
|
+
result_data = result["result"]
|
|
1348
|
+
|
|
1349
|
+
# Decode Option<SubnetNodeInfo> using custom type registry
|
|
1350
|
+
# This returns the same structure as get_subnet_node_info
|
|
1351
|
+
from ...utils.blockchain.type_registry import decode_option_subnet_node_info
|
|
1352
|
+
|
|
1353
|
+
decoded = decode_option_subnet_node_info(result_data)
|
|
1354
|
+
|
|
1355
|
+
if decoded is None:
|
|
1356
|
+
return None
|
|
1357
|
+
|
|
1358
|
+
# Convert decoded dict to SubnetNodeInfo model (same logic as SubnetNodeRpcClient)
|
|
1359
|
+
def to_bytes(val):
|
|
1360
|
+
return (
|
|
1361
|
+
bytes(val)
|
|
1362
|
+
if isinstance(val, list)
|
|
1363
|
+
else (val if isinstance(val, bytes) else b"")
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
def to_address(val):
|
|
1367
|
+
from ...utils.blockchain.formatting import to_checksum_address
|
|
1368
|
+
if isinstance(val, str):
|
|
1369
|
+
raw_addr = val if val.startswith("0x") else "0x" + val
|
|
1370
|
+
else:
|
|
1371
|
+
raw_addr = "0x" + (bytes(val).hex() if isinstance(val, (list, bytes)) else "")
|
|
1372
|
+
return to_checksum_address(raw_addr)
|
|
1373
|
+
|
|
1374
|
+
# Extract classification
|
|
1375
|
+
classification_data = decoded.get("classification", {})
|
|
1376
|
+
|
|
1377
|
+
return SubnetNodeInfo(
|
|
1378
|
+
subnet_id=decoded["subnet_id"],
|
|
1379
|
+
subnet_node_id=decoded["subnet_node_id"],
|
|
1380
|
+
coldkey=to_address(decoded["coldkey"]),
|
|
1381
|
+
hotkey=to_address(decoded["hotkey"]),
|
|
1382
|
+
peer_id=to_bytes(decoded["peer_id"]),
|
|
1383
|
+
bootnode_peer_id=to_bytes(decoded["bootnode_peer_id"]),
|
|
1384
|
+
client_peer_id=to_bytes(decoded["client_peer_id"]),
|
|
1385
|
+
bootnode=to_bytes(decoded.get("bootnode"))
|
|
1386
|
+
if decoded.get("bootnode")
|
|
1387
|
+
else None,
|
|
1388
|
+
identity=decoded.get("identity"),
|
|
1389
|
+
classification=classification_data,
|
|
1390
|
+
delegate_reward_rate=decoded.get("delegate_reward_rate", 0),
|
|
1391
|
+
last_delegate_reward_rate_update=decoded.get(
|
|
1392
|
+
"last_delegate_reward_rate_update", 0
|
|
1393
|
+
),
|
|
1394
|
+
unique=to_bytes(decoded.get("unique"))
|
|
1395
|
+
if decoded.get("unique")
|
|
1396
|
+
else None,
|
|
1397
|
+
non_unique=to_bytes(decoded.get("non_unique"))
|
|
1398
|
+
if decoded.get("non_unique")
|
|
1399
|
+
else None,
|
|
1400
|
+
stake_balance=decoded.get("stake_balance", 0),
|
|
1401
|
+
total_node_delegate_stake_shares=decoded.get("total_node_delegate_stake_shares", 0),
|
|
1402
|
+
node_delegate_stake_balance=decoded.get(
|
|
1403
|
+
"node_delegate_stake_balance", 0
|
|
1404
|
+
),
|
|
1405
|
+
coldkey_reputation=decoded.get("coldkey_reputation"),
|
|
1406
|
+
subnet_node_reputation=decoded.get("subnet_node_reputation", 0),
|
|
1407
|
+
node_slot_index=decoded.get("node_slot_index"),
|
|
1408
|
+
consecutive_idle_epochs=decoded.get("consecutive_idle_epochs", 0),
|
|
1409
|
+
consecutive_included_epochs=decoded.get("consecutive_included_epochs", 0),
|
|
1410
|
+
)
|
|
1411
|
+
|
|
1412
|
+
except Exception as e:
|
|
1413
|
+
logger.error(f"Error getting elected validator info: {e}")
|
|
1414
|
+
return None
|
|
1415
|
+
|
|
1416
|
+
def get_validators_and_attestors(self, subnet_id: int) -> list[SubnetNodeInfo]:
|
|
1417
|
+
"""
|
|
1418
|
+
Get validators and attestors for a subnet using custom RPC method.
|
|
1419
|
+
|
|
1420
|
+
Returns Vec<SubnetNodeInfo> - list of validator and attestor node information.
|
|
1421
|
+
|
|
1422
|
+
Args:
|
|
1423
|
+
subnet_id: The subnet ID
|
|
1424
|
+
|
|
1425
|
+
Returns:
|
|
1426
|
+
List of SubnetNodeInfo objects for validators and attestors
|
|
1427
|
+
"""
|
|
1428
|
+
if self.substrate is None:
|
|
1429
|
+
logger.error(
|
|
1430
|
+
"Cannot get validators and attestors: not connected to blockchain"
|
|
1431
|
+
)
|
|
1432
|
+
return []
|
|
1433
|
+
|
|
1434
|
+
try:
|
|
1435
|
+
# Call RPC method
|
|
1436
|
+
result = self.substrate.rpc_request(
|
|
1437
|
+
method="network_getValidatorsAndAttestors",
|
|
1438
|
+
params=[subnet_id],
|
|
1439
|
+
)
|
|
1440
|
+
|
|
1441
|
+
if not result or not result.get("result"):
|
|
1442
|
+
return []
|
|
1443
|
+
|
|
1444
|
+
result_data = result["result"]
|
|
1445
|
+
|
|
1446
|
+
# Decode Vec<SubnetNodeInfo> using custom type registry
|
|
1447
|
+
# This returns the same structure as get_subnet_nodes_info
|
|
1448
|
+
from ...utils.blockchain.type_registry import decode_vec_subnet_node_info
|
|
1449
|
+
|
|
1450
|
+
decoded_list = decode_vec_subnet_node_info(result_data)
|
|
1451
|
+
|
|
1452
|
+
if not decoded_list:
|
|
1453
|
+
return []
|
|
1454
|
+
|
|
1455
|
+
# Convert to list of SubnetNodeInfo objects (same logic as SubnetNodeRpcClient)
|
|
1456
|
+
def to_bytes(val):
|
|
1457
|
+
return (
|
|
1458
|
+
bytes(val)
|
|
1459
|
+
if isinstance(val, list)
|
|
1460
|
+
else (val if isinstance(val, bytes) else b"")
|
|
1461
|
+
)
|
|
1462
|
+
|
|
1463
|
+
def to_address(val):
|
|
1464
|
+
from ...utils.blockchain.formatting import to_checksum_address
|
|
1465
|
+
if isinstance(val, str):
|
|
1466
|
+
raw_addr = val if val.startswith("0x") else "0x" + val
|
|
1467
|
+
else:
|
|
1468
|
+
raw_addr = "0x" + (bytes(val).hex() if isinstance(val, (list, bytes)) else "")
|
|
1469
|
+
return to_checksum_address(raw_addr)
|
|
1470
|
+
|
|
1471
|
+
nodes = []
|
|
1472
|
+
for decoded in decoded_list:
|
|
1473
|
+
try:
|
|
1474
|
+
classification_data = decoded.get("classification", {})
|
|
1475
|
+
|
|
1476
|
+
node_info = SubnetNodeInfo(
|
|
1477
|
+
subnet_id=decoded["subnet_id"],
|
|
1478
|
+
subnet_node_id=decoded["subnet_node_id"],
|
|
1479
|
+
coldkey=to_address(decoded["coldkey"]),
|
|
1480
|
+
hotkey=to_address(decoded["hotkey"]),
|
|
1481
|
+
peer_id=to_bytes(decoded["peer_id"]),
|
|
1482
|
+
bootnode_peer_id=to_bytes(decoded["bootnode_peer_id"]),
|
|
1483
|
+
client_peer_id=to_bytes(decoded["client_peer_id"]),
|
|
1484
|
+
bootnode=to_bytes(decoded.get("bootnode"))
|
|
1485
|
+
if decoded.get("bootnode")
|
|
1486
|
+
else None,
|
|
1487
|
+
identity=decoded.get("identity"),
|
|
1488
|
+
classification=classification_data,
|
|
1489
|
+
delegate_reward_rate=decoded.get("delegate_reward_rate", 0),
|
|
1490
|
+
last_delegate_reward_rate_update=decoded.get(
|
|
1491
|
+
"last_delegate_reward_rate_update", 0
|
|
1492
|
+
),
|
|
1493
|
+
unique=to_bytes(decoded.get("unique"))
|
|
1494
|
+
if decoded.get("unique")
|
|
1495
|
+
else None,
|
|
1496
|
+
non_unique=to_bytes(decoded.get("non_unique"))
|
|
1497
|
+
if decoded.get("non_unique")
|
|
1498
|
+
else None,
|
|
1499
|
+
stake_balance=decoded.get("stake_balance", 0),
|
|
1500
|
+
total_node_delegate_stake_shares=decoded.get("total_node_delegate_stake_shares", 0),
|
|
1501
|
+
node_delegate_stake_balance=decoded.get(
|
|
1502
|
+
"node_delegate_stake_balance", 0
|
|
1503
|
+
),
|
|
1504
|
+
coldkey_reputation=decoded.get("coldkey_reputation"),
|
|
1505
|
+
subnet_node_reputation=decoded.get("subnet_node_reputation", 0),
|
|
1506
|
+
node_slot_index=decoded.get("node_slot_index"),
|
|
1507
|
+
consecutive_idle_epochs=decoded.get("consecutive_idle_epochs", 0),
|
|
1508
|
+
consecutive_included_epochs=decoded.get("consecutive_included_epochs", 0),
|
|
1509
|
+
)
|
|
1510
|
+
nodes.append(node_info)
|
|
1511
|
+
except Exception as e:
|
|
1512
|
+
logger.warning(
|
|
1513
|
+
f"Failed to convert validator/attestor node info: {e}"
|
|
1514
|
+
)
|
|
1515
|
+
continue
|
|
1516
|
+
|
|
1517
|
+
return nodes
|
|
1518
|
+
|
|
1519
|
+
except Exception as e:
|
|
1520
|
+
logger.error(f"Error getting validators and attestors: {e}")
|
|
1521
|
+
return []
|
|
1522
|
+
|
|
1523
|
+
def get_subnet_ids_for_coldkey(self, coldkey: str) -> set[int]:
|
|
1524
|
+
"""Get all subnet IDs associated with a coldkey.
|
|
1525
|
+
|
|
1526
|
+
Returns subnets where:
|
|
1527
|
+
- Coldkey owns the subnet (SubnetOwner)
|
|
1528
|
+
- Coldkey has nodes in the subnet (ColdkeySubnetNodes)
|
|
1529
|
+
|
|
1530
|
+
Args:
|
|
1531
|
+
coldkey: Coldkey address (hex string starting with 0x) or wallet name
|
|
1532
|
+
|
|
1533
|
+
Returns:
|
|
1534
|
+
Set of subnet IDs associated with the coldkey
|
|
1535
|
+
"""
|
|
1536
|
+
if not self.substrate:
|
|
1537
|
+
logger.error("Cannot get subnet IDs: not connected to blockchain")
|
|
1538
|
+
return set()
|
|
1539
|
+
|
|
1540
|
+
subnet_ids = set()
|
|
1541
|
+
|
|
1542
|
+
# Normalize coldkey address (lowercase for Ethereum addresses)
|
|
1543
|
+
if coldkey.startswith("0x"):
|
|
1544
|
+
coldkey = coldkey.lower()
|
|
1545
|
+
|
|
1546
|
+
try:
|
|
1547
|
+
# 1. Query ColdkeySubnetNodes storage (subnets where coldkey has nodes)
|
|
1548
|
+
# This is very efficient - direct lookup
|
|
1549
|
+
try:
|
|
1550
|
+
result = self.substrate.query(
|
|
1551
|
+
module="Network",
|
|
1552
|
+
storage_function="ColdkeySubnetNodes",
|
|
1553
|
+
params=[coldkey],
|
|
1554
|
+
)
|
|
1555
|
+
if result and hasattr(result, "value") and result.value:
|
|
1556
|
+
# result.value is dict: {subnet_id: [node_ids], ...}
|
|
1557
|
+
if isinstance(result.value, dict):
|
|
1558
|
+
subnet_ids.update(int(sid) for sid in result.value.keys())
|
|
1559
|
+
logger.debug(
|
|
1560
|
+
f"Found {len(subnet_ids)} subnets with nodes for coldkey {coldkey}"
|
|
1561
|
+
)
|
|
1562
|
+
except Exception as e:
|
|
1563
|
+
logger.debug(f"Failed to query ColdkeySubnetNodes for {coldkey}: {e}")
|
|
1564
|
+
|
|
1565
|
+
# 2. Query SubnetOwner for all subnets to find owned subnets
|
|
1566
|
+
# Get maximum subnet ID first
|
|
1567
|
+
try:
|
|
1568
|
+
total_subnets_result = self.substrate.query(
|
|
1569
|
+
module="Network", storage_function="TotalSubnetUids"
|
|
1570
|
+
)
|
|
1571
|
+
max_subnet_id = (
|
|
1572
|
+
total_subnets_result.value if total_subnets_result else 0
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
# Also check SubnetsData to find actual subnet IDs (handles non-sequential IDs)
|
|
1576
|
+
# Scan a reasonable range
|
|
1577
|
+
scan_range = max(
|
|
1578
|
+
max_subnet_id, 50
|
|
1579
|
+
) # Check at least up to TotalSubnetUids or 50
|
|
1580
|
+
|
|
1581
|
+
logger.debug(
|
|
1582
|
+
f"Scanning SubnetOwner storage for coldkey {coldkey} (range: 1-{scan_range})"
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
owned_count = 0
|
|
1586
|
+
for subnet_id in range(1, scan_range + 1):
|
|
1587
|
+
try:
|
|
1588
|
+
owner_result = self.substrate.query(
|
|
1589
|
+
module="Network",
|
|
1590
|
+
storage_function="SubnetOwner",
|
|
1591
|
+
params=[subnet_id],
|
|
1592
|
+
)
|
|
1593
|
+
if owner_result and owner_result.value:
|
|
1594
|
+
owner = self._to_address(owner_result.value)
|
|
1595
|
+
if owner and owner.lower() == coldkey.lower():
|
|
1596
|
+
subnet_ids.add(subnet_id)
|
|
1597
|
+
owned_count += 1
|
|
1598
|
+
except Exception:
|
|
1599
|
+
# Subnet doesn't exist or error - continue
|
|
1600
|
+
continue
|
|
1601
|
+
|
|
1602
|
+
if owned_count > 0:
|
|
1603
|
+
logger.debug(
|
|
1604
|
+
f"Found {owned_count} owned subnets for coldkey {coldkey}"
|
|
1605
|
+
)
|
|
1606
|
+
except Exception as e:
|
|
1607
|
+
logger.warning(
|
|
1608
|
+
f"Failed to query SubnetOwner for coldkey {coldkey}: {e}"
|
|
1609
|
+
)
|
|
1610
|
+
|
|
1611
|
+
logger.info(
|
|
1612
|
+
f"Total {len(subnet_ids)} unique subnets found for coldkey {coldkey}"
|
|
1613
|
+
)
|
|
1614
|
+
return subnet_ids
|
|
1615
|
+
|
|
1616
|
+
except Exception as e:
|
|
1617
|
+
logger.error(f"Error getting subnet IDs for coldkey {coldkey}: {e}")
|
|
1618
|
+
return set()
|
|
1619
|
+
|
|
1620
|
+
def check_subnet_activation_requirements(self, subnet_id: int) -> dict[str, Any]:
|
|
1621
|
+
"""
|
|
1622
|
+
Check if a subnet meets activation requirements including minimum registration epochs.
|
|
1623
|
+
|
|
1624
|
+
Args:
|
|
1625
|
+
subnet_id: The subnet ID to check
|
|
1626
|
+
|
|
1627
|
+
Returns:
|
|
1628
|
+
Dictionary with activation check results including:
|
|
1629
|
+
- can_activate: bool
|
|
1630
|
+
- requirements_met: list[str]
|
|
1631
|
+
- requirements_failed: list[str]
|
|
1632
|
+
- current_epoch: int
|
|
1633
|
+
- registration_epoch: Optional[int]
|
|
1634
|
+
- min_registration_epochs: int
|
|
1635
|
+
- epochs_elapsed: Optional[int]
|
|
1636
|
+
- epochs_remaining: Optional[int]
|
|
1637
|
+
- timeline: dict with block numbers, epochs, and time estimates
|
|
1638
|
+
"""
|
|
1639
|
+
try:
|
|
1640
|
+
if not self.substrate:
|
|
1641
|
+
raise Exception("Not connected to blockchain")
|
|
1642
|
+
|
|
1643
|
+
from ..rpc import ChainRpcClient
|
|
1644
|
+
|
|
1645
|
+
chain_rpc = ChainRpcClient(self.substrate)
|
|
1646
|
+
current_epoch = chain_rpc.get_current_epoch()
|
|
1647
|
+
current_block = chain_rpc.get_block_number()
|
|
1648
|
+
|
|
1649
|
+
# Get epoch length (blocks per epoch) for time calculations
|
|
1650
|
+
epoch_length_result = self.substrate.get_constant("Network", "EpochLength")
|
|
1651
|
+
epoch_length = epoch_length_result.value if epoch_length_result else 100 # Default fallback
|
|
1652
|
+
|
|
1653
|
+
# Block time in milliseconds (6000ms = 6 seconds per block on Hypertensor)
|
|
1654
|
+
# This is a constant from the runtime (MILLISECS_PER_BLOCK)
|
|
1655
|
+
block_time_ms = 6000
|
|
1656
|
+
|
|
1657
|
+
# Get subnet info to check registration epoch
|
|
1658
|
+
subnet_info = self.get_subnet_info(subnet_id)
|
|
1659
|
+
if not subnet_info:
|
|
1660
|
+
return {
|
|
1661
|
+
"success": False,
|
|
1662
|
+
"error": f"Subnet {subnet_id} not found",
|
|
1663
|
+
"data": {
|
|
1664
|
+
"subnet_id": subnet_id,
|
|
1665
|
+
"can_activate": False,
|
|
1666
|
+
"requirements_met": [],
|
|
1667
|
+
"requirements_failed": ["Subnet not found"],
|
|
1668
|
+
"current_epoch": current_epoch,
|
|
1669
|
+
"current_block": current_block,
|
|
1670
|
+
"registration_epoch": None,
|
|
1671
|
+
"min_registration_epochs": None,
|
|
1672
|
+
"epochs_elapsed": None,
|
|
1673
|
+
"epochs_remaining": None,
|
|
1674
|
+
"timeline": None,
|
|
1675
|
+
},
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
registration_epoch = subnet_info.registration_epoch
|
|
1680
|
+
|
|
1681
|
+
# Query MinSubnetRegistrationEpochs from storage
|
|
1682
|
+
min_registration_epochs_result = self.substrate.query(
|
|
1683
|
+
module="Network", storage_function="MinSubnetRegistrationEpochs", params=[]
|
|
1684
|
+
)
|
|
1685
|
+
min_registration_epochs = (
|
|
1686
|
+
min_registration_epochs_result.value
|
|
1687
|
+
if min_registration_epochs_result
|
|
1688
|
+
else None
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1691
|
+
requirements_met = []
|
|
1692
|
+
requirements_failed = []
|
|
1693
|
+
|
|
1694
|
+
# Check minimum registration epochs
|
|
1695
|
+
epochs_elapsed = None
|
|
1696
|
+
epochs_remaining = None
|
|
1697
|
+
min_epochs_met = False
|
|
1698
|
+
|
|
1699
|
+
if current_epoch is not None and registration_epoch is not None:
|
|
1700
|
+
epochs_elapsed = current_epoch - registration_epoch
|
|
1701
|
+
if min_registration_epochs is not None:
|
|
1702
|
+
epochs_remaining = max(0, min_registration_epochs - epochs_elapsed)
|
|
1703
|
+
min_epochs_met = epochs_elapsed >= min_registration_epochs
|
|
1704
|
+
|
|
1705
|
+
if min_epochs_met:
|
|
1706
|
+
requirements_met.append(
|
|
1707
|
+
f"Minimum registration epochs met ({epochs_elapsed}/{min_registration_epochs} epochs elapsed)"
|
|
1708
|
+
)
|
|
1709
|
+
else:
|
|
1710
|
+
requirements_failed.append(
|
|
1711
|
+
f"Minimum registration epochs not met ({epochs_elapsed}/{min_registration_epochs} epochs elapsed, {epochs_remaining} remaining)"
|
|
1712
|
+
)
|
|
1713
|
+
else:
|
|
1714
|
+
requirements_failed.append(
|
|
1715
|
+
"Could not query minimum registration epochs from chain"
|
|
1716
|
+
)
|
|
1717
|
+
else:
|
|
1718
|
+
if current_epoch is None:
|
|
1719
|
+
requirements_failed.append("Could not get current epoch")
|
|
1720
|
+
if registration_epoch is None:
|
|
1721
|
+
requirements_failed.append(
|
|
1722
|
+
"Subnet registration epoch not found (subnet may already be activated)"
|
|
1723
|
+
)
|
|
1724
|
+
|
|
1725
|
+
# Check subnet state
|
|
1726
|
+
if subnet_info.state == SubnetState.REGISTERED:
|
|
1727
|
+
requirements_met.append("Subnet is in Registered state")
|
|
1728
|
+
else:
|
|
1729
|
+
requirements_failed.append(
|
|
1730
|
+
f"Subnet is not in Registered state (current: {subnet_info.state.value if subnet_info.state else 'Unknown'})"
|
|
1731
|
+
)
|
|
1732
|
+
|
|
1733
|
+
# Check minimum active nodes
|
|
1734
|
+
min_subnet_nodes_result = self.substrate.query(
|
|
1735
|
+
module="Network", storage_function="MinSubnetNodes", params=[]
|
|
1736
|
+
)
|
|
1737
|
+
min_subnet_nodes = (
|
|
1738
|
+
min_subnet_nodes_result.value if min_subnet_nodes_result else None
|
|
1739
|
+
)
|
|
1740
|
+
|
|
1741
|
+
total_active_nodes_result = self.substrate.query(
|
|
1742
|
+
module="Network",
|
|
1743
|
+
storage_function="TotalActiveSubnetNodes",
|
|
1744
|
+
params=[subnet_id],
|
|
1745
|
+
)
|
|
1746
|
+
total_active_nodes = (
|
|
1747
|
+
total_active_nodes_result.value if total_active_nodes_result else 0
|
|
1748
|
+
)
|
|
1749
|
+
|
|
1750
|
+
nodes_met = False
|
|
1751
|
+
if min_subnet_nodes is not None:
|
|
1752
|
+
nodes_met = total_active_nodes >= min_subnet_nodes
|
|
1753
|
+
if nodes_met:
|
|
1754
|
+
requirements_met.append(
|
|
1755
|
+
f"Minimum nodes met ({total_active_nodes}/{min_subnet_nodes} active nodes)"
|
|
1756
|
+
)
|
|
1757
|
+
else:
|
|
1758
|
+
requirements_failed.append(
|
|
1759
|
+
f"Minimum nodes not met ({total_active_nodes}/{min_subnet_nodes} active nodes, need {min_subnet_nodes - total_active_nodes} more)"
|
|
1760
|
+
)
|
|
1761
|
+
else:
|
|
1762
|
+
requirements_failed.append("Could not query minimum subnet nodes from chain")
|
|
1763
|
+
|
|
1764
|
+
# Check minimum delegate stake balance
|
|
1765
|
+
total_delegate_stake_result = self.substrate.query(
|
|
1766
|
+
module="Network",
|
|
1767
|
+
storage_function="TotalSubnetDelegateStakeBalance",
|
|
1768
|
+
params=[subnet_id],
|
|
1769
|
+
)
|
|
1770
|
+
total_delegate_stake = (
|
|
1771
|
+
total_delegate_stake_result.value if total_delegate_stake_result else 0
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
# Query MinSubnetDelegateStakeFactor to calculate minimum
|
|
1775
|
+
min_delegate_stake_factor_result = self.substrate.query(
|
|
1776
|
+
module="Network",
|
|
1777
|
+
storage_function="MinSubnetDelegateStakeFactor",
|
|
1778
|
+
params=[],
|
|
1779
|
+
)
|
|
1780
|
+
min_delegate_stake_factor = (
|
|
1781
|
+
min_delegate_stake_factor_result.value
|
|
1782
|
+
if min_delegate_stake_factor_result
|
|
1783
|
+
else None
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1786
|
+
# Get total network issuance for calculation (from Balances pallet)
|
|
1787
|
+
total_issuance_result = self.substrate.query(
|
|
1788
|
+
module="Balances", storage_function="TotalIssuance", params=[]
|
|
1789
|
+
)
|
|
1790
|
+
total_issuance = (
|
|
1791
|
+
total_issuance_result.value if total_issuance_result else None
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
delegate_stake_met = False
|
|
1795
|
+
min_delegate_stake_required = None
|
|
1796
|
+
|
|
1797
|
+
# Try to calculate minimum delegate stake parameters
|
|
1798
|
+
if (
|
|
1799
|
+
min_delegate_stake_factor is not None
|
|
1800
|
+
and total_issuance is not None
|
|
1801
|
+
):
|
|
1802
|
+
# Simplified calculation: base_min = total_issuance * factor / 1e18
|
|
1803
|
+
# The actual calculation uses get_min_subnet_delegate_stake_balance which
|
|
1804
|
+
# includes node count multiplier, but this gives us a rough estimate
|
|
1805
|
+
base_min = (total_issuance * min_delegate_stake_factor) // (10**18)
|
|
1806
|
+
|
|
1807
|
+
# Use base_min as minimum
|
|
1808
|
+
min_delegate_stake_required = base_min
|
|
1809
|
+
delegate_stake_met = total_delegate_stake >= min_delegate_stake_required
|
|
1810
|
+
|
|
1811
|
+
if delegate_stake_met:
|
|
1812
|
+
requirements_met.append(
|
|
1813
|
+
f"Minimum delegate stake met ({total_delegate_stake / 1e18:.2f} TENSOR >= {min_delegate_stake_required / 1e18:.2f} TENSOR)"
|
|
1814
|
+
)
|
|
1815
|
+
else:
|
|
1816
|
+
requirements_failed.append(
|
|
1817
|
+
f"Minimum delegate stake not met ({total_delegate_stake / 1e18:.2f} TENSOR < {min_delegate_stake_required / 1e18:.2f} TENSOR, need {(min_delegate_stake_required - total_delegate_stake) / 1e18:.2f} more TENSOR)"
|
|
1818
|
+
)
|
|
1819
|
+
else:
|
|
1820
|
+
if total_delegate_stake == 0:
|
|
1821
|
+
requirements_failed.append(
|
|
1822
|
+
"No delegate stake found (need to delegate stake to subnet nodes)"
|
|
1823
|
+
)
|
|
1824
|
+
else:
|
|
1825
|
+
requirements_failed.append(
|
|
1826
|
+
"Could not calculate minimum delegate stake requirement (missing chain parameters)"
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
# Check subnet reputation (usually redundant but included for completeness)
|
|
1830
|
+
subnet_reputation = subnet_info.reputation if subnet_info.reputation else 0
|
|
1831
|
+
min_reputation_result = self.substrate.query(
|
|
1832
|
+
module="Network", storage_function="MinSubnetReputation", params=[]
|
|
1833
|
+
)
|
|
1834
|
+
min_reputation = (
|
|
1835
|
+
min_reputation_result.value if min_reputation_result else None
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
reputation_met = True # Usually always met
|
|
1839
|
+
if min_reputation is not None:
|
|
1840
|
+
reputation_met = subnet_reputation >= min_reputation
|
|
1841
|
+
if not reputation_met:
|
|
1842
|
+
requirements_failed.append(
|
|
1843
|
+
f"Subnet reputation too low ({subnet_reputation} < {min_reputation})"
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
# Check if subnet is in valid activation period (registration or enactment period)
|
|
1847
|
+
in_valid_period = False
|
|
1848
|
+
|
|
1849
|
+
if current_epoch is not None and registration_epoch is not None:
|
|
1850
|
+
# Query SubnetRegistrationEpochs (registration period length)
|
|
1851
|
+
subnet_registration_epochs_result = self.substrate.query(
|
|
1852
|
+
module="Network", storage_function="SubnetRegistrationEpochs", params=[]
|
|
1853
|
+
)
|
|
1854
|
+
subnet_registration_epochs = (
|
|
1855
|
+
subnet_registration_epochs_result.value
|
|
1856
|
+
if subnet_registration_epochs_result
|
|
1857
|
+
else None
|
|
1858
|
+
)
|
|
1859
|
+
|
|
1860
|
+
# Query SubnetEnactmentEpochs (enactment period length)
|
|
1861
|
+
subnet_enactment_epochs_result = self.substrate.query(
|
|
1862
|
+
module="Network", storage_function="SubnetEnactmentEpochs", params=[]
|
|
1863
|
+
)
|
|
1864
|
+
subnet_enactment_epochs = (
|
|
1865
|
+
subnet_enactment_epochs_result.value
|
|
1866
|
+
if subnet_enactment_epochs_result
|
|
1867
|
+
else None
|
|
1868
|
+
)
|
|
1869
|
+
|
|
1870
|
+
if subnet_registration_epochs is not None and subnet_enactment_epochs is not None:
|
|
1871
|
+
max_registration_epoch = registration_epoch + subnet_registration_epochs
|
|
1872
|
+
max_enactment_epoch = max_registration_epoch + subnet_enactment_epochs
|
|
1873
|
+
|
|
1874
|
+
# Check registration period: epoch <= max_registration_epoch
|
|
1875
|
+
if subnet_info.state == SubnetState.REGISTERED and current_epoch <= max_registration_epoch:
|
|
1876
|
+
in_valid_period = True
|
|
1877
|
+
requirements_met.append(
|
|
1878
|
+
f"In registration period (epoch {current_epoch} <= {max_registration_epoch})"
|
|
1879
|
+
)
|
|
1880
|
+
# Check enactment period: max_registration_epoch < epoch <= max_enactment_epoch
|
|
1881
|
+
elif subnet_info.state == SubnetState.REGISTERED and current_epoch > max_registration_epoch and current_epoch <= max_enactment_epoch:
|
|
1882
|
+
in_valid_period = True
|
|
1883
|
+
requirements_met.append(
|
|
1884
|
+
f"In enactment period (epoch {current_epoch} between {max_registration_epoch + 1} and {max_enactment_epoch})"
|
|
1885
|
+
)
|
|
1886
|
+
# Outside valid periods
|
|
1887
|
+
elif subnet_info.state == SubnetState.REGISTERED:
|
|
1888
|
+
requirements_failed.append(
|
|
1889
|
+
f"Subnet is outside valid activation periods (current epoch {current_epoch}, max enactment epoch {max_enactment_epoch})"
|
|
1890
|
+
)
|
|
1891
|
+
requirements_failed.append(
|
|
1892
|
+
"⚠️ Subnet will be removed if activation is attempted now!"
|
|
1893
|
+
)
|
|
1894
|
+
else:
|
|
1895
|
+
requirements_failed.append(
|
|
1896
|
+
"Could not query registration/enactment epoch periods from chain"
|
|
1897
|
+
)
|
|
1898
|
+
else:
|
|
1899
|
+
requirements_failed.append(
|
|
1900
|
+
"Could not determine if subnet is in valid activation period (missing epoch data)"
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
# Check if subnet can be activated (all critical requirements met)
|
|
1904
|
+
can_activate = (
|
|
1905
|
+
min_epochs_met
|
|
1906
|
+
and subnet_info.state == SubnetState.REGISTERED
|
|
1907
|
+
and nodes_met
|
|
1908
|
+
and delegate_stake_met
|
|
1909
|
+
and reputation_met
|
|
1910
|
+
and in_valid_period
|
|
1911
|
+
and current_epoch is not None
|
|
1912
|
+
and registration_epoch is not None
|
|
1913
|
+
)
|
|
1914
|
+
|
|
1915
|
+
# Build comprehensive timeline information
|
|
1916
|
+
timeline = None
|
|
1917
|
+
if registration_epoch is not None and current_epoch is not None:
|
|
1918
|
+
# Calculate registration block (approximate)
|
|
1919
|
+
# registration_block = current_block - (epochs_elapsed * epoch_length)
|
|
1920
|
+
registration_block = None
|
|
1921
|
+
if current_block is not None and epochs_elapsed is not None:
|
|
1922
|
+
registration_block = current_block - (epochs_elapsed * epoch_length)
|
|
1923
|
+
|
|
1924
|
+
# Get period lengths
|
|
1925
|
+
subnet_registration_epochs_result = self.substrate.query(
|
|
1926
|
+
module="Network", storage_function="SubnetRegistrationEpochs", params=[]
|
|
1927
|
+
)
|
|
1928
|
+
subnet_registration_epochs = (
|
|
1929
|
+
subnet_registration_epochs_result.value
|
|
1930
|
+
if subnet_registration_epochs_result
|
|
1931
|
+
else None
|
|
1932
|
+
)
|
|
1933
|
+
|
|
1934
|
+
subnet_enactment_epochs_result = self.substrate.query(
|
|
1935
|
+
module="Network", storage_function="SubnetEnactmentEpochs", params=[]
|
|
1936
|
+
)
|
|
1937
|
+
subnet_enactment_epochs = (
|
|
1938
|
+
subnet_enactment_epochs_result.value
|
|
1939
|
+
if subnet_enactment_epochs_result
|
|
1940
|
+
else None
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1943
|
+
# Calculate key milestones
|
|
1944
|
+
min_activation_epoch = None
|
|
1945
|
+
registration_deadline_epoch = None
|
|
1946
|
+
enactment_deadline_epoch = None
|
|
1947
|
+
|
|
1948
|
+
if min_registration_epochs is not None:
|
|
1949
|
+
min_activation_epoch = registration_epoch + min_registration_epochs
|
|
1950
|
+
|
|
1951
|
+
if subnet_registration_epochs is not None:
|
|
1952
|
+
registration_deadline_epoch = registration_epoch + subnet_registration_epochs
|
|
1953
|
+
|
|
1954
|
+
if subnet_registration_epochs is not None and subnet_enactment_epochs is not None:
|
|
1955
|
+
enactment_deadline_epoch = registration_epoch + subnet_registration_epochs + subnet_enactment_epochs
|
|
1956
|
+
|
|
1957
|
+
# Calculate time estimates (in seconds, human-readable, and datetime)
|
|
1958
|
+
from datetime import datetime, timedelta
|
|
1959
|
+
|
|
1960
|
+
def epochs_to_time(num_epochs: int, is_past: bool = False) -> dict:
|
|
1961
|
+
"""Convert epochs to time estimates with datetime.
|
|
1962
|
+
|
|
1963
|
+
Args:
|
|
1964
|
+
num_epochs: Number of epochs to convert
|
|
1965
|
+
is_past: If True, subtract from now (for past events), otherwise add (for future)
|
|
1966
|
+
"""
|
|
1967
|
+
total_blocks = num_epochs * epoch_length
|
|
1968
|
+
total_ms = total_blocks * block_time_ms
|
|
1969
|
+
total_seconds = total_ms / 1000
|
|
1970
|
+
total_minutes = total_seconds / 60
|
|
1971
|
+
total_hours = total_minutes / 60
|
|
1972
|
+
total_days = total_hours / 24
|
|
1973
|
+
|
|
1974
|
+
# Build human readable string
|
|
1975
|
+
if total_days >= 1:
|
|
1976
|
+
human_readable = f"{total_days:.1f} days"
|
|
1977
|
+
elif total_hours >= 1:
|
|
1978
|
+
human_readable = f"{total_hours:.1f} hours"
|
|
1979
|
+
elif total_minutes >= 1:
|
|
1980
|
+
human_readable = f"{total_minutes:.0f} minutes"
|
|
1981
|
+
else:
|
|
1982
|
+
human_readable = f"{total_seconds:.0f} seconds"
|
|
1983
|
+
|
|
1984
|
+
# Calculate datetime (in local timezone)
|
|
1985
|
+
now = datetime.now()
|
|
1986
|
+
if is_past:
|
|
1987
|
+
target_datetime = now - timedelta(seconds=total_seconds)
|
|
1988
|
+
else:
|
|
1989
|
+
target_datetime = now + timedelta(seconds=total_seconds)
|
|
1990
|
+
|
|
1991
|
+
# Format datetime for display
|
|
1992
|
+
datetime_str = target_datetime.strftime("%Y-%m-%d %H:%M:%S")
|
|
1993
|
+
datetime_short = target_datetime.strftime("%b %d, %H:%M")
|
|
1994
|
+
|
|
1995
|
+
return {
|
|
1996
|
+
"epochs": num_epochs,
|
|
1997
|
+
"blocks": total_blocks,
|
|
1998
|
+
"seconds": int(total_seconds),
|
|
1999
|
+
"human_readable": human_readable,
|
|
2000
|
+
"datetime": datetime_str,
|
|
2001
|
+
"datetime_short": datetime_short,
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
# Time until key milestones
|
|
2005
|
+
time_until_min_activation = None
|
|
2006
|
+
time_until_registration_deadline = None
|
|
2007
|
+
time_until_enactment_deadline = None
|
|
2008
|
+
|
|
2009
|
+
if min_activation_epoch is not None:
|
|
2010
|
+
epochs_until = max(0, min_activation_epoch - current_epoch)
|
|
2011
|
+
if epochs_until > 0:
|
|
2012
|
+
time_until_min_activation = epochs_to_time(epochs_until)
|
|
2013
|
+
else:
|
|
2014
|
+
time_until_min_activation = {"epochs": 0, "blocks": 0, "seconds": 0, "human_readable": "Now eligible"}
|
|
2015
|
+
|
|
2016
|
+
if registration_deadline_epoch is not None:
|
|
2017
|
+
epochs_until = max(0, registration_deadline_epoch - current_epoch)
|
|
2018
|
+
if epochs_until > 0:
|
|
2019
|
+
time_until_registration_deadline = epochs_to_time(epochs_until)
|
|
2020
|
+
else:
|
|
2021
|
+
time_until_registration_deadline = {"epochs": 0, "blocks": 0, "seconds": 0, "human_readable": "Passed"}
|
|
2022
|
+
|
|
2023
|
+
if enactment_deadline_epoch is not None:
|
|
2024
|
+
epochs_until = max(0, enactment_deadline_epoch - current_epoch)
|
|
2025
|
+
if epochs_until > 0:
|
|
2026
|
+
time_until_enactment_deadline = epochs_to_time(epochs_until)
|
|
2027
|
+
else:
|
|
2028
|
+
time_until_enactment_deadline = {"epochs": 0, "blocks": 0, "seconds": 0, "human_readable": "Passed - subnet at risk!"}
|
|
2029
|
+
|
|
2030
|
+
# Calculate registration datetime (past event)
|
|
2031
|
+
registration_datetime = None
|
|
2032
|
+
if epochs_elapsed is not None and epochs_elapsed > 0:
|
|
2033
|
+
registration_time = epochs_to_time(epochs_elapsed, is_past=True)
|
|
2034
|
+
registration_datetime = registration_time.get("datetime")
|
|
2035
|
+
|
|
2036
|
+
# Determine current phase
|
|
2037
|
+
current_phase = "Unknown"
|
|
2038
|
+
if current_epoch < min_activation_epoch if min_activation_epoch else False:
|
|
2039
|
+
current_phase = "Waiting Period (cannot activate yet)"
|
|
2040
|
+
elif registration_deadline_epoch and current_epoch <= registration_deadline_epoch:
|
|
2041
|
+
current_phase = "Registration Period"
|
|
2042
|
+
elif enactment_deadline_epoch and current_epoch <= enactment_deadline_epoch:
|
|
2043
|
+
current_phase = "Enactment Period (must activate or will be removed)"
|
|
2044
|
+
elif enactment_deadline_epoch and current_epoch > enactment_deadline_epoch:
|
|
2045
|
+
current_phase = "Expired (will be removed on activation attempt)"
|
|
2046
|
+
|
|
2047
|
+
timeline = {
|
|
2048
|
+
"current_block": current_block,
|
|
2049
|
+
"current_epoch": current_epoch,
|
|
2050
|
+
"registration_epoch": registration_epoch,
|
|
2051
|
+
"registration_block_approx": registration_block,
|
|
2052
|
+
"registration_datetime": registration_datetime,
|
|
2053
|
+
"epoch_length": epoch_length,
|
|
2054
|
+
"block_time_ms": block_time_ms,
|
|
2055
|
+
"min_activation_epoch": min_activation_epoch,
|
|
2056
|
+
"registration_deadline_epoch": registration_deadline_epoch,
|
|
2057
|
+
"enactment_deadline_epoch": enactment_deadline_epoch,
|
|
2058
|
+
"current_phase": current_phase,
|
|
2059
|
+
"time_until_min_activation": time_until_min_activation,
|
|
2060
|
+
"time_until_registration_deadline": time_until_registration_deadline,
|
|
2061
|
+
"time_until_enactment_deadline": time_until_enactment_deadline,
|
|
2062
|
+
"subnet_registration_epochs": subnet_registration_epochs,
|
|
2063
|
+
"subnet_enactment_epochs": subnet_enactment_epochs,
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
return {
|
|
2067
|
+
"success": True,
|
|
2068
|
+
"data": {
|
|
2069
|
+
"subnet_id": subnet_id,
|
|
2070
|
+
"can_activate": can_activate,
|
|
2071
|
+
"requirements_met": requirements_met,
|
|
2072
|
+
"requirements_failed": requirements_failed,
|
|
2073
|
+
"current_epoch": current_epoch,
|
|
2074
|
+
"current_block": current_block,
|
|
2075
|
+
"registration_epoch": registration_epoch,
|
|
2076
|
+
"min_registration_epochs": min_registration_epochs,
|
|
2077
|
+
"epochs_elapsed": epochs_elapsed,
|
|
2078
|
+
"epochs_remaining": epochs_remaining,
|
|
2079
|
+
"subnet_state": subnet_info.state.value if subnet_info.state else None,
|
|
2080
|
+
"total_active_nodes": total_active_nodes,
|
|
2081
|
+
"min_subnet_nodes": min_subnet_nodes,
|
|
2082
|
+
"total_delegate_stake": total_delegate_stake,
|
|
2083
|
+
"min_delegate_stake_required": min_delegate_stake_required,
|
|
2084
|
+
"timeline": timeline,
|
|
2085
|
+
},
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
except Exception as e:
|
|
2089
|
+
logger.error(f"Error checking activation requirements: {e}")
|
|
2090
|
+
return {
|
|
2091
|
+
"success": False,
|
|
2092
|
+
"error": str(e),
|
|
2093
|
+
"data": {
|
|
2094
|
+
"subnet_id": subnet_id,
|
|
2095
|
+
"can_activate": False,
|
|
2096
|
+
"requirements_met": [],
|
|
2097
|
+
"requirements_failed": [f"Error checking requirements: {str(e)}"],
|
|
2098
|
+
"current_epoch": None,
|
|
2099
|
+
"registration_epoch": None,
|
|
2100
|
+
"min_registration_epochs": None,
|
|
2101
|
+
"epochs_elapsed": None,
|
|
2102
|
+
"epochs_remaining": None,
|
|
2103
|
+
},
|
|
2104
|
+
}
|