diff --git a/CHANGELOG.md b/CHANGELOG.md index 3af5badae..4ee709887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 9.0.1 /2025-02-13 + +## What's Changed +* Fixes root tempo being 0 by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/312 +* Backmerge main to staging 900 by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/313 +* Fixes fmt err msg by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/314 +* Adds subnet identities set/get by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/316 +* Fix return type annotation for `alpha_to_tao_with_slippage` by @thewhaleking in https://github.com/opentensor/btcli/pull/311 +* Updates live view of btcli stake list + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.0.0...v9.0.1 + ## 9.0.0 /2025-02-13 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 55ce167e0..7b21ec4e8 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "9.0.0" +__version__ = "9.0.1" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 66aa4036a..0810152d2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -72,7 +72,7 @@ class GitError(Exception): pass -__version__ = "9.0.0" +__version__ = "9.0.1" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -656,7 +656,7 @@ def __init__(self): self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) self.config_app.command("clear")(self.del_config) - self.config_app.command("metagraph", hidden=True)(self.metagraph_config) + # self.config_app.command("metagraph", hidden=True)(self.metagraph_config) # wallet commands self.wallet_app.command( @@ -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..0a6d09a3a 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -20,7 +20,7 @@ class Constants: subvortex_entrypoint = "ws://subvortex.info:9944" local_entrypoint = "ws://127.0.0.1:9944" rao_entrypoint = "wss://rao.chain.opentensor.ai:443" - dev_entrypoint = "wss://dev.chain.opentensor.ai:443 " + dev_entrypoint = "wss://dev.chain.opentensor.ai:443" local_entrypoint = "ws://127.0.0.1:9944" latent_lite_entrypoint = "wss://lite.sub.latent.to:443" network_map = { @@ -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/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 3b5a4c89a..d873d51cf 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -750,7 +750,9 @@ def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: ) return alpha_returned, slippage, slippage_pct_float - def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: + def alpha_to_tao_with_slippage( + self, alpha: Balance + ) -> tuple[Balance, Balance, float]: """ Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state. Args: 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/stake/add.py b/bittensor_cli/src/commands/stake/add.py index d400ce967..cbc1ae242 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -115,7 +115,7 @@ async def safe_stake_extrinsic( await response.process_events() if not await response.is_success: err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" + f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) else: block_hash = await subtensor.substrate.get_chain_head() @@ -188,7 +188,7 @@ async def stake_extrinsic( await response.process_events() if not await response.is_success: err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" + f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) else: new_balance, new_stake = await asyncio.gather( diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 9f2ba7763..47fc2dfc2 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -45,7 +45,7 @@ async def get_stake_data(block_hash: str = None): coldkey_ss58=coldkey_address, block_hash=block_hash ), subtensor.get_delegate_identities(block_hash=block_hash), - subtensor.all_subnets(), + subtensor.all_subnets(block_hash=block_hash), ) # sub_stakes = substakes[coldkey_address] dynamic_info = {info.netuid: info for info in _dynamic_info} @@ -199,7 +199,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): issuance = pool.alpha_out if pool.is_dynamic else tao_locked # Per block emission cell - per_block_emission = substake_.emission.tao / pool.tempo + per_block_emission = substake_.emission.tao / (pool.tempo or 1) # Alpha ownership and TAO ownership cells if alpha_value.tao > 0.00009: if issuance.tao != 0: @@ -319,7 +319,7 @@ def format_cell( alpha_value = Balance.from_rao(int(substake.stake.rao)).set_unit(netuid) tao_value = pool.alpha_to_tao(alpha_value) total_tao_value += tao_value - swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( + swapped_tao_value, slippage, slippage_pct = pool.alpha_to_tao_with_slippage( substake.stake ) total_swapped_tao_value += swapped_tao_value @@ -341,7 +341,7 @@ def format_cell( "price": pool.price.tao, "tao_value": tao_value.tao, "swapped_value": swapped_tao_value.tao, - "emission": substake.emission.tao / pool.tempo, + "emission": substake.emission.tao / (pool.tempo or 1), "tao_ownership": tao_ownership.tao, } @@ -376,15 +376,6 @@ def format_cell( millify=True if not verbose else False, ) - if pool.is_dynamic: - slippage_pct = ( - 100 * float(slippage) / float(slippage + swapped_tao_value) - if slippage + swapped_tao_value != 0 - else 0 - ) - else: - slippage_pct = 0 - if netuid != 0: swap_cell = ( format_cell( @@ -400,7 +391,7 @@ def format_cell( else: swap_cell = f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A[/{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}] ({slippage_pct}%)" - emission_value = substake.emission.tao / pool.tempo + emission_value = substake.emission.tao / (pool.tempo or 1) emission_cell = format_cell( emission_value, prev.get("emission"), @@ -443,11 +434,12 @@ def format_cell( return table, current_data # Main execution + block_hash = await subtensor.substrate.get_chain_head() ( sub_stakes, registered_delegate_info, dynamic_info, - ) = await get_stake_data() + ) = await get_stake_data(block_hash) balance = await subtensor.get_balance(coldkey_address) # Iterate over substakes and aggregate them by hotkey. @@ -536,7 +528,7 @@ def format_cell( table, current_data = create_live_table( selected_stakes, registered_delegate_info, - dynamic_info, + dynamic_info_, hotkey_name, previous_data, ) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 5ce500f13..3197d395e 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -812,7 +812,7 @@ async def transfer_stake( 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, subtensor.substrate)}" + f"{format_error_message(await response.error_message)}" ) return False @@ -971,7 +971,7 @@ async def swap_stake( 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, subtensor.substrate)}" + f"{format_error_message(await response.error_message)}" ) return False diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 25f53e08e..a8d364b5f 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -561,7 +561,7 @@ async def _unstake_extrinsic( if not await response.is_success: err_out( f"{failure_prelude} with error: " - f"{format_error_message(await response.error_message, subtensor.substrate)}" + f"{format_error_message(await response.error_message)}" ) return @@ -674,7 +674,7 @@ async def _safe_unstake_extrinsic( await response.process_events() if not await response.is_success: err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message, subtensor.substrate)}" + f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) return diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 4f116e626..d71d726f8 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: @@ -183,7 +184,7 @@ async def _find_event_attributes_in_extrinsic_receipt( await response.process_events() if not await response.is_success: err_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message, substrate)}" + f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" ) await asyncio.sleep(0.5) return False @@ -889,7 +890,7 @@ async def show_root(): for netuid_ in range(len(all_subnets)): subnet = all_subnets[netuid_] emission_on_subnet = ( - root_state.emission_history[netuid_][idx] / subnet.tempo + root_state.emission_history[netuid_][idx] / (subnet.tempo or 1) ) total_emission_per_block += subnet.alpha_to_tao( Balance.from_rao(emission_on_subnet) @@ -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 diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index dbbdf5376..8530f3f94 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -692,10 +692,7 @@ async def senate_vote( return False # Unlock the wallet. - if ( - not unlock_key(wallet, "hot").success - and unlock_key(wallet, "cold").success - ): + if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success: return False console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") @@ -771,10 +768,7 @@ async def _do_set_take() -> bool: f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}" ) - if ( - not unlock_key(wallet, "hot").success - and unlock_key(wallet, "cold").success - ): + if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success: return False result_ = await _do_set_take()