meshtensor-cli 9.18.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. meshtensor_cli/__init__.py +22 -0
  2. meshtensor_cli/cli.py +10742 -0
  3. meshtensor_cli/doc_generation_helper.py +4 -0
  4. meshtensor_cli/src/__init__.py +1085 -0
  5. meshtensor_cli/src/commands/__init__.py +0 -0
  6. meshtensor_cli/src/commands/axon/__init__.py +0 -0
  7. meshtensor_cli/src/commands/axon/axon.py +132 -0
  8. meshtensor_cli/src/commands/crowd/__init__.py +0 -0
  9. meshtensor_cli/src/commands/crowd/contribute.py +621 -0
  10. meshtensor_cli/src/commands/crowd/contributors.py +200 -0
  11. meshtensor_cli/src/commands/crowd/create.py +783 -0
  12. meshtensor_cli/src/commands/crowd/dissolve.py +219 -0
  13. meshtensor_cli/src/commands/crowd/refund.py +233 -0
  14. meshtensor_cli/src/commands/crowd/update.py +418 -0
  15. meshtensor_cli/src/commands/crowd/utils.py +124 -0
  16. meshtensor_cli/src/commands/crowd/view.py +991 -0
  17. meshtensor_cli/src/commands/governance/__init__.py +0 -0
  18. meshtensor_cli/src/commands/governance/governance.py +794 -0
  19. meshtensor_cli/src/commands/liquidity/__init__.py +0 -0
  20. meshtensor_cli/src/commands/liquidity/liquidity.py +699 -0
  21. meshtensor_cli/src/commands/liquidity/utils.py +202 -0
  22. meshtensor_cli/src/commands/proxy.py +700 -0
  23. meshtensor_cli/src/commands/stake/__init__.py +0 -0
  24. meshtensor_cli/src/commands/stake/add.py +799 -0
  25. meshtensor_cli/src/commands/stake/auto_staking.py +306 -0
  26. meshtensor_cli/src/commands/stake/children_hotkeys.py +865 -0
  27. meshtensor_cli/src/commands/stake/claim.py +770 -0
  28. meshtensor_cli/src/commands/stake/list.py +738 -0
  29. meshtensor_cli/src/commands/stake/move.py +1211 -0
  30. meshtensor_cli/src/commands/stake/remove.py +1466 -0
  31. meshtensor_cli/src/commands/stake/wizard.py +323 -0
  32. meshtensor_cli/src/commands/subnets/__init__.py +0 -0
  33. meshtensor_cli/src/commands/subnets/mechanisms.py +515 -0
  34. meshtensor_cli/src/commands/subnets/price.py +733 -0
  35. meshtensor_cli/src/commands/subnets/subnets.py +2908 -0
  36. meshtensor_cli/src/commands/sudo.py +1294 -0
  37. meshtensor_cli/src/commands/tc/__init__.py +0 -0
  38. meshtensor_cli/src/commands/tc/tc.py +190 -0
  39. meshtensor_cli/src/commands/treasury/__init__.py +0 -0
  40. meshtensor_cli/src/commands/treasury/treasury.py +194 -0
  41. meshtensor_cli/src/commands/view.py +354 -0
  42. meshtensor_cli/src/commands/wallets.py +2311 -0
  43. meshtensor_cli/src/commands/weights.py +467 -0
  44. meshtensor_cli/src/meshtensor/__init__.py +0 -0
  45. meshtensor_cli/src/meshtensor/balances.py +313 -0
  46. meshtensor_cli/src/meshtensor/chain_data.py +1263 -0
  47. meshtensor_cli/src/meshtensor/extrinsics/__init__.py +0 -0
  48. meshtensor_cli/src/meshtensor/extrinsics/mev_shield.py +174 -0
  49. meshtensor_cli/src/meshtensor/extrinsics/registration.py +1861 -0
  50. meshtensor_cli/src/meshtensor/extrinsics/root.py +550 -0
  51. meshtensor_cli/src/meshtensor/extrinsics/serving.py +255 -0
  52. meshtensor_cli/src/meshtensor/extrinsics/transfer.py +239 -0
  53. meshtensor_cli/src/meshtensor/meshtensor_interface.py +2598 -0
  54. meshtensor_cli/src/meshtensor/minigraph.py +254 -0
  55. meshtensor_cli/src/meshtensor/networking.py +12 -0
  56. meshtensor_cli/src/meshtensor/templates/main-filters.j2 +24 -0
  57. meshtensor_cli/src/meshtensor/templates/main-header.j2 +36 -0
  58. meshtensor_cli/src/meshtensor/templates/neuron-details.j2 +111 -0
  59. meshtensor_cli/src/meshtensor/templates/price-multi.j2 +113 -0
  60. meshtensor_cli/src/meshtensor/templates/price-single.j2 +99 -0
  61. meshtensor_cli/src/meshtensor/templates/subnet-details-header.j2 +49 -0
  62. meshtensor_cli/src/meshtensor/templates/subnet-details.j2 +32 -0
  63. meshtensor_cli/src/meshtensor/templates/subnet-metrics.j2 +57 -0
  64. meshtensor_cli/src/meshtensor/templates/subnets-table.j2 +28 -0
  65. meshtensor_cli/src/meshtensor/templates/table.j2 +267 -0
  66. meshtensor_cli/src/meshtensor/templates/view.css +1058 -0
  67. meshtensor_cli/src/meshtensor/templates/view.j2 +43 -0
  68. meshtensor_cli/src/meshtensor/templates/view.js +1053 -0
  69. meshtensor_cli/src/meshtensor/utils.py +2007 -0
  70. meshtensor_cli/version.py +23 -0
  71. meshtensor_cli-9.18.1.dist-info/METADATA +261 -0
  72. meshtensor_cli-9.18.1.dist-info/RECORD +74 -0
  73. meshtensor_cli-9.18.1.dist-info/WHEEL +4 -0
  74. meshtensor_cli-9.18.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1294 @@
