bittensor-cli 9.0.0rc4__py3-none-any.whl → 9.0.2__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.
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING, Union, Optional
3
3
 
4
4
  import typer
5
5
  from bittensor_wallet import Wallet
6
- from bittensor_wallet.errors import KeyFileError
7
6
  from rich import box
8
7
  from rich.table import Column, Table
9
8
  from rich.prompt import Confirm
@@ -17,6 +16,10 @@ from bittensor_cli.src.bittensor.utils import (
17
16
  print_error,
18
17
  print_verbose,
19
18
  normalize_hyperparameters,
19
+ unlock_key,
20
+ blocks_to_duration,
21
+ float_to_u64,
22
+ float_to_u16,
20
23
  )
21
24
 
22
25
  if TYPE_CHECKING:
@@ -69,12 +72,76 @@ def allowed_value(
69
72
  return True, value
70
73
 
71
74
 
75
+ def search_metadata(
76
+ param_name: str, value: Union[str, bool, float, list[float]], netuid: int, metadata
77
+ ) -> tuple[bool, Optional[dict]]:
78
+ """
79
+ Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used
80
+ as call parameters for setting this hyperparameter.
81
+
82
+ Args:
83
+ param_name: the name of the hyperparameter
84
+ value: the value to set the hyperparameter
85
+ netuid: the specified netuid
86
+ metadata: the subtensor.substrate.metadata
87
+
88
+ Returns:
89
+ (success, dict of call params)
90
+
91
+ """
92
+
93
+ def string_to_bool(val) -> bool:
94
+ try:
95
+ return {"true": True, "1": True, "0": False, "false": False}[val.lower()]
96
+ except KeyError:
97
+ return ValueError
98
+
99
+ def type_converter_with_retry(type_, val, arg_name):
100
+ try:
101
+ if val is None:
102
+ val = input(
103
+ f"Enter a value for field '{arg_name}' with type '{arg_type_output[type_]}': "
104
+ )
105
+ return arg_types[type_](val)
106
+ except ValueError:
107
+ return type_converter_with_retry(type_, None, arg_name)
108
+
109
+ arg_types = {"bool": string_to_bool, "u16": float_to_u16, "u64": float_to_u64}
110
+ arg_type_output = {"bool": "bool", "u16": "float", "u64": "float"}
111
+
112
+ call_crafter = {"netuid": netuid}
113
+
114
+ for pallet in metadata.pallets:
115
+ if pallet.name == "AdminUtils":
116
+ for call in pallet.calls:
117
+ if call.name == param_name:
118
+ if "netuid" not in [x.name for x in call.args]:
119
+ return False, None
120
+ call_args = [
121
+ arg for arg in call.args if arg.value["name"] != "netuid"
122
+ ]
123
+ if len(call_args) == 1:
124
+ arg = call_args[0].value
125
+ call_crafter[arg["name"]] = type_converter_with_retry(
126
+ arg["typeName"], value, arg["name"]
127
+ )
128
+ else:
129
+ for arg_ in call_args:
130
+ arg = arg_.value
131
+ call_crafter[arg["name"]] = type_converter_with_retry(
132
+ arg["typeName"], None, arg["name"]
133
+ )
134
+ return True, call_crafter
135
+ else:
136
+ return False, None
137
+
138
+
72
139
  async def set_hyperparameter_extrinsic(
73
140
  subtensor: "SubtensorInterface",
74
141
  wallet: "Wallet",
75
142
  netuid: int,
76
143
  parameter: str,
77
- value: Union[str, bool, float, list[float]],
144
+ value: Optional[Union[str, bool, float, list[float]]],
78
145
  wait_for_inclusion: bool = False,
79
146
  wait_for_finalization: bool = True,
80
147
  ) -> bool:
@@ -106,68 +173,90 @@ async def set_hyperparameter_extrinsic(
106
173
  )
107
174
  return False
108
175
 
109
- try:
110
- wallet.unlock_coldkey()
111
- except KeyFileError:
112
- err_console.print("Error decrypting coldkey (possibly incorrect password)")
176
+ if not unlock_key(wallet).success:
113
177
  return False
114
178
 
115
- extrinsic = HYPERPARAMS.get(parameter)
116
- if extrinsic is None:
117
- err_console.print(":cross_mark: [red]Invalid hyperparameter specified.[/red]")
118
- return False
179
+ arbitrary_extrinsic = False
119
180
 
181
+ extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", False))
182
+ if not extrinsic:
183
+ arbitrary_extrinsic, call_params = search_metadata(
184
+ parameter, value, netuid, subtensor.substrate.metadata
185
+ )
186
+ extrinsic = parameter
187
+ if not arbitrary_extrinsic:
188
+ err_console.print(
189
+ ":cross_mark: [red]Invalid hyperparameter specified.[/red]"
190
+ )
191
+ return False
192
+
193
+ substrate = subtensor.substrate
194
+ msg_value = value if not arbitrary_extrinsic else call_params
120
195
  with console.status(
121
- f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{value}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...",
196
+ f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}"
197
+ f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{msg_value}"
198
+ f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
199
+ f"{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...",
122
200
  spinner="earth",
123
201
  ):
