diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee709887..ed6f82e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 9.0.2 /2025-02-20 + +## What's Changed +* Fix stake child get by @thewhaleking in https://github.com/opentensor/btcli/pull/321 +* Edge case alpha formatting by @thewhaleking in https://github.com/opentensor/btcli/pull/318 +* Adds Tao emissions to stake list by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/300 +* Updates balance command by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/322 +* Backmerge main to staging 101 by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/326 +* Updates stake list (with swap value) by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/327 +* Adds unstaking from all hotkeys + tests by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/325 +* Mnemonic helper text by @thewhaleking in https://github.com/opentensor/btcli/pull/329 +* fix: remove double conversion in stake swap functionality [--swap_all] by @ashikshafi08 in https://github.com/opentensor/btcli/pull/328 +* Arbitrary Hyperparams Setting by @thewhaleking in https://github.com/opentensor/btcli/pull/320 +* Bumps deps for btcli by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/330 +* SubtensorInterface async with logic by @thewhaleking in https://github.com/opentensor/btcli/pull/331 +* remove __version__ from cli.py by @igorsyl in https://github.com/opentensor/btcli/pull/323 + +## New Contributors +* @ashikshafi08 made their first contribution in https://github.com/opentensor/btcli/pull/328 +* @igorsyl made their first contribution in https://github.com/opentensor/btcli/pull/323 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.0.1...v9.0.2 + ## 9.0.1 /2025-02-13 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 7b21ec4e8..656f68a42 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -16,8 +16,7 @@ # DEALINGS IN THE SOFTWARE. from .cli import CLIManager +from .version import __version__, __version_as_int__ -__version__ = "9.0.1" - -__all__ = ["CLIManager", "__version__"] +__all__ = ["CLIManager"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0810152d2..2c5b70992 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -26,7 +26,9 @@ WalletValidationTypes as WV, Constants, COLOR_PALETTE, + HYPERPARAMS, ) +from bittensor_cli.version import __version__, __version_as_int__ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from async_substrate_interface.errors import SubstrateRequestException @@ -72,21 +74,8 @@ class GitError(Exception): pass -__version__ = "9.0.1" -_core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) -_version_split = _core_version.split(".") -__version_info__ = tuple(int(part) for part in _version_split) -_version_int_base = 1000 -assert max(__version_info__) < _version_int_base - -__version_as_int__: int = sum( - e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__)) -) -assert __version_as_int__ < 2**31 # fits in int32 -__new_signature_version__ = 360 - _epilog = "Made with [bold red]:heart:[/bold red] by The Openτensor Foundaτion" np.set_printoptions(precision=8, suppress=True, floatmode="fixed") @@ -124,7 +113,7 @@ class Options: ) mnemonic = typer.Option( None, - help="Mnemonic used to regenerate your key. For example: horse cart dog ...", + help='Mnemonic used to regenerate your key. For example: "horse cart dog ..."', ) seed = typer.Option( None, help="Seed hex string used to regenerate your key. For example: 0x1234..." @@ -2130,6 +2119,7 @@ def wallet_regen_hotkey( # Example usage: [green]$[/green] btcli wallet regen_hotkey --seed 0x1234... + [green]$[/green] btcli wallet regen-hotkey --mnemonic "word1 word2 ... word12" [bold]Note[/bold]: This command is essential for users who need to regenerate their hotkey, possibly for security upgrades or key recovery. It should be used with caution to avoid accidental overwriting of existing keys. @@ -3221,11 +3211,23 @@ def stake_remove( else: print_error("Invalid hotkey ss58 address.") raise typer.Exit() - else: - hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from", - default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + elif all_hotkeys: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], ) + else: + if not hotkey_ss58_address and not wallet_hotkey: + hotkey_or_ss58 = Prompt.ask( + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from [dim](or enter 'all' to unstake from all hotkeys)[/dim]", + default=self.config.get("wallet_hotkey") + or defaults.wallet.hotkey, + ) + else: + hotkey_or_ss58 = hotkey_ss58_address or wallet_hotkey + if is_valid_ss58_address(hotkey_or_ss58): hotkey_ss58_address = hotkey_or_ss58 wallet = self.wallet_ask( @@ -3234,6 +3236,14 @@ def stake_remove( wallet_hotkey, ask_for=[WO.NAME, WO.PATH], ) + elif hotkey_or_ss58 == "all": + all_hotkeys = True + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) else: wallet_hotkey = hotkey_or_ss58 wallet = self.wallet_ask( @@ -3249,6 +3259,9 @@ def stake_remove( subtensor=self.initialize_chain(network), hotkey_ss58_address=hotkey_ss58_address, unstake_all_alpha=unstake_all_alpha, + all_hotkeys=all_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, prompt=prompt, ) ) @@ -3961,7 +3974,7 @@ def sudo_set( param_name: str = typer.Option( "", "--param", "--parameter", help="The subnet hyperparameter to set" ), - param_value: str = typer.Option( + param_value: Optional[str] = typer.Option( "", "--value", help="Value to set the hyperparameter to." ), quiet: bool = Options.quiet, @@ -4010,9 +4023,12 @@ def sudo_set( param_value = f"{low_val},{high_val}" if not param_value: - param_value = Prompt.ask( - f"Enter the new value for [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{param_name}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] in the VALUE column format" - ) + if HYPERPARAMS.get(param_name): + param_value = Prompt.ask( + f"Enter the new value for [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{param_name}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] in the VALUE column format" + ) + else: + param_value = None wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 0a6d09a3a..e01563096 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -672,7 +672,7 @@ class WalletValidationTypes(Enum): "commit_reveal_weights_enabled": ("sudo_set_commit_reveal_weights_enabled", False), "alpha_values": ("sudo_set_alpha_values", False), "liquid_alpha_enabled": ("sudo_set_liquid_alpha_enabled", False), - "network_registration_allowed": ("sudo_set_network_registration_allowed", False), + "registration_allowed": ("sudo_set_network_registration_allowed", False), "network_pow_registration_allowed": ( "sudo_set_network_pow_registration_allowed", False, diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 3a9c2fc15..b83001634 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -76,7 +76,7 @@ def __str__(self): if self.unit == UNITS[0]: return f"{self.unit} {float(self.tao):,.4f}" else: - return f"{float(self.tao):,.4f} {self.unit}\u200e" + return f"\u200e{float(self.tao):,.4f} {self.unit}\u200e" def __rich__(self): return "[green]{}[/green][green]{}[/green][green].[/green][dim green]{}[/dim green]".format( diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index d873d51cf..9d71c675e 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -156,7 +156,7 @@ class SubnetHyperparameters(InfoBase): def _fix_decoded( cls, decoded: Union[dict, "SubnetHyperparameters"] ) -> "SubnetHyperparameters": - return SubnetHyperparameters( + return cls( rho=decoded.get("rho"), kappa=decoded.get("kappa"), immunity_period=decoded.get("immunity_period"), @@ -197,6 +197,7 @@ class StakeInfo(InfoBase): stake: Balance # Stake for the hotkey-coldkey pair locked: Balance # Stake which is locked. emission: Balance # Emission for the hotkey-coldkey pair + tao_emission: Balance # TAO emission for the hotkey-coldkey pair drain: int is_registered: bool @@ -208,11 +209,20 @@ def _fix_decoded(cls, decoded: Any) -> "StakeInfo": stake = Balance.from_rao(decoded.get("stake")).set_unit(netuid) locked = Balance.from_rao(decoded.get("locked")).set_unit(netuid) emission = Balance.from_rao(decoded.get("emission")).set_unit(netuid) + tao_emission = Balance.from_rao(decoded.get("tao_emission")) drain = int(decoded.get("drain")) is_registered = bool(decoded.get("is_registered")) - return StakeInfo( - hotkey, coldkey, netuid, stake, locked, emission, drain, is_registered + return cls( + hotkey, + coldkey, + netuid, + stake, + locked, + emission, + tao_emission, + drain, + is_registered, ) @@ -293,7 +303,7 @@ def _fix_decoded(cls, decoded: Any) -> "NeuronInfo": axon_info = decoded.get("axon_info", {}) coldkey = decode_account_id(decoded.get("coldkey")) hotkey = decode_account_id(decoded.get("hotkey")) - return NeuronInfo( + return cls( hotkey=hotkey, coldkey=coldkey, uid=decoded.get("uid"), @@ -555,7 +565,7 @@ class SubnetInfo(InfoBase): @classmethod def _fix_decoded(cls, decoded: "SubnetInfo") -> "SubnetInfo": - return SubnetInfo( + return cls( netuid=decoded.get("netuid"), rho=decoded.get("rho"), kappa=decoded.get("kappa"), @@ -594,7 +604,7 @@ class SubnetIdentity(InfoBase): @classmethod def _fix_decoded(cls, decoded: dict) -> "SubnetIdentity": - return SubnetIdentity( + return cls( subnet_name=bytes(decoded["subnet_name"]).decode(), github_repo=bytes(decoded["github_repo"]).decode(), subnet_contact=bytes(decoded["subnet_contact"]).decode(), @@ -828,7 +838,7 @@ class SubnetState(InfoBase): @classmethod def _fix_decoded(cls, decoded: Any) -> "SubnetState": netuid = decoded.get("netuid") - return SubnetState( + return cls( netuid=netuid, hotkeys=[decode_account_id(val) for val in decoded.get("hotkeys")], coldkeys=[decode_account_id(val) for val in decoded.get("coldkeys")], diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index cf5ac9e93..65474b324 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -112,8 +112,8 @@ async def __aenter__(self): f"[yellow]Connecting to Substrate:[/yellow][bold white] {self}..." ): try: - async with self.substrate: - return self + await self.substrate.initialize() + return self except TimeoutError: # TODO verify err_console.print( "\n[red]Error[/red]: Timeout occurred connecting to substrate. " @@ -352,7 +352,7 @@ async def get_total_stake_for_coldkey( *ss58_addresses, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, Balance]: + ) -> dict[str, tuple[Balance, Balance]]: """ Returns the total stake held on a coldkey. @@ -370,7 +370,8 @@ async def get_total_stake_for_coldkey( results = {} for ss58, stake_info_list in sub_stakes.items(): - all_staked_tao = 0 + total_tao_value = Balance(0) + total_swapped_tao_value = Balance(0) for sub_stake in stake_info_list: if sub_stake.stake.rao == 0: continue @@ -381,19 +382,20 @@ async def get_total_stake_for_coldkey( netuid ) - tao_locked = pool.tao_in - - issuance = pool.alpha_out if pool.is_dynamic else tao_locked - tao_ownership = Balance(0) + # Without slippage + tao_value = pool.alpha_to_tao(alpha_value) + total_tao_value += tao_value - if alpha_value.tao > 0.00009 and issuance.tao != 0: - tao_ownership = Balance.from_tao( - (alpha_value.tao / issuance.tao) * tao_locked.tao + # With slippage + if netuid == 0: + swapped_tao_value = tao_value + else: + swapped_tao_value, _, _ = pool.alpha_to_tao_with_slippage( + sub_stake.stake ) + total_swapped_tao_value += swapped_tao_value - all_staked_tao += tao_ownership.rao - - results[ss58] = Balance.from_rao(all_staked_tao) + results[ss58] = (total_tao_value, total_swapped_tao_value) return results async def get_total_stake_for_hotkey( diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index cbc1ae242..4aba39e69 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -107,9 +107,7 @@ async def safe_stake_extrinsic( ) return else: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e)}" - ) + err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") return else: await response.process_events() @@ -180,9 +178,7 @@ async def stake_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) except SubstrateRequestException as e: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e)}" - ) + err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") return else: await response.process_events() diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index c0a4914e4..ef50a5232 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -256,7 +256,7 @@ async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int params=[hotkey, netuid], ) if childkey_take_: - return int(childkey_take_.value) + return int(childkey_take_) except SubstrateRequestException as e: err_console.print(f"Error querying ChildKeys: {format_error_message(e)}") diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index 47fc2dfc2..8523320ac 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -58,7 +58,6 @@ async def get_stake_data(block_hash: str = None): def define_table( hotkey_name: str, rows: list[list[str]], - total_tao_ownership: Balance, total_tao_value: Balance, total_swapped_tao_value: Balance, live: bool = False, @@ -130,6 +129,11 @@ def define_table( style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="right", ) + table.add_column( + f"[white]Emission \n({Balance.get_unit(0)}/block)", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + justify="right", + ) return table def create_table(hotkey_: str, substakes: list[StakeInfo]): @@ -139,7 +143,6 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): else hotkey_ ) rows = [] - total_tao_ownership = Balance(0) total_tao_value = Balance(0) total_swapped_tao_value = Balance(0) root_stakes = [s for s in substakes if s.netuid == 0] @@ -155,14 +158,6 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): netuid = substake_.netuid pool = dynamic_info[netuid] symbol = f"{Balance.get_unit(netuid)}\u200e" - # TODO: what is this price var for? - price = ( - "{:.4f}{}".format( - pool.price.__float__(), f" τ/{Balance.get_unit(netuid)}\u200e" - ) - if pool.is_dynamic - else (f" 1.0000 τ/{symbol} ") - ) # Alpha value cell alpha_value = Balance.from_rao(int(substake_.stake.rao)).set_unit(netuid) @@ -192,30 +187,11 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): else f"{swapped_tao_value} ({slippage_percentage})" ) - # TAO locked cell - tao_locked = pool.tao_in - - # Issuance cell - issuance = pool.alpha_out if pool.is_dynamic else tao_locked - # Per block emission cell per_block_emission = substake_.emission.tao / (pool.tempo or 1) + per_block_tao_emission = substake_.tao_emission.tao / (pool.tempo or 1) # Alpha ownership and TAO ownership cells if alpha_value.tao > 0.00009: - if issuance.tao != 0: - # TODO figure out why this alpha_ownership does nothing - alpha_ownership = "{:.4f}".format( - (alpha_value.tao / issuance.tao) * 100 - ) - tao_ownership = Balance.from_tao( - (alpha_value.tao / issuance.tao) * tao_locked.tao - ) - total_tao_ownership += tao_ownership - else: - # TODO what's this var for? - alpha_ownership = "0.0000" - tao_ownership = Balance.from_tao(0) - stake_value = ( millify_tao(substake_.stake.tao) if not verbose @@ -243,15 +219,14 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): # Removing this flag for now, TODO: Confirm correct values are here w.r.t CHKs # if substake_.is_registered # else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]N/A", # Emission(α/block) + str(Balance.from_tao(per_block_tao_emission)), ] ) - table = define_table( - name, rows, total_tao_ownership, total_tao_value, total_swapped_tao_value - ) + table = define_table(name, rows, total_tao_value, total_swapped_tao_value) for row in rows: table.add_row(*row) console.print(table) - return total_tao_ownership, total_tao_value + return total_tao_value, total_swapped_tao_value def create_live_table( substakes: list, @@ -263,7 +238,6 @@ def create_live_table( rows = [] current_data = {} - total_tao_ownership = Balance(0) total_tao_value = Balance(0) total_swapped_tao_value = Balance(0) @@ -324,17 +298,6 @@ def format_cell( ) total_swapped_tao_value += swapped_tao_value - # Calculate TAO ownership - tao_locked = pool.tao_in - issuance = pool.alpha_out if pool.is_dynamic else tao_locked - if alpha_value.tao > 0.00009 and issuance.tao != 0: - tao_ownership = Balance.from_tao( - (alpha_value.tao / issuance.tao) * tao_locked.tao - ) - total_tao_ownership += tao_ownership - else: - tao_ownership = Balance.from_tao(0) - # Store current values for future delta tracking current_data[netuid] = { "stake": alpha_value.tao, @@ -342,7 +305,7 @@ def format_cell( "tao_value": tao_value.tao, "swapped_value": swapped_tao_value.tao, "emission": substake.emission.tao / (pool.tempo or 1), - "tao_ownership": tao_ownership.tao, + "tao_emission": substake.tao_emission.tao / (pool.tempo or 1), } # Get previous values for delta tracking @@ -399,6 +362,16 @@ def format_cell( unit_first=unit_first, precision=4, ) + + tao_emission_value = substake.tao_emission.tao / (pool.tempo or 1) + tao_emission_cell = format_cell( + tao_emission_value, + prev.get("tao_emission"), + unit="τ", + unit_first=unit_first, + precision=4, + ) + subnet_name_cell = ( f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" f" {get_subnet_name(dynamic_info[netuid])}" @@ -416,13 +389,13 @@ def format_cell( if substake.is_registered else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registration status emission_cell, # Emission rate + tao_emission_cell, # TAO emission rate ] ) table = define_table( hotkey_name, rows, - total_tao_ownership, total_tao_value, total_swapped_tao_value, live=True, @@ -458,7 +431,7 @@ def format_cell( raise typer.Exit() if live: - # Select one hokkey for live monitoring + # Select one hotkey for live monitoring if len(hotkeys_to_substakes) > 1: console.print( "\n[bold]Multiple hotkeys found. Please select one for live monitoring:[/bold]" @@ -562,27 +535,29 @@ def format_cell( # Iterate over each hotkey and make a table counter = 0 num_hotkeys = len(hotkeys_to_substakes) - all_hotkeys_total_global_tao = Balance(0) - all_hotkeys_total_tao_value = Balance(0) + all_hks_swapped_tao_value = Balance(0) + all_hks_tao_value = Balance(0) for hotkey in hotkeys_to_substakes.keys(): counter += 1 - stake, value = create_table(hotkey, hotkeys_to_substakes[hotkey]) - all_hotkeys_total_global_tao += stake - all_hotkeys_total_tao_value += value + tao_value, swapped_tao_value = create_table( + hotkey, hotkeys_to_substakes[hotkey] + ) + all_hks_tao_value += tao_value + all_hks_swapped_tao_value += swapped_tao_value if num_hotkeys > 1 and counter < num_hotkeys and prompt: console.print("\nPress Enter to continue to the next hotkey...") input() total_tao_value = ( - f"τ {millify_tao(all_hotkeys_total_tao_value.tao)}" + f"τ {millify_tao(all_hks_tao_value.tao)}" if not verbose - else all_hotkeys_total_tao_value + else all_hks_tao_value ) - total_tao_ownership = ( - f"τ {millify_tao(all_hotkeys_total_global_tao.tao)}" + total_swapped_tao_value = ( + f"τ {millify_tao(all_hks_swapped_tao_value.tao)}" if not verbose - else all_hotkeys_total_global_tao + else all_hks_swapped_tao_value ) console.print("\n\n") @@ -590,8 +565,8 @@ def format_cell( f"Wallet:\n" f" Coldkey SS58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{coldkey_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" f" Free Balance: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_ownership}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" - f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" + f" Total TAO Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total TAO Swapped Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_swapped_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) if not sub_stakes: console.print( diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 3197d395e..964f0ed1b 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -905,7 +905,7 @@ async def swap_stake( ) if swap_all: - amount_to_swap = Balance.from_tao(current_stake).set_unit(origin_netuid) + amount_to_swap = current_stake.set_unit(origin_netuid) else: amount_to_swap = Balance.from_tao(amount).set_unit(origin_netuid) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index a8d364b5f..048855fdc 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -49,26 +49,36 @@ async def unstake( f"Retrieving subnet data & identities from {subtensor.network}...", spinner="earth", ): - all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather( - subtensor.all_subnets(), - subtensor.fetch_coldkey_hotkey_identities(), - subtensor.get_delegate_identities(), + chain_head = await subtensor.substrate.get_chain_head() + ( + all_sn_dynamic_info_, + ck_hk_identities, + old_identities, + stake_infos, + ) = await asyncio.gather( + subtensor.all_subnets(block_hash=chain_head), + subtensor.fetch_coldkey_hotkey_identities(block_hash=chain_head), + subtensor.get_delegate_identities(block_hash=chain_head), + subtensor.get_stake_for_coldkey( + wallet.coldkeypub.ss58_address, block_hash=chain_head + ), ) all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} if interactive: hotkeys_to_unstake_from, unstake_all_from_hk = await _unstake_selection( - subtensor, - wallet, all_sn_dynamic_info, ck_hk_identities, old_identities, + stake_infos, netuid=netuid, ) if unstake_all_from_hk: hotkey_to_unstake_all = hotkeys_to_unstake_from[0] unstake_all_alpha = Confirm.ask( - "\nUnstake [blue]all alpha stakes[/blue] and stake back to [blue]root[/blue]? (No will unstake everything)", + "\nDo you want to:\n" + "[blue]Yes[/blue]: Unstake from all subnets and automatically restake to subnet 0 (root)\n" + "[blue]No[/blue]: Unstake everything (including subnet 0)", default=True, ) return await unstake_all( @@ -96,20 +106,17 @@ async def unstake( all_hotkeys=all_hotkeys, include_hotkeys=include_hotkeys, exclude_hotkeys=exclude_hotkeys, + stake_infos=stake_infos, + identities=ck_hk_identities, + old_identities=old_identities, ) with console.status( f"Retrieving stake data from {subtensor.network}...", spinner="earth", ): - # Fetch stake balances - chain_head = await subtensor.substrate.get_chain_head() - stake_info_list = await subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=chain_head, - ) stake_in_netuids = {} - for stake_info in stake_info_list: + for stake_info in stake_infos: if stake_info.hotkey_ss58 not in stake_in_netuids: stake_in_netuids[stake_info.hotkey_ss58] = {} stake_in_netuids[stake_info.hotkey_ss58][stake_info.netuid] = ( @@ -313,6 +320,9 @@ async def unstake_all( subtensor: "SubtensorInterface", hotkey_ss58_address: str, unstake_all_alpha: bool = False, + all_hotkeys: bool = False, + include_hotkeys: list[str] = [], + exclude_hotkeys: list[str] = [], prompt: bool = True, ) -> bool: """Unstakes all stakes from all hotkeys in all subnets.""" @@ -334,10 +344,27 @@ async def unstake_all( subtensor.all_subnets(), subtensor.get_balance(wallet.coldkeypub.ss58_address), ) - if not hotkey_ss58_address: - hotkey_ss58_address = wallet.hotkey.ss58_address + + if all_hotkeys: + hotkeys = _get_hotkeys_to_unstake( + wallet, + hotkey_ss58_address=hotkey_ss58_address, + all_hotkeys=all_hotkeys, + include_hotkeys=include_hotkeys, + exclude_hotkeys=exclude_hotkeys, + stake_infos=stake_info, + identities=ck_hk_identities, + old_identities=old_identities, + ) + elif not hotkey_ss58_address: + hotkeys = [(wallet.hotkey_str, wallet.hotkey.ss58_address)] + else: + hotkeys = [(None, hotkey_ss58_address)] + + hotkey_names = {ss58: name for name, ss58 in hotkeys if name is not None} + hotkey_ss58s = [ss58 for _, ss58 in hotkeys] stake_info = [ - stake for stake in stake_info if stake.hotkey_ss58 == hotkey_ss58_address + stake for stake in stake_info if stake.hotkey_ss58 in hotkey_ss58s ] if unstake_all_alpha: @@ -403,18 +430,7 @@ async def unstake_all( if stake.stake.rao == 0: continue - # Get hotkey identity - if hk_identity := ck_hk_identities["hotkeys"].get(stake.hotkey_ss58): - hotkey_name = hk_identity.get("identity", {}).get( - "name", "" - ) or hk_identity.get("display", "~") - hotkey_display = f"{hotkey_name}" - elif old_identity := old_identities.get(stake.hotkey_ss58): - hotkey_name = old_identity.display - hotkey_display = f"{hotkey_name}" - else: - hotkey_display = stake.hotkey_ss58 - + hotkey_display = hotkey_names.get(stake.hotkey_ss58, stake.hotkey_ss58) subnet_info = all_sn_dynamic_info.get(stake.netuid) stake_amount = stake.stake received_amount, slippage_pct, slippage_pct_float = _calculate_slippage( @@ -455,56 +471,16 @@ async def unstake_all( err_console.print("Error decrypting coldkey (possibly incorrect password)") return False - console_status = ( - ":satellite: Unstaking all Alpha stakes..." - if unstake_all_alpha - else ":satellite: Unstaking all stakes..." - ) - previous_root_stake = await subtensor.get_stake( - hotkey_ss58=hotkey_ss58_address, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=0, - ) - with console.status(console_status): - call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params={"hotkey": hotkey_ss58_address}, - ) - success, error_message = await subtensor.sign_and_send_extrinsic( - call=call, - wallet=wallet, - wait_for_inclusion=True, - wait_for_finalization=False, - ) - - if success: - success_message = ( - ":white_heavy_check_mark: [green]Successfully unstaked all stakes[/green]" - if not unstake_all_alpha - else ":white_heavy_check_mark: [green]Successfully unstaked all Alpha stakes[/green]" - ) - console.print(success_message) - new_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - console.print( - f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - if unstake_all_alpha: - root_stake = await subtensor.get_stake( - hotkey_ss58=hotkey_ss58_address, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=0, - ) - console.print( - f"Root Stake:\n [blue]{previous_root_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{root_stake}" - ) - return True - else: - err_console.print( - f":cross_mark: [red]Failed to unstake[/red]: {error_message}" + with console.status("Unstaking all stakes...") as status: + for hotkey_ss58 in hotkey_ss58s: + await _unstake_all_extrinsic( + wallet=wallet, + subtensor=subtensor, + hotkey_ss58=hotkey_ss58, + hotkey_name=hotkey_names.get(hotkey_ss58, hotkey_ss58), + unstake_all_alpha=unstake_all_alpha, + status=status, ) - return False # Extrinsics @@ -666,9 +642,7 @@ async def _safe_unstake_extrinsic( ) return else: - err_out( - f"\n{failure_prelude} with error: {format_error_message(e)}" - ) + err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") return await response.process_events() @@ -709,6 +683,115 @@ async def _safe_unstake_extrinsic( ) +async def _unstake_all_extrinsic( + wallet: Wallet, + subtensor: "SubtensorInterface", + hotkey_ss58: str, + hotkey_name: str, + unstake_all_alpha: bool, + status=None, +) -> None: + """Execute an unstake all extrinsic. + + Args: + wallet: Wallet instance + subtensor: Subtensor interface + hotkey_ss58: Hotkey SS58 address + hotkey_name: Display name of the hotkey + unstake_all_alpha: Whether to unstake only alpha stakes + status: Optional status for console updates + """ + err_out = partial(print_error, status=status) + failure_prelude = ( + f":cross_mark: [red]Failed[/red] to unstake all from {hotkey_name}" + ) + + if status: + status.update( + f"\n:satellite: {'Unstaking all Alpha stakes' if unstake_all_alpha else 'Unstaking all stakes'} from {hotkey_name} ..." + ) + + block_hash = await subtensor.substrate.get_chain_head() + if unstake_all_alpha: + previous_root_stake, current_balance = await asyncio.gather( + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=0, + block_hash=block_hash, + ), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + ) + else: + current_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ) + + call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params={"hotkey": hotkey_ss58}, + ) + + try: + response = await subtensor.substrate.submit_extrinsic( + extrinsic=await subtensor.substrate.create_signed_extrinsic( + call=call, + keypair=wallet.coldkey, + ), + wait_for_inclusion=True, + wait_for_finalization=False, + ) + await response.process_events() + + if not await response.is_success: + err_out( + f"{failure_prelude} with error: " + f"{format_error_message(await response.error_message)}" + ) + return + + # Fetch latest balance and stake + block_hash = await subtensor.substrate.get_chain_head() + if unstake_all_alpha: + new_root_stake, new_balance = await asyncio.gather( + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=0, + block_hash=block_hash, + ), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ), + ) + else: + new_balance = await subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ) + + success_message = ( + ":white_heavy_check_mark: [green]Finalized: Successfully unstaked all stakes[/green]" + if not unstake_all_alpha + else ":white_heavy_check_mark: [green]Finalized: Successfully unstaked all Alpha stakes[/green]" + ) + console.print(f"{success_message} from {hotkey_name}") + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + + if unstake_all_alpha: + console.print( + f"Root Stake for {hotkey_name}:\n [blue]{previous_root_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_root_stake}" + ) + + except Exception as e: + err_out(f"{failure_prelude} with error: {str(e)}") + + # Helpers def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: """Calculate slippage and received amount for unstaking operation. @@ -737,17 +820,12 @@ def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, flo async def _unstake_selection( - subtensor: "SubtensorInterface", - wallet: Wallet, dynamic_info, identities, old_identities, + stake_infos, netuid: Optional[int] = None, ): - stake_infos = await subtensor.get_stake_for_coldkey( - coldkey_ss58=wallet.coldkeypub.ss58_address - ) - if not stake_infos: print_error("You have no stakes to unstake.") raise typer.Exit() @@ -771,16 +849,11 @@ async def _unstake_selection( hotkeys_info = [] for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): - if hk_identity := identities["hotkeys"].get(hotkey_ss58): - hotkey_name = hk_identity.get("identity", {}).get( - "name", "" - ) or hk_identity.get("display", "~") - elif old_identity := old_identities.get(hotkey_ss58): - hotkey_name = old_identity.display - else: - hotkey_name = "~" - # TODO: Add wallet ids here. - + hotkey_name = get_hotkey_identity( + hotkey_ss58=hotkey_ss58, + identities=identities, + old_identities=old_identities, + ) hotkeys_info.append( { "index": idx, @@ -983,6 +1056,9 @@ def _get_hotkeys_to_unstake( all_hotkeys: bool, include_hotkeys: list[str], exclude_hotkeys: list[str], + stake_infos: list, + identities: dict, + old_identities: dict, ) -> list[tuple[Optional[str], str]]: """Get list of hotkeys to unstake from based on input parameters. @@ -1002,13 +1078,27 @@ def _get_hotkeys_to_unstake( if all_hotkeys: print_verbose("Unstaking from all hotkeys") - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - return [ + all_hotkeys_ = get_hotkey_wallets_for_wallet(wallet=wallet) + wallet_hotkeys = [ (wallet.hotkey_str, wallet.hotkey.ss58_address) for wallet in all_hotkeys_ if wallet.hotkey_str not in exclude_hotkeys ] + wallet_hotkey_addresses = {addr for _, addr in wallet_hotkeys} + chain_hotkeys = [ + ( + get_hotkey_identity(stake_info.hotkey_ss58, identities, old_identities), + stake_info.hotkey_ss58, + ) + for stake_info in stake_infos + if ( + stake_info.hotkey_ss58 not in wallet_hotkey_addresses + and stake_info.hotkey_ss58 not in exclude_hotkeys + ) + ] + return wallet_hotkeys + chain_hotkeys + if include_hotkeys: print_verbose("Unstaking from included hotkeys") result = [] @@ -1144,3 +1234,28 @@ def _print_table_and_slippage( - [bold white]Partial unstaking[/bold white]: If True, allows unstaking up to the rate tolerance limit. If False, the entire transaction will fail if rate tolerance is exceeded.\n""" console.print(base_description + (safe_staking_description if safe_staking else "")) + + +def get_hotkey_identity( + hotkey_ss58: str, + identities: dict, + old_identities: dict, +) -> str: + """Get identity name for a hotkey from identities or old_identities. + + Args: + hotkey_ss58 (str): The hotkey SS58 address + identities (dict): Current identities from fetch_coldkey_hotkey_identities + old_identities (dict): Old identities from get_delegate_identities + + Returns: + str: Identity name or truncated address + """ + if hk_identity := identities["hotkeys"].get(hotkey_ss58): + return hk_identity.get("identity", {}).get("name", "") or hk_identity.get( + "display", "~" + ) + elif old_identity := old_identities.get(hotkey_ss58): + return old_identity.display + else: + return f"{hotkey_ss58[:4]}...{hotkey_ss58[-4:]}" diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d71d726f8..f4e01486c 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -889,8 +889,8 @@ async def show_root(): total_emission_per_block = 0 for netuid_ in range(len(all_subnets)): subnet = all_subnets[netuid_] - emission_on_subnet = ( - root_state.emission_history[netuid_][idx] / (subnet.tempo or 1) + emission_on_subnet = root_state.emission_history[netuid_][idx] / ( + subnet.tempo or 1 ) total_emission_per_block += subnet.alpha_to_tao( Balance.from_rao(emission_on_subnet) @@ -2135,9 +2135,7 @@ async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str title = "Subnet Identity" if not await subtensor.subnet_exists(netuid): - print_error( - f"Subnet {netuid} does not exist." - ) + print_error(f"Subnet {netuid} does not exist.") raise typer.Exit() with console.status( diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8530f3f94..78106ae31 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -18,6 +18,8 @@ normalize_hyperparameters, unlock_key, blocks_to_duration, + float_to_u64, + float_to_u16, ) if TYPE_CHECKING: @@ -70,12 +72,76 @@ def allowed_value( return True, value +def search_metadata( + param_name: str, value: Union[str, bool, float, list[float]], netuid: int, metadata +) -> tuple[bool, Optional[dict]]: + """ + Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used + as call parameters for setting this hyperparameter. + + Args: + param_name: the name of the hyperparameter + value: the value to set the hyperparameter + netuid: the specified netuid + metadata: the subtensor.substrate.metadata + + Returns: + (success, dict of call params) + + """ + + def string_to_bool(val) -> bool: + try: + return {"true": True, "1": True, "0": False, "false": False}[val.lower()] + except KeyError: + return ValueError + + def type_converter_with_retry(type_, val, arg_name): + try: + if val is None: + val = input( + f"Enter a value for field '{arg_name}' with type '{arg_type_output[type_]}': " + ) + return arg_types[type_](val) + except ValueError: + return type_converter_with_retry(type_, None, arg_name) + + arg_types = {"bool": string_to_bool, "u16": float_to_u16, "u64": float_to_u64} + arg_type_output = {"bool": "bool", "u16": "float", "u64": "float"} + + call_crafter = {"netuid": netuid} + + for pallet in metadata.pallets: + if pallet.name == "AdminUtils": + for call in pallet.calls: + if call.name == param_name: + if "netuid" not in [x.name for x in call.args]: + return False, None + call_args = [ + arg for arg in call.args if arg.value["name"] != "netuid" + ] + if len(call_args) == 1: + arg = call_args[0].value + call_crafter[arg["name"]] = type_converter_with_retry( + arg["typeName"], value, arg["name"] + ) + else: + for arg_ in call_args: + arg = arg_.value + call_crafter[arg["name"]] = type_converter_with_retry( + arg["typeName"], None, arg["name"] + ) + return True, call_crafter + else: + return False, None + + async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", netuid: int, parameter: str, - value: Union[str, bool, float, list[float]], + value: Optional[Union[str, bool, float, list[float]]], wait_for_inclusion: bool = False, wait_for_finalization: bool = True, ) -> bool: @@ -110,44 +176,58 @@ async def set_hyperparameter_extrinsic( if not unlock_key(wallet).success: return False + arbitrary_extrinsic = False + extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", False)) - if extrinsic is None: - err_console.print(":cross_mark: [red]Invalid hyperparameter specified.[/red]") - return False + if not extrinsic: + arbitrary_extrinsic, call_params = search_metadata( + parameter, value, netuid, subtensor.substrate.metadata + ) + extrinsic = parameter + if not arbitrary_extrinsic: + err_console.print( + ":cross_mark: [red]Invalid hyperparameter specified.[/red]" + ) + return False + substrate = subtensor.substrate + msg_value = value if not arbitrary_extrinsic else call_params with console.status( - f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{value}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...", + f":satellite: Setting hyperparameter [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{parameter}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{msg_value}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] on subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] ...", spinner="earth", ): - substrate = subtensor.substrate - extrinsic_params = await substrate.get_metadata_call_function( - "AdminUtils", extrinsic - ) - call_params: dict[str, Union[str, bool, float]] = {"netuid": netuid} - - # if input value is a list, iterate through the list and assign values - if isinstance(value, list): - # Ensure that there are enough values for all non-netuid parameters - non_netuid_fields = [ - param["name"] - for param in extrinsic_params["fields"] - if "netuid" not in param["name"] - ] - - if len(value) < len(non_netuid_fields): - raise ValueError( - "Not enough values provided in the list for all parameters" - ) - - call_params.update( - {str(name): val for name, val in zip(non_netuid_fields, value)} + if not arbitrary_extrinsic: + extrinsic_params = await substrate.get_metadata_call_function( + "AdminUtils", extrinsic ) + call_params = {"netuid": netuid} + + # if input value is a list, iterate through the list and assign values + if isinstance(value, list): + # Ensure that there are enough values for all non-netuid parameters + non_netuid_fields = [ + param["name"] + for param in extrinsic_params["fields"] + if "netuid" not in param["name"] + ] + + if len(value) < len(non_netuid_fields): + raise ValueError( + "Not enough values provided in the list for all parameters" + ) - else: - value_argument = extrinsic_params["fields"][ - len(extrinsic_params["fields"]) - 1 - ] - call_params[str(value_argument["name"])] = value + call_params.update( + {str(name): val for name, val in zip(non_netuid_fields, value)} + ) + + else: + value_argument = extrinsic_params["fields"][ + len(extrinsic_params["fields"]) - 1 + ] + call_params[str(value_argument["name"])] = value # create extrinsic call call_ = await substrate.compose_call( @@ -167,11 +247,16 @@ async def set_hyperparameter_extrinsic( if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - + elif arbitrary_extrinsic: + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]" + ) # Successful registration, final check for membership else: console.print( - f":white_heavy_check_mark: [dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" ) return True @@ -470,7 +555,7 @@ async def sudo_set_hyperparameter( subtensor: "SubtensorInterface", netuid: int, param_name: str, - param_value: str, + param_value: Optional[str], ): """Set subnet hyperparameters.""" @@ -481,7 +566,12 @@ async def sudo_set_hyperparameter( "commit_reveal_weights_enabled", "liquid_alpha_enabled", ]: - normalized_value = param_value.lower() in ["true", "True", "1"] + normalized_value = param_value.lower() in ["true", "1"] + elif param_value in ("True", "False"): + normalized_value = { + "True": True, + "False": False, + }[param_value] else: normalized_value = param_value @@ -492,7 +582,6 @@ async def sudo_set_hyperparameter( f"Value is {normalized_value} but must be {value}" ) return - success = await set_hyperparameter_extrinsic( subtensor, wallet, netuid, param_name, value ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index e3a3e8d64..237f401f1 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -284,11 +284,7 @@ async def wallet_balance( """Retrieves the current balance of the specified wallet""" if ss58_addresses: coldkeys = ss58_addresses - identities = await subtensor.query_all_identities() - wallet_names = [ - f"{identities.get(coldkey, {'name': f'Provided address {i}'})['name']}" - for i, coldkey in enumerate(coldkeys) - ] + wallet_names = [f"Provided Address {i + 1}" for i in range(len(ss58_addresses))] elif not all_balances: if not wallet.coldkeypub_file.exists_on_device(): @@ -307,19 +303,29 @@ async def wallet_balance( wallet_names = [wallet.name] block_hash = await subtensor.substrate.get_chain_head() - free_balances = await subtensor.get_balances(*coldkeys, block_hash=block_hash) + free_balances, staked_balances = await asyncio.gather( + subtensor.get_balances(*coldkeys, block_hash=block_hash), + subtensor.get_total_stake_for_coldkey(*coldkeys, block_hash=block_hash), + ) total_free_balance = sum(free_balances.values()) + total_staked_balance = sum(stake[0] for stake in staked_balances.values()) + total_staked_with_slippage = sum(stake[1] for stake in staked_balances.values()) balances = { - name: (coldkey, free_balances[coldkey]) + name: ( + coldkey, + free_balances[coldkey], + staked_balances[coldkey][0], + staked_balances[coldkey][1], + ) for (name, coldkey) in zip(wallet_names, coldkeys) } table = Table( Column( "[white]Wallet Name", - style="bold bright_cyan", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], no_wrap=True, ), Column( @@ -333,7 +339,31 @@ async def wallet_balance( style=COLOR_PALETTE["GENERAL"]["BALANCE"], no_wrap=True, ), - title=f"\n [{COLOR_PALETTE['GENERAL']['HEADER']}]Wallet Coldkey Balance\nNetwork: {subtensor.network}", + Column( + "[white]Staked Value", + justify="right", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + no_wrap=True, + ), + Column( + "[white]Staked (w/slippage)", + justify="right", + style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], + no_wrap=True, + ), + Column( + "[white]Total Balance", + justify="right", + style=COLOR_PALETTE["GENERAL"]["BALANCE"], + no_wrap=True, + ), + Column( + "[white]Total (w/slippage)", + justify="right", + style=COLOR_PALETTE["GENERAL"]["BALANCE"], + no_wrap=True, + ), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Wallet Coldkey Balance[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Network: {subtensor.network}\n", show_footer=True, show_edge=False, border_style="bright_black", @@ -343,17 +373,25 @@ async def wallet_balance( leading=True, ) - for name, (coldkey, free) in balances.items(): + for name, (coldkey, free, staked, staked_slippage) in balances.items(): table.add_row( name, coldkey, str(free), + str(staked), + str(staked_slippage), + str(free + staked), + str(free + staked_slippage), ) table.add_row() table.add_row( "Total Balance", "", str(total_free_balance), + str(total_staked_balance), + str(total_staked_with_slippage), + str(total_free_balance + total_staked_balance), + str(total_free_balance + total_staked_with_slippage), ) console.print(Padding(table, (0, 0, 0, 4))) await subtensor.substrate.close() diff --git a/bittensor_cli/version.py b/bittensor_cli/version.py new file mode 100644 index 000000000..750e8ad1b --- /dev/null +++ b/bittensor_cli/version.py @@ -0,0 +1,18 @@ +import re + +def version_as_int(version): + _core_version = re.match(r"^\d+\.\d+\.\d+", version).group(0) + _version_split = _core_version.split(".") + __version_info__ = tuple(int(part) for part in _version_split) + _version_int_base = 1000 + assert max(__version_info__) < _version_int_base + + __version_as_int__: int = sum( + e * (_version_int_base**i) for i, e in enumerate(reversed(__version_info__)) + ) + assert __version_as_int__ < 2**31 # fits in int32 + __new_signature_version__ = 360 + return __version_as_int__ + +__version__ = "9.0.2" +__version_as_int__ = version_as_int(__version__) diff --git a/requirements.txt b/requirements.txt index 521e94944..7893040e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ wheel async-property==0.2.2 -async-substrate-interface>=1.0.0 +async-substrate-interface>=1.0.3 aiohttp~=3.10.2 backoff~=2.2.1 GitPython>=3.0.0 @@ -16,7 +16,7 @@ rich~=13.7 scalecodec==1.2.11 typer~=0.12 websockets>=14.1 -bittensor-wallet>=3.0.3 +bittensor-wallet>=3.0.4 plotille pywry plotly \ No newline at end of file diff --git a/setup.py b/setup.py index f9c92cda8..45bfc0fa4 100644 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ def read_requirements(path): # loading version from setup.py with codecs.open( - os.path.join(here, "bittensor_cli/cli.py"), encoding="utf-8" + os.path.join(here, "bittensor_cli/version.py"), encoding="utf-8" ) as init_file: version_match = re.search( r"^__version__ = ['\"]([^'\"]*)['\"]", init_file.read(), re.M diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py new file mode 100644 index 000000000..b885d8adf --- /dev/null +++ b/tests/e2e_tests/test_unstaking.py @@ -0,0 +1,281 @@ +import re + +from bittensor_cli.src.bittensor.balances import Balance + + +def test_unstaking(local_chain, wallet_setup): + """ + Test various unstaking scenarios including partial unstake, unstake all alpha, and unstake all. + + Steps: + 1. Create wallets for Alice and Bob + 2. Create 2 subnets with Alice + 3. Register Bob in one subnet + 4. Add stake from Bob to all subnets (except 1) + 5. Remove partial stake from one subnet and verify + 6. Remove all alpha stake and verify + 7. Add stake again to both subnets + 8. Remove all stake and verify + """ + print("Testing unstaking scenarios 🧪") + + # Create wallets for Alice and Bob + wallet_path_alice = "//Alice" + wallet_path_bob = "//Bob" + + # Setup Alice's wallet + keypair_alice, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup( + wallet_path_alice + ) + + # Setup Bob's wallet + keypair_bob, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup( + wallet_path_bob + ) + + # Create first subnet (netuid = 2) + result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--name", + "Test Subnet 2", + "--repo", + "https://github.com/username/repo", + "--contact", + "test@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "test#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Test subnet", + "--no-prompt", + ], + ) + assert "✅ Registered subnetwork with netuid: 2" in result.stdout + + # Create second subnet (netuid = 3) + result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--name", + "Test Subnet 3", + "--repo", + "https://github.com/username/repo", + "--contact", + "test@opentensor.dev", + "--url", + "https://testsubnet.com", + "--discord", + "test#1234", + "--description", + "A test subnet for e2e testing", + "--additional-info", + "Test subnet", + "--no-prompt", + ], + ) + assert "✅ Registered subnetwork with netuid: 3" in result.stdout + + # Register Bob in one subnet + register_result = exec_command_bob( + command="subnets", + sub_command="register", + extra_args=[ + "--netuid", + "2", + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "✅ Registered" in register_result.stdout + + # Add stake to subnets + for netuid in [0, 2, 3]: + stake_result = exec_command_bob( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + netuid, + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--amount", + "700", + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--partial", + "--tolerance", + "0.5", + ], + ) + assert "✅ Finalized" in stake_result.stdout + + stake_list = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + ], + ) + + cleaned_stake = [ + re.sub(r"\s+", " ", line) for line in stake_list.stdout.splitlines() + ] + inital_stake_netuid_2 = cleaned_stake[9].split("│")[3].strip().split()[0] + + # Remove partial stake from netuid 2 + partial_unstake_netuid_2 = exec_command_bob( + command="stake", + sub_command="remove", + extra_args=[ + "--netuid", + "2", + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--amount", + "100", + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--partial", + "--tolerance", + "0.5", + ], + ) + assert "✅ Finalized" in partial_unstake_netuid_2.stdout + + # Verify partial unstake + stake_list = exec_command_bob( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--verbose", + ], + ) + + # Verify stake amounts after partial unstake + cleaned_stake = [ + re.sub(r"\s+", " ", line) for line in stake_list.stdout.splitlines() + ] + stake_after_unstaking_netuid_2 = cleaned_stake[9].split("│")[3].strip().split()[0] + assert Balance.from_tao(float(stake_after_unstaking_netuid_2)) <= Balance.from_tao( + float(inital_stake_netuid_2) + ) + + # Remove all alpha stakes + unstake_alpha = exec_command_bob( + command="stake", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--all-alpha", + "--no-prompt", + ], + ) + assert ( + "✅ Finalized: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout + ) + + # Add stake again to subnets + for netuid in [0, 2, 3]: + stake_result = exec_command_bob( + command="stake", + sub_command="add", + extra_args=[ + "--netuid", + netuid, + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--amount", + "300", + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--partial", + "--tolerance", + "0.5", + ], + ) + assert "✅ Finalized" in stake_result.stdout + + # Remove all stakes + unstake_all = exec_command_bob( + command="stake", + sub_command="remove", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--hotkey", + wallet_bob.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--all", + "--no-prompt", + ], + ) + assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout + print("Passed unstaking tests 🎉")