1
+ import asyncio
2
+ import json
3
+ import re
4
+ import sys
5
+ from typing import TYPE_CHECKING, Union, Optional, Type
6
+
7
+ from async_substrate_interface import AsyncExtrinsicReceipt
8
+ from meshtensor_wallet import Wallet
9
+ from rich import box
10
+ from rich.table import Column, Table
11
+ from scalecodec import GenericCall
12
+
13
+ from meshtensor_cli.src import (
14
+ HYPERPARAMS,
15
+ HYPERPARAMS_MODULE,
16
+ HYPERPARAMS_METADATA,
17
+ RootSudoOnly,
18
+ DelegatesDetails,
19
+ COLOR_PALETTE,
20
+ )
21
+ from meshtensor_cli.src.meshtensor.chain_data import decode_account_id
22
+ from meshtensor_cli.src.meshtensor.utils import (
23
+ confirm_action,
24
+ console,
25
+ print_error,
26
+ print_success,
27
+ print_verbose,
28
+ normalize_hyperparameters,
29
+ unlock_key,
30
+ blocks_to_duration,
31
+ json_console,
32
+ string_to_u16,
33
+ string_to_u64,
34
+ get_hotkey_pub_ss58,
35
+ print_extrinsic_id,
36
+ )
37
+
38
+ if TYPE_CHECKING:
39
+ from meshtensor_cli.src.meshtensor.meshtensor_interface import (
40
+ MeshtensorInterface,
41
+ ProposalVoteData,
42
+ )
43
+ from scalecodec.types import GenericMetadataVersioned
44
+
45
+
46
+ # helpers and extrinsics
47
+ DEFAULT_PALLET = "AdminUtils"
48
+
49
+
50
+ def allowed_value(
51
+ param: str, value: Union[str, bool]
52
+ ) -> tuple[bool, Union[str, list[float], float, bool]]:
53
+ """
54
+ Check the allowed values on hyperparameters. Return False if value is out of bounds.
55
+
56
+ Reminder error message ends like: Value is {value} but must be {error_message}. (the second part of return
57
+ statement)
58
+
59
+ Check if value is a boolean, only allow boolean and floats
60
+ """
61
+ try:
62
+ if not isinstance(value, bool):
63
+ if param == "alpha_values":
64
+ # Split the string into individual values
65
+ alpha_low_str, alpha_high_str = value.split(",")
66
+ alpha_high = float(alpha_high_str)
67
+ alpha_low = float(alpha_low_str)
68
+
69
+ # Check alpha_high value
70
+ if alpha_high <= 52428 or alpha_high >= 65535:
71
+ return (
72
+ False,
73
+ f"between 52428 and 65535 for alpha_high (but is {alpha_high})",
74
+ )
75
+
76
+ # Check alpha_low value
77
+ if alpha_low < 0 or alpha_low > 52428:
78
+ return (
79
+ False,
80
+ f"between 0 and 52428 for alpha_low (but is {alpha_low})",
81
+ )
82
+
83
+ return True, [alpha_low, alpha_high]
84
+ except ValueError:
85
+ return False, "a number or a boolean"
86
+
87
+ return True, value
88
+
89
+
90
+ def string_to_bool(val) -> Union[bool, Type[ValueError]]:
91
+ try:
92
+ return {"true": True, "1": True, "0": False, "false": False}[val.lower()]
93
+ except KeyError:
94
+ return ValueError
95
+
96
+
97
+ def search_metadata(
98
+ param_name: str,
99
+ value: Union[str, bool, float, list[float]],
100
+ netuid: int,
101
+ metadata: "GenericMetadataVersioned",
102
+ pallet_name: str = DEFAULT_PALLET,
103
+ ) -> tuple[bool, Optional[dict]]:
104
+ """
105
+ Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used
106
+ as call parameters for setting this hyperparameter.
107
+
108
+ Args:
109
+ param_name: the name of the hyperparameter
110
+ value: the value to set the hyperparameter
111
+ netuid: the specified netuid
112
+ metadata: the meshtensor.substrate.metadata
113
+ pallet_name: the name of the module to use for the query. If not set, the default value is DEFAULT_PALLET
114
+
115
+ Returns:
116
+ (success, dict of call params)
117
+
118
+ """
119
+
120
+ def type_converter_with_retry(type_, val, arg_name):
121
+ try:
122
+ if val is None:
123
+ val = input(
124
+ f"Enter a value for field '{arg_name}' with type '{arg_type_output[type_]}': "
125
+ )
126
+ return arg_types[type_](val)
127
+ except ValueError:
128
+ return type_converter_with_retry(type_, None, arg_name)
129
+
130
+ arg_types = {"bool": string_to_bool, "u16": string_to_u16, "u64": string_to_u64}
131
+ arg_type_output = {"bool": "bool", "u16": "float", "u64": "float"}
132
+
133
+ call_crafter = {"netuid": netuid}
134
+
135
+ pallet = metadata.get_metadata_pallet(pallet_name)
136
+ for call in pallet.calls:
137
+ if call.name == param_name:
138
+ if "netuid" not in [x.name for x in call.args]:
139
+ return False, None
140
+ call_args = [arg for arg in call.args if arg.value["name"] != "netuid"]
141
+ if len(call_args) == 1:
142
+ arg = call_args[0].value
143
+ call_crafter[arg["name"]] = type_converter_with_retry(
144
+ arg["typeName"], value, arg["name"]
145
+ )
146
+ else:
147
+ for arg_ in call_args:
148
+ arg = arg_.value
149
+ call_crafter[arg["name"]] = type_converter_with_retry(
150
+ arg["typeName"], None, arg["name"]
151
+ )
152
+ return True, call_crafter
153
+ else:
154
+ return False, None
155
+
156
+
157
+ def requires_bool(metadata, param_name, pallet: str = DEFAULT_PALLET) -> bool:
158
+ """
159
+ Determines whether a given hyperparam takes a single arg (besides netuid) that is of bool type.
160
+ """
161
+ pallet = metadata.get_metadata_pallet(pallet)
162
+ for call in pallet.calls:
163
+ if call.name == param_name:
164
+ if "netuid" not in [x.name for x in call.args]:
165
+ return False
166
+ call_args = [arg for arg in call.args if arg.value["name"] != "netuid"]
167
+ if len(call_args) != 1:
168
+ return False
169
+ else:
170
+ arg = call_args[0].value
171
+ if arg["typeName"] == "bool":
172
+ return True
173
+ else:
174
+ return False
175
+ raise ValueError(f"{param_name} not found in pallet.")
176
+
177
+
178
+ async def set_mechanism_count_extrinsic(
179
+ meshtensor: "MeshtensorInterface",
180
+ wallet: "Wallet",
181
+ netuid: int,
182
+ proxy: Optional[str],
183
+ mech_count: int,
184
+ wait_for_inclusion: bool = True,
185
+ wait_for_finalization: bool = True,
186
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
187
+ """Sets the number of mechanisms for a subnet via AdminUtils."""
188
+
189
+ unlock_result = unlock_key(wallet)
190
+ if not unlock_result.success:
191
+ return False, unlock_result.message, None
192
+
193
+ substrate = meshtensor.substrate
194
+ call_params = {"netuid": netuid, "mechanism_count": mech_count}
195
+
196
+ with console.status(
197
+ f":satellite: Setting mechanism count to [white]{mech_count}[/white] on "
198
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] ...",
199
+ spinner="earth",
200
+ ):
201
+ call = await substrate.compose_call(
202
+ call_module=DEFAULT_PALLET,
203
+ call_function="sudo_set_mechanism_count",
204
+ call_params=call_params,
205
+ )
206
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
207
+ call,
208
+ wallet,
209
+ wait_for_inclusion=wait_for_inclusion,
210
+ wait_for_finalization=wait_for_finalization,
211
+ proxy=proxy,
212
+ )
213
+
214
+ if not success:
215
+ return False, err_msg, None
216
+
217
+ return True, "", ext_receipt
218
+
219
+
220
+ async def set_mechanism_emission_extrinsic(
221
+ meshtensor: "MeshtensorInterface",
222
+ wallet: "Wallet",
223
+ netuid: int,
224
+ proxy: Optional[str],
225
+ split: list[int],
226
+ wait_for_inclusion: bool = True,
227
+ wait_for_finalization: bool = True,
228
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
229
+ """Sets the emission split for a subnet's mechanisms via AdminUtils."""
230
+
231
+ unlock_result = unlock_key(wallet)
232
+ if not unlock_result.success:
233
+ return False, unlock_result.message, None
234
+
235
+ substrate = meshtensor.substrate
236
+
237
+ with console.status(
238
+ f":satellite: Setting emission split for subnet {netuid}...",
239
+ spinner="earth",
240
+ ):
241
+ call = await substrate.compose_call(
242
+ call_module=DEFAULT_PALLET,
243
+ call_function="sudo_set_mechanism_emission_split",
244
+ call_params={"netuid": netuid, "maybe_split": split},
245
+ )
246
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
247
+ call,
248
+ wallet,
249
+ wait_for_inclusion=wait_for_inclusion,
250
+ wait_for_finalization=wait_for_finalization,
251
+ proxy=proxy,
252
+ )
253
+
254
+ if not success:
255
+ return False, err_msg, None
256
+
257
+ return True, "", ext_receipt
258
+
259
+
260
+ async def set_hyperparameter_extrinsic(
261
+ meshtensor: "MeshtensorInterface",
262
+ wallet: "Wallet",
263
+ netuid: int,
264
+ proxy: Optional[str],
265
+ parameter: str,
266
+ value: Optional[Union[str, float, list[float]]],
267
+ wait_for_inclusion: bool = False,
268
+ wait_for_finalization: bool = True,
269
+ prompt: bool = True,
270
+ decline: bool = False,
271
+ quiet: bool = False,
272
+ ) -> tuple[bool, str, Optional[str]]:
273
+ """Sets a hyperparameter for a specific subnetwork.
274
+
275
+ :param meshtensor: initialized MeshtensorInterface object
276
+ :param wallet: meshtensor wallet object.
277
+ :param netuid: Subnetwork `uid`.
278
+ :param proxy: Optional proxy to use for this extrinsic submission.
279
+ :param parameter: Hyperparameter name.
280
+ :param value: New hyperparameter value.
281
+ :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
282
+ `False` if the extrinsic fails to enter the block within the timeout.
283
+ :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
284
+ or returns `False` if the extrinsic fails to be finalized within the timeout.
285
+ :param prompt: If set to False, will not prompt the user.
286
+
287
+ :return: tuple including:
288
+ success: `True` if extrinsic was finalized or included in the block. If we did not wait for
289
+ finalization/inclusion, the response is `True`.
290
+ message: error message if the extrinsic failed
291
+ extrinsic_identifier: optional extrinsic identifier if the extrinsic was included
292
+ """
293
+ print_verbose("Confirming subnet owner")
294
+ coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address
295
+ subnet_owner = await meshtensor.query(
296
+ module="MeshtensorModule",
297
+ storage_function="SubnetOwner",
298
+ params=[netuid],
299
+ )
300
+
301
+ if not (ulw := unlock_key(wallet)).success:
302
+ return False, ulw.message, None
303
+
304
+ arbitrary_extrinsic = False
305
+
306
+ extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", RootSudoOnly.FALSE))
307
+ call_params = {"netuid": netuid}
308
+ if not extrinsic:
309
+ arbitrary_extrinsic, call_params = search_metadata(
310
+ parameter, value, netuid, meshtensor.substrate.metadata
311
+ )
312
+ extrinsic = parameter
313
+ if not arbitrary_extrinsic:
314
+ err_msg = "Invalid hyperparameter specified."
315
+ print_error(err_msg)
316
+ return False, err_msg, None
317
+ if sudo_ is RootSudoOnly.TRUE and prompt:
318
+ if not confirm_action(
319
+ "This hyperparam is only settable by root sudo users. If you are not, this will fail. Please confirm",
320
+ decline=decline,
321
+ quiet=quiet,
322
+ ):
323
+ return False, "This hyperparam is only settable by root sudo users", None
324
+
325
+ substrate = meshtensor.substrate
326
+ msg_value = value if not arbitrary_extrinsic else call_params
327
+ pallet = HYPERPARAMS_MODULE.get(parameter) or DEFAULT_PALLET
328
+
329
+ if not arbitrary_extrinsic:
330
+ extrinsic_params = await substrate.get_metadata_call_function(
331
+ module_name=pallet, call_function_name=extrinsic
332
+ )
333
+
334
+ # if input value is a list, iterate through the list and assign values
335
+ if isinstance(value, list):
336
+ # Ensure that there are enough values for all non-netuid parameters
337
+ non_netuid_fields = [
338
+ pn_str
339
+ for param in extrinsic_params["fields"]
340
+ if "netuid" not in (pn_str := str(param["name"]))
341
+ ]
342
+
343
+ if len(value) < len(non_netuid_fields):
344
+ err_msg = "Not enough values provided in the list for all parameters"
345
+ print_error(err_msg)
346
+ return False, err_msg, None
347
+
348
+ call_params.update(
349
+ {name: val for name, val in zip(non_netuid_fields, value)}
350
+ )
351
+
352
+ else:
353
+ if requires_bool(
354
+ substrate.metadata, param_name=extrinsic, pallet=pallet
355
+ ) and isinstance(value, str):
356
+ value = string_to_bool(value)
357
+ value_argument = extrinsic_params["fields"][
358
+ len(extrinsic_params["fields"]) - 1
359
+ ]
360
+ call_params[str(value_argument["name"])] = value
361
+ # create extrinsic call
362
+ call_ = await substrate.compose_call(
363
+ call_module=pallet,
364
+ call_function=extrinsic,
365
+ call_params=call_params,
366
+ )
367
+ if sudo_ is RootSudoOnly.TRUE:
368
+ call = await substrate.compose_call(
369
+ call_module="Sudo", call_function="sudo", call_params={"call": call_}
370
+ )
371
+ elif sudo_ is RootSudoOnly.COMPLICATED:
372
+ if not prompt:
373
+ to_sudo_or_not_to_sudo = True # default to sudo true when no-prompt is set
374
+ else:
375
+ to_sudo_or_not_to_sudo = confirm_action(
376
+ "This hyperparam can be executed as sudo or not. Do you want to execute as sudo [y] or not [n]?",
377
+ decline=decline,
378
+ quiet=quiet,
379
+ )
380
+ if to_sudo_or_not_to_sudo:
381
+ call = await substrate.compose_call(
382
+ call_module="Sudo",
383
+ call_function="sudo",
384
+ call_params={"call": call_},
385
+ )
386
+ else:
387
+ if subnet_owner != coldkey_ss58:
388
+ err_msg = "This wallet doesn't own the specified subnet."
389
+ print_error(err_msg)
390
+ return False, err_msg, None
391
+ call = call_
392
+ else:
393
+ if subnet_owner != coldkey_ss58:
394
+ err_msg = "This wallet doesn't own the specified subnet."
395
+ print_error(err_msg)
396
+ return False, err_msg, None
397
+ call = call_
398
+ with console.status(
399
+ f":satellite: Setting hyperparameter [{COLOR_PALETTE.G.SUBHEAD}]{parameter}[/{COLOR_PALETTE.G.SUBHEAD}]"
400
+ f" to [{COLOR_PALETTE.G.SUBHEAD}]{msg_value}[/{COLOR_PALETTE.G.SUBHEAD}]"
401
+ f" on subnet: [{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] ...",
402
+ spinner="earth",
403
+ ):
404
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
405
+ call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy
406
+ )
407
+ if not success:
408
+ print_error(f"Failed: {err_msg}")
409
+ return False, err_msg, None
410
+ else:
411
+ ext_id = await ext_receipt.get_extrinsic_identifier()
412
+ await print_extrinsic_id(ext_receipt)
413
+ if arbitrary_extrinsic:
414
+ print_success(
415
+ f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]"
416
+ )
417
+ return True, "", ext_id
418
+ # Successful registration, final check for membership
419
+ else:
420
+ print_success(
421
+ f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]"
422
+ )
423
+ return True, "", ext_id
424
+
425
+
426
+ async def _get_senate_members(
427
+ meshtensor: "MeshtensorInterface", block_hash: Optional[str] = None
428
+ ) -> list[str]:
429
+ """
430
+ Gets all members of the senate on the given meshtensor's network
431
+
432
+ :param meshtensor: MeshtensorInterface object to use for the query
433
+
434
+ :return: list of the senate members' ss58 addresses
435
+ """
436
+ senate_members = await meshtensor.query(
437
+ module="SenateMembers",
438
+ storage_function="Members",
439
+ params=None,
440
+ block_hash=block_hash,
441
+ )
442
+ try:
443
+ return [
444
+ decode_account_id(i[x][0]) for i in senate_members for x in range(len(i))
445
+ ]
446
+ except (IndexError, TypeError):
447
+ print_error("Unable to retrieve senate members.")
448
+ return []
449
+
450
+
451
+ async def _get_proposals(
452
+ meshtensor: "MeshtensorInterface", block_hash: str
453
+ ) -> dict[str, tuple[dict, "ProposalVoteData"]]:
454
+ async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]:
455
+ proposal_data = await meshtensor.query(
456
+ module="Triumvirate",
457
+ storage_function="ProposalOf",
458
+ block_hash=block_hash,
459
+ params=[p_hash],
460
+ )
461
+ return proposal_data
462
+
463
+ ph = await meshtensor.query(
464
+ module="Triumvirate",
465
+ storage_function="Proposals",
466
+ params=None,
467
+ block_hash=block_hash,
468
+ )
469
+
470
+ try:
471
+ proposal_hashes: list[str] = [
472
+ f"0x{bytes(ph[0][x][0]).hex()}" for x in range(len(ph[0]))
473
+ ]
474
+ except (IndexError, TypeError):
475
+ print_error("Unable to retrieve proposal vote data")
476
+ return {}
477
+
478
+ call_data_, vote_data_ = await asyncio.gather(
479
+ asyncio.gather(*[get_proposal_call_data(h) for h in proposal_hashes]),
480
+ asyncio.gather(*[meshtensor.get_vote_data(h) for h in proposal_hashes]),
481
+ )
482
+ return {
483
+ proposal_hash: (cd, vd)
484
+ for cd, vd, proposal_hash in zip(call_data_, vote_data_, proposal_hashes)
485
+ }
486
+
487
+
488
+ def display_votes(
489
+ vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails]
490
+ ) -> str:
491
+ vote_list = list()
492
+
493
+ for address in vote_data.ayes:
494
+ vote_list.append(
495
+ "{}: {}".format(
496
+ delegate_info[address].display if address in delegate_info else address,
497
+ "[bold green]Aye[/bold green]",
498
+ )
499
+ )
500
+
501
+ for address in vote_data.nays:
502
+ vote_list.append(
503
+ "{}: {}".format(
504
+ delegate_info[address].display if address in delegate_info else address,
505
+ "[bold red]Nay[/bold red]",
506
+ )
507
+ )
508
+
509
+ return "\n".join(vote_list)
510
+
511
+
512
+ def serialize_vote_data(
513
+ vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails]
514
+ ) -> list[dict[str, bool]]:
515
+ vote_list = {}
516
+ for address in vote_data.ayes:
517
+ f_add = delegate_info[address].display if address in delegate_info else address
518
+ vote_list[f_add] = True
519
+ for address in vote_data.nays:
520
+ f_add = delegate_info[address].display if address in delegate_info else address
521
+ vote_list[f_add] = False
522
+ return vote_list
523
+
524
+
525
+ def format_call_data(call_data: dict) -> str:
526
+ # Extract the module and call details
527
+ module, call_details = next(iter(call_data.items()))
528
+
529
+ # Extract the call function name and arguments
530
+ call_info = call_details[0]
531
+ call_function, call_args = next(iter(call_info.items()))
532
+
533
+ # Format arguments, handle nested/large payloads
534
+ formatted_args = []
535
+ for arg_name, arg_value in call_args.items():
536
+ if isinstance(arg_value, (tuple, list, dict)):
537
+ # For large nested, show abbreviated version
538
+ content_str = str(arg_value)
539
+ if len(content_str) > 20:
540
+ formatted_args.append(f"{arg_name}: ... [{len(content_str)}] ...")
541
+ else:
542
+ formatted_args.append(f"{arg_name}: {arg_value}")
543
+ else:
544
+ formatted_args.append(f"{arg_name}: {arg_value}")
545
+
546
+ # Format the final output string
547
+ args_str = ", ".join(formatted_args)
548
+ return f"{module}.{call_function}({args_str})"
549
+
550
+
551
+ def _validate_proposal_hash(proposal_hash: str) -> bool:
552
+ if proposal_hash[0:2] != "0x" or len(proposal_hash) != 66:
553
+ return False
554
+ else:
555
+ return True
556
+
557
+
558
+ async def _is_senate_member(meshtensor: "MeshtensorInterface", hotkey_ss58: str) -> bool:
559
+ """
560
+ Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Meshtensor senate.
561
+ The senate is a key governance body within the Meshtensor network, responsible for overseeing and
562
+ approving various network operations and proposals.
563
+
564
+ :param meshtensor: MeshtensorInterface object to use for the query
565
+ :param hotkey_ss58: The `SS58` address of the neuron's hotkey.
566
+
567
+ :return: `True` if the neuron is a senate member at the given block, `False` otherwise.
568
+
569
+ This function is crucial for understanding the governance dynamics of the Meshtensor network and for
570
+ identifying the neurons that hold decision-making power within the network.
571
+ """
572
+
573
+ senate_members = await _get_senate_members(meshtensor)
574
+
575
+ if not hasattr(senate_members, "count"):
576
+ return False
577
+
578
+ return senate_members.count(hotkey_ss58) > 0
579
+
580
+
581
+ async def vote_senate_extrinsic(
582
+ meshtensor: "MeshtensorInterface",
583
+ wallet: Wallet,
584
+ proxy: Optional[str],
585
+ proposal_hash: str,
586
+ proposal_idx: int,
587
+ vote: bool,
588
+ wait_for_inclusion: bool = False,
589
+ wait_for_finalization: bool = True,
590
+ prompt: bool = False,
591
+ decline: bool = False,
592
+ quiet: bool = False,
593
+ ) -> bool:
594
+ """Votes ayes or nays on proposals.
595
+
596
+ :param meshtensor: The MeshtensorInterface object to use for the query
597
+ :param wallet: Meshtensor wallet object, with coldkey and hotkey unlocked.
598
+ :param proxy: Optional proxy address to use for the extrinsic submission
599
+ :param proposal_hash: The hash of the proposal for which voting data is requested.
600
+ :param proposal_idx: The index of the proposal to vote.
601
+ :param vote: Whether to vote aye or nay.
602
+ :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns
603
+ `False` if the extrinsic fails to enter the block within the timeout.
604
+ :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
605
+ or returns `False` if the extrinsic fails to be finalized within the timeout.
606
+ :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
607
+
608
+ :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for
609
+ finalization/inclusion, the response is `True`.
610
+ """
611
+
612
+ if prompt:
613
+ # Prompt user for confirmation.
614
+ if not confirm_action(f"Cast a vote of {vote}?", decline=decline, quiet=quiet):
615
+ return False
616
+
617
+ with console.status(":satellite: Casting vote..", spinner="aesthetic"):
618
+ call = await meshtensor.substrate.compose_call(
619
+ call_module="MeshtensorModule",
620
+ call_function="vote",
621
+ call_params={
622
+ "hotkey": get_hotkey_pub_ss58(wallet),
623
+ "proposal": proposal_hash,
624
+ "index": proposal_idx,
625
+ "approve": vote,
626
+ },
627
+ )
628
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
629
+ call, wallet, wait_for_inclusion, wait_for_finalization, proxy=proxy
630
+ )
631
+ if not success:
632
+ print_error(f"Failed: {err_msg}")
633
+ return False
634
+ # Successful vote, final check for data
635
+ else:
636
+ await print_extrinsic_id(ext_receipt)
637
+ if vote_data := await meshtensor.get_vote_data(proposal_hash):
638
+ hotkey_ss58 = get_hotkey_pub_ss58(wallet)
639
+ if (
640
+ vote_data.ayes.count(hotkey_ss58) > 0
641
+ or vote_data.nays.count(hotkey_ss58) > 0
642
+ ):
643
+ print_success("Vote cast.")
644
+ return True
645
+ else:
646
+ # hotkey not found in ayes/nays
647
+ print_error("Unknown error. Couldn't find vote.")
648
+ return False
649
+ else:
650
+ return False
651
+
652
+
653
+ async def set_take_extrinsic(
654
+ meshtensor: "MeshtensorInterface",
655
+ wallet: Wallet,
656
+ delegate_ss58: str,
657
+ take: float = 0.0,
658
+ proxy: Optional[str] = None,
659
+ ) -> tuple[bool, Optional[str]]:
660
+ """
661
+ Set delegate hotkey take
662
+
663
+ :param meshtensor: MeshtensorInterface (initialized)
664
+ :param wallet: The wallet containing the hotkey to be nominated.
665
+ :param delegate_ss58: Hotkey
666
+ :param take: Delegate take on subnet ID
667
+ :param proxy: Optional proxy address to use for the extrinsic submission
668
+
669
+ :return: `True` if the process is successful, `False` otherwise.
670
+
671
+ This function is a key part of the decentralized governance mechanism of Meshtensor, allowing for the
672
+ dynamic selection and participation of validators in the network's consensus process.
673
+ """
674
+
675
+ # Calculate u16 representation of the take
676
+ take_u16 = int(take * 0xFFFF)
677
+
678
+ print_verbose("Checking current take")
679
+ # Check if the new take is greater or lower than existing take or if existing is set
680
+ current_take = await get_current_take(meshtensor, wallet)
681
+ current_take_u16 = int(float(current_take) * 0xFFFF)
682
+
683
+ if take_u16 == current_take_u16:
684
+ console.print("Nothing to do, take hasn't changed")
685
+ return True, None
686
+
687
+ if current_take_u16 < take_u16:
688
+ console.print(
689
+ f"Current take is [{COLOR_PALETTE.P.RATE}]{current_take * 100.0:.2f}%[/{COLOR_PALETTE.P.RATE}]. "
690
+ f"Increasing to [{COLOR_PALETTE.P.RATE}]{take * 100:.2f}%."
691
+ )
692
+ with console.status(
693
+ f":satellite: Sending decrease_take_extrinsic call on [white]{meshtensor}[/white] ..."
694
+ ):
695
+ call = await meshtensor.substrate.compose_call(
696
+ call_module="MeshtensorModule",
697
+ call_function="increase_take",
698
+ call_params={
699
+ "hotkey": delegate_ss58,
700
+ "take": take_u16,
701
+ },
702
+ )
703
+ success, err, ext_receipt = await meshtensor.sign_and_send_extrinsic(
704
+ call, wallet, proxy=proxy
705
+ )
706
+
707
+ else:
708
+ console.print(
709
+ f"Current take is [{COLOR_PALETTE.P.RATE}]{current_take * 100.0:.2f}%[/{COLOR_PALETTE.P.RATE}]. "
710
+ f"Decreasing to [{COLOR_PALETTE.P.RATE}]{take * 100:.2f}%."
711
+ )
712
+ with console.status(
713
+ f":satellite: Sending increase_take_extrinsic call on [white]{meshtensor}[/white] ..."
714
+ ):
715
+ call = await meshtensor.substrate.compose_call(
716
+ call_module="MeshtensorModule",
717
+ call_function="decrease_take",
718
+ call_params={
719
+ "hotkey": delegate_ss58,
720
+ "take": take_u16,
721
+ },
722
+ )
723
+ success, err, ext_receipt = await meshtensor.sign_and_send_extrinsic(
724
+ call, wallet, proxy=proxy
725
+ )
726
+
727
+ if not success:
728
+ print_error(err)
729
+ ext_id = None
730
+ else:
731
+ print_success("Success")
732
+ ext_id = await ext_receipt.get_extrinsic_identifier()
733
+ await print_extrinsic_id(ext_receipt)
734
+ return success, ext_id
735
+
736
+
737
+ # commands
738
+
739
+
740
+ async def sudo_set_hyperparameter(
741
+ wallet: Wallet,
742
+ meshtensor: "MeshtensorInterface",
743
+ netuid: int,
744
+ proxy: Optional[str],
745
+ param_name: str,
746
+ param_value: Optional[str],
747
+ prompt: bool,
748
+ json_output: bool,
749
+ ) -> tuple[bool, str, Optional[str]]:
750
+ """Set subnet hyperparameters."""
751
+ is_allowed_value, value = allowed_value(param_name, param_value)
752
+ if not is_allowed_value:
753
+ err_msg = (
754
+ f"Hyperparameter [dark_orange]{param_name}[/dark_orange] value is not within bounds. "
755
+ f"Value is {param_value} but must be {value}"
756
+ )
757
+ if json_output:
758
+ json_str = json.dumps(
759
+ {"success": False, "err_msg": err_msg, "extrinsic_identifier": None},
760
+ ensure_ascii=True,
761
+ )
762
+ sys.stdout.write(json_str + "\n")
763
+ sys.stdout.flush()
764
+ else:
765
+ print_error(err_msg)
766
+ return False, err_msg, None
767
+ if json_output:
768
+ prompt = False
769
+ success, err_msg, ext_id = await set_hyperparameter_extrinsic(
770
+ meshtensor, wallet, netuid, proxy, param_name, value, prompt=prompt
771
+ )
772
+ if json_output:
773
+ return success, err_msg, ext_id
774
+ if success:
775
+ console.print("\n")
776
+ print_verbose("Fetching hyperparameters")
777
+ await get_hyperparameters(meshtensor, netuid=netuid)
778
+ return success, err_msg, ext_id
779
+
780
+
781
+ def _sanitize_json_string(
782
+ value: Union[str, int, float, bool, None],
783
+ ) -> Union[str, int, float, bool, None]:
784
+ """Sanitize string values for JSON output by removing control characters.
785
+
786
+ Non-string values are returned as-is.
787
+ """
788
+ if isinstance(value, str):
789
+ # Remove all control characters (0x00-0x1F and 0x7F-0x9F) and replace with space
790
+ sanitized = re.sub(r"[\x00-\x1f\x7f-\x9f]", " ", value)
791
+ # Collapse multiple spaces into single space
792
+ sanitized = " ".join(sanitized.split())
793
+ return sanitized
794
+ return value
795
+
796
+
797
+ async def get_hyperparameters(
798
+ meshtensor: "MeshtensorInterface",
799
+ netuid: int,
800
+ json_output: bool = False,
801
+ show_descriptions: bool = True,
802
+ ) -> bool:
803
+ """View hyperparameters of a subnetwork."""
804
+ print_verbose("Fetching hyperparameters")
805
+ try:
806
+ if not await meshtensor.subnet_exists(netuid):
807
+ error_msg = f"Subnet with netuid {netuid} does not exist."
808
+ if json_output:
809
+ json_str = json.dumps({"error": error_msg}, ensure_ascii=True)
810
+ sys.stdout.write(json_str + "\n")
811
+ sys.stdout.flush()
812
+ else:
813
+ print_error(error_msg)
814
+ return False
815
+ subnet, subnet_info = await asyncio.gather(
816
+ meshtensor.get_subnet_hyperparameters(netuid), meshtensor.subnet(netuid)
817
+ )
818
+ if subnet_info is None:
819
+ error_msg = f"Subnet with netuid {netuid} does not exist."
820
+ if json_output:
821
+ json_str = json.dumps({"error": error_msg}, ensure_ascii=True)
822
+ sys.stdout.write(json_str + "\n")
823
+ sys.stdout.flush()
824
+ else:
825
+ print_error(error_msg)
826
+ return False
827
+ except Exception as e:
828
+ if json_output:
829
+ json_str = json.dumps({"error": str(e)}, ensure_ascii=True)
830
+ sys.stdout.write(json_str + "\n")
831
+ sys.stdout.flush()
832
+ else:
833
+ raise
834
+ return False
835
+
836
+ # Determine if we should show extended info (descriptions, ownership)
837
+ show_extended = show_descriptions and not json_output
838
+
839
+ if show_extended:
840
+ table = Table(
841
+ Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER),
842
+ Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE),
843
+ Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL),
844
+ Column("[white]OWNER SETTABLE", style="bright_cyan"),
845
+ Column("[white]DESCRIPTION", style="dim", overflow="fold"),
846
+ title=f"[{COLOR_PALETTE.G.HEADER}]\nSubnet Hyperparameters\n NETUID: "
847
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}"
848
+ f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}"
849
+ f"[/{COLOR_PALETTE.G.SUBHEAD}]"
850
+ f" - Network: [{COLOR_PALETTE.G.SUBHEAD}]{meshtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n",
851
+ show_footer=True,
852
+ width=None,
853
+ pad_edge=False,
854
+ box=box.SIMPLE,
855
+ show_edge=True,
856
+ )
857
+ else:
858
+ table = Table(
859
+ Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER),
860
+ Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE),
861
+ Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL),
862
+ title=f"[{COLOR_PALETTE.G.HEADER}]\nSubnet Hyperparameters\n NETUID: "
863
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}"
864
+ f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}"
865
+ f"[/{COLOR_PALETTE.G.SUBHEAD}]"
866
+ f" - Network: [{COLOR_PALETTE.G.SUBHEAD}]{meshtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n",
867
+ show_footer=True,
868
+ width=None,
869
+ pad_edge=False,
870
+ box=box.SIMPLE,
871
+ show_edge=True,
872
+ )
873
+ dict_out = []
874
+
875
+ normalized_values = normalize_hyperparameters(subnet, json_output=json_output)
876
+ sorted_values = sorted(normalized_values, key=lambda x: x[0])
877
+ for param, value, norm_value in sorted_values:
878
+ if not json_output:
879
+ if show_extended:
880
+ # Get metadata for this hyperparameter
881
+ metadata = HYPERPARAMS_METADATA.get(param, {})
882
+ description = metadata.get("description", "No description available.")
883
+
884
+ # Check actual ownership from HYPERPARAMS
885
+ _, root_sudo = HYPERPARAMS.get(param, ("", RootSudoOnly.FALSE))
886
+ if root_sudo == RootSudoOnly.TRUE:
887
+ owner_settable_str = "[red]No (Root Only)[/red]"
888
+ elif root_sudo == RootSudoOnly.COMPLICATED:
889
+ owner_settable_str = "[yellow]COMPLICATED (Owner/Sudo)[/yellow]"
890
+ else:
891
+ owner_settable_str = "[green]Yes[/green]"
892
+
893
+ # Format description with docs link if available
894
+ docs_link = metadata.get("docs_link", "")
895
+ if docs_link:
896
+ # Use Rich markup to create description with clickable bright blue [link] at the end
897
+ description_with_link = f"{description} [bright_blue underline link=https://{docs_link}]link[/]"
898
+ else:
899
+ description_with_link = description
900
+
901
+ table.add_row(
902
+ " " + param,
903
+ value,
904
+ norm_value,
905
+ owner_settable_str,
906
+ description_with_link,
907
+ )
908
+ else:
909
+ table.add_row(" " + param, value, norm_value)
910
+ else:
911
+ metadata = HYPERPARAMS_METADATA.get(param, {})
912
+ # Sanitize all string fields for JSON output - remove control characters
913
+ description = metadata.get("description", "No description available.")
914
+ side_effects = metadata.get("side_effects", "No side effects documented.")
915
+ docs_link = metadata.get("docs_link", "")
916
+
917
+ # Sanitize all string values to ensure valid JSON output
918
+ dict_out.append(
919
+ {
920
+ "hyperparameter": _sanitize_json_string(str(param)),
921
+ "value": _sanitize_json_string(value),
922
+ "normalized_value": _sanitize_json_string(norm_value),
923
+ "owner_settable": bool(metadata.get("owner_settable", False)),
924
+ "description": _sanitize_json_string(description),
925
+ "side_effects": _sanitize_json_string(side_effects),
926
+ "docs_link": _sanitize_json_string(docs_link),
927
+ }
928
+ )
929
+ if json_output:
930
+ # Use ensure_ascii=True to properly escape all non-ASCII and control characters
931
+ # Write directly to stdout to avoid any Rich Console formatting
932
+ import sys
933
+
934
+ json_str = json.dumps(dict_out, ensure_ascii=True)
935
+ sys.stdout.write(json_str + "\n")
936
+ sys.stdout.flush()
937
+ return True
938
+ else:
939
+ console.print(table)
940
+ if show_extended:
941
+ console.print(
942
+ "\n[dim]💡 Tip: Use [bold]meshcli sudo set --param <name> --value <value>[/bold] to modify hyperparameters."
943
+ )
944
+ console.print(
945
+ "[dim]💡 Tip: Subnet owners can set parameters marked '[green]Yes[/green]'. "
946
+ "Parameters marked '[red]No (Root Only)[/red]' require root sudo access."
947
+ )
948
+ console.print(
949
+ "[dim]💡 Tip: To set custom hyperparameters not in this list, use the exact parameter name from the chain metadata."
950
+ )
951
+ console.print(
952
+ f"[dim] Example: [bold]meshcli sudo set --netuid {netuid} --param custom_param_name --value 123[/bold]"
953
+ )
954
+ console.print(
955
+ "[dim] The parameter name must match exactly as defined in the chain's AdminUtils pallet metadata."
956
+ )
957
+ console.print(
958
+ "[dim]📚 For detailed documentation, visit: [link]https://docs.meshtensor.com[/link]"
959
+ )
960
+ return True
961
+
962
+
963
+ async def get_senate(
964
+ meshtensor: "MeshtensorInterface", json_output: bool = False
965
+ ) -> None:
966
+ """View Meshtensor's senate members"""
967
+ with console.status(
968
+ f":satellite: Syncing with chain: [white]{meshtensor}[/white] ...",
969
+ spinner="aesthetic",
970
+ ) as status:
971
+ print_verbose("Fetching senate members", status)
972
+ senate_members = await _get_senate_members(meshtensor)
973
+
974
+ print_verbose("Fetching member details from Github and on-chain identities")
975
+ delegate_info: dict[
976
+ str, DelegatesDetails
977
+ ] = await meshtensor.get_delegate_identities()
978
+
979
+ table = Table(
980
+ Column(
981
+ "[bold white]NAME",
982
+ style="bright_cyan",
983
+ no_wrap=True,
984
+ ),
985
+ Column(
986
+ "[bold white]ADDRESS",
987
+ style="bright_magenta",
988
+ no_wrap=True,
989
+ ),
990
+ title=f"[underline dark_orange]Senate[/underline dark_orange]\n[dark_orange]Network: {meshtensor.network}\n",
991
+ show_footer=True,
992
+ show_edge=False,
993
+ expand=False,
994
+ border_style="bright_black",
995
+ leading=True,
996
+ )
997
+ dict_output = []
998
+
999
+ for ss58_address in senate_members:
1000
+ member_name = (
1001
+ delegate_info[ss58_address].display
1002
+ if ss58_address in delegate_info
1003
+ else "~"
1004
+ )
1005
+ table.add_row(
1006
+ member_name,
1007
+ ss58_address,
1008
+ )
1009
+ dict_output.append({"name": member_name, "ss58_address": ss58_address})
1010
+ if json_output:
1011
+ json_console.print(json.dumps(dict_output, ensure_ascii=True))
1012
+ return console.print(table)
1013
+
1014
+
1015
+ async def proposals(
1016
+ meshtensor: "MeshtensorInterface", verbose: bool, json_output: bool = False
1017
+ ) -> None:
1018
+ console.print(
1019
+ ":satellite: Syncing with chain: [white]{}[/white] ...".format(
1020
+ meshtensor.network
1021
+ )
1022
+ )
1023
+ block_hash = await meshtensor.substrate.get_chain_head()
1024
+ senate_members, all_proposals, current_block = await asyncio.gather(
1025
+ _get_senate_members(meshtensor, block_hash),
1026
+ _get_proposals(meshtensor, block_hash),
1027
+ meshtensor.substrate.get_block_number(block_hash),
1028
+ )
1029
+
1030
+ registered_delegate_info: dict[
1031
+ str, DelegatesDetails
1032
+ ] = await meshtensor.get_delegate_identities()
1033
+
1034
+ title = (
1035
+ f"[bold #4196D6]Meshtensor Governance Proposals[/bold #4196D6]\n"
1036
+ f"[steel_blue3]Current Block:[/steel_blue3] {current_block}\t"
1037
+ f"[steel_blue3]Network:[/steel_blue3] {meshtensor.network}\n\n"
1038
+ f"[steel_blue3]Active Proposals:[/steel_blue3] {len(all_proposals)}\t"
1039
+ f"[steel_blue3]Senate Size:[/steel_blue3] {len(senate_members)}\n"
1040
+ )
1041
+ table = Table(
1042
+ Column(
1043
+ "[white]HASH",
1044
+ style="light_goldenrod2",
1045
+ no_wrap=True,
1046
+ ),
1047
+ Column("[white]THRESHOLD", style="rgb(42,161,152)"),
1048
+ Column("[white]AYES", style="green"),
1049
+ Column("[white]NAYS", style="red"),
1050
+ Column(
1051
+ "[white]VOTES",
1052
+ style="rgb(50,163,219)",
1053
+ ),
1054
+ Column("[white]END", style="bright_cyan"),
1055
+ Column("[white]CALLDATA", style="dark_sea_green", width=30),
1056
+ title=title,
1057
+ show_footer=True,
1058
+ box=box.SIMPLE_HEAVY,
1059
+ pad_edge=False,
1060
+ width=None,
1061
+ border_style="bright_black",
1062
+ )
1063
+ dict_output = []
1064
+ for hash_, (call_data, vote_data) in all_proposals.items():
1065
+ blocks_remaining = vote_data.end - current_block
1066
+ if blocks_remaining > 0:
1067
+ duration_str = blocks_to_duration(blocks_remaining)
1068
+ vote_end_cell = f"{vote_data.end} [dim](in {duration_str})[/dim]"
1069
+ else:
1070
+ vote_end_cell = f"{vote_data.end} [red](expired)[/red]"
1071
+
1072
+ ayes_threshold = (
1073
+ (len(vote_data.ayes) / vote_data.threshold * 100)
1074
+ if vote_data.threshold > 0
1075
+ else 0
1076
+ )
1077
+ nays_threshold = (
1078
+ (len(vote_data.nays) / vote_data.threshold * 100)
1079
+ if vote_data.threshold > 0
1080
+ else 0
1081
+ )
1082
+ f_call_data = format_call_data(call_data)
1083
+ table.add_row(
1084
+ hash_ if verbose else f"{hash_[:4]}...{hash_[-4:]}",
1085
+ str(vote_data.threshold),
1086
+ f"{len(vote_data.ayes)} ({ayes_threshold:.2f}%)",
1087
+ f"{len(vote_data.nays)} ({nays_threshold:.2f}%)",
1088
+ display_votes(vote_data, registered_delegate_info),
1089
+ vote_end_cell,
1090
+ f_call_data,
1091
+ )
1092
+ dict_output.append(
1093
+ {
1094
+ "hash": hash_,
1095
+ "threshold": vote_data.threshold,
1096
+ "ayes": len(vote_data.ayes),
1097
+ "nays": len(vote_data.nays),
1098
+ "votes": serialize_vote_data(vote_data, registered_delegate_info),
1099
+ "end": vote_data.end,
1100
+ "call_data": f_call_data,
1101
+ }
1102
+ )
1103
+ if json_output:
1104
+ json_console.print(json.dumps(dict_output, ensure_ascii=True))
1105
+ console.print(table)
1106
+ console.print(
1107
+ "\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]"
1108
+ )
1109
+
1110
+
1111
+ async def senate_vote(
1112
+ wallet: Wallet,
1113
+ meshtensor: "MeshtensorInterface",
1114
+ proxy: Optional[str],
1115
+ proposal_hash: str,
1116
+ vote: bool,
1117
+ prompt: bool,
1118
+ ) -> bool:
1119
+ """Vote in Meshtensor's governance protocol proposals"""
1120
+
1121
+ if not proposal_hash:
1122
+ print_error(
1123
+ "Aborting: Proposal hash not specified. View all proposals with the `proposals` command."
1124
+ )
1125
+ return False
1126
+ elif not _validate_proposal_hash(proposal_hash):
1127
+ print_error(
1128
+ "Aborting. Proposal hash is invalid. Proposal hashes should start with '0x' and be 32 bytes long"
1129
+ )
1130
+ return False
1131
+
1132
+ print_verbose(f"Fetching senate status of {wallet.hotkey_str}")
1133
+ hotkey_ss58 = get_hotkey_pub_ss58(wallet)
1134
+ if not await _is_senate_member(meshtensor, hotkey_ss58=hotkey_ss58):
1135
+ print_error(f"Aborting: Hotkey {hotkey_ss58} isn't a senate member.")
1136
+ return False
1137
+
1138
+ # Unlock the wallet.
1139
+ if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success:
1140
+ return False
1141
+
1142
+ console.print(f"Fetching proposals in [dark_orange]network: {meshtensor.network}")
1143
+ vote_data = await meshtensor.get_vote_data(proposal_hash, reuse_block=True)
1144
+ if not vote_data:
1145
+ print_error("Failed: Proposal not found.")
1146
+ return False
1147
+
1148
+ success = await vote_senate_extrinsic(
1149
+ meshtensor=meshtensor,
1150
+ wallet=wallet,
1151
+ proxy=proxy,
1152
+ proposal_hash=proposal_hash,
1153
+ proposal_idx=vote_data.index,
1154
+ vote=vote,
1155
+ wait_for_inclusion=True,
1156
+ wait_for_finalization=False,
1157
+ prompt=prompt,
1158
+ )
1159
+
1160
+ return success
1161
+
1162
+
1163
+ async def get_current_take(meshtensor: "MeshtensorInterface", wallet: Wallet):
1164
+ current_take = await meshtensor.current_take(get_hotkey_pub_ss58(wallet))
1165
+ return current_take
1166
+
1167
+
1168
+ async def display_current_take(meshtensor: "MeshtensorInterface", wallet: Wallet) -> None:
1169
+ current_take = await get_current_take(meshtensor, wallet)
1170
+ console.print(
1171
+ f"Current take is [{COLOR_PALETTE.P.RATE}]{current_take * 100.0:.2f}%"
1172
+ )
1173
+
1174
+
1175
+ async def set_take(
1176
+ wallet: Wallet, meshtensor: "MeshtensorInterface", take: float, proxy: Optional[str]
1177
+ ) -> tuple[bool, Optional[str]]:
1178
+ """Set delegate take."""
1179
+
1180
+ async def _do_set_take() -> tuple[bool, Optional[str]]:
1181
+ if take > 0.18 or take < 0:
1182
+ print_error("ERROR: Take value should not exceed 18% or be below 0%")
1183
+ return False, None
1184
+
1185
+ block_hash = await meshtensor.substrate.get_chain_head()
1186
+ hotkey_ss58 = get_hotkey_pub_ss58(wallet)
1187
+ netuids_registered = await meshtensor.get_netuids_for_hotkey(
1188
+ hotkey_ss58, block_hash=block_hash
1189
+ )
1190
+ if not len(netuids_registered) > 0:
1191
+ print_error(
1192
+ f"Hotkey [{COLOR_PALETTE.G.HK}]{hotkey_ss58}[/{COLOR_PALETTE.G.HK}] is not registered to"
1193
+ f" any subnet. Please register using [{COLOR_PALETTE.G.SUBHEAD}]`meshcli subnets register`"
1194
+ f"[{COLOR_PALETTE.G.SUBHEAD}] and try again."
1195
+ )
1196
+ return False, None
1197
+
1198
+ result: tuple[bool, Optional[str]] = await set_take_extrinsic(
1199
+ meshtensor=meshtensor,
1200
+ wallet=wallet,
1201
+ delegate_ss58=hotkey_ss58,
1202
+ take=take,
1203
+ proxy=proxy,
1204
+ )
1205
+ success, ext_id = result
1206
+
1207
+ if not success:
1208
+ print_error("Could not set the take")
1209
+ return False, None
1210
+ else:
1211
+ new_take = await get_current_take(meshtensor, wallet)
1212
+ console.print(
1213
+ f"New take is [{COLOR_PALETTE.P.RATE}]{new_take * 100.0:.2f}%"
1214
+ )
1215
+ return True, ext_id
1216
+
1217
+ console.print(
1218
+ f"Setting take on [{COLOR_PALETTE.G.LINKS}]network: {meshtensor.network}"
1219
+ )
1220
+
1221
+ if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success:
1222
+ return False, None
1223
+
1224
+ return await _do_set_take()
1225
+
1226
+
1227
+ async def trim(
1228
+ wallet: Wallet,
1229
+ meshtensor: "MeshtensorInterface",
1230
+ netuid: int,
1231
+ proxy: Optional[str],
1232
+ max_n: int,
1233
+ period: int,
1234
+ prompt: bool,
1235
+ decline: bool,
1236
+ quiet: bool,
1237
+ json_output: bool,
1238
+ ) -> bool:
1239
+ """
1240
+ Trims a subnet's UIDs to a specified amount
1241
+ """
1242
+ print_verbose("Confirming subnet owner")
1243
+ subnet_owner = await meshtensor.query(
1244
+ module="MeshtensorModule",
1245
+ storage_function="SubnetOwner",
1246
+ params=[netuid],
1247
+ )
1248
+ # TODO should this check proxy also?
1249
+ if subnet_owner != wallet.coldkeypub.ss58_address:
1250
+ err_msg = "This wallet doesn't own the specified subnet."
1251
+ if json_output:
1252
+ json_console.print_json(data={"success": False, "message": err_msg})
1253
+ else:
1254
+ print_error(err_msg)
1255
+ return False
1256
+ if prompt and not json_output:
1257
+ if not confirm_action(
1258
+ f"You are about to trim UIDs on SN{netuid} to a limit of {max_n}",
1259
+ default=False,
1260
+ decline=decline,
1261
+ quiet=quiet,
1262
+ ):
1263
+ print_error("User aborted.")
1264
+ call = await meshtensor.substrate.compose_call(
1265
+ call_module="AdminUtils",
1266
+ call_function="sudo_trim_to_max_allowed_uids",
1267
+ call_params={"netuid": netuid, "max_n": max_n},
1268
+ )
1269
+ success, err_msg, ext_receipt = await meshtensor.sign_and_send_extrinsic(
1270
+ call=call, wallet=wallet, era={"period": period}, proxy=proxy
1271
+ )
1272
+ if not success:
1273
+ if json_output:
1274
+ json_console.print_json(
1275
+ data={
1276
+ "success": False,
1277
+ "message": err_msg,
1278
+ "extrinsic_identifier": None,
1279
+ }
1280
+ )
1281
+ else:
1282
+ print_error(err_msg)
1283
+ return False
1284
+ else:
1285
+ ext_id = await ext_receipt.get_extrinsic_identifier()
1286
+ msg = f"Successfully trimmed UIDs on SN{netuid} to {max_n}"
1287
+ if json_output:
1288
+ json_console.print_json(
1289
+ data={"success": True, "message": msg, "extrinsic_identifier": ext_id}
1290
+ )
1291
+ else:
1292
+ await print_extrinsic_id(ext_receipt)
1293
+ print_success(f"[dark_sea_green3]{msg}[/dark_sea_green3]")
1294
+ return True