124
- substrate = subtensor.substrate
125
- extrinsic_params = await substrate.get_metadata_call_function(
126
- "AdminUtils", extrinsic
127
- )
128
- call_params: dict[str, Union[str, bool, float]] = {"netuid": netuid}
129
-
130
- # if input value is a list, iterate through the list and assign values
131
- if isinstance(value, list):
132
- # Ensure that there are enough values for all non-netuid parameters
133
- non_netuid_fields = [
134
- param["name"]
135
- for param in extrinsic_params["fields"]
136
- if "netuid" not in param["name"]
137
- ]
138
-
139
- if len(value) < len(non_netuid_fields):
140
- raise ValueError(
141
- "Not enough values provided in the list for all parameters"
142
- )
143
-
144
- call_params.update(
145
- {str(name): val for name, val in zip(non_netuid_fields, value)}
202
+ if not arbitrary_extrinsic:
203
+ extrinsic_params = await substrate.get_metadata_call_function(
204
+ "AdminUtils", extrinsic
146
205
  )
206
+ call_params = {"netuid": netuid}
207
+
208
+ # if input value is a list, iterate through the list and assign values
209
+ if isinstance(value, list):
210
+ # Ensure that there are enough values for all non-netuid parameters
211
+ non_netuid_fields = [
212
+ param["name"]
213
+ for param in extrinsic_params["fields"]
214
+ if "netuid" not in param["name"]
215
+ ]
216
+
217
+ if len(value) < len(non_netuid_fields):
218
+ raise ValueError(
219
+ "Not enough values provided in the list for all parameters"
220
+ )
147
221
 
148
- else:
149
- value_argument = extrinsic_params["fields"][
150
- len(extrinsic_params["fields"]) - 1
151
- ]
152
- call_params[str(value_argument["name"])] = value
222
+ call_params.update(
223
+ {str(name): val for name, val in zip(non_netuid_fields, value)}
224
+ )
225
+
226
+ else:
227
+ value_argument = extrinsic_params["fields"][
228
+ len(extrinsic_params["fields"]) - 1
229
+ ]
230
+ call_params[str(value_argument["name"])] = value
153
231
 
154
232
  # create extrinsic call
155
- call = await substrate.compose_call(
233
+ call_ = await substrate.compose_call(
156
234
  call_module="AdminUtils",
157
235
  call_function=extrinsic,
158
236
  call_params=call_params,
159
237
  )
238
+ if sudo_:
239
+ call = await substrate.compose_call(
240
+ call_module="Sudo", call_function="sudo", call_params={"call": call_}
241
+ )
242
+ else:
243
+ call = call_
160
244
  success, err_msg = await subtensor.sign_and_send_extrinsic(
161
245
  call, wallet, wait_for_inclusion, wait_for_finalization
162
246
  )
163
247
  if not success:
164
248
  err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
165
249
  await asyncio.sleep(0.5)
166
-
250
+ elif arbitrary_extrinsic:
251
+ console.print(
252
+ f":white_heavy_check_mark: "
253
+ f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]"
254
+ )
167
255
  # Successful registration, final check for membership
168
256
  else:
169
257
  console.print(
170
- f":white_heavy_check_mark: [dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]"
258
+ f":white_heavy_check_mark: "
259
+ f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]"
171
260
  )
