From 88bfed5729e09bde1d9e194ca2a45ef9840ef4cd Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 16 Sep 2025 14:38:59 -0700 Subject: [PATCH 01/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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/54] 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 3c075ebff4896d0d8cab587f5f229c38818ded0e Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 19 Sep 2025 15:54:40 +0200 Subject: [PATCH 39/54] Revert "Revert "Merge pull request #628 from opentensor/feat/thewhaleking/trim-uids"" This reverts commit b9d8cd325905f950903d1fbe7b831d5a04207ae2. --- bittensor_cli/cli.py | 50 ++++++++++++++++++ bittensor_cli/src/__init__.py | 1 + bittensor_cli/src/commands/sudo.py | 56 +++++++++++++++++++++ tests/e2e_tests/test_hyperparams_setting.py | 49 ++++++++++++++++++ 4 files changed, 156 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 88ae40580..4484f5288 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -960,6 +960,9 @@ def __init__(self): self.sudo_app.command("get-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])( self.sudo_get_take ) + self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( + self.sudo_trim + ) # subnets commands self.subnets_app.command( @@ -5343,6 +5346,53 @@ def sudo_get_take( sudo.display_current_take(self.initialize_chain(network), wallet) ) + def sudo_trim( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + netuid: int = Options.netuid, + max_uids: int = typer.Option( + None, + "--max", + "--max-uids", + help="The maximum number of allowed uids to which to trim", + prompt="Max UIDs", + ), + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + prompt: bool = Options.prompt, + period: int = Options.period, + ): + """ + Allows subnet owners to trim UIDs on their subnet to a specified max number of netuids. + + EXAMPLE + [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 6 + """ + self.verbosity_handler(quiet, verbose, json_output) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + self._run_command( + sudo.trim( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + max_n=max_uids, + period=period, + json_output=json_output, + prompt=prompt, + ) + ) + def subnets_list( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index f93aed504..111cd0d8f 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -666,6 +666,7 @@ class WalletValidationTypes(Enum): "user_liquidity_enabled": ("toggle_user_liquidity", True), "bonds_reset_enabled": ("sudo_set_bonds_reset_enabled", False), "transfers_enabled": ("sudo_set_toggle_transfer", False), + "min_allowed_uids": ("sudo_set_min_allowed_uids", True), } HYPERPARAMS_MODULE = { diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index e6ac31185..bb4bb43de 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -955,3 +955,59 @@ async def _do_set_take() -> bool: result_ = await _do_set_take() return result_ + + +async def trim( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + max_n: int, + period: int, + prompt: bool, + json_output: bool, +) -> bool: + """ + Trims a subnet's UIDs to a specified amount + """ + print_verbose("Confirming subnet owner") + subnet_owner = await subtensor.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], + ) + if subnet_owner != wallet.coldkeypub.ss58_address: + err_msg = "This wallet doesn't own the specified subnet." + if json_output: + json_console.print_json(data={"success": False, "message": err_msg}) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + return False + if prompt and not json_output: + if not Confirm.ask( + f"You are about to trim UIDs on SN{netuid} to a limit of {max_n}", + default=False, + ): + err_console.print(":cross_mark: [red]User aborted.[/red]") + call = await subtensor.substrate.compose_call( + call_module="AdminUtils", + call_function="sudo_trim_to_max_allowed_uids", + call_params={"netuid": netuid, "max_n": max_n}, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call=call, wallet=wallet, era={"period": period} + ) + if not success: + if json_output: + json_console.print_json(data={"success": False, "message": err_msg}) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + return False + else: + msg = f"Successfully trimmed UIDs on SN{netuid} to {max_n}" + if json_output: + json_console.print_json(data={"success": True, "message": msg}) + else: + console.print( + f":white_heavy_check_mark: [dark_sea_green3]{msg}[/dark_sea_green3]" + ) + return True diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 3af86c140..916b00cac 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -120,4 +120,53 @@ def test_hyperparams_setting(local_chain, wallet_setup): cmd_json = json.loads(cmd.stdout) assert cmd_json["success"] is True, (key, new_val, cmd.stdout, cmd_json) print(f"Successfully set hyperparameter {key} to value {new_val}") + # also test hidden hyperparam + cmd = exec_command_alice( + command="sudo", + sub_command="set", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--network", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--json-out", + "--no-prompt", + "--param", + "min_allowed_uids", + "--value", + "110", + ], + ) + cmd_json = json.loads(cmd.stdout) + assert cmd_json["success"] is True, (cmd.stdout, cmd_json) print("Successfully set hyperparameters") + print("Testing trimming UIDs") + cmd = exec_command_alice( + command="sudo", + sub_command="trim", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--network", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--max", + "120", + "--json-out", + "--no-prompt", + ], + ) + cmd_json = json.loads(cmd.stdout) + assert cmd_json["success"] is True, (cmd.stdout, cmd_json) + print("Successfully trimmed UIDs") From 86915ac959d0a01eae22a230061bcda09bcb0de5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 22 Sep 2025 19:36:36 +0200 Subject: [PATCH 40/54] Wallets commands --- .../src/bittensor/extrinsics/registration.py | 23 +++--- .../src/bittensor/extrinsics/transfer.py | 51 ++++++------- .../src/bittensor/subtensor_interface.py | 16 ++-- bittensor_cli/src/bittensor/utils.py | 1 + bittensor_cli/src/commands/wallets.py | 74 ++++++++++++++----- 5 files changed, 103 insertions(+), 62 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 2c2371761..29bbecb81 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -18,6 +18,7 @@ from typing import Optional import subprocess +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from Crypto.Hash import keccak import numpy as np @@ -1749,7 +1750,7 @@ async def swap_hotkey_extrinsic( new_wallet: Wallet, netuid: Optional[int] = None, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """ Performs an extrinsic update for swapping two hotkeys on the chain @@ -1770,14 +1771,14 @@ async def swap_hotkey_extrinsic( err_console.print( f":cross_mark: [red]Failed[/red]: Original hotkey {hk_ss58} is not registered on subnet {netuid}" ) - return False + return False, None elif not len(netuids_registered) > 0: err_console.print( f"Original hotkey [dark_orange]{hk_ss58}[/dark_orange] is not registered on any subnet. " f"Please register and try again" ) - return False + return False, None if netuid is not None: if netuid in netuids_registered_new_hotkey: @@ -1785,17 +1786,17 @@ async def swap_hotkey_extrinsic( f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} " f"is already registered on subnet {netuid}" ) - return False + return False, None else: if len(netuids_registered_new_hotkey) > 0: err_console.print( f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} " f"is already registered on subnet(s) {netuids_registered_new_hotkey}" ) - return False + return False, None if not unlock_key(wallet).success: - return False + return False, None if prompt: # Prompt user for confirmation. @@ -1815,7 +1816,7 @@ async def swap_hotkey_extrinsic( ) if not Confirm.ask(confirm_message): - return False + return False, None print_verbose( f"Swapping {wallet.name}'s hotkey ({hk_ss58} - {wallet.hotkey_str}) with " f"{new_wallet.name}'s hotkey ({new_hk_ss58} - {new_wallet.hotkey_str})" @@ -1832,15 +1833,17 @@ async def swap_hotkey_extrinsic( call_function="swap_hotkey", call_params=call_params, ) - success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if success: console.print( f"Hotkey {hk_ss58} ({wallet.hotkey_str}) swapped for new hotkey: " f"{new_hk_ss58} ({new_wallet.hotkey_str})" ) - return True + return True, ext_receipt else: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") time.sleep(0.5) - return False + return False, ext_receipt diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index ad3168a23..65ecfbcf8 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -1,10 +1,11 @@ import asyncio +from typing import Optional, Union +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich.prompt import Confirm from async_substrate_interface.errors import SubstrateRequestException -from bittensor_cli.src import NETWORK_EXPLORER_MAP from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( @@ -12,7 +13,6 @@ err_console, print_verbose, format_error_message, - get_explorer_url_for_network, is_valid_bittensor_address_or_public_key, print_error, unlock_key, @@ -30,7 +30,7 @@ async def transfer_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Transfers funds from this wallet to the destination public key address. :param subtensor: initialized SubtensorInterface object used for transfer @@ -75,7 +75,7 @@ async def get_transfer_fee() -> Balance: return Balance.from_rao(payment_info["partial_fee"]) - async def do_transfer() -> tuple[bool, str, str]: + async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: """ Makes transfer from wallet to destination public key address. :return: success, block hash, formatted error message @@ -95,27 +95,32 @@ async def do_transfer() -> tuple[bool, str, str]: ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "", "" + return True, "", "", response # Otherwise continue with finalization. if await response.is_success: block_hash_ = response.block_hash - return True, block_hash_, "" + return True, block_hash_, "", response else: - return False, "", format_error_message(await response.error_message) + return ( + False, + "", + format_error_message(await response.error_message), + response, + ) # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): err_console.print( f":cross_mark: [red]Invalid destination SS58 address[/red]:[bold white]\n {destination}[/bold white]" ) - return False + return False, None console.print(f"[dark_orange]Initiating transfer on network: {subtensor.network}") # Unlock wallet coldkey. if not unlock_key(wallet).success: - return False + return False, None - call_params = {"dest": destination} + call_params: dict[str, Optional[Union[str, int]]] = {"dest": destination} if transfer_all: call_function = "transfer_all" if allow_death: @@ -158,7 +163,7 @@ async def do_transfer() -> tuple[bool, str, str]: f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" f"You can try again with `--allow-death`." ) - return False + return False, None elif account_balance < (amount + fee) and allow_death: print_error( ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" @@ -166,7 +171,7 @@ async def do_transfer() -> tuple[bool, str, str]: f" amount: [bright_red]{amount}[/bright_red]\n" f" for fee: [bright_red]{fee}[/bright_red]" ) - return False + return False, None # Ask before moving on. if prompt: @@ -179,27 +184,15 @@ async def do_transfer() -> tuple[bool, str, str]: f":warning:[bright_yellow]Transferring is not the same as staking. To instead stake, use " f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]:warning:" ): - return False + return False, None - with console.status(":satellite: Transferring...", spinner="earth") as status: - success, block_hash, err_msg = await do_transfer() + with console.status(":satellite: Transferring...", spinner="earth"): + success, block_hash, err_msg, ext_receipt = await do_transfer() if success: console.print(":white_heavy_check_mark: [green]Finalized[/green]") console.print(f"[green]Block Hash: {block_hash}[/green]") - if subtensor.network == "finney": - print_verbose("Fetching explorer URLs", status) - explorer_urls = get_explorer_url_for_network( - subtensor.network, block_hash, NETWORK_EXPLORER_MAP - ) - if explorer_urls != {} and explorer_urls: - console.print( - f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]" - ) - console.print( - f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]" - ) else: console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") @@ -212,6 +205,6 @@ async def do_transfer() -> tuple[bool, str, str]: f"Balance:\n" f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) - return True + return True, ext_receipt - return False + return False, None diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 27df7d94c..d554fdbb4 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -4,6 +4,7 @@ from typing import Optional, Any, Union, TypedDict, Iterable import aiohttp +from async_substrate_interface import AsyncExtrinsicReceipt from async_substrate_interface.async_substrate import ( DiskCachedAsyncSubstrateInterface, AsyncSubstrateInterface, @@ -1081,7 +1082,7 @@ async def sign_and_send_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, era: Optional[dict[str, int]] = None, - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1093,7 +1094,10 @@ async def sign_and_send_extrinsic( :return: (success, error message) """ - call_args = {"call": call, "keypair": wallet.coldkey} + call_args: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = { + "call": call, + "keypair": wallet.coldkey, + } if era is not None: call_args["era"] = era extrinsic = await self.substrate.create_signed_extrinsic( @@ -1107,13 +1111,13 @@ async def sign_and_send_extrinsic( ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "" + return True, "", response if await response.is_success: - return True, "" + return True, "", response else: - return False, format_error_message(await response.error_message) + return False, format_error_message(await response.error_message), None except SubstrateRequestException as e: - return False, format_error_message(e) + return False, format_error_message(e), None async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: """ diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 80aab6916..3616f9e65 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -507,6 +507,7 @@ def get_explorer_url_for_network( :return: The explorer url for the given block hash and network """ + # TODO remove explorer_urls: dict[str, str] = {} # Will be None if the network is not known. i.e. not in network_map diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3b13a7fca..059f5865f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -98,7 +98,7 @@ async def associate_hotkey( ) with console.status(":satellite: Associating hotkey on-chain..."): - 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=True, @@ -116,6 +116,8 @@ async def associate_hotkey( f"wallet [blue]{wallet.name}[/blue], " f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]" ) + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic has been included as {ext_id}") return True @@ -1481,7 +1483,7 @@ async def transfer( json_output: bool, ): """Transfer token of amount to destination.""" - result = await transfer_extrinsic( + result, ext_receipt = await transfer_extrinsic( subtensor=subtensor, wallet=wallet, destination=destination, @@ -1491,8 +1493,13 @@ async def transfer( era=era, prompt=prompt, ) + ext_id = (await ext_receipt.get_extrinsic_identifier()) if result else None if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id}) + ) + else: + console.print(f"Your extrinsic has been included as {ext_id}") return result @@ -1682,15 +1689,23 @@ async def swap_hotkey( json_output: bool, ): """Swap your hotkey for all registered axons on the network.""" - result = await swap_hotkey_extrinsic( + result, ext_receipt = await swap_hotkey_extrinsic( subtensor, original_wallet, new_wallet, netuid=netuid, prompt=prompt, ) + if result: + ext_id = await ext_receipt.get_extrinsic_identifier() + else: + ext_id = None if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id}) + ) + else: + console.print(f"Your extrinsic has been included as {ext_id}") return result @@ -1731,7 +1746,7 @@ async def set_id( github_repo: str, prompt: bool, json_output: bool = False, -): +) -> bool: """Create a new or update existing identity on-chain.""" output_dict = {"success": False, "identity": None, "error": ""} identity_data = { @@ -1756,16 +1771,20 @@ async def set_id( with console.status( " :satellite: [dark_sea_green3]Updating identity on-chain...", spinner="earth" ): - success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if not success: err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") output_dict["error"] = err_msg if json_output: json_console.print(json.dumps(output_dict)) - return + return False else: console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic has been included as {ext_id}") output_dict["success"] = True identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) @@ -1774,9 +1793,12 @@ async def set_id( for key, value in identity.items(): table.add_row(key, str(value) if value else "~") output_dict["identity"] = identity - console.print(table) + output_dict["extrinsic_identifier"] = ext_id if json_output: json_console.print(json.dumps(output_dict)) + else: + console.print(table) + return True async def get_id( @@ -2016,9 +2038,9 @@ async def schedule_coldkey_swap( }, ), ) - + swap_info = None with console.status(":satellite: Scheduling coldkey swap on-chain..."): - 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=True, @@ -2033,13 +2055,31 @@ async def schedule_coldkey_swap( console.print( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) + console.print( + f"Your extrinsic has been included as {await ext_receipt.get_extrinsic_identifier()}" + ) + for event in await ext_receipt.triggered_events: + if ( + event.get("event", {}).get("module_id") == "SubtensorModule" + and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled" + ): + attributes = event["event"].get("attributes", {}) + old_coldkey = decode_account_id(attributes["old_coldkey"][0]) - swap_info = await find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=block_pre_call, - end_block=block_post_call, - wallet_ss58=wallet.coldkeypub.ss58_address, - ) + if old_coldkey == wallet.coldkeypub.ss58_address: + swap_info = { + "block_num": block_pre_call, + "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), + "execution_block": attributes["execution_block"], + } + + if not swap_info: + swap_info = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, + ) if not swap_info: console.print( From 84540d27cd6565a2cc8c743a611c117a0abb0e3f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 22 Sep 2025 22:35:34 +0200 Subject: [PATCH 41/54] Stake commands --- bittensor_cli/cli.py | 24 ++-- bittensor_cli/src/commands/stake/add.py | 42 ++++--- .../src/commands/stake/children_hotkeys.py | 106 ++++++++++-------- bittensor_cli/src/commands/stake/move.py | 71 +++++++----- bittensor_cli/src/commands/stake/remove.py | 52 ++++++--- 5 files changed, 181 insertions(+), 114 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4484f5288..173cfca97 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4379,7 +4379,7 @@ def stake_move( f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" ) - result = self._run_command( + result, ext_id = self._run_command( move_stake.move_stake( subtensor=self.initialize_chain(network), wallet=wallet, @@ -4395,7 +4395,9 @@ def stake_move( ) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) return result def stake_transfer( @@ -4555,7 +4557,7 @@ def stake_transfer( f"era: {period}\n" f"stake_all: {stake_all}" ) - result = self._run_command( + result, ext_id = self._run_command( move_stake.transfer_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -4571,7 +4573,9 @@ def stake_transfer( ) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) return result def stake_swap( @@ -4675,7 +4679,7 @@ def stake_swap( f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) - result = self._run_command( + result, ext_id = self._run_command( move_stake.swap_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -4691,7 +4695,9 @@ def stake_swap( ) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) return result def stake_get_children( @@ -4990,7 +4996,7 @@ def stake_childkey_take( f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) - results: list[tuple[Optional[int], bool]] = self._run_command( + results: list[tuple[Optional[int], bool, Optional[str]]] = self._run_command( children_hotkeys.childkey_take( wallet=wallet, subtensor=self.initialize_chain(network), @@ -5004,8 +5010,8 @@ def stake_childkey_take( ) if json_output: output = {} - for netuid_, success in results: - output[netuid_] = success + for netuid_, success, ext_id in results: + output[netuid_] = {"success": success, "extrinsic_identifier": ext_id} json_console.print(json.dumps(output)) return results diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 18a507c6d..2a4982c31 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -4,6 +4,8 @@ from functools import partial from typing import TYPE_CHECKING, Optional + +from async_substrate_interface import AsyncExtrinsicReceipt from rich.table import Table from rich.prompt import Confirm, Prompt @@ -112,7 +114,7 @@ async def safe_stake_extrinsic( hotkey_ss58_: str, price_limit: Balance, status=None, - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}" @@ -153,15 +155,18 @@ async def safe_stake_extrinsic( else: err_msg = f"{failure_prelude} with error: {format_error_message(e)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None if not await response.is_success: err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None else: if json_output: # the rest of this checking is not necessary if using json_output - return True, "" + return True, "", response + console.print( + f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" + ) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -199,11 +204,11 @@ async def safe_stake_extrinsic( f":arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" ) - return True, "" + return True, "", response async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: err_out = partial(print_error, status=status) current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), @@ -231,16 +236,19 @@ async def stake_extrinsic( except SubstrateRequestException as e: err_msg = f"{failure_prelude} with error: {format_error_message(e)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None else: if not await response.is_success: err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None else: if json_output: # the rest of this is not necessary if using json_output - return True, "" + return True, "", response + console.print( + f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" + ) new_block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance( @@ -269,7 +277,7 @@ async def stake_extrinsic( f":arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" ) - return True, "" + return True, "", response netuids = ( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() @@ -470,15 +478,23 @@ async def stake_extrinsic( } successes = defaultdict(dict) error_messages = defaultdict(dict) + extrinsic_ids = defaultdict(dict) with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): # We can gather them all at once but balance reporting will be in race-condition. for (ni, staking_address), coroutine in stake_coroutines.items(): - success, er_msg = await coroutine + success, er_msg, ext_receipt = await coroutine successes[ni][staking_address] = success error_messages[ni][staking_address] = er_msg + extrinsic_ids[ni][ + staking_address + ] = await ext_receipt.get_extrinsic_identifier() if json_output: - json_console.print( - json.dumps({"staking_success": successes, "error_messages": error_messages}) + json_console.print_json( + data={ + "staking_success": successes, + "error_messages": error_messages, + "extrinsic_ids": extrinsic_ids, + } ) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index d01e8d147..12a5ed450 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -59,7 +59,7 @@ async def set_children_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """ Sets children hotkeys with proportions assigned from the parent. @@ -74,7 +74,7 @@ async def set_children_extrinsic( `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. :param: prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: A tuple containing a success flag and an optional error message. + :return: A tuple containing a success flag, an optional error message, and the extrinsic identifier """ # Check if all children are being revoked all_revoked = len(children_with_proportions) == 0 @@ -87,7 +87,7 @@ async def set_children_extrinsic( if not Confirm.ask( f"Do you want to revoke all children hotkeys for hotkey {hotkey} on netuid {netuid}?" ): - return False, "Operation Cancelled" + return False, "Operation Cancelled", None else: if not Confirm.ask( "Do you want to set children hotkeys:\n[bold white]{}[/bold white]?".format( @@ -97,11 +97,11 @@ async def set_children_extrinsic( ) ) ): - return False, "Operation Cancelled" + return False, "Operation Cancelled", None # Decrypt coldkey. if not (unlock_status := unlock_key(wallet, print_out=False)).success: - return False, unlock_status.message + return False, unlock_status.message, "" with console.status( f":satellite: {operation} on [white]{subtensor.network}[/white] ..." @@ -120,7 +120,7 @@ async def set_children_extrinsic( "netuid": netuid, }, ) - success, error_message = await subtensor.sign_and_send_extrinsic( + success, error_message, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization ) @@ -128,17 +128,20 @@ async def set_children_extrinsic( return ( True, f"Not waiting for finalization or inclusion. {operation} initiated.", + None, ) if success: - if wait_for_inclusion: - console.print(":white_heavy_check_mark: [green]Included[/green]") + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") + modifier = "included" if wait_for_finalization: console.print(":white_heavy_check_mark: [green]Finalized[/green]") - return True, f"Successfully {operation.lower()} and Finalized." + modifier = "finalized" + return True, f"{operation} successfully {modifier}.", ext_id else: err_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") - return False, error_message + return False, error_message, None async def set_childkey_take_extrinsic( @@ -150,7 +153,7 @@ async def set_childkey_take_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """ Sets childkey take. @@ -165,7 +168,7 @@ async def set_childkey_take_extrinsic( `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. :param: prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: A tuple containing a success flag and an optional error message. + :return: A tuple containing a success flag, an optional error message, and an optional extrinsic identifier """ # Ask before moving on. @@ -173,11 +176,11 @@ async def set_childkey_take_extrinsic( if not Confirm.ask( f"Do you want to set childkey take to: [bold white]{take * 100}%[/bold white]?" ): - return False, "Operation Cancelled" + return False, "Operation Cancelled", None # Decrypt coldkey. if not (unlock_status := unlock_key(wallet, print_out=False)).success: - return False, unlock_status.message + return False, unlock_status.message, None with console.status( f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..." @@ -186,7 +189,7 @@ async def set_childkey_take_extrinsic( if 0 <= take <= 0.18: take_u16 = float_to_u16(take) else: - return False, "Invalid take value" + return False, "Invalid take value", None call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -197,7 +200,11 @@ async def set_childkey_take_extrinsic( "netuid": netuid, }, ) - success, error_message = await subtensor.sign_and_send_extrinsic( + ( + success, + error_message, + ext_receipt, + ) = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization ) @@ -205,30 +212,34 @@ async def set_childkey_take_extrinsic( return ( True, "Not waiting for finalization or inclusion. Set childkey take initiated.", + None, ) if success: - if wait_for_inclusion: - console.print(":white_heavy_check_mark: [green]Included[/green]") + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") + modifier = "included" if wait_for_finalization: + modifier = "finalized" console.print(":white_heavy_check_mark: [green]Finalized[/green]") # bittensor.logging.success( # prefix="Setting childkey take", # suffix="Finalized: " + str(success), # ) - return True, "Successfully set childkey take and Finalized." + return True, f"Successfully {modifier} childkey take", ext_id else: console.print(f":cross_mark: [red]Failed[/red]: {error_message}") # bittensor.logging.warning( # prefix="Setting childkey take", # suffix="Failed: " + str(error_message), # ) - return False, error_message + return False, error_message, None except SubstrateRequestException as e: return ( False, f"Exception occurred while setting childkey take: {format_error_message(e)}", + None, ) @@ -519,7 +530,7 @@ async def set_children( children_with_proportions = list(zip(proportions, children)) successes = {} if netuid is not None: - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -534,6 +545,7 @@ async def set_children( "error": message, "completion_block": None, "set_block": None, + "extrinsic_identifier": ext_id, } # Result if success: @@ -561,7 +573,7 @@ async def set_children( if netuid_ == 0: # dont include root network continue console.print(f"Setting children on netuid {netuid_}.") - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid_, @@ -579,6 +591,7 @@ async def set_children( "error": message, "completion_block": completion_block, "set_block": current_block, + "extrinsic_identifier": ext_id, } console.print( f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around " @@ -605,7 +618,7 @@ async def revoke_children( """ dict_output = {} if netuid is not None: - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -620,6 +633,7 @@ async def revoke_children( "error": message, "set_block": None, "completion_block": None, + "extrinsic_identifier": ext_id, } # Result @@ -644,10 +658,10 @@ async def revoke_children( if netuid_ == 0: # dont include root network continue console.print(f"Revoking children from netuid {netuid_}.") - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, - netuid=netuid, + netuid=netuid, # TODO should this be able to allow netuid = None ? hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], prompt=prompt, @@ -659,6 +673,7 @@ async def revoke_children( "error": message, "set_block": None, "completion_block": None, + "extrinsic_identifier": ext_id, } if success: current_block, completion_block = await get_childkey_completion_block( @@ -688,12 +703,12 @@ async def childkey_take( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, -) -> list[tuple[Optional[int], bool]]: +) -> list[tuple[Optional[int], bool, Optional[str]]]: """ Get or Set childkey take. Returns: - List of (netuid, success) for specified netuid (or all) and their success in setting take + List of (netuid, success, extrinsic identifier) for specified netuid (or all) and their success in setting take """ def validate_take_value(take_value: float) -> bool: @@ -741,9 +756,11 @@ async def chk_all_subnets(ss58): console.print(table) - async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: + async def set_chk_take_subnet( + subnet: int, chk_take: float + ) -> tuple[bool, Optional[str]]: """Set the childkey take for a single subnet""" - success, message = await set_childkey_take_extrinsic( + success, message, ext_id = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, netuid=subnet, @@ -759,12 +776,12 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: console.print( f"The childkey take for {get_hotkey_pub_ss58(wallet)} is now set to {take * 100:.2f}%." ) - return True + return True, ext_id else: console.print( f":cross_mark:[red] Unable to set childkey take.[/red] {message}" ) - return False + return False, ext_id # Print childkey take for other user and return (dont offer to change take rate) wallet_hk = get_hotkey_pub_ss58(wallet) @@ -778,7 +795,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return [(netuid, False)] + return [(netuid, False, None)] else: # show child hotkey take on all subnets await chk_all_subnets(hotkey) @@ -786,12 +803,12 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return [(netuid, False)] + return [(netuid, False, None)] # Validate child SS58 addresses if not take: if not Confirm.ask("Would you like to change the child take?"): - return [(netuid, False)] + return [(netuid, False, None)] new_take_value = -1.0 while not validate_take_value(new_take_value): new_take_value = FloatPrompt.ask( @@ -800,22 +817,21 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: take = new_take_value else: if not validate_take_value(take): - return [(netuid, False)] + return [(netuid, False, None)] if netuid: - return [(netuid, await set_chk_take_subnet(subnet=netuid, chk_take=take))] + success, ext_id = await set_chk_take_subnet(subnet=netuid, chk_take=take) + return [(netuid, success, ext_id)] else: new_take_netuids = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) if new_take_netuids: - return [ - ( - new_take_netuids, - await set_chk_take_subnet(subnet=new_take_netuids, chk_take=take), - ) - ] + success, ext_id = await set_chk_take_subnet( + subnet=new_take_netuids, chk_take=take + ) + return [(new_take_netuids, success, ext_id)] else: netuids = await subtensor.get_all_subnet_netuids() @@ -823,8 +839,8 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: for netuid_ in netuids: if netuid_ == 0: continue - console.print(f"Sending to netuid {netuid_} take of {take * 100:.2f}%") - result = await set_childkey_take_extrinsic( + console.print(f"Setting take of {take * 100:.2f}% on netuid {netuid_}.") + result, _, ext_id = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid_, @@ -834,7 +850,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: wait_for_inclusion=True, wait_for_finalization=False, ) - output_list.append((netuid_, result)) + output_list.append((netuid_, result, ext_id)) console.print( f":white_heavy_check_mark: [green]Sent childkey take of {take * 100:.2f}% to all subnets.[/green]" ) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index b4360ffdf..fdbd5556a 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -436,12 +436,12 @@ async def move_stake( era: int, interactive_selection: bool = False, prompt: bool = True, -) -> bool: +) -> tuple[bool, str]: if interactive_selection: try: selection = await stake_move_transfer_selection(subtensor, wallet) except ValueError: - return False + return False, "" origin_hotkey = selection["origin_hotkey"] origin_netuid = selection["origin_netuid"] amount = selection["amount"] @@ -472,7 +472,7 @@ async def move_stake( f"in Netuid: " f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" ) - return False + return False, "" console.print( f"\nOrigin Netuid: " @@ -507,7 +507,7 @@ async def move_stake( f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" f"{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) - return False + return False, "" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -545,13 +545,13 @@ async def move_stake( extrinsic_fee=extrinsic_fee, ) except ValueError: - return False + return False, "" if not Confirm.ask("Would you like to continue?"): - return False + return False, "" # Perform moving operation. if not unlock_key(wallet).success: - return False + return False, "" with console.status( f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: " f"[blue]{origin_netuid}[/blue] \nto " @@ -563,17 +563,20 @@ async def move_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + console.print( + f"Your extrinsic has been included at {(ext_id := await response.get_extrinsic_identifier())}" + ) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True + return True, ext_id else: if not await response.is_success: err_console.print( f"\n:cross_mark: [red]Failed[/red] with error:" f" {format_error_message(await response.error_message)}" ) - return False + return False, "" else: console.print( ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" @@ -605,7 +608,7 @@ async def move_stake( f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" ) - return True + return True, ext_id async def transfer_stake( @@ -620,7 +623,7 @@ async def transfer_stake( interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, -) -> bool: +) -> tuple[bool, str]: """Transfers stake from one network to another. Args: @@ -653,11 +656,11 @@ async def transfer_stake( ) if not dest_exists: err_console.print(f"[red]Subnet {dest_netuid} does not exist[/red]") - return False + return False, "" if not origin_exists: err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]") - return False + return False, "" # Get current stake balances with console.status(f"Retrieving stake data from {subtensor.network}..."): @@ -676,7 +679,7 @@ async def transfer_stake( err_console.print( f"[red]No stake found for hotkey: {origin_hotkey} on netuid: {origin_netuid}[/red]" ) - return False + return False, "" if amount: amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid) @@ -694,7 +697,7 @@ async def transfer_stake( f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < " f"Transfer amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_transfer}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) - return False + return False, "" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -732,14 +735,14 @@ async def transfer_stake( extrinsic_fee=extrinsic_fee, ) except ValueError: - return False + return False, "" if not Confirm.ask("Would you like to continue?"): - return False + return False, "" # Perform transfer operation if not unlock_key(wallet).success: - return False + return False, "" with console.status("\n:satellite: Transferring stake ..."): extrinsic = await subtensor.substrate.create_signed_extrinsic( @@ -749,17 +752,20 @@ async def transfer_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + console.print( + f"Your extrinsic has been included as {(ext_id := await response.get_extrinsic_identifier())}" + ) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True + return True, ext_id if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " f"{format_error_message(await response.error_message)}" ) - return False + return False, "" # Get and display new stake balances new_stake, new_dest_stake = await asyncio.gather( @@ -783,7 +789,7 @@ async def transfer_stake( f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" ) - return True + return True, ext_id async def swap_stake( @@ -798,7 +804,7 @@ async def swap_stake( prompt: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> bool: +) -> tuple[bool, str]: """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. Args: @@ -833,11 +839,11 @@ async def swap_stake( ) if not dest_exists: err_console.print(f"[red]Subnet {destination_netuid} does not exist[/red]") - return False + return False, "" if not origin_exists: err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]") - return False + return False, "" # Get current stake balances with console.status(f"Retrieving stake data from {subtensor.network}..."): @@ -864,7 +870,7 @@ async def swap_stake( f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < " f"Swap amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_swap}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) - return False + return False, "" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -901,14 +907,14 @@ async def swap_stake( extrinsic_fee=extrinsic_fee, ) except ValueError: - return False + return False, "" if not Confirm.ask("Would you like to continue?"): - return False + return False, "" # Perform swap operation if not unlock_key(wallet).success: - return False + return False, "" with console.status( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " @@ -923,17 +929,20 @@ async def swap_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + console.print( + f"Your extrinsic has been included as {(ext_id := await response.get_extrinsic_identifier())}" + ) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True + return True, ext_id if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " f"{format_error_message(await response.error_message)}" ) - return False + return False, "" # Get and display new stake balances new_stake, new_dest_stake = await asyncio.gather( @@ -957,4 +966,4 @@ async def swap_stake( f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" ) - return True + return True, ext_id diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index ecec77fa5..ddc7c70ce 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt from rich.table import Table @@ -134,7 +135,8 @@ async def unstake( skip_remaining_subnets = False if len(netuids) > 1 and not amount: console.print( - "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n" + "[dark_sea_green3]Tip: Enter 'q' any time to stop going over " + "remaining subnets and process current unstakes.\n" ) # Iterate over hotkeys and netuids to collect unstake operations @@ -335,7 +337,8 @@ async def unstake( func = _unstake_extrinsic specific_args = {"current_stake": op["current_stake_balance"]} - suc = await func(**common_args, **specific_args) + suc, ext_receipt = await func(**common_args, **specific_args) + ext_id = await ext_receipt.get_extrinsic_identifier() if suc else None successes.append( { @@ -343,6 +346,7 @@ async def unstake( "hotkey_ss58": op["hotkey_ss58"], "unstake_amount": op["amount_to_unstake"].tao, "success": suc, + "extrinsic_identifier": ext_id, } ) @@ -350,7 +354,7 @@ async def unstake( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) if json_output: - json_console.print(json.dumps(successes)) + json_console.print_json(data=successes) return True @@ -533,7 +537,7 @@ async def unstake_all( successes = {} with console.status("Unstaking all stakes...") as status: for hotkey_ss58 in hotkey_ss58s: - successes[hotkey_ss58] = await _unstake_all_extrinsic( + success, ext_receipt = await _unstake_all_extrinsic( wallet=wallet, subtensor=subtensor, hotkey_ss58=hotkey_ss58, @@ -542,6 +546,11 @@ async def unstake_all( status=status, era=era, ) + ext_id = await ext_receipt.get_extrinsic_identifier() if successes else None + successes[hotkey_ss58] = { + "success": success, + "extrinsic_identifier": ext_id, + } if json_output: return json_console.print(json.dumps({"success": successes})) @@ -556,7 +565,7 @@ async def _unstake_extrinsic( hotkey_ss58: str, status=None, era: int = 3, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a standard unstake extrinsic. Args: @@ -604,8 +613,11 @@ async def _unstake_extrinsic( f"{failure_prelude} with error: " f"{format_error_message(await response.error_message)}" ) - return False + return False, None # Fetch latest balance and stake + console.print( + f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" + ) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -625,11 +637,11 @@ async def _unstake_extrinsic( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) - return True + return True, response except Exception as e: err_out(f"{failure_prelude} with error: {str(e)}") - return False + return False, None async def _safe_unstake_extrinsic( @@ -642,7 +654,7 @@ async def _safe_unstake_extrinsic( allow_partial_stake: bool, status=None, era: int = 3, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a safe unstake extrinsic with price limit. Args: @@ -708,14 +720,16 @@ async def _safe_unstake_extrinsic( ) else: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return False + return False, None if not await response.is_success: err_out( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) - return False - + return False, None + console.print( + f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" + ) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -745,7 +759,7 @@ async def _safe_unstake_extrinsic( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) - return True + return True, response async def _unstake_all_extrinsic( @@ -756,7 +770,7 @@ async def _unstake_all_extrinsic( unstake_all_alpha: bool, status=None, era: int = 3, -) -> None: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute an unstake all extrinsic. Args: @@ -774,7 +788,7 @@ async def _unstake_all_extrinsic( if status: status.update( - f"\n:satellite: {'Unstaking all Alpha stakes' if unstake_all_alpha else 'Unstaking all stakes'} from {hotkey_name} ..." + f"\n:satellite: Unstaking all {'Alpha ' if unstake_all_alpha else ''}stakes from {hotkey_name} ..." ) block_hash = await subtensor.substrate.get_chain_head() @@ -817,7 +831,11 @@ async def _unstake_all_extrinsic( f"{failure_prelude} with error: " f"{format_error_message(await response.error_message)}" ) - return + return False, None + else: + console.print( + f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" + ) # Fetch latest balance and stake block_hash = await subtensor.substrate.get_chain_head() @@ -855,9 +873,11 @@ async def _unstake_all_extrinsic( f"[blue]{previous_root_stake}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_root_stake}" ) + return True, response except Exception as e: err_out(f"{failure_prelude} with error: {str(e)}") + return False, None async def _get_extrinsic_fee( From 106c741e335e24d2159f73f9d19c6339ec503c20 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 22 Sep 2025 22:54:16 +0200 Subject: [PATCH 42/54] Sudo WIP --- bittensor_cli/cli.py | 7 ++-- bittensor_cli/src/commands/sudo.py | 59 +++++++++++++++++------------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 173cfca97..33691a385 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5134,7 +5134,7 @@ def sudo_set( f"param_name: {param_name}\n" f"param_value: {param_value}" ) - result, err_msg = self._run_command( + result, err_msg, ext_id = self._run_command( sudo.sudo_set_hyperparameter( wallet, self.initialize_chain(network), @@ -5146,7 +5146,7 @@ def sudo_set( ) ) if json_output: - json_console.print(json.dumps({"success": result, "err_msg": err_msg})) + json_console.print(json.dumps({"success": result, "err_msg": err_msg, "extrinsic_identifier": ext_id})) return result def sudo_get( @@ -5231,7 +5231,7 @@ def sudo_senate_vote( None, "--vote-aye/--vote-nay", prompt="Enter y to vote Aye, or enter n to vote Nay", - help="The vote casted on the proposal", + help="The vote cast on the proposal", ), ): """ @@ -5254,7 +5254,6 @@ def sudo_senate_vote( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - logger.debug(f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\n") return self._run_command( sudo.senate_vote( wallet, self.initialize_chain(network), proposal, vote, prompt diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index bb4bb43de..83977906e 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -178,7 +178,7 @@ async def set_hyperparameter_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Sets a hyperparameter for a specific subnetwork. :param subtensor: initialized SubtensorInterface object @@ -191,8 +191,11 @@ async def set_hyperparameter_extrinsic( :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. - :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for + :return: tuple including: + success: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. + message: error message if the extrinsic failed + extrinsic_identifier: optional extrinsic identifier if the extrinsic was included """ print_verbose("Confirming subnet owner") subnet_owner = await subtensor.query( @@ -205,10 +208,10 @@ async def set_hyperparameter_extrinsic( ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" ) err_console.print(err_msg) - return False, err_msg + return False, err_msg, None if not (ulw := unlock_key(wallet)).success: - return False, ulw.message + return False, ulw.message, None arbitrary_extrinsic = False @@ -227,7 +230,7 @@ async def set_hyperparameter_extrinsic( if not Confirm.ask( "This hyperparam is only settable by root sudo users. If you are not, this will fail. Please confirm" ): - return False, "This hyperparam is only settable by root sudo users" + return False, "This hyperparam is only settable by root sudo users", None substrate = subtensor.substrate msg_value = value if not arbitrary_extrinsic else call_params @@ -259,7 +262,7 @@ async def set_hyperparameter_extrinsic( "Not enough values provided in the list for all parameters" ) err_console.print(err_msg) - return False, err_msg + return False, err_msg, None call_params.update( {name: val for name, val in zip(non_netuid_fields, value)} @@ -287,25 +290,28 @@ async def set_hyperparameter_extrinsic( ) else: call = call_ - 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_finalization ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False, err_msg - elif arbitrary_extrinsic: - console.print( - f":white_heavy_check_mark: " - f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]" - ) - return True, "" - # Successful registration, final check for membership + return False, err_msg, None else: - console.print( - f":white_heavy_check_mark: " - f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" - ) - return True, "" + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") + if arbitrary_extrinsic: + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]" + ) + return True, "", ext_id + # Successful registration, final check for membership + else: + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" + ) + return True, "", ext_id async def _get_senate_members( @@ -506,7 +512,7 @@ async def vote_senate_extrinsic( "approve": vote, }, ) - 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_finalization ) if not success: @@ -515,6 +521,7 @@ async def vote_senate_extrinsic( return False # Successful vote, final check for data else: + console.print(f"Your extrinsic was included as {await ext_receipt.get_extrinsic_identifier()}") if vote_data := await subtensor.get_vote_data(proposal_hash): hotkey_ss58 = get_hotkey_pub_ss58(wallet) if ( @@ -621,7 +628,7 @@ async def sudo_set_hyperparameter( param_value: Optional[str], prompt: bool, json_output: bool, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Set subnet hyperparameters.""" is_allowed_value, value = allowed_value(param_name, param_value) if not is_allowed_value: @@ -630,17 +637,17 @@ async def sudo_set_hyperparameter( f"Value is {param_value} but must be {value}" ) err_console.print(err_msg) - return False, err_msg - success, err_msg = await set_hyperparameter_extrinsic( + return False, err_msg, None + success, err_msg, ext_id = await set_hyperparameter_extrinsic( subtensor, wallet, netuid, param_name, value, prompt=prompt ) if json_output: - return success, err_msg + return success, err_msg, ext_id if success: console.print("\n") print_verbose("Fetching hyperparameters") await get_hyperparameters(subtensor, netuid=netuid) - return success, err_msg + return success, err_msg, ext_id async def get_hyperparameters( From 5df9717ec4445c417d6d1274cdcc1ea1aed2944c Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 22 Sep 2025 23:19:28 +0200 Subject: [PATCH 43/54] Sudo Commands --- bittensor_cli/cli.py | 16 ++++++-- bittensor_cli/src/commands/sudo.py | 62 +++++++++++++++++++----------- 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 33691a385..d1f3a232e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5146,7 +5146,15 @@ def sudo_set( ) ) if json_output: - json_console.print(json.dumps({"success": result, "err_msg": err_msg, "extrinsic_identifier": ext_id})) + json_console.print( + json.dumps( + { + "success": result, + "err_msg": err_msg, + "extrinsic_identifier": ext_id, + } + ) + ) return result def sudo_get( @@ -5307,11 +5315,13 @@ def sudo_set_take( ) raise typer.Exit() logger.debug(f"args:\nnetwork: {network}\ntake: {take}") - result = self._run_command( + result, ext_id = self._run_command( sudo.set_take(wallet, self.initialize_chain(network), take) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id}) + ) return result def sudo_get_take( diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 83977906e..c431ff941 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -521,7 +521,9 @@ async def vote_senate_extrinsic( return False # Successful vote, final check for data else: - console.print(f"Your extrinsic was included as {await ext_receipt.get_extrinsic_identifier()}") + console.print( + f"Your extrinsic was included as {await ext_receipt.get_extrinsic_identifier()}" + ) if vote_data := await subtensor.get_vote_data(proposal_hash): hotkey_ss58 = get_hotkey_pub_ss58(wallet) if ( @@ -545,7 +547,7 @@ async def set_take_extrinsic( wallet: Wallet, delegate_ss58: str, take: float = 0.0, -) -> bool: +) -> tuple[bool, Optional[str]]: """ Set delegate hotkey take @@ -570,7 +572,7 @@ async def set_take_extrinsic( if take_u16 == current_take_u16: console.print("Nothing to do, take hasn't changed") - return True + return True, None if current_take_u16 < take_u16: console.print( @@ -588,7 +590,9 @@ async def set_take_extrinsic( "take": take_u16, }, ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) else: console.print( @@ -606,15 +610,20 @@ async def set_take_extrinsic( "take": take_u16, }, ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if not success: err_console.print(err) + ext_id = None else: console.print( - ":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]" + ":white_heavy_check_mark: [dark_sea_green_3]Success[/dark_sea_green_3]" ) - return success + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic was included at {ext_id}") + return success, ext_id # commands @@ -914,13 +923,13 @@ async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) async def set_take( wallet: Wallet, subtensor: "SubtensorInterface", take: float -) -> bool: +) -> tuple[bool, Optional[str]]: """Set delegate take.""" - async def _do_set_take() -> bool: + async def _do_set_take() -> tuple[bool, Optional[str]]: if take > 0.18 or take < 0: err_console.print("ERROR: Take value should not exceed 18% or be below 0%") - return False + return False, None block_hash = await subtensor.substrate.get_chain_head() hotkey_ss58 = get_hotkey_pub_ss58(wallet) @@ -933,35 +942,34 @@ async def _do_set_take() -> bool: f" any subnet. Please register using [{COLOR_PALETTE.G.SUBHEAD}]`btcli subnets register`" f"[{COLOR_PALETTE.G.SUBHEAD}] and try again." ) - return False + return False, None - result: bool = await set_take_extrinsic( + result: tuple[bool, Optional[str]] = await set_take_extrinsic( subtensor=subtensor, wallet=wallet, delegate_ss58=hotkey_ss58, take=take, ) + success, ext_id = result - if not result: + if not success: err_console.print("Could not set the take") - return False + return False, None else: new_take = await get_current_take(subtensor, wallet) console.print( f"New take is [{COLOR_PALETTE.P.RATE}]{new_take * 100.0:.2f}%" ) - return True + return True, ext_id console.print( f"Setting take on [{COLOR_PALETTE.G.LINKS}]network: {subtensor.network}" ) if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success: - return False - - result_ = await _do_set_take() + return False, None - return result_ + return await _do_set_take() async def trim( @@ -1000,20 +1008,30 @@ async def trim( call_function="sudo_trim_to_max_allowed_uids", call_params={"netuid": netuid, "max_n": max_n}, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": period} ) if not success: if json_output: - json_console.print_json(data={"success": False, "message": err_msg}) + json_console.print_json( + data={ + "success": False, + "message": err_msg, + "extrinsic_identifier": None, + } + ) else: err_console.print(f":cross_mark: [red]{err_msg}[/red]") return False else: + ext_id = await ext_receipt.get_extrinsic_identifier() msg = f"Successfully trimmed UIDs on SN{netuid} to {max_n}" if json_output: - json_console.print_json(data={"success": True, "message": msg}) + json_console.print_json( + data={"success": True, "message": msg, "extrinsic_identifier": ext_id} + ) else: + console.print(f"Your extrinsic was included as {ext_id}") console.print( f":white_heavy_check_mark: [dark_sea_green3]{msg}[/dark_sea_green3]" ) From b3b5465489f45e925ac4ba05e905b65f6af15cbc Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 22 Sep 2025 23:41:33 +0200 Subject: [PATCH 44/54] Subnets commands --- bittensor_cli/cli.py | 7 +- .../src/bittensor/extrinsics/registration.py | 16 +-- .../src/bittensor/extrinsics/root.py | 18 ++-- bittensor_cli/src/commands/subnets/subnets.py | 100 ++++++++++++------ 4 files changed, 91 insertions(+), 50 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d1f3a232e..10cc56e80 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5262,6 +5262,7 @@ def sudo_senate_vote( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug(f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\n") return self._run_command( sudo.senate_vote( wallet, self.initialize_chain(network), proposal, vote, prompt @@ -5885,13 +5886,15 @@ def subnets_set_identity( logger.debug( f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}" ) - success = self._run_command( + success, ext_id = self._run_command( subnets.set_identity( wallet, self.initialize_chain(network), netuid, identity, prompt ) ) if json_output: - json_console.print(json.dumps({"success": success})) + json_console.print( + json.dumps({"success": success, "extrinsic_identifier": ext_id}) + ) def subnets_pow_register( self, diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 29bbecb81..df24e0471 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -680,7 +680,7 @@ async def burned_register_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, era: Optional[int] = None, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Registers the wallet to chain by recycling TAO. :param subtensor: The SubtensorInterface object to use for the call, initialized @@ -699,7 +699,7 @@ async def burned_register_extrinsic( """ if not (unlock_status := unlock_key(wallet, print_out=False)).success: - return False, unlock_status.message + return False, unlock_status.message, None with console.status( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", @@ -743,7 +743,7 @@ async def burned_register_extrinsic( f"hotkey: [{COLOR_PALETTE.G.HK}]{neuron.hotkey}[/{COLOR_PALETTE.G.HK}]\n" f"coldkey: [{COLOR_PALETTE.G.CK}]{neuron.coldkey}[/{COLOR_PALETTE.G.CK}]" ) - return True, "Already registered" + return True, "Already registered", None with console.status( ":satellite: Recycling TAO for Registration...", spinner="aesthetic" @@ -756,16 +756,18 @@ async def burned_register_extrinsic( "hotkey": get_hotkey_pub_ss58(wallet), }, ) - 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_finalization, era=era_ ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - return False, err_msg + return False, err_msg, None # Successful registration, final check for neuron and pubkey else: + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") with console.status(":satellite: Checking Balance...", spinner="aesthetic"): block_hash = await subtensor.substrate.get_chain_head() new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( @@ -792,13 +794,13 @@ async def burned_register_extrinsic( console.print( f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" ) - return True, f"Registered on {netuid} with UID {my_uid}" + return True, f"Registered on {netuid} with UID {my_uid}", ext_id else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False, "Unknown error. Neuron not found." + return False, "Unknown error. Neuron not found.", ext_id async def run_faucet_extrinsic( diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 207fb8642..d5f69bbfd 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -18,7 +18,7 @@ import asyncio import hashlib import time -from typing import Union, List, TYPE_CHECKING +from typing import Union, List, TYPE_CHECKING, Optional from bittensor_wallet import Wallet, Keypair import numpy as np @@ -291,7 +291,7 @@ async def root_register_extrinsic( wallet: Wallet, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: r"""Registers the wallet to root network. :param subtensor: The SubtensorInterface object @@ -307,7 +307,7 @@ async def root_register_extrinsic( """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None print_verbose(f"Checking if hotkey ({wallet.hotkey_str}) is registered on root") is_registered = await is_hotkey_registered( @@ -317,7 +317,7 @@ async def root_register_extrinsic( console.print( ":white_heavy_check_mark: [green]Already registered on root network.[/green]" ) - return True, "Already registered on root network" + return True, "Already registered on root network", None with console.status(":satellite: Registering to root network...", spinner="earth"): call = await subtensor.substrate.compose_call( @@ -325,7 +325,7 @@ async def root_register_extrinsic( call_function="root_register", call_params={"hotkey": get_hotkey_pub_ss58(wallet)}, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, @@ -335,10 +335,12 @@ async def root_register_extrinsic( if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - return False, err_msg + return False, err_msg, None # Successful registration, final check for neuron and pubkey else: + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") uid = await subtensor.query( module="SubtensorModule", storage_function="Uids", @@ -348,13 +350,13 @@ async def root_register_extrinsic( console.print( f":white_heavy_check_mark: [green]Registered with UID {uid}[/green]" ) - return True, f"Registered with UID {uid}" + return True, f"Registered with UID {uid}", ext_id else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False, "Unknown error. Neuron not found." + return False, "Unknown error. Neuron not found.", ext_id async def set_root_weights_extrinsic( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d8571f3f6..bf94ddb86 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -54,7 +54,7 @@ async def register_subnetwork_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, -) -> tuple[bool, Optional[int]]: +) -> tuple[bool, Optional[int], Optional[str]]: """Registers a new subnetwork. wallet (bittensor.wallet): @@ -66,9 +66,11 @@ async def register_subnetwork_extrinsic( prompt (bool): If true, the call waits for confirmation from the user before proceeding. Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or included in the block. - If we did not wait for finalization / inclusion, the response is ``true``. + tuple including: + success: Flag is `True` if extrinsic was finalized or included in the block. + If we did not wait for finalization/inclusion, the response is `True`. + error_message: Optional error message. + extrinsic_identifier: Optional extrinsic identifier, if the extrinsic was included. """ async def _find_event_attributes_in_extrinsic_receipt( @@ -103,7 +105,7 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}] " f"to register a subnet." ) - return False, None + return False, None, None if prompt: console.print( @@ -112,7 +114,7 @@ async def _find_event_attributes_in_extrinsic_receipt( if not Confirm.ask( f"Do you want to burn [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost} to register a subnet?" ): - return False, None + return False, None, None call_params = { "hotkey": get_hotkey_pub_ss58(wallet), @@ -157,10 +159,10 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n" f"Value '{value.decode()}' is {len(value)} bytes." ) - return False, None + return False, None, None if not unlock_key(wallet).success: - return False, None + return False, None, None with console.status(":satellite: Registering subnet...", spinner="earth"): substrate = subtensor.substrate @@ -181,24 +183,27 @@ async def _find_event_attributes_in_extrinsic_receipt( # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, None + return True, None, None if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" ) await asyncio.sleep(0.5) - return False, None + return False, None, None # Successful registration, final check for membership else: attributes = await _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) + console.print( + f"You extrinsic was included as {(ext_id := await response.get_extrinsic_identifier())}" + ) console.print( f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) - return True, int(attributes[0]) + return True, int(attributes[0]), ext_id # commands @@ -1486,13 +1491,17 @@ async def create( """Register a subnetwork""" # Call register command. - success, netuid = await register_subnetwork_extrinsic( + success, netuid, ext_id = await register_subnetwork_extrinsic( subtensor, wallet, subnet_identity, prompt=prompt ) if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present # (2025/04/03), we always use the default `wait_for_finalization=True`, so it will always have a netuid. - json_console.print(json.dumps({"success": success, "netuid": netuid})) + json_console.print( + json.dumps( + {"success": success, "netuid": netuid, "extrinsic_identifier": ext_id} + ) + ) return success if success and prompt: # Prompt for user to set identity. @@ -1584,9 +1593,11 @@ async def register( 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"} - ) + data={ + "success": False, + "msg": f"Subnet {netuid} does not exist", + "extrinsic_identifier": None, + } ) return @@ -1604,9 +1615,12 @@ async def register( # Check balance is sufficient if balance < current_recycle: - err_console.print( - f"[red]Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO[/red]" - ) + err_msg = f"Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO" + err_console.print(f"[red]{err_msg}[/red]") + if json_output: + json_console.print_json( + data={"success": False, "msg": err_msg, "extrinsic_identifier": None} + ) return if prompt and not json_output: @@ -1671,9 +1685,9 @@ async def register( return if netuid == 0: - success, msg = await root_register_extrinsic(subtensor, wallet=wallet) + success, msg, ext_id = await root_register_extrinsic(subtensor, wallet=wallet) else: - success, msg = await burned_register_extrinsic( + success, msg, ext_id = await burned_register_extrinsic( subtensor, wallet=wallet, netuid=netuid, @@ -1681,7 +1695,9 @@ async def register( era=era, ) if json_output: - json_console.print(json.dumps({"success": success, "msg": msg})) + json_console.print( + json.dumps({"success": success, "msg": msg, "extrinsic_identifier": ext_id}) + ) else: if not success: err_console.print(f"Failure: {msg}") @@ -2207,12 +2223,12 @@ async def set_identity( netuid: int, subnet_identity: dict, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[str]]: """Set identity information for a subnet""" if not await subtensor.subnet_exists(netuid): err_console.print(f"Subnet {netuid} does not exist") - return False + return False, None identity_data = { "netuid": netuid, @@ -2227,13 +2243,13 @@ async def set_identity( } if not unlock_key(wallet).success: - return False + return False, None if prompt: if not Confirm.ask( "Are you sure you want to set subnet's identity? This is subject to a fee." ): - return False + return False, None call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -2245,12 +2261,15 @@ async def set_identity( " :satellite: [dark_sea_green3]Setting subnet identity on-chain...", spinner="earth", ): - success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if not success: err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") - return False - + return False, None + ext_id = await ext_receipt.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") console.print( ":white_heavy_check_mark: [dark_sea_green3]Successfully set subnet identity\n" ) @@ -2275,7 +2294,7 @@ async def set_identity( table.add_row(key, str(value) if value else "~") console.print(table) - return True + return True, ext_id async def get_identity( @@ -2433,6 +2452,9 @@ async def start_subnet( ) if await response.is_success: + console.print( + f"Your extrinsic was included as {await response.get_extrinsic_identifier()}" + ) console.print( f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]" ) @@ -2467,7 +2489,9 @@ async def set_symbol( if not await subtensor.subnet_exists(netuid): err = f"Subnet {netuid} does not exist." if json_output: - json_console.print_json(data={"success": False, "message": err}) + json_console.print_json( + data={"success": False, "message": err, "extrinsic_identifier": None} + ) else: err_console.print(err) return False @@ -2503,16 +2527,26 @@ async def set_symbol( wait_for_inclusion=True, ) if await response.is_success: + ext_id = await response.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") message = f"Successfully updated SN{netuid}'s symbol to {symbol}." if json_output: - json_console.print_json(data={"success": True, "message": message}) + json_console.print_json( + data={ + "success": True, + "message": message, + "extrinsic_identifier": ext_id, + } + ) else: console.print(f":white_heavy_check_mark:[dark_sea_green3] {message}\n") return True else: err = format_error_message(await response.error_message) if json_output: - json_console.print_json(data={"success": False, "message": err}) + json_console.print_json( + data={"success": False, "message": err, "extrinsic_identifier": None} + ) else: err_console.print(f":cross_mark: [red]Failed[/red]: {err}") return False From 827a58dad4651152cd163713adcd97e559ee5b56 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 22 Sep 2025 23:55:31 +0200 Subject: [PATCH 45/54] Weights commands --- bittensor_cli/cli.py | 1 + bittensor_cli/src/commands/weights.py | 115 +++++++++++++++----------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 10cc56e80..20ddb90d1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6222,6 +6222,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ self.verbosity_handler(quiet, verbose, json_output) + # TODO think we need to ','.split uids and weights ? uids = list_prompt(uids, int, "UIDs of interest for the specified netuid") weights = list_prompt( weights, float, "Corresponding weights for the specified UIDs" diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 63e3b72f3..2db47f143 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -2,7 +2,7 @@ import json import os from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet import numpy as np @@ -54,7 +54,7 @@ def __init__( self.wait_for_inclusion = wait_for_inclusion self.wait_for_finalization = wait_for_finalization - async def set_weights_extrinsic(self) -> tuple[bool, str]: + async def set_weights_extrinsic(self) -> tuple[bool, str, Optional[str]]: """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or trust a neuron places on other neurons in the network, which is a fundamental aspect @@ -80,7 +80,7 @@ async def set_weights_extrinsic(self) -> tuple[bool, str]: f"Do you want to set weights:\n[bold white]" f" weights: {formatted_weight_vals}\n uids: {weight_uids}[/bold white ]?" ): - return False, "Prompt refused." + return False, "Prompt refused.", None # Check if the commit-reveal mechanism is active for the given netuid. if bool( @@ -104,7 +104,7 @@ async def commit_weights( self, uids: list[int], weights: list[int], - ) -> tuple[bool, str]: + ) -> tuple[bool, Optional[str], Optional[str]]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. This action serves as a commitment or snapshot of the neuron's current weight distribution. @@ -121,12 +121,6 @@ async def commit_weights( enhancing transparency and accountability within the Bittensor network. """ - # _logger.info( - # "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( - # netuid, uids, weights, version_key - # ) - # ) - # Generate the hash of the weights commit_hash = generate_weight_hash( address=get_hotkey_pub_ss58(self.wallet), @@ -139,18 +133,21 @@ async def commit_weights( # _logger.info("Commit Hash: {}".format(commit_hash)) try: - success, message = await self.do_commit_weights(commit_hash=commit_hash) + success, message, ext_id = await self.do_commit_weights( + commit_hash=commit_hash + ) except SubstrateRequestException as e: err_console.print(f"Error committing weights: {format_error_message(e)}") # bittensor.logging.error(f"Error committing weights: {e}") success = False message = "No attempt made. Perhaps it is too soon to commit weights!" + ext_id = None - return success, message + return success, message, ext_id async def _commit_reveal( self, weight_uids: list[int], weight_vals: list[int] - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[str]]: interval = int( await self.subtensor.get_hyperparameter( param_name="get_commit_reveal_period", @@ -165,7 +162,7 @@ async def _commit_reveal( self.salt = list(os.urandom(salt_length)) # Attempt to commit the weights to the blockchain. - commit_success, commit_msg = await self.commit_weights( + commit_success, commit_msg, ext_id = await self.commit_weights( uids=weight_uids, weights=weight_vals, ) @@ -209,36 +206,42 @@ async def _commit_reveal( console.print(f":cross_mark: [red]Failed[/red]: error:{commit_msg}") # bittensor.logging.error(msg=commit_msg, prefix="Set weights with hash commit", # suffix=f"Failed: {commit_msg}") - return False, f"Failed to commit weights hash. {commit_msg}" + return False, f"Failed to commit weights hash. {commit_msg}", None - async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str]: + async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str, Optional[str]]: # Attempt to reveal the weights using the salt. - success, msg = await self.reveal_weights_extrinsic(weight_uids, weight_vals) + success, msg, ext_id = await self.reveal_weights_extrinsic( + weight_uids, weight_vals + ) if success: if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, "Not waiting for finalization or inclusion.", ext_id console.print( ":white_heavy_check_mark: [green]Weights hash revealed on chain[/green]" ) # bittensor.logging.success(prefix="Weights hash revealed", suffix=str(msg)) - return True, "Successfully revealed previously commited weights hash." + return ( + True, + "Successfully revealed previously commited weights hash.", + ext_id, + ) else: # bittensor.logging.error( # msg=msg, # prefix=f"Failed to reveal previously commited weights hash for salt: {salt}", # suffix="Failed: ", # ) - return False, "Failed to reveal weights." + return False, "Failed to reveal weights.", None async def _set_weights_without_commit_reveal( self, weight_uids, weight_vals, - ) -> tuple[bool, str]: - async def _do_set_weights(): + ) -> tuple[bool, str, Optional[str]]: + async def _do_set_weights() -> tuple[bool, str, Optional[str]]: call = await self.subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="set_weights", @@ -262,37 +265,39 @@ async def _do_set_weights(): wait_for_finalization=self.wait_for_finalization, ) except SubstrateRequestException as e: - return False, format_error_message(e) + return False, format_error_message(e), None # We only wait here if we expect finalization. if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, "Not waiting for finalization or inclusion.", None if await response.is_success: - return True, "Successfully set weights." + ext_id = await response.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") + return True, "Successfully set weights.", ext_id else: - return False, format_error_message(await response.error_message) + return False, format_error_message(await response.error_message), None with console.status( f":satellite: Setting weights on [white]{self.subtensor.network}[/white] ..." ): - success, error_message = await _do_set_weights() + success, error_message, ext_id = await _do_set_weights() if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, "Not waiting for finalization or inclusion.", None if success: console.print(":white_heavy_check_mark: [green]Finalized[/green]") # bittensor.logging.success(prefix="Set weights", suffix="Finalized: " + str(success)) - return True, "Successfully set weights and finalized." + return True, "Successfully set weights and finalized.", ext_id else: # bittensor.logging.error(msg=error_message, prefix="Set weights", suffix="Failed: ") - return False, error_message + return False, error_message, None async def reveal_weights_extrinsic( self, weight_uids, weight_vals - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[str]]: if self.prompt and not Confirm.ask("Would you like to reveal weights?"): - return False, "User cancelled the operation." + return False, "User cancelled the operation.", None call = await self.subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -316,28 +321,36 @@ async def reveal_weights_extrinsic( wait_for_finalization=self.wait_for_finalization, ) except SubstrateRequestException as e: - return False, format_error_message(e) + return False, format_error_message(e), None if not self.wait_for_finalization and not self.wait_for_inclusion: - success, error_message = True, "" + success, error_message, ext_id = True, "", None else: if await response.is_success: - success, error_message = True, "" + success, error_message, ext_id = ( + True, + "", + await response.get_extrinsic_identifier(), + ) + console.print(f"Your extrinsic was included as {ext_id}") else: - success, error_message = ( + success, error_message, ext_id = ( False, format_error_message(await response.error_message), + None, ) if success: # bittensor.logging.info("Successfully revealed weights.") - return True, "Successfully revealed weights." + return True, "Successfully revealed weights.", ext_id else: # bittensor.logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message + return False, error_message, ext_id - async def do_commit_weights(self, commit_hash): + async def do_commit_weights( + self, commit_hash + ) -> tuple[bool, Optional[str], Optional[str]]: call = await self.subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="commit_weights", @@ -357,12 +370,14 @@ async def do_commit_weights(self, commit_hash): ) if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, None + return True, None, None if await response.is_success: - return True, None + ext_id = await response.get_extrinsic_identifier() + console.print(f"Your extrinsic was included as {ext_id}") + return True, None, ext_id else: - return False, await response.error_message + return False, await response.error_message, None # commands @@ -399,9 +414,13 @@ async def reveal_weights( extrinsic = SetWeightsExtrinsic( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) - success, message = await extrinsic.reveal(weight_uids, weight_vals) + success, message, ext_id = await extrinsic.reveal(weight_uids, weight_vals) if json_output: - json_console.print(json.dumps({"success": success, "message": message})) + json_console.print( + json.dumps( + {"success": success, "message": message, "extrinsic_identifier": ext_id} + ) + ) else: if success: console.print("Weights revealed successfully") @@ -436,9 +455,13 @@ async def commit_weights( extrinsic = SetWeightsExtrinsic( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) - success, message = await extrinsic.set_weights_extrinsic() + success, message, ext_id = await extrinsic.set_weights_extrinsic() if json_output: - json_console.print(json.dumps({"success": success, "message": message})) + json_console.print( + json.dumps( + {"success": success, "message": message, "extrinsic_identifier": ext_id} + ) + ) else: if success: console.print("Weights set successfully") From fc78c444809774f2e3c8a1bac4b52ceaffaf71ea Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Sep 2025 16:19:00 -0700 Subject: [PATCH 46/54] remove icon --- bittensor_cli/src/bittensor/extrinsics/transfer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index ad3168a23..a885e369a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -176,8 +176,8 @@ async def do_transfer() -> tuple[bool, str, str]: f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : " f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]" f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]\n" - f":warning:[bright_yellow]Transferring is not the same as staking. To instead stake, use " - f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]:warning:" + f"[bright_yellow]Transferring is not the same as staking. To instead stake, use " + f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]" ): return False From 72ddb646ba97df78c5f2387a21b968e88252e27f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 23 Sep 2025 16:45:59 +0200 Subject: [PATCH 47/54] Added helper function to print out the extrinsic identifier and included tao dot app link (if applicable) --- .../src/bittensor/extrinsics/registration.py | 3 +- .../src/bittensor/extrinsics/root.py | 3 +- bittensor_cli/src/bittensor/utils.py | 28 +++++++++++++++++++ bittensor_cli/src/commands/stake/add.py | 9 ++---- .../src/commands/stake/children_hotkeys.py | 5 ++-- bittensor_cli/src/commands/stake/move.py | 16 +++++------ bittensor_cli/src/commands/stake/remove.py | 13 +++------ bittensor_cli/src/commands/subnets/subnets.py | 9 +++--- bittensor_cli/src/commands/sudo.py | 11 ++++---- bittensor_cli/src/commands/wallets.py | 14 ++++------ bittensor_cli/src/commands/weights.py | 11 ++++---- 11 files changed, 70 insertions(+), 52 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index df24e0471..a32bc1c3d 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -41,6 +41,7 @@ unlock_key, hex_to_bytes, get_hotkey_pub_ss58, + print_extrinsic_id, ) if typing.TYPE_CHECKING: @@ -767,7 +768,7 @@ async def burned_register_extrinsic( # Successful registration, final check for neuron and pubkey else: ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(ext_receipt) with console.status(":satellite: Checking Balance...", spinner="aesthetic"): block_hash = await subtensor.substrate.get_chain_head() new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index d5f69bbfd..ea515ed1a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -38,6 +38,7 @@ format_error_message, unlock_key, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -340,7 +341,7 @@ async def root_register_extrinsic( # Successful registration, final check for neuron and pubkey else: ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(ext_receipt) uid = await subtensor.query( module="SubtensorModule", storage_function="Uids", diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 3616f9e65..5ec5fd56a 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -10,6 +10,7 @@ from functools import partial import re +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet, Keypair from bittensor_wallet.utils import SS58_FORMAT from bittensor_wallet.errors import KeyFileError, PasswordError @@ -1463,3 +1464,30 @@ def get_hotkey_pub_ss58(wallet: Wallet) -> str: return wallet.hotkeypub.ss58_address except (KeyFileError, AttributeError): return wallet.hotkey.ss58_address + + +async def print_extrinsic_id( + extrinsic_receipt: Optional[AsyncExtrinsicReceipt], +) -> None: + """ + Prints the extrinsic identifier to the console. If the substrate attached to the extrinsic receipt is on a finney + node, it will also include a link to browse the extrinsic in tao dot app. + Args: + extrinsic_receipt: AsyncExtrinsicReceipt object from a successful extrinsic submission. + """ + if extrinsic_receipt is None: + return + substrate = extrinsic_receipt.substrate + ext_id = await extrinsic_receipt.get_extrinsic_identifier() + if substrate: + query = await substrate.rpc_request("system_chainType", []) + if query.get("result") == "Live": + console.print( + f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}: " + f"[blue]https://tao.app/extrinsic/{ext_id}[/blue]" + ) + return + console.print( + f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}" + ) + return diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 2a4982c31..9f3ffc0d0 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -23,6 +23,7 @@ unlock_key, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) from bittensor_wallet import Wallet @@ -164,9 +165,7 @@ async def safe_stake_extrinsic( if json_output: # the rest of this checking is not necessary if using json_output return True, "", response - console.print( - f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -246,9 +245,7 @@ async def stake_extrinsic( if json_output: # the rest of this is not necessary if using json_output return True, "", response - console.print( - f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(response) new_block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance( diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 12a5ed450..d50ecc65a 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -22,6 +22,7 @@ unlock_key, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) @@ -133,7 +134,7 @@ async def set_children_extrinsic( if success: ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(ext_receipt) modifier = "included" if wait_for_finalization: console.print(":white_heavy_check_mark: [green]Finalized[/green]") @@ -217,7 +218,7 @@ async def set_childkey_take_extrinsic( if success: ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(ext_receipt) modifier = "included" if wait_for_finalization: modifier = "finalized" diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index fdbd5556a..1443d611e 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -17,6 +17,7 @@ get_subnet_name, unlock_key, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -563,9 +564,8 @@ async def move_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - console.print( - f"Your extrinsic has been included at {(ext_id := await response.get_extrinsic_identifier())}" - ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -752,9 +752,8 @@ async def transfer_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - console.print( - f"Your extrinsic has been included as {(ext_id := await response.get_extrinsic_identifier())}" - ) + ext_id = await response.get_extrinsic_identifier() + await print_extrinsic_id(extrinsic) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -929,9 +928,8 @@ async def swap_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - console.print( - f"Your extrinsic has been included as {(ext_id := await response.get_extrinsic_identifier())}" - ) + ext_id = await response.get_extrinsic_identifier() + await print_extrinsic_id(response) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index ddc7c70ce..b4b6bbeb1 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -24,6 +24,7 @@ unlock_key, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -615,9 +616,7 @@ async def _unstake_extrinsic( ) return False, None # Fetch latest balance and stake - console.print( - f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -727,9 +726,7 @@ async def _safe_unstake_extrinsic( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) return False, None - console.print( - f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -833,9 +830,7 @@ async def _unstake_all_extrinsic( ) return False, None else: - console.print( - f"Your extrinsic has been included as {await response.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(response) # Fetch latest balance and stake block_hash = await subtensor.substrate.get_chain_head() diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index bf94ddb86..2d2f75d05 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -37,6 +37,7 @@ blocks_to_duration, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -2269,7 +2270,7 @@ async def set_identity( err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") return False, None ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(ext_receipt) console.print( ":white_heavy_check_mark: [dark_sea_green3]Successfully set subnet identity\n" ) @@ -2452,9 +2453,7 @@ async def start_subnet( ) if await response.is_success: - console.print( - f"Your extrinsic was included as {await response.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(response) console.print( f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]" ) @@ -2528,7 +2527,7 @@ async def set_symbol( ) if await response.is_success: ext_id = await response.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(response) message = f"Successfully updated SN{netuid}'s symbol to {symbol}." if json_output: json_console.print_json( diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index c431ff941..fe3601034 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -27,6 +27,7 @@ string_to_u16, string_to_u64, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -298,7 +299,7 @@ async def set_hyperparameter_extrinsic( return False, err_msg, None else: ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(ext_receipt) if arbitrary_extrinsic: console.print( f":white_heavy_check_mark: " @@ -521,9 +522,7 @@ async def vote_senate_extrinsic( return False # Successful vote, final check for data else: - console.print( - f"Your extrinsic was included as {await ext_receipt.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(ext_receipt) if vote_data := await subtensor.get_vote_data(proposal_hash): hotkey_ss58 = get_hotkey_pub_ss58(wallet) if ( @@ -622,7 +621,7 @@ async def set_take_extrinsic( ":white_heavy_check_mark: [dark_sea_green_3]Success[/dark_sea_green_3]" ) ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic was included at {ext_id}") + await print_extrinsic_id(ext_receipt) return success, ext_id @@ -1031,7 +1030,7 @@ async def trim( data={"success": True, "message": msg, "extrinsic_identifier": ext_id} ) else: - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(ext_receipt) console.print( f":white_heavy_check_mark: [dark_sea_green3]{msg}[/dark_sea_green3]" ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 059f5865f..42fe8a0a6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -49,6 +49,7 @@ blocks_to_duration, decode_account_id, get_hotkey_pub_ss58, + print_extrinsic_id, ) @@ -116,8 +117,7 @@ async def associate_hotkey( f"wallet [blue]{wallet.name}[/blue], " f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]" ) - ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic has been included as {ext_id}") + await print_extrinsic_id(ext_receipt) return True @@ -1499,7 +1499,7 @@ async def transfer( json.dumps({"success": result, "extrinsic_identifier": ext_id}) ) else: - console.print(f"Your extrinsic has been included as {ext_id}") + await print_extrinsic_id(ext_receipt) return result @@ -1705,7 +1705,7 @@ async def swap_hotkey( json.dumps({"success": result, "extrinsic_identifier": ext_id}) ) else: - console.print(f"Your extrinsic has been included as {ext_id}") + await print_extrinsic_id(ext_receipt) return result @@ -1784,7 +1784,7 @@ async def set_id( else: console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") ext_id = await ext_receipt.get_extrinsic_identifier() - console.print(f"Your extrinsic has been included as {ext_id}") + await print_extrinsic_id(ext_receipt) output_dict["success"] = True identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) @@ -2055,9 +2055,7 @@ async def schedule_coldkey_swap( console.print( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) - console.print( - f"Your extrinsic has been included as {await ext_receipt.get_extrinsic_identifier()}" - ) + await print_extrinsic_id(ext_receipt) for event in await ext_receipt.triggered_events: if ( event.get("event", {}).get("module_id") == "SubtensorModule" diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 2db47f143..b61bbd81f 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -16,6 +16,7 @@ format_error_message, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, @@ -271,9 +272,9 @@ async def _do_set_weights() -> tuple[bool, str, Optional[str]]: return True, "Not waiting for finalization or inclusion.", None if await response.is_success: - ext_id = await response.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") - return True, "Successfully set weights.", ext_id + ext_id_ = await response.get_extrinsic_identifier() + await print_extrinsic_id(response) + return True, "Successfully set weights.", ext_id_ else: return False, format_error_message(await response.error_message), None @@ -333,7 +334,7 @@ async def reveal_weights_extrinsic( "", await response.get_extrinsic_identifier(), ) - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(response) else: success, error_message, ext_id = ( False, @@ -374,7 +375,7 @@ async def do_commit_weights( if await response.is_success: ext_id = await response.get_extrinsic_identifier() - console.print(f"Your extrinsic was included as {ext_id}") + await print_extrinsic_id(response) return True, None, ext_id else: return False, await response.error_message, None From eb359d5e5717fd1d066cb6d33cb3483b6dded7b1 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 23 Sep 2025 17:09:17 +0200 Subject: [PATCH 48/54] Liquidity commands. --- .../src/commands/liquidity/liquidity.py | 68 ++++++++++++------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 60f5c6529..cc25ff1e9 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -2,6 +2,7 @@ import json from typing import TYPE_CHECKING, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from rich.prompt import Confirm from rich.table import Column, Table @@ -11,6 +12,7 @@ console, err_console, json_console, + print_extrinsic_id, ) from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src.commands.liquidity.utils import ( @@ -36,7 +38,7 @@ async def add_liquidity_extrinsic( price_high: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Adds liquidity to the specified price range. @@ -60,7 +62,7 @@ async def add_liquidity_extrinsic( `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None tick_low = price_to_tick(price_low.tao) tick_high = price_to_tick(price_high.tao) @@ -94,7 +96,7 @@ async def modify_liquidity_extrinsic( liquidity_delta: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Modifies liquidity in liquidity position by adding or removing liquidity from it. Arguments: @@ -116,7 +118,7 @@ async def modify_liquidity_extrinsic( Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None call = await subtensor.substrate.compose_call( call_module="Swap", @@ -145,7 +147,7 @@ async def remove_liquidity_extrinsic( position_id: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Remove liquidity and credit balances back to wallet's hotkey stake. Arguments: @@ -166,7 +168,7 @@ async def remove_liquidity_extrinsic( Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None call = await subtensor.substrate.compose_call( call_module="Swap", @@ -193,7 +195,7 @@ async def toggle_user_liquidity_extrinsic( enable: bool, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Allow to toggle user liquidity for specified subnet. Arguments: @@ -210,7 +212,7 @@ async def toggle_user_liquidity_extrinsic( - False and an error message if the submission fails or the wallet cannot be unlocked. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None call = await subtensor.substrate.compose_call( call_module="Swap", @@ -232,16 +234,16 @@ async def add_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: Optional[int], - liquidity: Optional[float], - price_low: Optional[float], - price_high: Optional[float], + liquidity: Balance, + price_low: Balance, + price_high: Balance, prompt: bool, json_output: bool, ) -> tuple[bool, str]: """Add liquidity position to provided subnet.""" # Check wallet access - if not unlock_key(wallet).success: - return False + if not (ulw := unlock_key(wallet)).success: + return False, ulw.message # Check that the subnet exists. if not await subtensor.subnet_exists(netuid=netuid): @@ -260,7 +262,7 @@ async def add_liquidity( if not Confirm.ask("Would you like to continue?"): return False, "User cancelled operation." - success, message = await add_liquidity_extrinsic( + success, message, ext_receipt = await add_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, @@ -269,8 +271,14 @@ async def add_liquidity( price_low=price_low, price_high=price_high, ) + await print_extrinsic_id(ext_receipt) + ext_id = await ext_receipt.get_extrinsic_identifier() if json_output: - json_console.print(json.dumps({"success": success, "message": message})) + json_console.print( + json.dumps( + {"success": success, "message": message, "extrinsic_identifier": ext_id} + ) + ) else: if success: console.print( @@ -278,6 +286,7 @@ async def add_liquidity( ) else: err_console.print(f"[red]Error: {message}[/red]") + return success, message async def get_liquidity_list( @@ -535,11 +544,12 @@ async def remove_liquidity( success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid) if not success: if json_output: - return json_console.print( - {"success": False, "err_msg": msg, "positions": positions} + json_console.print_json( + data={"success": False, "err_msg": msg, "positions": positions} ) else: return err_console.print(f"Error: {msg}") + return False, msg else: position_ids = [p.id for p in positions] else: @@ -568,16 +578,21 @@ async def remove_liquidity( ] ) if not json_output: - for (success, msg), posid in zip(results, position_ids): + for (success, msg, ext_receipt), posid in zip(results, position_ids): if success: + await print_extrinsic_id(ext_receipt) console.print(f"[green] Position {posid} has been removed.") else: err_console.print(f"[red] Error removing {posid}: {msg}") else: json_table = {} - for (success, msg), posid in zip(results, position_ids): - json_table[posid] = {"success": success, "err_msg": msg} - json_console.print(json.dumps(json_table)) + for (success, msg, ext_receipt), posid in zip(results, position_ids): + json_table[posid] = { + "success": success, + "err_msg": msg, + "extrinsic_identifier": await ext_receipt.get_extrinsic_identifier(), + } + json_console.print_json(data=json_table) async def modify_liquidity( @@ -586,7 +601,7 @@ async def modify_liquidity( hotkey_ss58: str, netuid: int, position_id: int, - liquidity_delta: Optional[float], + liquidity_delta: Balance, prompt: Optional[bool] = None, json_output: bool = False, ) -> bool: @@ -611,7 +626,7 @@ async def modify_liquidity( if not Confirm.ask("Would you like to continue?"): return False - success, msg = await modify_liquidity_extrinsic( + success, msg, ext_receipt = await modify_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, @@ -620,9 +635,14 @@ async def modify_liquidity( liquidity_delta=liquidity_delta, ) if json_output: - json_console.print(json.dumps({"success": success, "err_msg": msg})) + ext_id = await ext_receipt.get_extrinsic_identifier() if success else None + json_console.print_json( + data={"success": success, "err_msg": msg, "extrinsic_identifier": ext_id} + ) else: if success: + await print_extrinsic_id(ext_receipt) console.print(f"[green] Position {position_id} has been modified.") else: err_console.print(f"[red] Error modifying {position_id}: {msg}") + return success From 4afb2284d4f04e1e7a81a9d30457c132149eab0f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 23 Sep 2025 18:03:21 +0200 Subject: [PATCH 49/54] Tests --- bittensor_cli/src/commands/subnets/subnets.py | 5 +-- tests/e2e_tests/test_hyperparams_setting.py | 5 ++- tests/e2e_tests/test_liquidity.py | 8 ++++ tests/e2e_tests/test_senate.py | 6 +++ tests/e2e_tests/test_staking_sudo.py | 38 ++++++++++++++----- tests/e2e_tests/test_unstaking.py | 21 ++++++++++ tests/e2e_tests/test_wallet_interactions.py | 18 +++++---- 7 files changed, 79 insertions(+), 22 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 2d2f75d05..ef4ae59de 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -198,9 +198,8 @@ async def _find_event_attributes_in_extrinsic_receipt( attributes = await _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) - console.print( - f"You extrinsic was included as {(ext_id := await response.get_extrinsic_identifier())}" - ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() console.print( f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 916b00cac..24f83bdfe 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -62,7 +62,7 @@ def test_hyperparams_setting(local_chain, wallet_setup): result_output = json.loads(result.stdout) assert result_output["success"] is True assert result_output["netuid"] == netuid - print(result_output) + assert isinstance(result_output["extrinsic_identifier"], str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( @@ -119,6 +119,7 @@ def test_hyperparams_setting(local_chain, wallet_setup): ) cmd_json = json.loads(cmd.stdout) assert cmd_json["success"] is True, (key, new_val, cmd.stdout, cmd_json) + assert isinstance(cmd_json["extrinsic_identifier"], str) print(f"Successfully set hyperparameter {key} to value {new_val}") # also test hidden hyperparam cmd = exec_command_alice( @@ -145,6 +146,7 @@ def test_hyperparams_setting(local_chain, wallet_setup): ) cmd_json = json.loads(cmd.stdout) assert cmd_json["success"] is True, (cmd.stdout, cmd_json) + assert isinstance(cmd_json["extrinsic_identifier"], str) print("Successfully set hyperparameters") print("Testing trimming UIDs") cmd = exec_command_alice( @@ -169,4 +171,5 @@ def test_hyperparams_setting(local_chain, wallet_setup): ) cmd_json = json.loads(cmd.stdout) assert cmd_json["success"] is True, (cmd.stdout, cmd_json) + assert isinstance(cmd_json["extrinsic_identifier"], str) print("Successfully trimmed UIDs") diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 218ef91f0..faaf6e05e 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -85,6 +85,7 @@ def liquidity_list(): result_output = json.loads(result.stdout) assert result_output["success"] is True assert result_output["netuid"] == netuid + assert isinstance(result_output["extrinsic_identifier"], str) # verify no results for list thus far (subnet not yet started) liquidity_list_result = liquidity_list() @@ -115,6 +116,7 @@ def liquidity_list(): f"Successfully started subnet {netuid}'s emission schedule" in start_subnet_emissions.stdout ), start_subnet_emissions.stderr + assert "Your extrinsic has been included " in start_subnet_emissions.stdout liquidity_list_result = liquidity_list() result_output = json.loads(liquidity_list_result.stdout) @@ -146,6 +148,7 @@ def liquidity_list(): ) enable_user_liquidity_result = json.loads(enable_user_liquidity.stdout) assert enable_user_liquidity_result["success"] is True + assert isinstance(enable_user_liquidity_result["extrinsic_identifier"], str) add_liquidity = exec_command_alice( command="liquidity", @@ -174,6 +177,7 @@ def liquidity_list(): add_liquidity_result = json.loads(add_liquidity.stdout) assert add_liquidity_result["success"] is True assert add_liquidity_result["message"] == "" + assert isinstance(add_liquidity_result["extrinsic_identifier"], str) liquidity_list_result = liquidity_list() liquidity_list_result = json.loads(liquidity_list_result.stdout) @@ -212,6 +216,7 @@ def liquidity_list(): ) modify_liquidity_result = json.loads(modify_liquidity.stdout) assert modify_liquidity_result["success"] is True + assert isinstance(modify_liquidity_result["extrinsic_identifier"], str) liquidity_list_result = json.loads(liquidity_list().stdout) assert len(liquidity_list_result["positions"]) == 1 @@ -240,6 +245,9 @@ def liquidity_list(): ) removal_result = json.loads(removal.stdout) assert removal_result[str(liquidity_position["id"])]["success"] is True + assert isinstance( + removal_result[str(liquidity_position["id"])]["extrinsic_identifier"], str + ) liquidity_list_result = json.loads(liquidity_list().stdout) assert liquidity_list_result["success"] is True diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index 4cf12bb2b..c4cbebd7c 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -77,6 +77,9 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Registered" in root_register.stdout, root_register.stderr + assert "Your extrinsic has been included " in root_register.stdout, ( + root_register.stderr + ) # Fetch the senate members after registering to root root_senate_after_reg = exec_command_bob( @@ -156,6 +159,7 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Vote cast" in vote_aye.stdout + assert "Your extrinsic has been included " in vote_aye.stdout # Fetch proposals after voting aye proposals_after_aye = exec_command_bob( @@ -219,6 +223,7 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Registered" in root_register.stdout + assert "Your extrinsic has been included " in root_register.stdout # Vote on the proposal by Alice (vote nay) vote_nay = exec_command_alice( @@ -240,6 +245,7 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Vote cast" in vote_nay.stdout + assert "Your extrinsic has been included " in vote_nay.stdout # Fetch proposals after voting proposals_after_nay = exec_command_bob( diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 9034c51da..4096c87b0 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,6 +1,7 @@ import asyncio import json import re +from typing import Union from bittensor_cli.src.bittensor.balances import Balance from .utils import turn_off_hyperparam_freeze_window @@ -105,6 +106,7 @@ def test_staking(local_chain, wallet_setup): result_output = json.loads(result.stdout) assert result_output["success"] is True assert result_output["netuid"] == netuid + assert isinstance(result_output["extrinsic_identifier"], str) # Register another subnet with sudo as Alice result_for_second_repo = exec_command_alice( @@ -142,6 +144,7 @@ def test_staking(local_chain, wallet_setup): result_output_second = json.loads(result_for_second_repo.stdout) assert result_output_second["success"] is True assert result_output_second["netuid"] == multiple_netuids[1] + assert isinstance(result_output_second["extrinsic_identifier"], str) # Register Alice in netuid = 2 using her hotkey register_subnet = exec_command_alice( @@ -162,6 +165,7 @@ def test_staking(local_chain, wallet_setup): ], ) assert "✅ Already Registered" in register_subnet.stdout + assert "Your extrinsic has been included" not in register_subnet.stdout register_subnet_json = exec_command_alice( command="subnets", @@ -184,6 +188,7 @@ def test_staking(local_chain, wallet_setup): register_subnet_json_output = json.loads(register_subnet_json.stdout) assert register_subnet_json_output["success"] is True assert register_subnet_json_output["msg"] == "Already registered" + assert register_subnet_json_output["extrinsic_identifier"] is None # set identity set_identity = exec_command_alice( @@ -222,6 +227,7 @@ def test_staking(local_chain, wallet_setup): ) set_identity_output = json.loads(set_identity.stdout) assert set_identity_output["success"] is True + assert isinstance(set_identity_output["extrinsic_identifier"], str) get_identity = exec_command_alice( "subnets", @@ -271,6 +277,7 @@ def test_staking(local_chain, wallet_setup): set_symbol_output["message"] == f"Successfully updated SN{netuid}'s symbol to シ." ) + assert isinstance(set_identity_output["extrinsic_identifier"], str) get_s_price = exec_command_alice( "subnets", @@ -314,6 +321,9 @@ def test_staking(local_chain, wallet_setup): f"Successfully started subnet {netuid_}'s emission schedule" in start_subnet_emissions.stdout ), start_subnet_emissions.stderr + assert "Your extrinsic has been included" in start_subnet_emissions.stdout, ( + start_subnet_emissions.stdout + ) # Add stake to Alice's hotkey add_stake_single = exec_command_alice( @@ -341,6 +351,9 @@ def test_staking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in add_stake_single.stdout, add_stake_single.stderr + assert "Your extrinsic has been included" in add_stake_single.stdout, ( + add_stake_single.stdout + ) # Execute stake show for Alice's wallet show_stake_adding_single = exec_command_alice( @@ -408,6 +421,9 @@ def test_staking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in remove_stake.stdout + assert "Your extrinsic has been included" in remove_stake.stdout, ( + remove_stake.stdout + ) add_stake_multiple = exec_command_alice( command="stake", @@ -436,18 +452,15 @@ def test_staking(local_chain, wallet_setup): ) add_stake_multiple_output = json.loads(add_stake_multiple.stdout) for netuid_ in multiple_netuids: - assert ( - add_stake_multiple_output["staking_success"][str(netuid_)][ - wallet_alice.hotkey.ss58_address - ] - is True - ) - assert ( - add_stake_multiple_output["error_messages"][str(netuid_)][ + + def line(key: str) -> Union[str, bool]: + return add_stake_multiple_output[key][str(netuid_)][ wallet_alice.hotkey.ss58_address ] - == "" - ) + + assert line("staking_success") is True + assert line("error_messages") == "" + assert isinstance(line("extrinsic_ids"), str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( @@ -507,6 +520,9 @@ def test_staking(local_chain, wallet_setup): assert ( "✅ Hyperparameter max_burn changed to 10000000000" in change_hyperparams.stdout ) + assert "Your extrinsic has been included" in change_hyperparams.stdout, ( + change_hyperparams.stdout + ) # Fetch the hyperparameters again to verify updated_hyperparams = exec_command_alice( @@ -576,6 +592,7 @@ def test_staking(local_chain, wallet_setup): assert change_yuma3_hyperparam_json["success"] is True, ( change_yuma3_hyperparam.stdout ) + assert isinstance(change_yuma3_hyperparam_json["extrinsic_identifier"], str) changed_yuma3_hyperparam = exec_command_alice( command="sudo", @@ -626,3 +643,4 @@ def test_staking(local_chain, wallet_setup): change_arbitrary_hyperparam.stdout, change_arbitrary_hyperparam.stderr, ) + assert isinstance(change_yuma3_hyperparam_json["extrinsic_identifier"], str) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 4b7ca0765..d4e3eef76 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -89,6 +89,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Registered subnetwork with netuid: 2" in result.stdout + assert "Your extrinsic has been included" in result.stdout, result.stdout # Create second subnet (netuid = 3) result = exec_command_alice( @@ -123,6 +124,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Registered subnetwork with netuid: 3" in result.stdout + assert "Your extrinsic has been included" in result.stdout, result.stdout # Start emission schedule for subnets start_call_netuid_0 = exec_command_alice( @@ -144,6 +146,9 @@ def test_unstaking(local_chain, wallet_setup): "Successfully started subnet 0's emission schedule." in start_call_netuid_0.stdout ) + assert "Your extrinsic has been included" in start_call_netuid_0.stdout, ( + start_call_netuid_0.stdout + ) start_call_netuid_2 = exec_command_alice( command="subnets", sub_command="start", @@ -163,6 +168,7 @@ def test_unstaking(local_chain, wallet_setup): "Successfully started subnet 2's emission schedule." in start_call_netuid_2.stdout ) + assert "Your extrinsic has been included" in start_call_netuid_2.stdout start_call_netuid_3 = exec_command_alice( command="subnets", @@ -183,6 +189,7 @@ def test_unstaking(local_chain, wallet_setup): "Successfully started subnet 3's emission schedule." in start_call_netuid_3.stdout ) + assert "Your extrinsic has been included" in start_call_netuid_3.stdout # Register Bob in one subnet register_result = exec_command_bob( command="subnets", @@ -204,6 +211,9 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Registered" in register_result.stdout, register_result.stderr + assert "Your extrinsic has been included" in register_result.stdout, ( + register_result.stdout + ) # Add stake to subnets for netuid in [0, 2, 3]: @@ -232,6 +242,9 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in stake_result.stdout, stake_result.stderr + assert "Your extrinsic has been included" in stake_result.stdout, ( + stake_result.stdout + ) stake_list = exec_command_bob( command="stake", @@ -279,6 +292,9 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in partial_unstake_netuid_2.stdout + assert "Your extrinsic has been included" in partial_unstake_netuid_2.stdout, ( + partial_unstake_netuid_2.stdout + ) # Verify partial unstake stake_list = exec_command_bob( @@ -348,6 +364,9 @@ def test_unstaking(local_chain, wallet_setup): assert ( "✅ Finalized: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout ) + assert "Your extrinsic has been included" in unstake_alpha.stdout, ( + unstake_alpha.stdout + ) # Add stake again to subnets for netuid in [0, 2, 3]: @@ -376,6 +395,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in stake_result.stdout + assert "Your extrinsic has been included" in stake_result.stdout # Remove all stakes unstake_all = exec_command_bob( @@ -397,4 +417,5 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout + assert "Your extrinsic has been included" in unstake_all.stdout, unstake_all.stdout print("Passed unstaking tests 🎉") diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index e6a4bb22d..9dd3ea4f1 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -443,17 +443,19 @@ def test_wallet_identities(local_chain, wallet_setup): assert "✅ Success!" in set_id.stdout set_id_output = set_id.stdout.splitlines() - assert alice_identity["name"] in set_id_output[6] - assert alice_identity["url"] in set_id_output[7] - assert alice_identity["github_repo"] in set_id_output[8] - assert alice_identity["image"] in set_id_output[9] - assert alice_identity["discord"] in set_id_output[10] - assert alice_identity["description"] in set_id_output[11] - assert alice_identity["additional"] in set_id_output[12] + assert "Your extrinsic has been included as" in set_id_output[1] + + assert alice_identity["name"] in set_id_output[7] + assert alice_identity["url"] in set_id_output[8] + assert alice_identity["github_repo"] in set_id_output[9] + assert alice_identity["image"] in set_id_output[10] + assert alice_identity["discord"] in set_id_output[11] + assert alice_identity["description"] in set_id_output[12] + assert alice_identity["additional"] in set_id_output[13] # TODO: Currently coldkey + hotkey are the same for test wallets. # Maybe we can add a new key to help in distinguishing - assert wallet_alice.coldkeypub.ss58_address in set_id_output[5] + assert wallet_alice.coldkeypub.ss58_address in set_id_output[6] # Execute btcli get-identity using hotkey get_identity = exec_command_alice( From cddf8e1a69a11b4dba81cf184af14299bfce9748 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 23 Sep 2025 18:14:13 +0200 Subject: [PATCH 50/54] Update the example text for sudo trim --- 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 4484f5288..2f0013ac6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5370,7 +5370,7 @@ def sudo_trim( Allows subnet owners to trim UIDs on their subnet to a specified max number of netuids. EXAMPLE - [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 6 + [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64 """ self.verbosity_handler(quiet, verbose, json_output) From 34989b8052c0a9d7da3c6a6981795ebda0400875 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Tue, 23 Sep 2025 22:38:21 +0200 Subject: [PATCH 51/54] 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( From 9bfd96638fa4eccd8e4c93e6a78b5b0dcfcb32d8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 24 Sep 2025 14:04:15 -0700 Subject: [PATCH 52/54] Adds wallet_name param --- bittensor_cli/cli.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 07b64e6d1..453227f71 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1875,6 +1875,7 @@ def wallet_ask( def wallet_list( self, + wallet_name: Optional[str] = Options.wallet_name, wallet_path: str = Options.wallet_path, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -1898,7 +1899,13 @@ def wallet_list( wallet = self.wallet_ask( None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE ) - return self._run_command(wallets.wallet_list(wallet.path, json_output)) + return self._run_command( + wallets.wallet_list( + wallet.path, + json_output, + wallet_name=wallet_name, + ) + ) def wallet_overview( self, From d597f0c91997926223a5cc1aeaea090104452242 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Wed, 24 Sep 2025 14:04:31 -0700 Subject: [PATCH 53/54] display individual wallet --- bittensor_cli/src/commands/wallets.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 42fe8a0a6..63edd7ef3 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -813,13 +813,22 @@ async def wallet_history(wallet: Wallet): console.print(table) -async def wallet_list(wallet_path: str, json_output: bool): +async def wallet_list( + wallet_path: str, json_output: bool, wallet_name: Optional[str] = None +): """Lists wallets.""" wallets = utils.get_coldkey_wallets_for_path(wallet_path) print_verbose(f"Using wallets path: {wallet_path}") if not wallets: err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]") + if wallet_name: + wallets = [wallet for wallet in wallets if wallet.name == wallet_name] + if not wallets: + err_console.print( + f"[red]Wallet '{wallet_name}' not found in dir: {wallet_path}[/red]" + ) + root = Tree("Wallets") main_data_dict = {"wallets": []} for wallet in wallets: @@ -876,7 +885,12 @@ async def wallet_list(wallet_path: str, json_output: bool): if not wallets: print_verbose(f"No wallets found in path: {wallet_path}") - root.add("[bold red]No wallets found.") + message = ( + "[bold red]No wallets found." + if not wallet_name + else f"[bold red]Wallet '{wallet_name}' not found." + ) + root.add(message) if json_output: json_console.print(json.dumps(main_data_dict)) else: From b1fe54901ba2f10753995e3f195b6f72772d87df Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Thu, 25 Sep 2025 15:34:23 -0700 Subject: [PATCH 54/54] bumps version and changelog --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b5ca54b..95aae0d8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 9.12.0 /2025-09-25 +* Removes warning icon in transfer by @ibraheem-abe in https://github.com/opentensor/btcli/pull/634 +* Add Extrinsic Identifier Output by @thewhaleking in https://github.com/opentensor/btcli/pull/633 +* Update the example text for sudo trim by @thewhaleking in https://github.com/opentensor/btcli/pull/636 +* Feat/Individual wallet list by @ibraheem-abe in https://github.com/opentensor/btcli/pull/638 +* Feat/ subnet mechanisms by @ibraheem-abe in https://github.com/opentensor/btcli/pull/627 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.11.2...v9.12.0 + ## 9.11.2 /2025-09-19 * Fix: Stake movement between non-root sns by @ibraheem-abe in https://github.com/opentensor/btcli/pull/629 diff --git a/pyproject.toml b/pyproject.toml index b7c11b579..234acba94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.11.2" +version = "9.12.0" description = "Bittensor CLI" readme = "README.md" authors = [