From 4d5cdb2eb96ca3309aadf6c22ba2be21fe304d32 Mon Sep 17 00:00:00 2001 From: LeO Date: Wed, 19 Nov 2025 10:31:11 -0500 Subject: [PATCH 1/8] make btcli st wizard --- bittensor_cli/cli.py | 137 +++++++++ bittensor_cli/src/commands/stake/wizard.py | 325 +++++++++++++++++++++ 2 files changed, 462 insertions(+) create mode 100644 bittensor_cli/src/commands/stake/wizard.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 73e77736d..13188df31 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -88,6 +88,7 @@ add as add_stake, remove as remove_stake, claim as claim_stake, + wizard as stake_wizard, ) from bittensor_cli.src.commands.subnets import ( price, @@ -971,6 +972,9 @@ def __init__(self): self.stake_app.command( "swap", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] )(self.stake_swap) + self.stake_app.command( + "wizard", rich_help_panel=HELP_PANELS["STAKE"]["MOVEMENT"] + )(self.stake_wizard) self.stake_app.command( "set-claim", rich_help_panel=HELP_PANELS["STAKE"]["CLAIM"] )(self.stake_set_claim_type) @@ -5074,6 +5078,139 @@ def stake_swap( ) return result + def stake_wizard( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + period: int = Options.period, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Interactive wizard that guides you through stake movement operations. + + This wizard helps you understand and choose the right stake movement command: + - [bold]Move[/bold]: Move stake between hotkeys (same coldkey) + - [bold]Transfer[/bold]: Transfer stake between coldkeys (same hotkey) + - [bold]Swap[/bold]: Swap stake between subnets (same coldkey-hotkey pair) + + The wizard will: + 1. Explain the differences between each operation + 2. Help you select the appropriate operation + 3. Guide you through the selection process + 4. Execute the operation with your choices + + EXAMPLE + + Start the wizard: + [green]$[/green] btcli stake wizard + """ + self.verbosity_handler(quiet, verbose, json_output) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + subtensor = self.initialize_chain(network) + + try: + wizard_result = self._run_command( + stake_wizard.stake_movement_wizard( + subtensor=subtensor, + wallet=wallet, + ) + ) + + if not wizard_result or not isinstance(wizard_result, dict): + return False + + operation = wizard_result.get("operation") + if not operation: + return False + + if operation == "move": + # Execute move operation + result, ext_id = self._run_command( + move_stake.move_stake( + subtensor=subtensor, + wallet=wallet, + origin_netuid=wizard_result["origin_netuid"], + origin_hotkey=wizard_result["origin_hotkey"], + destination_netuid=wizard_result["destination_netuid"], + destination_hotkey=wizard_result["destination_hotkey"], + amount=wizard_result.get("amount"), + stake_all=wizard_result.get("stake_all", False), + era=period, + interactive_selection=False, + prompt=prompt, + ) + ) + elif operation == "transfer": + # Execute transfer operation + dest_coldkey = wizard_result.get("destination_coldkey") + if not is_valid_ss58_address(dest_coldkey): + # Assume it's a wallet name + dest_wallet = self.wallet_ask( + dest_coldkey, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + dest_coldkey = dest_wallet.coldkeypub.ss58_address + + result, ext_id = self._run_command( + move_stake.transfer_stake( + wallet=wallet, + subtensor=subtensor, + origin_hotkey=wizard_result["origin_hotkey"], + origin_netuid=wizard_result["origin_netuid"], + dest_netuid=wizard_result["destination_netuid"], + dest_coldkey_ss58=dest_coldkey, + amount=wizard_result.get("amount"), + stake_all=wizard_result.get("stake_all", False), + era=period, + interactive_selection=False, + prompt=prompt, + ) + ) + elif operation == "swap": + # Execute swap operation + result, ext_id = self._run_command( + move_stake.swap_stake( + wallet=wallet, + subtensor=subtensor, + origin_netuid=wizard_result["origin_netuid"], + destination_netuid=wizard_result["destination_netuid"], + amount=wizard_result.get("amount"), + swap_all=False, + era=period, + interactive_selection=False, + prompt=prompt, + ) + ) + else: + print_error(f"Unknown operation: {operation}") + return False + + if json_output: + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) + return result + + except ValueError: + # User cancelled or error occurred + return False + def stake_get_children( self, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py new file mode 100644 index 000000000..35f3cc25d --- /dev/null +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -0,0 +1,325 @@ +""" +Wizard command for guiding users through stake movement operations. + +This module provides an interactive wizard that helps users understand and select +the appropriate stake movement command (move, transfer, or swap) based on their needs. +""" + +import asyncio +from typing import TYPE_CHECKING + +from bittensor_wallet import Wallet +from rich.prompt import Prompt, Confirm +from rich.table import Table +from rich.panel import Panel + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + print_error, + is_valid_ss58_address, + get_hotkey_pub_ss58, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def stake_movement_wizard( + subtensor: "SubtensorInterface", + wallet: Wallet, +) -> dict: + """ + Interactive wizard that guides users through stake movement operations. + + This wizard helps users understand the differences between: + - move: Move stake between hotkeys (same coldkey) + - transfer: Transfer stake between coldkeys (same hotkey) + - swap: Swap stake between subnets (same coldkey-hotkey pair) + + Args: + subtensor: SubtensorInterface object + wallet: Wallet object + + Returns: + dict: Contains the operation type and parameters needed to execute the operation + """ + + # Display welcome message and explanation + console.print("\n") + console.print( + Panel( + "[bold cyan]Stake Movement Wizard[/bold cyan]\n\n" + "This wizard will help you choose the right stake movement operation.\n" + "There are three types of stake movements:\n\n" + "[bold]1. Move[/bold] - Move stake between [blue]hotkeys[/blue] while keeping the same [blue]coldkey[/blue]\n" + " Example: Moving stake from hotkey A to hotkey B (both owned by your coldkey)\n\n" + "[bold]2. Transfer[/bold] - Transfer stake between [blue]coldkeys[/blue] while keeping the same [blue]hotkey[/blue]\n" + " Example: Transferring stake ownership from your coldkey to another coldkey (same hotkey)\n\n" + "[bold]3. Swap[/bold] - Swap stake between [blue]subnets[/blue] while keeping the same [blue]coldkey-hotkey pair[/blue]\n" + " Example: Moving stake from subnet 1 to subnet 2 (same wallet and hotkey)\n", + title="Welcome", + border_style="cyan", + ) + ) + + # Ask user what they want to do + operation_choice = Prompt.ask( + "\n[bold]What would you like to do?[/bold]", + choices=["1", "2", "3", "move", "transfer", "swap", "q"], + default="1", + ) + + if operation_choice.lower() == "q": + console.print("[yellow]Wizard cancelled.[/yellow]") + raise ValueError("User cancelled wizard") + + # Normalize choice + if operation_choice in ["1", "move"]: + operation = "move" + operation_name = "Move" + description = "Move stake between hotkeys (same coldkey)" + elif operation_choice in ["2", "transfer"]: + operation = "transfer" + operation_name = "Transfer" + description = "Transfer stake between coldkeys (same hotkey)" + elif operation_choice in ["3", "swap"]: + operation = "swap" + operation_name = "Swap" + description = "Swap stake between subnets (same coldkey-hotkey pair)" + else: + print_error("Invalid choice") + raise ValueError("Invalid operation choice") + + console.print(f"\n[bold green]Selected: {operation_name}[/bold green]") + console.print(f"[dim]{description}[/dim]\n") + + # Get stakes for the wallet + with console.status("Retrieving stake information..."): + stakes, ck_hk_identities, old_identities = await asyncio.gather( + subtensor.get_stake_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + ) + + # Filter stakes with actual amounts + available_stakes = [s for s in stakes if s.stake.tao > 0] + + if not available_stakes: + print_error("You have no stakes available to move.") + raise ValueError("No stakes available") + + # Display available stakes + _display_available_stakes(available_stakes, ck_hk_identities, old_identities) + + # Guide user through the specific operation + if operation == "move": + return await _guide_move_operation( + subtensor, wallet, available_stakes, ck_hk_identities, old_identities + ) + elif operation == "transfer": + return await _guide_transfer_operation( + subtensor, wallet, available_stakes, ck_hk_identities, old_identities + ) + elif operation == "swap": + return await _guide_swap_operation( + subtensor, wallet, available_stakes + ) + else: + raise ValueError(f"Unknown operation: {operation}") + + +def _display_available_stakes( + stakes: list, + ck_hk_identities: dict, + old_identities: dict, +): + """Display a table of available stakes.""" + from bittensor_cli.src.bittensor.utils import get_subnet_name, group_subnets + + # Group stakes by hotkey + hotkey_stakes = {} + for stake in stakes: + hotkey = stake.hotkey_ss58 + if hotkey not in hotkey_stakes: + hotkey_stakes[hotkey] = {} + hotkey_stakes[hotkey][stake.netuid] = stake.stake + + # Get identities + def get_identity(hotkey_ss58: str) -> str: + if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58): + return hk_identity.get("identity", {}).get("name", "") or hk_identity.get("display", "~") + elif old_identity := old_identities.get(hotkey_ss58): + return old_identity.display + return "~" + + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Your Available Stakes[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", + show_edge=False, + header_style="bold white", + border_style="bright_black", + title_justify="center", + ) + + table.add_column("Hotkey Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) + table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) + table.add_column("Total Stake", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + + for hotkey_ss58, netuid_stakes in hotkey_stakes.items(): + identity = get_identity(hotkey_ss58) + netuids = sorted(netuid_stakes.keys()) + total_stake = sum(netuid_stakes.values(), start=stakes[0].stake.__class__.from_tao(0)) + + table.add_row( + identity, + f"{hotkey_ss58[:8]}...{hotkey_ss58[-8:]}", + group_subnets(netuids), + str(total_stake), + ) + + console.print(table) + + +async def _guide_move_operation( + subtensor: "SubtensorInterface", + wallet: Wallet, + available_stakes: list, + ck_hk_identities: dict, + old_identities: dict, +) -> dict: + """Guide user through move operation.""" + from bittensor_cli.src.commands.stake.move import stake_move_transfer_selection + from bittensor_cli.src.bittensor.utils import get_hotkey_wallets_for_wallet + + console.print( + "\n[bold cyan]Move Operation[/bold cyan]\n" + "You will move stake from one hotkey to another hotkey.\n" + "Both hotkeys must be owned by the same coldkey (your wallet).\n" + ) + + try: + selection = await stake_move_transfer_selection(subtensor, wallet) + + # Get available hotkeys for destination + all_hotkeys = get_hotkey_wallets_for_wallet(wallet=wallet) + available_hotkeys = [ + (hk.hotkey_str, get_hotkey_pub_ss58(hk)) + for hk in all_hotkeys + ] + + # Ask for destination hotkey + console.print("\n[bold]Destination Hotkey[/bold]") + if available_hotkeys: + console.print("\nAvailable hotkeys in your wallet:") + for idx, (name, ss58) in enumerate(available_hotkeys): + console.print(f" {idx}: {name} ({ss58[:8]}...{ss58[-8:]})") + + dest_choice = Prompt.ask( + "\nEnter the [blue]index[/blue] of the destination hotkey, or [blue]SS58 address[/blue]", + ) + + try: + dest_idx = int(dest_choice) + if 0 <= dest_idx < len(available_hotkeys): + dest_hotkey = available_hotkeys[dest_idx][1] + else: + raise ValueError("Invalid index") + except ValueError: + # Assume it's an SS58 address + if is_valid_ss58_address(dest_choice): + dest_hotkey = dest_choice + else: + print_error("Invalid hotkey selection. Please provide a valid index or SS58 address.") + raise ValueError("Invalid destination hotkey") + else: + dest_hotkey = Prompt.ask( + "Enter the [blue]destination hotkey[/blue] SS58 address" + ) + if not is_valid_ss58_address(dest_hotkey): + print_error("Invalid SS58 address") + raise ValueError("Invalid destination hotkey") + + return { + "operation": "move", + "origin_hotkey": selection["origin_hotkey"], + "origin_netuid": selection["origin_netuid"], + "destination_netuid": selection["destination_netuid"], + "destination_hotkey": dest_hotkey, + "amount": selection["amount"], + "stake_all": selection["stake_all"], + } + except ValueError: + raise + + +async def _guide_transfer_operation( + subtensor: "SubtensorInterface", + wallet: Wallet, + available_stakes: list, + ck_hk_identities: dict, + old_identities: dict, +) -> dict: + """Guide user through transfer operation.""" + from bittensor_cli.src.commands.stake.move import stake_move_transfer_selection + + console.print( + "\n[bold cyan]Transfer Operation[/bold cyan]\n" + "You will transfer stake ownership from one coldkey to another coldkey.\n" + "The hotkey remains the same, but ownership changes.\n" + "[yellow]Warning:[/yellow] Make sure the destination coldkey is not a validator hotkey.\n" + ) + + try: + selection = await stake_move_transfer_selection(subtensor, wallet) + + # Ask for destination coldkey + console.print("\n[bold]Destination Coldkey[/bold]") + dest_coldkey = Prompt.ask( + "Enter the [blue]destination coldkey[/blue] SS58 address or wallet name" + ) + + # Note: The CLI will handle wallet name resolution if it's not an SS58 address + + return { + "operation": "transfer", + "origin_hotkey": selection["origin_hotkey"], + "origin_netuid": selection["origin_netuid"], + "destination_netuid": selection["destination_netuid"], + "destination_coldkey": dest_coldkey, + "amount": selection["amount"], + "stake_all": selection["stake_all"], + } + except ValueError: + raise + + +async def _guide_swap_operation( + subtensor: "SubtensorInterface", + wallet: Wallet, + available_stakes: list, +) -> dict: + """Guide user through swap operation.""" + from bittensor_cli.src.commands.stake.move import stake_swap_selection + + console.print( + "\n[bold cyan]Swap Operation[/bold cyan]\n" + "You will swap stake between subnets.\n" + "The same coldkey-hotkey pair is used, but stake moves between subnets.\n" + ) + + try: + selection = await stake_swap_selection(subtensor, wallet) + + return { + "operation": "swap", + "origin_netuid": selection["origin_netuid"], + "destination_netuid": selection["destination_netuid"], + "amount": selection["amount"], + } + except ValueError: + raise + From 6db892520d08c9d317a080a6e8d7edbf867873f5 Mon Sep 17 00:00:00 2001 From: LeO Date: Wed, 19 Nov 2025 11:21:13 -0500 Subject: [PATCH 2/8] chore fixes --- bittensor_cli/src/commands/stake/wizard.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py index 35f3cc25d..db1bdf3e9 100644 --- a/bittensor_cli/src/commands/stake/wizard.py +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -20,6 +20,13 @@ print_error, is_valid_ss58_address, get_hotkey_pub_ss58, + get_subnet_name, + group_subnets, + get_hotkey_wallets_for_wallet, +) +from bittensor_cli.src.commands.stake.move import ( + stake_move_transfer_selection, + stake_swap_selection, ) if TYPE_CHECKING: @@ -68,7 +75,7 @@ async def stake_movement_wizard( operation_choice = Prompt.ask( "\n[bold]What would you like to do?[/bold]", choices=["1", "2", "3", "move", "transfer", "swap", "q"], - default="1", + default="q", ) if operation_choice.lower() == "q": @@ -138,8 +145,6 @@ def _display_available_stakes( old_identities: dict, ): """Display a table of available stakes.""" - from bittensor_cli.src.bittensor.utils import get_subnet_name, group_subnets - # Group stakes by hotkey hotkey_stakes = {} for stake in stakes: @@ -192,9 +197,6 @@ async def _guide_move_operation( old_identities: dict, ) -> dict: """Guide user through move operation.""" - from bittensor_cli.src.commands.stake.move import stake_move_transfer_selection - from bittensor_cli.src.bittensor.utils import get_hotkey_wallets_for_wallet - console.print( "\n[bold cyan]Move Operation[/bold cyan]\n" "You will move stake from one hotkey to another hotkey.\n" @@ -264,8 +266,6 @@ async def _guide_transfer_operation( old_identities: dict, ) -> dict: """Guide user through transfer operation.""" - from bittensor_cli.src.commands.stake.move import stake_move_transfer_selection - console.print( "\n[bold cyan]Transfer Operation[/bold cyan]\n" "You will transfer stake ownership from one coldkey to another coldkey.\n" @@ -303,8 +303,6 @@ async def _guide_swap_operation( available_stakes: list, ) -> dict: """Guide user through swap operation.""" - from bittensor_cli.src.commands.stake.move import stake_swap_selection - console.print( "\n[bold cyan]Swap Operation[/bold cyan]\n" "You will swap stake between subnets.\n" From b8d2ecf25e57705e5a820b51f1e4efdcb55d0482 Mon Sep 17 00:00:00 2001 From: LeO Date: Wed, 19 Nov 2025 11:22:04 -0500 Subject: [PATCH 3/8] remove unused dependencies --- bittensor_cli/src/commands/stake/wizard.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py index db1bdf3e9..b0268c499 100644 --- a/bittensor_cli/src/commands/stake/wizard.py +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -16,11 +16,9 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.utils import ( console, - err_console, print_error, is_valid_ss58_address, get_hotkey_pub_ss58, - get_subnet_name, group_subnets, get_hotkey_wallets_for_wallet, ) From 224b2942c97c7f47cbc00034604d59a8c2016ea9 Mon Sep 17 00:00:00 2001 From: LeO Date: Thu, 20 Nov 2025 07:26:08 -0500 Subject: [PATCH 4/8] lint fix --- bittensor_cli/src/commands/stake/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py index b0268c499..728f6ed95 100644 --- a/bittensor_cli/src/commands/stake/wizard.py +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING from bittensor_wallet import Wallet -from rich.prompt import Prompt, Confirm +from rich.prompt import Prompt from rich.table import Table from rich.panel import Panel From 5dab56eec053107c193d489a7ba201084c326ec8 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 20 Nov 2025 17:12:09 +0200 Subject: [PATCH 5/8] Changed logic flow from using try:except to Optional return, as the ValueError creates an ugly error inside `_run_command`. Also applies ruff. --- bittensor_cli/cli.py | 165 ++++++++++----------- bittensor_cli/src/commands/stake/wizard.py | 92 ++++++------ 2 files changed, 128 insertions(+), 129 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 13188df31..06f7b8d66 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5110,7 +5110,7 @@ def stake_wizard( [green]$[/green] btcli stake wizard """ self.verbosity_handler(quiet, verbose, json_output) - + wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5118,99 +5118,96 @@ def stake_wizard( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) - + subtensor = self.initialize_chain(network) - - try: - wizard_result = self._run_command( - stake_wizard.stake_movement_wizard( + + wizard_result = self._run_command( + stake_wizard.stake_movement_wizard( + subtensor=subtensor, + wallet=wallet, + ) + ) + + if not wizard_result or not isinstance(wizard_result, dict): + return False + + operation = wizard_result.get("operation") + if not operation: + return False + + if operation == "move": + # Execute move operation + result, ext_id = self._run_command( + move_stake.move_stake( subtensor=subtensor, wallet=wallet, + origin_netuid=wizard_result["origin_netuid"], + origin_hotkey=wizard_result["origin_hotkey"], + destination_netuid=wizard_result["destination_netuid"], + destination_hotkey=wizard_result["destination_hotkey"], + amount=wizard_result.get("amount"), + stake_all=wizard_result.get("stake_all", False), + era=period, + interactive_selection=False, + prompt=prompt, ) ) - - if not wizard_result or not isinstance(wizard_result, dict): - return False - - operation = wizard_result.get("operation") - if not operation: - return False - - if operation == "move": - # Execute move operation - result, ext_id = self._run_command( - move_stake.move_stake( - subtensor=subtensor, - wallet=wallet, - origin_netuid=wizard_result["origin_netuid"], - origin_hotkey=wizard_result["origin_hotkey"], - destination_netuid=wizard_result["destination_netuid"], - destination_hotkey=wizard_result["destination_hotkey"], - amount=wizard_result.get("amount"), - stake_all=wizard_result.get("stake_all", False), - era=period, - interactive_selection=False, - prompt=prompt, - ) - ) - elif operation == "transfer": - # Execute transfer operation - dest_coldkey = wizard_result.get("destination_coldkey") - if not is_valid_ss58_address(dest_coldkey): - # Assume it's a wallet name - dest_wallet = self.wallet_ask( - dest_coldkey, - wallet_path, - None, - ask_for=[WO.NAME, WO.PATH], - validate=WV.WALLET, - ) - dest_coldkey = dest_wallet.coldkeypub.ss58_address - - result, ext_id = self._run_command( - move_stake.transfer_stake( - wallet=wallet, - subtensor=subtensor, - origin_hotkey=wizard_result["origin_hotkey"], - origin_netuid=wizard_result["origin_netuid"], - dest_netuid=wizard_result["destination_netuid"], - dest_coldkey_ss58=dest_coldkey, - amount=wizard_result.get("amount"), - stake_all=wizard_result.get("stake_all", False), - era=period, - interactive_selection=False, - prompt=prompt, - ) + elif operation == "transfer": + # Execute transfer operation + dest_coldkey = wizard_result.get("destination_coldkey") + if not is_valid_ss58_address(dest_coldkey): + # Assume it's a wallet name + dest_wallet = self.wallet_ask( + dest_coldkey, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, ) - elif operation == "swap": - # Execute swap operation - result, ext_id = self._run_command( - move_stake.swap_stake( - wallet=wallet, - subtensor=subtensor, - origin_netuid=wizard_result["origin_netuid"], - destination_netuid=wizard_result["destination_netuid"], - amount=wizard_result.get("amount"), - swap_all=False, - era=period, - interactive_selection=False, - prompt=prompt, - ) + dest_coldkey = dest_wallet.coldkeypub.ss58_address + + result, ext_id = self._run_command( + move_stake.transfer_stake( + wallet=wallet, + subtensor=subtensor, + origin_hotkey=wizard_result["origin_hotkey"], + origin_netuid=wizard_result["origin_netuid"], + dest_netuid=wizard_result["destination_netuid"], + dest_coldkey_ss58=dest_coldkey, + amount=wizard_result.get("amount"), + stake_all=wizard_result.get("stake_all", False), + era=period, + interactive_selection=False, + prompt=prompt, ) - else: - print_error(f"Unknown operation: {operation}") - return False - - if json_output: - json_console.print( - json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) + elif operation == "swap": + # Execute swap operation + result, ext_id = self._run_command( + move_stake.swap_stake( + wallet=wallet, + subtensor=subtensor, + origin_netuid=wizard_result["origin_netuid"], + destination_netuid=wizard_result["destination_netuid"], + amount=wizard_result.get("amount"), + swap_all=False, + era=period, + interactive_selection=False, + prompt=prompt, ) - return result - - except ValueError: - # User cancelled or error occurred + ) + else: + print_error(f"Unknown operation: {operation}") return False + if json_output: + json_console.print( + json.dumps( + {"success": result, "extrinsic_identifier": ext_id or None} + ) + ) + return result + def stake_get_children( self, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py index 728f6ed95..127e88cec 100644 --- a/bittensor_cli/src/commands/stake/wizard.py +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -6,7 +6,7 @@ """ import asyncio -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet from rich.prompt import Prompt @@ -34,23 +34,23 @@ async def stake_movement_wizard( subtensor: "SubtensorInterface", wallet: Wallet, -) -> dict: +) -> Optional[dict]: """ Interactive wizard that guides users through stake movement operations. - + This wizard helps users understand the differences between: - move: Move stake between hotkeys (same coldkey) - transfer: Transfer stake between coldkeys (same hotkey) - swap: Swap stake between subnets (same coldkey-hotkey pair) - + Args: subtensor: SubtensorInterface object wallet: Wallet object - + Returns: dict: Contains the operation type and parameters needed to execute the operation """ - + # Display welcome message and explanation console.print("\n") console.print( @@ -68,18 +68,18 @@ async def stake_movement_wizard( border_style="cyan", ) ) - + # Ask user what they want to do operation_choice = Prompt.ask( "\n[bold]What would you like to do?[/bold]", choices=["1", "2", "3", "move", "transfer", "swap", "q"], default="q", ) - + if operation_choice.lower() == "q": console.print("[yellow]Wizard cancelled.[/yellow]") - raise ValueError("User cancelled wizard") - + return None + # Normalize choice if operation_choice in ["1", "move"]: operation = "move" @@ -95,11 +95,11 @@ async def stake_movement_wizard( description = "Swap stake between subnets (same coldkey-hotkey pair)" else: print_error("Invalid choice") - raise ValueError("Invalid operation choice") - + return None + console.print(f"\n[bold green]Selected: {operation_name}[/bold green]") console.print(f"[dim]{description}[/dim]\n") - + # Get stakes for the wallet with console.status("Retrieving stake information..."): stakes, ck_hk_identities, old_identities = await asyncio.gather( @@ -109,17 +109,17 @@ async def stake_movement_wizard( subtensor.fetch_coldkey_hotkey_identities(), subtensor.get_delegate_identities(), ) - + # Filter stakes with actual amounts available_stakes = [s for s in stakes if s.stake.tao > 0] - + if not available_stakes: print_error("You have no stakes available to move.") - raise ValueError("No stakes available") - + return None + # Display available stakes _display_available_stakes(available_stakes, ck_hk_identities, old_identities) - + # Guide user through the specific operation if operation == "move": return await _guide_move_operation( @@ -130,9 +130,7 @@ async def stake_movement_wizard( subtensor, wallet, available_stakes, ck_hk_identities, old_identities ) elif operation == "swap": - return await _guide_swap_operation( - subtensor, wallet, available_stakes - ) + return await _guide_swap_operation(subtensor, wallet, available_stakes) else: raise ValueError(f"Unknown operation: {operation}") @@ -150,15 +148,17 @@ def _display_available_stakes( if hotkey not in hotkey_stakes: hotkey_stakes[hotkey] = {} hotkey_stakes[hotkey][stake.netuid] = stake.stake - + # Get identities def get_identity(hotkey_ss58: str) -> str: if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58): - return hk_identity.get("identity", {}).get("name", "") or hk_identity.get("display", "~") + return hk_identity.get("identity", {}).get("name", "") or hk_identity.get( + "display", "~" + ) elif old_identity := old_identities.get(hotkey_ss58): return old_identity.display return "~" - + table = Table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Your Available Stakes[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n", show_edge=False, @@ -166,24 +166,26 @@ def get_identity(hotkey_ss58: str) -> str: border_style="bright_black", title_justify="center", ) - + table.add_column("Hotkey Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) table.add_column("Total Stake", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) - + for hotkey_ss58, netuid_stakes in hotkey_stakes.items(): identity = get_identity(hotkey_ss58) netuids = sorted(netuid_stakes.keys()) - total_stake = sum(netuid_stakes.values(), start=stakes[0].stake.__class__.from_tao(0)) - + total_stake = sum( + netuid_stakes.values(), start=stakes[0].stake.__class__.from_tao(0) + ) + table.add_row( identity, f"{hotkey_ss58[:8]}...{hotkey_ss58[-8:]}", group_subnets(netuids), str(total_stake), ) - + console.print(table) @@ -200,28 +202,27 @@ async def _guide_move_operation( "You will move stake from one hotkey to another hotkey.\n" "Both hotkeys must be owned by the same coldkey (your wallet).\n" ) - + try: selection = await stake_move_transfer_selection(subtensor, wallet) - + # Get available hotkeys for destination all_hotkeys = get_hotkey_wallets_for_wallet(wallet=wallet) available_hotkeys = [ - (hk.hotkey_str, get_hotkey_pub_ss58(hk)) - for hk in all_hotkeys + (hk.hotkey_str, get_hotkey_pub_ss58(hk)) for hk in all_hotkeys ] - + # Ask for destination hotkey console.print("\n[bold]Destination Hotkey[/bold]") if available_hotkeys: console.print("\nAvailable hotkeys in your wallet:") for idx, (name, ss58) in enumerate(available_hotkeys): console.print(f" {idx}: {name} ({ss58[:8]}...{ss58[-8:]})") - + dest_choice = Prompt.ask( "\nEnter the [blue]index[/blue] of the destination hotkey, or [blue]SS58 address[/blue]", ) - + try: dest_idx = int(dest_choice) if 0 <= dest_idx < len(available_hotkeys): @@ -233,7 +234,9 @@ async def _guide_move_operation( if is_valid_ss58_address(dest_choice): dest_hotkey = dest_choice else: - print_error("Invalid hotkey selection. Please provide a valid index or SS58 address.") + print_error( + "Invalid hotkey selection. Please provide a valid index or SS58 address." + ) raise ValueError("Invalid destination hotkey") else: dest_hotkey = Prompt.ask( @@ -242,7 +245,7 @@ async def _guide_move_operation( if not is_valid_ss58_address(dest_hotkey): print_error("Invalid SS58 address") raise ValueError("Invalid destination hotkey") - + return { "operation": "move", "origin_hotkey": selection["origin_hotkey"], @@ -270,18 +273,18 @@ async def _guide_transfer_operation( "The hotkey remains the same, but ownership changes.\n" "[yellow]Warning:[/yellow] Make sure the destination coldkey is not a validator hotkey.\n" ) - + try: selection = await stake_move_transfer_selection(subtensor, wallet) - + # Ask for destination coldkey console.print("\n[bold]Destination Coldkey[/bold]") dest_coldkey = Prompt.ask( "Enter the [blue]destination coldkey[/blue] SS58 address or wallet name" ) - + # Note: The CLI will handle wallet name resolution if it's not an SS58 address - + return { "operation": "transfer", "origin_hotkey": selection["origin_hotkey"], @@ -306,10 +309,10 @@ async def _guide_swap_operation( "You will swap stake between subnets.\n" "The same coldkey-hotkey pair is used, but stake moves between subnets.\n" ) - + try: selection = await stake_swap_selection(subtensor, wallet) - + return { "operation": "swap", "origin_netuid": selection["origin_netuid"], @@ -318,4 +321,3 @@ async def _guide_swap_operation( } except ValueError: raise - From 28e9705b219cc432cc574c05542eedadafc9a12e Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 20 Nov 2025 17:14:39 +0200 Subject: [PATCH 6/8] Drop the JSON output for the wizard, as it the wizard should not be used unattended (one of the aspects of JSON output in commands is the quieting of the main console for machine reading the output). --- bittensor_cli/cli.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 06f7b8d66..65c94f0b0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5088,7 +5088,6 @@ def stake_wizard( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, - json_output: bool = Options.json_output, ): """ Interactive wizard that guides you through stake movement operations. @@ -5109,7 +5108,7 @@ def stake_wizard( Start the wizard: [green]$[/green] btcli stake wizard """ - self.verbosity_handler(quiet, verbose, json_output) + self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, @@ -5199,13 +5198,6 @@ def stake_wizard( else: print_error(f"Unknown operation: {operation}") return False - - if json_output: - json_console.print( - json.dumps( - {"success": result, "extrinsic_identifier": ext_id or None} - ) - ) return result def stake_get_children( From 80093410b3a2a91a1e72f912cbd5e320387c5aff Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 20 Nov 2025 17:14:48 +0200 Subject: [PATCH 7/8] Name shadowing --- bittensor_cli/src/commands/stake/wizard.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/stake/wizard.py b/bittensor_cli/src/commands/stake/wizard.py index 127e88cec..f1886f65e 100644 --- a/bittensor_cli/src/commands/stake/wizard.py +++ b/bittensor_cli/src/commands/stake/wizard.py @@ -150,12 +150,12 @@ def _display_available_stakes( hotkey_stakes[hotkey][stake.netuid] = stake.stake # Get identities - def get_identity(hotkey_ss58: str) -> str: - if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58): + def get_identity(hotkey_ss58_: str) -> str: + if hk_identity := ck_hk_identities["hotkeys"].get(hotkey_ss58_): return hk_identity.get("identity", {}).get("name", "") or hk_identity.get( "display", "~" ) - elif old_identity := old_identities.get(hotkey_ss58): + elif old_identity := old_identities.get(hotkey_ss58_): return old_identity.display return "~" From 7f1f924e654aab7536e467bc4440b2a96b07a036 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 20 Nov 2025 10:51:51 -0800 Subject: [PATCH 8/8] prevent exiting early --- 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 65c94f0b0..9ad6c89df 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5124,7 +5124,8 @@ def stake_wizard( stake_wizard.stake_movement_wizard( subtensor=subtensor, wallet=wallet, - ) + ), + exit_early=False, ) if not wizard_result or not isinstance(wizard_result, dict):