diff --git a/CHANGELOG.md b/CHANGELOG.md index b0200574..660515c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 9.10.0 /2025-08-06 +* Sets default interval hours for subnets price to 4, bc of rate limiting. by @thewhaleking in https://github.com/opentensor/btcli/pull/568 +* Subnets Price --current + improvements by @thewhaleking in https://github.com/opentensor/btcli/pull/569 +* Reconfig Asyncio Runner by @thewhaleking in https://github.com/opentensor/btcli/pull/570 +* Show amount on `transfer --all` by @thewhaleking in https://github.com/opentensor/btcli/pull/571 +* Allows for typer>=0.16 and Click 8.2+ by @thewhaleking in https://github.com/opentensor/btcli/pull/572 +* BTCLI Config Updates by @thewhaleking in https://github.com/opentensor/btcli/pull/573 +* Added info about preinstalled macOS CPython by @thewhaleking in https://github.com/opentensor/btcli/pull/574 +* Click 8.2+/- compatibility by @thewhaleking in https://github.com/opentensor/btcli/pull/576 +* New command: `btcli w regen-hotkeypub` by @thewhaleking in https://github.com/opentensor/btcli/pull/575 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.9.0...v9.10.0 + ## 9.9.0 /2025-07-28 * Feat/wallet verify by @ibraheem-abe in https://github.com/opentensor/btcli/pull/561 * Improved speed of query_all_identities and fetch_coldkey_hotkey_identities by @thewhaleking in https://github.com/opentensor/btcli/pull/560 diff --git a/README.md b/README.md index f572fc4b..5d899fba 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,15 @@ 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, PyPI, or Homebrew. **Make sure you verify your installation after you install**: +You can install `btcli` on your local machine directly from source, PyPI, or Homebrew. +**Make sure you verify your installation after you install**. + +### For macOS users +Note that the macOS preinstalled CPython installation is compiled with LibreSSL instead of OpenSSL. There are a number +of issues with LibreSSL, and as such is not fully supported by the libraries used by btcli. Thus we highly recommend, if +you are using a Mac, to first install Python from [Homebrew](https://brew.sh/). Additionally, the Rust FFI bindings +[if installing from precompiled wheels (default)] require the Homebrew-installed OpenSSL pacakge. If you choose to use +the preinstalled Python version from macOS, things may not work completely. ### Install from [PyPI](https://pypi.org/project/bittensor/) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a11b2ed1..65230eda 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -58,6 +58,7 @@ validate_uri, prompt_for_subnet_identity, validate_rate_tolerance, + get_hotkey_pub_ss58, ) from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds @@ -250,7 +251,7 @@ def edit_help(cls, option_name: str, help_text: str): True, "--prompt/--no-prompt", " /--yes", - "--prompt/--no_prompt", + " /--no_prompt", " /-y", help="Enable or disable interactive prompts.", ) @@ -643,8 +644,21 @@ def __init__(self): # }, } self.subtensor = None + + if sys.version_info < (3, 10): + # For Python 3.9 or lower + self.event_loop = asyncio.new_event_loop() + else: + try: + uvloop = importlib.import_module("uvloop") + self.event_loop = uvloop.new_event_loop() + except ModuleNotFoundError: + self.event_loop = asyncio.new_event_loop() + self.config_base_path = os.path.expanduser(defaults.config.base_path) - self.config_path = os.path.expanduser(defaults.config.path) + self.config_path = os.getenv("BTCLI_CONFIG_PATH") or os.path.expanduser( + defaults.config.path + ) self.app = typer.Typer( rich_markup_mode="rich", @@ -652,7 +666,12 @@ def __init__(self): epilog=_epilog, no_args_is_help=True, ) - self.config_app = typer.Typer(epilog=_epilog) + 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}]", + ) self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) @@ -773,6 +792,9 @@ def __init__(self): self.wallet_app.command( "regen-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_regen_hotkey) + self.wallet_app.command( + "regen-hotkeypub", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + )(self.wallet_regen_hotkey_pub) self.wallet_app.command( "new-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"] )(self.wallet_new_hotkey) @@ -955,6 +977,10 @@ def __init__(self): "regen_hotkey", hidden=True, )(self.wallet_regen_hotkey) + self.wallet_app.command( + "regen_hotkeypub", + hidden=True, + )(self.wallet_regen_hotkey_pub) self.wallet_app.command( "new_hotkey", hidden=True, @@ -1079,11 +1105,11 @@ def initialize_chain( self.subtensor = SubtensorInterface(network_) elif self.config["network"]: - self.subtensor = SubtensorInterface(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"]) else: self.subtensor = SubtensorInterface(defaults.subtensor.network) return self.subtensor @@ -1097,12 +1123,9 @@ async def _run(): initiated = False try: if self.subtensor: - async with self.subtensor: - initiated = True - result = await cmd - else: - initiated = True - result = await cmd + await self.subtensor.substrate.initialize() + initiated = True + result = await cmd return result except (ConnectionRefusedError, ssl.SSLError, InvalidHandshake): err_console.print(f"Unable to connect to the chain: {self.subtensor}") @@ -1128,12 +1151,14 @@ async def _run(): exit_early is True ): # temporarily to handle multiple run commands in one session try: + if self.subtensor: + await self.subtensor.substrate.close() raise typer.Exit() except Exception as e: # ensures we always exit cleanly if not isinstance(e, (typer.Exit, RuntimeError)): err_console.print(f"An unknown error has occurred: {e}") - return self.asyncio_runner(_run()) + return self.event_loop.run_until_complete(_run()) def main_callback( self, @@ -1184,20 +1209,6 @@ def main_callback( if k in self.config.keys(): self.config[k] = v - if sys.version_info < (3, 10): - # For Python 3.9 or lower - self.asyncio_runner = asyncio.get_event_loop().run_until_complete - else: - try: - uvloop = importlib.import_module("uvloop") - if sys.version_info >= (3, 11): - self.asyncio_runner = uvloop.run - else: - uvloop.install() - self.asyncio_runner = asyncio.run - except ModuleNotFoundError: - self.asyncio_runner = asyncio.run - def verbosity_handler( self, quiet: bool, verbose: bool, json_output: bool = False ) -> None: @@ -1502,6 +1513,8 @@ 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}]", ) for key, value in self.config.items(): @@ -1722,7 +1735,7 @@ def wallet_ask( if return_wallet_and_hotkey: valid = utils.is_valid_wallet(wallet) if valid[1]: - return wallet, wallet.hotkey.ss58_address + return wallet, get_hotkey_pub_ss58(wallet) else: if wallet_hotkey and is_valid_ss58_address(wallet_hotkey): return wallet, wallet_hotkey @@ -2229,14 +2242,15 @@ def wallet_regen_coldkey( if not wallet_path: wallet_path = Prompt.ask( - "Enter the path for the wallets directory", default=defaults.wallet.path + "Enter the path for the wallets directory", + default=self.config.get("wallet_path") or defaults.wallet.path, ) wallet_path = os.path.expanduser(wallet_path) if not wallet_name: wallet_name = Prompt.ask( f"Enter the name of the [{COLORS.G.CK}]new wallet (coldkey)", - default=defaults.wallet.name, + default=self.config.get("wallet_name") or defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -2280,7 +2294,7 @@ def wallet_regen_coldkey_pub( EXAMPLE - [green]$[/green] btcli wallet regen_coldkeypub --ss58_address 5DkQ4... + [green]$[/green] btcli wallet regen-coldkeypub --ss58_address 5DkQ4... [bold]Note[/bold]: This command is particularly useful for users who need to regenerate their coldkeypub, perhaps due to file corruption or loss. You will need either ss58 address or public hex key from your old coldkeypub.txt for the wallet. It is a recovery-focused utility that ensures continued access to your wallet functionalities. """ @@ -2288,13 +2302,14 @@ def wallet_regen_coldkey_pub( if not wallet_path: wallet_path = Prompt.ask( - "Enter the path to the wallets directory", default=defaults.wallet.path + "Enter the path to the wallets directory", + default=self.config.get("wallet_path") or defaults.wallet.path, ) wallet_path = os.path.expanduser(wallet_path) if not wallet_name: wallet_name = Prompt.ask( - f"Enter the name of the [{COLORS.G.CK}]new wallet (coldkey)", + f"Enter the name of the [{COLORS.G.CK}]wallet for the new coldkeypub", default=defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -2311,7 +2326,7 @@ def wallet_regen_coldkey_pub( address=ss58_address if ss58_address else public_key_hex ): rich.print("[red]Error: Invalid SS58 address or public key![/red]") - raise typer.Exit() + return return self._run_command( wallets.regen_coldkey_pub( wallet, ss58_address, public_key_hex, overwrite, json_output @@ -2377,6 +2392,68 @@ def wallet_regen_hotkey( ) ) + def wallet_regen_hotkey_pub( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + public_key_hex: Optional[str] = Options.public_hex_key, + ss58_address: Optional[str] = Options.ss58_address, + overwrite: bool = Options.overwrite, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Regenerates the public part of a hotkey (hotkeypub.txt) for a wallet. + + Use this command when you need to move machine for subnet mining. Use the public key or SS58 address from your hotkeypub.txt that you have on another machine to regenerate the hotkeypub.txt on this new machine. + + USAGE + + The command requires either a public key in hexadecimal format or an ``SS58`` address from the existing hotkeypub.txt from old machine to regenerate the coldkeypub on the new machine. + + EXAMPLE + + [green]$[/green] btcli wallet regen-hotkeypub --ss58_address 5DkQ4... + + [bold]Note[/bold]: This command is particularly useful for users who need to regenerate their hotkeypub, perhaps due to file corruption or loss. You will need either ss58 address or public hex key from your old hotkeypub.txt for the wallet. It is a recovery-focused utility that ensures continued access to your wallet functionalities. + """ + self.verbosity_handler(quiet, verbose, json_output) + + if not wallet_path: + wallet_path = Prompt.ask( + "Enter the path to the wallets directory", + default=self.config.get("wallet_path") or defaults.wallet.path, + ) + wallet_path = os.path.expanduser(wallet_path) + + if not wallet_name: + wallet_name = Prompt.ask( + f"Enter the name of the [{COLORS.G.CK}]wallet for the new hotkeypub", + default=defaults.wallet.name, + ) + wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) + + if not ss58_address and not public_key_hex: + prompt_answer = typer.prompt( + "Enter the ss58_address or the public key in hex" + ) + if prompt_answer.startswith("0x"): + public_key_hex = prompt_answer + else: + ss58_address = prompt_answer + if not utils.is_valid_bittensor_address_or_public_key( + address=ss58_address if ss58_address else public_key_hex + ): + rich.print("[red]Error: Invalid SS58 address or public key![/red]") + return False + return self._run_command( + wallets.regen_hotkey_pub( + wallet, ss58_address, public_key_hex, overwrite, json_output + ) + ) + def wallet_new_hotkey( self, wallet_name: Optional[str] = Options.wallet_name, @@ -2417,7 +2494,7 @@ def wallet_new_hotkey( if not wallet_name: wallet_name = Prompt.ask( f"Enter the [{COLORS.G.CK}]wallet name", - default=defaults.wallet.name, + default=self.config.get("wallet_name") or defaults.wallet.name, ) if not wallet_hotkey: @@ -2472,11 +2549,11 @@ def wallet_associate_hotkey( if not wallet_hotkey: wallet_hotkey = Prompt.ask( "Enter the [blue]hotkey[/blue] name or " - "[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]" + "[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) - hotkey_display = None - if is_valid_ss58_address(wallet_hotkey): + if wallet_hotkey and is_valid_ss58_address(wallet_hotkey): hotkey_ss58 = wallet_hotkey wallet = self.wallet_ask( wallet_name, @@ -2496,8 +2573,11 @@ def wallet_associate_hotkey( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - hotkey_ss58 = wallet.hotkey.ss58_address - hotkey_display = f"hotkey [blue]{wallet_hotkey}[/blue] [{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" + hotkey_ss58 = get_hotkey_pub_ss58(wallet) + hotkey_display = ( + f"hotkey [blue]{wallet_hotkey}[/blue] " + f"[{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" + ) return self._run_command( wallets.associate_hotkey( @@ -2544,13 +2624,14 @@ def wallet_new_coldkey( if not wallet_path: wallet_path = Prompt.ask( - "Enter the path to the wallets directory", default=defaults.wallet.path + "Enter the path to the wallets directory", + default=self.config.get("wallet_path") or defaults.wallet.path, ) if not wallet_name: wallet_name = Prompt.ask( f"Enter the name of the [{COLORS.G.CK}]new wallet (coldkey)", - default=defaults.wallet.name, + default=self.config.get("wallet_name") or defaults.wallet.name, ) wallet = self.wallet_ask( @@ -2623,7 +2704,8 @@ def wallet_check_ck_swap( if not wallet_ss58_address: wallet_ss58_address = Prompt.ask( - "Enter [blue]wallet name[/blue] or [blue]SS58 address[/blue] [dim](leave blank to show all pending swaps)[/dim]" + "Enter [blue]wallet name[/blue] or [blue]SS58 address[/blue] [dim]" + "(leave blank to show all pending swaps)[/dim]" ) if not wallet_ss58_address: return self._run_command( @@ -2687,18 +2769,18 @@ def wallet_create_wallet( self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( - "Enter the path of wallets directory", default=defaults.wallet.path + "Enter the path of wallets directory", + default=self.config.get("wallet_path") or defaults.wallet.path, ) if not wallet_name: wallet_name = Prompt.ask( f"Enter the name of the [{COLORS.G.CK}]new wallet (coldkey)", - default=defaults.wallet.name, ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( f"Enter the the name of the [{COLORS.G.HK}]new hotkey", - default=defaults.wallet.hotkey, + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) wallet = self.wallet_ask( @@ -3507,7 +3589,7 @@ def stake_add( ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], validate=WV.WALLET_AND_HOTKEY, ) - include_hotkeys = wallet.hotkey.ss58_address + include_hotkeys = get_hotkey_pub_ss58(wallet) elif all_hotkeys or include_hotkeys or exclude_hotkeys: wallet = self.wallet_ask( @@ -3971,7 +4053,7 @@ def stake_move( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - destination_hotkey = destination_wallet.hotkey.ss58_address + destination_hotkey = get_hotkey_pub_ss58(destination_wallet) else: if is_valid_ss58_address(destination_hotkey): destination_hotkey = destination_hotkey @@ -4010,7 +4092,7 @@ def stake_move( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - origin_hotkey = wallet.hotkey.ss58_address + origin_hotkey = get_hotkey_pub_ss58(wallet) else: if is_valid_ss58_address(wallet_hotkey): origin_hotkey = wallet_hotkey @@ -4022,7 +4104,7 @@ def stake_move( ask_for=[], validate=WV.WALLET_AND_HOTKEY, ) - origin_hotkey = wallet.hotkey.ss58_address + origin_hotkey = get_hotkey_pub_ss58(wallet) if not interactive_selection: if origin_netuid is None: @@ -4158,7 +4240,8 @@ def stake_transfer( interactive_selection = False if not wallet_hotkey: origin_hotkey = Prompt.ask( - "Enter the [blue]origin hotkey[/blue] name or ss58 address [bold](stake will be transferred FROM here)[/bold] " + "Enter the [blue]origin hotkey[/blue] name or ss58 address [bold]" + "(stake will be transferred FROM here)[/bold] " "[dim](or press Enter to select from existing stakes)[/dim]" ) if origin_hotkey == "": @@ -4174,7 +4257,7 @@ def stake_transfer( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - origin_hotkey = wallet.hotkey.ss58_address + origin_hotkey = get_hotkey_pub_ss58(wallet) else: if is_valid_ss58_address(wallet_hotkey): origin_hotkey = wallet_hotkey @@ -4186,7 +4269,7 @@ def stake_transfer( ask_for=[], validate=WV.WALLET_AND_HOTKEY, ) - origin_hotkey = wallet.hotkey.ss58_address + origin_hotkey = get_hotkey_pub_ss58(wallet) if not interactive_selection: if origin_netuid is None: @@ -5009,7 +5092,7 @@ def subnets_price( "Netuids to show the price for. Separate multiple netuids with a comma, for example: `-n 0,1,2`.", ), interval_hours: int = typer.Option( - 24, + 4, "--interval-hours", "--interval", help="The number of hours to show the historical price for.", @@ -5026,6 +5109,11 @@ def subnets_price( "--log", help="Show the price in log scale.", ), + current_only: bool = typer.Option( + False, + "--current", + help="Show only the current data, and no historical data.", + ), html_output: bool = Options.html_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5048,9 +5136,31 @@ def subnets_price( [green]$[/green] btcli subnets price --netuids 1,2,3,4 --html """ if json_output and html_output: - print_error("Cannot specify both `--json-output` and `--html`") + print_error( + f"Cannot specify both [{COLORS.G.ARG}]--json-output[/{COLORS.G.ARG}] " + f"and [{COLORS.G.ARG}]--html[/{COLORS.G.ARG}]" + ) + 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}]" + ) return self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) + + subtensor = self.initialize_chain(network) + non_archives = ["finney", "latent-lite", "subvortex"] + if not current_only and subtensor.network in non_archives + [ + 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." + ) + return False + if netuids: netuids = parse_to_list( netuids, @@ -5080,10 +5190,11 @@ def subnets_price( return self._run_command( price.price( - self.initialize_chain(network), + subtensor, netuids, all_netuids, interval_hours, + current_only, html_output, log_scale, json_output, diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 8bbc8064..b2461d89 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -39,6 +39,7 @@ print_error, unlock_key, hex_to_bytes, + get_hotkey_pub_ss58, ) if typing.TYPE_CHECKING: @@ -490,7 +491,7 @@ async def register_extrinsic( async def get_neuron_for_pubkey_and_subnet(): uid = await subtensor.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + "SubtensorModule", "Uids", [netuid, get_hotkey_pub_ss58(wallet)] ) if uid is None: return NeuronInfo.get_null_neuron() @@ -525,7 +526,7 @@ async def get_neuron_for_pubkey_and_subnet(): if not Confirm.ask( f"Continue Registration?\n" f" hotkey [{COLOR_PALETTE.G.HK}]({wallet.hotkey_str})[/{COLOR_PALETTE.G.HK}]:" - f"\t[{COLOR_PALETTE.G.HK}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}]\n" + f"\t[{COLOR_PALETTE.G.HK}]{get_hotkey_pub_ss58(wallet)}[/{COLOR_PALETTE.G.HK}]\n" f" coldkey [{COLOR_PALETTE.G.CK}]({wallet.name})[/{COLOR_PALETTE.G.CK}]:" f"\t[{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" f" network:\t\t[{COLOR_PALETTE.G.LINKS}]{subtensor.network}[/{COLOR_PALETTE.G.LINKS}]\n" @@ -577,7 +578,7 @@ async def get_neuron_for_pubkey_and_subnet(): if not pow_result: # might be registered already on this subnet is_registered = await is_hotkey_registered( - subtensor, netuid=netuid, hotkey_ss58=wallet.hotkey.ss58_address + subtensor, netuid=netuid, hotkey_ss58=get_hotkey_pub_ss58(wallet) ) if is_registered: err_console.print( @@ -598,7 +599,7 @@ async def get_neuron_for_pubkey_and_subnet(): "block_number": pow_result.block_number, "nonce": pow_result.nonce, "work": [int(byte_) for byte_ in pow_result.seal], - "hotkey": wallet.hotkey.ss58_address, + "hotkey": get_hotkey_pub_ss58(wallet), "coldkey": wallet.coldkeypub.ss58_address, }, ) @@ -639,7 +640,7 @@ async def get_neuron_for_pubkey_and_subnet(): is_registered = await is_hotkey_registered( subtensor, netuid=netuid, - hotkey_ss58=wallet.hotkey.ss58_address, + hotkey_ss58=get_hotkey_pub_ss58(wallet), ) if is_registered: console.print( @@ -704,7 +705,7 @@ async def burned_register_extrinsic( spinner="aesthetic", ) as status: my_uid = await subtensor.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + "SubtensorModule", "Uids", [netuid, get_hotkey_pub_ss58(wallet)] ) block_hash = await subtensor.substrate.get_chain_head() @@ -751,7 +752,7 @@ async def burned_register_extrinsic( call_function="burned_register", call_params={ "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, + "hotkey": get_hotkey_pub_ss58(wallet), }, ) success, err_msg = await subtensor.sign_and_send_extrinsic( @@ -773,10 +774,10 @@ async def burned_register_extrinsic( reuse_block=False, ), subtensor.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, block_hash=block_hash + get_hotkey_pub_ss58(wallet), block_hash=block_hash ), subtensor.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + "SubtensorModule", "Uids", [netuid, get_hotkey_pub_ss58(wallet)] ), ) @@ -1146,7 +1147,7 @@ async def _block_solver( timeout = 0.15 if cuda else 0.15 while netuid == -1 or not await is_hotkey_registered( - subtensor, netuid, wallet.hotkey.ss58_address + subtensor, netuid, get_hotkey_pub_ss58(wallet) ): # Wait until a solver finds a solution try: @@ -1755,37 +1756,39 @@ async def swap_hotkey_extrinsic( :return: Success """ block_hash = await subtensor.substrate.get_chain_head() + hk_ss58 = get_hotkey_pub_ss58(wallet) netuids_registered = await subtensor.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, block_hash=block_hash + hk_ss58, block_hash=block_hash ) netuids_registered_new_hotkey = await subtensor.get_netuids_for_hotkey( - new_wallet.hotkey.ss58_address, block_hash=block_hash + hk_ss58, block_hash=block_hash ) if netuid is not None and netuid not in netuids_registered: err_console.print( - f":cross_mark: [red]Failed[/red]: Original hotkey {wallet.hotkey.ss58_address} is not registered on subnet {netuid}" + f":cross_mark: [red]Failed[/red]: Original hotkey {hk_ss58} is not registered on subnet {netuid}" ) return False elif not len(netuids_registered) > 0: err_console.print( - f"Original hotkey [dark_orange]{wallet.hotkey.ss58_address}[/dark_orange] is not registered on any subnet. " + f"Original hotkey [dark_orange]{hk_ss58}[/dark_orange] is not registered on any subnet. " f"Please register and try again" ) return False + new_hk_ss58 = get_hotkey_pub_ss58(new_wallet) if netuid is not None: if netuid in netuids_registered_new_hotkey: err_console.print( - f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} " + f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} " f"is already registered on subnet {netuid}" ) return False else: if len(netuids_registered_new_hotkey) > 0: err_console.print( - f":cross_mark: [red]Failed[/red]: New hotkey {new_wallet.hotkey.ss58_address} " + f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} " f"is already registered on subnet(s) {netuids_registered_new_hotkey}" ) return False @@ -1798,28 +1801,28 @@ async def swap_hotkey_extrinsic( if netuid is not None: confirm_message = ( f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t" - f"[dark_orange]{wallet.hotkey.ss58_address} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t" - f"[dark_orange]{new_wallet.hotkey.ss58_address} ({new_wallet.hotkey_str})[/dark_orange] on subnet {netuid}\n" + f"[dark_orange]{hk_ss58} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t" + f"[dark_orange]{new_hk_ss58} ({new_wallet.hotkey_str})[/dark_orange] on subnet {netuid}\n" "This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]" ) else: confirm_message = ( f"Do you want to swap [dark_orange]{wallet.name}[/dark_orange] hotkey \n\t" - f"[dark_orange]{wallet.hotkey.ss58_address} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t" - f"[dark_orange]{new_wallet.hotkey.ss58_address} ({new_wallet.hotkey_str})[/dark_orange] on all subnets\n" + f"[dark_orange]{hk_ss58} ({wallet.hotkey_str})[/dark_orange] with hotkey \n\t" + f"[dark_orange]{new_hk_ss58} ({new_wallet.hotkey_str})[/dark_orange] on all subnets\n" "This operation will cost [bold cyan]1 TAO (recycled)[/bold cyan]" ) if not Confirm.ask(confirm_message): return False print_verbose( - f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address} - {wallet.hotkey_str}) with " - f"{new_wallet.name}'s hotkey ({new_wallet.hotkey.ss58_address} - {new_wallet.hotkey_str})" + f"Swapping {wallet.name}'s hotkey ({hk_ss58} - {wallet.hotkey_str}) with " + f"{new_wallet.name}'s hotkey ({new_hk_ss58} - {new_wallet.hotkey_str})" ) with console.status(":satellite: Swapping hotkeys...", spinner="aesthetic"): call_params = { - "hotkey": wallet.hotkey.ss58_address, - "new_hotkey": new_wallet.hotkey.ss58_address, + "hotkey": hk_ss58, + "new_hotkey": new_hk_ss58, "netuid": netuid, } @@ -1832,7 +1835,8 @@ async def swap_hotkey_extrinsic( if success: console.print( - f"Hotkey {wallet.hotkey.ss58_address} ({wallet.hotkey_str}) swapped for new hotkey: {new_wallet.hotkey.ss58_address} ({new_wallet.hotkey_str})" + f"Hotkey {hk_ss58} ({wallet.hotkey_str}) swapped for new hotkey: " + f"{new_hk_ss58} ({new_wallet.hotkey_str})" ) return True else: diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index d8d4900a..207fb864 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -37,6 +37,7 @@ print_verbose, format_error_message, unlock_key, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -310,7 +311,7 @@ async def root_register_extrinsic( print_verbose(f"Checking if hotkey ({wallet.hotkey_str}) is registered on root") is_registered = await is_hotkey_registered( - subtensor, netuid=0, hotkey_ss58=wallet.hotkey.ss58_address + subtensor, netuid=0, hotkey_ss58=get_hotkey_pub_ss58(wallet) ) if is_registered: console.print( @@ -322,7 +323,7 @@ async def root_register_extrinsic( call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="root_register", - call_params={"hotkey": wallet.hotkey.ss58_address}, + call_params={"hotkey": get_hotkey_pub_ss58(wallet)}, ) success, err_msg = await subtensor.sign_and_send_extrinsic( call, @@ -341,7 +342,7 @@ async def root_register_extrinsic( uid = await subtensor.query( module="SubtensorModule", storage_function="Uids", - params=[0, wallet.hotkey.ss58_address], + params=[0, get_hotkey_pub_ss58(wallet)], ) if uid is not None: console.print( @@ -391,7 +392,7 @@ async def _do_set_weights(): "weights": weight_vals, "netuid": 0, "version_key": version_key, - "hotkey": wallet.hotkey.ss58_address, + "hotkey": get_hotkey_pub_ss58(wallet), }, ) # Period dictates how long the extrinsic will stay as part of waiting pool @@ -415,7 +416,7 @@ async def _do_set_weights(): return False, await response.error_message my_uid = await subtensor.query( - "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] + "SubtensorModule", "Uids", [0, get_hotkey_pub_ss58(wallet)] ) if my_uid is None: diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 5302a33d..cd435b64 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -172,7 +172,7 @@ async def do_transfer() -> tuple[bool, str, str]: if prompt: if not Confirm.ask( "Do you want to transfer:[bold white]\n" - f" amount: [bright_cyan]{amount}[/bright_cyan]\n" + f" amount: [bright_cyan]{amount if not transfer_all else account_balance}[/bright_cyan]\n" f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : " f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]" f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]" diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 3d8a632d..0684b31e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -39,6 +39,7 @@ validate_chain_endpoint, u16_normalized_float, U16_MAX, + get_hotkey_pub_ss58, ) SubstrateClass = ( @@ -666,7 +667,7 @@ async def filter_netuids_by_registered_hotkeys( for sublist in await asyncio.gather( *[ self.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, + get_hotkey_pub_ss58(wallet), reuse_block=reuse_block, block_hash=block_hash, ) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index ef5f2e4b..d7ffa6e4 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -275,7 +275,7 @@ def get_hotkey_wallets_for_wallet( (exists := hotkey_for_name.hotkey_file.exists_on_device()) and not hotkey_for_name.hotkey_file.is_encrypted() # and hotkey_for_name.coldkeypub.ss58_address - and hotkey_for_name.hotkey.ss58_address + and get_hotkey_pub_ss58(hotkey_for_name) ): hotkey_wallets.append(hotkey_for_name) elif ( @@ -1431,3 +1431,15 @@ def blocks_to_duration(blocks: int) -> str: results.append(f"{unit_count}{unit}") # Return only the first two non-zero units return " ".join(results[:2]) or "0s" + + +def get_hotkey_pub_ss58(wallet: Wallet) -> str: + """ + Helper fn to retrieve the hotkeypub ss58 of a wallet that may have been created before + bt-wallet 3.1.1 and thus not have a wallet hotkeypub. In this case, it will return the hotkey + SS58. + """ + try: + return wallet.hotkeypub.ss58_address + except KeyFileError: + return wallet.hotkey.ss58_address diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index b223eaf2..6579d976 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -20,6 +20,7 @@ print_verbose, unlock_key, json_console, + get_hotkey_pub_ss58, ) from bittensor_wallet import Wallet @@ -552,7 +553,7 @@ def _get_hotkeys_to_stake_to( # Stake to all hotkeys except excluded ones all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) return [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) + (wallet.hotkey_str, get_hotkey_pub_ss58(wallet)) for wallet in all_hotkeys_ if wallet.hotkey_str not in (exclude_hotkeys or []) ] @@ -572,7 +573,7 @@ def _get_hotkeys_to_stake_to( name=wallet.name, hotkey=hotkey_ss58_or_hotkey_name, ) - hotkeys.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address)) + hotkeys.append((wallet_.hotkey_str, get_hotkey_pub_ss58(wallet_))) return hotkeys @@ -581,7 +582,7 @@ def _get_hotkeys_to_stake_to( f"Staking to hotkey: ({wallet.hotkey_str}) in wallet: ({wallet.name})" ) assert wallet.hotkey is not None - return [(None, wallet.hotkey.ss58_address)] + return [(None, get_hotkey_pub_ss58(wallet))] def _define_stake_table( diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index ef16823e..d01e8d14 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -21,6 +21,7 @@ format_error_message, unlock_key, json_console, + get_hotkey_pub_ss58, ) @@ -464,7 +465,7 @@ async def _render_table( netuid_children_tuples = [] for netuid_ in netuids: success, children, err_mg = await subtensor.get_children( - wallet.hotkey.ss58_address, netuid_ + get_hotkey_pub_ss58(wallet), netuid_ ) if children: netuid_children_tuples.append((netuid_, children)) @@ -472,16 +473,16 @@ async def _render_table( err_console.print( f"Failed to get children from subtensor {netuid_}: {err_mg}" ) - await _render_table(wallet.hotkey.ss58_address, netuid_children_tuples) + await _render_table(get_hotkey_pub_ss58(wallet), netuid_children_tuples) else: success, children, err_mg = await subtensor.get_children( - wallet.hotkey.ss58_address, netuid + get_hotkey_pub_ss58(wallet), netuid ) if not success: err_console.print(f"Failed to get children from subtensor: {err_mg}") if children: netuid_children_tuples = [(netuid, children)] - await _render_table(wallet.hotkey.ss58_address, netuid_children_tuples) + await _render_table(get_hotkey_pub_ss58(wallet), netuid_children_tuples) return children @@ -500,12 +501,12 @@ async def set_children( """Set children hotkeys.""" # Validate children SS58 addresses # TODO check to see if this should be allowed to be specified by user instead of pulling from wallet - hotkey = wallet.hotkey.ss58_address + hotkey = get_hotkey_pub_ss58(wallet) for child in children: if not is_valid_ss58_address(child): err_console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") return - if child == wallet.hotkey.ss58_address: + if child == hotkey: err_console.print(":cross_mark:[red] Cannot set yourself as a child.[/red]") return @@ -608,7 +609,7 @@ async def revoke_children( subtensor=subtensor, wallet=wallet, netuid=netuid, - hotkey=wallet.hotkey.ss58_address, + hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], prompt=prompt, wait_for_inclusion=wait_for_inclusion, @@ -647,7 +648,7 @@ async def revoke_children( subtensor=subtensor, wallet=wallet, netuid=netuid, - hotkey=wallet.hotkey.ss58_address, + hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], prompt=prompt, wait_for_inclusion=True, @@ -746,7 +747,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: subtensor=subtensor, wallet=wallet, netuid=subnet, - hotkey=wallet.hotkey.ss58_address, + hotkey=get_hotkey_pub_ss58(wallet), take=chk_take, prompt=prompt, wait_for_inclusion=wait_for_inclusion, @@ -756,7 +757,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: if success: console.print(":white_heavy_check_mark: [green]Set childkey take.[/green]") console.print( - f"The childkey take for {wallet.hotkey.ss58_address} is now set to {take * 100:.2f}%." + f"The childkey take for {get_hotkey_pub_ss58(wallet)} is now set to {take * 100:.2f}%." ) return True else: @@ -766,9 +767,10 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: return False # Print childkey take for other user and return (dont offer to change take rate) - if not hotkey or hotkey == wallet.hotkey.ss58_address: - hotkey = wallet.hotkey.ss58_address - if hotkey != wallet.hotkey.ss58_address or not take: + wallet_hk = get_hotkey_pub_ss58(wallet) + if not hotkey or hotkey == wallet_hk: + hotkey = wallet_hk + if hotkey != wallet_hk or not take: # display childkey take for other users if netuid: await display_chk_take(hotkey, netuid) @@ -826,7 +828,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: subtensor=subtensor, wallet=wallet, netuid=netuid_, - hotkey=wallet.hotkey.ss58_address, + hotkey=wallet_hk, take=take, prompt=prompt, wait_for_inclusion=True, diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index b4efbd12..c72bbb41 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -16,6 +16,7 @@ group_subnets, get_subnet_name, unlock_key, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -343,8 +344,9 @@ async def stake_swap_selection( # Filter stakes for this hotkey hotkey_stakes = {} + hotkey_ss58 = get_hotkey_pub_ss58(wallet) for stake in stakes: - if stake.hotkey_ss58 == wallet.hotkey.ss58_address and stake.stake.tao > 0: + if stake.hotkey_ss58 == hotkey_ss58 and stake.stake.tao > 0: hotkey_stakes[stake.netuid] = { "stake": stake.stake, "is_registered": stake.is_registered, @@ -357,12 +359,12 @@ async def stake_swap_selection( # Display available stakes table = Table( title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]" - f"[{COLOR_PALETTE.G.HK}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}]\n", + f"[{COLOR_PALETTE.G.HK}]{wallet.hotkey_str}: {hotkey_ss58}[/{COLOR_PALETTE.G.HK}]\n", show_edge=False, header_style="bold white", border_style="bright_black", title_justify="center", - width=len(wallet.hotkey.ss58_address) + 20, + width=len(hotkey_ss58) + 20, ) table.add_column("Index", justify="right", style="cyan") @@ -817,7 +819,7 @@ async def swap_stake( Returns: bool: True if the swap was successful, False otherwise. """ - hotkey_ss58 = wallet.hotkey.ss58_address + hotkey_ss58 = get_hotkey_pub_ss58(wallet) if interactive_selection: try: selection = await stake_swap_selection(subtensor, wallet) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index a28254e3..67f0109f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -22,6 +22,7 @@ group_subnets, unlock_key, json_console, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -407,7 +408,7 @@ async def unstake_all( old_identities=old_identities, ) elif not hotkey_ss58_address: - hotkeys = [(wallet.hotkey_str, wallet.hotkey.ss58_address, None)] + hotkeys = [(wallet.hotkey_str, get_hotkey_pub_ss58(wallet), None)] else: hotkeys = [(None, hotkey_ss58_address, None)] @@ -1198,7 +1199,7 @@ def _get_hotkeys_to_unstake( print_verbose("Unstaking from all hotkeys") all_hotkeys_ = get_hotkey_wallets_for_wallet(wallet=wallet) wallet_hotkeys = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address, None) + (wallet.hotkey_str, get_hotkey_pub_ss58(wallet), None) for wallet in all_hotkeys_ if wallet.hotkey_str not in exclude_hotkeys ] @@ -1230,7 +1231,7 @@ def _get_hotkeys_to_unstake( path=wallet.path, hotkey=hotkey_identifier, ) - result.append((wallet_.hotkey_str, wallet_.hotkey.ss58_address, None)) + result.append((wallet_.hotkey_str, get_hotkey_pub_ss58(wallet_), None)) return result # Only cli.config.wallet.hotkey is specified @@ -1238,7 +1239,7 @@ def _get_hotkeys_to_unstake( f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" ) assert wallet.hotkey is not None - return [(wallet.hotkey_str, wallet.hotkey.ss58_address, None)] + return [(wallet.hotkey_str, get_hotkey_pub_ss58(wallet), None)] def _create_unstake_table( diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 461ed432..38a20d00 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -10,6 +10,7 @@ import plotly.graph_objects as go from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.chain_data import DynamicInfo from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -27,7 +28,8 @@ async def price( subtensor: "SubtensorInterface", netuids: list[int], all_netuids: bool = False, - interval_hours: int = 24, + interval_hours: int = 4, + current_only: bool = False, html_output: bool = False, log_scale: bool = False, json_output: bool = False, @@ -41,45 +43,96 @@ async def price( blocks_per_hour = int(3600 / 12) # ~300 blocks per hour total_blocks = blocks_per_hour * interval_hours - with console.status(":chart_increasing: Fetching historical price data..."): - current_block_hash = await subtensor.substrate.get_chain_head() - current_block = await subtensor.substrate.get_block_number(current_block_hash) + if not current_only: + with console.status(":chart_increasing: Fetching historical price data..."): + current_block_hash = await subtensor.substrate.get_chain_head() + current_block = await subtensor.substrate.get_block_number( + current_block_hash + ) - step = 300 - start_block = max(0, current_block - total_blocks) - block_numbers = list(range(start_block, current_block + 1, step)) + step = 300 + start_block = max(0, current_block - total_blocks) + block_numbers = list(range(start_block, current_block + 1, step)) - # Block hashes - block_hash_cors = [ - subtensor.substrate.get_block_hash(bn) for bn in block_numbers - ] - block_hashes = await asyncio.gather(*block_hash_cors) + # Block hashes + block_hash_cors = [ + subtensor.substrate.get_block_hash(bn) for bn in block_numbers + ] + block_hashes = await asyncio.gather(*block_hash_cors) - # We fetch all subnets when there is more than one netuid - if all_netuids or len(netuids) > 1: - subnet_info_cors = [subtensor.all_subnets(bh) for bh in block_hashes] - else: - # If there is only one netuid, we fetch the subnet info for that netuid - netuid = netuids[0] - subnet_info_cors = [subtensor.subnet(netuid, bh) for bh in block_hashes] - all_subnet_infos = await asyncio.gather(*subnet_info_cors) + # We fetch all subnets when there is more than one netuid + if all_netuids or len(netuids) > 1: + subnet_info_cors = [subtensor.all_subnets(bh) for bh in block_hashes] + else: + # If there is only one netuid, we fetch the subnet info for that netuid + netuid = netuids[0] + subnet_info_cors = [subtensor.subnet(netuid, bh) for bh in block_hashes] + all_subnet_infos = await asyncio.gather(*subnet_info_cors) subnet_data = _process_subnet_data( block_numbers, all_subnet_infos, netuids, all_netuids ) + if not subnet_data: + err_console.print("[red]No valid price data found for any subnet[/red]") + return - if not subnet_data: - err_console.print("[red]No valid price data found for any subnet[/red]") - return - - if html_output: - await _generate_html_output( - subnet_data, block_numbers, interval_hours, log_scale + if html_output: + await _generate_html_output( + subnet_data, block_numbers, interval_hours, log_scale + ) + elif json_output: + json_console.print(json.dumps(_generate_json_output(subnet_data))) + else: + _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) + else: + with console.status("Fetching current price data..."): + if all_netuids or len(netuids) > 1: + all_subnet_info = await subtensor.all_subnets() + else: + all_subnet_info = [await subtensor.subnet(netuid=netuids[0])] + subnet_data = _process_current_subnet_data( + all_subnet_info, netuids, all_netuids ) - elif json_output: - json_console.print(json.dumps(_generate_json_output(subnet_data))) + if json_output: + json_console.print(json.dumps(_generate_json_output(subnet_data))) + else: + _generate_cli_output_current(subnet_data) + + +def _process_current_subnet_data(subnet_infos: list[DynamicInfo], netuids, all_netuids): + subnet_data = {} + if all_netuids or len(netuids) > 1: + # Most recent data for statistics + for subnet_info in subnet_infos: + stats = { + "current_price": subnet_info.price, + "supply": subnet_info.alpha_in.tao + subnet_info.alpha_out.tao, + "market_cap": subnet_info.price.tao + * (subnet_info.alpha_in.tao + subnet_info.alpha_out.tao), + "emission": subnet_info.emission.tao, + "stake": subnet_info.alpha_out.tao, + "symbol": subnet_info.symbol, + "name": get_subnet_name(subnet_info), + } + subnet_data[subnet_info.netuid] = { + "stats": stats, + } else: - _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) + subnet_info = subnet_infos[0] + stats = { + "current_price": subnet_info.price.tao, + "supply": subnet_info.alpha_in.tao + subnet_info.alpha_out.tao, + "market_cap": subnet_info.price.tao + * (subnet_info.alpha_in.tao + subnet_info.alpha_out.tao), + "emission": subnet_info.emission.tao, + "stake": subnet_info.alpha_out.tao, + "symbol": subnet_info.symbol, + "name": get_subnet_name(subnet_info), + } + subnet_data[subnet_info.netuid] = { + "stats": stats, + } + return subnet_data def _process_subnet_data(block_numbers, all_subnet_infos, netuids, all_netuids): @@ -626,3 +679,46 @@ def color_label(text): ) console.print(stats_text) + + +def _generate_cli_output_current(subnet_data): + for netuid, data in subnet_data.items(): + stats = data["stats"] + + if netuid != 0: + console.print( + f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} " + f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n" + f"Current: [blue]{stats['current_price']:.6f}{stats['symbol']}[/blue]\n" + ) + else: + console.print( + f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} " + f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n" + f"Current: [blue]{stats['symbol']} {stats['current_price']:.6f}[/blue]\n" + ) + + if netuid != 0: + stats_text = ( + "\nLatest stats:\n" + f"Supply: [{COLOR_PALETTE.P.ALPHA_IN}]" + f"{stats['supply']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.P.ALPHA_IN}]\n" + f"Market Cap: [steel_blue3]{stats['market_cap']:,.2f} {stats['symbol']} / 21M[/steel_blue3]\n" + f"Emission: [{COLOR_PALETTE.P.EMISSION}]" + f"{stats['emission']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.P.EMISSION}]\n" + f"Stake: [{COLOR_PALETTE.S.TAO}]" + f"{stats['stake']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.S.TAO}]" + ) + else: + stats_text = ( + "\nLatest stats:\n" + f"Supply: [{COLOR_PALETTE.P.ALPHA_IN}]" + f"{stats['symbol']} {stats['supply']:,.2f}[/{COLOR_PALETTE.P.ALPHA_IN}]\n" + f"Market Cap: [steel_blue3]{stats['symbol']} {stats['market_cap']:,.2f} / 21M[/steel_blue3]\n" + f"Emission: [{COLOR_PALETTE.P.EMISSION}]" + f"{stats['symbol']} {stats['emission']:,.2f}[/{COLOR_PALETTE.P.EMISSION}]\n" + f"Stake: [{COLOR_PALETTE.S.TAO}]" + f"{stats['symbol']} {stats['stake']:,.2f}[/{COLOR_PALETTE.S.TAO}]" + ) + + console.print(stats_text) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 62a20b63..bb42132c 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -36,6 +36,7 @@ unlock_key, blocks_to_duration, json_console, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -114,7 +115,7 @@ async def _find_event_attributes_in_extrinsic_receipt( return False, None call_params = { - "hotkey": wallet.hotkey.ss58_address, + "hotkey": get_hotkey_pub_ss58(wallet), "mechid": 1, } call_function = "register_network" @@ -1654,7 +1655,7 @@ async def register( str(netuid), f"{Balance.get_unit(netuid)}", f"τ {current_recycle.tao:.4f}", - f"{wallet.hotkey.ss58_address}", + f"{get_hotkey_pub_ss58(wallet)}", f"{wallet.coldkeypub.ss58_address}", ) console.print(table) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8004dc90..1046c28c 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -26,6 +26,7 @@ json_console, string_to_u16, string_to_u64, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -497,7 +498,7 @@ async def vote_senate_extrinsic( call_module="SubtensorModule", call_function="vote", call_params={ - "hotkey": wallet.hotkey.ss58_address, + "hotkey": get_hotkey_pub_ss58(wallet), "proposal": proposal_hash, "index": proposal_idx, "approve": vote, @@ -513,9 +514,10 @@ async def vote_senate_extrinsic( # Successful vote, final check for data else: if vote_data := await subtensor.get_vote_data(proposal_hash): + hotkey_ss58 = get_hotkey_pub_ss58(wallet) if ( - vote_data.ayes.count(wallet.hotkey.ss58_address) > 0 - or vote_data.nays.count(wallet.hotkey.ss58_address) > 0 + vote_data.ayes.count(hotkey_ss58) > 0 + or vote_data.nays.count(hotkey_ss58) > 0 ): console.print(":white_heavy_check_mark: [green]Vote cast.[/green]") return True @@ -859,10 +861,9 @@ async def senate_vote( return False print_verbose(f"Fetching senate status of {wallet.hotkey_str}") - if not await _is_senate_member(subtensor, hotkey_ss58=wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." - ) + hotkey_ss58 = get_hotkey_pub_ss58(wallet) + if not await _is_senate_member(subtensor, hotkey_ss58=hotkey_ss58): + err_console.print(f"Aborting: Hotkey {hotkey_ss58} isn't a senate member.") return False # Unlock the wallet. @@ -890,7 +891,7 @@ async def senate_vote( async def get_current_take(subtensor: "SubtensorInterface", wallet: Wallet): - current_take = await subtensor.current_take(wallet.hotkey.ss58_address) + current_take = await subtensor.current_take(get_hotkey_pub_ss58(wallet)) return current_take @@ -912,12 +913,13 @@ async def _do_set_take() -> bool: return False block_hash = await subtensor.substrate.get_chain_head() + hotkey_ss58 = get_hotkey_pub_ss58(wallet) netuids_registered = await subtensor.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, block_hash=block_hash + hotkey_ss58, block_hash=block_hash ) if not len(netuids_registered) > 0: err_console.print( - f"Hotkey [{COLOR_PALETTE.G.HK}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}] is not registered to" + f"Hotkey [{COLOR_PALETTE.G.HK}]{hotkey_ss58}[/{COLOR_PALETTE.G.HK}] is not registered to" f" any subnet. Please register using [{COLOR_PALETTE.G.SUBHEAD}]`btcli subnets register`" f"[{COLOR_PALETTE.G.SUBHEAD}] and try again." ) @@ -926,7 +928,7 @@ async def _do_set_take() -> bool: result: bool = await set_take_extrinsic( subtensor=subtensor, wallet=wallet, - delegate_ss58=wallet.hotkey.ss58_address, + delegate_ss58=hotkey_ss58, take=take, ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 61efa696..00132d3d 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -3,7 +3,7 @@ import json import os from collections import defaultdict -from typing import Generator, Optional +from typing import Generator, Optional, Union import aiohttp from bittensor_wallet import Wallet, Keypair @@ -48,6 +48,7 @@ WalletLike, blocks_to_duration, decode_account_id, + get_hotkey_pub_ss58, ) @@ -159,7 +160,7 @@ async def regen_coldkey( "name": new_wallet.name, "path": new_wallet.path, "hotkey": new_wallet.hotkey_str, - "hotkey_ss58": new_wallet.hotkey.ss58_address, + "hotkey_ss58": get_hotkey_pub_ss58(new_wallet), "coldkey_ss58": new_wallet.coldkeypub.ss58_address, }, "error": "", @@ -209,7 +210,7 @@ async def regen_coldkey_pub( "name": new_coldkeypub.name, "path": new_coldkeypub.path, "hotkey": new_coldkeypub.hotkey_str, - "hotkey_ss58": new_coldkeypub.hotkey.ss58_address, + "hotkey_ss58": get_hotkey_pub_ss58(new_coldkeypub), "coldkey_ss58": new_coldkeypub.coldkeypub.ss58_address, }, "error": "", @@ -255,7 +256,7 @@ async def regen_hotkey( console.print( "\n✅ [dark_sea_green]Regenerated hotkey successfully!\n", f"[dark_sea_green]Wallet name: ({new_hotkey_.name}), path: ({new_hotkey_.path}), " - f"hotkey ss58: ({new_hotkey_.hotkey.ss58_address})", + f"hotkey ss58: ({new_hotkey_.hotkeypub.ss58_address})", ) if json_output: json_console.print( @@ -266,7 +267,7 @@ async def regen_hotkey( "name": new_hotkey_.name, "path": new_hotkey_.path, "hotkey": new_hotkey_.hotkey_str, - "hotkey_ss58": new_hotkey_.hotkey.ss58_address, + "hotkey_ss58": new_hotkey_.hotkeypub.ss58_address, "coldkey_ss58": new_hotkey_.coldkeypub.ss58_address, }, "error": "", @@ -287,6 +288,50 @@ async def regen_hotkey( ) +async def regen_hotkey_pub( + wallet: Wallet, + ss58_address: str, + public_key_hex: str, + overwrite: Optional[bool] = False, + json_output: bool = False, +): + """Creates a new hotkeypub under this wallet.""" + try: + new_hotkeypub = wallet.regenerate_hotkeypub( + ss58_address=ss58_address, + public_key=public_key_hex, + overwrite=overwrite, + ) + if isinstance(new_hotkeypub, Wallet): + console.print( + "\n✅ [dark_sea_green]Regenerated coldkeypub successfully!\n", + f"[dark_sea_green]Wallet name: ({new_hotkeypub.name}), path: ({new_hotkeypub.path}), " + f"coldkey ss58: ({new_hotkeypub.coldkeypub.ss58_address})", + ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": new_hotkeypub.name, + "path": new_hotkeypub.path, + "hotkey": new_hotkeypub.hotkey_str, + "hotkey_ss58": new_hotkeypub.hotkeypub.ss58_address, + "coldkey_ss58": new_hotkeypub.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) + except KeyFileError: + print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) + + async def new_hotkey( wallet: Wallet, n_words: int, @@ -323,7 +368,7 @@ async def new_hotkey( "name": wallet.name, "path": wallet.path, "hotkey": wallet.hotkey_str, - "hotkey_ss58": wallet.hotkey.ss58_address, + "hotkey_ss58": get_hotkey_pub_ss58(wallet), "coldkey_ss58": wallet.coldkeypub.ss58_address, }, "error": "", @@ -402,19 +447,24 @@ async def wallet_create( json_output: bool = False, ): """Creates a new wallet.""" - output_dict = {"success": False, "error": "", "data": None} + output_dict: dict[str, Optional[Union[bool, str, dict]]] = { + "success": False, + "error": "", + "data": None, + } if uri: try: keypair = Keypair.create_from_uri(uri) wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) output_dict["success"] = True output_dict["data"] = { "name": wallet.name, "path": wallet.path, "hotkey": wallet.hotkey_str, - "hotkey_ss58": wallet.hotkey.ss58_address, + "hotkey_ss58": wallet.hotkeypub.ss58_address, "coldkey_ss58": wallet.coldkeypub.ss58_address, } except Exception as e: @@ -455,7 +505,7 @@ async def wallet_create( "name": wallet.name, "path": wallet.path, "hotkey": wallet.hotkey_str, - "hotkey_ss58": wallet.hotkey.ss58_address, + "hotkey_ss58": wallet.hotkeypub.ss58_address, } except KeyFileError as error: err = str(error) @@ -794,13 +844,14 @@ 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: data = ( f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] " - f"ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n" + f"ss58_address [green]{hkey_ss58}[/green]\n" ) hk_data["name"] = hkey.hotkey_str - hk_data["ss58_address"] = hkey.hotkey.ss58_address + hk_data["ss58_address"] = hkey_ss58 except UnicodeDecodeError: pass wallet_tree.add(data) @@ -1253,7 +1304,7 @@ def _get_hotkeys( def is_hotkey_matched(wallet: Wallet, item: str) -> bool: if is_valid_ss58_address(item): - return wallet.hotkey.ss58_address == item + return get_hotkey_pub_ss58(wallet) == item else: return wallet.hotkey_str == item @@ -1285,9 +1336,10 @@ def _get_key_address(all_hotkeys: list[Wallet]) -> tuple[list[str], dict[str, Wa hotkey_coldkey_to_hotkey_wallet = {} for hotkey_wallet in all_hotkeys: if hotkey_wallet.coldkeypub: - if hotkey_wallet.hotkey.ss58_address not in hotkey_coldkey_to_hotkey_wallet: - hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address] = {} - hotkey_coldkey_to_hotkey_wallet[hotkey_wallet.hotkey.ss58_address][ + hotkey_ss58 = get_hotkey_pub_ss58(hotkey_wallet) + if hotkey_ss58 not in hotkey_coldkey_to_hotkey_wallet: + hotkey_coldkey_to_hotkey_wallet[hotkey_ss58] = {} + hotkey_coldkey_to_hotkey_wallet[hotkey_ss58][ hotkey_wallet.coldkeypub.ss58_address ] = hotkey_wallet else: @@ -1471,7 +1523,7 @@ def neuron_row_maker( if hotkey_names := [ w.hotkey_str for w in hotkeys - if w.hotkey.ss58_address == n.hotkey + if get_hotkey_pub_ss58(w) == n.hotkey ]: hotkey_name = f"{hotkey_names[0]}-" yield [""] * 5 + [ diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 68ec0308..63e3b72f 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -15,6 +15,7 @@ console, format_error_message, json_console, + get_hotkey_pub_ss58, ) from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, @@ -128,7 +129,7 @@ async def commit_weights( # Generate the hash of the weights commit_hash = generate_weight_hash( - address=self.wallet.hotkey.ss58_address, + address=get_hotkey_pub_ss58(self.wallet), netuid=self.netuid, uids=uids, values=weights, diff --git a/pyproject.toml b/pyproject.toml index 91a77e29..5a32d2d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.9.0" +version = "9.10.0" description = "Bittensor CLI" readme = "README.md" authors = [ @@ -18,7 +18,6 @@ dependencies = [ "async-substrate-interface>=1.4.2", "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", "netaddr~=1.3.0", "numpy>=2.0.1,<3.0.0", @@ -27,8 +26,9 @@ dependencies = [ "PyYAML~=6.0.1", "rich>=13.7,<15.0", "scalecodec==1.2.11", - "typer>=0.12,<0.16", - "bittensor-wallet>=3.0.7", + "typer>=0.16", + "bittensor-wallet>=4.0.0", + "packaging", "plotille>=5.0.0", "plotly>=6.0.0", ] diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 8cb5caca..cd4bf09c 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -11,6 +11,7 @@ * btcli subnets set-identity * btcli subnets get-identity * btcli subnets register +* btcli subnets price * btcli stake add * btcli stake remove * btcli stake show @@ -234,6 +235,25 @@ def test_staking(local_chain, wallet_setup): assert get_identity_output["logo_url"] == sn_logo_url assert get_identity_output["additional"] == sn_add_info + get_s_price = exec_command_alice( + "subnets", + "price", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--current", + "--json-output", + ], + ) + get_s_price_output = json.loads(get_s_price.stdout) + assert str(netuid) in get_s_price_output.keys() + stats = get_s_price_output[str(netuid)]["stats"] + assert stats["name"] == sn_name + assert stats["current_price"] == 0.0 + assert stats["market_cap"] == 0.0 + # Start emissions on SNs for netuid_ in multiple_netuids: start_subnet_emissions = exec_command_alice( diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 019cad6b..1e702067 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -16,6 +16,7 @@ * btcli w regen_coldkey * btcli w regen_coldkeypub * btcli w regen_hotkey +* btcli w regen_hotkeypub """ @@ -542,6 +543,36 @@ def test_wallet_regen(wallet_setup, capfd): ) print("Passed wallet regen_hotkey command ✅") + hotkeypub_path = os.path.join( + wallet_path, "new_wallet", "hotkeys", "new_hotkeypub.txt" + ) + initial_hotkeypub_mod_time = os.path.getmtime(hotkeypub_path) + result = exec_command( + command="wallet", + sub_command="regen-hotkeypub", + extra_args=[ + "--wallet-name", + "new_wallet", + "--hotkey", + "new_hotkey", + "--wallet-path", + wallet_path, + "--ss58-address", + ss58_address, + "--overwrite", + ], + ) + + # Wait a bit to ensure file system updates modification time + time.sleep(2) + + new_hotkeypub_mod_time = os.path.getmtime(hotkeypub_path) + + assert initial_hotkeypub_mod_time != new_hotkeypub_mod_time, ( + "Hotkey file was not regenerated as expected" + ) + print("Passed wallet regen_hotkeypub command ✅") + def test_wallet_balance_all(local_chain, wallet_setup, capfd): """ diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 7a3c0993..b8b729b3 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -1,3 +1,4 @@ +import importlib import inspect import os import re @@ -6,10 +7,12 @@ import sys from typing import TYPE_CHECKING, Optional -from bittensor_cli.cli import CLIManager from bittensor_wallet import Keypair, Wallet +from packaging.version import parse as parse_version, Version from typer.testing import CliRunner +from bittensor_cli.cli import CLIManager + if TYPE_CHECKING: from async_substrate_interface.async_substrate import AsyncSubstrateInterface @@ -55,7 +58,10 @@ def exec_command( extra_args.extend(["--network", "ws://127.0.0.1:9945"]) # Capture stderr separately from stdout - runner = CliRunner(mix_stderr=False) + if parse_version(importlib.metadata.version("click")) < Version("8.2.0"): + runner = CliRunner(mix_stderr=False) + else: + runner = CliRunner() # Prepare the command arguments args = [ command,