diff --git a/cli.py b/cli.py index 80400200..7754d84c 100755 --- a/cli.py +++ b/cli.py @@ -15,7 +15,7 @@ from yaml import safe_load, safe_dump from src import defaults, utils, HYPERPARAMS -from src.commands import wallets, root, stake, sudo +from src.commands import wallets, root, stake, sudo, subnets from src.subtensor_interface import SubtensorInterface from src.bittensor.async_substrate_interface import SubstrateRequestException from src.utils import console, err_console @@ -234,7 +234,9 @@ def __init__(self): self.app.add_typer(self.sudo_app, name="su", hidden=True) # subnets aliases - self.app.add_typer(self.subnets_app, name="subnets", short_help="Subnets commands") + self.app.add_typer( + self.subnets_app, name="subnets", short_help="Subnets commands" + ) # config commands self.config_app.command("set")(self.set_config) @@ -288,11 +290,12 @@ def __init__(self): # subnets commands self.subnets_app.command("hyperparameters")(self.sudo_get) + self.subnets_app.command("list")(self.subnets_list) def initialize_chain( self, - network: Optional[str] = typer.Option("default_network", help="Network name"), - chain: Optional[str] = typer.Option("default_chain", help="Chain name"), + network: Optional[str] = None, + chain: Optional[str] = None, ) -> SubtensorInterface: """ Intelligently initializes a connection to the chain, depending on the supplied (or in config) values. Set's the @@ -2760,6 +2763,58 @@ def sudo_get( sudo.get_hyperparameters(self.initialize_chain(network, chain), netuid) ) + def subnets_list(self, network: str = Options.network, chain: str = Options.chain): + """ + # subnets list + Executes the `list` command to list all subnets and their detailed information on the Bittensor network. + + This command is designed to provide users with comprehensive information about each subnet within the + network, including its unique identifier (netuid), the number of neurons, maximum neuron capacity, + emission rate, tempo, recycle register cost (burn), proof of work (PoW) difficulty, and the name or + SS58 address of the subnet owner. + + ## Usage: + + Upon invocation, the command performs the following actions: + + 1. It initializes the Bittensor subtensor object with the user's configuration. + + 2. It retrieves a list of all subnets in the network along with their detailed information. + + 3. The command compiles this data into a table format, displaying key information about each subnet. + + + In addition to the basic subnet details, the command also fetches delegate information to provide the + name of the subnet owner where available. If the owner's name is not available, the owner's ``SS58`` + address is displayed. + + The command structure includes: + + - Initializing the Bittensor subtensor and retrieving subnet information. + + - Calculating the total number of neurons across all subnets. + + - Constructing a table that includes columns for `NETUID`, `N` (current neurons), `MAX_N` + (maximum neurons), `EMISSION`, `TEMPO`, `BURN`, `POW` (proof of work difficulty), and + `SUDO` (owner's name or `SS58` address). + + - Displaying the table with a footer that summarizes the total number of subnets and neurons. + + + ### Example usage: + + ``` + btcli subnets list + ``` + + #### Note: + This command is particularly useful for users seeking an overview of the Bittensor network's structure and the + distribution of its resources and ownership information for each subnet. + """ + return self._run_command( + subnets.subnets_list(self.initialize_chain(network, chain)) + ) + def run(self): self.app() diff --git a/src/bittensor/chain_data.py b/src/bittensor/chain_data.py index 3f36e4f0..348c6a9e 100644 --- a/src/bittensor/chain_data.py +++ b/src/bittensor/chain_data.py @@ -109,6 +109,7 @@ def from_neuron_info(cls, neuron_info: dict) -> "AxonInfo": coldkey=neuron_info["coldkey"], ) + @dataclass class SubnetHyperparameters: """Dataclass for subnet hyperparameters.""" diff --git a/src/commands/stake.py b/src/commands/stake.py index acc241db..ad828418 100644 --- a/src/commands/stake.py +++ b/src/commands/stake.py @@ -1523,9 +1523,7 @@ async def render_table( success, children, err_mg = await subtensor.get_children(wallet.hotkey, netuid) if not success: - err_console.print( - f"Failed to get children from subtensor: {err_mg}" - ) + err_console.print(f"Failed to get children from subtensor: {err_mg}") await render_table(wallet.hotkey, children, netuid) return children diff --git a/src/commands/subnets.py b/src/commands/subnets.py index 1b676d3a..54ca8c19 100644 --- a/src/commands/subnets.py +++ b/src/commands/subnets.py @@ -1,34 +1,94 @@ +import asyncio from typing import TYPE_CHECKING from rich.table import Table, Column -from src.utils import console, normalize_hyperparameters +from src import Constants, DelegatesDetails +from src.bittensor.chain_data import SubnetInfo +from src.utils import ( + console, + err_console, + get_delegates_details_from_github, + millify, + RAO_PER_TAO, +) if TYPE_CHECKING: from src.subtensor_interface import SubtensorInterface -async def hyperparameters(subtensor: "SubtensorInterface", netuid: int): - """View hyperparameters of a subnetwork.""" - subnet = await subtensor.get_subnet_hyperparameters( - netuid +async def subnets_list(subtensor: "SubtensorInterface"): + """List all subnet netuids in the network.""" + + async def _get_all_subnets_info(): + json_body = await subtensor.substrate.rpc_request( + method="subnetInfo_getSubnetsInfo", # custom rpc method + params=[], + ) + + return ( + SubnetInfo.list_from_vec_u8(result) + if (result := json_body.get("result")) + else [] + ) + + subnets: list[SubnetInfo] + delegate_info: dict[str, DelegatesDetails] + + subnets, delegate_info = await asyncio.gather( + _get_all_subnets_info(), + get_delegates_details_from_github(url=Constants.delegates_detail_url), ) + if not subnets: + err_console.print("[red]No subnets found[/red]") + return + + rows = [] + total_neurons = 0 + + for subnet in subnets: + total_neurons += subnet.max_n + rows.append( + ( + str(subnet.netuid), + str(subnet.subnetwork_n), + str(millify(subnet.max_n)), + f"{subnet.emission_value / RAO_PER_TAO * 100:0.2f}%", + str(subnet.tempo), + f"{subnet.burn!s:8.8}", + str(millify(subnet.difficulty)), + f"{delegate_info[subnet.owner_ss58].name if subnet.owner_ss58 in delegate_info else subnet.owner_ss58}", + ) + ) table = Table( - Column("[overline white]HYPERPARAMETER", style="white"), - Column("[overline white]VALUE", style="green"), - Column("[overline white]NORMALIZED", style="cyan"), - title=f"[white]Subnet Hyperparameters - NETUID: {netuid} - {subtensor}", + Column( + "[overline white]NETUID", + str(len(subnets)), + footer_style="overline white", + style="bold green", + justify="center", + ), + Column( + "[overline white]N", + str(total_neurons), + footer_style="overline white", + style="green", + justify="center", + ), + Column("[overline white]MAX_N", style="white", justify="center"), + Column("[overline white]EMISSION", style="white", justify="center"), + Column("[overline white]TEMPO", style="white", justify="center"), + Column("[overline white]RECYCLE", style="white", justify="center"), + Column("[overline white]POW", style="white", justify="center"), + Column("[overline white]SUDO", style="white"), + title=f"[white]Subnets - {subtensor.network}", show_footer=True, width=None, pad_edge=True, box=None, show_edge=True, ) - - normalized_values = normalize_hyperparameters(subnet) - - for param, value, norm_value in normalized_values: - table.add_row(" " + param, value, norm_value) - + for row in rows: + table.add_row(*row) console.print(table)