diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 8fff761ce..c3c1da00c 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.2.0" +__version__ = "8.2.0+rao.1" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b012c989e..1781830fa 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1,8 +1,6 @@ #!/usr/bin/env python3 import asyncio -import binascii import curses -from functools import partial import os.path import re import ssl @@ -24,13 +22,14 @@ WalletOptions as WO, WalletValidationTypes as WV, Constants, + COLOR_PALETTE, ) from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.async_substrate_interface import ( SubstrateRequestException, ) -from bittensor_cli.src.commands import root, subnets, sudo, wallets +from bittensor_cli.src.commands import subnets, sudo, wallets from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.stake import children_hotkeys, stake from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface @@ -43,6 +42,11 @@ print_error, validate_chain_endpoint, retry_prompt, + validate_netuid, + is_rao_network, + get_effective_network, + prompt_for_identity, + validate_uri, ) from typing_extensions import Annotated from textwrap import dedent @@ -57,7 +61,7 @@ class GitError(Exception): pass -__version__ = "8.2.0" +__version__ = "8.2.0+rao.1" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -163,6 +167,17 @@ class Options: None, help="The netuid of the subnet in the root network, (e.g. 1).", prompt=True, + callback=validate_netuid, + ) + netuid_not_req = typer.Option( + None, + help="The netuid of the subnet in the root network, (e.g. 1).", + prompt=False, + ) + all_netuids = typer.Option( + False, + help="Use all netuids", + prompt=False, ) weights = typer.Option( None, @@ -208,6 +223,17 @@ class Options: "--quiet", help="Display only critical information on the console.", ) + live = typer.Option( + False, + "--live", + help="Display live view of the table", + ) + uri = typer.Option( + None, + "--uri", + help="Create wallet from uri (e.g. 'Alice', 'Bob', 'Charlie', 'Dave', 'Eve')", + callback=validate_uri, + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -270,6 +296,31 @@ def verbosity_console_handler(verbosity_level: int = 1) -> None: verbose_console.quiet = False +def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[int]: + """ + Parses options to determine if the user wants to use a specific netuid or all netuids (None) + + Returns: + None if using all netuids, otherwise int for the netuid to use + """ + if netuid is None and all_netuids is True: + return None + elif netuid is None and all_netuids is False: + answer = Prompt.ask( + f"Enter the [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]netuid[/{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}] to use. Leave blank for all netuids", + default=None, + show_default=False, + ) + if answer is None: + return None + if answer.lower() == "all": + return None + else: + return int(answer) + else: + return netuid + + def get_n_words(n_words: Optional[int]) -> int: """ Prompts the user to select the number of words used in the mnemonic if not supplied or not within the @@ -331,6 +382,12 @@ def get_creation_data( json = prompt_answer elif mnemonic: mnemonic = parse_mnemonic(mnemonic) + + if json: + if not os.path.exists(json): + print_error(f"The JSON file '{json}' does not exist.") + raise typer.Exit() + if json and not json_password: json_password = Prompt.ask( "Enter the backup password for JSON file.", password=True @@ -418,7 +475,6 @@ class CLIManager: :var app: the main CLI Typer app :var config_app: the Typer app as it relates to config commands :var wallet_app: the Typer app as it relates to wallet commands - :var root_app: the Typer app as it relates to root 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 subnets_app: the Typer app as it relates to subnets commands @@ -429,7 +485,6 @@ class CLIManager: app: typer.Typer config_app: typer.Typer wallet_app: typer.Typer - root_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer utils_app = typer.Typer(epilog=_epilog) @@ -443,7 +498,9 @@ def __init__(self): "use_cache": True, "metagraph_cols": { "UID": True, - "STAKE": True, + "GLOBAL_STAKE": True, + "LOCAL_STAKE": True, + "STAKE_WEIGHT": True, "RANK": True, "TRUST": True, "CONSENSUS": True, @@ -471,7 +528,6 @@ def __init__(self): ) self.config_app = typer.Typer(epilog=_epilog) self.wallet_app = typer.Typer(epilog=_epilog) - self.root_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) @@ -501,15 +557,6 @@ def __init__(self): self.wallet_app, name="wallets", hidden=True, no_args_is_help=True ) - # root aliases - self.app.add_typer( - self.root_app, - name="root", - short_help="Root commands, alias: `r`", - no_args_is_help=True, - ) - self.app.add_typer(self.root_app, name="r", hidden=True, no_args_is_help=True) - # stake aliases self.app.add_typer( self.stake_app, @@ -558,7 +605,9 @@ def __init__(self): ) # utils app - self.app.add_typer(self.utils_app, name="utils", no_args_is_help=True) + self.app.add_typer( + self.utils_app, name="utils", no_args_is_help=True, hidden=True + ) # config commands self.config_app.command("set")(self.set_config) @@ -598,13 +647,17 @@ def __init__(self): "history", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] )(self.wallet_history) self.wallet_app.command( - "overview", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] + "overview", + rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"], + hidden=True, )(self.wallet_overview) self.wallet_app.command( "transfer", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] )(self.wallet_transfer) self.wallet_app.command( - "inspect", rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"] + "inspect", + rich_help_panel=HELP_PANELS["WALLET"]["INFORMATION"], + hidden=True, )(self.wallet_inspect) self.wallet_app.command( "faucet", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] @@ -619,59 +672,19 @@ def __init__(self): "sign", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"] )(self.wallet_sign) - # root commands - self.root_app.command("list")(self.root_list) - self.root_app.command( - "set-weights", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_set_weights) - self.root_app.command( - "get-weights", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_get_weights) - self.root_app.command( - "boost", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_boost) - self.root_app.command( - "slash", rich_help_panel=HELP_PANELS["ROOT"]["WEIGHT_MGMT"] - )(self.root_slash) - self.root_app.command( - "senate", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_senate) - self.root_app.command( - "senate-vote", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_senate_vote) - self.root_app.command("register")(self.root_register) - self.root_app.command( - "proposals", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_proposals) - self.root_app.command( - "set-take", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_set_take) - self.root_app.command( - "delegate-stake", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_delegate_stake) - self.root_app.command( - "undelegate-stake", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_undelegate_stake) - self.root_app.command( - "my-delegates", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_my_delegates) - self.root_app.command( - "list-delegates", rich_help_panel=HELP_PANELS["ROOT"]["DELEGATION"] - )(self.root_list_delegates) - self.root_app.command( - "nominate", rich_help_panel=HELP_PANELS["ROOT"]["GOVERNANCE"] - )(self.root_nominate) - # stake commands - self.stake_app.command( - "show", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] - )(self.stake_show) self.stake_app.command( "add", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_add) self.stake_app.command( "remove", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] )(self.stake_remove) + self.stake_app.command( + "list", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] + )(self.stake_list) + self.stake_app.command( + "move", rich_help_panel=HELP_PANELS["STAKE"]["STAKE_MGMT"] + )(self.stake_move) # stake-children commands children_app = typer.Typer() @@ -697,6 +710,21 @@ def __init__(self): self.sudo_app.command("get", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_get ) + self.sudo_app.command( + "senate", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] + )(self.sudo_senate) + self.sudo_app.command( + "proposals", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] + )(self.sudo_proposals) + self.sudo_app.command( + "senate-vote", rich_help_panel=HELP_PANELS["SUDO"]["GOVERNANCE"] + )(self.sudo_senate_vote) + self.sudo_app.command("set-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])( + self.sudo_set_take + ) + self.sudo_app.command("get-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])( + self.sudo_get_take + ) # subnets commands self.subnets_app.command( @@ -706,8 +734,8 @@ def __init__(self): "list", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_list) self.subnets_app.command( - "lock-cost", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] - )(self.subnets_lock_cost) + "burn-cost", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] + )(self.subnets_burn_cost) self.subnets_app.command( "create", rich_help_panel=HELP_PANELS["SUBNETS"]["CREATION"] )(self.subnets_create) @@ -718,8 +746,11 @@ def __init__(self): "register", rich_help_panel=HELP_PANELS["SUBNETS"]["REGISTER"] )(self.subnets_register) self.subnets_app.command( - "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] + "metagraph", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"], hidden=True )(self.subnets_metagraph) + self.subnets_app.command( + "show", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] + )(self.subnets_show) # weights commands self.weights_app.command( @@ -764,22 +795,15 @@ def __init__(self): hidden=True, )(self.wallet_get_id) - # Root - self.root_app.command("set_weights", hidden=True)(self.root_set_weights) - self.root_app.command("get_weights", hidden=True)(self.root_get_weights) - self.root_app.command("senate_vote", hidden=True)(self.root_senate_vote) - self.root_app.command("set_take", hidden=True)(self.root_set_take) - self.root_app.command("delegate_stake", hidden=True)(self.root_delegate_stake) - self.root_app.command("undelegate_stake", hidden=True)( - self.root_undelegate_stake - ) - self.root_app.command("my_delegates", hidden=True)(self.root_my_delegates) - self.root_app.command("list_delegates", hidden=True)(self.root_list_delegates) - # Subnets - self.subnets_app.command("lock_cost", hidden=True)(self.subnets_lock_cost) + self.subnets_app.command("burn_cost", hidden=True)(self.subnets_burn_cost) self.subnets_app.command("pow_register", hidden=True)(self.subnets_pow_register) + # Sudo + self.sudo_app.command("senate_vote", hidden=True)(self.sudo_senate_vote) + self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take) + self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take) + def initialize_chain( self, network: Optional[list[str]] = None, @@ -810,7 +834,7 @@ def initialize_chain( elif self.config["network"]: self.subtensor = SubtensorInterface(self.config["network"]) console.print( - f"Using the specified network [dark_orange]{self.config['network']}[/dark_orange] from config" + f"Using the specified network [{COLOR_PALETTE['GENERAL']['LINKS']}]{self.config['network']}[/{COLOR_PALETTE['GENERAL']['LINKS']}] from config" ) else: self.subtensor = SubtensorInterface(defaults.subtensor.network) @@ -838,7 +862,6 @@ async def _run(): raise typer.Exit() except SubstrateRequestException as e: err_console.print(str(e)) - asyncio.create_task(cmd).cancel() raise typer.Exit() if sys.version_info < (3, 10): @@ -857,15 +880,33 @@ def main_callback( """ Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be overriden by passing them explicitly in the command line. """ - # create config file if it does not exist - if not os.path.exists(self.config_path): + + # Load or create the config file + if os.path.exists(self.config_path): + with open(self.config_path, "r") as f: + config = safe_load(f) + else: directory_path = Path(self.config_base_path) directory_path.mkdir(exist_ok=True, parents=True) - with open(self.config_path, "w+") as f: - safe_dump(defaults.config.dictionary, f) - # check config - with open(self.config_path, "r") as f: - config = safe_load(f) + config = defaults.config.dictionary.copy() + with open(self.config_path, "w") as f: + safe_dump(config, f) + + # Update missing values + updated = False + for key, value in defaults.config.dictionary.items(): + if key not in config: + config[key] = value + updated = True + elif isinstance(value, dict): + for sub_key, sub_value in value.items(): + if sub_key not in config[key]: + config[key][sub_key] = sub_value + updated = True + if updated: + with open(self.config_path, "w") as f: + safe_dump(config, f) + for k, v in config.items(): if k in self.config.keys(): self.config[k] = v @@ -1150,16 +1191,12 @@ def wallet_ask( if self.config.get("wallet_name"): wallet_name = self.config.get("wallet_name") console.print( - f"Using the wallet name from config:[bold cyan] {wallet_name}" + f"Using the [blue]wallet name[/blue] from config:[bold cyan] {wallet_name}" ) else: - wallet_name = typer.prompt( - typer.style("Enter the wallet name", fg="blue") - + typer.style( - " (Hint: You can set this with `btcli config set --wallet-name`)", - fg="green", - italic=True, - ), + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]" + + f" [{COLOR_PALETTE['GENERAL']['HINT']} italic](Hint: You can set this with `btcli config set --wallet-name`)", default=defaults.wallet.name, ) @@ -1167,16 +1204,12 @@ def wallet_ask( if self.config.get("wallet_hotkey"): wallet_hotkey = self.config.get("wallet_hotkey") console.print( - f"Using the wallet hotkey from config:[bold cyan] {wallet_hotkey}" + f"Using the [blue]wallet hotkey[/blue] from config:[bold cyan] {wallet_hotkey}" ) else: - wallet_hotkey = typer.prompt( - typer.style("Enter the wallet hotkey", fg="blue") - + typer.style( - " (Hint: You can set this with `btcli config set --wallet-hotkey`)", - fg="green", - italic=True, - ), + wallet_hotkey = Prompt.ask( + "Enter the [blue]wallet hotkey[/blue]" + + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-hotkey`)[/dark_sea_green3 italic]", default=defaults.wallet.hotkey, ) if wallet_path: @@ -1186,17 +1219,15 @@ def wallet_ask( elif self.config.get("wallet_path"): wallet_path = self.config.get("wallet_path") console.print( - f"Using the wallet path from config:[bold magenta] {wallet_path}" + f"Using the [blue]wallet path[/blue] from config:[bold magenta] {wallet_path}" ) + else: + wallet_path = defaults.wallet.path if WO.PATH in ask_for and not wallet_path: - wallet_path = typer.prompt( - typer.style("Enter the wallet path", fg="blue") - + typer.style( - " (Hint: You can set this with `btcli config set --wallet-path`)", - fg="green", - italic=True, - ), + wallet_path = Prompt.ask( + "Enter the [blue]wallet path[/blue]" + + " [dark_sea_green3 italic](Hint: You can set this with `btcli config set --wallet-path`)[/dark_sea_green3 italic]", default=defaults.wallet.path, ) # Create the Wallet object @@ -1377,6 +1408,12 @@ def wallet_overview( "Hotkeys names must be a comma-separated list, e.g., `--exclude-hotkeys hk1,hk2`.", ) + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + return self._run_command( wallets.overview( wallet, @@ -1445,6 +1482,13 @@ def wallet_transfer( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) + + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + subtensor = self.initialize_chain(network) return self._run_command( wallets.transfer( @@ -1570,6 +1614,12 @@ def wallet_inspect( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=ask_for, validate=validate ) + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + self.initialize_chain(network) return self._run_command( wallets.inspect( @@ -1711,7 +1761,8 @@ def wallet_regen_coldkey( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", + default=defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -1765,7 +1816,8 @@ def wallet_regen_coldkey_pub( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", + default=defaults.wallet.name, ) wallet = Wallet(wallet_name, wallet_hotkey, wallet_path) @@ -1859,6 +1911,7 @@ def wallet_new_hotkey( is_flag=True, flag_value=True, ), + uri: Optional[str] = Options.uri, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -1880,12 +1933,14 @@ def wallet_new_hotkey( if not wallet_name: wallet_name = Prompt.ask( - "Enter the wallet name", default=defaults.wallet.name + f"Enter the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]wallet name", + default=defaults.wallet.name, ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( - "Enter the name of the new hotkey", default=defaults.wallet.hotkey + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", + default=defaults.wallet.hotkey, ) wallet = self.wallet_ask( @@ -1895,8 +1950,9 @@ def wallet_new_hotkey( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET, ) - n_words = get_n_words(n_words) - return self._run_command(wallets.new_hotkey(wallet, n_words, use_password)) + if not uri: + n_words = get_n_words(n_words) + return self._run_command(wallets.new_hotkey(wallet, n_words, use_password, uri)) def wallet_new_coldkey( self, @@ -1910,6 +1966,7 @@ def wallet_new_coldkey( help="The number of words used in the mnemonic. Options: [12, 15, 18, 21, 24]", ), use_password: Optional[bool] = Options.use_password, + uri: Optional[str] = Options.uri, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -1935,7 +1992,8 @@ def wallet_new_coldkey( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet", default=defaults.wallet.name + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", + default=defaults.wallet.name, ) wallet = self.wallet_ask( @@ -1945,8 +2003,11 @@ def wallet_new_coldkey( ask_for=[WO.NAME, WO.PATH], validate=WV.NONE, ) - n_words = get_n_words(n_words) - return self._run_command(wallets.new_coldkey(wallet, n_words, use_password)) + if not uri: + n_words = get_n_words(n_words) + return self._run_command( + wallets.new_coldkey(wallet, n_words, use_password, uri) + ) def wallet_check_ck_swap( self, @@ -1980,6 +2041,7 @@ def wallet_create_wallet( wallet_hotkey: Optional[str] = Options.wallet_hotkey, n_words: Optional[int] = None, use_password: bool = Options.use_password, + uri: Optional[str] = Options.uri, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2003,12 +2065,13 @@ def wallet_create_wallet( if not wallet_name: wallet_name = Prompt.ask( - "Enter the name of the new wallet (coldkey)", + f"Enter the name of the [{COLOR_PALETTE['GENERAL']['COLDKEY']}]new wallet (coldkey)", default=defaults.wallet.name, ) if not wallet_hotkey: wallet_hotkey = Prompt.ask( - "Enter the the name of the new hotkey", default=defaults.wallet.hotkey + f"Enter the the name of the [{COLOR_PALETTE['GENERAL']['HOTKEY']}]new hotkey", + default=defaults.wallet.hotkey, ) self.verbosity_handler(quiet, verbose) @@ -2019,12 +2082,14 @@ def wallet_create_wallet( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.NONE, ) - n_words = get_n_words(n_words) + if not uri: + n_words = get_n_words(n_words) return self._run_command( wallets.wallet_create( wallet, n_words, use_password, + uri, ) ) @@ -2069,8 +2134,18 @@ def wallet_balance( """ self.verbosity_handler(quiet, verbose) - - if ss58_addresses: + wallet = None + if all_balances: + ask_for = [WO.PATH] + validate = WV.NONE + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=ask_for, + validate=validate, + ) + elif ss58_addresses: valid_ss58s = [ ss58 for ss58 in set(ss58_addresses) if is_valid_ss58_address(ss58) ] @@ -2080,20 +2155,45 @@ def wallet_balance( print_error(f"Incorrect ss58 address: {invalid_ss58}. Skipping.") if valid_ss58s: - wallet = None ss58_addresses = valid_ss58s else: raise typer.Exit() else: - ask_for = [WO.PATH] if all_balances else [WO.NAME, WO.PATH] - validate = WV.NONE if all_balances else WV.WALLET - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=ask_for, - validate=validate, - ) + if wallet_name: + coldkey_or_ss58 = wallet_name + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 addresses[/blue] (comma-separated)", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + + # Split and validate ss58 addresses + coldkey_or_ss58_list = [x.strip() for x in coldkey_or_ss58.split(",")] + if any(is_valid_ss58_address(x) for x in coldkey_or_ss58_list): + valid_ss58s = [ + ss58 for ss58 in coldkey_or_ss58_list if is_valid_ss58_address(ss58) + ] + invalid_ss58s = set(coldkey_or_ss58_list) - set(valid_ss58s) + for invalid_ss58 in invalid_ss58s: + print_error(f"Incorrect ss58 address: {invalid_ss58}. Skipping.") + + if valid_ss58s: + ss58_addresses = valid_ss58s + else: + raise typer.Exit() + else: + wallet_name = ( + coldkey_or_ss58_list[0] if coldkey_or_ss58_list else wallet_name + ) + ask_for = [WO.NAME, WO.PATH] + validate = WV.WALLET + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=ask_for, + validate=validate, + ) subtensor = self.initialize_chain(network) return self._run_command( wallets.wallet_balance(wallet, subtensor, all_balances, ss58_addresses) @@ -2119,12 +2219,16 @@ def wallet_history( [green]$[/green] btcli wallet history """ + # TODO: Fetch effective network and redirect users accordingly - this only works on finney + # no_use_config_str = "Using the network [dark_orange]finney[/dark_orange] and ignoring network/chain configs" - no_use_config_str = "Using the network [dark_orange]finney[/dark_orange] and ignoring network/chain configs" + # if self.config.get("network"): + # if self.config.get("network") != "finney": + # console.print(no_use_config_str) - if self.config.get("network"): - if self.config.get("network") != "finney": - console.print(no_use_config_str) + # For Rao games + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( @@ -2142,69 +2246,37 @@ def wallet_set_id( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, - display_name: str = typer.Option( + name: str = typer.Option( "", - "--display-name", - "--display", + "--name", help="The display name for the identity.", ), - legal_name: str = typer.Option( - "", - "--legal-name", - "--legal", - help="The legal name for the identity.", - ), web_url: str = typer.Option( "", "--web-url", "--web", help="The web URL for the identity.", ), - riot_handle: str = typer.Option( - "", - "--riot-handle", - "--riot", - help="The Riot handle for the identity.", - ), - email: str = typer.Option( - "", - help="The email address for the identity.", - ), - pgp_fingerprint: str = typer.Option( - "", - "--pgp-fingerprint", - "--pgp", - help="The PGP fingerprint for the identity.", - ), image_url: str = typer.Option( "", "--image-url", "--image", help="The image URL for the identity.", ), - info_: str = typer.Option( + discord_handle: str = typer.Option( "", - "--info", - "-i", - help="The info for the identity.", + "--discord", + help="The Discord handle for the identity.", ), - twitter_url: str = typer.Option( + description: str = typer.Option( "", - "-x", - "-𝕏", - "--twitter-url", - "--twitter", - help="The 𝕏 (Twitter) URL for the identity.", - ), - validator_id: Optional[bool] = typer.Option( - None, - "--validator/--not-validator", - help="Are you updating a validator hotkey identity?", + "--description", + help="The description for the identity.", ), - subnet_netuid: Optional[int] = typer.Option( - None, - "--netuid", - help="Netuid if you are updating identity of a subnet owner", + additional_info: str = typer.Option( + "", + "--additional", + help="Additional details for the identity.", ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -2232,94 +2304,64 @@ def wallet_set_id( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.HOTKEY, WO.PATH, WO.NAME], - validate=WV.WALLET_AND_HOTKEY, + ask_for=[WO.NAME], + validate=WV.WALLET, ) - if not any( - [ - display_name, - legal_name, - web_url, - riot_handle, - email, - pgp_fingerprint, - image_url, - info_, - twitter_url, - ] - ): - console.print( - "[yellow]All fields are optional. Press Enter to skip a field.[/yellow]" - ) - text_rejection = partial( - retry_prompt, - rejection=lambda x: sys.getsizeof(x) > 113, - rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.", + current_identity = self._run_command( + wallets.get_id( + self.initialize_chain(network), + wallet.coldkeypub.ss58_address, + "Current on-chain identity", ) + ) - def pgp_check(s: str): - try: - if s.startswith("0x"): - s = s[2:] # Strip '0x' - pgp_fingerprint_encoded = binascii.unhexlify(s.replace(" ", "")) - except Exception: - return True - return True if len(pgp_fingerprint_encoded) != 20 else False - - display_name = display_name or text_rejection("Display name") - legal_name = legal_name or text_rejection("Legal name") - web_url = web_url or text_rejection("Web URL") - riot_handle = riot_handle or text_rejection("Riot handle") - email = email or text_rejection("Email address") - pgp_fingerprint = pgp_fingerprint or retry_prompt( - "PGP fingerprint (Eg: A1B2 C3D4 E5F6 7890 1234 5678 9ABC DEF0 1234 5678)", - lambda s: False if not s else pgp_check(s), - "[red]Error:[/red] PGP Fingerprint must be exactly 20 bytes.", - ) - image_url = image_url or text_rejection("Image URL") - info_ = info_ or text_rejection("Enter info") - twitter_url = twitter_url or text_rejection("𝕏 (Twitter) URL") - - validator_id = validator_id or Confirm.ask( - "Are you updating a [bold blue]validator hotkey[/bold blue] identity or a [bold blue]subnet " - "owner[/bold blue] identity?\n" - "Enter [bold green]Y[/bold green] for [bold]validator hotkey[/bold] or [bold red]N[/bold red] for " - "[bold]subnet owner[/bold]", - show_choices=True, - ) + if prompt: + if not Confirm.ask( + "Cost to register an [blue]Identity[/blue] is [blue]0.1 TAO[/blue]," + " are you sure you wish to continue?" + ): + console.print(":cross_mark: Aborted!") + raise typer.Exit() - if validator_id is False: - subnet_netuid = IntPrompt.ask("Enter the netuid of the subnet you own") + identity = prompt_for_identity( + current_identity, + name, + web_url, + image_url, + discord_handle, + description, + additional_info, + ) return self._run_command( wallets.set_id( wallet, self.initialize_chain(network), - display_name, - legal_name, - web_url, - pgp_fingerprint, - riot_handle, - email, - image_url, - twitter_url, - info_, - validator_id, + identity["name"], + identity["url"], + identity["image"], + identity["discord"], + identity["description"], + identity["additional"], prompt, - subnet_netuid, ) ) def wallet_get_id( self, - target_ss58_address: str = typer.Option( + wallet_name: Optional[str] = Options.wallet_name, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + wallet_path: Optional[str] = Options.wallet_path, + coldkey_ss58=typer.Option( None, + "--ss58", + "--coldkey_ss58", + "--coldkey.ss58_address", + "--coldkey.ss58", "--key", "-k", - "--ss58", - help="The coldkey or hotkey ss58 address to query.", - prompt=True, + help="Coldkey address of the wallet", ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, @@ -2342,13 +2384,28 @@ def wallet_get_id( [bold]Note[/bold]: This command is primarily used for informational purposes and has no side effects on the blockchain network state. """ - if not is_valid_ss58_address(target_ss58_address): - print_error("You have entered an incorrect ss58 address. Please try again") - raise typer.Exit() + wallet = None + if coldkey_ss58: + if not is_valid_ss58_address(coldkey_ss58): + print_error("You entered an invalid ss58 address") + raise typer.Exit() + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if is_valid_ss58_address(coldkey_or_ss58): + coldkey_ss58 = coldkey_or_ss58 + else: + wallet_name = coldkey_or_ss58 if coldkey_or_ss58 else wallet_name + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME] + ) + coldkey_ss58 = wallet.coldkeypub.ss58_address self.verbosity_handler(quiet, verbose) return self._run_command( - wallets.get_id(self.initialize_chain(network), target_ss58_address) + wallets.get_id(self.initialize_chain(network), coldkey_ss58) ) def wallet_sign( @@ -2382,8 +2439,9 @@ def wallet_sign( self.verbosity_handler(quiet, verbose) if use_hotkey is None: use_hotkey = Confirm.ask( - "Would you like to sign the transaction using your [red]hotkey[/red]?" - "\n[Type [red]y[/red] for [red]hotkey[/red] and [blue]n[/blue] for [blue]coldkey[/blue]] (default is [blue]coldkey[/blue])", + f"Would you like to sign the transaction using your [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]?" + f"\n[Type [{COLOR_PALETTE['GENERAL']['HOTKEY']}]y[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] for [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f" and [{COLOR_PALETTE['GENERAL']['COLDKEY']}]n[/{COLOR_PALETTE['GENERAL']['COLDKEY']}] for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]] (default is [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}])", default=False, ) @@ -2394,920 +2452,179 @@ def wallet_sign( wallet_name, wallet_path, wallet_hotkey, ask_for=ask_for, validate=validate ) if not message: - message = typer.prompt("Enter the message to encode and sign") + message = Prompt.ask("Enter the [blue]message[/blue] to encode and sign") return self._run_command(wallets.sign(wallet, message, use_hotkey)) - def root_list( + def stake_list( self, network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + wallet_path: Optional[str] = Options.wallet_path, + coldkey_ss58=typer.Option( + None, + "--ss58", + "--coldkey_ss58", + "--coldkey.ss58_address", + "--coldkey.ss58", + help="Coldkey address of the wallet", + ), + live: bool = Options.live, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + # TODO add: all-wallets, reuse_last, html_output ): - """ - Show the neurons (root network validators) in the root network (netuid = 0). - - USAGE - - The command fetches and lists the neurons (root network validators) in the root network, showing their unique identifiers (UIDs), names, addresses, stakes, and whether they are part of the senate (network governance body). - - This command is useful for understanding the composition and governance structure of the Bittensor network's root network. It provides insights into which neurons hold significant influence and responsibility within the Bittensor network. + """List all stake accounts for wallet.""" + self.verbosity_handler(quiet, verbose) - EXAMPLE + wallet = None + if coldkey_ss58: + if not is_valid_ss58_address(coldkey_ss58): + print_error("You entered an invalid ss58 address") + raise typer.Exit() + else: + if wallet_name: + coldkey_or_ss58 = wallet_name + else: + coldkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet name[/blue] or [blue]coldkey ss58 address[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if is_valid_ss58_address(coldkey_or_ss58): + coldkey_ss58 = coldkey_or_ss58 + else: + wallet_name = coldkey_or_ss58 if coldkey_or_ss58 else wallet_name + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) - [green]$[/green] btcli root list - """ - self.verbosity_handler(quiet, verbose) return self._run_command( - root.root_list(subtensor=self.initialize_chain(network)) + stake.stake_list(wallet, coldkey_ss58, self.initialize_chain(network), live) ) - def root_set_weights( + def stake_add( self, - network: Optional[list[str]] = Options.network, + stake_all: bool = typer.Option( + False, + "--all-tokens", + "--all", + "-a", + help="When set, the command stakes all the available TAO from the coldkey.", + ), + amount: float = typer.Option( + 0.0, "--amount", help="The amount of TAO to stake" + ), + max_stake: float = typer.Option( + 0.0, + "--max-stake", + "-m", + help="Stake is sent to a hotkey only until the hotkey's total stake is less than or equal to this maximum staked TAO. If a hotkey already has stake greater than this amount, then stake is not added to this hotkey.", + ), + include_hotkeys: str = typer.Option( + "", + "--include-hotkeys", + "-in", + "--hotkey-ss58-address", + help="Specifies hotkeys by name or ss58 address to stake to. For example, `-in hk1,hk2`", + ), + exclude_hotkeys: str = typer.Option( + "", + "--exclude-hotkeys", + "-ex", + help="Specifies hotkeys by name or ss58 address to not to stake to (use this option only with `--all-hotkeys`)" + " i.e. `--all-hotkeys -ex hk3,hk4`", + ), + all_hotkeys: bool = typer.Option( + False, + help="When set, this command stakes to all hotkeys associated with the wallet. Do not use if specifying " + "hotkeys in `--include-hotkeys`.", + ), + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, - netuids=typer.Option( - None, - "--netuids", - "--netuid", - "-n", - help="Set the netuid(s) to set weights to. Separate multiple netuids with a comma, for example: `-n 0,1,2`.", - ), - weights: str = Options.weights, + network: Optional[list[str]] = Options.network, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Set the weights for different subnets, by setting them in the root network. + Stake TAO to one or more hotkeys associated with the user's coldkey. - To use this command, you should specify the netuids and corresponding weights you wish to assign. This command is used by validators registered to the root subnet to influence the distribution of subnet rewards and responsibilities. + This command is used by a subnet validator to stake to their own hotkey. Compare this command with "btcli root delegate" that is typically run by a TAO holder to delegate their TAO to a delegate's hotkey. - You must have a comprehensive understanding of the dynamics of the subnets to use this command. It is a powerful tool that directly impacts the subnet's operational mechanics and reward distribution. + This command is used by a subnet validator to allocate stake TAO to their different hotkeys, securing their position and influence on the network. EXAMPLE - With no spaces between the passed values: - - [green]$[/green] btcli root set-weights --netuids 1,2 --weights 0.2,0.3 - - or - - Include double quotes to include spaces between the passed values: - - [green]$[/green] btcli root set-weights --netuids "1, 2" --weights "0.2, 0.3" + [green]$[/green] btcli stake add --amount 100 --wallet-name --wallet-hotkey """ self.verbosity_handler(quiet, verbose) + netuid = get_optional_netuid(netuid, all_netuids) - if netuids: - netuids = parse_to_list( - netuids, - int, - "Netuids must be a comma-separated list of ints, e.g., `--netuid 1,2,3,4`.", - ) - else: - netuids = list_prompt(netuids, int, "Enter netuids (e.g: 1, 4, 6)") - - if weights: - weights = parse_to_list( - weights, - float, - "Weights must be a comma-separated list of floats, e.g., `--weights 0.3,0.4,0.3`.", - ) - else: - weights = list_prompt( - weights, float, "Enter weights (e.g. 0.02, 0.03, 0.01)" + if stake_all and amount: + err_console.print( + "Cannot specify an amount and 'stake-all'. Choose one or the other." ) + raise typer.Exit() - if len(netuids) != len(weights): - raise typer.BadParameter( - "The number of netuids and weights must be the same." - ) + if stake_all and not amount: + if not Confirm.ask("Stake all the available TAO tokens?", default=False): + raise typer.Exit() - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.HOTKEY, WO.PATH, WO.NAME], - validate=WV.WALLET_AND_HOTKEY, - ) - self._run_command( - root.set_weights( - wallet, self.initialize_chain(network), netuids, weights, prompt + if all_hotkeys and include_hotkeys: + err_console.print( + "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" + "should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." ) - ) - - def root_get_weights( - self, - network: Optional[list[str]] = Options.network, - limit_min_col: Optional[int] = typer.Option( - None, - "--limit-min-col", - "--min", - help="Limit the left display of the table to this column.", - ), - limit_max_col: Optional[int] = typer.Option( - None, - "--limit-max-col", - "--max", - help="Limit the right display of the table to this column.", - ), - reuse_last: bool = Options.reuse_last, - html_output: bool = Options.html_output, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Shows a table listing the weights assigned to each subnet in the root network. - - This command provides visibility into how network responsibilities and rewards are distributed among various subnets. This information is crucial for understanding the current influence and reward distribution across different subnets. Use this command if you are interested in the governance and operational dynamics of the Bittensor network. - - EXAMPLE + raise typer.Exit() - [green]$[/green] btcli root get_weights - """ - self.verbosity_handler(quiet, verbose) - if (reuse_last or html_output) and self.config.get("use_cache") is False: + if include_hotkeys and exclude_hotkeys: err_console.print( - "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'." - "Change it to 'False' using `btcli config set`." + "You have specified options for both including and excluding hotkeys. Select one or the other." ) raise typer.Exit() - if not reuse_last: - subtensor = self.initialize_chain(network) - else: - subtensor = None - return self._run_command( - root.get_weights( - subtensor, - limit_min_col, - limit_max_col, - reuse_last, - html_output, - not self.config.get("use_cache", True), - ) - ) - - def root_boost( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - netuid: int = Options.netuid, - amount: float = typer.Option( - None, - "--amount", - "--increase", - "-a", - prompt="Enter the boost amount (added to existing weight)", - help="Amount (float) to boost (added to the existing weight), (e.g. 0.01)", - ), - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Increase (boost) the weights for a specific subnet in the root network. Any amount provided will be added to the subnet's existing weight. - - EXAMPLE - - [green]$[/green] btcli root boost --netuid 1 --increase 0.01 - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.set_boost( - wallet, self.initialize_chain(network), netuid, amount, prompt - ) - ) - - def root_slash( - self, - network: Optional[list[str]] = Options.network, - wallet_name: str = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - netuid: int = Options.netuid, - amount: float = typer.Option( - None, - "--amount", - "--decrease", - "-a", - prompt="Enter the slash amount (subtracted from the existing weight)", - help="Amount (float) to slash (subtract from the existing weight), (e.g. 0.01)", - ), - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Decrease (slash) the weights for a specific subnet in the root network. Any amount provided will be subtracted from the subnet's existing weight. - - EXAMPLE - - [green]$[/green] btcli root slash --netuid 1 --decrease 0.01 - - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.set_slash( - wallet, self.initialize_chain(network), netuid, amount, prompt - ) - ) - - def root_senate_vote( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - proposal: str = typer.Option( - None, - "--proposal", - "--proposal-hash", - prompt="Enter the proposal hash", - help="The hash of the proposal to vote on.", - ), - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - vote: bool = typer.Option( - None, - "--vote-aye/--vote-nay", - prompt="Enter y to vote Aye, or enter n to vote Nay", - help="The vote casted on the proposal", - ), - ): - """ - Cast a vote on an active proposal in Bittensor's governance protocol. - - This command is used by Senate members to vote on various proposals that shape the network's future. Use `btcli root proposals` to see the active proposals and their hashes. - - USAGE - - The user must specify the hash of the proposal they want to vote on. The command then allows the Senate member to cast a 'Yes' or 'No' vote, contributing to the decision-making process on the proposal. This command is crucial for Senate members to exercise their voting rights on key proposals. It plays a vital role in the governance and evolution of the Bittensor network. - - EXAMPLE - - [green]$[/green] btcli root senate_vote --proposal - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.senate_vote( - wallet, self.initialize_chain(network), proposal, vote, prompt - ) - ) - - def root_senate( - self, - network: Optional[list[str]] = Options.network, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Shows the Senate members of the Bittensor's governance protocol. - - This command lists the delegates involved in the decision-making process of the Bittensor network, showing their names and wallet addresses. This information is crucial for understanding who holds governance roles within the network. - - EXAMPLE - - [green]$[/green] btcli root senate - """ - self.verbosity_handler(quiet, verbose) - return self._run_command(root.get_senate(self.initialize_chain(network))) - - def root_register( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Register a neuron to the root subnet by recycling some TAO to cover for the registration cost. - - This command adds a new neuron as a validator on the root network. This will allow the neuron owner to set subnet weights. - - # Usage: - - Before registering, the command checks if the specified subnet exists and whether the TAO balance in the user's wallet is sufficient to cover the registration cost. The registration cost is determined by the current recycle amount for the specified subnet. If the balance is insufficient or the subnet does not exist, the command will exit with an appropriate error message. - - # Example usage: - - [green]$[/green] btcli subnets register --netuid 1 - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.register(wallet, self.initialize_chain(network), prompt) - ) - - def root_proposals( - self, - network: Optional[list[str]] = Options.network, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - View active proposals for the senate in the Bittensor's governance protocol. - - This command displays the details of ongoing proposals, including proposal hashes, votes, thresholds, and proposal data. - - EXAMPLE - - [green]$[/green] btcli root proposals - """ - self.verbosity_handler(quiet, verbose) - return self._run_command(root.proposals(self.initialize_chain(network))) - - def root_set_take( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - take: float = typer.Option(None, help="The new take value."), - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Allows users to change their delegate take percentage. - - This command can be used to update the delegate takes individually for every subnet. To run the command, the user must have a configured wallet with both hotkey and coldkey. The command performs the below checks: - - 1. The provided hotkey is already a delegate. - 2. The new take value is within 0-18% range. - - EXAMPLE - - [green]$[/green] btcli root set_take --wallet-name my_wallet --wallet-hotkey my_hotkey - """ - max_value = 0.18 - min_value = 0.00 - self.verbosity_handler(quiet, verbose) - - if not take: - max_value_style = typer.style(f"Max: {max_value}", fg="magenta") - min_value_style = typer.style(f"Min: {min_value}", fg="magenta") - prompt_text = typer.style( - "Enter take value (0.18 for 18%)", fg="bright_cyan", bold=True - ) - take = FloatPrompt.ask(f"{prompt_text} {min_value_style} {max_value_style}") - - if not (min_value <= take <= max_value): - print_error( - f"Take value must be between {min_value} and {max_value}. Provided value: {take}" - ) - raise typer.Exit() - - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - - return self._run_command( - root.set_take(wallet, self.initialize_chain(network), take) - ) - - def root_delegate_stake( - self, - delegate_ss58key: str = typer.Option( - None, - help="The ss58 address of the delegate hotkey to stake TAO to.", - prompt="Enter the hotkey ss58 address you want to delegate TAO to.", - ), - amount: Optional[float] = typer.Option( - None, help="The amount of TAO to stake. Do no specify if using `--all`" - ), - stake_all: Optional[bool] = typer.Option( - False, - "--all", - "-a", - help="If specified, the command stakes all available TAO. Do not specify if using" - " `--amount`", - ), - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Stakes TAO to a specified delegate hotkey. - - This command allocates the user's TAO to the specified hotkey of a delegate, potentially earning staking rewards in return. If the - `--all` flag is used, it delegates the entire TAO balance available in the user's wallet. - - This command should be run by a TAO holder. Compare this command with "btcli stake add" that is typically run by a subnet validator to add stake to their own delegate hotkey. - - EXAMPLE - - [green]$[/green] btcli root delegate-stake --delegate_ss58key --amount - - [green]$[/green] btcli root delegate-stake --delegate_ss58key --all - - [blue bold]Note[/blue bold]: This command modifies the blockchain state and may incur transaction fees. The user should ensure the delegate's address and the amount to be staked are correct before executing the command. - """ - self.verbosity_handler(quiet, verbose) - if amount and stake_all: - err_console.print( - "Both `--amount` and `--all` are specified. Choose one or the other." - ) - if not stake_all and not amount: - while True: - amount = FloatPrompt.ask( - "[blue bold]Amount to stake (TAO τ)[/blue bold]", console=console - ) - confirmation = FloatPrompt.ask( - "[blue bold]Confirm the amount to stake (TAO τ)[/blue bold]", - console=console, - ) - if amount == confirmation: - break - else: - err_console.print( - "[red]The amounts do not match. Please try again.[/red]" - ) - - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) - return self._run_command( - root.delegate_stake( - wallet, - self.initialize_chain(network), - amount, - delegate_ss58key, - prompt, - ) - ) - - def root_undelegate_stake( - self, - delegate_ss58key: str = typer.Option( - None, - help="The ss58 address of the delegate to undelegate from.", - prompt="Enter the hotkey ss58 address you want to undelegate from", - ), - amount: Optional[float] = typer.Option( - None, help="The amount of TAO to unstake. Do no specify if using `--all`" - ), - unstake_all: Optional[bool] = typer.Option( - False, - "--all", - "-a", - help="If specified, the command undelegates all staked TAO from the delegate. Do not specify if using" - " `--amount`", - ), - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Allows users to withdraw their staked TAO from a delegate. - - The user must provide the delegate hotkey's ss58 address and the amount of TAO to undelegate. The function will then send a transaction to the blockchain to process the undelegation. This command can result in a change to the blockchain state and may incur transaction fees. - - EXAMPLE - - [green]$[/green] btcli undelegate --delegate_ss58key --amount - - [green]$[/green] btcli undelegate --delegate_ss58key --all - """ - self.verbosity_handler(quiet, verbose) - if amount and unstake_all: - err_console.print( - "Both `--amount` and `--all` are specified. Choose one or the other." - ) - if not unstake_all and not amount: - while True: - amount = FloatPrompt.ask( - "[blue bold]Amount to unstake (TAO τ)[/blue bold]", console=console - ) - confirmation = FloatPrompt.ask( - "[blue bold]Confirm the amount to unstake (TAO τ)[/blue bold]", - console=console, - ) - if amount == confirmation: - break - else: - err_console.print( - "[red]The amounts do not match. Please try again.[/red]" - ) - - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) - self._run_command( - root.delegate_unstake( - wallet, - self.initialize_chain(network), - amount, - delegate_ss58key, - prompt, - ) - ) - - def root_my_delegates( - self, - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - all_wallets: bool = typer.Option( - False, - "--all-wallets", - "--all", - "-a", - help="If specified, the command aggregates information across all the wallets.", - ), - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Shows a table with the details on the user's delegates. - - The table output includes the following columns: - - - Wallet: The name of the user's wallet (coldkey). - - - OWNER: The name of the delegate who owns the hotkey. - - - SS58: The truncated SS58 address of the delegate's hotkey. - - - Delegation: The amount of TAO staked by the user to the delegate. - - - τ/24h: The earnings from the delegate to the user over the past 24 hours. - - - NOMS: The number of nominators for the delegate. - - - OWNER STAKE(τ): The stake amount owned by the delegate. - - - TOTAL STAKE(τ): The total stake amount held by the delegate. - - - SUBNETS: The list of subnets the delegate is a part of. - - - VPERMIT: Validator permits held by the delegate for various subnets. - - - 24h/kτ: Earnings per 1000 TAO staked over the last 24 hours. - - - Desc: A description of the delegate. - - The command also sums and prints the total amount of TAO delegated across all wallets. - - EXAMPLE - - [green]$[/green] btcli root my-delegates - [green]$[/green] btcli root my-delegates --all - [green]$[/green] btcli root my-delegates --wallet-name my_wallet - - [blue bold]Note[/blue bold]: This command is not intended to be used directly in user code. - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=([WO.NAME, WO.PATH] if not all_wallets else [WO.PATH]), - validate=WV.WALLET if not all_wallets else WV.NONE, - ) - self._run_command( - root.my_delegates(wallet, self.initialize_chain(network), all_wallets) - ) - - def root_list_delegates( - self, - network: Optional[list[str]] = Options.network, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Displays a table of Bittensor network-wide delegates, providing a comprehensive overview of delegate statistics and information. - - This table helps users make informed decisions on which delegates to allocate their TAO stake. - - The table columns include: - - - INDEX: The delegate's index in the sorted list. - - - DELEGATE: The name of the delegate. - - - SS58: The delegate's unique ss58 address (truncated for display). - - - NOMINATORS: The count of nominators backing the delegate. - - - OWN STAKE(τ): The amount of delegate's own stake (not the TAO delegated from any nominators). - - - TOTAL STAKE(τ): The delegate's total stake, i.e., the sum of delegate's own stake and nominators' stakes. - - - CHANGE/(4h): The percentage change in the delegate's stake over the last four hours. - - - SUBNETS: The subnets in which the delegate is registered. - - - VPERMIT: Indicates the subnets in which the delegate has validator permits. - - - NOMINATOR/(24h)/kτ: The earnings per 1000 τ staked by nominators in the last 24 hours. - - - DELEGATE/(24h): The total earnings of the delegate in the last 24 hours. - - DESCRIPTION: A brief description of the delegate's purpose and operations. - - [blue bold]NOTES:[/blue bold] - - - Sorting is done based on the `TOTAL STAKE` column in descending order. - - Changes in stake are shown as: increases in green and decreases in red. - - Entries with no previous data are marked with `NA`. - - Each delegate's name is a hyperlink to more information, if available. - - EXAMPLE - - [green]$[/green] btcli root list_delegates - - [green]$[/green] btcli root list_delegates --subtensor.network finney # can also be `test` or `local` - - [blue bold]NOTE[/blue bold]: This command is intended for use within a - console application. It prints directly to the console and does not return any value. - """ - self.verbosity_handler(quiet, verbose) - - if network: - if "finney" in network: - network = ["wss://archive.chain.opentensor.ai:443"] - elif (conf_net := self.config.get("network")) == "finney": - network = ["wss://archive.chain.opentensor.ai:443"] - elif conf_net: - network = [conf_net] - else: - network = ["wss://archive.chain.opentensor.ai:443"] - - sub = self.initialize_chain(network) - return self._run_command(root.list_delegates(sub)) - - # TODO: Confirm if we need a command for this - currently registering to root auto makes u delegate - def root_nominate( - self, - wallet_name: Optional[str] = Options.wallet_name, - wallet_path: Optional[str] = Options.wallet_path, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Enables a wallet's hotkey to become a delegate. - - This command handles the nomination process, including wallet unlocking and verification of the hotkey's current delegate status. - - The command performs several checks: - - - Verifies that the hotkey is not already a delegate to prevent redundant nominations. - - - Tries to nominate the wallet and reports success or failure. - - Upon success, the wallet's hotkey is registered as a delegate on the network. - - To run the command, the user must have a configured wallet with both hotkey and coldkey. If the wallet is not already nominated, this command will initiate the process. - - EXAMPLE - - [green]$[/green] btcli root nominate - - [green]$[/green] btcli root nominate --wallet-name my_wallet --wallet-hotkey my_hotkey - - [blue bold]Note[/blue bold]: This command prints the output directly to the console. It should not be called programmatically in user code due to its interactive nature and side effects on the network state. - """ - self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - return self._run_command( - root.nominate(wallet, self.initialize_chain(network), prompt) - ) - - def stake_show( - self, - all_wallets: bool = typer.Option( - False, - "--all", - "--all-wallets", - "-a", - help="When set, the command checks all the coldkey wallets of the user instead of just the specified wallet.", - ), - network: Optional[list[str]] = Options.network, - wallet_name: Optional[str] = Options.wallet_name, - wallet_hotkey: Optional[str] = Options.wallet_hotkey, - wallet_path: Optional[str] = Options.wallet_path, - reuse_last: bool = Options.reuse_last, - html_output: bool = Options.html_output, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Lists all the stake accounts associated with a user's wallet. - - This command provides a comprehensive view of the stakes associated with the user's coldkeys. It shows both the user's own hotkeys and also the hotkeys of the delegates to which this user has staked. - - The command lists all the stake accounts for a specified wallet or all wallets in the user's configuration directory. It displays the coldkey, balance, hotkey details (own hotkey and delegate hotkey), stake amount, and the rate of return. - - The command shows a table with the below columns: - - - Coldkey: The coldkey associated with the wallet. - - - Balance: The balance of the coldkey. - - - Hotkey: The names of the coldkey's own hotkeys and the delegate hotkeys to which this coldkey has staked. - - - Stake: The amount of TAO staked to all the hotkeys. - - - Rate: The rate of return on the stake, shown in TAO per day. - - EXAMPLE - - [green]$[/green] btcli stake show --all - """ - self.verbosity_handler(quiet, verbose) - if (reuse_last or html_output) and self.config.get("use_cache") is False: - err_console.print( - "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'. " - "Please change the config to 'False' using `btcli config set`" - ) - raise typer.Exit() - if not reuse_last: - subtensor = self.initialize_chain(network) - else: - subtensor = None - - if all_wallets: - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.PATH], - validate=WV.NONE, - ) - else: - wallet = self.wallet_ask( - wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] - ) - - return self._run_command( - stake.show( - wallet, - subtensor, - all_wallets, - reuse_last, - html_output, - not self.config.get("use_cache", True), - ) - ) - - def stake_add( - self, - stake_all: bool = typer.Option( - False, - "--all-tokens", - "--all", - "-a", - help="When set, the command stakes all the available TAO from the coldkey.", - ), - amount: float = typer.Option( - 0.0, "--amount", help="The amount of TAO to stake" - ), - max_stake: float = typer.Option( - 0.0, - "--max-stake", - "-m", - help="Stake is sent to a hotkey only until the hotkey's total stake is less than or equal to this maximum staked TAO. If a hotkey already has stake greater than this amount, then stake is not added to this hotkey.", - ), - hotkey_ss58_address: str = typer.Option( - "", - help="The ss58 address of the hotkey to stake to.", - ), - include_hotkeys: str = typer.Option( - "", - "--include-hotkeys", - "-in", - help="Specifies hotkeys by name or ss58 address to stake to. For example, `-in hk1,hk2`", - ), - exclude_hotkeys: str = typer.Option( - "", - "--exclude-hotkeys", - "-ex", - help="Specifies hotkeys by name or ss58 address to not to stake to (use this option only with `--all-hotkeys`)" - " i.e. `--all-hotkeys -ex hk3,hk4`", - ), - all_hotkeys: bool = typer.Option( - False, - help="When set, this command stakes to all hotkeys associated with the wallet. Do not use if specifying " - "hotkeys in `--include-hotkeys`.", - ), - wallet_name: str = Options.wallet_name, - wallet_path: str = Options.wallet_path, - wallet_hotkey: str = Options.wallet_hotkey, - network: Optional[list[str]] = Options.network, - prompt: bool = Options.prompt, - quiet: bool = Options.quiet, - verbose: bool = Options.verbose, - ): - """ - Stake TAO to one or more hotkeys associated with the user's coldkey. - - This command is used by a subnet validator to stake to their own hotkey. Compare this command with "btcli root delegate" that is typically run by a TAO holder to delegate their TAO to a delegate's hotkey. - - This command is used by a subnet validator to allocate stake TAO to their different hotkeys, securing their position and influence on the network. - - EXAMPLE - - [green]$[/green] btcli stake add --amount 100 --wallet-name --wallet-hotkey - """ - self.verbosity_handler(quiet, verbose) - - if stake_all and amount: - err_console.print( - "Cannot specify an amount and 'stake-all'. Choose one or the other." - ) - raise typer.Exit() - - if not stake_all and not amount and not max_stake: - amount = FloatPrompt.ask("[blue bold]Amount to stake (TAO τ)[/blue bold]") - - if stake_all and not amount: - if not Confirm.ask("Stake all the available TAO tokens?", default=False): - raise typer.Exit() - - if all_hotkeys and include_hotkeys: - err_console.print( - "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" - "should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." - ) - raise typer.Exit() - - if include_hotkeys and exclude_hotkeys: - err_console.print( - "You have specified options for both including and excluding hotkeys. Select one or the other." - ) - raise typer.Exit() + if not wallet_hotkey and not all_hotkeys and not include_hotkeys: + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if netuid is not None: + hotkey_or_ss58 = Prompt.ask( + "Enter the [blue]wallet hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [dim](or Press Enter to view delegates)[/dim]", + ) + else: + hotkey_or_ss58 = Prompt.ask( + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", + default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, + ) - if ( - not wallet_hotkey - and not all_hotkeys - and not include_hotkeys - and not hotkey_ss58_address - ): - hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to stake to", - ) - if is_valid_ss58_address(hotkey_or_ss58): - hotkey_ss58_address = hotkey_or_ss58 + if hotkey_or_ss58 == "": + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) + selected_hotkey = self._run_command( + subnets.show( + subtensor=self.initialize_chain(network), + netuid=netuid, + max_rows=12, + prompt=False, + delegate_selection=True, + ) + ) + if selected_hotkey is None: + print_error("No delegate selected. Exiting.") + raise typer.Exit() + include_hotkeys = selected_hotkey + elif is_valid_ss58_address(hotkey_or_ss58): wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) + include_hotkeys = hotkey_or_ss58 else: wallet_hotkey = hotkey_or_ss58 wallet = self.wallet_ask( @@ -3317,8 +2634,9 @@ def stake_add( ask_for=[WO.NAME, WO.HOTKEY, WO.PATH], validate=WV.WALLET_AND_HOTKEY, ) + include_hotkeys = wallet.hotkey.ss58_address - elif all_hotkeys or include_hotkeys or exclude_hotkeys or hotkey_ss58_address: + elif all_hotkeys or include_hotkeys or exclude_hotkeys: wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) @@ -3332,33 +2650,66 @@ def stake_add( ) if include_hotkeys: - include_hotkeys = parse_to_list( + included_hotkeys = parse_to_list( include_hotkeys, str, "Hotkeys must be a comma-separated list of ss58s, e.g., `--include-hotkeys 5Grw....,5Grw....`.", is_ss58=True, ) + else: + included_hotkeys = [] if exclude_hotkeys: - exclude_hotkeys = parse_to_list( + excluded_hotkeys = parse_to_list( exclude_hotkeys, str, "Hotkeys must be a comma-separated list of ss58s, e.g., `--exclude-hotkeys 5Grw....,5Grw....`.", is_ss58=True, ) + else: + excluded_hotkeys = [] + + # TODO: Ask amount for each subnet explicitly if more than one + if not stake_all and not amount and not max_stake: + free_balance, staked_balance = self._run_command( + wallets.wallet_balance( + wallet, self.initialize_chain(network), False, None + ) + ) + if free_balance == Balance.from_tao(0): + print_error("You dont have any balance to stake.") + raise typer.Exit() + if netuid is not None: + amount = FloatPrompt.ask( + f"Amount to [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]stake (TAO τ)" + ) + else: + amount = FloatPrompt.ask( + f"Amount to [{COLOR_PALETTE['GENERAL']['SUBHEADING_MAIN']}]stake to each netuid (TAO τ)" + ) + + if amount <= 0: + print_error(f"You entered an incorrect stake amount: {amount}") + raise typer.Exit() + if Balance.from_tao(amount) > free_balance: + print_error( + f"You dont have enough balance to stake. Current free Balance: {free_balance}." + ) + raise typer.Exit() return self._run_command( stake.stake_add( wallet, self.initialize_chain(network), - amount, + netuid, stake_all, + amount, + False, + prompt, max_stake, - include_hotkeys, - exclude_hotkeys, all_hotkeys, - prompt, - hotkey_ss58_address, + included_hotkeys, + excluded_hotkeys, ) ) @@ -3368,11 +2719,19 @@ def stake_remove( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, unstake_all: bool = typer.Option( False, "--unstake-all", "--all", - help="When set, this commmand unstakes all staked TAO from the specified hotkeys.", + help="When set, this command unstakes all staked TAO + Alpha from the all hotkeys.", + ), + unstake_all_alpha: bool = typer.Option( + False, + "--unstake-all-alpha", + "--all-alpha", + help="When set, this command unstakes all staked Alpha from the all hotkeys.", ), amount: float = typer.Option( 0.0, "--amount", "-a", help="The amount of TAO to unstake." @@ -3406,6 +2765,12 @@ def stake_remove( "hotkeys in `--include-hotkeys`.", ), prompt: bool = Options.prompt, + interactive: bool = typer.Option( + False, + "--interactive", + "-i", + help="Enter interactive mode for unstaking.", + ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -3422,30 +2787,41 @@ def stake_remove( """ self.verbosity_handler(quiet, verbose) - if all_hotkeys and include_hotkeys: + if interactive and any( + [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] + ): err_console.print( - "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" - "should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." + "Interactive mode cannot be used with hotkey selection options like --include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." ) raise typer.Exit() - if include_hotkeys and exclude_hotkeys: - err_console.print( - "You have specified both including and excluding hotkeys options. Select one or the other." - ) + if unstake_all and unstake_all_alpha: + err_console.print("Cannot specify both unstake-all and unstake-all-alpha.") raise typer.Exit() - if unstake_all and amount: - err_console.print( - "Cannot specify both a specific amount and 'unstake-all'. Choose one or the other." - ) - raise typer.Exit() + if not interactive and not unstake_all and not unstake_all_alpha: + netuid = get_optional_netuid(netuid, all_netuids) + if all_hotkeys and include_hotkeys: + err_console.print( + "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" + " should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." + ) + raise typer.Exit() - if not unstake_all and not amount and not keep_stake: - amount = FloatPrompt.ask("[blue bold]Amount to unstake (TAO τ)[/blue bold]") + if include_hotkeys and exclude_hotkeys: + err_console.print( + "You have specified both including and excluding hotkeys options. Select one or the other." + ) + raise typer.Exit() + + if unstake_all and amount: + err_console.print( + "Cannot specify both a specific amount and 'unstake-all'. Choose one or the other." + ) + raise typer.Exit() - if unstake_all and not amount: - if not Confirm.ask("Unstake all staked TAO tokens?", default=False): + if amount and amount <= 0: + print_error(f"You entered an incorrect unstake amount: {amount}") raise typer.Exit() if ( @@ -3453,11 +2829,24 @@ def stake_remove( and not hotkey_ss58_address and not all_hotkeys and not include_hotkeys + and not interactive + and not unstake_all + and not unstake_all_alpha ): + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from" + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim](or Press Enter to view existing staked hotkeys)[/dim]", ) - if is_valid_ss58_address(hotkey_or_ss58): + if hotkey_or_ss58 == "": + wallet = self.wallet_ask( + wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] + ) + interactive = True + elif is_valid_ss58_address(hotkey_or_ss58): hotkey_ss58_address = hotkey_or_ss58 wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] @@ -3472,11 +2861,18 @@ def stake_remove( validate=WV.WALLET_AND_HOTKEY, ) - elif all_hotkeys or include_hotkeys or exclude_hotkeys or hotkey_ss58_address: + elif ( + all_hotkeys + or include_hotkeys + or exclude_hotkeys + or hotkey_ss58_address + or interactive + or unstake_all + or unstake_all_alpha + ): wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) - else: wallet = self.wallet_ask( wallet_name, @@ -3487,20 +2883,24 @@ def stake_remove( ) if include_hotkeys: - include_hotkeys = parse_to_list( + included_hotkeys = parse_to_list( include_hotkeys, str, - "Hotkeys must be a comma-separated list of ss58s, e.g., `--include-hotkeys 5Grw....,5Grw....`.", - is_ss58=True, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--include-hotkeys hk1,hk2`.", + is_ss58=False, ) + else: + included_hotkeys = [] if exclude_hotkeys: - exclude_hotkeys = parse_to_list( + excluded_hotkeys = parse_to_list( exclude_hotkeys, str, - "Hotkeys must be a comma-separated list of ss58s, e.g., `--exclude-hotkeys 5Grw....,5Grw....`.", - is_ss58=True, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", + is_ss58=False, ) + else: + excluded_hotkeys = [] return self._run_command( stake.unstake( @@ -3508,12 +2908,75 @@ def stake_remove( self.initialize_chain(network), hotkey_ss58_address, all_hotkeys, - include_hotkeys, - exclude_hotkeys, + included_hotkeys, + excluded_hotkeys, amount, keep_stake, unstake_all, prompt, + interactive, + netuid=netuid, + unstake_all_alpha=unstake_all_alpha, + ) + ) + + def stake_move( + self, + network=Options.network, + wallet_name=Options.wallet_name, + wallet_path=Options.wallet_path, + wallet_hotkey=Options.wallet_hotkey, + origin_netuid: int = typer.Option(help="Origin netuid", prompt=True), + destination_netuid: int = typer.Option(help="Destination netuid", prompt=True), + destination_hotkey: Optional[str] = typer.Option( + None, help="Destination hotkey", prompt=False + ), + amount: float = typer.Option( + None, + "--amount", + help="The amount of TAO to stake", + prompt=False, + ), + stake_all: bool = typer.Option( + False, "--stake-all", "--all", help="Stake all", prompt=False + ), + prompt: bool = Options.prompt, + ): + """ + Move Staked TAO to a hotkey from one subnet to another. + + THe move commands converts the origin subnet's dTao to Tao, and then converts Tao to destination subnet's dTao. + + EXAMPLE + + [green]$[/green] btcli stake move + """ + # TODO: Improve logic of moving stake (dest hotkey) + ask_for = ( + [WO.NAME, WO.PATH] if destination_hotkey else [WO.NAME, WO.HOTKEY, WO.PATH] + ) + validate = WV.WALLET if destination_hotkey else WV.WALLET_AND_HOTKEY + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=ask_for, + validate=validate, + ) + if not destination_hotkey: + destination_hotkey = wallet.hotkey.ss58_address + + return self._run_command( + stake.move_stake( + subtensor=self.initialize_chain(network), + wallet=wallet, + origin_netuid=origin_netuid, + destination_netuid=destination_netuid, + destination_hotkey=destination_hotkey, + amount=amount, + stake_all=stake_all, + prompt=prompt, ) ) @@ -3584,18 +3047,8 @@ def stake_set_children( wallet_hotkey: str = Options.wallet_hotkey, wallet_path: str = Options.wallet_path, network: Optional[list[str]] = Options.network, - netuid: Optional[int] = typer.Option( - None, - help="The netuid of the subnet, (e.g. 4)", - prompt=False, - ), - all_netuids: bool = typer.Option( - False, - "--all-netuids", - "--all", - "--allnetuids", - help="When this flag is used it sets child hotkeys on all subnets.", - ), + netuid: Optional[int] = Options.netuid_not_req, + all_netuids: bool = Options.all_netuids, proportions: list[float] = typer.Option( [], "--proportions", @@ -3621,15 +3074,8 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 -p 0.3 -p 0.7 """ self.verbosity_handler(quiet, verbose) - if all_netuids and netuid: - err_console.print("Specify either a netuid or `--all`, not both.") - raise typer.Exit() - if all_netuids: - netuid = None - elif not netuid: - netuid = IntPrompt.ask( - "Enter a netuid (leave blank for all)", default=None, show_default=True - ) + netuid = get_optional_netuid(netuid, all_netuids) + children = list_prompt( children, str, @@ -3854,7 +3300,7 @@ def sudo_set( if not param_value: param_value = Prompt.ask( - f"Enter the new value for [dark_orange]{param_name}[/dark_orange] in the VALUE column format" + f"Enter the new value for [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{param_name}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] in the VALUE column format" ) wallet = self.wallet_ask( @@ -3891,13 +3337,182 @@ def sudo_get( sudo.get_hyperparameters(self.initialize_chain(network), netuid) ) + def sudo_senate( + self, + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Shows the Senate members of the Bittensor's governance protocol. + + This command lists the delegates involved in the decision-making process of the Bittensor network, showing their names and wallet addresses. This information is crucial for understanding who holds governance roles within the network. + + EXAMPLE + [green]$[/green] btcli sudo senate + """ + self.verbosity_handler(quiet, verbose) + return self._run_command(sudo.get_senate(self.initialize_chain(network))) + + def sudo_proposals( + self, + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + View active proposals for the senate in the Bittensor's governance protocol. + + This command displays the details of ongoing proposals, including proposal hashes, votes, thresholds, and proposal data. + + EXAMPLE + [green]$[/green] btcli sudo proposals + """ + self.verbosity_handler(quiet, verbose) + return self._run_command(sudo.proposals(self.initialize_chain(network))) + + def sudo_senate_vote( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + proposal: str = typer.Option( + None, + "--proposal", + "--proposal-hash", + prompt="Enter the proposal hash", + help="The hash of the proposal to vote on.", + ), + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + vote: bool = typer.Option( + None, + "--vote-aye/--vote-nay", + prompt="Enter y to vote Aye, or enter n to vote Nay", + help="The vote casted on the proposal", + ), + ): + """ + Cast a vote on an active proposal in Bittensor's governance protocol. + + This command is used by Senate members to vote on various proposals that shape the network's future. Use `btcli sudo proposals` to see the active proposals and their hashes. + + USAGE + The user must specify the hash of the proposal they want to vote on. The command then allows the Senate member to cast a 'Yes' or 'No' vote, contributing to the decision-making process on the proposal. This command is crucial for Senate members to exercise their voting rights on key proposals. It plays a vital role in the governance and evolution of the Bittensor network. + + EXAMPLE + [green]$[/green] btcli sudo senate_vote --proposal + """ + self.verbosity_handler(quiet, verbose) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + return self._run_command( + sudo.senate_vote( + wallet, self.initialize_chain(network), proposal, vote, prompt + ) + ) + + def sudo_set_take( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + take: float = typer.Option(None, help="The new take value."), + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Allows users to change their delegate take percentage. + + This command can be used to update the delegate takes. To run the command, the user must have a configured wallet with both hotkey and coldkey. + The command makes sure the new take value is within 0-18% range. + + EXAMPLE + [green]$[/green] btcli sudo set-take --wallet-name my_wallet --wallet-hotkey my_hotkey + """ + max_value = 0.18 + min_value = 0.00 + self.verbosity_handler(quiet, verbose) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + + current_take = self._run_command( + sudo.get_current_take(self.initialize_chain(network), wallet) + ) + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + ) + + if not take: + take = FloatPrompt.ask( + f"Enter [blue]take value[/blue] (0.18 for 18%) [blue]Min: {min_value} Max: {max_value}" + ) + if not (min_value <= take <= max_value): + print_error( + f"Take value must be between {min_value} and {max_value}. Provided value: {take}" + ) + raise typer.Exit() + + return self._run_command( + sudo.set_take(wallet, self.initialize_chain(network), take) + ) + + def sudo_get_take( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Allows users to check their delegate take percentage. + + This command can be used to fetch the delegate take of your hotkey. + + EXAMPLE + [green]$[/green] btcli sudo get-take --wallet-name my_wallet --wallet-hotkey my_hotkey + """ + self.verbosity_handler(quiet, verbose) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + + current_take = self._run_command( + sudo.get_current_take(self.initialize_chain(network), wallet) + ) + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%" + ) + def subnets_list( self, network: Optional[list[str]] = Options.network, - reuse_last: bool = Options.reuse_last, - html_output: bool = Options.html_output, + # reuse_last: bool = Options.reuse_last, + # html_output: bool = Options.html_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + live_mode: bool = Options.live, ): """ List all subnets and their detailed information. @@ -3918,42 +3533,69 @@ def subnets_list( [green]$[/green] btcli subnets list """ self.verbosity_handler(quiet, verbose) - if (reuse_last or html_output) and self.config.get("use_cache") is False: - err_console.print( - "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'. " - "Change the config to 'False' using `btcli config set`." - ) - raise typer.Exit() - if reuse_last: - subtensor = None - else: - subtensor = self.initialize_chain(network) + # if (reuse_last or html_output) and self.config.get("use_cache") is False: + # err_console.print( + # "Unable to use `--reuse-last` or `--html` when config 'no-cache' is set to 'True'. " + # "Change the config to 'False' using `btcli config set`." + # ) + # raise typer.Exit() + # if reuse_last: + # subtensor = None + # else: + subtensor = self.initialize_chain(network) return self._run_command( subnets.subnets_list( subtensor, - reuse_last, - html_output, + False, # reuse-last + False, # html-output not self.config.get("use_cache", True), + live_mode, ) ) - def subnets_lock_cost( + def subnets_show( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + prompt: bool = Options.prompt, + ): + """ + Displays detailed information about a subnet including participants and their state. + + EXAMPLE + + [green]$[/green] btcli subnets list + """ + self.verbosity_handler(quiet, verbose) + subtensor = self.initialize_chain(network) + return self._run_command( + subnets.show( + subtensor, + netuid, + verbose=verbose, + prompt=prompt, + ) + ) + + def subnets_burn_cost( self, network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Shows the required amount of TAO to be locked for creating a new subnet, i.e., cost of registering a new subnet. + Shows the required amount of TAO to be recycled for creating a new subnet, i.e., cost of registering a new subnet. The current implementation anneals the cost of creating a subnet over a period of two days. If the displayed cost is unappealing to you, check back in a day or two to see if it has decreased to a more affordable level. EXAMPLE - [green]$[/green] btcli subnets lock_cost + [green]$[/green] btcli subnets burn_cost """ self.verbosity_handler(quiet, verbose) - return self._run_command(subnets.lock_cost(self.initialize_chain(network))) + return self._run_command(subnets.burn_cost(self.initialize_chain(network))) def subnets_create( self, @@ -3977,13 +3619,32 @@ def subnets_create( wallet_name, wallet_path, wallet_hotkey, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, + ask_for=[ + WO.NAME, + ], + validate=WV.WALLET, ) - return self._run_command( + success = self._run_command( subnets.create(wallet, self.initialize_chain(network), prompt) ) + if success and prompt: + set_id = Confirm.ask( + "[dark_sea_green3]Do you want to set/update your identity?", + default=False, + show_default=True, + ) + if set_id: + self.wallet_set_id( + wallet_name=wallet.name, + wallet_hotkey=wallet.hotkey, + wallet_path=wallet.path, + network=network, + prompt=prompt, + quiet=quiet, + verbose=verbose, + ) + def subnets_pow_register( self, wallet_name: Optional[str] = Options.wallet_name, @@ -4184,6 +3845,12 @@ def subnets_metagraph( ) raise typer.Exit() + # For Rao games + effective_network = get_effective_network(self.config, network) + if is_rao_network(effective_network): + print_error("This command is disabled on the 'rao' network.") + raise typer.Exit() + if reuse_last: if netuid is not None: console.print("Cannot specify netuid when using `--reuse-last`") diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index a5a87e16d..1627db0cc 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -4,16 +4,20 @@ class Constants: - networks = ["local", "finney", "test", "archive"] + networks = ["local", "finney", "test", "archive", "rao", "dev"] finney_entrypoint = "wss://entrypoint-finney.opentensor.ai:443" finney_test_entrypoint = "wss://test.finney.opentensor.ai:443" archive_entrypoint = "wss://archive.chain.opentensor.ai:443" - local_entrypoint = "ws://127.0.0.1:9444" + rao_entrypoint = "wss://rao.chain.opentensor.ai:443/" + dev_entrypoint = "wss://dev.chain.opentensor.ai:443 " + local_entrypoint = "ws://127.0.0.1:9944" network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, "archive": archive_entrypoint, "local": local_entrypoint, + "dev": dev_entrypoint, + "rao": rao_entrypoint, } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" @@ -73,7 +77,9 @@ class config: "use_cache": True, "metagraph_cols": { "UID": True, - "STAKE": True, + "GLOBAL_STAKE": True, + "LOCAL_STAKE": True, + "STAKE_WEIGHT": True, "RANK": True, "TRUST": True, "CONSENSUS": True, @@ -91,7 +97,7 @@ class config: } class subtensor: - network = "finney" + network = "rao" chain_endpoint = None _mock = False @@ -206,24 +212,34 @@ class WalletValidationTypes(Enum): "StakeInfoRuntimeApi": { "methods": { "get_stake_info_for_coldkey": { + "params": [{"name": "coldkey_account_vec", "type": "Vec"}], + "type": "Vec", + }, + "get_stake_info_for_coldkeys": { "params": [ - { - "name": "coldkey_account_vec", - "type": "Vec", - }, + {"name": "coldkey_account_vecs", "type": "Vec>"} ], "type": "Vec", }, - "get_stake_info_for_coldkeys": { + "get_subnet_stake_info_for_coldkeys": { "params": [ - { - "name": "coldkey_account_vecs", - "type": "Vec>", - }, + {"name": "coldkey_account_vecs", "type": "Vec>"}, + {"name": "netuid", "type": "u16"}, ], "type": "Vec", }, - }, + "get_subnet_stake_info_for_coldkey": { + "params": [ + {"name": "coldkey_account_vec", "type": "Vec"}, + {"name": "netuid", "type": "u16"}, + ], + "type": "Vec", + }, + "get_total_subnet_stake": { + "params": [{"name": "netuid", "type": "u16"}], + "type": "Vec", + }, + } }, "ValidatorIPRuntimeApi": { "methods": { @@ -262,6 +278,31 @@ class WalletValidationTypes(Enum): "params": [], "type": "Vec", }, + "get_subnet_info_v2": { + "params": [ + { + "name": "netuid", + "type": "u16", + }, + ], + "type": "Vec", + }, + "get_subnets_info_v2": { + "params": [], + "type": "Vec", + }, + "get_all_dynamic_info": { + "params": [], + "type": "Vec", + }, + "get_dynamic_info": { + "params": [{"name": "netuid", "type": "u16"}], + "type": "Vec", + }, + "get_subnet_state": { + "params": [{"name": "netuid", "type": "u16"}], + "type": "Vec", + }, } }, "SubnetRegistrationRuntimeApi": { @@ -301,6 +342,142 @@ class WalletValidationTypes(Enum): }, } +UNITS = [ + "\u03c4", # τ (tau, 0) + "\u03b1", # α (alpha, 1) + "\u03b2", # β (beta, 2) + "\u03b3", # γ (gamma, 3) + "\u03b4", # δ (delta, 4) + "\u03b5", # ε (epsilon, 5) + "\u03b6", # ζ (zeta, 6) + "\u03b7", # η (eta, 7) + "\u03b8", # θ (theta, 8) + "\u03b9", # ι (iota, 9) + "\u03ba", # κ (kappa, 10) + "\u03bb", # λ (lambda, 11) + "\u03bc", # μ (mu, 12) + "\u03bd", # ν (nu, 13) + "\u03be", # ξ (xi, 14) + "\u03bf", # ο (omicron, 15) + "\u03c0", # π (pi, 16) + "\u03c1", # ρ (rho, 17) + "\u03c3", # σ (sigma, 18) + "t", # t (tau, 19) + "\u03c5", # υ (upsilon, 20) + "\u03c6", # φ (phi, 21) + "\u03c7", # χ (chi, 22) + "\u03c8", # ψ (psi, 23) + "\u03c9", # ω (omega, 24) + # Hebrew letters + "\u05d0", # א (aleph, 25) + "\u05d1", # ב (bet, 26) + "\u05d2", # ג (gimel, 27) + "\u05d3", # ד (dalet, 28) + "\u05d4", # ה (he, 29) + "\u05d5", # ו (vav, 30) + "\u05d6", # ז (zayin, 31) + "\u05d7", # ח (het, 32) + "\u05d8", # ט (tet, 33) + "\u05d9", # י (yod, 34) + "\u05da", # ך (final kaf, 35) + "\u05db", # כ (kaf, 36) + "\u05dc", # ל (lamed, 37) + "\u05dd", # ם (final mem, 38) + "\u05de", # מ (mem, 39) + "\u05df", # ן (final nun, 40) + "\u05e0", # נ (nun, 41) + "\u05e1", # ס (samekh, 42) + "\u05e2", # ע (ayin, 43) + "\u05e3", # ף (final pe, 44) + "\u05e4", # פ (pe, 45) + "\u05e5", # ץ (final tsadi, 46) + "\u05e6", # צ (tsadi, 47) + "\u05e7", # ק (qof, 48) + "\u05e8", # ר (resh, 49) + "\u05e9", # ש (shin, 50) + "\u05ea", # ת (tav, 51) + # Georgian Alphabet (Mkhedruli) + "\u10d0", # ა (Ani, 97) + "\u10d1", # ბ (Bani, 98) + "\u10d2", # გ (Gani, 99) + "\u10d3", # დ (Doni, 100) + "\u10d4", # ე (Eni, 101) + "\u10d5", # ვ (Vini, 102) + # Armenian Alphabet + "\u0531", # Ա (Ayp, 103) + "\u0532", # Բ (Ben, 104) + "\u0533", # Գ (Gim, 105) + "\u0534", # Դ (Da, 106) + "\u0535", # Ե (Ech, 107) + "\u0536", # Զ (Za, 108) + # "\u055e", # ՞ (Question mark, 109) + # Runic Alphabet + "\u16a0", # ᚠ (Fehu, wealth, 81) + "\u16a2", # ᚢ (Uruz, strength, 82) + "\u16a6", # ᚦ (Thurisaz, giant, 83) + "\u16a8", # ᚨ (Ansuz, god, 84) + "\u16b1", # ᚱ (Raidho, ride, 85) + "\u16b3", # ᚲ (Kaunan, ulcer, 86) + "\u16c7", # ᛇ (Eihwaz, yew, 87) + "\u16c9", # ᛉ (Algiz, protection, 88) + "\u16d2", # ᛒ (Berkanan, birch, 89) + # Cyrillic Alphabet + "\u0400", # Ѐ (Ie with grave, 110) + "\u0401", # Ё (Io, 111) + "\u0402", # Ђ (Dje, 112) + "\u0403", # Ѓ (Gje, 113) + "\u0404", # Є (Ukrainian Ie, 114) + "\u0405", # Ѕ (Dze, 115) + # Coptic Alphabet + "\u2c80", # Ⲁ (Alfa, 116) + "\u2c81", # ⲁ (Small Alfa, 117) + "\u2c82", # Ⲃ (Vida, 118) + "\u2c83", # ⲃ (Small Vida, 119) + "\u2c84", # Ⲅ (Gamma, 120) + "\u2c85", # ⲅ (Small Gamma, 121) + # Arabic letters + "\u0627", # ا (alef, 52) + "\u0628", # ب (ba, 53) + "\u062a", # ت (ta, 54) + "\u062b", # ث (tha, 55) + "\u062c", # ج (jeem, 56) + "\u062d", # ح (ha, 57) + "\u062e", # خ (kha, 58) + "\u062f", # د (dal, 59) + "\u0630", # ذ (dhal, 60) + "\u0631", # ر (ra, 61) + "\u0632", # ز (zay, 62) + "\u0633", # س (seen, 63) + "\u0634", # ش (sheen, 64) + "\u0635", # ص (sad, 65) + "\u0636", # ض (dad, 66) + "\u0637", # ط (ta, 67) + "\u0638", # ظ (dha, 68) + "\u0639", # ع (ain, 69) + "\u063a", # غ (ghain, 70) + "\u0641", # ف (fa, 71) + "\u0642", # ق (qaf, 72) + "\u0643", # ك (kaf, 73) + "\u0644", # ل (lam, 74) + "\u0645", # م (meem, 75) + "\u0646", # ن (noon, 76) + "\u0647", # ه (ha, 77) + "\u0648", # و (waw, 78) + "\u0649", # ى (alef maksura, 79) + "\u064a", # ي (ya, 80) + # Ogham Alphabet + "\u1680", #   (Space, 90) + "\u1681", # ᚁ (Beith, birch, 91) + "\u1682", # ᚂ (Luis, rowan, 92) + "\u1683", # ᚃ (Fearn, alder, 93) + "\u1684", # ᚄ (Sail, willow, 94) + "\u1685", # ᚅ (Nion, ash, 95) + "\u169b", # ᚛ (Forfeda, 96) + # Tifinagh Alphabet + "\u2d30", # ⴰ (Ya, 127) + "\u2d31", # ⴱ (Yab, 128) +] + NETWORK_EXPLORER_MAP = { "opentensor": { "local": "https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Fentrypoint-finney.opentensor.ai%3A443#/explorer", @@ -363,6 +540,8 @@ class WalletValidationTypes(Enum): }, "SUDO": { "CONFIG": "Subnet Configuration", + "GOVERNANCE": "Governance", + "TAKE": "Delegate take configuration", }, "SUBNETS": { "INFO": "Subnet Information", @@ -371,3 +550,126 @@ class WalletValidationTypes(Enum): }, "WEIGHTS": {"COMMIT_REVEAL": "Commit / Reveal"}, } + +COLOR_PALETTE = { + "GENERAL": { + "HEADER": "#4196D6", # Light Blue + "LINKS": "#8CB9E9", # Sky Blue + "HINT": "#A2E5B8", # Mint Green + "COLDKEY": "#9EF5E4", # Aqua + "HOTKEY": "#ECC39D", # Light Orange/Peach + "SUBHEADING_MAIN": "#7ECFEC", # Light Cyan + "SUBHEADING": "#AFEFFF", # Pale Blue + "SUBHEADING_EXTRA_1": "#96A3C5", # Grayish Blue + "SUBHEADING_EXTRA_2": "#6D7BAF", # Slate Blue + "CONFIRMATION_Y_N_Q": "#EE8DF8", # Light Purple/Pink + "SYMBOL": "#E7CC51", # Gold + "BALANCE": "#4F91C6", # Medium Blue + "COST": "#53B5A0", # Teal + "SUCCESS": "#53B5A0", # Teal + "NETUID": "#CBA880", # Tan + "NETUID_EXTRA": "#DDD5A9", # Light Khaki + "TEMPO": "#67A3A5", # Grayish Teal + }, + "STAKE": { + "STAKE_AMOUNT": "#53B5A0", # Teal + "STAKE_ALPHA": "#53B5A0", # Teal + "STAKE_SWAP": "#67A3A5", # Grayish Teal + "TAO": "#4F91C6", # Medium Blue + "SLIPPAGE_TEXT": "#C25E7C", # Rose + "SLIPPAGE_PERCENT": "#E7B195", # Light Coral + "NOT_REGISTERED": "#EB6A6C", # Salmon Red + "EXTRA_1": "#D781BB", # Pink + }, + "POOLS": { + "TAO": "#4F91C6", # Medium Blue + "ALPHA_IN": "#D09FE9", # Light Purple + "ALPHA_OUT": "#AB7CC8", # Medium Purple + "RATE": "#F8D384", # Light Orange + "TAO_EQUIV": "#8CB9E9", # Sky Blue + "EMISSION": "#F8D384", # Light Orange + "EXTRA_1": "#CAA8FB", # Lavender + "EXTRA_2": "#806DAF", # Dark Purple + }, + "GREY": { + "GREY_100": "#F8F9FA", # Almost White + "GREY_200": "#F1F3F4", # Very Light Grey + "GREY_300": "#DBDDE1", # Light Grey + "GREY_400": "#BDC1C6", # Medium Light Grey + "GREY_500": "#5F6368", # Medium Grey + "GREY_600": "#2E3134", # Medium Dark Grey + "GREY_700": "#282A2D", # Dark Grey + "GREY_800": "#17181B", # Very Dark Grey + "GREY_900": "#0E1013", # Almost Black + "BLACK": "#000000", # Pure Black + }, + "SUDO": { + "HYPERPARAMETER": "#4F91C6", # Medium Blue + "VALUE": "#D09FE9", # Light Purple + "NORMALIZED": "#AB7CC8", # Medium Purple + }, +} + + +SUBNETS = { + 0: "root", + 1: "apex", + 2: "omron", + 3: "templar", + 4: "targon", + 5: "kaito", + 6: "infinite", + 7: "subvortex", + 8: "ptn", + 9: "pretrain", + 10: "sturday", + 11: "dippy", + 12: "horde", + 13: "dataverse", + 14: "palaidn", + 15: "deval", + 16: "bitads", + 17: "3gen", + 18: "cortex", + 19: "inference", + 20: "bitagent", + 21: "any-any", + 22: "meta", + 23: "social", + 24: "omega", + 25: "protein", + 26: "alchemy", + 27: "compute", + 28: "oracle", + 29: "coldint", + 30: "bet", + 31: "naschain", + 32: "itsai", + 33: "ready", + 34: "mind", + 35: "logic", + 36: "automata", + 37: "tuning", + 38: "distributed", + 39: "edge", + 40: "chunk", + 41: "sportsensor", + 42: "masa", + 43: "graphite", + 44: "score", + 45: "gen42", + 46: "neural", + 47: "condense", + 48: "nextplace", + 49: "automl", + 50: "audio", + 51: "celium", + 52: "dojo", + 53: "frontier", + 54: "docs-insight", + 56: "gradients", + 57: "gaia", + 58: "dippy-speech", + 59: "agent-arena", + 61: "red-team", +} diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 0c40830d6..bd6bbb987 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -460,6 +460,9 @@ def __init__(self, chain, runtime_config, metadata, type_registry): self.runtime_config = runtime_config self.metadata = metadata + def __str__(self): + return f"Runtime: {self.chain} | {self.config}" + @property def implements_scaleinfo(self) -> bool: """ @@ -897,9 +900,10 @@ async def init_runtime( async def get_runtime(block_hash, block_id) -> Runtime: # Check if runtime state already set to current block - if (block_hash and block_hash == self.last_block_hash) or ( - block_id and block_id == self.block_id - ): + if ( + (block_hash and block_hash == self.last_block_hash) + or (block_id and block_id == self.block_id) + ) and self.metadata is not None: return Runtime( self.chain, self.runtime_config, @@ -945,9 +949,11 @@ async def get_runtime(block_hash, block_id) -> Runtime: raise SubstrateRequestException( f"No runtime information for block '{block_hash}'" ) - # Check if runtime state already set to current block - if runtime_info.get("specVersion") == self.runtime_version: + if ( + runtime_info.get("specVersion") == self.runtime_version + and self.metadata is not None + ): return Runtime( self.chain, self.runtime_config, @@ -962,16 +968,19 @@ async def get_runtime(block_hash, block_id) -> Runtime: if self.runtime_version in self.__metadata_cache: # Get metadata from cache # self.debug_message('Retrieved metadata for {} from memory'.format(self.runtime_version)) - self.metadata = self.__metadata_cache[self.runtime_version] + metadata = self.metadata = self.__metadata_cache[ + self.runtime_version + ] else: - self.metadata = await self.get_block_metadata( + metadata = self.metadata = await self.get_block_metadata( block_hash=runtime_block_hash, decode=True ) # self.debug_message('Retrieved metadata for {} from Substrate node'.format(self.runtime_version)) # Update metadata cache self.__metadata_cache[self.runtime_version] = self.metadata - + else: + metadata = self.metadata # Update type registry self.reload_type_registry(use_remote_preset=False, auto_discover=True) @@ -1012,7 +1021,10 @@ async def get_runtime(block_hash, block_id) -> Runtime: if block_id and block_hash: raise ValueError("Cannot provide block_hash and block_id at the same time") - if not (runtime := self.runtime_cache.retrieve(block_id, block_hash)): + if ( + not (runtime := self.runtime_cache.retrieve(block_id, block_hash)) + or runtime.metadata is None + ): runtime = await get_runtime(block_hash, block_id) self.runtime_cache.add_item(block_id, block_hash, runtime) return runtime @@ -1123,7 +1135,7 @@ async def create_storage_key( ------- StorageKey """ - await self.init_runtime(block_hash=block_hash) + runtime = await self.init_runtime(block_hash=block_hash) return StorageKey.create_from_storage_function( pallet, @@ -1707,9 +1719,7 @@ async def rpc_request( ) result = await self._make_rpc_request(payloads, runtime=runtime) if "error" in result[payload_id][0]: - raise SubstrateRequestException( - result[payload_id][0]["error"]["message"] - ) + raise SubstrateRequestException(result[payload_id][0]["error"]["message"]) if "result" in result[payload_id][0]: return result[payload_id][0] else: @@ -2274,7 +2284,7 @@ async def get_metadata_constant(self, module_name, constant_name, block_hash=Non MetadataModuleConstants """ - # await self.init_runtime(block_hash=block_hash) + await self.init_runtime(block_hash=block_hash) for module in self.metadata.pallets: if module_name == module.name and module.constants: diff --git a/bittensor_cli/src/bittensor/balances.py b/bittensor_cli/src/bittensor/balances.py index 1e678c9de..4a7955319 100644 --- a/bittensor_cli/src/bittensor/balances.py +++ b/bittensor_cli/src/bittensor/balances.py @@ -18,6 +18,7 @@ # DEALINGS IN THE SOFTWARE. from typing import Union +from bittensor_cli.src import UNITS class Balance: @@ -72,7 +73,10 @@ def __str__(self): """ Returns the Balance object as a string in the format "symbolvalue", where the value is in tao. """ - return f"{self.unit}{float(self.tao):,.9f}" + if self.unit == UNITS[0]: + return f"{self.unit} {float(self.tao):,.4f}" + else: + return f"{float(self.tao):,.4f} {self.unit}\u200e" def __rich__(self): return "[green]{}[/green][green]{}[/green][green].[/green][dim green]{}[/dim green]".format( @@ -278,4 +282,22 @@ def from_rao(amount: int): :return: A Balance object representing the given amount. """ - return Balance(amount) + return Balance(int(amount)) + + @staticmethod + def get_unit(netuid: int): + units = UNITS + base = len(units) + if netuid < base: + return units[netuid] + else: + result = "" + while netuid > 0: + result = units[netuid % base] + result + netuid //= base + return result + + def set_unit(self, netuid: int): + self.unit = Balance.get_unit(netuid) + self.rao_unit = Balance.get_unit(netuid) + return self diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 73f41b1fb..9465edda9 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -1,8 +1,12 @@ from dataclasses import dataclass -from typing import Optional +from enum import Enum +from typing import Optional, Any, Union import bt_decode import netaddr +from scalecodec import ScaleBytes +from scalecodec.base import RuntimeConfiguration +from scalecodec.type_registry import load_type_registry_preset from scalecodec.utils.ss58 import ss58_encode from bittensor_cli.src.bittensor.balances import Balance @@ -10,11 +14,88 @@ from bittensor_cli.src.bittensor.utils import SS58_FORMAT, u16_normalized_float +class ChainDataType(Enum): + NeuronInfo = 1 + SubnetInfoV2 = 2 + DelegateInfo = 3 + NeuronInfoLite = 4 + DelegatedInfo = 5 + StakeInfo = 6 + IPInfo = 7 + SubnetHyperparameters = 8 + SubstakeElements = 9 + DynamicPoolInfoV2 = 10 + DelegateInfoLite = 11 + DynamicInfo = 12 + ScheduledColdkeySwapInfo = 13 + SubnetInfo = 14 + SubnetState = 15 + + +def from_scale_encoding_using_type_string( + input_: Union[list[int], bytes, ScaleBytes], type_string: str +) -> Optional[dict]: + if isinstance(input_, ScaleBytes): + as_scale_bytes = input_ + else: + if isinstance(input_, list) and all([isinstance(i, int) for i in input_]): + vec_u8 = input_ + as_bytes = bytes(vec_u8) + elif isinstance(input_, bytes): + as_bytes = input_ + else: + raise TypeError( + f"input must be a list[int], bytes, or ScaleBytes, not {type(input_)}" + ) + as_scale_bytes = ScaleBytes(as_bytes) + rpc_runtime_config = RuntimeConfiguration() + rpc_runtime_config.update_type_registry(load_type_registry_preset("legacy")) + rpc_runtime_config.update_type_registry(custom_rpc_type_registry) + obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes) + return obj.decode() + + +def from_scale_encoding( + input_: Union[list[int], bytes, ScaleBytes], + type_name: ChainDataType, + is_vec: bool = False, + is_option: bool = False, +) -> Optional[dict]: + type_string = type_name.name + if type_name == ChainDataType.DelegatedInfo: + # DelegatedInfo is a tuple of (DelegateInfo, Compact) + type_string = f"({ChainDataType.DelegateInfo.name}, Compact)" + if is_option: + type_string = f"Option<{type_string}>" + if is_vec: + type_string = f"Vec<{type_string}>" + + return from_scale_encoding_using_type_string(input_, type_string) + + def decode_account_id(account_id_bytes: tuple): # Convert the AccountId bytes to a Base64 string return ss58_encode(bytes(account_id_bytes).hex(), SS58_FORMAT) +def decode_hex_identity(info_dictionary): + decoded_info = {} + for k, v in info_dictionary.items(): + if isinstance(v, dict): + item = next(iter(v.values())) + else: + item = v + + if isinstance(item, tuple): + try: + decoded_info[k] = bytes(item).decode() + except UnicodeDecodeError: + print(f"Could not decode: {k}: {item}") + else: + decoded_info[k] = item + return decoded_info + + def process_stake_data(stake_data): decoded_stake_data = {} for account_id_bytes, stake_ in stake_data: @@ -133,22 +214,68 @@ class StakeInfo: hotkey_ss58: str # Hotkey address coldkey_ss58: str # Coldkey address + netuid: int stake: Balance # Stake for the hotkey-coldkey pair + locked: Balance # Stake which is locked. + emission: Balance # Emission for the hotkey-coldkey pair + drain: int + is_registered: bool @classmethod - def list_from_vec_u8(cls, vec_u8: bytes) -> list["StakeInfo"]: - """ - Returns a list of StakeInfo objects from a `vec_u8`. - """ - decoded = bt_decode.StakeInfo.decode_vec(vec_u8) - results = [] - for d in decoded: - hotkey = decode_account_id(d.hotkey) - coldkey = decode_account_id(d.coldkey) - stake = Balance.from_rao(d.stake) - results.append(StakeInfo(hotkey, coldkey, stake)) + def fix_decoded_values(cls, decoded: Any) -> "StakeInfo": + """Fixes the decoded values.""" + return cls( + hotkey_ss58=ss58_encode(decoded["hotkey"], SS58_FORMAT), + coldkey_ss58=ss58_encode(decoded["coldkey"], SS58_FORMAT), + netuid=int(decoded["netuid"]), + stake=Balance.from_rao(decoded["stake"]).set_unit(decoded["netuid"]), + locked=Balance.from_rao(decoded["locked"]).set_unit(decoded["netuid"]), + emission=Balance.from_rao(decoded["emission"]).set_unit(decoded["netuid"]), + drain=int(decoded["drain"]), + is_registered=bool(decoded["is_registered"]), + ) - return results + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["StakeInfo"]: + """Returns a StakeInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo) + if decoded is None: + return None + + return StakeInfo.fix_decoded_values(decoded) + + @classmethod + def list_of_tuple_from_vec_u8( + cls, vec_u8: list[int] + ) -> dict[str, list["StakeInfo"]]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded: Optional[list[tuple[str, list[object]]]] = ( + from_scale_encoding_using_type_string( + vec_u8, type_string="Vec<(AccountId, Vec)>" + ) + ) + + if decoded is None: + return {} + + return { + ss58_encode(address=account_id, ss58_format=SS58_FORMAT): [ + StakeInfo.fix_decoded_values(d) for d in stake_info + ] + for account_id, stake_info in decoded + } + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["StakeInfo"]: + """Returns a list of StakeInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding(vec_u8, ChainDataType.StakeInfo, is_vec=True) + if decoded is None: + return [] + + return [StakeInfo.fix_decoded_values(d) for d in decoded] @dataclass @@ -516,6 +643,75 @@ def delegated_list_from_vec_u8( return results +@dataclass +class DelegateInfoLite: + """ + Dataclass for light delegate information. + + Args: + hotkey_ss58 (str): Hotkey of the delegate for which the information is being fetched. + owner_ss58 (str): Coldkey of the owner. + total_stake (int): Total stake of the delegate. + owner_stake (int): Own stake of the delegate. + take (float): Take of the delegate as a percentage. None if custom + """ + + hotkey_ss58: str # Hotkey of delegate + owner_ss58: str # Coldkey of owner + take: Optional[float] + total_stake: Balance # Total stake of the delegate + previous_total_stake: Optional[Balance] # Total stake of the delegate + owner_stake: Balance # Own stake of the delegate + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "DelegateInfoLite": + """Fixes the decoded values.""" + decoded_take = decoded["take"] + + if decoded_take == 65535: + fixed_take = None + else: + fixed_take = u16_normalized_float(decoded_take) + + return cls( + hotkey_ss58=ss58_encode(decoded["delegate_ss58"], SS58_FORMAT), + owner_ss58=ss58_encode(decoded["owner_ss58"], SS58_FORMAT), + take=fixed_take, + total_stake=Balance.from_rao(decoded["total_stake"]), + owner_stake=Balance.from_rao(decoded["owner_stake"]), + previous_total_stake=None, + ) + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DelegateInfoLite"]: + """Returns a DelegateInfoLite object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.DelegateInfoLite) + + if decoded is None: + return None + + decoded = DelegateInfoLite.fix_decoded_values(decoded) + + return decoded + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["DelegateInfoLite"]: + """Returns a list of DelegateInfoLite objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.DelegateInfoLite, is_vec=True + ) + + if decoded is None: + return [] + + decoded = [DelegateInfoLite.fix_decoded_values(d) for d in decoded] + + return decoded + + @dataclass class SubnetInfo: """Dataclass for subnet info.""" @@ -572,6 +768,529 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: return result +@dataclass +class SubnetInfoV2: + """Dataclass for subnet info.""" + + netuid: int + owner_ss58: str + max_allowed_validators: int + scaling_law_power: float + subnetwork_n: int + max_n: int + blocks_since_epoch: int + modality: int + emission_value: float + burn: Balance + tao_locked: Balance + hyperparameters: "SubnetHyperparameters" + dynamic_pool: "DynamicPool" + + @classmethod + def from_vec_u8(cls, vec_u8: bytes) -> Optional["SubnetInfoV2"]: + """Returns a SubnetInfoV2 object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + decoded = bt_decode.SubnetInfoV2.decode(vec_u8) # TODO fix values + + if decoded is None: + return None + + return cls.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfoV2"]: + """Returns a list of SubnetInfoV2 objects from a ``vec_u8``.""" + decoded = bt_decode.SubnetInfoV2.decode_vec(vec_u8) # TODO fix values + + if decoded is None: + return [] + + decoded = [cls.fix_decoded_values(d) for d in decoded] + + return decoded + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "SubnetInfoV2": + """Returns a SubnetInfoV2 object from a decoded SubnetInfoV2 dictionary.""" + # init dynamic pool object + pool_info = decoded["dynamic_pool"] + if pool_info: + pool = DynamicPool( + True, + pool_info["netuid"], + pool_info["alpha_issuance"], + pool_info["alpha_outstanding"], + pool_info["alpha_reserve"], + pool_info["tao_reserve"], + pool_info["k"], + ) + else: + pool = DynamicPool(False, decoded["netuid"], 0, 0, 0, 0, 0) + + return SubnetInfoV2( + netuid=decoded["netuid"], + owner_ss58=ss58_encode(decoded["owner"], SS58_FORMAT), + max_allowed_validators=decoded["max_allowed_validators"], + scaling_law_power=decoded["scaling_law_power"], + subnetwork_n=decoded["subnetwork_n"], + max_n=decoded["max_allowed_uids"], + blocks_since_epoch=decoded["blocks_since_last_step"], + modality=decoded["network_modality"], + emission_value=decoded["emission_values"], + burn=Balance.from_rao(decoded["burn"]), + tao_locked=Balance.from_rao(decoded["tao_locked"]), + hyperparameters=decoded["hyperparameters"], + dynamic_pool=pool, + ) + + +@dataclass +class DynamicInfo: + owner: str + netuid: int + tempo: int + last_step: int + blocks_since_last_step: int + emission: Balance + alpha_in: Balance + alpha_out: Balance + tao_in: Balance + total_locked: Balance + owner_locked: Balance + price: Balance + k: float + is_dynamic: bool + symbol: str + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicInfo"]: + if len(vec_u8) == 0: + return None + decoded = from_scale_encoding(vec_u8, ChainDataType.DynamicInfo, is_option=True) + if decoded is None: + return None + return DynamicInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: Union[list[int], bytes]) -> list["DynamicInfo"]: + decoded = from_scale_encoding( + vec_u8, ChainDataType.DynamicInfo, is_vec=True, is_option=True + ) + if decoded is None: + return [] + decoded = [DynamicInfo.fix_decoded_values(d) for d in decoded] + return decoded + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "DynamicInfo": + netuid = int(decoded["netuid"]) + symbol = Balance.get_unit(netuid) + emission = Balance.from_rao(decoded["emission"]).set_unit(0) + alpha_out = Balance.from_rao(decoded["alpha_out"]).set_unit(netuid) + alpha_in = Balance.from_rao(decoded["alpha_in"]).set_unit(netuid) + tao_in = Balance.from_rao(decoded["tao_in"]).set_unit(0) + total_locked = Balance.from_rao(decoded["total_locked"]).set_unit(netuid) + owner_locked = Balance.from_rao(decoded["owner_locked"]).set_unit(netuid) + price = ( + Balance.from_tao(tao_in.tao / alpha_in.tao) + if alpha_in.tao > 0 + else Balance.from_tao(1) + ) + is_dynamic = True if decoded["alpha_in"] > 0 else False + return DynamicInfo( + owner=ss58_encode(decoded["owner"], SS58_FORMAT), + netuid=netuid, + tempo=decoded["tempo"], + last_step=decoded["last_step"], + blocks_since_last_step=decoded["blocks_since_last_step"], + emission=emission, + alpha_out=alpha_out, + alpha_in=alpha_in, + tao_in=tao_in, + total_locked=total_locked, + owner_locked=owner_locked, + price=price, + k=tao_in.rao * alpha_in.rao, + is_dynamic=is_dynamic, + symbol=symbol, + ) + + def tao_to_alpha(self, tao: Balance) -> Balance: + if self.price.tao != 0: + return Balance.from_tao(tao.tao / self.price.tao).set_unit(self.netuid) + else: + return Balance.from_tao(0) + + def alpha_to_tao(self, alpha: Balance) -> Balance: + return Balance.from_tao(alpha.tao * self.price.tao) + + def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: + """ + Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool state. + Args: + tao: Amount of TAO to stake. + Returns: + Tuple of balances where the first part is the amount of Alpha received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_tao_in = self.tao_in + tao + if new_tao_in == 0: + return tao, Balance.from_rao(0) + new_alpha_in = self.k / new_tao_in + + # Amount of alpha given to the staker + alpha_returned = Balance.from_rao( + self.alpha_in.rao - new_alpha_in.rao + ).set_unit(self.netuid) + + # Ideal conversion as if there is no slippage, just price + alpha_ideal = self.tao_to_alpha(tao) + + if alpha_ideal.tao > alpha_returned.tao: + slippage = Balance.from_tao( + alpha_ideal.tao - alpha_returned.tao + ).set_unit(self.netuid) + else: + slippage = Balance.from_tao(0) + else: + alpha_returned = tao.set_unit(self.netuid) + slippage = Balance.from_tao(0) + return alpha_returned, slippage + + def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: + """ + Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state. + Args: + alpha: Amount of Alpha to stake. + Returns: + Tuple of balances where the first part is the amount of TAO received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_alpha_in = self.alpha_in + alpha + new_tao_reserve = self.k / new_alpha_in + # Amount of TAO given to the unstaker + tao_returned = Balance.from_rao(self.tao_in - new_tao_reserve) + + # Ideal conversion as if there is no slippage, just price + tao_ideal = self.alpha_to_tao(alpha) + + if tao_ideal > tao_returned: + slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) + else: + slippage = Balance.from_tao(0) + else: + tao_returned = alpha.set_unit(0) + slippage = Balance.from_tao(0) + return tao_returned, slippage + + +@dataclass +class DynamicPoolInfoV2: + """Dataclass for dynamic pool info.""" + + netuid: int + alpha_issuance: int + alpha_outstanding: int + alpha_reserve: int + tao_reserve: int + k: int + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["DynamicPoolInfoV2"]: + """Returns a DynamicPoolInfoV2 object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + return from_scale_encoding(vec_u8, ChainDataType.DynamicPoolInfoV2) + + +@dataclass +class DynamicPool: + is_dynamic: bool + alpha_issuance: Balance + alpha_outstanding: Balance + alpha_reserve: Balance + tao_reserve: Balance + k: int + price: Balance + netuid: int + + def __init__( + self, + is_dynamic: bool, + netuid: int, + alpha_issuance: Union[int, Balance], + alpha_outstanding: Union[int, Balance], + alpha_reserve: Union[int, Balance], + tao_reserve: Union[int, Balance], + k: int, + ): + self.is_dynamic = is_dynamic + self.netuid = netuid + self.alpha_issuance = ( + alpha_issuance + if isinstance(alpha_issuance, Balance) + else Balance.from_rao(alpha_issuance).set_unit(netuid) + ) + self.alpha_outstanding = ( + alpha_outstanding + if isinstance(alpha_outstanding, Balance) + else Balance.from_rao(alpha_outstanding).set_unit(netuid) + ) + self.alpha_reserve = ( + alpha_reserve + if isinstance(alpha_reserve, Balance) + else Balance.from_rao(alpha_reserve).set_unit(netuid) + ) + self.tao_reserve = ( + tao_reserve + if isinstance(tao_reserve, Balance) + else Balance.from_rao(tao_reserve).set_unit(0) + ) + self.k = k + if is_dynamic: + if self.alpha_reserve.tao > 0: + self.price = Balance.from_tao( + self.tao_reserve.tao / self.alpha_reserve.tao + ) + else: + self.price = Balance.from_tao(0.0) + else: + self.price = Balance.from_tao(1.0) + + def __str__(self) -> str: + return ( + f"DynamicPool( alpha_issuance={self.alpha_issuance}, " + f"alpha_outstanding={self.alpha_outstanding}, " + f"alpha_reserve={self.alpha_reserve}, " + f"tao_reserve={self.tao_reserve}, k={self.k}, price={self.price} )" + ) + + def __repr__(self) -> str: + return self.__str__() + + def tao_to_alpha(self, tao: Balance) -> Balance: + if self.price.tao != 0: + return Balance.from_tao(tao.tao / self.price.tao).set_unit(self.netuid) + else: + return Balance.from_tao(0) + + def alpha_to_tao(self, alpha: Balance) -> Balance: + return Balance.from_tao(alpha.tao * self.price.tao) + + def tao_to_alpha_with_slippage(self, tao: Balance) -> tuple[Balance, Balance]: + """ + Returns an estimate of how much Alpha would a staker receive if they stake their tao + using the current pool state + Args: + tao: Amount of TAO to stake. + Returns: + Tuple of balances where the first part is the amount of Alpha received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_tao_in = self.tao_reserve + tao + if new_tao_in == 0: + return tao, Balance.from_rao(0) + new_alpha_in = self.k / new_tao_in + + # Amount of alpha given to the staker + alpha_returned = Balance.from_rao( + self.alpha_reserve.rao - new_alpha_in.rao + ).set_unit(self.netuid) + + # Ideal conversion as if there is no slippage, just price + alpha_ideal = self.tao_to_alpha(tao) + + if alpha_ideal.tao > alpha_returned.tao: + slippage = Balance.from_tao( + alpha_ideal.tao - alpha_returned.tao + ).set_unit(self.netuid) + else: + slippage = Balance.from_tao(0) + else: + alpha_returned = tao.set_unit(self.netuid) + slippage = Balance.from_tao(0) + return alpha_returned, slippage + + def alpha_to_tao_with_slippage(self, alpha: Balance) -> tuple[Balance, Balance]: + """ + Returns an estimate of how much TAO would a staker receive if they unstake their + alpha using the current pool state + Args: + alpha: Amount of Alpha to stake. + Returns: + Tuple of balances where the first part is the amount of TAO received, and the + second part (slippage) is the difference between the estimated amount and ideal + amount as if there was no slippage + """ + if self.is_dynamic: + new_alpha_in = self.alpha_reserve + alpha + new_tao_reserve = self.k / new_alpha_in + # Amount of TAO given to the unstaker + tao_returned = Balance.from_rao(self.tao_reserve - new_tao_reserve) + + # Ideal conversion as if there is no slippage, just price + tao_ideal = self.alpha_to_tao(alpha) + + if tao_ideal > tao_returned: + slippage = Balance.from_tao(tao_ideal.tao - tao_returned.tao) + else: + slippage = Balance.from_tao(0) + else: + tao_returned = alpha.set_unit(0) + slippage = Balance.from_tao(0) + return tao_returned, slippage + + +@dataclass +class ScheduledColdkeySwapInfo: + """Dataclass for scheduled coldkey swap information.""" + + old_coldkey: str + new_coldkey: str + arbitration_block: int + + @classmethod + def fix_decoded_values(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": + """Fixes the decoded values.""" + return cls( + old_coldkey=ss58_encode(decoded["old_coldkey"], SS58_FORMAT), + new_coldkey=ss58_encode(decoded["new_coldkey"], SS58_FORMAT), + arbitration_block=decoded["arbitration_block"], + ) + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["ScheduledColdkeySwapInfo"]: + """Returns a ScheduledColdkeySwapInfo object from a ``vec_u8``.""" + if len(vec_u8) == 0: + return None + + decoded = from_scale_encoding(vec_u8, ChainDataType.ScheduledColdkeySwapInfo) + if decoded is None: + return None + + return ScheduledColdkeySwapInfo.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["ScheduledColdkeySwapInfo"]: + """Returns a list of ScheduledColdkeySwapInfo objects from a ``vec_u8``.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo, is_vec=True + ) + if decoded is None: + return [] + + return [ScheduledColdkeySwapInfo.fix_decoded_values(d) for d in decoded] + + @classmethod + def decode_account_id_list(cls, vec_u8: list[int]) -> Optional[list[str]]: + """Decodes a list of AccountIds from vec_u8.""" + decoded = from_scale_encoding( + vec_u8, ChainDataType.ScheduledColdkeySwapInfo.AccountId, is_vec=True + ) + if decoded is None: + return None + return [ss58_encode(account_id, SS58_FORMAT) for account_id in decoded] + + +@dataclass +class SubnetState: + netuid: int + hotkeys: list[str] + coldkeys: list[str] + active: list[bool] + validator_permit: list[bool] + pruning_score: list[float] + last_update: list[int] + emission: list[Balance] + dividends: list[float] + incentives: list[float] + consensus: list[float] + trust: list[float] + rank: list[float] + block_at_registration: list[int] + local_stake: list[Balance] + global_stake: list[Balance] + stake_weight: list[float] + emission_history: list[list[int]] + + @classmethod + def from_vec_u8(cls, vec_u8: list[int]) -> Optional["SubnetState"]: + if len(vec_u8) == 0: + return None + decoded = from_scale_encoding(vec_u8, ChainDataType.SubnetState, is_option=True) + if decoded is None: + return None + return SubnetState.fix_decoded_values(decoded) + + @classmethod + def list_from_vec_u8(cls, vec_u8: list[int]) -> list["SubnetState"]: + decoded = from_scale_encoding( + vec_u8, ChainDataType.SubnetState, is_vec=True, is_option=True + ) + if decoded is None: + return [] + decoded = [SubnetState.fix_decoded_values(d) for d in decoded] + return decoded + + @classmethod + def fix_decoded_values(cls, decoded: dict) -> "SubnetState": + netuid = decoded["netuid"] + return SubnetState( + netuid=netuid, + hotkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["hotkeys"]], + coldkeys=[ss58_encode(val, SS58_FORMAT) for val in decoded["coldkeys"]], + active=decoded["active"], + validator_permit=decoded["validator_permit"], + pruning_score=[ + u16_normalized_float(val) for val in decoded["pruning_score"] + ], + last_update=decoded["last_update"], + emission=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["emission"] + ], + dividends=[u16_normalized_float(val) for val in decoded["dividends"]], + incentives=[u16_normalized_float(val) for val in decoded["incentives"]], + consensus=[u16_normalized_float(val) for val in decoded["consensus"]], + trust=[u16_normalized_float(val) for val in decoded["trust"]], + rank=[u16_normalized_float(val) for val in decoded["rank"]], + block_at_registration=decoded["block_at_registration"], + local_stake=[ + Balance.from_rao(val).set_unit(netuid) for val in decoded["local_stake"] + ], + global_stake=[ + Balance.from_rao(val).set_unit(0) for val in decoded["global_stake"] + ], + stake_weight=[u16_normalized_float(val) for val in decoded["stake_weight"]], + emission_history=decoded["emission_history"], + ) + + +class SubstakeElements: + @staticmethod + def decode(result: list[int]) -> list[dict]: + descaled = from_scale_encoding( + input_=result, type_name=ChainDataType.SubstakeElements, is_vec=True + ) + result = [] + for item in descaled: + result.append( + { + "hotkey": ss58_encode(item["hotkey"], SS58_FORMAT), + "coldkey": ss58_encode(item["coldkey"], SS58_FORMAT), + "netuid": item["netuid"], + "stake": Balance.from_rao(item["stake"]), + } + ) + return result + + custom_rpc_type_registry = { "types": { "SubnetInfo": { @@ -597,6 +1316,35 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ["owner", "AccountId"], ], }, + "DynamicPoolInfoV2": { + "type": "struct", + "type_mapping": [ + ["netuid", "u16"], + ["alpha_issuance", "u64"], + ["alpha_outstanding", "u64"], + ["alpha_reserve", "u64"], + ["tao_reserve", "u64"], + ["k", "u128"], + ], + }, + "SubnetInfoV2": { + "type": "struct", + "type_mapping": [ + ["netuid", "u16"], + ["owner", "AccountId"], + ["max_allowed_validators", "u16"], + ["scaling_law_power", "u16"], + ["subnetwork_n", "u16"], + ["max_allowed_uids", "u16"], + ["blocks_since_last_step", "Compact"], + ["network_modality", "u16"], + ["emission_values", "Compact"], + ["burn", "Compact"], + ["tao_locked", "Compact"], + ["hyperparameters", "SubnetHyperparameters"], + ["dynamic_pool", "Option"], + ], + }, "DelegateInfo": { "type": "struct", "type_mapping": [ @@ -610,6 +1358,16 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ["total_daily_return", "Compact"], ], }, + "DelegateInfoLite": { + "type": "struct", + "type_mapping": [ + ["delegate_ss58", "AccountId"], + ["owner_ss58", "AccountId"], + ["take", "u16"], + ["owner_stake", "Compact"], + ["total_stake", "Compact"], + ], + }, "NeuronInfo": { "type": "struct", "type_mapping": [ @@ -688,11 +1446,72 @@ def list_from_vec_u8(cls, vec_u8: bytes) -> list["SubnetInfo"]: ["ip_type_and_protocol", "Compact"], ], }, + "ScheduledColdkeySwapInfo": { + "type": "struct", + "type_mapping": [ + ["old_coldkey", "AccountId"], + ["new_coldkey", "AccountId"], + ["arbitration_block", "Compact"], + ], + }, + "SubnetState": { + "type": "struct", + "type_mapping": [ + ["netuid", "Compact"], + ["hotkeys", "Vec"], + ["coldkeys", "Vec"], + ["active", "Vec"], + ["validator_permit", "Vec"], + ["pruning_score", "Vec>"], + ["last_update", "Vec>"], + ["emission", "Vec>"], + ["dividends", "Vec>"], + ["incentives", "Vec>"], + ["consensus", "Vec>"], + ["trust", "Vec>"], + ["rank", "Vec>"], + ["block_at_registration", "Vec>"], + ["local_stake", "Vec>"], + ["global_stake", "Vec>"], + ["stake_weight", "Vec>"], + ["emission_history", "Vec>>"], + ], + }, "StakeInfo": { "type": "struct", "type_mapping": [ ["hotkey", "AccountId"], ["coldkey", "AccountId"], + ["netuid", "Compact"], + ["stake", "Compact"], + ["locked", "Compact"], + ["emission", "Compact"], + ["drain", "Compact"], + ["is_registered", "bool"], + ], + }, + "DynamicInfo": { + "type": "struct", + "type_mapping": [ + ["owner", "AccountId"], + ["netuid", "Compact"], + ["tempo", "Compact"], + ["last_step", "Compact"], + ["blocks_since_last_step", "Compact"], + ["emission", "Compact"], + ["alpha_in", "Compact"], + ["alpha_out", "Compact"], + ["tao_in", "Compact"], + ["total_locked", "Compact"], + ["owner_locked", "Compact"], + ], + }, + "SubstakeElements": { + "type": "struct", + "type_mapping": [ + ["hotkey", "AccountId"], + ["coldkey", "AccountId"], + ["netuid", "Compact"], ["stake", "Compact"], ], }, diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index af6962106..ae5db605d 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -28,7 +28,9 @@ from rich.status import Status from substrateinterface.exceptions import SubstrateRequestException +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.chain_data import NeuronInfo +from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -493,16 +495,21 @@ async def get_neuron_for_pubkey_and_subnet(): if uid is None: return NeuronInfo.get_null_neuron() - params = [netuid, uid] - json_body = await subtensor.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, + hex_bytes_result = await subtensor.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid], ) - if not (result := json_body.get("result", None)): + if not (result := hex_bytes_result): return NeuronInfo.get_null_neuron() - return NeuronInfo.from_vec_u8(bytes(result)) + if result.startswith("0x"): + bytes_result = bytes.fromhex(result[2:]) + else: + bytes_result = bytes.fromhex(result) + + return NeuronInfo.from_vec_u8(bytes_result) print_verbose("Checking subnet status") if not await subtensor.subnet_exists(netuid): @@ -526,9 +533,9 @@ async def get_neuron_for_pubkey_and_subnet(): if prompt: if not Confirm.ask( f"Continue Registration?\n" - f" hotkey ({wallet.hotkey_str}):\t[bold white]{wallet.hotkey.ss58_address}[/bold white]\n" - f" coldkey ({wallet.name}):\t[bold white]{wallet.coldkeypub.ss58_address}[/bold white]\n" - f" network:\t\t[bold white]{subtensor.network}[/bold white]" + f" hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]({wallet.hotkey_str})[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]:\t[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" + f" coldkey [{COLOR_PALETTE['GENERAL']['COLDKEY']}]({wallet.name})[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]:\t[{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f" network:\t\t[{COLOR_PALETTE['GENERAL']['LINKS']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['LINKS']}]\n" ): return False @@ -581,7 +588,7 @@ async def get_neuron_for_pubkey_and_subnet(): ) if is_registered: err_console.print( - f":white_heavy_check_mark: [green]Already registered on netuid:{netuid}[/green]" + f":white_heavy_check_mark: [dark_sea_green3]Already registered on netuid:{netuid}[/dark_sea_green3]" ) return True @@ -628,8 +635,8 @@ async def get_neuron_for_pubkey_and_subnet(): if "HotKeyAlreadyRegisteredInSubNet" in err_msg: console.print( - f":white_heavy_check_mark: [green]Already Registered on " - f"[bold]subnet:{netuid}[/bold][/green]" + f":white_heavy_check_mark: [dark_sea_green3]Already Registered on " + f"[bold]subnet:{netuid}[/bold][/dark_sea_green3]" ) return True err_console.print( @@ -647,7 +654,7 @@ async def get_neuron_for_pubkey_and_subnet(): ) if is_registered: console.print( - ":white_heavy_check_mark: [green]Registered[/green]" + ":white_heavy_check_mark: [dark_sea_green3]Registered[/dark_sea_green3]" ) return True else: @@ -674,6 +681,117 @@ async def get_neuron_for_pubkey_and_subnet(): return False +async def burned_register_extrinsic( + subtensor: "SubtensorInterface", + wallet: Wallet, + netuid: int, + old_balance: Balance, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, + prompt: bool = False, +) -> bool: + """Registers the wallet to chain by recycling TAO. + + :param subtensor: The SubtensorInterface object to use for the call, initialized + :param wallet: Bittensor wallet object. + :param netuid: The `netuid` of the subnet to register on. + :param old_balance: The wallet balance prior to the registration burn. + :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, + or returns `False` if the extrinsic fails to be finalized within the timeout. + :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + + :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for + finalization/inclusion, the response is `True`. + """ + + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + with console.status( + f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", + spinner="aesthetic", + ) as status: + my_uid = await subtensor.substrate.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + ) + + print_verbose("Checking if already registered", status) + neuron = await subtensor.neuron_for_uid( + uid=my_uid, + netuid=netuid, + block_hash=subtensor.substrate.last_block_hash, + ) + + if not neuron.is_null: + console.print( + ":white_heavy_check_mark: [dark_sea_green3]Already Registered[/dark_sea_green3]:\n" + f"uid: [{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]{neuron.uid}[/{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]\n" + f"netuid: [{COLOR_PALETTE['GENERAL']['NETUID']}]{neuron.netuid}[/{COLOR_PALETTE['GENERAL']['NETUID']}]\n" + f"hotkey: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{neuron.hotkey}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" + f"coldkey: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{neuron.coldkey}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + ) + return True + + with console.status( + ":satellite: Recycling TAO for Registration...", spinner="aesthetic" + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization + ) + + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + await asyncio.sleep(0.5) + return False + # Successful registration, final check for neuron and pubkey + else: + with console.status(":satellite: Checking Balance...", spinner="aesthetic"): + block_hash = await subtensor.substrate.get_chain_head() + new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( + subtensor.get_balance( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, + reuse_block=False, + ), + subtensor.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, block_hash=block_hash + ), + subtensor.substrate.query( + "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] + ), + ) + + console.print( + "Balance:\n" + f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance[wallet.coldkey.ss58_address]}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + ) + + if len(netuids_for_hotkey) > 0: + console.print( + f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" + ) + return True + else: + # neuron not found, try again + err_console.print( + ":cross_mark: [red]Unknown error. Neuron not found.[/red]" + ) + return False + + async def run_faucet_extrinsic( subtensor: "SubtensorInterface", wallet: Wallet, diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 620c20d76..9d0d5a722 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -98,7 +98,7 @@ async def do_transfer() -> tuple[bool, str, str]: block_hash_ = response.block_hash return True, block_hash_, "" else: - return False, "", format_error_message(await response.error_message) + return False, "", format_error_message(await response.error_message, subtensor.substrate) # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): diff --git a/bittensor_cli/src/bittensor/minigraph.py b/bittensor_cli/src/bittensor/minigraph.py index 3f2aac0f2..3d652d6d0 100644 --- a/bittensor_cli/src/bittensor/minigraph.py +++ b/bittensor_cli/src/bittensor/minigraph.py @@ -3,7 +3,7 @@ import numpy as np from numpy.typing import NDArray -from bittensor_cli.src.bittensor.chain_data import NeuronInfo +from bittensor_cli.src.bittensor.chain_data import NeuronInfo, SubnetState from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( convert_root_weight_uids_and_vals_to_tensor, @@ -18,6 +18,7 @@ def __init__( netuid: int, neurons: list[NeuronInfo], subtensor: "SubtensorInterface", + subnet_state: "SubnetState", block: int, ): self.neurons = neurons @@ -62,12 +63,14 @@ def __init__( self.validator_trust = self._create_tensor( [neuron.validator_trust for neuron in self.neurons], dtype=np.float32 ) - self.total_stake = self._create_tensor( - [neuron.total_stake.tao for neuron in self.neurons], dtype=np.float32 - ) - self.stake = self._create_tensor( - [neuron.stake for neuron in self.neurons], dtype=np.float32 + + # Fetch stakes from subnet_state until we get updated data in NeuronInfo + global_stake_list, local_stake_list, stake_weights_list = self._process_stakes( + neurons, subnet_state ) + self.global_stake = self._create_tensor(global_stake_list, dtype=np.float32) + self.local_stake = self._create_tensor(local_stake_list, dtype=np.float32) + self.stake_weights = self._create_tensor(stake_weights_list, dtype=np.float32) async def __aenter__(self): if not self.weights: @@ -120,6 +123,41 @@ async def _set_weights_and_bonds(self): [neuron.bonds for neuron in self.neurons], "bonds" ) + def _process_stakes( + self, + neurons: list[NeuronInfo], + subnet_state: SubnetState, + ) -> tuple[list[float], list[float], list[float]]: + """ + Processes the global_stake, local_stake, and stake_weights based on the neuron's hotkey. + + Args: + neurons (List[NeuronInfo]): List of neurons. + subnet_state (SubnetState): The subnet state containing stake information. + + Returns: + tuple[list[float], list[float], list[float]]: Lists of global_stake, local_stake, and stake_weights. + """ + global_stake_list = [] + local_stake_list = [] + stake_weights_list = [] + hotkey_to_index = { + hotkey: idx for idx, hotkey in enumerate(subnet_state.hotkeys) + } + + for neuron in neurons: + idx = hotkey_to_index.get(neuron.hotkey) + if idx is not None: + global_stake_list.append(subnet_state.global_stake[idx].tao) + local_stake_list.append(subnet_state.local_stake[idx].tao) + stake_weights_list.append(subnet_state.stake_weight[idx]) + else: + global_stake_list.append(0.0) + local_stake_list.append(0.0) + stake_weights_list.append(0.0) + + return global_stake_list, local_stake_list, stake_weights_list + def _process_weights_or_bonds(self, data, attribute: str) -> NDArray: """ Processes the raw weights or bonds data and converts it into a structured tensor format. This method handles diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 62063c206..7bd01d42c 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -24,6 +24,9 @@ NeuronInfo, SubnetHyperparameters, decode_account_id, + decode_hex_identity, + DelegateInfoLite, + DynamicInfo, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance @@ -35,6 +38,8 @@ err_console, decode_hex_identity_dict, validate_chain_endpoint, + u16_normalized_float, + u64_normalized_float, ) @@ -379,46 +384,118 @@ async def get_total_stake_for_coldkey( :return: {address: Balance objects} """ - calls = [ - ( - await self.substrate.create_storage_key( - "SubtensorModule", - "TotalColdkeyStake", - [address], - block_hash=block_hash, - ) - ) - for address in ss58_addresses - ] - batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + sub_stakes = await self.get_stake_info_for_coldkeys( + ss58_addresses, block_hash=block_hash + ) + # Token pricing info + dynamic_info = await self.get_all_subnet_dynamic_info() + results = {} - for item in batch_call: - results.update({item[0].params[0]: Balance.from_rao(item[1] or 0)}) + for ss58, stake_info_list in sub_stakes.items(): + all_staked_tao = 0 + for sub_stake in stake_info_list: + if sub_stake.stake.rao == 0: + continue + netuid = sub_stake.netuid + pool = dynamic_info[netuid] + + alpha_value = Balance.from_rao(int(sub_stake.stake.rao)).set_unit( + netuid + ) + + tao_locked = pool.tao_in + + issuance = pool.alpha_out if pool.is_dynamic else tao_locked + tao_ownership = Balance(0) + + if alpha_value.tao > 0.00009 and issuance.tao != 0: + tao_ownership = Balance.from_tao( + (alpha_value.tao / issuance.tao) * tao_locked.tao + ) + + all_staked_tao += tao_ownership.rao + + results[ss58] = Balance.from_rao(all_staked_tao) return results async def get_total_stake_for_hotkey( self, *ss58_addresses, + netuids: Optional[list[int]] = None, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> dict[str, Balance]: + ) -> dict[str, dict[int, "Balance"]]: """ Returns the total stake held on a hotkey. :param ss58_addresses: The SS58 address(es) of the hotkey(s) + :param netuids: The netuids to retrieve the stake from. If not specified, will use all subnets. :param block_hash: The hash of the block number to retrieve the stake from. :param reuse_block: Whether to reuse the last-used block hash when retrieving info. - :return: {address: Balance objects} + :return: + { + hotkey_ss58_1: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + hotkey_ss58_2: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + ... + } """ - results = await self.substrate.query_multiple( - params=[s for s in ss58_addresses], + netuids = netuids or await self.get_all_subnet_netuids(block_hash=block_hash) + query: dict[tuple[str, int], int] = await self.substrate.query_multiple( + params=[(ss58, netuid) for ss58 in ss58_addresses for netuid in netuids], module="SubtensorModule", - storage_function="TotalHotkeyStake", + storage_function="TotalHotkeyAlpha", block_hash=block_hash, reuse_block_hash=reuse_block, ) - return {k: Balance.from_rao(r or 0) for (k, r) in results.items()} + results: dict[str, dict[int, "Balance"]] = { + hk_ss58: {} for hk_ss58 in ss58_addresses + } + for idx, (_, val) in enumerate(query): + hotkey_ss58 = ss58_addresses[idx // len(netuids)] + netuid = netuids[idx % len(netuids)] + value = (Balance.from_rao(val) if val is not None else Balance(0)).set_unit( + netuid + ) + results[hotkey_ss58][netuid] = value + return results + + async def current_take( + self, + hotkey_ss58: int, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> bool: + """ + Retrieves the delegate 'take' percentage for a neuron identified by its hotkey. The 'take' + represents the percentage of rewards that the delegate claims from its nominators' stakes. + + Args: + hotkey_ss58 (str): The ``SS58`` address of the neuron's hotkey. + block (Optional[int], optional): The blockchain block number for the query. + + Returns: + Optional[float]: The delegate take percentage, None if not available. + + The delegate take is a critical parameter in the network's incentive structure, influencing + the distribution of rewards among neurons and their nominators. + """ + result = await self.substrate.query( + module="SubtensorModule", + storage_function="Delegates", + params=[hotkey_ss58], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + return u16_normalized_float(result) async def get_netuids_for_hotkey( self, @@ -682,16 +759,21 @@ async def neuron_for_uid( if uid is None: return NeuronInfo.get_null_neuron() - params = [netuid, uid, block_hash] if block_hash else [netuid, uid] - json_body = await self.substrate.rpc_request( - method="neuronInfo_getNeuron", - params=params, # custom rpc method + hex_bytes_result = await self.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neuron", + params=[netuid, uid], + block_hash=block_hash, ) - if not (result := json_body.get("result", None)): + if not (result := hex_bytes_result): return NeuronInfo.get_null_neuron() - bytes_result = bytes(result) + if result.startswith("0x"): + bytes_result = bytes.fromhex(result[2:]) + else: + bytes_result = bytes.fromhex(result) + return NeuronInfo.from_vec_u8(bytes_result) async def get_delegated( @@ -720,15 +802,53 @@ async def get_delegated( else (self.substrate.last_block_hash if reuse_block else None) ) encoded_coldkey = ss58_to_vec_u8(coldkey_ss58) - json_body = await self.substrate.rpc_request( - method="delegateInfo_getDelegated", - params=([block_hash, encoded_coldkey] if block_hash else [encoded_coldkey]), + + hex_bytes_result = await self.query_runtime_api( + runtime_api="DelegateInfoRuntimeApi", + method="get_delegated", + params=[encoded_coldkey], + block_hash=block_hash, ) - if not (result := json_body.get("result")): + if not (result := hex_bytes_result): return [] - return DelegateInfo.delegated_list_from_vec_u8(bytes(result)) + if result.startswith("0x"): + bytes_result = bytes.fromhex(result[2:]) + else: + bytes_result = bytes.fromhex(result) + + return DelegateInfo.delegated_list_from_vec_u8(bytes_result) + + async def query_all_identities( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, dict]: + """ + Queries all identities on the Bittensor blockchain. + + :param block_hash: The hash of the blockchain block number at which to perform the query. + :param reuse_block: Whether to reuse the last-used blockchain block hash. + + :return: A dictionary mapping addresses to their decoded identity data. + """ + + identities = await self.substrate.query_map( + module="SubtensorModule", + storage_function="Identities", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + if identities is None: + return {} + + all_identities = { + decode_account_id(ss58_address[0]): decode_hex_identity(identity) + for ss58_address, identity in identities + } + return all_identities async def query_identity( self, @@ -754,43 +874,61 @@ async def query_identity( The identity information can include various attributes such as the neuron's stake, rank, and other network-specific details, providing insights into the neuron's role and status within the Bittensor network. """ - - def decode_hex_identity_dict(info_dictionary): - for k, v in info_dictionary.items(): - if isinstance(v, dict): - item = next(iter(v.values())) - else: - item = v - if isinstance(item, tuple) and item: - if len(item) > 1: - try: - info_dictionary[k] = ( - bytes(item).hex(sep=" ", bytes_per_sep=2).upper() - ) - except UnicodeDecodeError: - print(f"Could not decode: {k}: {item}") - else: - try: - info_dictionary[k] = bytes(item[0]).decode("utf-8") - except UnicodeDecodeError: - print(f"Could not decode: {k}: {item}") - else: - info_dictionary[k] = item - - return info_dictionary - identity_info = await self.substrate.query( - module="Registry", - storage_function="IdentityOf", + module="SubtensorModule", + storage_function="Identities", params=[key], block_hash=block_hash, reuse_block_hash=reuse_block, ) + if not identity_info: + return {} try: - return decode_hex_identity_dict(identity_info["info"]) + return decode_hex_identity(identity_info) except TypeError: return {} + async def fetch_coldkey_hotkey_identities( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> dict[str, dict]: + """ + Builds a dictionary containing coldkeys and hotkeys with their associated identities and relationships. + :param block_hash: The hash of the blockchain block number for the query. + :param reuse_block: Whether to reuse the last-used blockchain block hash. + :return: Dict with 'coldkeys' and 'hotkeys' as keys. + """ + + coldkey_identities = await self.query_all_identities() + identities = {"coldkeys": {}, "hotkeys": {}} + if not coldkey_identities: + return identities + query = await self.substrate.query_multiple( + params=[(ss58) for ss58, _ in coldkey_identities.items()], + module="SubtensorModule", + storage_function="OwnedHotkeys", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + for coldkey_ss58, hotkeys in query.items(): + coldkey_identity = coldkey_identities.get(coldkey_ss58) + hotkeys = [decode_account_id(hotkey[0]) for hotkey in hotkeys or []] + + identities["coldkeys"][coldkey_ss58] = { + "identity": coldkey_identity, + "hotkeys": hotkeys, + } + + for hotkey_ss58 in hotkeys: + identities["hotkeys"][hotkey_ss58] = { + "coldkey": coldkey_ss58, + "identity": coldkey_identity, + } + + return identities + async def weights( self, netuid: int, block_hash: Optional[str] = None ) -> list[tuple[int, list[tuple[int, int]]]]: @@ -1087,3 +1225,190 @@ async def get_delegate_identities( ) return all_delegates_details + + async def get_delegates_by_netuid_light( + self, netuid: int, block_hash: Optional[str] = None + ) -> list[DelegateInfoLite]: + """ + Retrieves a list of all delegate neurons within the Bittensor network. This function provides an overview of the neurons that are actively involved in the network's delegation system. + + Analyzing the delegate population offers insights into the network's governance dynamics and the distribution of trust and responsibility among participating neurons. + + Args: + netuid: the netuid to query + block_hash: The hash of the blockchain block number for the query. + + Returns: + A list of DelegateInfo objects detailing each delegate's characteristics. + + """ + # TODO (Ben): doesn't exist + params = [netuid] if not block_hash else [netuid, block_hash] + json_body = await self.substrate.rpc_request( + method="delegateInfo_getDelegatesLight", # custom rpc method + params=params, + ) + + result = json_body["result"] + + if result in (None, []): + return [] + + return DelegateInfoLite.list_from_vec_u8(result) # TODO this won't work yet + + async def get_subnet_dynamic_info( + self, netuid: int, block_hash: Optional[str] = None + ) -> "DynamicInfo": + hex_bytes_result = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_dynamic_info", + params=[netuid], + block_hash=block_hash, + ) + + if hex_bytes_result is None: + return None + + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) + + subnets = DynamicInfo.from_vec_u8(bytes_result) + return subnets + + async def get_stake_for_coldkey_and_hotkey_on_netuid( + self, + hotkey_ss58: str, + coldkey_ss58: str, + netuid: int, + block_hash: Optional[str] = None, + ) -> "Balance": + """Returns the stake under a coldkey - hotkey - netuid pairing""" + _result = await self.substrate.query( + "SubtensorModule", "Alpha", [hotkey_ss58, coldkey_ss58, netuid], block_hash + ) + if _result is None: + return Balance(0).set_unit(netuid) + else: + return Balance.from_rao(_result).set_unit(int(netuid)) + + async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( + self, + hotkey_ss58s: list[str], + coldkey_ss58: str, + netuids: list[int], + block_hash: Optional[str] = None, + ) -> dict[str, dict[int, "Balance"]]: + """ + Queries the stake for multiple hotkey - coldkey - netuid pairings. + + Args: + hotkey_ss58s: list of hotkey ss58 addresses + coldkey_ss58: a single coldkey ss58 address + netuids: list of netuids + block_hash: hash of the blockchain block, if any + + Returns: + { + hotkey_ss58_1: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + hotkey_ss58_2: { + netuid_1: netuid1_stake, + netuid_2: netuid2_stake, + ... + }, + ... + } + + """ + calls = [ + ( + await self.substrate.create_storage_key( + "SubtensorModule", + "Alpha", + [hk_ss58, coldkey_ss58, netuid], + block_hash=block_hash, + ) + ) + for hk_ss58 in hotkey_ss58s + for netuid in netuids + ] + batch_call = await self.substrate.query_multi(calls, block_hash=block_hash) + results: dict[str, dict[int, "Balance"]] = { + hk_ss58: {} for hk_ss58 in hotkey_ss58s + } + for idx, (_, val) in enumerate(batch_call): + hotkey_idx = idx // len(netuids) + netuid_idx = idx % len(netuids) + hotkey_ss58 = hotkey_ss58s[hotkey_idx] + netuid = netuids[netuid_idx] + value = ( + Balance.from_rao(val).set_unit(netuid) + if val is not None + else Balance(0).set_unit(netuid) + ) + results[hotkey_ss58][netuid] = value + return results + + async def get_stake_info_for_coldkeys( + self, coldkey_ss58_list: list[str], block_hash: Optional[str] = None + ) -> Optional[dict[str, list[StakeInfo]]]: + """ + Retrieves stake information for a list of coldkeys. This function aggregates stake data for multiple + accounts, providing a collective view of their stakes and delegations. + + Args: + coldkey_ss58_list: A list of SS58 addresses of the accounts' coldkeys. + block_hash: The blockchain block number for the query. + + Returns: + A dictionary mapping each coldkey to a list of its StakeInfo objects. + + This function is useful for analyzing the stake distribution and delegation patterns of multiple + accounts simultaneously, offering a broader perspective on network participation and investment strategies. + """ + encoded_coldkeys = [ + ss58_to_vec_u8(coldkey_ss58) for coldkey_ss58 in coldkey_ss58_list + ] + + hex_bytes_result = await self.query_runtime_api( + runtime_api="StakeInfoRuntimeApi", + method="get_stake_info_for_coldkeys", + params=[encoded_coldkeys], + block_hash=block_hash, + ) + + if hex_bytes_result is None: + return None + + if hex_bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(hex_bytes_result[2:]) + else: + bytes_result = bytes.fromhex(hex_bytes_result) + + return StakeInfo.list_of_tuple_from_vec_u8(bytes_result) # type: ignore + + async def get_all_subnet_dynamic_info(self) -> list["DynamicInfo"]: + query = await self.substrate.runtime_call( + "SubnetInfoRuntimeApi", + "get_all_dynamic_info", + ) + subnets = DynamicInfo.list_from_vec_u8(bytes.fromhex(query.decode()[2:])) + return subnets + + async def get_global_weights( + self, netuids: list[int], block_hash: Optional[str] = None + ): + result = await self.substrate.query_multiple( + module="SubtensorModule", + storage_function="GlobalWeight", + params=[netuid for netuid in netuids], + block_hash=block_hash, + ) + return { + netuid: u64_normalized_float(weight) for (netuid, weight) in result.items() + } diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index e63807a83..7987db000 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -3,9 +3,11 @@ import os import sqlite3 import webbrowser +import sys from pathlib import Path from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable from urllib.parse import urlparse +from functools import partial from bittensor_wallet import Wallet, Keypair from bittensor_wallet.utils import SS58_FORMAT @@ -16,6 +18,7 @@ import numpy as np from numpy.typing import NDArray from rich.console import Console +from rich.prompt import Prompt import scalecodec from scalecodec.base import RuntimeConfiguration from scalecodec.type_registry import load_type_registry_preset @@ -23,6 +26,7 @@ from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src import defaults, Constants if TYPE_CHECKING: @@ -443,7 +447,7 @@ def get_explorer_url_for_network( explorer_opentensor_url = "{root_url}/query/{block_hash}".format( root_url=explorer_root_urls.get("opentensor"), block_hash=block_hash ) - explorer_taostats_url = "{root_url}/extrinsic/{block_hash}".format( + explorer_taostats_url = "{root_url}/hash/{block_hash}".format( root_url=explorer_root_urls.get("taostats"), block_hash=block_hash ) explorer_urls["opentensor"] = explorer_opentensor_url @@ -953,7 +957,7 @@ def retry_prompt( rejection_text: str, default="", show_default=False, - prompt_type=typer.prompt, + prompt_type=Prompt.ask, ): """ Allows for asking prompts again if they do not meet a certain criteria (as defined in `rejection`) @@ -974,3 +978,111 @@ def retry_prompt( return var else: err_console.print(rejection_text) + + +def validate_netuid(value: int) -> int: + if value is not None and value < 0: + raise typer.BadParameter("Negative netuid passed. Please use correct netuid.") + return value + + +def validate_uri(uri: str) -> str: + if not uri: + raise ValueError("URI cannot be empty") + clean_uri = uri.lstrip("/").lower() + if not clean_uri.isalnum(): + raise typer.BadParameter( + f"Invalid URI format: {uri}. URI must contain only alphanumeric characters (e.g. 'alice', 'bob')" + ) + return f"//{clean_uri.capitalize()}" + + +def get_effective_network(config, network: Optional[list[str]]) -> str: + """ + Determines the effective network to be used, considering the network parameter, + the configuration, and the default. + """ + if network: + for item in network: + if item.startswith("ws"): + network_ = item + break + else: + network_ = item + return network_ + elif config.get("network"): + return config["network"] + else: + return defaults.subtensor.network + + +def is_rao_network(network: str) -> bool: + """Check if the given network is 'rao'.""" + network = network.lower() + rao_identifiers = [ + "rao", + Constants.rao_entrypoint, + ] + return ( + network == "rao" + or network in rao_identifiers + or "rao.chain.opentensor.ai" in network + ) + + +def prompt_for_identity( + current_identity: dict, + name: Optional[str], + web_url: Optional[str], + image_url: Optional[str], + discord_handle: Optional[str], + description: Optional[str], + additional_info: Optional[str], +): + """ + Prompts the user for identity fields with validation. + Returns a dictionary with the updated fields. + """ + identity_fields = {} + + fields = [ + ("name", "[blue]Display name[/blue]", name), + ("url", "[blue]Web URL[/blue]", web_url), + ("image", "[blue]Image URL[/blue]", image_url), + ("discord", "[blue]Discord handle[/blue]", discord_handle), + ("description", "[blue]Description[/blue]", description), + ("additional", "[blue]Additional information[/blue]", additional_info), + ] + + text_rejection = partial( + retry_prompt, + rejection=lambda x: sys.getsizeof(x) > 113, + rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.", + ) + + if not any( + [ + name, + web_url, + image_url, + discord_handle, + description, + additional_info, + ] + ): + console.print( + "\n[yellow]All fields are optional. Press Enter to skip and keep the default/existing value.[/yellow]\n" + "[dark_sea_green3]Tip: Entering a space and pressing Enter will clear existing default value.\n" + ) + + for key, prompt, value in fields: + if value: + identity_fields[key] = value + else: + identity_fields[key] = text_rejection( + prompt, + default=current_identity.get(key, ""), + show_default=True, + ) + + return identity_fields diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py deleted file mode 100644 index 2401eb002..000000000 --- a/bittensor_cli/src/commands/root.py +++ /dev/null @@ -1,1770 +0,0 @@ -import asyncio -import json -from typing import Optional, TYPE_CHECKING - -from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError -import numpy as np -from numpy.typing import NDArray -from rich import box -from rich.prompt import Confirm -from rich.table import Column, Table -from rich.text import Text -from scalecodec import GenericCall, ScaleType -from substrateinterface.exceptions import SubstrateRequestException -import typer - -from bittensor_cli.src import DelegatesDetails -from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.chain_data import ( - DelegateInfo, - NeuronInfoLite, - decode_account_id, -) -from bittensor_cli.src.bittensor.extrinsics.root import ( - root_register_extrinsic, - set_root_weights_extrinsic, -) -from bittensor_cli.src.commands.wallets import ( - get_coldkey_wallets_for_path, - set_id, - set_id_prompts, -) -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -from bittensor_cli.src.bittensor.utils import ( - console, - convert_weight_uids_and_vals_to_tensor, - create_table, - err_console, - print_verbose, - get_metadata_table, - render_table, - ss58_to_vec_u8, - update_metadata_table, - group_subnets, -) - -if TYPE_CHECKING: - from bittensor_cli.src.bittensor.subtensor_interface import ProposalVoteData - -# helpers - - -def display_votes( - vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails] -) -> str: - vote_list = list() - - for address in vote_data.ayes: - vote_list.append( - "{}: {}".format( - delegate_info[address].display if address in delegate_info else address, - "[bold green]Aye[/bold green]", - ) - ) - - for address in vote_data.nays: - vote_list.append( - "{}: {}".format( - delegate_info[address].display if address in delegate_info else address, - "[bold red]Nay[/bold red]", - ) - ) - - return "\n".join(vote_list) - - -def format_call_data(call_data: dict) -> str: - # Extract the module and call details - module, call_details = next(iter(call_data.items())) - - # Extract the call function name and arguments - call_info = call_details[0] - call_function, call_args = next(iter(call_info.items())) - - # Extract the argument, handling tuple values - formatted_args = ", ".join( - str(arg[0]) if isinstance(arg, tuple) else str(arg) - for arg in call_args.values() - ) - - # Format the final output string - return f"{call_function}({formatted_args})" - - -async def _get_senate_members( - subtensor: SubtensorInterface, block_hash: Optional[str] = None -) -> list[str]: - """ - Gets all members of the senate on the given subtensor's network - - :param subtensor: SubtensorInterface object to use for the query - - :return: list of the senate members' ss58 addresses - """ - senate_members = await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - block_hash=block_hash, - ) - try: - return [ - decode_account_id(i[x][0]) for i in senate_members for x in range(len(i)) - ] - except (IndexError, TypeError): - err_console.print("Unable to retrieve senate members.") - return [] - - -async def _get_proposals( - subtensor: SubtensorInterface, block_hash: str -) -> dict[str, tuple[dict, "ProposalVoteData"]]: - async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: - proposal_data = await subtensor.substrate.query( - module="Triumvirate", - storage_function="ProposalOf", - block_hash=block_hash, - params=[p_hash], - ) - return proposal_data - - ph = await subtensor.substrate.query( - module="Triumvirate", - storage_function="Proposals", - params=None, - block_hash=block_hash, - ) - - try: - proposal_hashes: list[str] = [ - f"0x{bytes(ph[0][x][0]).hex()}" for x in range(len(ph[0])) - ] - except (IndexError, TypeError): - err_console.print("Unable to retrieve proposal vote data") - return {} - - call_data_, vote_data_ = await asyncio.gather( - asyncio.gather(*[get_proposal_call_data(h) for h in proposal_hashes]), - asyncio.gather(*[subtensor.get_vote_data(h) for h in proposal_hashes]), - ) - return { - proposal_hash: (cd, vd) - for cd, vd, proposal_hash in zip(call_data_, vote_data_, proposal_hashes) - } - - -def _validate_proposal_hash(proposal_hash: str) -> bool: - if proposal_hash[0:2] != "0x" or len(proposal_hash) != 66: - return False - else: - return True - - -async def _is_senate_member(subtensor: SubtensorInterface, hotkey_ss58: str) -> bool: - """ - Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Bittensor senate. - The senate is a key governance body within the Bittensor network, responsible for overseeing and - approving various network operations and proposals. - - :param subtensor: SubtensorInterface object to use for the query - :param hotkey_ss58: The `SS58` address of the neuron's hotkey. - - :return: `True` if the neuron is a senate member at the given block, `False` otherwise. - - This function is crucial for understanding the governance dynamics of the Bittensor network and for - identifying the neurons that hold decision-making power within the network. - """ - - senate_members = await _get_senate_members(subtensor) - - if not hasattr(senate_members, "count"): - return False - - return senate_members.count(hotkey_ss58) > 0 - - -async def vote_senate_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - proposal_hash: str, - proposal_idx: int, - vote: bool, - wait_for_inclusion: bool = False, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Votes ayes or nays on proposals. - - :param subtensor: The SubtensorInterface object to use for the query - :param wallet: Bittensor wallet object, with coldkey and hotkey unlocked. - :param proposal_hash: The hash of the proposal for which voting data is requested. - :param proposal_idx: The index of the proposal to vote. - :param vote: Whether to vote aye or nay. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. - """ - - if prompt: - # Prompt user for confirmation. - if not Confirm.ask(f"Cast a vote of {vote}?"): - return False - - with console.status(":satellite: Casting vote..", spinner="aesthetic"): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="vote", - call_params={ - "hotkey": wallet.hotkey.ss58_address, - "proposal": proposal_hash, - "index": proposal_idx, - "approve": vote, - }, - ) - success, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - await asyncio.sleep(0.5) - return False - # Successful vote, final check for data - else: - if vote_data := await subtensor.get_vote_data(proposal_hash): - if ( - vote_data.ayes.count(wallet.hotkey.ss58_address) > 0 - or vote_data.nays.count(wallet.hotkey.ss58_address) > 0 - ): - console.print(":white_heavy_check_mark: [green]Vote cast.[/green]") - return True - else: - # hotkey not found in ayes/nays - err_console.print( - ":cross_mark: [red]Unknown error. Couldn't find vote.[/red]" - ) - return False - else: - return False - - -async def burned_register_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - netuid: int, - recycle_amount: Balance, - old_balance: Balance, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = True, - prompt: bool = False, -) -> bool: - """Registers the wallet to chain by recycling TAO. - - :param subtensor: The SubtensorInterface object to use for the call, initialized - :param wallet: Bittensor wallet object. - :param netuid: The `netuid` of the subnet to register on. - :param recycle_amount: The amount of TAO required for this burn. - :param old_balance: The wallet balance prior to the registration burn. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - - :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. - """ - - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - with console.status( - f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", - spinner="aesthetic", - ) as status: - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] - ) - - print_verbose("Checking if already registered", status) - neuron = await subtensor.neuron_for_uid( - uid=my_uid, - netuid=netuid, - block_hash=subtensor.substrate.last_block_hash, - ) - - if not neuron.is_null: - console.print( - ":white_heavy_check_mark: [green]Already Registered[/green]:\n" - f"uid: [bold white]{neuron.uid}[/bold white]\n" - f"netuid: [bold white]{neuron.netuid}[/bold white]\n" - f"hotkey: [bold white]{neuron.hotkey}[/bold white]\n" - f"coldkey: [bold white]{neuron.coldkey}[/bold white]" - ) - return True - - with console.status( - ":satellite: Recycling TAO for Registration...", spinner="aesthetic" - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="burned_register", - call_params={ - "netuid": netuid, - "hotkey": wallet.hotkey.ss58_address, - }, - ) - success, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - await asyncio.sleep(0.5) - return False - # Successful registration, final check for neuron and pubkey - else: - with console.status(":satellite: Checking Balance...", spinner="aesthetic"): - block_hash = await subtensor.substrate.get_chain_head() - new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( - subtensor.get_balance( - wallet.coldkeypub.ss58_address, - block_hash=block_hash, - reuse_block=False, - ), - subtensor.get_netuids_for_hotkey( - wallet.hotkey.ss58_address, block_hash=block_hash - ), - subtensor.substrate.query( - "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] - ), - ) - - console.print( - "Balance:\n" - f" [blue]{old_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]" - ) - - if len(netuids_for_hotkey) > 0: - console.print( - f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" - ) - return True - else: - # neuron not found, try again - err_console.print( - ":cross_mark: [red]Unknown error. Neuron not found.[/red]" - ) - return False - - -async def set_take_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - delegate_ss58: str, - take: float = 0.0, - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, -) -> bool: - """ - Set delegate hotkey take - - :param subtensor: SubtensorInterface (initialized) - :param wallet: The wallet containing the hotkey to be nominated. - :param delegate_ss58: Hotkey - :param take: Delegate take on subnet ID - :param wait_for_finalization: If `True`, waits until the transaction is finalized on the - blockchain. - :param wait_for_inclusion: If `True`, waits until the transaction is included in a block. - - :return: `True` if the process is successful, `False` otherwise. - - This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the - dynamic selection and participation of validators in the network's consensus process. - """ - - async def _get_delegate_by_hotkey(ss58: str) -> Optional[DelegateInfo]: - """Retrieves the delegate info for a given hotkey's ss58 address""" - encoded_hotkey = ss58_to_vec_u8(ss58) - json_body = await subtensor.substrate.rpc_request( - method="delegateInfo_getDelegate", # custom rpc method - params=([encoded_hotkey, subtensor.substrate.last_block_hash]), - ) - if not (result := json_body.get("result", None)): - return None - else: - return DelegateInfo.from_vec_u8(bytes(result)) - - # Calculate u16 representation of the take - take_u16 = int(take * 0xFFFF) - - print_verbose("Checking current take") - # Check if the new take is greater or lower than existing take or if existing is set - delegate = await _get_delegate_by_hotkey(delegate_ss58) - current_take = None - if delegate is not None: - current_take = int( - float(delegate.take) * 65535.0 - ) # TODO verify this, why not u16_float_to_int? - - if take_u16 == current_take: - console.print("Nothing to do, take hasn't changed") - return True - if current_take is None or current_take < take_u16: - console.print( - f"Current take is {float(delegate.take):.4f}. Increasing to {take:.4f}." - ) - with console.status( - f":satellite: Sending decrease_take_extrinsic call on [white]{subtensor}[/white] ..." - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="increase_take", - call_params={ - "hotkey": delegate_ss58, - "take": take_u16, - }, - ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) - - else: - console.print( - f"Current take is {float(delegate.take):.4f}. Decreasing to {take:.4f}." - ) - with console.status( - f":satellite: Sending increase_take_extrinsic call on [white]{subtensor}[/white] ..." - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="decrease_take", - call_params={ - "hotkey": delegate_ss58, - "take": take_u16, - }, - ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) - - if not success: - err_console.print(err) - else: - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - return success - - -async def delegate_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - delegate_ss58: str, - amount: Optional[float], - wait_for_inclusion: bool = True, - wait_for_finalization: bool = False, - prompt: bool = False, - delegate: bool = True, -) -> bool: - """Delegates the specified amount of stake to the passed delegate. - - :param subtensor: The SubtensorInterface used to perform the delegation, initialized. - :param wallet: Bittensor wallet object. - :param delegate_ss58: The `ss58` address of the delegate. - :param amount: Amount to stake as bittensor balance, None to stake all available TAO. - :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns - `False` if the extrinsic fails to enter the block within the timeout. - :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, - or returns `False` if the extrinsic fails to be finalized within the timeout. - :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :param delegate: whether to delegate (`True`) or undelegate (`False`) - - :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, - the response is `True`. - """ - - async def _do_delegation(staking_balance_: Balance) -> tuple[bool, str]: - """Performs the delegation extrinsic call to the chain.""" - if delegate: - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": delegate_ss58, - "amount_staked": staking_balance_.rao, - }, - ) - else: - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": delegate_ss58, - "amount_unstaked": staking_balance_.rao, - }, - ) - return await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization - ) - - async def get_hotkey_owner(ss58: str, block_hash_: str): - """Returns the coldkey owner of the passed hotkey.""" - if not await subtensor.does_hotkey_exist(ss58, block_hash=block_hash_): - return None - _result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Owner", - params=[ss58], - block_hash=block_hash_, - ) - return decode_account_id(_result[0]) - - async def get_stake_for_coldkey_and_hotkey( - hotkey_ss58: str, coldkey_ss58: str, block_hash_: str - ): - """Returns the stake under a coldkey - hotkey pairing.""" - _result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="Stake", - params=[hotkey_ss58, coldkey_ss58], - block_hash=block_hash_, - ) - return Balance.from_rao(_result or 0) - - delegate_string = "delegate" if delegate else "undelegate" - - # Decrypt key - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - print_verbose("Checking if hotkey is a delegate") - if not await subtensor.is_hotkey_delegate(delegate_ss58): - err_console.print(f"Hotkey: {delegate_ss58} is not a delegate.") - return False - - # Get state. - with console.status( - f":satellite: Syncing with [bold white]{subtensor}[/bold white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Fetching balance, stake, and ownership", status) - initial_block_hash = await subtensor.substrate.get_chain_head() - ( - my_prev_coldkey_balance_, - delegate_owner, - my_prev_delegated_stake, - ) = await asyncio.gather( - subtensor.get_balance( - wallet.coldkey.ss58_address, block_hash=initial_block_hash - ), - get_hotkey_owner(delegate_ss58, block_hash_=initial_block_hash), - get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block_hash_=initial_block_hash, - ), - ) - - my_prev_coldkey_balance = my_prev_coldkey_balance_[wallet.coldkey.ss58_address] - - # Convert to bittensor.Balance - if amount is None: - # Stake it all. - if delegate_string == "delegate": - staking_balance = Balance.from_tao(my_prev_coldkey_balance.tao) - else: - # Unstake all - staking_balance = Balance.from_tao(my_prev_delegated_stake.tao) - else: - staking_balance = Balance.from_tao(amount) - - # Check enough balance to stake. - if delegate_string == "delegate" and staking_balance > my_prev_coldkey_balance: - err_console.print( - ":cross_mark: [red]Not enough balance to stake[/red]:\n" - f" [bold blue]current balance[/bold blue]:{my_prev_coldkey_balance}\n" - f" [bold red]amount staking[/bold red]: {staking_balance}\n" - f" [bold white]coldkey: {wallet.name}[/bold white]" - ) - return False - - if delegate_string == "undelegate" and ( - my_prev_delegated_stake is None or staking_balance > my_prev_delegated_stake - ): - err_console.print( - "\n:cross_mark: [red]Not enough balance to unstake[/red]:\n" - f" [bold blue]current stake[/bold blue]: {my_prev_delegated_stake}\n" - f" [bold red]amount unstaking[/bold red]: {staking_balance}\n" - f" [bold white]coldkey: {wallet.name}[bold white]\n\n" - ) - return False - - if delegate: - # Grab the existential deposit. - existential_deposit = await subtensor.get_existential_deposit() - - # Remove existential balance to keep key alive. - if staking_balance > my_prev_coldkey_balance - existential_deposit: - staking_balance = my_prev_coldkey_balance - existential_deposit - else: - staking_balance = staking_balance - - # Ask before moving on. - if prompt: - if not Confirm.ask( - f"\n[bold blue]Current stake[/bold blue]: [blue]{my_prev_delegated_stake}[/blue]\n" - f"[bold white]Do you want to {delegate_string}:[/bold white]\n" - f" [bold red]amount[/bold red]: [red]{staking_balance}\n[/red]" - f" [bold yellow]{'to' if delegate_string == 'delegate' else 'from'} hotkey[/bold yellow]: [yellow]{delegate_ss58}\n[/yellow]" - f" [bold green]hotkey owner[/bold green]: [green]{delegate_owner}[/green]" - ): - return False - - with console.status( - f":satellite: Staking to: [bold white]{subtensor}[/bold white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Transmitting delegate operation call") - staking_response, err_msg = await _do_delegation(staking_balance) - - if staking_response is True: # If we successfully staked. - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True - - console.print(":white_heavy_check_mark: [green]Finalized[/green]\n") - with console.status( - f":satellite: Checking Balance on: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Fetching balance and stakes", status) - block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_delegate_stake = await asyncio.gather( - subtensor.get_balance( - wallet.coldkey.ss58_address, block_hash=block_hash - ), - get_stake_for_coldkey_and_hotkey( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=delegate_ss58, - block_hash_=block_hash, - ), - ) - - console.print( - "Balance:\n" - f" [blue]{my_prev_coldkey_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]\n" - "Stake:\n" - f" [blue]{my_prev_delegated_stake}[/blue] :arrow_right: [green]{new_delegate_stake}[/green]" - ) - return True - else: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False - - -async def nominate_extrinsic( - subtensor: SubtensorInterface, - wallet: Wallet, - wait_for_finalization: bool = False, - wait_for_inclusion: bool = True, -) -> bool: - """Becomes a delegate for the hotkey. - - :param wallet: The unlocked wallet to become a delegate for. - :param subtensor: The SubtensorInterface to use for the transaction - :param wait_for_finalization: Wait for finalization or not - :param wait_for_inclusion: Wait for inclusion or not - - :return: success - """ - with console.status( - ":satellite: Sending nominate call on [white]{}[/white] ...".format( - subtensor.network - ) - ): - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="become_delegate", - call_params={"hotkey": wallet.hotkey.ss58_address}, - ) - success, err_msg = await subtensor.sign_and_send_extrinsic( - call, - wallet, - wait_for_inclusion=wait_for_inclusion, - wait_for_finalization=wait_for_finalization, - ) - - if success is True: - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - - else: - err_console.print(f":cross_mark: [red]Failed[/red]: error:{err_msg}") - return success - - -# Commands - - -async def root_list(subtensor: SubtensorInterface): - """List the root network""" - - async def _get_list() -> tuple: - senate_query = await subtensor.substrate.query( - module="SenateMembers", - storage_function="Members", - params=None, - ) - sm = [decode_account_id(i[x][0]) for i in senate_query for x in range(len(i))] - - rn: list[NeuronInfoLite] = await subtensor.neurons_lite(netuid=0) - if not rn: - return [], [], {}, {} - - di: dict[str, DelegatesDetails] = await subtensor.get_delegate_identities() - ts: dict[str, ScaleType] = await subtensor.substrate.query_multiple( - [n.hotkey for n in rn], - module="SubtensorModule", - storage_function="TotalHotkeyStake", - reuse_block_hash=True, - ) - return sm, rn, di, ts - - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ): - senate_members, root_neurons, delegate_info, total_stakes = await _get_list() - total_tao = sum( - float(Balance.from_rao(total_stakes[neuron.hotkey])) - for neuron in root_neurons - ) - - table = Table( - Column( - "[bold white]UID", - style="dark_orange", - no_wrap=True, - footer=f"[bold]{len(root_neurons)}[/bold]", - ), - Column( - "[bold white]NAME", - style="bright_cyan", - no_wrap=True, - ), - Column( - "[bold white]ADDRESS", - style="bright_magenta", - no_wrap=True, - ), - Column( - "[bold white]STAKE(\u03c4)", - justify="right", - style="light_goldenrod2", - no_wrap=True, - footer=f"{total_tao:.2f} (\u03c4) ", - ), - Column( - "[bold white]SENATOR", - style="dark_sea_green", - no_wrap=True, - ), - title=f"[underline dark_orange]Root Network[/underline dark_orange]\n[dark_orange]Network {subtensor.network}", - show_footer=True, - show_edge=False, - expand=False, - border_style="bright_black", - leading=True, - ) - - if not root_neurons: - err_console.print( - f"[red]Error: No neurons detected on the network:[/red] [white]{subtensor}" - ) - raise typer.Exit() - - sorted_root_neurons = sorted( - root_neurons, - key=lambda neuron: float(Balance.from_rao(total_stakes[neuron.hotkey])), - reverse=True, - ) - - for neuron_data in sorted_root_neurons: - table.add_row( - str(neuron_data.uid), - ( - delegate_info[neuron_data.hotkey].display - if neuron_data.hotkey in delegate_info - else "~" - ), - neuron_data.hotkey, - "{:.5f}".format(float(Balance.from_rao(total_stakes[neuron_data.hotkey]))), - "Yes" if neuron_data.hotkey in senate_members else "No", - ) - - return console.print(table) - - -async def set_weights( - wallet: Wallet, - subtensor: SubtensorInterface, - netuids: list[int], - weights: list[float], - prompt: bool, -): - """Set weights for root network.""" - netuids_ = np.array(netuids, dtype=np.int64) - weights_ = np.array(weights, dtype=np.float32) - console.print(f"Setting weights in [dark_orange]network: {subtensor.network}") - - # Run the set weights operation. - - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=netuids_, - weights=weights_, - version_key=0, - prompt=prompt, - wait_for_finalization=True, - wait_for_inclusion=True, - ) - - -async def get_weights( - subtensor: SubtensorInterface, - limit_min_col: Optional[int], - limit_max_col: Optional[int], - reuse_last: bool, - html_output: bool, - no_cache: bool, -): - """Get weights for root network.""" - if not reuse_last: - with console.status( - ":satellite: Fetching weights from chain...", spinner="aesthetic" - ): - weights = await subtensor.weights(0) - - uid_to_weights: dict[int, dict] = {} - netuids = set() - for matrix in weights: - [uid, weights_data] = matrix - - if not len(weights_data): - uid_to_weights[uid] = {} - normalized_weights = [] - else: - normalized_weights = np.array(weights_data)[:, 1] / max( - np.sum(weights_data, axis=0)[1], 1 - ) - - for weight_data, normalized_weight in zip(weights_data, normalized_weights): - [netuid, _] = weight_data - netuids.add(netuid) - if uid not in uid_to_weights: - uid_to_weights[uid] = {} - - uid_to_weights[uid][netuid] = normalized_weight - rows: list[list[str]] = [] - for uid in uid_to_weights: - row = [str(uid)] - - uid_weights = uid_to_weights[uid] - for netuid in netuids: - if netuid in uid_weights: - row.append("{:0.2f}%".format(uid_weights[netuid] * 100)) - else: - row.append("~") - rows.append(row) - - if not no_cache: - db_cols = [("UID", "INTEGER")] - for netuid in netuids: - db_cols.append((f"_{netuid}", "TEXT")) - create_table("rootgetweights", db_cols, rows) - netuids = list(netuids) - update_metadata_table( - "rootgetweights", - {"rows": json.dumps(rows), "netuids": json.dumps(netuids)}, - ) - else: - metadata = get_metadata_table("rootgetweights") - rows = json.loads(metadata["rows"]) - netuids = json.loads(metadata["netuids"]) - - _min_lim = limit_min_col if limit_min_col is not None else 0 - _max_lim = limit_max_col + 1 if limit_max_col is not None else len(netuids) - _max_lim = min(_max_lim, len(netuids)) - - if _min_lim is not None and _min_lim > len(netuids): - err_console.print("Minimum limit greater than number of netuids") - return - - if not html_output: - table = Table( - show_footer=True, - box=None, - pad_edge=False, - width=None, - title="[white]Root Network Weights", - ) - table.add_column( - "[white]UID", - header_style="overline white", - footer_style="overline white", - style="rgb(50,163,219)", - no_wrap=True, - ) - netuids = list(netuids) - for netuid in netuids[_min_lim:_max_lim]: - table.add_column( - f"[white]{netuid}", - header_style="overline white", - footer_style="overline white", - justify="right", - style="green", - no_wrap=True, - ) - - if not rows: - err_console.print("No weights exist on the root network.") - return - - # Adding rows - for row in rows: - new_row = [row[0]] + row[_min_lim + 1 : _max_lim + 1] - table.add_row(*new_row) - - return console.print(table) - - else: - html_cols = [{"title": "UID", "field": "UID"}] - for netuid in netuids[_min_lim:_max_lim]: - html_cols.append({"title": str(netuid), "field": f"_{netuid}"}) - render_table( - "rootgetweights", - "Root Network Weights", - html_cols, - ) - - -async def _get_my_weights( - subtensor: SubtensorInterface, ss58_address: str, my_uid: str -) -> NDArray[np.float32]: - """Retrieves the weight array for a given hotkey SS58 address.""" - - my_weights_, total_subnets_ = await asyncio.gather( - subtensor.substrate.query( - "SubtensorModule", "Weights", [0, my_uid], reuse_block_hash=True - ), - subtensor.substrate.query( - "SubtensorModule", "TotalNetworks", reuse_block_hash=True - ), - ) - # If setting weights for the first time, pass 0 root weights - my_weights: list[tuple[int, int]] = ( - my_weights_ if my_weights_ is not None else [(0, 0)] - ) - total_subnets: int = total_subnets_ - - print_verbose("Fetching current weights") - for _, w in enumerate(my_weights): - if w: - print_verbose(f"{w}") - - uids, values = zip(*my_weights) - weight_array = convert_weight_uids_and_vals_to_tensor(total_subnets, uids, values) - return weight_array - - -async def set_boost( - wallet: Wallet, - subtensor: SubtensorInterface, - netuid: int, - amount: float, - prompt: bool, -): - """Boosts weight of a given netuid for root network.""" - console.print(f"Boosting weights in [dark_orange]network: {subtensor.network}") - print_verbose(f"Fetching uid of hotkey on root: {wallet.hotkey_str}") - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] - ) - - if my_uid is None: - err_console.print("Your hotkey is not registered to the root network") - return False - - print_verbose("Fetching current weights") - my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address, my_uid) - prev_weights = my_weights.copy() - my_weights[netuid] += amount - all_netuids = np.arange(len(my_weights)) - - console.print( - f"Boosting weight for netuid {netuid}\n\tfrom {prev_weights[netuid]} to {my_weights[netuid]}\n" - ) - console.print( - f"Previous weights -> Raw weights: \n\t{prev_weights} -> \n\t{my_weights}" - ) - - print_verbose(f"All netuids: {all_netuids}") - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - ) - - -async def set_slash( - wallet: Wallet, - subtensor: SubtensorInterface, - netuid: int, - amount: float, - prompt: bool, -): - """Slashes weight""" - console.print(f"Slashing weights in [dark_orange]network: {subtensor.network}") - print_verbose(f"Fetching uid of hotkey on root: {wallet.hotkey_str}") - my_uid = await subtensor.substrate.query( - "SubtensorModule", "Uids", [0, wallet.hotkey.ss58_address] - ) - if my_uid is None: - err_console.print("Your hotkey is not registered to the root network") - return False - - print_verbose("Fetching current weights") - my_weights = await _get_my_weights(subtensor, wallet.hotkey.ss58_address, my_uid) - prev_weights = my_weights.copy() - my_weights[netuid] -= amount - my_weights[my_weights < 0] = 0 # Ensure weights don't go negative - all_netuids = np.arange(len(my_weights)) - - console.print( - f"Slashing weight for netuid {netuid}\n\tfrom {prev_weights[netuid]} to {my_weights[netuid]}\n" - ) - console.print( - f"Previous weights -> Raw weights: \n\t{prev_weights} -> \n\t{my_weights}" - ) - - await set_root_weights_extrinsic( - subtensor=subtensor, - wallet=wallet, - netuids=all_netuids, - weights=my_weights, - version_key=0, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - ) - - -async def senate_vote( - wallet: Wallet, - subtensor: SubtensorInterface, - proposal_hash: str, - vote: bool, - prompt: bool, -) -> bool: - """Vote in Bittensor's governance protocol proposals""" - - if not proposal_hash: - err_console.print( - "Aborting: Proposal hash not specified. View all proposals with the `proposals` command." - ) - return False - elif not _validate_proposal_hash(proposal_hash): - err_console.print( - "Aborting. Proposal hash is invalid. Proposal hashes should start with '0x' and be 32 bytes long" - ) - return False - - print_verbose(f"Fetching senate status of {wallet.hotkey_str}") - if not await _is_senate_member(subtensor, hotkey_ss58=wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." - ) - return False - - # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") - vote_data = await subtensor.get_vote_data(proposal_hash, reuse_block=True) - if not vote_data: - err_console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") - return False - - success = await vote_senate_extrinsic( - subtensor=subtensor, - wallet=wallet, - proposal_hash=proposal_hash, - proposal_idx=vote_data.index, - vote=vote, - wait_for_inclusion=True, - wait_for_finalization=False, - prompt=prompt, - ) - - return success - - -async def get_senate(subtensor: SubtensorInterface): - """View Bittensor's governance protocol proposals""" - with console.status( - f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", - spinner="aesthetic", - ) as status: - print_verbose("Fetching senate members", status) - senate_members = await _get_senate_members(subtensor) - - print_verbose("Fetching member details from Github") - delegate_info: dict[ - str, DelegatesDetails - ] = await subtensor.get_delegate_identities() - - table = Table( - Column( - "[bold white]NAME", - style="bright_cyan", - no_wrap=True, - ), - Column( - "[bold white]ADDRESS", - style="bright_magenta", - no_wrap=True, - ), - title=f"[underline dark_orange]Senate[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - show_edge=False, - expand=False, - border_style="bright_black", - leading=True, - ) - - for ss58_address in senate_members: - table.add_row( - ( - delegate_info[ss58_address].display - if ss58_address in delegate_info - else "~" - ), - ss58_address, - ) - - return console.print(table) - - -async def register(wallet: Wallet, subtensor: SubtensorInterface, prompt: bool): - """Register neuron by recycling some TAO.""" - - console.print( - f"Registering on [dark_orange]netuid 0[/dark_orange] on network: [dark_orange]{subtensor.network}" - ) - - # Check current recycle amount - print_verbose("Fetching recycle amount & balance") - recycle_call, balance_ = await asyncio.gather( - subtensor.get_hyperparameter(param_name="Burn", netuid=0, reuse_block=True), - subtensor.get_balance(wallet.coldkeypub.ss58_address, reuse_block=True), - ) - current_recycle = Balance.from_rao(int(recycle_call)) - try: - balance: Balance = balance_[wallet.coldkeypub.ss58_address] - except TypeError as e: - err_console.print(f"Unable to retrieve current recycle. {e}") - return False - except KeyError: - err_console.print("Unable to retrieve current balance.") - return False - - # Check balance is sufficient - if balance < current_recycle: - err_console.print( - f"[red]Insufficient balance {balance} to register neuron. " - f"Current recycle is {current_recycle} TAO[/red]" - ) - return False - - if prompt: - if not Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\n" - f"The cost to register by recycle is [bold red]{current_recycle}[/bold red]\n" - f"Do you want to continue?", - default=False, - ): - return False - - await root_register_extrinsic( - subtensor, - wallet, - wait_for_inclusion=True, - wait_for_finalization=True, - prompt=prompt, - ) - - -async def proposals(subtensor: SubtensorInterface): - console.print( - ":satellite: Syncing with chain: [white]{}[/white] ...".format( - subtensor.network - ) - ) - print_verbose("Fetching senate members & proposals") - block_hash = await subtensor.substrate.get_chain_head() - senate_members, all_proposals = await asyncio.gather( - _get_senate_members(subtensor, block_hash), - _get_proposals(subtensor, block_hash), - ) - - print_verbose("Fetching member information from Chain") - registered_delegate_info: dict[ - str, DelegatesDetails - ] = await subtensor.get_delegate_identities() - - table = Table( - Column( - "[white]HASH", - style="light_goldenrod2", - no_wrap=True, - ), - Column("[white]THRESHOLD", style="rgb(42,161,152)"), - Column("[white]AYES", style="green"), - Column("[white]NAYS", style="red"), - Column( - "[white]VOTES", - style="rgb(50,163,219)", - ), - Column("[white]END", style="bright_cyan"), - Column("[white]CALLDATA", style="dark_sea_green"), - title=f"\n[dark_orange]Proposals\t\t\nActive Proposals: {len(all_proposals)}\t\tSenate Size: {len(senate_members)}\nNetwork: {subtensor.network}", - show_footer=True, - box=box.SIMPLE_HEAVY, - pad_edge=False, - width=None, - border_style="bright_black", - ) - for hash_, (call_data, vote_data) in all_proposals.items(): - table.add_row( - hash_, - str(vote_data.threshold), - str(len(vote_data.ayes)), - str(len(vote_data.nays)), - display_votes(vote_data, registered_delegate_info), - str(vote_data.end), - format_call_data(call_data), - ) - return console.print(table) - - -async def set_take(wallet: Wallet, subtensor: SubtensorInterface, take: float) -> bool: - """Set delegate take.""" - - async def _do_set_take() -> bool: - """ - Just more easily allows an early return and to close the substrate interface after the logic - """ - print_verbose("Checking if hotkey is a delegate") - # Check if the hotkey is not a delegate. - if not await subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} is NOT a delegate." - ) - return False - - if take > 0.18 or take < 0: - err_console.print("ERROR: Take value should not exceed 18% or be below 0%") - return False - - result: bool = await set_take_extrinsic( - subtensor=subtensor, - wallet=wallet, - delegate_ss58=wallet.hotkey.ss58_address, - take=take, - ) - - if not result: - err_console.print("Could not set the take") - return False - else: - # Check if we are a delegate. - is_delegate: bool = await subtensor.is_hotkey_delegate( - wallet.hotkey.ss58_address - ) - if not is_delegate: - err_console.print( - "Could not set the take [white]{}[/white]".format(subtensor.network) - ) - return False - else: - console.print( - "Successfully set the take on [white]{}[/white]".format( - subtensor.network - ) - ) - return True - - console.print(f"Setting take on [dark_orange]network: {subtensor.network}") - # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - result_ = await _do_set_take() - - return result_ - - -async def delegate_stake( - wallet: Wallet, - subtensor: SubtensorInterface, - amount: Optional[float], - delegate_ss58key: str, - prompt: bool, -): - """Delegates stake to a chain delegate.""" - console.print(f"Delegating stake on [dark_orange]network: {subtensor.network}") - await delegate_extrinsic( - subtensor, - wallet, - delegate_ss58key, - amount, - wait_for_inclusion=True, - prompt=prompt, - delegate=True, - ) - - -async def delegate_unstake( - wallet: Wallet, - subtensor: SubtensorInterface, - amount: Optional[float], - delegate_ss58key: str, - prompt: bool, -): - """Undelegates stake from a chain delegate.""" - console.print(f"Undelegating stake on [dark_orange]network: {subtensor.network}") - await delegate_extrinsic( - subtensor, - wallet, - delegate_ss58key, - amount, - wait_for_inclusion=True, - prompt=prompt, - delegate=False, - ) - - -async def my_delegates( - wallet: Wallet, subtensor: SubtensorInterface, all_wallets: bool -): - """Delegates stake to a chain delegate.""" - - async def wallet_to_delegates( - w: Wallet, bh: str - ) -> tuple[Optional[Wallet], Optional[list[tuple[DelegateInfo, Balance]]]]: - """Helper function to retrieve the validity of the wallet (if it has a coldkeypub on the device) - and its delegate info.""" - if not w.coldkeypub_file.exists_on_device(): - return None, None - else: - delegates_ = await subtensor.get_delegated( - w.coldkeypub.ss58_address, block_hash=bh - ) - return w, delegates_ - - wallets = get_coldkey_wallets_for_path(wallet.path) if all_wallets else [wallet] - - table = Table( - Column("[white]Wallet", style="bright_cyan"), - Column( - "[white]OWNER", - style="bold bright_cyan", - overflow="fold", - justify="left", - ratio=1, - ), - Column( - "[white]SS58", - style="bright_magenta", - justify="left", - overflow="fold", - ratio=3, - ), - Column("[white]Delegation", style="dark_orange", no_wrap=True, ratio=1), - Column("[white]\u03c4/24h", style="bold green", ratio=1), - Column( - "[white]NOMS", - justify="center", - style="rgb(42,161,152)", - no_wrap=True, - ratio=1, - ), - Column( - "[white]OWNER STAKE(\u03c4)", - justify="right", - style="light_goldenrod2", - no_wrap=True, - ratio=1, - ), - Column( - "[white]TOTAL STAKE(\u03c4)", - justify="right", - style="light_goldenrod2", - no_wrap=True, - ratio=1, - ), - Column("[white]SUBNETS", justify="right", style="white", ratio=1), - Column("[white]VPERMIT", justify="right"), - Column( - "[white]24h/k\u03c4", style="rgb(42,161,152)", justify="center", ratio=1 - ), - Column("[white]Desc", style="rgb(50,163,219)", ratio=3), - title=f"[underline dark_orange]My Delegates[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - show_edge=False, - expand=False, - box=box.SIMPLE_HEAVY, - border_style="bright_black", - leading=True, - ) - - total_delegated = 0 - - # TODO: this doesnt work when passed to wallets_with_delegates - # block_hash = await subtensor.substrate.get_chain_head() - - registered_delegate_info: dict[str, DelegatesDetails] - wallets_with_delegates: tuple[ - tuple[Optional[Wallet], Optional[list[tuple[DelegateInfo, Balance]]]] - ] - - print_verbose("Fetching delegate information") - wallets_with_delegates, registered_delegate_info = await asyncio.gather( - asyncio.gather(*[wallet_to_delegates(wallet_, None) for wallet_ in wallets]), - subtensor.get_delegate_identities(), - ) - if not registered_delegate_info: - console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - - print_verbose("Processing delegate information") - for wall, delegates in wallets_with_delegates: - if not wall or not delegates: - continue - - my_delegates_ = {} # hotkey, amount - for delegate in delegates: - for coldkey_addr, staked in delegate[0].nominators: - if coldkey_addr == wall.coldkeypub.ss58_address and staked.tao > 0: - my_delegates_[delegate[0].hotkey_ss58] = staked - - delegates.sort(key=lambda d: d[0].total_stake, reverse=True) - total_delegated += sum(my_delegates_.values()) - - for i, delegate in enumerate(delegates): - owner_stake = next( - ( - stake - for owner, stake in delegate[0].nominators - if owner == delegate[0].owner_ss58 - ), - Balance.from_rao(0), # default to 0 if no owner stake. - ) - if delegate[0].hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].display - delegate_url = registered_delegate_info[delegate[0].hotkey_ss58].web - delegate_description = registered_delegate_info[ - delegate[0].hotkey_ss58 - ].additional - else: - delegate_name = "~" - delegate_url = "" - delegate_description = "" - - if delegate[0].hotkey_ss58 in my_delegates_: - twenty_four_hour = delegate[0].total_daily_return.tao * ( - my_delegates_[delegate[0].hotkey_ss58] / delegate[0].total_stake.tao - ) - table.add_row( - wall.name, - Text(delegate_name, style=f"link {delegate_url}"), - f"{delegate[0].hotkey_ss58}", - f"{my_delegates_[delegate[0].hotkey_ss58]!s:13.13}", - f"{twenty_four_hour!s:6.6}", - str(len(delegate[0].nominators)), - f"{owner_stake!s:13.13}", - f"{delegate[0].total_stake!s:13.13}", - group_subnets(delegate[0].registrations), - group_subnets(delegate[0].validator_permits), - f"{delegate[0].total_daily_return.tao * (1000 / (0.001 + delegate[0].total_stake.tao))!s:6.6}", - str(delegate_description), - ) - if console.width < 150: - console.print( - "[yellow]Warning: Your terminal width might be too small to view all the information clearly" - ) - console.print(table) - console.print(f"Total delegated TAO: {total_delegated}") - - -async def list_delegates(subtensor: SubtensorInterface): - """List all delegates on the network.""" - - with console.status( - ":satellite: Loading delegates...", spinner="aesthetic" - ) as status: - print_verbose("Fetching delegate details from chain", status) - block_hash = await subtensor.substrate.get_chain_head() - registered_delegate_info, block_number, delegates = await asyncio.gather( - subtensor.get_delegate_identities(block_hash=block_hash), - subtensor.substrate.get_block_number(block_hash), - subtensor.get_delegates(block_hash=block_hash), - ) - - print_verbose("Fetching previous delegates info from chain", status) - - async def get_prev_delegates(fallback_offsets=(1200, 200)): - for offset in fallback_offsets: - try: - prev_block_hash = await subtensor.substrate.get_block_hash( - max(0, block_number - offset) - ) - return await subtensor.get_delegates(block_hash=prev_block_hash) - except SubstrateRequestException: - continue - return None - - prev_delegates = await get_prev_delegates() - - if prev_delegates is None: - err_console.print( - ":warning: [yellow]Could not fetch delegates history. [/yellow]" - ) - - delegates.sort(key=lambda d: d.total_stake, reverse=True) - prev_delegates_dict = {} - if prev_delegates is not None: - for prev_delegate in prev_delegates: - prev_delegates_dict[prev_delegate.hotkey_ss58] = prev_delegate - - if not registered_delegate_info: - console.print( - ":warning:[yellow]Could not get delegate info from chain.[/yellow]" - ) - table = Table( - Column( - "[white]INDEX\n\n", - str(len(delegates)), - style="bold white", - ), - Column( - "[white]DELEGATE\n\n", - style="bold bright_cyan", - justify="left", - overflow="fold", - ratio=1, - ), - Column( - "[white]SS58\n\n", - style="bright_magenta", - no_wrap=False, - overflow="fold", - ratio=2, - ), - Column( - "[white]NOMINATORS\n\n", - justify="center", - style="gold1", - no_wrap=True, - ratio=1, - ), - Column( - "[white]OWN STAKE\n(\u03c4)\n", - justify="right", - style="orange1", - no_wrap=True, - ratio=1, - ), - Column( - "[white]TOTAL STAKE\n(\u03c4)\n", - justify="right", - style="light_goldenrod2", - no_wrap=True, - ratio=1, - ), - Column("[white]CHANGE\n/(4h)\n", style="grey0", justify="center", ratio=1), - Column("[white]TAKE\n\n", style="white", no_wrap=True, ratio=1), - Column( - "[white]NOMINATOR\n/(24h)/k\u03c4\n", - style="dark_olive_green3", - justify="center", - ratio=1, - ), - Column( - "[white]DELEGATE\n/(24h)\n", - style="dark_olive_green3", - justify="center", - ratio=1, - ), - Column( - "[white]VPERMIT\n\n", - justify="center", - no_wrap=False, - max_width=20, - style="dark_sea_green", - ratio=2, - ), - Column("[white]Desc\n\n", style="rgb(50,163,219)", max_width=30, ratio=2), - title=f"[underline dark_orange]Root Delegates[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - pad_edge=False, - box=None, - ) - - for i, delegate in enumerate(delegates): - owner_stake = next( - ( - stake - for owner, stake in delegate.nominators - if owner == delegate.owner_ss58 - ), - Balance.from_rao(0), # default to 0 if no owner stake. - ) - if delegate.hotkey_ss58 in registered_delegate_info: - delegate_name = registered_delegate_info[delegate.hotkey_ss58].display - delegate_url = registered_delegate_info[delegate.hotkey_ss58].web - delegate_description = registered_delegate_info[ - delegate.hotkey_ss58 - ].additional - else: - delegate_name = "~" - delegate_url = "" - delegate_description = "" - - if delegate.hotkey_ss58 in prev_delegates_dict: - prev_stake = prev_delegates_dict[delegate.hotkey_ss58].total_stake - if prev_stake == 0: - if delegate.total_stake > 0: - rate_change_in_stake_str = "[green]100%[/green]" - else: - rate_change_in_stake_str = "[grey0]0%[/grey0]" - else: - rate_change_in_stake = ( - 100 - * (float(delegate.total_stake) - float(prev_stake)) - / float(prev_stake) - ) - if rate_change_in_stake > 0: - rate_change_in_stake_str = "[green]{:.2f}%[/green]".format( - rate_change_in_stake - ) - elif rate_change_in_stake < 0: - rate_change_in_stake_str = "[red]{:.2f}%[/red]".format( - rate_change_in_stake - ) - else: - rate_change_in_stake_str = "[grey0]0%[/grey0]" - else: - rate_change_in_stake_str = "[grey0]NA[/grey0]" - table.add_row( - # INDEX - str(i), - # DELEGATE - Text(delegate_name, style=f"link {delegate_url}"), - # SS58 - f"{delegate.hotkey_ss58}", - # NOMINATORS - str(len([nom for nom in delegate.nominators if nom[1].rao > 0])), - # DELEGATE STAKE - f"{owner_stake!s:13.13}", - # TOTAL STAKE - f"{delegate.total_stake!s:13.13}", - # CHANGE/(4h) - rate_change_in_stake_str, - # TAKE - f"{delegate.take * 100:.1f}%", - # NOMINATOR/(24h)/k - f"{Balance.from_tao(delegate.total_daily_return.tao * (1000 / (0.001 + delegate.total_stake.tao)))!s:6.6}", - # DELEGATE/(24h) - f"{Balance.from_tao(delegate.total_daily_return.tao * 0.18) !s:6.6}", - # VPERMIT - str(group_subnets(delegate.registrations)), - # Desc - str(delegate_description), - end_section=True, - ) - console.print(table) - - -async def nominate(wallet: Wallet, subtensor: SubtensorInterface, prompt: bool): - """Nominate wallet.""" - - console.print(f"Nominating on [dark_orange]network: {subtensor.network}") - # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") - return False - - print_verbose(f"Checking hotkey ({wallet.hotkey_str}) is a delegate") - # Check if the hotkey is already a delegate. - if await subtensor.is_hotkey_delegate(wallet.hotkey.ss58_address): - err_console.print( - f"Aborting: Hotkey {wallet.hotkey.ss58_address} is already a delegate." - ) - return - - print_verbose("Nominating hotkey as a delegate") - result: bool = await nominate_extrinsic(subtensor, wallet) - if not result: - err_console.print( - f"Could not became a delegate on [white]{subtensor.network}[/white]" - ) - return - else: - # Check if we are a delegate. - print_verbose("Confirming delegate status") - is_delegate: bool = await subtensor.is_hotkey_delegate( - wallet.hotkey.ss58_address - ) - if not is_delegate: - err_console.print( - f"Could not became a delegate on [white]{subtensor.network}[/white]" - ) - return - console.print( - f"Successfully became a delegate on [white]{subtensor.network}[/white]" - ) - - # Prompt use to set identity on chain. - if prompt: - do_set_identity = Confirm.ask("Would you like to set your identity? [y/n]") - - if do_set_identity: - id_prompts = set_id_prompts(validator=True) - await set_id(wallet, subtensor, *id_prompts, prompt=prompt) diff --git a/bittensor_cli/src/commands/stake/__init__.py b/bittensor_cli/src/commands/stake/__init__.py index e69de29bb..6b0b0fc91 100644 --- a/bittensor_cli/src/commands/stake/__init__.py +++ b/bittensor_cli/src/commands/stake/__init__.py @@ -0,0 +1,155 @@ +from typing import Optional, TYPE_CHECKING + +import rich.prompt +from rich.table import Table + +from bittensor_cli.src import DelegatesDetails +from bittensor_cli.src.bittensor.chain_data import DelegateInfo, DelegateInfoLite +from bittensor_cli.src.bittensor.utils import console, err_console + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def select_delegate(subtensor: "SubtensorInterface", netuid: int): + # Get a list of delegates and sort them by total stake in descending order + delegates: list[DelegateInfoLite] = ( + await subtensor.get_delegates_by_netuid_light(netuid) + ).sort(key=lambda x: x.total_stake, reverse=True) + + # Get registered delegates details. + registered_delegate_info = await subtensor.get_delegate_identities() + + # Create a table to display delegate information + table = Table( + show_header=True, + header_style="bold", + border_style="rgb(7,54,66)", + style="rgb(0,43,54)", + ) + + # Add columns to the table with specific styles + table.add_column("Index", style="rgb(253,246,227)", no_wrap=True) + table.add_column("Delegate Name", no_wrap=True) + table.add_column("Hotkey SS58", style="rgb(211,54,130)", no_wrap=True) + table.add_column("Owner SS58", style="rgb(133,153,0)", no_wrap=True) + table.add_column("Take", style="rgb(181,137,0)", no_wrap=True) + table.add_column( + "Total Stake", style="rgb(38,139,210)", no_wrap=True, justify="right" + ) + table.add_column( + "Owner Stake", style="rgb(220,50,47)", no_wrap=True, justify="right" + ) + # table.add_column("Return per 1000", style="rgb(108,113,196)", no_wrap=True, justify="right") + # table.add_column("Total Daily Return", style="rgb(42,161,152)", no_wrap=True, justify="right") + + # List to store visible delegates + visible_delegates = [] + + def get_user_input() -> str: + return rich.prompt.Prompt.ask( + 'Press Enter to scroll, enter a number (1-N) to select, or type "h" for help: ', + choices=["", "h"] + [str(x) for x in range(1, len(delegates) - 1)], + show_choices=True, + ) + + # TODO: Add pagination to handle large number of delegates more efficiently + # Iterate through delegates and display their information + + def loop_selections() -> Optional[int]: + idx = 0 + selected_idx = None + while idx < len(delegates): + if idx < len(delegates): + delegate = delegates[idx] + + # Add delegate to visible list + visible_delegates.append(delegate) + + # Add a row to the table with delegate information + table.add_row( + str(idx), + registered_delegate_info[delegate.hotkey_ss58].name + if delegate.hotkey_ss58 in registered_delegate_info + else "", + delegate.hotkey_ss58[:5] + + "..." + + delegate.hotkey_ss58[-5:], # Show truncated hotkey + delegate.owner_ss58[:5] + + "..." + + delegate.owner_ss58[-5:], # Show truncated owner address + f"{delegate.take:.6f}", + f"τ{delegate.total_stake.tao:,.4f}", + f"τ{delegate.owner_stake.tao:,.4f}", + # f"τ{delegate.return_per_1000.tao:,.4f}", + # f"τ{delegate.total_daily_return.tao:,.4f}", + ) + + # Clear console and print updated table + console.clear() + console.print(table) + + # Prompt user for input + user_input: str = get_user_input() + + # Add a help option to display information about each column + if user_input == "h": + console.print("\nColumn Information:") + console.print( + "[rgb(253,246,227)]Index:[/rgb(253,246,227)] Position in the list of delegates" + ) + console.print( + "[rgb(211,54,130)]Hotkey SS58:[/rgb(211,54,130)] Truncated public key of the delegate's hotkey" + ) + console.print( + "[rgb(133,153,0)]Owner SS58:[/rgb(133,153,0)] Truncated public key of the delegate's owner" + ) + console.print( + "[rgb(181,137,0)]Take:[/rgb(181,137,0)] Percentage of rewards the delegate takes" + ) + console.print( + "[rgb(38,139,210)]Total Stake:[/rgb(38,139,210)] Total amount staked to this delegate" + ) + console.print( + "[rgb(220,50,47)]Owner Stake:[/rgb(220,50,47)] Amount staked by the delegate owner" + ) + console.print( + "[rgb(108,113,196)]Return per 1000:[/rgb(108,113,196)] Estimated return for 1000 Tao staked" + ) + console.print( + "[rgb(42,161,152)]Total Daily Return:[/rgb(42,161,152)] Estimated total daily return for all stake" + ) + user_input = get_user_input() + + # If user presses Enter, continue to next delegate + if user_input and user_input != "h": + selected_idx = int(user_input) + break + + if idx < len(delegates): + idx += 1 + + return selected_idx + + # TODO( const ): uncomment for check + # Add a confirmation step before returning the selected delegate + # console.print(f"\nSelected delegate: [rgb(211,54,130)]{visible_delegates[selected_idx].hotkey_ss58}[/rgb(211,54,130)]") + # console.print(f"Take: [rgb(181,137,0)]{visible_delegates[selected_idx].take:.6f}[/rgb(181,137,0)]") + # console.print(f"Total Stake: [rgb(38,139,210)]{visible_delegates[selected_idx].total_stake}[/rgb(38,139,210)]") + + # confirmation = Prompt.ask("Do you want to proceed with this delegate? (y/n)") + # if confirmation.lower() != 'yes' and confirmation.lower() != 'y': + # return select_delegate( subtensor, netuid ) + + # Return the selected delegate + while True: + selected_idx_ = loop_selections() + if selected_idx_ is None: + if not rich.prompt.Confirm.ask( + "You've reached the end of the list. You must make a selection. Loop through again?" + ): + raise IndexError + else: + continue + else: + return delegates[selected_idx_] diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 074722403..52d30dd18 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -270,6 +270,7 @@ def prepare_child_proportions(children_with_proportions): async def get_children( wallet: Wallet, subtensor: "SubtensorInterface", netuid: Optional[int] = None ): + # TODO rao asks separately for the hotkey from the user, should we do this, or the way we do it now? """ Retrieves the child hotkeys for the specified wallet. @@ -287,39 +288,7 @@ async def get_children( - If netuid is not specified, generates and prints a summary table of all child hotkeys across all subnets. """ - async def get_total_stake_for_hk(hotkey: str, parent: bool = False): - """ - Fetches and displays the total stake for a specified hotkey from the Subtensor blockchain network. - If `parent` is True, it prints the hotkey and its corresponding stake. - - Parameters: - - hotkey (str): The hotkey for which the stake needs to be fetched. - - parent (bool, optional): A flag to indicate whether the hotkey is the parent key. Defaults to False. - - Returns: - - Balance: The total stake associated with the specified hotkey. - """ - _result = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="TotalHotkeyStake", - params=[hotkey], - reuse_block_hash=True, - ) - stake = ( - Balance.from_rao(_result.value) - if getattr(_result, "value", None) - else Balance(0) - ) - if parent: - console.print( - f"\nYour Hotkey: [bright_magenta]{hotkey}[/bright_magenta] | Total Stake: [dark_orange]{stake}t[/dark_orange]\n", - end="", - no_wrap=True, - ) - - return stake - - async def get_take(child: tuple) -> float: + async def get_take(child: tuple, netuid__: int) -> float: """ Get the take value for a given subtensor, hotkey, and netuid. @@ -330,7 +299,7 @@ async def get_take(child: tuple) -> float: """ child_hotkey = child[1] take_u16 = await get_childkey_take( - subtensor=subtensor, hotkey=child_hotkey, netuid=netuid + subtensor=subtensor, hotkey=child_hotkey, netuid=netuid__ ) if take_u16: return u16_to_float(take_u16) @@ -339,7 +308,7 @@ async def get_take(child: tuple) -> float: async def _render_table( parent_hotkey: str, - netuid_children_tuples: list[tuple[int, list[tuple[int, str]]]], + netuid_children_: list[tuple[int, list[tuple[int, str]]]], ): """ Retrieves and renders children hotkeys and their details for a given parent hotkey. @@ -362,10 +331,11 @@ async def _render_table( "Current Stake Weight", style="bold red", no_wrap=True, justify="right" ) - if not netuid_children_tuples: + if not netuid_children_: console.print(table) console.print( - f"[bold red]There are currently no child hotkeys with parent hotkey: {wallet.name} ({parent_hotkey}).[/bold red]" + f"[bold red]There are currently no child hotkeys with parent hotkey: " + f"{wallet.name} | {wallet.hotkey_str} ({parent_hotkey}).[/bold red]" ) return @@ -373,48 +343,64 @@ async def _render_table( total_proportion = 0 total_stake_weight = 0 - netuid_children_tuples.sort( - key=lambda x: x[0] - ) # Sort by netuid in ascending order + netuid_children_.sort(key=lambda x: x[0]) # Sort by netuid in ascending order + unique_keys = set( + [parent_hotkey] + + [s for _, child_list in netuid_children_ for _, s in child_list] + ) + hotkey_stake_dict = await subtensor.get_total_stake_for_hotkey( + *unique_keys, + netuids=[n[0] for n in netuid_children_], + ) + parent_total = sum(hotkey_stake_dict[parent_hotkey].values()) + insert_text = ( + " " + if netuid is None + else f" on netuids: {', '.join(str(n[0]) for n in netuid_children_)} " + ) + console.print( + f"The total stake of parent hotkey '{parent_hotkey}'{insert_text}is {parent_total}." + ) - for index, (netuid, children_) in enumerate(netuid_children_tuples): + for index, (netuid_, children_) in enumerate(netuid_children_): # calculate totals total_proportion_per_netuid = 0 total_stake_weight_per_netuid = 0 - avg_take_per_netuid = 0 + avg_take_per_netuid = 0.0 - hotkey_stake_dict = await subtensor.get_total_stake_for_hotkey( - parent_hotkey - ) - hotkey_stake = hotkey_stake_dict.get(parent_hotkey, Balance(0)) + hotkey_stake: dict[int, Balance] = hotkey_stake_dict[parent_hotkey] children_info = [] - child_stakes = await asyncio.gather( - *[get_total_stake_for_hk(c[1]) for c in children_] + child_takes = await asyncio.gather( + *[get_take(c, netuid_) for c in children_] ) - child_takes = await asyncio.gather(*[get_take(c) for c in children_]) - for child, child_stake, child_take in zip( - children_, child_stakes, child_takes - ): + for child, child_take in zip(children_, child_takes): proportion = child[0] child_hotkey = child[1] # add to totals avg_take_per_netuid += child_take - proportion = u64_to_float(proportion) + converted_proportion = u64_to_float(proportion) children_info.append( - (proportion, child_hotkey, child_stake, child_take) + ( + converted_proportion, + child_hotkey, + hotkey_stake_dict[child_hotkey][netuid_], + child_take, + ) ) children_info.sort( key=lambda x: x[0], reverse=True ) # sorting by proportion (highest first) - for proportion, hotkey, stake, child_take in children_info: - proportion_percent = proportion * 100 # Proportion in percent - proportion_tao = hotkey_stake.tao * proportion # Proportion in TAO + for proportion_, hotkey, stake, child_take in children_info: + proportion_percent = proportion_ * 100 # Proportion in percent + proportion_tao = ( + hotkey_stake[netuid_].tao * proportion_ + ) # Proportion in TAO total_proportion_per_netuid += proportion_percent @@ -424,9 +410,9 @@ async def _render_table( total_stake_weight_per_netuid += stake_weight take_str = f"{child_take * 100:.3f}%" - hotkey = Text(hotkey, style="italic red" if proportion == 0 else "") + hotkey = Text(hotkey, style="italic red" if proportion_ == 0 else "") table.add_row( - str(netuid), + str(netuid_), hotkey, proportion_str, take_str, @@ -450,7 +436,7 @@ async def _render_table( total_stake_weight += total_stake_weight_per_netuid # Add a dividing line if there are more than one netuid - if len(netuid_children_tuples) > 1: + if len(netuid_children_) > 1: table.add_section() console.print(table) @@ -459,17 +445,16 @@ async def _render_table( if netuid is None: # get all netuids netuids = await subtensor.get_all_subnet_netuids() - await get_total_stake_for_hk(wallet.hotkey.ss58_address, True) netuid_children_tuples = [] - for netuid in netuids: + for netuid_ in netuids: success, children, err_mg = await subtensor.get_children( - wallet.hotkey.ss58_address, netuid + wallet.hotkey.ss58_address, netuid_ ) if children: - netuid_children_tuples.append((netuid, children)) + netuid_children_tuples.append((netuid_, children)) if not success: err_console.print( - f"Failed to get children from subtensor {netuid}: {err_mg}" + f"Failed to get children from subtensor {netuid_}: {err_mg}" ) await _render_table(wallet.hotkey.ss58_address, netuid_children_tuples) else: @@ -478,7 +463,6 @@ async def _render_table( ) if not success: err_console.print(f"Failed to get children from subtensor: {err_mg}") - await get_total_stake_for_hk(wallet.hotkey.ss58_address, True) if children: netuid_children_tuples = [(netuid, children)] await _render_table(wallet.hotkey.ss58_address, netuid_children_tuples) @@ -491,12 +475,15 @@ async def set_children( subtensor: "SubtensorInterface", children: list[str], proportions: list[float], - netuid: Optional[int] = None, + netuid: Optional[int], wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + prompt: bool = True, ): """Set children hotkeys.""" # Validate children SS58 addresses + # TODO check to see if this should be allowed to be specified by user instead of pulling from wallet + hotkey = wallet.hotkey.ss58_address for child in children: if not is_valid_ss58_address(child): err_console.print(f":cross_mark:[red] Invalid SS58 address: {child}[/red]") @@ -511,16 +498,15 @@ async def set_children( f"Invalid proportion: The sum of all proportions cannot be greater than 1. " f"Proposed sum of proportions is {total_proposed}." ) - children_with_proportions = list(zip(proportions, children)) - if netuid: + if netuid is not None: success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, - hotkey=wallet.hotkey.ss58_address, + hotkey=hotkey, children_with_proportions=children_with_proportions, - prompt=True, + prompt=prompt, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) @@ -547,9 +533,9 @@ async def set_children( subtensor=subtensor, wallet=wallet, netuid=netuid, - hotkey=wallet.hotkey.ss58_address, + hotkey=hotkey, children_with_proportions=children_with_proportions, - prompt=False, + prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, ) @@ -565,6 +551,7 @@ async def revoke_children( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, ): + # TODO seek clarification on use of asking hotkey vs how we do it now """ Revokes the children hotkeys associated with a given network identifier (netuid). """ diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 819f02cca..b22a7513a 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -1,33 +1,33 @@ import asyncio -import copy -import json -import sqlite3 -from contextlib import suppress +from functools import partial from typing import TYPE_CHECKING, Optional, Sequence, Union, cast +import typer from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError -from rich.prompt import Confirm -from rich.table import Table, Column -import typer - - +from rich.prompt import Confirm, FloatPrompt, Prompt +from rich.table import Table +from rich import box +from rich.progress import Progress, BarColumn, TextColumn +from rich.console import Console, Group +from rich.live import Live +from substrateinterface.exceptions import SubstrateRequestException + +from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance +from bittensor_cli.src.bittensor.chain_data import StakeInfo from bittensor_cli.src.bittensor.utils import ( + # TODO add back in caching console, - create_table, err_console, print_verbose, print_error, - get_coldkey_wallets_for_path, get_hotkey_wallets_for_wallet, is_valid_ss58_address, - get_metadata_table, - update_metadata_table, - render_tree, u16_normalized_float, - validate_coldkey_presence, + format_error_message, + group_subnets, ) if TYPE_CHECKING: @@ -825,355 +825,70 @@ async def unstake_multiple_extrinsic( # Commands - - -async def show( - wallet: Wallet, - subtensor: Optional["SubtensorInterface"], - all_wallets: bool, - reuse_last: bool, - html_output: bool, - no_cache: bool, -): - """Show all stake accounts.""" - - async def get_stake_accounts( - wallet_, block_hash: str - ) -> dict[str, Union[str, Balance, dict[str, Union[str, Balance]]]]: - """Get stake account details for the given wallet. - - :param wallet_: The wallet object to fetch the stake account details for. - - :return: A dictionary mapping SS58 addresses to their respective stake account details. - """ - - wallet_stake_accounts = {} - - # Get this wallet's coldkey balance. - cold_balance_, stakes_from_hk, stakes_from_d = await asyncio.gather( - subtensor.get_balance( - wallet_.coldkeypub.ss58_address, block_hash=block_hash - ), - get_stakes_from_hotkeys(wallet_, block_hash=block_hash), - get_stakes_from_delegates(wallet_), - ) - - cold_balance = cold_balance_[wallet_.coldkeypub.ss58_address] - - # Populate the stake accounts with local hotkeys data. - wallet_stake_accounts.update(stakes_from_hk) - - # Populate the stake accounts with delegations data. - wallet_stake_accounts.update(stakes_from_d) - - return { - "name": wallet_.name, - "balance": cold_balance, - "accounts": wallet_stake_accounts, - } - - async def get_stakes_from_hotkeys( - wallet_, block_hash: str - ) -> dict[str, dict[str, Union[str, Balance]]]: - """Fetch stakes from hotkeys for the provided wallet. - - :param wallet_: The wallet object to fetch the stakes for. - - :return: A dictionary of stakes related to hotkeys. - """ - - async def get_all_neurons_for_pubkey(hk): - netuids = await subtensor.get_netuids_for_hotkey(hk, block_hash=block_hash) - uid_query = await asyncio.gather( - *[ - subtensor.substrate.query( - module="SubtensorModule", - storage_function="Uids", - params=[netuid, hk], - block_hash=block_hash, - ) - for netuid in netuids - ] - ) - uids = [_result for _result in uid_query] - neurons = await asyncio.gather( - *[ - subtensor.neuron_for_uid(uid, net) - for (uid, net) in zip(uids, netuids) - ] - ) - return neurons - - async def get_emissions_and_stake(hk: str): - neurons, stake = await asyncio.gather( - get_all_neurons_for_pubkey(hk), - subtensor.substrate.query( - module="SubtensorModule", - storage_function="Stake", - params=[hk, wallet_.coldkeypub.ss58_address], - block_hash=block_hash, - ), - ) - emission_ = sum([n.emission for n in neurons]) if neurons else 0.0 - return emission_, Balance.from_rao(stake) if stake else Balance(0) - - hotkeys = cast(list[Wallet], get_hotkey_wallets_for_wallet(wallet_)) - stakes = {} - query = await asyncio.gather( - *[get_emissions_and_stake(hot.hotkey.ss58_address) for hot in hotkeys] - ) - for hot, (emission, hotkey_stake) in zip(hotkeys, query): - stakes[hot.hotkey.ss58_address] = { - "name": hot.hotkey_str, - "stake": hotkey_stake, - "rate": emission, - } - return stakes - - async def get_stakes_from_delegates( - wallet_, - ) -> dict[str, dict[str, Union[str, Balance]]]: - """Fetch stakes from delegates for the provided wallet. - - :param wallet_: The wallet object to fetch the stakes for. - - :return: A dictionary of stakes related to delegates. - """ - delegates = await subtensor.get_delegated( - coldkey_ss58=wallet_.coldkeypub.ss58_address, block_hash=None - ) - stakes = {} - for dele, staked in delegates: - for nom in dele.nominators: - if nom[0] == wallet_.coldkeypub.ss58_address: - delegate_name = ( - registered_delegate_info[dele.hotkey_ss58].display - if dele.hotkey_ss58 in registered_delegate_info - else None - ) - stakes[dele.hotkey_ss58] = { - "name": delegate_name if delegate_name else dele.hotkey_ss58, - "stake": nom[1], - "rate": dele.total_daily_return.tao - * (nom[1] / dele.total_stake.tao), - } - return stakes - - async def get_all_wallet_accounts( - block_hash: str, - ) -> list[dict[str, Union[str, Balance, dict[str, Union[str, Balance]]]]]: - """Fetch stake accounts for all provided wallets using a ThreadPool. - - :param block_hash: The block hash to fetch the stake accounts for. - - :return: A list of dictionaries, each dictionary containing stake account details for each wallet. - """ - - accounts_ = await asyncio.gather( - *[get_stake_accounts(w, block_hash=block_hash) for w in wallets] - ) - return accounts_ - - if not reuse_last: - cast("SubtensorInterface", subtensor) - if all_wallets: - wallets = get_coldkey_wallets_for_path(wallet.path) - valid_wallets, invalid_wallets = validate_coldkey_presence(wallets) - wallets = valid_wallets - for invalid_wallet in invalid_wallets: - print_error(f"No coldkeypub found for wallet: ({invalid_wallet.name})") - else: - wallets = [wallet] - - with console.status( - ":satellite: Retrieving account data...", spinner="aesthetic" - ): - block_hash_ = await subtensor.substrate.get_chain_head() - registered_delegate_info = await subtensor.get_delegate_identities( - block_hash=block_hash_ - ) - accounts = await get_all_wallet_accounts(block_hash=block_hash_) - - total_stake: float = 0.0 - total_balance: float = 0.0 - total_rate: float = 0.0 - rows = [] - db_rows = [] - for acc in accounts: - cast(str, acc["name"]) - cast(Balance, acc["balance"]) - rows.append([acc["name"], str(acc["balance"]), "", "", ""]) - db_rows.append( - [acc["name"], float(acc["balance"]), None, None, None, None, 0] - ) - total_balance += cast(Balance, acc["balance"]).tao - for key, value in cast(dict, acc["accounts"]).items(): - if value["name"] and value["name"] != key: - account_display_name = f"{value['name']}" - else: - account_display_name = "(~)" - rows.append( - [ - "", - "", - account_display_name, - key, - str(value["stake"]), - str(value["rate"]), - ] - ) - db_rows.append( - [ - acc["name"], - None, - value["name"], - float(value["stake"]), - float(value["rate"]), - key, - 1, - ] - ) - total_stake += cast(Balance, value["stake"]).tao - total_rate += float(value["rate"]) - metadata = { - "total_stake": "\u03c4{:.5f}".format(total_stake), - "total_balance": "\u03c4{:.5f}".format(total_balance), - "total_rate": "\u03c4{:.5f}/d".format(total_rate), - "rows": json.dumps(rows), - } - if not no_cache: - create_table( - "stakeshow", - [ - ("COLDKEY", "TEXT"), - ("BALANCE", "REAL"), - ("ACCOUNT", "TEXT"), - ("STAKE", "REAL"), - ("RATE", "REAL"), - ("HOTKEY", "TEXT"), - ("CHILD", "INTEGER"), - ], - db_rows, - ) - update_metadata_table("stakeshow", metadata) - else: - try: - metadata = get_metadata_table("stakeshow") - rows = json.loads(metadata["rows"]) - except sqlite3.OperationalError: - err_console.print( - "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use " - "`--reuse-last` before running the command a first time. In rare cases, this could also be due to " - "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your " - "issue." - ) - return - if not html_output: - table = Table( - Column("[bold white]Coldkey", style="dark_orange", ratio=1), - Column( - "[bold white]Balance", - metadata["total_balance"], - style="dark_sea_green", - ratio=1, - ), - Column("[bold white]Account", style="bright_cyan", ratio=3), - Column("[bold white]Hotkey", ratio=7, no_wrap=True, style="bright_magenta"), - Column( - "[bold white]Stake", - metadata["total_stake"], - style="light_goldenrod2", - ratio=1, - ), - Column( - "[bold white]Rate /d", - metadata["total_rate"], - style="rgb(42,161,152)", - ratio=1, - ), - title=f"[underline dark_orange]Stake Show[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", - show_footer=True, - show_edge=False, - expand=False, - border_style="bright_black", - ) - - for i, row in enumerate(rows): - is_last_row = i + 1 == len(rows) - table.add_row(*row) - - # If last row or new coldkey starting next - if is_last_row or (rows[i + 1][0] != ""): - table.add_row(end_section=True) - console.print(table) - - else: - render_tree( - "stakeshow", - f"Stakes | Total Balance: {metadata['total_balance']} - Total Stake: {metadata['total_stake']} " - f"Total Rate: {metadata['total_rate']}", - [ - {"title": "Coldkey", "field": "COLDKEY"}, - { - "title": "Balance", - "field": "BALANCE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Account", - "field": "ACCOUNT", - "width": 425, - }, - { - "title": "Stake", - "field": "STAKE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Daily Rate", - "field": "RATE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Hotkey", - "field": "HOTKEY", - "width": 425, - }, - ], - 0, - ) - - async def stake_add( wallet: Wallet, subtensor: "SubtensorInterface", - amount: float, + netuid: Optional[int], stake_all: bool, + amount: float, + delegate: bool, + prompt: bool, max_stake: float, + all_hotkeys: bool, include_hotkeys: list[str], exclude_hotkeys: list[str], - all_hotkeys: bool, - prompt: bool, - hotkey_ss58: Optional[str] = None, -) -> None: - """Stake token of amount to hotkey(s).""" +): + """ + + Args: + wallet: wallet object + subtensor: SubtensorInterface object + netuid: the netuid to stake to (None indicates all subnets) + stake_all: whether to stake all available balance + amount: specified amount of balance to stake + delegate: whether to delegate stake, currently unused + prompt: whether to prompt the user + max_stake: maximum amount to stake (used in combination with stake_all), currently unused + all_hotkeys: whether to stake all hotkeys + include_hotkeys: list of hotkeys to include in staking process (if not specifying `--all`) + exclude_hotkeys: list of hotkeys to exclude in staking (if specifying `--all`) + + Returns: + + """ + netuids = ( + [int(netuid)] + if netuid is not None + else await subtensor.get_all_subnet_netuids() + ) + # Init the table. + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\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, + ) - async def is_hotkey_registered_any(hk: str, bh: str) -> bool: - return len(await subtensor.get_netuids_for_hotkey(hk, bh)) > 0 + # Determine the amount we are staking. + rows = [] + stake_amount_balance = [] + current_stake_balances = [] + current_wallet_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + current_wallet_balance = current_wallet_balance_[ + wallet.coldkeypub.ss58_address + ].set_unit(0) + remaining_wallet_balance = current_wallet_balance + max_slippage = 0.0 - # Get the hotkey_names (if any) and the hotkey_ss58s. hotkeys_to_stake_to: list[tuple[Optional[str], str]] = [] - if hotkey_ss58: - if not is_valid_ss58_address(hotkey_ss58): - print_error("The entered ss58 address is incorrect") - typer.Exit() - - # Stake to specific hotkey. - hotkeys_to_stake_to = [(None, hotkey_ss58)] - elif all_hotkeys: + if all_hotkeys: # Stake to all hotkeys. all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) # Get the hotkeys to exclude. (d)efault to no exclusions. @@ -1212,247 +927,1850 @@ async def is_hotkey_registered_any(hk: str, bh: str) -> bool: hotkey_ss58_or_name = wallet.hotkey.ss58_address hotkeys_to_stake_to = [(None, hotkey_ss58_or_name)] - try: - # Get coldkey balance - print_verbose("Fetching coldkey balance") - wallet_balance_: dict[str, Balance] = await subtensor.get_balance( - wallet.coldkeypub.ss58_address - ) - block_hash = subtensor.substrate.last_block_hash - wallet_balance: Balance = wallet_balance_[wallet.coldkeypub.ss58_address] - old_balance = copy.copy(wallet_balance) - final_hotkeys: list[tuple[Optional[str], str]] = [] - final_amounts: list[Union[float, Balance]] = [] - hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - - print_verbose("Checking if hotkeys are registered") - registered_ = asyncio.gather( - *[is_hotkey_registered_any(h[1], block_hash) for h in hotkeys_to_stake_to] - ) - if max_stake: - hotkey_stakes_ = asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=h[1], - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=block_hash, - ) - for h in hotkeys_to_stake_to - ] + starting_chain_head = await subtensor.substrate.get_chain_head() + all_dynamic_info, initial_stake_balances = await asyncio.gather( + asyncio.gather( + *[ + subtensor.get_subnet_dynamic_info(x, starting_chain_head) + for x in netuids + ] + ), + subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( + hotkey_ss58s=[x[1] for x in hotkeys_to_stake_to], + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuids=netuids, + block_hash=starting_chain_head, + ), + ) + for hk_name, hk_ss58 in hotkeys_to_stake_to: + if not is_valid_ss58_address(hk_ss58): + print_error( + f"The entered hotkey ss58 address is incorrect: {hk_name} | {hk_ss58}" ) - else: - - async def null(): - return [None] * len(hotkeys_to_stake_to) - - hotkey_stakes_ = null() - registered: list[bool] - hotkey_stakes: list[Optional[Balance]] - registered, hotkey_stakes = await asyncio.gather(registered_, hotkey_stakes_) - - for hotkey, reg, hotkey_stake in zip( - hotkeys_to_stake_to, registered, hotkey_stakes - ): - if not reg: - # Hotkey is not registered. - if len(hotkeys_to_stake_to) == 1: - # Only one hotkey, error - err_console.print( - f"[red]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Aborting.[/red]" - ) - raise ValueError + return False + for hotkey in hotkeys_to_stake_to: + for netuid, dynamic_info in zip(netuids, all_dynamic_info): + # Check that the subnet exists. + if not dynamic_info: + err_console.print(f"Subnet with netuid: {netuid} does not exist.") + continue + current_stake_balances.append(initial_stake_balances[hotkey[1]][netuid]) + + # Get the amount. + amount_to_stake_as_balance = Balance(0) + if amount: + amount_to_stake_as_balance = Balance.from_tao(amount) + elif stake_all: + amount_to_stake_as_balance = current_wallet_balance / len(netuids) + elif not amount and not max_stake: + if Confirm.ask(f"Stake all: [bold]{remaining_wallet_balance}[/bold]?"): + amount_to_stake_as_balance = remaining_wallet_balance else: - # Otherwise, print warning and skip - console.print( - f"[yellow]Hotkey [bold]{hotkey[1]}[/bold] is not registered. Skipping.[/yellow]" - ) - continue - - stake_amount_tao: float = amount - if max_stake: - stake_amount_tao = max_stake - hotkey_stake.tao - - # If the max_stake is greater than the current wallet balance, stake the entire balance. - stake_amount_tao = min(stake_amount_tao, wallet_balance.tao) - if ( - stake_amount_tao <= 0.00001 - ): # Threshold because of fees, might create a loop otherwise - # Skip hotkey if max_stake is less than current stake. - continue - wallet_balance = Balance.from_tao(wallet_balance.tao - stake_amount_tao) - - if wallet_balance.tao < 0: - # No more balance to stake. - break + try: + amount = FloatPrompt.ask( + f"Enter amount to stake in {Balance.get_unit(0)} to subnet: {netuid}" + ) + amount_to_stake_as_balance = Balance.from_tao(amount) + except ValueError: + err_console.print( + f":cross_mark:[red]Invalid amount: {amount}[/red]" + ) + return False + stake_amount_balance.append(amount_to_stake_as_balance) + + # Check enough to stake. + amount_to_stake_as_balance.set_unit(0) + if amount_to_stake_as_balance > remaining_wallet_balance: + err_console.print( + f"[red]Not enough stake[/red]:[bold white]\n wallet balance:{remaining_wallet_balance} < " + f"staking amount: {amount_to_stake_as_balance}[/bold white]" + ) + return False + remaining_wallet_balance -= amount_to_stake_as_balance - final_amounts.append(stake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. + # Slippage warning + received_amount, slippage = dynamic_info.tao_to_alpha_with_slippage( + amount_to_stake_as_balance + ) + if dynamic_info.is_dynamic: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount) + if slippage + received_amount != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]N/A[/{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]" + max_slippage = max(slippage_pct_float, max_slippage) + rows.append( + ( + str(netuid), + # f"{staking_address_ss58[:3]}...{staking_address_ss58[-3:]}", + f"{hotkey[1]}", + str(amount_to_stake_as_balance), + str(1 / (float(dynamic_info.price) or 1)) + + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", + str(received_amount.set_unit(netuid)), + str(slippage_pct), + ) + ) + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], + ) + table.add_column( + f"Rate (per {Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + "Received", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) + for row in rows: + table.add_row(*row) + console.print(table) + message = "" + if max_slippage > 5: + message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + console.print( + """ +[bold white]Description[/bold white]: +The table displays information about the stake operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are staking to. + - [bold white]Hotkey[/bold white]: The ss58 address of the hotkey you are staking to. + - [bold white]Amount[/bold white]: The TAO you are staking into this subnet onto this hotkey. + - [bold white]Rate[/bold white]: The rate of exchange between your TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of stake you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the stake operation. (0% if the subnet is not dynamic i.e. root). +""" + ) + if prompt: + if not Confirm.ask("Would you like to continue?"): + raise typer.Exit() - if len(final_hotkeys) == 0: - # No hotkeys to stake to. - err_console.print( - "Not enough balance to stake to any hotkeys or max_stake is less than current stake." + async def send_extrinsic( + netuid_i, amount_, current, staking_address_ss58, status=None + ): + err_out = partial(print_error, status=status) + failure_prelude = ( + f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" + ) + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_staked": amount_.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + try: + response = await subtensor.substrate.submit_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, subtensor.substrate)}" ) - raise ValueError - - if len(final_hotkeys) == 1: - # do regular stake - await add_stake_extrinsic( - subtensor, - wallet=wallet, - old_balance=old_balance, - hotkey_ss58=final_hotkeys[0][1], - amount=None if stake_all else final_amounts[0], - wait_for_inclusion=True, - prompt=prompt, + return + if not prompt: # TODO verbose? + console.print( + f":white_heavy_check_mark: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Submitted {amount_} to {netuid_i}[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) else: - await add_stake_multiple_extrinsic( - subtensor, - wallet=wallet, - old_balance=old_balance, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if stake_all else final_amounts, - wait_for_inclusion=True, - prompt=prompt, + 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)}" + ) + else: + new_balance_, new_stake_ = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=staking_address_ss58, + netuid=netuid_i, + ), + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_stake = new_stake_.set_unit(netuid_i) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] Stake:\n [blue]{current}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) + + # Perform staking operation. + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + extrinsics_coroutines = [ + send_extrinsic(ni, am, curr, staking_address) + for i, (ni, am, curr) in enumerate( + zip(netuids, stake_amount_balance, current_stake_balances) + ) + for _, staking_address in hotkeys_to_stake_to + ] + if len(extrinsics_coroutines) == 1: + with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): + await extrinsics_coroutines[0] + else: + with console.status(":satellite: Checking transaction rate limit ..."): + tx_rate_limit_blocks = await subtensor.substrate.query( + module="SubtensorModule", storage_function="TxRateLimit" ) - except ValueError: - pass + netuid_hk_pairs = [(ni, hk) for ni in netuids for hk in hotkeys_to_stake_to] + for item, kp in zip(extrinsics_coroutines, netuid_hk_pairs): + ni, hk = kp + with console.status( + f"\n:satellite: Staking on netuid {ni} with hotkey {hk}... ..." + ): + await item + if tx_rate_limit_blocks > 0: + with console.status( + f":hourglass: [yellow]Waiting for tx rate limit:" + f" [white]{tx_rate_limit_blocks}[/white] blocks[/yellow]" + ): + await asyncio.sleep(tx_rate_limit_blocks * 12) # 12 sec per block -async def unstake( - wallet: Wallet, +async def unstake_selection( subtensor: "SubtensorInterface", - hotkey_ss58_address: str, - all_hotkeys: bool, - include_hotkeys: list[str], - exclude_hotkeys: list[str], - amount: float, - keep_stake: float, - unstake_all: bool, - prompt: bool, + wallet: Wallet, + dynamic_info, + identities, + old_identities, + netuid: Optional[int] = None, ): - """Unstake token of amount from hotkey(s).""" - - # Get the hotkey_names (if any) and the hotkey_ss58s. - hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] - if hotkey_ss58_address: - print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})") - # Unstake to specific hotkey. - hotkeys_to_unstake_from = [(None, hotkey_ss58_address)] - elif all_hotkeys: - print_verbose("Unstaking from all hotkeys") - # Unstake to all hotkeys. - all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) - # Exclude hotkeys that are specified. - hotkeys_to_unstake_from = [ - (wallet.hotkey_str, wallet.hotkey.ss58_address) - for wallet in all_hotkeys_ - if wallet.hotkey_str not in exclude_hotkeys - ] # definitely wallets + stake_infos = await subtensor.get_stake_info_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) - elif include_hotkeys: - print_verbose("Unstaking from included hotkeys") - # Unstake to specific hotkeys. - for hotkey_ss58_or_hotkey_name in include_hotkeys: - if is_valid_ss58_address(hotkey_ss58_or_hotkey_name): - # If the hotkey is a valid ss58 address, we add it to the list. - hotkeys_to_unstake_from.append((None, hotkey_ss58_or_hotkey_name)) - else: - # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. - # We then get the hotkey from the wallet and add it to the list. - wallet_ = Wallet( - name=wallet.name, - path=wallet.path, - hotkey=hotkey_ss58_or_hotkey_name, - ) - hotkeys_to_unstake_from.append( - (wallet_.hotkey_str, wallet_.hotkey.ss58_address) - ) - else: - # Only cli.config.wallet.hotkey is specified. - # so we stake to that single hotkey. - print_verbose( - f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" - ) - assert wallet.hotkey is not None - hotkeys_to_unstake_from = [(None, wallet.hotkey.ss58_address)] + if not stake_infos: + print_error("You have no stakes to unstake.") + return - final_hotkeys: list[tuple[str, str]] = [] - final_amounts: list[Union[float, Balance]] = [] - hotkey: tuple[Optional[str], str] # (hotkey_name (or None), hotkey_ss58) - with suppress(ValueError): - with console.status( - f":satellite:Syncing with chain {subtensor}", spinner="earth" - ) as status: - print_verbose("Fetching stake", status) - block_hash = await subtensor.substrate.get_chain_head() - hotkey_stakes = await asyncio.gather( - *[ - subtensor.get_stake_for_coldkey_and_hotkey( - hotkey_ss58=hotkey[1], - coldkey_ss58=wallet.coldkeypub.ss58_address, - block_hash=block_hash, - ) - for hotkey in hotkeys_to_unstake_from - ] + hotkey_stakes = {} + for stake_info in stake_infos: + if netuid is not None and stake_info.netuid != netuid: + continue + hotkey_ss58 = stake_info.hotkey_ss58 + netuid_ = stake_info.netuid + stake_amount = stake_info.stake + if stake_amount.tao > 0: + hotkey_stakes.setdefault(hotkey_ss58, {})[netuid_] = stake_amount + + if not hotkey_stakes: + if netuid is not None: + print_error(f"You have no stakes to unstake in subnet {netuid}.") + else: + print_error("You have no stakes to unstake.") + return + + hotkeys_info = [] + for idx, (hotkey_ss58, netuid_stakes) in enumerate(hotkey_stakes.items()): + identity = identities["hotkeys"].get(hotkey_ss58) or old_identities.get( + hotkey_ss58 + ) + hotkey_name = "~" + if identity: + hotkey_name = identity.get("identity", {}).get("name", "") or identity.get( + "display", "~" ) - for hotkey, hotkey_stake in zip(hotkeys_to_unstake_from, hotkey_stakes): - unstake_amount_tao: float = amount - - if unstake_all: - unstake_amount_tao = hotkey_stake.tao - if keep_stake: - # Get the current stake of the hotkey from this coldkey. - unstake_amount_tao = hotkey_stake.tao - keep_stake - amount = unstake_amount_tao - if unstake_amount_tao < 0: - # Skip if max_stake is greater than current stake. - continue - else: - if unstake_amount_tao > hotkey_stake.tao: - # Skip if the specified amount is greater than the current stake. - continue + # TODO: Add wallet ids here. + + hotkeys_info.append( + { + "index": idx, + "identity": hotkey_name, + "netuids": list(netuid_stakes.keys()), + "hotkey_ss58": hotkey_ss58, + } + ) - final_amounts.append(unstake_amount_tao) - final_hotkeys.append(hotkey) # add both the name and the ss58 address. + # Display existing hotkeys, id, and staked netuids. + subnet_filter = f" for Subnet {netuid}" if netuid is not None else "" + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkeys with Stakes{subnet_filter}\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, + ) + table.add_column("Index", justify="right") + table.add_column("Identity", style=COLOR_PALETTE["GENERAL"]["SUBHEADING"]) + table.add_column("Netuids", style=COLOR_PALETTE["GENERAL"]["NETUID"]) + table.add_column("Hotkey Address", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]) + + for hotkey_info in hotkeys_info: + index = str(hotkey_info["index"]) + identity = hotkey_info["identity"] + netuids = group_subnets([n for n in hotkey_info["netuids"]]) + hotkey_ss58 = hotkey_info["hotkey_ss58"] + table.add_row(index, identity, netuids, hotkey_ss58) + + console.print("\n", table) + + # Prompt to select hotkey to unstake. + hotkey_options = [str(hotkey_info["index"]) for hotkey_info in hotkeys_info] + hotkey_idx = Prompt.ask( + "\nEnter the index of the hotkey you want to unstake from", + choices=hotkey_options, + ) + selected_hotkey_info = hotkeys_info[int(hotkey_idx)] + selected_hotkey_ss58 = selected_hotkey_info["hotkey_ss58"] + selected_hotkey_name = selected_hotkey_info["identity"] + netuid_stakes = hotkey_stakes[selected_hotkey_ss58] + + # Display hotkey's staked netuids with amount. + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Stakes for hotkey \n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey_name}\n{selected_hotkey_ss58}\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, + ) + table.add_column("Subnet", justify="right") + table.add_column("Symbol", style=COLOR_PALETTE["GENERAL"]["SYMBOL"]) + table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"]) + table.add_column( + f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + style=COLOR_PALETTE["POOLS"]["RATE"], + justify="left", + ) - if len(final_hotkeys) == 0: - # No hotkeys to unstake from. - err_console.print( - "Not enough stake to unstake from any hotkeys or max_stake is more than current stake." + for netuid_, stake_amount in netuid_stakes.items(): + symbol = dynamic_info[netuid_].symbol + rate = f"{dynamic_info[netuid_].price.tao:.4f} τ/{symbol}" + table.add_row(str(netuid_), symbol, str(stake_amount), rate) + console.print("\n", table, "\n") + + # Ask which netuids to unstake from for the selected hotkey. + if netuid is not None: + selected_netuids = [netuid] + else: + while True: + netuid_input = Prompt.ask( + "\nEnter the netuids of the [blue]subnets to unstake[/blue] from (comma-separated), or '[blue]all[/blue]' to unstake from all", + default="all", ) - return None - # Ask to unstake - if prompt: - if not Confirm.ask( - f"Do you want to unstake from the following keys to {wallet.name}:\n" - + "".join( - [ - f" [bold white]- {hotkey[0] + ':' if hotkey[0] else ''}{hotkey[1]}: " - f"{f'{amount} {Balance.unit}' if amount else 'All'}[/bold white]\n" - for hotkey, amount in zip(final_hotkeys, final_amounts) - ] + if netuid_input.lower() == "all": + selected_netuids = list(netuid_stakes.keys()) + break + else: + try: + netuid_list = [int(n.strip()) for n in netuid_input.split(",")] + invalid_netuids = [n for n in netuid_list if n not in netuid_stakes] + if invalid_netuids: + print_error( + f"The following netuids are invalid or not available: {', '.join(map(str, invalid_netuids))}. Please try again." + ) + else: + selected_netuids = netuid_list + break + except ValueError: + print_error( + "Please enter valid netuids (numbers), separated by commas, or 'all'." + ) + + hotkeys_to_unstake_from = [] + for netuid_ in selected_netuids: + hotkeys_to_unstake_from.append( + (selected_hotkey_name, selected_hotkey_ss58, netuid_) + ) + return hotkeys_to_unstake_from + + +def ask_unstake_amount( + current_stake_balance: Balance, + netuid: int, + staking_address_name: str, + staking_address_ss58: str, + interactive: bool, +) -> Optional[Balance]: + """Prompt the user to decide the amount to unstake.""" + while True: + response = Prompt.ask( + f"Unstake all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" from [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{staking_address_name if staking_address_name else staking_address_ss58}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" on netuid: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]? [y/n/q]", + choices=["y", "n", "q"], + default="n" if interactive else "y", + show_choices=True, + ).lower() + + if response == "q": + return None # Quit + + elif response == "y": + return current_stake_balance + + elif response == "n": + while True: + amount_input = Prompt.ask( + f"Enter amount to unstake in [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" from subnet: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{netuid}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" (Max: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}])" ) - ): - return None - if len(final_hotkeys) == 1: - # do regular unstake - await unstake_extrinsic( - subtensor, - wallet=wallet, - hotkey_ss58=final_hotkeys[0][1], - amount=None if unstake_all else final_amounts[0], - wait_for_inclusion=True, - prompt=prompt, - ) + if amount_input.lower() == "q": + return None # Quit + + try: + amount_value = float(amount_input) + if amount_value <= 0: + console.print("[red]Amount must be greater than zero.[/red]") + continue # Re-prompt + + amount_to_unstake = Balance.from_tao(amount_value) + amount_to_unstake.set_unit(netuid) + if amount_to_unstake > current_stake_balance: + console.print( + f"[red]Amount exceeds current stake balance of {current_stake_balance}.[/red]" + ) + continue # Re-prompt + + return amount_to_unstake + + except ValueError: + console.print( + "[red]Invalid input. Please enter a numeric value or 'q' to quit.[/red]" + ) + else: - await unstake_multiple_extrinsic( - subtensor, - wallet=wallet, - hotkey_ss58s=[hotkey_ss58 for _, hotkey_ss58 in final_hotkeys], - amounts=None if unstake_all else final_amounts, - wait_for_inclusion=True, - prompt=prompt, - ) + console.print("[red]Invalid input. Please enter 'y', 'n', or 'q'.[/red]") + + +async def _unstake_all( + wallet: Wallet, + subtensor: "SubtensorInterface", + unstake_all_alpha: bool = False, + prompt: bool = True, +) -> bool: + """Unstakes all stakes from all hotkeys in all subnets.""" + + with console.status( + f"Retrieving stake information & identities from {subtensor.network}...", + spinner="earth", + ): + ( + stake_info, + ck_hk_identities, + old_identities, + all_sn_dynamic_info_, + current_wallet_balance, + ) = await asyncio.gather( + subtensor.get_stake_info_for_coldkey(wallet.coldkeypub.ss58_address), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + subtensor.get_all_subnet_dynamic_info(), + subtensor.get_balance(wallet.coldkeypub.ss58_address), + ) + + if unstake_all_alpha: + stake_info = [stake for stake in stake_info if stake.netuid != 0] + + if not stake_info: + console.print("[red]No stakes found to unstake[/red]") + return False + + all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + + # Calculate total value and slippage for all stakes + total_received_value = Balance(0) + table_title = ( + "Unstaking Summary - All Stakes" + if not unstake_all_alpha + else "Unstaking Summary - All Alpha Stakes" + ) + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]{table_title}\nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\nNetwork: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\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, + ) + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Current Stake ({Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + ) + table.add_column( + f"Rate ({Balance.unit}/{Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + f"Recieved ({Balance.unit})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "Slippage", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) + max_slippage = 0.0 + for stake in stake_info: + if stake.stake.rao == 0: + continue + + dynamic_info = all_sn_dynamic_info.get(stake.netuid) + stake_amount = stake.stake + received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( + stake_amount + ) + + total_received_value += received_amount + + # Get hotkey identity + identity = ck_hk_identities["hotkeys"].get( + stake.hotkey_ss58 + ) or old_identities.get(stake.hotkey_ss58) + hotkey_display = stake.hotkey_ss58 + if identity: + hotkey_name = identity.get("identity", {}).get( + "name", "" + ) or identity.get("display", "~") + hotkey_display = f"{hotkey_name}" + + if dynamic_info.is_dynamic: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount) + if slippage + received_amount != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = "[red]N/A[/red]" + + max_slippage = max(max_slippage, slippage_pct_float) + + table.add_row( + str(stake.netuid), + hotkey_display, + str(stake_amount), + str(float(dynamic_info.price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})", + str(received_amount), + slippage_pct, + ) + console.print(table) + message = "" + if max_slippage > 5: + message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + + console.print( + f"Expected return after slippage: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{total_received_value}" + ) + + if prompt and not Confirm.ask( + "\nDo you want to proceed with unstaking everything?" + ): + return False + + try: + wallet.unlock_coldkey() + except KeyFileError: + 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..." + ) + 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={}, + ) + 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) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + console.print( + f"Balance:\n [blue]{current_wallet_balance[wallet.coldkeypub.ss58_address]}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + return True + else: + err_console.print( + f":cross_mark: [red]Failed to unstake[/red]: {error_message}" + ) + return False + + +async def unstake( + wallet: Wallet, + subtensor: "SubtensorInterface", + hotkey_ss58_address: str, + all_hotkeys: bool, + include_hotkeys: list[str], + exclude_hotkeys: list[str], + amount: float, + keep_stake: float, + unstake_all: bool, + prompt: bool, + interactive: bool = False, + netuid: Optional[int] = None, + unstake_all_alpha: bool = False, +): + """Unstake tokens from hotkey(s).""" + + if unstake_all or unstake_all_alpha: + return await _unstake_all(wallet, subtensor, unstake_all_alpha, prompt) + + with console.status( + f"Retrieving subnet data & identities from {subtensor.network}...", + spinner="earth", + ): + all_sn_dynamic_info_, ck_hk_identities, old_identities = await asyncio.gather( + subtensor.get_all_subnet_dynamic_info(), + subtensor.fetch_coldkey_hotkey_identities(), + subtensor.get_delegate_identities(), + ) + all_sn_dynamic_info = {info.netuid: info for info in all_sn_dynamic_info_} + + if interactive: + hotkeys_to_unstake_from = await unstake_selection( + subtensor, + wallet, + all_sn_dynamic_info, + ck_hk_identities, + old_identities, + netuid=netuid, + ) + if not hotkeys_to_unstake_from: + console.print("[red]No unstake operations to perform.[/red]") + return False + netuids = list({netuid for _, _, netuid in hotkeys_to_unstake_from}) + + else: + netuids = ( + [int(netuid)] + if netuid is not None + else await subtensor.get_all_subnet_netuids() + ) + + # Get the hotkey_names (if any) and the hotkey_ss58s. + hotkeys_to_unstake_from: list[tuple[Optional[str], str]] = [] + if hotkey_ss58_address: + print_verbose(f"Unstaking from ss58 ({hotkey_ss58_address})") + # Unstake from specific hotkey. + hotkeys_to_unstake_from = [(None, hotkey_ss58_address)] + elif all_hotkeys: + print_verbose("Unstaking from all hotkeys") + # Unstake from all hotkeys. + all_hotkeys_: list[Wallet] = get_hotkey_wallets_for_wallet(wallet=wallet) + # Exclude hotkeys that are specified. + hotkeys_to_unstake_from = [ + (wallet.hotkey_str, wallet.hotkey.ss58_address) + for wallet in all_hotkeys_ + if wallet.hotkey_str not in exclude_hotkeys + ] + elif include_hotkeys: + print_verbose("Unstaking from included hotkeys") + # Unstake from specific hotkeys. + for hotkey_identifier in include_hotkeys: + if is_valid_ss58_address(hotkey_identifier): + # If the hotkey is a valid ss58 address, we add it to the list. + hotkeys_to_unstake_from.append((None, hotkey_identifier)) + else: + # If the hotkey is not a valid ss58 address, we assume it is a hotkey name. + # We then get the hotkey from the wallet and add it to the list. + wallet_ = Wallet( + name=wallet.name, + path=wallet.path, + hotkey=hotkey_identifier, + ) + hotkeys_to_unstake_from.append( + (wallet_.hotkey_str, wallet_.hotkey.ss58_address) + ) + else: + # Only cli.config.wallet.hotkey is specified. + # So we unstake from that single hotkey. + print_verbose( + f"Unstaking from wallet: ({wallet.name}) from hotkey: ({wallet.hotkey_str})" + ) + assert wallet.hotkey is not None + hotkeys_to_unstake_from = [(wallet.hotkey_str, wallet.hotkey.ss58_address)] + + with console.status( + f"Retrieving stake data from {subtensor.network}...", + spinner="earth", + ): + # Prepare unstaking transactions + unstake_operations = [] + total_received_amount = Balance.from_tao(0) + current_wallet_balance: Balance = ( + await subtensor.get_balance(wallet.coldkeypub.ss58_address) + )[wallet.coldkeypub.ss58_address] + max_float_slippage = 0 + + # Fetch stake balances + chain_head = await subtensor.substrate.get_chain_head() + stake_in_netuids = ( + await subtensor.multi_get_stake_for_coldkey_and_hotkey_on_netuid( + hotkey_ss58s=[hk[1] for hk in hotkeys_to_unstake_from], + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuids=netuids, + block_hash=chain_head, + ) + ) + + # Flag to check if user wants to quit + skip_remaining_subnets = False + if hotkeys_to_unstake_from: + console.print( + "[dark_sea_green3]Tip: Enter 'q' any time to skip further entries and process existing unstakes" + ) + + # Iterate over hotkeys and netuids to collect unstake operations + for hotkey in hotkeys_to_unstake_from: + if skip_remaining_subnets: + break + + if interactive: + staking_address_name, staking_address_ss58, netuid = hotkey + netuids_to_process = [netuid] + else: + staking_address_name, staking_address_ss58 = hotkey + netuids_to_process = netuids + + initial_amount = amount + + if len(netuids_to_process) > 1: + console.print( + "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n" + ) + + for netuid in netuids_to_process: + if skip_remaining_subnets: + break # Exit the loop over netuids + + dynamic_info = all_sn_dynamic_info.get(netuid) + current_stake_balance = stake_in_netuids[staking_address_ss58][netuid] + if current_stake_balance.tao == 0: + continue # No stake to unstake + + # Determine the amount we are unstaking. + if initial_amount: + amount_to_unstake_as_balance = Balance.from_tao(initial_amount) + elif unstake_all: + amount_to_unstake_as_balance = current_stake_balance + else: + amount_to_unstake_as_balance = ask_unstake_amount( + current_stake_balance, + netuid, + staking_address_name + if staking_address_name + else staking_address_ss58, + staking_address_ss58, + interactive, + ) + if amount_to_unstake_as_balance is None: + skip_remaining_subnets = True + break + + # Check enough stake to remove. + amount_to_unstake_as_balance.set_unit(netuid) + if amount_to_unstake_as_balance > current_stake_balance: + err_console.print( + f"[red]Not enough stake to remove[/red]:\n Stake balance: [dark_orange]{current_stake_balance}[/dark_orange]" + f" < Unstaking amount: [dark_orange]{amount_to_unstake_as_balance}[/dark_orange]" + ) + continue # Skip to the next subnet - useful when single amount is specified for all subnets + + received_amount, slippage = dynamic_info.alpha_to_tao_with_slippage( + amount_to_unstake_as_balance + ) + total_received_amount += received_amount + if dynamic_info.is_dynamic: + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount) + if slippage + received_amount != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + else: + slippage_pct_float = 0 + slippage_pct = "[red]N/A[/red]" + max_float_slippage = max(max_float_slippage, slippage_pct_float) + + unstake_operations.append( + { + "netuid": netuid, + "hotkey_name": staking_address_name + if staking_address_name + else staking_address_ss58, + "hotkey_ss58": staking_address_ss58, + "amount_to_unstake": amount_to_unstake_as_balance, + "current_stake_balance": current_stake_balance, + "received_amount": received_amount, + "slippage_pct": slippage_pct, + "slippage_pct_float": slippage_pct_float, + "dynamic_info": dynamic_info, + } + ) + + if not unstake_operations: + console.print("[red]No unstake operations to perform.[/red]") + return False + + # Build the table + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Unstaking to: \nWallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]," + f" Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\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, + ) + table.add_column("Netuid", justify="center", style="grey89") + table.add_column( + "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"Amount ({Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO"], + ) + table.add_column( + f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + f"Received ({Balance.get_unit(0)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + footer=f"{total_received_amount}", + ) + table.add_column( + "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"] + ) + + for op in unstake_operations: + dynamic_info = op["dynamic_info"] + table.add_row( + str(op["netuid"]), + op["hotkey_name"], + str(op["amount_to_unstake"]), + str(float(dynamic_info.price)) + + f"({Balance.get_unit(0)}/{Balance.get_unit(op['netuid'])})", + str(op["received_amount"]), + op["slippage_pct"], + ) + + console.print(table) + + if max_float_slippage > 5: + console.print( + "\n" + f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_float_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]," + " this may result in a loss of funds.\n" + f"-------------------------------------------------------------------------------------------------------------------\n" + ) + + console.print( + """ +[bold white]Description[/bold white]: +The table displays information about the stake remove operation you are about to perform. +The columns are as follows: + - [bold white]Netuid[/bold white]: The netuid of the subnet you are unstaking from. + - [bold white]Hotkey[/bold white]: The ss58 address or identity of the hotkey you are unstaking from. + - [bold white]Amount[/bold white]: The stake amount you are removing from this key. + - [bold white]Rate[/bold white]: The rate of exchange between TAO and the subnet's stake. + - [bold white]Received[/bold white]: The amount of free balance TAO you will receive on this subnet after slippage. + - [bold white]Slippage[/bold white]: The slippage percentage of the unstake operation. (0% if the subnet is not dynamic i.e. root). +""" + ) + if prompt: + if not Confirm.ask("Would you like to continue?"): + raise typer.Exit() + + # Perform unstaking operations + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + + with console.status("\n:satellite: Performing unstaking operations...") as status: + for op in unstake_operations: + netuid_i = op["netuid"] + staking_address_name = op["hotkey_name"] + staking_address_ss58 = op["hotkey_ss58"] + amount = op["amount_to_unstake"] + current_stake_balance = op["current_stake_balance"] + + status.update( + f"\n:satellite: Unstaking {amount} from {staking_address_name} on netuid: {netuid_i} ..." + ) + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_unstaked": amount.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + else: + await response.process_events() + if not await response.is_success: + print_error( + f":cross_mark: [red]Failed[/red] with error: " + f"{format_error_message(await response.error_message, subtensor.substrate)}", + status, + ) + else: + new_balance_ = await subtensor.get_balance( + wallet.coldkeypub.ss58_address + ) + new_balance = new_balance_[wallet.coldkeypub.ss58_address] + new_stake = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=staking_address_ss58, + netuid=netuid_i, + ) + ).set_unit(netuid_i) + console.print( + f"Balance:\n [blue]{current_wallet_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" Stake:\n [blue]{current_stake_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" + ) + console.print( + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." + ) + + +async def stake_list( + wallet: Wallet, + coldkey_ss58: str, + subtensor: "SubtensorInterface", + live: bool = False, +): + coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address + + async def get_stake_data(block_hash: str = None): + ( + substakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + ) = await asyncio.gather( + subtensor.get_stake_info_for_coldkeys( + coldkey_ss58_list=[coldkey_address], block_hash=block_hash + ), + subtensor.get_delegate_identities(block_hash=block_hash), + subtensor.get_all_subnet_dynamic_info(), + subtensor.substrate.query( + "SubtensorModule", "HotkeyEmissionTempo", block_hash=block_hash + ), + ) + sub_stakes = substakes[coldkey_address] + return ( + sub_stakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + ) + + 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, + ): + title = f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Hotkey: {hotkey_name}\nNetwork: {subtensor.network}\n\n" + if not live: + title += f"[{COLOR_PALETTE['GENERAL']['HINT']}]See below for an explanation of the columns\n" + table = Table( + title=title, + 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, + ) + table.add_column( + "[white]Netuid", + footer=f"{len(rows)}", + footer_style="overline white", + style="grey89", + ) + table.add_column( + "[white]Symbol", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + justify="center", + no_wrap=True, + ) + table.add_column( + f"[white]Stake ({Balance.get_unit(1)})", + footer_style="overline white", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="center", + ) + table.add_column( + f"[white]Rate \n({Balance.unit}_in/{Balance.get_unit(1)}_in)", + footer_style="white", + style=COLOR_PALETTE["POOLS"]["RATE"], + justify="center", + ) + table.add_column( + f"[white]TAO equiv \n({Balance.unit}_in x {Balance.get_unit(1)}/{Balance.get_unit(1)}_out)", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + justify="right", + footer=f"{total_tao_ownership}", + ) + table.add_column( + f"[white]Exchange Value \n({Balance.get_unit(1)} x {Balance.unit}/{Balance.get_unit(1)})", + footer_style="overline white", + style=COLOR_PALETTE["STAKE"]["TAO"], + justify="right", + footer=f"{total_tao_value}", + ) + table.add_column( + f"[white]Swap ({Balance.get_unit(1)} -> {Balance.unit})", + footer_style="overline white", + style=COLOR_PALETTE["STAKE"]["STAKE_SWAP"], + justify="right", + footer=f"{total_swapped_tao_value}", + ) + table.add_column( + "[white]Registered", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="right", + ) + table.add_column( + f"[white]Emission \n({Balance.get_unit(1)}/block)", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + justify="right", + ) + return table + + def create_table(hotkey_: str, substakes: list[StakeInfo]): + name = ( + f"{registered_delegate_info[hotkey_].display} ({hotkey_})" + if hotkey_ in registered_delegate_info + else hotkey_ + ) + rows = [] + total_tao_ownership = Balance(0) + total_tao_value = Balance(0) + total_swapped_tao_value = Balance(0) + for substake_ in substakes: + 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) + + # TAO value cell + tao_value = pool.alpha_to_tao(alpha_value) + total_tao_value += tao_value + + # Swapped TAO value and slippage cell + swapped_tao_value, slippage = pool.alpha_to_tao_with_slippage( + substake_.stake + ) + total_swapped_tao_value += swapped_tao_value + + # Slippage percentage cell + if pool.is_dynamic: + slippage_percentage_ = ( + 100 * float(slippage) / float(slippage + swapped_tao_value) + if slippage + swapped_tao_value != 0 + else 0 + ) + slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_percentage_:.3f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" + else: + slippage_percentage = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]0.000%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]" + + # 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 / emission_drain_tempo + + # Alpha ownership and TAO ownership cells + if alpha_value.tao > 0.00009: + if issuance.tao != 0: + 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 = "0.0000" + + rows.append( + [ + str(netuid), # Number + symbol if netuid != 0 else "\u03a4", # Symbol + f"{substake_.stake.tao:,.4f} {symbol}" + if netuid != 0 + else f"{symbol} {substake_.stake.tao:,.4f}", # Stake (a) + f"{pool.price.tao:.4f} τ/{symbol}", # Rate (t/a) + f"{tao_ownership}", # TAO equiv + f"{tao_value}", # Exchange Value (α x τ/α) + f"{swapped_tao_value} ({slippage_percentage})", # Swap(α) -> τ + "YES" + if substake_.is_registered + else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registered + str(Balance.from_tao(per_block_emission).set_unit(netuid)), + # 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) + ] + ) + table = define_table( + name, rows, total_tao_ownership, 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 + + def create_live_table( + substakes: list, + registered_delegate_info: dict, + dynamic_info: dict, + emission_drain_tempo: int, + hotkey_name: str, + previous_data: Optional[dict] = None, + ) -> tuple[Table, dict, Balance, Balance, Balance]: + rows = [] + current_data = {} + + total_tao_ownership = Balance(0) + total_tao_value = Balance(0) + total_swapped_tao_value = Balance(0) + + def format_cell(value, previous_value, unit="", unit_first=False, precision=4): + if previous_value is not None: + change = value - previous_value + if abs(change) > 10 ** (-precision): + change_text = ( + f" [pale_green3](+{change:.{precision}f})[/pale_green3]" + if change > 0 + else f" [hot_pink3]({change:.{precision}f})[/hot_pink3]" + ) + else: + change_text = "" + else: + change_text = "" + return ( + f"{value:,.{precision}f} {unit}{change_text}" + if not unit_first + else f"{unit} {value:,.{precision}f}{change_text}" + ) + + # Process each stake + for substake in substakes: + netuid = substake.netuid + pool = dynamic_info.get(netuid) + if substake.stake.rao == 0 or not pool: + continue + + # Calculate base values + symbol = f"{Balance.get_unit(netuid)}\u200e" + 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( + substake.stake + ) + 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, + "price": pool.price.tao, + "tao_value": tao_value.tao, + "swapped_value": swapped_tao_value.tao, + "emission": substake.emission.tao / emission_drain_tempo, + "tao_ownership": tao_ownership.tao, + } + + # Get previous values for delta tracking + prev = previous_data.get(netuid, {}) if previous_data else {} + unit_first = True if netuid == 0 else False + + stake_cell = format_cell( + alpha_value.tao, + prev.get("stake"), + unit=symbol, + unit_first=unit_first, + precision=4, + ) + + rate_cell = format_cell( + pool.price.tao, + prev.get("price"), + unit=f"τ/{symbol}", + unit_first=False, + precision=5, + ) + + tao_ownership_cell = format_cell( + tao_ownership.tao, + prev.get("tao_ownership"), + unit="τ", + unit_first=True, + precision=4, + ) + + exchange_cell = format_cell( + tao_value.tao, + prev.get("tao_value"), + unit="τ", + unit_first=True, + precision=4, + ) + + 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 + + swap_cell = ( + format_cell( + swapped_tao_value.tao, + prev.get("swapped_value"), + unit="τ", + unit_first=True, + precision=4, + ) + + f" ({slippage_pct:.2f}%)" + ) + + emission_value = substake.emission.tao / emission_drain_tempo + emission_cell = format_cell( + emission_value, + prev.get("emission"), + unit=symbol, + unit_first=unit_first, + precision=4, + ) + + rows.append( + [ + str(netuid), # Netuid + symbol if netuid != 0 else "\u03a4", # Symbol + stake_cell, # Stake amount + rate_cell, # Rate + tao_ownership_cell, # TAO equivalent + exchange_cell, # Exchange value + swap_cell, # Swap value with slippage + "YES" + if substake.is_registered + else f"[{COLOR_PALETTE['STAKE']['NOT_REGISTERED']}]NO", # Registration status + emission_cell, # Emission rate + ] + ) + + table = define_table( + hotkey_name, + rows, + total_tao_ownership, + total_tao_value, + total_swapped_tao_value, + live=True, + ) + + for row in rows: + table.add_row(*row) + + return table, current_data + + # Main execution + ( + sub_stakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + ) = await get_stake_data() + + # Iterate over substakes and aggregate them by hotkey. + hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} + + for substake in sub_stakes: + hotkey = substake.hotkey_ss58 + if substake.stake.rao == 0: + continue + if hotkey not in hotkeys_to_substakes: + hotkeys_to_substakes[hotkey] = [] + hotkeys_to_substakes[hotkey].append(substake) + + if live: + # Select one hokkey for live monitoring + if len(hotkeys_to_substakes) > 1: + console.print( + "\n[bold]Multiple hotkeys found. Please select one for live monitoring:[/bold]" + ) + for idx, hotkey in enumerate(hotkeys_to_substakes.keys()): + name = ( + f"{registered_delegate_info[hotkey].display} ({hotkey})" + if hotkey in registered_delegate_info + else hotkey + ) + console.print(f"[{idx}] [{COLOR_PALETTE['GENERAL']['HEADER']}]{name}") + + selected_idx = Prompt.ask( + "Enter hotkey index", + choices=[str(i) for i in range(len(hotkeys_to_substakes))], + ) + selected_hotkey = list(hotkeys_to_substakes.keys())[int(selected_idx)] + selected_stakes = hotkeys_to_substakes[selected_hotkey] + else: + selected_hotkey = list(hotkeys_to_substakes.keys())[0] + selected_stakes = hotkeys_to_substakes[selected_hotkey] + + hotkey_name = ( + f"{registered_delegate_info[selected_hotkey].display} ({selected_hotkey})" + if selected_hotkey in registered_delegate_info + else selected_hotkey + ) + + refresh_interval = 10 # seconds + progress = Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=20), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + console=console, + ) + progress_task = progress.add_task("Updating: ", total=refresh_interval) + + previous_block = None + current_block = None + previous_data = None + + with Live(console=console, screen=True, auto_refresh=True) as live: + try: + while True: + block_hash = await subtensor.substrate.get_chain_head() + ( + sub_stakes, + registered_delegate_info, + dynamic_info_, + emission_drain_tempo, + ) = await get_stake_data(block_hash) + selected_stakes = [ + stake + for stake in sub_stakes + if stake.hotkey_ss58 == selected_hotkey + ] + + dynamic_info = {info.netuid: info for info in dynamic_info_} + block_number = await subtensor.substrate.get_block_number(None) + + previous_block = current_block + current_block = block_number + new_blocks = ( + "N/A" + if previous_block is None + else str(current_block - previous_block) + ) + + table, current_data = create_live_table( + selected_stakes, + registered_delegate_info, + dynamic_info, + emission_drain_tempo, + hotkey_name, + previous_data, + ) + + previous_data = current_data + progress.reset(progress_task) + start_time = asyncio.get_event_loop().time() + + block_info = ( + f"Previous: [dark_sea_green]{previous_block}[/dark_sea_green] " + f"Current: [dark_sea_green]{current_block}[/dark_sea_green] " + f"Diff: [dark_sea_green]{new_blocks}[/dark_sea_green]" + ) + + message = f"\nLive stake view - Press [bold red]Ctrl+C[/bold red] to exit\n{block_info}" + live_render = Group(message, progress, table) + live.update(live_render) + + while not progress.finished: + await asyncio.sleep(0.1) + elapsed = asyncio.get_event_loop().time() - start_time + progress.update( + progress_task, completed=min(elapsed, refresh_interval) + ) + + except KeyboardInterrupt: + console.print("\n[bold]Stopped live updates[/bold]") + return + + else: + # 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) + 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 + + if num_hotkeys > 1 and counter < num_hotkeys: + console.print("\nPress Enter to continue to the next hotkey...") + input() + + balance = await subtensor.get_balance(coldkey_address) + console.print("\n\n") + console.print( + 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[coldkey_address]}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total TAO ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_global_tao}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" + f" Total Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{all_hotkeys_total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" + ) + if not sub_stakes: + console.print( + f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})" + ) + else: + display_table = Prompt.ask( + "\nPress Enter to view column descriptions or type 'q' to skip:", + choices=["", "q"], + default="", + show_choices=True, + ).lower() + + if display_table == "q": + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped." + ) + else: + header = """ + [bold white]Description[/bold white]: Each table displays information about stake associated with a hotkey. The columns are as follows: + """ + console.print(header) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) + + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Stake (α)[/bold tan]", + "The stake amount this hotkey holds in the subnet, expressed in subnet's alpha token currency. This can change whenever staking or unstaking occurs on this hotkey in this subnet. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#staking[/blue].", + ), + ( + "[bold tan]TAO Reserves (τ_in)[/bold tan]", + 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', + ), + ( + "[bold tan]Alpha Reserves (α_in)[/bold tan]", + "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + "Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].", + ), + ( + "[bold tan]Alpha out (α_out)[/bold tan]", + "Total stake in the subnet, expressed in subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out", + ), + ( + "[bold tan]TAO Equiv (τ_in x α/α_out)[/bold tan]", + 'TAO-equivalent value of the hotkeys stake α (i.e., Stake(α)). Calculated as (TAO Reserves(τ_in) x (Stake(α) / ALPHA Out(α_out)). This value is weighted with (1-γ), where γ is the local weight coefficient, and used in determining the overall stake weight of the hotkey in this subnet. Also see the "Local weight coeff (γ)" column of "btcli subnet list" command output. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#local-weight-or-tao-equiv-%CF%84_in-x-%CE%B1%CE%B1_out[/blue].', + ), + ( + "[bold tan]Exchange Value (α x τ/α)[/bold tan]", + "This is the potential τ you will receive, without considering slippage, if you unstake from this hotkey now on this subnet. See Swap(α → τ) column description. Note: The TAO Equiv(τ_in x α/α_out) indicates validator stake weight while this Exchange Value shows τ you will receive if you unstake now. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#exchange-value-%CE%B1-x-%CF%84%CE%B1[/blue].", + ), + ( + "[bold tan]Swap (α → τ)[/bold tan]", + "This is the actual τ you will receive, after factoring in the slippage charge, if you unstake from this hotkey now on this subnet. The slippage is calculated as 1 - (Swap(α → τ)/Exchange Value(α x τ/α)), and is displayed in brackets. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#swap-%CE%B1--%CF%84[/blue].", + ), + ( + "[bold tan]Registered[/bold tan]", + "Indicates if the hotkey is registered in this subnet or not. \nFor more, see [blue]https://docs.bittensor.com/learn/anatomy-of-incentive-mechanism#tempo[/blue].", + ), + ( + "[bold tan]Emission (α/block)[/bold tan]", + "Shows the portion of the one α/block emission into this subnet that is received by this hotkey, according to YC2 in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", + ), + ] + + description_table.add_column( + "Field", + no_wrap=True, + style="bold tan", + ) + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) + + +async def move_stake( + subtensor: "SubtensorInterface", + wallet: Wallet, + origin_netuid: int, + destination_netuid: int, + destination_hotkey: str, + amount: float, + stake_all: bool, + prompt: bool = True, +): + origin_hotkey_ss58 = wallet.hotkey.ss58_address + # Get the wallet stake balances. + origin_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey_ss58, + netuid=origin_netuid, + ) + ).set_unit(origin_netuid) + + destination_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=destination_hotkey, + netuid=destination_netuid, + ) + ).set_unit(destination_netuid) + + if origin_stake_balance == Balance.from_tao(0).set_unit(origin_netuid): + print_error( + f"Your balance is [{COLOR_PALETTE['POOLS']['TAO']}]0[/{COLOR_PALETTE['POOLS']['TAO']}] in Netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}" + ) + raise typer.Exit() + + console.print( + f"\nOrigin Netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], Origin stake: [{COLOR_PALETTE['POOLS']['TAO']}]{origin_stake_balance}" + ) + console.print( + f"Destination netuid: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{destination_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}], Destination stake: [{COLOR_PALETTE['POOLS']['TAO']}]{destination_stake_balance}\n" + ) + + # Determine the amount we are moving. + amount_to_move_as_balance = None + if amount: + amount_to_move_as_balance = Balance.from_tao(amount) + elif stake_all: + amount_to_move_as_balance = origin_stake_balance + else: # max_stake + # TODO improve this + if Confirm.ask( + f"Move all: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]?" + ): + amount_to_move_as_balance = origin_stake_balance + else: + try: + amount = float( + Prompt.ask( + f"Enter amount to move in [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}" + ) + ) + amount_to_move_as_balance = Balance.from_tao(amount) + except ValueError: + print_error(f":cross_mark: Invalid amount: {amount}") + return False + + # Check enough to move. + amount_to_move_as_balance.set_unit(origin_netuid) + if amount_to_move_as_balance > origin_stake_balance: + err_console.print( + f"[red]Not enough stake[/red]:\n Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + ) + return False + + # Slippage warning + if prompt: + if origin_netuid == destination_netuid: + received_amount_destination = amount_to_move_as_balance + slippage_pct_float = 0 + slippage_pct = f"{slippage_pct_float}%" + price = Balance.from_tao(1).set_unit(origin_netuid) + price_str = ( + str(float(price.tao)) + + f"{Balance.get_unit(origin_netuid)}/{Balance.get_unit(origin_netuid)}" + ) + else: + dynamic_origin, dynamic_destination = await asyncio.gather( + subtensor.get_subnet_dynamic_info(origin_netuid), + subtensor.get_subnet_dynamic_info(destination_netuid), + ) + price = ( + float(dynamic_origin.price) + * 1 + / (float(dynamic_destination.price) or 1) + ) + received_amount_tao, slippage = dynamic_origin.alpha_to_tao_with_slippage( + amount_to_move_as_balance + ) + received_amount_destination, slippage = ( + dynamic_destination.tao_to_alpha_with_slippage(received_amount_tao) + ) + received_amount_destination.set_unit(destination_netuid) + slippage_pct_float = ( + 100 * float(slippage) / float(slippage + received_amount_destination) + if slippage + received_amount_destination != 0 + else 0 + ) + slippage_pct = f"{slippage_pct_float:.4f} %" + price_str = ( + str(float(price)) + + f"{Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)}" + ) + + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Moving stake from: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] to: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\nNetwork: {subtensor.network}\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, + ) + table.add_column( + "origin netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] + ) + table.add_column( + "origin hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + "dest netuid", justify="center", style=COLOR_PALETTE["GENERAL"]["SYMBOL"] + ) + table.add_column( + "dest hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"] + ) + table.add_column( + f"amount ({Balance.get_unit(origin_netuid)})", + justify="center", + style=COLOR_PALETTE["STAKE"]["TAO"], + ) + table.add_column( + f"rate ({Balance.get_unit(destination_netuid)}/{Balance.get_unit(origin_netuid)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["RATE"], + ) + table.add_column( + f"received ({Balance.get_unit(destination_netuid)})", + justify="center", + style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], + ) + table.add_column( + "slippage", + justify="center", + style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"], + ) + + table.add_row( + f"{Balance.get_unit(origin_netuid)}({origin_netuid})", + f"{origin_hotkey_ss58[:3]}...{origin_hotkey_ss58[-3:]}", + # TODO f-strings + Balance.get_unit(destination_netuid) + "(" + str(destination_netuid) + ")", + f"{destination_hotkey[:3]}...{destination_hotkey[-3:]}", + str(amount_to_move_as_balance), + price_str, + str(received_amount_destination.set_unit(destination_netuid)), + str(slippage_pct), + ) + + console.print(table) + message = "" + if slippage_pct_float > 5: + message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" + message += f"[bold]WARNING:\tSlippage is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{slippage_pct}[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.[/bold] \n" + message += "-------------------------------------------------------------------------------------------------------------------\n" + console.print(message) + if not Confirm.ask("Would you like to continue?"): + return True + + # Perform staking operation. + try: + wallet.unlock_coldkey() + except KeyFileError: + err_console.print("Error decrypting coldkey (possibly incorrect password)") + return False + with console.status( + f"\n:satellite: Moving {amount_to_move_as_balance} from {origin_hotkey_ss58} on netuid: {origin_netuid} to " + f"{destination_hotkey} on netuid: {destination_netuid} ..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="move_stake", + call_params={ + "origin_hotkey": origin_hotkey_ss58, + "origin_netuid": origin_netuid, + "destination_hotkey": destination_hotkey, + "destination_netuid": destination_netuid, + "alpha_amount": amount_to_move_as_balance.rao, + }, + ) + extrinsic = await subtensor.substrate.create_signed_extrinsic( + call=call, keypair=wallet.coldkey + ) + response = await subtensor.substrate.submit_extrinsic( + extrinsic, wait_for_inclusion=True, wait_for_finalization=False + ) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True + else: + await response.process_events() + if not await response.is_success: + err_console.print( + f":cross_mark: [red]Failed[/red] with error:" + f" {format_error_message(response.error_message, subtensor.substrate)}" + ) + return + else: + new_origin_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey_ss58, + netuid=origin_netuid, + ) + ).set_unit(origin_netuid) + new_destination_stake_balance: Balance = ( + await subtensor.get_stake_for_coldkey_and_hotkey_on_netuid( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=destination_hotkey, + netuid=destination_netuid, + ) + ).set_unit(destination_netuid) + console.print( + f"Origin Stake:\n [blue]{origin_stake_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_origin_stake_balance}" + ) + console.print( + f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" + ) + return + + +async def fetch_coldkey_stake(subtensor: "SubtensorInterface", wallet: Wallet): + sub_stakes = await subtensor.get_stake_info_for_coldkey( + coldkey_ss58=wallet.coldkeypub.ss58_address + ) + return sub_stakes diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 3e9349827..39050dfa5 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -1,21 +1,30 @@ import asyncio import json import sqlite3 -from textwrap import dedent from typing import TYPE_CHECKING, Optional, cast +import typer from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError -from rich.prompt import Confirm +from rich.prompt import Confirm, Prompt +from rich.console import Console, Group +from rich.spinner import Spinner +from rich.text import Text +from rich.progress import Progress, BarColumn, TextColumn from rich.table import Column, Table +from rich import box -from bittensor_cli.src import DelegatesDetails +from bittensor_cli.src import COLOR_PALETTE, SUBNETS from bittensor_cli.src.bittensor.balances import Balance -from bittensor_cli.src.bittensor.chain_data import SubnetInfo -from bittensor_cli.src.bittensor.extrinsics.registration import register_extrinsic +from bittensor_cli.src.bittensor.chain_data import SubnetState +from bittensor_cli.src.bittensor.extrinsics.registration import ( + register_extrinsic, + burned_register_extrinsic, +) +from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic +from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph -from bittensor_cli.src.commands.root import burned_register_extrinsic -from bittensor_cli.src.commands.wallets import set_id, set_id_prompts +from bittensor_cli.src.commands.wallets import set_id, get_id from bittensor_cli.src.bittensor.utils import ( RAO_PER_TAO, console, @@ -28,6 +37,7 @@ millify, render_table, update_metadata_table, + prompt_for_identity, ) if TYPE_CHECKING: @@ -84,19 +94,21 @@ async def _find_event_attributes_in_extrinsic_receipt( your_balance_ = await subtensor.get_balance(wallet.coldkeypub.ss58_address) your_balance = your_balance_[wallet.coldkeypub.ss58_address] - print_verbose("Fetching lock_cost") - burn_cost = await lock_cost(subtensor) - if burn_cost > your_balance: + print_verbose("Fetching burn_cost") + sn_burn_cost = await burn_cost(subtensor) + if sn_burn_cost > your_balance: err_console.print( - f"Your balance of: [green]{your_balance}[/green] is not enough to pay the subnet lock cost of: " - f"[green]{burn_cost}[/green]" + f"Your balance of: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}[{COLOR_PALETTE['POOLS']['TAO']}] is not enough to pay the subnet lock cost of: " + f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}]" ) return False if prompt: - console.print(f"Your balance is: [green]{your_balance}[/green]") + console.print( + f"Your balance is: [{COLOR_PALETTE['POOLS']['TAO']}]{your_balance}" + ) if not Confirm.ask( - f"Do you want to register a subnet for [green]{burn_cost}[/green]?" + f"Do you want to register a subnet for [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}?" ): return False @@ -112,7 +124,10 @@ async def _find_event_attributes_in_extrinsic_receipt( call = await substrate.compose_call( call_module="SubtensorModule", call_function="register_network", - call_params={"immunity_period": 0, "reg_allowed": True}, + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "mechid": 1, + }, ) extrinsic = await substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey @@ -141,7 +156,7 @@ async def _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) console.print( - f":white_heavy_check_mark: [green]Registered subnetwork with netuid: {attributes[0]}[/green]" + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) return True @@ -150,115 +165,463 @@ async def _find_event_attributes_in_extrinsic_receipt( async def subnets_list( - subtensor: "SubtensorInterface", reuse_last: bool, html_output: bool, no_cache: bool + subtensor: "SubtensorInterface", + reuse_last: bool, + html_output: bool, + no_cache: bool, + live: bool, ): """List all subnet netuids in the network.""" - async def _get_all_subnets_info(): - hex_bytes_result = await subtensor.query_runtime_api( - runtime_api="SubnetInfoRuntimeApi", method="get_subnets_info", params=[] + async def fetch_subnet_data(): + subnets = await subtensor.get_all_subnet_dynamic_info() + global_weights, identities = await asyncio.gather( + subtensor.get_global_weights([subnet.netuid for subnet in subnets]), + subtensor.query_all_identities(), ) - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) + return subnets, global_weights, identities - return SubnetInfo.list_from_vec_u8(bytes_result) + def define_table(total_emissions: float, total_rate: float, total_netuids: int): + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" + f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\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, + ) - if not reuse_last: - subnets: list[SubnetInfo] - delegate_info: dict[str, DelegatesDetails] + table.add_column("[bold white]Netuid", style="grey89", justify="center", footer=str(total_netuids)) + table.add_column( + "[bold white]Symbol", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + justify="right", + ) + table.add_column("[bold white]Name", style="cyan", justify="left") + table.add_column( + f"[bold white]RATE ({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", + style="#AB7CC8", + justify="left", + footer=f"τ {total_rate:.4f}", + ) + table.add_column( + f"[bold white]Emission ({Balance.get_unit(0)})", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + justify="left", + footer=f"τ {total_emissions:.4f}", + ) + table.add_column( + f"[bold white]TAO Pool ({Balance.get_unit(0)}_in)", + style=COLOR_PALETTE["STAKE"]["TAO"], + justify="left", + ) + table.add_column( + f"[bold white]Alpha Pool ({Balance.get_unit(1)}_in)", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + justify="left", + ) + table.add_column( + f"[bold white]Stake ({Balance.get_unit(1)}_out)", + style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], + justify="left", + ) + table.add_column( + "[bold white]Tempo (k/n)", + style=COLOR_PALETTE["GENERAL"]["TEMPO"], + justify="left", + overflow="fold", + ) + table.add_column( + "[bold white]Local weight coeff. (γ)", style="steel_blue", justify="left" + ) + return table - print_verbose("Fetching subnet and delegate information") - subnets, delegate_info = await asyncio.gather( - _get_all_subnets_info(), - subtensor.get_delegate_identities(), + # Non-live mode + def create_table(subnets, global_weights, identities): + rows = [] + for subnet in subnets: + netuid = subnet.netuid + global_weight = global_weights.get(netuid) + symbol = f"{subnet.symbol}\u200e" + + if netuid == 0: + emission_tao = 0.0 + identity = "~" + else: + emission_tao = subnet.emission.tao + identity = identities.get(subnet.owner, {}).get("name", "~") + + # Prepare cells + netuid_cell = str(netuid) + symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" + subnet_name_cell = SUBNETS.get(netuid, "~") + emission_cell = f"τ {emission_tao:,.4f}" + price_cell = f"{subnet.price.tao:.4f} τ/{symbol}" + tao_in_cell = f"τ {subnet.tao_in.tao:,.4f}" + alpha_in_cell = f"{subnet.alpha_in.tao:,.4f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_in.tao:,.4f}" + alpha_out_cell = f"{subnet.alpha_out.tao:,.5f} {symbol}" if netuid != 0 else f"{symbol} {subnet.alpha_out.tao:,.5f}" + tempo_cell = f"{subnet.blocks_since_last_step}/{subnet.tempo}" + global_weight_cell = ( + f"{global_weight:.4f}" if global_weight is not None else "N/A" + ) + + rows.append( + ( + netuid_cell, # Netuid + symbol_cell, # Symbol + subnet_name_cell, # Name + price_cell, # Rate τ_in/α_in + emission_cell, # Emission (τ) + tao_in_cell, # TAO Pool τ_in + alpha_in_cell, # Alpha Pool α_in + alpha_out_cell, # Stake α_out + tempo_cell, # Tempo k/n + global_weight_cell,# Local weight coeff. (γ) + ) + ) + + total_emissions = sum( + float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 ) + total_rate = sum( + float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 + ) + total_netuids = len(subnets) + table = define_table(total_emissions, total_rate, total_netuids) - if not subnets: - err_console.print("[red]No subnets found[/red]") - return + # Sort rows by stake, keeping the root subnet in the first position + sorted_rows = [rows[0]] + sorted( + rows[1:], key=lambda x: float(str(x[7]).split()[0].replace(",", "")), reverse=True + ) + + for row in sorted_rows: + table.add_row(*row) + return table + + # Live mode + def create_table_live(subnets, global_weights, identities, previous_data): + def format_cell(value, previous_value, unit="", unit_first=False, precision=4): + if previous_value is not None: + change = value - previous_value + if change > 0.01: + change_text = ( + f" [pale_green3](+{change:.2f})[/pale_green3]" + ) + elif change < -0.01: + change_text = ( + f" [hot_pink3]({change:.2f})[/hot_pink3]" + ) + else: + change_text = "" + else: + change_text = "" + return f"{value:,.{precision}f} {unit}{change_text}" if not unit_first else f"{unit} {value:,.{precision}f}{change_text}" rows = [] - db_rows = [] - total_neurons = 0 - max_neurons = 0 + current_data = {} # To store current values for comparison in the next update for subnet in subnets: - total_neurons += subnet.subnetwork_n - max_neurons += subnet.max_n + netuid = subnet.netuid + global_weight = global_weights.get(netuid) + symbol = f"{subnet.symbol}\u200e" + + if netuid == 0: + emission_tao = 0.0 + identity = "~" + else: + emission_tao = subnet.emission.tao + identity = identities.get(subnet.owner, {}).get("name", "~") + + # Store current values for comparison + current_data[netuid] = { + "emission_tao": emission_tao, + "alpha_out": subnet.alpha_out.tao, + "tao_in": subnet.tao_in.tao, + "alpha_in": subnet.alpha_in.tao, + "price": subnet.price.tao, + "blocks_since_last_step": subnet.blocks_since_last_step, + "global_weight": global_weight, + } + prev = previous_data.get(netuid) if previous_data else {} + + # Prepare cells + netuid_cell = str(netuid) + symbol_cell = f"{subnet.symbol}" if netuid != 0 else "\u03A4" + subnet_name_cell = SUBNETS.get(netuid, "~") + if netuid == 0: + unit_first = True + else: + unit_first = False + emission_cell = format_cell( + emission_tao, prev.get("emission_tao"), unit="τ", unit_first=True, precision=4 + ) + price_cell = format_cell( + subnet.price.tao, prev.get("price"), unit=f"τ/{symbol}", precision=4 + ) + tao_in_cell = format_cell( + subnet.tao_in.tao, prev.get("tao_in"), unit="τ", unit_first=True, precision=4 + ) + alpha_in_cell = format_cell( + subnet.alpha_in.tao, + prev.get("alpha_in"), + unit=f"{symbol}", + unit_first=unit_first, + precision=4, + ) + alpha_out_cell = format_cell( + subnet.alpha_out.tao, + prev.get("alpha_out"), + unit=f"{symbol}", + unit_first=unit_first, + precision=5, + ) + + # Tempo cell + prev_blocks_since_last_step = prev.get("blocks_since_last_step") + if prev_blocks_since_last_step is not None: + if subnet.blocks_since_last_step >= prev_blocks_since_last_step: + block_change = ( + subnet.blocks_since_last_step - prev_blocks_since_last_step + ) + else: + # Tempo restarted + block_change = ( + subnet.blocks_since_last_step + subnet.tempo + 1 + ) - prev_blocks_since_last_step + if block_change > 0: + block_change_text = f" [pale_green3](+{block_change})[/pale_green3]" + elif block_change < 0: + block_change_text = f" [hot_pink3]({block_change})[/hot_pink3]" + else: + block_change_text = "" + else: + block_change_text = "" + tempo_cell = ( + f"{subnet.blocks_since_last_step}/{subnet.tempo}{block_change_text}" + ) + + # Local weight coeff cell + prev_global_weight = prev.get("global_weight") + if prev_global_weight is not None and global_weight is not None: + weight_change = float(global_weight) - float(prev_global_weight) + if weight_change > 0: + weight_change_text = ( + f" [pale_green3](+{weight_change:.6f})[/pale_green3]" + ) + elif weight_change < 0: + weight_change_text = ( + f" [hot_pink3]({weight_change:.6f})[/hot_pink3]" + ) + else: + weight_change_text = "" + else: + weight_change_text = "" + + global_weight_cell = ( + f"{global_weight:.4f}{weight_change_text}" + if global_weight is not None + else "N/A" + ) + 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)), - str( - delegate_info[subnet.owner_ss58].display - if subnet.owner_ss58 in delegate_info - else subnet.owner_ss58 - ), + netuid_cell, # Netuid + symbol_cell, # Symbol + subnet_name_cell, # Name + price_cell, # Rate τ_in/α_in + emission_cell, # Emission (τ) + tao_in_cell, # TAO Pool τ_in + alpha_in_cell, # Alpha Pool α_in + alpha_out_cell, # Stake α_out + tempo_cell, # Tempo k/n + global_weight_cell,# Local weight coeff. (γ) ) ) - db_rows.append( - [ - int(subnet.netuid), - int(subnet.subnetwork_n), - int(subnet.max_n), # millified in HTML table - float( - subnet.emission_value / RAO_PER_TAO * 100 - ), # shown as percentage in HTML table - int(subnet.tempo), - float(subnet.burn), - int(subnet.difficulty), # millified in HTML table - str( - delegate_info[subnet.owner_ss58].display - if subnet.owner_ss58 in delegate_info - else subnet.owner_ss58 - ), - ] - ) - metadata = { - "network": subtensor.network, - "netuid_count": len(subnets), - "N": total_neurons, - "MAX_N": max_neurons, - "rows": json.dumps(rows), - } - if not no_cache: - create_table( - "subnetslist", - [ - ("NETUID", "INTEGER"), - ("N", "INTEGER"), - ("MAX_N", "BLOB"), - ("EMISSION", "REAL"), - ("TEMPO", "INTEGER"), - ("RECYCLE", "REAL"), - ("DIFFICULTY", "BLOB"), - ("SUDO", "TEXT"), - ], - db_rows, - ) - update_metadata_table("subnetslist", values=metadata) + + total_emissions = sum( + float(subnet.emission.tao) for subnet in subnets if subnet.netuid != 0 + ) + total_rate = sum( + float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0 + ) + total_netuids = len(subnets) + table = define_table(total_emissions, total_rate, total_netuids) + + # Sort rows by stake, keeping the first subnet in the first position + sorted_rows = [rows[0]] + sorted( + rows[1:], key=lambda x: float(str(x[7]).split()[0].replace(",", "")), reverse=True + ) + for row in sorted_rows: + table.add_row(*row) + return table, current_data + + # Live mode + if live: + refresh_interval = 15 # seconds + + progress = Progress( + TextColumn("[progress.description]{task.description}"), + BarColumn(bar_width=20, style="green", complete_style="green"), + TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), + console=console, + auto_refresh=True, + ) + progress_task = progress.add_task("Updating:", total=refresh_interval) + + previous_block = None + current_block = None + previous_data = None + + with Live(console=console, screen=True, auto_refresh=True) as live: + try: + while True: + subnets = await subtensor.get_all_subnet_dynamic_info() + global_weights, identities, block_number = await asyncio.gather( + subtensor.get_global_weights([subnet.netuid for subnet in subnets]), + subtensor.query_all_identities(), + subtensor.substrate.get_block_number(None) + ) + + # Update block numbers + previous_block = current_block + current_block = block_number + new_blocks = "N/A" if previous_block is None else str(current_block - previous_block) + + table, current_data = create_table_live( + subnets, global_weights, identities, previous_data + ) + previous_data = current_data + progress.reset(progress_task) + start_time = asyncio.get_event_loop().time() + + block_info = ( + f"Previous: [dark_sea_green]{previous_block if previous_block else 'N/A'}[/dark_sea_green] " + f"Current: [dark_sea_green]{current_block}[/dark_sea_green] " + f"Diff: [dark_sea_green]{new_blocks}[/dark_sea_green] " + ) + + message = f"Live view active. Press [bold red]Ctrl + C[/bold red] to exit\n{block_info}" + + live_render = Group(message, progress, table) + live.update(live_render) + + while not progress.finished: + await asyncio.sleep(0.1) + elapsed = asyncio.get_event_loop().time() - start_time + progress.update(progress_task, completed=elapsed) + + except KeyboardInterrupt: + pass # Ctrl + C else: - try: - metadata = get_metadata_table("subnetslist") - rows = json.loads(metadata["rows"]) - except sqlite3.OperationalError: + # Non-live mode + subnets, global_weights, identities = await fetch_subnet_data() + table = create_table(subnets, global_weights, identities) + console.print(table) + + display_table = Prompt.ask( + "\nPress Enter to view column descriptions or type 'q' to skip:", + choices=["", "q"], + default="", + ).lower() + + if display_table == "q": + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]Column descriptions skipped." + ) + else: + header = """ + [bold white]Description[/bold white]: The table displays information about each subnet. The columns are as follows: + """ + console.print(header) + description_table = Table( + show_header=False, box=box.SIMPLE, show_edge=False, show_lines=True + ) + + fields = [ + ("[bold tan]Netuid[/bold tan]", "The netuid of the subnet."), + ( + "[bold tan]Symbol[/bold tan]", + "The symbol for the subnet's dynamic TAO token.", + ), + ( + "[bold tan]Emission (τ)[/bold tan]", + "Shows how the one τ per block emission is distributed among all the subnet pools. For each subnet, this fraction is first calculated by dividing the subnet's alpha token price by the sum of all alpha prices across all the subnets. This fraction of TAO is then added to the TAO Pool (τ_in) of the subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#emissions[/blue].", + ), + ( + "[bold tan]STAKE (α_out)[/bold tan]", + "Total stake in the subnet, expressed in the subnet's alpha token currency. This is the sum of all the stakes present in all the hotkeys in this subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#stake-%CE%B1_out-or-alpha-out-%CE%B1_out[/blue].", + ), + ( + "[bold tan]TAO Reserves (τ_in)[/bold tan]", + 'Number of TAO in the TAO reserves of the pool for this subnet. Attached to every subnet is a subnet pool, containing a TAO reserve and the alpha reserve. See also "Alpha Pool (α_in)" description. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].', + ), + ( + "[bold tan]Alpha Reserves (α_in)[/bold tan]", + "Number of subnet alpha tokens in the alpha reserves of the pool for this subnet. This reserve, together with 'TAO Pool (τ_in)', form the subnet pool for every subnet. This can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#subnet-pool[/blue].", + ), + ( + "[bold tan]RATE (τ_in/α_in)[/bold tan]", + 'Exchange rate between TAO and subnet dTAO token. Calculated as the reserve ratio: (TAO Pool (τ_in) / Alpha Pool (α_in)). Note that the terms relative price, alpha token price, alpha price are the same as exchange rate. This rate can change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#rate-%CF%84_in%CE%B1_in[/blue].', + ), + ( + "[bold tan]Tempo (k/n)[/bold tan]", + 'The tempo status of the subnet. Represented as (k/n) where "k" is the number of blocks elapsed since the last tempo and "n" is the total number of blocks in the tempo. The number "n" is a subnet hyperparameter and does not change every block. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#tempo-kn[/blue].', + ), + ( + "[bold tan]Local weight coeff (γ)[/bold tan]", + "This is the global_split coefficient. It is a multiplication factor between 0 and 1, and it controls the balance between a validator's normalized global and local weights. In effect, the global_split parameter controls the balance between the validator hotkey's local and global influence. This is a subnet parameter. \nFor more, see [blue]https://new-docs-50g07lci2-rajkaramchedus-projects.vercel.app/dynamic-tao/dtao-guide#global-split[/blue].", + ), + ] + + description_table.add_column("Field", no_wrap=True, style="bold tan") + description_table.add_column("Description", overflow="fold") + for field_name, description in fields: + description_table.add_row(field_name, description) + console.print(description_table) + + +async def show( + subtensor: "SubtensorInterface", + netuid: int, + max_rows: Optional[int] = None, + delegate_selection: bool = False, + verbose: bool = False, + prompt: bool = True, +) -> Optional[str]: + async def show_root(): + all_subnets = await subtensor.get_all_subnet_dynamic_info() + root_info = all_subnets[0] + + hex_bytes_result, identities, old_identities = await asyncio.gather( + subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[0], + ), + subtensor.query_all_identities(), + subtensor.get_delegate_identities(), + ) + + if (bytes_result := hex_bytes_result) is None: + err_console.print("The root subnet does not exist") + return + + if bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(bytes_result[2:]) + + root_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) + if len(root_state.hotkeys) == 0: err_console.print( - "[red]Error[/red] Unable to retrieve table data. This is usually caused by attempting to use " - "`--reuse-last` before running the command a first time. In rare cases, this could also be due to " - "a corrupted database. Re-run the command (do not use `--reuse-last`) and see if that resolves your " - "issue." + "The root-subnet is currently empty with 0 UIDs registered." ) return - if not html_output: + table = Table( - title=f"[underline dark_orange]Subnets[/underline dark_orange]\n[dark_orange]Network: {metadata['network']}[/dark_orange]\n", + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Root Network\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -268,92 +631,383 @@ async def _get_all_subnets_info(): show_lines=False, pad_edge=True, ) - + # if delegate_selection: + # table.add_column("#", style="cyan", justify="right") + table.add_column("[bold white]Position", style="white", justify="center") + table.add_column( + f"[bold white] TAO ({Balance.get_unit(0)})", + style=COLOR_PALETTE["STAKE"]["TAO"], + justify="center", + ) table.add_column( - "[bold white]NETUID", - footer=f"[white]{metadata['netuid_count']}[/white]", - style="white", + f"[bold white]Stake ({Balance.get_unit(0)})", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="center", ) table.add_column( - "[bold white]N", - footer=f"[white]{metadata['N']}[/white]", - style="bright_cyan", + f"[bold white]Emission ({Balance.get_unit(0)}/block)", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + justify="center", + ) + table.add_column( + "[bold white]Hotkey", + style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + justify="center", + ) + table.add_column( + "[bold white]Coldkey", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + justify="center", + ) + table.add_column( + "[bold white]Identity", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + justify="left", + ) + + sorted_hotkeys = sorted( + enumerate(root_state.hotkeys), + key=lambda x: root_state.global_stake[x[0]], + reverse=True, + ) + sorted_rows = [] + sorted_hks_delegation = [] + for pos, (idx, hk) in enumerate(sorted_hotkeys): + 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 + ) + total_emission_per_block += subnet.alpha_to_tao( + Balance.from_rao(emission_on_subnet) + ) + + # Get identity for this validator + coldkey_identity = identities.get(root_state.coldkeys[idx], {}).get("name", "") + hotkey_identity = old_identities.get(root_state.hotkeys[idx]) + validator_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "") + + sorted_rows.append( + ( + str((pos + 1)), + str(root_state.global_stake[idx]), + str(root_state.local_stake[idx]), + f"{(total_emission_per_block)}", + f"{root_state.hotkeys[idx][:6]}" if not verbose else f"{root_state.hotkeys[idx]}", + f"{root_state.coldkeys[idx][:6]}" if not verbose else f"{root_state.coldkeys[idx]}", + validator_identity, + ) + ) + sorted_hks_delegation.append(root_state.hotkeys[idx]) + + for pos, row in enumerate(sorted_rows, 1): + table_row = [] + # if delegate_selection: + # table_row.append(str(pos)) + table_row.extend(row) + table.add_row(*table_row) + if delegate_selection and pos == max_rows: + break + # Print the table + console.print(table) + console.print("\n") + + if not delegate_selection: + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Root Network (Subnet 0)[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.price.tao:.4f} τ/{root_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{root_info.symbol} 0[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol} {root_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{root_info.symbol}{root_info.alpha_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.symbol} {root_info.alpha_out.tao:,.5f}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{root_info.blocks_since_last_step}/{root_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + ) + console.print( + """ + Description: + The table displays the root subnet participants and their metrics. + The columns are as follows: + - Position: The sorted position of the hotkey by total TAO. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - Stake: The stake balance of this hotkey on root (measured in TAO). + - Emission: The emission accrued to this hotkey across all subnets every block measured in TAO. + - Hotkey: The hotkey ss58 address. + - Coldkey: The coldkey ss58 address. + """ + ) + if delegate_selection: + while True: + selection = Prompt.ask( + "\nEnter the position of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", + default="" + ) + + if selection == "": + return None + + try: + idx = int(selection) + if 1 <= idx <= max_rows: + selected_hotkey = sorted_hks_delegation[idx - 1] + row_data = sorted_rows[idx - 1] + identity = row_data[6] + identity_str = f" ({identity})" if identity else "" + console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{selected_hotkey}{identity_str}") + + return selected_hotkey + else: + console.print(f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") + + async def show_subnet(netuid_: int): + subnet_info, hex_bytes_result, identities, old_identities = await asyncio.gather( + subtensor.get_subnet_dynamic_info(netuid_), + subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[netuid_], + ), + subtensor.query_all_identities(), + subtensor.get_delegate_identities(), + ) + owner_ss58 = subnet_info.owner if subnet_info else "" + owner_identity = identities.get(owner_ss58, {}).get("name", old_identities.get(owner_ss58).display if old_identities.get(owner_ss58) else "") + + if (bytes_result := hex_bytes_result) is None: + err_console.print(f"Subnet {netuid_} does not exist") + return + + if bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(bytes_result[2:]) + + subnet_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) + if subnet_info is None: + err_console.print(f"Subnet {netuid_} does not exist") + return + elif len(subnet_state.hotkeys) == 0: + err_console.print( + f"Subnet {netuid_} is currently empty with 0 UIDs registered." + ) + return + + # Define table properties + table = Table( + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}" + f"{': ' + SUBNETS.get(netuid_, '') if SUBNETS.get(netuid_) else ''}" + f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\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, + ) + + # Add index for selection if selecting delegates + if delegate_selection: + table.add_column("#", style="cyan", justify="right") + + rows = [] + emission_sum = sum( + [ + subnet_state.emission[idx].tao + for idx in range(len(subnet_state.emission)) + ] + ) + tao_sum = Balance(0) + stake_sum = Balance(0) + relative_emissions_sum = 0 + stake_weight_sum = 0 + + for idx, hk in enumerate(subnet_state.hotkeys): + hotkey_block_emission = ( + subnet_state.emission[idx].tao / emission_sum + if emission_sum != 0 + else 0 + ) + relative_emissions_sum += hotkey_block_emission + tao_sum += subnet_state.global_stake[idx] + stake_sum += subnet_state.local_stake[idx] + stake_weight_sum += subnet_state.stake_weight[idx] + + # Get identity for this uid + coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get("name", "") + hotkey_identity = old_identities.get(subnet_state.hotkeys[idx]) + uid_identity = coldkey_identity if coldkey_identity else (hotkey_identity.display if hotkey_identity else "~") + + rows.append( + ( + str(idx), # UID + str(subnet_state.global_stake[idx]), # TAO + f"{subnet_state.local_stake[idx].tao:.4f} {subnet_info.symbol}", # Stake + f"{subnet_state.stake_weight[idx]:.4f}", # Weight + # str(subnet_state.dividends[idx]), + f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Dividends + str(subnet_state.incentives[idx]), # Incentive + # f"{Balance.from_tao(hotkey_block_emission).set_unit(netuid_).tao:.5f}", # Emissions relative + f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.5f} {subnet_info.symbol}", # Emissions + f"{subnet_state.hotkeys[idx][:6]}" if not verbose else f"{subnet_state.hotkeys[idx]}", # Hotkey + f"{subnet_state.coldkeys[idx][:6]}" if not verbose else f"{subnet_state.coldkeys[idx]}", # Coldkey + uid_identity, # Identity + ) + ) + + # Sort rows by stake + sorted_rows = sorted( + rows, + key=lambda x: float(str(x[2]).split()[0].replace(",", "")), + reverse=True + ) + + # Add columns to the table + table.add_column("UID", style="grey89", no_wrap=True, justify="center") + table.add_column( + f"TAO({Balance.get_unit(0)})", + style=COLOR_PALETTE["STAKE"]["TAO"], + no_wrap=True, justify="right", + footer=str(tao_sum), ) table.add_column( - "[bold white]MAX_N", - footer=f"[white]{metadata['MAX_N']}[/white]", - style="bright_cyan", + f"Stake({Balance.get_unit(netuid_)})", + style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], + no_wrap=True, justify="right", + footer=f"{stake_sum.set_unit(subnet_info.netuid)}", ) table.add_column( - "[bold white]EMISSION", style="light_goldenrod2", justify="right" + f"Weight({Balance.get_unit(0)}•{Balance.get_unit(netuid_)})", + style="blue", + no_wrap=True, + justify="center", + footer=f"{stake_weight_sum:.3f}", ) - table.add_column("[bold white]TEMPO", style="rgb(42,161,152)", justify="right") - table.add_column("[bold white]RECYCLE", style="light_salmon3", justify="right") - table.add_column("[bold white]POW", style="medium_purple", justify="right") table.add_column( - "[bold white]SUDO", style="bright_magenta", justify="right", overflow="fold" + "Dividends", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + no_wrap=True, + justify="center", + footer=f"{relative_emissions_sum:.3f}", ) + table.add_column("Incentive", style="#5fd7ff", no_wrap=True, justify="center") - for row in rows: - table.add_row(*row) + # Hiding relative emissions for now + # table.add_column( + # "Emissions", + # style="light_goldenrod2", + # no_wrap=True, + # justify="center", + # footer=f"{relative_emissions_sum:.3f}", + # ) + table.add_column( + f"Emissions ({Balance.get_unit(netuid_)})", + style=COLOR_PALETTE["POOLS"]["EMISSION"], + no_wrap=True, + justify="center", + footer=str(Balance.from_tao(emission_sum).set_unit(subnet_info.netuid)), + ) + table.add_column( + "Hotkey", + style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + no_wrap=True, + justify="center", + ) + table.add_column( + "Coldkey", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + no_wrap=True, + justify="center", + ) + table.add_column( + "Identity", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + no_wrap=True, + justify="left", + ) + for pos, row in enumerate(sorted_rows, 1): + table_row = [] + if delegate_selection: + table_row.append(str(pos)) + table_row.extend(row) + table.add_row(*table_row) + if delegate_selection and pos == max_rows: + break + # Print the table + console.print("\n\n") console.print(table) - console.print( - dedent( + console.print("\n") + + if not delegate_selection: + subnet_name = SUBNETS.get(netuid_, '') + subnet_name_display = f": {subnet_name}" if subnet_name else "" + + console.print( + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner}{' (' + 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']}]" + f"\n TAO Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]τ {subnet_info.tao_in.tao:,.4f}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Alpha Pool: [{COLOR_PALETTE['POOLS']['ALPHA_IN']}]{subnet_info.alpha_in.tao:,.4f} {subnet_info.symbol}[/{COLOR_PALETTE['POOLS']['ALPHA_IN']}]" + f"\n Stake: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.alpha_out.tao:,.5f} {subnet_info.symbol}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + f"\n Tempo: [{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]{subnet_info.blocks_since_last_step}/{subnet_info.tempo}[/{COLOR_PALETTE['STAKE']['STAKE_ALPHA']}]" + ) + console.print( """ - Description: - The table displays the list of subnets registered in the Bittensor network. - - NETUID: The network identifier of the subnet. - - N: The current UIDs registered to the network. - - MAX_N: The total UIDs allowed on the network. - - EMISSION: The emission accrued by this subnet in the network. - - TEMPO: A duration of a number of blocks. Several subnet events occur at the end of every tempo period. - - RECYCLE: Cost to register to the subnet. - - POW: Proof of work metric of the subnet. - - SUDO: Owner's identity. - """ + Description: + The table displays the subnet participants and their metrics. + The columns are as follows: + - UID: The hotkey index in the subnet. + - TAO: The sum of all TAO balances for this hotkey accross all subnets. + - Stake: The stake balance of this hotkey on this subnet. + - Weight: The stake-weight of this hotkey on this subnet. Computed as an average of the normalized TAO and Stake columns of this subnet. + - Dividends: Validating dividends earned by the hotkey. + - Incentives: Mining incentives earned by the hotkey (always zero in the RAO demo.) + - Emission: The emission accrued to this hokey on this subnet every block (in staking units). + - Hotkey: The hotkey ss58 address. + - Coldkey: The coldkey ss58 address. + """ ) - ) + + if delegate_selection: + while True: + selection = Prompt.ask( + "\nEnter the number of the delegate you want to stake to [dim](or press Enter to cancel)[/dim]", + default="" + ) + + if selection == "": + return None + + try: + idx = int(selection) + if 1 <= idx <= max_rows: + uid = int(sorted_rows[idx-1][0]) + hotkey = subnet_state.hotkeys[uid] + row_data = sorted_rows[idx-1] + identity = row_data[9] + identity_str = f" ({identity})" if identity else "" + console.print(f"\nSelected delegate: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{hotkey}{identity_str}") + return hotkey + else: + console.print(f"[red]Invalid selection. Please enter a number between 1 and {max_rows}[/red]") + except ValueError: + console.print("[red]Please enter a valid number[/red]") + + return None + + if netuid == 0: + result = await show_root() + return result else: - render_table( - "subnetslist", - f"Subnets List | Network: {metadata['network']} - " - f"Netuids: {metadata['netuid_count']} - N: {metadata['N']}", - columns=[ - {"title": "NetUID", "field": "NETUID"}, - {"title": "N", "field": "N"}, - {"title": "MAX_N", "field": "MAX_N", "customFormatter": "millify"}, - { - "title": "EMISSION", - "field": "EMISSION", - "formatter": "money", - "formatterParams": { - "symbolAfter": "p", - "symbol": "%", - "precision": 2, - }, - }, - {"title": "Tempo", "field": "TEMPO"}, - { - "title": "Recycle", - "field": "RECYCLE", - "formatter": "money", - "formatterParams": {"symbol": "τ", "precision": 5}, - }, - { - "title": "Difficulty", - "field": "DIFFICULTY", - "customFormatter": "millify", - }, - {"title": "sudo", "field": "SUDO"}, - ], - ) - - -async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: + result = await show_subnet(netuid) + return result + +async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: """View locking cost of creating a new subnetwork""" with console.status( f":satellite:Retrieving lock cost from {subtensor.network}...", @@ -365,11 +1019,13 @@ async def lock_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: params=[], ) if lc: - lock_cost_ = Balance(lc) - console.print(f"Subnet lock cost: [green]{lock_cost_}[/green]") - return lock_cost_ + burn_cost_ = Balance(lc) + console.print( + f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{burn_cost_}" + ) + return burn_cost_ else: - err_console.print("Subnet lock cost: [red]Failed to get subnet lock cost[/red]") + err_console.print("Subnet burn cost: [red]Failed to get subnet burn cost[/red]") return None @@ -381,12 +1037,42 @@ async def create(wallet: Wallet, subtensor: "SubtensorInterface", prompt: bool): if success and prompt: # Prompt for user to set identity. do_set_identity = Confirm.ask( - "Subnetwork registered successfully. Would you like to set your identity?" + "Would you like to set your [blue]identity?[/blue]" ) if do_set_identity: - id_prompts = set_id_prompts(validator=False) - await set_id(wallet, subtensor, *id_prompts, prompt=prompt) + current_identity = await get_id( + subtensor, wallet.coldkeypub.ss58_address, "Current on-chain identity" + ) + if prompt: + if not Confirm.ask( + "\nCost to register an [blue]Identity[/blue] is [blue]0.1 TAO[/blue]," + " are you sure you wish to continue?" + ): + console.print(":cross_mark: Aborted!") + raise typer.Exit() + + identity = prompt_for_identity( + current_identity=current_identity, + name=None, + web_url=None, + image_url=None, + discord_handle=None, + description=None, + additional_info=None, + ) + + await set_id( + wallet, + subtensor, + identity["name"], + identity["url"], + identity["image"], + identity["discord"], + identity["description"], + identity["additional"], + prompt, + ) async def pow_register( @@ -451,25 +1137,77 @@ async def register( return if prompt: + # TODO make this a reusable function, also used in subnets list + # Show creation table. + table = Table( + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Register to [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]netuid: {netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\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, + ) + table.add_column( + "Netuid", style="rgb(253,246,227)", no_wrap=True, justify="center" + ) + table.add_column( + "Symbol", + style=COLOR_PALETTE["GENERAL"]["SYMBOL"], + no_wrap=True, + justify="center", + ) + table.add_column( + f"Cost ({Balance.get_unit(0)})", + style=COLOR_PALETTE["POOLS"]["TAO"], + no_wrap=True, + justify="center", + ) + table.add_column( + "Hotkey", + style=COLOR_PALETTE["GENERAL"]["HOTKEY"], + no_wrap=True, + justify="center", + ) + table.add_column( + "Coldkey", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], + no_wrap=True, + justify="center", + ) + table.add_row( + str(netuid), + f"{Balance.get_unit(netuid)}", + f"τ {current_recycle.tao:.4f}", + f"{wallet.hotkey.ss58_address}", + f"{wallet.coldkeypub.ss58_address}", + ) + console.print(table) if not ( Confirm.ask( - f"Your balance is: [bold green]{balance}[/bold green]\nThe cost to register by recycle is " - f"[bold red]{current_recycle}[/bold red]\nDo you want to continue?", + f"Your balance is: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\nThe cost to register by recycle is " + f"[{COLOR_PALETTE['GENERAL']['COST']}]{current_recycle}[/{COLOR_PALETTE['GENERAL']['COST']}]\nDo you want to continue?", default=False, ) ): return - await burned_register_extrinsic( - subtensor, - wallet=wallet, - netuid=netuid, - prompt=False, - recycle_amount=current_recycle, - old_balance=balance, - ) + if netuid == 0: + await root_register_extrinsic(subtensor, wallet=wallet) + else: + await burned_register_extrinsic( + subtensor, + wallet=wallet, + netuid=netuid, + prompt=False, + old_balance=balance, + ) +# TODO: Confirm emissions, incentive, Dividends are to be fetched from subnet_state or keep NeuronInfo async def metagraph_cmd( subtensor: Optional["SubtensorInterface"], netuid: Optional[int], @@ -507,14 +1245,33 @@ async def metagraph_cmd( subtensor.substrate.get_block_number(block_hash=block_hash), ) + hex_bytes_result = await subtensor.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_subnet_state", + params=[netuid], + ) + if not (bytes_result := hex_bytes_result): + err_console.print(f"Subnet {netuid} does not exist") + return + + if bytes_result.startswith("0x"): + bytes_result = bytes.fromhex(bytes_result[2:]) + + subnet_state: "SubnetState" = SubnetState.from_vec_u8(bytes_result) + difficulty = int(difficulty_) total_issuance = Balance.from_rao(total_issuance_) metagraph = MiniGraph( - netuid=netuid, neurons=neurons, subtensor=subtensor, block=block + netuid=netuid, + neurons=neurons, + subtensor=subtensor, + subnet_state=subnet_state, + block=block, ) table_data = [] db_table = [] - total_stake = 0.0 + total_global_stake = 0.0 + total_local_stake = 0.0 total_rank = 0.0 total_validator_trust = 0.0 total_trust = 0.0 @@ -527,7 +1284,9 @@ async def metagraph_cmd( ep = metagraph.axons[uid] row = [ str(neuron.uid), - "{:.5f}".format(metagraph.total_stake[uid]), + "{:.4f}".format(metagraph.global_stake[uid]), + "{:.4f}".format(metagraph.local_stake[uid]), + "{:.4f}".format(metagraph.stake_weights[uid]), "{:.5f}".format(metagraph.ranks[uid]), "{:.5f}".format(metagraph.trust[uid]), "{:.5f}".format(metagraph.consensus[uid]), @@ -548,7 +1307,9 @@ async def metagraph_cmd( ] db_row = [ neuron.uid, - float(metagraph.total_stake[uid]), + float(metagraph.global_stake[uid]), + float(metagraph.local_stake[uid]), + float(metagraph.stake_weights[uid]), float(metagraph.ranks[uid]), float(metagraph.trust[uid]), float(metagraph.consensus[uid]), @@ -564,7 +1325,8 @@ async def metagraph_cmd( ep.coldkey[:10], ] db_table.append(db_row) - total_stake += metagraph.total_stake[uid] + total_global_stake += metagraph.global_stake[uid] + total_local_stake += metagraph.local_stake[uid] total_rank += metagraph.ranks[uid] total_validator_trust += metagraph.validator_trust[uid] total_trust += metagraph.trust[uid] @@ -574,8 +1336,9 @@ async def metagraph_cmd( total_emission += int(metagraph.emission[uid] * 1000000000) table_data.append(row) metadata_info = { - "stake": str(Balance.from_tao(total_stake)), - "total_stake": "\u03c4{:.5f}".format(total_stake), + "total_global_stake": "\u03c4 {:.5f}".format(total_global_stake), + "total_local_stake": f"{Balance.get_unit(netuid)} " + + "{:.5f}".format(total_local_stake), "rank": "{:.5f}".format(total_rank), "validator_trust": "{:.5f}".format(total_validator_trust), "trust": "{:.5f}".format(total_trust), @@ -599,7 +1362,9 @@ async def metagraph_cmd( "metagraph", columns=[ ("UID", "INTEGER"), - ("STAKE", "REAL"), + ("GLOBAL_STAKE", "REAL"), + ("LOCAL_STAKE", "REAL"), + ("STAKE_WEIGHT", "REAL"), ("RANK", "REAL"), ("TRUST", "REAL"), ("CONSENSUS", "REAL"), @@ -643,11 +1408,26 @@ async def metagraph_cmd( columns=[ {"title": "UID", "field": "UID"}, { - "title": "Stake", - "field": "STAKE", + "title": "Global Stake", + "field": "GLOBAL_STAKE", "formatter": "money", "formatterParams": {"symbol": "τ", "precision": 5}, }, + { + "title": "Local Stake", + "field": "LOCAL_STAKE", + "formatter": "money", + "formatterParams": { + "symbol": f"{Balance.get_unit(netuid)}", + "precision": 5, + }, + }, + { + "title": "Stake Weight", + "field": "STAKE_WEIGHT", + "formatter": "money", + "formatterParams": {"precision": 5}, + }, { "title": "Rank", "field": "RANK", @@ -711,19 +1491,40 @@ async def metagraph_cmd( ratio=0.75, ), ), - "STAKE": ( + "GLOBAL_STAKE": ( 1, Column( - "[bold white]STAKE(\u03c4)", - footer=metadata_info["total_stake"], + "[bold white]GLOBAL STAKE(\u03c4)", + footer=metadata_info["total_global_stake"], style="bright_cyan", justify="right", no_wrap=True, + ratio=1.6, + ), + ), + "LOCAL_STAKE": ( + 2, + Column( + f"[bold white]LOCAL STAKE({Balance.get_unit(netuid)})", + footer=metadata_info["total_local_stake"], + style="bright_green", + justify="right", + no_wrap=True, ratio=1.5, ), ), + "STAKE_WEIGHT": ( + 3, + Column( + f"[bold white]WEIGHT (\u03c4x{Balance.get_unit(netuid)})", + style="purple", + justify="right", + no_wrap=True, + ratio=1.3, + ), + ), "RANK": ( - 2, + 4, Column( "[bold white]RANK", footer=metadata_info["rank"], @@ -734,7 +1535,7 @@ async def metagraph_cmd( ), ), "TRUST": ( - 3, + 5, Column( "[bold white]TRUST", footer=metadata_info["trust"], @@ -745,7 +1546,7 @@ async def metagraph_cmd( ), ), "CONSENSUS": ( - 4, + 6, Column( "[bold white]CONSENSUS", footer=metadata_info["consensus"], @@ -756,7 +1557,7 @@ async def metagraph_cmd( ), ), "INCENTIVE": ( - 5, + 7, Column( "[bold white]INCENTIVE", footer=metadata_info["incentive"], @@ -767,7 +1568,7 @@ async def metagraph_cmd( ), ), "DIVIDENDS": ( - 6, + 8, Column( "[bold white]DIVIDENDS", footer=metadata_info["dividends"], @@ -778,7 +1579,7 @@ async def metagraph_cmd( ), ), "EMISSION": ( - 7, + 9, Column( "[bold white]EMISSION(\u03c1)", footer=metadata_info["emission"], @@ -789,7 +1590,7 @@ async def metagraph_cmd( ), ), "VTRUST": ( - 8, + 10, Column( "[bold white]VTRUST", footer=metadata_info["validator_trust"], @@ -800,21 +1601,21 @@ async def metagraph_cmd( ), ), "VAL": ( - 9, + 11, Column( "[bold white]VAL", justify="center", style="bright_white", no_wrap=True, - ratio=0.4, + ratio=0.7, ), ), "UPDATED": ( - 10, + 12, Column("[bold white]UPDATED", justify="right", no_wrap=True, ratio=1), ), "ACTIVE": ( - 11, + 13, Column( "[bold white]ACTIVE", justify="center", @@ -824,7 +1625,7 @@ async def metagraph_cmd( ), ), "AXON": ( - 12, + 14, Column( "[bold white]AXON", justify="left", @@ -834,7 +1635,7 @@ async def metagraph_cmd( ), ), "HOTKEY": ( - 13, + 15, Column( "[bold white]HOTKEY", justify="center", @@ -844,7 +1645,7 @@ async def metagraph_cmd( ), ), "COLDKEY": ( - 14, + 16, Column( "[bold white]COLDKEY", justify="center", @@ -877,7 +1678,7 @@ async def metagraph_cmd( f"Net: [bright_cyan]{metadata_info['net']}[/bright_cyan], " f"Block: [bright_cyan]{metadata_info['block']}[/bright_cyan], " f"N: [bright_green]{metadata_info['N0']}[/bright_green]/[bright_red]{metadata_info['N1']}[/bright_red], " - f"Stake: [dark_orange]{metadata_info['stake']}[/dark_orange], " + f"Total Local Stake: [dark_orange]{metadata_info['total_local_stake']}[/dark_orange], " f"Issuance: [bright_blue]{metadata_info['issuance']}[/bright_blue], " f"Difficulty: [bright_cyan]{metadata_info['difficulty']}[/bright_cyan]\n" ), diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 6ebbb0eca..4a9ee5da1 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,12 +1,14 @@ import asyncio -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Union, Optional from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich import box from rich.table import Column, Table +from rich.prompt import Confirm +from scalecodec import GenericCall -from bittensor_cli.src import HYPERPARAMS +from bittensor_cli.src import HYPERPARAMS, DelegatesDetails, COLOR_PALETTE, SUBNETS from bittensor_cli.src.bittensor.chain_data import decode_account_id from bittensor_cli.src.bittensor.utils import ( console, @@ -17,7 +19,10 @@ ) if TYPE_CHECKING: - from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + ProposalVoteData, + ) # helpers and extrinsics @@ -113,7 +118,7 @@ async def set_hyperparameter_extrinsic( return False with console.status( - f":satellite: Setting hyperparameter {parameter} to {value} on subnet: {netuid} ...", + 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']}] ...", spinner="earth", ): substrate = subtensor.substrate @@ -162,11 +167,287 @@ async def set_hyperparameter_extrinsic( # Successful registration, final check for membership else: console.print( - f":white_heavy_check_mark: [green]Hyperparameter {parameter} changed to {value}[/green]" + f":white_heavy_check_mark: [dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" ) return True +async def _get_senate_members( + subtensor: "SubtensorInterface", block_hash: Optional[str] = None +) -> list[str]: + """ + Gets all members of the senate on the given subtensor's network + + :param subtensor: SubtensorInterface object to use for the query + + :return: list of the senate members' ss58 addresses + """ + senate_members = await subtensor.substrate.query( + module="SenateMembers", + storage_function="Members", + params=None, + block_hash=block_hash, + ) + try: + return [ + decode_account_id(i[x][0]) for i in senate_members for x in range(len(i)) + ] + except (IndexError, TypeError): + err_console.print("Unable to retrieve senate members.") + return [] + + +async def _get_proposals( + subtensor: "SubtensorInterface", block_hash: str +) -> dict[str, tuple[dict, "ProposalVoteData"]]: + async def get_proposal_call_data(p_hash: str) -> Optional[GenericCall]: + proposal_data = await subtensor.substrate.query( + module="Triumvirate", + storage_function="ProposalOf", + block_hash=block_hash, + params=[p_hash], + ) + return proposal_data + + ph = await subtensor.substrate.query( + module="Triumvirate", + storage_function="Proposals", + params=None, + block_hash=block_hash, + ) + + try: + proposal_hashes: list[str] = [ + f"0x{bytes(ph[0][x][0]).hex()}" for x in range(len(ph[0])) + ] + except (IndexError, TypeError): + err_console.print("Unable to retrieve proposal vote data") + return {} + + call_data_, vote_data_ = await asyncio.gather( + asyncio.gather(*[get_proposal_call_data(h) for h in proposal_hashes]), + asyncio.gather(*[subtensor.get_vote_data(h) for h in proposal_hashes]), + ) + return { + proposal_hash: (cd, vd) + for cd, vd, proposal_hash in zip(call_data_, vote_data_, proposal_hashes) + } + + +def display_votes( + vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails] +) -> str: + vote_list = list() + + for address in vote_data.ayes: + vote_list.append( + "{}: {}".format( + delegate_info[address].display if address in delegate_info else address, + "[bold green]Aye[/bold green]", + ) + ) + + for address in vote_data.nays: + vote_list.append( + "{}: {}".format( + delegate_info[address].display if address in delegate_info else address, + "[bold red]Nay[/bold red]", + ) + ) + + return "\n".join(vote_list) + + +def format_call_data(call_data: dict) -> str: + # Extract the module and call details + module, call_details = next(iter(call_data.items())) + + # Extract the call function name and arguments + call_info = call_details[0] + call_function, call_args = next(iter(call_info.items())) + + # Extract the argument, handling tuple values + formatted_args = ", ".join( + str(arg[0]) if isinstance(arg, tuple) else str(arg) + for arg in call_args.values() + ) + + # Format the final output string + return f"{call_function}({formatted_args})" + + +def _validate_proposal_hash(proposal_hash: str) -> bool: + if proposal_hash[0:2] != "0x" or len(proposal_hash) != 66: + return False + else: + return True + + +async def _is_senate_member(subtensor: "SubtensorInterface", hotkey_ss58: str) -> bool: + """ + Checks if a given neuron (identified by its hotkey SS58 address) is a member of the Bittensor senate. + The senate is a key governance body within the Bittensor network, responsible for overseeing and + approving various network operations and proposals. + + :param subtensor: SubtensorInterface object to use for the query + :param hotkey_ss58: The `SS58` address of the neuron's hotkey. + + :return: `True` if the neuron is a senate member at the given block, `False` otherwise. + + This function is crucial for understanding the governance dynamics of the Bittensor network and for + identifying the neurons that hold decision-making power within the network. + """ + + senate_members = await _get_senate_members(subtensor) + + if not hasattr(senate_members, "count"): + return False + + return senate_members.count(hotkey_ss58) > 0 + + +async def vote_senate_extrinsic( + subtensor: "SubtensorInterface", + wallet: Wallet, + proposal_hash: str, + proposal_idx: int, + vote: bool, + wait_for_inclusion: bool = False, + wait_for_finalization: bool = True, + prompt: bool = False, +) -> bool: + """Votes ayes or nays on proposals. + + :param subtensor: The SubtensorInterface object to use for the query + :param wallet: Bittensor wallet object, with coldkey and hotkey unlocked. + :param proposal_hash: The hash of the proposal for which voting data is requested. + :param proposal_idx: The index of the proposal to vote. + :param vote: Whether to vote aye or nay. + :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns + `False` if the extrinsic fails to enter the block within the timeout. + :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, + or returns `False` if the extrinsic fails to be finalized within the timeout. + :param prompt: If `True`, the call waits for confirmation from the user before proceeding. + + :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for + finalization/inclusion, the response is `True`. + """ + + if prompt: + # Prompt user for confirmation. + if not Confirm.ask(f"Cast a vote of {vote}?"): + return False + + with console.status(":satellite: Casting vote..", spinner="aesthetic"): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="vote", + call_params={ + "hotkey": wallet.hotkey.ss58_address, + "proposal": proposal_hash, + "index": proposal_idx, + "approve": vote, + }, + ) + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, wallet, wait_for_inclusion, wait_for_finalization + ) + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + await asyncio.sleep(0.5) + return False + # Successful vote, final check for data + else: + if vote_data := await subtensor.get_vote_data(proposal_hash): + if ( + vote_data.ayes.count(wallet.hotkey.ss58_address) > 0 + or vote_data.nays.count(wallet.hotkey.ss58_address) > 0 + ): + console.print(":white_heavy_check_mark: [green]Vote cast.[/green]") + return True + else: + # hotkey not found in ayes/nays + err_console.print( + ":cross_mark: [red]Unknown error. Couldn't find vote.[/red]" + ) + return False + else: + return False + + +async def set_take_extrinsic( + subtensor: "SubtensorInterface", + wallet: Wallet, + delegate_ss58: str, + take: float = 0.0, +) -> bool: + """ + Set delegate hotkey take + + :param subtensor: SubtensorInterface (initialized) + :param wallet: The wallet containing the hotkey to be nominated. + :param delegate_ss58: Hotkey + :param take: Delegate take on subnet ID + + :return: `True` if the process is successful, `False` otherwise. + + This function is a key part of the decentralized governance mechanism of Bittensor, allowing for the + dynamic selection and participation of validators in the network's consensus process. + """ + + # Calculate u16 representation of the take + take_u16 = int(take * 0xFFFF) + + print_verbose("Checking current take") + # Check if the new take is greater or lower than existing take or if existing is set + current_take = await get_current_take(subtensor, wallet) + current_take_u16 = int(float(current_take) * 0xFFFF) + + if take_u16 == current_take_u16: + console.print("Nothing to do, take hasn't changed") + return True + + if current_take_u16 < take_u16: + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Increasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%." + ) + with console.status( + f":satellite: Sending decrease_take_extrinsic call on [white]{subtensor}[/white] ..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="increase_take", + call_params={ + "hotkey": delegate_ss58, + "take": take_u16, + }, + ) + success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + + else: + console.print( + f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Decreasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%." + ) + with console.status( + f":satellite: Sending increase_take_extrinsic call on [white]{subtensor}[/white] ..." + ): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="decrease_take", + call_params={ + "hotkey": delegate_ss58, + "take": take_u16, + }, + ) + success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + + if not success: + err_console.print(err) + else: + console.print(":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]") + return success + + # commands @@ -215,11 +496,14 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): subnet = await subtensor.get_subnet_hyperparameters(netuid) table = Table( - Column("[white]HYPERPARAMETER", style="bright_magenta"), - Column("[white]VALUE", style="light_goldenrod2"), - Column("[white]NORMALIZED", style="light_goldenrod3"), - title=f"[underline dark_orange]\nSubnet Hyperparameters[/underline dark_orange]\n NETUID: [dark_orange]" - f"{netuid}[/dark_orange] - Network: [dark_orange]{subtensor.network}[/dark_orange]\n", + Column("[white]HYPERPARAMETER", style=COLOR_PALETTE['SUDO']['HYPERPARAMETER']), + Column("[white]VALUE", style=COLOR_PALETTE['SUDO']['VALUE']), + Column("[white]NORMALIZED", style=COLOR_PALETTE['SUDO']['NORMALIZED']), + title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: " + f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}" + f"{f' ({SUBNETS.get(netuid)})' if SUBNETS.get(netuid) else ''}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, width=None, pad_edge=False, @@ -234,3 +518,209 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): console.print(table) return True + + +async def get_senate(subtensor: "SubtensorInterface"): + """View Bittensor's senate memebers""" + with console.status( + f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", + spinner="aesthetic", + ) as status: + print_verbose("Fetching senate members", status) + senate_members = await _get_senate_members(subtensor) + + print_verbose("Fetching member details from Github and on-chain identities") + delegate_info: dict[ + str, DelegatesDetails + ] = await subtensor.get_delegate_identities() + + table = Table( + Column( + "[bold white]NAME", + style="bright_cyan", + no_wrap=True, + ), + Column( + "[bold white]ADDRESS", + style="bright_magenta", + no_wrap=True, + ), + title=f"[underline dark_orange]Senate[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}\n", + show_footer=True, + show_edge=False, + expand=False, + border_style="bright_black", + leading=True, + ) + + for ss58_address in senate_members: + table.add_row( + ( + delegate_info[ss58_address].display + if ss58_address in delegate_info + else "~" + ), + ss58_address, + ) + + return console.print(table) + + +async def proposals(subtensor: "SubtensorInterface"): + console.print( + ":satellite: Syncing with chain: [white]{}[/white] ...".format( + subtensor.network + ) + ) + print_verbose("Fetching senate members & proposals") + block_hash = await subtensor.substrate.get_chain_head() + senate_members, all_proposals = await asyncio.gather( + _get_senate_members(subtensor, block_hash), + _get_proposals(subtensor, block_hash), + ) + + print_verbose("Fetching member information from Chain") + registered_delegate_info: dict[ + str, DelegatesDetails + ] = await subtensor.get_delegate_identities() + + table = Table( + Column( + "[white]HASH", + style="light_goldenrod2", + no_wrap=True, + ), + Column("[white]THRESHOLD", style="rgb(42,161,152)"), + Column("[white]AYES", style="green"), + Column("[white]NAYS", style="red"), + Column( + "[white]VOTES", + style="rgb(50,163,219)", + ), + Column("[white]END", style="bright_cyan"), + Column("[white]CALLDATA", style="dark_sea_green"), + title=f"\n[dark_orange]Proposals\t\t\nActive Proposals: {len(all_proposals)}\t\tSenate Size: {len(senate_members)}\nNetwork: {subtensor.network}", + show_footer=True, + box=box.SIMPLE_HEAVY, + pad_edge=False, + width=None, + border_style="bright_black", + ) + for hash_, (call_data, vote_data) in all_proposals.items(): + table.add_row( + hash_, + str(vote_data.threshold), + str(len(vote_data.ayes)), + str(len(vote_data.nays)), + display_votes(vote_data, registered_delegate_info), + str(vote_data.end), + format_call_data(call_data), + ) + return console.print(table) + + +async def senate_vote( + wallet: Wallet, + subtensor: "SubtensorInterface", + proposal_hash: str, + vote: bool, + prompt: bool, +) -> bool: + """Vote in Bittensor's governance protocol proposals""" + + if not proposal_hash: + err_console.print( + "Aborting: Proposal hash not specified. View all proposals with the `proposals` command." + ) + return False + elif not _validate_proposal_hash(proposal_hash): + err_console.print( + "Aborting. Proposal hash is invalid. Proposal hashes should start with '0x' and be 32 bytes long" + ) + return False + + print_verbose(f"Fetching senate status of {wallet.hotkey_str}") + if not await _is_senate_member(subtensor, hotkey_ss58=wallet.hotkey.ss58_address): + err_console.print( + f"Aborting: Hotkey {wallet.hotkey.ss58_address} isn't a senate member." + ) + return False + + # Unlock the wallet. + try: + wallet.unlock_hotkey() + wallet.unlock_coldkey() + except KeyFileError: + return False + + console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") + vote_data = await subtensor.get_vote_data(proposal_hash, reuse_block=True) + if not vote_data: + err_console.print(":cross_mark: [red]Failed[/red]: Proposal not found.") + return False + + success = await vote_senate_extrinsic( + subtensor=subtensor, + wallet=wallet, + proposal_hash=proposal_hash, + proposal_idx=vote_data.index, + vote=vote, + wait_for_inclusion=True, + wait_for_finalization=False, + prompt=prompt, + ) + + return success + + +async def get_current_take(subtensor: "SubtensorInterface", wallet: Wallet): + current_take = await subtensor.current_take(wallet.hotkey.ss58_address) + return current_take + + +async def set_take( + wallet: Wallet, subtensor: "SubtensorInterface", take: float +) -> bool: + """Set delegate take.""" + + async def _do_set_take() -> bool: + if take > 0.18 or take < 0: + err_console.print("ERROR: Take value should not exceed 18% or be below 0%") + return False + + block_hash = await subtensor.substrate.get_chain_head() + netuids_registered = await subtensor.get_netuids_for_hotkey( + wallet.hotkey.ss58_address, block_hash=block_hash + ) + if not len(netuids_registered) > 0: + err_console.print( + f"Hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] is not registered to any subnet. Please register using [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]`btcli subnets register`[{COLOR_PALETTE['GENERAL']['SUBHEADING']}] and try again." + ) + return False + + result: bool = await set_take_extrinsic( + subtensor=subtensor, + wallet=wallet, + delegate_ss58=wallet.hotkey.ss58_address, + take=take, + ) + + if not result: + err_console.print("Could not set the take") + return False + else: + new_take = await get_current_take(subtensor, wallet) + console.print(f"New take is [{COLOR_PALETTE['POOLS']['RATE']}]{new_take * 100.:.2f}%") + return True + + console.print(f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}") + + try: + wallet.unlock_hotkey() + wallet.unlock_coldkey() + except KeyFileError: + return False + + result_ = await _do_set_take() + + return result_ diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3faefd23c..34e5399a9 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -10,7 +10,7 @@ from typing import Any, Collection, Generator, Optional import aiohttp -from bittensor_wallet import Wallet +from bittensor_wallet import Wallet, Keypair from bittensor_wallet.errors import KeyFileError from bittensor_wallet.keyfile import Keyfile from fuzzywuzzy import fuzz @@ -25,7 +25,7 @@ import scalecodec import typer -from bittensor_cli.src import TYPE_REGISTRY +from bittensor_cli.src import TYPE_REGISTRY, COLOR_PALETTE from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -135,14 +135,26 @@ async def new_hotkey( wallet: Wallet, n_words: int, use_password: bool, + uri: Optional[str] = None, ): """Creates a new hotkey under this wallet.""" try: - wallet.create_new_hotkey( - n_words=n_words, - use_password=use_password, - overwrite=False, - ) + if uri: + try: + keypair = Keypair.create_from_uri(uri) + except Exception as e: + print_error(f"Failed to create keypair from URI {uri}: {str(e)}") + wallet.set_hotkey(keypair=keypair, encrypt=use_password) + console.print( + f"[dark_sea_green]Hotkey created from URI: {uri}[/dark_sea_green]" + ) + else: + wallet.create_new_hotkey( + n_words=n_words, + use_password=use_password, + overwrite=False, + ) + console.print(f"[dark_sea_green]Hotkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") @@ -151,14 +163,27 @@ async def new_coldkey( wallet: Wallet, n_words: int, use_password: bool, + uri: Optional[str] = None, ): """Creates a new coldkey under this wallet.""" try: - wallet.create_new_coldkey( - n_words=n_words, - use_password=use_password, - overwrite=False, - ) + if uri: + try: + keypair = Keypair.create_from_uri(uri) + except Exception as e: + print_error(f"Failed to create keypair from URI {uri}: {str(e)}") + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) + console.print( + f"[dark_sea_green]Coldkey created from URI: {uri}[/dark_sea_green]" + ) + else: + wallet.create_new_coldkey( + n_words=n_words, + use_password=use_password, + overwrite=False, + ) + console.print(f"[dark_sea_green]Coldkey created[/dark_sea_green]") except KeyFileError: print_error("KeyFileError: File is not writable") @@ -167,25 +192,40 @@ async def wallet_create( wallet: Wallet, n_words: int = 12, use_password: bool = True, + uri: Optional[str] = None, ): """Creates a new wallet.""" - try: - wallet.create_new_coldkey( - n_words=n_words, - use_password=use_password, - overwrite=False, + if uri: + try: + keypair = Keypair.create_from_uri(uri) + except Exception as e: + print_error(f"Failed to create keypair from URI: {str(e)}") + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) + wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False) + console.print( + f"[dark_sea_green]Wallet created from URI: {uri}[/dark_sea_green]" ) - except KeyFileError: - print_error("KeyFileError: File is not writable") + else: + try: + wallet.create_new_coldkey( + n_words=n_words, + use_password=use_password, + overwrite=False, + ) + console.print(f"[dark_sea_green]Coldkey created[/dark_sea_green]") + except KeyFileError: + print_error("KeyFileError: File is not writable") - try: - wallet.create_new_hotkey( - n_words=n_words, - use_password=False, - overwrite=False, - ) - except KeyFileError: - print_error("KeyFileError: File is not writable") + try: + wallet.create_new_hotkey( + n_words=n_words, + use_password=False, + overwrite=False, + ) + console.print(f"[dark_sea_green]Hotkey created[/dark_sea_green]") + except KeyFileError: + print_error("KeyFileError: File is not writable") def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: @@ -229,7 +269,11 @@ async def wallet_balance( """Retrieves the current balance of the specified wallet""" if ss58_addresses: coldkeys = ss58_addresses - wallet_names = [f"Provided Address {i + 1}" for i in range(len(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) + ] elif not all_balances: if not wallet.coldkeypub_file.exists_on_device(): @@ -269,28 +313,28 @@ async def wallet_balance( ), Column( "[white]Coldkey Address", - style="bright_magenta", + style=COLOR_PALETTE["GENERAL"]["COLDKEY"], no_wrap=True, ), Column( "[white]Free Balance", justify="right", - style="light_goldenrod2", + style=COLOR_PALETTE["GENERAL"]["BALANCE"], no_wrap=True, ), Column( "[white]Staked Balance", justify="right", - style="orange1", + style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], no_wrap=True, ), Column( "[white]Total Balance", justify="right", - style="green", + style=COLOR_PALETTE["GENERAL"]["BALANCE"], no_wrap=True, ), - title=f"[underline dark_orange]Wallet Coldkey Balance[/underline dark_orange]\n[dark_orange]Network: {subtensor.network}", + title=f"\n [{COLOR_PALETTE['GENERAL']['HEADER']}]Wallet Coldkey Balance\nNetwork: {subtensor.network}", show_footer=True, show_edge=False, border_style="bright_black", @@ -318,6 +362,7 @@ async def wallet_balance( ) console.print(Padding(table, (0, 0, 0, 4))) await subtensor.substrate.close() + return total_free_balance, total_staked_balance async def get_wallet_transfers(wallet_address: str) -> list[dict]: @@ -1113,17 +1158,12 @@ async def _fetch_neuron_for_netuid( """ async def neurons_lite_for_uid(uid: int) -> dict[Any, Any]: - call_definition = TYPE_REGISTRY["runtime_api"]["NeuronInfoRuntimeApi"][ - "methods" - ]["get_neurons_lite"] - data = await subtensor.encode_params( - call_definition=call_definition, params=[uid] - ) block_hash = subtensor.substrate.last_block_hash - hex_bytes_result = await subtensor.substrate.rpc_request( - method="state_call", - params=["NeuronInfoRuntimeApi_get_neurons_lite", data, block_hash], - reuse_block_hash=True, + hex_bytes_result = await subtensor.query_runtime_api( + runtime_api="NeuronInfoRuntimeApi", + method="get_neurons_lite", + params=[uid], + block_hash=block_hash, ) return hex_bytes_result @@ -1143,25 +1183,6 @@ async def _fetch_all_neurons( ) -def _partial_decode(args): - """ - Helper function for passing to ProcessPoolExecutor that decodes scale bytes based on a set return type and - rpc type registry, passing this back to the Executor with its specified netuid for easier mapping - - :param args: (return type, scale bytes object, custom rpc type registry, netuid) - - :return: (original netuid, decoded object) - """ - return_type, as_scale_bytes, custom_rpc_type_registry_, netuid_ = args - decoded = decode_scale_bytes(return_type, as_scale_bytes, custom_rpc_type_registry_) - if decoded.startswith("0x"): - bytes_result = bytes.fromhex(decoded[2:]) - else: - bytes_result = bytes.fromhex(decoded) - - return netuid_, NeuronInfoLite.list_from_vec_u8(bytes_result) - - def _process_neurons_for_netuids( netuids_with_all_neurons_hex_bytes: list[tuple[int, list[ScaleBytes]]], ) -> list[tuple[int, list[NeuronInfoLite]]]: @@ -1171,22 +1192,10 @@ def _process_neurons_for_netuids( :param netuids_with_all_neurons_hex_bytes: netuids with hex-bytes neurons :return: netuids mapped to decoded neurons """ - - def make_map(res_): - netuid_, json_result = res_ - hex_bytes_result = json_result["result"] - as_scale_bytes = scalecodec.ScaleBytes(hex_bytes_result) - return [return_type, as_scale_bytes, custom_rpc_type_registry, netuid_] - - return_type = TYPE_REGISTRY["runtime_api"]["NeuronInfoRuntimeApi"]["methods"][ - "get_neurons_lite" - ]["type"] - - preprocessed = [make_map(r) for r in netuids_with_all_neurons_hex_bytes] - with ProcessPoolExecutor() as executor: - results = list(executor.map(_partial_decode, preprocessed)) - - all_results = [(netuid, result) for netuid, result in results] + all_results = [ + (netuid, NeuronInfoLite.list_from_vec_u8(bytes.fromhex(result[2:]))) + for netuid, result in netuids_with_all_neurons_hex_bytes + ] return all_results @@ -1448,173 +1457,61 @@ async def swap_hotkey( ) -def set_id_prompts( - validator: bool, -) -> tuple[str, str, str, str, str, str, str, str, str, bool, int]: - """ - Used to prompt the user to input their info for setting the ID - :return: (display_name, legal_name, web_url, riot_handle, email,pgp_fingerprint, image_url, info_, twitter_url, - validator_id) - """ - text_rejection = partial( - retry_prompt, - rejection=lambda x: sys.getsizeof(x) > 113, - rejection_text="[red]Error:[/red] Identity field must be <= 64 raw bytes.", - ) +def create_identity_table(title: str = None): + if not title: + title = "On-Chain Identity" - def pgp_check(s: str): - try: - if s.startswith("0x"): - s = s[2:] # Strip '0x' - pgp_fingerprint_encoded = binascii.unhexlify(s.replace(" ", "")) - except Exception: - return True - return True if len(pgp_fingerprint_encoded) != 20 else False - - display_name = text_rejection("Display name") - legal_name = text_rejection("Legal name") - web_url = text_rejection("Web URL") - riot_handle = text_rejection("Riot handle") - email = text_rejection("Email address") - pgp_fingerprint = retry_prompt( - "PGP fingerprint (Eg: A1B2 C3D4 E5F6 7890 1234 5678 9ABC DEF0 1234 5678)", - lambda s: False if not s else pgp_check(s), - "[red]Error:[/red] PGP Fingerprint must be exactly 20 bytes.", - ) - image_url = text_rejection("Image URL") - info_ = text_rejection("Enter info") - twitter_url = text_rejection("𝕏 (Twitter) URL") - - subnet_netuid = None - if validator is False: - subnet_netuid = IntPrompt.ask("Enter the netuid of the subnet you own") - - return ( - display_name, - legal_name, - web_url, - pgp_fingerprint, - riot_handle, - email, - image_url, - twitter_url, - info_, - validator, - subnet_netuid, + 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}", + 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_id( wallet: Wallet, subtensor: SubtensorInterface, - display_name: str, - legal_name: str, + name: str, web_url: str, - pgp_fingerprint: str, - riot_handle: str, - email: str, - image: str, - twitter: str, - info_: str, - validator_id: bool, - subnet_netuid: int, + image_url: str, + discord_handle: str, + description: str, + additional_info: str, prompt: bool, ): """Create a new or update existing identity on-chain.""" - id_dict = { - "additional": [[]], - "display": display_name, - "legal": legal_name, - "web": web_url, - "pgp_fingerprint": pgp_fingerprint, - "riot": riot_handle, - "email": email, - "image": image, - "twitter": twitter, - "info": info_, + identity_data = { + "name": name.encode(), + "url": web_url.encode(), + "image": image_url.encode(), + "discord": discord_handle.encode(), + "description": description.encode(), + "additional": additional_info.encode(), } - try: - pgp_fingerprint_encoded = binascii.unhexlify(pgp_fingerprint.replace(" ", "")) - except Exception as e: - print_error(f"The PGP is not in the correct format: {e}") - raise typer.Exit() - - for field, string in id_dict.items(): - if ( - field == "pgp_fingerprint" - and pgp_fingerprint - and len(pgp_fingerprint_encoded) != 20 - ): + for field, value in identity_data.items(): + max_size = 64 # bytes + if len(value) > max_size: err_console.print( - "[red]Error:[/red] PGP Fingerprint must be exactly 20 bytes." + f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n" + f"Value '{value.decode()}' is {len(value)} bytes." ) return False - elif (size := getsizeof(string)) > 113: # 64 + 49 overhead bytes for string - err_console.print( - f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= 64 raw bytes.\n" - f"Value: '{string}' currently [white]{size} bytes[/white]." - ) - return False - - identified = ( - wallet.hotkey.ss58_address if validator_id else wallet.coldkey.ss58_address - ) - encoded_id_dict = { - "info": { - "additional": [[]], - "display": {f"Raw{len(display_name.encode())}": display_name.encode()}, - "legal": {f"Raw{len(legal_name.encode())}": legal_name.encode()}, - "web": {f"Raw{len(web_url.encode())}": web_url.encode()}, - "riot": {f"Raw{len(riot_handle.encode())}": riot_handle.encode()}, - "email": {f"Raw{len(email.encode())}": email.encode()}, - "pgp_fingerprint": pgp_fingerprint_encoded if pgp_fingerprint else None, - "image": {f"Raw{len(image.encode())}": image.encode()}, - "info": {f"Raw{len(info_.encode())}": info_.encode()}, - "twitter": {f"Raw{len(twitter.encode())}": twitter.encode()}, - }, - "identified": identified, - } - - if prompt: - if not Confirm.ask( - "Cost to register an Identity is [bold white italic]0.1 Tao[/bold white italic]," - " are you sure you wish to continue?" - ): - console.print(":cross_mark: Aborted!") - raise typer.Exit() - - if validator_id: - block_hash = await subtensor.substrate.get_chain_head() - - is_registered_on_root, hotkey_owner = await asyncio.gather( - is_hotkey_registered( - subtensor, netuid=0, hotkey_ss58=wallet.hotkey.ss58_address - ), - subtensor.get_hotkey_owner( - hotkey_ss58=wallet.hotkey.ss58_address, block_hash=block_hash - ), - ) - - if not is_registered_on_root: - print_error("The hotkey is not registered on root. Aborting.") - return False - - own_hotkey = wallet.coldkeypub.ss58_address == hotkey_owner - if not own_hotkey: - print_error("The hotkey doesn't belong to the coldkey wallet. Aborting.") - return False - else: - subnet_owner_ = await subtensor.substrate.query( - module="SubtensorModule", - storage_function="SubnetOwner", - params=[subnet_netuid], - ) - subnet_owner = decode_account_id(subnet_owner_[0]) - if subnet_owner != wallet.coldkeypub.ss58_address: - print_error(f":cross_mark: This wallet doesn't own subnet {subnet_netuid}.") - return False try: wallet.unlock_coldkey() @@ -1622,39 +1519,33 @@ async def set_id( err_console.print("Error decrypting coldkey (possibly incorrect password)") return False + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="set_identity", + call_params=identity_data, + ) + with console.status( - ":satellite: [bold green]Updating identity on-chain...", spinner="earth" + " :satellite: [dark_sea_green3]Updating identity on-chain...", spinner="earth" ): - call = await subtensor.substrate.compose_call( - call_module="Registry", - call_function="set_identity", - call_params=encoded_id_dict, - ) 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 - console.print(":white_heavy_check_mark: Success!") - identity = await subtensor.query_identity( - identified or wallet.coldkey.ss58_address - ) - - table = Table( - Column("Key", justify="right", style="cyan", no_wrap=True), - Column("Value", style="magenta"), - title="[bold white italic]Updated On-Chain Identity", - ) + console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") + identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) - table.add_row("Address", identified or wallet.coldkey.ss58_address) + table = create_identity_table(title="New on-chain Identity") + table.add_row("Address", wallet.coldkeypub.ss58_address) for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "~") + table.add_row(key, str(value) if value else "~") return console.print(table) -async def get_id(subtensor: SubtensorInterface, ss58_address: str): +async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = None): with console.status( ":satellite: [bold green]Querying chain identity...", spinner="earth" ): @@ -1662,22 +1553,19 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str): if not identity: err_console.print( - f"[red]Identity not found[/red]" - f" for [light_goldenrod3]{ss58_address}[/light_goldenrod3]" - f" on [white]{subtensor}[/white]" + f"[blue]Existing identity not found[/blue]" + f" for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + f" on {subtensor}" ) - return - table = Table( - Column("Item", justify="right", style="cyan", no_wrap=True), - Column("Value", style="magenta"), - title="[bold white italic]On-Chain Identity", - ) + return {} + table = create_identity_table(title) table.add_row("Address", ss58_address) for key, value in identity.items(): - table.add_row(key, str(value) if value is not None else "~") + table.add_row(key, str(value) if value else "~") - return console.print(table) + console.print(table) + return identity async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): @@ -1729,11 +1617,15 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str): ) if not use_hotkey: keypair = wallet.coldkey - print_verbose(f"Signing using coldkey: {wallet.name}") + print_verbose( + f"Signing using [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey: {wallet.name}" + ) else: keypair = wallet.hotkey - print_verbose(f"Signing using hotkey: {wallet.hotkey_str}") + print_verbose( + f"Signing using [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey: {wallet.hotkey_str}" + ) signed_message = keypair.sign(message.encode("utf-8")).hex() - console.print("[bold green]Message signed successfully:") + console.print("[dark_sea_green3]Message signed successfully:") console.print(signed_message) diff --git a/requirements.txt b/requirements.txt index 2e891bcf3..250668754 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,6 @@ rich~=13.7 scalecodec==1.2.11 substrate-interface~=1.7.9 typer~=0.12 -websockets>=12.0 +websockets==13.0 bittensor-wallet>=2.0.2 bt-decode==0.2.0a0 \ No newline at end of file