diff --git a/CHANGELOG.md b/CHANGELOG.md index 29dcf853..598bba94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 9.11.0 /2025-09-05 +* Better arg naming + type annotations by @thewhaleking in https://github.com/opentensor/btcli/pull/590 +* disk cache in config by @thewhaleking in https://github.com/opentensor/btcli/pull/588 +* Unstake no prompts by @thewhaleking in https://github.com/opentensor/btcli/pull/591 +* expand readme by @thewhaleking in https://github.com/opentensor/btcli/pull/598 +* Better arg formatting for readability by @thewhaleking in https://github.com/opentensor/btcli/pull/592 +* Update childkey proportion CLI argument in docs by @HudsonGraeme in https://github.com/opentensor/btcli/pull/602 +* Update example for show command by @HudsonGraeme in https://github.com/opentensor/btcli/pull/604 +* New cmd: `btcli utils latency` by @thewhaleking in https://github.com/opentensor/btcli/pull/599 +* Fix: Swap hotkey - new hotkey reg check by @ibraheem-abe in https://github.com/opentensor/btcli/pull/608 +* Update cli.py to remove double negative typo by @calebcgates in https://github.com/opentensor/btcli/pull/606 +* Better shows hotkeypubs in `w list` by @thewhaleking in https://github.com/opentensor/btcli/pull/611 +* subnet symbol set command by @thewhaleking in https://github.com/opentensor/btcli/pull/613 +* Debug log by @thewhaleking in https://github.com/opentensor/btcli/pull/597 +* Debug log additional by @thewhaleking in https://github.com/opentensor/btcli/pull/615 + +### New Contributors +* @calebcgates made their first contribution in https://github.com/opentensor/btcli/pull/606 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.10.1...v9.11.0 + ## 9.10.2 /2025-09-05 * Fixes swap-hotkey in by @ibraheem-abe in https://github.com/opentensor/btcli/commit/aec630ec06fb679957c1c2940f37a28751fd427f diff --git a/README.md b/README.md index 5d899fba..e3d8d38a 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,13 @@ You can set the commonly used values, such as your hotkey and coldkey names, the The default location of the config file is: `~/.bittensor/config.yml`. An example of a `config.yml` is shown below: ```yaml -chain: ws://127.0.0.1:9945 network: local -no_cache: False -wallet_hotkey: hotkey-user1 +use_cache: true +dashboard_path: null +disk_cache: false +rate_tolerance: null +safe_staking: true +wallet_hotkey: default wallet_name: coldkey-user1 wallet_path: ~/.bittensor/wallets metagraph_cols: @@ -165,6 +168,29 @@ metagraph_cols: btcli config --help ``` +### ENV VARS +BTCLI accepts a few environment variables that can alter how it works: + - USE_TORCH (default 0): If set to 1, will use torch instead of numpy + - DISK_CACHE (default 0, also settable in config): If set to 1 (or set in config), will use disk caching for various safe-cachable substrate +calls (such as block number to block hash mapping), which can speed up subsequent calls. + - BTCLI_CONFIG_PATH (default `~/.bittensor/config.yml`): This will set the config file location, creating if it does not exist. + - BTCLI_DEBUG_FILE (default `~/.bittensor/debug.txt`): The file stores the most recent's command's debug log. + +--- + +## Debugging +BTCLI will store a debug log for every command run. This file is overwritten for each new command run. The default location +of this file is `~/.bittensor/debug.txt` and can be set with the `BTCLI_DEBUG_FILE` env var (see above section). + +The debug log will **NOT** contain any sensitive data (private keys), and is intended to be sent to the developers +for debugging. This file contains basic information about the command being run, the config, and the back and forth of requests and responses +to and from the chain. + +If you encounter an issue, and would like to save the file somewhere it won't be overwritten, run `btcli --debug`, +and set the save file location. We recommend doing this first before anything, and then starting your debugging with +us on our [Discord](https://discord.gg/bittensor), or by opening an issue on [GitHub](https://github.com/opentensor/btcli/issues/new) +(where you can also upload your debug file). + --- ## License diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6fddc03d..88ae4058 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4,6 +4,7 @@ import curses import importlib import json +import logging import os.path import re import ssl @@ -12,7 +13,7 @@ import warnings from dataclasses import fields from pathlib import Path -from typing import Coroutine, Optional, Union +from typing import Coroutine, Optional, Union, Literal import numpy as np import rich @@ -42,7 +43,10 @@ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + best_connection, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -86,11 +90,19 @@ class GitError(Exception): pass +logger = logging.getLogger("btcli") _epilog = "Made with [bold red]:heart:[/bold red] by The Openτensor Foundaτion" np.set_printoptions(precision=8, suppress=True, floatmode="fixed") +def arg__(arg_name: str) -> str: + """ + Helper function to 'arg' format a string for rich console + """ + return f"[{COLORS.G.ARG}]{arg_name}[/{COLORS.G.ARG}]" + + class Options: """ Re-usable typer args @@ -592,6 +604,42 @@ def commands_callback(value: bool): if value: cli = CLIManager() console.print(cli.generate_command_tree()) + + +def debug_callback(value: bool): + if value: + debug_file_loc = Path( + os.getenv("BTCLI_DEBUG_FILE") + or os.path.expanduser(defaults.config.debug_file_path) + ) + if not debug_file_loc.exists(): + err_console.print( + f"[red]Error: The debug file '{arg__(str(debug_file_loc))}' does not exist. This indicates that you have" + f" not run a command which has logged debug output, or you deleted this file. Debug logging only occurs" + f" if {arg__('use_cache')} is set to True in your config ({arg__('btcli config set')}). If the debug " + f"file was created using the {arg__('BTCLI_DEBUG_FILE')} environment variable, please set the value for" + f" the same location, and re-run this {arg__('btcli --debug')} command.[/red]" + ) + raise typer.Exit() + save_file_loc_ = Prompt.ask( + "Enter the file location to save the debug log for the previous command.", + default="~/.bittensor/debug-export", + ).strip() + save_file_loc = Path(os.path.expanduser(save_file_loc_)) + if not save_file_loc.parent.exists(): + if Confirm.ask( + f"The directory '{save_file_loc.parent}' does not exist. Would you like to create it?" + ): + save_file_loc.parent.mkdir(parents=True, exist_ok=True) + try: + with ( + open(save_file_loc, "w+") as save_file, + open(debug_file_loc, "r") as current_file, + ): + save_file.write(current_file.read()) + console.print(f"Saved debug log to {save_file_loc}") + except FileNotFoundError as e: + print_error(str(e)) raise typer.Exit() @@ -612,7 +660,7 @@ class CLIManager: wallet_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer - utils_app = typer.Typer(epilog=_epilog) + utils_app: typer.Typer view_app: typer.Typer asyncio_runner = asyncio @@ -623,6 +671,7 @@ def __init__(self): "wallet_hotkey": None, "network": None, "use_cache": True, + "disk_cache": False, "rate_tolerance": None, "safe_staking": True, "allow_partial_stake": False, @@ -664,6 +713,9 @@ def __init__(self): self.config_path = os.getenv("BTCLI_CONFIG_PATH") or os.path.expanduser( defaults.config.path ) + self.debug_file_path = os.getenv("BTCLI_DEBUG_FILE") or os.path.expanduser( + defaults.config.debug_file_path + ) self.app = typer.Typer( rich_markup_mode="rich", @@ -674,8 +726,8 @@ def __init__(self): self.config_app = typer.Typer( epilog=_epilog, help=f"Allows for getting/setting the config. " - f"Default path for the config file is [{COLORS.G.ARG}]{defaults.config.path}[/{COLORS.G.ARG}]. " - f"You can set your own with the env var [{COLORS.G.ARG}]BTCLI_CONFIG_PATH[/{COLORS.G.ARG}]", + f"Default path for the config file is {arg__(defaults.config.path)}. " + f"You can set your own with the env var {arg__('BTCLI_CONFIG_PATH')}", ) self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) @@ -684,6 +736,7 @@ def __init__(self): self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) + self.utils_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -758,7 +811,7 @@ def __init__(self): # utils app self.app.add_typer( - self.utils_app, name="utils", no_args_is_help=True, hidden=True + self.utils_app, name="utils", no_args_is_help=True, hidden=False ) # view app @@ -948,6 +1001,9 @@ def __init__(self): self.subnets_app.command( "check-start", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_check_start) + self.subnets_app.command( + "set-symbol", rich_help_panel=HELP_PANELS["SUBNETS"]["IDENTITY"] + )(self.subnets_set_symbol) # weights commands self.weights_app.command( @@ -1040,6 +1096,10 @@ def __init__(self): "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] )(self.liquidity_remove) + # utils app + self.utils_app.command("convert")(self.convert) + self.utils_app.command("latency")(self.best_connection) + def generate_command_tree(self) -> Tree: """ Generates a rich.Tree of the commands, subcommands, and groups of this app @@ -1092,6 +1152,7 @@ def initialize_chain( "Verify this is intended.", ) if not self.subtensor: + use_disk_cache = self.config.get("disk_cache", False) if network: network_ = None for item in network: @@ -1105,18 +1166,24 @@ def initialize_chain( if not_selected_networks: console.print( f"Networks not selected: " - f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" + f"{arg__(', '.join(not_selected_networks))}" ) - self.subtensor = SubtensorInterface(network_) + self.subtensor = SubtensorInterface( + network_, use_disk_cache=use_disk_cache + ) elif self.config["network"]: console.print( f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" f"[/{COLORS.G.LINKS}] from config" ) - self.subtensor = SubtensorInterface(self.config["network"]) + self.subtensor = SubtensorInterface( + self.config["network"], use_disk_cache=use_disk_cache + ) else: - self.subtensor = SubtensorInterface(defaults.subtensor.network) + self.subtensor = SubtensorInterface( + defaults.subtensor.network, use_disk_cache=use_disk_cache + ) return self.subtensor def _run_command(self, cmd: Coroutine, exit_early: bool = True): @@ -1179,10 +1246,18 @@ def main_callback( "--commands", callback=commands_callback, help="Show BTCLI commands" ), ] = None, + debug_log: Annotated[ + Optional[bool], + typer.Option( + "--debug", + callback=debug_callback, + help="Saves the debug log from the last used command", + ), + ] = None, ): """ Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be - overriden by passing them explicitly in the command line. + overridden by passing them explicitly in the command line. """ # Load or create the config file if os.path.exists(self.config_path): @@ -1206,6 +1281,9 @@ def main_callback( if sub_key not in config[key]: config[key][sub_key] = sub_value updated = True + elif isinstance(value, bool) and config[key] is None: + config[key] = value + updated = True if updated: with open(self.config_path, "w") as f: safe_dump(config, f) @@ -1213,6 +1291,27 @@ def main_callback( for k, v in config.items(): if k in self.config.keys(): self.config[k] = v + if self.config.get("use_cache", False): + with open(self.debug_file_path, "w+") as f: + f.write( + f"BTCLI {__version__}\n" + f"Async-Substrate-Interface: {importlib.metadata.version('async-substrate-interface')}\n" + f"Bittensor-Wallet: {importlib.metadata.version('bittensor-wallet')}\n" + f"Command: {' '.join(sys.argv)}\n" + f"Config: {self.config}\n" + f"Python: {sys.version}\n" + f"System: {sys.platform}\n\n" + ) + asi_logger = logging.getLogger("async_substrate_interface") + asi_logger.setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(name)s - %(module)s:%(lineno)d - %(message)s" + ) + handler = logging.FileHandler(self.debug_file_path) + handler.setFormatter(formatter) + asi_logger.addHandler(handler) + logger.addHandler(handler) def verbosity_handler( self, quiet: bool, verbose: bool, json_output: bool = False @@ -1273,6 +1372,13 @@ def set_config( help="Disable caching of some commands. This will disable the `--reuse-last` and `--html` flags on " "commands such as `subnets metagraph`, `stake show` and `subnets list`.", ), + disk_cache: Optional[bool] = typer.Option( + None, + "--disk-cache/--no-disk-cache", + " /--no-disk-cache", + help="Enables or disables the caching on disk. Enabling this can significantly speed up commands run " + "sequentially", + ), rate_tolerance: Optional[float] = typer.Option( None, "--tolerance", @@ -1319,12 +1425,13 @@ def set_config( "wallet_hotkey": wallet_hotkey, "network": network, "use_cache": use_cache, + "disk_cache": disk_cache, "rate_tolerance": rate_tolerance, "safe_staking": safe_staking, "allow_partial_stake": allow_partial_stake, "dashboard_path": dashboard_path, } - bools = ["use_cache", "safe_staking", "allow_partial_stake"] + bools = ["use_cache", "disk_cache", "safe_staking", "allow_partial_stake"] if all(v is None for v in args.values()): # Print existing configs self.get_config() @@ -1373,8 +1480,7 @@ def set_config( if n := args.get("network"): if n in Constants.networks: if not Confirm.ask( - f"You provided a network [{COLORS.G.ARG}]{n}[/{COLORS.G.ARG}] which is mapped to " - f"[{COLORS.G.ARG}]{Constants.network_map[n]}[/{COLORS.G.ARG}]\n" + f"You provided a network {arg__(n)} which is mapped to {arg__(Constants.network_map[n])}\n" "Do you want to continue?" ): typer.Exit() @@ -1389,14 +1495,13 @@ def set_config( ) args["network"] = known_network if not Confirm.ask( - f"You provided an endpoint [{COLORS.G.ARG}]{n}[/{COLORS.G.ARG}] which is mapped to " - f"[{COLORS.G.ARG}]{known_network}[/{COLORS.G.ARG}]\n" + f"You provided an endpoint {arg__(n)} which is mapped to {arg__(known_network)}\n" "Do you want to continue?" ): raise typer.Exit() else: if not Confirm.ask( - f"You provided a chain endpoint URL [{COLORS.G.ARG}]{n}[/{COLORS.G.ARG}]\n" + f"You provided a chain endpoint URL {arg__(n)}\n" "Do you want to continue?" ): raise typer.Exit() @@ -1406,6 +1511,7 @@ def set_config( for arg, val in args.items(): if val is not None: + logger.debug(f"Config: setting {arg} to {val}") self.config[arg] = val with open(self.config_path, "w") as f: safe_dump(self.config, f) @@ -1470,17 +1576,12 @@ def del_config( if not any(args.values()): for arg in args.keys(): if self.config.get(arg) is not None: - if Confirm.ask( - f"Do you want to clear the [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config?" - ): + if Confirm.ask(f"Do you want to clear the {arg__(arg)} config?"): + logger.debug(f"Config: clearing {arg}.") self.config[arg] = None - console.print( - f"Cleared [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config and set to 'None'." - ) + console.print(f"Cleared {arg__(arg)} config and set to 'None'.") else: - console.print( - f"Skipped clearing [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config." - ) + console.print(f"Skipped clearing {arg__(arg)} config.") else: # Check each specified argument @@ -1488,21 +1589,19 @@ def del_config( if should_clear: if self.config.get(arg) is not None: if Confirm.ask( - f"Do you want to clear the [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}]" + f"Do you want to clear the {arg__(arg)}" f" [bold cyan]({self.config.get(arg)})[/bold cyan] config?" ): self.config[arg] = None + logger.debug(f"Config: clearing {arg}.") console.print( - f"Cleared [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config and set to 'None'." + f"Cleared {arg__(arg)} config and set to 'None'." ) else: - console.print( - f"Skipped clearing [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config." - ) + console.print(f"Skipped clearing {arg__(arg)} config.") else: console.print( - f"No config set for [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}]." - f" Use [{COLORS.G.ARG}]`btcli config set`[/{COLORS.G.ARG}] to set it." + f"No config set for {arg__(arg)}. Use {arg__('btcli config set')} to set it." ) with open(self.config_path, "w") as f: safe_dump(self.config, f) @@ -1518,8 +1617,7 @@ def get_config(self): Column("[bold white]Value", style="gold1"), Column("", style="medium_purple"), box=box.SIMPLE_HEAD, - title=f"[{COLORS.G.HEADER}]BTCLI Config[/{COLORS.G.HEADER}]: " - f"[{COLORS.G.ARG}]{self.config_path}[/{COLORS.G.ARG}]", + title=f"[{COLORS.G.HEADER}]BTCLI Config[/{COLORS.G.HEADER}]: {arg__(self.config_path)}", ) for key, value in self.config.items(): @@ -1593,26 +1691,31 @@ def ask_safe_staking( bool: Safe staking setting """ if safe_staking is not None: + enabled = "enabled" if safe_staking else "disabled" console.print( - f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan]." + f"[dim][blue]Safe staking[/blue]: [bold cyan]{enabled}[/bold cyan]." ) + logger.debug(f"Safe staking {enabled}") return safe_staking elif self.config.get("safe_staking") is not None: safe_staking = self.config["safe_staking"] + enabled = "enabled" if safe_staking else "disabled" console.print( - f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] (from config)." + f"[dim][blue]Safe staking[/blue]: [bold cyan]{enabled}[/bold cyan] (from config)." ) + logger.debug(f"Safe staking {enabled}") return safe_staking else: safe_staking = True console.print( "[dim][blue]Safe staking[/blue]: " - + f"[bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] " - + "by default. Set this using " - + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " - + "or " - + "[dark_sea_green3 italic]`--safe/--unsafe`[/dark_sea_green3 italic] flag[/dim]" + f"[bold cyan]enabled[/bold cyan] " + "by default. Set this using " + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + "or " + "[dark_sea_green3 italic]`--safe/--unsafe`[/dark_sea_green3 italic] flag[/dim]" ) + logger.debug(f"Safe staking enabled.") return safe_staking def ask_partial_stake( @@ -1629,25 +1732,31 @@ def ask_partial_stake( bool: Partial stake setting """ if allow_partial_stake is not None: + partial_staking = "enabled" if allow_partial_stake else "disabled" console.print( - f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan]." + f"[dim][blue]Partial staking[/blue]: [bold cyan]{partial_staking}[/bold cyan]." ) + logger.debug(f"Partial staking {partial_staking}") return allow_partial_stake elif self.config.get("allow_partial_stake") is not None: config_partial = self.config["allow_partial_stake"] + partial_staking = "enabled" if allow_partial_stake else "disabled" console.print( - f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if config_partial else 'disabled'}[/bold cyan] (from config)." + f"[dim][blue]Partial staking[/blue]: [bold cyan]{partial_staking}[/bold cyan] (from config)." ) + logger.debug(f"Partial staking {partial_staking}") return config_partial else: + partial_staking = "enabled" if allow_partial_stake else "disabled" console.print( "[dim][blue]Partial staking[/blue]: " - + f"[bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan] " - + "by default. Set this using " - + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " - + "or " - + "[dark_sea_green3 italic]`--partial/--no-partial`[/dark_sea_green3 italic] flag[/dim]" + f"[bold cyan]{partial_staking}[/bold cyan] " + "by default. Set this using " + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + "or " + "[dark_sea_green3 italic]`--partial/--no-partial`[/dark_sea_green3 italic] flag[/dim]" ) + logger.debug(f"Partial staking {partial_staking}") return False def wallet_ask( @@ -1655,7 +1764,7 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: Optional[list[str]] = None, + ask_for: Optional[list[Literal[WO.NAME, WO.PATH, WO.HOTKEY]]] = None, validate: WV = WV.WALLET, return_wallet_and_hotkey: bool = False, ) -> Union[Wallet, tuple[Wallet, str]]: @@ -1720,13 +1829,14 @@ def wallet_ask( if wallet_path: wallet_path = os.path.expanduser(wallet_path) wallet = Wallet(name=wallet_name, path=wallet_path, hotkey=wallet_hotkey) + logger.debug(f"Using wallet {wallet}") # Validate the wallet if required if validate == WV.WALLET or validate == WV.WALLET_AND_HOTKEY: valid = utils.is_valid_wallet(wallet) if not valid[0]: utils.err_console.print( - f"[red]Error: Wallet does not not exist. \n" + f"[red]Error: Wallet does not exist. \n" f"Please verify your wallet information: {wallet}[/red]" ) raise typer.Exit() @@ -1875,6 +1985,15 @@ def wallet_overview( str, "Hotkeys names must be a comma-separated list, e.g., `--exclude-hotkeys hk1,hk2`.", ) + logger.debug( + "args:\n" + f"all_wallets: {all_wallets}\n" + f"sort_by: {sort_by}\n" + f"sort_order: {sort_order}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"netuids: {netuids}\n" + ) return self._run_command( wallets.overview( @@ -1964,6 +2083,15 @@ def wallet_transfer( amount = 0 elif not amount: amount = FloatPrompt.ask("Enter amount (in TAO) to transfer.") + logger.debug( + "args:\n" + f"destination: {destination_ss58_address}\n" + f"amount: {amount}\n" + f"transfer_all: {transfer_all}\n" + f"allow_death: {allow_death}\n" + f"period: {period}\n" + f"prompt: {prompt}\n" + ) return self._run_command( wallets.transfer( wallet=wallet, @@ -2033,6 +2161,13 @@ def wallet_swap_hotkey( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + "args:\n" + f"original_wallet: {original_wallet}\n" + f"new_wallet: {new_wallet}\n" + f"netuid: {netuid}\n" + f"prompt: {prompt}\n" + ) self.initialize_chain(network) return self._run_command( wallets.swap_hotkey( @@ -2196,6 +2331,17 @@ def wallet_faucet( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) + logger.debug( + "args:\n" + f"network {network}\n" + f"threads_per_block {threads_per_block}\n" + f"update_interval {update_interval}\n" + f"processors {processors}\n" + f"use_cuda {use_cuda}\n" + f"dev_id {dev_id}\n" + f"output_in_place {output_in_place}\n" + f"max_successes {max_successes}\n" + ) return self._run_command( wallets.faucet( wallet, @@ -2263,6 +2409,7 @@ def wallet_regen_coldkey( mnemonic, seed, json_path, json_password = get_creation_data( mnemonic, seed, json_path, json_password ) + # logger.debug should NOT be used here, it's simply too risky return self._run_command( wallets.regen_coldkey( wallet, @@ -2332,6 +2479,7 @@ def wallet_regen_coldkey_pub( ): rich.print("[red]Error: Invalid SS58 address or public key![/red]") return + # do not logger.debug any creation cmds return self._run_command( wallets.regen_coldkey_pub( wallet, ss58_address, public_key_hex, overwrite, json_output @@ -2384,6 +2532,7 @@ def wallet_regen_hotkey( mnemonic, seed, json_path, json_password = get_creation_data( mnemonic, seed, json_path, json_password ) + # do not logger.debug any creation cmds return self._run_command( wallets.regen_hotkey( wallet, @@ -2453,6 +2602,7 @@ def wallet_regen_hotkey_pub( ): rich.print("[red]Error: Invalid SS58 address or public key![/red]") return False + # do not logger.debug any creation cmds return self._run_command( wallets.regen_hotkey_pub( wallet, ss58_address, public_key_hex, overwrite, json_output @@ -2517,6 +2667,7 @@ def wallet_new_hotkey( ) if not uri: n_words = get_n_words(n_words) + # do not logger.debug any creation cmds return self._run_command( wallets.new_hotkey( wallet, n_words, use_password, uri, overwrite, json_output @@ -2583,7 +2734,13 @@ def wallet_associate_hotkey( f"hotkey [blue]{wallet_hotkey}[/blue] " f"[{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" ) - + logger.debug( + "args:\n" + f"network {network}\n" + f"hotkey_ss58 {hotkey_ss58}\n" + f"hotkey_display {hotkey_display}\n" + f"prompt {prompt}\n" + ) return self._run_command( wallets.associate_hotkey( wallet, @@ -2731,7 +2888,8 @@ def wallet_check_ck_swap( if not scheduled_block: block_input = Prompt.ask( - "[blue]Enter the block number[/blue] where the swap was scheduled [dim](optional, press enter to skip)[/dim]", + "[blue]Enter the block number[/blue] where the swap was scheduled " + "[dim](optional, press enter to skip)[/dim]", default="", ) if block_input: @@ -2740,7 +2898,12 @@ def wallet_check_ck_swap( except ValueError: print_error("Invalid block number") raise typer.Exit() - + logger.debug( + "args:\n" + f"scheduled_block {scheduled_block}\n" + f"ss58_address {ss58_address}\n" + f"network {network}\n" + ) return self._run_command( wallets.check_swap_status(self.subtensor, ss58_address, scheduled_block) ) @@ -2797,6 +2960,7 @@ def wallet_create_wallet( ) if not uri: n_words = get_n_words(n_words) + # do not logger.debug any creation commands return self._run_command( wallets.wallet_create( wallet, n_words, use_password, uri, overwrite, json_output @@ -2905,6 +3069,12 @@ def wallet_balance( ask_for=ask_for, validate=validate, ) + logger.debug( + "args:\n" + f"all_balances {all_balances}\n" + f"ss58_addresses {ss58_addresses}\n" + f"network {network}" + ) subtensor = self.initialize_chain(network) return self._run_command( wallets.wallet_balance( @@ -3055,6 +3225,7 @@ def wallet_set_id( additional, github_repo, ) + logger.debug(f"args:\nidentity {identity}\nnetwork {network}\n") return self._run_command( wallets.set_id( @@ -3316,7 +3487,12 @@ def wallet_swap_coldkey( f"[dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" ) new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address - + logger.debug( + "args:\n" + f"network {network}\n" + f"new_coldkey_ss58 {new_wallet_coldkey_ss58}\n" + f"force_swap {force_swap}" + ) return self._run_command( wallets.schedule_coldkey_swap( wallet=wallet, @@ -3388,7 +3564,13 @@ def stake_list( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) - + logger.debug( + "args:\n" + f"coldkey_ss58 {coldkey_ss58}\n" + f"network {network}\n" + f"live: {live}\n" + f"no_prompt: {no_prompt}\n" + ) return self._run_command( list_stake.stake_list( wallet, @@ -3637,6 +3819,7 @@ def stake_add( ), exit_early=False, ) + logger.debug(f"Free balance: {free_balance}") if free_balance == Balance.from_tao(0): print_error("You dont have any balance to stake.") return @@ -3657,7 +3840,21 @@ def stake_add( f"You dont have enough balance to stake. Current free Balance: {free_balance}." ) raise typer.Exit() - + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuids: {netuids}\n" + f"stake_all: {stake_all}\n" + f"amount: {amount}\n" + f"prompt: {prompt}\n" + f"all_hotkeys: {all_hotkeys}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"safe_staking: {safe_staking}\n" + f"rate_tolerance: {rate_tolerance}\n" + f"allow_partial_stake: {allow_partial_stake}\n" + f"period: {period}\n" + ) return self._run_command( add_stake.stake_add( wallet, @@ -3782,11 +3979,11 @@ def stake_remove( "Interactive mode cannot be used with hotkey selection options like " "--include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." ) - raise typer.Exit() + return False if unstake_all and unstake_all_alpha: print_error("Cannot specify both unstake-all and unstake-all-alpha.") - raise typer.Exit() + return False if not interactive and not unstake_all and not unstake_all_alpha: netuid = get_optional_netuid(netuid, all_netuids) @@ -3795,23 +3992,39 @@ def stake_remove( "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" " should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." ) - raise typer.Exit() + return False if include_hotkeys and exclude_hotkeys: print_error( "You have specified both including and excluding hotkeys options. Select one or the other." ) - raise typer.Exit() + return False if unstake_all and amount: print_error( "Cannot specify both a specific amount and 'unstake-all'. Choose one or the other." ) - raise typer.Exit() + return False if amount and amount <= 0: print_error(f"You entered an incorrect unstake amount: {amount}") - raise typer.Exit() + return False + + if include_hotkeys: + include_hotkeys = parse_to_list( + include_hotkeys, + str, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--include-hotkeys hk1,hk2`.", + is_ss58=False, + ) + + if exclude_hotkeys: + exclude_hotkeys = parse_to_list( + exclude_hotkeys, + str, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", + is_ss58=False, + ) if ( not wallet_hotkey @@ -3828,7 +4041,8 @@ def stake_remove( default=self.config.get("wallet_name") or defaults.wallet.name, ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim](or Press Enter to view existing staked hotkeys)[/dim]", + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim]" + "(or Press Enter to view existing staked hotkeys)[/dim]", ) if hotkey_or_ss58 == "": wallet = self.wallet_ask( @@ -3859,12 +4073,12 @@ def stake_remove( if include_hotkeys: if len(include_hotkeys) > 1: print_error("Cannot unstake_all from multiple hotkeys at once.") - raise typer.Exit() + return False elif is_valid_ss58_address(include_hotkeys[0]): hotkey_ss58_address = include_hotkeys[0] else: print_error("Invalid hotkey ss58 address.") - raise typer.Exit() + return False elif all_hotkeys: wallet = self.wallet_ask( wallet_name, @@ -3875,7 +4089,8 @@ def stake_remove( else: if not hotkey_ss58_address and not wallet_hotkey: hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from [dim](or enter 'all' to unstake from all hotkeys)[/dim]", + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from [dim]" + "(or enter 'all' to unstake from all hotkeys)[/dim]", default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) @@ -3907,6 +4122,17 @@ def stake_remove( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"hotkey_ss58_address: {hotkey_ss58_address}\n" + f"unstake_all: {unstake_all}\n" + f"unstake_all_alpha: {unstake_all_alpha}\n" + f"all_hotkeys: {all_hotkeys}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"era: {period}" + ) return self._run_command( remove_stake.unstake_all( wallet=wallet, @@ -3941,22 +4167,38 @@ def stake_remove( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - - if include_hotkeys: - include_hotkeys = parse_to_list( - include_hotkeys, - str, - "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--include-hotkeys hk1,hk2`.", - is_ss58=False, + if not amount and not prompt: + print_error( + f"Ambiguous request! Specify {arg__('--amount')}, {arg__('--all')}, or {arg__('--all-alpha')} " + f"to use {arg__('--no-prompt')}" ) + return False - if exclude_hotkeys: - exclude_hotkeys = parse_to_list( - exclude_hotkeys, - str, - "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", - is_ss58=False, + if not amount and json_output: + json_console.print_json( + data={ + "success": False, + "err_msg": "Ambiguous request! Specify '--amount', '--all', " + "or '--all-alpha' to use '--json-output'", + } ) + return False + logger.debug( + "args:\n" + f"network: {network}\n" + f"hotkey_ss58_address: {hotkey_ss58_address}\n" + f"all_hotkeys: {all_hotkeys}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"amount: {amount}\n" + f"prompt: {prompt}\n" + f"interactive: {interactive}\n" + f"netuid: {netuid}\n" + f"safe_staking: {safe_staking}\n" + f"rate_tolerance: {rate_tolerance}\n" + f"allow_partial_stake: {allow_partial_stake}\n" + f"era: {period}" + ) return self._run_command( remove_stake.unstake( @@ -4121,7 +4363,19 @@ def stake_move( destination_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" ) - + logger.debug( + "args:\n" + f"network: {network}\n" + f"origin_netuid: {origin_netuid}\n" + f"origin_hotkey: {origin_hotkey}\n" + f"destination_hotkey: {destination_hotkey}\n" + f"destination_netuid: {destination_netuid}\n" + f"amount: {amount}\n" + f"stake_all: {stake_all}\n" + f"era: {period}\n" + f"interactive_selection: {interactive_selection}\n" + f"prompt: {prompt}\n" + ) result = self._run_command( move_stake.move_stake( subtensor=self.initialize_chain(network), @@ -4286,7 +4540,18 @@ def stake_transfer( dest_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid)" ) - + logger.debug( + "args:\n" + f"network: {network}\n" + f"origin_hotkey: {origin_hotkey}\n" + f"origin_netuid: {origin_netuid}\n" + f"dest_netuid: {dest_netuid}\n" + f"dest_hotkey: {origin_hotkey}\n" + f"dest_coldkey_ss58: {dest_ss58}\n" + f"amount: {amount}\n" + f"era: {period}\n" + f"stake_all: {stake_all}" + ) result = self._run_command( move_stake.transfer_stake( wallet=wallet, @@ -4394,7 +4659,19 @@ def stake_swap( ) if not amount and not swap_all: amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to swap") - + logger.debug( + "args:\n" + f"network: {network}\n" + f"origin_netuid: {origin_netuid}\n" + f"dest_netuid: {dest_netuid}\n" + f"amount: {amount}\n" + f"swap_all: {swap_all}\n" + f"era: {period}\n" + f"interactive_selection: {interactive_selection}\n" + f"prompt: {prompt}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) result = self._run_command( move_stake.swap_stake( wallet=wallet, @@ -4510,7 +4787,7 @@ def stake_set_children( EXAMPLE - [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 -p 0.3 -p 0.7 + [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 --prop 0.3 --prop 0.7 """ self.verbosity_handler(quiet, verbose, json_output) netuid = get_optional_netuid(netuid, all_netuids) @@ -4543,6 +4820,15 @@ def stake_set_children( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"children: {children}\n" + f"proportions: {proportions}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) return self._run_command( children_hotkeys.set_children( wallet=wallet, @@ -4608,6 +4894,13 @@ def stake_revoke_children( netuid = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) return self._run_command( children_hotkeys.revoke_children( wallet, @@ -4686,6 +4979,14 @@ def stake_childkey_take( netuid = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"take: {take}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) results: list[tuple[Optional[int], bool]] = self._run_command( children_hotkeys.childkey_take( wallet=wallet, @@ -4771,12 +5072,8 @@ def sudo_set( ) return False param_name = "alpha_values" - low_val = FloatPrompt.ask( - f"Enter the new value for [{COLORS.G.ARG}]alpha_low[/{COLORS.G.ARG}]" - ) - high_val = FloatPrompt.ask( - f"Enter the new value for [{COLORS.G.ARG}]alpha_high[/{COLORS.G.ARG}]" - ) + low_val = FloatPrompt.ask(f"Enter the new value for {arg__('alpha_low')}") + high_val = FloatPrompt.ask(f"Enter the new value for {arg__('alpha_high')}") param_value = f"{low_val},{high_val}" if param_name == "yuma_version": if not prompt: @@ -4800,7 +5097,7 @@ def sudo_set( if param_name == "subnet_is_active": err_console.print( f"[{COLORS.SU.HYPERPARAM}]subnet_is_active[/{COLORS.SU.HYPERPARAM}] " - f"is set by using [{COLORS.G.ARG}]`btcli subnets start`[/{COLORS.G.ARG}] command." + f"is set by using {arg__('btcli subnets start')} command." ) return False @@ -4821,6 +5118,13 @@ def sudo_set( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"param_name: {param_name}\n" + f"param_value: {param_value}" + ) result, err_msg = self._run_command( sudo.sudo_set_hyperparameter( wallet, @@ -4941,6 +5245,7 @@ def sudo_senate_vote( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug(f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\n") return self._run_command( sudo.senate_vote( wallet, self.initialize_chain(network), proposal, vote, prompt @@ -4993,7 +5298,7 @@ def sudo_set_take( f"Take value must be between {min_value} and {max_value}. Provided value: {take}" ) raise typer.Exit() - + logger.debug(f"args:\nnetwork: {network}\ntake: {take}") result = self._run_command( sudo.set_take(wallet, self.initialize_chain(network), take) ) @@ -5142,14 +5447,12 @@ def subnets_price( """ if json_output and html_output: print_error( - f"Cannot specify both [{COLORS.G.ARG}]--json-output[/{COLORS.G.ARG}] " - f"and [{COLORS.G.ARG}]--html[/{COLORS.G.ARG}]" + f"Cannot specify both {arg__('--json-output')} and {arg__('--html')}" ) return if current_only and html_output: print_error( - f"Cannot specify both [{COLORS.G.ARG}]--current[/{COLORS.G.ARG}] " - f"and [{COLORS.G.ARG}]--html[/{COLORS.G.ARG}]" + f"Cannot specify both {arg__('--current')} and {arg__('--html')}" ) return self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) @@ -5160,9 +5463,8 @@ def subnets_price( Constants.network_map[x] for x in non_archives ]: err_console.print( - f"[red]Error[/red] Running this command without [{COLORS.G.ARG}]--current[/{COLORS.G.ARG}] requires " - "use of an archive node. " - f"Try running again with the [{COLORS.G.ARG}]--network archive[/{COLORS.G.ARG}] flag." + f"[red]Error[/red] Running this command without {arg__('--current')} requires use of an archive node. " + f"Try running again with the {arg__('--network archive')} flag." ) return False @@ -5225,7 +5527,7 @@ def subnets_show( EXAMPLE - [green]$[/green] btcli subnets list + [green]$[/green] btcli subnets show """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5337,6 +5639,7 @@ def subnets_create( logo_url=logo_url, additional=additional_info, ) + logger.debug(f"args:\nnetwork: {network}\nidentity: {identity}\n") self._run_command( subnets.create( wallet, self.initialize_chain(network), identity, json_output, prompt @@ -5398,6 +5701,7 @@ def subnets_start( ], validate=WV.WALLET, ) + logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\n") return self._run_command( subnets.start_subnet( wallet, @@ -5513,7 +5817,9 @@ def subnets_set_identity( logo_url=logo_url, additional=additional_info, ) - + logger.debug( + f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}" + ) success = self._run_command( subnets.set_identity( wallet, self.initialize_chain(network), netuid, identity, prompt @@ -5651,6 +5957,7 @@ def subnets_register( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\nperiod: {period}\n") return self._run_command( subnets.register( wallet, @@ -5763,6 +6070,55 @@ def subnets_metagraph( ) ) + def subnets_set_symbol( + self, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + json_output: bool = Options.json_output, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + symbol: str = typer.Argument(help="The symbol to set for your subnet."), + ): + """ + Allows the user to update their subnet symbol to a different available symbol. The full list of available symbols can be found here: + [#8CB9E9]https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/symbols.rs#L8[/#8CB9E9] + + + EXAMPLE + + [green]$[/green] btcli subnets set-symbol [dark_orange]--netuid 1 シ[/dark_orange] + + + JSON OUTPUT: + If --json-output is used, the output will be in the following schema: + [#AFEFFF]{success: [dark_orange]bool[/dark_orange], message: [dark_orange]str[/dark_orange]}[/#AFEFFF] + """ + self.verbosity_handler(quiet, verbose, json_output) + if len(symbol) > 1: + err_console.print("Your symbol must be a single character.") + return False + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + return self._run_command( + subnets.set_symbol( + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + symbol=symbol, + prompt=prompt, + json_output=json_output, + ) + ) + def weights_reveal( self, network: Optional[list[str]] = Options.network, @@ -5848,7 +6204,6 @@ def weights_reveal( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - return self._run_command( weights_cmds.reveal_weights( self.initialize_chain(network), @@ -6094,7 +6449,14 @@ def liquidity_add( if price_low >= price_high: err_console.print("The low price must be lower than the high price.") return False - + logger.debug( + f"args:\n" + f"hotkey: {hotkey}\n" + f"netuid: {netuid}\n" + f"liquidity: {liquidity_}\n" + f"price_low: {price_low}\n" + f"price_high: {price_high}\n" + ) return self._run_command( liquidity.add_liquidity( subtensor=self.initialize_chain(network), @@ -6195,6 +6557,14 @@ def liquidity_remove( validate=WV.WALLET, return_wallet_and_hotkey=True, ) + logger.debug( + f"args:\n" + f"network: {network}\n" + f"hotkey: {hotkey}\n" + f"netuid: {netuid}\n" + f"position_id: {position_id}\n" + f"all_liquidity_ids: {all_liquidity_ids}\n" + ) return self._run_command( liquidity.remove_liquidity( subtensor=self.initialize_chain(network), @@ -6259,6 +6629,14 @@ def liquidity_modify( f"[blue]{position_id}[/blue] (can be positive or negative)", negative_allowed=True, ) + logger.debug( + f"args:\n" + f"network: {network}\n" + f"hotkey: {hotkey}\n" + f"netuid: {netuid}\n" + f"position_id: {position_id}\n" + f"liquidity_delta: {liquidity_delta}" + ) return self._run_command( liquidity.modify_liquidity( @@ -6274,7 +6652,6 @@ def liquidity_modify( ) @staticmethod - @utils_app.command("convert") def convert( from_rao: Optional[str] = typer.Option( None, "--rao", help="Convert amount from Rao" @@ -6304,6 +6681,66 @@ def convert( f"{Balance.from_tao(tao).rao}{Balance.rao_unit}", ) + def best_connection( + self, + additional_networks: Optional[list[str]] = typer.Option( + None, + "--network", + help="Network(s) to test for the best connection", + ), + ): + """ + This command will give you the latency of all finney-like network in additional to any additional networks you specify via the '--network' flag + + The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. + + EXAMPLE + + [green]$[/green] btcli utils latency --network ws://189.234.12.45 --network wss://mysubtensor.duckdns.org + + """ + additional_networks = additional_networks or [] + if any(not x.startswith("ws") for x in additional_networks): + err_console.print( + "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint" + f" (starting with [{COLORS.G.LINKS}]ws://[/{COLORS.G.LINKS}] or " + f"[{COLORS.G.LINKS}]wss://[/{COLORS.G.LINKS}]).", + ) + return False + results: dict[str, list[float]] = self._run_command( + best_connection(Constants.lite_nodes + additional_networks) + ) + sorted_results = { + k: v for k, v in sorted(results.items(), key=lambda item: item[1][0]) + } + table = Table( + Column("Network"), + Column("End to End Latency", style="cyan"), + Column("Single Request Ping", style="cyan"), + Column("Chain Head Request Latency", style="cyan"), + title="Connection Latencies (seconds)", + caption="lower value is faster", + ) + for n_name, ( + overall_latency, + single_request, + chain_head, + ) in sorted_results.items(): + table.add_row( + n_name, str(overall_latency), str(single_request), str(chain_head) + ) + console.print(table) + fastest = next(iter(sorted_results.keys())) + if conf_net := self.config.get("network", ""): + if not conf_net.startswith("ws") and conf_net in Constants.networks: + conf_net = Constants.network_map[conf_net] + if conf_net != fastest: + console.print( + f"The fastest network is {fastest}. You currently have {conf_net} selected as your default network." + f"\nYou can update this with {arg__(f'btcli config set --network {fastest}')}" + ) + return True + def run(self): self.app() diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 7cd09ab8..7160cbe2 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -23,6 +23,7 @@ class Constants: dev_entrypoint = "wss://dev.chain.opentensor.ai:443" local_entrypoint = "ws://127.0.0.1:9944" latent_lite_entrypoint = "wss://lite.sub.latent.to:443" + lite_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, @@ -88,12 +89,14 @@ class Defaults: class config: base_path = "~/.bittensor" path = "~/.bittensor/config.yml" + debug_file_path = "~/.bittensor/debug.txt" dictionary = { "network": None, "wallet_path": None, "wallet_name": None, "wallet_hotkey": None, "use_cache": True, + "disk_cache": False, "metagraph_cols": { "UID": True, "GLOBAL_STAKE": True, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 0684b31e..cb3b295f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,21 +1,22 @@ import asyncio import os +import time from typing import Optional, Any, Union, TypedDict, Iterable import aiohttp +from async_substrate_interface.async_substrate import ( + DiskCachedAsyncSubstrateInterface, + AsyncSubstrateInterface, +) +from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.utils.storage import StorageKey from bittensor_wallet import Wallet from bittensor_wallet.bittensor_wallet import Keypair from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall -from async_substrate_interface.errors import SubstrateRequestException import typer +import websockets - -from async_substrate_interface.async_substrate import ( - DiskCachedAsyncSubstrateInterface, - AsyncSubstrateInterface, -) from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, StakeInfo, @@ -42,12 +43,6 @@ get_hotkey_pub_ss58, ) -SubstrateClass = ( - DiskCachedAsyncSubstrateInterface - if os.getenv("DISK_CACHE", "0") == "1" - else AsyncSubstrateInterface -) - class ParamWithTypes(TypedDict): name: str # Name of the parameter. @@ -81,7 +76,7 @@ class SubtensorInterface: Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls. """ - def __init__(self, network): + def __init__(self, network, use_disk_cache: bool = False): if network in Constants.network_map: self.chain_endpoint = Constants.network_map[network] self.network = network @@ -111,8 +106,12 @@ def __init__(self, network): ) self.chain_endpoint = Constants.network_map[defaults.subtensor.network] self.network = defaults.subtensor.network - - self.substrate = SubstrateClass( + substrate_class = ( + DiskCachedAsyncSubstrateInterface + if (use_disk_cache or os.getenv("DISK_CACHE", "0") == "1") + else AsyncSubstrateInterface + ) + self.substrate = substrate_class( url=self.chain_endpoint, ss58_format=SS58_FORMAT, type_registry=TYPE_REGISTRY, @@ -1656,3 +1655,32 @@ async def get_subnet_prices( map_[netuid_] = Balance.from_rao(int(current_price * 1e9)) return map_ + + +async def best_connection(networks: list[str]): + """ + Basic function to compare the latency of a given list of websocket endpoints + Args: + networks: list of network URIs + + Returns: + {network_name: [end_to_end_latency, single_request_latency, chain_head_request_latency]} + + """ + results = {} + for network in networks: + try: + t1 = time.monotonic() + async with websockets.connect(network) as websocket: + pong = await websocket.ping() + latency = await pong + pt1 = time.monotonic() + await websocket.send( + "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" + ) + await websocket.recv() + t2 = time.monotonic() + results[network] = [t2 - t1, latency, t2 - pt1] + except Exception as e: + err_console.print(f"Error attempting network {network}: {e}") + return results diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 8611db3f..1497470a 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -269,10 +269,26 @@ def get_hotkey_wallets_for_wallet( except FileNotFoundError: hotkeys = [] for h_name in hotkeys: - hotkey_for_name = Wallet(path=str(wallet_path), name=wallet.name, hotkey=h_name) + if h_name.endswith("pub.txt"): + if h_name.split("pub.txt")[0] in hotkeys: + continue + else: + hotkey_for_name = Wallet( + path=str(wallet_path), + name=wallet.name, + hotkey=h_name.split("pub.txt")[0], + ) + else: + hotkey_for_name = Wallet( + path=str(wallet_path), name=wallet.name, hotkey=h_name + ) try: + exists = ( + hotkey_for_name.hotkey_file.exists_on_device() + or hotkey_for_name.hotkeypub_file.exists_on_device() + ) if ( - (exists := hotkey_for_name.hotkey_file.exists_on_device()) + exists and not hotkey_for_name.hotkey_file.is_encrypted() # and hotkey_for_name.coldkeypub.ss58_address and get_hotkey_pub_ss58(hotkey_for_name) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 67f0109f..3a37b8cb 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -361,6 +361,7 @@ async def unstake( ) if json_output: json_console.print(json.dumps(successes)) + return True async def unstake_all( diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 38a20d00..8e488a95 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -52,7 +52,17 @@ async def price( step = 300 start_block = max(0, current_block - total_blocks) - block_numbers = list(range(start_block, current_block + 1, step)) + + # snap start block down to nearest multiple of 10 + start_block -= start_block % 10 + + block_numbers = [] + for b in range(start_block, current_block + 1, step): + if b == current_block: + block_numbers.append(b) # exact current block + else: + block_numbers.append(b - (b % 5)) # snap down to multiple of 10 + block_numbers = sorted(set(block_numbers)) # Block hashes block_hash_cors = [ diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index bb42132c..d8571f3f 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -2448,3 +2448,71 @@ async def start_subnet( await get_start_schedule(subtensor, netuid) print_error(f":cross_mark: Failed to start subnet: {error_msg}") return False + + +async def set_symbol( + wallet: "Wallet", + subtensor: "SubtensorInterface", + netuid: int, + symbol: str, + prompt: bool = False, + json_output: bool = False, +) -> bool: + """ + Set a subtensor's symbol, given the netuid and symbol. + + The symbol must be a symbol that subtensor recognizes as available + (defined in https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/symbols.rs#L8) + """ + if not await subtensor.subnet_exists(netuid): + err = f"Subnet {netuid} does not exist." + if json_output: + json_console.print_json(data={"success": False, "message": err}) + else: + err_console.print(err) + return False + + if prompt and not json_output: + sn_info = await subtensor.subnet(netuid=netuid) + if not Confirm.ask( + f"Your current subnet symbol for SN{netuid} is {sn_info.symbol}. Do you want to update it to {symbol}?" + ): + return False + + if not (unlock_status := unlock_key(wallet, print_out=False)).success: + err = unlock_status.message + if json_output: + json_console.print_json(data={"success": False, "message": err}) + else: + console.print(err) + return False + + start_call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="update_symbol", + call_params={"netuid": netuid, "symbol": symbol.encode("utf-8")}, + ) + + signed_ext = await subtensor.substrate.create_signed_extrinsic( + call=start_call, + keypair=wallet.coldkey, + ) + + response = await subtensor.substrate.submit_extrinsic( + extrinsic=signed_ext, + wait_for_inclusion=True, + ) + if await response.is_success: + message = f"Successfully updated SN{netuid}'s symbol to {symbol}." + if json_output: + json_console.print_json(data={"success": True, "message": message}) + else: + console.print(f":white_heavy_check_mark:[dark_sea_green3] {message}\n") + return True + else: + err = format_error_message(await response.error_message) + if json_output: + json_console.print_json(data={"success": False, "message": err}) + else: + err_console.print(f":cross_mark: [red]Failed[/red]: {err}") + return False diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 6d4ae13e..e6ac3118 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -34,6 +34,7 @@ SubtensorInterface, ProposalVoteData, ) + from scalecodec.types import GenericMetadataVersioned # helpers and extrinsics @@ -91,8 +92,8 @@ def search_metadata( param_name: str, value: Union[str, bool, float, list[float]], netuid: int, - metadata, - pallet: str = DEFAULT_PALLET, + metadata: "GenericMetadataVersioned", + pallet_name: str = DEFAULT_PALLET, ) -> tuple[bool, Optional[dict]]: """ Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used @@ -103,7 +104,7 @@ def search_metadata( value: the value to set the hyperparameter netuid: the specified netuid metadata: the subtensor.substrate.metadata - pallet: the name of the module to use for the query. If not set, the default value is DEFAULT_PALLET + pallet_name: the name of the module to use for the query. If not set, the default value is DEFAULT_PALLET Returns: (success, dict of call params) @@ -125,7 +126,7 @@ def type_converter_with_retry(type_, val, arg_name): call_crafter = {"netuid": netuid} - pallet = metadata.get_metadata_pallet(pallet) + pallet = metadata.get_metadata_pallet(pallet_name) for call in pallet.calls: if call.name == param_name: if "netuid" not in [x.name for x in call.args]: diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ff37a50e..7b90641f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -846,12 +846,21 @@ async def wallet_list(wallet_path: str, json_output: bool): data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)" hk_data = {"name": hkey.name, "ss58_address": "?"} if hkey: - hkey_ss58 = get_hotkey_pub_ss58(hkey) + try: + hkey_ss58 = hkey.get_hotkey().ss58_address + pub_only = False + except KeyFileError: + hkey_ss58 = hkey.get_hotkeypub().ss58_address + pub_only = True try: data = ( f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] " - f"ss58_address [green]{hkey_ss58}[/green]\n" + f"ss58_address [green]{hkey_ss58}[/green]" ) + if pub_only: + data += " [blue](hotkeypub only)[/blue]\n" + else: + data += "\n" hk_data["name"] = hkey.hotkey_str hk_data["ss58_address"] = hkey_ss58 except UnicodeDecodeError: @@ -1836,7 +1845,7 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): async def sign( - wallet: Wallet, message: str, use_hotkey: str, json_output: bool = False + wallet: Wallet, message: str, use_hotkey: bool, json_output: bool = False ): """Sign a message using the provided wallet or hotkey.""" diff --git a/pyproject.toml b/pyproject.toml index 67014961..651cdaf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.10.2" +version = "9.11.0" description = "Bittensor CLI" readme = "README.md" authors = [ @@ -15,7 +15,7 @@ scripts = { btcli = "bittensor_cli.cli:main" } requires-python = ">=3.9,<3.14" dependencies = [ "wheel", - "async-substrate-interface>=1.4.2", + "async-substrate-interface>=1.5.2", "aiohttp~=3.10.2", "backoff~=2.2.1", "GitPython>=3.0.0", diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index cd4bf09c..359fbb50 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -10,6 +10,7 @@ * btcli subnets create * btcli subnets set-identity * btcli subnets get-identity +* btcli subnets set-symbol * btcli subnets register * btcli subnets price * btcli stake add @@ -235,6 +236,34 @@ def test_staking(local_chain, wallet_setup): assert get_identity_output["logo_url"] == sn_logo_url assert get_identity_output["additional"] == sn_add_info + # set symbol + set_symbol = exec_command_alice( + "subnets", + "set-symbol", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--json-output", + "--no-prompt", + "シ", + ], + ) + set_symbol_output = json.loads(set_symbol.stdout) + assert set_symbol_output["success"] is True, set_symbol_output + assert set_symbol_output["success"] is True, set_symbol_output + assert ( + set_symbol_output["message"] + == f"Successfully updated SN{netuid}'s symbol to シ." + ) + get_s_price = exec_command_alice( "subnets", "price",