From 88bfed5729e09bde1d9e194ca2a45ef9840ef4cd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 14:38:59 -0700 Subject: [PATCH 01/39] adds SubsubnetCountCurrent extrinsic --- .../src/bittensor/subtensor_interface.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index cafef0439..f3da23931 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1171,6 +1171,23 @@ async def get_subnet_hyperparameters( return SubnetHyperparameters.from_any(result) + async def get_sub_subnet_count( + self, netuid: int, block_hash: Optional[str] = None + ) -> int: + """Return the number of sub-subnets that belong to the provided subnet.""" + + result = await self.query( + module="SubtensorModule", + storage_function="SubsubnetCountCurrent", + params=[netuid], + block_hash=block_hash, + ) + + if result is None: + return 1 + + return int(result) + async def burn_cost(self, block_hash: Optional[str] = None) -> Optional[Balance]: result = await self.query_runtime_api( runtime_api="SubnetRegistrationRuntimeApi", From 09c49af018727ccd6579ea1afb15cf1d5d6484ad Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 14:39:40 -0700 Subject: [PATCH 02/39] subnet.count() --- bittensor_cli/src/commands/subnets/subnets.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d8571f3f6..d006fd8f3 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1443,6 +1443,68 @@ async def show_subnet(netuid_: int): return result +async def count( + subtensor: "SubtensorInterface", + netuid: int, + json_output: bool = False, +) -> Optional[int]: + """Display how many sub-subnets exist for the provided subnet.""" + + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): + err_console.print(f"[red]Subnet {netuid} does not exist[/red]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": f"Subnet {netuid} does not exist"} + ) + ) + return None + + with console.status( + f":satellite:Retrieving sub-subnet count from {subtensor.network}...", + spinner="aesthetic", + ): + sub_subnet_count = await subtensor.get_sub_subnet_count( + netuid, block_hash=block_hash + ) + if not sub_subnet_count: + if json_output: + json_console.print( + json.dumps( + { + "netuid": netuid, + "count": None, + "error": "Failed to get sub-subnet count", + } + ) + ) + else: + err_console.print( + "Subnet sub-subnet count: [red]Failed to get sub-subnet count[/red]" + ) + return None + + if json_output: + json_console.print( + json.dumps( + { + "netuid": netuid, + "count": sub_subnet_count, + "error": "", + } + ) + ) + else: + sub_subnets_count = max(sub_subnet_count - 1, 0) + console.print( + f"[blue]Subnet {netuid}[/blue] currently has [blue]{sub_subnets_count}[/blue] sub-subnet" + f"{'s' if sub_subnets_count < 2 else ''}." + ) + + return sub_subnet_count + + async def burn_cost( subtensor: "SubtensorInterface", json_output: bool = False ) -> Optional[Balance]: From 9512cdb8e91fa0cd7e773fd904dbd775cc6b1e64 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 14:39:57 -0700 Subject: [PATCH 03/39] btcli subnet count --- bittensor_cli/cli.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 88ae40580..0af6f62f9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -968,6 +968,9 @@ def __init__(self): self.subnets_app.command( "list", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_list) + self.subnets_app.command( + "count", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] + )(self.subnets_count) self.subnets_app.command( "burn-cost", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] )(self.subnets_burn_cost) @@ -5544,6 +5547,31 @@ def subnets_show( ) ) + def subnets_count( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Show the number of sub-subnets registered under a subnet. + + EXAMPLE + + [green]$[/green] btcli subnets count --netuid 1 + """ + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + return self._run_command( + subnets.count( + subtensor=subtensor, + netuid=netuid, + json_output=json_output, + ) + ) + def subnets_burn_cost( self, network: Optional[list[str]] = Options.network, From aed6db53e3d81874dbb1b89c97f0ca45e80eee42 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 16:14:03 -0700 Subject: [PATCH 04/39] add more clarity --- bittensor_cli/src/commands/subnets/subnets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d006fd8f3..709514df7 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1500,6 +1500,7 @@ async def count( console.print( f"[blue]Subnet {netuid}[/blue] currently has [blue]{sub_subnets_count}[/blue] sub-subnet" f"{'s' if sub_subnets_count < 2 else ''}." + f"\n[dim](Raw count: {sub_subnet_count}; a value of 1 means there are no sub-subnets beyond the main subnet)[/dim]" ) return sub_subnet_count From df0e04b495e6e429c67c48b7f3ef614e7f0e4d26 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 16:31:20 -0700 Subject: [PATCH 05/39] sudo_sub + btcli sudo sub count --- bittensor_cli/cli.py | 115 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0af6f62f9..1ac30c194 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -658,6 +658,8 @@ class CLIManager: app: typer.Typer config_app: typer.Typer wallet_app: typer.Typer + sudo_app: typer.Typer + sudo_sub_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer utils_app: typer.Typer @@ -732,6 +734,7 @@ def __init__(self): self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) + self.sudo_sub_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) @@ -779,6 +782,12 @@ def __init__(self): no_args_is_help=True, ) self.app.add_typer(self.sudo_app, name="su", hidden=True, no_args_is_help=True) + self.sudo_app.add_typer( + self.sudo_sub_app, + name="sub", + short_help="Sub-subnet admin commands", + no_args_is_help=True, + ) # subnets aliases self.app.add_typer( @@ -939,6 +948,9 @@ def __init__(self): children_app.command("take")(self.stake_childkey_take) # sudo commands + self.sudo_sub_app.command( + "count", rich_help_panel=HELP_PANELS["SUDO"]["SUB"] + )(self.sudo_sub_count) self.sudo_app.command("set", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_set ) @@ -5009,6 +5021,109 @@ def stake_childkey_take( json_console.print(json.dumps(output)) return results + def sudo_sub_count( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: int = Options.netuid, + sub_count: Optional[int] = typer.Option( + None, + "--count", + "--sub-count", + help="Number of sub-subnets to set for the subnet.", + ), + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Set the number of sub-subnets registered under a subnet. + + EXAMPLE + + [green]$[/green] btcli sudo sub count --netuid 1 --count 2 + """ + + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + if not json_output: + current_count = self._run_command( + subnets.count( + subtensor=subtensor, + netuid=netuid, + json_output=False, + ), + exit_early=False, + ) + else: + current_count = self._run_command( + subtensor.get_sub_subnet_count(netuid), + exit_early=False, + ) + + if sub_count is None: + if not prompt: + err_console.print( + "Sub-subnet count not supplied with `--no-prompt` flag. Cannot continue." + ) + return False + sub_count = IntPrompt.ask( + f"\nEnter the [blue]number of sub-subnets[/blue] to set.[dim](Current raw count: {current_count})[/dim]" + ) + + if sub_count == current_count: + visible_count = max(sub_count - 1, 0) + message = ( + ":white_heavy_check_mark: " + f"[dark_sea_green3]Subnet {netuid} already has {visible_count} sub-subnet" + f"{'s' if visible_count != 1 else ''}.[/dark_sea_green3]" + ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": f"Subnet {netuid} already has {visible_count} sub-subnets.", + } + ) + ) + else: + console.print(message) + return True + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) + + logger.debug( + f"args:\nnetwork: {network}\nnetuid: {netuid}\nsub_count: {sub_count}\n" + ) + + result, err_msg = self._run_command( + sudo.sudo_set_sub_subnet_count( + wallet=wallet, + subtensor=subtensor, + netuid=netuid, + sub_count=sub_count, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + json_output=json_output, + ) + ) + + if json_output: + json_console.print(json.dumps({"success": result, "err_msg": err_msg})) + + return result + def sudo_set( self, network: Optional[list[str]] = Options.network, From 35d686f5eb53c19f0871cf1a223127c553b663c8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 16:31:31 -0700 Subject: [PATCH 06/39] update help panel --- bittensor_cli/src/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index ba96fe488..c31ba1d7d 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -698,6 +698,7 @@ class WalletValidationTypes(Enum): "CONFIG": "Subnet Configuration", "GOVERNANCE": "Governance", "TAKE": "Delegate take configuration", + "SUB": "Sub-subnet configuration", }, "SUBNETS": { "INFO": "Subnet Information", From 7e6296faa4fb9a192664f05e94472176c96b5cb2 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 16:33:53 -0700 Subject: [PATCH 07/39] add set_sub_subnet_count_extrinsic --- bittensor_cli/src/commands/sudo.py | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index e6ac31185..e5cf8443a 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -169,6 +169,51 @@ def requires_bool(metadata, param_name, pallet: str = DEFAULT_PALLET) -> bool: raise ValueError(f"{param_name} not found in pallet.") +async def set_sub_subnet_count_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + sub_count: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Sets the number of sub-subnets for a subnet via AdminUtils.""" + + unlock_result = unlock_key(wallet) + if not unlock_result.success: + return False, unlock_result.message + + substrate = subtensor.substrate + call_params = {"netuid": netuid, "subsub_count": sub_count} + + with console.status( + f":satellite: Setting sub-subnet count to [white]{sub_count}[/white] on " + f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] ...", + spinner="earth", + ): + call_ = await substrate.compose_call( + call_module=DEFAULT_PALLET, + call_function="sudo_set_subsubnet_count", + call_params=call_params, + ) + call = await substrate.compose_call( + call_module="Sudo", + call_function="sudo", + call_params={"call": call_}, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + return False, err_msg + + return True, "" + + async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", From c538ec7a1e62159cc21bae579ec2f6ddcc47734d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 16:34:06 -0700 Subject: [PATCH 08/39] add cmd --- bittensor_cli/src/commands/sudo.py | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index e5cf8443a..3ffdd07d2 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -658,6 +658,55 @@ async def set_take_extrinsic( # commands +async def sudo_set_sub_subnet_count( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + sub_count: int, + wait_for_inclusion: bool, + wait_for_finalization: bool, + json_output: bool, +) -> tuple[bool, str]: + """Set the number of sub-subnets for a subnet.""" + + if sub_count < 1: + err_msg = "Sub-subnet count must be greater than or equal to one." + if not json_output: + err_console.print(err_msg) + return False, err_msg + + if not await subtensor.subnet_exists(netuid): + err_msg = f"Subnet with netuid {netuid} does not exist." + if not json_output: + err_console.print(err_msg) + return False, err_msg + + if not Confirm.ask(f"Set sub-subnet count to {sub_count} for subnet {netuid}?"): + return False, "User cancelled" + + success, err_msg = await set_sub_subnet_count_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + sub_count=sub_count, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if json_output: + return success, err_msg + + if success: + console.print( + ":white_heavy_check_mark: " + f"[dark_sea_green3]Sub-subnet count set to {sub_count} for subnet {netuid}[/dark_sea_green3]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg + + async def sudo_set_hyperparameter( wallet: Wallet, subtensor: "SubtensorInterface", From 0225ada98643e9c7d13223fa8b6ffd051baf356b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 17:38:39 -0700 Subject: [PATCH 09/39] add btcli sub or btcli subsubnets --- bittensor_cli/cli.py | 99 ++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1ac30c194..4971c2127 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -650,6 +650,7 @@ class CLIManager: :var wallet_app: the Typer app as it relates to wallet commands :var stake_app: the Typer app as it relates to stake commands :var sudo_app: the Typer app as it relates to sudo commands + :var subsubnets_app: the Typer app as it relates to sub-subnet commands :var subnets_app: the Typer app as it relates to subnets commands :var subtensor: the `SubtensorInterface` object passed to the various commands that require it """ @@ -659,7 +660,7 @@ class CLIManager: config_app: typer.Typer wallet_app: typer.Typer sudo_app: typer.Typer - sudo_sub_app: typer.Typer + sub_subnets_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer utils_app: typer.Typer @@ -734,7 +735,8 @@ def __init__(self): self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) - self.sudo_sub_app = typer.Typer(epilog=_epilog) + self.sub_subnets_app = typer.Typer(epilog=_epilog) + self.sub_subnets_count = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) @@ -782,10 +784,24 @@ def __init__(self): no_args_is_help=True, ) self.app.add_typer(self.sudo_app, name="su", hidden=True, no_args_is_help=True) - self.sudo_app.add_typer( - self.sudo_sub_app, - name="sub", - short_help="Sub-subnet admin commands", + + # sub-subnet aliases + self.app.add_typer( + self.sub_subnets_app, + name="subsubnets", + short_help="Sub-subnet commands, alias: `sub`", + no_args_is_help=True, + ) + self.app.add_typer( + self.sub_subnets_app, name="subsubnet", hidden=True, no_args_is_help=True + ) + self.app.add_typer( + self.sub_subnets_app, name="sub", hidden=True, no_args_is_help=True + ) + self.sub_subnets_app.add_typer( + self.sub_subnets_count, + name="count", + short_help="Manage sub-subnet counts", no_args_is_help=True, ) @@ -947,10 +963,15 @@ def __init__(self): children_app.command("revoke")(self.stake_revoke_children) children_app.command("take")(self.stake_childkey_take) + # sub-subnet commands + self.sub_subnets_count.command( + "set", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["CONFIG"] + )(self.sub_subnets_count_set) + self.sub_subnets_count.command( + "get", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["CONFIG"] + )(self.sub_subnets_count_get) + # sudo commands - self.sudo_sub_app.command( - "count", rich_help_panel=HELP_PANELS["SUDO"]["SUB"] - )(self.sudo_sub_count) self.sudo_app.command("set", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_set ) @@ -980,9 +1001,6 @@ def __init__(self): self.subnets_app.command( "list", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_list) - self.subnets_app.command( - "count", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] - )(self.subnets_count) self.subnets_app.command( "burn-cost", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] )(self.subnets_burn_cost) @@ -5021,7 +5039,7 @@ def stake_childkey_take( json_console.print(json.dumps(output)) return results - def sudo_sub_count( + def sub_subnets_count_set( self, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, @@ -5046,7 +5064,7 @@ def sudo_sub_count( EXAMPLE - [green]$[/green] btcli sudo sub count --netuid 1 --count 2 + [green]$[/green] btcli subsubnets count set --netuid 1 --count 2 """ self.verbosity_handler(quiet, verbose, json_output) @@ -5073,7 +5091,7 @@ def sudo_sub_count( ) return False sub_count = IntPrompt.ask( - f"\nEnter the [blue]number of sub-subnets[/blue] to set.[dim](Current raw count: {current_count})[/dim]" + ("Enter the [blue]number of sub-subnets[/blue] to set") ) if sub_count == current_count: @@ -5124,6 +5142,32 @@ def sudo_sub_count( return result + def sub_subnets_count_get( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Display the number of sub-subnets registered under a subnet. + + EXAMPLE + + [green]$[/green] btcli sub count get --netuid 1 + """ + + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + return self._run_command( + subnets.count( + subtensor=subtensor, + netuid=netuid, + json_output=json_output, + ) + ) + def sudo_set( self, network: Optional[list[str]] = Options.network, @@ -5662,31 +5706,6 @@ def subnets_show( ) ) - def subnets_count( - self, - network: Optional[list[str]] = Options.network, - netuid: int = Options.netuid, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - json_output: bool = Options.json_output, - ): - """ - Show the number of sub-subnets registered under a subnet. - - EXAMPLE - - [green]$[/green] btcli subnets count --netuid 1 - """ - self.verbosity_handler(quiet, verbose, json_output) - subtensor = self.initialize_chain(network) - return self._run_command( - subnets.count( - subtensor=subtensor, - netuid=netuid, - json_output=json_output, - ) - ) - def subnets_burn_cost( self, network: Optional[list[str]] = Options.network, From 2028eca745e452bf5db0b8dcab338d32415dceb5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 17:38:58 -0700 Subject: [PATCH 10/39] update help disp + vocab --- bittensor_cli/src/__init__.py | 4 +++- bittensor_cli/src/commands/subnets/subnets.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index c31ba1d7d..2d46311c4 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -698,7 +698,9 @@ class WalletValidationTypes(Enum): "CONFIG": "Subnet Configuration", "GOVERNANCE": "Governance", "TAKE": "Delegate take configuration", - "SUB": "Sub-subnet configuration", + }, + "SUBSUBNETS": { + "CONFIG": "Sub-subnet Configuration", }, "SUBNETS": { "INFO": "Subnet Information", diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 709514df7..73957eb98 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1499,7 +1499,7 @@ async def count( sub_subnets_count = max(sub_subnet_count - 1, 0) console.print( f"[blue]Subnet {netuid}[/blue] currently has [blue]{sub_subnets_count}[/blue] sub-subnet" - f"{'s' if sub_subnets_count < 2 else ''}." + f"{'s' if sub_subnets_count != 1 else ''}." f"\n[dim](Raw count: {sub_subnet_count}; a value of 1 means there are no sub-subnets beyond the main subnet)[/dim]" ) From 42f45baab7d26ea41890c5248c50fa6917f064ef Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:28:57 -0700 Subject: [PATCH 11/39] update extrinsics --- .../src/bittensor/subtensor_interface.py | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f3da23931..73c4c3149 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1171,23 +1171,39 @@ async def get_subnet_hyperparameters( return SubnetHyperparameters.from_any(result) - async def get_sub_subnet_count( + async def get_subnet_mechanism_count( self, netuid: int, block_hash: Optional[str] = None ) -> int: """Return the number of sub-subnets that belong to the provided subnet.""" result = await self.query( module="SubtensorModule", - storage_function="SubsubnetCountCurrent", + storage_function="MechanismCountCurrent", params=[netuid], block_hash=block_hash, ) if result is None: - return 1 - + return 0 return int(result) + async def get_mechanism_emission_split( + self, netuid: int, block_hash: Optional[str] = None + ) -> list[int]: + """Return the emission split configured for the provided subnet.""" + + result = await self.query( + module="SubtensorModule", + storage_function="MechanismEmissionSplit", + params=[netuid], + block_hash=block_hash, + ) + + if not result: + return [] + + return [int(value) for value in result] + async def burn_cost(self, block_hash: Optional[str] = None) -> Optional[Balance]: result = await self.query_runtime_api( runtime_api="SubnetRegistrationRuntimeApi", From 4a0218901603565eb49b6558e54df942e19a603d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:32:24 -0700 Subject: [PATCH 12/39] update mechanism count --- bittensor_cli/src/commands/sudo.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 3ffdd07d2..5bad81b30 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -169,38 +169,33 @@ def requires_bool(metadata, param_name, pallet: str = DEFAULT_PALLET) -> bool: raise ValueError(f"{param_name} not found in pallet.") -async def set_sub_subnet_count_extrinsic( +async def set_mechanism_count_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, - sub_count: int, + mech_count: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ) -> tuple[bool, str]: - """Sets the number of sub-subnets for a subnet via AdminUtils.""" + """Sets the number of mechanisms for a subnet via AdminUtils.""" unlock_result = unlock_key(wallet) if not unlock_result.success: return False, unlock_result.message substrate = subtensor.substrate - call_params = {"netuid": netuid, "subsub_count": sub_count} + call_params = {"netuid": netuid, "mechanism_count": mech_count} with console.status( - f":satellite: Setting sub-subnet count to [white]{sub_count}[/white] on " + f":satellite: Setting mechanism count to [white]{mech_count}[/white] on " f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] ...", spinner="earth", ): - call_ = await substrate.compose_call( + call = await substrate.compose_call( call_module=DEFAULT_PALLET, - call_function="sudo_set_subsubnet_count", + call_function="sudo_set_mechanism_count", call_params=call_params, ) - call = await substrate.compose_call( - call_module="Sudo", - call_function="sudo", - call_params={"call": call_}, - ) success, err_msg = await subtensor.sign_and_send_extrinsic( call, wallet, @@ -214,6 +209,7 @@ async def set_sub_subnet_count_extrinsic( return True, "" + async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", @@ -684,11 +680,11 @@ async def sudo_set_sub_subnet_count( if not Confirm.ask(f"Set sub-subnet count to {sub_count} for subnet {netuid}?"): return False, "User cancelled" - success, err_msg = await set_sub_subnet_count_extrinsic( + success, err_msg = await set_mechanism_count_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, - sub_count=sub_count, + mech_count=sub_count, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -707,6 +703,7 @@ async def sudo_set_sub_subnet_count( return success, err_msg + async def sudo_set_hyperparameter( wallet: Wallet, subtensor: "SubtensorInterface", From 0e8c447ec1ad369bfd5fc7c6b4d6ce622f5f7de4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:33:06 -0700 Subject: [PATCH 13/39] sudo_set_mechanism_emission --- bittensor_cli/src/commands/sudo.py | 75 ++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 5bad81b30..b2c34344c 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -209,6 +209,43 @@ async def set_mechanism_count_extrinsic( return True, "" +async def set_mechanism_emission_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + split: list[int], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str]: + """Sets the emission split for a subnet's mechanisms via AdminUtils.""" + + unlock_result = unlock_key(wallet) + if not unlock_result.success: + return False, unlock_result.message + + substrate = subtensor.substrate + + with console.status( + f":satellite: Setting emission split for subnet {netuid}...", + spinner="earth", + ): + call = await substrate.compose_call( + call_module=DEFAULT_PALLET, + call_function="sudo_set_mechanism_emission_split", + call_params={"netuid": netuid, "maybe_split": split}, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + return False, err_msg + + return True, "" + async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", @@ -703,6 +740,44 @@ async def sudo_set_sub_subnet_count( return success, err_msg +async def sudo_set_mechanism_emission( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + split: list[int], + wait_for_inclusion: bool, + wait_for_finalization: bool, + json_output: bool, +) -> tuple[bool, str]: + """Set the emission split for mechanisms within a subnet.""" + + if not split: + err_msg = "Emission split must include at least one weight." + if not json_output: + err_console.print(err_msg) + return False, err_msg + + success, err_msg = await set_mechanism_emission_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + split=split, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if json_output: + return success, err_msg + + if success: + console.print( + ":white_heavy_check_mark: [dark_sea_green3]Updated mechanism emission split.[/dark_sea_green3]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg + async def sudo_set_hyperparameter( wallet: Wallet, From 257e1651b6eb449fd9ae02a73be9803b5ea7c89e Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:34:28 -0700 Subject: [PATCH 14/39] update mech_count --- bittensor_cli/src/commands/subnets/subnets.py | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 73957eb98..895cc9589 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1,5 +1,6 @@ import asyncio import json +import math import sqlite3 from typing import TYPE_CHECKING, Optional, cast @@ -19,6 +20,7 @@ from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph +from bittensor_cli.src.commands import sudo from bittensor_cli.src.commands.wallets import set_id, get_id from bittensor_cli.src.bittensor.utils import ( console, @@ -37,6 +39,7 @@ blocks_to_duration, json_console, get_hotkey_pub_ss58, + U16_MAX, ) if TYPE_CHECKING: @@ -1462,26 +1465,26 @@ async def count( return None with console.status( - f":satellite:Retrieving sub-subnet count from {subtensor.network}...", + f":satellite:Retrieving mechanism count from {subtensor.network}...", spinner="aesthetic", ): - sub_subnet_count = await subtensor.get_sub_subnet_count( + mechanism_count = await subtensor.get_subnet_mechanism_count( netuid, block_hash=block_hash ) - if not sub_subnet_count: + if not mechanism_count: if json_output: json_console.print( json.dumps( { "netuid": netuid, "count": None, - "error": "Failed to get sub-subnet count", + "error": "Failed to get mechanism count", } ) ) else: err_console.print( - "Subnet sub-subnet count: [red]Failed to get sub-subnet count[/red]" + "Subnet mechanism count: [red]Failed to get mechanism count[/red]" ) return None @@ -1490,20 +1493,21 @@ async def count( json.dumps( { "netuid": netuid, - "count": sub_subnet_count, + "count": mechanism_count, "error": "", } ) ) else: - sub_subnets_count = max(sub_subnet_count - 1, 0) + mechanism_count = max(mechanism_count - 1, 0) console.print( - f"[blue]Subnet {netuid}[/blue] currently has [blue]{sub_subnets_count}[/blue] sub-subnet" - f"{'s' if sub_subnets_count != 1 else ''}." - f"\n[dim](Raw count: {sub_subnet_count}; a value of 1 means there are no sub-subnets beyond the main subnet)[/dim]" + f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] sub-subnet" + f"{'s' if mechanism_count != 1 else ''}." + f"\n[dim](Raw count: {mechanism_count}; a value of 1 means there are no mechanisms beyond the main subnet)[/dim]" ) - return sub_subnet_count + return mechanism_count + async def burn_cost( From 73f65d59d7cd74eb214a3f5a64dec7964db58db1 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:34:57 -0700 Subject: [PATCH 15/39] normalize_emission_weights --- bittensor_cli/src/commands/subnets/subnets.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 895cc9589..dd8936000 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1509,6 +1509,30 @@ async def count( return mechanism_count +def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[float]]: + total = sum(values) + if total <= 0: + raise ValueError("Sum of emission weights must be greater than zero.") + + fractions = [value / total for value in values] + scaled = [fraction * U16_MAX for fraction in fractions] + base = [math.floor(value) for value in scaled] + remainder = int(U16_MAX - sum(base)) + + if remainder > 0: + fractional_parts = [value - math.floor(value) for value in scaled] + order = sorted( + range(len(base)), key=lambda idx: fractional_parts[idx], reverse=True + ) + idx = 0 + length = len(order) + while remainder > 0 and length > 0: + base[order[idx % length]] += 1 + remainder -= 1 + idx += 1 + + return [int(value) for value in base], fractions + async def burn_cost( subtensor: "SubtensorInterface", json_output: bool = False From 9a63ddef9642363162a3fbd88ba64115a984c053 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:35:18 -0700 Subject: [PATCH 16/39] get_emission_split --- bittensor_cli/src/commands/subnets/subnets.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index dd8936000..c3601fe64 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1509,6 +1509,106 @@ async def count( return mechanism_count +async def get_emission_split( + subtensor: "SubtensorInterface", + netuid: int, + json_output: bool = False, +) -> Optional[dict]: + """Display the emission split across mechanisms for a subnet.""" + + count = await subtensor.get_subnet_mechanism_count(netuid) + if count == 1: + console.print(f"Subnet {netuid} does not currently contain any mechanisms.") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": "Subnet does not contain any mechanisms.", + } + ) + ) + return None + + emission_split = await subtensor.get_mechanism_emission_split(netuid) or [] + print(f"Emission split: {emission_split}") + + even_distribution = False + total_sum = sum(emission_split) + if total_sum == 0 and count > 0: + even_distribution = True + base, remainder = divmod(U16_MAX, count) + emission_split = [base for _ in range(count)] + if remainder: + emission_split[0] += remainder + total_sum = sum(emission_split) + + emission_percentages = ( + [round((value / total_sum) * 100, 6) for value in emission_split] + if total_sum > 0 + else [0.0 for _ in emission_split] + ) + + data = { + "netuid": netuid, + "raw_count": count, + "visible_count": max(count - 1, 0), + "split": emission_split if count else [], + "percentages": emission_percentages if count else [], + "even_distribution": even_distribution, + } + + if json_output: + json_console.print(json.dumps(data)) + else: + table = Table( + Column( + "[bold white]Mechanism Index[/]", + justify="center", + style=COLOR_PALETTE.G.NETUID, + ), + Column( + "[bold white]Weight (u16)[/]", + justify="right", + style=COLOR_PALETTE.STAKE.STAKE_ALPHA, + ), + Column( + "[bold white]Share (%)[/]", + justify="right", + style=COLOR_PALETTE.POOLS.EMISSION, + ), + title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet {netuid} emission split[/]", + box=box.SIMPLE, + show_footer=True, + border_style="bright_black", + ) + + total_weight = sum(emission_split) + share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 + + for idx, value in enumerate(emission_split): + share = ( + emission_percentages[idx] if idx < len(emission_percentages) else 0.0 + ) + table.add_row(str(idx), str(value), f"{share:.6f}") + + table.add_row( + "[dim]Total[/dim]", + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", + f"[{COLOR_PALETTE.POOLS.EMISSION}]{share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", + ) + + console.print(table) + footer = "[dim]Totals are expressed as a fraction of 65535 (U16_MAX).[/dim]" + if even_distribution: + footer += ( + "\n[dim]No custom split found; displaying an even distribution.[/dim]" + ) + console.print(footer) + + return data + + def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[float]]: total = sum(values) if total <= 0: From ee80655b12e3f858842d1161c1eedc4ff476738a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:35:34 -0700 Subject: [PATCH 17/39] set_emission_split --- bittensor_cli/src/commands/subnets/subnets.py | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index c3601fe64..46c719ad3 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1609,6 +1609,206 @@ async def get_emission_split( return data +async def set_emission_split( + subtensor: "SubtensorInterface", + wallet: Wallet, + netuid: int, + new_emission_split: Optional[str], + wait_for_inclusion: bool, + wait_for_finalization: bool, + prompt: bool, + json_output: bool, +) -> bool: + """Set the emission split across mechanisms for a subnet.""" + + mech_count, existing_split = await asyncio.gather( + subtensor.get_subnet_mechanism_count(netuid), + subtensor.get_mechanism_emission_split(netuid), + ) + + if mech_count == 0: + message = ( + f"Subnet {netuid} does not currently contain any mechanisms to configure." + ) + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + if not json_output: + await get_emission_split( + subtensor=subtensor, + netuid=netuid, + json_output=False, + ) + + existing_split = [int(value) for value in existing_split] + if len(existing_split) < mech_count: + existing_split.extend([0] * (mech_count - len(existing_split))) + + if new_emission_split is not None: + try: + weights = [ + float(item.strip()) + for item in new_emission_split.split(",") + if item.strip() != "" + ] + except ValueError: + message = ( + "Invalid `--split` values. Provide a comma-separated list of numbers." + ) + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + else: + if not prompt: + err_console.print( + "Split values not supplied with `--no-prompt` flag. Cannot continue." + ) + return False + + weights: list[float] = [] + total_existing = sum(existing_split) or 1 + console.print("\n[dim]You either provide U16 values or percentages.[/dim]") + for idx in range(mech_count): + current_value = existing_split[idx] + current_percent = ( + (current_value / total_existing) * 100 if total_existing else 0 + ) + label = ( + "[blue]Main Mechanism (1)[/blue]" + if idx == 0 + else f"[blue]Mechanism {idx + 1}[/blue]" + ) + response = Prompt.ask( + ( + f"Relative weight for {label} " + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}](current: {current_value} ~ {current_percent:.2f}%)[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]" + ) + ) + try: + weights.append(float(response)) + except ValueError: + err_console.print("Invalid number provided. Aborting.") + return False + + if len(weights) != mech_count: + message = f"Expected {mech_count} weight values, received {len(weights)}." + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + if any(value < 0 for value in weights): + message = "Weights must be non-negative." + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + try: + normalized_weights, fractions = _normalize_emission_weights(weights) + except ValueError as exc: + message = str(exc) + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + if normalized_weights == existing_split: + message = ":white_heavy_check_mark: [dark_sea_green3]Emission split unchanged.[/dark_sea_green3]" + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": "Emission split unchanged.", + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + } + ) + ) + else: + console.print(message) + return True + + if not json_output: + table = Table( + Column( + "[bold white]Mechanism Index[/]", + justify="center", + style=COLOR_PALETTE.G.NETUID, + ), + Column( + "[bold white]Weight (u16)[/]", + justify="right", + style=COLOR_PALETTE.STAKE.STAKE_ALPHA, + ), + Column( + "[bold white]Share (%)[/]", + justify="right", + style=COLOR_PALETTE.POOLS.EMISSION, + ), + title=( + f"\n[{COLOR_PALETTE.G.HEADER}]Proposed emission split[/{COLOR_PALETTE.G.HEADER}]\n" + f"[{COLOR_PALETTE.G.SUBHEAD}]Subnet {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" + ), + box=box.SIMPLE, + show_footer=True, + border_style="bright_black", + ) + + total_weight = sum(normalized_weights) + total_share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 + + for idx, weight in enumerate(normalized_weights): + share_percent = fractions[idx] * 100 if idx < len(fractions) else 0.0 + table.add_row(str(idx), str(weight), f"{share_percent:.6f}") + + table.add_row("", "", "", style="dim") + table.add_row( + "[dim]Total[/dim]", + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", + f"[{COLOR_PALETTE.POOLS.EMISSION}]{total_share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", + ) + + console.print(table) + + if not Confirm.ask("Proceed with these emission weights?", default=True): + console.print(":cross_mark: Aborted!") + return False + + success, err_msg = await sudo.sudo_set_mechanism_emission( + wallet=wallet, + subtensor=subtensor, + netuid=netuid, + split=normalized_weights, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + json_output=json_output, + ) + + if json_output: + json_console.print( + json.dumps( + { + "success": success, + "err_msg": err_msg, + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + } + ) + ) + + return success + + def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[float]]: total = sum(values) if total <= 0: From 8dc0f019a91440635066a4f8b7a276e464e8065f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 12:36:26 -0700 Subject: [PATCH 18/39] update cli cmds --- bittensor_cli/cli.py | 134 ++++++++++++++++++++++++++-------- bittensor_cli/src/__init__.py | 1 + 2 files changed, 104 insertions(+), 31 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4971c2127..a53758ed6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -660,7 +660,7 @@ class CLIManager: config_app: typer.Typer wallet_app: typer.Typer sudo_app: typer.Typer - sub_subnets_app: typer.Typer + subsubnets_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer utils_app: typer.Typer @@ -735,8 +735,9 @@ def __init__(self): self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) - self.sub_subnets_app = typer.Typer(epilog=_epilog) - self.sub_subnets_count = typer.Typer(epilog=_epilog) + self.subsubnets_app = typer.Typer(epilog=_epilog) + self.subsubnets_count_app = typer.Typer(epilog=_epilog) + self.subsubnets_emission_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) @@ -787,23 +788,29 @@ def __init__(self): # sub-subnet aliases self.app.add_typer( - self.sub_subnets_app, + self.subsubnets_app, name="subsubnets", short_help="Sub-subnet commands, alias: `sub`", no_args_is_help=True, ) self.app.add_typer( - self.sub_subnets_app, name="subsubnet", hidden=True, no_args_is_help=True + self.subsubnets_app, name="subsubnet", hidden=True, no_args_is_help=True ) self.app.add_typer( - self.sub_subnets_app, name="sub", hidden=True, no_args_is_help=True + self.subsubnets_app, name="sub", hidden=True, no_args_is_help=True ) - self.sub_subnets_app.add_typer( - self.sub_subnets_count, + self.subsubnets_app.add_typer( + self.subsubnets_count_app, name="count", short_help="Manage sub-subnet counts", no_args_is_help=True, ) + self.subsubnets_app.add_typer( + self.subsubnets_emission_app, + name="emission", + short_help="Manage sub-subnet emission splits", + no_args_is_help=True, + ) # subnets aliases self.app.add_typer( @@ -964,12 +971,18 @@ def __init__(self): children_app.command("take")(self.stake_childkey_take) # sub-subnet commands - self.sub_subnets_count.command( + self.subsubnets_count_app.command( "set", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["CONFIG"] - )(self.sub_subnets_count_set) - self.sub_subnets_count.command( + )(self.subsubnets_count_set) + self.subsubnets_count_app.command( "get", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["CONFIG"] - )(self.sub_subnets_count_get) + )(self.subsubnets_count_get) + self.subsubnets_emission_app.command( + "set", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["EMISSION"] + )(self.subsubnets_emission_set) + self.subsubnets_emission_app.command( + "get", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["EMISSION"] + )(self.subsubnets_emission_get) # sudo commands self.sudo_app.command("set", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( @@ -5039,7 +5052,7 @@ def stake_childkey_take( json_console.print(json.dumps(output)) return results - def sub_subnets_count_set( + def subsubnets_count_set( self, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, @@ -5059,16 +5072,11 @@ def sub_subnets_count_set( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """ - Set the number of sub-subnets registered under a subnet. - - EXAMPLE - - [green]$[/green] btcli subsubnets count set --netuid 1 --count 2 - """ + """Set the number of sub-subnets registered under a subnet.""" self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) + if not json_output: current_count = self._run_command( subnets.count( @@ -5080,7 +5088,7 @@ def sub_subnets_count_set( ) else: current_count = self._run_command( - subtensor.get_sub_subnet_count(netuid), + subtensor.get_subnet_mechanism_count(netuid), exit_early=False, ) @@ -5090,9 +5098,11 @@ def sub_subnets_count_set( "Sub-subnet count not supplied with `--no-prompt` flag. Cannot continue." ) return False - sub_count = IntPrompt.ask( - ("Enter the [blue]number of sub-subnets[/blue] to set") + prompt_text = ( + "Enter the [blue]number of sub-subnets[/blue] to set.\n" + f"[dim](Current raw count: {current_count}; a value of 1 means there are no sub-subnets beyond the root.)[/dim]" ) + sub_count = IntPrompt.ask(prompt_text) if sub_count == current_count: visible_count = max(sub_count - 1, 0) @@ -5122,7 +5132,10 @@ def sub_subnets_count_set( ) logger.debug( - f"args:\nnetwork: {network}\nnetuid: {netuid}\nsub_count: {sub_count}\n" + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"sub_count: {sub_count}\n" ) result, err_msg = self._run_command( @@ -5142,7 +5155,7 @@ def sub_subnets_count_set( return result - def sub_subnets_count_get( + def subsubnets_count_get( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, @@ -5150,24 +5163,83 @@ def sub_subnets_count_get( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """ - Display the number of sub-subnets registered under a subnet. + """Display the number of sub-subnets registered under a subnet.""" - EXAMPLE + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + return self._run_command( + subnets.count( + subtensor=subtensor, + netuid=netuid, + json_output=json_output, + ) + ) - [green]$[/green] btcli sub count get --netuid 1 - """ + def subsubnets_emission_set( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: int = Options.netuid, + split: Optional[str] = typer.Option( + None, + "--split", + help="Comma-separated relative weights for each sub-subnet (will be normalised).", + ), + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Set the emission split across sub-subnets for a subnet.""" self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) + return self._run_command( - subnets.count( + subnets.set_emission_split( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + new_emission_split=split, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + json_output=json_output, + ) + ) + + + def subsubnets_emission_get( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """Display the emission split across sub-subnets for a subnet.""" + + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + return self._run_command( + subnets.get_emission_split( subtensor=subtensor, netuid=netuid, json_output=json_output, ) ) + def sudo_set( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 2d46311c4..cee05cd03 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -701,6 +701,7 @@ class WalletValidationTypes(Enum): }, "SUBSUBNETS": { "CONFIG": "Sub-subnet Configuration", + "EMISSION": "Sub-subnet Emission", }, "SUBNETS": { "INFO": "Subnet Information", From 3de8f23ea41d1f8c8ad99c88e73f6b19961cace7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 13:05:01 -0700 Subject: [PATCH 19/39] update to mechanisms --- bittensor_cli/src/__init__.py | 6 +++--- .../src/bittensor/subtensor_interface.py | 2 +- bittensor_cli/src/commands/subnets/subnets.py | 7 +++---- bittensor_cli/src/commands/sudo.py | 16 ++++++++-------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index cee05cd03..cee5016a5 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -699,9 +699,9 @@ class WalletValidationTypes(Enum): "GOVERNANCE": "Governance", "TAKE": "Delegate take configuration", }, - "SUBSUBNETS": { - "CONFIG": "Sub-subnet Configuration", - "EMISSION": "Sub-subnet Emission", + "MECHANISMS": { + "CONFIG": "Mechanism Configuration", + "EMISSION": "Mechanism Emission", }, "SUBNETS": { "INFO": "Subnet Information", diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 73c4c3149..df101169a 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1174,7 +1174,7 @@ async def get_subnet_hyperparameters( async def get_subnet_mechanism_count( self, netuid: int, block_hash: Optional[str] = None ) -> int: - """Return the number of sub-subnets that belong to the provided subnet.""" + """Return the number of mechanisms that belong to the provided subnet.""" result = await self.query( module="SubtensorModule", diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 46c719ad3..44efd5afc 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1451,7 +1451,7 @@ async def count( netuid: int, json_output: bool = False, ) -> Optional[int]: - """Display how many sub-subnets exist for the provided subnet.""" + """Display how many mechanisms exist for the provided subnet.""" block_hash = await subtensor.substrate.get_chain_head() if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): @@ -1499,11 +1499,10 @@ async def count( ) ) else: - mechanism_count = max(mechanism_count - 1, 0) console.print( - f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] sub-subnet" + f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] mechanism" f"{'s' if mechanism_count != 1 else ''}." - f"\n[dim](Raw count: {mechanism_count}; a value of 1 means there are no mechanisms beyond the main subnet)[/dim]" + f"\n[dim](Tip: 1 mechanism means there are no mechanisms beyond the main subnet)[/dim]" ) return mechanism_count diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index b2c34344c..70c0f4eaf 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -691,19 +691,19 @@ async def set_take_extrinsic( # commands -async def sudo_set_sub_subnet_count( +async def sudo_set_mechanism_count( wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, - sub_count: int, + mechanism_count: int, wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, ) -> tuple[bool, str]: - """Set the number of sub-subnets for a subnet.""" + """Set the number of mechanisms for a subnet.""" - if sub_count < 1: - err_msg = "Sub-subnet count must be greater than or equal to one." + if mechanism_count < 1: + err_msg = "Mechanism count must be greater than or equal to one." if not json_output: err_console.print(err_msg) return False, err_msg @@ -714,14 +714,14 @@ async def sudo_set_sub_subnet_count( err_console.print(err_msg) return False, err_msg - if not Confirm.ask(f"Set sub-subnet count to {sub_count} for subnet {netuid}?"): + if not Confirm.ask(f"Set mechanism count to {mechanism_count} for subnet {netuid}?"): return False, "User cancelled" success, err_msg = await set_mechanism_count_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, - mech_count=sub_count, + mech_count=mechanism_count, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -732,7 +732,7 @@ async def sudo_set_sub_subnet_count( if success: console.print( ":white_heavy_check_mark: " - f"[dark_sea_green3]Sub-subnet count set to {sub_count} for subnet {netuid}[/dark_sea_green3]" + f"[dark_sea_green3]Mechanism count set to {mechanism_count} for subnet {netuid}[/dark_sea_green3]" ) else: err_console.print(f":cross_mark: [red]{err_msg}[/red]") From dc86c6523e9813b46e74410098b96c5c017c69fa Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 13:11:16 -0700 Subject: [PATCH 20/39] add mech app --- bittensor_cli/cli.py | 134 +++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a53758ed6..53c92011c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -650,7 +650,7 @@ class CLIManager: :var wallet_app: the Typer app as it relates to wallet commands :var stake_app: the Typer app as it relates to stake commands :var sudo_app: the Typer app as it relates to sudo commands - :var subsubnets_app: the Typer app as it relates to sub-subnet commands + :var subnet_mechanisms_app: the Typer app for subnet mechanism commands :var subnets_app: the Typer app as it relates to subnets commands :var subtensor: the `SubtensorInterface` object passed to the various commands that require it """ @@ -660,8 +660,8 @@ class CLIManager: config_app: typer.Typer wallet_app: typer.Typer sudo_app: typer.Typer - subsubnets_app: typer.Typer subnets_app: typer.Typer + subnet_mechanisms_app: typer.Typer weights_app: typer.Typer utils_app: typer.Typer view_app: typer.Typer @@ -735,10 +735,10 @@ def __init__(self): self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) - self.subsubnets_app = typer.Typer(epilog=_epilog) - self.subsubnets_count_app = typer.Typer(epilog=_epilog) - self.subsubnets_emission_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) + self.subnet_mechanisms_app = typer.Typer(epilog=_epilog) + self.subnets_mechanisms_count_app = typer.Typer(epilog=_epilog) + self.subnets_mechanisms_emission_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) @@ -786,44 +786,44 @@ def __init__(self): ) self.app.add_typer(self.sudo_app, name="su", hidden=True, no_args_is_help=True) - # sub-subnet aliases + # subnets aliases self.app.add_typer( - self.subsubnets_app, - name="subsubnets", - short_help="Sub-subnet commands, alias: `sub`", + self.subnets_app, + name="subnets", + short_help="Subnets commands, alias: `s`, `subnet`", no_args_is_help=True, ) self.app.add_typer( - self.subsubnets_app, name="subsubnet", hidden=True, no_args_is_help=True + self.subnets_app, name="s", hidden=True, no_args_is_help=True ) self.app.add_typer( - self.subsubnets_app, name="sub", hidden=True, no_args_is_help=True + self.subnets_app, name="subnet", hidden=True, no_args_is_help=True ) - self.subsubnets_app.add_typer( - self.subsubnets_count_app, - name="count", - short_help="Manage sub-subnet counts", + + # subnet mechanisms aliases + self.subnets_app.add_typer( + self.subnet_mechanisms_app, + name="mechanisms", + short_help="Subnet mechanism commands, alias: `mech`", no_args_is_help=True, ) - self.subsubnets_app.add_typer( - self.subsubnets_emission_app, - name="emission", - short_help="Manage sub-subnet emission splits", + self.subnets_app.add_typer( + self.subnet_mechanisms_app, + name="mech", + hidden=True, no_args_is_help=True, ) - - # subnets aliases - self.app.add_typer( - self.subnets_app, - name="subnets", - short_help="Subnets commands, alias: `s`, `subnet`", + self.subnet_mechanisms_app.add_typer( + self.subnets_mechanisms_count_app, + name="count", + short_help="Manage mechanism instances", no_args_is_help=True, ) - self.app.add_typer( - self.subnets_app, name="s", hidden=True, no_args_is_help=True - ) - self.app.add_typer( - self.subnets_app, name="subnet", hidden=True, no_args_is_help=True + self.subnet_mechanisms_app.add_typer( + self.subnets_mechanisms_emission_app, + name="emission", + short_help="Manage mechanism emission splits", + no_args_is_help=True, ) # weights aliases @@ -970,19 +970,19 @@ def __init__(self): children_app.command("revoke")(self.stake_revoke_children) children_app.command("take")(self.stake_childkey_take) - # sub-subnet commands - self.subsubnets_count_app.command( - "set", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["CONFIG"] - )(self.subsubnets_count_set) - self.subsubnets_count_app.command( - "get", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["CONFIG"] - )(self.subsubnets_count_get) - self.subsubnets_emission_app.command( - "set", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["EMISSION"] - )(self.subsubnets_emission_set) - self.subsubnets_emission_app.command( - "get", rich_help_panel=HELP_PANELS["SUBSUBNETS"]["EMISSION"] - )(self.subsubnets_emission_get) + # subnet mechanism commands + self.subnets_mechanisms_count_app.command( + "set", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] + )(self.mechanism_count_set) + self.subnets_mechanisms_count_app.command( + "get", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] + )(self.mechanism_count_get) + self.subnets_mechanisms_emission_app.command( + "set", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] + )(self.mechanism_emission_set) + self.subnets_mechanisms_emission_app.command( + "get", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] + )(self.mechanism_emission_get) # sudo commands self.sudo_app.command("set", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( @@ -5052,18 +5052,18 @@ def stake_childkey_take( json_console.print(json.dumps(output)) return results - def subsubnets_count_set( + def mechanism_count_set( self, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, netuid: int = Options.netuid, - sub_count: Optional[int] = typer.Option( + mechanism_count: Optional[int] = typer.Option( None, "--count", - "--sub-count", - help="Number of sub-subnets to set for the subnet.", + "--mech-count", + help="Number of mechanisms to set for the subnet.", ), wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -5072,7 +5072,7 @@ def subsubnets_count_set( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Set the number of sub-subnets registered under a subnet.""" + """Set the number of mechanisms registered under a subnet.""" self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5092,23 +5092,23 @@ def subsubnets_count_set( exit_early=False, ) - if sub_count is None: + if mechanism_count is None: if not prompt: err_console.print( - "Sub-subnet count not supplied with `--no-prompt` flag. Cannot continue." + "Mechanism count not supplied with `--no-prompt` flag. Cannot continue." ) return False prompt_text = ( - "Enter the [blue]number of sub-subnets[/blue] to set.\n" - f"[dim](Current raw count: {current_count}; a value of 1 means there are no sub-subnets beyond the root.)[/dim]" + "Enter the [blue]number of mechanisms[/blue] to set.\n" + f"[dim](Current raw count: {current_count}; a value of 1 means there are no mechanisms beyond the root.)[/dim]" ) - sub_count = IntPrompt.ask(prompt_text) + mechanism_count = IntPrompt.ask(prompt_text) - if sub_count == current_count: - visible_count = max(sub_count - 1, 0) + if mechanism_count == current_count: + visible_count = max(mechanism_count - 1, 0) message = ( ":white_heavy_check_mark: " - f"[dark_sea_green3]Subnet {netuid} already has {visible_count} sub-subnet" + f"[dark_sea_green3]Subnet {netuid} already has {visible_count} mechanism" f"{'s' if visible_count != 1 else ''}.[/dark_sea_green3]" ) if json_output: @@ -5116,7 +5116,7 @@ def subsubnets_count_set( json.dumps( { "success": True, - "message": f"Subnet {netuid} already has {visible_count} sub-subnets.", + "message": f"Subnet {netuid} already has {visible_count} mechanisms.", } ) ) @@ -5135,15 +5135,15 @@ def subsubnets_count_set( "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" - f"sub_count: {sub_count}\n" + f"mechanism_count: {mechanism_count}\n" ) result, err_msg = self._run_command( - sudo.sudo_set_sub_subnet_count( + sudo.sudo_set_mechanism_count( wallet=wallet, subtensor=subtensor, netuid=netuid, - sub_count=sub_count, + mechanism_count=mechanism_count, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, json_output=json_output, @@ -5155,7 +5155,7 @@ def subsubnets_count_set( return result - def subsubnets_count_get( + def mechanism_count_get( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, @@ -5163,7 +5163,7 @@ def subsubnets_count_get( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Display the number of sub-subnets registered under a subnet.""" + """Display the number of mechanisms registered under a subnet.""" self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5175,7 +5175,7 @@ def subsubnets_count_get( ) ) - def subsubnets_emission_set( + def mechanism_emission_set( self, network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, @@ -5185,7 +5185,7 @@ def subsubnets_emission_set( split: Optional[str] = typer.Option( None, "--split", - help="Comma-separated relative weights for each sub-subnet (will be normalised).", + help="Comma-separated relative weights for each mechanism (normalised automatically).", ), wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -5194,7 +5194,7 @@ def subsubnets_emission_set( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Set the emission split across sub-subnets for a subnet.""" + """Set the emission split across mechanisms for a subnet.""" self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5219,7 +5219,7 @@ def subsubnets_emission_set( ) - def subsubnets_emission_get( + def mechanism_emission_get( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, @@ -5227,7 +5227,7 @@ def subsubnets_emission_get( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Display the emission split across sub-subnets for a subnet.""" + """Display the emission split across mechanisms for a subnet.""" self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) From 0cd40b6ce5a1ec766a2a6e42f7736692f41bb321 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 13:46:32 -0700 Subject: [PATCH 21/39] remove from subnets --- bittensor_cli/src/commands/subnets/subnets.py | 390 ------------------ 1 file changed, 390 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 44efd5afc..d8571f3f6 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1,6 +1,5 @@ import asyncio import json -import math import sqlite3 from typing import TYPE_CHECKING, Optional, cast @@ -20,7 +19,6 @@ from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph -from bittensor_cli.src.commands import sudo from bittensor_cli.src.commands.wallets import set_id, get_id from bittensor_cli.src.bittensor.utils import ( console, @@ -39,7 +37,6 @@ blocks_to_duration, json_console, get_hotkey_pub_ss58, - U16_MAX, ) if TYPE_CHECKING: @@ -1446,393 +1443,6 @@ async def show_subnet(netuid_: int): return result -async def count( - subtensor: "SubtensorInterface", - netuid: int, - json_output: bool = False, -) -> Optional[int]: - """Display how many mechanisms exist for the provided subnet.""" - - block_hash = await subtensor.substrate.get_chain_head() - if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): - err_console.print(f"[red]Subnet {netuid} does not exist[/red]") - if json_output: - json_console.print( - json.dumps( - {"success": False, "error": f"Subnet {netuid} does not exist"} - ) - ) - return None - - with console.status( - f":satellite:Retrieving mechanism count from {subtensor.network}...", - spinner="aesthetic", - ): - mechanism_count = await subtensor.get_subnet_mechanism_count( - netuid, block_hash=block_hash - ) - if not mechanism_count: - if json_output: - json_console.print( - json.dumps( - { - "netuid": netuid, - "count": None, - "error": "Failed to get mechanism count", - } - ) - ) - else: - err_console.print( - "Subnet mechanism count: [red]Failed to get mechanism count[/red]" - ) - return None - - if json_output: - json_console.print( - json.dumps( - { - "netuid": netuid, - "count": mechanism_count, - "error": "", - } - ) - ) - else: - console.print( - f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] mechanism" - f"{'s' if mechanism_count != 1 else ''}." - f"\n[dim](Tip: 1 mechanism means there are no mechanisms beyond the main subnet)[/dim]" - ) - - return mechanism_count - - -async def get_emission_split( - subtensor: "SubtensorInterface", - netuid: int, - json_output: bool = False, -) -> Optional[dict]: - """Display the emission split across mechanisms for a subnet.""" - - count = await subtensor.get_subnet_mechanism_count(netuid) - if count == 1: - console.print(f"Subnet {netuid} does not currently contain any mechanisms.") - if json_output: - json_console.print( - json.dumps( - { - "success": False, - "error": "Subnet does not contain any mechanisms.", - } - ) - ) - return None - - emission_split = await subtensor.get_mechanism_emission_split(netuid) or [] - print(f"Emission split: {emission_split}") - - even_distribution = False - total_sum = sum(emission_split) - if total_sum == 0 and count > 0: - even_distribution = True - base, remainder = divmod(U16_MAX, count) - emission_split = [base for _ in range(count)] - if remainder: - emission_split[0] += remainder - total_sum = sum(emission_split) - - emission_percentages = ( - [round((value / total_sum) * 100, 6) for value in emission_split] - if total_sum > 0 - else [0.0 for _ in emission_split] - ) - - data = { - "netuid": netuid, - "raw_count": count, - "visible_count": max(count - 1, 0), - "split": emission_split if count else [], - "percentages": emission_percentages if count else [], - "even_distribution": even_distribution, - } - - if json_output: - json_console.print(json.dumps(data)) - else: - table = Table( - Column( - "[bold white]Mechanism Index[/]", - justify="center", - style=COLOR_PALETTE.G.NETUID, - ), - Column( - "[bold white]Weight (u16)[/]", - justify="right", - style=COLOR_PALETTE.STAKE.STAKE_ALPHA, - ), - Column( - "[bold white]Share (%)[/]", - justify="right", - style=COLOR_PALETTE.POOLS.EMISSION, - ), - title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet {netuid} emission split[/]", - box=box.SIMPLE, - show_footer=True, - border_style="bright_black", - ) - - total_weight = sum(emission_split) - share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 - - for idx, value in enumerate(emission_split): - share = ( - emission_percentages[idx] if idx < len(emission_percentages) else 0.0 - ) - table.add_row(str(idx), str(value), f"{share:.6f}") - - table.add_row( - "[dim]Total[/dim]", - f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", - f"[{COLOR_PALETTE.POOLS.EMISSION}]{share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", - ) - - console.print(table) - footer = "[dim]Totals are expressed as a fraction of 65535 (U16_MAX).[/dim]" - if even_distribution: - footer += ( - "\n[dim]No custom split found; displaying an even distribution.[/dim]" - ) - console.print(footer) - - return data - - -async def set_emission_split( - subtensor: "SubtensorInterface", - wallet: Wallet, - netuid: int, - new_emission_split: Optional[str], - wait_for_inclusion: bool, - wait_for_finalization: bool, - prompt: bool, - json_output: bool, -) -> bool: - """Set the emission split across mechanisms for a subnet.""" - - mech_count, existing_split = await asyncio.gather( - subtensor.get_subnet_mechanism_count(netuid), - subtensor.get_mechanism_emission_split(netuid), - ) - - if mech_count == 0: - message = ( - f"Subnet {netuid} does not currently contain any mechanisms to configure." - ) - if json_output: - json_console.print(json.dumps({"success": False, "error": message})) - else: - err_console.print(message) - return False - - if not json_output: - await get_emission_split( - subtensor=subtensor, - netuid=netuid, - json_output=False, - ) - - existing_split = [int(value) for value in existing_split] - if len(existing_split) < mech_count: - existing_split.extend([0] * (mech_count - len(existing_split))) - - if new_emission_split is not None: - try: - weights = [ - float(item.strip()) - for item in new_emission_split.split(",") - if item.strip() != "" - ] - except ValueError: - message = ( - "Invalid `--split` values. Provide a comma-separated list of numbers." - ) - if json_output: - json_console.print(json.dumps({"success": False, "error": message})) - else: - err_console.print(message) - return False - else: - if not prompt: - err_console.print( - "Split values not supplied with `--no-prompt` flag. Cannot continue." - ) - return False - - weights: list[float] = [] - total_existing = sum(existing_split) or 1 - console.print("\n[dim]You either provide U16 values or percentages.[/dim]") - for idx in range(mech_count): - current_value = existing_split[idx] - current_percent = ( - (current_value / total_existing) * 100 if total_existing else 0 - ) - label = ( - "[blue]Main Mechanism (1)[/blue]" - if idx == 0 - else f"[blue]Mechanism {idx + 1}[/blue]" - ) - response = Prompt.ask( - ( - f"Relative weight for {label} " - f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}](current: {current_value} ~ {current_percent:.2f}%)[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]" - ) - ) - try: - weights.append(float(response)) - except ValueError: - err_console.print("Invalid number provided. Aborting.") - return False - - if len(weights) != mech_count: - message = f"Expected {mech_count} weight values, received {len(weights)}." - if json_output: - json_console.print(json.dumps({"success": False, "error": message})) - else: - err_console.print(message) - return False - - if any(value < 0 for value in weights): - message = "Weights must be non-negative." - if json_output: - json_console.print(json.dumps({"success": False, "error": message})) - else: - err_console.print(message) - return False - - try: - normalized_weights, fractions = _normalize_emission_weights(weights) - except ValueError as exc: - message = str(exc) - if json_output: - json_console.print(json.dumps({"success": False, "error": message})) - else: - err_console.print(message) - return False - - if normalized_weights == existing_split: - message = ":white_heavy_check_mark: [dark_sea_green3]Emission split unchanged.[/dark_sea_green3]" - if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": "Emission split unchanged.", - "split": normalized_weights, - "percentages": [round(value * 100, 6) for value in fractions], - } - ) - ) - else: - console.print(message) - return True - - if not json_output: - table = Table( - Column( - "[bold white]Mechanism Index[/]", - justify="center", - style=COLOR_PALETTE.G.NETUID, - ), - Column( - "[bold white]Weight (u16)[/]", - justify="right", - style=COLOR_PALETTE.STAKE.STAKE_ALPHA, - ), - Column( - "[bold white]Share (%)[/]", - justify="right", - style=COLOR_PALETTE.POOLS.EMISSION, - ), - title=( - f"\n[{COLOR_PALETTE.G.HEADER}]Proposed emission split[/{COLOR_PALETTE.G.HEADER}]\n" - f"[{COLOR_PALETTE.G.SUBHEAD}]Subnet {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" - ), - box=box.SIMPLE, - show_footer=True, - border_style="bright_black", - ) - - total_weight = sum(normalized_weights) - total_share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 - - for idx, weight in enumerate(normalized_weights): - share_percent = fractions[idx] * 100 if idx < len(fractions) else 0.0 - table.add_row(str(idx), str(weight), f"{share_percent:.6f}") - - table.add_row("", "", "", style="dim") - table.add_row( - "[dim]Total[/dim]", - f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", - f"[{COLOR_PALETTE.POOLS.EMISSION}]{total_share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", - ) - - console.print(table) - - if not Confirm.ask("Proceed with these emission weights?", default=True): - console.print(":cross_mark: Aborted!") - return False - - success, err_msg = await sudo.sudo_set_mechanism_emission( - wallet=wallet, - subtensor=subtensor, - netuid=netuid, - split=normalized_weights, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - json_output=json_output, - ) - - if json_output: - json_console.print( - json.dumps( - { - "success": success, - "err_msg": err_msg, - "split": normalized_weights, - "percentages": [round(value * 100, 6) for value in fractions], - } - ) - ) - - return success - - -def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[float]]: - total = sum(values) - if total <= 0: - raise ValueError("Sum of emission weights must be greater than zero.") - - fractions = [value / total for value in values] - scaled = [fraction * U16_MAX for fraction in fractions] - base = [math.floor(value) for value in scaled] - remainder = int(U16_MAX - sum(base)) - - if remainder > 0: - fractional_parts = [value - math.floor(value) for value in scaled] - order = sorted( - range(len(base)), key=lambda idx: fractional_parts[idx], reverse=True - ) - idx = 0 - length = len(order) - while remainder > 0 and length > 0: - base[order[idx % length]] += 1 - remainder -= 1 - idx += 1 - - return [int(value) for value in base], fractions - - async def burn_cost( subtensor: "SubtensorInterface", json_output: bool = False ) -> Optional[Balance]: From 6f51ae2b2c9343190ec6f9f72db8f03263faa54d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 13:46:40 -0700 Subject: [PATCH 22/39] cleanup sudo --- bittensor_cli/src/commands/sudo.py | 88 ------------------------------ 1 file changed, 88 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 70c0f4eaf..e86144a6a 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -691,94 +691,6 @@ async def set_take_extrinsic( # commands -async def sudo_set_mechanism_count( - wallet: Wallet, - subtensor: "SubtensorInterface", - netuid: int, - mechanism_count: int, - wait_for_inclusion: bool, - wait_for_finalization: bool, - json_output: bool, -) -> tuple[bool, str]: - """Set the number of mechanisms for a subnet.""" - - if mechanism_count < 1: - err_msg = "Mechanism count must be greater than or equal to one." - if not json_output: - err_console.print(err_msg) - return False, err_msg - - if not await subtensor.subnet_exists(netuid): - err_msg = f"Subnet with netuid {netuid} does not exist." - if not json_output: - err_console.print(err_msg) - return False, err_msg - - if not Confirm.ask(f"Set mechanism count to {mechanism_count} for subnet {netuid}?"): - return False, "User cancelled" - - success, err_msg = await set_mechanism_count_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - mech_count=mechanism_count, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if json_output: - return success, err_msg - - if success: - console.print( - ":white_heavy_check_mark: " - f"[dark_sea_green3]Mechanism count set to {mechanism_count} for subnet {netuid}[/dark_sea_green3]" - ) - else: - err_console.print(f":cross_mark: [red]{err_msg}[/red]") - - return success, err_msg - - -async def sudo_set_mechanism_emission( - wallet: Wallet, - subtensor: "SubtensorInterface", - netuid: int, - split: list[int], - wait_for_inclusion: bool, - wait_for_finalization: bool, - json_output: bool, -) -> tuple[bool, str]: - """Set the emission split for mechanisms within a subnet.""" - - if not split: - err_msg = "Emission split must include at least one weight." - if not json_output: - err_console.print(err_msg) - return False, err_msg - - success, err_msg = await set_mechanism_emission_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuid=netuid, - split=split, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if json_output: - return success, err_msg - - if success: - console.print( - ":white_heavy_check_mark: [dark_sea_green3]Updated mechanism emission split.[/dark_sea_green3]" - ) - else: - err_console.print(f":cross_mark: [red]{err_msg}[/red]") - - return success, err_msg - - async def sudo_set_hyperparameter( wallet: Wallet, subtensor: "SubtensorInterface", From 8b4567839ea9e503f8dc6364351afc5c4e3869f4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 13:47:21 -0700 Subject: [PATCH 23/39] mechanisms.py --- .../src/commands/subnets/mechanisms.py | 498 ++++++++++++++++++ 1 file changed, 498 insertions(+) create mode 100644 bittensor_cli/src/commands/subnets/mechanisms.py diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py new file mode 100644 index 000000000..f6d039a8e --- /dev/null +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -0,0 +1,498 @@ +import asyncio +import json +import math +from typing import TYPE_CHECKING, Optional + +from bittensor_wallet import Wallet +from rich.prompt import Confirm, Prompt +from rich.table import Column, Table +from rich import box + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.commands import sudo +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + json_console, + U16_MAX, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def count( + subtensor: "SubtensorInterface", + netuid: int, + json_output: bool = False, +) -> Optional[int]: + """Display how many mechanisms exist for the provided subnet.""" + + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): + err_console.print(f"[red]Subnet {netuid} does not exist[/red]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": f"Subnet {netuid} does not exist"} + ) + ) + return None + + with console.status( + f":satellite:Retrieving mechanism count from {subtensor.network}...", + spinner="aesthetic", + ): + mechanism_count = await subtensor.get_subnet_mechanism_count( + netuid, block_hash=block_hash + ) + if not mechanism_count: + if json_output: + json_console.print( + json.dumps( + { + "netuid": netuid, + "count": None, + "error": "Failed to get mechanism count", + } + ) + ) + else: + err_console.print( + "Subnet mechanism count: [red]Failed to get mechanism count[/red]" + ) + return None + + if json_output: + json_console.print( + json.dumps( + { + "netuid": netuid, + "count": mechanism_count, + "error": "", + } + ) + ) + else: + console.print( + f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] mechanism" + f"{'s' if mechanism_count != 1 else ''}." + f"\n[dim](Tip: 1 mechanism means there are no mechanisms beyond the main subnet)[/dim]" + ) + + return mechanism_count + + +async def get_emission_split( + subtensor: "SubtensorInterface", + netuid: int, + json_output: bool = False, +) -> Optional[dict]: + """Display the emission split across mechanisms for a subnet.""" + + count_ = await subtensor.get_subnet_mechanism_count(netuid) + if count_ == 1: + console.print(f"Subnet {netuid} does not currently contain any mechanisms.") + if json_output: + json_console.print( + json.dumps( + { + "success": False, + "error": "Subnet does not contain any mechanisms.", + } + ) + ) + return None + + emission_split = await subtensor.get_mechanism_emission_split(netuid) or [] + + even_distribution = False + total_sum = sum(emission_split) + if total_sum == 0 and count_ > 0: + even_distribution = True + base, remainder = divmod(U16_MAX, count_) + emission_split = [base for _ in range(count_)] + if remainder: + emission_split[0] += remainder + total_sum = sum(emission_split) + + emission_percentages = ( + [round((value / total_sum) * 100, 6) for value in emission_split] + if total_sum > 0 + else [0.0 for _ in emission_split] + ) + + data = { + "netuid": netuid, + "raw_count": count_, + "visible_count": max(count_ - 1, 0), + "split": emission_split if count_ else [], + "percentages": emission_percentages if count_ else [], + "even_distribution": even_distribution, + } + + if json_output: + json_console.print(json.dumps(data)) + else: + table = Table( + Column( + "[bold white]Mechanism Index[/]", + justify="center", + style=COLOR_PALETTE.G.NETUID, + ), + Column( + "[bold white]Weight (u16)[/]", + justify="right", + style=COLOR_PALETTE.STAKE.STAKE_ALPHA, + ), + Column( + "[bold white]Share (%)[/]", + justify="right", + style=COLOR_PALETTE.POOLS.EMISSION, + ), + title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet {netuid} emission split[/]", + box=box.SIMPLE, + show_footer=True, + border_style="bright_black", + ) + + total_weight = sum(emission_split) + share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 + + for idx, value in enumerate(emission_split): + share = ( + emission_percentages[idx] if idx < len(emission_percentages) else 0.0 + ) + table.add_row(str(idx), str(value), f"{share:.6f}") + + table.add_row( + "[dim]Total[/dim]", + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", + f"[{COLOR_PALETTE.POOLS.EMISSION}]{share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", + ) + + console.print(table) + footer = "[dim]Totals are expressed as a fraction of 65535 (U16_MAX).[/dim]" + if even_distribution: + footer += ( + "\n[dim]No custom split found; displaying an even distribution.[/dim]" + ) + console.print(footer) + + return data + + +async def set_emission_split( + subtensor: "SubtensorInterface", + wallet: Wallet, + netuid: int, + new_emission_split: Optional[str], + wait_for_inclusion: bool, + wait_for_finalization: bool, + prompt: bool, + json_output: bool, +) -> bool: + """Set the emission split across mechanisms for a subnet.""" + + mech_count, existing_split = await asyncio.gather( + subtensor.get_subnet_mechanism_count(netuid), + subtensor.get_mechanism_emission_split(netuid), + ) + + if mech_count == 0: + message = ( + f"Subnet {netuid} does not currently contain any mechanisms to configure." + ) + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + if not json_output: + await get_emission_split( + subtensor=subtensor, + netuid=netuid, + json_output=False, + ) + + existing_split = [int(value) for value in existing_split] + if len(existing_split) < mech_count: + existing_split.extend([0] * (mech_count - len(existing_split))) + + if new_emission_split is not None: + try: + weights = [ + float(item.strip()) + for item in new_emission_split.split(",") + if item.strip() != "" + ] + except ValueError: + message = ( + "Invalid `--split` values. Provide a comma-separated list of numbers." + ) + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + else: + if not prompt: + err_console.print( + "Split values not supplied with `--no-prompt` flag. Cannot continue." + ) + return False + + weights: list[float] = [] + total_existing = sum(existing_split) or 1 + console.print("\n[dim]You either provide U16 values or percentages.[/dim]") + for idx in range(mech_count): + current_value = existing_split[idx] + current_percent = ( + (current_value / total_existing) * 100 if total_existing else 0 + ) + label = ( + "[blue]Main Mechanism (1)[/blue]" + if idx == 0 + else f"[blue]Mechanism {idx + 1}[/blue]" + ) + response = Prompt.ask( + ( + f"Relative weight for {label} " + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}](current: {current_value} ~ {current_percent:.2f}%)[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]" + ) + ) + try: + weights.append(float(response)) + except ValueError: + err_console.print("Invalid number provided. Aborting.") + return False + + if len(weights) != mech_count: + message = f"Expected {mech_count} weight values, received {len(weights)}." + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + if any(value < 0 for value in weights): + message = "Weights must be non-negative." + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + try: + normalized_weights, fractions = _normalize_emission_weights(weights) + except ValueError as exc: + message = str(exc) + if json_output: + json_console.print(json.dumps({"success": False, "error": message})) + else: + err_console.print(message) + return False + + if normalized_weights == existing_split: + message = ":white_heavy_check_mark: [dark_sea_green3]Emission split unchanged.[/dark_sea_green3]" + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": "Emission split unchanged.", + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + } + ) + ) + else: + console.print(message) + return True + + if not json_output: + table = Table( + Column( + "[bold white]Mechanism Index[/]", + justify="center", + style=COLOR_PALETTE.G.NETUID, + ), + Column( + "[bold white]Weight (u16)[/]", + justify="right", + style=COLOR_PALETTE.STAKE.STAKE_ALPHA, + ), + Column( + "[bold white]Share (%)[/]", + justify="right", + style=COLOR_PALETTE.POOLS.EMISSION, + ), + title=( + f"\n[{COLOR_PALETTE.G.HEADER}]Proposed emission split[/{COLOR_PALETTE.G.HEADER}]\n" + f"[{COLOR_PALETTE.G.SUBHEAD}]Subnet {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" + ), + box=box.SIMPLE, + show_footer=True, + border_style="bright_black", + ) + + total_weight = sum(normalized_weights) + total_share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 + + for idx, weight in enumerate(normalized_weights): + share_percent = fractions[idx] * 100 if idx < len(fractions) else 0.0 + table.add_row(str(idx), str(weight), f"{share_percent:.6f}") + + table.add_row("", "", "", style="dim") + table.add_row( + "[dim]Total[/dim]", + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", + f"[{COLOR_PALETTE.POOLS.EMISSION}]{total_share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", + ) + + console.print(table) + + if not Confirm.ask("Proceed with these emission weights?", default=True): + console.print(":cross_mark: Aborted!") + return False + + success, err_msg = await set_mechanism_emission( + wallet=wallet, + subtensor=subtensor, + netuid=netuid, + split=normalized_weights, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + json_output=json_output, + ) + + if json_output: + json_console.print( + json.dumps( + { + "success": success, + "err_msg": err_msg, + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + } + ) + ) + + return success + + +def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[float]]: + total = sum(values) + if total <= 0: + raise ValueError("Sum of emission weights must be greater than zero.") + + fractions = [value / total for value in values] + scaled = [fraction * U16_MAX for fraction in fractions] + base = [math.floor(value) for value in scaled] + remainder = int(U16_MAX - sum(base)) + + if remainder > 0: + fractional_parts = [value - math.floor(value) for value in scaled] + order = sorted( + range(len(base)), key=lambda idx: fractional_parts[idx], reverse=True + ) + idx = 0 + length = len(order) + while remainder > 0 and length > 0: + base[order[idx % length]] += 1 + remainder -= 1 + idx += 1 + + return [int(value) for value in base], fractions + + +async def set_mechanism_count( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + mechanism_count: int, + wait_for_inclusion: bool, + wait_for_finalization: bool, + json_output: bool, +) -> tuple[bool, str]: + """Set the number of mechanisms for a subnet.""" + + if mechanism_count < 1: + err_msg = "Mechanism count must be greater than or equal to one." + if not json_output: + err_console.print(err_msg) + return False, err_msg + + if not await subtensor.subnet_exists(netuid): + err_msg = f"Subnet with netuid {netuid} does not exist." + if not json_output: + err_console.print(err_msg) + return False, err_msg + + if not Confirm.ask( + f"Set mechanism count to {mechanism_count} for subnet {netuid}?" + ): + return False, "User cancelled" + + success, err_msg = await sudo.set_mechanism_count_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + mech_count=mechanism_count, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if json_output: + return success, err_msg + + if success: + console.print( + ":white_heavy_check_mark: " + f"[dark_sea_green3]Mechanism count set to {mechanism_count} for subnet {netuid}[/dark_sea_green3]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg + + +async def set_mechanism_emission( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + split: list[int], + wait_for_inclusion: bool, + wait_for_finalization: bool, + json_output: bool, +) -> tuple[bool, str]: + """Set the emission split for mechanisms within a subnet.""" + + if not split: + err_msg = "Emission split must include at least one weight." + if not json_output: + err_console.print(err_msg) + return False, err_msg + + success, err_msg = await sudo.set_mechanism_emission_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + split=split, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if json_output: + return success, err_msg + + if success: + console.print( + ":white_heavy_check_mark: " + f"[dark_sea_green3]Emission split updated for subnet {netuid}[/dark_sea_green3]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg From c8de4718d58ad55c198d697baaf6b7f782a34574 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 13:47:35 -0700 Subject: [PATCH 24/39] wip --- bittensor_cli/cli.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 53c92011c..ffad11fe8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -78,7 +78,11 @@ add as add_stake, remove as remove_stake, ) -from bittensor_cli.src.commands.subnets import price, subnets +from bittensor_cli.src.commands.subnets import ( + price, + subnets, + mechanisms as subnet_mechanisms, +) from bittensor_cli.version import __version__, __version_as_int__ try: @@ -5079,7 +5083,7 @@ def mechanism_count_set( if not json_output: current_count = self._run_command( - subnets.count( + subnet_mechanisms.count( subtensor=subtensor, netuid=netuid, json_output=False, @@ -5099,8 +5103,7 @@ def mechanism_count_set( ) return False prompt_text = ( - "Enter the [blue]number of mechanisms[/blue] to set.\n" - f"[dim](Current raw count: {current_count}; a value of 1 means there are no mechanisms beyond the root.)[/dim]" + "\n\nEnter the [blue]number of mechanisms[/blue] to set" ) mechanism_count = IntPrompt.ask(prompt_text) @@ -5139,7 +5142,7 @@ def mechanism_count_set( ) result, err_msg = self._run_command( - sudo.sudo_set_mechanism_count( + subnet_mechanisms.set_mechanism_count( wallet=wallet, subtensor=subtensor, netuid=netuid, @@ -5168,7 +5171,7 @@ def mechanism_count_get( self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) return self._run_command( - subnets.count( + subnet_mechanisms.count( subtensor=subtensor, netuid=netuid, json_output=json_output, @@ -5206,7 +5209,7 @@ def mechanism_emission_set( ) return self._run_command( - subnets.set_emission_split( + subnet_mechanisms.set_emission_split( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -5218,7 +5221,6 @@ def mechanism_emission_set( ) ) - def mechanism_emission_get( self, network: Optional[list[str]] = Options.network, @@ -5232,14 +5234,13 @@ def mechanism_emission_get( self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) return self._run_command( - subnets.get_emission_split( + subnet_mechanisms.get_emission_split( subtensor=subtensor, netuid=netuid, json_output=json_output, ) ) - def sudo_set( self, network: Optional[list[str]] = Options.network, From df47c71fdd04672b753114fd0e887e91afc99d20 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 13:48:41 -0700 Subject: [PATCH 25/39] ruff --- bittensor_cli/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ffad11fe8..33143a3fe 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5102,9 +5102,7 @@ def mechanism_count_set( "Mechanism count not supplied with `--no-prompt` flag. Cannot continue." ) return False - prompt_text = ( - "\n\nEnter the [blue]number of mechanisms[/blue] to set" - ) + prompt_text = "\n\nEnter the [blue]number of mechanisms[/blue] to set" mechanism_count = IntPrompt.ask(prompt_text) if mechanism_count == current_count: From 849c6a071ed23dad0ef2e70b0f72852f198329ce Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 14:04:11 -0700 Subject: [PATCH 26/39] updates cmds --- bittensor_cli/cli.py | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 33143a3fe..22b9ef7ed 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -741,8 +741,6 @@ def __init__(self): self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) self.subnet_mechanisms_app = typer.Typer(epilog=_epilog) - self.subnets_mechanisms_count_app = typer.Typer(epilog=_epilog) - self.subnets_mechanisms_emission_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) @@ -817,19 +815,6 @@ def __init__(self): hidden=True, no_args_is_help=True, ) - self.subnet_mechanisms_app.add_typer( - self.subnets_mechanisms_count_app, - name="count", - short_help="Manage mechanism instances", - no_args_is_help=True, - ) - self.subnet_mechanisms_app.add_typer( - self.subnets_mechanisms_emission_app, - name="emission", - short_help="Manage mechanism emission splits", - no_args_is_help=True, - ) - # weights aliases self.app.add_typer( self.weights_app, @@ -975,18 +960,18 @@ def __init__(self): children_app.command("take")(self.stake_childkey_take) # subnet mechanism commands - self.subnets_mechanisms_count_app.command( + self.subnet_mechanisms_app.command( + "count", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] + )(self.mechanism_count_get) + self.subnet_mechanisms_app.command( "set", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] )(self.mechanism_count_set) - self.subnets_mechanisms_count_app.command( - "get", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] - )(self.mechanism_count_get) - self.subnets_mechanisms_emission_app.command( - "set", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] - )(self.mechanism_emission_set) - self.subnets_mechanisms_emission_app.command( - "get", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] + self.subnet_mechanisms_app.command( + "emissions", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] )(self.mechanism_emission_get) + self.subnet_mechanisms_app.command( + "emissions-split", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] + )(self.mechanism_emission_set) # sudo commands self.sudo_app.command("set", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( From 13f5168273522693890d25ca317b90364b2dbde8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 17:27:40 -0700 Subject: [PATCH 27/39] add get_mechagraph_info --- .../src/bittensor/subtensor_interface.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index df101169a..fb5603400 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1329,37 +1329,51 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( else: return Balance.from_rao(fixed_to_float(_result)).set_unit(int(netuid)) + async def get_mechagraph_info( + self, netuid: int, mech_id: int, block_hash: Optional[str] = None + ) -> Optional[MetagraphInfo]: + """ + Returns the metagraph info for a given subnet and mechanism id. + And yes, it is indeed 'mecha'graph + """ + query = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_mechagraph", + params=[netuid, mech_id], + block_hash=block_hash, + ) + + if query is None: + return None + + return MetagraphInfo.from_any(query) + async def get_metagraph_info( self, netuid: int, block_hash: Optional[str] = None ) -> Optional[MetagraphInfo]: - hex_bytes_result = await self.query_runtime_api( + query = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_metagraph", params=[netuid], block_hash=block_hash, ) - if hex_bytes_result is None: + if query is None: return None - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return MetagraphInfo.from_any(bytes_result) + return MetagraphInfo.from_any(query) async def get_all_metagraphs_info( self, block_hash: Optional[str] = None ) -> list[MetagraphInfo]: - hex_bytes_result = await self.query_runtime_api( + query = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_all_metagraphs", params=[], block_hash=block_hash, ) - return MetagraphInfo.list_from_any(hex_bytes_result) + return MetagraphInfo.list_from_any(query) async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( self, From 5e78532b127f9f65f20a8ff04c3894344888bed8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 17:30:07 -0700 Subject: [PATCH 28/39] handle mech_id in subnet show --- bittensor_cli/cli.py | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 22b9ef7ed..4b92e40ea 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -233,6 +233,14 @@ def edit_help(cls, option_name: str, help_text: str): help="The netuid of the subnet in the network, (e.g. 1).", prompt=False, ) + mechanism_id = typer.Option( + None, + "--mech-id", + "--mech_id", + "--mechanism_id", + "--mechanism-id", + help="Mechanism ID within the subnet (defaults to 0).", + ) all_netuids = typer.Option( False, help="Use all netuids", @@ -1794,6 +1802,43 @@ def ask_partial_stake( logger.debug(f"Partial staking {partial_staking}") return False + def ask_subnet_mechanism( + self, + mechanism_id: Optional[int], + mechanism_count: int, + netuid: int, + ) -> int: + """Resolve the mechanism ID to use.""" + + if mechanism_count is None or mechanism_count <= 0: + err_console.print(f"Subnet {netuid} does not exist.") + raise typer.Exit() + + if mechanism_id is not None: + if mechanism_id < 0 or mechanism_id >= mechanism_count: + err_console.print( + f"Mechanism ID {mechanism_id} is out of range for subnet {netuid}. " + f"Valid range: [bold cyan]0[/bold cyan] to [bold cyan]{mechanism_count - 1}[/bold cyan]." + ) + raise typer.Exit() + return mechanism_id + + if mechanism_count == 1: + return 0 + + while True: + selected_mechanism_id = IntPrompt.ask( + f"Select mechanism ID for subnet {netuid}" + f"([bold cyan]0[/bold cyan] to [bold cyan]{mechanism_count - 1}[/bold cyan])", + default=0, + ) + if 0 <= selected_mechanism_id < mechanism_count: + return selected_mechanism_id + err_console.print( + f"Mechanism ID {selected_mechanism_id} is out of range for subnet {netuid}. " + f"Valid range: [bold cyan]0[/bold cyan] to [bold cyan]{mechanism_count - 1}[/bold cyan]." + ) + def wallet_ask( self, wallet_name: Optional[str], @@ -3786,6 +3831,8 @@ def stake_add( subnets.show( subtensor=self.initialize_chain(network), netuid=netuid_, + mechanism_id=0, + mechanism_count=1, sort=False, max_rows=12, prompt=False, @@ -5730,6 +5777,7 @@ def subnets_show( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + mechanism_id: Optional[int] = Options.mechanism_id, sort: bool = typer.Option( False, "--sort", @@ -5749,10 +5797,27 @@ def subnets_show( """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) + if netuid == 0: + mechanism_count = 1 + selected_mechanism_id = 0 + if mechanism_id not in (None, 0): + console.print( + "[dim]Mechanism selection ignored for the root subnet (only mechanism 0 exists).[/dim]" + ) + else: + mechanism_count = self._run_command( + subtensor.get_subnet_mechanism_count(netuid), exit_early=False + ) + selected_mechanism_id = self.ask_subnet_mechanism( + mechanism_id, mechanism_count, netuid + ) + return self._run_command( subnets.show( subtensor=subtensor, netuid=netuid, + mechanism_id=selected_mechanism_id, + mechanism_count=mechanism_count, sort=sort, max_rows=None, delegate_selection=False, From b7ce4bd33ab567398b801d6bafd8903881f9ff71 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 17:30:23 -0700 Subject: [PATCH 29/39] get_netuid_and_subuid_by_storage_index --- bittensor_cli/src/bittensor/utils.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 80aab6916..c1a958429 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -34,6 +34,7 @@ BT_DOCS_LINK = "https://docs.learnbittensor.org" +GLOBAL_MAX_SUBNET_COUNT = 4096 console = Console() json_console = Console() @@ -1462,3 +1463,23 @@ def get_hotkey_pub_ss58(wallet: Wallet) -> str: return wallet.hotkeypub.ss58_address except (KeyFileError, AttributeError): return wallet.hotkey.ss58_address + + +def get_netuid_and_subuid_by_storage_index(storage_index: int) -> tuple[int, int]: + """Returns the netuid and subuid from the storage index. + + Chain APIs (e.g., SubMetagraph response) returns netuid which is storage index that encodes both the netuid and + subuid. This function reverses the encoding to extract these components. + + Parameters: + storage_index: The storage index of the subnet. + + Returns: + tuple[int, int]: + - netuid subnet identifier. + - subuid identifier. + """ + return ( + storage_index % GLOBAL_MAX_SUBNET_COUNT, + storage_index // GLOBAL_MAX_SUBNET_COUNT, + ) From 8c5564c3aa2cca1c335c04770754e8be11ed7d8d Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 17:30:44 -0700 Subject: [PATCH 30/39] update MetagraphInfo --- bittensor_cli/src/bittensor/chain_data.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 07fd8c906..ffd4ba3ff 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -13,6 +13,7 @@ u16_normalized_float as u16tf, u64_normalized_float as u64tf, decode_account_id, + get_netuid_and_subuid_by_storage_index, ) @@ -1084,12 +1085,13 @@ class MetagraphInfo(InfoBase): alpha_dividends_per_hotkey: list[ tuple[str, Balance] ] # List of dividend payout in alpha via subnet. + subuid: int = 0 @classmethod def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": """Returns a MetagraphInfo object from decoded chain data.""" # Subnet index - _netuid = decoded["netuid"] + _netuid, _subuid = get_netuid_and_subuid_by_storage_index(decoded["netuid"]) # Name and symbol decoded.update({"name": bytes(decoded.get("name")).decode()}) @@ -1102,6 +1104,7 @@ def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": return cls( # Subnet index netuid=_netuid, + subuid=_subuid, # Name and symbol name=decoded["name"], symbol=decoded["symbol"], From 44aa14ffe0a50a8422953f9af037a3570612a49a Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 17:31:32 -0700 Subject: [PATCH 31/39] update metagraph to use mechanism ids --- bittensor_cli/src/commands/subnets/subnets.py | 141 ++++++++++-------- 1 file changed, 78 insertions(+), 63 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d8571f3f6..8acd9e682 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -872,6 +872,8 @@ def format_liquidity_cell( async def show( subtensor: "SubtensorInterface", netuid: int, + mechanism_id: Optional[int] = None, + mechanism_count: Optional[int] = None, sort: bool = False, max_rows: Optional[int] = None, delegate_selection: bool = False, @@ -1085,43 +1087,57 @@ async def show_root(): ) return selected_hotkey - async def show_subnet(netuid_: int): + async def show_subnet( + netuid_: int, + mechanism_id: Optional[int], + mechanism_count: Optional[int], + ): if not await subtensor.subnet_exists(netuid=netuid): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") return False + block_hash = await subtensor.substrate.get_chain_head() ( subnet_info, - subnet_state, identities, old_identities, current_burn_cost, ) = await asyncio.gather( subtensor.subnet(netuid=netuid_, block_hash=block_hash), - subtensor.get_subnet_state(netuid=netuid_, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), subtensor.get_delegate_identities(block_hash=block_hash), subtensor.get_hyperparameter( param_name="Burn", netuid=netuid_, block_hash=block_hash ), ) - if subnet_state is None: - print_error(f"Subnet {netuid_} does not exist") + + selected_mechanism_id = mechanism_id or 0 + + metagraph_info = await subtensor.get_mechagraph_info( + netuid_, selected_mechanism_id, block_hash=block_hash + ) + + if metagraph_info is None: + print_error( + f"Subnet {netuid_} with mechanism: {selected_mechanism_id} does not exist" + ) return False if subnet_info is None: print_error(f"Subnet {netuid_} does not exist") return False - if len(subnet_state.hotkeys) == 0: + if len(metagraph_info.hotkeys) == 0: print_error(f"Subnet {netuid_} is currently empty with 0 UIDs registered.") return False # Define table properties + mechanism_label = f"Mechanism {selected_mechanism_id}" + table = Table( title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}" f"{': ' + get_subnet_name(subnet_info)}" - f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + f"\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network} • {mechanism_label}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1133,33 +1149,11 @@ async def show_subnet(netuid_: int): ) # For table footers - alpha_sum = sum( - [ - subnet_state.alpha_stake[idx].tao - for idx in range(len(subnet_state.alpha_stake)) - ] - ) - stake_sum = sum( - [ - subnet_state.total_stake[idx].tao - for idx in range(len(subnet_state.total_stake)) - ] - ) - tao_sum = sum( - [ - subnet_state.tao_stake[idx].tao * TAO_WEIGHT - for idx in range(len(subnet_state.tao_stake)) - ] - ) - dividends_sum = sum( - subnet_state.dividends[idx] for idx in range(len(subnet_state.dividends)) - ) - emission_sum = sum( - [ - subnet_state.emission[idx].tao - for idx in range(len(subnet_state.emission)) - ] - ) + alpha_sum = sum(stake.tao for stake in metagraph_info.alpha_stake) + stake_sum = sum(stake.tao for stake in metagraph_info.total_stake) + tao_sum = sum((stake * TAO_WEIGHT).tao for stake in metagraph_info.tao_stake) + dividends_sum = sum(metagraph_info.dividends) + emission_sum = sum(emission.tao for emission in metagraph_info.emission) owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner_coldkey) if subnet_info.owner_hotkey not in owner_hotkeys: @@ -1174,7 +1168,7 @@ async def show_subnet(netuid_: int): break sorted_indices = sorted( - range(len(subnet_state.hotkeys)), + range(len(metagraph_info.hotkeys)), key=lambda i: ( # If sort is True, sort only by UIDs i @@ -1183,11 +1177,11 @@ async def show_subnet(netuid_: int): # Otherwise # Sort by owner status first not ( - subnet_state.coldkeys[i] == subnet_info.owner_coldkey - or subnet_state.hotkeys[i] in owner_hotkeys + metagraph_info.coldkeys[i] == subnet_info.owner_coldkey + or metagraph_info.hotkeys[i] in owner_hotkeys ), # Then sort by stake amount (higher stakes first) - -subnet_state.total_stake[i].tao, + -metagraph_info.total_stake[i].tao, ) ), ) @@ -1196,10 +1190,10 @@ async def show_subnet(netuid_: int): json_out_rows = [] for idx in sorted_indices: # Get identity for this uid - coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get( + coldkey_identity = identities.get(metagraph_info.coldkeys[idx], {}).get( "name", "" ) - hotkey_identity = old_identities.get(subnet_state.hotkeys[idx]) + hotkey_identity = old_identities.get(metagraph_info.hotkeys[idx]) uid_identity = ( coldkey_identity if coldkey_identity @@ -1207,8 +1201,8 @@ async def show_subnet(netuid_: int): ) if ( - subnet_state.coldkeys[idx] == subnet_info.owner_coldkey - or subnet_state.hotkeys[idx] in owner_hotkeys + metagraph_info.coldkeys[idx] == subnet_info.owner_coldkey + or metagraph_info.hotkeys[idx] in owner_hotkeys ): if uid_identity == "~": uid_identity = ( @@ -1220,44 +1214,44 @@ async def show_subnet(netuid_: int): ) # Modify tao stake with TAO_WEIGHT - tao_stake = subnet_state.tao_stake[idx] * TAO_WEIGHT + tao_stake = metagraph_info.tao_stake[idx] * TAO_WEIGHT rows.append( ( str(idx), # UID - f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}" + f"{metagraph_info.total_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose - else f"{millify_tao(subnet_state.total_stake[idx])} {subnet_info.symbol}", # Stake - f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" + else f"{millify_tao(metagraph_info.total_stake[idx])} {subnet_info.symbol}", # Stake + f"{metagraph_info.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose - else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake + else f"{millify_tao(metagraph_info.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake f"τ {tao_stake.tao:.4f}" if verbose else f"τ {millify_tao(tao_stake)}", # Tao Stake - f"{subnet_state.dividends[idx]:.6f}", # Dividends - f"{subnet_state.incentives[idx]:.6f}", # Incentive - f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions - f"{subnet_state.hotkeys[idx][:6]}" + f"{metagraph_info.dividends[idx]:.6f}", # Dividends + f"{metagraph_info.incentives[idx]:.6f}", # Incentive + f"{Balance.from_tao(metagraph_info.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions + f"{metagraph_info.hotkeys[idx][:6]}" if not verbose - else f"{subnet_state.hotkeys[idx]}", # Hotkey - f"{subnet_state.coldkeys[idx][:6]}" + else f"{metagraph_info.hotkeys[idx]}", # Hotkey + f"{metagraph_info.coldkeys[idx][:6]}" if not verbose - else f"{subnet_state.coldkeys[idx]}", # Coldkey + else f"{metagraph_info.coldkeys[idx]}", # Coldkey uid_identity, # Identity ) ) json_out_rows.append( { "uid": idx, - "stake": subnet_state.total_stake[idx].tao, - "alpha_stake": subnet_state.alpha_stake[idx].tao, + "stake": metagraph_info.total_stake[idx].tao, + "alpha_stake": metagraph_info.alpha_stake[idx].tao, "tao_stake": tao_stake.tao, - "dividends": subnet_state.dividends[idx], - "incentive": subnet_state.incentives[idx], - "emissions": Balance.from_tao(subnet_state.emission[idx].tao) + "dividends": metagraph_info.dividends[idx], + "incentive": metagraph_info.incentives[idx], + "emissions": Balance.from_tao(metagraph_info.emission[idx].tao) .set_unit(netuid_) .tao, - "hotkey": subnet_state.hotkeys[idx], - "coldkey": subnet_state.coldkeys[idx], + "hotkey": metagraph_info.hotkeys[idx], + "coldkey": metagraph_info.coldkeys[idx], "identity": uid_identity, } ) @@ -1353,8 +1347,16 @@ async def show_subnet(netuid_: int): if current_burn_cost else Balance(0) ) + total_mechanisms = mechanism_count if mechanism_count is not None else 1 + output_dict = { "netuid": netuid_, + "mechanism_id": selected_mechanism_id, + **( + {"mechanism_count": mechanism_count} + if mechanism_count is not None + else {} + ), "name": subnet_name_display, "owner": subnet_info.owner_coldkey, "owner_identity": owner_identity, @@ -1372,8 +1374,21 @@ async def show_subnet(netuid_: int): if json_output: json_console.print(json.dumps(output_dict)) + mech_line = ( + f"\n Mechanism ID: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]#{selected_mechanism_id}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]" + if total_mechanisms > 1 + else "" + ) + total_mech_line = ( + f"\n Total mechanisms: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]" + f"{total_mechanisms}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]" + ) + console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{mech_line}" + f"{total_mech_line}" f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" @@ -1419,7 +1434,7 @@ async def show_subnet(netuid_: int): # Check if the UID exists in the subnet if uid in [int(row[0]) for row in rows]: row_data = next(row for row in rows if int(row[0]) == uid) - hotkey = subnet_state.hotkeys[uid] + hotkey = metagraph_info.hotkeys[uid] identity = "" if row_data[9] == "~" else row_data[9] identity_str = f" ({identity})" if identity else "" console.print( @@ -1439,7 +1454,7 @@ async def show_subnet(netuid_: int): result = await show_root() return result else: - result = await show_subnet(netuid) + result = await show_subnet(netuid, mechanism_id, mechanism_count) return result From 8a51aa9186dc892d8b17a553bc1649178e5f92d4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 17:41:49 -0700 Subject: [PATCH 32/39] add mechid alias --- bittensor_cli/cli.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4b92e40ea..7dc16679f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -235,6 +235,7 @@ def edit_help(cls, option_name: str, help_text: str): ) mechanism_id = typer.Option( None, + "mechid", "--mech-id", "--mech_id", "--mechanism_id", From 271b2ce1cfd769a546101f44dddcfcdddb0df238 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 17 Sep 2025 17:49:18 -0700 Subject: [PATCH 33/39] wip --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7dc16679f..a313fcec9 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -235,7 +235,7 @@ def edit_help(cls, option_name: str, help_text: str): ) mechanism_id = typer.Option( None, - "mechid", + "--mechid", "--mech-id", "--mech_id", "--mechanism_id", From 7905fc7efbdc8615fe4a821c4a5803da54942017 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 18 Sep 2025 09:38:10 -0700 Subject: [PATCH 34/39] improve mech prompt info --- bittensor_cli/src/commands/subnets/mechanisms.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py index f6d039a8e..5f13880e4 100644 --- a/bittensor_cli/src/commands/subnets/mechanisms.py +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -92,13 +92,15 @@ async def get_emission_split( count_ = await subtensor.get_subnet_mechanism_count(netuid) if count_ == 1: - console.print(f"Subnet {netuid} does not currently contain any mechanisms.") + console.print( + f"Subnet {netuid} only has the primary mechanism (mechanism 0). No emission split to display." + ) if json_output: json_console.print( json.dumps( { "success": False, - "error": "Subnet does not contain any mechanisms.", + "error": "Subnet only has the primary mechanism (mechanism 0). No emission split to display.", } ) ) @@ -150,7 +152,8 @@ async def get_emission_split( justify="right", style=COLOR_PALETTE.POOLS.EMISSION, ), - title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet {netuid} emission split[/]", + title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet {netuid} • Emission split[/]\n" + f"[{COLOR_PALETTE.G.SUBHEAD}]Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]", box=box.SIMPLE, show_footer=True, border_style="bright_black", @@ -412,6 +415,7 @@ async def set_mechanism_count( subtensor: "SubtensorInterface", netuid: int, mechanism_count: int, + previous_count: int, wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, @@ -431,7 +435,9 @@ async def set_mechanism_count( return False, err_msg if not Confirm.ask( - f"Set mechanism count to {mechanism_count} for subnet {netuid}?" + f"Subnet [blue]{netuid}[/blue] currently has [blue]{previous_count}[/blue] mechanism" + f"{'s' if previous_count != 1 else ''}." + f" Set it to [blue]{mechanism_count}[/blue]?" ): return False, "User cancelled" From cc8bad153f343bc8d082416186dec3de16b31720 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 18 Sep 2025 09:38:26 -0700 Subject: [PATCH 35/39] fmt --- bittensor_cli/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a313fcec9..8bd2e02c7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1819,7 +1819,7 @@ def ask_subnet_mechanism( if mechanism_id < 0 or mechanism_id >= mechanism_count: err_console.print( f"Mechanism ID {mechanism_id} is out of range for subnet {netuid}. " - f"Valid range: [bold cyan]0[/bold cyan] to [bold cyan]{mechanism_count - 1}[/bold cyan]." + f"Valid range: [bold cyan]0 to {mechanism_count - 1}[/bold cyan]." ) raise typer.Exit() return mechanism_id @@ -1829,15 +1829,15 @@ def ask_subnet_mechanism( while True: selected_mechanism_id = IntPrompt.ask( - f"Select mechanism ID for subnet {netuid}" - f"([bold cyan]0[/bold cyan] to [bold cyan]{mechanism_count - 1}[/bold cyan])", + f"Select mechanism ID for subnet {netuid} " + f"([bold cyan]0 to {mechanism_count - 1}[/bold cyan])", default=0, ) if 0 <= selected_mechanism_id < mechanism_count: return selected_mechanism_id err_console.print( f"Mechanism ID {selected_mechanism_id} is out of range for subnet {netuid}. " - f"Valid range: [bold cyan]0[/bold cyan] to [bold cyan]{mechanism_count - 1}[/bold cyan]." + f"Valid range: [bold cyan]0 to {mechanism_count - 1}[/bold cyan]." ) def wallet_ask( @@ -5178,6 +5178,7 @@ def mechanism_count_set( subtensor=subtensor, netuid=netuid, mechanism_count=mechanism_count, + previous_count=current_count or 0, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, json_output=json_output, From e69e64564fdc82926727167bb2ea5417dd0051fd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 18 Sep 2025 12:42:16 -0700 Subject: [PATCH 36/39] get_all_subnet_mechanisms --- .../src/bittensor/subtensor_interface.py | 18 +++++++++++++++++- .../src/commands/subnets/mechanisms.py | 6 +++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index fb5603400..001a1080e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1171,7 +1171,7 @@ async def get_subnet_hyperparameters( return SubnetHyperparameters.from_any(result) - async def get_subnet_mechanism_count( + async def get_subnet_mechanisms( self, netuid: int, block_hash: Optional[str] = None ) -> int: """Return the number of mechanisms that belong to the provided subnet.""" @@ -1187,6 +1187,22 @@ async def get_subnet_mechanism_count( return 0 return int(result) + async def get_all_subnet_mechanisms( + self, block_hash: Optional[str] = None + ) -> dict[int, int]: + """Return mechanism counts for every subnet with a recorded value.""" + + results = await self.substrate.query_map( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[], + block_hash=block_hash, + ) + res = {} + async for netuid, count in results: + res[int(netuid)] = int(count.value) + return res + async def get_mechanism_emission_split( self, netuid: int, block_hash: Optional[str] = None ) -> list[int]: diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py index 5f13880e4..8308443b2 100644 --- a/bittensor_cli/src/commands/subnets/mechanisms.py +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -43,7 +43,7 @@ async def count( f":satellite:Retrieving mechanism count from {subtensor.network}...", spinner="aesthetic", ): - mechanism_count = await subtensor.get_subnet_mechanism_count( + mechanism_count = await subtensor.get_subnet_mechanisms( netuid, block_hash=block_hash ) if not mechanism_count: @@ -90,7 +90,7 @@ async def get_emission_split( ) -> Optional[dict]: """Display the emission split across mechanisms for a subnet.""" - count_ = await subtensor.get_subnet_mechanism_count(netuid) + count_ = await subtensor.get_subnet_mechanisms(netuid) if count_ == 1: console.print( f"Subnet {netuid} only has the primary mechanism (mechanism 0). No emission split to display." @@ -198,7 +198,7 @@ async def set_emission_split( """Set the emission split across mechanisms for a subnet.""" mech_count, existing_split = await asyncio.gather( - subtensor.get_subnet_mechanism_count(netuid), + subtensor.get_subnet_mechanisms(netuid), subtensor.get_mechanism_emission_split(netuid), ) From ff9b43355d4df75cfc3e1fd0efe1e8762ab027a6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 18 Sep 2025 12:46:43 -0700 Subject: [PATCH 37/39] add mechanisms to subnets list --- bittensor_cli/cli.py | 4 +- bittensor_cli/src/commands/subnets/subnets.py | 38 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8bd2e02c7..eac53eef6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5125,7 +5125,7 @@ def mechanism_count_set( ) else: current_count = self._run_command( - subtensor.get_subnet_mechanism_count(netuid), + subtensor.get_subnet_mechanisms(netuid), exit_early=False, ) @@ -5808,7 +5808,7 @@ def subnets_show( ) else: mechanism_count = self._run_command( - subtensor.get_subnet_mechanism_count(netuid), exit_early=False + subtensor.get_subnet_mechanisms(netuid), exit_early=False ) selected_mechanism_id = self.ask_subnet_mechanism( mechanism_id, mechanism_count, netuid diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 8acd9e682..0d0f7a159 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -216,8 +216,12 @@ async def subnets_list( """List all subnet netuids in the network.""" async def fetch_subnet_data(): - block_number_ = await subtensor.substrate.get_block_number(None) - subnets_ = await subtensor.all_subnets() + block_hash = await subtensor.substrate.get_chain_head() + subnets_, mechanisms, block_number_ = await asyncio.gather( + subtensor.all_subnets(block_hash=block_hash), + subtensor.get_all_subnet_mechanisms(block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), + ) # Sort subnets by market cap, keeping the root subnet in the first position root_subnet = next(s for s in subnets_ if s.netuid == 0) @@ -227,7 +231,7 @@ async def fetch_subnet_data(): reverse=True, ) sorted_subnets = [root_subnet] + other_subnets - return sorted_subnets, block_number_ + return sorted_subnets, block_number_, mechanisms def calculate_emission_stats( subnets_: list, block_number_: int @@ -315,10 +319,15 @@ def define_table( justify="left", overflow="fold", ) + defined_table.add_column( + "[bold white]Mechanisms", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_1"], + justify="center", + ) return defined_table # Non-live mode - def _create_table(subnets_, block_number_): + def _create_table(subnets_, block_number_, mechanisms): rows = [] _, percentage_string = calculate_emission_stats(subnets_, block_number_) @@ -398,6 +407,8 @@ def _create_table(subnets_, block_number_): else: tempo_cell = "-/-" + mechanisms_cell = str(mechanisms.get(netuid, 1)) + rows.append( ( netuid_cell, # Netuid @@ -409,6 +420,7 @@ def _create_table(subnets_, block_number_): alpha_out_cell, # Stake α_out supply_cell, # Supply tempo_cell, # Tempo k/n + mechanisms_cell, # Mechanism count ) ) @@ -430,7 +442,7 @@ def _create_table(subnets_, block_number_): defined_table.add_row(*row) return defined_table - def dict_table(subnets_, block_number_) -> dict: + def dict_table(subnets_, block_number_, mechanisms) -> dict: subnet_rows = {} total_tao_emitted, _ = calculate_emission_stats(subnets_, block_number_) total_emissions = 0.0 @@ -470,6 +482,7 @@ def dict_table(subnets_, block_number_) -> dict: "alpha_out": alpha_out, "supply": supply, "tempo": tempo, + "mechanisms": mechanisms.get(netuid, 1), } output = { "total_tao_emitted": total_tao_emitted, @@ -482,7 +495,7 @@ def dict_table(subnets_, block_number_) -> dict: return output # Live mode - def create_table_live(subnets_, previous_data_, block_number_): + def create_table_live(subnets_, previous_data_, block_number_, mechanisms): def format_cell( value, previous_value, unit="", unit_first=False, precision=4, millify=False ): @@ -718,6 +731,7 @@ def format_liquidity_cell( alpha_out_cell, # Stake α_out supply_cell, # Supply tempo_cell, # Tempo k/n + str(mechanisms.get(netuid, 1)), # Mechanisms ) ) @@ -764,7 +778,7 @@ def format_liquidity_cell( with Live(console=console, screen=True, auto_refresh=True) as live: try: while True: - subnets, block_number = await fetch_subnet_data() + subnets, block_number, mechanisms = await fetch_subnet_data() # Update block numbers previous_block = current_block @@ -776,7 +790,7 @@ def format_liquidity_cell( ) table, current_data = create_table_live( - subnets, previous_data, block_number + subnets, previous_data, block_number, mechanisms ) previous_data = current_data progress.reset(progress_task) @@ -802,11 +816,13 @@ def format_liquidity_cell( pass # Ctrl + C else: # Non-live mode - subnets, block_number = await fetch_subnet_data() + subnets, block_number, mechanisms = await fetch_subnet_data() if json_output: - json_console.print(json.dumps(dict_table(subnets, block_number))) + json_console.print( + json.dumps(dict_table(subnets, block_number, mechanisms)) + ) else: - table = _create_table(subnets, block_number) + table = _create_table(subnets, block_number, mechanisms) console.print(table) return From 31e1934add3b39771f7f2863e0d4aae5f7714ca4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 18 Sep 2025 16:43:03 -0700 Subject: [PATCH 38/39] improves docs --- bittensor_cli/cli.py | 63 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index eac53eef6..c63c123b0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5109,7 +5109,20 @@ def mechanism_count_set( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Set the number of mechanisms registered under a subnet.""" + """ + Configure how many mechanisms are registered for a subnet. + + The base mechanism at index 0 and new ones are incremented by 1. + + [bold]Common Examples:[/bold] + + 1. Prompt for the new mechanism count interactively: + [green]$[/green] btcli subnet mech set --netuid 12 + + 2. Set the count to 2 using a specific wallet: + [green]$[/green] btcli subnet mech set --netuid 12 --count 2 --wallet.name my_wallet --wallet.hotkey admin + + """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5198,7 +5211,14 @@ def mechanism_count_get( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Display the number of mechanisms registered under a subnet.""" + """ + Display how many mechanisms are registered under a subnet. + + Includes the base mechanism (index 0). Helpful for verifying the active + mechanism counts in a subnet. + + [green]$[/green] btcli subnet mech count --netuid 12 + """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5229,7 +5249,21 @@ def mechanism_emission_set( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Set the emission split across mechanisms for a subnet.""" + """ + Update the emission split across mechanisms for a subnet. + + Accepts comma-separated weights (U16 values or percentages). When `--split` + is omitted and prompts remain enabled, you will be guided interactively and + the CLI automatically normalises the weights. + + [bold]Common Examples:[/bold] + + 1. Configure the split interactively: + [green]$[/green] btcli subnet mech emissions-split --netuid 12 + + 2. Apply a 70/30 distribution in one command: + [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin + """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5261,7 +5295,14 @@ def mechanism_emission_get( verbose: bool = Options.verbose, json_output: bool = Options.json_output, ): - """Display the emission split across mechanisms for a subnet.""" + """ + Display the current emission split across mechanisms for a subnet. + + Shows raw U16 weights alongside percentage shares for each mechanism. Useful + for verifying the emission split in a subnet. + + [green]$[/green] btcli subnet mech emissions --netuid 12 + """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -5791,11 +5832,19 @@ def subnets_show( json_output: bool = Options.json_output, ): """ - Displays detailed information about a subnet including participants and their state. + Inspect the metagraph for a subnet. - EXAMPLE + Shows miners, validators, stake, ranks, emissions, and other runtime stats. + When multiple mechanisms exist, the CLI prompts for one unless `--mechid` + is supplied. Netuid 0 always uses mechid 0. + + [bold]Common Examples:[/bold] + + 1. Inspect the mechanism with prompts for selection: + [green]$[/green] btcli subnets show --netuid 12 - [green]$[/green] btcli subnets show + 2. Pick mechanism 1 explicitly: + [green]$[/green] btcli subnets show --netuid 12 --mechid 1 """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) From 34989b8052c0a9d7da3c6a6981795ebda0400875 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 23 Sep 2025 22:38:21 +0200 Subject: [PATCH 39/39] Update to include extrinsic identifiers --- bittensor_cli/cli.py | 11 +- .../src/commands/subnets/mechanisms.py | 126 +++++++++--------- bittensor_cli/src/commands/sudo.py | 21 +-- 3 files changed, 80 insertions(+), 78 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 71a75605e..0d8e53d99 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5173,6 +5173,7 @@ def mechanism_count_set( { "success": True, "message": f"Subnet {netuid} already has {visible_count} mechanisms.", + "extrinsic_identifier": None, } ) ) @@ -5194,7 +5195,7 @@ def mechanism_count_set( f"mechanism_count: {mechanism_count}\n" ) - result, err_msg = self._run_command( + result, err_msg, ext_id = self._run_command( subnet_mechanisms.set_mechanism_count( wallet=wallet, subtensor=subtensor, @@ -5208,7 +5209,13 @@ def mechanism_count_set( ) if json_output: - json_console.print(json.dumps({"success": result, "err_msg": err_msg})) + json_console.print_json( + data={ + "success": result, + "message": err_msg, + "extrinsic_identifier": ext_id, + } + ) return result diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py index 8308443b2..bf329513a 100644 --- a/bittensor_cli/src/commands/subnets/mechanisms.py +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -1,5 +1,4 @@ import asyncio -import json import math from typing import TYPE_CHECKING, Optional @@ -15,6 +14,7 @@ err_console, json_console, U16_MAX, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -32,10 +32,8 @@ async def count( if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") if json_output: - json_console.print( - json.dumps( - {"success": False, "error": f"Subnet {netuid} does not exist"} - ) + json_console.print_json( + data={"success": False, "error": f"Subnet {netuid} does not exist"} ) return None @@ -48,14 +46,12 @@ async def count( ) if not mechanism_count: if json_output: - json_console.print( - json.dumps( - { - "netuid": netuid, - "count": None, - "error": "Failed to get mechanism count", - } - ) + json_console.print_json( + data={ + "netuid": netuid, + "count": None, + "error": "Failed to get mechanism count", + } ) else: err_console.print( @@ -64,14 +60,12 @@ async def count( return None if json_output: - json_console.print( - json.dumps( - { - "netuid": netuid, - "count": mechanism_count, - "error": "", - } - ) + json_console.print_json( + data={ + "netuid": netuid, + "count": mechanism_count, + "error": "", + } ) else: console.print( @@ -96,13 +90,11 @@ async def get_emission_split( f"Subnet {netuid} only has the primary mechanism (mechanism 0). No emission split to display." ) if json_output: - json_console.print( - json.dumps( - { - "success": False, - "error": "Subnet only has the primary mechanism (mechanism 0). No emission split to display.", - } - ) + json_console.print_json( + data={ + "success": False, + "error": "Subnet only has the primary mechanism (mechanism 0). No emission split to display.", + } ) return None @@ -134,7 +126,7 @@ async def get_emission_split( } if json_output: - json_console.print(json.dumps(data)) + json_console.print_json(data=data) else: table = Table( Column( @@ -207,7 +199,7 @@ async def set_emission_split( f"Subnet {netuid} does not currently contain any mechanisms to configure." ) if json_output: - json_console.print(json.dumps({"success": False, "error": message})) + json_console.print_json(data={"success": False, "error": message}) else: err_console.print(message) return False @@ -235,7 +227,7 @@ async def set_emission_split( "Invalid `--split` values. Provide a comma-separated list of numbers." ) if json_output: - json_console.print(json.dumps({"success": False, "error": message})) + json_console.print_json(data={"success": False, "error": message}) else: err_console.print(message) return False @@ -274,7 +266,7 @@ async def set_emission_split( if len(weights) != mech_count: message = f"Expected {mech_count} weight values, received {len(weights)}." if json_output: - json_console.print(json.dumps({"success": False, "error": message})) + json_console.print_json(data={"success": False, "error": message}) else: err_console.print(message) return False @@ -282,7 +274,7 @@ async def set_emission_split( if any(value < 0 for value in weights): message = "Weights must be non-negative." if json_output: - json_console.print(json.dumps({"success": False, "error": message})) + json_console.print_json(data={"success": False, "error": message}) else: err_console.print(message) return False @@ -292,7 +284,7 @@ async def set_emission_split( except ValueError as exc: message = str(exc) if json_output: - json_console.print(json.dumps({"success": False, "error": message})) + json_console.print_json(data={"success": False, "error": message}) else: err_console.print(message) return False @@ -300,15 +292,14 @@ async def set_emission_split( if normalized_weights == existing_split: message = ":white_heavy_check_mark: [dark_sea_green3]Emission split unchanged.[/dark_sea_green3]" if json_output: - json_console.print( - json.dumps( - { - "success": True, - "message": "Emission split unchanged.", - "split": normalized_weights, - "percentages": [round(value * 100, 6) for value in fractions], - } - ) + json_console.print_json( + data={ + "success": True, + "message": "Emission split unchanged.", + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + "extrinsic_identifier": None, + } ) else: console.print(message) @@ -360,7 +351,7 @@ async def set_emission_split( console.print(":cross_mark: Aborted!") return False - success, err_msg = await set_mechanism_emission( + success, err_msg, ext_id = await set_mechanism_emission( wallet=wallet, subtensor=subtensor, netuid=netuid, @@ -371,15 +362,14 @@ async def set_emission_split( ) if json_output: - json_console.print( - json.dumps( - { - "success": success, - "err_msg": err_msg, - "split": normalized_weights, - "percentages": [round(value * 100, 6) for value in fractions], - } - ) + json_console.print_json( + data={ + "success": success, + "err_msg": err_msg, + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + "extrinsic_identifier": ext_id, + } ) return success @@ -398,7 +388,7 @@ def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[fl if remainder > 0: fractional_parts = [value - math.floor(value) for value in scaled] order = sorted( - range(len(base)), key=lambda idx: fractional_parts[idx], reverse=True + range(len(base)), key=lambda idx_: fractional_parts[idx_], reverse=True ) idx = 0 length = len(order) @@ -419,29 +409,29 @@ async def set_mechanism_count( wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Set the number of mechanisms for a subnet.""" if mechanism_count < 1: err_msg = "Mechanism count must be greater than or equal to one." if not json_output: err_console.print(err_msg) - return False, err_msg + return False, err_msg, None if not await subtensor.subnet_exists(netuid): err_msg = f"Subnet with netuid {netuid} does not exist." if not json_output: err_console.print(err_msg) - return False, err_msg + return False, err_msg, None if not Confirm.ask( f"Subnet [blue]{netuid}[/blue] currently has [blue]{previous_count}[/blue] mechanism" f"{'s' if previous_count != 1 else ''}." f" Set it to [blue]{mechanism_count}[/blue]?" ): - return False, "User cancelled" + return False, "User cancelled", None - success, err_msg = await sudo.set_mechanism_count_extrinsic( + success, err_msg, ext_receipt = await sudo.set_mechanism_count_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -449,11 +439,13 @@ async def set_mechanism_count( 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: - return success, err_msg + return success, err_msg, ext_id if success: + await print_extrinsic_id(ext_receipt) console.print( ":white_heavy_check_mark: " f"[dark_sea_green3]Mechanism count set to {mechanism_count} for subnet {netuid}[/dark_sea_green3]" @@ -461,7 +453,7 @@ async def set_mechanism_count( else: err_console.print(f":cross_mark: [red]{err_msg}[/red]") - return success, err_msg + return success, err_msg, ext_id async def set_mechanism_emission( @@ -472,16 +464,16 @@ async def set_mechanism_emission( wait_for_inclusion: bool, wait_for_finalization: bool, json_output: bool, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Set the emission split for mechanisms within a subnet.""" if not split: err_msg = "Emission split must include at least one weight." if not json_output: err_console.print(err_msg) - return False, err_msg + return False, err_msg, None - success, err_msg = await sudo.set_mechanism_emission_extrinsic( + success, err_msg, ext_receipt = await sudo.set_mechanism_emission_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -489,11 +481,13 @@ async def set_mechanism_emission( 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: - return success, err_msg + return success, err_msg, ext_id if success: + await print_extrinsic_id(ext_receipt) console.print( ":white_heavy_check_mark: " f"[dark_sea_green3]Emission split updated for subnet {netuid}[/dark_sea_green3]" @@ -501,4 +495,4 @@ async def set_mechanism_emission( else: err_console.print(f":cross_mark: [red]{err_msg}[/red]") - return success, err_msg + return success, err_msg, ext_id diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 1175ec46c..955f52435 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -2,6 +2,7 @@ import json from typing import TYPE_CHECKING, Union, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich import box from rich.table import Column, Table @@ -177,12 +178,12 @@ async def set_mechanism_count_extrinsic( mech_count: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Sets the number of mechanisms for a subnet via AdminUtils.""" unlock_result = unlock_key(wallet) if not unlock_result.success: - return False, unlock_result.message + return False, unlock_result.message, None substrate = subtensor.substrate call_params = {"netuid": netuid, "mechanism_count": mech_count} @@ -197,7 +198,7 @@ async def set_mechanism_count_extrinsic( call_function="sudo_set_mechanism_count", call_params=call_params, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion=wait_for_inclusion, @@ -205,9 +206,9 @@ async def set_mechanism_count_extrinsic( ) if not success: - return False, err_msg + return False, err_msg, None - return True, "" + return True, "", ext_receipt async def set_mechanism_emission_extrinsic( @@ -217,12 +218,12 @@ async def set_mechanism_emission_extrinsic( split: list[int], wait_for_inclusion: bool = True, wait_for_finalization: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Sets the emission split for a subnet's mechanisms via AdminUtils.""" unlock_result = unlock_key(wallet) if not unlock_result.success: - return False, unlock_result.message + return False, unlock_result.message, None substrate = subtensor.substrate @@ -235,7 +236,7 @@ async def set_mechanism_emission_extrinsic( call_function="sudo_set_mechanism_emission_split", call_params={"netuid": netuid, "maybe_split": split}, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion=wait_for_inclusion, @@ -243,9 +244,9 @@ async def set_mechanism_emission_extrinsic( ) if not success: - return False, err_msg + return False, err_msg, None - return True, "" + return True, "", ext_receipt async def set_hyperparameter_extrinsic(