From a43d8ee071009688ca69d7e614f8728e22118941 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 21:03:51 +0200 Subject: [PATCH 01/41] Sets default interval hours for subnets price to 4, bc of rate limiting. --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/subnets/price.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a11b2ed1..4520a17c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5009,7 +5009,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.", diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 461ed432..e5dae1b6 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -27,7 +27,7 @@ async def price( subtensor: "SubtensorInterface", netuids: list[int], all_netuids: bool = False, - interval_hours: int = 24, + interval_hours: int = 4, html_output: bool = False, log_scale: bool = False, json_output: bool = False, From 77d7a41742b065355b2563778094d053be08c0c4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:21:20 +0200 Subject: [PATCH 02/41] Added flag for current only --- bittensor_cli/cli.py | 17 +++ bittensor_cli/src/commands/subnets/price.py | 151 ++++++++++++++++---- 2 files changed, 139 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4520a17c..78e79690 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5026,6 +5026,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, @@ -5050,6 +5055,17 @@ def subnets_price( if json_output and html_output: print_error("Cannot specify both `--json-output` and `--html`") return + non_archives = ["finney", "latent-lite", "subvortex"] + if not current_only and self._determine_network(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 + self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) if netuids: netuids = parse_to_list( @@ -5084,6 +5100,7 @@ def subnets_price( netuids, all_netuids, interval_hours, + current_only, html_output, log_scale, json_output, diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index e5dae1b6..6189a8e5 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, @@ -28,6 +29,7 @@ async def price( netuids: list[int], all_netuids: bool = False, interval_hours: int = 4, + current_only: bool = False, html_output: bool = False, log_scale: bool = False, json_output: bool = False, @@ -41,45 +43,93 @@ 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))) + _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, + "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 +676,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'].tao:.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'].tao:.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) From a2ac201003bd08fa524f8c3e2d58ca58d846e60c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:22:06 +0200 Subject: [PATCH 03/41] Changed `_determine_network` to its own method to use it without instantiating a SubtensorInterface obj --- bittensor_cli/cli.py | 51 +++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 78e79690..1a898a16 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1061,32 +1061,35 @@ def initialize_chain( "Verify this is intended.", ) if not self.subtensor: - if network: - network_ = None - for item in network: - if item.startswith("ws"): - network_ = item - break - else: - network_ = item - - not_selected_networks = [net for net in network if net != network_] - if not_selected_networks: - console.print( - f"Networks not selected: " - f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" - ) + self.subtensor = SubtensorInterface(self._determine_network(network)) + return self.subtensor - 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" - ) + def _determine_network(self, network: Optional[list[str]] = None): + if network: + network_ = None + for item in network: + if item.startswith("ws"): + network_ = item + break else: - self.subtensor = SubtensorInterface(defaults.subtensor.network) - return self.subtensor + network_ = item + + not_selected_networks = [net for net in network if net != network_] + if not_selected_networks: + console.print( + f"Networks not selected: " + f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" + ) + + return network_ + elif self.config["network"]: + console.print( + f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" + f"[/{COLORS.G.LINKS}] from config" + ) + return self.config["network"] + else: + return defaults.subtensor.network def _run_command(self, cmd: Coroutine, exit_early: bool = True): """ From dbca3877af6a344f273a5a98e55b56d5e5061c06 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:26:29 +0200 Subject: [PATCH 04/41] Reverted _determine_network change. --- bittensor_cli/cli.py | 56 +++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1a898a16..34b28233 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1061,35 +1061,32 @@ def initialize_chain( "Verify this is intended.", ) if not self.subtensor: - self.subtensor = SubtensorInterface(self._determine_network(network)) - return self.subtensor - - def _determine_network(self, network: Optional[list[str]] = None): - if network: - network_ = None - for item in network: - if item.startswith("ws"): - network_ = item - break - else: - network_ = item + if network: + network_ = None + for item in network: + if item.startswith("ws"): + network_ = item + break + else: + network_ = item - not_selected_networks = [net for net in network if net != network_] - if not_selected_networks: - console.print( - f"Networks not selected: " - f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" - ) + not_selected_networks = [net for net in network if net != network_] + if not_selected_networks: + console.print( + f"Networks not selected: " + f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" + ) - return network_ - elif self.config["network"]: - console.print( - f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" - f"[/{COLORS.G.LINKS}] from config" - ) - return self.config["network"] - else: - return defaults.subtensor.network + self.subtensor = network_ + 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 = self.config["network"] + else: + self.subtensor = defaults.subtensor.network + return self.subtensor def _run_command(self, cmd: Coroutine, exit_early: bool = True): """ @@ -5058,8 +5055,9 @@ def subnets_price( if json_output and html_output: print_error("Cannot specify both `--json-output` and `--html`") return + subtensor = self.initialize_chain(network) non_archives = ["finney", "latent-lite", "subvortex"] - if not current_only and self._determine_network(network) in non_archives + [ + if not current_only and subtensor.network in non_archives + [ Constants.network_map[x] for x in non_archives ]: err_console.print( @@ -5099,7 +5097,7 @@ def subnets_price( return self._run_command( price.price( - self.initialize_chain(network), + subtensor, netuids, all_netuids, interval_hours, From 92f54f3a7ef95fddd9baf8213c9eb0d508e3f1c2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:30:53 +0200 Subject: [PATCH 05/41] Reverted _determine_network change. --- bittensor_cli/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 34b28233..b13c3e93 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1077,15 +1077,15 @@ def initialize_chain( f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" ) - self.subtensor = network_ + self.subtensor = SubtensorInterface(network_) 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 = self.config["network"] + self.subtensor = SubtensorInterface(self.config["network"]) else: - self.subtensor = defaults.subtensor.network + self.subtensor = SubtensorInterface(defaults.subtensor.network) return self.subtensor def _run_command(self, cmd: Coroutine, exit_early: bool = True): From 3b7025d186ae817cf11519a00c0823e528b8bebb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:39:29 +0200 Subject: [PATCH 06/41] Added JSON output support --- bittensor_cli/cli.py | 3 ++- bittensor_cli/src/commands/subnets/price.py | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b13c3e93..0abe3d40 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5055,6 +5055,8 @@ def subnets_price( if json_output and html_output: print_error("Cannot specify both `--json-output` and `--html`") 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 + [ @@ -5067,7 +5069,6 @@ def subnets_price( ) return False - self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) if netuids: netuids = parse_to_list( netuids, diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 6189a8e5..38a20d00 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -93,7 +93,10 @@ async def price( subnet_data = _process_current_subnet_data( all_subnet_info, netuids, all_netuids ) - _generate_cli_output_current(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): @@ -117,7 +120,7 @@ def _process_current_subnet_data(subnet_infos: list[DynamicInfo], netuids, all_n else: subnet_info = subnet_infos[0] stats = { - "current_price": subnet_info.price, + "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), @@ -686,13 +689,13 @@ def _generate_cli_output_current(subnet_data): 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'].tao:.6f}{stats['symbol']}[/blue]\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'].tao:.6f}[/blue]\n" + f"Current: [blue]{stats['symbol']} {stats['current_price']:.6f}[/blue]\n" ) if netuid != 0: From d3d3950a3b67f23908c8bf5b25dc1f7c57ac16c7 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:47:12 +0200 Subject: [PATCH 07/41] Handle html --- bittensor_cli/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0abe3d40..2549dd03 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5053,7 +5053,16 @@ 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) From 6ac423214d5a3326fd5c99b218e04c83f6207fa8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 23:00:15 +0200 Subject: [PATCH 08/41] E2E test --- tests/e2e_tests/test_staking_sudo.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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( From 5e6992cb0f31595bdc60f72d3b5e7d2c76d473d6 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 4 Aug 2025 18:47:48 +0200 Subject: [PATCH 09/41] Reconfigure the asyncio runner to use a single event loop for everything. --- bittensor_cli/cli.py | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2549dd03..e58e3f4e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -643,6 +643,17 @@ 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) @@ -1097,12 +1108,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 +1136,13 @@ async def _run(): exit_early is True ): # temporarily to handle multiple run commands in one session try: + 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 +1193,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: From de0259f927178723ce0316706e5ae2d2c140ab4a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 19:16:45 +0200 Subject: [PATCH 10/41] Shows account balance if transferring all. --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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]" From 2d82c63b97b0b7ee1ad391b28fded825d83256c4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 20:08:16 +0200 Subject: [PATCH 11/41] Allows for typer>=0.16 and Click 8.2+ --- bittensor_cli/cli.py | 2 +- pyproject.toml | 3 +-- tests/e2e_tests/utils.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e58e3f4e..b4b4afbe 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -250,7 +250,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.", ) diff --git a/pyproject.toml b/pyproject.toml index 91a77e29..42ea0367 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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,7 +26,7 @@ dependencies = [ "PyYAML~=6.0.1", "rich>=13.7,<15.0", "scalecodec==1.2.11", - "typer>=0.12,<0.16", + "typer>=0.16", "bittensor-wallet>=3.0.7", "plotille>=5.0.0", "plotly>=6.0.0", diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 7a3c0993..a2a87e48 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -55,7 +55,7 @@ def exec_command( extra_args.extend(["--network", "ws://127.0.0.1:9945"]) # Capture stderr separately from stdout - runner = CliRunner(mix_stderr=False) + runner = CliRunner() # Prepare the command arguments args = [ command, From 42331cb172ed246b5c9207472598e328a3bd95b8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 20:24:24 +0200 Subject: [PATCH 12/41] Allow custom config path using env var --- bittensor_cli/cli.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b4b4afbe..90e405b5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -655,7 +655,9 @@ def __init__(self): 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", @@ -663,7 +665,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) @@ -1497,6 +1504,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(): From 24387012b3cd2ad3dded964a1aa981dba2e9841b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 22:32:36 +0200 Subject: [PATCH 13/41] Respect config sets --- bittensor_cli/cli.py | 49 +++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 90e405b5..7768f02e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2233,14 +2233,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) @@ -2292,14 +2293,15 @@ 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)", - default=defaults.wallet.name, + default=self.config.get("wallet_name") or defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -2418,18 +2420,6 @@ def wallet_new_hotkey( """ self.verbosity_handler(quiet, verbose, json_output) - if not wallet_name: - wallet_name = Prompt.ask( - f"Enter the [{COLORS.G.CK}]wallet name", - default=defaults.wallet.name, - ) - - if not wallet_hotkey: - wallet_hotkey = Prompt.ask( - f"Enter the name of the [{COLORS.G.HK}]new hotkey", - default=defaults.wallet.hotkey, - ) - wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2479,8 +2469,7 @@ def wallet_associate_hotkey( "[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]" ) - 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, @@ -2501,7 +2490,10 @@ def wallet_associate_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_display = ( + f"hotkey [blue]{wallet_hotkey}[/blue] " + f"[{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" + ) return self._run_command( wallets.associate_hotkey( @@ -2627,7 +2619,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( @@ -2691,18 +2684,19 @@ 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, + default=self.config.get("wallet_name") or 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( @@ -3999,7 +3993,8 @@ def stake_move( origin_hotkey = Prompt.ask( "Enter the [blue]origin hotkey[/blue] name or " "[blue]ss58 address[/blue] where the stake will be moved from " - "[dim](or Press Enter to view existing stakes)[/dim]" + "[dim](or Press Enter to view existing stakes)[/dim]", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) if origin_hotkey == "": interactive_selection = True @@ -4162,8 +4157,10 @@ 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] " - "[dim](or press Enter to select from existing stakes)[/dim]" + "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]", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) if origin_hotkey == "": interactive_selection = True From b121e6785dda5b0d44dc9440b18ba61f912a1072 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 22:35:26 +0200 Subject: [PATCH 14/41] Add more defaults. --- bittensor_cli/cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7768f02e..f1ac1532 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2466,7 +2466,8 @@ 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, ) if wallet_hotkey and is_valid_ss58_address(wallet_hotkey): @@ -2540,13 +2541,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( From ba7cca5d25e656b14ec10f8123a2bd755b0309e9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 22:40:37 +0200 Subject: [PATCH 15/41] Removed some defaults --- bittensor_cli/cli.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f1ac1532..bc95fd39 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3995,8 +3995,7 @@ def stake_move( origin_hotkey = Prompt.ask( "Enter the [blue]origin hotkey[/blue] name or " "[blue]ss58 address[/blue] where the stake will be moved from " - "[dim](or Press Enter to view existing stakes)[/dim]", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + "[dim](or Press Enter to view existing stakes)[/dim]" ) if origin_hotkey == "": interactive_selection = True @@ -4161,8 +4160,7 @@ def stake_transfer( origin_hotkey = Prompt.ask( "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]", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + "[dim](or press Enter to select from existing stakes)[/dim]" ) if origin_hotkey == "": interactive_selection = True From e4ba58683bbf682bd9ea7fb9a49dda03251b63cc Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 22:42:28 +0200 Subject: [PATCH 16/41] More defaults --- bittensor_cli/cli.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index bc95fd39..587fec97 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2420,6 +2420,18 @@ def wallet_new_hotkey( """ self.verbosity_handler(quiet, verbose, json_output) + if not wallet_name: + wallet_name = Prompt.ask( + f"Enter the [{COLORS.G.CK}]wallet name", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + + if not wallet_hotkey: + wallet_hotkey = Prompt.ask( + f"Enter the name of the [{COLORS.G.HK}]new hotkey", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + ) + wallet = self.wallet_ask( wallet_name, wallet_path, From 452411911dd21d778a0c8540c17d167b9594fff0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 22:58:47 +0200 Subject: [PATCH 17/41] Removed some defaults --- bittensor_cli/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 587fec97..961a1f0f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2301,7 +2301,7 @@ def wallet_regen_coldkey_pub( if not wallet_name: wallet_name = Prompt.ask( f"Enter the name of the [{COLORS.G.CK}]new wallet (coldkey)", - default=self.config.get("wallet_name") or defaults.wallet.name, + default=defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -2429,7 +2429,7 @@ def wallet_new_hotkey( if not wallet_hotkey: wallet_hotkey = Prompt.ask( f"Enter the name of the [{COLORS.G.HK}]new hotkey", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + default=defaults.wallet.hotkey, ) wallet = self.wallet_ask( @@ -2705,7 +2705,6 @@ def wallet_create_wallet( if not wallet_name: wallet_name = Prompt.ask( f"Enter the name of the [{COLORS.G.CK}]new wallet (coldkey)", - default=self.config.get("wallet_name") or defaults.wallet.name, ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( From cfc18e32e51a6b827a12ea9de43b086256f4817b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 5 Aug 2025 23:27:47 +0200 Subject: [PATCH 18/41] Added info about preinstalled macOS CPython. --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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/) From da471d0af5bfb5351e39fcb385018d3fe65e7b63 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 17:39:01 +0200 Subject: [PATCH 19/41] Basic implementation --- bittensor_cli/cli.py | 66 ++++++++++++++++++++++++++- bittensor_cli/src/commands/wallets.py | 44 ++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 961a1f0f..d7c7df6e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2285,7 +2285,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. """ @@ -2317,7 +2317,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 @@ -2383,6 +2383,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}]new wallet (coldkey)", + 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, diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 61efa696..237a4849 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -287,6 +287,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.hotkey.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, diff --git a/pyproject.toml b/pyproject.toml index 42ea0367..426b1532 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "rich>=13.7,<15.0", "scalecodec==1.2.11", "typer>=0.16", - "bittensor-wallet>=3.0.7", + "bittensor-wallet>=3.1.0", "plotille>=5.0.0", "plotly>=6.0.0", ] From b8e6d79fc3af7ba2265e2607f8c6f4c4dc1413d2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 17:39:09 +0200 Subject: [PATCH 20/41] Ruff --- bittensor_cli/src/commands/wallets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 237a4849..da9dde9f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -288,7 +288,7 @@ async def regen_hotkey( async def regen_hotkey_pub( -wallet: Wallet, + wallet: Wallet, ss58_address: str, public_key_hex: str, overwrite: Optional[bool] = False, From 580b8a74278095c06467536c3728d58d7234e1fb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 17:40:53 +0200 Subject: [PATCH 21/41] Added commands --- bittensor_cli/cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d7c7df6e..e9d1a0e2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -791,6 +791,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-hotkey-pub", 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) @@ -973,6 +976,10 @@ def __init__(self): "regen_hotkey", hidden=True, )(self.wallet_regen_hotkey) + self.wallet_app.command( + "regen_hotkey_pub", + hidden=True, + )(self.wallet_regen_hotkey_pub) self.wallet_app.command( "new_hotkey", hidden=True, From b959fe35d9754800264069420bbc46714a7e1020 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 17:44:25 +0200 Subject: [PATCH 22/41] Added commands --- bittensor_cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e9d1a0e2..7106529d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -792,7 +792,7 @@ def __init__(self): "regen-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_regen_hotkey) self.wallet_app.command( - "regen-hotkey-pub", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + "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"] @@ -977,7 +977,7 @@ def __init__(self): hidden=True, )(self.wallet_regen_hotkey) self.wallet_app.command( - "regen_hotkey_pub", + "regen_hotkeypub", hidden=True, )(self.wallet_regen_hotkey_pub) self.wallet_app.command( From 6d975c3b737895e82b107f69931edcab5d331d95 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 17:48:14 +0200 Subject: [PATCH 23/41] Error handling --- bittensor_cli/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7106529d..7f5056a8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1150,7 +1150,8 @@ async def _run(): exit_early is True ): # temporarily to handle multiple run commands in one session try: - await self.subtensor.substrate.close() + 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)): From 75501bb05d7fedb24dd43db91712657e0ae79899 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 17:52:08 +0200 Subject: [PATCH 24/41] Improved text --- bittensor_cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7f5056a8..8a9194f9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2308,7 +2308,7 @@ def wallet_regen_coldkey_pub( 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) @@ -2429,7 +2429,7 @@ def wallet_regen_hotkey_pub( 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 hotkeypub", default=defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) From 9aacee83c5eb7f37cb7d4614511445b476f11bfb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 17:59:10 +0200 Subject: [PATCH 25/41] Add test --- tests/e2e_tests/test_wallet_creations.py | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 019cad6b..b2478f53 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,34 @@ def test_wallet_regen(wallet_setup, capfd): ) print("Passed wallet regen_hotkey command ✅") + hotkeypub_path = os.path.join(wallet_path, "new_wallet", "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(hotkey_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): """ From ed21df78aae9b01b86dd6e712a7a626ebb7589ff Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 18:39:39 +0200 Subject: [PATCH 26/41] Updated reqs. Will need to be fixed upstream first. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 426b1532..4238ae4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "rich>=13.7,<15.0", "scalecodec==1.2.11", "typer>=0.16", - "bittensor-wallet>=3.1.0", + "bittensor-wallet>=3.1.1", "plotille>=5.0.0", "plotly>=6.0.0", ] From 8dc3a8f5103feffdd521363a3194ee04aefcfda6 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 18:40:51 +0200 Subject: [PATCH 27/41] Update test --- tests/e2e_tests/test_wallet_creations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index b2478f53..692405d0 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -543,7 +543,7 @@ def test_wallet_regen(wallet_setup, capfd): ) print("Passed wallet regen_hotkey command ✅") - hotkeypub_path = os.path.join(wallet_path, "new_wallet", "hotkeypub.txt") + 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", From 1c19e0fd5bda79ecc9130ac822b16b7bfae818e4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 18:40:58 +0200 Subject: [PATCH 28/41] Ruff --- tests/e2e_tests/test_wallet_creations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 692405d0..7dcbc597 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -543,7 +543,9 @@ 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") + 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", From 5e257b53c88686431f170dae019ddf87e244e6e9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 19:02:39 +0200 Subject: [PATCH 29/41] Specify click version --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 42ea0367..957f9a9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "async-substrate-interface>=1.4.2", "aiohttp~=3.10.2", "backoff~=2.2.1", + "click>=8.2.0", "GitPython>=3.0.0", "netaddr~=1.3.0", "numpy>=2.0.1,<3.0.0", From 20814e11ede6d53bc4e5d032efd4807685e8a0f2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 19:18:15 +0200 Subject: [PATCH 30/41] Check version --- pyproject.toml | 2 +- tests/e2e_tests/utils.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 957f9a9d..254cb51d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,6 @@ dependencies = [ "async-substrate-interface>=1.4.2", "aiohttp~=3.10.2", "backoff~=2.2.1", - "click>=8.2.0", "GitPython>=3.0.0", "netaddr~=1.3.0", "numpy>=2.0.1,<3.0.0", @@ -29,6 +28,7 @@ dependencies = [ "scalecodec==1.2.11", "typer>=0.16", "bittensor-wallet>=3.0.7", + "packaging", "plotille>=5.0.0", "plotly>=6.0.0", ] diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index a2a87e48..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() + 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, From 714cafb97dd90a20396ec2171e82356205931cd4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:03:18 +0200 Subject: [PATCH 31/41] New fn to retrieve wallet hotkey ss58 --- bittensor_cli/src/bittensor/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index ef5f2e4b..735abe52 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -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 From aa70fb3ac4daf30a20820c4a63b197403d59e16e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:16:59 +0200 Subject: [PATCH 32/41] Update some commands. --- bittensor_cli/src/commands/sudo.py | 24 ++++++++------- bittensor_cli/src/commands/wallets.py | 42 ++++++++++++++++----------- 2 files changed, 38 insertions(+), 28 deletions(-) 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 da9dde9f..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": "", @@ -316,7 +317,7 @@ async def regen_hotkey_pub( "name": new_hotkeypub.name, "path": new_hotkeypub.path, "hotkey": new_hotkeypub.hotkey_str, - "hotkey_ss58": new_hotkeypub.hotkey.ss58_address, + "hotkey_ss58": new_hotkeypub.hotkeypub.ss58_address, "coldkey_ss58": new_hotkeypub.coldkeypub.ss58_address, }, "error": "", @@ -367,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": "", @@ -446,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: @@ -499,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) @@ -838,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) @@ -1297,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 @@ -1329,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: @@ -1515,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 + [ From b6c587603a07f0f99414ef61322075baeab65313 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:19:24 +0200 Subject: [PATCH 33/41] More commands. --- bittensor_cli/src/commands/stake/move.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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) From 68e5afc62fb0a3973c6bec1c31007d57f3a6620e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:27:31 +0200 Subject: [PATCH 34/41] More commands. --- bittensor_cli/cli.py | 17 +++++++++-------- .../src/bittensor/subtensor_interface.py | 3 ++- bittensor_cli/src/bittensor/utils.py | 2 +- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8a9194f9..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 @@ -1734,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 @@ -2572,7 +2573,7 @@ def wallet_associate_hotkey( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - hotkey_ss58 = wallet.hotkey.ss58_address + hotkey_ss58 = get_hotkey_pub_ss58(wallet) hotkey_display = ( f"hotkey [blue]{wallet_hotkey}[/blue] " f"[{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" @@ -3588,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( @@ -4052,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 @@ -4091,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 @@ -4103,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: @@ -4256,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 @@ -4268,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: 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 735abe52..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 ( From f40e8b56e12043629b460834d7069d411fb8e32b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:40:02 +0200 Subject: [PATCH 35/41] More commands. --- .../src/bittensor/extrinsics/root.py | 12 +++---- bittensor_cli/src/commands/stake/add.py | 8 ++--- .../src/commands/stake/children_hotkeys.py | 31 ++++++++++--------- bittensor_cli/src/commands/stake/remove.py | 10 +++--- bittensor_cli/src/commands/subnets/subnets.py | 6 ++-- bittensor_cli/src/commands/weights.py | 4 +-- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index d8d4900a..3f3c4f03 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -36,7 +36,7 @@ u16_normalized_float, print_verbose, format_error_message, - unlock_key, + unlock_key, get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -310,7 +310,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 +322,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 +341,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 +391,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 +415,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/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index b223eaf2..e80efe45 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -19,7 +19,7 @@ print_error, print_verbose, unlock_key, - json_console, + json_console, get_hotkey_pub_ss58, ) from bittensor_wallet import Wallet @@ -552,7 +552,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 +572,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 +581,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..82e1271d 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -20,7 +20,7 @@ is_valid_ss58_address, format_error_message, unlock_key, - json_console, + json_console, get_hotkey_pub_ss58, ) @@ -464,7 +464,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 +472,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 +500,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 +608,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 +647,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 +746,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 +756,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 +766,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 +827,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/remove.py b/bittensor_cli/src/commands/stake/remove.py index a28254e3..1125770a 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -21,7 +21,7 @@ format_error_message, group_subnets, unlock_key, - json_console, + json_console, get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -407,7 +407,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 +1198,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 +1230,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 +1238,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/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 62a20b63..1e546d09 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -35,7 +35,7 @@ get_subnet_name, unlock_key, blocks_to_duration, - json_console, + json_console, get_hotkey_pub_ss58, ) if TYPE_CHECKING: @@ -114,7 +114,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 +1654,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/weights.py b/bittensor_cli/src/commands/weights.py index 68ec0308..68539866 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -14,7 +14,7 @@ err_console, console, format_error_message, - json_console, + json_console, get_hotkey_pub_ss58, ) from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, @@ -128,7 +128,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, From edfac733c9693ed4a3537f868b6a411e84025dd5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:40:12 +0200 Subject: [PATCH 36/41] Ruff --- bittensor_cli/src/commands/stake/add.py | 3 ++- bittensor_cli/src/commands/stake/children_hotkeys.py | 3 ++- bittensor_cli/src/commands/stake/remove.py | 3 ++- bittensor_cli/src/commands/subnets/subnets.py | 3 ++- bittensor_cli/src/commands/weights.py | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index e80efe45..6579d976 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -19,7 +19,8 @@ print_error, print_verbose, unlock_key, - json_console, get_hotkey_pub_ss58, + json_console, + get_hotkey_pub_ss58, ) from bittensor_wallet import Wallet diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 82e1271d..d01e8d14 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -20,7 +20,8 @@ is_valid_ss58_address, format_error_message, unlock_key, - json_console, get_hotkey_pub_ss58, + json_console, + get_hotkey_pub_ss58, ) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 1125770a..67f0109f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -21,7 +21,8 @@ format_error_message, group_subnets, unlock_key, - json_console, get_hotkey_pub_ss58, + json_console, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 1e546d09..bb42132c 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -35,7 +35,8 @@ get_subnet_name, unlock_key, blocks_to_duration, - json_console, get_hotkey_pub_ss58, + json_console, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 68539866..63e3b72f 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -14,7 +14,8 @@ err_console, console, format_error_message, - json_console, get_hotkey_pub_ss58, + json_console, + get_hotkey_pub_ss58, ) from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, From 95a4af5ef9c864f89f0b169775a545b1c0b6891d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:45:39 +0200 Subject: [PATCH 37/41] Think this is it --- .../src/bittensor/extrinsics/registration.py | 55 ++++++++++--------- .../src/bittensor/extrinsics/root.py | 3 +- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 8bbc8064..d1800e1a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -38,7 +38,7 @@ print_verbose, print_error, unlock_key, - hex_to_bytes, + hex_to_bytes, get_hotkey_pub_ss58, ) if typing.TYPE_CHECKING: @@ -490,7 +490,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 +525,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 +577,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 +598,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 +639,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 +704,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 +751,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 +773,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 +1146,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 +1755,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 +1800,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 +1834,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 3f3c4f03..207fb864 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -36,7 +36,8 @@ u16_normalized_float, print_verbose, format_error_message, - unlock_key, get_hotkey_pub_ss58, + unlock_key, + get_hotkey_pub_ss58, ) if TYPE_CHECKING: From 411fadd1ca2937f01b9ebc0217baad88da464233 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 20:45:58 +0200 Subject: [PATCH 38/41] =?UTF-8?q?Ruff=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bittensor_cli/src/bittensor/extrinsics/registration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index d1800e1a..b2461d89 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -38,7 +38,8 @@ print_verbose, print_error, unlock_key, - hex_to_bytes, get_hotkey_pub_ss58, + hex_to_bytes, + get_hotkey_pub_ss58, ) if typing.TYPE_CHECKING: From 29cb6b9516214822f236e806f1a47df7c78634d5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 21:19:48 +0200 Subject: [PATCH 39/41] Req --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8e29fb20..95709532 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ "rich>=13.7,<15.0", "scalecodec==1.2.11", "typer>=0.16", - "bittensor-wallet>=3.1.1", + "bittensor-wallet>=4.0.0", "packaging", "plotille>=5.0.0", "plotly>=6.0.0", From 8d4819887fa9a945809874061b2868f6510e3b91 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 22:36:18 +0200 Subject: [PATCH 40/41] Typo --- tests/e2e_tests/test_wallet_creations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 7dcbc597..1e702067 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -566,7 +566,7 @@ def test_wallet_regen(wallet_setup, capfd): # Wait a bit to ensure file system updates modification time time.sleep(2) - new_hotkeypub_mod_time = os.path.getmtime(hotkey_path) + 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" From 53adefb1947593bf8684e1687703e8432eeddca4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 6 Aug 2025 23:08:49 +0200 Subject: [PATCH 41/41] Changelog + version --- CHANGELOG.md | 13 +++++++++++++ pyproject.toml | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) 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/pyproject.toml b/pyproject.toml index 95709532..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 = [