diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4484f5288..20ddb90d1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4379,7 +4379,7 @@ def stake_move( f"interactive_selection: {interactive_selection}\n" f"prompt: {prompt}\n" ) - result = self._run_command( + result, ext_id = self._run_command( move_stake.move_stake( subtensor=self.initialize_chain(network), wallet=wallet, @@ -4395,7 +4395,9 @@ def stake_move( ) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) return result def stake_transfer( @@ -4555,7 +4557,7 @@ def stake_transfer( f"era: {period}\n" f"stake_all: {stake_all}" ) - result = self._run_command( + result, ext_id = self._run_command( move_stake.transfer_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -4571,7 +4573,9 @@ def stake_transfer( ) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) return result def stake_swap( @@ -4675,7 +4679,7 @@ def stake_swap( f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) - result = self._run_command( + result, ext_id = self._run_command( move_stake.swap_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -4691,7 +4695,9 @@ def stake_swap( ) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id or None}) + ) return result def stake_get_children( @@ -4990,7 +4996,7 @@ def stake_childkey_take( f"wait_for_inclusion: {wait_for_inclusion}\n" f"wait_for_finalization: {wait_for_finalization}\n" ) - results: list[tuple[Optional[int], bool]] = self._run_command( + results: list[tuple[Optional[int], bool, Optional[str]]] = self._run_command( children_hotkeys.childkey_take( wallet=wallet, subtensor=self.initialize_chain(network), @@ -5004,8 +5010,8 @@ def stake_childkey_take( ) if json_output: output = {} - for netuid_, success in results: - output[netuid_] = success + for netuid_, success, ext_id in results: + output[netuid_] = {"success": success, "extrinsic_identifier": ext_id} json_console.print(json.dumps(output)) return results @@ -5128,7 +5134,7 @@ def sudo_set( f"param_name: {param_name}\n" f"param_value: {param_value}" ) - result, err_msg = self._run_command( + result, err_msg, ext_id = self._run_command( sudo.sudo_set_hyperparameter( wallet, self.initialize_chain(network), @@ -5140,7 +5146,15 @@ def sudo_set( ) ) if json_output: - json_console.print(json.dumps({"success": result, "err_msg": err_msg})) + json_console.print( + json.dumps( + { + "success": result, + "err_msg": err_msg, + "extrinsic_identifier": ext_id, + } + ) + ) return result def sudo_get( @@ -5225,7 +5239,7 @@ def sudo_senate_vote( None, "--vote-aye/--vote-nay", prompt="Enter y to vote Aye, or enter n to vote Nay", - help="The vote casted on the proposal", + help="The vote cast on the proposal", ), ): """ @@ -5302,11 +5316,13 @@ def sudo_set_take( ) raise typer.Exit() logger.debug(f"args:\nnetwork: {network}\ntake: {take}") - result = self._run_command( + result, ext_id = self._run_command( sudo.set_take(wallet, self.initialize_chain(network), take) ) if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id}) + ) return result def sudo_get_take( @@ -5870,13 +5886,15 @@ def subnets_set_identity( logger.debug( f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}" ) - success = self._run_command( + success, ext_id = self._run_command( subnets.set_identity( wallet, self.initialize_chain(network), netuid, identity, prompt ) ) if json_output: - json_console.print(json.dumps({"success": success})) + json_console.print( + json.dumps({"success": success, "extrinsic_identifier": ext_id}) + ) def subnets_pow_register( self, @@ -6204,6 +6222,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ self.verbosity_handler(quiet, verbose, json_output) + # TODO think we need to ','.split uids and weights ? uids = list_prompt(uids, int, "UIDs of interest for the specified netuid") weights = list_prompt( weights, float, "Corresponding weights for the specified UIDs" diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 2c2371761..a32bc1c3d 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -18,6 +18,7 @@ from typing import Optional import subprocess +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from Crypto.Hash import keccak import numpy as np @@ -40,6 +41,7 @@ unlock_key, hex_to_bytes, get_hotkey_pub_ss58, + print_extrinsic_id, ) if typing.TYPE_CHECKING: @@ -679,7 +681,7 @@ async def burned_register_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, era: Optional[int] = None, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Registers the wallet to chain by recycling TAO. :param subtensor: The SubtensorInterface object to use for the call, initialized @@ -698,7 +700,7 @@ async def burned_register_extrinsic( """ if not (unlock_status := unlock_key(wallet, print_out=False)).success: - return False, unlock_status.message + return False, unlock_status.message, None with console.status( f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...", @@ -742,7 +744,7 @@ async def burned_register_extrinsic( f"hotkey: [{COLOR_PALETTE.G.HK}]{neuron.hotkey}[/{COLOR_PALETTE.G.HK}]\n" f"coldkey: [{COLOR_PALETTE.G.CK}]{neuron.coldkey}[/{COLOR_PALETTE.G.CK}]" ) - return True, "Already registered" + return True, "Already registered", None with console.status( ":satellite: Recycling TAO for Registration...", spinner="aesthetic" @@ -755,16 +757,18 @@ async def burned_register_extrinsic( "hotkey": get_hotkey_pub_ss58(wallet), }, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization, era=era_ ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - return False, err_msg + return False, err_msg, None # Successful registration, final check for neuron and pubkey else: + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) with console.status(":satellite: Checking Balance...", spinner="aesthetic"): block_hash = await subtensor.substrate.get_chain_head() new_balance, netuids_for_hotkey, my_uid = await asyncio.gather( @@ -791,13 +795,13 @@ async def burned_register_extrinsic( console.print( f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" ) - return True, f"Registered on {netuid} with UID {my_uid}" + return True, f"Registered on {netuid} with UID {my_uid}", ext_id else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False, "Unknown error. Neuron not found." + return False, "Unknown error. Neuron not found.", ext_id async def run_faucet_extrinsic( @@ -1749,7 +1753,7 @@ async def swap_hotkey_extrinsic( new_wallet: Wallet, netuid: Optional[int] = None, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """ Performs an extrinsic update for swapping two hotkeys on the chain @@ -1770,14 +1774,14 @@ async def swap_hotkey_extrinsic( err_console.print( f":cross_mark: [red]Failed[/red]: Original hotkey {hk_ss58} is not registered on subnet {netuid}" ) - return False + return False, None elif not len(netuids_registered) > 0: err_console.print( f"Original hotkey [dark_orange]{hk_ss58}[/dark_orange] is not registered on any subnet. " f"Please register and try again" ) - return False + return False, None if netuid is not None: if netuid in netuids_registered_new_hotkey: @@ -1785,17 +1789,17 @@ async def swap_hotkey_extrinsic( f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} " f"is already registered on subnet {netuid}" ) - return False + return False, None else: if len(netuids_registered_new_hotkey) > 0: err_console.print( f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} " f"is already registered on subnet(s) {netuids_registered_new_hotkey}" ) - return False + return False, None if not unlock_key(wallet).success: - return False + return False, None if prompt: # Prompt user for confirmation. @@ -1815,7 +1819,7 @@ async def swap_hotkey_extrinsic( ) if not Confirm.ask(confirm_message): - return False + return False, None print_verbose( f"Swapping {wallet.name}'s hotkey ({hk_ss58} - {wallet.hotkey_str}) with " f"{new_wallet.name}'s hotkey ({new_hk_ss58} - {new_wallet.hotkey_str})" @@ -1832,15 +1836,17 @@ async def swap_hotkey_extrinsic( call_function="swap_hotkey", call_params=call_params, ) - success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if success: console.print( f"Hotkey {hk_ss58} ({wallet.hotkey_str}) swapped for new hotkey: " f"{new_hk_ss58} ({new_wallet.hotkey_str})" ) - return True + return True, ext_receipt else: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") time.sleep(0.5) - return False + return False, ext_receipt diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 207fb8642..ea515ed1a 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -18,7 +18,7 @@ import asyncio import hashlib import time -from typing import Union, List, TYPE_CHECKING +from typing import Union, List, TYPE_CHECKING, Optional from bittensor_wallet import Wallet, Keypair import numpy as np @@ -38,6 +38,7 @@ format_error_message, unlock_key, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -291,7 +292,7 @@ async def root_register_extrinsic( wallet: Wallet, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: r"""Registers the wallet to root network. :param subtensor: The SubtensorInterface object @@ -307,7 +308,7 @@ async def root_register_extrinsic( """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None print_verbose(f"Checking if hotkey ({wallet.hotkey_str}) is registered on root") is_registered = await is_hotkey_registered( @@ -317,7 +318,7 @@ async def root_register_extrinsic( console.print( ":white_heavy_check_mark: [green]Already registered on root network.[/green]" ) - return True, "Already registered on root network" + return True, "Already registered on root network", None with console.status(":satellite: Registering to root network...", spinner="earth"): call = await subtensor.substrate.compose_call( @@ -325,7 +326,7 @@ async def root_register_extrinsic( call_function="root_register", call_params={"hotkey": get_hotkey_pub_ss58(wallet)}, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, @@ -335,10 +336,12 @@ async def root_register_extrinsic( if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - return False, err_msg + return False, err_msg, None # Successful registration, final check for neuron and pubkey else: + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) uid = await subtensor.query( module="SubtensorModule", storage_function="Uids", @@ -348,13 +351,13 @@ async def root_register_extrinsic( console.print( f":white_heavy_check_mark: [green]Registered with UID {uid}[/green]" ) - return True, f"Registered with UID {uid}" + return True, f"Registered with UID {uid}", ext_id else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False, "Unknown error. Neuron not found." + return False, "Unknown error. Neuron not found.", ext_id async def set_root_weights_extrinsic( diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index ad3168a23..65ecfbcf8 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -1,10 +1,11 @@ import asyncio +from typing import Optional, Union +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich.prompt import Confirm from async_substrate_interface.errors import SubstrateRequestException -from bittensor_cli.src import NETWORK_EXPLORER_MAP from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( @@ -12,7 +13,6 @@ err_console, print_verbose, format_error_message, - get_explorer_url_for_network, is_valid_bittensor_address_or_public_key, print_error, unlock_key, @@ -30,7 +30,7 @@ async def transfer_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Transfers funds from this wallet to the destination public key address. :param subtensor: initialized SubtensorInterface object used for transfer @@ -75,7 +75,7 @@ async def get_transfer_fee() -> Balance: return Balance.from_rao(payment_info["partial_fee"]) - async def do_transfer() -> tuple[bool, str, str]: + async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]: """ Makes transfer from wallet to destination public key address. :return: success, block hash, formatted error message @@ -95,27 +95,32 @@ async def do_transfer() -> tuple[bool, str, str]: ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "", "" + return True, "", "", response # Otherwise continue with finalization. if await response.is_success: block_hash_ = response.block_hash - return True, block_hash_, "" + return True, block_hash_, "", response else: - return False, "", format_error_message(await response.error_message) + return ( + False, + "", + format_error_message(await response.error_message), + response, + ) # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): err_console.print( f":cross_mark: [red]Invalid destination SS58 address[/red]:[bold white]\n {destination}[/bold white]" ) - return False + return False, None console.print(f"[dark_orange]Initiating transfer on network: {subtensor.network}") # Unlock wallet coldkey. if not unlock_key(wallet).success: - return False + return False, None - call_params = {"dest": destination} + call_params: dict[str, Optional[Union[str, int]]] = {"dest": destination} if transfer_all: call_function = "transfer_all" if allow_death: @@ -158,7 +163,7 @@ async def do_transfer() -> tuple[bool, str, str]: f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n" f"You can try again with `--allow-death`." ) - return False + return False, None elif account_balance < (amount + fee) and allow_death: print_error( ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n" @@ -166,7 +171,7 @@ async def do_transfer() -> tuple[bool, str, str]: f" amount: [bright_red]{amount}[/bright_red]\n" f" for fee: [bright_red]{fee}[/bright_red]" ) - return False + return False, None # Ask before moving on. if prompt: @@ -179,27 +184,15 @@ async def do_transfer() -> tuple[bool, str, str]: f":warning:[bright_yellow]Transferring is not the same as staking. To instead stake, use " f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]:warning:" ): - return False + return False, None - with console.status(":satellite: Transferring...", spinner="earth") as status: - success, block_hash, err_msg = await do_transfer() + with console.status(":satellite: Transferring...", spinner="earth"): + success, block_hash, err_msg, ext_receipt = await do_transfer() if success: console.print(":white_heavy_check_mark: [green]Finalized[/green]") console.print(f"[green]Block Hash: {block_hash}[/green]") - if subtensor.network == "finney": - print_verbose("Fetching explorer URLs", status) - explorer_urls = get_explorer_url_for_network( - subtensor.network, block_hash, NETWORK_EXPLORER_MAP - ) - if explorer_urls != {} and explorer_urls: - console.print( - f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]" - ) - console.print( - f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]" - ) else: console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") @@ -212,6 +205,6 @@ async def do_transfer() -> tuple[bool, str, str]: f"Balance:\n" f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]" ) - return True + return True, ext_receipt - return False + return False, None diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 27df7d94c..d554fdbb4 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -4,6 +4,7 @@ from typing import Optional, Any, Union, TypedDict, Iterable import aiohttp +from async_substrate_interface import AsyncExtrinsicReceipt from async_substrate_interface.async_substrate import ( DiskCachedAsyncSubstrateInterface, AsyncSubstrateInterface, @@ -1081,7 +1082,7 @@ async def sign_and_send_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, era: Optional[dict[str, int]] = None, - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1093,7 +1094,10 @@ async def sign_and_send_extrinsic( :return: (success, error message) """ - call_args = {"call": call, "keypair": wallet.coldkey} + call_args: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = { + "call": call, + "keypair": wallet.coldkey, + } if era is not None: call_args["era"] = era extrinsic = await self.substrate.create_signed_extrinsic( @@ -1107,13 +1111,13 @@ async def sign_and_send_extrinsic( ) # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, "" + return True, "", response if await response.is_success: - return True, "" + return True, "", response else: - return False, format_error_message(await response.error_message) + return False, format_error_message(await response.error_message), None except SubstrateRequestException as e: - return False, format_error_message(e) + return False, format_error_message(e), None async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]: """ diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 80aab6916..5ec5fd56a 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -10,6 +10,7 @@ from functools import partial import re +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet, Keypair from bittensor_wallet.utils import SS58_FORMAT from bittensor_wallet.errors import KeyFileError, PasswordError @@ -507,6 +508,7 @@ def get_explorer_url_for_network( :return: The explorer url for the given block hash and network """ + # TODO remove explorer_urls: dict[str, str] = {} # Will be None if the network is not known. i.e. not in network_map @@ -1462,3 +1464,30 @@ def get_hotkey_pub_ss58(wallet: Wallet) -> str: return wallet.hotkeypub.ss58_address except (KeyFileError, AttributeError): return wallet.hotkey.ss58_address + + +async def print_extrinsic_id( + extrinsic_receipt: Optional[AsyncExtrinsicReceipt], +) -> None: + """ + Prints the extrinsic identifier to the console. If the substrate attached to the extrinsic receipt is on a finney + node, it will also include a link to browse the extrinsic in tao dot app. + Args: + extrinsic_receipt: AsyncExtrinsicReceipt object from a successful extrinsic submission. + """ + if extrinsic_receipt is None: + return + substrate = extrinsic_receipt.substrate + ext_id = await extrinsic_receipt.get_extrinsic_identifier() + if substrate: + query = await substrate.rpc_request("system_chainType", []) + if query.get("result") == "Live": + console.print( + f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}: " + f"[blue]https://tao.app/extrinsic/{ext_id}[/blue]" + ) + return + console.print( + f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}" + ) + return diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py index 60f5c6529..cc25ff1e9 100644 --- a/bittensor_cli/src/commands/liquidity/liquidity.py +++ b/bittensor_cli/src/commands/liquidity/liquidity.py @@ -2,6 +2,7 @@ import json from typing import TYPE_CHECKING, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from rich.prompt import Confirm from rich.table import Column, Table @@ -11,6 +12,7 @@ console, err_console, json_console, + print_extrinsic_id, ) from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src.commands.liquidity.utils import ( @@ -36,7 +38,7 @@ async def add_liquidity_extrinsic( price_high: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Adds liquidity to the specified price range. @@ -60,7 +62,7 @@ async def add_liquidity_extrinsic( `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None tick_low = price_to_tick(price_low.tao) tick_high = price_to_tick(price_high.tao) @@ -94,7 +96,7 @@ async def modify_liquidity_extrinsic( liquidity_delta: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Modifies liquidity in liquidity position by adding or removing liquidity from it. Arguments: @@ -116,7 +118,7 @@ async def modify_liquidity_extrinsic( Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None call = await subtensor.substrate.compose_call( call_module="Swap", @@ -145,7 +147,7 @@ async def remove_liquidity_extrinsic( position_id: int, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Remove liquidity and credit balances back to wallet's hotkey stake. Arguments: @@ -166,7 +168,7 @@ async def remove_liquidity_extrinsic( Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None call = await subtensor.substrate.compose_call( call_module="Swap", @@ -193,7 +195,7 @@ async def toggle_user_liquidity_extrinsic( enable: bool, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """Allow to toggle user liquidity for specified subnet. Arguments: @@ -210,7 +212,7 @@ async def toggle_user_liquidity_extrinsic( - False and an error message if the submission fails or the wallet cannot be unlocked. """ if not (unlock := unlock_key(wallet)).success: - return False, unlock.message + return False, unlock.message, None call = await subtensor.substrate.compose_call( call_module="Swap", @@ -232,16 +234,16 @@ async def add_liquidity( wallet: "Wallet", hotkey_ss58: str, netuid: Optional[int], - liquidity: Optional[float], - price_low: Optional[float], - price_high: Optional[float], + liquidity: Balance, + price_low: Balance, + price_high: Balance, prompt: bool, json_output: bool, ) -> tuple[bool, str]: """Add liquidity position to provided subnet.""" # Check wallet access - if not unlock_key(wallet).success: - return False + if not (ulw := unlock_key(wallet)).success: + return False, ulw.message # Check that the subnet exists. if not await subtensor.subnet_exists(netuid=netuid): @@ -260,7 +262,7 @@ async def add_liquidity( if not Confirm.ask("Would you like to continue?"): return False, "User cancelled operation." - success, message = await add_liquidity_extrinsic( + success, message, ext_receipt = await add_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, @@ -269,8 +271,14 @@ async def add_liquidity( price_low=price_low, price_high=price_high, ) + await print_extrinsic_id(ext_receipt) + ext_id = await ext_receipt.get_extrinsic_identifier() if json_output: - json_console.print(json.dumps({"success": success, "message": message})) + json_console.print( + json.dumps( + {"success": success, "message": message, "extrinsic_identifier": ext_id} + ) + ) else: if success: console.print( @@ -278,6 +286,7 @@ async def add_liquidity( ) else: err_console.print(f"[red]Error: {message}[/red]") + return success, message async def get_liquidity_list( @@ -535,11 +544,12 @@ async def remove_liquidity( success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid) if not success: if json_output: - return json_console.print( - {"success": False, "err_msg": msg, "positions": positions} + json_console.print_json( + data={"success": False, "err_msg": msg, "positions": positions} ) else: return err_console.print(f"Error: {msg}") + return False, msg else: position_ids = [p.id for p in positions] else: @@ -568,16 +578,21 @@ async def remove_liquidity( ] ) if not json_output: - for (success, msg), posid in zip(results, position_ids): + for (success, msg, ext_receipt), posid in zip(results, position_ids): if success: + await print_extrinsic_id(ext_receipt) console.print(f"[green] Position {posid} has been removed.") else: err_console.print(f"[red] Error removing {posid}: {msg}") else: json_table = {} - for (success, msg), posid in zip(results, position_ids): - json_table[posid] = {"success": success, "err_msg": msg} - json_console.print(json.dumps(json_table)) + for (success, msg, ext_receipt), posid in zip(results, position_ids): + json_table[posid] = { + "success": success, + "err_msg": msg, + "extrinsic_identifier": await ext_receipt.get_extrinsic_identifier(), + } + json_console.print_json(data=json_table) async def modify_liquidity( @@ -586,7 +601,7 @@ async def modify_liquidity( hotkey_ss58: str, netuid: int, position_id: int, - liquidity_delta: Optional[float], + liquidity_delta: Balance, prompt: Optional[bool] = None, json_output: bool = False, ) -> bool: @@ -611,7 +626,7 @@ async def modify_liquidity( if not Confirm.ask("Would you like to continue?"): return False - success, msg = await modify_liquidity_extrinsic( + success, msg, ext_receipt = await modify_liquidity_extrinsic( subtensor=subtensor, wallet=wallet, hotkey_ss58=hotkey_ss58, @@ -620,9 +635,14 @@ async def modify_liquidity( liquidity_delta=liquidity_delta, ) if json_output: - json_console.print(json.dumps({"success": success, "err_msg": msg})) + ext_id = await ext_receipt.get_extrinsic_identifier() if success else None + json_console.print_json( + data={"success": success, "err_msg": msg, "extrinsic_identifier": ext_id} + ) else: if success: + await print_extrinsic_id(ext_receipt) console.print(f"[green] Position {position_id} has been modified.") else: err_console.print(f"[red] Error modifying {position_id}: {msg}") + return success diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 18a507c6d..9f3ffc0d0 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -4,6 +4,8 @@ from functools import partial from typing import TYPE_CHECKING, Optional + +from async_substrate_interface import AsyncExtrinsicReceipt from rich.table import Table from rich.prompt import Confirm, Prompt @@ -21,6 +23,7 @@ unlock_key, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) from bittensor_wallet import Wallet @@ -112,7 +115,7 @@ async def safe_stake_extrinsic( hotkey_ss58_: str, price_limit: Balance, status=None, - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}" @@ -153,15 +156,16 @@ async def safe_stake_extrinsic( else: err_msg = f"{failure_prelude} with error: {format_error_message(e)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None if not await response.is_success: err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None else: if json_output: # the rest of this checking is not necessary if using json_output - return True, "" + return True, "", response + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -199,11 +203,11 @@ async def safe_stake_extrinsic( f":arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" ) - return True, "" + return True, "", response async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: err_out = partial(print_error, status=status) current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address), @@ -231,16 +235,17 @@ async def stake_extrinsic( except SubstrateRequestException as e: err_msg = f"{failure_prelude} with error: {format_error_message(e)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None else: if not await response.is_success: err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}" err_out("\n" + err_msg) - return False, err_msg + return False, err_msg, None else: if json_output: # the rest of this is not necessary if using json_output - return True, "" + return True, "", response + await print_extrinsic_id(response) new_block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance( @@ -269,7 +274,7 @@ async def stake_extrinsic( f":arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" ) - return True, "" + return True, "", response netuids = ( netuids if netuids is not None else await subtensor.get_all_subnet_netuids() @@ -470,15 +475,23 @@ async def stake_extrinsic( } successes = defaultdict(dict) error_messages = defaultdict(dict) + extrinsic_ids = defaultdict(dict) with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."): # We can gather them all at once but balance reporting will be in race-condition. for (ni, staking_address), coroutine in stake_coroutines.items(): - success, er_msg = await coroutine + success, er_msg, ext_receipt = await coroutine successes[ni][staking_address] = success error_messages[ni][staking_address] = er_msg + extrinsic_ids[ni][ + staking_address + ] = await ext_receipt.get_extrinsic_identifier() if json_output: - json_console.print( - json.dumps({"staking_success": successes, "error_messages": error_messages}) + json_console.print_json( + data={ + "staking_success": successes, + "error_messages": error_messages, + "extrinsic_ids": extrinsic_ids, + } ) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index d01e8d147..d50ecc65a 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -22,6 +22,7 @@ unlock_key, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) @@ -59,7 +60,7 @@ async def set_children_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = False, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """ Sets children hotkeys with proportions assigned from the parent. @@ -74,7 +75,7 @@ async def set_children_extrinsic( `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. :param: prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: A tuple containing a success flag and an optional error message. + :return: A tuple containing a success flag, an optional error message, and the extrinsic identifier """ # Check if all children are being revoked all_revoked = len(children_with_proportions) == 0 @@ -87,7 +88,7 @@ async def set_children_extrinsic( if not Confirm.ask( f"Do you want to revoke all children hotkeys for hotkey {hotkey} on netuid {netuid}?" ): - return False, "Operation Cancelled" + return False, "Operation Cancelled", None else: if not Confirm.ask( "Do you want to set children hotkeys:\n[bold white]{}[/bold white]?".format( @@ -97,11 +98,11 @@ async def set_children_extrinsic( ) ) ): - return False, "Operation Cancelled" + return False, "Operation Cancelled", None # Decrypt coldkey. if not (unlock_status := unlock_key(wallet, print_out=False)).success: - return False, unlock_status.message + return False, unlock_status.message, "" with console.status( f":satellite: {operation} on [white]{subtensor.network}[/white] ..." @@ -120,7 +121,7 @@ async def set_children_extrinsic( "netuid": netuid, }, ) - success, error_message = await subtensor.sign_and_send_extrinsic( + success, error_message, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization ) @@ -128,17 +129,20 @@ async def set_children_extrinsic( return ( True, f"Not waiting for finalization or inclusion. {operation} initiated.", + None, ) if success: - if wait_for_inclusion: - console.print(":white_heavy_check_mark: [green]Included[/green]") + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) + modifier = "included" if wait_for_finalization: console.print(":white_heavy_check_mark: [green]Finalized[/green]") - return True, f"Successfully {operation.lower()} and Finalized." + modifier = "finalized" + return True, f"{operation} successfully {modifier}.", ext_id else: err_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") - return False, error_message + return False, error_message, None async def set_childkey_take_extrinsic( @@ -150,7 +154,7 @@ async def set_childkey_take_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = False, prompt: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """ Sets childkey take. @@ -165,7 +169,7 @@ async def set_childkey_take_extrinsic( `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. :param: prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: A tuple containing a success flag and an optional error message. + :return: A tuple containing a success flag, an optional error message, and an optional extrinsic identifier """ # Ask before moving on. @@ -173,11 +177,11 @@ async def set_childkey_take_extrinsic( if not Confirm.ask( f"Do you want to set childkey take to: [bold white]{take * 100}%[/bold white]?" ): - return False, "Operation Cancelled" + return False, "Operation Cancelled", None # Decrypt coldkey. if not (unlock_status := unlock_key(wallet, print_out=False)).success: - return False, unlock_status.message + return False, unlock_status.message, None with console.status( f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..." @@ -186,7 +190,7 @@ async def set_childkey_take_extrinsic( if 0 <= take <= 0.18: take_u16 = float_to_u16(take) else: - return False, "Invalid take value" + return False, "Invalid take value", None call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -197,7 +201,11 @@ async def set_childkey_take_extrinsic( "netuid": netuid, }, ) - success, error_message = await subtensor.sign_and_send_extrinsic( + ( + success, + error_message, + ext_receipt, + ) = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization ) @@ -205,30 +213,34 @@ async def set_childkey_take_extrinsic( return ( True, "Not waiting for finalization or inclusion. Set childkey take initiated.", + None, ) if success: - if wait_for_inclusion: - console.print(":white_heavy_check_mark: [green]Included[/green]") + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) + modifier = "included" if wait_for_finalization: + modifier = "finalized" console.print(":white_heavy_check_mark: [green]Finalized[/green]") # bittensor.logging.success( # prefix="Setting childkey take", # suffix="Finalized: " + str(success), # ) - return True, "Successfully set childkey take and Finalized." + return True, f"Successfully {modifier} childkey take", ext_id else: console.print(f":cross_mark: [red]Failed[/red]: {error_message}") # bittensor.logging.warning( # prefix="Setting childkey take", # suffix="Failed: " + str(error_message), # ) - return False, error_message + return False, error_message, None except SubstrateRequestException as e: return ( False, f"Exception occurred while setting childkey take: {format_error_message(e)}", + None, ) @@ -519,7 +531,7 @@ async def set_children( children_with_proportions = list(zip(proportions, children)) successes = {} if netuid is not None: - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -534,6 +546,7 @@ async def set_children( "error": message, "completion_block": None, "set_block": None, + "extrinsic_identifier": ext_id, } # Result if success: @@ -561,7 +574,7 @@ async def set_children( if netuid_ == 0: # dont include root network continue console.print(f"Setting children on netuid {netuid_}.") - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid_, @@ -579,6 +592,7 @@ async def set_children( "error": message, "completion_block": completion_block, "set_block": current_block, + "extrinsic_identifier": ext_id, } console.print( f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around " @@ -605,7 +619,7 @@ async def revoke_children( """ dict_output = {} if netuid is not None: - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -620,6 +634,7 @@ async def revoke_children( "error": message, "set_block": None, "completion_block": None, + "extrinsic_identifier": ext_id, } # Result @@ -644,10 +659,10 @@ async def revoke_children( if netuid_ == 0: # dont include root network continue console.print(f"Revoking children from netuid {netuid_}.") - success, message = await set_children_extrinsic( + success, message, ext_id = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, - netuid=netuid, + netuid=netuid, # TODO should this be able to allow netuid = None ? hotkey=get_hotkey_pub_ss58(wallet), children_with_proportions=[], prompt=prompt, @@ -659,6 +674,7 @@ async def revoke_children( "error": message, "set_block": None, "completion_block": None, + "extrinsic_identifier": ext_id, } if success: current_block, completion_block = await get_childkey_completion_block( @@ -688,12 +704,12 @@ async def childkey_take( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, -) -> list[tuple[Optional[int], bool]]: +) -> list[tuple[Optional[int], bool, Optional[str]]]: """ Get or Set childkey take. Returns: - List of (netuid, success) for specified netuid (or all) and their success in setting take + List of (netuid, success, extrinsic identifier) for specified netuid (or all) and their success in setting take """ def validate_take_value(take_value: float) -> bool: @@ -741,9 +757,11 @@ async def chk_all_subnets(ss58): console.print(table) - async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: + async def set_chk_take_subnet( + subnet: int, chk_take: float + ) -> tuple[bool, Optional[str]]: """Set the childkey take for a single subnet""" - success, message = await set_childkey_take_extrinsic( + success, message, ext_id = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, netuid=subnet, @@ -759,12 +777,12 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: console.print( f"The childkey take for {get_hotkey_pub_ss58(wallet)} is now set to {take * 100:.2f}%." ) - return True + return True, ext_id else: console.print( f":cross_mark:[red] Unable to set childkey take.[/red] {message}" ) - return False + return False, ext_id # Print childkey take for other user and return (dont offer to change take rate) wallet_hk = get_hotkey_pub_ss58(wallet) @@ -778,7 +796,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return [(netuid, False)] + return [(netuid, False, None)] else: # show child hotkey take on all subnets await chk_all_subnets(hotkey) @@ -786,12 +804,12 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return [(netuid, False)] + return [(netuid, False, None)] # Validate child SS58 addresses if not take: if not Confirm.ask("Would you like to change the child take?"): - return [(netuid, False)] + return [(netuid, False, None)] new_take_value = -1.0 while not validate_take_value(new_take_value): new_take_value = FloatPrompt.ask( @@ -800,22 +818,21 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: take = new_take_value else: if not validate_take_value(take): - return [(netuid, False)] + return [(netuid, False, None)] if netuid: - return [(netuid, await set_chk_take_subnet(subnet=netuid, chk_take=take))] + success, ext_id = await set_chk_take_subnet(subnet=netuid, chk_take=take) + return [(netuid, success, ext_id)] else: new_take_netuids = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) if new_take_netuids: - return [ - ( - new_take_netuids, - await set_chk_take_subnet(subnet=new_take_netuids, chk_take=take), - ) - ] + success, ext_id = await set_chk_take_subnet( + subnet=new_take_netuids, chk_take=take + ) + return [(new_take_netuids, success, ext_id)] else: netuids = await subtensor.get_all_subnet_netuids() @@ -823,8 +840,8 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: for netuid_ in netuids: if netuid_ == 0: continue - console.print(f"Sending to netuid {netuid_} take of {take * 100:.2f}%") - result = await set_childkey_take_extrinsic( + console.print(f"Setting take of {take * 100:.2f}% on netuid {netuid_}.") + result, _, ext_id = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid_, @@ -834,7 +851,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: wait_for_inclusion=True, wait_for_finalization=False, ) - output_list.append((netuid_, result)) + output_list.append((netuid_, result, ext_id)) console.print( f":white_heavy_check_mark: [green]Sent childkey take of {take * 100:.2f}% to all subnets.[/green]" ) diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index b4360ffdf..1443d611e 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -17,6 +17,7 @@ get_subnet_name, unlock_key, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -436,12 +437,12 @@ async def move_stake( era: int, interactive_selection: bool = False, prompt: bool = True, -) -> bool: +) -> tuple[bool, str]: if interactive_selection: try: selection = await stake_move_transfer_selection(subtensor, wallet) except ValueError: - return False + return False, "" origin_hotkey = selection["origin_hotkey"] origin_netuid = selection["origin_netuid"] amount = selection["amount"] @@ -472,7 +473,7 @@ async def move_stake( f"in Netuid: " f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" ) - return False + return False, "" console.print( f"\nOrigin Netuid: " @@ -507,7 +508,7 @@ async def move_stake( f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" f"{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) - return False + return False, "" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -545,13 +546,13 @@ async def move_stake( extrinsic_fee=extrinsic_fee, ) except ValueError: - return False + return False, "" if not Confirm.ask("Would you like to continue?"): - return False + return False, "" # Perform moving operation. if not unlock_key(wallet).success: - return False + return False, "" with console.status( f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: " f"[blue]{origin_netuid}[/blue] \nto " @@ -563,17 +564,19 @@ async def move_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True + return True, ext_id else: if not await response.is_success: err_console.print( f"\n:cross_mark: [red]Failed[/red] with error:" f" {format_error_message(await response.error_message)}" ) - return False + return False, "" else: console.print( ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" @@ -605,7 +608,7 @@ async def move_stake( f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}" ) - return True + return True, ext_id async def transfer_stake( @@ -620,7 +623,7 @@ async def transfer_stake( interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, -) -> bool: +) -> tuple[bool, str]: """Transfers stake from one network to another. Args: @@ -653,11 +656,11 @@ async def transfer_stake( ) if not dest_exists: err_console.print(f"[red]Subnet {dest_netuid} does not exist[/red]") - return False + return False, "" if not origin_exists: err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]") - return False + return False, "" # Get current stake balances with console.status(f"Retrieving stake data from {subtensor.network}..."): @@ -676,7 +679,7 @@ async def transfer_stake( err_console.print( f"[red]No stake found for hotkey: {origin_hotkey} on netuid: {origin_netuid}[/red]" ) - return False + return False, "" if amount: amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid) @@ -694,7 +697,7 @@ async def transfer_stake( f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < " f"Transfer amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_transfer}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) - return False + return False, "" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -732,14 +735,14 @@ async def transfer_stake( extrinsic_fee=extrinsic_fee, ) except ValueError: - return False + return False, "" if not Confirm.ask("Would you like to continue?"): - return False + return False, "" # Perform transfer operation if not unlock_key(wallet).success: - return False + return False, "" with console.status("\n:satellite: Transferring stake ..."): extrinsic = await subtensor.substrate.create_signed_extrinsic( @@ -749,17 +752,19 @@ async def transfer_stake( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) + ext_id = await response.get_extrinsic_identifier() + await print_extrinsic_id(extrinsic) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True + return True, ext_id if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " f"{format_error_message(await response.error_message)}" ) - return False + return False, "" # Get and display new stake balances new_stake, new_dest_stake = await asyncio.gather( @@ -783,7 +788,7 @@ async def transfer_stake( f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" ) - return True + return True, ext_id async def swap_stake( @@ -798,7 +803,7 @@ async def swap_stake( prompt: bool = True, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, -) -> bool: +) -> tuple[bool, str]: """Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership. Args: @@ -833,11 +838,11 @@ async def swap_stake( ) if not dest_exists: err_console.print(f"[red]Subnet {destination_netuid} does not exist[/red]") - return False + return False, "" if not origin_exists: err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]") - return False + return False, "" # Get current stake balances with console.status(f"Retrieving stake data from {subtensor.network}..."): @@ -864,7 +869,7 @@ async def swap_stake( f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < " f"Swap amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_swap}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) - return False + return False, "" call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -901,14 +906,14 @@ async def swap_stake( extrinsic_fee=extrinsic_fee, ) except ValueError: - return False + return False, "" if not Confirm.ask("Would you like to continue?"): - return False + return False, "" # Perform swap operation if not unlock_key(wallet).success: - return False + return False, "" with console.status( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " @@ -923,17 +928,19 @@ async def swap_stake( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + ext_id = await response.get_extrinsic_identifier() + await print_extrinsic_id(response) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True + return True, ext_id if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " f"{format_error_message(await response.error_message)}" ) - return False + return False, "" # Get and display new stake balances new_stake, new_dest_stake = await asyncio.gather( @@ -957,4 +964,4 @@ async def swap_stake( f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}" ) - return True + return True, ext_id diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index ecec77fa5..b4b6bbeb1 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich.prompt import Confirm, Prompt from rich.table import Table @@ -23,6 +24,7 @@ unlock_key, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -134,7 +136,8 @@ async def unstake( skip_remaining_subnets = False if len(netuids) > 1 and not amount: console.print( - "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n" + "[dark_sea_green3]Tip: Enter 'q' any time to stop going over " + "remaining subnets and process current unstakes.\n" ) # Iterate over hotkeys and netuids to collect unstake operations @@ -335,7 +338,8 @@ async def unstake( func = _unstake_extrinsic specific_args = {"current_stake": op["current_stake_balance"]} - suc = await func(**common_args, **specific_args) + suc, ext_receipt = await func(**common_args, **specific_args) + ext_id = await ext_receipt.get_extrinsic_identifier() if suc else None successes.append( { @@ -343,6 +347,7 @@ async def unstake( "hotkey_ss58": op["hotkey_ss58"], "unstake_amount": op["amount_to_unstake"].tao, "success": suc, + "extrinsic_identifier": ext_id, } ) @@ -350,7 +355,7 @@ async def unstake( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) if json_output: - json_console.print(json.dumps(successes)) + json_console.print_json(data=successes) return True @@ -533,7 +538,7 @@ async def unstake_all( successes = {} with console.status("Unstaking all stakes...") as status: for hotkey_ss58 in hotkey_ss58s: - successes[hotkey_ss58] = await _unstake_all_extrinsic( + success, ext_receipt = await _unstake_all_extrinsic( wallet=wallet, subtensor=subtensor, hotkey_ss58=hotkey_ss58, @@ -542,6 +547,11 @@ async def unstake_all( status=status, era=era, ) + ext_id = await ext_receipt.get_extrinsic_identifier() if successes else None + successes[hotkey_ss58] = { + "success": success, + "extrinsic_identifier": ext_id, + } if json_output: return json_console.print(json.dumps({"success": successes})) @@ -556,7 +566,7 @@ async def _unstake_extrinsic( hotkey_ss58: str, status=None, era: int = 3, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a standard unstake extrinsic. Args: @@ -604,8 +614,9 @@ async def _unstake_extrinsic( f"{failure_prelude} with error: " f"{format_error_message(await response.error_message)}" ) - return False + return False, None # Fetch latest balance and stake + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -625,11 +636,11 @@ async def _unstake_extrinsic( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) - return True + return True, response except Exception as e: err_out(f"{failure_prelude} with error: {str(e)}") - return False + return False, None async def _safe_unstake_extrinsic( @@ -642,7 +653,7 @@ async def _safe_unstake_extrinsic( allow_partial_stake: bool, status=None, era: int = 3, -) -> bool: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute a safe unstake extrinsic with price limit. Args: @@ -708,14 +719,14 @@ async def _safe_unstake_extrinsic( ) else: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return False + return False, None if not await response.is_success: err_out( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) - return False - + return False, None + await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), @@ -745,7 +756,7 @@ async def _safe_unstake_extrinsic( f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}" ) - return True + return True, response async def _unstake_all_extrinsic( @@ -756,7 +767,7 @@ async def _unstake_all_extrinsic( unstake_all_alpha: bool, status=None, era: int = 3, -) -> None: +) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]: """Execute an unstake all extrinsic. Args: @@ -774,7 +785,7 @@ async def _unstake_all_extrinsic( if status: status.update( - f"\n:satellite: {'Unstaking all Alpha stakes' if unstake_all_alpha else 'Unstaking all stakes'} from {hotkey_name} ..." + f"\n:satellite: Unstaking all {'Alpha ' if unstake_all_alpha else ''}stakes from {hotkey_name} ..." ) block_hash = await subtensor.substrate.get_chain_head() @@ -817,7 +828,9 @@ async def _unstake_all_extrinsic( f"{failure_prelude} with error: " f"{format_error_message(await response.error_message)}" ) - return + return False, None + else: + await print_extrinsic_id(response) # Fetch latest balance and stake block_hash = await subtensor.substrate.get_chain_head() @@ -855,9 +868,11 @@ async def _unstake_all_extrinsic( f"[blue]{previous_root_stake}[/blue] :arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_root_stake}" ) + return True, response except Exception as e: err_out(f"{failure_prelude} with error: {str(e)}") + return False, None async def _get_extrinsic_fee( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d8571f3f6..ef4ae59de 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -37,6 +37,7 @@ blocks_to_duration, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -54,7 +55,7 @@ async def register_subnetwork_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, -) -> tuple[bool, Optional[int]]: +) -> tuple[bool, Optional[int], Optional[str]]: """Registers a new subnetwork. wallet (bittensor.wallet): @@ -66,9 +67,11 @@ async def register_subnetwork_extrinsic( prompt (bool): If true, the call waits for confirmation from the user before proceeding. Returns: - success (bool): - Flag is ``true`` if extrinsic was finalized or included in the block. - If we did not wait for finalization / inclusion, the response is ``true``. + tuple including: + success: Flag is `True` if extrinsic was finalized or included in the block. + If we did not wait for finalization/inclusion, the response is `True`. + error_message: Optional error message. + extrinsic_identifier: Optional extrinsic identifier, if the extrinsic was included. """ async def _find_event_attributes_in_extrinsic_receipt( @@ -103,7 +106,7 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}] " f"to register a subnet." ) - return False, None + return False, None, None if prompt: console.print( @@ -112,7 +115,7 @@ async def _find_event_attributes_in_extrinsic_receipt( if not Confirm.ask( f"Do you want to burn [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost} to register a subnet?" ): - return False, None + return False, None, None call_params = { "hotkey": get_hotkey_pub_ss58(wallet), @@ -157,10 +160,10 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n" f"Value '{value.decode()}' is {len(value)} bytes." ) - return False, None + return False, None, None if not unlock_key(wallet).success: - return False, None + return False, None, None with console.status(":satellite: Registering subnet...", spinner="earth"): substrate = subtensor.substrate @@ -181,24 +184,26 @@ async def _find_event_attributes_in_extrinsic_receipt( # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True, None + return True, None, None if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" ) await asyncio.sleep(0.5) - return False, None + return False, None, None # Successful registration, final check for membership else: attributes = await _find_event_attributes_in_extrinsic_receipt( response, "NetworkAdded" ) + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() console.print( f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) - return True, int(attributes[0]) + return True, int(attributes[0]), ext_id # commands @@ -1486,13 +1491,17 @@ async def create( """Register a subnetwork""" # Call register command. - success, netuid = await register_subnetwork_extrinsic( + success, netuid, ext_id = await register_subnetwork_extrinsic( subtensor, wallet, subnet_identity, prompt=prompt ) if json_output: # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present # (2025/04/03), we always use the default `wait_for_finalization=True`, so it will always have a netuid. - json_console.print(json.dumps({"success": success, "netuid": netuid})) + json_console.print( + json.dumps( + {"success": success, "netuid": netuid, "extrinsic_identifier": ext_id} + ) + ) return success if success and prompt: # Prompt for user to set identity. @@ -1584,9 +1593,11 @@ async def register( err_console.print(f"[red]Subnet {netuid} does not exist[/red]") if json_output: json_console.print( - json.dumps( - {"success": False, "error": f"Subnet {netuid} does not exist"} - ) + data={ + "success": False, + "msg": f"Subnet {netuid} does not exist", + "extrinsic_identifier": None, + } ) return @@ -1604,9 +1615,12 @@ async def register( # Check balance is sufficient if balance < current_recycle: - err_console.print( - f"[red]Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO[/red]" - ) + err_msg = f"Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO" + err_console.print(f"[red]{err_msg}[/red]") + if json_output: + json_console.print_json( + data={"success": False, "msg": err_msg, "extrinsic_identifier": None} + ) return if prompt and not json_output: @@ -1671,9 +1685,9 @@ async def register( return if netuid == 0: - success, msg = await root_register_extrinsic(subtensor, wallet=wallet) + success, msg, ext_id = await root_register_extrinsic(subtensor, wallet=wallet) else: - success, msg = await burned_register_extrinsic( + success, msg, ext_id = await burned_register_extrinsic( subtensor, wallet=wallet, netuid=netuid, @@ -1681,7 +1695,9 @@ async def register( era=era, ) if json_output: - json_console.print(json.dumps({"success": success, "msg": msg})) + json_console.print( + json.dumps({"success": success, "msg": msg, "extrinsic_identifier": ext_id}) + ) else: if not success: err_console.print(f"Failure: {msg}") @@ -2207,12 +2223,12 @@ async def set_identity( netuid: int, subnet_identity: dict, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[str]]: """Set identity information for a subnet""" if not await subtensor.subnet_exists(netuid): err_console.print(f"Subnet {netuid} does not exist") - return False + return False, None identity_data = { "netuid": netuid, @@ -2227,13 +2243,13 @@ async def set_identity( } if not unlock_key(wallet).success: - return False + return False, None if prompt: if not Confirm.ask( "Are you sure you want to set subnet's identity? This is subject to a fee." ): - return False + return False, None call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -2245,12 +2261,15 @@ async def set_identity( " :satellite: [dark_sea_green3]Setting subnet identity on-chain...", spinner="earth", ): - success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if not success: err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") - return False - + return False, None + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) console.print( ":white_heavy_check_mark: [dark_sea_green3]Successfully set subnet identity\n" ) @@ -2275,7 +2294,7 @@ async def set_identity( table.add_row(key, str(value) if value else "~") console.print(table) - return True + return True, ext_id async def get_identity( @@ -2433,6 +2452,7 @@ async def start_subnet( ) if await response.is_success: + await print_extrinsic_id(response) console.print( f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]" ) @@ -2467,7 +2487,9 @@ async def set_symbol( if not await subtensor.subnet_exists(netuid): err = f"Subnet {netuid} does not exist." if json_output: - json_console.print_json(data={"success": False, "message": err}) + json_console.print_json( + data={"success": False, "message": err, "extrinsic_identifier": None} + ) else: err_console.print(err) return False @@ -2503,16 +2525,26 @@ async def set_symbol( wait_for_inclusion=True, ) if await response.is_success: + ext_id = await response.get_extrinsic_identifier() + await print_extrinsic_id(response) message = f"Successfully updated SN{netuid}'s symbol to {symbol}." if json_output: - json_console.print_json(data={"success": True, "message": message}) + json_console.print_json( + data={ + "success": True, + "message": message, + "extrinsic_identifier": ext_id, + } + ) else: console.print(f":white_heavy_check_mark:[dark_sea_green3] {message}\n") return True else: err = format_error_message(await response.error_message) if json_output: - json_console.print_json(data={"success": False, "message": err}) + json_console.print_json( + data={"success": False, "message": err, "extrinsic_identifier": None} + ) else: err_console.print(f":cross_mark: [red]Failed[/red]: {err}") return False diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index bb4bb43de..fe3601034 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -27,6 +27,7 @@ string_to_u16, string_to_u64, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -178,7 +179,7 @@ async def set_hyperparameter_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = True, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Sets a hyperparameter for a specific subnetwork. :param subtensor: initialized SubtensorInterface object @@ -191,8 +192,11 @@ async def set_hyperparameter_extrinsic( :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. - :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for + :return: tuple including: + success: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, the response is `True`. + message: error message if the extrinsic failed + extrinsic_identifier: optional extrinsic identifier if the extrinsic was included """ print_verbose("Confirming subnet owner") subnet_owner = await subtensor.query( @@ -205,10 +209,10 @@ async def set_hyperparameter_extrinsic( ":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]" ) err_console.print(err_msg) - return False, err_msg + return False, err_msg, None if not (ulw := unlock_key(wallet)).success: - return False, ulw.message + return False, ulw.message, None arbitrary_extrinsic = False @@ -227,7 +231,7 @@ async def set_hyperparameter_extrinsic( if not Confirm.ask( "This hyperparam is only settable by root sudo users. If you are not, this will fail. Please confirm" ): - return False, "This hyperparam is only settable by root sudo users" + return False, "This hyperparam is only settable by root sudo users", None substrate = subtensor.substrate msg_value = value if not arbitrary_extrinsic else call_params @@ -259,7 +263,7 @@ async def set_hyperparameter_extrinsic( "Not enough values provided in the list for all parameters" ) err_console.print(err_msg) - return False, err_msg + return False, err_msg, None call_params.update( {name: val for name, val in zip(non_netuid_fields, value)} @@ -287,25 +291,28 @@ async def set_hyperparameter_extrinsic( ) else: call = call_ - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization ) if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False, err_msg - elif arbitrary_extrinsic: - console.print( - f":white_heavy_check_mark: " - f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]" - ) - return True, "" - # Successful registration, final check for membership + return False, err_msg, None else: - console.print( - f":white_heavy_check_mark: " - f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" - ) - return True, "" + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) + if arbitrary_extrinsic: + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]" + ) + return True, "", ext_id + # Successful registration, final check for membership + else: + console.print( + f":white_heavy_check_mark: " + f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]" + ) + return True, "", ext_id async def _get_senate_members( @@ -506,7 +513,7 @@ async def vote_senate_extrinsic( "approve": vote, }, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion, wait_for_finalization ) if not success: @@ -515,6 +522,7 @@ async def vote_senate_extrinsic( return False # Successful vote, final check for data else: + await print_extrinsic_id(ext_receipt) if vote_data := await subtensor.get_vote_data(proposal_hash): hotkey_ss58 = get_hotkey_pub_ss58(wallet) if ( @@ -538,7 +546,7 @@ async def set_take_extrinsic( wallet: Wallet, delegate_ss58: str, take: float = 0.0, -) -> bool: +) -> tuple[bool, Optional[str]]: """ Set delegate hotkey take @@ -563,7 +571,7 @@ async def set_take_extrinsic( if take_u16 == current_take_u16: console.print("Nothing to do, take hasn't changed") - return True + return True, None if current_take_u16 < take_u16: console.print( @@ -581,7 +589,9 @@ async def set_take_extrinsic( "take": take_u16, }, ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) else: console.print( @@ -599,15 +609,20 @@ async def set_take_extrinsic( "take": take_u16, }, ) - success, err = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if not success: err_console.print(err) + ext_id = None else: console.print( - ":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]" + ":white_heavy_check_mark: [dark_sea_green_3]Success[/dark_sea_green_3]" ) - return success + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) + return success, ext_id # commands @@ -621,7 +636,7 @@ async def sudo_set_hyperparameter( param_value: Optional[str], prompt: bool, json_output: bool, -) -> tuple[bool, str]: +) -> tuple[bool, str, Optional[str]]: """Set subnet hyperparameters.""" is_allowed_value, value = allowed_value(param_name, param_value) if not is_allowed_value: @@ -630,17 +645,17 @@ async def sudo_set_hyperparameter( f"Value is {param_value} but must be {value}" ) err_console.print(err_msg) - return False, err_msg - success, err_msg = await set_hyperparameter_extrinsic( + return False, err_msg, None + success, err_msg, ext_id = await set_hyperparameter_extrinsic( subtensor, wallet, netuid, param_name, value, prompt=prompt ) if json_output: - return success, err_msg + return success, err_msg, ext_id if success: console.print("\n") print_verbose("Fetching hyperparameters") await get_hyperparameters(subtensor, netuid=netuid) - return success, err_msg + return success, err_msg, ext_id async def get_hyperparameters( @@ -907,13 +922,13 @@ async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) async def set_take( wallet: Wallet, subtensor: "SubtensorInterface", take: float -) -> bool: +) -> tuple[bool, Optional[str]]: """Set delegate take.""" - async def _do_set_take() -> bool: + async def _do_set_take() -> tuple[bool, Optional[str]]: if take > 0.18 or take < 0: err_console.print("ERROR: Take value should not exceed 18% or be below 0%") - return False + return False, None block_hash = await subtensor.substrate.get_chain_head() hotkey_ss58 = get_hotkey_pub_ss58(wallet) @@ -926,35 +941,34 @@ async def _do_set_take() -> bool: f" any subnet. Please register using [{COLOR_PALETTE.G.SUBHEAD}]`btcli subnets register`" f"[{COLOR_PALETTE.G.SUBHEAD}] and try again." ) - return False + return False, None - result: bool = await set_take_extrinsic( + result: tuple[bool, Optional[str]] = await set_take_extrinsic( subtensor=subtensor, wallet=wallet, delegate_ss58=hotkey_ss58, take=take, ) + success, ext_id = result - if not result: + if not success: err_console.print("Could not set the take") - return False + return False, None else: new_take = await get_current_take(subtensor, wallet) console.print( f"New take is [{COLOR_PALETTE.P.RATE}]{new_take * 100.0:.2f}%" ) - return True + return True, ext_id console.print( f"Setting take on [{COLOR_PALETTE.G.LINKS}]network: {subtensor.network}" ) if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success: - return False - - result_ = await _do_set_take() + return False, None - return result_ + return await _do_set_take() async def trim( @@ -993,20 +1007,30 @@ async def trim( call_function="sudo_trim_to_max_allowed_uids", call_params={"netuid": netuid, "max_n": max_n}, ) - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": period} ) if not success: if json_output: - json_console.print_json(data={"success": False, "message": err_msg}) + json_console.print_json( + data={ + "success": False, + "message": err_msg, + "extrinsic_identifier": None, + } + ) else: err_console.print(f":cross_mark: [red]{err_msg}[/red]") return False else: + ext_id = await ext_receipt.get_extrinsic_identifier() msg = f"Successfully trimmed UIDs on SN{netuid} to {max_n}" if json_output: - json_console.print_json(data={"success": True, "message": msg}) + json_console.print_json( + data={"success": True, "message": msg, "extrinsic_identifier": ext_id} + ) else: + await print_extrinsic_id(ext_receipt) console.print( f":white_heavy_check_mark: [dark_sea_green3]{msg}[/dark_sea_green3]" ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3b13a7fca..42fe8a0a6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -49,6 +49,7 @@ blocks_to_duration, decode_account_id, get_hotkey_pub_ss58, + print_extrinsic_id, ) @@ -98,7 +99,7 @@ async def associate_hotkey( ) with console.status(":satellite: Associating hotkey on-chain..."): - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion=True, @@ -116,6 +117,7 @@ async def associate_hotkey( f"wallet [blue]{wallet.name}[/blue], " f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]" ) + await print_extrinsic_id(ext_receipt) return True @@ -1481,7 +1483,7 @@ async def transfer( json_output: bool, ): """Transfer token of amount to destination.""" - result = await transfer_extrinsic( + result, ext_receipt = await transfer_extrinsic( subtensor=subtensor, wallet=wallet, destination=destination, @@ -1491,8 +1493,13 @@ async def transfer( era=era, prompt=prompt, ) + ext_id = (await ext_receipt.get_extrinsic_identifier()) if result else None if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id}) + ) + else: + await print_extrinsic_id(ext_receipt) return result @@ -1682,15 +1689,23 @@ async def swap_hotkey( json_output: bool, ): """Swap your hotkey for all registered axons on the network.""" - result = await swap_hotkey_extrinsic( + result, ext_receipt = await swap_hotkey_extrinsic( subtensor, original_wallet, new_wallet, netuid=netuid, prompt=prompt, ) + if result: + ext_id = await ext_receipt.get_extrinsic_identifier() + else: + ext_id = None if json_output: - json_console.print(json.dumps({"success": result})) + json_console.print( + json.dumps({"success": result, "extrinsic_identifier": ext_id}) + ) + else: + await print_extrinsic_id(ext_receipt) return result @@ -1731,7 +1746,7 @@ async def set_id( github_repo: str, prompt: bool, json_output: bool = False, -): +) -> bool: """Create a new or update existing identity on-chain.""" output_dict = {"success": False, "identity": None, "error": ""} identity_data = { @@ -1756,16 +1771,20 @@ async def set_id( with console.status( " :satellite: [dark_sea_green3]Updating identity on-chain...", spinner="earth" ): - success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, wallet + ) if not success: err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}") output_dict["error"] = err_msg if json_output: json_console.print(json.dumps(output_dict)) - return + return False else: console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") + ext_id = await ext_receipt.get_extrinsic_identifier() + await print_extrinsic_id(ext_receipt) output_dict["success"] = True identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) @@ -1774,9 +1793,12 @@ async def set_id( for key, value in identity.items(): table.add_row(key, str(value) if value else "~") output_dict["identity"] = identity - console.print(table) + output_dict["extrinsic_identifier"] = ext_id if json_output: json_console.print(json.dumps(output_dict)) + else: + console.print(table) + return True async def get_id( @@ -2016,9 +2038,9 @@ async def schedule_coldkey_swap( }, ), ) - + swap_info = None with console.status(":satellite: Scheduling coldkey swap on-chain..."): - success, err_msg = await subtensor.sign_and_send_extrinsic( + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, wait_for_inclusion=True, @@ -2033,13 +2055,29 @@ async def schedule_coldkey_swap( console.print( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) + await print_extrinsic_id(ext_receipt) + for event in await ext_receipt.triggered_events: + if ( + event.get("event", {}).get("module_id") == "SubtensorModule" + and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled" + ): + attributes = event["event"].get("attributes", {}) + old_coldkey = decode_account_id(attributes["old_coldkey"][0]) - swap_info = await find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=block_pre_call, - end_block=block_post_call, - wallet_ss58=wallet.coldkeypub.ss58_address, - ) + if old_coldkey == wallet.coldkeypub.ss58_address: + swap_info = { + "block_num": block_pre_call, + "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), + "execution_block": attributes["execution_block"], + } + + if not swap_info: + swap_info = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, + ) if not swap_info: console.print( diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 63e3b72f3..b61bbd81f 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -2,7 +2,7 @@ import json import os from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet import numpy as np @@ -16,6 +16,7 @@ format_error_message, json_console, get_hotkey_pub_ss58, + print_extrinsic_id, ) from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, @@ -54,7 +55,7 @@ def __init__( self.wait_for_inclusion = wait_for_inclusion self.wait_for_finalization = wait_for_finalization - async def set_weights_extrinsic(self) -> tuple[bool, str]: + async def set_weights_extrinsic(self) -> tuple[bool, str, Optional[str]]: """ Sets the inter-neuronal weights for the specified neuron. This process involves specifying the influence or trust a neuron places on other neurons in the network, which is a fundamental aspect @@ -80,7 +81,7 @@ async def set_weights_extrinsic(self) -> tuple[bool, str]: f"Do you want to set weights:\n[bold white]" f" weights: {formatted_weight_vals}\n uids: {weight_uids}[/bold white ]?" ): - return False, "Prompt refused." + return False, "Prompt refused.", None # Check if the commit-reveal mechanism is active for the given netuid. if bool( @@ -104,7 +105,7 @@ async def commit_weights( self, uids: list[int], weights: list[int], - ) -> tuple[bool, str]: + ) -> tuple[bool, Optional[str], Optional[str]]: """ Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet. This action serves as a commitment or snapshot of the neuron's current weight distribution. @@ -121,12 +122,6 @@ async def commit_weights( enhancing transparency and accountability within the Bittensor network. """ - # _logger.info( - # "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format( - # netuid, uids, weights, version_key - # ) - # ) - # Generate the hash of the weights commit_hash = generate_weight_hash( address=get_hotkey_pub_ss58(self.wallet), @@ -139,18 +134,21 @@ async def commit_weights( # _logger.info("Commit Hash: {}".format(commit_hash)) try: - success, message = await self.do_commit_weights(commit_hash=commit_hash) + success, message, ext_id = await self.do_commit_weights( + commit_hash=commit_hash + ) except SubstrateRequestException as e: err_console.print(f"Error committing weights: {format_error_message(e)}") # bittensor.logging.error(f"Error committing weights: {e}") success = False message = "No attempt made. Perhaps it is too soon to commit weights!" + ext_id = None - return success, message + return success, message, ext_id async def _commit_reveal( self, weight_uids: list[int], weight_vals: list[int] - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[str]]: interval = int( await self.subtensor.get_hyperparameter( param_name="get_commit_reveal_period", @@ -165,7 +163,7 @@ async def _commit_reveal( self.salt = list(os.urandom(salt_length)) # Attempt to commit the weights to the blockchain. - commit_success, commit_msg = await self.commit_weights( + commit_success, commit_msg, ext_id = await self.commit_weights( uids=weight_uids, weights=weight_vals, ) @@ -209,36 +207,42 @@ async def _commit_reveal( console.print(f":cross_mark: [red]Failed[/red]: error:{commit_msg}") # bittensor.logging.error(msg=commit_msg, prefix="Set weights with hash commit", # suffix=f"Failed: {commit_msg}") - return False, f"Failed to commit weights hash. {commit_msg}" + return False, f"Failed to commit weights hash. {commit_msg}", None - async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str]: + async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str, Optional[str]]: # Attempt to reveal the weights using the salt. - success, msg = await self.reveal_weights_extrinsic(weight_uids, weight_vals) + success, msg, ext_id = await self.reveal_weights_extrinsic( + weight_uids, weight_vals + ) if success: if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, "Not waiting for finalization or inclusion.", ext_id console.print( ":white_heavy_check_mark: [green]Weights hash revealed on chain[/green]" ) # bittensor.logging.success(prefix="Weights hash revealed", suffix=str(msg)) - return True, "Successfully revealed previously commited weights hash." + return ( + True, + "Successfully revealed previously commited weights hash.", + ext_id, + ) else: # bittensor.logging.error( # msg=msg, # prefix=f"Failed to reveal previously commited weights hash for salt: {salt}", # suffix="Failed: ", # ) - return False, "Failed to reveal weights." + return False, "Failed to reveal weights.", None async def _set_weights_without_commit_reveal( self, weight_uids, weight_vals, - ) -> tuple[bool, str]: - async def _do_set_weights(): + ) -> tuple[bool, str, Optional[str]]: + async def _do_set_weights() -> tuple[bool, str, Optional[str]]: call = await self.subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="set_weights", @@ -262,37 +266,39 @@ async def _do_set_weights(): wait_for_finalization=self.wait_for_finalization, ) except SubstrateRequestException as e: - return False, format_error_message(e) + return False, format_error_message(e), None # 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." + return True, "Not waiting for finalization or inclusion.", None if await response.is_success: - return True, "Successfully set weights." + ext_id_ = await response.get_extrinsic_identifier() + await print_extrinsic_id(response) + return True, "Successfully set weights.", ext_id_ else: - return False, format_error_message(await response.error_message) + return False, format_error_message(await response.error_message), None with console.status( f":satellite: Setting weights on [white]{self.subtensor.network}[/white] ..." ): - success, error_message = await _do_set_weights() + success, error_message, ext_id = await _do_set_weights() if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, "Not waiting for finalization or inclusion." + return True, "Not waiting for finalization or inclusion.", None 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." + return True, "Successfully set weights and finalized.", ext_id else: # bittensor.logging.error(msg=error_message, prefix="Set weights", suffix="Failed: ") - return False, error_message + return False, error_message, None async def reveal_weights_extrinsic( self, weight_uids, weight_vals - ) -> tuple[bool, str]: + ) -> tuple[bool, str, Optional[str]]: if self.prompt and not Confirm.ask("Would you like to reveal weights?"): - return False, "User cancelled the operation." + return False, "User cancelled the operation.", None call = await self.subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -316,28 +322,36 @@ async def reveal_weights_extrinsic( wait_for_finalization=self.wait_for_finalization, ) except SubstrateRequestException as e: - return False, format_error_message(e) + return False, format_error_message(e), None if not self.wait_for_finalization and not self.wait_for_inclusion: - success, error_message = True, "" + success, error_message, ext_id = True, "", None else: if await response.is_success: - success, error_message = True, "" + success, error_message, ext_id = ( + True, + "", + await response.get_extrinsic_identifier(), + ) + await print_extrinsic_id(response) else: - success, error_message = ( + success, error_message, ext_id = ( False, format_error_message(await response.error_message), + None, ) if success: # bittensor.logging.info("Successfully revealed weights.") - return True, "Successfully revealed weights." + return True, "Successfully revealed weights.", ext_id else: # bittensor.logging.error(f"Failed to reveal weights: {error_message}") - return False, error_message + return False, error_message, ext_id - async def do_commit_weights(self, commit_hash): + async def do_commit_weights( + self, commit_hash + ) -> tuple[bool, Optional[str], Optional[str]]: call = await self.subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="commit_weights", @@ -357,12 +371,14 @@ async def do_commit_weights(self, commit_hash): ) if not self.wait_for_finalization and not self.wait_for_inclusion: - return True, None + return True, None, None if await response.is_success: - return True, None + ext_id = await response.get_extrinsic_identifier() + await print_extrinsic_id(response) + return True, None, ext_id else: - return False, await response.error_message + return False, await response.error_message, None # commands @@ -399,9 +415,13 @@ async def reveal_weights( extrinsic = SetWeightsExtrinsic( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) - success, message = await extrinsic.reveal(weight_uids, weight_vals) + success, message, ext_id = await extrinsic.reveal(weight_uids, weight_vals) if json_output: - json_console.print(json.dumps({"success": success, "message": message})) + json_console.print( + json.dumps( + {"success": success, "message": message, "extrinsic_identifier": ext_id} + ) + ) else: if success: console.print("Weights revealed successfully") @@ -436,9 +456,13 @@ async def commit_weights( extrinsic = SetWeightsExtrinsic( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) - success, message = await extrinsic.set_weights_extrinsic() + success, message, ext_id = await extrinsic.set_weights_extrinsic() if json_output: - json_console.print(json.dumps({"success": success, "message": message})) + json_console.print( + json.dumps( + {"success": success, "message": message, "extrinsic_identifier": ext_id} + ) + ) else: if success: console.print("Weights set successfully") diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 916b00cac..24f83bdfe 100644 --- a/tests/e2e_tests/test_hyperparams_setting.py +++ b/tests/e2e_tests/test_hyperparams_setting.py @@ -62,7 +62,7 @@ def test_hyperparams_setting(local_chain, wallet_setup): result_output = json.loads(result.stdout) assert result_output["success"] is True assert result_output["netuid"] == netuid - print(result_output) + assert isinstance(result_output["extrinsic_identifier"], str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( @@ -119,6 +119,7 @@ def test_hyperparams_setting(local_chain, wallet_setup): ) cmd_json = json.loads(cmd.stdout) assert cmd_json["success"] is True, (key, new_val, cmd.stdout, cmd_json) + assert isinstance(cmd_json["extrinsic_identifier"], str) print(f"Successfully set hyperparameter {key} to value {new_val}") # also test hidden hyperparam cmd = exec_command_alice( @@ -145,6 +146,7 @@ def test_hyperparams_setting(local_chain, wallet_setup): ) cmd_json = json.loads(cmd.stdout) assert cmd_json["success"] is True, (cmd.stdout, cmd_json) + assert isinstance(cmd_json["extrinsic_identifier"], str) print("Successfully set hyperparameters") print("Testing trimming UIDs") cmd = exec_command_alice( @@ -169,4 +171,5 @@ def test_hyperparams_setting(local_chain, wallet_setup): ) cmd_json = json.loads(cmd.stdout) assert cmd_json["success"] is True, (cmd.stdout, cmd_json) + assert isinstance(cmd_json["extrinsic_identifier"], str) print("Successfully trimmed UIDs") diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py index 218ef91f0..faaf6e05e 100644 --- a/tests/e2e_tests/test_liquidity.py +++ b/tests/e2e_tests/test_liquidity.py @@ -85,6 +85,7 @@ def liquidity_list(): result_output = json.loads(result.stdout) assert result_output["success"] is True assert result_output["netuid"] == netuid + assert isinstance(result_output["extrinsic_identifier"], str) # verify no results for list thus far (subnet not yet started) liquidity_list_result = liquidity_list() @@ -115,6 +116,7 @@ def liquidity_list(): f"Successfully started subnet {netuid}'s emission schedule" in start_subnet_emissions.stdout ), start_subnet_emissions.stderr + assert "Your extrinsic has been included " in start_subnet_emissions.stdout liquidity_list_result = liquidity_list() result_output = json.loads(liquidity_list_result.stdout) @@ -146,6 +148,7 @@ def liquidity_list(): ) enable_user_liquidity_result = json.loads(enable_user_liquidity.stdout) assert enable_user_liquidity_result["success"] is True + assert isinstance(enable_user_liquidity_result["extrinsic_identifier"], str) add_liquidity = exec_command_alice( command="liquidity", @@ -174,6 +177,7 @@ def liquidity_list(): add_liquidity_result = json.loads(add_liquidity.stdout) assert add_liquidity_result["success"] is True assert add_liquidity_result["message"] == "" + assert isinstance(add_liquidity_result["extrinsic_identifier"], str) liquidity_list_result = liquidity_list() liquidity_list_result = json.loads(liquidity_list_result.stdout) @@ -212,6 +216,7 @@ def liquidity_list(): ) modify_liquidity_result = json.loads(modify_liquidity.stdout) assert modify_liquidity_result["success"] is True + assert isinstance(modify_liquidity_result["extrinsic_identifier"], str) liquidity_list_result = json.loads(liquidity_list().stdout) assert len(liquidity_list_result["positions"]) == 1 @@ -240,6 +245,9 @@ def liquidity_list(): ) removal_result = json.loads(removal.stdout) assert removal_result[str(liquidity_position["id"])]["success"] is True + assert isinstance( + removal_result[str(liquidity_position["id"])]["extrinsic_identifier"], str + ) liquidity_list_result = json.loads(liquidity_list().stdout) assert liquidity_list_result["success"] is True diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index 4cf12bb2b..c4cbebd7c 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -77,6 +77,9 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Registered" in root_register.stdout, root_register.stderr + assert "Your extrinsic has been included " in root_register.stdout, ( + root_register.stderr + ) # Fetch the senate members after registering to root root_senate_after_reg = exec_command_bob( @@ -156,6 +159,7 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Vote cast" in vote_aye.stdout + assert "Your extrinsic has been included " in vote_aye.stdout # Fetch proposals after voting aye proposals_after_aye = exec_command_bob( @@ -219,6 +223,7 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Registered" in root_register.stdout + assert "Your extrinsic has been included " in root_register.stdout # Vote on the proposal by Alice (vote nay) vote_nay = exec_command_alice( @@ -240,6 +245,7 @@ def test_senate(local_chain, wallet_setup): ], ) assert "✅ Vote cast" in vote_nay.stdout + assert "Your extrinsic has been included " in vote_nay.stdout # Fetch proposals after voting proposals_after_nay = exec_command_bob( diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 9034c51da..4096c87b0 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,6 +1,7 @@ import asyncio import json import re +from typing import Union from bittensor_cli.src.bittensor.balances import Balance from .utils import turn_off_hyperparam_freeze_window @@ -105,6 +106,7 @@ def test_staking(local_chain, wallet_setup): result_output = json.loads(result.stdout) assert result_output["success"] is True assert result_output["netuid"] == netuid + assert isinstance(result_output["extrinsic_identifier"], str) # Register another subnet with sudo as Alice result_for_second_repo = exec_command_alice( @@ -142,6 +144,7 @@ def test_staking(local_chain, wallet_setup): result_output_second = json.loads(result_for_second_repo.stdout) assert result_output_second["success"] is True assert result_output_second["netuid"] == multiple_netuids[1] + assert isinstance(result_output_second["extrinsic_identifier"], str) # Register Alice in netuid = 2 using her hotkey register_subnet = exec_command_alice( @@ -162,6 +165,7 @@ def test_staking(local_chain, wallet_setup): ], ) assert "✅ Already Registered" in register_subnet.stdout + assert "Your extrinsic has been included" not in register_subnet.stdout register_subnet_json = exec_command_alice( command="subnets", @@ -184,6 +188,7 @@ def test_staking(local_chain, wallet_setup): register_subnet_json_output = json.loads(register_subnet_json.stdout) assert register_subnet_json_output["success"] is True assert register_subnet_json_output["msg"] == "Already registered" + assert register_subnet_json_output["extrinsic_identifier"] is None # set identity set_identity = exec_command_alice( @@ -222,6 +227,7 @@ def test_staking(local_chain, wallet_setup): ) set_identity_output = json.loads(set_identity.stdout) assert set_identity_output["success"] is True + assert isinstance(set_identity_output["extrinsic_identifier"], str) get_identity = exec_command_alice( "subnets", @@ -271,6 +277,7 @@ def test_staking(local_chain, wallet_setup): set_symbol_output["message"] == f"Successfully updated SN{netuid}'s symbol to シ." ) + assert isinstance(set_identity_output["extrinsic_identifier"], str) get_s_price = exec_command_alice( "subnets", @@ -314,6 +321,9 @@ def test_staking(local_chain, wallet_setup): f"Successfully started subnet {netuid_}'s emission schedule" in start_subnet_emissions.stdout ), start_subnet_emissions.stderr + assert "Your extrinsic has been included" in start_subnet_emissions.stdout, ( + start_subnet_emissions.stdout + ) # Add stake to Alice's hotkey add_stake_single = exec_command_alice( @@ -341,6 +351,9 @@ def test_staking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in add_stake_single.stdout, add_stake_single.stderr + assert "Your extrinsic has been included" in add_stake_single.stdout, ( + add_stake_single.stdout + ) # Execute stake show for Alice's wallet show_stake_adding_single = exec_command_alice( @@ -408,6 +421,9 @@ def test_staking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in remove_stake.stdout + assert "Your extrinsic has been included" in remove_stake.stdout, ( + remove_stake.stdout + ) add_stake_multiple = exec_command_alice( command="stake", @@ -436,18 +452,15 @@ def test_staking(local_chain, wallet_setup): ) add_stake_multiple_output = json.loads(add_stake_multiple.stdout) for netuid_ in multiple_netuids: - assert ( - add_stake_multiple_output["staking_success"][str(netuid_)][ - wallet_alice.hotkey.ss58_address - ] - is True - ) - assert ( - add_stake_multiple_output["error_messages"][str(netuid_)][ + + def line(key: str) -> Union[str, bool]: + return add_stake_multiple_output[key][str(netuid_)][ wallet_alice.hotkey.ss58_address ] - == "" - ) + + assert line("staking_success") is True + assert line("error_messages") == "" + assert isinstance(line("extrinsic_ids"), str) # Fetch the hyperparameters of the subnet hyperparams = exec_command_alice( @@ -507,6 +520,9 @@ def test_staking(local_chain, wallet_setup): assert ( "✅ Hyperparameter max_burn changed to 10000000000" in change_hyperparams.stdout ) + assert "Your extrinsic has been included" in change_hyperparams.stdout, ( + change_hyperparams.stdout + ) # Fetch the hyperparameters again to verify updated_hyperparams = exec_command_alice( @@ -576,6 +592,7 @@ def test_staking(local_chain, wallet_setup): assert change_yuma3_hyperparam_json["success"] is True, ( change_yuma3_hyperparam.stdout ) + assert isinstance(change_yuma3_hyperparam_json["extrinsic_identifier"], str) changed_yuma3_hyperparam = exec_command_alice( command="sudo", @@ -626,3 +643,4 @@ def test_staking(local_chain, wallet_setup): change_arbitrary_hyperparam.stdout, change_arbitrary_hyperparam.stderr, ) + assert isinstance(change_yuma3_hyperparam_json["extrinsic_identifier"], str) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 4b7ca0765..d4e3eef76 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -89,6 +89,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Registered subnetwork with netuid: 2" in result.stdout + assert "Your extrinsic has been included" in result.stdout, result.stdout # Create second subnet (netuid = 3) result = exec_command_alice( @@ -123,6 +124,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Registered subnetwork with netuid: 3" in result.stdout + assert "Your extrinsic has been included" in result.stdout, result.stdout # Start emission schedule for subnets start_call_netuid_0 = exec_command_alice( @@ -144,6 +146,9 @@ def test_unstaking(local_chain, wallet_setup): "Successfully started subnet 0's emission schedule." in start_call_netuid_0.stdout ) + assert "Your extrinsic has been included" in start_call_netuid_0.stdout, ( + start_call_netuid_0.stdout + ) start_call_netuid_2 = exec_command_alice( command="subnets", sub_command="start", @@ -163,6 +168,7 @@ def test_unstaking(local_chain, wallet_setup): "Successfully started subnet 2's emission schedule." in start_call_netuid_2.stdout ) + assert "Your extrinsic has been included" in start_call_netuid_2.stdout start_call_netuid_3 = exec_command_alice( command="subnets", @@ -183,6 +189,7 @@ def test_unstaking(local_chain, wallet_setup): "Successfully started subnet 3's emission schedule." in start_call_netuid_3.stdout ) + assert "Your extrinsic has been included" in start_call_netuid_3.stdout # Register Bob in one subnet register_result = exec_command_bob( command="subnets", @@ -204,6 +211,9 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Registered" in register_result.stdout, register_result.stderr + assert "Your extrinsic has been included" in register_result.stdout, ( + register_result.stdout + ) # Add stake to subnets for netuid in [0, 2, 3]: @@ -232,6 +242,9 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in stake_result.stdout, stake_result.stderr + assert "Your extrinsic has been included" in stake_result.stdout, ( + stake_result.stdout + ) stake_list = exec_command_bob( command="stake", @@ -279,6 +292,9 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in partial_unstake_netuid_2.stdout + assert "Your extrinsic has been included" in partial_unstake_netuid_2.stdout, ( + partial_unstake_netuid_2.stdout + ) # Verify partial unstake stake_list = exec_command_bob( @@ -348,6 +364,9 @@ def test_unstaking(local_chain, wallet_setup): assert ( "✅ Finalized: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout ) + assert "Your extrinsic has been included" in unstake_alpha.stdout, ( + unstake_alpha.stdout + ) # Add stake again to subnets for netuid in [0, 2, 3]: @@ -376,6 +395,7 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized" in stake_result.stdout + assert "Your extrinsic has been included" in stake_result.stdout # Remove all stakes unstake_all = exec_command_bob( @@ -397,4 +417,5 @@ def test_unstaking(local_chain, wallet_setup): ], ) assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout + assert "Your extrinsic has been included" in unstake_all.stdout, unstake_all.stdout print("Passed unstaking tests 🎉") diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index e6a4bb22d..9dd3ea4f1 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -443,17 +443,19 @@ def test_wallet_identities(local_chain, wallet_setup): assert "✅ Success!" in set_id.stdout set_id_output = set_id.stdout.splitlines() - assert alice_identity["name"] in set_id_output[6] - assert alice_identity["url"] in set_id_output[7] - assert alice_identity["github_repo"] in set_id_output[8] - assert alice_identity["image"] in set_id_output[9] - assert alice_identity["discord"] in set_id_output[10] - assert alice_identity["description"] in set_id_output[11] - assert alice_identity["additional"] in set_id_output[12] + assert "Your extrinsic has been included as" in set_id_output[1] + + assert alice_identity["name"] in set_id_output[7] + assert alice_identity["url"] in set_id_output[8] + assert alice_identity["github_repo"] in set_id_output[9] + assert alice_identity["image"] in set_id_output[10] + assert alice_identity["discord"] in set_id_output[11] + assert alice_identity["description"] in set_id_output[12] + assert alice_identity["additional"] in set_id_output[13] # TODO: Currently coldkey + hotkey are the same for test wallets. # Maybe we can add a new key to help in distinguishing - assert wallet_alice.coldkeypub.ss58_address in set_id_output[5] + assert wallet_alice.coldkeypub.ss58_address in set_id_output[6] # Execute btcli get-identity using hotkey get_identity = exec_command_alice(