diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 08f8cb08..b17c3f80 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -127,8 +127,6 @@ class Options: use_password = typer.Option( True, help="Set this to `True` to protect the generated Bittensor key with a password.", - is_flag=True, - flag_value=False, ) public_hex_key = typer.Option(None, help="The public key in hex format.") ss58_address = typer.Option( @@ -1843,8 +1841,6 @@ def wallet_regen_hotkey( use_password: bool = typer.Option( False, # Overriden to False help="Set to 'True' to protect the generated Bittensor key with a password.", - is_flag=True, - flag_value=True, ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -1901,8 +1897,6 @@ def wallet_new_hotkey( use_password: bool = typer.Option( False, # Overriden to False help="Set to 'True' to protect the generated Bittensor key with a password.", - is_flag=True, - flag_value=True, ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3665,7 +3659,7 @@ def stake_set_children( prompt: bool = Options.prompt, ): """ - Set child hotkeys on specified subnets. + Set child hotkeys on a specified subnet (or all). Overrides currently set children. Users can specify the 'proportion' to delegate to child hotkeys (ss58 address). The sum of proportions cannot be greater than 1. @@ -3750,7 +3744,7 @@ def stake_revoke_children( prompt: bool = Options.prompt, ): """ - Remove all children hotkeys on a specified subnet. + Remove all children hotkeys on a specified subnet (or all). This command is used to remove delegated authority from all child hotkeys, removing their position and influence on the subnet. @@ -3890,12 +3884,12 @@ def sudo_set( """ self.verbosity_handler(quiet, verbose) - hyperparams = self._run_command( - sudo.get_hyperparameters(self.initialize_chain(network), netuid) - ) - - if not hyperparams: - raise typer.Exit() + if not param_name or not param_value: + hyperparams = self._run_command( + sudo.get_hyperparameters(self.initialize_chain(network), netuid) + ) + if not hyperparams: + raise typer.Exit() if not param_name: hyperparam_list = [field.name for field in fields(SubnetHyperparameters)] @@ -3910,6 +3904,16 @@ def sudo_set( ) param_name = hyperparam_list[choice - 1] + if param_name in ["alpha_high", "alpha_low"]: + param_name = "alpha_values" + low_val = FloatPrompt.ask( + "Enter the new value for [dark_orange]alpha_low[/dark_orange]" + ) + high_val = FloatPrompt.ask( + "Enter the new value for [dark_orange]alpha_high[/dark_orange]" + ) + param_value = f"{low_val},{high_val}" + 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" diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 73c2c5a8..81ee7747 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -156,28 +156,36 @@ class WalletValidationTypes(Enum): HYPERPARAMS = { - "serving_rate_limit": "sudo_set_serving_rate_limit", - "min_difficulty": "sudo_set_min_difficulty", - "max_difficulty": "sudo_set_max_difficulty", - "weights_version": "sudo_set_weights_version_key", - "weights_rate_limit": "sudo_set_weights_set_rate_limit", - "max_weight_limit": "sudo_set_max_weight_limit", - "immunity_period": "sudo_set_immunity_period", - "min_allowed_weights": "sudo_set_min_allowed_weights", - "activity_cutoff": "sudo_set_activity_cutoff", - "network_registration_allowed": "sudo_set_network_registration_allowed", - "network_pow_registration_allowed": "sudo_set_network_pow_registration_allowed", - "min_burn": "sudo_set_min_burn", - "max_burn": "sudo_set_max_burn", - "adjustment_alpha": "sudo_set_adjustment_alpha", - "rho": "sudo_set_rho", - "kappa": "sudo_set_kappa", - "difficulty": "sudo_set_difficulty", - "bonds_moving_avg": "sudo_set_bonds_moving_average", - "commit_reveal_weights_interval": "sudo_set_commit_reveal_weights_interval", - "commit_reveal_weights_enabled": "sudo_set_commit_reveal_weights_enabled", - "alpha_values": "sudo_set_alpha_values", - "liquid_alpha_enabled": "sudo_set_liquid_alpha_enabled", + # btcli name: (subtensor method, sudo bool) + "rho": ("sudo_set_rho", False), + "kappa": ("sudo_set_kappa", False), + "immunity_period": ("sudo_set_immunity_period", False), + "min_allowed_weights": ("sudo_set_min_allowed_weights", False), + "max_weights_limit": ("sudo_set_max_weight_limit", False), + "tempo": ("sudo_set_tempo", True), + "min_difficulty": ("sudo_set_min_difficulty", False), + "max_difficulty": ("sudo_set_max_difficulty", False), + "weights_version": ("sudo_set_weights_version_key", False), + "weights_rate_limit": ("sudo_set_weights_set_rate_limit", False), + "adjustment_interval": ("sudo_set_adjustment_interval", True), + "activity_cutoff": ("sudo_set_activity_cutoff", False), + "target_regs_per_interval": ("sudo_set_target_registrations_per_interval", True), + "min_burn": ("sudo_set_min_burn", False), + "max_burn": ("sudo_set_max_burn", False), + "bonds_moving_avg": ("sudo_set_bonds_moving_average", False), + "max_regs_per_block": ("sudo_set_max_registrations_per_block", True), + "serving_rate_limit": ("sudo_set_serving_rate_limit", False), + "max_validators": ("sudo_set_max_allowed_validators", True), + "adjustment_alpha": ("sudo_set_adjustment_alpha", False), + "difficulty": ("sudo_set_difficulty", False), + "commit_reveal_weights_interval": ( + "sudo_set_commit_reveal_weights_interval", + False, + ), + "commit_reveal_weights_enabled": ("sudo_set_commit_reveal_weights_enabled", False), + "alpha_values": ("sudo_set_alpha_values", False), + "liquid_alpha_enabled": ("sudo_set_liquid_alpha_enabled", False), + "registration_allowed": ("sudo_set_network_registration_allowed", False), } # Help Panels for cli help diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index fb088ad7..82d1c919 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from hashlib import blake2b from itertools import chain -from typing import Optional, Any, Union, Callable, Awaitable, cast +from typing import Optional, Any, Union, Callable, Awaitable, cast, TYPE_CHECKING from types import SimpleNamespace from bt_decode import ( @@ -15,6 +15,7 @@ MetadataV15, ) from async_property import async_property +from bittensor_wallet import Keypair from scalecodec import GenericExtrinsic from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject from scalecodec.type_registry import load_type_registry_preset @@ -26,12 +27,16 @@ BlockNotFound, ) from substrateinterface.storage import StorageKey -import websockets +from websockets.asyncio.client import connect +from websockets.exceptions import ConnectionClosed from .utils import bytes_from_hex_string_result, encode_account_id from bittensor_cli.src.bittensor.utils import hex_to_bytes +if TYPE_CHECKING: + from websockets.asyncio.client import ClientConnection + ResultHandler = Callable[[dict, Any], Awaitable[tuple[dict, bool]]] @@ -471,7 +476,7 @@ def add_item( self.block_hashes[block_hash] = runtime def retrieve( - self, block: Optional[int], block_hash: Optional[str] + self, block: Optional[int] = None, block_hash: Optional[str] = None ) -> Optional["Runtime"]: if block is not None: return self.blocks.get(block) @@ -662,7 +667,7 @@ def __init__( # TODO allow setting max concurrent connections and rpc subscriptions per connection # TODO reconnection logic self.ws_url = ws_url - self.ws: Optional[websockets.WebSocketClientProtocol] = None + self.ws: Optional["ClientConnection"] = None self.id = 0 self.max_subscriptions = max_subscriptions self.max_connections = max_connections @@ -684,15 +689,12 @@ async def __aenter__(self): self._exit_task.cancel() if not self._initialized: self._initialized = True - await self._connect() + self.ws = await asyncio.wait_for( + connect(self.ws_url, **self._options), timeout=10 + ) self._receiving_task = asyncio.create_task(self._start_receiving()) return self - async def _connect(self): - self.ws = await asyncio.wait_for( - websockets.connect(self.ws_url, **self._options), timeout=10 - ) - async def __aexit__(self, exc_type, exc_val, exc_tb): async with self._lock: self._in_use -= 1 @@ -733,9 +735,7 @@ async def shutdown(self): async def _recv(self) -> None: try: - response = json.loads( - await cast(websockets.WebSocketClientProtocol, self.ws).recv() - ) + response = json.loads(await self.ws.recv()) async with self._lock: self._open_subscriptions -= 1 if "id" in response: @@ -744,7 +744,7 @@ async def _recv(self) -> None: self._received[response["params"]["subscription"]] = response else: raise KeyError(response) - except websockets.ConnectionClosed: + except ConnectionClosed: raise except KeyError as e: raise e @@ -755,7 +755,7 @@ async def _start_receiving(self): await self._recv() except asyncio.CancelledError: pass - except websockets.ConnectionClosed: + except ConnectionClosed: # TODO try reconnect, but only if it's needed raise @@ -772,7 +772,7 @@ async def send(self, payload: dict) -> int: try: await self.ws.send(json.dumps({**payload, **{"id": original_id}})) return original_id - except websockets.ConnectionClosed: + except ConnectionClosed: raise async def retrieve(self, item_id: int) -> Optional[dict]: @@ -810,13 +810,13 @@ def __init__( """ self.chain_endpoint = chain_endpoint self.__chain = chain_name - options = { - "max_size": 2**32, - "write_limit": 2**16, - } - if version.parse(websockets.__version__) < version.parse("14.0"): - options.update({"read_limit": 2**16}) - self.ws = Websocket(chain_endpoint, options=options) + self.ws = Websocket( + chain_endpoint, + options={ + "max_size": 2**32, + "write_limit": 2**16, + }, + ) self._lock = asyncio.Lock() self.last_block_hash: Optional[str] = None self.config = { @@ -1259,7 +1259,7 @@ async def create_storage_key( ------- StorageKey """ - runtime = await self.init_runtime(block_hash=block_hash) + await self.init_runtime(block_hash=block_hash) return StorageKey.create_from_storage_function( pallet, @@ -1679,7 +1679,7 @@ async def _process_response( self, response: dict, subscription_id: Union[int, str], - value_scale_type: Optional[str], + value_scale_type: Optional[str] = None, storage_item: Optional[ScaleType] = None, runtime: Optional[Runtime] = None, result_handler: Optional[ResultHandler] = None, @@ -1893,7 +1893,6 @@ async def compose_call( call_params = {} await self.init_runtime(block_hash=block_hash) - call = self.runtime_config.create_scale_object( type_string="Call", metadata=self.metadata ) @@ -2211,7 +2210,8 @@ async def create_signed_extrinsic( :return: The signed Extrinsic """ - await self.init_runtime() + if not self.metadata: + await self.init_runtime() # Check requirements if not isinstance(call, GenericCall): @@ -2264,7 +2264,6 @@ async def create_signed_extrinsic( extrinsic = self.runtime_config.create_scale_object( type_string="Extrinsic", metadata=self.metadata ) - value = { "account_id": f"0x{keypair.public_key.hex()}", "signature": f"0x{signature.hex()}", @@ -2282,9 +2281,7 @@ async def create_signed_extrinsic( signature_cls = self.runtime_config.get_decoder_class("ExtrinsicSignature") if issubclass(signature_cls, self.runtime_config.get_decoder_class("Enum")): value["signature_version"] = signature_version - extrinsic.encode(value) - return extrinsic async def get_chain_finalised_head(self): diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 33c9c156..84f761a8 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -18,7 +18,6 @@ from typing import Optional import subprocess -import backoff from bittensor_wallet import Wallet from Crypto.Hash import keccak import numpy as np @@ -688,7 +687,7 @@ async def run_faucet_extrinsic( tpb: int = 256, num_processes: Optional[int] = None, update_interval: Optional[int] = None, - log_verbose: bool = False, + log_verbose: bool = True, max_successes: int = 3, ) -> tuple[bool, str]: r"""Runs a continual POW to get a faucet of TAO on the test net. @@ -736,8 +735,12 @@ async def run_faucet_extrinsic( # Attempt rolling registration. attempts = 1 successes = 1 + pow_result: Optional[POWSolution] while True: try: + account_nonce = await subtensor.substrate.get_account_nonce( + wallet.coldkey.ss58_address + ) pow_result = None while pow_result is None or await pow_result.is_stale(subtensor=subtensor): # Solve latest POW. @@ -746,7 +749,7 @@ async def run_faucet_extrinsic( if prompt: err_console.print("CUDA is not available.") return False, "CUDA is not available." - pow_result: Optional[POWSolution] = await create_pow( + pow_result = await create_pow( subtensor, wallet, -1, @@ -759,7 +762,7 @@ async def run_faucet_extrinsic( log_verbose=log_verbose, ) else: - pow_result: Optional[POWSolution] = await create_pow( + pow_result = await create_pow( subtensor, wallet, -1, @@ -779,7 +782,7 @@ async def run_faucet_extrinsic( }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, nonce=account_nonce ) response = await subtensor.substrate.submit_extrinsic( extrinsic, @@ -1246,8 +1249,6 @@ def _terminate_workers_and_wait_for_exit( worker.terminate() -# TODO verify this works with async -@backoff.on_exception(backoff.constant, Exception, interval=1, max_tries=3) async def _get_block_with_retry( subtensor: "SubtensorInterface", netuid: int ) -> tuple[int, int, str]: @@ -1262,10 +1263,9 @@ async def _get_block_with_retry( :raises Exception: If the block hash is None. :raises ValueError: If the difficulty is None. """ - block_number = await subtensor.substrate.get_block_number(None) - block_hash = await subtensor.substrate.get_block_hash( - block_number - ) # TODO check if I need to do all this + block = await subtensor.substrate.get_block() + block_hash = block["header"]["hash"] + block_number = block["header"]["number"] try: difficulty = ( 1_000_000 diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 6ff78216..cc3c2b81 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -559,7 +559,12 @@ def format_error_message( err_type = error_message.get("type", err_type) err_name = error_message.get("name", err_name) err_docs = error_message.get("docs", [err_description]) - err_description = err_docs[0] if err_docs else err_description + if not err_docs: + err_description = err_description + elif isinstance(err_docs, str): + err_description = err_docs + else: + err_description = err_docs[0] return f"Subtensor returned `{err_name}({err_type})` error. This means: '{err_description}'." diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py index 795a83d4..89fc226d 100644 --- a/bittensor_cli/src/commands/root.py +++ b/bittensor_cli/src/commands/root.py @@ -867,11 +867,13 @@ async def get_weights( uid_to_weights[uid][netuid] = normalized_weight rows: list[list[str]] = [] + sorted_netuids: list = list(netuids) + sorted_netuids.sort() for uid in uid_to_weights: row = [str(uid)] uid_weights = uid_to_weights[uid] - for netuid in netuids: + for netuid in sorted_netuids: if netuid in uid_weights: row.append("{:0.2f}%".format(uid_weights[netuid] * 100)) else: @@ -880,24 +882,23 @@ async def get_weights( if not no_cache: db_cols = [("UID", "INTEGER")] - for netuid in netuids: + for netuid in sorted_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)}, + {"rows": json.dumps(rows), "netuids": json.dumps(sorted_netuids)}, ) else: metadata = get_metadata_table("rootgetweights") rows = json.loads(metadata["rows"]) - netuids = json.loads(metadata["netuids"]) + sorted_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)) + _max_lim = limit_max_col + 1 if limit_max_col is not None else len(sorted_netuids) + _max_lim = min(_max_lim, len(sorted_netuids)) - if _min_lim is not None and _min_lim > len(netuids): + if _min_lim is not None and _min_lim > len(sorted_netuids): err_console.print("Minimum limit greater than number of netuids") return @@ -916,8 +917,7 @@ async def get_weights( style="rgb(50,163,219)", no_wrap=True, ) - netuids = list(netuids) - for netuid in netuids[_min_lim:_max_lim]: + for netuid in sorted_netuids[_min_lim:_max_lim]: table.add_column( f"[white]{netuid}", header_style="overline white", @@ -940,7 +940,7 @@ async def get_weights( else: html_cols = [{"title": "UID", "field": "UID"}] - for netuid in netuids[_min_lim:_max_lim]: + for netuid in sorted_netuids[_min_lim:_max_lim]: html_cols.append({"title": str(netuid), "field": f"_{netuid}"}) render_table( "rootgetweights", diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 5711a3c2..2be3c0f6 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -104,7 +104,7 @@ async def set_hyperparameter_extrinsic( if not unlock_key(wallet).success: return False - extrinsic = HYPERPARAMS.get(parameter) + extrinsic, sudo_ = HYPERPARAMS.get(parameter, ("", False)) if extrinsic is None: err_console.print(":cross_mark: [red]Invalid hyperparameter specified.[/red]") return False @@ -144,11 +144,17 @@ async def set_hyperparameter_extrinsic( call_params[str(value_argument["name"])] = value # create extrinsic call - call = await substrate.compose_call( + call_ = await substrate.compose_call( call_module="AdminUtils", call_function=extrinsic, call_params=call_params, ) + if sudo_: + call = await substrate.compose_call( + call_module="Sudo", call_function="sudo", call_params={"call": call_} + ) + else: + call = call_ success, err_msg = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization ) @@ -178,19 +184,20 @@ async def sudo_set_hyperparameter( normalized_value: Union[str, bool] if param_name in [ - "network_registration_allowed", + "registration_allowed", "network_pow_registration_allowed", "commit_reveal_weights_enabled", "liquid_alpha_enabled", ]: - normalized_value = param_value.lower() in ["true", "1"] + normalized_value = param_value.lower() in ["true", "True", "1"] else: normalized_value = param_value is_allowed_value, value = allowed_value(param_name, normalized_value) if not is_allowed_value: err_console.print( - f"Hyperparameter {param_name} value is not within bounds. Value is {normalized_value} but must be {value}" + f"Hyperparameter [dark_orange]{param_name}[/dark_orange] value is not within bounds. " + f"Value is {normalized_value} but must be {value}" ) return diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 90aaab6a..1616cde5 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -658,8 +658,13 @@ async def overview( neurons ) + has_alerts = False alerts_table = Table(show_header=True, header_style="bold magenta") alerts_table.add_column("🥩 alert!") + alerts_table.add_row( + "[bold]Detected the following stake(s) associated with coldkey(s) that are not linked to any local hotkeys:[/bold]" + ) + alerts_table.add_row("") coldkeys_to_check = [] ck_stakes = await subtensor.get_total_stake_for_coldkey( @@ -682,12 +687,23 @@ async def overview( if difference == 0: continue # We have all our stake registered. + has_alerts = True coldkeys_to_check.append(coldkey_wallet) alerts_table.add_row( - "Found [light_goldenrod2]{}[/light_goldenrod2] stake with coldkey [bright_magenta]{}[/bright_magenta] that is not registered.".format( - abs(difference), coldkey_wallet.coldkeypub.ss58_address + "[light_goldenrod2]{}[/light_goldenrod2] stake associated with coldkey [bright_magenta]{}[/bright_magenta] (ss58: [bright_magenta]{}[/bright_magenta])".format( + abs(difference), + coldkey_wallet.name, + coldkey_wallet.coldkeypub.ss58_address, ) ) + if has_alerts: + alerts_table.add_row("") + alerts_table.add_row( + "[bold yellow]Note:[/bold yellow] This stake might be delegated, staked to another user's hotkey, or associated with a hotkey not present in your wallet." + ) + alerts_table.add_row( + "You can find out more by executing `[bold]btcli wallet inspect[/bold]` command." + ) if coldkeys_to_check: # We have some stake that is not with a registered hotkey. @@ -741,7 +757,7 @@ async def overview( grid = Table.grid(pad_edge=True) # If there are any alerts, add them to the grid - if len(alerts_table.rows) > 0: + if has_alerts: grid.add_row(alerts_table) # Add title diff --git a/requirements.txt b/requirements.txt index f8dbf5f6..5714e41b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ fuzzywuzzy~=0.18.0 netaddr~=1.3.0 numpy>=2.0.1 Jinja2 -packaging pycryptodome # Crypto PyYAML~=6.0.1 pytest @@ -16,6 +15,6 @@ rich~=13.7 scalecodec==1.2.11 substrate-interface~=1.7.9 typer~=0.12 -websockets>=12.0 -bittensor-wallet>=2.1.0 -bt-decode==0.4.0a0 \ No newline at end of file +websockets>=14.1 +bittensor-wallet>=2.1.3 +bt-decode==0.4.0a0