172
261
  return True
173
262
 
@@ -266,14 +355,22 @@ def format_call_data(call_data: dict) -> str:
266
355
  call_info = call_details[0]
267
356
  call_function, call_args = next(iter(call_info.items()))
268
357
 
269
- # Extract the argument, handling tuple values
270
- formatted_args = ", ".join(
271
- str(arg[0]) if isinstance(arg, tuple) else str(arg)
272
- for arg in call_args.values()
273
- )
358
+ # Format arguments, handle nested/large payloads
359
+ formatted_args = []
360
+ for arg_name, arg_value in call_args.items():
361
+ if isinstance(arg_value, (tuple, list, dict)):
362
+ # For large nested, show abbreviated version
363
+ content_str = str(arg_value)
364
+ if len(content_str) > 20:
365
+ formatted_args.append(f"{arg_name}: ... [{len(content_str)}] ...")
366
+ else:
367
+ formatted_args.append(f"{arg_name}: {arg_value}")
368
+ else:
369
+ formatted_args.append(f"{arg_name}: {arg_value}")
274
370
 
275
371
  # Format the final output string
276
- return f"{call_function}({formatted_args})"
372
+ args_str = ", ".join(formatted_args)
373
+ return f"{module}.{call_function}({args_str})"
277
374
 
278
375
 
279
376
  def _validate_proposal_hash(proposal_hash: str) -> bool:
@@ -458,28 +555,33 @@ async def sudo_set_hyperparameter(
458
555
  subtensor: "SubtensorInterface",
459
556
  netuid: int,
460
557
  param_name: str,
461
- param_value: str,
558
+ param_value: Optional[str],
462
559
  ):
463
560
  """Set subnet hyperparameters."""
464
561
 
465
562
  normalized_value: Union[str, bool]
466
563
  if param_name in [
467
- "network_registration_allowed",
564
+ "registration_allowed",
468
565
  "network_pow_registration_allowed",
469
566
  "commit_reveal_weights_enabled",
470
567
  "liquid_alpha_enabled",
471
568
  ]:
472
569
  normalized_value = param_value.lower() in ["true", "1"]
570
+ elif param_value in ("True", "False"):
571
+ normalized_value = {
572
+ "True": True,
573
+ "False": False,
574
+ }[param_value]
473
575
  else:
474
576
  normalized_value = param_value
475
577
 
476
578
  is_allowed_value, value = allowed_value(param_name, normalized_value)
477
579
  if not is_allowed_value:
478
580
  err_console.print(
479
- f"Hyperparameter {param_name} value is not within bounds. Value is {normalized_value} but must be {value}"
581
+ f"Hyperparameter [dark_orange]{param_name}[/dark_orange] value is not within bounds. "
582
+ f"Value is {normalized_value} but must be {value}"
480
583
  )
481
584
  return
482
-
483
585
  success = await set_hyperparameter_extrinsic(
484
586
  subtensor, wallet, netuid, param_name, value
485
587
  )
@@ -572,24 +674,30 @@ async def get_senate(subtensor: "SubtensorInterface"):
572
674
  return console.print(table)
573
675
 
574
676
 
575
- async def proposals(subtensor: "SubtensorInterface"):
677
+ async def proposals(subtensor: "SubtensorInterface", verbose: bool):
576
678
  console.print(
577
679
  ":satellite: Syncing with chain: [white]{}[/white] ...".format(
578
680
  subtensor.network
579
681
  )
580
682
  )
581
- print_verbose("Fetching senate members & proposals")
582
683
  block_hash = await subtensor.substrate.get_chain_head()
