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,912 @@
1
+ """
2
+ Wallet RPC operations for read-only queries.
3
+ Handles balance checks, stake queries, and account information.
4
+ """
5
+
6
+ from typing import Optional
7
+
8
+ from substrateinterface import SubstrateInterface
9
+
10
+ from ...models.responses import (
11
+ BalanceResponse,
12
+ ColdkeyStakesResponse,
13
+ ColdkeySubnetNodesResponse,
14
+ DelegateStakeInfo,
15
+ DelegateStakesResponse,
16
+ NodeDelegateStakeInfo,
17
+ NodeDelegateStakesResponse,
18
+ NodeStakeInfo,
19
+ SubnetNodeInfo,
20
+ SubnetNodeStakeInfo,
21
+ ValidatorStakesResponse,
22
+ ValidatorSubnetNodesResponse,
23
+ )
24
+ from ...utils.blockchain.type_registry import (
25
+ decode_vec_delegate_stake_info,
26
+ decode_vec_node_delegate_stake_info,
27
+ decode_vec_node_stake_info,
28
+ decode_vec_subnet_node_info,
29
+ decode_vec_subnet_node_stake_info,
30
+ )
31
+ from ...utils.logging import get_logger
32
+ from ...utils.scale_codec import decode_hex_string, format_balance
33
+
34
+ logger = get_logger(__name__)
35
+
36
+
37
+ def _rpc_result_to_bytes(raw_data):
38
+ if isinstance(raw_data, str) and raw_data.startswith("0x"):
39
+ return bytes.fromhex(raw_data[2:])
40
+ if isinstance(raw_data, (bytes, bytearray)):
41
+ return bytes(raw_data)
42
+ if isinstance(raw_data, (list, tuple)) and all(
43
+ isinstance(item, int) for item in raw_data
44
+ ):
45
+ return bytes(raw_data)
46
+ return raw_data
47
+
48
+
49
+ class WalletRpcClient:
50
+ """RPC client for wallet read-only operations."""
51
+
52
+ def __init__(self, substrate: SubstrateInterface):
53
+ self.substrate = substrate
54
+
55
+ def get_account_subnet_stake(self, account: str, subnet_id: int):
56
+ """Get account stake for a subnet using storage query."""
57
+ try:
58
+ if not self.substrate:
59
+ raise Exception("Not connected to blockchain")
60
+
61
+ # For now, use the account directly - proper SS58 handling can be added later
62
+ account_bytes = account
63
+
64
+ stake_data = self.substrate.query(
65
+ module="Network",
66
+ storage_function="AccountStake",
67
+ params=[account_bytes, subnet_id],
68
+ )
69
+
70
+ result = {
71
+ "success": True,
72
+ "message": "Account subnet stake retrieved successfully",
73
+ "data": {
74
+ "account": account,
75
+ "subnet_id": subnet_id,
76
+ "stake": stake_data.value if stake_data else 0,
77
+ },
78
+ }
79
+
80
+ return result
81
+ except Exception as e:
82
+ logger.error(f"Failed to get account subnet stake: {str(e)}")
83
+ raise
84
+
85
+ def get_balance(self, address: str) -> BalanceResponse:
86
+ """Get account balance using System.Account storage query."""
87
+ try:
88
+ if not self.substrate:
89
+ raise Exception("Not connected to blockchain")
90
+
91
+ # Query account info directly
92
+ # Note: SubstrateInterface should have metadata loaded via init_runtime() in connect()
93
+ account_info = self.substrate.query("System", "Account", [address])
94
+
95
+ balance = (
96
+ account_info.value["data"]["free"]
97
+ if account_info and account_info.value
98
+ else 0
99
+ )
100
+
101
+ return BalanceResponse(
102
+ success=True,
103
+ transaction_hash=None,
104
+ error=None,
105
+ block_number=None,
106
+ epoch=None,
107
+ address=address,
108
+ balance=balance,
109
+ locked_balance=None,
110
+ available_balance=balance,
111
+ reserved_balance=None,
112
+ )
113
+ except Exception as e:
114
+ logger.error(f"Failed to get balance for {address}: {str(e)}")
115
+ raise
116
+
117
+ def get_delegate_stake_info(self, account: str, subnet_id: Optional[int] = None):
118
+ """Get delegate stake information for an account."""
119
+ try:
120
+ if not self.substrate:
121
+ raise Exception("Not connected to blockchain")
122
+
123
+ # For now, use the account directly - proper SS58 handling can be added later
124
+ account_bytes = account
125
+
126
+ if subnet_id is not None:
127
+ # Get stake for specific subnet
128
+ delegate_stake = self.substrate.query(
129
+ module="Network",
130
+ storage_function="DelegateStake",
131
+ params=[account_bytes, subnet_id],
132
+ )
133
+
134
+ return {
135
+ "success": True,
136
+ "message": "Delegate stake info retrieved successfully",
137
+ "data": {
138
+ "account": account,
139
+ "subnet_id": subnet_id,
140
+ "delegate_stake": delegate_stake.value if delegate_stake else 0,
141
+ },
142
+ }
143
+ else:
144
+ # Get all delegate stakes for account
145
+ # This would require iterating through all subnets or using a different storage function
146
+ return {
147
+ "success": True,
148
+ "message": "All delegate stakes query not implemented yet",
149
+ "data": {
150
+ "account": account,
151
+ "message": "Use subnet_id parameter for specific subnet stake",
152
+ },
153
+ }
154
+ except Exception as e:
155
+ logger.error(f"Failed to get delegate stake info: {str(e)}")
156
+ raise
157
+
158
+ def get_unbonding_info(self, account: str):
159
+ """Get unbonding information for an account."""
160
+ try:
161
+ if not self.substrate:
162
+ raise Exception("Not connected to blockchain")
163
+
164
+ # For now, use the account directly - proper SS58 handling can be added later
165
+ account_bytes = account
166
+
167
+ unbonding_data = self.substrate.query(
168
+ module="Network", storage_function="Unbondings", params=[account_bytes]
169
+ )
170
+
171
+ return {
172
+ "success": True,
173
+ "message": "Unbonding info retrieved successfully",
174
+ "data": {
175
+ "account": account,
176
+ "unbondings": unbonding_data.value if unbonding_data else [],
177
+ },
178
+ }
179
+ except Exception as e:
180
+ logger.error(f"Failed to get unbonding info: {str(e)}")
181
+ raise
182
+
183
+ def get_staking_summary(self, account: str):
184
+ """Get comprehensive staking summary for an account."""
185
+ try:
186
+ # Get balance
187
+ balance_info = self.get_balance(account)
188
+
189
+ # Get unbonding info
190
+ unbonding_info = self.get_unbonding_info(account)
191
+
192
+ return {
193
+ "success": True,
194
+ "message": "Staking summary retrieved successfully",
195
+ "data": {
196
+ "account": account,
197
+ "balance": balance_info.data,
198
+ "unbondings": unbonding_info["data"]["unbondings"],
199
+ },
200
+ }
201
+ except Exception as e:
202
+ logger.error(f"Failed to get staking summary: {str(e)}")
203
+ raise
204
+
205
+ # RPC Methods Implementation
206
+ def network_get_coldkey_subnet_nodes_info(
207
+ self, coldkey: str, at: Optional[str] = None
208
+ ) -> ColdkeySubnetNodesResponse:
209
+ """RPC method: network_getColdkeySubnetNodesInfo - Returns subnet node info associated with a given coldkey account."""
210
+ try:
211
+ if not self.substrate:
212
+ raise Exception("Not connected to blockchain")
213
+
214
+ # Convert address to proper format for substrate
215
+ # Based on other RPC methods, we should use the address as-is or in hex format
216
+ if coldkey.startswith("0x"):
217
+ # EVM address format - use hex string without 0x prefix, normalize to lowercase
218
+ coldkey_param = coldkey[
219
+ 2:
220
+ ].lower() # Remove 0x prefix and normalize case
221
+ else:
222
+ # SS58 address format - try to decode to hex
223
+ from substrateinterface.utils.ss58 import ss58_decode
224
+
225
+ try:
226
+ coldkey_bytes = ss58_decode(coldkey)
227
+ coldkey_param = coldkey_bytes.hex()
228
+ except Exception:
229
+ # If SS58 decode fails, use address as-is
230
+ coldkey_param = coldkey
231
+
232
+ params = [coldkey_param]
233
+ if at:
234
+ params.append(at)
235
+
236
+ result = self.substrate.rpc_request(
237
+ "network_getColdkeySubnetNodesInfo", params
238
+ )
239
+
240
+ if result and result.get("result"):
241
+ # Get raw data and use proper SCALE decoding
242
+ raw_data = result["result"]
243
+ if isinstance(raw_data, str) and raw_data.startswith("0x"):
244
+ hex_bytes = bytes.fromhex(raw_data[2:])
245
+ elif isinstance(raw_data, (list, tuple)):
246
+ hex_bytes = bytes(raw_data)
247
+ else:
248
+ hex_bytes = raw_data
249
+
250
+ # Use proper SCALE decoding for Vec<SubnetNodeInfo>
251
+ # Use the specialized decoder from type_registry which includes legacy fallback
252
+ try:
253
+ decoded_list = decode_vec_subnet_node_info(hex_bytes)
254
+ if not decoded_list:
255
+ return ColdkeySubnetNodesResponse(
256
+ success=True,
257
+ message=f"No subnet nodes found for coldkey {coldkey}",
258
+ data=[],
259
+ )
260
+
261
+ subnet_nodes = []
262
+ for decoded_data in decoded_list:
263
+ try:
264
+ # Decode text fields
265
+ bootnode = decode_hex_string(
266
+ decoded_data.get("bootnode", "")
267
+ )
268
+ unique = decode_hex_string(decoded_data.get("unique", ""))
269
+ non_unique = decode_hex_string(
270
+ decoded_data.get("non_unique", "")
271
+ )
272
+ # Format balances
273
+ stake_balance_formatted = format_balance(
274
+ decoded_data.get("stake_balance", 0)
275
+ )
276
+ delegate_stake_formatted = format_balance(
277
+ decoded_data.get("node_delegate_stake_balance", 0)
278
+ )
279
+
280
+ node_data = {
281
+ "subnet_id": decoded_data.get("subnet_id", 0),
282
+ "subnet_node_id": decoded_data.get("subnet_node_id", 0),
283
+ "coldkey": decoded_data.get("coldkey", ""),
284
+ "hotkey": decoded_data.get("hotkey", ""),
285
+ "peer_id": decoded_data.get("peer_id", ""),
286
+ "bootnode_peer_id": decoded_data.get(
287
+ "bootnode_peer_id", ""
288
+ ),
289
+ "client_peer_id": decoded_data.get(
290
+ "client_peer_id", ""
291
+ ),
292
+ "bootnode": bootnode,
293
+ "identity": decoded_data.get("identity", {}),
294
+ "classification": decoded_data.get(
295
+ "classification", {}
296
+ ),
297
+ "delegate_reward_rate": decoded_data.get(
298
+ "delegate_reward_rate", 0
299
+ ),
300
+ "last_delegate_reward_rate_update": decoded_data.get(
301
+ "last_delegate_reward_rate_update", 0
302
+ ),
303
+ "unique": unique,
304
+ "non_unique": non_unique,
305
+ "stake_balance": decoded_data.get("stake_balance", 0),
306
+ "stake_balance_formatted": stake_balance_formatted,
307
+ "node_delegate_stake_balance": decoded_data.get(
308
+ "node_delegate_stake_balance", 0
309
+ ),
310
+ " node_delegate_stake_balance_formatted": delegate_stake_formatted,
311
+ "coldkey_reputation": decoded_data.get(
312
+ "coldkey_reputation", {}
313
+ ),
314
+ "subnet_node_reputation": decoded_data.get(
315
+ "subnet_node_reputation", 0
316
+ ),
317
+ }
318
+
319
+ subnet_nodes.append(SubnetNodeInfo(**node_data))
320
+
321
+ except Exception as e:
322
+ logger.warning(
323
+ f"Failed to decode node data for coldkey {coldkey}: {e}"
324
+ )
325
+ continue
326
+
327
+ except Exception as decode_error:
328
+ # Log at debug level to avoid cluttering output
329
+ logger.debug(
330
+ f"SCALE decoding failed for coldkey {coldkey}, falling back to manual parsing: {decode_error}"
331
+ )
332
+ # Fallback to manual parsing
333
+ nodes_data = raw_data
334
+ if isinstance(nodes_data, list):
335
+ # Check if the list contains dictionaries (proper node data)
336
+ if nodes_data and isinstance(nodes_data[0], dict):
337
+ subnet_nodes = [
338
+ SubnetNodeInfo(**node_data) for node_data in nodes_data
339
+ ]
340
+ else:
341
+ # Handle case where result is a list of non-dict items (e.g., [0])
342
+ subnet_nodes = []
343
+ else:
344
+ # Handle case where result is not a list (e.g., single integer)
345
+ subnet_nodes = []
346
+
347
+ return ColdkeySubnetNodesResponse(
348
+ success=True,
349
+ message=f"Coldkey {coldkey} subnet nodes info retrieved successfully",
350
+ data=subnet_nodes,
351
+ )
352
+ else:
353
+ return ColdkeySubnetNodesResponse(
354
+ success=False,
355
+ message=f"No subnet nodes found for coldkey {coldkey}",
356
+ data=[],
357
+ )
358
+ except Exception as e:
359
+ logger.error(
360
+ f"Failed to get coldkey subnet nodes info via RPC for {coldkey}: {str(e)}"
361
+ )
362
+ return ColdkeySubnetNodesResponse(
363
+ success=False,
364
+ message=f"Failed to get coldkey subnet nodes info: {str(e)}",
365
+ data=None,
366
+ )
367
+
368
+ def network_get_validator_subnet_nodes_info(
369
+ self, validator_id: int, at: Optional[str] = None
370
+ ) -> ValidatorSubnetNodesResponse:
371
+ """RPC method: network_getValidatorSubnetNodesInfo."""
372
+ try:
373
+ if not self.substrate:
374
+ raise Exception("Not connected to blockchain")
375
+
376
+ params = [validator_id]
377
+ if at:
378
+ params.append(at)
379
+
380
+ result = self.substrate.rpc_request(
381
+ "network_getValidatorSubnetNodesInfo", params
382
+ )
383
+
384
+ if result and result.get("result"):
385
+ raw_data = result["result"]
386
+ rpc_bytes = _rpc_result_to_bytes(raw_data)
387
+
388
+ try:
389
+ decoded_list = decode_vec_subnet_node_info(rpc_bytes)
390
+ if not decoded_list:
391
+ return ValidatorSubnetNodesResponse(
392
+ success=True,
393
+ message=(
394
+ f"No subnet nodes found for validator {validator_id}"
395
+ ),
396
+ data=[],
397
+ )
398
+
399
+ subnet_nodes = []
400
+ for decoded_data in decoded_list:
401
+ try:
402
+ bootnode = decode_hex_string(
403
+ decoded_data.get("bootnode", "")
404
+ )
405
+ unique = decode_hex_string(decoded_data.get("unique", ""))
406
+ non_unique = decode_hex_string(
407
+ decoded_data.get("non_unique", "")
408
+ )
409
+
410
+ node_data = {
411
+ "subnet_id": decoded_data.get("subnet_id", 0),
412
+ "subnet_node_id": decoded_data.get("subnet_node_id", 0),
413
+ "coldkey": decoded_data.get("coldkey", ""),
414
+ "hotkey": decoded_data.get("hotkey", ""),
415
+ "peer_id": decoded_data.get("peer_id", ""),
416
+ "bootnode_peer_id": decoded_data.get(
417
+ "bootnode_peer_id", ""
418
+ ),
419
+ "client_peer_id": decoded_data.get(
420
+ "client_peer_id", ""
421
+ ),
422
+ "bootnode": bootnode,
423
+ "identity": decoded_data.get("identity", {}),
424
+ "classification": decoded_data.get(
425
+ "classification", {}
426
+ ),
427
+ "delegate_reward_rate": decoded_data.get(
428
+ "delegate_reward_rate", 0
429
+ ),
430
+ "last_delegate_reward_rate_update": decoded_data.get(
431
+ "last_delegate_reward_rate_update", 0
432
+ ),
433
+ "unique": unique,
434
+ "non_unique": non_unique,
435
+ "stake_balance": decoded_data.get("stake_balance", 0),
436
+ "node_delegate_stake_balance": decoded_data.get(
437
+ "node_delegate_stake_balance", 0
438
+ ),
439
+ "coldkey_reputation": decoded_data.get(
440
+ "coldkey_reputation", {}
441
+ ),
442
+ "subnet_node_reputation": decoded_data.get(
443
+ "subnet_node_reputation", 0
444
+ ),
445
+ }
446
+ subnet_nodes.append(SubnetNodeInfo(**node_data))
447
+ except Exception as e:
448
+ logger.warning(
449
+ "Failed to decode node data for validator "
450
+ f"{validator_id}: {e}"
451
+ )
452
+ continue
453
+
454
+ except Exception as decode_error:
455
+ logger.debug(
456
+ "SCALE decoding failed for validator subnet nodes "
457
+ f"{validator_id}, trying fallback: {decode_error}"
458
+ )
459
+ if (
460
+ isinstance(raw_data, list)
461
+ and raw_data
462
+ and isinstance(raw_data[0], dict)
463
+ ):
464
+ subnet_nodes = [
465
+ SubnetNodeInfo(**node_data) for node_data in raw_data
466
+ ]
467
+ else:
468
+ subnet_nodes = []
469
+
470
+ return ValidatorSubnetNodesResponse(
471
+ success=True,
472
+ message=(
473
+ f"Validator {validator_id} subnet nodes info retrieved "
474
+ "successfully"
475
+ ),
476
+ data=subnet_nodes,
477
+ )
478
+
479
+ return ValidatorSubnetNodesResponse(
480
+ success=False,
481
+ message=f"No subnet nodes found for validator {validator_id}",
482
+ data=[],
483
+ )
484
+ except Exception as e:
485
+ logger.error(
486
+ "Failed to get validator subnet nodes info via RPC for "
487
+ f"{validator_id}: {str(e)}"
488
+ )
489
+ return ValidatorSubnetNodesResponse(
490
+ success=False,
491
+ message=f"Failed to get validator subnet nodes info: {str(e)}",
492
+ data=None,
493
+ )
494
+
495
+ def network_get_coldkey_stakes(
496
+ self, coldkey: str, at: Optional[str] = None
497
+ ) -> ColdkeyStakesResponse:
498
+ """RPC method: network_getColdkeyStakes - Fetches stake information for a given coldkey."""
499
+ try:
500
+ if not self.substrate:
501
+ raise Exception("Not connected to blockchain")
502
+
503
+ # Convert address to proper format for substrate (manual conversion works reliably)
504
+ if coldkey.startswith("0x"):
505
+ # EVM address format - convert to hex string
506
+ coldkey_hex = coldkey[2:] # Remove 0x prefix
507
+ else:
508
+ # SS58 address format - decode to hex string
509
+ from substrateinterface.utils.ss58 import ss58_decode
510
+
511
+ try:
512
+ coldkey_bytes = ss58_decode(coldkey)
513
+ coldkey_hex = coldkey_bytes.hex()
514
+ except Exception:
515
+ # If SS58 decode fails, try to use address as-is
516
+ coldkey_hex = coldkey
517
+
518
+ params = [coldkey_hex]
519
+ if at:
520
+ params.append(at)
521
+
522
+ result = self.substrate.rpc_request("network_getColdkeyStakes", params)
523
+
524
+ if result and result.get("result"):
525
+ # Get raw data and decode SCALE-encoded bytes
526
+ raw_data = result["result"]
527
+ if isinstance(raw_data, str) and raw_data.startswith("0x"):
528
+ hex_bytes = bytes.fromhex(raw_data[2:])
529
+ elif isinstance(raw_data, (list, tuple)):
530
+ hex_bytes = bytes(raw_data)
531
+ else:
532
+ hex_bytes = raw_data
533
+
534
+ # Use proper SCALE decoding for Vec<SubnetNodeStakeInfo>
535
+ try:
536
+ decoded_list = decode_vec_subnet_node_stake_info(hex_bytes)
537
+ if not decoded_list:
538
+ return ColdkeyStakesResponse(
539
+ success=True,
540
+ message=f"No stakes found for coldkey {coldkey}",
541
+ data=[],
542
+ )
543
+
544
+ stakes = []
545
+ for decoded_data in decoded_list:
546
+ try:
547
+ stake_info = SubnetNodeStakeInfo(
548
+ subnet_id=decoded_data.get("subnet_id"),
549
+ subnet_node_id=decoded_data.get("subnet_node_id"),
550
+ hotkey=decoded_data.get("hotkey", ""),
551
+ balance=decoded_data.get("balance", 0),
552
+ )
553
+ stakes.append(stake_info)
554
+ except Exception as e:
555
+ logger.warning(
556
+ f"Failed to decode stake data for coldkey {coldkey}: {e}"
557
+ )
558
+ continue
559
+
560
+ return ColdkeyStakesResponse(
561
+ success=True,
562
+ message=f"Coldkey {coldkey} stakes retrieved successfully",
563
+ data=stakes,
564
+ )
565
+ except Exception as decode_error:
566
+ logger.debug(
567
+ f"SCALE decoding failed for coldkey stakes {coldkey}, trying fallback: {decode_error}"
568
+ )
569
+ # Fallback: try to parse as already-decoded data
570
+ if (
571
+ isinstance(raw_data, list)
572
+ and raw_data
573
+ and isinstance(raw_data[0], dict)
574
+ ):
575
+ stakes = [
576
+ SubnetNodeStakeInfo(**stake_data) for stake_data in raw_data
577
+ ]
578
+ return ColdkeyStakesResponse(
579
+ success=True,
580
+ message=f"Coldkey {coldkey} stakes retrieved successfully",
581
+ data=stakes,
582
+ )
583
+ return ColdkeyStakesResponse(
584
+ success=True,
585
+ message=f"No stakes found for coldkey {coldkey}",
586
+ data=[],
587
+ )
588
+ else:
589
+ return ColdkeyStakesResponse(
590
+ success=False,
591
+ message=f"No stakes found for coldkey {coldkey}",
592
+ data=[],
593
+ )
594
+ except Exception as e:
595
+ logger.error(
596
+ f"Failed to get coldkey stakes via RPC for {coldkey}: {str(e)}"
597
+ )
598
+ return ColdkeyStakesResponse(
599
+ success=False,
600
+ message=f"Failed to get coldkey stakes: {str(e)}",
601
+ data=None,
602
+ )
603
+
604
+ def network_get_validator_stakes(
605
+ self, validator_id: int, at: Optional[str] = None
606
+ ) -> ValidatorStakesResponse:
607
+ """RPC method: network_getValidatorStakes - Fetches stake information for a validator."""
608
+ try:
609
+ if not self.substrate:
610
+ raise Exception("Not connected to blockchain")
611
+
612
+ params = [validator_id]
613
+ if at:
614
+ params.append(at)
615
+
616
+ result = self.substrate.rpc_request("network_getValidatorStakes", params)
617
+
618
+ if result and result.get("result"):
619
+ raw_data = result["result"]
620
+ rpc_bytes = _rpc_result_to_bytes(raw_data)
621
+
622
+ try:
623
+ decoded_list = decode_vec_node_stake_info(rpc_bytes)
624
+ if not decoded_list:
625
+ return ValidatorStakesResponse(
626
+ success=True,
627
+ message=f"No stakes found for validator {validator_id}",
628
+ data=[],
629
+ )
630
+
631
+ stakes = []
632
+ for decoded_data in decoded_list:
633
+ try:
634
+ stake_info = NodeStakeInfo(
635
+ subnet_id=decoded_data.get("subnet_id"),
636
+ subnet_node_id=decoded_data.get("subnet_node_id"),
637
+ balance=decoded_data.get("balance", 0),
638
+ )
639
+ stakes.append(stake_info)
640
+ except Exception as e:
641
+ logger.warning(
642
+ "Failed to decode stake data for validator "
643
+ f"{validator_id}: {e}"
644
+ )
645
+ continue
646
+
647
+ return ValidatorStakesResponse(
648
+ success=True,
649
+ message=(
650
+ f"Validator {validator_id} stakes retrieved successfully"
651
+ ),
652
+ data=stakes,
653
+ )
654
+ except Exception as decode_error:
655
+ logger.debug(
656
+ "SCALE decoding failed for validator stakes "
657
+ f"{validator_id}, trying fallback: {decode_error}"
658
+ )
659
+ if (
660
+ isinstance(raw_data, list)
661
+ and raw_data
662
+ and isinstance(raw_data[0], dict)
663
+ ):
664
+ stakes = [
665
+ NodeStakeInfo(**stake_data) for stake_data in raw_data
666
+ ]
667
+ return ValidatorStakesResponse(
668
+ success=True,
669
+ message=(
670
+ f"Validator {validator_id} stakes retrieved "
671
+ "successfully"
672
+ ),
673
+ data=stakes,
674
+ )
675
+ return ValidatorStakesResponse(
676
+ success=True,
677
+ message=f"No stakes found for validator {validator_id}",
678
+ data=[],
679
+ )
680
+
681
+ return ValidatorStakesResponse(
682
+ success=False,
683
+ message=f"No stakes found for validator {validator_id}",
684
+ data=[],
685
+ )
686
+ except Exception as e:
687
+ logger.error(
688
+ f"Failed to get validator stakes via RPC for {validator_id}: {str(e)}"
689
+ )
690
+ return ValidatorStakesResponse(
691
+ success=False,
692
+ message=f"Failed to get validator stakes: {str(e)}",
693
+ data=None,
694
+ )
695
+
696
+ def network_get_delegate_stakes(
697
+ self, account_id: str, at: Optional[str] = None
698
+ ) -> DelegateStakesResponse:
699
+ """RPC method: network_getDelegateStakes - Returns delegate stakes associated with an account."""
700
+ try:
701
+ if not self.substrate:
702
+ raise Exception("Not connected to blockchain")
703
+
704
+ # Convert address to proper format for substrate (manual conversion works reliably)
705
+ if account_id.startswith("0x"):
706
+ # EVM address format - convert to hex string
707
+ account_hex = account_id[2:] # Remove 0x prefix
708
+ else:
709
+ # SS58 address format - decode to hex string
710
+ from substrateinterface.utils.ss58 import ss58_decode
711
+
712
+ try:
713
+ account_bytes = ss58_decode(account_id)
714
+ account_hex = account_bytes.hex()
715
+ except Exception:
716
+ # If SS58 decode fails, try to use address as-is
717
+ account_hex = account_id
718
+
719
+ params = [account_hex]
720
+ if at:
721
+ params.append(at)
722
+
723
+ result = self.substrate.rpc_request("network_getDelegateStakes", params)
724
+
725
+ if result and result.get("result"):
726
+ # Get raw data and decode SCALE-encoded bytes
727
+ raw_data = result["result"]
728
+ if isinstance(raw_data, str) and raw_data.startswith("0x"):
729
+ hex_bytes = bytes.fromhex(raw_data[2:])
730
+ elif isinstance(raw_data, (list, tuple)):
731
+ hex_bytes = bytes(raw_data)
732
+ else:
733
+ hex_bytes = raw_data
734
+
735
+ # Use proper SCALE decoding for Vec<DelegateStakeInfo>
736
+ try:
737
+ decoded_list = decode_vec_delegate_stake_info(hex_bytes)
738
+ if not decoded_list:
739
+ return DelegateStakesResponse(
740
+ success=True,
741
+ message=f"No delegate stakes found for account {account_id}",
742
+ data=[],
743
+ )
744
+
745
+ stakes = []
746
+ for decoded_data in decoded_list:
747
+ try:
748
+ stake_info = DelegateStakeInfo(
749
+ subnet_id=decoded_data.get("subnet_id", 0),
750
+ shares=decoded_data.get("shares", 0),
751
+ balance=decoded_data.get("balance", 0),
752
+ )
753
+ stakes.append(stake_info)
754
+ except Exception as e:
755
+ logger.warning(
756
+ f"Failed to decode delegate stake data for account {account_id}: {e}"
757
+ )
758
+ continue
759
+
760
+ return DelegateStakesResponse(
761
+ success=True,
762
+ message=f"Delegate stakes for account {account_id} retrieved successfully",
763
+ data=stakes,
764
+ )
765
+ except Exception as decode_error:
766
+ logger.debug(
767
+ f"SCALE decoding failed for delegate stakes {account_id}, trying fallback: {decode_error}"
768
+ )
769
+ # Fallback: try to parse as already-decoded data
770
+ if (
771
+ isinstance(raw_data, list)
772
+ and raw_data
773
+ and isinstance(raw_data[0], dict)
774
+ ):
775
+ stakes = [
776
+ DelegateStakeInfo(**stake_data) for stake_data in raw_data
777
+ ]
778
+ return DelegateStakesResponse(
779
+ success=True,
780
+ message=f"Delegate stakes for account {account_id} retrieved successfully",
781
+ data=stakes,
782
+ )
783
+ return DelegateStakesResponse(
784
+ success=True,
785
+ message=f"No delegate stakes found for account {account_id}",
786
+ data=[],
787
+ )
788
+ else:
789
+ return DelegateStakesResponse(
790
+ success=False,
791
+ message=f"No delegate stakes found for account {account_id}",
792
+ data=[],
793
+ )
794
+ except Exception as e:
795
+ logger.error(
796
+ f"Failed to get delegate stakes via RPC for {account_id}: {str(e)}"
797
+ )
798
+ return DelegateStakesResponse(
799
+ success=False,
800
+ message=f"Failed to get delegate stakes: {str(e)}",
801
+ data=None,
802
+ )
803
+
804
+ def network_get_node_delegate_stakes(
805
+ self, account_id: str, at: Optional[str] = None
806
+ ) -> NodeDelegateStakesResponse:
807
+ """RPC method: network_getNodeDelegateStakes - Fetches delegate stake info specifically for nodes linked to an account."""
808
+ try:
809
+ if not self.substrate:
810
+ raise Exception("Not connected to blockchain")
811
+
812
+ # Convert address to proper format for substrate (manual conversion works reliably)
813
+ if account_id.startswith("0x"):
814
+ # EVM address format - convert to hex string
815
+ account_hex = account_id[2:] # Remove 0x prefix
816
+ else:
817
+ # SS58 address format - decode to hex string
818
+ from substrateinterface.utils.ss58 import ss58_decode
819
+
820
+ try:
821
+ account_bytes = ss58_decode(account_id)
822
+ account_hex = account_bytes.hex()
823
+ except Exception:
824
+ # If SS58 decode fails, try to use address as-is
825
+ account_hex = account_id
826
+
827
+ params = [account_hex]
828
+ if at:
829
+ params.append(at)
830
+
831
+ result = self.substrate.rpc_request("network_getNodeDelegateStakes", params)
832
+
833
+ if result and result.get("result"):
834
+ # Get raw data and decode SCALE-encoded bytes
835
+ raw_data = result["result"]
836
+ if isinstance(raw_data, str) and raw_data.startswith("0x"):
837
+ hex_bytes = bytes.fromhex(raw_data[2:])
838
+ elif isinstance(raw_data, (list, tuple)):
839
+ hex_bytes = bytes(raw_data)
840
+ else:
841
+ hex_bytes = raw_data
842
+
843
+ # Use proper SCALE decoding for Vec<NodeDelegateStakeInfo>
844
+ try:
845
+ decoded_list = decode_vec_node_delegate_stake_info(hex_bytes)
846
+ if not decoded_list:
847
+ return NodeDelegateStakesResponse(
848
+ success=True,
849
+ message=f"No node delegate stakes found for account {account_id}",
850
+ data=[],
851
+ )
852
+
853
+ stakes = []
854
+ for decoded_data in decoded_list:
855
+ try:
856
+ stake_info = NodeDelegateStakeInfo(
857
+ subnet_id=decoded_data.get("subnet_id", 0),
858
+ subnet_node_id=decoded_data.get("subnet_node_id", 0),
859
+ shares=decoded_data.get("shares", 0),
860
+ balance=decoded_data.get("balance", 0),
861
+ )
862
+ stakes.append(stake_info)
863
+ except Exception as e:
864
+ logger.warning(
865
+ f"Failed to decode node delegate stake data for account {account_id}: {e}"
866
+ )
867
+ continue
868
+
869
+ return NodeDelegateStakesResponse(
870
+ success=True,
871
+ message=f"Node delegate stakes for account {account_id} retrieved successfully",
872
+ data=stakes,
873
+ )
874
+ except Exception as decode_error:
875
+ logger.debug(
876
+ f"SCALE decoding failed for node delegate stakes {account_id}, trying fallback: {decode_error}"
877
+ )
878
+ # Fallback: try to parse as already-decoded data
879
+ if (
880
+ isinstance(raw_data, list)
881
+ and raw_data
882
+ and isinstance(raw_data[0], dict)
883
+ ):
884
+ stakes = [
885
+ NodeDelegateStakeInfo(**stake_data)
886
+ for stake_data in raw_data
887
+ ]
888
+ return NodeDelegateStakesResponse(
889
+ success=True,
890
+ message=f"Node delegate stakes for account {account_id} retrieved successfully",
891
+ data=stakes,
892
+ )
893
+ return NodeDelegateStakesResponse(
894
+ success=True,
895
+ message=f"No node delegate stakes found for account {account_id}",
896
+ data=[],
897
+ )
898
+ else:
899
+ return NodeDelegateStakesResponse(
900
+ success=False,
901
+ message=f"No node delegate stakes found for account {account_id}",
902
+ data=[],
903
+ )
904
+ except Exception as e:
905
+ logger.error(
906
+ f"Failed to get node delegate stakes via RPC for {account_id}: {str(e)}"
907
+ )
908
+ return NodeDelegateStakesResponse(
909
+ success=False,
910
+ message=f"Failed to get node delegate stakes: {str(e)}",
911
+ data=None,
912
+ )