meshtensor-cli 9.18.1__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 (74) hide show
  1. meshtensor_cli/__init__.py +22 -0
  2. meshtensor_cli/cli.py +10742 -0
  3. meshtensor_cli/doc_generation_helper.py +4 -0
  4. meshtensor_cli/src/__init__.py +1085 -0
  5. meshtensor_cli/src/commands/__init__.py +0 -0
  6. meshtensor_cli/src/commands/axon/__init__.py +0 -0
  7. meshtensor_cli/src/commands/axon/axon.py +132 -0
  8. meshtensor_cli/src/commands/crowd/__init__.py +0 -0
  9. meshtensor_cli/src/commands/crowd/contribute.py +621 -0
  10. meshtensor_cli/src/commands/crowd/contributors.py +200 -0
  11. meshtensor_cli/src/commands/crowd/create.py +783 -0
  12. meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
  13. meshtensor_cli/src/commands/crowd/refund.py +233 -0
  14. meshtensor_cli/src/commands/crowd/update.py +418 -0
  15. meshtensor_cli/src/commands/crowd/utils.py +124 -0
  16. meshtensor_cli/src/commands/crowd/view.py +991 -0
  17. meshtensor_cli/src/commands/governance/__init__.py +0 -0
  18. meshtensor_cli/src/commands/governance/governance.py +794 -0
  19. meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
  20. meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
  21. meshtensor_cli/src/commands/liquidity/utils.py +202 -0
  22. meshtensor_cli/src/commands/proxy.py +700 -0
  23. meshtensor_cli/src/commands/stake/__init__.py +0 -0
  24. meshtensor_cli/src/commands/stake/add.py +799 -0
  25. meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
  26. meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
  27. meshtensor_cli/src/commands/stake/claim.py +770 -0
  28. meshtensor_cli/src/commands/stake/list.py +738 -0
  29. meshtensor_cli/src/commands/stake/move.py +1211 -0
  30. meshtensor_cli/src/commands/stake/remove.py +1466 -0
  31. meshtensor_cli/src/commands/stake/wizard.py +323 -0
  32. meshtensor_cli/src/commands/subnets/__init__.py +0 -0
  33. meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
  34. meshtensor_cli/src/commands/subnets/price.py +733 -0
  35. meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
  36. meshtensor_cli/src/commands/sudo.py +1294 -0
  37. meshtensor_cli/src/commands/tc/__init__.py +0 -0
  38. meshtensor_cli/src/commands/tc/tc.py +190 -0
  39. meshtensor_cli/src/commands/treasury/__init__.py +0 -0
  40. meshtensor_cli/src/commands/treasury/treasury.py +194 -0
  41. meshtensor_cli/src/commands/view.py +354 -0
  42. meshtensor_cli/src/commands/wallets.py +2311 -0
  43. meshtensor_cli/src/commands/weights.py +467 -0
  44. meshtensor_cli/src/meshtensor/__init__.py +0 -0
  45. meshtensor_cli/src/meshtensor/balances.py +313 -0
  46. meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
  47. meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
  48. meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
  49. meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
  50. meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
  51. meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
  52. meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
  53. meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
  54. meshtensor_cli/src/meshtensor/minigraph.py +254 -0
  55. meshtensor_cli/src/meshtensor/networking.py +12 -0
  56. meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
  57. meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
  58. meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
  59. meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
  60. meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
  61. meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
  62. meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
  63. meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
  64. meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
  65. meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
  66. meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
  67. meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
  68. meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
  69. meshtensor_cli/src/meshtensor/utils.py +2007 -0
  70. meshtensor_cli/version.py +23 -0
  71. meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
  72. meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
  73. meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
  74. meshtensor_cli-9.18.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,2598 @@
