diff --git a/CHANGELOG.md b/CHANGELOG.md index e3eeac9b3..604e56662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,26 @@ # Changelog -## 8.3.0 /2024-11-06 +## 8.3.1 /2024-11-13 ## What's Changed +* Better handle incorrect file path for wallets. by @thewhaleking in https://github.com/opentensor/btcli/pull/230 +* Handle websockets version 14, verbose error output by @thewhaleking in https://github.com/opentensor/btcli/pull/236 +* Handles the new PasswordError from bt-wallet by @thewhaleking in https://github.com/opentensor/btcli/pull/232 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v8.3.0...v.8.3.1 +## 8.3.0 /2024-11-06 + +## What's Changed * Better handle incorrect password by @thewhaleking in https://github.com/opentensor/btcli/pull/187 * Fixes success path of pow register by @ibraheem-opentensor in https://github.com/opentensor/btcli/pull/189 * Adds `--all` flag to transfer by @thewhaleking in https://github.com/opentensor/btcli/pull/181 -* Various fixes by @thewhaleking in https://github.com/opentensor/btcli/pull/199 +* In `do_transfer`, we check the balance with coldkeypub.ss58, but then retrieve it from the dict with coldkey.ss58. Resolve this. by @thewhaleking in https://github.com/opentensor/btcli/pull/199 +* Handle KeyboardInterrupt in CLI to gracefully exit (no traceback) by @thewhaleking in https://github.com/opentensor/btcli/pull/199 +* Handle race conditions where self.metadata may not be set before finishing initialising runtime (this may need optimised in the future) by @thewhaleking in https://github.com/opentensor/btcli/pull/199 +* Error description output by @thewhaleking in https://github.com/opentensor/btcli/pull/199 +* Taostats link fixed by @thewhaleking in https://github.com/opentensor/btcli/pull/199 +* Fixes not showing confirmation if --no-prompt is specified on stake remove by @thewhaleking in https://github.com/opentensor/btcli/pull/199 * Fix wallets in overview by @thewhaleking in https://github.com/opentensor/btcli/pull/197 * fix handling null neurons by @thewhaleking in https://github.com/opentensor/btcli/pull/214 * Fix cuda pow registration by @thewhaleking in https://github.com/opentensor/btcli/pull/215 @@ -16,6 +29,7 @@ * Support hotkey names for include/exclude in st add/remove by @thewhaleking in https://github.com/opentensor/btcli/pull/216 * Subvortex network added by @thewhaleking in https://github.com/opentensor/btcli/pull/223 * Add prompt option to all commands which use Confirm prompts by @thewhaleking in https://github.com/opentensor/btcli/pull/227 +* fix: local subtensor port by @distributedstatemachine in https://github.com/opentensor/btcli/pull/228 * Update local subtensor port by @distributedstatemachine in https://github.com/opentensor/btcli/pull/228 **Full Changelog**: https://github.com/opentensor/btcli/compare/v8.2.0...v8.3.0 diff --git a/bittensor_cli/__init__.py b/bittensor_cli/__init__.py index 6eaa6edc4..165a7ac22 100644 --- a/bittensor_cli/__init__.py +++ b/bittensor_cli/__init__.py @@ -18,6 +18,6 @@ from .cli import CLIManager -__version__ = "8.3.0" +__version__ = "8.3.1" __all__ = ["CLIManager", "__version__"] diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 319480d42..08f8cb08a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -7,6 +7,7 @@ import re import ssl import sys +import traceback from pathlib import Path from typing import Coroutine, Optional from dataclasses import fields @@ -57,7 +58,7 @@ class GitError(Exception): pass -__version__ = "8.3.0" +__version__ = "8.3.1" _core_version = re.match(r"^\d+\.\d+\.\d+", __version__).group(0) @@ -840,6 +841,7 @@ async def _run(): return result except (ConnectionRefusedError, ssl.SSLError): err_console.print(f"Unable to connect to the chain: {self.subtensor}") + verbose_console.print(traceback.format_exc()) except ( ConnectionClosed, SubstrateRequestException, @@ -847,6 +849,10 @@ async def _run(): ) as e: if isinstance(e, SubstrateRequestException): err_console.print(str(e)) + verbose_console.print(traceback.format_exc()) + except Exception as e: + err_console.print(f"An unknown error has occurred: {e}") + verbose_console.print(traceback.format_exc()) finally: if initiated is False: asyncio.create_task(cmd).cancel() diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index bd6bbb987..08e0cc9ba 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -6,13 +6,14 @@ from hashlib import blake2b from typing import Optional, Any, Union, Callable, Awaitable, cast -from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 from async_property import async_property +from bt_decode import PortableRegistry, decode as decode_by_type_string, MetadataV15 +from bittensor_wallet import Keypair +from packaging import version from scalecodec import GenericExtrinsic from scalecodec.base import ScaleBytes, ScaleType, RuntimeConfigurationObject from scalecodec.type_registry import load_type_registry_preset from scalecodec.types import GenericCall -from bittensor_wallet import Keypair from substrateinterface.exceptions import ( SubstrateRequestException, ExtrinsicNotFound, @@ -771,14 +772,13 @@ def __init__( """ self.chain_endpoint = chain_endpoint self.__chain = chain_name - self.ws = Websocket( - chain_endpoint, - options={ - "max_size": 2**32, - "read_limit": 2**16, - "write_limit": 2**16, - }, - ) + options = { + "max_size": 2**32, + "write_limit": 2**16, + } + if version.parse(websockets.__version__) < version.parse("14.0"): + options.update({"read_limit": 2**16}) + self.ws = Websocket(chain_endpoint, options=options) self._lock = asyncio.Lock() self.last_block_hash: Optional[str] = None self.config = { diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 584d1291c..d73837d2b 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -20,7 +20,6 @@ import backoff from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from Crypto.Hash import keccak import numpy as np from rich.prompt import Confirm @@ -37,6 +36,7 @@ get_human_readable, print_verbose, print_error, + unlock_key, ) if typing.TYPE_CHECKING: @@ -726,10 +726,8 @@ async def run_faucet_extrinsic( return False, "Requires torch" # Unlock coldkey - try: - wallet.unlock_coldkey() - except KeyFileError: - return False, "There was an error unlocking your coldkey" + if not (unlock_status := unlock_key(wallet, print_out=False)).success: + return False, unlock_status.message # Get previous balance. old_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) @@ -1639,10 +1637,7 @@ async def swap_hotkey_extrinsic( ) return False - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False if prompt: diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 295ee640b..22ed70a04 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -21,7 +21,6 @@ from typing import Union, List, TYPE_CHECKING 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 @@ -37,6 +36,7 @@ u16_normalized_float, print_verbose, format_error_message, + unlock_key, ) if TYPE_CHECKING: @@ -306,10 +306,7 @@ async def root_register_extrinsic( the response is `True`. """ - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False print_verbose(f"Checking if hotkey ({wallet.hotkey_str}) is registered on root") @@ -427,10 +424,7 @@ async def _do_set_weights(): err_console.print("Your hotkey is not registered to the root network") return False - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False # First convert types. diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index eada5ec70..3ca657cab 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -1,7 +1,6 @@ import asyncio from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm from substrateinterface.exceptions import SubstrateRequestException @@ -16,6 +15,7 @@ get_explorer_url_for_network, is_valid_bittensor_address_or_public_key, print_error, + unlock_key, ) @@ -115,10 +115,7 @@ async def do_transfer() -> tuple[bool, str, str]: return False console.print(f"[dark_orange]Initiating transfer on network: {subtensor.network}") # Unlock wallet coldkey. - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False # Check balance. diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index b6b05baae..a3954a85d 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1,4 +1,5 @@ import ast +from collections import namedtuple import math import os import sqlite3 @@ -9,7 +10,7 @@ from bittensor_wallet import Wallet, Keypair from bittensor_wallet.utils import SS58_FORMAT -from bittensor_wallet.errors import KeyFileError +from bittensor_wallet.errors import KeyFileError, PasswordError from bittensor_wallet import utils from jinja2 import Template from markupsafe import Markup @@ -35,6 +36,8 @@ err_console = Console(stderr=True) verbose_console = Console(quiet=True) +UnlockStatus = namedtuple("UnlockStatus", ["success", "message"]) + def print_console(message: str, colour: str, title: str, console: Console): console.print( @@ -238,11 +241,14 @@ def get_hotkey_wallets_for_wallet( def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: """Gets all wallets with coldkeys from a given path""" wallet_path = Path(path).expanduser() - wallets = [ - Wallet(name=directory.name, path=path) - for directory in wallet_path.iterdir() - if directory.is_dir() - ] + try: + wallets = [ + Wallet(name=directory.name, path=path) + for directory in wallet_path.iterdir() + if directory.is_dir() + ] + except FileNotFoundError: + wallets = [] return wallets @@ -974,3 +980,39 @@ def retry_prompt( return var else: err_console.print(rejection_text) + + +def unlock_key( + wallet: Wallet, unlock_type="cold", print_out: bool = True +) -> "UnlockStatus": + """ + Attempts to decrypt a wallet's coldkey or hotkey + Args: + wallet: a Wallet object + unlock_type: the key type, 'cold' or 'hot' + print_out: whether to print out the error message to the err_console + + Returns: UnlockStatus for success status of unlock, with error message if unsuccessful + + """ + if unlock_type == "cold": + unlocker = "unlock_coldkey" + elif unlock_type == "hot": + unlocker = "unlock_hotkey" + else: + raise ValueError( + f"Invalid unlock type provided: {unlock_type}. Must be 'cold' or 'hot'." + ) + try: + getattr(wallet, unlocker)() + return UnlockStatus(True, "") + except PasswordError: + err_msg = f"The password used to decrypt your {unlock_type.capitalize()}key Keyfile is invalid." + if print_out: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + return UnlockStatus(False, err_msg) + except KeyFileError: + err_msg = f"{unlock_type.capitalize()}key Keyfile is corrupt, non-writable, or non-readable, or non-existent." + if print_out: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + return UnlockStatus(False, err_msg) diff --git a/bittensor_cli/src/commands/root.py b/bittensor_cli/src/commands/root.py index 2401eb002..0af9c4ebb 100644 --- a/bittensor_cli/src/commands/root.py +++ b/bittensor_cli/src/commands/root.py @@ -3,7 +3,6 @@ from typing import Optional, TYPE_CHECKING from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError import numpy as np from numpy.typing import NDArray from rich import box @@ -42,6 +41,7 @@ ss58_to_vec_u8, update_metadata_table, group_subnets, + unlock_key, ) if TYPE_CHECKING: @@ -280,10 +280,7 @@ async def burned_register_extrinsic( finalization/inclusion, the response is `True`. """ - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False with console.status( @@ -537,10 +534,7 @@ async def get_stake_for_coldkey_and_hotkey( delegate_string = "delegate" if delegate else "undelegate" # Decrypt key - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False print_verbose("Checking if hotkey is a delegate") @@ -1098,11 +1092,7 @@ async def senate_vote( return False # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success and unlock_key(wallet, "hot").success: return False console.print(f"Fetching proposals in [dark_orange]network: {subtensor.network}") @@ -1322,11 +1312,7 @@ async def _do_set_take() -> bool: console.print(f"Setting take on [dark_orange]network: {subtensor.network}") # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success and unlock_key(wallet, "hot").success: return False result_ = await _do_set_take() @@ -1724,11 +1710,7 @@ async def nominate(wallet: Wallet, subtensor: SubtensorInterface, prompt: bool): console.print(f"Nominating on [dark_orange]network: {subtensor.network}") # Unlock the wallet. - try: - wallet.unlock_hotkey() - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success and unlock_key(wallet, "hot").success: return False print_verbose(f"Checking hotkey ({wallet.hotkey_str}) is a delegate") diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 97f8e5fc9..c64d486f6 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -2,7 +2,6 @@ from typing import Optional from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm, Prompt, IntPrompt from rich.table import Table from rich.text import Text @@ -19,6 +18,7 @@ u64_to_float, is_valid_ss58_address, format_error_message, + unlock_key, ) @@ -72,10 +72,8 @@ async def set_children_extrinsic( return False, "Operation Cancelled" # Decrypt coldkey. - try: - wallet.unlock_coldkey() - except KeyFileError: - return False, "There was an error unlocking your coldkey." + if not (unlock_status := unlock_key(wallet, print_out=False)).success: + return False, unlock_status.message with console.status( f":satellite: {operation} on [white]{subtensor.network}[/white] ..." @@ -158,10 +156,8 @@ async def set_childkey_take_extrinsic( return False, "Operation Cancelled" # Decrypt coldkey. - try: - wallet.unlock_coldkey() - except KeyFileError: - return False, "There was an error unlocking your coldkey." + if not (unlock_status := unlock_key(wallet, print_out=False)).success: + return False, unlock_status.message with console.status( f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..." diff --git a/bittensor_cli/src/commands/stake/stake.py b/bittensor_cli/src/commands/stake/stake.py index 144e40f84..7c7c83fe0 100644 --- a/bittensor_cli/src/commands/stake/stake.py +++ b/bittensor_cli/src/commands/stake/stake.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Optional, Sequence, Union, cast from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm from rich.table import Table, Column import typer @@ -28,6 +27,7 @@ render_tree, u16_normalized_float, validate_coldkey_presence, + unlock_key, ) if TYPE_CHECKING: @@ -103,10 +103,7 @@ async def add_stake_extrinsic( """ # Decrypt keys, - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False # Default to wallet's own hotkey if the value is not passed. @@ -310,10 +307,7 @@ async def add_stake_multiple_extrinsic( return True # Decrypt coldkey. - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False with console.status( @@ -491,11 +485,8 @@ async def unstake_extrinsic( :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. """ - # Decrypt keys, - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + # Decrypt coldkey + if not unlock_key(wallet).success: return False if hotkey_ss58 is None: @@ -663,10 +654,7 @@ async def unstake_multiple_extrinsic( return True # Unlock coldkey. - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False with console.status( diff --git a/bittensor_cli/src/commands/subnets.py b/bittensor_cli/src/commands/subnets.py index dba96bfd2..1d4620b73 100644 --- a/bittensor_cli/src/commands/subnets.py +++ b/bittensor_cli/src/commands/subnets.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Optional, cast from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich.prompt import Confirm from rich.table import Column, Table @@ -28,6 +27,7 @@ millify, render_table, update_metadata_table, + unlock_key, ) if TYPE_CHECKING: @@ -100,10 +100,7 @@ async def _find_event_attributes_in_extrinsic_receipt( ): return False - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False with console.status(":satellite: Registering subnet...", spinner="earth"): diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 6ebbb0eca..5711a3c27 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING, Union from bittensor_wallet import Wallet -from bittensor_wallet.errors import KeyFileError from rich import box from rich.table import Column, Table @@ -14,6 +13,7 @@ print_error, print_verbose, normalize_hyperparameters, + unlock_key, ) if TYPE_CHECKING: @@ -101,10 +101,7 @@ async def set_hyperparameter_extrinsic( ) return False - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False extrinsic = HYPERPARAMS.get(parameter) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index acce3b1a4..69188053e 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -55,6 +55,7 @@ is_valid_ss58_address, validate_coldkey_presence, retry_prompt, + unlock_key, ) @@ -1616,10 +1617,7 @@ async def set_id( print_error(f":cross_mark: This wallet doesn't own subnet {subnet_netuid}.") return False - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print("Error decrypting coldkey (possibly incorrect password)") + if not unlock_key(wallet).success: return False with console.status( @@ -1719,18 +1717,14 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): async def sign(wallet: Wallet, message: str, use_hotkey: str): """Sign a message using the provided wallet or hotkey.""" - - try: - wallet.unlock_coldkey() - except KeyFileError: - err_console.print( - ":cross_mark: [red]Keyfile is corrupt, non-writable, non-readable or the password used to decrypt is " - "invalid[/red]:[bold white]\n [/bold white]" - ) if not use_hotkey: + if not unlock_key(wallet).success: + return False keypair = wallet.coldkey print_verbose(f"Signing using coldkey: {wallet.name}") else: + if not unlock_key(wallet, "hot").success: + return False keypair = wallet.hotkey print_verbose(f"Signing using hotkey: {wallet.hotkey_str}") diff --git a/requirements.txt b/requirements.txt index 2e891bcf3..bb50a3a7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ fuzzywuzzy~=0.18.0 netaddr~=1.3.0 numpy>=2.0.1 Jinja2 +packaging pycryptodome # Crypto PyYAML~=6.0.1 pytest @@ -16,5 +17,5 @@ scalecodec==1.2.11 substrate-interface~=1.7.9 typer~=0.12 websockets>=12.0 -bittensor-wallet>=2.0.2 +bittensor-wallet>=2.1.0 bt-decode==0.2.0a0 \ No newline at end of file