From f02fbe8740c82ed3a2b84026d81ae21c8016bc54 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 19 Sep 2025 16:44:00 -0700 Subject: [PATCH 1/7] Add get_auto_stake_destinations --- .../src/bittensor/subtensor_interface.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 27df7d94c..a8d6aa1b8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -224,6 +224,30 @@ async def get_stake_for_coldkey( stakes: list[StakeInfo] = StakeInfo.list_from_any(result) return [stake for stake in stakes if stake.stake > 0] + async def get_auto_stake_destinations( + self, + coldkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[int, str]: + """Retrieve auto-stake destinations configured for a coldkey.""" + + query = await self.substrate.query_map( + module="SubtensorModule", + storage_function="AutoStakeDestination", + params=[coldkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + destinations: dict[int, str] = {} + async for netuid, destination in query: + hotkey_ss58 = decode_account_id(destination.value[0]) + if hotkey_ss58: + destinations[int(netuid)] = hotkey_ss58 + + return destinations + async def get_stake_for_coldkey_and_hotkey( self, hotkey_ss58: str, From 1321ab42263a9d8bd6622d87b302a7d2ea88f52f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 19 Sep 2025 16:44:38 -0700 Subject: [PATCH 2/7] add cmd get_auto_stake --- bittensor_cli/cli.py | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4484f5288..5bb30fd48 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -72,6 +72,7 @@ prompt_position_id, ) from bittensor_cli.src.commands.stake import ( + auto_staking as auto_stake, children_hotkeys, list as list_stake, move as move_stake, @@ -905,6 +906,9 @@ def __init__(self): self.stake_app.command( "add", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_add) + self.stake_app.command( + "auto", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] + )(self.get_auto_stake) self.stake_app.command( "remove", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_remove) @@ -3505,6 +3509,62 @@ def wallet_swap_coldkey( ) ) + def get_auto_stake( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + coldkey_ss58=typer.Option( + None, + "--ss58", + "--coldkey_ss58", + "--coldkey.ss58_address", + "--coldkey.ss58", + help="Coldkey address of the wallet", + ), + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Display auto-stake destinations for a wallet across all subnets.""" + + self.verbosity_handler(quiet, verbose, json_output) + + wallet = None + if coldkey_ss58: + if not is_valid_ss58_address(coldkey_ss58): + print_error("You entered an invalid ss58 address") + raise typer.Exit() + else: + if wallet_name: + coldkey_or_ss58 = wallet_name + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if is_valid_ss58_address(coldkey_or_ss58): + coldkey_ss58 = coldkey_or_ss58 + else: + wallet_name = coldkey_or_ss58 if coldkey_or_ss58 else wallet_name + wallet = self.wallet_ask( + wallet_name, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + return self._run_command( + auto_stake.show_auto_destinations( + wallet, + self.initialize_chain(network), + coldkey_ss58=coldkey_ss58, + json_output=json_output, + verbose=verbose, + ) + ) + def stake_list( self, network: Optional[list[str]] = Options.network, From 9e6503bfa398c02ab983e03920b5857ec3412d7b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 19 Sep 2025 16:50:18 -0700 Subject: [PATCH 3/7] add set-auto --- bittensor_cli/cli.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5bb30fd48..961be3189 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -909,6 +909,9 @@ def __init__(self): self.stake_app.command( "auto", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.get_auto_stake) + self.stake_app.command( + "set-auto", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] + )(self.set_auto_stake) self.stake_app.command( "remove", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_remove) @@ -3565,6 +3568,81 @@ def get_auto_stake( ) ) + def set_auto_stake( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + netuid: Optional[int] = Options.netuid_not_req, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + prompt: bool = Options.prompt, + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + json_output: bool = Options.json_output, + ): + """Set the auto-stake destination hotkey for a coldkey.""" + + self.verbosity_handler(quiet, verbose, json_output) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + if netuid is None: + netuid = IntPrompt.ask( + "Enter the [blue]netuid[/blue] to configure", + default=defaults.netuid, + ) + validate_netuid(netuid) + + hotkey_prompt = Prompt.ask( + "Enter the [blue]hotkey ss58 address[/blue] to auto-stake to " + "[dim](Press Enter to view delegates)[/dim]", + default="", + show_default=False, + ).strip() + + if not hotkey_prompt: + selected_hotkey = self._run_command( + subnets.show( + subtensor=self.initialize_chain(network), + netuid=netuid, + sort=False, + max_rows=20, + prompt=False, + delegate_selection=True, + ), + exit_early=False, + ) + if not selected_hotkey: + print_error("No delegate selected. Exiting.") + return + hotkey_ss58 = selected_hotkey + else: + hotkey_ss58 = hotkey_prompt + + if not is_valid_ss58_address(hotkey_ss58): + print_error("You entered an invalid hotkey ss58 address") + return + + return self._run_command( + auto_stake.set_auto_stake_destination( + wallet, + self.initialize_chain(network), + netuid, + hotkey_ss58, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt_user=prompt, + json_output=json_output, + ) + ) + def stake_list( self, network: Optional[list[str]] = Options.network, From b65de6080b5040fa65fee0d9f87f54d3e7b780f7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 19 Sep 2025 16:50:51 -0700 Subject: [PATCH 4/7] show_auto_stake --- .../src/commands/stake/auto_staking.py | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 bittensor_cli/src/commands/stake/auto_staking.py diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py new file mode 100644 index 000000000..82c38a33d --- /dev/null +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -0,0 +1,169 @@ +import asyncio +import json +from typing import Optional, TYPE_CHECKING + +from bittensor_wallet import Wallet +from rich import box +from rich.table import Table +from rich.prompt import Confirm + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.utils import ( + console, + json_console, + get_subnet_name, + is_valid_ss58_address, + print_error, + err_console, + unlock_key, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def show_auto_destinations( + wallet: Optional[Wallet], + subtensor: "SubtensorInterface", + coldkey_ss58: Optional[str] = None, + json_output: bool = False, + verbose: bool = False, +) -> Optional[dict[int, dict[str, Optional[str]]]]: + """Display auto-stake destinations for the supplied wallet.""" + + wallet_name: Optional[str] = wallet.name if wallet else None + coldkey_ss58 = coldkey_ss58 or (wallet.coldkeypub.ss58_address if wallet else None) + if not coldkey_ss58: + raise ValueError("A wallet or coldkey SS58 address must be provided") + + with console.status( + f"Retrieving auto-stake configuration from {subtensor.network}...", + spinner="earth", + ): + chain_head = await subtensor.substrate.get_chain_head() + ( + subnet_info, + auto_destinations, + identities, + delegate_identities, + ) = await asyncio.gather( + subtensor.all_subnets(block_hash=chain_head), + subtensor.get_auto_stake_destinations( + coldkey_ss58=coldkey_ss58, + block_hash=chain_head, + reuse_block=True, + ), + subtensor.fetch_coldkey_hotkey_identities(block_hash=chain_head), + subtensor.get_delegate_identities(block_hash=chain_head), + ) + + subnet_map = {info.netuid: info for info in subnet_info} + auto_destinations = auto_destinations or {} + identities = identities or {} + delegate_identities = delegate_identities or {} + hotkey_identities = identities.get("hotkeys", {}) + + def resolve_identity(hotkey: str) -> Optional[str]: + if not hotkey: + return None + + identity_entry = hotkey_identities.get(hotkey, {}).get("identity") + if identity_entry: + display_name = identity_entry.get("name") or identity_entry.get("display") + if display_name: + return display_name + + delegate_info = delegate_identities.get(hotkey) + if delegate_info and getattr(delegate_info, "display", ""): + return delegate_info.display + + return None + + coldkey_display = wallet_name + if not coldkey_display: + coldkey_identity = identities.get("coldkeys", {}).get(coldkey_ss58, {}) + if identity_data := coldkey_identity.get("identity"): + coldkey_display = identity_data.get("name") or identity_data.get("display") + if not coldkey_display: + coldkey_display = f"{coldkey_ss58[:6]}...{coldkey_ss58[-6:]}" + + rows = [] + data_output: dict[int, dict[str, Optional[str]]] = {} + + for netuid in sorted(subnet_map): + subnet = subnet_map[netuid] + subnet_name = get_subnet_name(subnet) + hotkey_ss58 = auto_destinations.get(netuid) + identity_str = resolve_identity(hotkey_ss58) if hotkey_ss58 else None + is_custom = hotkey_ss58 is not None + + data_output[netuid] = { + "subnet_name": subnet_name, + "status": "custom" if is_custom else "default", + "destination": hotkey_ss58, + "identity": identity_str, + } + + if json_output: + continue + + status_text = ( + f"[{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]Custom[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + if is_custom + else f"[{COLOR_PALETTE['GENERAL']['HINT']}]Default[/{COLOR_PALETTE['GENERAL']['HINT']}]" + ) + + rows.append( + ( + str(netuid), + subnet_name, + status_text, + hotkey_ss58, + identity_str or "", + ) + ) + + if json_output: + json_console.print(json.dumps(data_output)) + return data_output + + table = Table( + title=( + f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Auto Stake Destinations" + f" for [bold]{coldkey_display}[/bold]\n" + f"Network: {subtensor.network}\n" + f"Coldkey: {coldkey_ss58}\n" + f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]" + ), + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + box=box.SIMPLE_HEAD, + ) + + table.add_column( + "Netuid", style=COLOR_PALETTE["GENERAL"]["SYMBOL"], justify="center" + ) + table.add_column("Subnet", style="cyan", justify="left") + table.add_column("Status", style="white", justify="center") + table.add_column( + "Destination Hotkey", style=COLOR_PALETTE["GENERAL"]["HOTKEY"], justify="center" + ) + table.add_column( + "Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], justify="left" + ) + + for row in rows: + table.add_row(*row) + + console.print(table) + console.print( + f"\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Total subnets:[/] {len(subnet_map)} " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Custom destinations:[/] {len(auto_destinations)}" + ) + + return None From 8b40ff2e01e7490758a70ae1fa9298a4d04469b7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 19 Sep 2025 16:51:07 -0700 Subject: [PATCH 5/7] set_auto_stake_destination --- .../src/commands/stake/auto_staking.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index 82c38a33d..e3f082434 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -167,3 +167,127 @@ def resolve_identity(hotkey: str) -> Optional[str]: ) return None + + +async def set_auto_stake_destination( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + hotkey_ss58: str, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt_user: bool = True, + json_output: bool = False, +) -> bool: + """Set the auto-stake destination hotkey for a coldkey on a subnet.""" + + if not is_valid_ss58_address(hotkey_ss58): + print_error("You entered an invalid hotkey ss58 address") + return False + + try: + chain_head = await subtensor.substrate.get_chain_head() + subnet_info, identities, delegate_identities = await asyncio.gather( + subtensor.subnet(netuid, block_hash=chain_head), + subtensor.fetch_coldkey_hotkey_identities(block_hash=chain_head), + subtensor.get_delegate_identities(block_hash=chain_head), + ) + except ValueError: + print_error(f"Subnet with netuid {netuid} does not exist") + return False + + hotkey_identity = "" + identities = identities or {} + delegate_identities = delegate_identities or {} + + hotkey_identity_entry = identities.get("hotkeys", {}).get(hotkey_ss58, {}) + if identity_data := hotkey_identity_entry.get("identity"): + hotkey_identity = ( + identity_data.get("name") or identity_data.get("display") or "" + ) + if not hotkey_identity: + delegate_info = delegate_identities.get(hotkey_ss58) + if delegate_info and getattr(delegate_info, "display", ""): + hotkey_identity = delegate_info.display + + if prompt_user and not json_output: + table = Table( + title=( + f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Confirm Auto-Stake Destination" + f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]" + ), + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + box=box.SIMPLE_HEAD, + ) + table.add_column( + "Netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] + ) + table.add_column("Subnet", style="cyan", justify="left") + table.add_column( + "Destination Hotkey", + style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + justify="center", + ) + table.add_column( + "Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"], justify="left" + ) + table.add_row( + str(netuid), + get_subnet_name(subnet_info), + hotkey_ss58, + hotkey_identity or "", + ) + console.print(table) + + if not Confirm.ask("\nSet this auto-stake destination?", default=True): + return False + + if not unlock_key(wallet).success: + return False + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_coldkey_auto_stake_hotkey", + call_params={ + "netuid": netuid, + "hotkey": hotkey_ss58, + }, + ) + + with console.status( + f":satellite: Setting auto-stake destination on [white]{subtensor.network}[/white]...", + spinner="earth", + ): + success, error_message = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if json_output: + json_console.print( + json.dumps( + { + "success": success, + "error": error_message, + "netuid": netuid, + "hotkey": hotkey_ss58, + } + ) + ) + + if success: + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Auto-stake destination set for netuid {netuid}[/dark_sea_green3]" + ) + return True + + err_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") + return False From dbaffe3e4c981e6672b00434e248636477e6d313 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 19 Sep 2025 16:51:35 -0700 Subject: [PATCH 6/7] cleanup --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/stake/auto_staking.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 961be3189..637165ad7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3559,7 +3559,7 @@ def get_auto_stake( ) return self._run_command( - auto_stake.show_auto_destinations( + auto_stake.show_auto_stake_destinations( wallet, self.initialize_chain(network), coldkey_ss58=coldkey_ss58, diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index e3f082434..f5e5fdb5b 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -22,7 +22,7 @@ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -async def show_auto_destinations( +async def show_auto_stake_destinations( wallet: Optional[Wallet], subtensor: "SubtensorInterface", coldkey_ss58: Optional[str] = None, From c705a0fe37ccce867efb8be0878dd9ec97157dc0 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 23 Sep 2025 22:43:01 +0200 Subject: [PATCH 7/7] Update to include extrinsic identifiers --- bittensor_cli/src/commands/stake/auto_staking.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/auto_staking.py b/bittensor_cli/src/commands/stake/auto_staking.py index f5e5fdb5b..d697df8d4 100644 --- a/bittensor_cli/src/commands/stake/auto_staking.py +++ b/bittensor_cli/src/commands/stake/auto_staking.py @@ -16,6 +16,7 @@ print_error, err_console, unlock_key, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -264,13 +265,15 @@ async def set_auto_stake_destination( f":satellite: Setting auto-stake destination on [white]{subtensor.network}[/white]...", spinner="earth", ): - success, error_message = await subtensor.sign_and_send_extrinsic( + success, error_message, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + ext_id = await ext_receipt.get_extrinsic_identifier() if success else None + if json_output: json_console.print( json.dumps( @@ -279,11 +282,13 @@ async def set_auto_stake_destination( "error": error_message, "netuid": netuid, "hotkey": hotkey_ss58, + "extrinsic_identifier": ext_id, } ) ) if success: + await print_extrinsic_id(ext_receipt) console.print( f":white_heavy_check_mark: [dark_sea_green3]Auto-stake destination set for netuid {netuid}[/dark_sea_green3]" )