diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bbc0128..be2bc9ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 9.4.1 /2025-04-17 + +## What's Changed +* Fixes `test_staking_sudo` setting `max_burn` by @thewhaleking in https://github.com/opentensor/btcli/pull/440 +* Fixes Error Formatter by @thewhaleking in https://github.com/opentensor/btcli/pull/439 +* Pulls shares in a gather rather than one-at-a-time by @thewhaleking in https://github.com/opentensor/btcli/pull/438 +* Pull emission start schedule dynamically by @thewhaleking in https://github.com/opentensor/btcli/pull/442 +* Lengthen default era period + rename "era" to "period" by @thewhaleking in https://github.com/opentensor/btcli/pull/443 +* docs: fixed redundant "from" by @mdqst in https://github.com/opentensor/btcli/pull/429 +* click version 8.2.0 broken by @thewhaleking in https://github.com/opentensor/btcli/pull/447 +* JSON Name shadowing bug by @thewhaleking in https://github.com/opentensor/btcli/pull/445 +* Stop Parsing, Start Asking by @thewhaleking in https://github.com/opentensor/btcli/pull/446 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.4.0...v9.4.1 + ## 9.4.0 /2025-04-17 ## What's Changed diff --git a/README.md b/README.md index 5d33a2760..4e9a5d195 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Installation steps are described below. For a full documentation on how to use ` ## Install on macOS and Linux -You can install `btcli` on your local machine directly from source, or from from PyPI. **Make sure you verify your installation after you install**: +You can install `btcli` on your local machine directly from source, or from PyPI. **Make sure you verify your installation after you install**: ### Install from PyPI diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9df39d24e..ce64f1950 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -293,8 +293,11 @@ class Options: "--json-out", help="Outputs the result of the command as JSON.", ) - era: int = typer.Option( - 3, help="Length (in blocks) for which the transaction should be valid." + period: int = typer.Option( + 16, + "--period", + "--era", + help="Length (in blocks) for which the transaction should be valid.", ) @@ -436,36 +439,49 @@ def parse_mnemonic(mnemonic: str) -> str: def get_creation_data( mnemonic: Optional[str], seed: Optional[str], - json: Optional[str], + json_path: Optional[str], json_password: Optional[str], ) -> tuple[str, str, str, str]: """ Determines which of the key creation elements have been supplied, if any. If None have been supplied, prompts to user, and determines what they've supplied. Returns all elements in a tuple. """ - if not mnemonic and not seed and not json: - prompt_answer = Prompt.ask( - "Enter the mnemonic, or the seed hex string, or the location of the JSON file." + if not mnemonic and not seed and not json_path: + choices = { + 1: "mnemonic", + 2: "seed hex string", + 3: "path to JSON File", + } + type_answer = IntPrompt.ask( + "Select one of the following to enter\n" + f"[{COLORS.G.HINT}][1][/{COLORS.G.HINT}] Mnemonic\n" + f"[{COLORS.G.HINT}][2][/{COLORS.G.HINT}] Seed hex string\n" + f"[{COLORS.G.HINT}][3][/{COLORS.G.HINT}] Path to JSON File\n", + choices=["1", "2", "3"], + show_choices=False, ) - if prompt_answer.startswith("0x"): + prompt_answer = Prompt.ask(f"Please enter your {choices[type_answer]}") + if type_answer == 1: + mnemonic = prompt_answer + elif type_answer == 2: seed = prompt_answer - elif len(prompt_answer.split(" ")) > 1: - mnemonic = parse_mnemonic(prompt_answer) - else: - json = prompt_answer + if seed.startswith("0x"): + seed = seed[2:] + elif type_answer == 3: + json_path = prompt_answer elif mnemonic: mnemonic = parse_mnemonic(mnemonic) - if json: - if not os.path.exists(json): - print_error(f"The JSON file '{json}' does not exist.") + if json_path: + if not os.path.exists(json_path): + print_error(f"The JSON file '{json_path}' does not exist.") raise typer.Exit() - if json and not json_password: + if json_path and not json_password: json_password = Prompt.ask( "Enter the backup password for JSON file.", password=True ) - return mnemonic, seed, json, json_password + return mnemonic, seed, json_path, json_password def config_selector(conf: dict, title: str): @@ -1792,7 +1808,7 @@ def wallet_transfer( transfer_all: bool = typer.Option( False, "--all", prompt=False, help="Transfer all available balance." ), - era: int = Options.era, + period: int = Options.period, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -1847,7 +1863,7 @@ def wallet_transfer( destination=destination_ss58_address, amount=amount, transfer_all=transfer_all, - era=era, + era=period, prompt=prompt, json_output=json_output, ) @@ -2090,7 +2106,7 @@ def wallet_regen_coldkey( wallet_hotkey: Optional[str] = Options.wallet_hotkey, mnemonic: Optional[str] = Options.mnemonic, seed: Optional[str] = Options.seed, - json: Optional[str] = Options.json, + json_path: Optional[str] = Options.json, json_password: Optional[str] = Options.json_password, use_password: Optional[bool] = Options.use_password, overwrite: bool = Options.overwrite, @@ -2130,15 +2146,15 @@ def wallet_regen_coldkey( wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) - mnemonic, seed, json, json_password = get_creation_data( - mnemonic, seed, json, json_password + mnemonic, seed, json_path, json_password = get_creation_data( + mnemonic, seed, json_path, json_password ) return self._run_command( wallets.regen_coldkey( wallet, mnemonic, seed, - json, + json_path, json_password, use_password, overwrite, @@ -2214,7 +2230,7 @@ def wallet_regen_hotkey( wallet_hotkey: Optional[str] = Options.wallet_hotkey, mnemonic: Optional[str] = Options.mnemonic, seed: Optional[str] = Options.seed, - json: Optional[str] = Options.json, + json_path: Optional[str] = Options.json, json_password: Optional[str] = Options.json_password, use_password: bool = typer.Option( False, # Overriden to False @@ -2250,15 +2266,15 @@ def wallet_regen_hotkey( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET, ) - mnemonic, seed, json, json_password = get_creation_data( - mnemonic, seed, json, json_password + mnemonic, seed, json_path, json_password = get_creation_data( + mnemonic, seed, json_path, json_password ) return self._run_command( wallets.regen_hotkey( wallet, mnemonic, seed, - json, + json_path, json_password, use_password, overwrite, @@ -3190,7 +3206,7 @@ def stake_add( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, - era: int = Options.era, + period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3386,7 +3402,7 @@ def stake_add( rate_tolerance, allow_partial_stake, json_output, - era, + period, ) ) @@ -3438,7 +3454,7 @@ def stake_remove( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, - era: int = Options.era, + period: int = Options.period, prompt: bool = Options.prompt, interactive: bool = typer.Option( False, @@ -3631,7 +3647,7 @@ def stake_remove( exclude_hotkeys=exclude_hotkeys, prompt=prompt, json_output=json_output, - era=era, + era=period, ) ) elif ( @@ -3687,7 +3703,7 @@ def stake_remove( rate_tolerance=rate_tolerance, allow_partial_stake=allow_partial_stake, json_output=json_output, - era=era, + era=period, ) ) @@ -3715,7 +3731,7 @@ def stake_move( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), - era: int = Options.era, + period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3845,7 +3861,7 @@ def stake_move( destination_hotkey=destination_hotkey, amount=amount, stake_all=stake_all, - era=era, + era=period, interactive_selection=interactive_selection, prompt=prompt, ) @@ -3886,7 +3902,7 @@ def stake_transfer( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), - era: int = Options.era, + period: int = Options.period, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4008,7 +4024,7 @@ def stake_transfer( dest_netuid=dest_netuid, dest_coldkey_ss58=dest_ss58, amount=amount, - era=era, + era=period, interactive_selection=interactive_selection, stake_all=stake_all, prompt=prompt, @@ -4050,7 +4066,7 @@ def stake_swap( "--all", help="Swap all available stake", ), - era: int = Options.era, + period: int = Options.period, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -4115,7 +4131,7 @@ def stake_swap( destination_netuid=dest_netuid, amount=amount, swap_all=swap_all, - era=era, + era=period, interactive_selection=interactive_selection, prompt=prompt, wait_for_inclusion=wait_for_inclusion, @@ -4430,6 +4446,7 @@ def sudo_set( param_value: Optional[str] = typer.Option( "", "--value", help="Value to set the hyperparameter to." ), + prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, json_output: bool = Options.json_output, @@ -4454,6 +4471,11 @@ def sudo_set( raise typer.Exit() if not param_name: + if not prompt: + err_console.print( + "Param name not supplied with `--no-prompt` flag. Cannot continue" + ) + return False hyperparam_list = [field.name for field in fields(SubnetHyperparameters)] console.print("Available hyperparameters:\n") for idx, param in enumerate(hyperparam_list, start=1): @@ -4467,6 +4489,11 @@ def sudo_set( param_name = hyperparam_list[choice - 1] if param_name in ["alpha_high", "alpha_low"]: + if not prompt: + err_console.print( + "`alpha_high` and `alpha_low` values cannot be set with `--no-prompt`" + ) + return False param_name = "alpha_values" low_val = FloatPrompt.ask( "Enter the new value for [dark_orange]alpha_low[/dark_orange]" @@ -4477,6 +4504,11 @@ def sudo_set( param_value = f"{low_val},{high_val}" if not param_value: + if not prompt: + err_console.print( + "Param value not supplied with `--no-prompt` flag. Cannot continue." + ) + return False if HYPERPARAMS.get(param_name): param_value = Prompt.ask( f"Enter the new value for [{COLORS.G.SUBHEAD}]{param_name}[/{COLORS.G.SUBHEAD}] " @@ -4495,6 +4527,7 @@ def sudo_set( netuid, param_name, param_value, + prompt, json_output, ) ) @@ -5258,10 +5291,12 @@ def subnets_register( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, - era: Optional[ + period: Optional[ int - ] = typer.Option( # Should not be Options.era bc this needs to be an Optional[int] + ] = typer.Option( # Should not be Options.period bc this needs to be an Optional[int] None, + "--period", + "--era", help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), @@ -5294,7 +5329,7 @@ def subnets_register( wallet, self.initialize_chain(network), netuid, - era, + period, json_output, prompt, ) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 5c508812c..ce55bfcd6 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -38,7 +38,6 @@ class Constants: "test": "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105", } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" - emission_start_schedule = 7 * 24 * 60 * 60 / 12 # 7 days @dataclass @@ -640,7 +639,7 @@ class WalletValidationTypes(Enum): "activity_cutoff": ("sudo_set_activity_cutoff", False), "target_regs_per_interval": ("sudo_set_target_registrations_per_interval", True), "min_burn": ("sudo_set_min_burn", True), - "max_burn": ("sudo_set_max_burn", False), + "max_burn": ("sudo_set_max_burn", True), "bonds_moving_avg": ("sudo_set_bonds_moving_average", False), "max_regs_per_block": ("sudo_set_max_registrations_per_block", True), "serving_rate_limit": ("sudo_set_serving_rate_limit", False), diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7f21f90d4..2bd0057bd 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -238,25 +238,25 @@ async def get_stake_for_coldkey_and_hotkey( :return: Balance: The stake under the coldkey - hotkey pairing. """ - alpha_shares = await self.query( - module="SubtensorModule", - storage_function="Alpha", - params=[hotkey_ss58, coldkey_ss58, netuid], - block_hash=block_hash, - ) - - hotkey_alpha = await self.query( - module="SubtensorModule", - storage_function="TotalHotkeyAlpha", - params=[hotkey_ss58, netuid], - block_hash=block_hash, - ) - - hotkey_shares = await self.query( - module="SubtensorModule", - storage_function="TotalHotkeyShares", - params=[hotkey_ss58, netuid], - block_hash=block_hash, + alpha_shares, hotkey_alpha, hotkey_shares = await asyncio.gather( + self.query( + module="SubtensorModule", + storage_function="Alpha", + params=[hotkey_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + ), + self.query( + module="SubtensorModule", + storage_function="TotalHotkeyAlpha", + params=[hotkey_ss58, netuid], + block_hash=block_hash, + ), + self.query( + module="SubtensorModule", + storage_function="TotalHotkeyShares", + params=[hotkey_ss58, netuid], + block_hash=block_hash, + ), ) alpha_shares_as_float = fixed_to_float(alpha_shares or 0) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 992c2a2e3..ca95b1b45 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -558,7 +558,10 @@ def format_error_message(error_message: Union[dict, Exception]) -> str: err_type = error_message.get("type", err_type) err_name = error_message.get("name", err_name) err_docs = error_message.get("docs", [err_description]) - err_description = err_docs[0] if err_docs else err_description + if isinstance(err_docs, list): + err_description = " ".join(err_docs) + else: + err_description = err_docs return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index f5eb939c3..fd050bd0a 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -2320,14 +2320,21 @@ async def get_start_schedule( if not await subtensor.subnet_exists(netuid): print_error(f"Subnet {netuid} does not exist.") return None - - registration_block = await subtensor.query( - module="SubtensorModule", - storage_function="NetworkRegisteredAt", - params=[netuid], + block_hash = await subtensor.substrate.get_chain_head() + registration_block, min_blocks_to_start, current_block = await asyncio.gather( + subtensor.query( + module="SubtensorModule", + storage_function="NetworkRegisteredAt", + params=[netuid], + block_hash=block_hash, + ), + subtensor.substrate.get_constant( + module_name="SubtensorModule", + constant_name="DurationOfStartCall", + block_hash=block_hash, + ), + subtensor.substrate.get_block_number(block_hash=block_hash), ) - min_blocks_to_start = Constants.emission_start_schedule - current_block = await subtensor.substrate.get_block_number() potential_start_block = registration_block + min_blocks_to_start if current_block < potential_start_block: @@ -2412,7 +2419,9 @@ async def start_subnet( else: error_msg = format_error_message(await response.error_message) if "FirstEmissionBlockNumberAlreadySet" in error_msg: - console.print(f"[dark_sea_green3]Subnet {netuid} already has an emission schedule.[/dark_sea_green3]") + console.print( + f"[dark_sea_green3]Subnet {netuid} already has an emission schedule.[/dark_sea_green3]" + ) return True await get_start_schedule(subtensor, netuid) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index e5502714a..87a54d520 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -145,6 +145,7 @@ async def set_hyperparameter_extrinsic( value: Optional[Union[str, bool, float, list[float]]], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, + prompt: bool = True, ) -> bool: """Sets a hyperparameter for a specific subnetwork. @@ -190,7 +191,7 @@ async def set_hyperparameter_extrinsic( ":cross_mark: [red]Invalid hyperparameter specified.[/red]" ) return False - if sudo_: + if sudo_ and prompt: if not Confirm.ask( "This hyperparam is only settable by root sudo users. If you are not, this will fail. Please confirm" ): @@ -574,6 +575,7 @@ async def sudo_set_hyperparameter( netuid: int, param_name: str, param_value: Optional[str], + prompt: bool, json_output: bool, ): """Set subnet hyperparameters.""" @@ -602,7 +604,7 @@ async def sudo_set_hyperparameter( ) return False success = await set_hyperparameter_extrinsic( - subtensor, wallet, netuid, param_name, value + subtensor, wallet, netuid, param_name, value, prompt=prompt ) if json_output: return success diff --git a/pyproject.toml b/pyproject.toml index c9843b421..240c6f892 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.4.0" +version = "9.4.1" description = "Bittensor CLI" readme = "README.md" authors = [ @@ -19,6 +19,7 @@ dependencies = [ "async-substrate-interface>=1.1.0", "aiohttp~=3.10.2", "backoff~=2.2.1", + "click<8.2.0", # typer.testing.CliRunner(mix_stderr=) is broken in click 8.2.0+ "GitPython>=3.0.0", "fuzzywuzzy~=0.18.0", "netaddr~=1.3.0", diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 0877f64be..bd7350616 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -339,6 +339,7 @@ def test_staking(local_chain, wallet_setup): "max_burn", "--value", "10000000000", # In RAO, TAO = 10 + "--no-prompt", ], ) assert ( diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 02b25489c..4db52d2b5 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -6,6 +6,7 @@ from btcli.tests.e2e_tests.utils import set_storage_extrinsic + def test_unstaking(local_chain, wallet_setup): """ Test various unstaking scenarios including partial unstake, unstake all alpha, and unstake all. @@ -38,16 +39,22 @@ def test_unstaking(local_chain, wallet_setup): # Call to make Alice root owner items = [ - ( - bytes.fromhex("658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000"), - bytes.fromhex("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d") + ( + bytes.fromhex( + "658faa385070e074c85bf6b568cf055536e3e82152c8758267395fe524fbbd160000" + ), + bytes.fromhex( + "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + ), ) ] - asyncio.run(set_storage_extrinsic( - local_chain, - wallet=wallet_alice, - items=items, - )) + asyncio.run( + set_storage_extrinsic( + local_chain, + wallet=wallet_alice, + items=items, + ) + ) # Create first subnet (netuid = 2) result = exec_command_alice(