From 35f8af65a57aca4dab6023a81fbf23abb5d6b1b5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes <37844818+thewhaleking@users.noreply.github.com> Date: Sun, 6 Oct 2024 18:44:35 +0200 Subject: [PATCH 01/20] Handle git not installed (#164) --- bittensor_cli/cli.py | 11 +++++++++-- bittensor_cli/src/bittensor/extrinsics/transfer.py | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 53478d98e..24e46e5c8 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 @@ -49,6 +48,14 @@ from websockets import ConnectionClosed from yaml import safe_dump, safe_load +try: + from git import Repo, GitError +except ImportError: + + class GitError(Exception): + pass + + __version__ = "8.1.0" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -368,7 +375,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() diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index c5d3789f7..7c7800808 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -121,7 +121,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] From b82ca0e1e3f742222a0ac529c2493ca5f9acd75d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Oct 2024 17:55:38 +0200 Subject: [PATCH 02/20] Handle cancelling receiving task before it's initiated --- bittensor_cli/src/bittensor/async_substrate_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 464635958..c953d02f5 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -679,7 +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() + except AttributeError: + pass try: await self._receiving_task except asyncio.CancelledError: From b781384d9aa98f4ce274d96d7fe056052418bf33 Mon Sep 17 00:00:00 2001 From: Benjamin Himes <37844818+thewhaleking@users.noreply.github.com> Date: Mon, 7 Oct 2024 18:38:34 +0200 Subject: [PATCH 03/20] Change network option to a list so that it can be correctly parsed if multiple options are given (#165) * Change network option to a list so that it can be correctly parsed * Make network Optional. * Adds info which networks were not selected --------- Co-authored-by: ibraheem-opentensor --- bittensor_cli/cli.py | 99 +++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 24e46e5c8..64202e17a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -746,7 +746,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 @@ -757,7 +757,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( @@ -1234,7 +1247,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, ): @@ -1361,7 +1374,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, @@ -1408,7 +1421,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." ), @@ -1470,7 +1483,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, @@ -1536,7 +1549,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, @@ -1904,7 +1917,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, ): @@ -1990,7 +2003,7 @@ def wallet_balance( "-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, ): @@ -2065,7 +2078,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", @@ -2245,7 +2258,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, ): @@ -2324,7 +2337,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, ): @@ -2348,7 +2361,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, @@ -2425,7 +2438,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", @@ -2476,7 +2489,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, @@ -2516,7 +2529,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, @@ -2557,7 +2570,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, @@ -2607,7 +2620,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, ): @@ -2625,7 +2638,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, @@ -2660,7 +2673,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, ): @@ -2678,7 +2691,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, @@ -2748,7 +2761,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, @@ -2823,7 +2836,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, @@ -2875,7 +2888,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, @@ -2942,7 +2955,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, ): @@ -3014,7 +3027,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, @@ -3063,7 +3076,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, @@ -3174,7 +3187,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, @@ -3288,7 +3301,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, @@ -3446,7 +3459,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)", @@ -3507,7 +3520,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)", @@ -3598,7 +3611,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)", @@ -3657,7 +3670,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, @@ -3731,7 +3744,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, @@ -3796,7 +3809,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, @@ -3817,7 +3830,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, @@ -3863,7 +3876,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, ): @@ -3884,7 +3897,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, @@ -3913,7 +3926,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( @@ -4000,7 +4013,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, @@ -4041,7 +4054,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, @@ -4131,7 +4144,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, @@ -4227,7 +4240,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, From 65589311fb291245fbf026b88db641a6b4b0449f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Oct 2024 20:43:05 +0200 Subject: [PATCH 04/20] Fixes receiving task cancellation (does not try to await if not started) --- bittensor_cli/src/bittensor/async_substrate_interface.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index c953d02f5..0268a39a2 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -681,11 +681,8 @@ async def shutdown(self): async with self._lock: try: self._receiving_task.cancel() - except AttributeError: - pass - try: await self._receiving_task - except asyncio.CancelledError: + except (AttributeError, asyncio.CancelledError): pass await self.ws.close() self.ws = None From c9277a4a4c4aba90567644dc6913d1bfbd27ee66 Mon Sep 17 00:00:00 2001 From: Benjamin Himes <37844818+thewhaleking@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:50:35 +0200 Subject: [PATCH 05/20] mnemonic change: support numbered mnemonic (#167) * Allow for sending mnemonics with the format "1-hello 2-how 3-are 4-you" * Now fails for numbered mnemonics not starting with 1, missing numbers in numbered mnemonics, and duplicate numbers in numbered mnemonics. * Unit tests for `parse_mnemonic` --- bittensor_cli/cli.py | 32 ++++++++++++++++++++++++++++++-- tests/unit_tests/__init__.py | 0 tests/unit_tests/test_cli.py | 18 ++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/__init__.py create mode 100644 tests/unit_tests/test_cli.py diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 64202e17a..46109f4ab 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -282,8 +282,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, @@ -296,9 +322,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 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") From ef941241a23d6b0d1164296d023e163062d842c8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes <37844818+thewhaleking@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:06:42 +0200 Subject: [PATCH 06/20] Handle custom errors from subtensor (#79) --- .../src/bittensor/extrinsics/registration.py | 8 +- .../src/bittensor/extrinsics/root.py | 14 +-- .../src/bittensor/extrinsics/transfer.py | 5 +- .../src/bittensor/subtensor_interface.py | 8 +- bittensor_cli/src/bittensor/utils.py | 87 ++++++++++++++++--- .../src/commands/stake/children_hotkeys.py | 12 ++- bittensor_cli/src/commands/subnets.py | 2 +- bittensor_cli/src/commands/weights.py | 87 ++++++++++--------- 8 files changed, 153 insertions(+), 70 deletions(-) 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 7c7800808..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']}" ) 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..b4dda0440 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]: 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/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/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: From 1df2e8dc60d70ab5ee29c94369a4648d496d7fb3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 7 Oct 2024 16:17:21 -0700 Subject: [PATCH 07/20] Ignore port --- bittensor_cli/src/bittensor/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 743ae71a2..b3e83bf2d 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -877,8 +877,8 @@ 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" + # if not parsed.port: + # return False, "No port specified in the URL" return True, "" From 78c2b95f7d5d54ec647d01b4e89dd173ef1f1c4a Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Mon, 7 Oct 2024 16:18:21 -0700 Subject: [PATCH 08/20] Removes validation for port --- bittensor_cli/src/bittensor/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index b3e83bf2d..05d0b180d 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -877,8 +877,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, "" From 8177ec55cbec14c4d96c80cd78286b866c7041e9 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 8 Oct 2024 17:42:17 -0700 Subject: [PATCH 09/20] Adds support for ss58 addresses in wallet balance --- bittensor_cli/cli.py | 30 ++++++++++++++++++++------- bittensor_cli/src/commands/wallets.py | 15 +++++++++++--- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 669c354b4..6fc932497 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -129,7 +129,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.", @@ -2026,6 +2028,7 @@ 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_address: Optional[str] = Options.ss58_address, all_balances: Optional[bool] = typer.Option( False, "--all", @@ -2055,14 +2058,27 @@ def wallet_balance( """ 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_address: + if is_valid_ss58_address(ss58_address): + wallet = None + else: + print_error( + "You have entered an incorrect ss58 address. Please try again" + ) + 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_address) ) def wallet_history( diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index dd91c1999..b95f5ee9b 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_address: Optional[str] = None, ): """Retrieves the current balance of the specified wallet""" - if not all_balances: + if ss58_address: + coldkeys = [ss58_address] + wallet_names = ["Provided Address"] + + 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_address: + print_verbose(f"Fetching data for ss58 address: {ss58_address}", status) + elif all_balances: print_verbose("Fetching data for all wallets", status) coldkeys, wallet_names = _get_coldkey_ss58_addresses_for_path(wallet.path) else: From cc957049de430f299d85aea99ada6084e072b79c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 9 Oct 2024 11:16:12 -0700 Subject: [PATCH 10/20] Shifts conversion to correct place --- bittensor_cli/src/commands/stake/stake.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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", From fa97cc7a5d380f85642982670808df30adb216b1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 9 Oct 2024 11:41:23 -0700 Subject: [PATCH 11/20] Adds support for multiple coldkeys --- bittensor_cli/cli.py | 16 +++++++++++----- bittensor_cli/src/commands/wallets.py | 4 ++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6fc932497..91d2e2b78 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2028,7 +2028,7 @@ 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_address: Optional[str] = Options.ss58_address, + ss58_address: Optional[list[str]] = Options.ss58_address, all_balances: Optional[bool] = typer.Option( False, "--all", @@ -2059,12 +2059,18 @@ def wallet_balance( self.verbosity_handler(quiet, verbose) if ss58_address: - if is_valid_ss58_address(ss58_address): + valid_ss58s = [ + ss58 for ss58 in set(ss58_address) if is_valid_ss58_address(ss58) + ] + + invalid_ss58s = set(ss58_address) - set(valid_ss58s) + for invalid_ss58 in invalid_ss58s: + print_error(f"Incorrect ss58 address: {invalid_ss58}. Skipping.") + + if valid_ss58s: wallet = None + ss58_address = valid_ss58s else: - print_error( - "You have entered an incorrect ss58 address. Please try again" - ) raise typer.Exit() else: ask_for = [WO.PATH] if all_balances else [WO.NAME, WO.PATH] diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b95f5ee9b..f7d12456c 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -228,8 +228,8 @@ async def wallet_balance( ): """Retrieves the current balance of the specified wallet""" if ss58_address: - coldkeys = [ss58_address] - wallet_names = ["Provided Address"] + coldkeys = ss58_address + wallet_names = [f"Provided Address {i + 1}" for i in range(len(ss58_address))] elif not all_balances: if not wallet.coldkeypub_file.exists_on_device(): From 306fd30c6a30d33117b8d2b0b43ad9538576b60e Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 9 Oct 2024 11:46:44 -0700 Subject: [PATCH 12/20] Renames ss58_address -> ss58_addresses --- bittensor_cli/cli.py | 12 ++++++------ bittensor_cli/src/commands/wallets.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 91d2e2b78..cc7b55cfc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2028,7 +2028,7 @@ 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_address: Optional[list[str]] = Options.ss58_address, + ss58_addresses: Optional[list[str]] = Options.ss58_address, all_balances: Optional[bool] = typer.Option( False, "--all", @@ -2058,18 +2058,18 @@ def wallet_balance( """ self.verbosity_handler(quiet, verbose) - if ss58_address: + if ss58_addresses: valid_ss58s = [ - ss58 for ss58 in set(ss58_address) if is_valid_ss58_address(ss58) + ss58 for ss58 in set(ss58_addresses) if is_valid_ss58_address(ss58) ] - invalid_ss58s = set(ss58_address) - set(valid_ss58s) + 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_address = valid_ss58s + ss58_addresses = valid_ss58s else: raise typer.Exit() else: @@ -2084,7 +2084,7 @@ def wallet_balance( ) subtensor = self.initialize_chain(network) return self._run_command( - wallets.wallet_balance(wallet, subtensor, all_balances, ss58_address) + wallets.wallet_balance(wallet, subtensor, all_balances, ss58_addresses) ) def wallet_history( diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index f7d12456c..a1dd22623 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -224,12 +224,12 @@ async def wallet_balance( wallet: Optional[Wallet], subtensor: SubtensorInterface, all_balances: bool, - ss58_address: Optional[str] = None, + ss58_addresses: Optional[str] = None, ): """Retrieves the current balance of the specified wallet""" - if ss58_address: - coldkeys = ss58_address - wallet_names = [f"Provided Address {i + 1}" for i in range(len(ss58_address))] + 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(): @@ -237,8 +237,8 @@ async def wallet_balance( return with console.status("Retrieving balances", spinner="aesthetic") as status: - if ss58_address: - print_verbose(f"Fetching data for ss58 address: {ss58_address}", status) + 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) From 4c8407a81c964320c78b712e86c2def112c13948 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 9 Oct 2024 12:17:22 -0700 Subject: [PATCH 13/20] Updates docs --- bittensor_cli/cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index cc7b55cfc..b78c831ab 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2042,6 +2042,8 @@ def wallet_balance( """ 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: @@ -2055,6 +2057,11 @@ 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) From 861e4a5349e519b1b6aa60e4afaf3feeb96e7249 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 9 Oct 2024 15:02:06 -0700 Subject: [PATCH 14/20] Fixes network instantiation in root list-delegates --- bittensor_cli/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 669c354b4..19b929f4b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3038,14 +3038,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)) From 5c89e8ce4a21b7fe7564c9e4f556d35ce9497994 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 10 Oct 2024 14:17:43 +0200 Subject: [PATCH 15/20] Adds a utils app with a single subcommand (convert) to easily convert between rao and tao. --- bittensor_cli/cli.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 64202e17a..3571743b8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -26,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, ) @@ -400,6 +401,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 = { @@ -524,6 +526,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) @@ -4333,6 +4338,32 @@ 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" + ), + ): + 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: + console.print( + f"{from_rao}{Balance.rao_unit}", + "=", + Balance.from_rao(int(float(from_rao))), + ) + if from_tao is not None: + console.print( + f"{Balance.unit}{from_tao}", + "=", + f"{Balance.from_tao(float(from_tao)).rao}{Balance.rao_unit}", + ) + def run(self): self.app() From 3e16772986c373117b3f3e357e7d90a8a9612e9b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 10 Oct 2024 14:23:48 +0200 Subject: [PATCH 16/20] Better UX --- bittensor_cli/cli.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3571743b8..3fe783a5f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4348,20 +4348,25 @@ def convert( 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"{from_rao}{Balance.rao_unit}", + f"{rao}{Balance.rao_unit}", "=", - Balance.from_rao(int(float(from_rao))), + Balance.from_rao(rao), ) if from_tao is not None: + tao = float(from_tao) console.print( - f"{Balance.unit}{from_tao}", + f"{Balance.unit}{tao}", "=", - f"{Balance.from_tao(float(from_tao)).rao}{Balance.rao_unit}", + f"{Balance.from_tao(tao).rao}{Balance.rao_unit}", ) def run(self): From f36deb060676b09e7b37fde322165c769a5df486 Mon Sep 17 00:00:00 2001 From: Benjamin Himes <37844818+thewhaleking@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:05:10 +0200 Subject: [PATCH 17/20] Fixes for rpc request error handler, root list default empty values, prev delegate fetching (#175) * Fixes issue from #131 where "rpc_request" was not changed to `payload_id` in the error handler. * Correctly use default empty values to avoid None issues. * Previous delegates fetching fix for non-archive nodes. --- .../bittensor/async_substrate_interface.py | 2 +- bittensor_cli/src/commands/root.py | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 0268a39a2..0f4dd0d43 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -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/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) From 2a2bb3827fbfde674eef28b9418c56b5036b6231 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 10 Oct 2024 13:28:01 -0700 Subject: [PATCH 18/20] Bumps version, updates requirement --- bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 96f9a2a5a..a9dbfaf53 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.1.2" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b35c432e8..6eead3b38 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -57,7 +57,7 @@ class GitError(Exception): pass -__version__ = "8.1.1" +__version__ = "8.1.2" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) diff --git a/requirements.txt b/requirements.txt index 3131f6cb4..9464e1246 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.1 bt-decode==0.2.0a0 \ No newline at end of file From 833a65119d84b85b4e4038668e13e271bfe45611 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 10 Oct 2024 13:33:19 -0700 Subject: [PATCH 19/20] Bumps wallet version to 2.0.2 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9464e1246..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 From b17b1a1f3233c41d6e242479af92dc1901924e64 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 10 Oct 2024 13:57:42 -0700 Subject: [PATCH 20/20] Updates changelog and bumps version --- CHANGELOG.md | 20 ++++++++++++++++++++ bittensor_cli/__init__.py | 2 +- bittensor_cli/cli.py | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) 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 a9dbfaf53..8fff761ce 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.1.2" +__version__ = "8.2.0" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6eead3b38..b012c989e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -57,7 +57,7 @@ class GitError(Exception): pass -__version__ = "8.1.2" +__version__ = "8.2.0" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0)