1
+ import asyncio
2
+ import os
3
+ import time
4
+ from typing import Optional, Any, Union, TypedDict, Iterable, Literal
5
+
6
+ from async_substrate_interface import AsyncExtrinsicReceipt
7
+ from async_substrate_interface.async_substrate import (
8
+ DiskCachedAsyncSubstrateInterface,
9
+ AsyncSubstrateInterface,
10
+ )
11
+ from async_substrate_interface.errors import SubstrateRequestException
12
+ from async_substrate_interface.utils.storage import StorageKey
13
+ from meshtensor_wallet import Wallet
14
+ from meshtensor_wallet.meshtensor_wallet import Keypair
15
+ from meshtensor_wallet.utils import SS58_FORMAT
16
+ from scalecodec import GenericCall, ScaleBytes
17
+ import typer
18
+ import websockets
19
+
20
+ from meshtensor_cli.src.meshtensor.chain_data import (
21
+ DelegateInfo,
22
+ StakeInfo,
23
+ NeuronInfoLite,
24
+ NeuronInfo,
25
+ SubnetHyperparameters,
26
+ decode_account_id,
27
+ decode_hex_identity,
28
+ DynamicInfo,
29
+ SubnetState,
30
+ MetagraphInfo,
31
+ SimSwapResult,
32
+ CrowdloanData,
33
+ )
34
+ from meshtensor_cli.src import DelegatesDetails
35
+ from meshtensor_cli.src.meshtensor.balances import Balance, fixed_to_float
36
+ from meshtensor_cli.src import Constants, defaults, TYPE_REGISTRY
37
+ from meshtensor_cli.src.meshtensor.extrinsics.mev_shield import encrypt_extrinsic
38
+ from meshtensor_cli.src.meshtensor.utils import (
39
+ format_error_message,
40
+ console,
41
+ print_error,
42
+ decode_hex_identity_dict,
43
+ validate_chain_endpoint,
44
+ u16_normalized_float,
45
+ MEV_SHIELD_PUBLIC_KEY_SIZE,
46
+ get_hotkey_pub_ss58,
47
+ ProxyAnnouncements,
48
+ )
49
+
50
+ GENESIS_ADDRESS = "5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM"
51
+
52
+
53
+ class ParamWithTypes(TypedDict):
54
+ name: str # Name of the parameter.
55
+ type: str # ScaleType string of the parameter.
56
+
57
+
58
+ class ProposalVoteData:
59
+ index: int
60
+ threshold: int
61
+ ayes: list[str]
62
+ nays: list[str]
63
+ end: int
64
+
65
+ def __init__(self, proposal_dict: dict) -> None:
66
+ self.index = proposal_dict["index"]
67
+ self.threshold = proposal_dict["threshold"]
68
+ self.ayes = self.decode_ss58_tuples(proposal_dict["ayes"])
69
+ self.nays = self.decode_ss58_tuples(proposal_dict["nays"])
70
+ self.end = proposal_dict["end"]
71
+
72
+ @staticmethod
73
+ def decode_ss58_tuples(data: tuple):
74
+ """
75
+ Decodes a tuple of ss58 addresses formatted as bytes tuples
76
+ """
77
+ return [decode_account_id(data[x][0]) for x in range(len(data))]
78
+
79
+
80
+ class MeshtensorInterface:
81
+ """
82
+ Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls.
83
+ """
84
+
85
+ def __init__(self, network, use_disk_cache: bool = True):
86
+ if network in Constants.network_map:
87
+ self.chain_endpoint = Constants.network_map[network]
88
+ self.network = network
89
+ if network == "local":
90
+ console.log(
91
+ "[yellow]Warning[/yellow]: Verify your local meshtensor is running on port 9944."
92
+ )
93
+ else:
94
+ is_valid, _ = validate_chain_endpoint(network)
95
+ if is_valid:
96
+ self.chain_endpoint = network
97
+ if network in Constants.network_map.values():
98
+ self.network = next(
99
+ key
100
+ for key, value in Constants.network_map.items()
101
+ if value == network
102
+ )
103
+ else:
104
+ self.network = "custom"
105
+ else:
106
+ console.log(
107
+ f"Network not specified or not valid. Using default chain endpoint: "
108
+ f"{Constants.network_map[defaults.meshtensor.network]}.\n"
109
+ f"You can set this for commands with the `--network` flag, or by setting this"
110
+ f" in the config. If you're sure you're using the correct URL, ensure it begins"
111
+ f" with 'ws://' or 'wss://'"
112
+ )
113
+ self.chain_endpoint = Constants.network_map[defaults.meshtensor.network]
114
+ self.network = defaults.meshtensor.network
115
+ substrate_class = (
116
+ DiskCachedAsyncSubstrateInterface
117
+ if (use_disk_cache or os.getenv("DISK_CACHE", "1") == "1")
118
+ else AsyncSubstrateInterface
119
+ )
120
+ self.substrate = substrate_class(
121
+ url=self.chain_endpoint,
122
+ ss58_format=SS58_FORMAT,
123
+ type_registry=TYPE_REGISTRY,
124
+ chain_name="Meshtensor",
125
+ ws_shutdown_timer=None,
126
+ )
127
+
128
+ def __str__(self):
129
+ return f"Network: {self.network}, Chain: {self.chain_endpoint}"
130
+
131
+ async def __aenter__(self):
132
+ with console.status(
133
+ f"[yellow]Connecting to Substrate:[/yellow][bold white] {self}..."
134
+ ):
135
+ try:
136
+ await self.substrate.initialize()
137
+ return self
138
+ except TimeoutError: # TODO verify
139
+ print_error(
140
+ "\nError: Timeout occurred connecting to substrate. "
141
+ f"Verify your chain and network settings: {self}"
142
+ )
143
+ raise typer.Exit(code=1)
144
+
145
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
146
+ await self.substrate.close()
147
+
148
+ async def query(
149
+ self,
150
+ module: str,
151
+ storage_function: str,
152
+ params: Optional[list] = None,
153
+ block_hash: Optional[str] = None,
154
+ raw_storage_key: Optional[bytes] = None,
155
+ subscription_handler=None,
156
+ reuse_block_hash: bool = False,
157
+ ) -> Any:
158
+ """
159
+ Pass-through to substrate.query which automatically returns the .value if it's a ScaleObj
160
+ """
161
+ result = await self.substrate.query(
162
+ module,
163
+ storage_function,
164
+ params,
165
+ block_hash,
166
+ raw_storage_key,
167
+ subscription_handler,
168
+ reuse_block_hash,
169
+ )
170
+ if hasattr(result, "value"):
171
+ return result.value
172
+ else:
173
+ return result
174
+
175
+ async def _decode_inline_call(
176
+ self,
177
+ call_option: Any,
178
+ block_hash: Optional[str] = None,
179
+ ) -> Optional[dict[str, Any]]:
180
+ """
181
+ Decode an `Option<BoundedCall>` returned from storage into a structured dictionary.
182
+ """
183
+ if not call_option or "Inline" not in call_option:
184
+ return None
185
+ inline_bytes = bytes(call_option["Inline"][0][0])
186
+ call_obj = await self.substrate.create_scale_object(
187
+ "Call",
188
+ data=ScaleBytes(inline_bytes),
189
+ block_hash=block_hash,
190
+ )
191
+ call_value = call_obj.decode()
192
+
193
+ if not isinstance(call_value, dict):
194
+ return None
195
+
196
+ call_args = call_value.get("call_args") or []
197
+ args_map: dict[str, dict[str, Any]] = {}
198
+ for arg in call_args:
199
+ if isinstance(arg, dict) and arg.get("name"):
200
+ args_map[arg["name"]] = {
201
+ "type": arg.get("type"),
202
+ "value": arg.get("value"),
203
+ }
204
+
205
+ return {
206
+ "call_index": call_value.get("call_index"),
207
+ "pallet": call_value.get("call_module"),
208
+ "method": call_value.get("call_function"),
209
+ "args": args_map,
210
+ "hash": call_value.get("call_hash"),
211
+ }
212
+
213
+ async def get_all_subnet_netuids(
214
+ self, block_hash: Optional[str] = None
215
+ ) -> list[int]:
216
+ """
217
+ Retrieves the list of all subnet unique identifiers (netuids) currently present in the Meshtensor network.
218
+
219
+ :param block_hash: The hash of the block to retrieve the subnet unique identifiers from.
220
+
221
+ :return: A list of subnet netuids.
222
+
223
+ This function provides a comprehensive view of the subnets within the Meshtensor network,
224
+ offering insights into its diversity and scale.
225
+ """
226
+ result = await self.substrate.query_map(
227
+ module="MeshtensorModule",
228
+ storage_function="NetworksAdded",
229
+ block_hash=block_hash,
230
+ reuse_block_hash=True,
231
+ )
232
+ res = []
233
+ async for netuid, exists in result:
234
+ if exists.value:
235
+ res.append(netuid)
236
+ return res
237
+
238
+ async def get_stake_for_coldkey(
239
+ self,
240
+ coldkey_ss58: str,
241
+ block_hash: Optional[str] = None,
242
+ reuse_block: bool = False,
243
+ ) -> list[StakeInfo]:
244
+ """
245
+ Retrieves stake information associated with a specific coldkey. This function provides details
246
+ about the stakes held by an account, including the staked amounts and associated delegates.
247
+
248
+ :param coldkey_ss58: The ``SS58`` address of the account's coldkey.
249
+ :param block_hash: The hash of the blockchain block number for the query.
250
+ :param reuse_block: Whether to reuse the last-used block hash.
251
+
252
+ :return: A list of StakeInfo objects detailing the stake allocations for the account.
253
+
254
+ Stake information is vital for account holders to assess their investment and participation
255
+ in the network's delegation and consensus processes.
256
+ """
257
+
258
+ result = await self.query_runtime_api(
259
+ runtime_api="StakeInfoRuntimeApi",
260
+ method="get_stake_info_for_coldkey",
261
+ params=[coldkey_ss58],
262
+ block_hash=block_hash,
263
+ reuse_block=reuse_block,
264
+ )
265
+
266
+ if result is None:
267
+ return []
268
+ stakes: list[StakeInfo] = StakeInfo.list_from_any(result)
269
+ return [stake for stake in stakes if stake.stake > 0]
270
+
271
+ async def get_auto_stake_destinations(
272
+ self,
273
+ coldkey_ss58: str,
274
+ block_hash: Optional[str] = None,
275
+ reuse_block: bool = False,
276
+ ) -> dict[int, str]:
277
+ """Retrieve auto-stake destinations configured for a coldkey."""
278
+
279
+ query = await self.substrate.query_map(
280
+ module="MeshtensorModule",
281
+ storage_function="AutoStakeDestination",
282
+ params=[coldkey_ss58],
283
+ block_hash=block_hash,
284
+ reuse_block_hash=reuse_block,
285
+ )
286
+
287
+ destinations: dict[int, str] = {}
288
+ async for netuid, destination in query:
289
+ hotkey_ss58 = decode_account_id(destination.value[0])
290
+ if hotkey_ss58:
291
+ destinations[int(netuid)] = hotkey_ss58
292
+
293
+ return destinations
294
+
295
+ async def get_stake_for_coldkey_and_hotkey(
296
+ self,
297
+ hotkey_ss58: str,
298
+ coldkey_ss58: str,
299
+ netuid: Optional[int] = None,
300
+ block_hash: Optional[str] = None,
301
+ ) -> Balance:
302
+ """
303
+ Returns the stake under a coldkey - hotkey pairing.
304
+
305
+ :param hotkey_ss58: The SS58 address of the hotkey.
306
+ :param coldkey_ss58: The SS58 address of the coldkey.
307
+ :param netuid: The subnet ID to filter by. If provided, only returns stake for this specific
308
+ subnet.
309
+ :param block_hash: The block hash at which to query the stake information.
310
+
311
+ :return: Balance: The stake under the coldkey - hotkey pairing.
312
+ """
313
+ alpha_shares, hotkey_alpha, hotkey_shares = await asyncio.gather(
314
+ self.query(
315
+ module="MeshtensorModule",
316
+ storage_function="Alpha",
317
+ params=[hotkey_ss58, coldkey_ss58, netuid],
318
+ block_hash=block_hash,
319
+ ),
320
+ self.query(
321
+ module="MeshtensorModule",
322
+ storage_function="TotalHotkeyAlpha",
323
+ params=[hotkey_ss58, netuid],
324
+ block_hash=block_hash,
325
+ ),
326
+ self.query(
327
+ module="MeshtensorModule",
328
+ storage_function="TotalHotkeyShares",
329
+ params=[hotkey_ss58, netuid],
330
+ block_hash=block_hash,
331
+ ),
332
+ )
333
+
334
+ alpha_shares_as_float = fixed_to_float(alpha_shares or 0)
335
+ hotkey_shares_as_float = fixed_to_float(hotkey_shares or 0)
336
+
337
+ if hotkey_shares_as_float == 0:
338
+ return Balance.from_meshlet(0).set_unit(netuid=netuid)
339
+
340
+ stake = alpha_shares_as_float / hotkey_shares_as_float * (hotkey_alpha or 0)
341
+
342
+ return Balance.from_meshlet(int(stake)).set_unit(netuid=netuid)
343
+
344
+ # Alias
345
+ get_stake = get_stake_for_coldkey_and_hotkey
346
+
347
+ async def query_runtime_api(
348
+ self,
349
+ runtime_api: str,
350
+ method: str,
351
+ params: Optional[Union[list, dict]] = None,
352
+ block_hash: Optional[str] = None,
353
+ reuse_block: Optional[bool] = False,
354
+ ) -> Optional[Any]:
355
+ """
356
+ Queries the runtime API of the Meshtensor blockchain, providing a way to interact with the underlying
357
+ runtime and retrieve data encoded in Scale Bytes format. This function is essential for advanced users
358
+ who need to interact with specific runtime methods and decode complex data types.
359
+
360
+ :param runtime_api: The name of the runtime API to query.
361
+ :param method: The specific method within the runtime API to call.
362
+ :param params: The parameters to pass to the method call.
363
+ :param block_hash: The hash of the blockchain block number at which to perform the query.
364
+ :param reuse_block: Whether to reuse the last-used block hash.
365
+
366
+ :return: The decoded result from the runtime API call, or ``None`` if the call fails.
367
+
368
+ This function enables access to the deeper layers of the Meshtensor blockchain, allowing for detailed
369
+ and specific interactions with the network's runtime environment.
370
+ """
371
+ if reuse_block:
372
+ block_hash = self.substrate.last_block_hash
373
+ result = (
374
+ await self.substrate.runtime_call(runtime_api, method, params, block_hash)
375
+ ).value
376
+
377
+ return result
378
+
379
+ async def get_balance(
380
+ self,
381
+ address: str,
382
+ block_hash: Optional[str] = None,
383
+ reuse_block: bool = False,
384
+ ) -> Balance:
385
+ """
386
+ Retrieves the balance for a single coldkey address
387
+
388
+ :param address: coldkey address
389
+ :param block_hash: the block hash, optional
390
+ :param reuse_block: Whether to reuse the last-used block hash when retrieving info.
391
+ :return: Balance object representing the address's balance
392
+ """
393
+ result = await self.query(
394
+ module="System",
395
+ storage_function="Account",
396
+ params=[address],
397
+ block_hash=block_hash,
398
+ reuse_block_hash=reuse_block,
399
+ )
400
+ value = result or {"data": {"free": 0}}
401
+ return Balance(value["data"]["free"])
402
+
403
+ async def get_balances(
404
+ self,
405
+ *addresses: str,
406
+ block_hash: Optional[str] = None,
407
+ reuse_block: bool = False,
408
+ ) -> dict[str, Balance]:
409
+ """
410
+ Retrieves the balance for given coldkey(s)
411
+ :param addresses: coldkey addresses(s)
412
+ :param block_hash: the block hash, optional
413
+ :param reuse_block: Whether to reuse the last-used block hash when retrieving info.
414
+ :return: dict of {address: Balance objects}
415
+ """
416
+ if reuse_block:
417
+ block_hash = self.substrate.last_block_hash
418
+ calls = [
419
+ (
420
+ await self.substrate.create_storage_key(
421
+ "System", "Account", [address], block_hash=block_hash
422
+ )
423
+ )
424
+ for address in addresses
425
+ ]
426
+ batch_call = await self.substrate.query_multi(calls, block_hash=block_hash)
427
+ results = {}
428
+ for item in batch_call:
429
+ value = item[1] or {"data": {"free": 0}}
430
+ results.update({item[0].params[0]: Balance(value["data"]["free"])})
431
+ return results
432
+
433
+ async def get_total_stake_for_coldkey(
434
+ self,
435
+ *ss58_addresses,
436
+ block_hash: Optional[str] = None,
437
+ ) -> dict[str, tuple[Balance, Balance]]:
438
+ """
439
+ Returns the total stake held on a coldkey.
440
+
441
+ :param ss58_addresses: The SS58 address(es) of the coldkey(s)
442
+ :param block_hash: The hash of the block number to retrieve the stake from.
443
+
444
+ :return: {address: Balance objects}
445
+ """
446
+ sub_stakes, dynamic_info = await asyncio.gather(
447
+ self.get_stake_for_coldkeys(list(ss58_addresses), block_hash=block_hash),
448
+ # Token pricing info
449
+ self.all_subnets(block_hash=block_hash),
450
+ )
451
+
452
+ results = {}
453
+ for ss58, stake_info_list in sub_stakes.items():
454
+ total_tao_value = Balance(0)
455
+ total_swapped_tao_value = Balance(0)
456
+ for sub_stake in stake_info_list:
457
+ if sub_stake.stake.meshlet == 0:
458
+ continue
459
+ netuid = sub_stake.netuid
460
+ pool = dynamic_info[netuid]
461
+
462
+ alpha_value = Balance.from_meshlet(int(sub_stake.stake.meshlet)).set_unit(
463
+ netuid
464
+ )
465
+
466
+ # Without slippage
467
+ tao_value = pool.alpha_to_tao(alpha_value)
468
+ total_tao_value += tao_value
469
+
470
+ # With slippage
471
+ if netuid == 0:
472
+ swapped_tao_value = tao_value
473
+ else:
474
+ swapped_tao_value, _, _ = pool.alpha_to_tao_with_slippage(
475
+ sub_stake.stake
476
+ )
477
+ total_swapped_tao_value += swapped_tao_value
478
+
479
+ results[ss58] = (total_tao_value, total_swapped_tao_value)
480
+ return results
481
+
482
+ async def get_total_stake_for_hotkey(
483
+ self,
484
+ *ss58_addresses,
485
+ netuids: Optional[list[int]] = None,
486
+ block_hash: Optional[str] = None,
487
+ reuse_block: bool = False,
488
+ ) -> dict[str, dict[int, Balance]]:
489
+ """
490
+ Returns the total stake held on a hotkey.
491
+
492
+ :param ss58_addresses: The SS58 address(es) of the hotkey(s)
493
+ :param netuids: The netuids to retrieve the stake from. If not specified, will use all subnets.
494
+ :param block_hash: The hash of the block number to retrieve the stake from.
495
+ :param reuse_block: Whether to reuse the last-used block hash when retrieving info.
496
+
497
+ :return:
498
+ {
499
+ hotkey_ss58_1: {
500
+ netuid_1: netuid1_stake,
501
+ netuid_2: netuid2_stake,
502
+ ...
503
+ },
504
+ hotkey_ss58_2: {
505
+ netuid_1: netuid1_stake,
506
+ netuid_2: netuid2_stake,
507
+ ...
508
+ },
509
+ ...
510
+ }
511
+ """
512
+ if not block_hash:
513
+ if reuse_block:
514
+ block_hash = self.substrate.last_block_hash
515
+ else:
516
+ block_hash = await self.substrate.get_chain_head()
517
+
518
+ netuids = netuids or await self.get_all_subnet_netuids(block_hash=block_hash)
519
+ calls = [
520
+ (
521
+ await self.substrate.create_storage_key(
522
+ "MeshtensorModule",
523
+ "TotalHotkeyAlpha",
524
+ params=[ss58, netuid],
525
+ block_hash=block_hash,
526
+ )
527
+ )
528
+ for ss58 in ss58_addresses
529
+ for netuid in netuids
530
+ ]
531
+ query = await self.substrate.query_multi(calls, block_hash=block_hash)
532
+ results: dict[str, dict[int, "Balance"]] = {
533
+ hk_ss58: {} for hk_ss58 in ss58_addresses
534
+ }
535
+ for idx, (_, val) in enumerate(query):
536
+ hotkey_ss58 = ss58_addresses[idx // len(netuids)]
537
+ netuid = netuids[idx % len(netuids)]
538
+ value = (Balance.from_meshlet(val) if val is not None else Balance(0)).set_unit(
539
+ netuid
540
+ )
541
+ results[hotkey_ss58][netuid] = value
542
+ return results
543
+
544
+ async def current_take(
545
+ self,
546
+ hotkey_ss58: str,
547
+ block_hash: Optional[str] = None,
548
+ reuse_block: bool = False,
549
+ ) -> Optional[float]:
550
+ """
551
+ Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take'
552
+ represents the percentage of rewards that the delegate claims from its nominators' stakes.
553
+
554
+ :param hotkey_ss58: The `SS58` address of the neuron's hotkey.
555
+ :param block_hash: The hash of the block number to retrieve the stake from.
556
+ :param reuse_block: Whether to reuse the last-used block hash when retrieving info.
557
+
558
+ :return: The delegate take percentage, None if not available.
559
+
560
+ The delegate take is a critical parameter in the network's incentive structure, influencing
561
+ the distribution of rewards among neurons and their nominators.
562
+ """
563
+ result = await self.query(
564
+ module="MeshtensorModule",
565
+ storage_function="Delegates",
566
+ params=[hotkey_ss58],
567
+ block_hash=block_hash,
568
+ reuse_block_hash=reuse_block,
569
+ )
570
+ if result is None:
571
+ return None
572
+ else:
573
+ return u16_normalized_float(result)
574
+
575
+ async def get_netuids_for_hotkey(
576
+ self,
577
+ hotkey_ss58: str,
578
+ block_hash: Optional[str] = None,
579
+ reuse_block: bool = False,
580
+ ) -> list[int]:
581
+ """
582
+ Retrieves a list of subnet UIDs (netuids) for which a given hotkey is a member. This function
583
+ identifies the specific subnets within the Meshtensor network where the neuron associated with
584
+ the hotkey is active.
585
+
586
+ :param hotkey_ss58: The ``SS58`` address of the neuron's hotkey.
587
+ :param block_hash: The hash of the blockchain block number at which to perform the query.
588
+ :param reuse_block: Whether to reuse the last-used block hash when retrieving info.
589
+
590
+ :return: A list of netuids where the neuron is a member.
591
+ """
592
+
593
+ result = await self.substrate.query_map(
594
+ module="MeshtensorModule",
595
+ storage_function="IsNetworkMember",
596
+ params=[hotkey_ss58],
597
+ block_hash=block_hash,
598
+ reuse_block_hash=reuse_block,
599
+ )
600
+ res = []
601
+ async for record in result:
602
+ if record[1].value:
603
+ res.append(record[0])
604
+ return res
605
+
606
+ async def is_subnet_active(
607
+ self,
608
+ netuid: int,
609
+ block_hash: Optional[str] = None,
610
+ reuse_block: bool = False,
611
+ ) -> bool:
612
+ """Verify if subnet with provided netuid is active.
613
+
614
+ Args:
615
+ netuid (int): The unique identifier of the subnet.
616
+ block_hash (Optional[str]): The blockchain block_hash representation of block id.
617
+ reuse_block (bool): Whether to reuse the last-used block hash.
618
+
619
+ Returns:
620
+ True if subnet is active, False otherwise.
621
+
622
+ This means whether the `start_call` was initiated or not.
623
+ """
624
+ query = await self.substrate.query(
625
+ module="MeshtensorModule",
626
+ storage_function="FirstEmissionBlockNumber",
627
+ block_hash=block_hash,
628
+ reuse_block_hash=reuse_block,
629
+ params=[netuid],
630
+ )
631
+ return True if query and query.value > 0 else False
632
+
633
+ async def subnet_exists(
634
+ self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False
635
+ ) -> bool:
636
+ """
637
+ Checks if a subnet with the specified unique identifier (netuid) exists within the Meshtensor network.
638
+
639
+ :param netuid: The unique identifier of the subnet.
640
+ :param block_hash: The hash of the blockchain block number at which to check the subnet existence.
641
+ :param reuse_block: Whether to reuse the last-used block hash.
642
+
643
+ :return: `True` if the subnet exists, `False` otherwise.
644
+
645
+ This function is critical for verifying the presence of specific subnets in the network,
646
+ enabling a deeper understanding of the network's structure and composition.
647
+ """
648
+ result = await self.query(
649
+ module="MeshtensorModule",
650
+ storage_function="NetworksAdded",
651
+ params=[netuid],
652
+ block_hash=block_hash,
653
+ reuse_block_hash=reuse_block,
654
+ )
655
+ return result
656
+
657
+ async def total_networks(
658
+ self, block_hash: Optional[str] = None, reuse_block: bool = False
659
+ ) -> int:
660
+ """
661
+ Returns the total number of subnets in the Meshtensor network.
662
+
663
+ :param block_hash: The hash of the blockchain block number at which to check the subnet existence.
664
+ :param reuse_block: Whether to reuse the last-used block hash.
665
+
666
+ :return: The total number of subnets in the network.
667
+ """
668
+ result = await self.query(
669
+ module="MeshtensorModule",
670
+ storage_function="TotalNetworks",
671
+ params=[],
672
+ block_hash=block_hash,
673
+ reuse_block_hash=reuse_block,
674
+ )
675
+ return result
676
+
677
+ async def get_subnet_state(
678
+ self, netuid: int, block_hash: Optional[str] = None
679
+ ) -> Optional["SubnetState"]:
680
+ """
681
+ Retrieves the state of a specific subnet within the Meshtensor network.
682
+
683
+ :param netuid: The network UID of the subnet to query.
684
+ :param block_hash: The hash of the blockchain block number for the query.
685
+
686
+ :return: SubnetState object containing the subnet's state information, or None if the subnet doesn't exist.
687
+ """
688
+ result = await self.query_runtime_api(
689
+ runtime_api="SubnetInfoRuntimeApi",
690
+ method="get_subnet_state",
691
+ params=[netuid],
692
+ block_hash=block_hash,
693
+ )
694
+
695
+ if result is None:
696
+ return None
697
+
698
+ return SubnetState.from_any(result)
699
+
700
+ async def get_hyperparameter(
701
+ self,
702
+ param_name: str,
703
+ netuid: int,
704
+ block_hash: Optional[str] = None,
705
+ reuse_block: bool = False,
706
+ ) -> Optional[Any]:
707
+ """
708
+ Retrieves a specified hyperparameter for a specific subnet.
709
+
710
+ :param param_name: The name of the hyperparameter to retrieve.
711
+ :param netuid: The unique identifier of the subnet.
712
+ :param block_hash: The hash of blockchain block number for the query.
713
+ :param reuse_block: Whether to reuse the last-used block hash.
714
+
715
+ :return: The value of the specified hyperparameter if the subnet exists, or None
716
+ """
717
+ if not await self.subnet_exists(netuid, block_hash):
718
+ print("subnet does not exist")
719
+ return None
720
+
721
+ result = await self.query(
722
+ module="MeshtensorModule",
723
+ storage_function=param_name,
724
+ params=[netuid],
725
+ block_hash=block_hash,
726
+ reuse_block_hash=reuse_block,
727
+ )
728
+
729
+ if result is None:
730
+ return None
731
+
732
+ return result
733
+
734
+ async def filter_netuids_by_registered_hotkeys(
735
+ self,
736
+ all_netuids: Iterable[int],
737
+ filter_for_netuids: Iterable[int],
738
+ all_hotkeys: Iterable[Wallet],
739
+ block_hash: Optional[str] = None,
740
+ reuse_block: bool = False,
741
+ ) -> list[int]:
742
+ """
743
+ Filters a given list of all netuids for certain specified netuids and hotkeys
744
+
745
+ :param all_netuids: A list of netuids to filter.
746
+ :param filter_for_netuids: A subset of all_netuids to filter from the main list
747
+ :param all_hotkeys: Hotkeys to filter from the main list
748
+ :param block_hash: hash of the blockchain block number at which to perform the query.
749
+ :param reuse_block: whether to reuse the last-used blockchain hash when retrieving info.
750
+
751
+ :return: the filtered list of netuids.
752
+ """
753
+ netuids_with_registered_hotkeys = [
754
+ item
755
+ for sublist in await asyncio.gather(
756
+ *[
757
+ self.get_netuids_for_hotkey(
758
+ get_hotkey_pub_ss58(wallet),
759
+ reuse_block=reuse_block,
760
+ block_hash=block_hash,
761
+ )
762
+ for wallet in all_hotkeys
763
+ ]
764
+ )
765
+ for item in sublist
766
+ ]
767
+
768
+ if not filter_for_netuids:
769
+ all_netuids = netuids_with_registered_hotkeys
770
+
771
+ else:
772
+ filtered_netuids = [
773
+ netuid for netuid in all_netuids if netuid in filter_for_netuids
774
+ ]
775
+
776
+ registered_hotkeys_filtered = [
777
+ netuid
778
+ for netuid in netuids_with_registered_hotkeys
779
+ if netuid in filter_for_netuids
780
+ ]
781
+
782
+ # Combine both filtered lists
783
+ all_netuids = filtered_netuids + registered_hotkeys_filtered
784
+
785
+ return list(set(all_netuids))
786
+
787
+ async def get_existential_deposit(
788
+ self, block_hash: Optional[str] = None, reuse_block: bool = False
789
+ ) -> Balance:
790
+ """
791
+ Retrieves the existential deposit amount for the Meshtensor blockchain. The existential deposit
792
+ is the minimum amount of MESH required for an account to exist on the blockchain. Accounts with
793
+ balances below this threshold can be reaped to conserve network resources.
794
+
795
+ :param block_hash: Block hash at which to query the deposit amount. If `None`, the current block is used.
796
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
797
+
798
+ :return: The existential deposit amount
799
+
800
+ The existential deposit is a fundamental economic parameter in the Meshtensor network, ensuring
801
+ efficient use of storage and preventing the proliferation of dust accounts.
802
+ """
803
+ result = getattr(
804
+ await self.substrate.get_constant(
805
+ module_name="Balances",
806
+ constant_name="ExistentialDeposit",
807
+ block_hash=block_hash,
808
+ reuse_block_hash=reuse_block,
809
+ ),
810
+ "value",
811
+ None,
812
+ )
813
+
814
+ if result is None:
815
+ raise Exception("Unable to retrieve existential deposit amount.")
816
+
817
+ return Balance.from_meshlet(result)
818
+
819
+ async def neurons(
820
+ self, netuid: int, block_hash: Optional[str] = None
821
+ ) -> list[NeuronInfo]:
822
+ """
823
+ Retrieves a list of all neurons within a specified subnet of the Meshtensor network. This function
824
+ provides a snapshot of the subnet's neuron population, including each neuron's attributes and network
825
+ interactions.
826
+
827
+ :param netuid: The unique identifier of the subnet.
828
+ :param block_hash: The hash of the blockchain block number for the query.
829
+
830
+ :return: A list of NeuronInfo objects detailing each neuron's characteristics in the subnet.
831
+
832
+ Understanding the distribution and status of neurons within a subnet is key to comprehending the
833
+ network's decentralized structure and the dynamics of its consensus and governance processes.
834
+ """
835
+ neurons_lite, weights, bonds = await asyncio.gather(
836
+ self.neurons_lite(netuid=netuid, block_hash=block_hash),
837
+ self.weights(netuid=netuid, block_hash=block_hash),
838
+ self.bonds(netuid=netuid, block_hash=block_hash),
839
+ )
840
+
841
+ weights_as_dict = {uid: w for uid, w in weights}
842
+ bonds_as_dict = {uid: b for uid, b in bonds}
843
+
844
+ neurons = [
845
+ NeuronInfo.from_weights_bonds_and_neuron_lite(
846
+ neuron_lite, weights_as_dict, bonds_as_dict
847
+ )
848
+ for neuron_lite in neurons_lite
849
+ ]
850
+
851
+ return neurons
852
+
853
+ async def neurons_lite(
854
+ self, netuid: int, block_hash: Optional[str] = None, reuse_block: bool = False
855
+ ) -> list[NeuronInfoLite]:
856
+ """
857
+ Retrieves a list of neurons in a 'lite' format from a specific subnet of the Meshtensor network.
858
+ This function provides a streamlined view of the neurons, focusing on key attributes such as stake
859
+ and network participation.
860
+
861
+ :param netuid: The unique identifier of the subnet.
862
+ :param block_hash: The hash of the blockchain block number for the query.
863
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
864
+
865
+ :return: A list of simplified neuron information for the subnet.
866
+
867
+ This function offers a quick overview of the neuron population within a subnet, facilitating
868
+ efficient analysis of the network's decentralized structure and neuron dynamics.
869
+ """
870
+ result = await self.query_runtime_api(
871
+ runtime_api="NeuronInfoRuntimeApi",
872
+ method="get_neurons_lite",
873
+ params=[netuid],
874
+ block_hash=block_hash,
875
+ reuse_block=reuse_block,
876
+ )
877
+
878
+ if result is None:
879
+ return []
880
+
881
+ return NeuronInfoLite.list_from_any(result)
882
+
883
+ async def neuron_for_uid(
884
+ self, uid: Optional[int], netuid: int, block_hash: Optional[str] = None
885
+ ) -> NeuronInfo:
886
+ """
887
+ Retrieves detailed information about a specific neuron identified by its unique identifier (UID)
888
+ within a specified subnet (netuid) of the Meshtensor network. This function provides a comprehensive
889
+ view of a neuron's attributes, including its stake, rank, and operational status.
890
+
891
+
892
+ :param uid: The unique identifier of the neuron.
893
+ :param netuid: The unique identifier of the subnet.
894
+ :param block_hash: The hash of the blockchain block number for the query.
895
+
896
+ :return: Detailed information about the neuron if found, a null neuron otherwise
897
+
898
+ This function is crucial for analyzing individual neurons' contributions and status within a specific
899
+ subnet, offering insights into their roles in the network's consensus and validation mechanisms.
900
+ """
901
+ if uid is None:
902
+ return NeuronInfo.get_null_neuron()
903
+
904
+ result = await self.query_runtime_api(
905
+ runtime_api="NeuronInfoRuntimeApi",
906
+ method="get_neuron",
907
+ params=[
908
+ netuid,
909
+ uid,
910
+ ], # TODO check to see if this can accept more than one at a time
911
+ block_hash=block_hash,
912
+ )
913
+
914
+ if not result:
915
+ return NeuronInfo.get_null_neuron()
916
+
917
+ return NeuronInfo.from_any(result)
918
+
919
+ async def get_delegated(
920
+ self,
921
+ coldkey_ss58: str,
922
+ block_hash: Optional[str] = None,
923
+ reuse_block: bool = False,
924
+ ) -> list[tuple[DelegateInfo, Balance]]:
925
+ """
926
+ Retrieves a list of delegates and their associated stakes for a given coldkey. This function
927
+ identifies the delegates that a specific account has staked tokens on.
928
+
929
+ :param coldkey_ss58: The `SS58` address of the account's coldkey.
930
+ :param block_hash: The hash of the blockchain block number for the query.
931
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
932
+
933
+ :return: A list of tuples, each containing a delegate's information and staked amount.
934
+
935
+ This function is important for account holders to understand their stake allocations and their
936
+ involvement in the network's delegation and consensus mechanisms.
937
+ """
938
+
939
+ block_hash = (
940
+ block_hash
941
+ if block_hash
942
+ else (self.substrate.last_block_hash if reuse_block else None)
943
+ )
944
+ result = await self.query_runtime_api(
945
+ runtime_api="DelegateInfoRuntimeApi",
946
+ method="get_delegated",
947
+ params=[coldkey_ss58],
948
+ block_hash=block_hash,
949
+ )
950
+
951
+ if not result:
952
+ return []
953
+
954
+ return DelegateInfo.list_from_any(result)
955
+
956
+ async def query_all_identities(
957
+ self,
958
+ block_hash: Optional[str] = None,
959
+ reuse_block: bool = False,
960
+ ) -> dict[str, dict]:
961
+ """
962
+ Queries all identities on the Meshtensor blockchain.
963
+
964
+ :param block_hash: The hash of the blockchain block number at which to perform the query.
965
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
966
+
967
+ :return: A dictionary mapping addresses to their decoded identity data.
968
+ """
969
+
970
+ identities = await self.substrate.query_map(
971
+ module="MeshtensorModule",
972
+ storage_function="IdentitiesV2",
973
+ block_hash=block_hash,
974
+ reuse_block_hash=reuse_block,
975
+ fully_exhaust=True,
976
+ )
977
+ all_identities = {}
978
+ for ss58_address, identity in identities.records:
979
+ all_identities[decode_account_id(ss58_address[0])] = decode_hex_identity(
980
+ identity.value
981
+ )
982
+
983
+ return all_identities
984
+
985
+ async def query_identity(
986
+ self,
987
+ key: str,
988
+ block_hash: Optional[str] = None,
989
+ reuse_block: bool = False,
990
+ ) -> dict:
991
+ """
992
+ Queries the identity of a neuron on the Meshtensor blockchain using the given key. This function retrieves
993
+ detailed identity information about a specific neuron, which is a crucial aspect of the network's decentralized
994
+ identity and governance system.
995
+
996
+ Note:
997
+ See the `Meshtensor CLI documentation <https://docs.meshtensor.com/reference/meshcli>`_ for supported identity
998
+ parameters.
999
+
1000
+ :param key: The key used to query the neuron's identity, typically the neuron's SS58 address.
1001
+ :param block_hash: The hash of the blockchain block number at which to perform the query.
1002
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
1003
+
1004
+ :return: An object containing the identity information of the neuron if found, ``None`` otherwise.
1005
+
1006
+ The identity information can include various attributes such as the neuron's stake, rank, and other
1007
+ network-specific details, providing insights into the neuron's role and status within the Meshtensor network.
1008
+ """
1009
+ identity_info = await self.query(
1010
+ module="MeshtensorModule",
1011
+ storage_function="IdentitiesV2",
1012
+ params=[key],
1013
+ block_hash=block_hash,
1014
+ reuse_block_hash=reuse_block,
1015
+ )
1016
+ if not identity_info:
1017
+ return {}
1018
+ try:
1019
+ return decode_hex_identity(identity_info)
1020
+ except TypeError:
1021
+ return {}
1022
+
1023
+ async def fetch_coldkey_hotkey_identities(
1024
+ self,
1025
+ block_hash: Optional[str] = None,
1026
+ reuse_block: bool = False,
1027
+ ) -> dict[str, dict]:
1028
+ """
1029
+ Builds a dictionary containing coldkeys and hotkeys with their associated identities and relationships.
1030
+ :param block_hash: The hash of the blockchain block number for the query.
1031
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
1032
+ :return: Dict with 'coldkeys' and 'hotkeys' as keys.
1033
+ """
1034
+ if block_hash is None:
1035
+ block_hash = await self.substrate.get_chain_head()
1036
+ coldkey_identities = await self.query_all_identities(block_hash=block_hash)
1037
+ identities = {"coldkeys": {}, "hotkeys": {}}
1038
+ sks = [
1039
+ await self.substrate.create_storage_key(
1040
+ "MeshtensorModule", "OwnedHotkeys", [ck], block_hash=block_hash
1041
+ )
1042
+ for ck in coldkey_identities.keys()
1043
+ ]
1044
+ query = await self.substrate.query_multi(sks, block_hash=block_hash)
1045
+
1046
+ storage_key: StorageKey
1047
+ for storage_key, hotkeys in query:
1048
+ coldkey_ss58 = storage_key.params[0]
1049
+ coldkey_identity = coldkey_identities.get(coldkey_ss58)
1050
+
1051
+ identities["coldkeys"][coldkey_ss58] = {
1052
+ "identity": coldkey_identity,
1053
+ "hotkeys": hotkeys,
1054
+ }
1055
+
1056
+ for hotkey_ss58 in hotkeys:
1057
+ identities["hotkeys"][hotkey_ss58] = {
1058
+ "coldkey": coldkey_ss58,
1059
+ "identity": coldkey_identity,
1060
+ }
1061
+
1062
+ return identities
1063
+
1064
+ async def weights(
1065
+ self, netuid: int, block_hash: Optional[str] = None
1066
+ ) -> list[tuple[int, list[tuple[int, int]]]]:
1067
+ """
1068
+ Retrieves the weight distribution set by neurons within a specific subnet of the Meshtensor network.
1069
+ This function maps each neuron's UID to the weights it assigns to other neurons, reflecting the
1070
+ network's trust and value assignment mechanisms.
1071
+
1072
+ :param netuid: The network UID of the subnet to query.
1073
+ :param block_hash: The hash of the blockchain block for the query.
1074
+
1075
+ :return: A list of tuples mapping each neuron's UID to its assigned weights.
1076
+
1077
+ The weight distribution is a key factor in the network's consensus algorithm and the ranking of neurons,
1078
+ influencing their influence and reward allocation within the subnet.
1079
+ """
1080
+ w_map_encoded = await self.substrate.query_map(
1081
+ module="MeshtensorModule",
1082
+ storage_function="Weights",
1083
+ params=[netuid],
1084
+ block_hash=block_hash,
1085
+ )
1086
+ w_map = []
1087
+ async for uid, w in w_map_encoded:
1088
+ w_map.append((uid, w.value))
1089
+
1090
+ return w_map
1091
+
1092
+ async def bonds(
1093
+ self, netuid: int, block_hash: Optional[str] = None
1094
+ ) -> list[tuple[int, list[tuple[int, int]]]]:
1095
+ """
1096
+ Retrieves the bond distribution set by neurons within a specific subnet of the Meshtensor network.
1097
+ Bonds represent the investments or commitments made by neurons in one another, indicating a level
1098
+ of trust and perceived value. This bonding mechanism is integral to the network's market-based approach
1099
+ to measuring and rewarding machine intelligence.
1100
+
1101
+ :param netuid: The network UID of the subnet to query.
1102
+ :param block_hash: The hash of the blockchain block number for the query.
1103
+
1104
+ :return: list of tuples mapping each neuron's UID to its bonds with other neurons.
1105
+
1106
+ Understanding bond distributions is crucial for analyzing the trust dynamics and market behavior
1107
+ within the subnet. It reflects how neurons recognize and invest in each other's intelligence and
1108
+ contributions, supporting diverse and niche systems within the Meshtensor ecosystem.
1109
+ """
1110
+ b_map_encoded = await self.substrate.query_map(
1111
+ module="MeshtensorModule",
1112
+ storage_function="Bonds",
1113
+ params=[netuid],
1114
+ block_hash=block_hash,
1115
+ )
1116
+ b_map = []
1117
+ async for uid, b in b_map_encoded:
1118
+ b_map.append((uid, b))
1119
+
1120
+ return b_map
1121
+
1122
+ async def does_hotkey_exist(
1123
+ self,
1124
+ hotkey_ss58: str,
1125
+ block_hash: Optional[str] = None,
1126
+ reuse_block: bool = False,
1127
+ ) -> bool:
1128
+ """
1129
+ Returns true if the hotkey is known by the chain and there are accounts.
1130
+
1131
+ :param hotkey_ss58: The SS58 address of the hotkey.
1132
+ :param block_hash: The hash of the block number to check the hotkey against.
1133
+ :param reuse_block: Whether to reuse the last-used blockchain hash.
1134
+
1135
+ :return: `True` if the hotkey is known by the chain and there are accounts, `False` otherwise.
1136
+ """
1137
+ result = await self.query(
1138
+ module="MeshtensorModule",
1139
+ storage_function="Owner",
1140
+ params=[hotkey_ss58],
1141
+ block_hash=block_hash,
1142
+ reuse_block_hash=reuse_block,
1143
+ )
1144
+ return_val = result != GENESIS_ADDRESS
1145
+ return return_val
1146
+
1147
+ async def get_hotkey_owner(
1148
+ self,
1149
+ hotkey_ss58: str,
1150
+ block_hash: Optional[str] = None,
1151
+ check_exists: bool = True,
1152
+ ) -> Optional[str]:
1153
+ val = await self.query(
1154
+ module="MeshtensorModule",
1155
+ storage_function="Owner",
1156
+ params=[hotkey_ss58],
1157
+ block_hash=block_hash,
1158
+ )
1159
+ if check_exists:
1160
+ if val:
1161
+ exists = await self.does_hotkey_exist(
1162
+ hotkey_ss58, block_hash=block_hash
1163
+ )
1164
+ else:
1165
+ exists = False
1166
+ else:
1167
+ exists = True
1168
+ hotkey_owner = val if exists else None
1169
+ return hotkey_owner
1170
+
1171
+ async def sign_and_send_extrinsic(
1172
+ self,
1173
+ call: GenericCall,
1174
+ wallet: Wallet,
1175
+ wait_for_inclusion: bool = True,
1176
+ wait_for_finalization: bool = False,
1177
+ era: Optional[dict[str, int]] = None,
1178
+ proxy: Optional[str] = None,
1179
+ nonce: Optional[int] = None,
1180
+ sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey",
1181
+ announce_only: bool = False,
1182
+ mev_protection: bool = False,
1183
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
1184
+ """
1185
+ Helper method to sign and submit an extrinsic call to chain.
1186
+
1187
+ :param call: a prepared Call object
1188
+ :param wallet: the wallet whose coldkey will be used to sign the extrinsic
1189
+ :param wait_for_inclusion: whether to wait until the extrinsic call is included on the chain
1190
+ :param wait_for_finalization: whether to wait until the extrinsic call is finalized on the chain
1191
+ :param era: The length (in blocks) for which a transaction should be valid.
1192
+ :param proxy: The real account used to create the proxy. None if not using a proxy for this call.
1193
+ :param nonce: The nonce used to submit this extrinsic call.
1194
+ :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call.
1195
+ :param announce_only: If set, makes the call as an announcement, rather than making the call. Cannot
1196
+ be used with `mev_protection=True`.
1197
+ :param mev_protection: If set, uses Mev Protection on the extrinsic, thus encrypting it. Cannot be
1198
+ used with `announce_only=True`.
1199
+
1200
+ :return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None)
1201
+ """
1202
+
1203
+ async def create_signed(call_to_sign, n):
1204
+ kwargs = {
1205
+ "call": call_to_sign,
1206
+ "keypair": keypair,
1207
+ "nonce": n,
1208
+ }
1209
+ if era is not None:
1210
+ kwargs["era"] = era
1211
+ return await self.substrate.create_signed_extrinsic(**kwargs)
1212
+
1213
+ if announce_only and mev_protection:
1214
+ raise ValueError(
1215
+ "Cannot use announce-only and mev-protection. Calls should be announced without mev protection,"
1216
+ "and executed with them."
1217
+ )
1218
+ if proxy is not None:
1219
+ if announce_only:
1220
+ call_to_announce = call
1221
+ call = await self.substrate.compose_call(
1222
+ "Proxy",
1223
+ "announce",
1224
+ {
1225
+ "real": proxy,
1226
+ "call_hash": f"0x{call_to_announce.call_hash.hex()}",
1227
+ },
1228
+ )
1229
+ else:
1230
+ call = await self.substrate.compose_call(
1231
+ "Proxy",
1232
+ "proxy",
1233
+ {"real": proxy, "call": call, "force_proxy_type": None},
1234
+ )
1235
+ keypair = getattr(wallet, sign_with)
1236
+ call_args: dict[str, Union[GenericCall, Keypair, dict[str, int], int]] = {
1237
+ "call": call,
1238
+ # sign with specified key
1239
+ "keypair": keypair,
1240
+ }
1241
+ if era is not None:
1242
+ call_args["era"] = era
1243
+ if nonce is not None:
1244
+ call_args["nonce"] = nonce
1245
+ else:
1246
+ call_args["nonce"] = await self.substrate.get_account_next_index(
1247
+ keypair.ss58_address
1248
+ )
1249
+ inner_hash = ""
1250
+ if mev_protection:
1251
+ next_nonce = await self.substrate.get_account_next_index(
1252
+ keypair.ss58_address
1253
+ )
1254
+ inner_extrinsic = await create_signed(call, next_nonce)
1255
+ inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}"
1256
+ shield_call = await encrypt_extrinsic(self, inner_extrinsic)
1257
+ extrinsic = await create_signed(shield_call, nonce)
1258
+ else:
1259
+ extrinsic = await self.substrate.create_signed_extrinsic(**call_args)
1260
+ try:
1261
+ response = await self.substrate.submit_extrinsic(
1262
+ extrinsic,
1263
+ wait_for_inclusion=wait_for_inclusion,
1264
+ wait_for_finalization=wait_for_finalization,
1265
+ )
1266
+ # We only wait here if we expect finalization.
1267
+ if not wait_for_finalization and not wait_for_inclusion:
1268
+ return True, inner_hash, response
1269
+ if await response.is_success:
1270
+ if announce_only:
1271
+ block = await self.substrate.get_block_number(response.block_hash)
1272
+ with ProxyAnnouncements.get_db() as (conn, cursor):
1273
+ ProxyAnnouncements.add_entry(
1274
+ conn,
1275
+ cursor,
1276
+ address=proxy,
1277
+ epoch_time=int(time.time()),
1278
+ block=block,
1279
+ call_hash=call_to_announce.call_hash.hex(),
1280
+ call=call_to_announce,
1281
+ )
1282
+ console.print(
1283
+ f"Added entry [green]{call_to_announce.call_hash.hex()}[/green] "
1284
+ f"at block {block} to your ProxyAnnouncements address book. You can execute this with\n"
1285
+ f"[blue]meshcli proxy execute --call-hash {call_to_announce.call_hash.hex()}[/blue]"
1286
+ )
1287
+ return True, inner_hash, response
1288
+ else:
1289
+ return False, format_error_message(await response.error_message), None
1290
+ except SubstrateRequestException as e:
1291
+ err_msg = format_error_message(e)
1292
+ if proxy and "Invalid Transaction" in err_msg:
1293
+ extrinsic_fee, signer_balance = await asyncio.gather(
1294
+ self.get_extrinsic_fee(
1295
+ call, keypair=wallet.coldkeypub, proxy=proxy
1296
+ ),
1297
+ self.get_balance(wallet.coldkeypub.ss58_address),
1298
+ )
1299
+ if extrinsic_fee > signer_balance:
1300
+ err_msg += (
1301
+ "\nAs this is a proxy transaction, the signing account needs to pay the extrinsic fee. "
1302
+ f"However, the balance of the signing account is {signer_balance}, and the extrinsic fee is "
1303
+ f"{extrinsic_fee}."
1304
+ )
1305
+ return False, err_msg, None
1306
+
1307
+ async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]:
1308
+ """
1309
+ This method retrieves the children of a given hotkey and netuid. It queries the MeshtensorModule's ChildKeys
1310
+ storage function to get the children and formats them before returning as a tuple.
1311
+
1312
+ :param hotkey: The hotkey value.
1313
+ :param netuid: The netuid value.
1314
+
1315
+ :return: A tuple containing a boolean indicating success or failure, a list of formatted children, and an error
1316
+ message (if applicable)
1317
+ """
1318
+ try:
1319
+ children = await self.query(
1320
+ module="MeshtensorModule",
1321
+ storage_function="ChildKeys",
1322
+ params=[hotkey, netuid],
1323
+ )
1324
+ if children:
1325
+ formatted_children = []
1326
+ for proportion, child in children:
1327
+ # Convert U64 to int
1328
+ formatted_child = decode_account_id(child[0])
1329
+ int_proportion = int(proportion)
1330
+ formatted_children.append((int_proportion, formatted_child))
1331
+ return True, formatted_children, ""
1332
+ else:
1333
+ return True, [], ""
1334
+ except SubstrateRequestException as e:
1335
+ return False, [], format_error_message(e)
1336
+
1337
+ async def get_subnet_hyperparameters(
1338
+ self, netuid: int, block_hash: Optional[str] = None
1339
+ ) -> Optional[Union[list, SubnetHyperparameters]]:
1340
+ """
1341
+ Retrieves the hyperparameters for a specific subnet within the Meshtensor network. These hyperparameters
1342
+ define the operational settings and rules governing the subnet's behavior.
1343
+
1344
+ :param netuid: The network UID of the subnet to query.
1345
+ :param block_hash: The hash of the blockchain block number for the query.
1346
+
1347
+ :return: The subnet's hyperparameters, or `None` if not available.
1348
+
1349
+ Understanding the hyperparameters is crucial for comprehending how subnets are configured and
1350
+ managed, and how they interact with the network's consensus and incentive mechanisms.
1351
+ """
1352
+ result = await self.query_runtime_api(
1353
+ runtime_api="SubnetInfoRuntimeApi",
1354
+ method="get_subnet_hyperparams_v2",
1355
+ params=[netuid],
1356
+ block_hash=block_hash,
1357
+ )
1358
+ if not result:
1359
+ return []
1360
+
1361
+ return SubnetHyperparameters.from_any(result)
1362
+
1363
+ async def get_subnet_mechanisms(
1364
+ self, netuid: int, block_hash: Optional[str] = None
1365
+ ) -> int:
1366
+ """Return the number of mechanisms that belong to the provided subnet."""
1367
+
1368
+ result = await self.query(
1369
+ module="MeshtensorModule",
1370
+ storage_function="MechanismCountCurrent",
1371
+ params=[netuid],
1372
+ block_hash=block_hash,
1373
+ )
1374
+
1375
+ if result is None:
1376
+ return 0
1377
+ return int(result)
1378
+
1379
+ async def get_all_subnet_mechanisms(
1380
+ self, block_hash: Optional[str] = None
1381
+ ) -> dict[int, int]:
1382
+ """Return mechanism counts for every subnet with a recorded value."""
1383
+
1384
+ results = await self.substrate.query_map(
1385
+ module="MeshtensorModule",
1386
+ storage_function="MechanismCountCurrent",
1387
+ params=[],
1388
+ block_hash=block_hash,
1389
+ )
1390
+ res = {}
1391
+ async for netuid, count in results:
1392
+ res[int(netuid)] = int(count.value)
1393
+ return res
1394
+
1395
+ async def get_mechanism_emission_split(
1396
+ self, netuid: int, block_hash: Optional[str] = None
1397
+ ) -> list[int]:
1398
+ """Return the emission split configured for the provided subnet."""
1399
+
1400
+ result = await self.query(
1401
+ module="MeshtensorModule",
1402
+ storage_function="MechanismEmissionSplit",
1403
+ params=[netuid],
1404
+ block_hash=block_hash,
1405
+ )
1406
+
1407
+ if not result:
1408
+ return []
1409
+
1410
+ return [int(value) for value in result]
1411
+
1412
+ async def burn_cost(self, block_hash: Optional[str] = None) -> Optional[Balance]:
1413
+ result = await self.query_runtime_api(
1414
+ runtime_api="SubnetRegistrationRuntimeApi",
1415
+ method="get_network_registration_cost",
1416
+ params=[],
1417
+ block_hash=block_hash,
1418
+ )
1419
+ return Balance.from_meshlet(result) if result is not None else None
1420
+
1421
+ async def get_vote_data(
1422
+ self,
1423
+ proposal_hash: str,
1424
+ block_hash: Optional[str] = None,
1425
+ reuse_block: bool = False,
1426
+ ) -> Optional["ProposalVoteData"]:
1427
+ """
1428
+ Retrieves the voting data for a specific proposal on the Meshtensor blockchain. This data includes
1429
+ information about how senate members have voted on the proposal.
1430
+
1431
+ :param proposal_hash: The hash of the proposal for which voting data is requested.
1432
+ :param block_hash: The hash of the blockchain block number to query the voting data.
1433
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
1434
+
1435
+ :return: An object containing the proposal's voting data, or `None` if not found.
1436
+
1437
+ This function is important for tracking and understanding the decision-making processes within
1438
+ the Meshtensor network, particularly how proposals are received and acted upon by the governing body.
1439
+ """
1440
+ vote_data = await self.query(
1441
+ module="Triumvirate",
1442
+ storage_function="Voting",
1443
+ params=[proposal_hash],
1444
+ block_hash=block_hash,
1445
+ reuse_block_hash=reuse_block,
1446
+ )
1447
+ if vote_data is None:
1448
+ return None
1449
+ else:
1450
+ return ProposalVoteData(vote_data)
1451
+
1452
+ async def get_delegate_identities(
1453
+ self, block_hash: Optional[str] = None
1454
+ ) -> dict[str, DelegatesDetails]:
1455
+ """
1456
+ Fetches delegates identities from the chain and GitHub. Preference is given to chain data, and missing info
1457
+ is filled-in by the info from GitHub. At some point, we want to totally move away from fetching this info
1458
+ from GitHub, but chain data is still limited in that regard.
1459
+
1460
+ :param block_hash: the hash of the blockchain block for the query
1461
+
1462
+ :return: {ss58: DelegatesDetails, ...}
1463
+
1464
+ """
1465
+ identities_info = await self.substrate.query_map(
1466
+ module="Registry",
1467
+ storage_function="IdentityOf",
1468
+ block_hash=block_hash,
1469
+ )
1470
+
1471
+ all_delegates_details = {}
1472
+ async for ss58_address, identity in identities_info:
1473
+ all_delegates_details.update(
1474
+ {
1475
+ decode_account_id(
1476
+ ss58_address[0]
1477
+ ): DelegatesDetails.from_chain_data(
1478
+ decode_hex_identity_dict(identity.value["info"])
1479
+ )
1480
+ }
1481
+ )
1482
+
1483
+ return all_delegates_details
1484
+
1485
+ async def get_stake_for_coldkey_and_hotkey_on_netuid(
1486
+ self,
1487
+ hotkey_ss58: str,
1488
+ coldkey_ss58: str,
1489
+ netuid: int,
1490
+ block_hash: Optional[str] = None,
1491
+ ) -> "Balance":
1492
+ """Returns the stake under a coldkey - hotkey - netuid pairing"""
1493
+ _result = await self.query(
1494
+ "MeshtensorModule",
1495
+ "Alpha",
1496
+ [hotkey_ss58, coldkey_ss58, netuid],
1497
+ block_hash,
1498
+ )
1499
+ if _result is None:
1500
+ return Balance(0).set_unit(netuid)
1501
+ else:
1502
+ return Balance.from_meshlet(fixed_to_float(_result)).set_unit(int(netuid))
1503
+
1504
+ async def get_mechagraph_info(
1505
+ self, netuid: int, mech_id: int, block_hash: Optional[str] = None
1506
+ ) -> Optional[MetagraphInfo]:
1507
+ """
1508
+ Returns the metagraph info for a given subnet and mechanism id.
1509
+ And yes, it is indeed 'mecha'graph
1510
+ """
1511
+ query = await self.query_runtime_api(
1512
+ runtime_api="SubnetInfoRuntimeApi",
1513
+ method="get_mechagraph",
1514
+ params=[netuid, mech_id],
1515
+ block_hash=block_hash,
1516
+ )
1517
+
1518
+ if query is None:
1519
+ return None
1520
+
1521
+ return MetagraphInfo.from_any(query)
1522
+
1523
+ async def get_metagraph_info(
1524
+ self, netuid: int, block_hash: Optional[str] = None
1525
+ ) -> Optional[MetagraphInfo]:
1526
+ query = await self.query_runtime_api(
1527
+ runtime_api="SubnetInfoRuntimeApi",
1528
+ method="get_metagraph",
1529
+ params=[netuid],
1530
+ block_hash=block_hash,
1531
+ )
1532
+
1533
+ if query is None:
1534
+ return None
1535
+
1536
+ return MetagraphInfo.from_any(query)
1537
+
1538
+ async def get_all_metagraphs_info(
1539
+ self, block_hash: Optional[str] = None
1540
+ ) -> list[MetagraphInfo]:
1541
+ query = await self.query_runtime_api(
1542
+ runtime_api="SubnetInfoRuntimeApi",
1543
+ method="get_all_metagraphs",
1544
+ params=[],
1545
+ block_hash=block_hash,
1546
+ )
1547
+
1548
+ return MetagraphInfo.list_from_any(query)
1549
+
1550
+ async def multi_get_stake_for_coldkey_and_hotkey_on_netuid(
1551
+ self,
1552
+ hotkey_ss58s: list[str],
1553
+ coldkey_ss58: str,
1554
+ netuids: list[int],
1555
+ block_hash: Optional[str] = None,
1556
+ ) -> dict[str, dict[int, "Balance"]]:
1557
+ """
1558
+ Queries the stake for multiple hotkey - coldkey - netuid pairings.
1559
+
1560
+ :param hotkey_ss58s: list of hotkey ss58 addresses
1561
+ :param coldkey_ss58: a single coldkey ss58 address
1562
+ :param netuids: list of netuids
1563
+ :param block_hash: hash of the blockchain block, if any
1564
+
1565
+ :return:
1566
+ {
1567
+ hotkey_ss58_1: {
1568
+ netuid_1: netuid1_stake,
1569
+ netuid_2: netuid2_stake,
1570
+ ...
1571
+ },
1572
+ hotkey_ss58_2: {
1573
+ netuid_1: netuid1_stake,
1574
+ netuid_2: netuid2_stake,
1575
+ ...
1576
+ },
1577
+ ...
1578
+ }
1579
+
1580
+ """
1581
+ calls = [
1582
+ (
1583
+ await self.substrate.create_storage_key(
1584
+ "MeshtensorModule",
1585
+ "Alpha",
1586
+ [hk_ss58, coldkey_ss58, netuid],
1587
+ block_hash=block_hash,
1588
+ )
1589
+ )
1590
+ for hk_ss58 in hotkey_ss58s
1591
+ for netuid in netuids
1592
+ ]
1593
+ batch_call = await self.substrate.query_multi(calls, block_hash=block_hash)
1594
+ results: dict[str, dict[int, "Balance"]] = {
1595
+ hk_ss58: {} for hk_ss58 in hotkey_ss58s
1596
+ }
1597
+ for idx, (_, val) in enumerate(batch_call):
1598
+ hotkey_idx = idx // len(netuids)
1599
+ netuid_idx = idx % len(netuids)
1600
+ hotkey_ss58 = hotkey_ss58s[hotkey_idx]
1601
+ netuid = netuids[netuid_idx]
1602
+ value = (
1603
+ Balance.from_meshlet(val).set_unit(netuid)
1604
+ if val is not None
1605
+ else Balance(0).set_unit(netuid)
1606
+ )
1607
+ results[hotkey_ss58][netuid] = value
1608
+ return results
1609
+
1610
+ async def get_stake_for_coldkeys(
1611
+ self, coldkey_ss58_list: list[str], block_hash: Optional[str] = None
1612
+ ) -> Optional[dict[str, list[StakeInfo]]]:
1613
+ """
1614
+ Retrieves stake information for a list of coldkeys. This function aggregates stake data for multiple
1615
+ accounts, providing a collective view of their stakes and delegations.
1616
+
1617
+ :param coldkey_ss58_list: A list of SS58 addresses of the accounts' coldkeys.
1618
+ :param block_hash: The blockchain block number for the query.
1619
+
1620
+ :return: A dictionary mapping each coldkey to a list of its StakeInfo objects.
1621
+
1622
+ This function is useful for analyzing the stake distribution and delegation patterns of multiple
1623
+ accounts simultaneously, offering a broader perspective on network participation and investment strategies.
1624
+ """
1625
+ batch_size = 60
1626
+
1627
+ tasks = []
1628
+ for i in range(0, len(coldkey_ss58_list), batch_size):
1629
+ ss58_chunk = coldkey_ss58_list[i : i + batch_size]
1630
+ tasks.append(
1631
+ self.query_runtime_api(
1632
+ runtime_api="StakeInfoRuntimeApi",
1633
+ method="get_stake_info_for_coldkeys",
1634
+ params=[ss58_chunk],
1635
+ block_hash=block_hash,
1636
+ )
1637
+ )
1638
+ results = await asyncio.gather(*tasks)
1639
+ stake_info_map = {}
1640
+ for result in results:
1641
+ if result is None:
1642
+ continue
1643
+ for coldkey_bytes, stake_info_list in result:
1644
+ coldkey_ss58 = decode_account_id(coldkey_bytes)
1645
+ stake_info_map[coldkey_ss58] = StakeInfo.list_from_any(stake_info_list)
1646
+
1647
+ return stake_info_map if stake_info_map else None
1648
+
1649
+ async def all_subnets(self, block_hash: Optional[str] = None) -> list[DynamicInfo]:
1650
+ result, prices = await asyncio.gather(
1651
+ self.query_runtime_api(
1652
+ "SubnetInfoRuntimeApi",
1653
+ "get_all_dynamic_info",
1654
+ block_hash=block_hash,
1655
+ ),
1656
+ self.get_subnet_prices(block_hash=block_hash, page_size=129),
1657
+ )
1658
+ sns: list[DynamicInfo] = DynamicInfo.list_from_any(result)
1659
+ for sn in sns:
1660
+ if sn.netuid == 0:
1661
+ sn.price = Balance.from_tao(1.0)
1662
+ else:
1663
+ try:
1664
+ sn.price = prices[sn.netuid]
1665
+ except KeyError:
1666
+ sn.price = sn.tao_in / sn.alpha_in
1667
+ return sns
1668
+
1669
+ async def subnet(
1670
+ self, netuid: int, block_hash: Optional[str] = None
1671
+ ) -> "DynamicInfo":
1672
+ result, price = await asyncio.gather(
1673
+ self.query_runtime_api(
1674
+ "SubnetInfoRuntimeApi",
1675
+ "get_dynamic_info",
1676
+ params=[netuid],
1677
+ block_hash=block_hash,
1678
+ ),
1679
+ self.get_subnet_price(netuid=netuid, block_hash=block_hash),
1680
+ )
1681
+ if not result:
1682
+ raise ValueError(f"Subnet {netuid} not found")
1683
+ subnet_ = DynamicInfo.from_any(result)
1684
+ subnet_.price = price if netuid != 0 else Balance.from_tao(1.0)
1685
+ return subnet_
1686
+
1687
+ async def get_owned_hotkeys(
1688
+ self,
1689
+ coldkey_ss58: str,
1690
+ block_hash: Optional[str] = None,
1691
+ reuse_block: bool = False,
1692
+ ) -> list[str]:
1693
+ """
1694
+ Retrieves all hotkeys owned by a specific coldkey address.
1695
+
1696
+ :param coldkey_ss58: The SS58 address of the coldkey to query.
1697
+ :param block_hash: The hash of the blockchain block number for the query.
1698
+ :param reuse_block: Whether to reuse the last-used blockchain block hash.
1699
+
1700
+ :return: A list of hotkey SS58 addresses owned by the coldkey.
1701
+ """
1702
+ owned_hotkeys = await self.query(
1703
+ module="MeshtensorModule",
1704
+ storage_function="OwnedHotkeys",
1705
+ params=[coldkey_ss58],
1706
+ block_hash=block_hash,
1707
+ reuse_block_hash=reuse_block,
1708
+ )
1709
+
1710
+ return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []]
1711
+
1712
+ async def get_extrinsic_fee(
1713
+ self, call: GenericCall, keypair: Keypair, proxy: Optional[str] = None
1714
+ ) -> Balance:
1715
+ """
1716
+ Determines the fee for the extrinsic call.
1717
+ Args:
1718
+ call: Created extrinsic call
1719
+ keypair: The keypair that would sign the extrinsic (usually you would just want to use the *pub for this)
1720
+ proxy: Optional proxy for the extrinsic call
1721
+
1722
+ Returns:
1723
+ Balance object representing the fee for this extrinsic.
1724
+ """
1725
+ if proxy is not None:
1726
+ call = await self.substrate.compose_call(
1727
+ "Proxy",
1728
+ "proxy",
1729
+ {"real": proxy, "call": call, "force_proxy_type": None},
1730
+ )
1731
+ fee_dict = await self.substrate.get_payment_info(call, keypair)
1732
+ return Balance.from_meshlet(fee_dict["partial_fee"])
1733
+
1734
+ async def sim_swap(
1735
+ self,
1736
+ origin_netuid: int,
1737
+ destination_netuid: int,
1738
+ amount: int,
1739
+ block_hash: Optional[str] = None,
1740
+ ) -> SimSwapResult:
1741
+ """
1742
+ Hits the SimSwap Runtime API to calculate the fee and result for a given transaction. This should be used
1743
+ instead of get_stake_fee for staking fee calculations. The SimSwapResult contains the staking fees and expected
1744
+ returned amounts of a given transaction. This does not include the transaction (extrinsic) fee.
1745
+
1746
+ Args:
1747
+ origin_netuid: Netuid of the source subnet (0 if new stake)
1748
+ destination_netuid: Netuid of the destination subnet
1749
+ amount: Amount to transfer in Rao
1750
+ block_hash: The hash of the blockchain block number for the query.
1751
+
1752
+ Returns:
1753
+ SimSwapResult object representing the result
1754
+ """
1755
+ block_hash = block_hash or await self.substrate.get_chain_head()
1756
+ if origin_netuid > 0 and destination_netuid > 0:
1757
+ # for cross-subnet moves where neither origin nor destination is root
1758
+ intermediate_result_, sn_price = await asyncio.gather(
1759
+ self.query_runtime_api(
1760
+ "SwapRuntimeApi",
1761
+ "sim_swap_alpha_for_tao",
1762
+ params={"netuid": origin_netuid, "alpha": amount},
1763
+ block_hash=block_hash,
1764
+ ),
1765
+ self.get_subnet_price(origin_netuid, block_hash=block_hash),
1766
+ )
1767
+ intermediate_result = SimSwapResult.from_dict(
1768
+ intermediate_result_, origin_netuid
1769
+ )
1770
+ result = SimSwapResult.from_dict(
1771
+ await self.query_runtime_api(
1772
+ "SwapRuntimeApi",
1773
+ "sim_swap_tao_for_alpha",
1774
+ params={
1775
+ "netuid": destination_netuid,
1776
+ "mesh": intermediate_result.tao_amount.meshlet,
1777
+ },
1778
+ block_hash=block_hash,
1779
+ ),
1780
+ destination_netuid,
1781
+ )
1782
+ secondary_fee = (result.tao_fee / sn_price.tao).set_unit(origin_netuid)
1783
+ result.alpha_fee = result.alpha_fee + secondary_fee
1784
+ return result
1785
+ elif origin_netuid > 0:
1786
+ # dynamic to tao
1787
+ return SimSwapResult.from_dict(
1788
+ await self.query_runtime_api(
1789
+ "SwapRuntimeApi",
1790
+ "sim_swap_alpha_for_tao",
1791
+ params={"netuid": origin_netuid, "alpha": amount},
1792
+ block_hash=block_hash,
1793
+ ),
1794
+ origin_netuid,
1795
+ )
1796
+ else:
1797
+ # mesh to dynamic or unstaked to staked mesh (SN0)
1798
+ return SimSwapResult.from_dict(
1799
+ await self.query_runtime_api(
1800
+ "SwapRuntimeApi",
1801
+ "sim_swap_tao_for_alpha",
1802
+ params={"netuid": destination_netuid, "mesh": amount},
1803
+ block_hash=block_hash,
1804
+ ),
1805
+ destination_netuid,
1806
+ )
1807
+
1808
+ async def get_scheduled_coldkey_swap(
1809
+ self,
1810
+ block_hash: Optional[str] = None,
1811
+ reuse_block: bool = False,
1812
+ ) -> Optional[list[str]]:
1813
+ """
1814
+ Queries the chain to fetch the list of coldkeys that are scheduled for a swap.
1815
+
1816
+ :param block_hash: Block hash at which to perform query.
1817
+ :param reuse_block: Whether to reuse the last-used block hash.
1818
+
1819
+ :return: A list of SS58 addresses of the coldkeys that are scheduled for a coldkey swap.
1820
+ """
1821
+ result = await self.substrate.query_map(
1822
+ module="MeshtensorModule",
1823
+ storage_function="ColdkeySwapScheduled",
1824
+ block_hash=block_hash,
1825
+ reuse_block_hash=reuse_block,
1826
+ )
1827
+
1828
+ keys_pending_swap = []
1829
+ async for ss58, _ in result:
1830
+ keys_pending_swap.append(decode_account_id(ss58))
1831
+ return keys_pending_swap
1832
+
1833
+ async def get_crowdloans(
1834
+ self, block_hash: Optional[str] = None
1835
+ ) -> list[CrowdloanData]:
1836
+ """Retrieves all crowdloans from the network.
1837
+
1838
+ Args:
1839
+ block_hash (Optional[str]): The blockchain block hash at which to perform the query.
1840
+
1841
+ Returns:
1842
+ dict[int, CrowdloanData]: A dictionary mapping crowdloan IDs to CrowdloanData objects
1843
+ containing details such as creator, deposit, cap, raised amount, and finalization status.
1844
+
1845
+ This function fetches information about all crowdloans
1846
+ """
1847
+ crowdloans_data = await self.substrate.query_map(
1848
+ module="Crowdloan",
1849
+ storage_function="Crowdloans",
1850
+ block_hash=block_hash,
1851
+ fully_exhaust=True,
1852
+ )
1853
+ crowdloans = {}
1854
+ async for fund_id, fund_info in crowdloans_data:
1855
+ decoded_call = await self._decode_inline_call(
1856
+ fund_info["call"],
1857
+ block_hash=block_hash,
1858
+ )
1859
+ info_dict = dict(fund_info.value)
1860
+ info_dict["call_details"] = decoded_call
1861
+ crowdloans[fund_id] = CrowdloanData.from_any(info_dict)
1862
+
1863
+ return crowdloans
1864
+
1865
+ async def get_single_crowdloan(
1866
+ self,
1867
+ crowdloan_id: int,
1868
+ block_hash: Optional[str] = None,
1869
+ ) -> Optional[CrowdloanData]:
1870
+ """Retrieves detailed information about a specific crowdloan.
1871
+
1872
+ Args:
1873
+ crowdloan_id (int): The unique identifier of the crowdloan to retrieve.
1874
+ block_hash (Optional[str]): The blockchain block hash at which to perform the query.
1875
+
1876
+ Returns:
1877
+ Optional[CrowdloanData]: A CrowdloanData object containing the crowdloan's details if found,
1878
+ None if the crowdloan does not exist.
1879
+
1880
+ The returned data includes crowdloan details such as funding targets,
1881
+ contribution minimums, timeline, and current funding status
1882
+ """
1883
+ crowdloan_info = await self.query(
1884
+ module="Crowdloan",
1885
+ storage_function="Crowdloans",
1886
+ params=[crowdloan_id],
1887
+ block_hash=block_hash,
1888
+ )
1889
+ if crowdloan_info:
1890
+ decoded_call = await self._decode_inline_call(
1891
+ crowdloan_info.get("call"),
1892
+ block_hash=block_hash,
1893
+ )
1894
+ crowdloan_info["call_details"] = decoded_call
1895
+ return CrowdloanData.from_any(crowdloan_info)
1896
+ return None
1897
+
1898
+ async def get_crowdloan_contribution(
1899
+ self,
1900
+ crowdloan_id: int,
1901
+ contributor: str,
1902
+ block_hash: Optional[str] = None,
1903
+ ) -> Optional[Balance]:
1904
+ """Retrieves a user's contribution to a specific crowdloan.
1905
+
1906
+ Args:
1907
+ crowdloan_id (int): The ID of the crowdloan.
1908
+ contributor (str): The SS58 address of the contributor.
1909
+ block_hash (Optional[str]): The blockchain block hash at which to perform the query.
1910
+
1911
+ Returns:
1912
+ Optional[Balance]: The contribution amount as a Balance object if found, None otherwise.
1913
+
1914
+ This function queries the Contributions storage to find the amount a specific address
1915
+ has contributed to a given crowdloan.
1916
+ """
1917
+ contribution = await self.query(
1918
+ module="Crowdloan",
1919
+ storage_function="Contributions",
1920
+ params=[crowdloan_id, contributor],
1921
+ block_hash=block_hash,
1922
+ )
1923
+
1924
+ if contribution:
1925
+ return Balance.from_meshlet(contribution)
1926
+ return None
1927
+
1928
+ async def get_crowdloan_contributors(
1929
+ self,
1930
+ crowdloan_id: int,
1931
+ block_hash: Optional[str] = None,
1932
+ ) -> dict[str, Balance]:
1933
+ """Retrieves all contributors and their contributions for a specific crowdloan.
1934
+
1935
+ Args:
1936
+ crowdloan_id (int): The ID of the crowdloan.
1937
+ block_hash (Optional[str]): The blockchain block hash at which to perform the query.
1938
+
1939
+ Returns:
1940
+ dict[str, Balance]: A dictionary mapping contributor SS58 addresses to their
1941
+ contribution amounts as Balance objects.
1942
+
1943
+ This function queries the Contributions storage map with the crowdloan_id as the first key
1944
+ to retrieve all contributors and their contribution amounts.
1945
+ """
1946
+ contributors_data = await self.substrate.query_map(
1947
+ module="Crowdloan",
1948
+ storage_function="Contributions",
1949
+ params=[crowdloan_id],
1950
+ block_hash=block_hash,
1951
+ fully_exhaust=True,
1952
+ )
1953
+
1954
+ contributor_contributions = {}
1955
+ async for contributor_key, contribution_amount in contributors_data:
1956
+ try:
1957
+ contributor_address = decode_account_id(contributor_key[0])
1958
+ contribution_balance = Balance.from_meshlet(contribution_amount.value)
1959
+ contributor_contributions[contributor_address] = contribution_balance
1960
+ except Exception:
1961
+ continue
1962
+
1963
+ return contributor_contributions
1964
+
1965
+ async def get_coldkey_swap_schedule_duration(
1966
+ self,
1967
+ block_hash: Optional[str] = None,
1968
+ reuse_block: bool = False,
1969
+ ) -> int:
1970
+ """
1971
+ Retrieves the duration (in blocks) required for a coldkey swap to be executed.
1972
+
1973
+ Args:
1974
+ block_hash: The hash of the blockchain block number for the query.
1975
+ reuse_block: Whether to reuse the last-used blockchain block hash.
1976
+
1977
+ Returns:
1978
+ int: The number of blocks required for the coldkey swap schedule duration.
1979
+ """
1980
+ result = await self.query(
1981
+ module="MeshtensorModule",
1982
+ storage_function="ColdkeySwapScheduleDuration",
1983
+ params=[],
1984
+ block_hash=block_hash,
1985
+ reuse_block_hash=reuse_block,
1986
+ )
1987
+
1988
+ return result
1989
+
1990
+ async def get_coldkey_claim_type(
1991
+ self,
1992
+ coldkey_ss58: str,
1993
+ block_hash: Optional[str] = None,
1994
+ reuse_block: bool = False,
1995
+ ) -> dict:
1996
+ """
1997
+ Retrieves the root claim type for a specific coldkey.
1998
+
1999
+ Root claim types control how staking emissions are handled on the ROOT network (subnet 0):
2000
+ - "Swap": Future Root Alpha Emissions are swapped to MESH at claim time and added to your root stake
2001
+ - "Keep": Future Root Alpha Emissions are kept as Alpha
2002
+ - "KeepSubnets": Specific subnets kept as Alpha, rest swapped to MESH
2003
+
2004
+ Args:
2005
+ coldkey_ss58: The SS58 address of the coldkey to query.
2006
+ block_hash: The hash of the blockchain block number for the query.
2007
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2008
+
2009
+ Returns:
2010
+ dict: Claim type information in one of these formats:
2011
+ - {"type": "Swap"}
2012
+ - {"type": "Keep"}
2013
+ - {"type": "KeepSubnets", "subnets": [1, 5, 10, ...]}
2014
+ """
2015
+ result = await self.query(
2016
+ module="MeshtensorModule",
2017
+ storage_function="RootClaimType",
2018
+ params=[coldkey_ss58],
2019
+ block_hash=block_hash,
2020
+ reuse_block_hash=reuse_block,
2021
+ )
2022
+
2023
+ if result is None:
2024
+ return {"type": "Swap"}
2025
+
2026
+ claim_type_key = next(iter(result.keys()))
2027
+
2028
+ if claim_type_key == "KeepSubnets":
2029
+ subnets_data = result["KeepSubnets"]["subnets"]
2030
+ subnet_list = sorted([subnet for subnet in subnets_data[0]])
2031
+ return {"type": "KeepSubnets", "subnets": subnet_list}
2032
+ else:
2033
+ return {"type": claim_type_key}
2034
+
2035
+ async def get_all_coldkeys_claim_type(
2036
+ self,
2037
+ block_hash: Optional[str] = None,
2038
+ reuse_block: bool = False,
2039
+ ) -> dict[str, dict]:
2040
+ """
2041
+ Retrieves all root claim types for all coldkeys in the network.
2042
+
2043
+ Args:
2044
+ block_hash: The hash of the blockchain block number for the query.
2045
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2046
+
2047
+ Returns:
2048
+ dict[str, dict]: Mapping of coldkey SS58 addresses to claim type dicts
2049
+ """
2050
+ result = await self.substrate.query_map(
2051
+ module="MeshtensorModule",
2052
+ storage_function="RootClaimType",
2053
+ params=[],
2054
+ block_hash=block_hash,
2055
+ reuse_block_hash=reuse_block,
2056
+ )
2057
+
2058
+ root_claim_types = {}
2059
+ async for coldkey, claim_type_data in result:
2060
+ coldkey_ss58 = decode_account_id(coldkey[0])
2061
+
2062
+ claim_type_key = next(iter(claim_type_data.value.keys()))
2063
+
2064
+ if claim_type_key == "KeepSubnets":
2065
+ subnets_data = claim_type_data.value["KeepSubnets"]["subnets"]
2066
+ subnet_list = sorted([subnet for subnet in subnets_data[0]])
2067
+ root_claim_types[coldkey_ss58] = {
2068
+ "type": "KeepSubnets",
2069
+ "subnets": subnet_list,
2070
+ }
2071
+ else:
2072
+ root_claim_types[coldkey_ss58] = {"type": claim_type_key}
2073
+
2074
+ return root_claim_types
2075
+
2076
+ async def get_staking_hotkeys(
2077
+ self,
2078
+ coldkey_ss58: str,
2079
+ block_hash: Optional[str] = None,
2080
+ reuse_block: bool = False,
2081
+ ) -> list[str]:
2082
+ """Retrieves all hotkeys that a coldkey is staking to.
2083
+
2084
+ Args:
2085
+ coldkey_ss58: The SS58 address of the coldkey.
2086
+ block_hash: The hash of the blockchain block for the query.
2087
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2088
+
2089
+ Returns:
2090
+ list[str]: A list of hotkey SS58 addresses that the coldkey has staked to.
2091
+ """
2092
+ result = await self.query(
2093
+ module="MeshtensorModule",
2094
+ storage_function="StakingHotkeys",
2095
+ params=[coldkey_ss58],
2096
+ block_hash=block_hash,
2097
+ reuse_block_hash=reuse_block,
2098
+ )
2099
+ staked_hotkeys = [decode_account_id(hotkey) for hotkey in result]
2100
+ return staked_hotkeys
2101
+
2102
+ async def get_claimed_amount(
2103
+ self,
2104
+ coldkey_ss58: str,
2105
+ hotkey_ss58: str,
2106
+ netuid: int,
2107
+ block_hash: Optional[str] = None,
2108
+ reuse_block: bool = False,
2109
+ ) -> Balance:
2110
+ """Retrieves the root claimed Alpha shares for coldkey from hotkey in provided subnet.
2111
+
2112
+ Args:
2113
+ coldkey_ss58: The SS58 address of the staker.
2114
+ hotkey_ss58: The SS58 address of the root validator.
2115
+ netuid: The unique identifier of the subnet.
2116
+ block_hash: The blockchain block hash for the query.
2117
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2118
+
2119
+ Returns:
2120
+ Balance: The number of Alpha stake claimed from the root validator.
2121
+ """
2122
+ query = await self.query(
2123
+ module="MeshtensorModule",
2124
+ storage_function="RootClaimed",
2125
+ params=[netuid, hotkey_ss58, coldkey_ss58],
2126
+ block_hash=block_hash,
2127
+ reuse_block_hash=reuse_block,
2128
+ )
2129
+ return Balance.from_meshlet(query).set_unit(netuid=netuid)
2130
+
2131
+ async def get_claimed_amount_all_netuids(
2132
+ self,
2133
+ coldkey_ss58: str,
2134
+ hotkey_ss58: str,
2135
+ block_hash: Optional[str] = None,
2136
+ reuse_block: bool = False,
2137
+ ) -> dict[int, Balance]:
2138
+ """Retrieves the root claimed Alpha shares for coldkey from hotkey in all subnets.
2139
+
2140
+ Args:
2141
+ coldkey_ss58: The SS58 address of the staker.
2142
+ hotkey_ss58: The SS58 address of the root validator.
2143
+ block_hash: The blockchain block hash for the query.
2144
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2145
+
2146
+ Returns:
2147
+ dict[int, Balance]: Dictionary mapping netuid to claimed stake.
2148
+ """
2149
+ query = await self.substrate.query_map(
2150
+ module="MeshtensorModule",
2151
+ storage_function="RootClaimed",
2152
+ params=[hotkey_ss58, coldkey_ss58],
2153
+ block_hash=block_hash,
2154
+ reuse_block_hash=reuse_block,
2155
+ )
2156
+ total_claimed = {}
2157
+ async for netuid, claimed in query:
2158
+ total_claimed[netuid] = Balance.from_meshlet(claimed.value).set_unit(
2159
+ netuid=netuid
2160
+ )
2161
+ return total_claimed
2162
+
2163
+ async def get_claimable_rate_all_netuids(
2164
+ self,
2165
+ hotkey_ss58: str,
2166
+ block_hash: Optional[str] = None,
2167
+ reuse_block: bool = False,
2168
+ ) -> dict[int, float]:
2169
+ """Retrieves all root claimable rates from a given hotkey address for all subnets with this validator.
2170
+
2171
+ Args:
2172
+ hotkey_ss58: The SS58 address of the root validator.
2173
+ block_hash: The blockchain block hash for the query.
2174
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2175
+
2176
+ Returns:
2177
+ dict[int, float]: Dictionary mapping netuid to claimable rate.
2178
+ """
2179
+ query = await self.query(
2180
+ module="MeshtensorModule",
2181
+ storage_function="RootClaimable",
2182
+ params=[hotkey_ss58],
2183
+ block_hash=block_hash,
2184
+ reuse_block_hash=reuse_block,
2185
+ )
2186
+
2187
+ if not query:
2188
+ return {}
2189
+
2190
+ bits_list = next(iter(query))
2191
+ return {bits[0]: fixed_to_float(bits[1], frac_bits=32) for bits in bits_list}
2192
+
2193
+ async def get_claimable_rate_netuid(
2194
+ self,
2195
+ hotkey_ss58: str,
2196
+ netuid: int,
2197
+ block_hash: Optional[str] = None,
2198
+ reuse_block: bool = False,
2199
+ ) -> float:
2200
+ """Retrieves the root claimable rate from a given hotkey address for provided netuid.
2201
+
2202
+ Args:
2203
+ hotkey_ss58: The SS58 address of the root validator.
2204
+ netuid: The unique identifier of the subnet to get the rate.
2205
+ block_hash: The blockchain block hash for the query.
2206
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2207
+
2208
+ Returns:
2209
+ float: The rate of claimable stake from validator's hotkey for provided subnet.
2210
+ """
2211
+ all_rates = await self.get_claimable_rate_all_netuids(
2212
+ hotkey_ss58=hotkey_ss58,
2213
+ block_hash=block_hash,
2214
+ reuse_block=reuse_block,
2215
+ )
2216
+ return all_rates.get(netuid, 0.0)
2217
+
2218
+ async def get_claimable_stake_for_netuid(
2219
+ self,
2220
+ coldkey_ss58: str,
2221
+ hotkey_ss58: str,
2222
+ netuid: int,
2223
+ block_hash: Optional[str] = None,
2224
+ reuse_block: bool = False,
2225
+ ) -> Balance:
2226
+ """Retrieves the root claimable stake for a given coldkey address.
2227
+
2228
+ Args:
2229
+ coldkey_ss58: Delegate's ColdKey SS58 address.
2230
+ hotkey_ss58: The root validator hotkey SS58 address.
2231
+ netuid: Delegate's netuid where stake will be claimed.
2232
+ block_hash: The blockchain block hash for the query.
2233
+ reuse_block: Whether to reuse the last-used blockchain block hash.
2234
+
2235
+ Returns:
2236
+ Balance: Available for claiming root stake.
2237
+
2238
+ Note:
2239
+ After manual claim, claimable (available) stake will be added to subnet stake.
2240
+ """
2241
+ root_stake, root_claimable_rate, root_claimed = await asyncio.gather(
2242
+ self.get_stake_for_coldkey_and_hotkey_on_netuid(
2243
+ coldkey_ss58=coldkey_ss58,
2244
+ hotkey_ss58=hotkey_ss58,
2245
+ netuid=0,
2246
+ block_hash=block_hash,
2247
+ ),
2248
+ self.get_claimable_rate_netuid(
2249
+ hotkey_ss58=hotkey_ss58,
2250
+ netuid=netuid,
2251
+ block_hash=block_hash,
2252
+ reuse_block=reuse_block,
2253
+ ),
2254
+ self.get_claimed_amount(
2255
+ coldkey_ss58=coldkey_ss58,
2256
+ hotkey_ss58=hotkey_ss58,
2257
+ netuid=netuid,
2258
+ block_hash=block_hash,
2259
+ reuse_block=reuse_block,
2260
+ ),
2261
+ )
2262
+
2263
+ root_claimable_stake = (root_claimable_rate * root_stake).set_unit(
2264
+ netuid=netuid
2265
+ )
2266
+ # Return the difference (what's left to claim)
2267
+ return max(
2268
+ root_claimable_stake - root_claimed,
2269
+ Balance.from_meshlet(0).set_unit(netuid=netuid),
2270
+ )
2271
+
2272
+ async def get_claimable_stakes_for_coldkey(
2273
+ self,
2274
+ coldkey_ss58: str,
2275
+ stakes_info: list["StakeInfo"],
2276
+ block_hash: Optional[str] = None,
2277
+ ) -> dict[str, dict[int, "Balance"]]:
2278
+ """Batch query claimable stakes for multiple hotkey-netuid pairs.
2279
+
2280
+ Args:
2281
+ coldkey_ss58: The coldkey SS58 address.
2282
+ stakes_info: List of StakeInfo objects containing stake data.
2283
+ block_hash: Optional block hash for the query.
2284
+
2285
+ Returns:
2286
+ dict[str, dict[int, Balance]]: Mapping of hotkey to netuid to claimable Balance.
2287
+ """
2288
+ if not stakes_info:
2289
+ return {}
2290
+
2291
+ root_stakes: dict[str, Balance] = {}
2292
+ for stake_info in stakes_info:
2293
+ if stake_info.netuid == 0 and stake_info.stake.meshlet > 0:
2294
+ root_stakes[stake_info.hotkey_ss58] = stake_info.stake
2295
+
2296
+ target_pairs = []
2297
+ for s in stakes_info:
2298
+ if s.netuid != 0 and s.stake.meshlet > 0 and s.hotkey_ss58 in root_stakes:
2299
+ pair = (s.hotkey_ss58, s.netuid)
2300
+ target_pairs.append(pair)
2301
+
2302
+ if not target_pairs:
2303
+ return {}
2304
+
2305
+ unique_hotkeys = list(set(h for h, _ in target_pairs))
2306
+ if not unique_hotkeys:
2307
+ return {}
2308
+
2309
+ batch_claimable_calls = []
2310
+ batch_claimed_calls = []
2311
+
2312
+ # Get the claimable rate
2313
+ for hotkey in unique_hotkeys:
2314
+ batch_claimable_calls.append(
2315
+ await self.substrate.create_storage_key(
2316
+ "MeshtensorModule", "RootClaimable", [hotkey], block_hash=block_hash
2317
+ )
2318
+ )
2319
+
2320
+ # Get already claimed
2321
+ claimed_pairs = target_pairs
2322
+ for hotkey, netuid in claimed_pairs:
2323
+ batch_claimed_calls.append(
2324
+ await self.substrate.create_storage_key(
2325
+ "MeshtensorModule",
2326
+ "RootClaimed",
2327
+ [netuid, hotkey, coldkey_ss58],
2328
+ block_hash=block_hash,
2329
+ )
2330
+ )
2331
+
2332
+ batch_claimable, batch_claimed = await asyncio.gather(
2333
+ self.substrate.query_multi(batch_claimable_calls, block_hash=block_hash),
2334
+ self.substrate.query_multi(batch_claimed_calls, block_hash=block_hash),
2335
+ )
2336
+
2337
+ claimable_rates: dict[str, dict[int, float]] = {}
2338
+ claimed_amounts: dict[tuple[str, int], Balance] = {}
2339
+ for idx, (_, result) in enumerate(batch_claimable):
2340
+ hotkey = unique_hotkeys[idx]
2341
+ if result:
2342
+ for netuid, rate in result:
2343
+ if hotkey not in claimable_rates:
2344
+ claimable_rates[hotkey] = {}
2345
+ claimable_rates[hotkey][netuid] = fixed_to_float(rate, frac_bits=32)
2346
+
2347
+ for idx, (_, result) in enumerate(batch_claimed):
2348
+ hotkey, netuid = claimed_pairs[idx]
2349
+ value = result or 0
2350
+ claimed_amounts[(hotkey, netuid)] = Balance.from_meshlet(value).set_unit(netuid)
2351
+
2352
+ # Calculate the claimable stake for each pair
2353
+ results = {}
2354
+ already_claimed: Balance
2355
+ net_claimable: Balance
2356
+ rate: float
2357
+ root_stake: Balance
2358
+ claimable_stake: Balance
2359
+ for hotkey, netuid in target_pairs:
2360
+ root_stake = root_stakes.get(hotkey, Balance(0))
2361
+ rate = claimable_rates.get(hotkey, {}).get(netuid, 0.0)
2362
+ claimable_stake = rate * root_stake
2363
+ already_claimed = claimed_amounts.get((hotkey, netuid), Balance(0))
2364
+ net_claimable = max(claimable_stake - already_claimed, Balance(0))
2365
+ if hotkey not in results:
2366
+ results[hotkey] = {}
2367
+ results[hotkey][netuid] = net_claimable.set_unit(netuid)
2368
+ return results
2369
+
2370
+ async def get_subnet_price(
2371
+ self,
2372
+ netuid: int = None,
2373
+ block_hash: Optional[str] = None,
2374
+ ) -> Balance:
2375
+ """
2376
+ Gets the current Alpha price in MESH for a specific subnet.
2377
+
2378
+ :param netuid: The unique identifier of the subnet.
2379
+ :param block_hash: The hash of the block to retrieve the price from.
2380
+
2381
+ :return: The current Alpha price in MESH units for the specified subnet.
2382
+ """
2383
+ # TODO update this to use the runtime call SwapRuntimeAPI.current_alpha_price
2384
+ current_sqrt_price = await self.query(
2385
+ module="Swap",
2386
+ storage_function="AlphaSqrtPrice",
2387
+ params=[netuid],
2388
+ block_hash=block_hash,
2389
+ )
2390
+
2391
+ current_sqrt_price = fixed_to_float(current_sqrt_price)
2392
+ current_price = current_sqrt_price * current_sqrt_price
2393
+ return Balance.from_meshlet(int(current_price * 1e9))
2394
+
2395
+ async def get_subnet_prices(
2396
+ self, block_hash: Optional[str] = None, page_size: int = 100
2397
+ ) -> dict[int, Balance]:
2398
+ """
2399
+ Gets the current Alpha prices in MESH for all subnets.
2400
+
2401
+ :param block_hash: The hash of the block to retrieve prices from.
2402
+ :param page_size: The page size for batch queries (default: 100).
2403
+
2404
+ :return: A dictionary mapping netuid to the current Alpha price in MESH units.
2405
+ """
2406
+ query = await self.substrate.query_map(
2407
+ module="Swap",
2408
+ storage_function="AlphaSqrtPrice",
2409
+ page_size=page_size,
2410
+ block_hash=block_hash,
2411
+ )
2412
+
2413
+ map_ = {}
2414
+ async for netuid_, current_sqrt_price in query:
2415
+ current_sqrt_price_ = fixed_to_float(current_sqrt_price.value)
2416
+ current_price = current_sqrt_price_**2
2417
+ map_[netuid_] = Balance.from_meshlet(int(current_price * 1e9))
2418
+
2419
+ return map_
2420
+
2421
+ async def get_all_subnet_ema_tao_inflow(
2422
+ self,
2423
+ block_hash: Optional[str] = None,
2424
+ page_size: int = 100,
2425
+ ) -> dict[int, Balance]:
2426
+ """
2427
+ Query EMA MESH inflow for all subnets.
2428
+
2429
+ This represents the exponential moving average of MESH flowing
2430
+ into or out of a subnet. Negative values indicate net outflow.
2431
+
2432
+ Args:
2433
+ block_hash: Optional block hash to query at.
2434
+ page_size: The page size for batch queries (default: 100).
2435
+
2436
+ Returns:
2437
+ Dict mapping netuid -> Balance(EMA MESH inflow).
2438
+ """
2439
+ query = await self.substrate.query_map(
2440
+ module="MeshtensorModule",
2441
+ storage_function="SubnetEmaTaoFlow",
2442
+ page_size=page_size,
2443
+ block_hash=block_hash,
2444
+ )
2445
+ ema_map = {}
2446
+ async for netuid, value in query:
2447
+ if not value:
2448
+ ema_map[netuid] = Balance.from_meshlet(0)
2449
+ else:
2450
+ _, raw_ema_value = value
2451
+ ema_value = int(fixed_to_float(raw_ema_value))
2452
+ ema_map[netuid] = Balance.from_meshlet(ema_value)
2453
+ return ema_map
2454
+
2455
+ async def get_subnet_ema_tao_inflow(
2456
+ self,
2457
+ netuid: int,
2458
+ block_hash: Optional[str] = None,
2459
+ ) -> Balance:
2460
+ """
2461
+ Query EMA MESH inflow for a specific subnet.
2462
+
2463
+ This represents the exponential moving average of MESH flowing
2464
+ into or out of a subnet. Negative values indicate net outflow.
2465
+
2466
+ Args:
2467
+ netuid: The unique identifier of the subnet.
2468
+ block_hash: Optional block hash to query at.
2469
+
2470
+ Returns:
2471
+ Balance(EMA MESH inflow).
2472
+ """
2473
+ value = await self.substrate.query(
2474
+ module="MeshtensorModule",
2475
+ storage_function="SubnetEmaTaoFlow",
2476
+ params=[netuid],
2477
+ block_hash=block_hash,
2478
+ )
2479
+ if not value:
2480
+ return Balance.from_meshlet(0)
2481
+ _, raw_ema_value = value
2482
+ ema_value = int(fixed_to_float(raw_ema_value))
2483
+ return Balance.from_meshlet(ema_value)
2484
+
2485
+ async def get_mev_shield_next_key(
2486
+ self,
2487
+ block_hash: Optional[str] = None,
2488
+ ) -> bytes:
2489
+ """
2490
+ Get the next MEV Shield public key and epoch from chain storage.
2491
+
2492
+ Args:
2493
+ block_hash: Optional block hash to query at.
2494
+
2495
+ Returns:
2496
+ Tuple of (public_key_bytes, epoch) or None if not available.
2497
+ """
2498
+ result = await self.query(
2499
+ module="MevShield",
2500
+ storage_function="NextKey",
2501
+ block_hash=block_hash,
2502
+ )
2503
+ public_key_bytes = bytes(next(iter(result)))
2504
+
2505
+ if len(public_key_bytes) != MEV_SHIELD_PUBLIC_KEY_SIZE:
2506
+ raise ValueError(
2507
+ f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. "
2508
+ f"Expected exactly {MEV_SHIELD_PUBLIC_KEY_SIZE} bytes."
2509
+ )
2510
+
2511
+ return public_key_bytes
2512
+
2513
+ async def get_mev_shield_current_key(
2514
+ self,
2515
+ block_hash: Optional[str] = None,
2516
+ ) -> bytes:
2517
+ """
2518
+ Get the current MEV Shield public key and epoch from chain storage.
2519
+
2520
+ Args:
2521
+ block_hash: Optional block hash to query at.
2522
+
2523
+ Returns:
2524
+ Tuple of (public_key_bytes, epoch) or None if not available.
2525
+ """
2526
+ result = await self.query(
2527
+ module="MevShield",
2528
+ storage_function="CurrentKey",
2529
+ block_hash=block_hash,
2530
+ )
2531
+ public_key_bytes = bytes(next(iter(result)))
2532
+
2533
+ if len(public_key_bytes) != MEV_SHIELD_PUBLIC_KEY_SIZE:
2534
+ raise ValueError(
2535
+ f"Invalid ML-KEM-768 public key size: {len(public_key_bytes)} bytes. "
2536
+ f"Expected exactly {MEV_SHIELD_PUBLIC_KEY_SIZE} bytes."
2537
+ )
2538
+
2539
+ return public_key_bytes
2540
+
2541
+ async def compose_custom_crowdloan_call(
2542
+ self,
2543
+ pallet_name: str,
2544
+ method_name: str,
2545
+ call_params: dict,
2546
+ block_hash: Optional[str] = None,
2547
+ ) -> tuple[Optional[GenericCall], Optional[str]]:
2548
+ """
2549
+ Compose a custom Substrate call.
2550
+
2551
+ Args:
2552
+ pallet_name: Name of the pallet/module
2553
+ method_name: Name of the method/function
2554
+ call_params: Dictionary of call parameters
2555
+ block_hash: Optional block hash for the query
2556
+
2557
+ Returns:
2558
+ Tuple of (GenericCall or None, error_message or None)
2559
+ """
2560
+ try:
2561
+ call = await self.substrate.compose_call(
2562
+ call_module=pallet_name,
2563
+ call_function=method_name,
2564
+ call_params=call_params,
2565
+ block_hash=block_hash,
2566
+ )
2567
+ return call, None
2568
+ except Exception as e:
2569
+ return None, f"Failed to compose call: {str(e)}"
2570
+
2571
+
2572
+ async def best_connection(networks: list[str]):
2573
+ """
2574
+ Basic function to compare the latency of a given list of websocket endpoints
2575
+ Args:
2576
+ networks: list of network URIs
2577
+
2578
+ Returns:
2579
+ {network_name: [end_to_end_latency, single_request_latency, chain_head_request_latency]}
2580
+
2581
+ """
2582
+ results = {}
2583
+ for network in networks:
2584
+ try:
2585
+ t1 = time.monotonic()
2586
+ async with websockets.connect(network) as websocket:
2587
+ pong = await websocket.ping()
2588
+ latency = await pong
2589
+ pt1 = time.monotonic()
2590
+ await websocket.send(
2591
+ "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}"
2592
+ )
2593
+ await websocket.recv()
2594
+ t2 = time.monotonic()
2595
+ results[network] = [t2 - t1, latency, t2 - pt1]
2596
+ except Exception as e:
2597
+ print_error(f"Error attempting network {network}: {e}")
2598
+ return results