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.
- meshtensor_cli/__init__.py +22 -0
- meshtensor_cli/cli.py +10742 -0
- meshtensor_cli/doc_generation_helper.py +4 -0
- meshtensor_cli/src/__init__.py +1085 -0
- meshtensor_cli/src/commands/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/__init__.py +0 -0
- meshtensor_cli/src/commands/axon/axon.py +132 -0
- meshtensor_cli/src/commands/crowd/__init__.py +0 -0
- meshtensor_cli/src/commands/crowd/contribute.py +621 -0
- meshtensor_cli/src/commands/crowd/contributors.py +200 -0
- meshtensor_cli/src/commands/crowd/create.py +783 -0
- meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
- meshtensor_cli/src/commands/crowd/refund.py +233 -0
- meshtensor_cli/src/commands/crowd/update.py +418 -0
- meshtensor_cli/src/commands/crowd/utils.py +124 -0
- meshtensor_cli/src/commands/crowd/view.py +991 -0
- meshtensor_cli/src/commands/governance/__init__.py +0 -0
- meshtensor_cli/src/commands/governance/governance.py +794 -0
- meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
- meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
- meshtensor_cli/src/commands/liquidity/utils.py +202 -0
- meshtensor_cli/src/commands/proxy.py +700 -0
- meshtensor_cli/src/commands/stake/__init__.py +0 -0
- meshtensor_cli/src/commands/stake/add.py +799 -0
- meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
- meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
- meshtensor_cli/src/commands/stake/claim.py +770 -0
- meshtensor_cli/src/commands/stake/list.py +738 -0
- meshtensor_cli/src/commands/stake/move.py +1211 -0
- meshtensor_cli/src/commands/stake/remove.py +1466 -0
- meshtensor_cli/src/commands/stake/wizard.py +323 -0
- meshtensor_cli/src/commands/subnets/__init__.py +0 -0
- meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
- meshtensor_cli/src/commands/subnets/price.py +733 -0
- meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
- meshtensor_cli/src/commands/sudo.py +1294 -0
- meshtensor_cli/src/commands/tc/__init__.py +0 -0
- meshtensor_cli/src/commands/tc/tc.py +190 -0
- meshtensor_cli/src/commands/treasury/__init__.py +0 -0
- meshtensor_cli/src/commands/treasury/treasury.py +194 -0
- meshtensor_cli/src/commands/view.py +354 -0
- meshtensor_cli/src/commands/wallets.py +2311 -0
- meshtensor_cli/src/commands/weights.py +467 -0
- meshtensor_cli/src/meshtensor/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/balances.py +313 -0
- meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
- meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
- meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
- meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
- meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
- meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
- meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
- meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
- meshtensor_cli/src/meshtensor/minigraph.py +254 -0
- meshtensor_cli/src/meshtensor/networking.py +12 -0
- meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
- meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
- meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
- meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
- meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
- meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
- meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
- meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
- meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
- meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
- meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
- meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
- meshtensor_cli/src/meshtensor/utils.py +2007 -0
- meshtensor_cli/version.py +23 -0
- meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
- meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
- meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
- 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
|