htcli 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. htcli-1.1.0.dist-info/METADATA +509 -0
  2. htcli-1.1.0.dist-info/RECORD +140 -0
  3. htcli-1.1.0.dist-info/WHEEL +4 -0
  4. htcli-1.1.0.dist-info/entry_points.txt +2 -0
  5. htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
  6. src/__init__.py +0 -0
  7. src/htcli/__init__.py +5 -0
  8. src/htcli/client/__init__.py +338 -0
  9. src/htcli/client/extrinsics/__init__.py +26 -0
  10. src/htcli/client/extrinsics/base.py +487 -0
  11. src/htcli/client/extrinsics/consensus.py +79 -0
  12. src/htcli/client/extrinsics/governance.py +714 -0
  13. src/htcli/client/extrinsics/identity.py +490 -0
  14. src/htcli/client/extrinsics/node.py +1054 -0
  15. src/htcli/client/extrinsics/overwatch.py +401 -0
  16. src/htcli/client/extrinsics/staking.py +1504 -0
  17. src/htcli/client/extrinsics/subnet.py +2218 -0
  18. src/htcli/client/extrinsics/validator.py +203 -0
  19. src/htcli/client/extrinsics/wallet.py +323 -0
  20. src/htcli/client/offchain/__init__.py +10 -0
  21. src/htcli/client/offchain/backup.py +385 -0
  22. src/htcli/client/offchain/config.py +541 -0
  23. src/htcli/client/offchain/wallet.py +839 -0
  24. src/htcli/client/rpc/__init__.py +20 -0
  25. src/htcli/client/rpc/chain.py +568 -0
  26. src/htcli/client/rpc/node.py +783 -0
  27. src/htcli/client/rpc/overwatch.py +680 -0
  28. src/htcli/client/rpc/staking.py +216 -0
  29. src/htcli/client/rpc/subnet.py +2104 -0
  30. src/htcli/client/rpc/wallet.py +912 -0
  31. src/htcli/commands/__init__.py +31 -0
  32. src/htcli/commands/chain/__init__.py +66 -0
  33. src/htcli/commands/chain/display.py +204 -0
  34. src/htcli/commands/chain/handlers.py +260 -0
  35. src/htcli/commands/config/__init__.py +158 -0
  36. src/htcli/commands/config/display.py +353 -0
  37. src/htcli/commands/config/handlers.py +347 -0
  38. src/htcli/commands/config/prompts.py +357 -0
  39. src/htcli/commands/consensus/__init__.py +61 -0
  40. src/htcli/commands/consensus/handlers.py +100 -0
  41. src/htcli/commands/governance/__init__.py +49 -0
  42. src/htcli/commands/governance/handlers.py +81 -0
  43. src/htcli/commands/node/__init__.py +304 -0
  44. src/htcli/commands/node/display.py +749 -0
  45. src/htcli/commands/node/error_handling.py +470 -0
  46. src/htcli/commands/node/handlers.py +844 -0
  47. src/htcli/commands/node/prompts.py +346 -0
  48. src/htcli/commands/overwatch/__init__.py +219 -0
  49. src/htcli/commands/overwatch/display.py +396 -0
  50. src/htcli/commands/overwatch/error_handling.py +276 -0
  51. src/htcli/commands/overwatch/handlers.py +443 -0
  52. src/htcli/commands/overwatch/prompts.py +359 -0
  53. src/htcli/commands/stake/__init__.py +736 -0
  54. src/htcli/commands/stake/display.py +1103 -0
  55. src/htcli/commands/stake/error_handling.py +425 -0
  56. src/htcli/commands/stake/handlers.py +1902 -0
  57. src/htcli/commands/stake/prompts.py +1080 -0
  58. src/htcli/commands/subnet/__init__.py +639 -0
  59. src/htcli/commands/subnet/display.py +801 -0
  60. src/htcli/commands/subnet/error_handling.py +524 -0
  61. src/htcli/commands/subnet/handlers.py +2855 -0
  62. src/htcli/commands/subnet/prompts.py +1225 -0
  63. src/htcli/commands/validator/__init__.py +192 -0
  64. src/htcli/commands/validator/display.py +54 -0
  65. src/htcli/commands/validator/handlers.py +340 -0
  66. src/htcli/commands/wallet/__init__.py +546 -0
  67. src/htcli/commands/wallet/display.py +806 -0
  68. src/htcli/commands/wallet/error_handling.py +210 -0
  69. src/htcli/commands/wallet/handlers.py +3040 -0
  70. src/htcli/commands/wallet/prompts.py +1518 -0
  71. src/htcli/config.py +184 -0
  72. src/htcli/dependencies.py +186 -0
  73. src/htcli/errors/__init__.py +63 -0
  74. src/htcli/errors/base.py +141 -0
  75. src/htcli/errors/display.py +20 -0
  76. src/htcli/errors/handlers.py +710 -0
  77. src/htcli/main.py +343 -0
  78. src/htcli/models/__init__.py +21 -0
  79. src/htcli/models/enums/enum_types.py +35 -0
  80. src/htcli/models/errors.py +103 -0
  81. src/htcli/models/requests/__init__.py +197 -0
  82. src/htcli/models/requests/config.py +70 -0
  83. src/htcli/models/requests/consensus.py +19 -0
  84. src/htcli/models/requests/governance.py +38 -0
  85. src/htcli/models/requests/identity.py +51 -0
  86. src/htcli/models/requests/key.py +22 -0
  87. src/htcli/models/requests/node.py +91 -0
  88. src/htcli/models/requests/overwatch.py +64 -0
  89. src/htcli/models/requests/staking.py +580 -0
  90. src/htcli/models/requests/subnet.py +195 -0
  91. src/htcli/models/requests/validator.py +139 -0
  92. src/htcli/models/requests/wallet.py +118 -0
  93. src/htcli/models/responses/__init__.py +147 -0
  94. src/htcli/models/responses/base.py +18 -0
  95. src/htcli/models/responses/chain.py +39 -0
  96. src/htcli/models/responses/config.py +58 -0
  97. src/htcli/models/responses/identity.py +102 -0
  98. src/htcli/models/responses/overwatch.py +51 -0
  99. src/htcli/models/responses/staking.py +502 -0
  100. src/htcli/models/responses/subnet.py +856 -0
  101. src/htcli/models/responses/wallet.py +185 -0
  102. src/htcli/ui/__init__.py +87 -0
  103. src/htcli/ui/colors.py +309 -0
  104. src/htcli/ui/components/__init__.py +60 -0
  105. src/htcli/ui/components/panels.py +174 -0
  106. src/htcli/ui/components/progress.py +166 -0
  107. src/htcli/ui/components/spinners.py +92 -0
  108. src/htcli/ui/components/tables.py +809 -0
  109. src/htcli/ui/components/trees.py +721 -0
  110. src/htcli/ui/display.py +336 -0
  111. src/htcli/ui/prompts.py +870 -0
  112. src/htcli/utils/__init__.py +76 -0
  113. src/htcli/utils/blockchain/__init__.py +75 -0
  114. src/htcli/utils/blockchain/formatting.py +368 -0
  115. src/htcli/utils/blockchain/patches.py +286 -0
  116. src/htcli/utils/blockchain/peer_id.py +186 -0
  117. src/htcli/utils/blockchain/staking.py +448 -0
  118. src/htcli/utils/blockchain/type_registry.py +1373 -0
  119. src/htcli/utils/blockchain/validation.py +179 -0
  120. src/htcli/utils/cache.py +613 -0
  121. src/htcli/utils/constants.py +38 -0
  122. src/htcli/utils/legacy/__init__.py +12 -0
  123. src/htcli/utils/legacy/colors.py +311 -0
  124. src/htcli/utils/legacy/crypto.py +1176 -0
  125. src/htcli/utils/legacy/formatting.py +452 -0
  126. src/htcli/utils/legacy/interactive.py +306 -0
  127. src/htcli/utils/legacy/subnet_manifest.py +265 -0
  128. src/htcli/utils/legacy/validation.py +488 -0
  129. src/htcli/utils/logging.py +183 -0
  130. src/htcli/utils/network/__init__.py +20 -0
  131. src/htcli/utils/network/subnet.py +344 -0
  132. src/htcli/utils/prompts.py +27 -0
  133. src/htcli/utils/scale_codec.py +155 -0
  134. src/htcli/utils/validation/__init__.py +57 -0
  135. src/htcli/utils/validation/prompt_validators.py +267 -0
  136. src/htcli/utils/wallet/__init__.py +65 -0
  137. src/htcli/utils/wallet/auth.py +151 -0
  138. src/htcli/utils/wallet/core.py +1069 -0
  139. src/htcli/utils/wallet/crypto.py +1615 -0
  140. src/htcli/utils/wallet/migration.py +159 -0
@@ -0,0 +1,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
+ }