diff --git a/bittensor/cli.py b/bittensor/cli.py index e86fa013c..cc87c122e 100644 --- a/bittensor/cli.py +++ b/bittensor/cli.py @@ -73,6 +73,8 @@ SetChildrenCommand, GetChildrenCommand, RevokeChildrenCommand, + SetChildKeyTakeCommand, + GetChildKeyTakeCommand, ) # Create a console instance for CLI display. @@ -175,6 +177,8 @@ "get_children": GetChildrenCommand, "set_children": SetChildrenCommand, "revoke_children": RevokeChildrenCommand, + "set_childkey_take": SetChildKeyTakeCommand, + "get_childkey_take": GetChildKeyTakeCommand, }, }, "weights": { diff --git a/bittensor/commands/__init__.py b/bittensor/commands/__init__.py index 0692253a4..e1ac8c74c 100644 --- a/bittensor/commands/__init__.py +++ b/bittensor/commands/__init__.py @@ -67,6 +67,8 @@ StakeShow, SetChildrenCommand, GetChildrenCommand, + SetChildKeyTakeCommand, + GetChildKeyTakeCommand, ) from .unstake import UnStakeCommand, RevokeChildrenCommand from .overview import OverviewCommand diff --git a/bittensor/commands/stake.py b/bittensor/commands/stake.py index 3061ea7f7..132529a13 100644 --- a/bittensor/commands/stake.py +++ b/bittensor/commands/stake.py @@ -36,10 +36,48 @@ ) from . import defaults # type: ignore from ..utils import wallet_utils -from ..utils.formatting import u64_to_float +from ..utils.formatting import u64_to_float, u16_to_float console = bittensor.__console__ +MAX_CHILDREN = 5 + + +def get_netuid( + cli: "bittensor.cli", subtensor: "bittensor.subtensor" +) -> Tuple[bool, int]: + """Retrieve and validate the netuid from the user or configuration.""" + console = Console() + if not cli.config.is_set("netuid"): + try: + cli.config.netuid = int(Prompt.ask("Enter netuid")) + except ValueError: + console.print( + "[red]Invalid input. Please enter a valid integer for netuid.[/red]" + ) + return False, -1 + netuid = cli.config.netuid + if not subtensor.subnet_exists(netuid=netuid): + console.print( + "[red]Network with netuid {} does not exist. Please try again.[/red]".format( + netuid + ) + ) + return False, -1 + return True, netuid + + +def get_hotkey(wallet: "bittensor.wallet", config: "bittensor.config") -> str: + """Retrieve the hotkey from the wallet or config.""" + if wallet and wallet.hotkey: + return wallet.hotkey.ss58_address + elif config.is_set("hotkey"): + return config.hotkey + elif config.is_set("ss58"): + return config.ss58 + else: + return Prompt.ask("Enter hotkey (ss58)") + class StakeCommand: """ @@ -573,6 +611,259 @@ def add_args(parser: argparse.ArgumentParser): bittensor.subtensor.add_args(list_parser) +class SetChildKeyTakeCommand: + """ + Executes the ``set_childkey_take`` command to modify your childkey take on a specified subnet on the Bittensor network to the caller. + + This command is used to modify your childkey take on a specified subnet on the Bittensor network. + + Usage: + Users can specify the amount or 'take' for their child hotkeys (``SS58`` address), + the user needs to have access to the ss58 hotkey this call, and the take must be between 0 and 18%. + + The command prompts for confirmation before executing the set_childkey_take operation. + + Example usage:: + + btcli stake set_childkey_take --hotkey --netuid 1 --take 0.18 + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """Set childkey take.""" + try: + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=cli.config, log_verbose=False + ) + SetChildKeyTakeCommand._run(cli, subtensor) + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + console = Console() + wallet = bittensor.wallet(config=cli.config) + + # Get values if not set. + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + if not cli.config.is_set("take"): + cli.config.take = Prompt.ask( + "Enter the percentage of take for your child hotkey (between 0 and 0.18 representing 0-18%)" + ) + + # extract take from cli input + try: + take = float(cli.config.take) + except ValueError: + print( + ":cross_mark:[red]Take must be a float value using characters between 0 and 9.[/red]" + ) + return + + if take < 0 or take > 0.18: + console.print( + f":cross_mark:[red]Invalid take: Childkey Take must be between 0 and 0.18 (representing 0% to 18%). Proposed take is {take}.[/red]" + ) + return + + success, message = subtensor.set_childkey_take( + wallet=wallet, + netuid=netuid, + hotkey=hotkey, + take=take, + wait_for_inclusion=cli.config.wait_for_inclusion, + wait_for_finalization=cli.config.wait_for_finalization, + prompt=cli.config.prompt, + ) + + # Result + if success: + console.print(":white_heavy_check_mark: [green]Set childkey take.[/green]") + console.print( + f"The childkey take for {hotkey} is now set to {take * 100:.3f}%." + ) + else: + console.print( + f":cross_mark:[red] Unable to set childkey take.[/red] {message}" + ) + + @staticmethod + def check_config(config: "bittensor.config"): + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + config.wallet.name = str(wallet_name) + if not config.is_set("wallet.hotkey") and not config.no_prompt: + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) + + @staticmethod + def add_args(parser: argparse.ArgumentParser): + set_childkey_take_parser = parser.add_parser( + "set_childkey_take", help="""Set childkey take.""" + ) + set_childkey_take_parser.add_argument( + "--netuid", dest="netuid", type=int, required=False + ) + set_childkey_take_parser.add_argument( + "--hotkey", dest="hotkey", type=str, required=False + ) + set_childkey_take_parser.add_argument( + "--take", dest="take", type=float, required=False + ) + set_childkey_take_parser.add_argument( + "--wait_for_inclusion", + dest="wait_for_inclusion", + action="store_true", + default=True, + help="""Wait for the transaction to be included in a block.""", + ) + set_childkey_take_parser.add_argument( + "--wait_for_finalization", + dest="wait_for_finalization", + action="store_true", + default=True, + help="""Wait for the transaction to be finalized.""", + ) + set_childkey_take_parser.add_argument( + "--prompt", + dest="prompt", + action="store_true", + default=True, + help="""Prompt for confirmation before proceeding.""", + ) + set_childkey_take_parser.add_argument( + "--y", + "--yes", + "--no_prompt", + dest="prompt", + action="store_false", + help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", + ) + bittensor.wallet.add_args(set_childkey_take_parser) + bittensor.subtensor.add_args(set_childkey_take_parser) + + +class GetChildKeyTakeCommand: + """ + Executes the ``get_childkey_take`` command to get your childkey take on a specified subnet on the Bittensor network to the caller. + + This command is used to get your childkey take on a specified subnet on the Bittensor network. + + Usage: + Users can get the amount or 'take' for their child hotkeys (``SS58`` address) + + Example usage:: + + btcli stake get_childkey_take --hotkey --netuid 1 + """ + + @staticmethod + def run(cli: "bittensor.cli"): + """Get childkey take.""" + try: + subtensor: "bittensor.subtensor" = bittensor.subtensor( + config=cli.config, log_verbose=False + ) + GetChildKeyTakeCommand._run(cli, subtensor) + finally: + if "subtensor" in locals(): + subtensor.close() + bittensor.logging.debug("closing subtensor connection") + + @staticmethod + def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + console = Console() + wallet = bittensor.wallet(config=cli.config) + + # Get values if not set. + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + take_u16 = subtensor.get_childkey_take( + netuid=netuid, + hotkey=hotkey, + ) + + # Result + if take_u16: + take = u16_to_float(take_u16) + console.print(f"The childkey take for {hotkey} is {take * 100:.3f}%.") + else: + console.print(":cross_mark:[red] Unable to get childkey take.[/red]") + + @staticmethod + def check_config(config: "bittensor.config"): + if not config.is_set("wallet.name") and not config.no_prompt: + wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) + config.wallet.name = str(wallet_name) + if not config.is_set("wallet.hotkey") and not config.no_prompt: + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) + + @staticmethod + def add_args(parser: argparse.ArgumentParser): + get_childkey_take_parser = parser.add_parser( + "get_childkey_take", help="""Get childkey take.""" + ) + get_childkey_take_parser.add_argument( + "--netuid", dest="netuid", type=int, required=False + ) + get_childkey_take_parser.add_argument( + "--hotkey", dest="hotkey", type=str, required=False + ) + bittensor.wallet.add_args(get_childkey_take_parser) + bittensor.subtensor.add_args(get_childkey_take_parser) + + @staticmethod + def get_take(subtensor, hotkey, netuid) -> float: + """ + Get the take value for a given subtensor, hotkey, and netuid. + + @param subtensor: The subtensor object. + @param hotkey: The hotkey to retrieve the take value for. + @param netuid: The netuid to retrieve the take value for. + + @return: The take value as a float. If the take value is not available, it returns 0. + + """ + take_u16 = subtensor.get_childkey_take( + netuid=netuid, + hotkey=hotkey, + ) + if take_u16: + return u16_to_float(take_u16) + else: + return 0 + + class SetChildrenCommand: """ Executes the ``set_children`` command to add children hotkeys on a specified subnet on the Bittensor network to the caller. @@ -610,81 +901,94 @@ def run(cli: "bittensor.cli"): @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): + console = Console() wallet = bittensor.wallet(config=cli.config) # Get values if not set. - if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask("Enter netuid")) - - netuid = cli.config.netuid - total_subnets = subtensor.get_total_subnets() - if total_subnets is not None and total_subnets <= netuid <= 0: - raise ValueError("Netuid is outside the current subnet range") + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return - if not cli.config.is_set("hotkey"): - cli.config.hotkey = Prompt.ask("Enter parent hotkey (ss58)") - if not wallet_utils.is_valid_ss58_address(cli.config.hotkey): - console.print( - f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]" - ) + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") return - # get children + # get current children curr_children = GetChildrenCommand.retrieve_children( subtensor=subtensor, - hotkey=cli.config.hotkey, - netuid=cli.config.netuid, + hotkey=hotkey, + netuid=netuid, render_table=False, ) if curr_children: - GetChildrenCommand.retrieve_children( + # print the table of current children + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + GetChildrenCommand.render_table( subtensor=subtensor, - hotkey=cli.config.hotkey, - netuid=cli.config.netuid, - render_table=True, - ) - raise ValueError( - f"There are already children hotkeys under parent hotkey {cli.config.hotkey}. " - f"Call revoke_children command before attempting to set_children again, or call the get_children command to view them." + hotkey=hotkey, + hotkey_stake=hotkey_stake, + children=curr_children, + netuid=netuid, + prompt=False, ) + # get new children if not cli.config.is_set("children"): cli.config.children = Prompt.ask( - "Enter child(ren) hotkeys (ss58) as comma-separated values" + "Enter child hotkeys (ss58) as comma-separated values" ) - children = [str(x) for x in re.split(r"[ ,]+", cli.config.children)] + proposed_children = [str(x) for x in re.split(r"[ ,]+", cli.config.children)] + + # Set max 5 children + if len(proposed_children) > MAX_CHILDREN: + console.print( + ":cross_mark:[red] Too many children. Maximum 5 children per hotkey[/red]" + ) + return # Validate children SS58 addresses - for child in children: + for child in proposed_children: if not wallet_utils.is_valid_ss58_address(child): console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") return - if ( - len(children) == 1 - ): # if only one child, then they have full proportion by default - cli.config.proportions = 1.0 - + # get proportions for new children if not cli.config.is_set("proportions"): cli.config.proportions = Prompt.ask( - "Enter the percentage of proportion for each child as comma-separated values (total must equal 1)" + "Enter the percentage of proportion for each child as comma-separated values (total from all children must be less than or equal to 1)" ) # extract proportions and child addresses from cli input - proportions = [float(x) for x in re.split(r"[ ,]+", cli.config.proportions)] + proportions = [ + float(x) for x in re.split(r"[ ,]+", str(cli.config.proportions)) + ] total_proposed = sum(proportions) - if total_proposed != 1: - raise ValueError( - f"Invalid proportion: The sum of all proportions must equal 1 (representing 100% of the allocation). Proposed sum of proportions is {total_proposed}." + if total_proposed > 1: + console.print( + f":cross_mark:[red]Invalid proportion: The sum of all proportions must be less or equal to than 1 (representing 100% of the allocation). Proposed sum addition is proportions is {total_proposed}.[/red]" + ) + return + + if len(proportions) != len(proposed_children): + console.print( + ":cross_mark:[red]Invalid proportion and children length: The count of children and number of proportion values entered do not match.[/red]" ) + return + + # combine proposed and current children + children_with_proportions = list(zip(proportions, proposed_children)) - children_with_proportions = list(zip(proportions, children)) + SetChildrenCommand.print_current_stake( + subtensor=subtensor, children=proposed_children, hotkey=hotkey + ) success, message = subtensor.set_children( wallet=wallet, netuid=netuid, - hotkey=cli.config.hotkey, + hotkey=hotkey, children_with_proportions=children_with_proportions, wait_for_inclusion=cli.config.wait_for_inclusion, wait_for_finalization=cli.config.wait_for_finalization, @@ -693,12 +997,14 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): # Result if success: - GetChildrenCommand.retrieve_children( - subtensor=subtensor, - hotkey=cli.config.hotkey, - netuid=cli.config.netuid, - render_table=True, - ) + if cli.config.wait_for_finalization and cli.config.wait_for_inclusion: + console.print("New Status:") + GetChildrenCommand.retrieve_children( + subtensor=subtensor, + hotkey=hotkey, + netuid=netuid, + render_table=True, + ) console.print( ":white_heavy_check_mark: [green]Set children hotkeys.[/green]" ) @@ -713,8 +1019,13 @@ def check_config(config: "bittensor.config"): wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -737,7 +1048,7 @@ def add_args(parser: argparse.ArgumentParser): "--wait_for_inclusion", dest="wait_for_inclusion", action="store_true", - default=False, + default=True, help="""Wait for the transaction to be included in a block.""", ) set_children_parser.add_argument( @@ -751,12 +1062,33 @@ def add_args(parser: argparse.ArgumentParser): "--prompt", dest="prompt", action="store_true", - default=False, + default=True, help="""Prompt for confirmation before proceeding.""", ) + set_children_parser.add_argument( + "--y", + "--yes", + "--no_prompt", + dest="prompt", + action="store_false", + help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", + ) bittensor.wallet.add_args(set_children_parser) bittensor.subtensor.add_args(set_children_parser) + @staticmethod + def print_current_stake(subtensor, children, hotkey): + console = Console() + parent_stake = subtensor.get_total_stake_for_hotkey(ss58_address=hotkey) + console.print("Current Status:") + console.print(f"My Hotkey: {hotkey} | ", style="cyan", end="", no_wrap=True) + console.print(f"Total Stake: {parent_stake}τ") + for child in children: + child_stake = subtensor.get_total_stake_for_hotkey(child) + console.print( + f"Child Hotkey: {child} | Current Child Stake: {child_stake}τ" + ) + class GetChildrenCommand: """ @@ -790,6 +1122,9 @@ def run(cli: "bittensor.cli"): config=cli.config, log_verbose=False ) return GetChildrenCommand._run(cli, subtensor) + except Exception as e: + console = Console() + console.print(f":cross_mark:[red] An error occurred: {str(e)}[/red]") finally: if "subtensor" in locals(): subtensor.close() @@ -797,37 +1132,62 @@ def run(cli: "bittensor.cli"): @staticmethod def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): - # Get values if not set. - if not cli.config.is_set("netuid"): - cli.config.netuid = int(Prompt.ask("Enter netuid")) - netuid = cli.config.netuid - total_subnets = subtensor.get_total_subnets() - if total_subnets is not None and total_subnets <= netuid <= 0: - raise ValueError("Netuid is outside the current subnet range") + console = Console() + wallet = bittensor.wallet(config=cli.config) - # Get values if not set. - if not cli.config.is_set("hotkey"): - cli.config.hotkey = Prompt.ask("Enter parent hotkey (ss58)") - hotkey = cli.config.hotkey - if not wallet_utils.is_valid_ss58_address(cli.config.hotkey): + # check all + if not cli.config.is_set("all"): + exists, netuid = get_netuid(cli, subtensor) + if not exists: + return + + # get parent hotkey + hotkey = get_hotkey(wallet, cli.config) + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return + + try: + netuids = ( + subtensor.get_all_subnet_netuids() + if cli.config.is_set("all") + else [netuid] + ) + hotkey_stake = GetChildrenCommand.get_parent_stake_info( + console, subtensor, hotkey + ) + for netuid in netuids: + children = subtensor.get_children(hotkey, netuid) + if children: + GetChildrenCommand.render_table( + subtensor, + hotkey, + hotkey_stake, + children, + netuid, + not cli.config.is_set("all"), + ) + except Exception as e: console.print( - f":cross_mark:[red] Invalid SS58 address: {cli.config.hotkey}[/red]" + f":cross_mark:[red] An error occurred while retrieving children: {str(e)}[/red]" ) return - children = subtensor.get_children(hotkey, netuid) - hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + return children - GetChildrenCommand.render_table( - subtensor, hotkey, hotkey_stake, children, netuid, True + @staticmethod + def get_parent_stake_info(console, subtensor, hotkey): + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + console.print( + f"\nYour Hotkey: {hotkey} | ", style="cyan", end="", no_wrap=True ) - - return children + console.print(f"Total Stake: {hotkey_stake}τ") + return hotkey_stake @staticmethod def retrieve_children( subtensor: "bittensor.subtensor", hotkey: str, netuid: int, render_table: bool - ): + ) -> list[tuple[int, str]]: """ Static method to retrieve children for a given subtensor. @@ -842,13 +1202,20 @@ def retrieve_children( List[str]: A list of children hotkeys. """ - children = subtensor.get_children(hotkey, netuid) - if render_table: - hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) - GetChildrenCommand.render_table( - subtensor, hotkey, hotkey_stake, children, netuid, False + try: + children = subtensor.get_children(hotkey, netuid) + if render_table: + hotkey_stake = subtensor.get_total_stake_for_hotkey(hotkey) + GetChildrenCommand.render_table( + subtensor, hotkey, hotkey_stake, children, netuid, False + ) + return children + except Exception as e: + console = Console() + console.print( + f":cross_mark:[red] An error occurred while retrieving children: {str(e)}[/red]" ) - return children + return [] @staticmethod def check_config(config: "bittensor.config"): @@ -856,8 +1223,13 @@ def check_config(config: "bittensor.config"): wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -866,6 +1238,12 @@ def add_args(parser: argparse.ArgumentParser): ) parser.add_argument("--netuid", dest="netuid", type=int, required=False) parser.add_argument("--hotkey", dest="hotkey", type=str, required=False) + parser.add_argument( + "--all", + dest="all", + action="store_true", + help="Retrieve children from all subnets.", + ) bittensor.wallet.add_args(parser) bittensor.subtensor.add_args(parser) @@ -910,40 +1288,40 @@ def render_table( table = Table( show_header=True, header_style="bold magenta", - border_style="green", - style="green", + border_style="blue", + style="dim", ) # Add columns to the table with specific styles - table.add_column("Index", style="cyan", no_wrap=True, justify="right") - table.add_column("ChildHotkey", style="cyan", no_wrap=True) - table.add_column("Proportion", style="cyan", no_wrap=True, justify="right") - table.add_column("Child Stake", style="cyan", no_wrap=True, justify="right") + table.add_column("Index", style="bold yellow", no_wrap=True, justify="center") + table.add_column("ChildHotkey", style="bold green") + table.add_column("Proportion", style="bold cyan", no_wrap=True, justify="right") + table.add_column( + "Childkey Take", style="bold blue", no_wrap=True, justify="right" + ) table.add_column( - "Total Stake Weight", style="cyan", no_wrap=True, justify="right" + "Current Stake Weight", style="bold red", no_wrap=True, justify="right" ) if not children: console.print(table) console.print( - f"There are currently no child hotkeys on subnet {netuid} with Parent HotKey {hotkey}." + f"[bold white]There are currently no child hotkeys on subnet {netuid} with Parent HotKey {hotkey}.[/bold white]" ) if prompt: command = f"btcli stake set_children --children --hotkey --netuid {netuid} --proportion " console.print( - f"To add a child hotkey you can run the command: [white]{command}[/white]" + f"[bold cyan]To add a child hotkey you can run the command: [white]{command}[/white][/bold cyan]" ) return - console.print( - f"Parent HotKey: {hotkey} | ", style="cyan", end="", no_wrap=True - ) - console.print(f"Total Parent Stake: {hotkey_stake.tao}τ") + console.print(f"\nChildren for netuid: {netuid} ", style="cyan") # calculate totals total_proportion = 0 total_stake = 0 total_stake_weight = 0 + avg_take = 0 children_info = [] for child in children: @@ -953,44 +1331,52 @@ def render_table( ss58_address=child_hotkey ) or Balance(0) + child_take = subtensor.get_childkey_take(child_hotkey, netuid) + child_take = u16_to_float(child_take) + # add to totals total_stake += child_stake.tao + avg_take += child_take proportion = u64_to_float(proportion) - children_info.append((proportion, child_hotkey, child_stake)) + children_info.append((proportion, child_hotkey, child_stake, child_take)) children_info.sort( key=lambda x: x[0], reverse=True ) # sorting by proportion (highest first) # add the children info to the table - for i, (proportion, hotkey, stake) in enumerate(children_info, 1): + for i, (proportion, hotkey, stake, child_take) in enumerate(children_info, 1): proportion_percent = proportion * 100 # Proportion in percent proportion_tao = hotkey_stake.tao * proportion # Proportion in TAO total_proportion += proportion_percent # Conditionally format text - proportion_str = f"{proportion_percent}% ({proportion_tao}τ)" + proportion_str = f"{proportion_percent:.3f}% ({proportion_tao:.3f}τ)" stake_weight = stake.tao + proportion_tao total_stake_weight += stake_weight + take_str = f"{child_take * 100:.3f}%" - hotkey = Text(hotkey, style="red" if proportion == 0 else "") + hotkey = Text(hotkey, style="italic red" if proportion == 0 else "") table.add_row( str(i), hotkey, proportion_str, - str(stake.tao), - str(stake_weight), + take_str, + str(f"{stake_weight:.3f}"), ) + avg_take = avg_take / len(children_info) + # add totals row table.add_row( "", - "Total", - f"{total_proportion}%", - f"{total_stake}τ", - f"{total_stake_weight}τ", + "[dim]Total[/dim]", + f"[dim]{total_proportion:.3f}%[/dim]", + f"[dim](avg) {avg_take * 100:.3f}%[/dim]", + f"[dim]{total_stake_weight:.3f}τ[/dim]", + style="dim", ) console.print(table) diff --git a/bittensor/commands/unstake.py b/bittensor/commands/unstake.py index cb51c081b..291aeb6e9 100644 --- a/bittensor/commands/unstake.py +++ b/bittensor/commands/unstake.py @@ -26,6 +26,7 @@ from bittensor.utils.balance import Balance from . import defaults, GetChildrenCommand from .utils import get_hotkey_wallets_for_wallet +from ..utils import wallet_utils console = bittensor.__console__ @@ -341,28 +342,31 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): if not cli.config.is_set("netuid"): cli.config.netuid = int(Prompt.ask("Enter netuid")) - if not cli.config.is_set("hotkey"): - cli.config.hotkey = Prompt.ask("Enter parent hotkey (ss58)") - - # Get and display current children information - current_children = GetChildrenCommand.retrieve_children( - subtensor=subtensor, - hotkey=cli.config.hotkey, - netuid=cli.config.netuid, - render_table=False, - ) - - # Parse from strings netuid = cli.config.netuid + total_subnets = subtensor.get_total_subnets() + if total_subnets is not None and not (0 <= netuid < total_subnets): + console.print("Netuid is outside the current subnet range") + return + + # get parent hotkey + if wallet and wallet.hotkey: + hotkey = wallet.hotkey.ss58_address + elif cli.config.is_set("hotkey"): + hotkey = cli.config.hotkey + elif cli.config.is_set("ss58"): + hotkey = cli.config.ss58 + else: + hotkey = Prompt.ask("Enter parent hotkey (ss58)") - # Prepare children with zero proportions - children_with_zero_proportions = [(0.0, child[1]) for child in current_children] + if not wallet_utils.is_valid_ss58_address(hotkey): + console.print(f":cross_mark:[red] Invalid SS58 address: {hotkey}[/red]") + return success, message = subtensor.set_children( wallet=wallet, netuid=netuid, - children_with_proportions=children_with_zero_proportions, - hotkey=cli.config.hotkey, + children_with_proportions=[], + hotkey=hotkey, wait_for_inclusion=cli.config.wait_for_inclusion, wait_for_finalization=cli.config.wait_for_finalization, prompt=cli.config.prompt, @@ -373,8 +377,8 @@ def _run(cli: "bittensor.cli", subtensor: "bittensor.subtensor"): if cli.config.wait_for_finalization and cli.config.wait_for_inclusion: GetChildrenCommand.retrieve_children( subtensor=subtensor, - hotkey=cli.config.hotkey, - netuid=cli.config.netuid, + hotkey=hotkey, + netuid=netuid, render_table=True, ) console.print( @@ -391,8 +395,13 @@ def check_config(config: "bittensor.config"): wallet_name = Prompt.ask("Enter wallet name", default=defaults.wallet.name) config.wallet.name = str(wallet_name) if not config.is_set("wallet.hotkey") and not config.no_prompt: - hotkey = Prompt.ask("Enter hotkey name", default=defaults.wallet.hotkey) - config.wallet.hotkey = str(hotkey) + hotkey_or_ss58 = Prompt.ask( + "Enter hotkey name or ss58", default=defaults.wallet.hotkey + ) + if wallet_utils.is_valid_ss58_address(hotkey_or_ss58): + config.ss58 = str(hotkey_or_ss58) + else: + config.wallet.hotkey = str(hotkey_or_ss58) @staticmethod def add_args(parser: argparse.ArgumentParser): @@ -405,22 +414,30 @@ def add_args(parser: argparse.ArgumentParser): "--wait_for_inclusion", dest="wait_for_inclusion", action="store_true", - default=False, + default=True, help="""Wait for the transaction to be included in a block.""", ) parser.add_argument( "--wait_for_finalization", dest="wait_for_finalization", action="store_true", - default=False, + default=True, help="""Wait for the transaction to be finalized.""", ) parser.add_argument( "--prompt", dest="prompt", action="store_true", - default=False, + default=True, help="""Prompt for confirmation before proceeding.""", ) + parser.add_argument( + "--y", + "--yes", + "--no_prompt", + dest="prompt", + action="store_false", + help="""Disable prompt for confirmation before proceeding. Defaults to Yes for all prompts.""", + ) bittensor.wallet.add_args(parser) bittensor.subtensor.add_args(parser) diff --git a/bittensor/extrinsics/staking.py b/bittensor/extrinsics/staking.py index 864b29a6c..b6d5cf5d6 100644 --- a/bittensor/extrinsics/staking.py +++ b/bittensor/extrinsics/staking.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2021 Yuma Rao # Copyright © 2023 Opentensor Foundation -from math import floor # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated # documentation files (the “Software”), to deal in the Software without restriction, including without limitation @@ -22,10 +21,12 @@ from typing import List, Union, Optional, Tuple import bittensor -from ..utils.formatting import float_to_u64 +from ..utils.formatting import float_to_u64, float_to_u16 from bittensor.utils.balance import Balance +console = bittensor.__console__ + def _check_threshold_amount( subtensor: "bittensor.subtensor", stake_balance: Balance @@ -535,24 +536,24 @@ def __do_add_stake_single( return success -def set_children_extrinsic( +def set_childkey_take_extrinsic( subtensor: "bittensor.subtensor", wallet: "bittensor.wallet", hotkey: str, netuid: int, - children_with_proportions: List[Tuple[float, str]], + take: float, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, ) -> Tuple[bool, str]: """ - Sets children hotkeys with proportions assigned from the parent. + Sets childkey take. Args: subtensor (bittensor.subtensor): Subtensor endpoint to use. wallet (bittensor.wallet): Bittensor wallet object. - hotkey (str): Parent hotkey. - children_with_proportions (List[str]): Children hotkeys. + hotkey (str): Childkey hotkey. + take (float): Childkey take value. netuid (int): Unique identifier of for the subnet. wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. @@ -567,17 +568,98 @@ def set_children_extrinsic( """ + # Ask before moving on. + if prompt: + if not Confirm.ask( + f"Do you want to set childkey take to: [bold white]{take*100}%[/bold white]?" + ): + return False, "Operation Cancelled" + # Decrypt coldkey. wallet.coldkey - user_hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. - if hotkey != user_hotkey_ss58: - raise ValueError("Can only call children for other hotkeys.") + with bittensor.__console__.status( + f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..." + ): + try: + if 0 < take <= 0.18: + take_u16 = float_to_u16(take) + else: + return False, "Invalid take value" + + success, error_message = subtensor._do_set_childkey_take( + wallet=wallet, + hotkey=hotkey, + netuid=netuid, + take=take_u16, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return ( + True, + "Not waiting for finalization or inclusion. Set childkey take initiated.", + ) + + if success: + bittensor.__console__.print( + ":white_heavy_check_mark: [green]Finalized[/green]" + ) + bittensor.logging.success( + prefix="Setting childkey take", + suffix="Finalized: " + str(success), + ) + return True, "Successfully set childkey take and Finalized." + else: + bittensor.__console__.print( + f":cross_mark: [red]Failed[/red]: {error_message}" + ) + bittensor.logging.warning( + prefix="Setting childkey take", + suffix="Failed: " + str(error_message), + ) + return False, error_message + + except Exception as e: + return False, f"Exception occurred while setting childkey take: {str(e)}" + +def set_children_extrinsic( + subtensor: "bittensor.subtensor", + wallet: "bittensor.wallet", + hotkey: str, + netuid: int, + children_with_proportions: List[Tuple[float, str]], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, +) -> Tuple[bool, str]: + """ + Sets children hotkeys with proportions assigned from the parent. + + Args: + subtensor (bittensor.subtensor): Subtensor endpoint to use. + wallet (bittensor.wallet): Bittensor wallet object. + hotkey (str): Parent hotkey. + children_with_proportions (List[str]): Children hotkeys. + netuid (int): Unique identifier of for the subnet. + wait_for_inclusion (bool): If set, waits for the extrinsic to enter a block before returning ``true``, or returns ``false`` if the extrinsic fails to enter the block within the timeout. + wait_for_finalization (bool): If set, waits for the extrinsic to be finalized on the chain before returning ``true``, or returns ``false`` if the extrinsic fails to be finalized within the timeout. + prompt (bool): If ``true``, the call waits for confirmation from the user before proceeding. + + Returns: + Tuple[bool, Optional[str]]: A tuple containing a success flag and an optional error message. + + Raises: + bittensor.errors.ChildHotkeyError: If the extrinsic fails to be finalized or included in the block. + bittensor.errors.NotRegisteredError: If the hotkey is not registered in any subnets. + + """ # Check if all children are being revoked - all_revoked = all(prop == 0.0 for prop, _ in children_with_proportions) + all_revoked = len(children_with_proportions) == 0 - operation = "Revoke all children hotkeys" if all_revoked else "Set children hotkeys" + operation = "Revoking all child hotkeys" if all_revoked else "Setting child hotkeys" # Ask before moving on. if prompt: @@ -588,7 +670,7 @@ def set_children_extrinsic( return False, "Operation Cancelled" else: if not Confirm.ask( - "Do you want to set children hotkeys:\n[bold white]{}[/bold white]?".format( + "Do you want to set children hotkeys with proportions:\n[bold white]{}[/bold white]?".format( "\n".join( f" {child[1]}: {child[0]}" for child in children_with_proportions @@ -597,15 +679,23 @@ def set_children_extrinsic( ): return False, "Operation Cancelled" + # Decrypt coldkey. + wallet.coldkey + + user_hotkey_ss58 = wallet.hotkey.ss58_address # Default to wallet's own hotkey. + if hotkey != user_hotkey_ss58: + raise ValueError("Cannot set/revoke child hotkeys for others.") + with bittensor.__console__.status( f":satellite: {operation} on [white]{subtensor.network}[/white] ..." ): try: - normalized_children = ( - prepare_child_proportions(children_with_proportions) - if not all_revoked - else children_with_proportions - ) + if not all_revoked: + normalized_children = prepare_child_proportions( + children_with_proportions + ) + else: + normalized_children = [] success, error_message = subtensor._do_set_children( wallet=wallet, @@ -647,37 +737,24 @@ def set_children_extrinsic( def prepare_child_proportions(children_with_proportions): """ - Convert proportions to u64 and normalize + Convert proportions to u64 and normalize, ensuring total does not exceed u64 max. """ children_u64 = [ - (float_to_u64(prop), child) for prop, child in children_with_proportions + (float_to_u64(proportion), child) + for proportion, child in children_with_proportions ] - normalized_children = normalize_children_and_proportions(children_u64) - return normalized_children - - -def normalize_children_and_proportions( - children: List[Tuple[int, str]], -) -> List[Tuple[int, str]]: - """ - Normalizes the proportions of children so that they sum to u64::MAX. - """ - total = sum(prop for prop, _ in children) - u64_max = 2**64 - 1 - normalized_children = [ - (int(floor(prop * (u64_max - 1) / total)), child) for prop, child in children - ] - sum_norm = sum(prop for prop, _ in normalized_children) - - # if the sum is more, subtract the excess from the first child - if sum_norm > u64_max: - if abs(sum_norm - u64_max) > 10: - raise ValueError( - "The sum of normalized proportions is out of the acceptable range." - ) - normalized_children[0] = ( - normalized_children[0][0] - (sum_norm - (u64_max - 1)), - normalized_children[0][1], + total = sum(proportion for proportion, _ in children_u64) + + if total > (2**64 - 1): + excess = total - (2**64 - 1) + if excess > (2**64 * 0.01): # Example threshold of 1% of u64 max + raise ValueError("Excess is too great to normalize proportions") + largest_child_index = max( + range(len(children_u64)), key=lambda i: children_u64[i][0] + ) + children_u64[largest_child_index] = ( + children_u64[largest_child_index][0] - excess, + children_u64[largest_child_index][1], ) - return normalized_children + return children_u64 diff --git a/bittensor/subtensor.py b/bittensor/subtensor.py index 05ee9bb2c..ac22a3a14 100644 --- a/bittensor/subtensor.py +++ b/bittensor/subtensor.py @@ -102,6 +102,7 @@ add_stake_extrinsic, add_stake_multiple_extrinsic, set_children_extrinsic, + set_childkey_take_extrinsic, ) from .extrinsics.transfer import transfer_extrinsic from .extrinsics.unstaking import ( @@ -417,7 +418,7 @@ def determine_chain_endpoint_and_network(network: str): elif "127.0.0.1" in network or "localhost" in network: return "local", network else: - return "unknown", network + return "unknown network", network @staticmethod def setup_config(network: str, config: "bittensor.config"): @@ -2301,6 +2302,97 @@ def make_substrate_call_with_retry(): # Child hotkeys # ################### + def set_childkey_take( + self, + wallet: "bittensor.wallet", + hotkey: str, + take: float, + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + prompt: bool = False, + ) -> tuple[bool, str]: + """Sets a childkey take extrinsic on the subnet. + + Args: + wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + hotkey: (str): Hotkey ``ss58`` address of the child for which take is getting set. + netuid (int): Unique identifier of for the subnet. + take (float): Value of childhotkey take on subnet. + wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. + wait_for_finalization (bool): If ``true``, waits for finalization before returning. + prompt (bool, optional): If ``True``, prompts for user confirmation before proceeding. + Returns: + success (bool): ``True`` if the extrinsic was successful. + Raises: + ChildHotkeyError: If the extrinsic failed. + """ + + return set_childkey_take_extrinsic( + self, + wallet=wallet, + hotkey=hotkey, + take=take, + netuid=netuid, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + ) + + def _do_set_childkey_take( + self, + wallet: "bittensor.wallet", + hotkey: str, + take: int, + netuid: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = False, + ) -> tuple[bool, Optional[str]]: + """Sends a set_children hotkey extrinsic on the chain. + + Args: + wallet (:func:`bittensor.wallet`): Wallet object that can sign the extrinsic. + hotkey: (str): Hotkey ``ss58`` address of the wallet for which take is getting set. + take: (int): The take that this ss58 hotkey will have if assigned as a child hotkey as u16 value. + netuid (int): Unique identifier for the network. + wait_for_inclusion (bool): If ``true``, waits for inclusion before returning. + wait_for_finalization (bool): If ``true``, waits for finalization before returning. + Returns: + success (bool): ``True`` if the extrinsic was successful. + """ + + @retry(delay=1, tries=3, backoff=2, max_delay=4, logger=_logger) + def make_substrate_call_with_retry(): + # create extrinsic call + call = self.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_childkey_take", + call_params={ + "hotkey": hotkey, + "take": take, + "netuid": netuid, + }, + ) + extrinsic = self.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = self.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not wait_for_finalization and not wait_for_inclusion: + return True, None + + response.process_events() + if not response.is_success: + return False, format_error_message(response.error_message) + else: + return True, None + + return make_substrate_call_with_retry() + def set_children( self, wallet: "bittensor.wallet", @@ -4593,7 +4685,37 @@ def make_substrate_call_with_retry(encoded_coldkey_: List[int]): # Child Hotkey Information # ############################ - def get_children(self, hotkey, netuid): + def get_childkey_take( + self, hotkey: str, netuid: int, block: Optional[int] = None + ) -> Optional[int]: + """ + Get the childkey take of a hotkey on a specific network. + Args: + - hotkey (str): The hotkey to search for. + - netuid (int): The netuid to search for. + - block (Optional[int]): Optional parameter specifying the block number. Defaults to None. + + Returns: + - Optional[int]: The value of the "ChildkeyTake" if found, or None if any error occurs. + """ + try: + childkey_take = self.query_subtensor( + name="ChildkeyTake", + block=block, + params=[hotkey, netuid], + ) + if childkey_take: + return int(childkey_take.value) + + except SubstrateRequestException as e: + print(f"Error querying ChildKeys: {e}") + return None + except Exception as e: + print(f"Unexpected error in get_children: {e}") + return None + return None + + def get_children(self, hotkey, netuid) -> list[tuple[int, str]] | list[Any] | None: """ Get the children of a hotkey on a specific network. Args: diff --git a/bittensor/utils/formatting.py b/bittensor/utils/formatting.py index 46dfc7f8f..22fbe74c1 100644 --- a/bittensor/utils/formatting.py +++ b/bittensor/utils/formatting.py @@ -37,8 +37,10 @@ def convert_blocks_to_time(blocks: int, block_time: int = 12) -> tuple[int, int, return hours, minutes, remaining_seconds -def float_to_u16(value: int) -> int: +def float_to_u16(value: float) -> int: # Ensure the input is within the expected range + if value is None: + return 0 if not (0 <= value <= 1): raise ValueError("Input value must be between 0 and 1") @@ -49,6 +51,8 @@ def float_to_u16(value: int) -> int: def u16_to_float(value: int) -> float: # Ensure the input is within the expected range + if value is None: + return 0.0 if not (0 <= value <= 65535): raise ValueError("Input value must be between 0 and 65535") @@ -58,12 +62,14 @@ def u16_to_float(value: int) -> float: def float_to_u64(value: float) -> int: + if value == 0.0: + return 0 # Ensure the input is within the expected range - if not (0 <= value < 1): + if not (0 <= value <= 1): raise ValueError("Input value must be between 0 and 1") # Convert the float to a u64 value, take the floor value - return int(math.floor((value * (2**64 - 1)))) - 1 + return int(math.floor((value * (2**64 - 1)))) def u64_to_float(value: int) -> float: diff --git a/bittensor/utils/subtensor.py b/bittensor/utils/subtensor.py index 279a68322..0df9c64d8 100644 --- a/bittensor/utils/subtensor.py +++ b/bittensor/utils/subtensor.py @@ -154,7 +154,7 @@ def format_parent(proportion, parent) -> Tuple[str, str]: return int_proportion, parent.value -def format_children(children) -> List[Tuple[str, str]]: +def format_children(children) -> List[Tuple[int, str]]: """ Formats raw children data into a list of tuples. Args: diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 9db51c100..6f648be13 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -3,6 +3,7 @@ import re import shlex import signal +import socket import subprocess import time @@ -18,66 +19,86 @@ logging.basicConfig(level=logging.INFO) +# Function to check if the process is running by port +def is_chain_running(port): + """Check if a node is running on the given port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + # Attempt to connect to the given port on localhost + s.connect(("127.0.0.1", port)) + return True + except (ConnectionRefusedError, OSError): + # If the connection is refused or there's an OS error, the node is not running + return False + + # Fixture for setting up and tearing down a localnet.sh chain between tests @pytest.fixture(scope="function") def local_chain(request): param = request.param if hasattr(request, "param") else None - # Get the environment variable for the script path script_path = os.getenv("LOCALNET_SH_PATH") if not script_path: - # Skip the test if the localhost.sh path is not set logging.warning("LOCALNET_SH_PATH env variable is not set, e2e test skipped.") pytest.skip("LOCALNET_SH_PATH environment variable is not set.") - # Check if param is None, and handle it accordingly - args = "" if param is None else f"{param}" - - # compile commands to send to process - cmds = shlex.split(f"{script_path} {args}") - # Start new node process - process = subprocess.Popen( - cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid - ) - - # Pattern match indicates node is compiled and ready - pattern = re.compile(r"Imported #1") + # Determine the port to check based on `param` + port = 9945 # Default port if `param` is None - # install neuron templates - logging.info("downloading and installing neuron templates from github") + # Always perform template installation + logging.info("Downloading and installing neuron templates from GitHub") templates_dir = clone_or_update_templates() install_templates(templates_dir) - timestamp = int(time.time()) - - def wait_for_node_start(process, pattern): - for line in process.stdout: - print(line.strip()) - # 20 min as timeout - if int(time.time()) - timestamp > 20 * 60: - pytest.fail("Subtensor not started in time") - if pattern.search(line): - print("Node started!") - break - - wait_for_node_start(process, pattern) + already_running = False + if is_chain_running(port): + already_running = True + logging.info(f"Chain already running on port {port}, skipping start.") + else: + logging.info(f"Starting new chain on port {port}...") + # compile commands to send to process + cmds = shlex.split(f"{script_path} {param}") + # Start new node process + process = subprocess.Popen( + cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid + ) + + # Wait for the node to start using the existing pattern match + pattern = re.compile(r"Imported #1") + timestamp = int(time.time()) + + def wait_for_node_start(process, pattern): + for line in process.stdout: + print(line.strip()) + if int(time.time()) - timestamp > 20 * 60: + pytest.fail("Subtensor not started in time") + if pattern.search(line): + print("Node started!") + break + + wait_for_node_start(process, pattern) + + # Continue with installing templates + logging.info("Downloading and installing neuron templates from GitHub") + templates_dir = clone_or_update_templates() + install_templates(templates_dir) - # Run the test, passing in substrate interface - yield SubstrateInterface(url="ws://127.0.0.1:9945") + # Run the test, passing in the substrate interface + yield SubstrateInterface(url=f"ws://127.0.0.1:{port}") - # Terminate the process group (includes all child processes) - os.killpg(os.getpgid(process.pid), signal.SIGTERM) + if not already_running: + # Terminate the process group (includes all child processes) + os.killpg(os.getpgid(process.pid), signal.SIGTERM) - # Give some time for the process to terminate - time.sleep(1) + # Give some time for the process to terminate + time.sleep(1) - # If the process is not terminated, send SIGKILL - if process.poll() is None: - os.killpg(os.getpgid(process.pid), signal.SIGKILL) + # If the process is not terminated, send SIGKILL + if process.poll() is None: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) - # Ensure the process has terminated - process.wait() + # Ensure the process has terminated + process.wait() - # uninstall templates - logging.info("uninstalling neuron templates") + logging.info("Uninstalling neuron templates") uninstall_templates(templates_dir) diff --git a/tests/e2e_tests/subcommands/stake/test_childkeys.py b/tests/e2e_tests/subcommands/stake/test_childkeys.py index d29fb877d..1715c6f7d 100644 --- a/tests/e2e_tests/subcommands/stake/test_childkeys.py +++ b/tests/e2e_tests/subcommands/stake/test_childkeys.py @@ -8,12 +8,13 @@ RevokeChildrenCommand, GetChildrenCommand, ) +from bittensor.commands.stake import SetChildKeyTakeCommand, GetChildKeyTakeCommand from bittensor.extrinsics.staking import prepare_child_proportions from tests.e2e_tests.utils import setup_wallet, wait_interval @pytest.mark.asyncio -async def test_set_revoke_children(local_chain, capsys): +async def test_set_revoke_children_multiple(local_chain, capsys): """ Test the setting and revoking of children hotkeys for staking. @@ -49,11 +50,11 @@ async def test_set_revoke_children(local_chain, capsys): bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") eve_keypair, eve_exec_command, eve_wallet = setup_wallet("//Eve") - alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) - assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + # alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + # assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: - exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + exec_command(RegisterCommand, ["s", "register", "--netuid", "3"]) alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) @@ -74,8 +75,8 @@ async def wait(): await wait() children_with_proportions = [ - [0.6, bob_keypair.ss58_address], - [0.4, eve_keypair.ss58_address], + [0.4, bob_keypair.ss58_address], + [0.2, eve_keypair.ss58_address], ] # Test 1: Set multiple children @@ -92,6 +93,10 @@ async def wait(): str(alice_keypair.ss58_address), "--proportions", f"{children_with_proportions[0][0]},{children_with_proportions[1][0]}", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", "--wait_for_inclusion", "True", "--wait_for_finalization", @@ -125,14 +130,9 @@ async def wait(): ], ) output = capsys.readouterr().out - assert ( - "Parent HotKey: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY | Total Parent Stake: 100000.0" - in output - ) - assert "ChildHotkey ┃ Proportion" in output - assert "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92U… │ 60.0%" in output - assert "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZc… │ 40.0%" in output - assert "Total │ 100.0%" in output + assert "5FHne… │ 40.000%" in output + assert "5HGjW… │ 20.000%" in output + assert "Total │ 60.000%" in output await wait() @@ -146,6 +146,10 @@ async def wait(): "1", "--hotkey", str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", "--wait_for_inclusion", "True", "--wait_for_finalization", @@ -174,3 +178,295 @@ async def wait(): ) output = capsys.readouterr().out assert "There are currently no child hotkeys on subnet" in output + + +@pytest.mark.asyncio +async def test_set_revoke_childkey_take(local_chain, capsys): + """ + Test the setting and retrieving of childkey take amounts for staking. + + This test case covers the following scenarios: + 1. Setting a childkey take amount for a specific hotkey + 2. Retrieving the childkey take amount + 3. Verifying the retrieved childkey take amount + + The test uses one wallet (Alice) and performs operations + on a local blockchain. + + Args: + local_chain: A fixture providing access to the local blockchain + capsys: A pytest fixture for capturing stdout and stderr + + The test performs the following steps: + - Set up wallets for Alice, Bob, and Eve + - Create a subnet and register wallets + - Set a childkey take amount for Alice + - Verify the setting operation was successful + - Retrieve the set childkey take amount + - Verify the retrieved amount is correct + + This test ensures the proper functioning of setting and retrieving + childkey take amounts in the staking system. + """ + # Setup + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + for exec_command in [alice_exec_command]: + exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + + # Test 1: Set multiple children + alice_exec_command( + SetChildKeyTakeCommand, + [ + "stake", + "set_childkey_take", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--take", + "0.12", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + output = capsys.readouterr().out + assert ( + "The childkey take for 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY is now \nset to 12.000%." + in output + ) + + # Test 1: Set multiple children + alice_exec_command( + GetChildKeyTakeCommand, + [ + "stake", + "get_childkey_take", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + ], + ) + + output = capsys.readouterr().out + assert ( + "The childkey take for 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY is \n12.000%." + in output + ) + + +@pytest.mark.asyncio +async def test_set_revoke_children_singular(local_chain, capsys): + """ + Test the setting and revoking of children hotkeys for staking. + + This test case covers the following scenarios: + 1. Setting multiple children hotkeys with specified proportions (set one at a time) + 2. Retrieving children information + 3. Revoking children hotkeys (one at a time) + 4. Verifying the absence of children after revocation + + The test uses three wallets (Alice, Bob, and Eve) and performs operations + on a local blockchain. + + Args: + local_chain: A fixture providing access to the local blockchain + capsys: A pytest fixture for capturing stdout and stderr + + The test performs the following steps: + - Set up wallets for Alice, Bob, and Eve + - Create a subnet and register wallets + - Add stake to Alice's wallet + - Set Bob and Eve as children of Alice with specific proportions + - Verify the children are set correctly + - Get and verify children information + - Revoke all children + - Verify children are revoked + - Check that no children exist after revocation + + This test ensures the proper functioning of setting children hotkeys, + retrieving children information, and revoking children in the staking system. + """ + # Setup + alice_keypair, alice_exec_command, alice_wallet = setup_wallet("//Alice") + bob_keypair, bob_exec_command, bob_wallet = setup_wallet("//Bob") + eve_keypair, eve_exec_command, eve_wallet = setup_wallet("//Eve") + + alice_exec_command(RegisterSubnetworkCommand, ["s", "create"]) + assert local_chain.query("SubtensorModule", "NetworksAdded", [1]).serialize() + + for exec_command in [alice_exec_command, bob_exec_command, eve_exec_command]: + exec_command(RegisterCommand, ["s", "register", "--netuid", "1"]) + + alice_exec_command(StakeCommand, ["stake", "add", "--amount", "100000"]) + + async def wait(): + # wait rate limit, until we are allowed to get children + + rate_limit = ( + subtensor.query_constant( + module_name="SubtensorModule", constant_name="InitialTempo" + ).value + * 2 + ) + curr_block = subtensor.get_current_block() + await wait_interval(rate_limit + curr_block + 1, subtensor) + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + + await wait() + + children_with_proportions = [ + [0.6, bob_keypair.ss58_address], + [0.4, eve_keypair.ss58_address], + ] + + # Test 1: Set first children + alice_exec_command( + SetChildrenCommand, + [ + "stake", + "set_children", + "--netuid", + "1", + "--children", + f"{children_with_proportions[0][1]}", + "--hotkey", + str(alice_keypair.ss58_address), + "--proportions", + f"{children_with_proportions[0][0]}", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + output = capsys.readouterr().out + assert "5FHn… │ 60.000%" in output + + await wait() + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + + assert len(children_info) == 1, "Failed to set child hotkeys" + + # Test 2: Set second child + alice_exec_command( + SetChildrenCommand, + [ + "stake", + "set_children", + "--netuid", + "1", + "--children", + f"{children_with_proportions[1][1]}", + "--hotkey", + str(alice_keypair.ss58_address), + "--proportions", + f"{children_with_proportions[1][0]}", + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + + await wait() + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + + assert len(children_info) == 1, "Failed to set child hotkey" + + # Test 2: Get children information + alice_exec_command( + GetChildrenCommand, + [ + "stake", + "get_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + ], + ) + output = capsys.readouterr().out + assert "5HGj… │ 40.000%" in output + + await wait() + + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + assert len(children_info) == 1, "Failed to revoke child hotkey" + + # Test 4: Revoke second child + alice_exec_command( + RevokeChildrenCommand, + [ + "stake", + "revoke_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + "--wait_for_inclusion", + "True", + "--wait_for_finalization", + "True", + ], + ) + await wait() + subtensor = bittensor.subtensor(network="ws://localhost:9945") + children_info = subtensor.get_children(hotkey=alice_keypair.ss58_address, netuid=1) + assert len(children_info) == 0, "Failed to revoke child hotkey" + + # Test 4: Get children after revocation + alice_exec_command( + GetChildrenCommand, + [ + "stake", + "get_children", + "--netuid", + "1", + "--hotkey", + str(alice_keypair.ss58_address), + "--wallet.name", + "default", + "--wallet.hotkey", + "default", + ], + ) + output = capsys.readouterr().out + assert ( + "There are currently no child hotkeys on subnet 1 with Parent HotKey \n5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY." + in output + ) diff --git a/tests/unit_tests/test_subtensor.py b/tests/unit_tests/test_subtensor.py index 731285c22..c651eaa57 100644 --- a/tests/unit_tests/test_subtensor.py +++ b/tests/unit_tests/test_subtensor.py @@ -269,7 +269,7 @@ def mock_add_argument(*args, **kwargs): ("localhost", "local", "localhost"), # Edge cases (None, None, None), - ("unknown", "unknown", "unknown"), + ("unknown", "unknown network", "unknown"), ], ) def test_determine_chain_endpoint_and_network(