Skip to content
31 changes: 30 additions & 1 deletion bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2286,15 +2286,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 '<destination_hotkey>'} "
f"--wallet-name {wallet_name or '<wallet_name>'} "
f"--wallet-hotkey {wallet_hotkey or '<original_hotkey>'}[/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,
Expand Down
181 changes: 179 additions & 2 deletions tests/unit_tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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
)
Loading