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