diff --git a/CHANGELOG.md b/CHANGELOG.md index b691afafe..43ae097df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 8.2.0 /2024-10-10 + +## What's Changed +* Handle git not installed by @thewhaleking in https://github.com/opentensor/btcli/pull/164 +* Handle receiving task cancellation by @thewhaleking in https://github.com/opentensor/btcli/pull/166 +* Change network option to a list so that it can be correctly parsed if multiple options are given by @thewhaleking in https://github.com/opentensor/btcli/pull/165 +* Receiving task cancellation improvement by @thewhaleking in https://github.com/opentensor/btcli/pull/168 +* mnemonic change: support numbered mnemonic by @thewhaleking in https://github.com/opentensor/btcli/pull/167 +* Backmerge release 8.1.1 by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/169 +* Handle custom errors from subtensor by @thewhaleking in https://github.com/opentensor/btcli/pull/79 +* Removes check for port in chain endpoint by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/170 +* Shifts Tao conversion to correct place in stake remove by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/172 +* Adds support for ss58 addresses in wallet balance by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/171 +* Fixes network instantiation in root list-delegates by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/173 +* Utils App with convert command by @thewhaleking in https://github.com/opentensor/btcli/pull/174 +* Fixes for rpc request error handler, root list default empty values, prev delegate fetching by @thewhaleking in https://github.com/opentensor/btcli/pull/175 +* Bumps version, updates requirement for 8.1.2 by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/176 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v8.1.1...v8.2.0 + ## 8.1.1 /2024-10-04 ## What's Changed diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 96f9a2a5a..8fff761ce 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.1.1" +__version__ = "8.2.0" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e14db7b99..b012c989e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -15,7 +15,6 @@ import typer import numpy as np from bittensor_wallet import Wallet -from git import Repo, GitError from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table @@ -27,6 +26,7 @@ Constants, ) 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, ) @@ -49,7 +49,16 @@ from websockets import ConnectionClosed from yaml import safe_dump, safe_load -__version__ = "8.1.1" +try: + from git import Repo, GitError +except ImportError: + + class GitError(Exception): + pass + + +__version__ = "8.2.0" + _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) _version_split = _core_version.split(".") @@ -121,7 +130,9 @@ class Options: flag_value=False, ) public_hex_key = typer.Option(None, help="The public key in hex format.") - ss58_address = typer.Option(None, help="The SS58 address of the coldkey.") + ss58_address = typer.Option( + None, "--ss58", "--ss58-address", help="The SS58 address of the coldkey." + ) overwrite_coldkey = typer.Option( False, help="Overwrite the old coldkey with the newly generated coldkey.", @@ -275,8 +286,34 @@ def get_n_words(n_words: Optional[int]) -> int: return n_words +def parse_mnemonic(mnemonic: str) -> str: + if "-" in mnemonic: + items = sorted( + [tuple(item.split("-")) for item in mnemonic.split(" ")], + key=lambda x: int(x[0]), + ) + if int(items[0][0]) != 1: + err_console.print("Numbered mnemonics must begin with 1") + raise typer.Exit() + if [int(x[0]) for x in items] != list( + range(int(items[0][0]), int(items[-1][0]) + 1) + ): + err_console.print( + "Missing or duplicate numbers in a numbered mnemonic. " + "Double-check your numbered mnemonics and try again." + ) + raise typer.Exit() + response = " ".join(item[1] for item in items) + else: + response = mnemonic + return response + + def get_creation_data( - mnemonic: str, seed: str, json: str, json_password: str + mnemonic: Optional[str], + seed: Optional[str], + json: Optional[str], + json_password: Optional[str], ) -> tuple[str, str, str, str]: """ Determines which of the key creation elements have been supplied, if any. If None have been supplied, @@ -289,9 +326,11 @@ def get_creation_data( if prompt_answer.startswith("0x"): seed = prompt_answer elif len(prompt_answer.split(" ")) > 1: - mnemonic = prompt_answer + mnemonic = parse_mnemonic(prompt_answer) else: json = prompt_answer + elif mnemonic: + mnemonic = parse_mnemonic(mnemonic) if json and not json_password: json_password = Prompt.ask( "Enter the backup password for JSON file.", password=True @@ -368,7 +407,7 @@ def version_callback(value: bool): f"{repo.active_branch.name}/" f"{repo.commit()}" ) - except GitError: + except (NameError, GitError): version = f"BTCLI version: {__version__}" typer.echo(version) raise typer.Exit() @@ -393,6 +432,7 @@ class CLIManager: root_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer + utils_app = typer.Typer(epilog=_epilog) def __init__(self): self.config = { @@ -517,6 +557,9 @@ def __init__(self): self.weights_app, name="weight", hidden=True, no_args_is_help=True ) + # utils app + self.app.add_typer(self.utils_app, name="utils", no_args_is_help=True) + # config commands self.config_app.command("set")(self.set_config) self.config_app.command("get")(self.get_config) @@ -739,7 +782,7 @@ def __init__(self): def initialize_chain( self, - network: Optional[str] = None, + network: Optional[list[str]] = None, ) -> SubtensorInterface: """ Intelligently initializes a connection to the chain, depending on the supplied (or in config) values. Sets the @@ -750,7 +793,20 @@ def initialize_chain( """ if not self.subtensor: if network: - self.subtensor = SubtensorInterface(network) + for item in network: + if item.startswith("ws"): + network_ = item + break + else: + network_ = item + + not_selected_networks = [net for net in network if net != network_] + if not_selected_networks: + console.print( + f"Networks not selected: [dark_orange]{', '.join(not_selected_networks)}[/dark_orange]" + ) + + self.subtensor = SubtensorInterface(network_) elif self.config["network"]: self.subtensor = SubtensorInterface(self.config["network"]) console.print( @@ -1227,7 +1283,7 @@ def wallet_overview( "If left empty, all hotkeys, except those in the '--include-hotkeys', will be excluded.", ), netuids: str = Options.netuids, - network: str = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -1354,7 +1410,7 @@ def wallet_transfer( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, - network: str = Options.network, + network: Optional[list[str]] = Options.network, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -1401,7 +1457,7 @@ def wallet_swap_hotkey( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, destination_hotkey_name: Optional[str] = typer.Argument( None, help="Destination hotkey name." ), @@ -1463,7 +1519,7 @@ def wallet_inspect( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, - network: str = Options.network, + network: Optional[list[str]] = Options.network, netuids: str = Options.netuids, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -1529,7 +1585,7 @@ def wallet_faucet( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, # TODO add the following to config processors: Optional[int] = typer.Option( defaults.pow_register.num_processes, @@ -1897,7 +1953,7 @@ def wallet_check_ck_swap( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -1977,19 +2033,22 @@ def wallet_balance( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, + ss58_addresses: Optional[list[str]] = Options.ss58_address, all_balances: Optional[bool] = typer.Option( False, "--all", "-a", help="Whether to display the balances for all the wallets.", ), - network: str = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ Check the balance of the wallet. This command shows a detailed view of the wallet's coldkey balances, including free and staked balances. + You can also pass multiple ss58 addresses of coldkeys to check their balance (using --ss58). + EXAMPLES: - To display the balance of a single wallet, use the command with the `--wallet-name` argument and provide the wallet name: @@ -2003,17 +2062,41 @@ def wallet_balance( - To display the balances of all your wallets, use the `--all` argument: [green]$[/green] btcli w balance --all + + - To display the balances of ss58 addresses, use the `--ss58` argument: + + [green]$[/green] btcli w balance --ss58 --ss58 + """ self.verbosity_handler(quiet, verbose) - 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 ss58_addresses: + valid_ss58s = [ + ss58 for ss58 in set(ss58_addresses) if is_valid_ss58_address(ss58) + ] + + invalid_ss58s = set(ss58_addresses) - set(valid_ss58s) + for invalid_ss58 in invalid_ss58s: + 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, + ) subtensor = self.initialize_chain(network) return self._run_command( - wallets.wallet_balance(wallet, subtensor, all_balances) + wallets.wallet_balance(wallet, subtensor, all_balances, ss58_addresses) ) def wallet_history( @@ -2058,7 +2141,7 @@ def wallet_set_id( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, display_name: str = typer.Option( "", "--display-name", @@ -2238,7 +2321,7 @@ def wallet_get_id( help="The coldkey or hotkey ss58 address to query.", prompt=True, ), - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2317,7 +2400,7 @@ def wallet_sign( def root_list( self, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2341,7 +2424,7 @@ def root_list( def root_set_weights( self, - network: str = Options.network, + network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -2418,7 +2501,7 @@ def root_set_weights( def root_get_weights( self, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, limit_min_col: Optional[int] = typer.Option( None, "--limit-min-col", @@ -2469,7 +2552,7 @@ def root_get_weights( def root_boost( self, - network: Optional[str] = Options.network, + 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, @@ -2509,7 +2592,7 @@ def root_boost( def root_slash( self, - network: Optional[str] = Options.network, + 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, @@ -2550,7 +2633,7 @@ def root_slash( def root_senate_vote( self, - network: Optional[str] = Options.network, + 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, @@ -2600,7 +2683,7 @@ def root_senate_vote( def root_senate( self, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2618,7 +2701,7 @@ def root_senate( def root_register( self, - network: Optional[str] = Options.network, + 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, @@ -2653,7 +2736,7 @@ def root_register( def root_proposals( self, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2671,7 +2754,7 @@ def root_proposals( def root_set_take( self, - network: Optional[str] = Options.network, + 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, @@ -2741,7 +2824,7 @@ def root_delegate_stake( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -2816,7 +2899,7 @@ def root_undelegate_stake( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -2868,7 +2951,7 @@ def root_undelegate_stake( def root_my_delegates( self, - network: Optional[str] = Options.network, + 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, @@ -2935,7 +3018,7 @@ def root_my_delegates( def root_list_delegates( self, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2989,14 +3072,14 @@ def root_list_delegates( self.verbosity_handler(quiet, verbose) if network: - if network == "finney": - network = "wss://archive.chain.opentensor.ai:443" + 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" + network = ["wss://archive.chain.opentensor.ai:443"] elif conf_net: - network = conf_net + network = [conf_net] else: - network = "wss://archive.chain.opentensor.ai:443" + network = ["wss://archive.chain.opentensor.ai:443"] sub = self.initialize_chain(network) return self._run_command(root.list_delegates(sub)) @@ -3007,7 +3090,7 @@ def root_nominate( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3056,7 +3139,7 @@ def stake_show( "-a", help="When set, the command checks all the coldkey wallets of the user instead of just the specified wallet.", ), - network: Optional[str] = Options.network, + 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, @@ -3167,7 +3250,7 @@ def stake_add( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, - network: str = Options.network, + network: Optional[list[str]] = Options.network, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3281,7 +3364,7 @@ def stake_add( def stake_remove( self, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -3439,7 +3522,7 @@ def stake_get_children( wallet_name: Optional[str] = Options.wallet_name, wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, netuid: Optional[int] = typer.Option( None, help="The netuid of the subnet (e.g. 2)", @@ -3500,7 +3583,7 @@ def stake_set_children( wallet_name: str = Options.wallet_name, wallet_hotkey: str = Options.wallet_hotkey, wallet_path: str = Options.wallet_path, - network: str = Options.network, + network: Optional[list[str]] = Options.network, netuid: Optional[int] = typer.Option( None, help="The netuid of the subnet, (e.g. 4)", @@ -3591,7 +3674,7 @@ def stake_revoke_children( wallet_name: Optional[str] = Options.wallet_name, wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, netuid: Optional[int] = typer.Option( None, help="The netuid of the subnet, (e.g. 8)", @@ -3650,7 +3733,7 @@ def stake_childkey_take( wallet_name: Optional[str] = Options.wallet_name, wallet_hotkey: Optional[str] = Options.wallet_hotkey, wallet_path: Optional[str] = Options.wallet_path, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, hotkey: Optional[str] = None, netuid: Optional[int] = typer.Option( None, @@ -3724,7 +3807,7 @@ def stake_childkey_take( def sudo_set( self, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -3789,7 +3872,7 @@ def sudo_set( def sudo_get( self, - network: str = Options.network, + network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3810,7 +3893,7 @@ def sudo_get( def subnets_list( self, - network: str = Options.network, + network: Optional[list[str]] = Options.network, reuse_last: bool = Options.reuse_last, html_output: bool = Options.html_output, quiet: bool = Options.quiet, @@ -3856,7 +3939,7 @@ def subnets_list( def subnets_lock_cost( self, - network: str = Options.network, + network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -3877,7 +3960,7 @@ def subnets_create( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, - network: str = Options.network, + network: Optional[list[str]] = Options.network, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3906,7 +3989,7 @@ def subnets_pow_register( wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - network: Optional[str] = Options.network, + network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, # TODO add the following to config processors: Optional[int] = typer.Option( @@ -3993,7 +4076,7 @@ def subnets_register( wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, - network: str = Options.network, + network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, prompt: bool = Options.prompt, quiet: bool = Options.quiet, @@ -4034,7 +4117,7 @@ def subnets_metagraph( help="The netuid of the subnet (e.g. 1). This option " "is ignored when used with `--reuse-last`.", ), - network: str = Options.network, + network: Optional[list[str]] = Options.network, reuse_last: bool = Options.reuse_last, html_output: bool = Options.html_output, quiet: bool = Options.quiet, @@ -4124,7 +4207,7 @@ def subnets_metagraph( def weights_reveal( self, - network: str = Options.network, + network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -4220,7 +4303,7 @@ def weights_reveal( def weights_commit( self, - network: str = Options.network, + network: Optional[list[str]] = Options.network, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -4313,6 +4396,37 @@ def weights_commit( ) ) + @staticmethod + @utils_app.command("convert") + def convert( + from_rao: Optional[str] = typer.Option( + None, "--rao", help="Convert amount from Rao" + ), + from_tao: Optional[float] = typer.Option( + None, "--tao", help="Convert amount from Tao" + ), + ): + """ + Allows for converting between tao and rao using the specified flags + """ + if from_tao is None and from_rao is None: + err_console.print("Specify `--rao` and/or `--tao`.") + raise typer.Exit() + if from_rao is not None: + rao = int(float(from_rao)) + console.print( + f"{rao}{Balance.rao_unit}", + "=", + Balance.from_rao(rao), + ) + if from_tao is not None: + tao = float(from_tao) + console.print( + f"{Balance.unit}{tao}", + "=", + f"{Balance.from_tao(tao).rao}{Balance.rao_unit}", + ) + def run(self): self.app() diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 464635958..0f4dd0d43 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -679,10 +679,10 @@ async def _exit_with_timer(self): async def shutdown(self): async with self._lock: - self._receiving_task.cancel() try: + self._receiving_task.cancel() await self._receiving_task - except asyncio.CancelledError: + except (AttributeError, asyncio.CancelledError): pass await self.ws.close() self.ws = None @@ -1708,7 +1708,7 @@ async def rpc_request( result = await self._make_rpc_request(payloads, runtime=runtime) if "error" in result[payload_id][0]: raise SubstrateRequestException( - result["rpc_request"][0]["error"]["message"] + result[payload_id][0]["error"]["message"] ) if "result" in result[payload_id][0]: return result[payload_id][0] diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 1566a342e..5354ac520 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -614,7 +614,10 @@ async def get_neuron_for_pubkey_and_subnet(): if not await response.is_success: success, err_msg = ( False, - format_error_message(await response.error_message), + format_error_message( + await response.error_message, + substrate=subtensor.substrate, + ), ) if not success: @@ -785,7 +788,8 @@ async def run_faucet_extrinsic( await response.process_events() if not await response.is_success: err_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" + f":cross_mark: [red]Failed[/red]: " + f"{format_error_message(await response.error_message, subtensor.substrate)}" ) if attempts == max_allowed_attempts: raise MaxAttemptsException diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 712a70f07..fcbf7d0c1 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -20,15 +20,15 @@ import time from typing import Union, List, TYPE_CHECKING -from bittensor_wallet import Wallet +from bittensor_wallet import Wallet, Keypair from bittensor_wallet.errors import KeyFileError import numpy as np from numpy.typing import NDArray from rich.prompt import Confirm from rich.table import Table, Column from scalecodec import ScaleBytes, U16, Vec +from substrateinterface.exceptions import SubstrateRequestException -from bittensor_wallet import Keypair from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.extrinsics.registration import is_hotkey_registered from bittensor_cli.src.bittensor.utils import ( @@ -36,6 +36,7 @@ err_console, u16_normalized_float, print_verbose, + format_error_message, ) if TYPE_CHECKING: @@ -481,7 +482,6 @@ async def _do_set_weights(): ) success, error_message = await _do_set_weights() - console.print(success, error_message) if not wait_for_finalization and not wait_for_inclusion: return True @@ -490,9 +490,11 @@ async def _do_set_weights(): console.print(":white_heavy_check_mark: [green]Finalized[/green]") return True else: - err_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") + fmt_err = format_error_message(error_message, subtensor.substrate) + err_console.print(f":cross_mark: [red]Failed[/red]: {fmt_err}") return False - except Exception as e: - err_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) + except SubstrateRequestException as e: + fmt_err = format_error_message(e, subtensor.substrate) + err_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(fmt_err)) return False diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index c5d3789f7..8ae37e9b6 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -3,6 +3,7 @@ from bittensor_wallet import Wallet from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm +from substrateinterface.exceptions import SubstrateRequestException from bittensor_cli.src import NETWORK_EXPLORER_MAP from bittensor_cli.src.bittensor.balances import Balance @@ -59,11 +60,11 @@ async def get_transfer_fee() -> Balance: payment_info = await subtensor.substrate.get_payment_info( call=call, keypair=wallet.coldkeypub ) - except Exception as e: + except SubstrateRequestException as e: payment_info = {"partialFee": int(2e7)} # assume 0.02 Tao err_console.print( f":cross_mark: [red]Failed to get payment info[/red]:[bold white]\n" - f" {e}[/bold white]\n" + f" {format_error_message(e, subtensor.substrate)}[/bold white]\n" f" Defaulting to default transfer fee: {payment_info['partialFee']}" ) @@ -121,7 +122,9 @@ async def do_transfer() -> tuple[bool, str, str]: print_verbose("Fetching existential and fee", status) block_hash = await subtensor.substrate.get_chain_head() account_balance_, existential_deposit = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=block_hash + ), subtensor.get_existential_deposit(block_hash=block_hash), ) account_balance = account_balance_[wallet.coldkeypub.ss58_address] diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f53c97d9c..62063c206 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -927,9 +927,11 @@ async def sign_and_send_extrinsic( if await response.is_success: return True, "" else: - return False, format_error_message(await response.error_message) + return False, format_error_message( + await response.error_message, substrate=self.substrate + ) except SubstrateRequestException as e: - return False, e + return False, format_error_message(e, substrate=self.substrate) async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: """ @@ -959,7 +961,7 @@ async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: else: return True, [], "" except SubstrateRequestException as e: - return False, [], str(e) + return False, [], format_error_message(e, self.substrate) async def get_subnet_hyperparameters( self, netuid: int, block_hash: Optional[str] = None diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 743ae71a2..e63807a83 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1,3 +1,4 @@ +import ast import math import os import sqlite3 @@ -26,6 +27,9 @@ if TYPE_CHECKING: from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters + from bittensor_cli.src.bittensor.async_substrate_interface import ( + AsyncSubstrateInterface, + ) console = Console() err_console = Console(stderr=True) @@ -448,24 +452,87 @@ def get_explorer_url_for_network( return explorer_urls -def format_error_message(error_message: dict) -> str: +def format_error_message( + error_message: Union[dict, Exception], substrate: "AsyncSubstrateInterface" +) -> str: """ - Formats an error message from the Subtensor error information to using in extrinsics. + Formats an error message from the Subtensor error information for use in extrinsics. - :param error_message: A dictionary containing the error information from Subtensor. + Args: + error_message: A dictionary containing the error information from Subtensor, or a SubstrateRequestException + containing dictionary literal args. + substrate: The initialised SubstrateInterface object to use. - :return: A formatted error message string. + Returns: + str: A formatted error message string. """ - err_type = "UnknownType" err_name = "UnknownError" + err_type = "UnknownType" err_description = "Unknown Description" + if isinstance(error_message, Exception): + # generally gotten through SubstrateRequestException args + new_error_message = None + for arg in error_message.args: + try: + d = ast.literal_eval(arg) + if isinstance(d, dict): + if "error" in d: + new_error_message = d["error"] + break + elif all(x in d for x in ["code", "message", "data"]): + new_error_message = d + break + except ValueError: + pass + if new_error_message is None: + return_val = " ".join(error_message.args) + return f"Subtensor returned: {return_val}" + else: + error_message = new_error_message + if isinstance(error_message, dict): - 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_docs[0] if len(err_docs) > 0 else err_description - return f"Subtensor returned `{err_name} ({err_type})` error. This means: `{err_description}`" + # subtensor error structure + if ( + error_message.get("code") + and error_message.get("message") + and error_message.get("data") + ): + err_name = "SubstrateRequestException" + err_type = error_message.get("message", "") + err_data = error_message.get("data", "") + + # subtensor custom error marker + if err_data.startswith("Custom error:") and substrate: + if substrate.metadata: + try: + pallet = substrate.metadata.get_metadata_pallet( + "SubtensorModule" + ) + error_index = int(err_data.split("Custom error:")[-1]) + + error_dict = pallet.errors[error_index].value + err_type = error_dict.get("message", err_type) + err_docs = error_dict.get("docs", []) + err_description = err_docs[0] if err_docs else err_description + except (AttributeError, IndexError): + err_console.print( + "Substrate pallets data unavailable. This is usually caused by an uninitialized substrate." + ) + else: + err_description = err_data + + elif ( + error_message.get("type") + and error_message.get("name") + and error_message.get("docs") + ): + 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 + + return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." def convert_blocks_to_time(blocks: int, block_time: int = 12) -> tuple[int, int, int]: @@ -877,8 +944,6 @@ def validate_chain_endpoint(endpoint_url) -> tuple[bool, str]: ) if not parsed.netloc: return False, "Invalid URL passed as the endpoint" - if not parsed.port: - return False, "No port specified in the URL" return True, "" diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py index 361b4f737..9a19c7361 100644 --- a/bittensor_cli/src/commands/root.py +++ b/bittensor_cli/src/commands/root.py @@ -11,9 +11,10 @@ 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 Constants, DelegatesDetails +from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, @@ -721,7 +722,7 @@ async def _get_list() -> tuple: rn: list[NeuronInfoLite] = await subtensor.neurons_lite(netuid=0) if not rn: - return None, None, None, None + return [], [], {}, {} di: dict[str, DelegatesDetails] = await subtensor.get_delegate_identities() ts: dict[str, ScaleType] = await subtensor.substrate.query_multiple( @@ -1539,16 +1540,24 @@ async def list_delegates(subtensor: SubtensorInterface): subtensor.get_delegates(block_hash=block_hash), ) - # TODO keep an eye on this, was not working at one point print_verbose("Fetching previous delegates info from chain", status) - prev_block_hash = await subtensor.substrate.get_block_hash( - max(0, block_number - 1200) - ) - prev_delegates = await subtensor.get_delegates(block_hash=prev_block_hash) + + 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]" + ":warning: [yellow]Could not fetch delegates history. [/yellow]" ) delegates.sort(key=lambda d: d.total_stake, reverse=True) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 5a95ac144..074722403 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -18,6 +18,7 @@ u16_to_float, u64_to_float, is_valid_ss58_address, + format_error_message, ) @@ -208,8 +209,11 @@ async def set_childkey_take_extrinsic( # ) return False, error_message - except Exception as e: - return False, f"Exception occurred while setting childkey take: {str(e)}" + except SubstrateRequestException as e: + return ( + False, + f"Exception occurred while setting childkey take: {format_error_message(e, subtensor.substrate)}", + ) async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int]: @@ -232,7 +236,9 @@ async def get_childkey_take(subtensor, hotkey: str, netuid: int) -> Optional[int return int(childkey_take_.value) except SubstrateRequestException as e: - err_console.print(f"Error querying ChildKeys: {e}") + err_console.print( + f"Error querying ChildKeys: {format_error_message(e, subtensor.substrate)}" + ) return None diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 095c9441f..0f48d6195 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -523,7 +523,7 @@ async def unstake_extrinsic( # Unstake it all. unstaking_balance = old_stake else: - unstaking_balance = amount + unstaking_balance = Balance.from_tao(amount) # Check enough to unstake. stake_on_uid = old_stake @@ -561,7 +561,6 @@ async def unstake_extrinsic( f":satellite: Unstaking from chain: [white]{subtensor}[/white] ...", spinner="earth", ): - unstaking_balance = Balance.from_tao(unstaking_balance) call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index 4440d034b..122fec6ae 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -129,7 +129,7 @@ async def _find_event_attributes_in_extrinsic_receipt( await response.process_events() if not await response.is_success: err_console.print( - f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" + f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message, substrate)}" ) await asyncio.sleep(0.5) return False diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index dd91c1999..a1dd22623 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -221,16 +221,25 @@ def _get_coldkey_ss58_addresses_for_path(path: str) -> tuple[list[str], list[str async def wallet_balance( - wallet: Wallet, subtensor: SubtensorInterface, all_balances: bool + wallet: Optional[Wallet], + subtensor: SubtensorInterface, + all_balances: bool, + ss58_addresses: Optional[str] = None, ): """Retrieves the current balance of the specified wallet""" - if not all_balances: + if ss58_addresses: + coldkeys = ss58_addresses + wallet_names = [f"Provided Address {i + 1}" for i in range(len(ss58_addresses))] + + elif not all_balances: if not wallet.coldkeypub_file.exists_on_device(): err_console.print("[bold red]No wallets found.[/bold red]") return with console.status("Retrieving balances", spinner="aesthetic") as status: - if all_balances: + if ss58_addresses: + print_verbose(f"Fetching data for ss58 address: {ss58_addresses}", status) + elif all_balances: print_verbose("Fetching data for all wallets", status) coldkeys, wallet_names = _get_coldkey_ss58_addresses_for_path(wallet.path) else: diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 7dbb18a62..cc5a7d379 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -7,6 +7,7 @@ import numpy as np from numpy.typing import NDArray from rich.prompt import Confirm +from substrateinterface.exceptions import SubstrateRequestException from bittensor_cli.src.bittensor.utils import err_console, console, format_error_message from bittensor_cli.src.bittensor.extrinsics.root import ( @@ -132,8 +133,10 @@ async def commit_weights( # _logger.info("Commit Hash: {}".format(commit_hash)) try: success, message = await self.do_commit_weights(commit_hash=commit_hash) - except Exception as e: - err_console.print(f"Error committing weights: {e}") + except SubstrateRequestException as e: + err_console.print( + f"Error committing weights: {format_error_message(e, self.subtensor.substrate)}" + ) # bittensor.logging.error(f"Error committing weights: {e}") success = False message = "No attempt made. Perhaps it is too soon to commit weights!" @@ -156,14 +159,11 @@ async def _commit_reveal( salt_length = 8 self.salt = list(os.urandom(salt_length)) - try: - # Attempt to commit the weights to the blockchain. - commit_success, commit_msg = await self.commit_weights( - uids=weight_uids, - weights=weight_vals, - ) - except Exception as e: - commit_success, commit_msg = False, str(e) + # Attempt to commit the weights to the blockchain. + commit_success, commit_msg = await self.commit_weights( + uids=weight_uids, + weights=weight_vals, + ) if commit_success: current_time = datetime.now().astimezone().replace(microsecond=0) @@ -207,11 +207,8 @@ async def _commit_reveal( return False, f"Failed to commit weights hash. {commit_msg}" async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str]: - try: - # Attempt to reveal the weights using the salt. - success, msg = await self.reveal_weights_extrinsic(weight_uids, weight_vals) - except Exception as e: - success, msg = False, str(e) + # Attempt to reveal the weights using the salt. + success, msg = await self.reveal_weights_extrinsic(weight_uids, weight_vals) if success: if not self.wait_for_finalization and not self.wait_for_inclusion: @@ -253,11 +250,14 @@ async def _do_set_weights(): keypair=self.wallet.hotkey, era={"period": 5}, ) - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=self.wait_for_inclusion, - wait_for_finalization=self.wait_for_finalization, - ) + try: + response = await self.subtensor.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=self.wait_for_inclusion, + wait_for_finalization=self.wait_for_finalization, + ) + except SubstrateRequestException as e: + return False, format_error_message(e, self.subtensor.substrate) # We only wait here if we expect finalization. if not self.wait_for_finalization and not self.wait_for_inclusion: return True, "Not waiting for finalization or inclusion." @@ -266,29 +266,25 @@ async def _do_set_weights(): if await response.is_success: return True, "Successfully set weights." else: - return False, format_error_message(await response.error_message) + return False, format_error_message( + await response.error_message, self.subtensor.substrate + ) with console.status( f":satellite: Setting weights on [white]{self.subtensor.network}[/white] ..." ): - try: - success, error_message = await _do_set_weights() - - if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + success, error_message = await _do_set_weights() - if success: - console.print(":white_heavy_check_mark: [green]Finalized[/green]") - # bittensor.logging.success(prefix="Set weights", suffix="Finalized: " + str(success)) - return True, "Successfully set weights and finalized." - else: - # bittensor.logging.error(msg=error_message, prefix="Set weights", suffix="Failed: ") - return False, error_message + if not self.wait_for_finalization and not self.wait_for_inclusion: + return True, "Not waiting for finalization or inclusion." - except Exception as e: - err_console.print(":cross_mark: [red]Failed[/red]: error:{}".format(e)) - # bittensor.logging.warning(prefix="Set weights", suffix="Failed: " + str(e)) - return False, str(e) + if success: + console.print(":white_heavy_check_mark: [green]Finalized[/green]") + # bittensor.logging.success(prefix="Set weights", suffix="Finalized: " + str(success)) + return True, "Successfully set weights and finalized." + else: + # bittensor.logging.error(msg=error_message, prefix="Set weights", suffix="Failed: ") + return False, error_message async def reveal_weights_extrinsic( self, weight_uids, weight_vals @@ -311,11 +307,14 @@ async def reveal_weights_extrinsic( call=call, keypair=self.wallet.hotkey, ) - response = await self.subtensor.substrate.submit_extrinsic( - extrinsic, - wait_for_inclusion=self.wait_for_inclusion, - wait_for_finalization=self.wait_for_finalization, - ) + try: + response = await self.subtensor.substrate.submit_extrinsic( + extrinsic, + wait_for_inclusion=self.wait_for_inclusion, + wait_for_finalization=self.wait_for_finalization, + ) + except SubstrateRequestException as e: + return False, format_error_message(e, self.subtensor.substrate) if not self.wait_for_finalization and not self.wait_for_inclusion: success, error_message = True, "" @@ -327,7 +326,9 @@ async def reveal_weights_extrinsic( else: success, error_message = ( False, - format_error_message(await response.error_message), + format_error_message( + await response.error_message, self.subtensor.substrate + ), ) if success: diff --git a/requirements.txt b/requirements.txt index 3131f6cb4..2e891bcf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,5 +16,5 @@ scalecodec==1.2.11 substrate-interface~=1.7.9 typer~=0.12 websockets>=12.0 -bittensor-wallet==2.0.1 +bittensor-wallet>=2.0.2 bt-decode==0.2.0a0 \ No newline at end of file diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py new file mode 100644 index 000000000..34c9b2d3c --- /dev/null +++ b/tests/unit_tests/test_cli.py @@ -0,0 +1,18 @@ +import pytest +import typer + +from bittensor_cli.cli import parse_mnemonic + + +def test_parse_mnemonic(): + # standard + assert parse_mnemonic("hello how are you") == "hello how are you" + # numbered + assert parse_mnemonic("1-hello 2-how 3-are 4-you") == "hello how are you" + with pytest.raises(typer.Exit): + # not starting with 1 + parse_mnemonic("2-hello 3-how 4-are 5-you") + # duplicate numbers + parse_mnemonic("1-hello 1-how 2-are 3-you") + # missing numbers + parse_mnemonic("1-hello 3-are 4-you")