From 1215d33fecc9954335e6dbdeb0a55232c1a863b3 Mon Sep 17 00:00:00 2001 From: Nikolay Stankov Date: Mon, 20 Oct 2025 13:54:28 -0400 Subject: [PATCH 01/16] feature/fix-btcli-subnet-0-swap --- bittensor_cli/cli.py | 36 +++++++- tests/unit_tests/test_cli.py | 172 ++++++++++++++++++++++++++++++++++- 2 files changed, 205 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a73aa2e81..26b853948 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2237,15 +2237,49 @@ 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 move child hotkey delegation mappings on root, NOT a full swap. 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 moves child hotkey delegation, 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 move the child hotkey delegation mappings " + "on the root network.[/yellow]\n" + ) + console.print( + "[yellow]This is NOT a full hotkey swap across all subnets.[/yellow]\n" + ) + console.print( + "[bold cyan]💡 Recommended:[/bold cyan] Use this command [bold]WITHOUT[/bold] the --netuid flag " + "to swap your hotkey across ALL subnets:\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 child hotkey delegation)?", + default=False + ): + return original_wallet = self.wallet_ask( wallet_name, wallet_path, diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index b7933e226..68d95d30b 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,171 @@ 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 + assert mock_console.print.call_count >= 4 # Multiple warning messages + warning_calls = [str(call) for call in mock_console.print.call_args_list] + assert any("WARNING" in str(call) for call in warning_calls) + assert any("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 + + # 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) From adbb410b0f3ebacc466755e8c9669b66823c1946 Mon Sep 17 00:00:00 2001 From: Nikolay Stankov Date: Mon, 20 Oct 2025 14:03:15 -0400 Subject: [PATCH 02/16] feature/fix-btcli-subnet-0-swap --- CHANGELOG.md | 3 +++ bittensor_cli/cli.py | 15 +++++---------- tests/unit_tests/test_cli.py | 11 ++++++----- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f96f8ad..d59961b83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ **Full Changelog**: https://github.com/opentensor/btcli/compare/v9.13.1...v9.14.0 +# 9.13.2 /2025-10-20 +* 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. + # 9.13.1 /2025-10-14 * Fix for complicated (user_liquidity_enabled) hyperparams by @thewhaleking in https://github.com/opentensor/btcli/pull/652 * Fixes a number of type annotations by @thewhaleking in https://github.com/opentensor/btcli/pull/653 diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2597bc9a0..67b043f6b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2288,7 +2288,7 @@ def wallet_swap_hotkey( - Make sure that you use a newly created hotkeyB in this command. A hotkeyB that is already registered cannot be used in this command. - 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 move child hotkey delegation mappings on root, NOT a full swap. Use without --netuid for full swap. + - 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 @@ -2302,21 +2302,16 @@ def wallet_swap_hotkey( netuid = get_optional_netuid(netuid, all_netuids) self.verbosity_handler(quiet, verbose, json_output) - # Warning for netuid 0 - only moves child hotkey delegation, not a full swap + # 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 move the child hotkey delegation mappings " - "on the root network.[/yellow]\n" + "[yellow]Specifying --netuid 0 will ONLY swap the hotkey on the root network (netuid 0).[/yellow]\n" ) console.print( - "[yellow]This is NOT a full hotkey swap across all subnets.[/yellow]\n" - ) - console.print( - "[bold cyan]💡 Recommended:[/bold cyan] Use this command [bold]WITHOUT[/bold] the --netuid flag " - "to swap your hotkey across ALL subnets:\n" + "[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 ''} " @@ -2325,7 +2320,7 @@ def wallet_swap_hotkey( ) if not Confirm.ask( - "Are you SURE you want to proceed with --netuid 0 (only child hotkey delegation)?", + "Are you SURE you want to proceed with --netuid 0 (only root network swap)?", default=False ): return diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 68d95d30b..f80fea352 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -87,17 +87,18 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): json_output=False ) - # Assert: Warning was displayed - assert mock_console.print.call_count >= 4 # Multiple warning messages + # 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) for call in warning_calls) - assert any("child hotkey delegation" in str(call) for call in warning_calls) + 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 + 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 From e449831d8126597d4069e3214fd5bffd75245189 Mon Sep 17 00:00:00 2001 From: Nikolay Stankov Date: Mon, 20 Oct 2025 14:12:15 -0400 Subject: [PATCH 03/16] ruff --- PR.md | 107 +++++++++++++++++++++++++++++++++++ bittensor_cli/cli.py | 6 +- tests/unit_tests/test_cli.py | 92 ++++++++++++++++-------------- 3 files changed, 160 insertions(+), 45 deletions(-) create mode 100644 PR.md diff --git a/PR.md b/PR.md new file mode 100644 index 000000000..ac70c7f2b --- /dev/null +++ b/PR.md @@ -0,0 +1,107 @@ +# Add Warning for `swap_hotkey --netuid 0` to Prevent Accidental Misuse + +## Problem + +Users accidentally using `btcli wallet swap_hotkey --netuid 0` may not realize that this command only swaps the hotkey on the root network (netuid 0) and does NOT move child hotkey delegation mappings. This is NOT a full hotkey swap across all subnets, which can lead to unexpected behavior and confusion. + +### What Happens with `--netuid 0` +- ❌ Only swaps on root network (netuid 0) +- ❌ Does NOT move child hotkey delegation mappings +- ❌ Does NOT swap across all other subnets + +### Expected Behavior (Without `--netuid`) +- ✅ Swaps hotkey across ALL subnets +- ✅ Complete hotkey migration +- ✅ Recommended for most users + +## Solution + +This PR adds a prominent warning and confirmation prompt when users attempt to use `--netuid 0` with the `swap_hotkey` command. The warning clearly explains: +1. What `--netuid 0` actually does (only root network swap) +2. What it does NOT do (move child delegation, full swap) +3. The recommended command to use instead + +## Changes Made + +### 1. `bittensor_cli/cli.py` +- Added warning check for `netuid == 0` in `wallet_swap_hotkey()` method +- Warning displays: + - Clear explanation of `--netuid 0` behavior + - Statement that it won't move child hotkey delegation mappings + - Recommended command without `--netuid` flag + - Requires explicit user confirmation to proceed +- Updated docstring to clarify netuid behavior +- Only shows warning when `prompt=True` (skips for automated scripts) + +### 2. `tests/unit_tests/test_cli.py` +Added 4 comprehensive unit tests: +- `test_swap_hotkey_netuid_0_warning_with_prompt`: Verifies warning is shown and user can decline +- `test_swap_hotkey_netuid_0_proceeds_with_confirmation`: Verifies operation proceeds when user confirms +- `test_swap_hotkey_netuid_0_no_warning_with_no_prompt`: Verifies no warning when `--no-prompt` is used +- `test_swap_hotkey_netuid_1_no_warning`: Verifies no warning for other netuids + +### 3. `CHANGELOG.md` +- Documented the fix in version 9.13.2 + +## Warning Example + +When a user runs: +```bash +btcli wallet swap_hotkey new_hotkey \ + --wallet-name wallet \ + --wallet-hotkey old_hotkey \ + --netuid 0 +``` + +They now see: +``` +⚠️ WARNING: Using --netuid 0 for swap_hotkey + +Specifying --netuid 0 will ONLY swap the hotkey on the root network (netuid 0). + +It will NOT move child hotkey delegation mappings on root. + +btcli wallet swap_hotkey new_hotkey --wallet-name wallet --wallet-hotkey old_hotkey + +Are you SURE you want to proceed with --netuid 0 (only root network swap)? [y/n] (n): +``` + +## Testing + +### Unit Tests +All 4 new unit tests pass: +```bash +pytest tests/unit_tests/test_cli.py -k "swap_hotkey" -v +# ✅ 4 passed +``` + +### Manual CLI Testing +- ✅ `--netuid 0` with prompt: Shows warning, requires confirmation +- ✅ `--netuid 1` (normal): No warning shown +- ✅ `--netuid 0 --no-prompt`: No warning (automation support) +- ✅ No `--netuid` flag: No warning (recommended usage) + +## Behavior + +| Command | Warning Shown? | Behavior | +|---------|----------------|----------| +| `swap_hotkey ... --netuid 0` | ⚠️ YES | Shows warning, requires confirmation | +| `swap_hotkey ... --netuid 1` | ✅ NO | Swaps on netuid 1 only (as expected) | +| `swap_hotkey ...` (no netuid) | ✅ NO | Full swap across all subnets (recommended) | +| `swap_hotkey ... --netuid 0 --no-prompt` | ✅ NO | Skips warning for automation | + +## Related Context + +This fix addresses user confusion discovered when a user accidentally ran `swap_hotkey` with `--netuid 0` expecting a full swap but only got a root network swap without child delegation movement. The warning prevents this mistake and guides users to the correct usage. + +## Checklist + +- [x] Code follows existing patterns in the codebase +- [x] Uses `Confirm.ask()` consistent with other warnings +- [x] Added comprehensive unit tests (4 tests) +- [x] Updated CHANGELOG.md +- [x] Updated docstring documentation +- [x] Tested manually with CLI +- [x] Warning only shows when appropriate (not for automation) +- [x] No new linting errors introduced + diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 67b043f6b..e54a5e690 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2301,7 +2301,7 @@ def wallet_swap_hotkey( """ 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( @@ -2318,10 +2318,10 @@ def wallet_swap_hotkey( 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 + default=False, ): return original_wallet = self.wallet_ask( diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index f80fea352..a17ed8406 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -53,8 +53,8 @@ async def test_subnet_sets_price_correctly(): assert subnet_info.price == mock_price -@patch('bittensor_cli.cli.Confirm') -@patch('bittensor_cli.cli.console') +@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, @@ -66,12 +66,12 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): # 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'), + 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", @@ -84,28 +84,32 @@ def test_swap_hotkey_netuid_0_warning_with_prompt(mock_console, mock_confirm): quiet=False, verbose=False, prompt=True, - json_output=False + 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( + "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 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') +@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 @@ -113,17 +117,17 @@ def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_conf # 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, + 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", @@ -136,34 +140,34 @@ def test_swap_hotkey_netuid_0_proceeds_with_confirmation(mock_console, mock_conf quiet=False, verbose=False, prompt=True, - json_output=False + 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') +@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'), + 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", @@ -176,32 +180,34 @@ def test_swap_hotkey_netuid_0_no_warning_with_no_prompt(mock_console): quiet=False, verbose=False, prompt=False, # No prompt - json_output=False + 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) + assert not any( + "WARNING" in str(call) and "netuid 0" in str(call) for call in warning_calls + ) -@patch('bittensor_cli.cli.console') +@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'), + 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", @@ -214,9 +220,11 @@ def test_swap_hotkey_netuid_1_no_warning(mock_console): quiet=False, verbose=False, prompt=True, - json_output=False + 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) + assert not any( + "WARNING" in str(call) and "netuid 0" in str(call) for call in warning_calls + ) From 51c373869a7b43698835d6b3330015e92ed7b4a1 Mon Sep 17 00:00:00 2001 From: Nikolay Stankov Date: Mon, 20 Oct 2025 14:13:11 -0400 Subject: [PATCH 04/16] rm pr.md --- PR.md | 107 ---------------------------------------------------------- 1 file changed, 107 deletions(-) delete mode 100644 PR.md diff --git a/PR.md b/PR.md deleted file mode 100644 index ac70c7f2b..000000000 --- a/PR.md +++ /dev/null @@ -1,107 +0,0 @@ -# Add Warning for `swap_hotkey --netuid 0` to Prevent Accidental Misuse - -## Problem - -Users accidentally using `btcli wallet swap_hotkey --netuid 0` may not realize that this command only swaps the hotkey on the root network (netuid 0) and does NOT move child hotkey delegation mappings. This is NOT a full hotkey swap across all subnets, which can lead to unexpected behavior and confusion. - -### What Happens with `--netuid 0` -- ❌ Only swaps on root network (netuid 0) -- ❌ Does NOT move child hotkey delegation mappings -- ❌ Does NOT swap across all other subnets - -### Expected Behavior (Without `--netuid`) -- ✅ Swaps hotkey across ALL subnets -- ✅ Complete hotkey migration -- ✅ Recommended for most users - -## Solution - -This PR adds a prominent warning and confirmation prompt when users attempt to use `--netuid 0` with the `swap_hotkey` command. The warning clearly explains: -1. What `--netuid 0` actually does (only root network swap) -2. What it does NOT do (move child delegation, full swap) -3. The recommended command to use instead - -## Changes Made - -### 1. `bittensor_cli/cli.py` -- Added warning check for `netuid == 0` in `wallet_swap_hotkey()` method -- Warning displays: - - Clear explanation of `--netuid 0` behavior - - Statement that it won't move child hotkey delegation mappings - - Recommended command without `--netuid` flag - - Requires explicit user confirmation to proceed -- Updated docstring to clarify netuid behavior -- Only shows warning when `prompt=True` (skips for automated scripts) - -### 2. `tests/unit_tests/test_cli.py` -Added 4 comprehensive unit tests: -- `test_swap_hotkey_netuid_0_warning_with_prompt`: Verifies warning is shown and user can decline -- `test_swap_hotkey_netuid_0_proceeds_with_confirmation`: Verifies operation proceeds when user confirms -- `test_swap_hotkey_netuid_0_no_warning_with_no_prompt`: Verifies no warning when `--no-prompt` is used -- `test_swap_hotkey_netuid_1_no_warning`: Verifies no warning for other netuids - -### 3. `CHANGELOG.md` -- Documented the fix in version 9.13.2 - -## Warning Example - -When a user runs: -```bash -btcli wallet swap_hotkey new_hotkey \ - --wallet-name wallet \ - --wallet-hotkey old_hotkey \ - --netuid 0 -``` - -They now see: -``` -⚠️ WARNING: Using --netuid 0 for swap_hotkey - -Specifying --netuid 0 will ONLY swap the hotkey on the root network (netuid 0). - -It will NOT move child hotkey delegation mappings on root. - -btcli wallet swap_hotkey new_hotkey --wallet-name wallet --wallet-hotkey old_hotkey - -Are you SURE you want to proceed with --netuid 0 (only root network swap)? [y/n] (n): -``` - -## Testing - -### Unit Tests -All 4 new unit tests pass: -```bash -pytest tests/unit_tests/test_cli.py -k "swap_hotkey" -v -# ✅ 4 passed -``` - -### Manual CLI Testing -- ✅ `--netuid 0` with prompt: Shows warning, requires confirmation -- ✅ `--netuid 1` (normal): No warning shown -- ✅ `--netuid 0 --no-prompt`: No warning (automation support) -- ✅ No `--netuid` flag: No warning (recommended usage) - -## Behavior - -| Command | Warning Shown? | Behavior | -|---------|----------------|----------| -| `swap_hotkey ... --netuid 0` | ⚠️ YES | Shows warning, requires confirmation | -| `swap_hotkey ... --netuid 1` | ✅ NO | Swaps on netuid 1 only (as expected) | -| `swap_hotkey ...` (no netuid) | ✅ NO | Full swap across all subnets (recommended) | -| `swap_hotkey ... --netuid 0 --no-prompt` | ✅ NO | Skips warning for automation | - -## Related Context - -This fix addresses user confusion discovered when a user accidentally ran `swap_hotkey` with `--netuid 0` expecting a full swap but only got a root network swap without child delegation movement. The warning prevents this mistake and guides users to the correct usage. - -## Checklist - -- [x] Code follows existing patterns in the codebase -- [x] Uses `Confirm.ask()` consistent with other warnings -- [x] Added comprehensive unit tests (4 tests) -- [x] Updated CHANGELOG.md -- [x] Updated docstring documentation -- [x] Tested manually with CLI -- [x] Warning only shows when appropriate (not for automation) -- [x] No new linting errors introduced - From 6cd5f2572deb17e5ce8544034e78723c07c47912 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 21 Oct 2025 19:35:59 +0200 Subject: [PATCH 05/16] Updates kappa to root sudo only in-line with devnet-ready --- bittensor_cli/src/__init__.py | 2 +- tests/e2e_tests/test_hyperparams_setting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 9b7d749b1..598f97167 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/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 24f83bdfe..c336f6615 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): From 97cfcc3d27c19e61a4f372199c81dede1c6559d0 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 22 Oct 2025 16:08:35 +0200 Subject: [PATCH 06/16] Childkey take was incorrectly labeled. --- 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 220de3d40..3d6712a8c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5229,7 +5229,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 +5306,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, From 26b0e8a9b5793f5dc8896721412918dc5d104e36 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 16:57:38 +0200 Subject: [PATCH 07/16] Adds additional warnings for move vs transfer --- bittensor_cli/cli.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 220de3d40..eae4d0fed 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4573,9 +4573,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 +4778,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( From c239c00f62e97d4ffc533ec11c290a4d73becc90 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 17:18:51 +0200 Subject: [PATCH 08/16] Checks if hotkey has owner and provides confirmation --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 9 +++++++++ bittensor_cli/src/bittensor/subtensor_interface.py | 12 +++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 720e8d356..13f255ff4 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -175,6 +175,15 @@ 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" diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a0d6dd6ee..2ef90d284 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 From ab41c23f2fd7e49ddd9e895386abb7ef2293ec50 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 17:21:45 +0200 Subject: [PATCH 09/16] Moves the unlock wallet fn after the confirmations/balance check --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 13f255ff4..6886fb41a 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: @@ -188,7 +185,7 @@ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: "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" @@ -196,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() From 305969257ab10ff12c767799b3e6af0af2f688e2 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 18:48:10 +0200 Subject: [PATCH 10/16] Removes from changelog --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d59961b83..90f96f8ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,6 @@ **Full Changelog**: https://github.com/opentensor/btcli/compare/v9.13.1...v9.14.0 -# 9.13.2 /2025-10-20 -* 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. - # 9.13.1 /2025-10-14 * Fix for complicated (user_liquidity_enabled) hyperparams by @thewhaleking in https://github.com/opentensor/btcli/pull/652 * Fixes a number of type annotations by @thewhaleking in https://github.com/opentensor/btcli/pull/653 From 3bfd513d44f0578de66273e90a3750f5c906df76 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 18:55:33 +0200 Subject: [PATCH 11/16] Updates the help text of crownloan refund --- bittensor_cli/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 220de3d40..a43cff4cc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7764,10 +7764,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) From 1d12cc1e78dc55e534e9ba77b929cf533e301b93 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 19:11:55 +0200 Subject: [PATCH 12/16] Ensures we exit gracefully if there's an error in connection. --- bittensor_cli/cli.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 220de3d40..5411b144f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1331,13 +1331,13 @@ async def _run(): 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() - 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}") + 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}") + raise typer.Exit() return self.event_loop.run_until_complete(_run()) From 39653e17b3b074e2cf0a0af10474494d7b1caf47 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 19:18:39 +0200 Subject: [PATCH 13/16] Ensure we don't print the extrinsic success message if the extrinsic failed. --- bittensor_cli/src/bittensor/utils.py | 2 +- bittensor_cli/src/commands/stake/move.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3846dc7b3..f9ade4507 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 53587a577..c986b859a 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( From fceada915b34d7fd5a58c60490103eaf50dbfb7d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 20:30:36 +0200 Subject: [PATCH 14/16] Only raise typer.Exit on exception occurring --- bittensor_cli/cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5411b144f..facf58224 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1302,6 +1302,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 +1312,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,9 +1324,11 @@ 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() @@ -1337,7 +1341,8 @@ async def _run(): 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}") - raise typer.Exit() + if exception_occurred: + raise typer.Exit() return self.event_loop.run_until_complete(_run()) From 3be3dda4b1664619a598921232e43712c33eb2bd Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 21:16:02 +0200 Subject: [PATCH 15/16] Adds wallet balance sorting --- bittensor_cli/cli.py | 11 +++++-- bittensor_cli/src/commands/wallets.py | 41 +++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e3a62e1a6..595320aa4 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: @@ -1910,7 +1912,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]]: @@ -3161,6 +3163,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, @@ -3260,7 +3267,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 ) ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 4d74773c5..6473f2c69 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( From a12639bad2e655d3166ab2103fb228f697c682b7 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 23 Oct 2025 22:46:57 +0200 Subject: [PATCH 16/16] changelog + version --- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f96f8ad..91b60a5b2 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/pyproject.toml b/pyproject.toml index a9f725de0..47d49b92a 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 = [