583
- senate_members, all_proposals = await asyncio.gather(
684
+ senate_members, all_proposals, current_block = await asyncio.gather(
584
685
  _get_senate_members(subtensor, block_hash),
585
686
  _get_proposals(subtensor, block_hash),
687
+ subtensor.substrate.get_block_number(block_hash),
586
688
  )
587
689
 
588
- print_verbose("Fetching member information from Chain")
589
690
  registered_delegate_info: dict[
590
691
  str, DelegatesDetails
591
692
  ] = await subtensor.get_delegate_identities()
592
693
 
694
+ title = (
695
+ f"[bold #4196D6]Bittensor Governance Proposals[/bold #4196D6]\n"
696
+ f"[steel_blue3]Current Block:[/steel_blue3] {current_block}\t"
697
+ f"[steel_blue3]Network:[/steel_blue3] {subtensor.network}\n\n"
698
+ f"[steel_blue3]Active Proposals:[/steel_blue3] {len(all_proposals)}\t"
699
+ f"[steel_blue3]Senate Size:[/steel_blue3] {len(senate_members)}\n"
700
+ )
593
701
  table = Table(
594
702
  Column(
595
703
  "[white]HASH",
@@ -604,8 +712,8 @@ async def proposals(subtensor: "SubtensorInterface"):
604
712
  style="rgb(50,163,219)",
605
713
  ),
606
714
  Column("[white]END", style="bright_cyan"),
607
- Column("[white]CALLDATA", style="dark_sea_green"),
608
- title=f"\n[dark_orange]Proposals\t\t\nActive Proposals: {len(all_proposals)}\t\tSenate Size: {len(senate_members)}\nNetwork: {subtensor.network}",
715
+ Column("[white]CALLDATA", style="dark_sea_green", width=30),
716
+ title=title,
609
717
  show_footer=True,
610
718
  box=box.SIMPLE_HEAVY,
611
719
  pad_edge=False,
@@ -613,16 +721,36 @@ async def proposals(subtensor: "SubtensorInterface"):
613
721
  border_style="bright_black",
614
722
  )
615
723
  for hash_, (call_data, vote_data) in all_proposals.items():
724
+ blocks_remaining = vote_data.end - current_block
725
+ if blocks_remaining > 0:
726
+ duration_str = blocks_to_duration(blocks_remaining)
727
+ vote_end_cell = f"{vote_data.end} [dim](in {duration_str})[/dim]"
728
+ else:
729
+ vote_end_cell = f"{vote_data.end} [red](expired)[/red]"
730
+
731
+ ayes_threshold = (
732
+ (len(vote_data.ayes) / vote_data.threshold * 100)
733
+ if vote_data.threshold > 0
734
+ else 0
735
+ )
736
+ nays_threshold = (
737
+ (len(vote_data.nays) / vote_data.threshold * 100)
738
+ if vote_data.threshold > 0
739
+ else 0
740
+ )
616
741
  table.add_row(
617
- hash_,
742
+ hash_ if verbose else f"{hash_[:4]}...{hash_[-4:]}",
618
743
  str(vote_data.threshold),
619
- str(len(vote_data.ayes)),
620
- str(len(vote_data.nays)),
744
+ f"{len(vote_data.ayes)} ({ayes_threshold:.2f}%)",
745
+ f"{len(vote_data.nays)} ({nays_threshold:.2f}%)",
621
746
  display_votes(vote_data, registered_delegate_info),
622
- str(vote_data.end),
747
+ vote_end_cell,
623
748
  format_call_data(call_data),
624
749
  )
625
- return console.print(table)
750
+ console.print(table)
751
+ console.print(
752
+ "\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]"
753
+ )
626
754
 
627
755
 
628
756
  async def senate_vote(
@@ -653,10 +781,7 @@ async def senate_vote(
653
781
  return False
654
782
 
655
783
  # Unlock the wallet.
656
- try:
657
- wallet.unlock_hotkey()
658
- wallet.unlock_coldkey()
659
- except KeyFileError:
784
+ if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success:
660
785
  return False
661
786
 
662
787
  console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}")
@@ -732,10 +857,7 @@ async def set_take(
732
857
  f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}"
733
858
  )
734
859
 
735
- try:
736
- wallet.unlock_hotkey()
737
- wallet.unlock_coldkey()
738
- except KeyFileError:
860
+ if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success:
739
861
  return False
740
862
 
741
863
  result_ = await _do_set_take()