bittensor-cli 9.5.1__tar.gz → 9.7.0__tar.gz

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 (54) hide show
  1. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/PKG-INFO +9 -3
  2. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/README.md +8 -2
  3. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/cli.py +77 -22
  4. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/__init__.py +2 -0
  5. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/chain_data.py +4 -0
  6. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/extrinsics/registration.py +56 -16
  7. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/subtensor_interface.py +15 -7
  8. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/utils.py +30 -10
  9. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/stake/add.py +46 -41
  10. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/subnets/subnets.py +2 -1
  11. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/sudo.py +71 -61
  12. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/wallets.py +2 -0
  13. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli.egg-info/PKG-INFO +9 -3
  14. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/pyproject.toml +1 -1
  15. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/MANIFEST.in +0 -0
  16. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/__init__.py +0 -0
  17. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/doc_generation_helper.py +0 -0
  18. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/__init__.py +0 -0
  19. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/balances.py +0 -0
  20. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/extrinsics/__init__.py +0 -0
  21. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/extrinsics/root.py +0 -0
  22. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/extrinsics/transfer.py +0 -0
  23. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/minigraph.py +0 -0
  24. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/networking.py +0 -0
  25. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/main-filters.j2 +0 -0
  26. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/main-header.j2 +0 -0
  27. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/neuron-details.j2 +0 -0
  28. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/price-multi.j2 +0 -0
  29. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/price-single.j2 +0 -0
  30. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/subnet-details-header.j2 +0 -0
  31. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/subnet-details.j2 +0 -0
  32. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/subnet-metrics.j2 +0 -0
  33. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/subnets-table.j2 +0 -0
  34. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/table.j2 +0 -0
  35. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/view.css +0 -0
  36. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/view.j2 +0 -0
  37. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/bittensor/templates/view.js +0 -0
  38. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/__init__.py +0 -0
  39. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/stake/__init__.py +0 -0
  40. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/stake/children_hotkeys.py +0 -0
  41. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/stake/list.py +0 -0
  42. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/stake/move.py +0 -0
  43. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/stake/remove.py +0 -0
  44. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/subnets/__init__.py +0 -0
  45. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/subnets/price.py +0 -0
  46. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/view.py +0 -0
  47. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/src/commands/weights.py +0 -0
  48. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli/version.py +0 -0
  49. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli.egg-info/SOURCES.txt +0 -0
  50. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli.egg-info/dependency_links.txt +0 -0
  51. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli.egg-info/entry_points.txt +0 -0
  52. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli.egg-info/requires.txt +0 -0
  53. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/bittensor_cli.egg-info/top_level.txt +0 -0
  54. {bittensor_cli-9.5.1 → bittensor_cli-9.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bittensor-cli
3
- Version: 9.5.1
3
+ Version: 9.7.0
4
4
  Summary: Bittensor CLI
5
5
  Author: bittensor.com
6
6
  Project-URL: homepage, https://github.com/opentensor/btcli
@@ -71,10 +71,10 @@ Installation steps are described below. For a full documentation on how to use `
71
71
 
72
72
  ## Install on macOS and Linux
73
73
 
74
- You can install `btcli` on your local machine directly from source, or from PyPI. **Make sure you verify your installation after you install**:
74
+ You can install `btcli` on your local machine directly from source, PyPI, or Homebrew. **Make sure you verify your installation after you install**:
75
75
 
76
76
 
77
- ### Install from PyPI
77
+ ### Install from [PyPI](https://pypi.org/project/bittensor/)
78
78
 
79
79
  Run
80
80
  ```
@@ -86,6 +86,12 @@ Alternatively, if you prefer to use [uv](https://pypi.org/project/uv/):
86
86
  uv pip install bittensor-cli
87
87
  ```
88
88
 
89
+ ### Install from [Homebrew](https://formulae.brew.sh/formula/btcli#default)
90
+
91
+ ```shell
92
+ brew install btcli
93
+ ```
94
+
89
95
  ### Install from source
90
96
 
91
97
  1. Create and activate a virtual environment.
@@ -38,10 +38,10 @@ Installation steps are described below. For a full documentation on how to use `
38
38
 
39
39
  ## Install on macOS and Linux
40
40
 
41
- You can install `btcli` on your local machine directly from source, or from PyPI. **Make sure you verify your installation after you install**:
41
+ You can install `btcli` on your local machine directly from source, PyPI, or Homebrew. **Make sure you verify your installation after you install**:
42
42
 
43
43
 
44
- ### Install from PyPI
44
+ ### Install from [PyPI](https://pypi.org/project/bittensor/)
45
45
 
46
46
  Run
47
47
  ```
@@ -53,6 +53,12 @@ Alternatively, if you prefer to use [uv](https://pypi.org/project/uv/):
53
53
  uv pip install bittensor-cli
54
54
  ```
55
55
 
56
+ ### Install from [Homebrew](https://formulae.brew.sh/formula/btcli#default)
57
+
58
+ ```shell
59
+ brew install btcli
60
+ ```
61
+
56
62
  ### Install from source
57
63
 
58
64
  1. Create and activate a virtual environment.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  import asyncio
3
3
  import curses
4
+ import copy
4
5
  import importlib
5
6
  import json
6
7
  import os.path
@@ -10,7 +11,7 @@ import sys
10
11
  import traceback
11
12
  import warnings
12
13
  from pathlib import Path
13
- from typing import Coroutine, Optional
14
+ from typing import Coroutine, Optional, Union
14
15
  from dataclasses import fields
15
16
 
16
17
  import rich
@@ -89,6 +90,23 @@ class Options:
89
90
  Re-usable typer args
90
91
  """
91
92
 
93
+ @classmethod
94
+ def edit_help(cls, option_name: str, help_text: str):
95
+ """
96
+ Edits the `help` attribute of a copied given Typer option in this class, returning
97
+ the modified Typer option.
98
+
99
+ Args:
100
+ option_name: the name of the option (e.g. "wallet_name")
101
+ help_text: New help text to be used (e.g. "Wallet's name")
102
+
103
+ Returns:
104
+ Modified Typer Option with new help text.
105
+ """
106
+ copied_attr = copy.copy(getattr(cls, option_name))
107
+ setattr(copied_attr, "help", help_text)
108
+ return copied_attr
109
+
92
110
  wallet_name = typer.Option(
93
111
  None,
94
112
  "--wallet-name",
@@ -1879,6 +1897,8 @@ class CLIManager:
1879
1897
  wallet_name: Optional[str] = Options.wallet_name,
1880
1898
  wallet_path: Optional[str] = Options.wallet_path,
1881
1899
  wallet_hotkey: Optional[str] = Options.wallet_hotkey,
1900
+ netuid: Optional[int] = Options.netuid_not_req,
1901
+ all_netuids: bool = Options.all_netuids,
1882
1902
  network: Optional[list[str]] = Options.network,
1883
1903
  destination_hotkey_name: Optional[str] = typer.Argument(
1884
1904
  None, help="Destination hotkey name."
@@ -1899,12 +1919,14 @@ class CLIManager:
1899
1919
 
1900
1920
  - Make sure that your original key pair (coldkeyA, hotkeyA) is already registered.
1901
1921
  - Make sure that you use a newly created hotkeyB in this command. A hotkeyB that is already registered cannot be used in this command.
1922
+ - You can specify the netuid for which you want to swap the hotkey for. If it is not defined, the swap will be initiated for all subnets.
1902
1923
  - Finally, note that this command requires a fee of 1 TAO for recycling and this fee is taken from your wallet (coldkeyA).
1903
1924
 
1904
1925
  EXAMPLE
1905
1926
 
1906
- [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey
1927
+ [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey --netuid 1
1907
1928
  """
1929
+ netuid = get_optional_netuid(netuid, all_netuids)
1908
1930
  self.verbosity_handler(quiet, verbose, json_output)
1909
1931
  original_wallet = self.wallet_ask(
1910
1932
  wallet_name,
@@ -1928,7 +1950,7 @@ class CLIManager:
1928
1950
  self.initialize_chain(network)
1929
1951
  return self._run_command(
1930
1952
  wallets.swap_hotkey(
1931
- original_wallet, new_wallet, self.subtensor, prompt, json_output
1953
+ original_wallet, new_wallet, self.subtensor, netuid, prompt, json_output
1932
1954
  )
1933
1955
  )
1934
1956
 
@@ -3202,7 +3224,11 @@ class CLIManager:
3202
3224
  help="When set, this command stakes to all hotkeys associated with the wallet. Do not use if specifying "
3203
3225
  "hotkeys in `--include-hotkeys`.",
3204
3226
  ),
3205
- netuid: Optional[int] = Options.netuid_not_req,
3227
+ netuids: Optional[str] = Options.edit_help(
3228
+ "netuids",
3229
+ "Netuid(s) to for which to add stake. Specify multiple netuids by separating with a comma, e.g."
3230
+ "`btcli st add -n 1,2,3",
3231
+ ),
3206
3232
  all_netuids: bool = Options.all_netuids,
3207
3233
  wallet_name: str = Options.wallet_name,
3208
3234
  wallet_path: str = Options.wallet_path,
@@ -3242,42 +3268,65 @@ class CLIManager:
3242
3268
  6. Stake all balance to a subnet:
3243
3269
  [green]$[/green] btcli stake add --all --netuid 3
3244
3270
 
3271
+ 7. Stake the same amount to multiple subnets:
3272
+ [green]$[/green] btcli stake add --amount 100 --netuids 4,5,6
3273
+
3245
3274
  [bold]Safe Staking Parameters:[/bold]
3246
3275
  • [blue]--safe[/blue]: Enables rate tolerance checks
3247
3276
  • [blue]--tolerance[/blue]: Maximum % rate change allowed (0.05 = 5%)
3248
3277
  • [blue]--partial[/blue]: Complete partial stake if rates exceed tolerance
3249
3278
 
3250
3279
  """
3280
+ netuids = netuids or []
3251
3281
  self.verbosity_handler(quiet, verbose, json_output)
3252
3282
  safe_staking = self.ask_safe_staking(safe_staking)
3253
3283
  if safe_staking:
3254
3284
  rate_tolerance = self.ask_rate_tolerance(rate_tolerance)
3255
3285
  allow_partial_stake = self.ask_partial_stake(allow_partial_stake)
3256
3286
  console.print("\n")
3257
- netuid = get_optional_netuid(netuid, all_netuids)
3287
+
3288
+ if netuids:
3289
+ netuids = parse_to_list(
3290
+ netuids, int, "Netuids must be ints separated by commas", False
3291
+ )
3292
+ else:
3293
+ netuid_ = get_optional_netuid(None, all_netuids)
3294
+ netuids = [netuid_] if netuid_ else None
3295
+ if netuids:
3296
+ for netuid_ in netuids:
3297
+ # ensure no negative netuids make it into our list
3298
+ validate_netuid(netuid_)
3258
3299
 
3259
3300
  if stake_all and amount:
3260
3301
  print_error(
3261
3302
  "Cannot specify an amount and 'stake-all'. Choose one or the other."
3262
3303
  )
3263
- raise typer.Exit()
3304
+ return
3264
3305
 
3265
3306
  if stake_all and not amount:
3266
3307
  if not Confirm.ask("Stake all the available TAO tokens?", default=False):
3267
- raise typer.Exit()
3308
+ return
3309
+
3310
+ if (
3311
+ stake_all
3312
+ and (isinstance(netuids, list) and len(netuids) > 1)
3313
+ or (netuids is None)
3314
+ ):
3315
+ print_error("Cannot stake all to multiple subnets.")
3316
+ return
3268
3317
 
3269
3318
  if all_hotkeys and include_hotkeys:
3270
3319
  print_error(
3271
3320
  "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag"
3272
3321
  "should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`."
3273
3322
  )
3274
- raise typer.Exit()
3323
+ return
3275
3324
 
3276
3325
  if include_hotkeys and exclude_hotkeys:
3277
3326
  print_error(
3278
3327
  "You have specified options for both including and excluding hotkeys. Select one or the other."
3279
3328
  )
3280
- raise typer.Exit()
3329
+ return
3281
3330
 
3282
3331
  if not wallet_hotkey and not all_hotkeys and not include_hotkeys:
3283
3332
  if not wallet_name:
@@ -3285,9 +3334,10 @@ class CLIManager:
3285
3334
  "Enter the [blue]wallet name[/blue]",
3286
3335
  default=self.config.get("wallet_name") or defaults.wallet.name,
3287
3336
  )
3288
- if netuid is not None:
3337
+ if netuids is not None:
3289
3338
  hotkey_or_ss58 = Prompt.ask(
3290
- "Enter the [blue]wallet hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [dim](or Press Enter to view delegates)[/dim]",
3339
+ "Enter the [blue]wallet hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [dim]"
3340
+ "(or Press Enter to view delegates)[/dim]",
3291
3341
  )
3292
3342
  else:
3293
3343
  hotkey_or_ss58 = Prompt.ask(
@@ -3299,10 +3349,18 @@ class CLIManager:
3299
3349
  wallet = self.wallet_ask(
3300
3350
  wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH]
3301
3351
  )
3352
+ if len(netuids) > 1:
3353
+ netuid_ = IntPrompt.ask(
3354
+ "Enter the netuid for which to show delegates",
3355
+ choices=[str(x) for x in netuids],
3356
+ )
3357
+ else:
3358
+ netuid_ = netuids[0]
3359
+
3302
3360
  selected_hotkey = self._run_command(
3303
3361
  subnets.show(
3304
3362
  subtensor=self.initialize_chain(network),
3305
- netuid=netuid,
3363
+ netuid=netuid_,
3306
3364
  sort=False,
3307
3365
  max_rows=12,
3308
3366
  prompt=False,
@@ -3312,7 +3370,7 @@ class CLIManager:
3312
3370
  )
3313
3371
  if not selected_hotkey:
3314
3372
  print_error("No delegate selected. Exiting.")
3315
- raise typer.Exit()
3373
+ return
3316
3374
  include_hotkeys = selected_hotkey
3317
3375
  elif is_valid_ss58_address(hotkey_or_ss58):
3318
3376
  wallet = self.wallet_ask(
@@ -3373,8 +3431,8 @@ class CLIManager:
3373
3431
  )
3374
3432
  if free_balance == Balance.from_tao(0):
3375
3433
  print_error("You dont have any balance to stake.")
3376
- raise typer.Exit()
3377
- if netuid is not None:
3434
+ return
3435
+ if netuids:
3378
3436
  amount = FloatPrompt.ask(
3379
3437
  f"Amount to [{COLORS.G.SUBHEAD_MAIN}]stake (TAO τ)"
3380
3438
  )
@@ -3396,7 +3454,7 @@ class CLIManager:
3396
3454
  add_stake.stake_add(
3397
3455
  wallet,
3398
3456
  self.initialize_chain(network),
3399
- netuid,
3457
+ netuids,
3400
3458
  stake_all,
3401
3459
  amount,
3402
3460
  prompt,
@@ -4796,12 +4854,9 @@ class CLIManager:
4796
4854
  def subnets_price(
4797
4855
  self,
4798
4856
  network: Optional[list[str]] = Options.network,
4799
- netuids: str = typer.Option(
4800
- None,
4801
- "--netuids",
4802
- "--netuid",
4803
- "-n",
4804
- help="Netuid(s) to show the price for.",
4857
+ netuids: str = Options.edit_help(
4858
+ "netuids",
4859
+ "Netuids to show the price for. Separate multiple netuids with a comma, for example: `-n 0,1,2`.",
4805
4860
  ),
4806
4861
  interval_hours: int = typer.Option(
4807
4862
  24,
@@ -658,6 +658,8 @@ HYPERPARAMS = {
658
658
  "sudo_set_network_pow_registration_allowed",
659
659
  False,
660
660
  ),
661
+ "yuma3_enabled": ("sudo_set_yuma3_enabled", False),
662
+ "alpha_sigmoid_steepness": ("sudo_set_alpha_sigmoid_steepness", True),
661
663
  }
662
664
 
663
665
  # Help Panels for cli help
@@ -177,6 +177,8 @@ class SubnetHyperparameters(InfoBase):
177
177
  alpha_high: int
178
178
  alpha_low: int
179
179
  liquid_alpha_enabled: bool
180
+ yuma3_enabled: bool
181
+ alpha_sigmoid_steepness: int
180
182
 
181
183
  @classmethod
182
184
  def _fix_decoded(
@@ -210,6 +212,8 @@ class SubnetHyperparameters(InfoBase):
210
212
  alpha_high=decoded.get("alpha_high"),
211
213
  alpha_low=decoded.get("alpha_low"),
212
214
  liquid_alpha_enabled=decoded.get("liquid_alpha_enabled"),
215
+ yuma3_enabled=decoded.get("yuma3_enabled"),
216
+ alpha_sigmoid_steepness=decoded.get("alpha_sigmoid_steepness"),
213
217
  )
214
218
 
215
219
 
@@ -1611,7 +1611,8 @@ def _update_curr_block(
1611
1611
  """
1612
1612
  Update the current block data with the provided block information and difficulty.
1613
1613
 
1614
- This function updates the current block and its difficulty in a thread-safe manner. It sets the current block
1614
+ This function updates the current block
1615
+ and its difficulty in a thread-safe manner. It sets the current block
1615
1616
  number, hashes the block with the hotkey, updates the current block bytes, and packs the difficulty.
1616
1617
 
1617
1618
  :param curr_diff: Shared array to store the current difficulty.
@@ -1745,6 +1746,7 @@ async def swap_hotkey_extrinsic(
1745
1746
  subtensor: "SubtensorInterface",
1746
1747
  wallet: Wallet,
1747
1748
  new_wallet: Wallet,
1749
+ netuid: Optional[int] = None,
1748
1750
  prompt: bool = False,
1749
1751
  ) -> bool:
1750
1752
  """
@@ -1756,43 +1758,81 @@ async def swap_hotkey_extrinsic(
1756
1758
  netuids_registered = await subtensor.get_netuids_for_hotkey(
1757
1759
  wallet.hotkey.ss58_address, block_hash=block_hash
1758
1760
  )
1759
- if not len(netuids_registered) > 0:
1761
+ netuids_registered_new_hotkey = await subtensor.get_netuids_for_hotkey(
1762
+ new_wallet.hotkey.ss58_address, block_hash=block_hash
1763
+ )
1764
+
1765
+ if netuid is not None and netuid not in netuids_registered:
1766
+ err_console.print(
1767
+ f":cross_mark: [red]Failed[/red]: Original hotkey {wallet.hotkey.ss58_address} is not registered on subnet {netuid}"
1768
+ )
1769
+ return False
1770
+
1771
+ elif not len(netuids_registered) > 0:
1760
1772
  err_console.print(
1761
- f"Destination hotkey [dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] is not registered. "
1773
+ f"Original hotkey [dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] is not registered on any subnet. "
1762
1774
  f"Please register and try again"
1763
1775
  )
1764
1776
  return False
1765
1777
 
1778
+ if netuid is not None:
1779
+ if netuid in netuids_registered_new_hotkey:
1780
+ err_console.print(
1781
+ f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} "
1782
+ f"is already registered on subnet {netuid}"
1783
+ )
1784
+ return False
1785
+ else:
1786
+ if len(netuids_registered_new_hotkey) > 0:
1787
+ err_console.print(
1788
+ f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} "
1789
+ f"is already registered on subnet(s) {netuids_registered_new_hotkey}"
1790
+ )
1791
+ return False
1792
+
1766
1793
  if not unlock_key(wallet).success:
1767
1794
  return False
1768
1795
 
1769
1796
  if prompt:
1770
1797
  # Prompt user for confirmation.
1771
- if not Confirm.ask(
1772
- f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
1773
- f"[dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] with hotkey \n\t"
1774
- f"[dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange]\n"
1775
- "This operation will cost [bold cyan]1 TAO t (recycled)[/bold cyan]"
1776
- ):
1798
+ if netuid is not None:
1799
+ confirm_message = (
1800
+ f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
1801
+ f"[dark_orange]{wallet.hotkey.ss58_address} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t"
1802
+ f"[dark_orange]{new_wallet.hotkey.ss58_address} ({new_wallet.hotkey_str})[/dark_orange] on subnet {netuid}\n"
1803
+ "This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]"
1804
+ )
1805
+ else:
1806
+ confirm_message = (
1807
+ f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t"
1808
+ f"[dark_orange]{wallet.hotkey.ss58_address} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t"
1809
+ f"[dark_orange]{new_wallet.hotkey.ss58_address} ({new_wallet.hotkey_str})[/dark_orange] on all subnets\n"
1810
+ "This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]"
1811
+ )
1812
+
1813
+ if not Confirm.ask(confirm_message):
1777
1814
  return False
1778
1815
  print_verbose(
1779
- f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address}) with "
1780
- f"{new_wallet.name}s hotkey ({new_wallet.hotkey.ss58_address})"
1816
+ f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address} - {wallet.hotkey_str}) with "
1817
+ f"{new_wallet.name}'s hotkey ({new_wallet.hotkey.ss58_address} - {new_wallet.hotkey_str})"
1781
1818
  )
1782
1819
  with console.status(":satellite: Swapping hotkeys...", spinner="aesthetic"):
1820
+ call_params = {
1821
+ "hotkey": wallet.hotkey.ss58_address,
1822
+ "new_hotkey": new_wallet.hotkey.ss58_address,
1823
+ "netuid": netuid,
1824
+ }
1825
+
1783
1826
  call = await subtensor.substrate.compose_call(
1784
1827
  call_module="SubtensorModule",
1785
1828
  call_function="swap_hotkey",
1786
- call_params={
1787
- "hotkey": wallet.hotkey.ss58_address,
1788
- "new_hotkey": new_wallet.hotkey.ss58_address,
1789
- },
1829
+ call_params=call_params,
1790
1830
  )
1791
1831
  success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet)
1792
1832
 
1793
1833
  if success:
1794
1834
  console.print(
1795
- f"Hotkey {wallet.hotkey} swapped for new hotkey: {new_wallet.hotkey}"
1835
+ f"Hotkey {wallet.hotkey.ss58_address} ({wallet.hotkey_str}) swapped for new hotkey: {new_wallet.hotkey.ss58_address} ({new_wallet.hotkey_str})"
1796
1836
  )
1797
1837
  return True
1798
1838
  else:
@@ -1128,14 +1128,22 @@ class SubtensorInterface:
1128
1128
  Understanding the hyperparameters is crucial for comprehending how subnets are configured and
1129
1129
  managed, and how they interact with the network's consensus and incentive mechanisms.
1130
1130
  """
1131
- result = await self.query_runtime_api(
1132
- runtime_api="SubnetInfoRuntimeApi",
1133
- method="get_subnet_hyperparams",
1134
- params=[netuid],
1135
- block_hash=block_hash,
1131
+ main_result, yuma3_result, sigmoid_steepness = await asyncio.gather(
1132
+ self.query_runtime_api(
1133
+ runtime_api="SubnetInfoRuntimeApi",
1134
+ method="get_subnet_hyperparams",
1135
+ params=[netuid],
1136
+ block_hash=block_hash,
1137
+ ),
1138
+ self.query("SubtensorModule", "Yuma3On", [netuid]),
1139
+ self.query("SubtensorModule", "AlphaSigmoidSteepness", [netuid]),
1136
1140
  )
1137
-
1138
- if not result:
1141
+ result = {
1142
+ **main_result,
1143
+ **{"yuma3_enabled": yuma3_result},
1144
+ **{"alpha_sigmoid_steepness": sigmoid_steepness},
1145
+ }
1146
+ if not main_result:
1139
1147
  return []
1140
1148
 
1141
1149
  return SubnetHyperparameters.from_any(result)
@@ -3,7 +3,6 @@ from collections import namedtuple
3
3
  import math
4
4
  import os
5
5
  import sqlite3
6
- import platform
7
6
  import webbrowser
8
7
  from pathlib import Path
9
8
  from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable
@@ -33,6 +32,9 @@ if TYPE_CHECKING:
33
32
  from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters
34
33
  from rich.prompt import PromptBase
35
34
 
35
+ BT_DOCS_LINK = "https://docs.bittensor.com"
36
+
37
+
36
38
  console = Console()
37
39
  json_console = Console()
38
40
  err_console = Console(stderr=True)
@@ -527,10 +529,11 @@ def format_error_message(error_message: Union[dict, Exception]) -> str:
527
529
  elif all(x in d for x in ["code", "message", "data"]):
528
530
  new_error_message = d
529
531
  break
530
- except ValueError:
532
+ except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
531
533
  pass
532
534
  if new_error_message is None:
533
535
  return_val = " ".join(error_message.args)
536
+
534
537
  return f"Subtensor returned: {return_val}"
535
538
  else:
536
539
  error_message = new_error_message
@@ -549,8 +552,7 @@ def format_error_message(error_message: Union[dict, Exception]) -> str:
549
552
  # subtensor custom error marker
550
553
  if err_data.startswith("Custom error:"):
551
554
  err_description = (
552
- f"{err_data} | Please consult "
553
- f"https://docs.bittensor.com/subtensor-nodes/subtensor-error-messages"
555
+ f"{err_data} | Please consult {BT_DOCS_LINK}/errors/custom"
554
556
  )
555
557
  else:
556
558
  err_description = err_data
@@ -563,10 +565,20 @@ def format_error_message(error_message: Union[dict, Exception]) -> str:
563
565
  err_type = error_message.get("type", err_type)
564
566
  err_name = error_message.get("name", err_name)
565
567
  err_docs = error_message.get("docs", [err_description])
566
- if isinstance(err_docs, list):
567
- err_description = " ".join(err_docs)
568
- else:
569
- err_description = err_docs
568
+ err_description = " ".join(err_docs)
569
+ err_description += (
570
+ f" | Please consult {BT_DOCS_LINK}/errors/subtensor#{err_name.lower()}"
571
+ )
572
+
573
+ elif error_message.get("code") and error_message.get("message"):
574
+ err_type = error_message.get("code", err_name)
575
+ err_name = "Custom type"
576
+ err_description = error_message.get("message", err_description)
577
+
578
+ else:
579
+ print_error(
580
+ f"String representation of real error_message: {str(error_message)}"
581
+ )
570
582
 
571
583
  return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`."
572
584
 
@@ -706,11 +718,14 @@ def millify_tao(n: float, start_at: str = "K") -> str:
706
718
 
707
719
  def normalize_hyperparameters(
708
720
  subnet: "SubnetHyperparameters",
721
+ json_output: bool = False,
709
722
  ) -> list[tuple[str, str, str]]:
710
723
  """
711
724
  Normalizes the hyperparameters of a subnet.
712
725
 
713
726
  :param subnet: The subnet hyperparameters object.
727
+ :param json_output: Whether this normalisation will be for a JSON output or console string (determines whether
728
+ items get stringified or safe for JSON encoding)
714
729
 
715
730
  :return: A list of tuples containing the parameter name, value, and normalized value.
716
731
  """
@@ -724,6 +739,7 @@ def normalize_hyperparameters(
724
739
  "kappa": u16_normalized_float,
725
740
  "alpha_high": u16_normalized_float,
726
741
  "alpha_low": u16_normalized_float,
742
+ "alpha_sigmoid_steepness": u16_normalized_float,
727
743
  "min_burn": Balance.from_rao,
728
744
  "max_burn": Balance.from_rao,
729
745
  }
@@ -737,13 +753,17 @@ def normalize_hyperparameters(
737
753
  norm_value = param_mappings[param](value)
738
754
  if isinstance(norm_value, float):
739
755
  norm_value = f"{norm_value:.{10}g}"
756
+ if isinstance(norm_value, Balance) and json_output:
757
+ norm_value = norm_value.to_dict()
740
758
  else:
741
759
  norm_value = value
742
760
  except Exception:
743
761
  # bittensor.logging.warning(f"Error normalizing parameter '{param}': {e}")
744
762
  norm_value = "-"
745
-
746
- normalized_values.append((param, str(value), str(norm_value)))
763
+ if not json_output:
764
+ normalized_values.append((param, str(value), str(norm_value)))
765
+ else:
766
+ normalized_values.append((param, value, norm_value))
747
767
 
748
768
  return normalized_values
749
769
 
@@ -31,7 +31,7 @@ if TYPE_CHECKING:
31
31
  async def stake_add(
32
32
  wallet: Wallet,
33
33
  subtensor: "SubtensorInterface",
34
- netuid: Optional[int],
34
+ netuids: Optional[list[int]],
35
35
  stake_all: bool,
36
36
  amount: float,
37
37
  prompt: bool,
@@ -48,7 +48,7 @@ async def stake_add(
48
48
  Args:
49
49
  wallet: wallet object
50
50
  subtensor: SubtensorInterface object
51
- netuid: the netuid to stake to (None indicates all subnets)
51
+ netuids: the netuids to stake to (None indicates all subnets)
52
52
  stake_all: whether to stake all available balance
53
53
  amount: specified amount of balance to stake
54
54
  prompt: whether to prompt the user
@@ -72,7 +72,7 @@ async def stake_add(
72
72
  hotkey_ss58_: str,
73
73
  price_limit: Balance,
74
74
  status=None,
75
- ) -> bool:
75
+ ) -> tuple[bool, str]:
76
76
  err_out = partial(print_error, status=status)
77
77
  failure_prelude = (
78
78
  f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}"
@@ -104,25 +104,24 @@ async def stake_add(
104
104
  )
105
105
  except SubstrateRequestException as e:
106
106
  if "Custom error: 8" in str(e):
107
- print_error(
108
- f"\n{failure_prelude}: Price exceeded tolerance limit. "
107
+ err_msg = (
108
+ f"{failure_prelude}: Price exceeded tolerance limit. "
109
109
  f"Transaction rejected because partial staking is disabled. "
110
- f"Either increase price tolerance or enable partial staking.",
111
- status=status,
110
+ f"Either increase price tolerance or enable partial staking."
112
111
  )
113
- return False
112
+ print_error("\n" + err_msg, status=status)
114
113
  else:
115
- err_out(f"\n{failure_prelude} with error: {format_error_message(e)}")
116
- return False
114
+ err_msg = f"{failure_prelude} with error: {format_error_message(e)}"
115
+ err_out("\n" + err_msg)
116
+ return False, err_msg
117
117
  if not await response.is_success:
118
- err_out(
119
- f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
120
- )
121
- return False
118
+ err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}"
119
+ err_out("\n" + err_msg)
120
+ return False, err_msg
122
121
  else:
123
122
  if json_output:
124
123
  # the rest of this checking is not necessary if using json_output
125
- return True
124
+ return True, ""
126
125
  block_hash = await subtensor.substrate.get_chain_head()
127
126
  new_balance, new_stake = await asyncio.gather(
128
127
  subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
@@ -160,11 +159,11 @@ async def stake_add(
160
159
  f":arrow_right: "
161
160
  f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
162
161
  )
163
- return True
162
+ return True, ""
164
163
 
165
164
  async def stake_extrinsic(
166
165
  netuid_i, amount_, current, staking_address_ss58, status=None
167
- ) -> bool:
166
+ ) -> tuple[bool, str]:
168
167
  err_out = partial(print_error, status=status)
169
168
  current_balance, next_nonce, call = await asyncio.gather(
170
169
  subtensor.get_balance(wallet.coldkeypub.ss58_address),
@@ -190,18 +189,18 @@ async def stake_add(
190
189
  extrinsic, wait_for_inclusion=True, wait_for_finalization=False
191
190
  )
192
191
  except SubstrateRequestException as e:
193
- err_out(f"\n{failure_prelude} with error: {format_error_message(e)}")
194
- return False
192
+ err_msg = f"{failure_prelude} with error: {format_error_message(e)}"
193
+ err_out("\n" + err_msg)
194
+ return False, err_msg
195
195
  else:
196
196
  if not await response.is_success:
197
- err_out(
198
- f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
199
- )
200
- return False
197
+ err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}"
198
+ err_out("\n" + err_msg)
199
+ return False, err_msg
201
200
  else:
202
201
  if json_output:
203
202
  # the rest of this is not necessary if using json_output
204
- return True
203
+ return True, ""
205
204
  new_block_hash = await subtensor.substrate.get_chain_head()
206
205
  new_balance, new_stake = await asyncio.gather(
207
206
  subtensor.get_balance(
@@ -230,12 +229,10 @@ async def stake_add(
230
229
  f":arrow_right: "
231
230
  f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
232
231
  )
233
- return True
232
+ return True, ""
234
233
 
235
234
  netuids = (
236
- [int(netuid)]
237
- if netuid is not None
238
- else await subtensor.get_all_subnet_netuids()
235
+ netuids if netuids is not None else await subtensor.get_all_subnet_netuids()
239
236
  )
240
237
 
241
238
  hotkeys_to_stake_to = _get_hotkeys_to_stake_to(
@@ -419,13 +416,17 @@ async def stake_add(
419
416
  for _, staking_address in hotkeys_to_stake_to
420
417
  }
421
418
  successes = defaultdict(dict)
419
+ error_messages = defaultdict(dict)
422
420
  with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."):
423
421
  # We can gather them all at once but balance reporting will be in race-condition.
424
422
  for (ni, staking_address), coroutine in stake_coroutines.items():
425
- success = await coroutine
423
+ success, er_msg = await coroutine
426
424
  successes[ni][staking_address] = success
425
+ error_messages[ni][staking_address] = er_msg
427
426
  if json_output:
428
- json_console.print(json.dumps({"staking_success": successes}))
427
+ json_console.print(
428
+ json.dumps({"staking_success": successes, "error_messages": error_messages})
429
+ )
429
430
 
430
431
 
431
432
  # Helper functions
@@ -445,10 +446,10 @@ def _prompt_stake_amount(
445
446
  while True:
446
447
  amount_input = Prompt.ask(
447
448
  f"\nEnter the amount to {action_name}"
448
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
449
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
449
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
450
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}](max: {current_balance})[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
450
451
  f"or "
451
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
452
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]'all'[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
452
453
  f"for entire balance"
453
454
  )
454
455
 
@@ -463,7 +464,7 @@ def _prompt_stake_amount(
463
464
  if amount > current_balance.tao:
464
465
  console.print(
465
466
  f"[red]Amount exceeds available balance of "
466
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
467
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
467
468
  f"[/red]"
468
469
  )
469
470
  continue
@@ -542,10 +543,10 @@ def _define_stake_table(
542
543
  Table: An initialized rich Table object with appropriate columns
543
544
  """
544
545
  table = Table(
545
- title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n"
546
- f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], "
547
- f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n"
548
- f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n",
546
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Staking to:\n"
547
+ f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet.name}[/{COLOR_PALETTE.G.CK}], "
548
+ f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n"
549
+ f"Network: {subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n",
549
550
  show_footer=True,
550
551
  show_edge=False,
551
552
  header_style="bold white",
@@ -609,9 +610,13 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b
609
610
 
610
611
  # Greater than 5%
611
612
  if max_slippage > 5:
612
- message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
613
- message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n"
614
- message += "-------------------------------------------------------------------------------------------------------------------\n"
613
+ message = (
614
+ f"[{COLOR_PALETTE.S.SLIPPAGE_TEXT}]" + ("-" * 115) + "\n"
615
+ f"[bold]WARNING:[/bold] The slippage on one of your operations is high: "
616
+ f"[{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]{max_slippage} %[/{COLOR_PALETTE.S.SLIPPAGE_PERCENT}], "
617
+ f"this may result in a loss of funds.\n" + ("-" * 115) + "\n"
618
+ )
619
+
615
620
  console.print(message)
616
621
 
617
622
  # Table description
@@ -2323,7 +2323,7 @@ async def get_start_schedule(
2323
2323
  print_error(f"Subnet {netuid} does not exist.")
2324
2324
  return None
2325
2325
  block_hash = await subtensor.substrate.get_chain_head()
2326
- registration_block, min_blocks_to_start, current_block = await asyncio.gather(
2326
+ registration_block, min_blocks_to_start_, current_block = await asyncio.gather(
2327
2327
  subtensor.query(
2328
2328
  module="SubtensorModule",
2329
2329
  storage_function="NetworkRegisteredAt",
@@ -2337,6 +2337,7 @@ async def get_start_schedule(
2337
2337
  ),
2338
2338
  subtensor.substrate.get_block_number(block_hash=block_hash),
2339
2339
  )
2340
+ min_blocks_to_start = getattr(min_blocks_to_start_, "value", min_blocks_to_start_)
2340
2341
 
2341
2342
  potential_start_block = registration_block + min_blocks_to_start
2342
2343
  if current_block < potential_start_block:
@@ -73,6 +73,13 @@ def allowed_value(
73
73
  return True, value
74
74
 
75
75
 
76
+ def string_to_bool(val) -> bool:
77
+ try:
78
+ return {"true": True, "1": True, "0": False, "false": False}[val.lower()]
79
+ except KeyError:
80
+ return ValueError
81
+
82
+
76
83
  def search_metadata(
77
84
  param_name: str, value: Union[str, bool, float, list[float]], netuid: int, metadata
78
85
  ) -> tuple[bool, Optional[dict]]:
@@ -91,12 +98,6 @@ def search_metadata(
91
98
 
92
99
  """
93
100
 
94
- def string_to_bool(val) -> bool:
95
- try:
96
- return {"true": True, "1": True, "0": False, "false": False}[val.lower()]
97
- except KeyError:
98
- return ValueError
99
-
100
101
  def type_converter_with_retry(type_, val, arg_name):
101
102
  try:
102
103
  if val is None:
@@ -112,37 +113,55 @@ def search_metadata(
112
113
 
113
114
  call_crafter = {"netuid": netuid}
114
115
 
115
- for pallet in metadata.pallets:
116
- if pallet.name == "AdminUtils":
117
- for call in pallet.calls:
118
- if call.name == param_name:
119
- if "netuid" not in [x.name for x in call.args]:
120
- return False, None
121
- call_args = [
122
- arg for arg in call.args if arg.value["name"] != "netuid"
123
- ]
124
- if len(call_args) == 1:
125
- arg = call_args[0].value
126
- call_crafter[arg["name"]] = type_converter_with_retry(
127
- arg["typeName"], value, arg["name"]
128
- )
129
- else:
130
- for arg_ in call_args:
131
- arg = arg_.value
132
- call_crafter[arg["name"]] = type_converter_with_retry(
133
- arg["typeName"], None, arg["name"]
134
- )
135
- return True, call_crafter
116
+ pallet = metadata.get_metadata_pallet("AdminUtils")
117
+ for call in pallet.calls:
118
+ if call.name == param_name:
119
+ if "netuid" not in [x.name for x in call.args]:
120
+ return False, None
121
+ call_args = [arg for arg in call.args if arg.value["name"] != "netuid"]
122
+ if len(call_args) == 1:
123
+ arg = call_args[0].value
124
+ call_crafter[arg["name"]] = type_converter_with_retry(
125
+ arg["typeName"], value, arg["name"]
126
+ )
127
+ else:
128
+ for arg_ in call_args:
129
+ arg = arg_.value
130
+ call_crafter[arg["name"]] = type_converter_with_retry(
131
+ arg["typeName"], None, arg["name"]
132
+ )
133
+ return True, call_crafter
136
134
  else:
137
135
  return False, None
138
136
 
139
137
 
138
+ def requires_bool(metadata, param_name) -> bool:
139
+ """
140
+ Determines whether a given hyperparam takes a single arg (besides netuid) that is of bool type.
141
+ """
142
+ pallet = metadata.get_metadata_pallet("AdminUtils")
143
+ for call in pallet.calls:
144
+ if call.name == param_name:
145
+ if "netuid" not in [x.name for x in call.args]:
146
+ return False, None
147
+ call_args = [arg for arg in call.args if arg.value["name"] != "netuid"]
148
+ if len(call_args) != 1:
149
+ return False
150
+ else:
151
+ arg = call_args[0].value
152
+ if arg["typeName"] == "bool":
153
+ return True
154
+ else:
155
+ return False
156
+ raise ValueError(f"{param_name} not found in pallet.")
157
+
158
+
140
159
  async def set_hyperparameter_extrinsic(
141
160
  subtensor: "SubtensorInterface",
142
161
  wallet: "Wallet",
143
162
  netuid: int,
144
163
  parameter: str,
145
- value: Optional[Union[str, bool, float, list[float]]],
164
+ value: Optional[Union[str, float, list[float]]],
146
165
  wait_for_inclusion: bool = False,
147
166
  wait_for_finalization: bool = True,
148
167
  prompt: bool = True,
@@ -221,15 +240,20 @@ async def set_hyperparameter_extrinsic(
221
240
  ]
222
241
 
223
242
  if len(value) < len(non_netuid_fields):
224
- raise ValueError(
243
+ err_console.print(
225
244
  "Not enough values provided in the list for all parameters"
226
245
  )
246
+ return False
227
247
 
228
248
  call_params.update(
229
249
  {str(name): val for name, val in zip(non_netuid_fields, value)}
230
250
  )
231
251
 
232
252
  else:
253
+ if requires_bool(
254
+ substrate.metadata, param_name=extrinsic
255
+ ) and isinstance(value, str):
256
+ value = string_to_bool(value)
233
257
  value_argument = extrinsic_params["fields"][
234
258
  len(extrinsic_params["fields"]) - 1
235
259
  ]
@@ -252,12 +276,13 @@ async def set_hyperparameter_extrinsic(
252
276
  )
253
277
  if not success:
254
278
  err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
255
- await asyncio.sleep(0.5)
279
+ return False
256
280
  elif arbitrary_extrinsic:
257
281
  console.print(
258
282
  f":white_heavy_check_mark: "
259
283
  f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]"
260
284
  )
285
+ return True
261
286
  # Successful registration, final check for membership
262
287
  else:
263
288
  console.print(
@@ -581,28 +606,11 @@ async def sudo_set_hyperparameter(
581
606
  json_output: bool,
582
607
  ):
583
608
  """Set subnet hyperparameters."""
584
-
585
- normalized_value: Union[str, bool]
586
- if param_name in [
587
- "registration_allowed",
588
- "network_pow_registration_allowed",
589
- "commit_reveal_weights_enabled",
590
- "liquid_alpha_enabled",
591
- ]:
592
- normalized_value = param_value.lower() in ["true", "1"]
593
- elif param_value in ("True", "False"):
594
- normalized_value = {
595
- "True": True,
596
- "False": False,
597
- }[param_value]
598
- else:
599
- normalized_value = param_value
600
-
601
- is_allowed_value, value = allowed_value(param_name, normalized_value)
609
+ is_allowed_value, value = allowed_value(param_name, param_value)
602
610
  if not is_allowed_value:
603
611
  err_console.print(
604
612
  f"Hyperparameter [dark_orange]{param_name}[/dark_orange] value is not within bounds. "
605
- f"Value is {normalized_value} but must be {value}"
613
+ f"Value is {param_value} but must be {value}"
606
614
  )
607
615
  return False
608
616
  success = await set_hyperparameter_extrinsic(
@@ -625,8 +633,9 @@ async def get_hyperparameters(
625
633
  if not await subtensor.subnet_exists(netuid):
626
634
  print_error(f"Subnet with netuid {netuid} does not exist.")
627
635
  return False
628
- subnet = await subtensor.get_subnet_hyperparameters(netuid)
629
- subnet_info = await subtensor.subnet(netuid)
636
+ subnet, subnet_info = await asyncio.gather(
637
+ subtensor.get_subnet_hyperparameters(netuid), subtensor.subnet(netuid)
638
+ )
630
639
  if subnet_info is None:
631
640
  print_error(f"Subnet with netuid {netuid} does not exist.")
632
641
  return False
@@ -648,17 +657,18 @@ async def get_hyperparameters(
648
657
  )
649
658
  dict_out = []
650
659
 
651
- normalized_values = normalize_hyperparameters(subnet)
652
-
660
+ normalized_values = normalize_hyperparameters(subnet, json_output=json_output)
653
661
  for param, value, norm_value in normalized_values:
654
- table.add_row(" " + param, value, norm_value)
655
- dict_out.append(
656
- {
657
- "hyperparameter": param,
658
- "value": value,
659
- "normalized_value": norm_value,
660
- }
661
- )
662
+ if not json_output:
663
+ table.add_row(" " + param, value, norm_value)
664
+ else:
665
+ dict_out.append(
666
+ {
667
+ "hyperparameter": param,
668
+ "value": value,
669
+ "normalized_value": norm_value,
670
+ }
671
+ )
662
672
  if json_output:
663
673
  json_console.print(json.dumps(dict_out))
664
674
  else:
@@ -1632,6 +1632,7 @@ async def swap_hotkey(
1632
1632
  original_wallet: Wallet,
1633
1633
  new_wallet: Wallet,
1634
1634
  subtensor: SubtensorInterface,
1635
+ netuid: Optional[int],
1635
1636
  prompt: bool,
1636
1637
  json_output: bool,
1637
1638
  ):
@@ -1640,6 +1641,7 @@ async def swap_hotkey(
1640
1641
  subtensor,
1641
1642
  original_wallet,
1642
1643
  new_wallet,
1644
+ netuid=netuid,
1643
1645
  prompt=prompt,
1644
1646
  )
1645
1647
  if json_output:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bittensor-cli
3
- Version: 9.5.1
3
+ Version: 9.7.0
4
4
  Summary: Bittensor CLI
5
5
  Author: bittensor.com
6
6
  Project-URL: homepage, https://github.com/opentensor/btcli
@@ -71,10 +71,10 @@ Installation steps are described below. For a full documentation on how to use `
71
71
 
72
72
  ## Install on macOS and Linux
73
73
 
74
- You can install `btcli` on your local machine directly from source, or from PyPI. **Make sure you verify your installation after you install**:
74
+ You can install `btcli` on your local machine directly from source, PyPI, or Homebrew. **Make sure you verify your installation after you install**:
75
75
 
76
76
 
77
- ### Install from PyPI
77
+ ### Install from [PyPI](https://pypi.org/project/bittensor/)
78
78
 
79
79
  Run
80
80
  ```
@@ -86,6 +86,12 @@ Alternatively, if you prefer to use [uv](https://pypi.org/project/uv/):
86
86
  uv pip install bittensor-cli
87
87
  ```
88
88
 
89
+ ### Install from [Homebrew](https://formulae.brew.sh/formula/btcli#default)
90
+
91
+ ```shell
92
+ brew install btcli
93
+ ```
94
+
89
95
  ### Install from source
90
96
 
91
97
  1. Create and activate a virtual environment.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "bittensor-cli"
7
- version = "9.5.1"
7
+ version = "9.7.0"
8
8
  description = "Bittensor CLI"
9
9
  readme = "README.md"
10
10
  authors = [
File without changes
File without changes