diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 20ddb90d1..0d8e53d99 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: @@ -229,6 +233,15 @@ 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, + "--mechid", + "--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", @@ -650,6 +663,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 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 """ @@ -658,7 +672,9 @@ class CLIManager: app: typer.Typer config_app: typer.Typer wallet_app: typer.Typer + sudo_app: typer.Typer subnets_app: typer.Typer + subnet_mechanisms_app: typer.Typer weights_app: typer.Typer utils_app: typer.Typer view_app: typer.Typer @@ -733,6 +749,7 @@ def __init__(self): self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) + self.subnet_mechanisms_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) @@ -794,6 +811,19 @@ def __init__(self): self.subnets_app, name="subnet", hidden=True, no_args_is_help=True ) + # 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.subnets_app.add_typer( + self.subnet_mechanisms_app, + name="mech", + hidden=True, + no_args_is_help=True, + ) # weights aliases self.app.add_typer( self.weights_app, @@ -938,6 +968,20 @@ def __init__(self): children_app.command("revoke")(self.stake_revoke_children) children_app.command("take")(self.stake_childkey_take) + # subnet mechanism commands + 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.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"])( self.sudo_set @@ -1762,6 +1806,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 to {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 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 to {mechanism_count - 1}[/bold cyan]." + ) + def wallet_ask( self, wallet_name: Optional[str], @@ -3754,6 +3835,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, @@ -5015,6 +5098,238 @@ def stake_childkey_take( json_console.print(json.dumps(output)) return results + 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, + mechanism_count: Optional[int] = typer.Option( + None, + "--count", + "--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, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + 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) + + if not json_output: + current_count = self._run_command( + subnet_mechanisms.count( + subtensor=subtensor, + netuid=netuid, + json_output=False, + ), + exit_early=False, + ) + else: + current_count = self._run_command( + subtensor.get_subnet_mechanisms(netuid), + exit_early=False, + ) + + if mechanism_count is None: + if not prompt: + err_console.print( + "Mechanism count not supplied with `--no-prompt` flag. Cannot continue." + ) + return False + prompt_text = "\n\nEnter the [blue]number of mechanisms[/blue] to set" + mechanism_count = IntPrompt.ask(prompt_text) + + 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} mechanism" + 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} mechanisms.", + "extrinsic_identifier": None, + } + ) + ) + else: + console.print(message) + return True + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) + + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"mechanism_count: {mechanism_count}\n" + ) + + result, err_msg, ext_id = self._run_command( + subnet_mechanisms.set_mechanism_count( + wallet=wallet, + 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, + ) + ) + + if json_output: + json_console.print_json( + data={ + "success": result, + "message": err_msg, + "extrinsic_identifier": ext_id, + } + ) + + return result + + def mechanism_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 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) + return self._run_command( + subnet_mechanisms.count( + subtensor=subtensor, + netuid=netuid, + json_output=json_output, + ) + ) + + def mechanism_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 mechanism (normalised automatically).", + ), + 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, + ): + """ + 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) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) + + return self._run_command( + subnet_mechanisms.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 mechanism_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 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) + return self._run_command( + subnet_mechanisms.get_emission_split( + subtensor=subtensor, + netuid=netuid, + json_output=json_output, + ) + ) + def sudo_set( self, network: Optional[list[str]] = Options.network, @@ -5578,6 +5893,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", @@ -5589,18 +5905,43 @@ 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) + 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_mechanisms(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, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 111cd0d8f..bc46bd485 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -700,6 +700,10 @@ class WalletValidationTypes(Enum): "GOVERNANCE": "Governance", "TAKE": "Delegate take configuration", }, + "MECHANISMS": { + "CONFIG": "Mechanism Configuration", + "EMISSION": "Mechanism Emission", + }, "SUBNETS": { "INFO": "Subnet Information", "CREATION": "Subnet Creation & Management", 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"], diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index d554fdbb4..d37d5f3db 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1175,6 +1175,55 @@ async def get_subnet_hyperparameters( return SubnetHyperparameters.from_any(result) + 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.""" + + result = await self.query( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[netuid], + block_hash=block_hash, + ) + + if result is None: + 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]: + """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", @@ -1300,37 +1349,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, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 5ec5fd56a..c8be33563 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -35,6 +35,7 @@ BT_DOCS_LINK = "https://docs.learnbittensor.org" +GLOBAL_MAX_SUBNET_COUNT = 4096 console = Console() json_console = Console() @@ -1466,6 +1467,26 @@ def get_hotkey_pub_ss58(wallet: Wallet) -> str: 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, + ) + + async def print_extrinsic_id( extrinsic_receipt: Optional[AsyncExtrinsicReceipt], ) -> None: diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py new file mode 100644 index 000000000..bf329513a --- /dev/null +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -0,0 +1,498 @@ +import asyncio +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, + print_extrinsic_id, +) + +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( + data={"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_mechanisms( + netuid, block_hash=block_hash + ) + if not mechanism_count: + if json_output: + json_console.print_json( + data={ + "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( + data={ + "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_mechanisms(netuid) + if count_ == 1: + console.print( + f"Subnet {netuid} only has the primary mechanism (mechanism 0). No emission split to display." + ) + if json_output: + json_console.print_json( + data={ + "success": False, + "error": "Subnet only has the primary mechanism (mechanism 0). No emission split to display.", + } + ) + 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(data=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[/]\n" + f"[{COLOR_PALETTE.G.SUBHEAD}]Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]", + 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_mechanisms(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(data={"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(data={"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(data={"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(data={"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(data={"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( + 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) + 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, ext_id = 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( + 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 + + +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, + previous_count: int, + wait_for_inclusion: bool, + wait_for_finalization: bool, + json_output: bool, +) -> 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, 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, 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", None + + success, err_msg, ext_receipt = 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, + ) + ext_id = await ext_receipt.get_extrinsic_identifier() if success else None + + if json_output: + 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]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg, ext_id + + +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, 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, None + + success, err_msg, ext_receipt = 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, + ) + ext_id = await ext_receipt.get_extrinsic_identifier() if success else None + + if json_output: + 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]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg, ext_id diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index ef4ae59de..664657986 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -221,8 +221,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) @@ -232,7 +236,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 @@ -320,10 +324,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_) @@ -403,6 +412,8 @@ def _create_table(subnets_, block_number_): else: tempo_cell = "-/-" + mechanisms_cell = str(mechanisms.get(netuid, 1)) + rows.append( ( netuid_cell, # Netuid @@ -414,6 +425,7 @@ def _create_table(subnets_, block_number_): alpha_out_cell, # Stake α_out supply_cell, # Supply tempo_cell, # Tempo k/n + mechanisms_cell, # Mechanism count ) ) @@ -435,7 +447,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 @@ -475,6 +487,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, @@ -487,7 +500,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 ): @@ -723,6 +736,7 @@ def format_liquidity_cell( alpha_out_cell, # Stake α_out supply_cell, # Supply tempo_cell, # Tempo k/n + str(mechanisms.get(netuid, 1)), # Mechanisms ) ) @@ -769,7 +783,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 @@ -781,7 +795,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) @@ -807,11 +821,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 @@ -877,6 +893,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, @@ -1090,43 +1108,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", @@ -1138,33 +1170,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: @@ -1179,7 +1189,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 @@ -1188,11 +1198,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, ) ), ) @@ -1201,10 +1211,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 @@ -1212,8 +1222,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 = ( @@ -1225,44 +1235,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, } ) @@ -1358,8 +1368,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, @@ -1377,8 +1395,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']}]" @@ -1424,7 +1455,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( @@ -1444,7 +1475,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 diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index fe3601034..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 @@ -170,6 +171,84 @@ def requires_bool(metadata, param_name, pallet: str = DEFAULT_PALLET) -> bool: raise ValueError(f"{param_name} not found in pallet.") +async def set_mechanism_count_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + mech_count: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> 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, None + + substrate = subtensor.substrate + call_params = {"netuid": netuid, "mechanism_count": mech_count} + + with console.status( + 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_module=DEFAULT_PALLET, + call_function="sudo_set_mechanism_count", + call_params=call_params, + ) + success, err_msg, ext_receipt = 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, None + + return True, "", ext_receipt + + +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, 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, None + + 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, ext_receipt = 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, None + + return True, "", ext_receipt + + async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet",