diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 66aa4036a..0de749e2c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -804,6 +804,12 @@ def __init__(self): self.subnets_app.command( "price", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_price) + self.subnets_app.command( + "set-identity", rich_help_panel=HELP_PANELS["SUBNETS"]["IDENTITY"] + )(self.subnets_set_identity) + self.subnets_app.command( + "get-identity", rich_help_panel=HELP_PANELS["SUBNETS"]["IDENTITY"] + )(self.subnets_get_identity) # weights commands self.weights_app.command( @@ -4452,6 +4458,7 @@ def subnets_create( validate=WV.WALLET_AND_HOTKEY, ) identity = prompt_for_subnet_identity( + current_identity={}, subnet_name=subnet_name, github_repo=github_repo, subnet_contact=subnet_contact, @@ -4482,6 +4489,118 @@ def subnets_create( verbose=verbose, ) + def subnets_get_identity( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Get the identity information for a subnet. + + This command displays the identity information of a subnet including name, GitHub repo, contact details, etc. + + [green]$[/green] btcli subnets get-identity --netuid 1 + """ + self.verbosity_handler(quiet, verbose) + return self._run_command( + subnets.get_identity( + self.initialize_chain(network), + netuid, + ) + ) + + def subnets_set_identity( + self, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + subnet_name: Optional[str] = typer.Option( + None, "--subnet-name", "--name", help="Name of the subnet" + ), + github_repo: Optional[str] = typer.Option( + None, "--github-repo", "--repo", help="GitHub repository URL" + ), + subnet_contact: Optional[str] = typer.Option( + None, + "--subnet-contact", + "--contact", + "--email", + help="Contact email for subnet", + ), + subnet_url: Optional[str] = typer.Option( + None, "--subnet-url", "--url", help="Subnet URL" + ), + discord: Optional[str] = typer.Option( + None, "--discord-handle", "--discord", help="Discord handle" + ), + description: Optional[str] = typer.Option( + None, "--description", help="Description" + ), + additional_info: Optional[str] = typer.Option( + None, "--additional-info", help="Additional information" + ), + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Set or update the identity information for a subnet. + + This command allows subnet owners to set or update identity information like name, GitHub repo, contact details, etc. + + [bold]Common Examples:[/bold] + + 1. Interactive subnet identity setting: + [green]$[/green] btcli subnets set-identity --netuid 1 + + 2. Set subnet identity with specific values: + [green]$[/green] btcli subnets set-identity --netuid 1 --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net + """ + self.verbosity_handler(quiet, verbose) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + validate=WV.WALLET, + ) + + current_identity = self._run_command( + subnets.get_identity( + self.initialize_chain(network), + netuid, + f"Current Subnet {netuid}'s Identity", + ), + exit_early=False, + ) + if current_identity is None: + raise typer.Exit() + + identity = prompt_for_subnet_identity( + current_identity=current_identity, + subnet_name=subnet_name, + github_repo=github_repo, + subnet_contact=subnet_contact, + subnet_url=subnet_url, + discord=discord, + description=description, + additional=additional_info, + ) + + return self._run_command( + subnets.set_identity( + wallet, + self.initialize_chain(network), + netuid, + identity, + prompt, + ) + ) + def subnets_pow_register( self, wallet_name: Optional[str] = Options.wallet_name, diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 23a9a1148..c9bda86cf 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -710,6 +710,7 @@ class WalletValidationTypes(Enum): "INFO": "Subnet Information", "CREATION": "Subnet Creation & Management", "REGISTER": "Neuron Registration", + "IDENTITY": "Subnet Identity Management", }, "WEIGHTS": {"COMMIT_REVEAL": "Commit / Reveal"}, } diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index de113e1c3..a1a4f1ae4 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1126,6 +1126,7 @@ def prompt_for_identity( def prompt_for_subnet_identity( + current_identity: dict, subnet_name: Optional[str], github_repo: Optional[str], subnet_contact: Optional[str], @@ -1210,7 +1211,7 @@ def prompt_for_subnet_identity( prompt, rejection=rejection_func, rejection_text=rejection_msg, - default=None, # Maybe we can add some defaults later + default=current_identity.get(key, ""), show_default=True, ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 55d2b78b2..e5d397e0f 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -35,6 +35,7 @@ update_metadata_table, prompt_for_identity, get_subnet_name, + unlock_key, ) if TYPE_CHECKING: @@ -2026,3 +2027,146 @@ async def metagraph_cmd( table.add_row(*row) console.print(table) + + +def create_identity_table(title: str = None): + if not title: + title = "Subnet Identity" + + table = Table( + Column( + "Item", + justify="right", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + no_wrap=True, + ), + Column("Value", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{title}\n", + show_footer=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + return table + + +async def set_identity( + wallet: "Wallet", + subtensor: "SubtensorInterface", + netuid: int, + subnet_identity: dict, + prompt: bool = False, +) -> bool: + """Set identity information for a subnet""" + + if not await subtensor.subnet_exists(netuid): + err_console.print(f"Subnet {netuid} does not exist") + return False + + identity_data = { + "netuid": netuid, + "subnet_name": subnet_identity.get("subnet_name", ""), + "github_repo": subnet_identity.get("github_repo", ""), + "subnet_contact": subnet_identity.get("subnet_contact", ""), + "subnet_url": subnet_identity.get("subnet_url", ""), + "discord": subnet_identity.get("discord", ""), + "description": subnet_identity.get("description", ""), + "additional": subnet_identity.get("additional", ""), + } + + if not unlock_key(wallet).success: + return False + + if prompt: + if not Confirm.ask( + "Are you sure you want to set subnet's identity? This is subject to a fee." + ): + return False + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_subnet_identity", + call_params=identity_data, + ) + + with console.status( + " :satellite: [dark_sea_green3]Setting subnet identity on-chain...", + spinner="earth", + ): + success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + + if not success: + err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") + return False + + console.print( + ":white_heavy_check_mark: [dark_sea_green3]Successfully set subnet identity\n" + ) + + subnet = await subtensor.subnet(netuid) + identity = subnet.subnet_identity if subnet else None + + if identity: + table = create_identity_table(title=f"New Subnet {netuid} Identity") + table.add_row("Netuid", str(netuid)) + for key in [ + "subnet_name", + "github_repo", + "subnet_contact", + "subnet_url", + "discord", + "description", + "additional", + ]: + value = getattr(identity, key, None) + table.add_row(key, str(value) if value else "~") + console.print(table) + + return True + + +async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str = None): + """Fetch and display existing subnet identity information.""" + if not title: + title = "Subnet Identity" + + if not await subtensor.subnet_exists(netuid): + print_error( + f"Subnet {netuid} does not exist." + ) + raise typer.Exit() + + with console.status( + ":satellite: [bold green]Querying subnet identity...", spinner="earth" + ): + subnet = await subtensor.subnet(netuid) + identity = subnet.subnet_identity if subnet else None + + if not identity: + err_console.print( + f"Existing subnet identity not found" + f" for subnet [blue]{netuid}[/blue]" + f" on {subtensor}" + ) + return {} + + if identity: + table = create_identity_table(title=f"Current Subnet {netuid} Identity") + table.add_row("Netuid", str(netuid)) + for key in [ + "subnet_name", + "github_repo", + "subnet_contact", + "subnet_url", + "discord", + "description", + "additional", + ]: + value = getattr(identity, key, None) + table.add_row(key, str(value) if value else "~") + console.print(table) + return identity