diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f96f8a..91b60a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,19 @@ # Changelog +## 9.14.1 /2025-10-23 +* Updates kappa to root sudo only in-line with devnet-ready by @thewhaleking in https://github.com/opentensor/btcli/pull/668 +* Adds additional warnings for move vs transfer by @thewhaleking in https://github.com/opentensor/btcli/pull/672 +* Childkey take was incorrectly labeled. by @thewhaleking in https://github.com/opentensor/btcli/pull/669 +* Updates the help text of crownloan refund by @thewhaleking in https://github.com/opentensor/btcli/pull/674 +* Add a warn flag when --netuid 0 is used for btcli hotkey swap by @nstankov-stkd in https://github.com/opentensor/btcli/pull/666 + * Add warning and confirmation for `wallet swap_hotkey --netuid 0` to prevent accidental misuse. Using `--netuid 0` only swaps the hotkey on the root network (netuid 0) and does NOT move child hotkey delegation mappings. This is not a full swap across all subnets. Updated documentation and added comprehensive unit tests to clarify proper usage. +* Edge case bug fixes by @thewhaleking in https://github.com/opentensor/btcli/pull/675 +* Adds wallet balance sorting by @thewhaleking in https://github.com/opentensor/btcli/pull/676 + +## New Contributors +* @nstankov-stkd made their first contribution in https://github.com/opentensor/btcli/pull/666 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.14.0...v9.14.1 + ## 9.14.0 /2025-10-20 * Skips senate tests by @thewhaleking in https://github.com/opentensor/btcli/pull/658 * Feat/crowdloans by @ibraheem-abe in https://github.com/opentensor/btcli/pull/657 diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 220de3d4..3b609604 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -39,6 +39,7 @@ Constants, COLORS, HYPERPARAMS, + WalletOptions, ) from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance @@ -92,6 +93,7 @@ subnets, mechanisms as subnet_mechanisms, ) +from bittensor_cli.src.commands.wallets import SortByBalance from bittensor_cli.version import __version__, __version_as_int__ try: @@ -1302,6 +1304,7 @@ def _run_command(self, cmd: Coroutine, exit_early: bool = True): async def _run(): initiated = False + exception_occurred = False try: if self.subtensor: await self.subtensor.substrate.initialize() @@ -1311,6 +1314,7 @@ async def _run(): except (ConnectionRefusedError, ssl.SSLError, InvalidHandshake): err_console.print(f"Unable to connect to the chain: {self.subtensor}") verbose_console.print(traceback.format_exc()) + exception_occurred = True except ( ConnectionClosed, SubstrateRequestException, @@ -1322,22 +1326,25 @@ async def _run(): elif isinstance(e, RuntimeError): pass # Temporarily to handle loop bound issues verbose_console.print(traceback.format_exc()) + exception_occurred = True except Exception as e: err_console.print(f"An unknown error has occurred: {e}") verbose_console.print(traceback.format_exc()) + exception_occurred = True finally: if initiated is False: asyncio.create_task(cmd).cancel() if ( exit_early is True ): # temporarily to handle multiple run commands in one session - try: - if self.subtensor: + if self.subtensor: + try: await self.subtensor.substrate.close() + 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}") + if exception_occurred: 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.event_loop.run_until_complete(_run()) @@ -1910,7 +1917,7 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: Optional[list[Literal[WO.NAME, WO.PATH, WO.HOTKEY]]] = None, + ask_for: Optional[list[WalletOptions]] = None, validate: WV = WV.WALLET, return_wallet_and_hotkey: bool = False, ) -> Union[Wallet, tuple[Wallet, str]]: @@ -2286,15 +2293,44 @@ def wallet_swap_hotkey( - Make sure that your original key pair (coldkeyA, hotkeyA) is already registered. - Make sure that you use a newly created hotkeyB in this command. A hotkeyB that is already registered cannot be used in this command. - - You can specify the netuid for which you want to swap the hotkey for. If it is not defined, the swap will be initiated for all subnets. + - If NO netuid is specified, the swap will be initiated for ALL subnets (recommended for most users). + - If a SPECIFIC netuid is specified (e.g., --netuid 1), the swap will only affect that particular subnet. + - WARNING: Using --netuid 0 will ONLY swap on the root network (netuid 0), NOT a full swap across all subnets. Use without --netuid for full swap. - Finally, note that this command requires a fee of 1 TAO for recycling and this fee is taken from your wallet (coldkeyA). EXAMPLE + Full swap across all subnets (recommended): + [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey + + Swap for a specific subnet only: [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey --netuid 1 """ netuid = get_optional_netuid(netuid, all_netuids) self.verbosity_handler(quiet, verbose, json_output) + + # Warning for netuid 0 - only swaps on root network, not a full swap + if netuid == 0 and prompt: + console.print( + "\n[bold yellow]⚠️ WARNING: Using --netuid 0 for swap_hotkey[/bold yellow]\n" + ) + console.print( + "[yellow]Specifying --netuid 0 will ONLY swap the hotkey on the root network (netuid 0).[/yellow]\n" + ) + console.print( + "[yellow]It will NOT move child hotkey delegation mappings on root.[/yellow]\n" + ) + console.print( + f"[bold green]btcli wallet swap_hotkey {destination_hotkey_name or ''} " + f"--wallet-name {wallet_name or ''} " + f"--wallet-hotkey {wallet_hotkey or ''}[/bold green]\n" + ) + + if not Confirm.ask( + "Are you SURE you want to proceed with --netuid 0 (only root network swap)?", + default=False, + ): + return original_wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3132,6 +3168,11 @@ def wallet_balance( "-a", help="Whether to display the balances for all the wallets.", ), + sort_by: Optional[wallets.SortByBalance] = typer.Option( + None, + "--sort", + help="When using `--all`, sorts the wallets by a given column", + ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3231,7 +3272,7 @@ def wallet_balance( subtensor = self.initialize_chain(network) return self._run_command( wallets.wallet_balance( - wallet, subtensor, all_balances, ss58_addresses, json_output + wallet, subtensor, all_balances, ss58_addresses, sort_by, json_output ) ) @@ -4573,9 +4614,17 @@ def stake_move( [green]$[/green] btcli stake move """ self.verbosity_handler(quiet, verbose, json_output) - console.print( - "[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]" - ) + if prompt: + if not Confirm.ask( + "This transaction will [bold]move stake[/bold] to another hotkey while keeping the same " + "coldkey ownership. Do you wish to continue? ", + default=False, + ): + raise typer.Exit() + else: + console.print( + "[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]" + ) if not destination_hotkey: dest_wallet_or_ss58 = Prompt.ask( "Enter the [blue]destination wallet[/blue] where destination hotkey is located or " @@ -4770,9 +4819,18 @@ def stake_transfer( [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 """ self.verbosity_handler(quiet, verbose, json_output) - console.print( - "[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]" - ) + if prompt: + if not Confirm.ask( + "This transaction will [bold]transfer ownership[/bold] from one coldkey to another, in subnets " + "which have enabled it. You should ensure that the destination coldkey is " + "[bold]not a validator hotkey[/bold] before continuing. Do you wish to continue?", + default=False, + ): + raise typer.Exit() + else: + console.print( + "[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]" + ) if not dest_ss58: dest_ss58 = Prompt.ask( @@ -5229,7 +5287,7 @@ def stake_childkey_take( network: Optional[list[str]] = Options.network, child_hotkey_ss58: Optional[str] = typer.Option( None, - "child-hotkey-ss58", + "--child-hotkey-ss58", help="The hotkey SS58 to designate as child (not specifying will use the provided wallet's hotkey)", prompt=False, ), @@ -5306,7 +5364,7 @@ def stake_childkey_take( subtensor=self.initialize_chain(network), netuid=netuid, take=take, - hotkey=hotkey, + hotkey=child_hotkey_ss58, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, prompt=prompt, @@ -7764,10 +7822,10 @@ def crowd_refund( """ Refund contributors of a non-finalized crowdloan. - Any account may call this once the crowdloan is no longer wanted. Each call - refunds up to the on-chain `RefundContributorsLimit` contributors (currently - 50) excluding the creator. Run it repeatedly until everyone except the creator - has been reimbursed. + Only the creator may call this. Each call refunds up to the on-chain `RefundContributorsLimit` contributors + (currently 50) excluding the creator. Run it repeatedly until everyone except the creator has been reimbursed. + + Contributors can call `btcli crowdloan withdraw` at will. """ self.verbosity_handler(quiet, verbose, json_output) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 9b7d749b..598f9716 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -635,7 +635,7 @@ class RootSudoOnly(Enum): HYPERPARAMS = { # btcli name: (subtensor method, root-only enum) "rho": ("sudo_set_rho", RootSudoOnly.FALSE), - "kappa": ("sudo_set_kappa", RootSudoOnly.FALSE), + "kappa": ("sudo_set_kappa", RootSudoOnly.TRUE), "immunity_period": ("sudo_set_immunity_period", RootSudoOnly.FALSE), "min_allowed_weights": ("sudo_set_min_allowed_weights", RootSudoOnly.FALSE), "max_weights_limit": ("sudo_set_max_weight_limit", RootSudoOnly.FALSE), diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 720e8d35..6886fb41 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -116,9 +116,6 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: ) return False, None console.print(f"[dark_orange]Initiating transfer on network: {subtensor.network}") - # Unlock wallet coldkey. - if not unlock_key(wallet).success: - return False, None call_params: dict[str, Optional[Union[str, int]]] = {"dest": destination} if transfer_all: @@ -175,11 +172,20 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: # Ask before moving on. if prompt: + hk_owner = await subtensor.get_hotkey_owner(destination, check_exists=False) + if hk_owner and hk_owner != destination: + if not Confirm.ask( + f"The destination appears to be a hotkey, owned by [bright_magenta]{hk_owner}[/bright_magenta]. " + f"Only proceed if you are absolutely sure that [bright_magenta]{destination}[/bright_magenta] is the " + f"correct destination.", + default=False, + ): + return False, None if not Confirm.ask( "Do you want to transfer:[bold white]\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"[bright_magenta]{wallet.coldkeypub.ss58_address}\n[/bright_magenta]" f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]\n" f"[bright_yellow]Transferring is not the same as staking. To instead stake, use " f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow].\n" @@ -187,6 +193,10 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: ): return False, None + # Unlock wallet coldkey. + if not unlock_key(wallet).success: + return False, None + with console.status(":satellite: Transferring...", spinner="earth"): success, block_hash, err_msg, ext_receipt = await do_transfer() diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a0d6dd6e..2ef90d28 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1123,6 +1123,7 @@ async def get_hotkey_owner( self, hotkey_ss58: str, block_hash: Optional[str] = None, + check_exists: bool = True, ) -> Optional[str]: val = await self.query( module="SubtensorModule", @@ -1130,10 +1131,15 @@ async def get_hotkey_owner( params=[hotkey_ss58], block_hash=block_hash, ) - if val: - exists = await self.does_hotkey_exist(hotkey_ss58, block_hash=block_hash) + if check_exists: + if val: + exists = await self.does_hotkey_exist( + hotkey_ss58, block_hash=block_hash + ) + else: + exists = False else: - exists = False + exists = True hotkey_owner = val if exists else None return hotkey_owner diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3846dc7b..f9ade450 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1496,7 +1496,7 @@ async def print_extrinsic_id( Args: extrinsic_receipt: AsyncExtrinsicReceipt object from a successful extrinsic submission. """ - if extrinsic_receipt is None: + if extrinsic_receipt is None or not (await extrinsic_receipt.is_success): return substrate = extrinsic_receipt.substrate ext_id = await extrinsic_receipt.get_extrinsic_identifier() diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 53587a57..c986b859 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -566,7 +566,6 @@ async def move_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - await print_extrinsic_id(response) ext_id = await response.get_extrinsic_identifier() if not prompt: @@ -580,6 +579,7 @@ async def move_stake( ) return False, "" else: + await print_extrinsic_id(response) console.print( ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" ) @@ -755,7 +755,6 @@ async def transfer_stake( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) ext_id = await response.get_extrinsic_identifier() - await print_extrinsic_id(extrinsic) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -767,7 +766,7 @@ async def transfer_stake( f"{format_error_message(await response.error_message)}" ) return False, "" - + await print_extrinsic_id(extrinsic) # Get and display new stake balances new_stake, new_dest_stake = await asyncio.gather( subtensor.get_stake( @@ -933,7 +932,6 @@ async def swap_stake( wait_for_finalization=wait_for_finalization, ) ext_id = await response.get_extrinsic_identifier() - await print_extrinsic_id(response) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -945,7 +943,7 @@ async def swap_stake( f"{format_error_message(await response.error_message)}" ) return False, "" - + await print_extrinsic_id(response) # Get and display new stake balances new_stake, new_dest_stake = await asyncio.gather( subtensor.get_stake( diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 4d74773c..6473f2c6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -3,6 +3,7 @@ import json import os from collections import defaultdict +from enum import Enum from typing import Generator, Optional, Union import aiohttp @@ -53,6 +54,27 @@ ) +class SortByBalance(Enum): + name = "name" + free = "free" + staked = "staked" + total = "total" + + +def _sort_by_balance_key(sort_by: SortByBalance): + """Get the sort key function based on the enum""" + if sort_by == SortByBalance.name: + return lambda row: row[0].lower() # Case-insensitive alphabetical sort + elif sort_by == SortByBalance.free: + return lambda row: row[2] + elif sort_by == SortByBalance.staked: + return lambda row: row[3] + elif sort_by == SortByBalance.total: + return lambda row: row[4] + else: + raise ValueError("Invalid sort key") + + async def associate_hotkey( wallet: Wallet, subtensor: SubtensorInterface, @@ -565,6 +587,7 @@ async def wallet_balance( subtensor: SubtensorInterface, all_balances: bool, ss58_addresses: Optional[str] = None, + sort_by: Optional[SortByBalance] = None, json_output: bool = False, ): """Retrieves the current balance of the specified wallet""" @@ -644,14 +667,26 @@ async def wallet_balance( width=None, leading=True, ) - - for name, (coldkey, free, staked) in balances.items(): + balance_rows = [ + (name, coldkey, free, staked, free + staked) + for (name, (coldkey, free, staked)) in balances.items() + ] + sorted_balances = ( + sorted( + balance_rows, + key=_sort_by_balance_key(sort_by), + reverse=(sort_by != SortByBalance.name), + ) + if sort_by is not None + else balance_rows + ) + for name, coldkey, free, staked, total in sorted_balances: table.add_row( name, coldkey, str(free), str(staked), - str(free + staked), + str(total), ) table.add_row() table.add_row( diff --git a/pyproject.toml b/pyproject.toml index a9f725de..47d49b92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.14.0" +version = "9.14.1" description = "Bittensor CLI" readme = "README.md" authors = [ diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 24f83bdf..c336f661 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -1,7 +1,7 @@ import asyncio import json -from bittensor_cli.src import HYPERPARAMS +from bittensor_cli.src import HYPERPARAMS, RootSudoOnly from .utils import turn_off_hyperparam_freeze_window """ @@ -83,7 +83,7 @@ def test_hyperparams_setting(local_chain, wallet_setup): for hyperparam in all_hyperparams: hp[hyperparam["hyperparameter"]] = hyperparam["value"] for key, (_, sudo_only) in HYPERPARAMS.items(): - if key in hp.keys() and not sudo_only: + if key in hp.keys() and sudo_only == RootSudoOnly.FALSE: if isinstance(hp[key], bool): new_val = not hp[key] elif isinstance(hp[key], int): diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index b7933e22..a17ed840 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -1,8 +1,8 @@ import pytest import typer -from bittensor_cli.cli import parse_mnemonic -from unittest.mock import AsyncMock, patch, MagicMock +from bittensor_cli.cli import parse_mnemonic, CLIManager +from unittest.mock import AsyncMock, patch, MagicMock, Mock def test_parse_mnemonic(): @@ -51,3 +51,180 @@ async def test_subnet_sets_price_correctly(): ) mock_price_method.assert_awaited_once_with(netuid=1, block_hash=None) assert subnet_info.price == mock_price + + +@patch("bittensor_cli.cli.Confirm") +@patch("bittensor_cli.cli.console") +def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): + """ + Test that swap_hotkey shows warning when netuid=0 and prompt=True, + and exits when user declines confirmation + """ + # Setup + cli_manager = CLIManager() + mock_confirm.ask.return_value = False # User declines + + # Mock dependencies to prevent actual execution + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + ): + mock_wallet_ask.return_value = Mock() + + # Call the method with netuid=0 and prompt=True + result = cli_manager.wallet_swap_hotkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="old_hotkey", + netuid=0, + all_netuids=False, + network=None, + destination_hotkey_name="new_hotkey", + quiet=False, + verbose=False, + prompt=True, + json_output=False, + ) + + # Assert: Warning was displayed (4 console.print calls for the warning) + assert mock_console.print.call_count >= 4 + warning_calls = [str(call) for call in mock_console.print.call_args_list] + assert any( + "WARNING" in str(call) and "netuid 0" in str(call) for call in warning_calls + ) + assert any("root network" in str(call) for call in warning_calls) + assert any( + "NOT move child hotkey delegation" in str(call) for call in warning_calls + ) + + # Assert: User was asked to confirm + mock_confirm.ask.assert_called_once() + confirm_message = mock_confirm.ask.call_args[0][0] + assert "SURE" in confirm_message + assert "netuid 0" in confirm_message or "root network" in confirm_message + + # Assert: Function returned None (early exit) because user declined + assert result is None + + +@patch("bittensor_cli.cli.Confirm") +@patch("bittensor_cli.cli.console") +def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_confirm): + """ + Test that swap_hotkey proceeds when netuid=0 and user confirms + """ + # Setup + cli_manager = CLIManager() + mock_confirm.ask.return_value = True # User confirms + + # Mock dependencies + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command") as mock_run_command, + ): + mock_wallet = Mock() + mock_wallet_ask.return_value = mock_wallet + + # Call the method + cli_manager.wallet_swap_hotkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="old_hotkey", + netuid=0, + all_netuids=False, + network=None, + destination_hotkey_name="new_hotkey", + quiet=False, + verbose=False, + prompt=True, + json_output=False, + ) + + # Assert: Warning was shown and confirmed + mock_confirm.ask.assert_called_once() + + # Assert: Command execution proceeded + mock_run_command.assert_called_once() + + +@patch("bittensor_cli.cli.console") +def test_swap_hotkey_netuid_0_no_warning_with_no_prompt(mock_console): + """ + Test that swap_hotkey does NOT show warning when prompt=False + """ + # Setup + cli_manager = CLIManager() + + # Mock dependencies + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + ): + mock_wallet = Mock() + mock_wallet_ask.return_value = mock_wallet + + # Call the method with prompt=False + cli_manager.wallet_swap_hotkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="old_hotkey", + netuid=0, + all_netuids=False, + network=None, + destination_hotkey_name="new_hotkey", + quiet=False, + verbose=False, + prompt=False, # No prompt + json_output=False, + ) + + # Assert: No warning messages about netuid 0 + warning_calls = [str(call) for call in mock_console.print.call_args_list] + assert not any( + "WARNING" in str(call) and "netuid 0" in str(call) for call in warning_calls + ) + + +@patch("bittensor_cli.cli.console") +def test_swap_hotkey_netuid_1_no_warning(mock_console): + """ + Test that swap_hotkey does NOT show warning when netuid != 0 + """ + # Setup + cli_manager = CLIManager() + + # Mock dependencies + with ( + patch.object(cli_manager, "verbosity_handler"), + patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, + patch.object(cli_manager, "initialize_chain"), + patch.object(cli_manager, "_run_command"), + ): + mock_wallet = Mock() + mock_wallet_ask.return_value = mock_wallet + + # Call the method with netuid=1 + cli_manager.wallet_swap_hotkey( + wallet_name="test_wallet", + wallet_path="/tmp/test", + wallet_hotkey="old_hotkey", + netuid=1, # Not 0 + all_netuids=False, + network=None, + destination_hotkey_name="new_hotkey", + quiet=False, + verbose=False, + prompt=True, + json_output=False, + ) + + # Assert: No warning messages about netuid 0 + warning_calls = [str(call) for call in mock_console.print.call_args_list] + assert not any( + "WARNING" in str(call) and "netuid 0" in str(call) for call in warning_calls + )