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,1373 @@
1
+ """
2
+ Custom type registry for SCALE codec decoding of Hypertensor blockchain types.
3
+
4
+ This module provides the complete type definitions needed to properly decode
5
+ RPC responses from the Hypertensor blockchain. The type definitions must match
6
+ the Rust struct definitions in hypertensor-evm/pallets/network/src/lib.rs exactly.
7
+
8
+ Reference:
9
+ - mesh-template/mesh/substrate/chain_data.py (lines 11-258)
10
+ - hypertensor-evm/pallets/network/src/lib.rs (SubnetInfo line 1082, SubnetNodeInfo line 1184)
11
+ """
12
+
13
+ import copy
14
+
15
+ from scalecodec import ScaleBytes
16
+ from scalecodec.base import RuntimeConfiguration
17
+ from scalecodec.exceptions import RemainingScaleBytesNotEmptyException
18
+ from scalecodec.type_registry import load_type_registry_preset
19
+
20
+ from ..logging import get_logger
21
+
22
+ logger = get_logger(__name__)
23
+
24
+ # Complete custom type registry matching hypertensor-evm blockchain structures
25
+ # Field order MUST match Rust struct definitions exactly for SCALE decoding
26
+ CUSTOM_RPC_TYPE_REGISTRY = {
27
+ "types": {
28
+ # RegistrationSubnetData (used for subnet registration)
29
+ "RegistrationSubnetData": {
30
+ "type": "struct",
31
+ "type_mapping": [
32
+ ["name", "Vec<u8>"],
33
+ ["repo", "Vec<u8>"],
34
+ ["description", "Vec<u8>"],
35
+ ["misc", "Vec<u8>"],
36
+ ["min_stake", "u128"],
37
+ ["max_stake", "u128"],
38
+ ["delegate_stake_percentage", "u128"],
39
+ ["initial_validators", "BTreeMap<u32, u32>"],
40
+ [
41
+ "bootnodes",
42
+ "BTreeMap<PeerId, BoundedVec<u8, DefaultMaxVectorLength>>",
43
+ ],
44
+ ],
45
+ },
46
+ # Subnet Data (basic registration data)
47
+ "SubnetData": {
48
+ "type": "struct",
49
+ "type_mapping": [
50
+ ["id", "u32"],
51
+ ["friendly_id", "u32"],
52
+ ["name", "Vec<u8>"],
53
+ ["repo", "Vec<u8>"],
54
+ ["description", "Vec<u8>"],
55
+ ["misc", "Vec<u8>"],
56
+ ["state", "SubnetState"],
57
+ ["start_epoch", "u32"],
58
+ ],
59
+ },
60
+ # Complete Subnet Info (RPC helper with aggregated data)
61
+ # Reference: hypertensor-evm/pallets/network/src/lib.rs lines 1082-1116
62
+ "SubnetInfo": {
63
+ "type": "struct",
64
+ "type_mapping": [
65
+ ["id", "u32"],
66
+ ["friendly_id", "Option<u32>"],
67
+ ["name", "Vec<u8>"],
68
+ ["repo", "Vec<u8>"],
69
+ ["description", "Vec<u8>"],
70
+ ["misc", "Vec<u8>"],
71
+ ["state", "SubnetState"],
72
+ ["start_epoch", "u32"],
73
+ ["churn_limit", "u32"],
74
+ # NOTE: churn_limit_multiplier exists in Rust struct but is NOT included
75
+ # in the RPC SubnetInfo response from the deployed testnet. Removed to fix
76
+ # SCALE decoding alignment issue.
77
+ ["min_stake", "u128"],
78
+ ["max_stake", "u128"],
79
+ ["queue_immunity_epochs", "u32"],
80
+ ["target_node_registrations_per_epoch", "u32"],
81
+ ["node_registrations_this_epoch", "u32"],
82
+ ["subnet_node_queue_epochs", "u32"],
83
+ ["idle_classification_epochs", "u32"],
84
+ ["included_classification_epochs", "u32"],
85
+ ["delegate_stake_percentage", "u128"],
86
+ ["last_delegate_stake_rewards_update", "u32"],
87
+ ["node_burn_rate_alpha", "u128"],
88
+ ["current_node_burn_rate", "u128"],
89
+ ["initial_coldkeys", "Option<BTreeMap<[u8; 20], u32>>"],
90
+ ["initial_coldkey_data", "Option<BTreeMap<[u8; 20], u32>>"],
91
+ ["max_registered_nodes", "u32"],
92
+ ["owner", "Option<[u8; 20]>"],
93
+ ["pending_owner", "Option<[u8; 20]>"],
94
+ ["registration_epoch", "Option<u32>"],
95
+ ["prev_pause_epoch", "u32"],
96
+ ["key_types", "BTreeSet<KeyType>"],
97
+ ["slot_index", "Option<u32>"],
98
+ ["slot_assignment", "Option<u32>"],
99
+ ["subnet_node_min_weight_decrease_reputation_threshold", "u128"],
100
+ ["reputation", "u128"],
101
+ ["min_subnet_node_reputation", "u128"],
102
+ ["absent_decrease_reputation_factor", "u128"],
103
+ ["included_increase_reputation_factor", "u128"],
104
+ ["below_min_weight_decrease_reputation_factor", "u128"],
105
+ ["non_attestor_decrease_reputation_factor", "u128"],
106
+ ["non_consensus_attestor_decrease_reputation_factor", "u128"],
107
+ ["validator_absent_subnet_node_reputation_factor", "u128"],
108
+ ["validator_non_consensus_subnet_node_reputation_factor", "u128"],
109
+ ["bootnode_access", "BTreeSet<[u8; 20]>"],
110
+ ["bootnodes", "BTreeSet<BoundedVec<u8, DefaultMaxVectorLength>>"],
111
+ ["total_nodes", "u32"],
112
+ ["total_active_nodes", "u32"],
113
+ ["total_electable_nodes", "u32"],
114
+ ["current_min_delegate_stake", "u128"],
115
+ ["total_subnet_stake", "u128"],
116
+ ["total_subnet_delegate_stake_shares", "u128"],
117
+ ["total_subnet_delegate_stake_balance", "u128"],
118
+ ],
119
+ },
120
+ # Subnet State enum
121
+ "SubnetState": {
122
+ "type": "enum",
123
+ "value_list": [
124
+ "Registered",
125
+ "Active",
126
+ "Paused",
127
+ ],
128
+ },
129
+ # Key Type enum (cryptographic key types)
130
+ "KeyType": {
131
+ "type": "enum",
132
+ "value_list": [
133
+ "Rsa",
134
+ "Ed25519",
135
+ "Secp256k1",
136
+ "Ecdsa",
137
+ ],
138
+ },
139
+ # Subnet Node Classification (node class and epoch)
140
+ "SubnetNodeClassification": {
141
+ "type": "struct",
142
+ "type_mapping": [
143
+ ["node_class", "SubnetNodeClass"],
144
+ ["start_epoch", "u32"],
145
+ ],
146
+ },
147
+ "SubnetNodeClass": {
148
+ "type": "enum",
149
+ "value_list": [
150
+ "Registered",
151
+ "Idle",
152
+ "Included",
153
+ "Validator",
154
+ ],
155
+ },
156
+ # Subnet Node (core node data)
157
+ "SubnetNode": {
158
+ "type": "struct",
159
+ "type_mapping": [
160
+ ["id", "u32"],
161
+ ["hotkey", "[u8; 20]"],
162
+ ["peer_id", "PeerId"],
163
+ ["bootnode_peer_id", "PeerId"],
164
+ ["client_peer_id", "PeerId"],
165
+ ["bootnode", "Option<BoundedVec<u8, DefaultMaxVectorLength>>"],
166
+ ["classification", "SubnetNodeClassification"],
167
+ ["delegate_reward_rate", "u128"],
168
+ ["last_delegate_reward_rate_update", "u32"],
169
+ ["unique", "Option<BoundedVec<u8, DefaultMaxVectorLength>>"],
170
+ ["non_unique", "Option<BoundedVec<u8, DefaultMaxVectorLength>>"],
171
+ ],
172
+ },
173
+ # Subnet Node Info (RPC helper with aggregated data)
174
+ # Reference: hypertensor-evm/pallets/network/src/lib.rs lines 1184-1203
175
+ "SubnetNodeInfo": {
176
+ "type": "struct",
177
+ "type_mapping": [
178
+ ["subnet_id", "u32"],
179
+ ["subnet_node_id", "u32"],
180
+ ["coldkey", "[u8; 20]"],
181
+ ["hotkey", "[u8; 20]"],
182
+ ["peer_id", "PeerId"],
183
+ ["bootnode_peer_id", "PeerId"],
184
+ ["client_peer_id", "PeerId"],
185
+ ["bootnode", "Option<BoundedVec<u8, DefaultMaxVectorLength>>"],
186
+ ["identity", "ColdkeyIdentityData"],
187
+ ["classification", "SubnetNodeClassification"],
188
+ ["delegate_reward_rate", "u128"],
189
+ ["last_delegate_reward_rate_update", "u32"],
190
+ ["unique", "Option<BoundedVec<u8, DefaultMaxVectorLength>>"],
191
+ ["non_unique", "Option<BoundedVec<u8, DefaultMaxVectorLength>>"],
192
+ ["stake_balance", "u128"],
193
+ ["total_node_delegate_stake_shares", "u128"],
194
+ ["node_delegate_stake_balance", "u128"],
195
+ ["coldkey_reputation", "Reputation"],
196
+ ["subnet_node_reputation", "u128"],
197
+ ["node_slot_index", "Option<u32>"],
198
+ ["consecutive_idle_epochs", "u32"],
199
+ ["consecutive_included_epochs", "u32"],
200
+ ],
201
+ },
202
+ "ColdkeyIdentityData": {
203
+ "type": "struct",
204
+ "type_mapping": [
205
+ ["name", "BoundedVec<u8, DefaultMaxVectorLength>"],
206
+ ["url", "BoundedVec<u8, DefaultMaxUrlLength>"],
207
+ ["image", "BoundedVec<u8, DefaultMaxUrlLength>"],
208
+ ["discord", "BoundedVec<u8, DefaultMaxSocialIdLength>"],
209
+ ["x", "BoundedVec<u8, DefaultMaxSocialIdLength>"],
210
+ ["telegram", "BoundedVec<u8, DefaultMaxSocialIdLength>"],
211
+ ["github", "BoundedVec<u8, DefaultMaxUrlLength>"],
212
+ ["hugging_face", "BoundedVec<u8, DefaultMaxUrlLength>"],
213
+ ["description", "BoundedVec<u8, DefaultMaxVectorLength>"],
214
+ ["misc", "BoundedVec<u8, DefaultMaxVectorLength>"],
215
+ ],
216
+ },
217
+ # Reputation
218
+ "Reputation": {
219
+ "type": "struct",
220
+ "type_mapping": [
221
+ ["start_epoch", "u32"],
222
+ ["score", "u128"],
223
+ ["lifetime_node_count", "u32"],
224
+ ["total_active_nodes", "u32"],
225
+ ["total_increases", "u32"],
226
+ ["total_decreases", "u32"],
227
+ ["average_attestation", "u128"],
228
+ ["last_validator_epoch", "u32"],
229
+ ["ow_score", "u128"],
230
+ ],
231
+ },
232
+ # Stake Info structures
233
+ "SubnetNodeStakeInfo": {
234
+ "type": "struct",
235
+ "type_mapping": [
236
+ ["subnet_id", "Option<u32>"],
237
+ ["subnet_node_id", "Option<u32>"],
238
+ ["hotkey", "[u8; 20]"],
239
+ ["balance", "u128"],
240
+ ],
241
+ },
242
+ "NodeStakeInfo": {
243
+ "type": "struct",
244
+ "type_mapping": [
245
+ ["subnet_id", "Option<u32>"],
246
+ ["subnet_node_id", "Option<u32>"],
247
+ ["balance", "u128"],
248
+ ],
249
+ },
250
+ "DelegateStakeInfo": {
251
+ "type": "struct",
252
+ "type_mapping": [
253
+ ["subnet_id", "u32"],
254
+ ["shares", "u128"],
255
+ ["balance", "u128"],
256
+ ],
257
+ },
258
+ "NodeDelegateStakeInfo": {
259
+ "type": "struct",
260
+ "type_mapping": [
261
+ ["subnet_id", "u32"],
262
+ ["subnet_node_id", "u32"],
263
+ ["shares", "u128"],
264
+ ["balance", "u128"],
265
+ ],
266
+ },
267
+ # Consensus structures
268
+ "SubnetNodeConsensusData": {
269
+ "type": "struct",
270
+ "type_mapping": [
271
+ ["subnet_node_id", "u32"],
272
+ ["score", "u128"],
273
+ ],
274
+ },
275
+ "AttestEntry": {
276
+ "type": "struct",
277
+ "type_mapping": [
278
+ ["block", "u32"],
279
+ ["attestor_progress", "u128"],
280
+ ["reward_factor", "u128"],
281
+ ["data", "Option<BoundedVec<u8, DefaultValidatorArgsLimit>>"],
282
+ ],
283
+ },
284
+ "ConsensusData": {
285
+ "type": "struct",
286
+ "type_mapping": [
287
+ ["validator_id", "u32"],
288
+ ["block", "u32"],
289
+ ["validator_epoch_progress", "u128"],
290
+ ["validator_reward_factor", "u128"],
291
+ ["attests", "BTreeMap<u32, AttestEntry>"],
292
+ ["subnet_nodes", "Vec<SubnetNode<[u8; 20]>>"],
293
+ ["prioritize_queue_node_id", "Option<u32>"],
294
+ ["remove_queue_node_id", "Option<u32>"],
295
+ ["data", "Vec<SubnetNodeConsensusData>"],
296
+ ["args", "Option<BoundedVec<u8, DefaultValidatorArgsLimit>>"],
297
+ ],
298
+ },
299
+ "ConsensusSubmissionData": {
300
+ "type": "struct",
301
+ "type_mapping": [
302
+ ["validator_subnet_node_id", "u32"],
303
+ ["validator_epoch_progress", "u128"],
304
+ ["validator_reward_factor", "u128"],
305
+ ["attestation_ratio", "u128"],
306
+ ["weight_sum", "u128"],
307
+ ["data_length", "u32"],
308
+ ["data", "Vec<SubnetNodeConsensusData>"],
309
+ ["attests", "BTreeMap<u32, AttestEntry>"],
310
+ ["subnet_nodes", "Vec<SubnetNode<[u8; 20]>>"],
311
+ ["prioritize_queue_node_id", "Option<u32>"],
312
+ ["remove_queue_node_id", "Option<u32>"],
313
+ ],
314
+ },
315
+ "RewardsData": {
316
+ "type": "struct",
317
+ "type_mapping": [
318
+ ["overall_subnet_reward", "u128"],
319
+ ["subnet_owner_reward", "u128"],
320
+ ["subnet_rewards", "u128"],
321
+ ["delegate_stake_rewards", "u128"],
322
+ ["subnet_node_rewards", "u128"],
323
+ ],
324
+ },
325
+ "AllSubnetBootnodes": {
326
+ "type": "struct",
327
+ "type_mapping": [
328
+ ["bootnodes", "BTreeSet<BoundedVec<u8, DefaultMaxVectorLength>>"],
329
+ ["node_bootnodes", "BTreeSet<BoundedVec<u8, DefaultMaxVectorLength>>"],
330
+ ],
331
+ },
332
+ # Overwatch Node types
333
+ # Reference: hypertensor-evm/pallets/network/src/lib.rs lines 1741-1744
334
+ "OverwatchNode": {
335
+ "type": "struct",
336
+ "type_mapping": [
337
+ ["id", "u32"],
338
+ ["hotkey", "[u8; 20]"],
339
+ ],
340
+ },
341
+ # Reference: hypertensor-evm/pallets/network/src/lib.rs lines 1721-1727
342
+ "OverwatchNodeInfo": {
343
+ "type": "struct",
344
+ "type_mapping": [
345
+ ["overwatch_node_id", "u32"],
346
+ ["coldkey", "[u8; 20]"],
347
+ ["hotkey", "[u8; 20]"],
348
+ ["peer_ids", "Vec<PeerId>"],
349
+ ["reputation", "Reputation"],
350
+ ],
351
+ },
352
+ # Type aliases for bounded vectors and collections
353
+ "PeerId": "Vec<u8>",
354
+ "OpaquePeerId": "Vec<u8>",
355
+ # AccountId is AccountId20 which is [u8; 20]
356
+ "AccountId": "[u8; 20]",
357
+ "BTreeSet<KeyType>": "Vec<KeyType>",
358
+ "BTreeSet<[u8; 20]>": "Vec<[u8; 20]>",
359
+ "BTreeSet<BoundedVec<u8, DefaultMaxVectorLength>>": "Vec<BoundedVec<u8, DefaultMaxVectorLength>>",
360
+ "BTreeMap<[u8; 20], u32>": "Vec<([u8; 20], u32)>", # BTreeMap decoded as Vec of tuples
361
+ "BTreeMap<AccountId, u32>": "Vec<([u8; 20], u32)>", # AccountId is [u8; 20]
362
+ "BTreeMap<u32, AttestEntry>": "Vec<(u32, AttestEntry)>",
363
+ "BoundedVec<u8, DefaultMaxVectorLength>": "Vec<u8>",
364
+ "BoundedVec<u8, DefaultMaxUrlLength>": "Vec<u8>",
365
+ "BoundedVec<u8, DefaultMaxSocialIdLength>": "Vec<u8>",
366
+ "BoundedVec<u8, DefaultValidatorArgsLimit>": "Vec<u8>",
367
+ "Option<BoundedVec<u8, DefaultMaxVectorLength>>": "Option<Vec<u8>>",
368
+ "Option<BoundedVec<u8, DefaultMaxUrlLength>>": "Option<Vec<u8>>",
369
+ "Option<BoundedVec<u8, DefaultMaxSocialIdLength>>": "Option<Vec<u8>>",
370
+ "Option<BoundedVec<u8, DefaultValidatorArgsLimit>>": "Option<Vec<u8>>",
371
+ "Option<BTreeMap<[u8; 20], u32>>": "Option<Vec<([u8; 20], u32)>>",
372
+ }
373
+ }
374
+
375
+ LEGACY_RPC_TYPE_REGISTRY = copy.deepcopy(CUSTOM_RPC_TYPE_REGISTRY)
376
+ LEGACY_RPC_TYPE_REGISTRY["types"]["SubnetData"]["type_mapping"] = [
377
+ mapping
378
+ for mapping in CUSTOM_RPC_TYPE_REGISTRY["types"]["SubnetData"]["type_mapping"]
379
+ if mapping[0] != "friendly_id"
380
+ ]
381
+ LEGACY_RPC_TYPE_REGISTRY["types"]["SubnetInfo"]["type_mapping"] = [
382
+ mapping
383
+ for mapping in CUSTOM_RPC_TYPE_REGISTRY["types"]["SubnetInfo"]["type_mapping"]
384
+ if mapping[0] != "friendly_id"
385
+ ]
386
+
387
+
388
+ def get_rpc_runtime_config(use_legacy: bool = False) -> RuntimeConfiguration:
389
+ """
390
+ Get properly configured RuntimeConfiguration for RPC SCALE decoding.
391
+
392
+ This creates a RuntimeConfiguration with both the legacy type registry
393
+ and our custom Hypertensor types, matching the pattern from mesh-template.
394
+
395
+ Args:
396
+ use_legacy: Build a config that matches the legacy subnet schema.
397
+
398
+ Returns:
399
+ RuntimeConfiguration configured for decoding Hypertensor RPC responses.
400
+ """
401
+ config = RuntimeConfiguration()
402
+ config.update_type_registry(load_type_registry_preset("legacy"))
403
+ config.update_type_registry(
404
+ LEGACY_RPC_TYPE_REGISTRY if use_legacy else CUSTOM_RPC_TYPE_REGISTRY
405
+ )
406
+ return config
407
+
408
+
409
+ def _normalize_scale_input(data):
410
+ if isinstance(data, list):
411
+ return bytes(data)
412
+ if isinstance(data, str):
413
+ hex_data = data[2:] if data.startswith("0x") else data
414
+ return bytes.fromhex(hex_data)
415
+ if isinstance(data, bytes):
416
+ return data
417
+ return bytes(data)
418
+
419
+
420
+ def _decode_scale_object(data, type_name: str, allow_legacy_fallback: bool = False):
421
+ normalized = _normalize_scale_input(data)
422
+ configs = [get_rpc_runtime_config()]
423
+ if allow_legacy_fallback:
424
+ configs.append(get_rpc_runtime_config(use_legacy=True))
425
+
426
+ last_error: Exception | None = None
427
+ for idx, config in enumerate(configs):
428
+ obj = config.create_scale_object(type_name)
429
+ try:
430
+ decoded = obj.decode(ScaleBytes(normalized))
431
+ if allow_legacy_fallback and idx == 1:
432
+ logger.info("Decoded %s with legacy registry", type_name)
433
+ return decoded
434
+ except RemainingScaleBytesNotEmptyException as exc:
435
+ last_error = exc
436
+ if allow_legacy_fallback and idx == 0:
437
+ logger.warning(
438
+ "Remaining SCALE bytes when decoding %s with latest schema (%s); "
439
+ "retrying legacy registry",
440
+ type_name,
441
+ exc,
442
+ )
443
+ continue
444
+ raise
445
+ except Exception as exc: # pragma: no cover
446
+ last_error = exc
447
+ if allow_legacy_fallback and idx == 0:
448
+ logger.warning(
449
+ "Error decoding %s with latest schema (%s); retrying legacy registry",
450
+ type_name,
451
+ exc,
452
+ )
453
+ continue
454
+ raise
455
+
456
+ if last_error:
457
+ raise last_error
458
+ return None
459
+
460
+
461
+ def decode_option_subnet_info(data: bytes) -> dict:
462
+ """
463
+ Decode Option<SubnetInfo> from SCALE bytes.
464
+
465
+ This function handles the case where decoding raises RemainingScaleBytesNotEmptyException
466
+ but the decoded value is still available in obj.value. It uses the same approach as
467
+ decode_vec_subnet_info - decode the SubnetInfo directly and extract the value even when
468
+ an exception is raised.
469
+
470
+ Args:
471
+ data: SCALE-encoded bytes (list of ints or bytes object)
472
+
473
+ Returns:
474
+ Decoded SubnetInfo dict or None if subnet not found
475
+ """
476
+ normalized = _normalize_scale_input(data)
477
+ if not normalized:
478
+ return None
479
+
480
+ config = get_rpc_runtime_config()
481
+
482
+ # Check first byte to see if Option is Some (0x01) or None (0x00)
483
+ if len(normalized) == 0:
484
+ return None
485
+
486
+ option_byte = normalized[0]
487
+ if option_byte == 0x00:
488
+ # Option::None
489
+ return None
490
+ elif option_byte == 0x01:
491
+ # Option::Some - decode the SubnetInfo
492
+ # Skip the Option discriminant byte (0x01)
493
+ subnet_bytes = normalized[1:]
494
+
495
+ # Decode SubnetInfo directly (same approach as decode_vec_subnet_info)
496
+ subnet_obj = config.create_scale_object("SubnetInfo")
497
+ try:
498
+ subnet_obj.decode(ScaleBytes(subnet_bytes))
499
+ # Decode succeeded without exception
500
+ except RemainingScaleBytesNotEmptyException:
501
+ # Exception is expected - scalecodec sets obj.value before raising it
502
+ pass
503
+ except Exception as e:
504
+ logger.error(f"Failed to decode SubnetInfo in Option: {e}")
505
+ return None
506
+
507
+ # Extract decoded value (always set even when exception was raised)
508
+ if hasattr(subnet_obj, "value") and subnet_obj.value:
509
+ return subnet_obj.value
510
+
511
+ logger.error("Failed to decode SubnetInfo from Option - no value extracted")
512
+ return None
513
+ else:
514
+ logger.error(f"Invalid Option discriminant byte: {option_byte}")
515
+ return None
516
+
517
+
518
+ def decode_vec_subnet_info(data: bytes) -> list[dict]:
519
+ """
520
+ Decode Vec<SubnetInfo> from SCALE bytes.
521
+
522
+ This function attempts to decode the entire Vec<SubnetInfo> at once using scalecodec,
523
+ and extracts all decoded values from the Vec object even if exceptions are raised.
524
+
525
+ Args:
526
+ data: SCALE-encoded bytes (includes Vec length byte at the start)
527
+
528
+ Returns:
529
+ List of decoded SubnetInfo dicts
530
+ """
531
+ normalized = _normalize_scale_input(data)
532
+ if not normalized:
533
+ logger.warning("No data provided to decode_vec_subnet_info")
534
+ return []
535
+
536
+ config = get_rpc_runtime_config()
537
+
538
+ try:
539
+ # Try direct Vec decoding - scalecodec should handle Vec<SubnetInfo>
540
+ vec_obj = config.create_scale_object("Vec<SubnetInfo>")
541
+
542
+ # Decode and catch exceptions - value may still be populated
543
+ exception_occurred = False
544
+ exception_msg = None
545
+ try:
546
+ vec_obj.decode(ScaleBytes(normalized))
547
+ logger.debug("Vec<SubnetInfo> decoded without exception")
548
+ except RemainingScaleBytesNotEmptyException as e:
549
+ # This is expected - Vec decoding may raise this even when values are decoded
550
+ exception_occurred = True
551
+ exception_msg = str(e)
552
+ logger.debug(
553
+ f"RemainingScaleBytesNotEmptyException during Vec decode: {exception_msg}"
554
+ )
555
+ except Exception as e:
556
+ logger.warning(
557
+ f"Exception during Vec decode: {e}, trying to extract partial results"
558
+ )
559
+ exception_occurred = True
560
+ exception_msg = str(e)
561
+
562
+ # Try to extract decoded values from the Vec object
563
+ # scalecodec's Vec type stores elements in different places depending on the state
564
+ decoded_list = None
565
+
566
+ # Method 1: Direct value attribute (most common)
567
+ if hasattr(vec_obj, "value") and vec_obj.value is not None:
568
+ decoded_list = vec_obj.value
569
+ logger.debug(
570
+ f"Found value in vec_obj.value: {type(decoded_list)}, length: {len(decoded_list) if isinstance(decoded_list, list) else 'N/A'}"
571
+ )
572
+
573
+ # Method 2: Check if Vec has a value_list attribute (some Vec implementations)
574
+ if (
575
+ decoded_list is None
576
+ or (isinstance(decoded_list, list) and len(decoded_list) == 0)
577
+ ) and hasattr(vec_obj, "value_list"):
578
+ if vec_obj.value_list:
579
+ decoded_list = vec_obj.value_list
580
+ logger.debug(
581
+ f"Found value in vec_obj.value_list: {len(decoded_list)} items"
582
+ )
583
+
584
+ # Method 3: Try to access elements via Vec's internal structure
585
+ # scalecodec Vec objects may have _elements or elements attributes
586
+ if decoded_list is None or (
587
+ isinstance(decoded_list, list) and len(decoded_list) == 0
588
+ ):
589
+ # Try accessing internal elements
590
+ for attr_name in [
591
+ "elements",
592
+ "_elements",
593
+ "decoded_elements",
594
+ "value_serialized",
595
+ ]:
596
+ if hasattr(vec_obj, attr_name):
597
+ try:
598
+ attr_value = getattr(vec_obj, attr_name)
599
+ if attr_value is not None:
600
+ # Check if it's a list or something we can convert
601
+ if isinstance(attr_value, list) and len(attr_value) > 0:
602
+ decoded_list = attr_value
603
+ logger.debug(
604
+ f"Found value in vec_obj.{attr_name}: {len(decoded_list)} items"
605
+ )
606
+ break
607
+ elif hasattr(attr_value, "__iter__") and not isinstance(
608
+ attr_value, (str, bytes)
609
+ ):
610
+ # Try to convert to list
611
+ try:
612
+ decoded_list = list(attr_value)
613
+ if decoded_list:
614
+ logger.debug(
615
+ f"Found value in vec_obj.{attr_name} (converted): {len(decoded_list)} items"
616
+ )
617
+ break
618
+ except Exception:
619
+ pass
620
+ except Exception as attr_err:
621
+ logger.debug(
622
+ f"Could not access vec_obj.{attr_name}: {attr_err}"
623
+ )
624
+ continue
625
+
626
+ # Normalize to list
627
+ if decoded_list is not None:
628
+ if not isinstance(decoded_list, list):
629
+ decoded_list = [decoded_list]
630
+
631
+ # Filter out None values
632
+ decoded_list = [item for item in decoded_list if item is not None]
633
+
634
+ if decoded_list:
635
+ logger.info(
636
+ f"Successfully decoded {len(decoded_list)} subnet(s) from Vec<SubnetInfo> "
637
+ f"(exception occurred: {exception_occurred})"
638
+ )
639
+ return decoded_list
640
+
641
+ logger.warning(
642
+ "Could not extract decoded values from Vec object using direct Vec decode"
643
+ )
644
+ logger.debug(
645
+ "Attempting to manually process Vec elements using Vec's internal decoder"
646
+ )
647
+
648
+ # Try to use Vec's internal decoder to process elements one by one
649
+ try:
650
+ # Access Vec's data and sub_type to decode elements manually
651
+ vec_length_byte = normalized[0]
652
+
653
+ # Read Vec length
654
+ if (vec_length_byte & 0x03) == 0x00:
655
+ vec_length = vec_length_byte >> 2
656
+ element_offset = 1
657
+ elif (vec_length_byte & 0x03) == 0x01:
658
+ vec_length = ((vec_length_byte >> 2) | (normalized[1] << 6)) & 0x3FFF
659
+ element_offset = 2
660
+ elif (vec_length_byte & 0x03) == 0x02:
661
+ vec_length = (
662
+ ((vec_length_byte >> 2) & 0x3F)
663
+ | (normalized[1] << 6)
664
+ | (normalized[2] << 14)
665
+ | (normalized[3] << 22)
666
+ ) & 0x3FFFFFFF
667
+ element_offset = 4
668
+ else:
669
+ vec_length = 0
670
+ element_offset = 1
671
+
672
+ logger.debug(
673
+ f"Vec length from compact encoding: {vec_length}, elements start at offset {element_offset}"
674
+ )
675
+
676
+ # Try to use Vec's sub_type decoder to decode each element
677
+ if hasattr(vec_obj, "sub_type") and vec_length > 0:
678
+ decoded_elements = []
679
+ current_offset = element_offset
680
+
681
+ for i in range(vec_length):
682
+ if current_offset >= len(normalized):
683
+ logger.warning(
684
+ f"Ran out of bytes at element {i + 1}/{vec_length}"
685
+ )
686
+ break
687
+
688
+ # Create a SubnetInfo decoder
689
+ element_obj = config.create_scale_object("SubnetInfo")
690
+ element_bytes = normalized[current_offset:]
691
+
692
+ try:
693
+ element_obj.decode(ScaleBytes(element_bytes))
694
+ # Check if we got a value
695
+ if (
696
+ hasattr(element_obj, "value")
697
+ and element_obj.value is not None
698
+ ):
699
+ decoded_elements.append(element_obj.value)
700
+ # Get consumed bytes
701
+ if hasattr(element_obj, "data") and hasattr(
702
+ element_obj.data, "offset"
703
+ ):
704
+ consumed = element_obj.data.offset
705
+ if 0 < consumed <= len(element_bytes):
706
+ current_offset += consumed
707
+ logger.debug(
708
+ f"Element {i + 1} decoded, consumed {consumed} bytes, new offset: {current_offset}"
709
+ )
710
+ else:
711
+ logger.warning(
712
+ f"Invalid consumed bytes {consumed} for element {i + 1}, estimating"
713
+ )
714
+ # Estimate based on average
715
+ estimated = (
716
+ len(normalized) // vec_length
717
+ if vec_length > 0
718
+ else 500
719
+ )
720
+ current_offset += min(estimated, len(element_bytes))
721
+ else:
722
+ # Estimate
723
+ estimated = (
724
+ len(normalized) // vec_length
725
+ if vec_length > 0
726
+ else 500
727
+ )
728
+ current_offset += min(estimated, len(element_bytes))
729
+ else:
730
+ logger.warning(f"Element {i + 1} decode returned no value")
731
+ break
732
+ except RemainingScaleBytesNotEmptyException as e:
733
+ # Exception is OK if we got a value
734
+ if (
735
+ hasattr(element_obj, "value")
736
+ and element_obj.value is not None
737
+ ):
738
+ decoded_elements.append(element_obj.value)
739
+ # Try to get consumed bytes
740
+ if hasattr(element_obj, "data") and hasattr(
741
+ element_obj.data, "offset"
742
+ ):
743
+ consumed = element_obj.data.offset
744
+ if 0 < consumed <= len(element_bytes):
745
+ current_offset += consumed
746
+ else:
747
+ # Try to extract from exception
748
+ exc_msg = str(e)
749
+ if "Current offset:" in exc_msg:
750
+ try:
751
+ consumed = int(
752
+ exc_msg.split("Current offset:")[1]
753
+ .split("/")[0]
754
+ .strip()
755
+ )
756
+ if 0 < consumed <= len(element_bytes):
757
+ current_offset += consumed
758
+ else:
759
+ break
760
+ except Exception:
761
+ break
762
+ else:
763
+ # Estimate
764
+ estimated = (
765
+ len(normalized) // vec_length
766
+ if vec_length > 0
767
+ else 500
768
+ )
769
+ current_offset += min(
770
+ estimated, len(element_bytes)
771
+ )
772
+ else:
773
+ break
774
+ else:
775
+ logger.warning(
776
+ f"Element {i + 1} decode failed with no value: {e}"
777
+ )
778
+ break
779
+ except Exception as e:
780
+ logger.error(f"Element {i + 1} decode failed: {e}")
781
+ break
782
+
783
+ if decoded_elements and len(decoded_elements) == vec_length:
784
+ logger.info(
785
+ f"Successfully decoded all {len(decoded_elements)} subnet(s) using Vec element iteration"
786
+ )
787
+ return decoded_elements
788
+ elif decoded_elements:
789
+ logger.warning(
790
+ f"Only decoded {len(decoded_elements)}/{vec_length} elements using Vec iteration"
791
+ )
792
+ # Don't return partial - let it fall through to manual iteration
793
+ except Exception as e:
794
+ logger.debug(
795
+ f"Vec element iteration failed: {e}, falling back to manual iteration"
796
+ )
797
+
798
+ # Manual iteration approach: decode Vec length, then each SubnetInfo individually
799
+ logger.debug("Attempting manual iteration for Vec<SubnetInfo>")
800
+
801
+ # Read Vec length (compact-encoded)
802
+ if len(normalized) < 1:
803
+ logger.error("Not enough data to read Vec length")
804
+ return []
805
+
806
+ vec_length_byte = normalized[0]
807
+ # Compact encoding: first 2 bits determine format
808
+ # 00 = single byte (max 63), 01 = 2 bytes, 10 = 4 bytes, 11 = large integer
809
+ if (vec_length_byte & 0x03) == 0x00:
810
+ vec_length = vec_length_byte >> 2
811
+ offset = 1
812
+ elif (vec_length_byte & 0x03) == 0x01:
813
+ # 2-byte compact
814
+ if len(normalized) < 2:
815
+ logger.error("Not enough data for 2-byte compact length")
816
+ return []
817
+ vec_length = ((vec_length_byte >> 2) | (normalized[1] << 6)) & 0x3FFF
818
+ offset = 2
819
+ elif (vec_length_byte & 0x03) == 0x02:
820
+ # 4-byte compact
821
+ if len(normalized) < 4:
822
+ logger.error("Not enough data for 4-byte compact length")
823
+ return []
824
+ vec_length = (
825
+ ((vec_length_byte >> 2) & 0x3F)
826
+ | (normalized[1] << 6)
827
+ | (normalized[2] << 14)
828
+ | (normalized[3] << 22)
829
+ ) & 0x3FFFFFFF
830
+ offset = 4
831
+ else:
832
+ logger.error("Large integer compact encoding not supported for Vec length")
833
+ return []
834
+
835
+ logger.debug(f"Vec length: {vec_length}, starting at offset {offset}")
836
+
837
+ decoded_subnets = []
838
+
839
+ # Decode each SubnetInfo individually
840
+ for i in range(vec_length):
841
+ if offset >= len(normalized):
842
+ logger.warning(
843
+ f"Ran out of data while decoding subnet {i + 1}/{vec_length} at offset {offset}"
844
+ )
845
+ break
846
+
847
+ subnet_bytes = normalized[offset:]
848
+ logger.debug(
849
+ f"Decoding subnet {i + 1}/{vec_length} at offset {offset}, {len(subnet_bytes)} bytes remaining"
850
+ )
851
+
852
+ single_obj = config.create_scale_object("SubnetInfo")
853
+ exception_raised = None
854
+ consumed_bytes = 0
855
+
856
+ try:
857
+ single_obj.decode(ScaleBytes(subnet_bytes))
858
+ # Success - no exception
859
+ except RemainingScaleBytesNotEmptyException as e:
860
+ # Expected - SubnetInfo decode might raise this even on success
861
+ exception_raised = e
862
+ # Try to extract consumed bytes from exception message in various formats
863
+ exc_msg = str(e)
864
+ if "Current offset:" in exc_msg:
865
+ try:
866
+ # Parse offset from exception: "Decoding <SubnetInfo> - Current offset: 359 / length: 1560"
867
+ parts = (
868
+ exc_msg.split("Current offset:")[1].split("/")[0].strip()
869
+ )
870
+ consumed_bytes = int(parts)
871
+ logger.debug(
872
+ f"Extracted consumed bytes from 'Current offset:' pattern: {consumed_bytes}"
873
+ )
874
+ except (ValueError, IndexError) as parse_error:
875
+ logger.debug(
876
+ f"Could not parse offset from 'Current offset:' pattern: {parse_error}"
877
+ )
878
+ elif "needed:" in exc_msg and "/ total:" in exc_msg:
879
+ # Handle "Decoding <Bytes> - No more bytes available (needed: X / total: Y)"
880
+ # This usually means we ran out of bytes, but check if data.offset gives us a value
881
+ try:
882
+ if hasattr(single_obj, "data") and hasattr(
883
+ single_obj.data, "offset"
884
+ ):
885
+ # The offset should tell us how many bytes were consumed before the error
886
+ potential_offset = single_obj.data.offset
887
+ # If offset is reasonable (less than remaining bytes), use it
888
+ if 0 < potential_offset <= len(subnet_bytes):
889
+ consumed_bytes = potential_offset
890
+ logger.debug(
891
+ f"Extracted consumed bytes from data.offset after 'needed/total' exception: {consumed_bytes}"
892
+ )
893
+ except Exception as offset_error:
894
+ logger.debug(f"Could not get offset from data: {offset_error}")
895
+ except Exception as e:
896
+ logger.error(f"Failed to decode subnet {i + 1}: {e}")
897
+ break
898
+
899
+ # Extract value even if exception was raised
900
+ # Check if value exists and is not None
901
+ has_value = hasattr(single_obj, "value") and single_obj.value is not None
902
+
903
+ if has_value:
904
+ decoded_subnets.append(single_obj.value)
905
+
906
+ # Get consumed bytes - try multiple methods
907
+ if consumed_bytes == 0:
908
+ # Try data.offset first
909
+ if hasattr(single_obj, "data") and hasattr(
910
+ single_obj.data, "offset"
911
+ ):
912
+ potential_offset = single_obj.data.offset
913
+ # Only use if it's reasonable
914
+ if 0 < potential_offset <= len(subnet_bytes):
915
+ consumed_bytes = potential_offset
916
+ logger.debug(
917
+ f"Got consumed bytes from data.offset: {consumed_bytes}"
918
+ )
919
+
920
+ # Advance offset
921
+ if consumed_bytes > 0:
922
+ offset += consumed_bytes
923
+ logger.debug(
924
+ f"Subnet {i + 1} decoded successfully, consumed {consumed_bytes} bytes, new offset: {offset}"
925
+ )
926
+ else:
927
+ # Last resort: estimate based on remaining bytes
928
+ if i < vec_length - 1:
929
+ # Estimate size based on remaining bytes divided by remaining subnets
930
+ remaining_subnets = vec_length - i
931
+ estimated_size = len(subnet_bytes) // remaining_subnets
932
+ # Use minimum of estimated size or a reasonable maximum (500 bytes)
933
+ consumed_bytes = min(estimated_size, 500)
934
+ offset += consumed_bytes
935
+ logger.warning(
936
+ f"Subnet {i + 1} decoded but couldn't determine consumed bytes, using estimate: {consumed_bytes} bytes"
937
+ )
938
+ else:
939
+ logger.debug(
940
+ f"Last subnet {i + 1} decoded, no need to advance offset"
941
+ )
942
+ else:
943
+ # No value extracted - this subnet decoding failed
944
+ # If we have consumed_bytes from data.offset, we might still want to try skipping forward
945
+ if consumed_bytes > 0 and consumed_bytes <= len(subnet_bytes):
946
+ logger.warning(
947
+ f"Subnet {i + 1} decoding failed but got consumed_bytes={consumed_bytes}, advancing anyway"
948
+ )
949
+ offset += consumed_bytes
950
+ else:
951
+ logger.error(f"No value extracted for subnet {i + 1}")
952
+ if exception_raised:
953
+ logger.debug(f"Exception was: {exception_raised}")
954
+ # Try to estimate and continue if not the last subnet
955
+ if i < vec_length - 1:
956
+ remaining_subnets = vec_length - i
957
+ estimated_size = len(subnet_bytes) // remaining_subnets
958
+ estimated_size = min(estimated_size, 500)
959
+ logger.warning(
960
+ f"Attempting to continue with estimated size: {estimated_size} bytes"
961
+ )
962
+ offset += estimated_size
963
+ else:
964
+ break
965
+
966
+ if decoded_subnets:
967
+ # Check if we decoded all expected subnets
968
+ if len(decoded_subnets) < vec_length:
969
+ logger.warning(
970
+ f"Manual iteration only decoded {len(decoded_subnets)}/{vec_length} subnets. "
971
+ "Returning empty list to trigger fallback method."
972
+ )
973
+ return []
974
+ logger.info(
975
+ f"Successfully decoded {len(decoded_subnets)} subnet(s) using manual iteration"
976
+ )
977
+ return decoded_subnets
978
+ else:
979
+ logger.error("Manual iteration decoded 0 subnets")
980
+ return []
981
+
982
+ except Exception as e:
983
+ logger.error(f"Unexpected error decoding Vec<SubnetInfo>: {e}")
984
+ return []
985
+
986
+
987
+ def _manual_decode_vec_subnet_node_info(
988
+ normalized: bytes, config, vec_length: int
989
+ ) -> list[dict]:
990
+ """
991
+ Manually decode Vec<SubnetNodeInfo> by iterating through each element.
992
+
993
+ This is used as a fallback when the direct Vec decode only returns partial results.
994
+ """
995
+ try:
996
+ # Read Vec length from compact encoding (don't trust the passed vec_length parameter)
997
+ vec_length_byte = normalized[0]
998
+ actual_vec_length = vec_length # Default to passed value
999
+
1000
+ # Decode actual Vec length from compact encoding
1001
+ if (vec_length_byte & 0x03) == 0x00:
1002
+ actual_vec_length = vec_length_byte >> 2
1003
+ offset = 1
1004
+ elif (vec_length_byte & 0x03) == 0x01:
1005
+ if len(normalized) >= 2:
1006
+ actual_vec_length = (
1007
+ (vec_length_byte >> 2) | (normalized[1] << 6)
1008
+ ) & 0x3FFF
1009
+ offset = 2
1010
+ else:
1011
+ offset = 2
1012
+ elif (vec_length_byte & 0x03) == 0x02:
1013
+ if len(normalized) >= 4:
1014
+ actual_vec_length = (
1015
+ ((vec_length_byte >> 2) & 0x3F)
1016
+ | (normalized[1] << 6)
1017
+ | (normalized[2] << 14)
1018
+ | (normalized[3] << 22)
1019
+ ) & 0x3FFFFFFF
1020
+ offset = 4
1021
+ else:
1022
+ offset = 4
1023
+ else:
1024
+ offset = 1
1025
+
1026
+ decoded_nodes = []
1027
+
1028
+ # Decode each SubnetNodeInfo individually using the actual Vec length
1029
+ for i in range(actual_vec_length):
1030
+ if offset >= len(normalized):
1031
+ logger.warning(
1032
+ f"Ran out of data while decoding node {i + 1}/{actual_vec_length} at offset {offset}"
1033
+ )
1034
+ break
1035
+
1036
+ node_bytes = normalized[offset:]
1037
+
1038
+ single_obj = config.create_scale_object("SubnetNodeInfo")
1039
+ consumed_bytes = 0
1040
+
1041
+ try:
1042
+ single_obj.decode(ScaleBytes(node_bytes))
1043
+ # Success - no exception
1044
+ except RemainingScaleBytesNotEmptyException as e:
1045
+ # Expected - SubnetNodeInfo decode might raise this even on success
1046
+ exc_msg = str(e)
1047
+ # Try to extract consumed bytes from exception message in various formats
1048
+ if "Current offset:" in exc_msg:
1049
+ try:
1050
+ # Parse offset from exception: "Decoding <SubnetNodeInfo> - Current offset: 359 / length: 1560"
1051
+ parts = (
1052
+ exc_msg.split("Current offset:")[1].split("/")[0].strip()
1053
+ )
1054
+ consumed_bytes = int(parts)
1055
+ except (ValueError, IndexError):
1056
+ pass
1057
+ elif "needed:" in exc_msg and "/ total:" in exc_msg:
1058
+ # Handle "Decoding <Bytes> - No more bytes available (needed: X / total: Y)"
1059
+ # This usually means we ran out of bytes, but check if data.offset gives us a value
1060
+ try:
1061
+ if hasattr(single_obj, "data") and hasattr(
1062
+ single_obj.data, "offset"
1063
+ ):
1064
+ potential_offset = single_obj.data.offset
1065
+ if 0 < potential_offset <= len(node_bytes):
1066
+ consumed_bytes = potential_offset
1067
+ except Exception:
1068
+ pass
1069
+ except Exception as e:
1070
+ logger.error(f"Failed to decode node {i + 1}: {e}")
1071
+ break
1072
+
1073
+ # Extract value even if exception was raised
1074
+ has_value = hasattr(single_obj, "value") and single_obj.value is not None
1075
+
1076
+ if has_value:
1077
+ decoded_nodes.append(single_obj.value)
1078
+
1079
+ # Get consumed bytes - try multiple methods
1080
+ if consumed_bytes == 0:
1081
+ # Try data.offset first
1082
+ if hasattr(single_obj, "data") and hasattr(
1083
+ single_obj.data, "offset"
1084
+ ):
1085
+ potential_offset = single_obj.data.offset
1086
+ # Only use if it's reasonable
1087
+ if 0 < potential_offset <= len(node_bytes):
1088
+ consumed_bytes = potential_offset
1089
+
1090
+ # Advance offset
1091
+ if consumed_bytes > 0:
1092
+ offset += consumed_bytes
1093
+ else:
1094
+ # Last resort: estimate based on remaining bytes
1095
+ if i < actual_vec_length - 1:
1096
+ # Estimate size based on remaining bytes divided by remaining nodes
1097
+ remaining_nodes = actual_vec_length - i
1098
+ estimated_size = len(node_bytes) // remaining_nodes
1099
+ # Use minimum of estimated size or a reasonable maximum (500 bytes)
1100
+ consumed_bytes = min(estimated_size, 500)
1101
+ offset += consumed_bytes
1102
+ else:
1103
+ if i < actual_vec_length - 1:
1104
+ remaining_nodes = actual_vec_length - i
1105
+ estimated_size = len(node_bytes) // remaining_nodes
1106
+ estimated_size = min(estimated_size, 500)
1107
+ offset += estimated_size
1108
+ else:
1109
+ break
1110
+
1111
+ if decoded_nodes:
1112
+ logger.info(
1113
+ f"Manually decoded {len(decoded_nodes)}/{actual_vec_length} node(s)"
1114
+ )
1115
+ return decoded_nodes
1116
+ else:
1117
+ logger.error("Manual decode decoded 0 nodes")
1118
+ return []
1119
+
1120
+ except Exception as e:
1121
+ logger.error(f"Unexpected error in manual Vec<SubnetNodeInfo> decode: {e}")
1122
+ return []
1123
+
1124
+
1125
+ def decode_option_subnet_node_info(data: bytes) -> dict:
1126
+ """
1127
+ Decode Option<SubnetNodeInfo> from SCALE bytes.
1128
+
1129
+ Args:
1130
+ data: SCALE-encoded bytes
1131
+
1132
+ Returns:
1133
+ Decoded SubnetNodeInfo dict or None if not found
1134
+ """
1135
+ normalized = _normalize_scale_input(data)
1136
+ if not normalized:
1137
+ return None
1138
+
1139
+ config = get_rpc_runtime_config()
1140
+
1141
+ # Check first byte to see if Option is Some (0x01) or None (0x00)
1142
+ if len(normalized) == 0:
1143
+ return None
1144
+
1145
+ option_byte = normalized[0]
1146
+ if option_byte == 0x00:
1147
+ # Option::None
1148
+ return None
1149
+ elif option_byte == 0x01:
1150
+ # Option::Some - decode the SubnetNodeInfo directly (skip discriminant)
1151
+ info_bytes = normalized[1:]
1152
+
1153
+ info_obj = config.create_scale_object("SubnetNodeInfo")
1154
+ try:
1155
+ info_obj.decode(ScaleBytes(info_bytes))
1156
+ except RemainingScaleBytesNotEmptyException:
1157
+ # Expected for some registry mismatches; value is still populated
1158
+ pass
1159
+ except Exception as e:
1160
+ logger.error(f"Failed to decode SubnetNodeInfo in Option: {e}")
1161
+ return None
1162
+
1163
+ if hasattr(info_obj, "value") and info_obj.value:
1164
+ return info_obj.value
1165
+
1166
+ return None
1167
+
1168
+ # Unexpected discriminant
1169
+ logger.warning(f"Unknown Option<SubnetNodeInfo> discriminant byte: {option_byte}")
1170
+ return None
1171
+
1172
+
1173
+ def decode_vec_subnet_node_info(data: bytes) -> list[dict]:
1174
+ """
1175
+ Decode Vec<SubnetNodeInfo> from SCALE bytes.
1176
+
1177
+ This mirrors the more defensive logic used for Vec<SubnetInfo> and
1178
+ tolerates RemainingScaleBytesNotEmptyException by extracting already
1179
+ decoded values from the Vec object.
1180
+
1181
+ Args:
1182
+ data: SCALE-encoded bytes (list of ints or bytes object)
1183
+
1184
+ Returns:
1185
+ List of decoded SubnetNodeInfo dicts
1186
+ """
1187
+ normalized = _normalize_scale_input(data)
1188
+ if not normalized:
1189
+ return []
1190
+
1191
+ config = get_rpc_runtime_config()
1192
+
1193
+ try:
1194
+ vec_obj = config.create_scale_object("Vec<SubnetNodeInfo>")
1195
+
1196
+ exception_occurred = False
1197
+ exception_msg = None
1198
+ try:
1199
+ vec_obj.decode(ScaleBytes(normalized))
1200
+ logger.debug("Vec<SubnetNodeInfo> decoded without exception")
1201
+ except RemainingScaleBytesNotEmptyException as e:
1202
+ # Expected in some cases; values are still populated on the object
1203
+ exception_occurred = True
1204
+ exception_msg = str(e)
1205
+ logger.debug(
1206
+ f"RemainingScaleBytesNotEmptyException during Vec<SubnetNodeInfo> decode: {exception_msg}"
1207
+ )
1208
+ except Exception as e:
1209
+ logger.warning(
1210
+ f"Exception during Vec<SubnetNodeInfo> decode: {e}, trying to extract partial results"
1211
+ )
1212
+ exception_occurred = True
1213
+ exception_msg = str(e)
1214
+
1215
+ decoded_list = None
1216
+
1217
+ # Method 1: direct .value
1218
+ if hasattr(vec_obj, "value") and vec_obj.value is not None:
1219
+ decoded_list = vec_obj.value
1220
+
1221
+ # Method 2: value_list
1222
+ if (
1223
+ (
1224
+ decoded_list is None
1225
+ or (isinstance(decoded_list, list) and len(decoded_list) == 0)
1226
+ )
1227
+ and hasattr(vec_obj, "value_list")
1228
+ and vec_obj.value_list
1229
+ ):
1230
+ decoded_list = vec_obj.value_list
1231
+
1232
+ # Method 3: internal elements attributes
1233
+ if decoded_list is None or (
1234
+ isinstance(decoded_list, list) and len(decoded_list) == 0
1235
+ ):
1236
+ elements = None
1237
+ if hasattr(vec_obj, "_elements"):
1238
+ elements = vec_obj._elements
1239
+ elif hasattr(vec_obj, "elements"):
1240
+ elements = vec_obj.elements
1241
+
1242
+ if elements:
1243
+ try:
1244
+ decoded_list = [el.value for el in elements]
1245
+ except Exception:
1246
+ decoded_list = elements
1247
+
1248
+ if decoded_list is None:
1249
+ if exception_occurred:
1250
+ logger.warning(
1251
+ f"Could not extract SubnetNodeInfo list after Vec decode error: {exception_msg}"
1252
+ )
1253
+ return []
1254
+
1255
+ # Ensure we always return a plain list of dicts
1256
+ result: list[dict] = []
1257
+ for item in decoded_list:
1258
+ if isinstance(item, dict):
1259
+ result.append(item)
1260
+ elif hasattr(item, "value") and isinstance(item.value, dict):
1261
+ result.append(item.value)
1262
+ else:
1263
+ # Fallback: try model_dump if it's a Pydantic-like object
1264
+ try:
1265
+ dumped = item.model_dump()
1266
+ if isinstance(dumped, dict):
1267
+ result.append(dumped)
1268
+ except Exception:
1269
+ logger.debug(
1270
+ f"Skipping non-dict Vec<SubnetNodeInfo> element: {item}"
1271
+ )
1272
+
1273
+ # If we got a partial decode (exception occurred or we have large data but few results),
1274
+ # try manual iteration to decode all elements
1275
+ should_try_manual = False
1276
+ vec_length = len(result) # Default to what we have
1277
+
1278
+ # Check if we should try manual decode:
1279
+ # 1. Exception occurred during decode, OR
1280
+ # 2. We have significant data (>100 bytes) but only decoded 1-2 items (suggests partial decode)
1281
+ if exception_occurred or (len(normalized) > 100 and len(result) <= 2):
1282
+ should_try_manual = True
1283
+
1284
+ # Try to read actual Vec length from compact encoding
1285
+ try:
1286
+ vec_length_byte = normalized[0]
1287
+ # Compact encoding: first 2 bits determine format
1288
+ if (vec_length_byte & 0x03) == 0x00:
1289
+ vec_length = vec_length_byte >> 2
1290
+ elif (vec_length_byte & 0x03) == 0x01:
1291
+ if len(normalized) >= 2:
1292
+ vec_length = (
1293
+ (vec_length_byte >> 2) | (normalized[1] << 6)
1294
+ ) & 0x3FFF
1295
+ elif (vec_length_byte & 0x03) == 0x02:
1296
+ if len(normalized) >= 4:
1297
+ vec_length = (
1298
+ ((vec_length_byte >> 2) & 0x3F)
1299
+ | (normalized[1] << 6)
1300
+ | (normalized[2] << 14)
1301
+ | (normalized[3] << 22)
1302
+ ) & 0x3FFFFFFF
1303
+ except Exception as e:
1304
+ logger.debug(f"Could not read Vec length: {e}")
1305
+ # Estimate vec_length from data size (rough estimate: ~200-500 bytes per node)
1306
+ estimated_length = max(len(result), len(normalized) // 300)
1307
+ vec_length = min(estimated_length, 100) # Cap at reasonable limit
1308
+
1309
+ if should_try_manual and vec_length > len(result):
1310
+ manual_result = _manual_decode_vec_subnet_node_info(
1311
+ normalized, config, vec_length
1312
+ )
1313
+ if manual_result and len(manual_result) >= len(result):
1314
+ return manual_result
1315
+
1316
+ return result
1317
+
1318
+ except Exception as e:
1319
+ logger.error(f"Failed to decode Vec<SubnetNodeInfo>: {e}")
1320
+ return []
1321
+
1322
+
1323
+ def decode_vec_subnet_node_stake_info(data: bytes) -> list[dict]:
1324
+ """
1325
+ Decode Vec<SubnetNodeStakeInfo> from SCALE bytes.
1326
+
1327
+ Args:
1328
+ data: SCALE-encoded bytes
1329
+
1330
+ Returns:
1331
+ List of decoded SubnetNodeStakeInfo dicts
1332
+ """
1333
+ decoded = _decode_scale_object(data, "Vec<SubnetNodeStakeInfo>")
1334
+ return decoded or []
1335
+
1336
+
1337
+ def decode_vec_node_stake_info(data: bytes) -> list[dict]:
1338
+ """
1339
+ Decode Vec<NodeStakeInfo> from SCALE bytes.
1340
+
1341
+ Validator stake RPCs return NodeStakeInfo, which omits the hotkey field
1342
+ present in legacy SubnetNodeStakeInfo responses.
1343
+ """
1344
+ decoded = _decode_scale_object(data, "Vec<NodeStakeInfo>")
1345
+ return decoded or []
1346
+
1347
+
1348
+ def decode_vec_delegate_stake_info(data: bytes) -> list[dict]:
1349
+ """
1350
+ Decode Vec<DelegateStakeInfo> from SCALE bytes.
1351
+
1352
+ Args:
1353
+ data: SCALE-encoded bytes
1354
+
1355
+ Returns:
1356
+ List of decoded DelegateStakeInfo dicts
1357
+ """
1358
+ decoded = _decode_scale_object(data, "Vec<DelegateStakeInfo>")
1359
+ return decoded or []
1360
+
1361
+
1362
+ def decode_vec_node_delegate_stake_info(data: bytes) -> list[dict]:
1363
+ """
1364
+ Decode Vec<NodeDelegateStakeInfo> from SCALE bytes.
1365
+
1366
+ Args:
1367
+ data: SCALE-encoded bytes
1368
+
1369
+ Returns:
1370
+ List of decoded NodeDelegateStakeInfo dicts
1371
+ """
1372
+ decoded = _decode_scale_object(data, "Vec<NodeDelegateStakeInfo>")
1373
+ return decoded or []