diff --git a/CHANGELOG.md b/CHANGELOG.md index 27b5ca54b..95aae0d8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 9.12.0 /2025-09-25 +* Removes warning icon in transfer by @ibraheem-abe in https://github.com/opentensor/btcli/pull/634 +* Add Extrinsic Identifier Output by @thewhaleking in https://github.com/opentensor/btcli/pull/633 +* Update the example text for sudo trim by @thewhaleking in https://github.com/opentensor/btcli/pull/636 +* Feat/Individual wallet list by @ibraheem-abe in https://github.com/opentensor/btcli/pull/638 +* Feat/ subnet mechanisms by @ibraheem-abe in https://github.com/opentensor/btcli/pull/627 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.11.2...v9.12.0 + ## 9.11.2 /2025-09-19 * Fix: Stake movement between non-root sns by @ibraheem-abe in https://github.com/opentensor/btcli/pull/629 diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 88ae40580..7fc4fbde8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -78,7 +78,11 @@ add as add_stake, remove as remove_stake, ) -from bittensor_cli.src.commands.subnets import price, subnets +from bittensor_cli.src.commands.subnets import ( + price, + subnets, + mechanisms as subnet_mechanisms, +) from bittensor_cli.version import __version__, __version_as_int__ try: @@ -229,6 +233,15 @@ def edit_help(cls, option_name: str, help_text: str): help="The netuid of the subnet in the network, (e.g. 1).", prompt=False, ) + mechanism_id = typer.Option( + None, + "--mechid", + "--mech-id", + "--mech_id", + "--mechanism_id", + "--mechanism-id", + help="Mechanism ID within the subnet (defaults to 0).", + ) all_netuids = typer.Option( False, help="Use all netuids", @@ -650,6 +663,7 @@ class CLIManager: :var wallet_app: the Typer app as it relates to wallet commands :var stake_app: the Typer app as it relates to stake commands :var sudo_app: the Typer app as it relates to sudo commands + :var subnet_mechanisms_app: the Typer app for subnet mechanism commands :var subnets_app: the Typer app as it relates to subnets commands :var subtensor: the `SubtensorInterface` object passed to the various commands that require it """ @@ -658,7 +672,9 @@ class CLIManager: app: typer.Typer config_app: typer.Typer wallet_app: typer.Typer + sudo_app: typer.Typer subnets_app: typer.Typer + subnet_mechanisms_app: typer.Typer weights_app: typer.Typer utils_app: typer.Typer view_app: typer.Typer @@ -733,6 +749,7 @@ def __init__(self): self.stake_app = typer.Typer(epilog=_epilog) self.sudo_app = typer.Typer(epilog=_epilog) self.subnets_app = typer.Typer(epilog=_epilog) + self.subnet_mechanisms_app = typer.Typer(epilog=_epilog) self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) @@ -794,6 +811,19 @@ def __init__(self): self.subnets_app, name="subnet", hidden=True, no_args_is_help=True ) + # subnet mechanisms aliases + self.subnets_app.add_typer( + self.subnet_mechanisms_app, + name="mechanisms", + short_help="Subnet mechanism commands, alias: `mech`", + no_args_is_help=True, + ) + self.subnets_app.add_typer( + self.subnet_mechanisms_app, + name="mech", + hidden=True, + no_args_is_help=True, + ) # weights aliases self.app.add_typer( self.weights_app, @@ -938,6 +968,20 @@ def __init__(self): children_app.command("revoke")(self.stake_revoke_children) children_app.command("take")(self.stake_childkey_take) + # subnet mechanism commands + self.subnet_mechanisms_app.command( + "count", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] + )(self.mechanism_count_get) + self.subnet_mechanisms_app.command( + "set", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"] + )(self.mechanism_count_set) + self.subnet_mechanisms_app.command( + "emissions", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] + )(self.mechanism_emission_get) + self.subnet_mechanisms_app.command( + "emissions-split", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"] + )(self.mechanism_emission_set) + # sudo commands self.sudo_app.command("set", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( self.sudo_set @@ -960,6 +1004,9 @@ def __init__(self): self.sudo_app.command("get-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])( self.sudo_get_take ) + self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])( + self.sudo_trim + ) # subnets commands self.subnets_app.command( @@ -1759,6 +1806,43 @@ def ask_partial_stake( logger.debug(f"Partial staking {partial_staking}") return False + def ask_subnet_mechanism( + self, + mechanism_id: Optional[int], + mechanism_count: int, + netuid: int, + ) -> int: + """Resolve the mechanism ID to use.""" + + if mechanism_count is None or mechanism_count <= 0: + err_console.print(f"Subnet {netuid} does not exist.") + raise typer.Exit() + + if mechanism_id is not None: + if mechanism_id < 0 or mechanism_id >= mechanism_count: + err_console.print( + f"Mechanism ID {mechanism_id} is out of range for subnet {netuid}. " + f"Valid range: [bold cyan]0 to {mechanism_count - 1}[/bold cyan]." + ) + raise typer.Exit() + return mechanism_id + + if mechanism_count == 1: + return 0 + + while True: + selected_mechanism_id = IntPrompt.ask( + f"Select mechanism ID for subnet {netuid} " + f"([bold cyan]0 to {mechanism_count - 1}[/bold cyan])", + default=0, + ) + if 0 <= selected_mechanism_id < mechanism_count: + return selected_mechanism_id + err_console.print( + f"Mechanism ID {selected_mechanism_id} is out of range for subnet {netuid}. " + f"Valid range: [bold cyan]0 to {mechanism_count - 1}[/bold cyan]." + ) + def wallet_ask( self, wallet_name: Optional[str], @@ -1872,6 +1956,7 @@ def wallet_ask( def wallet_list( self, + wallet_name: Optional[str] = Options.wallet_name, wallet_path: str = Options.wallet_path, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -1895,7 +1980,13 @@ def wallet_list( wallet = self.wallet_ask( None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE ) - return self._run_command(wallets.wallet_list(wallet.path, json_output)) + return self._run_command( + wallets.wallet_list( + wallet.path, + json_output, + wallet_name=wallet_name, + ) + ) def wallet_overview( self, @@ -3751,6 +3842,8 @@ def stake_add( subnets.show( subtensor=self.initialize_chain(network), netuid=netuid_, + mechanism_id=0, + mechanism_count=1, sort=False, max_rows=12, prompt=False, @@ -4376,7 +4469,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, @@ -4392,7 +4485,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( @@ -4552,7 +4647,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), @@ -4568,7 +4663,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( @@ -4672,7 +4769,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), @@ -4688,7 +4785,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( @@ -4987,7 +5086,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), @@ -5001,11 +5100,243 @@ 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 + def mechanism_count_set( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: int = Options.netuid, + mechanism_count: Optional[int] = typer.Option( + None, + "--count", + "--mech-count", + help="Number of mechanisms to set for the subnet.", + ), + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Configure how many mechanisms are registered for a subnet. + + The base mechanism at index 0 and new ones are incremented by 1. + + [bold]Common Examples:[/bold] + + 1. Prompt for the new mechanism count interactively: + [green]$[/green] btcli subnet mech set --netuid 12 + + 2. Set the count to 2 using a specific wallet: + [green]$[/green] btcli subnet mech set --netuid 12 --count 2 --wallet.name my_wallet --wallet.hotkey admin + + """ + + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + + if not json_output: + current_count = self._run_command( + subnet_mechanisms.count( + subtensor=subtensor, + netuid=netuid, + json_output=False, + ), + exit_early=False, + ) + else: + current_count = self._run_command( + subtensor.get_subnet_mechanisms(netuid), + exit_early=False, + ) + + if mechanism_count is None: + if not prompt: + err_console.print( + "Mechanism count not supplied with `--no-prompt` flag. Cannot continue." + ) + return False + prompt_text = "\n\nEnter the [blue]number of mechanisms[/blue] to set" + mechanism_count = IntPrompt.ask(prompt_text) + + if mechanism_count == current_count: + visible_count = max(mechanism_count - 1, 0) + message = ( + ":white_heavy_check_mark: " + f"[dark_sea_green3]Subnet {netuid} already has {visible_count} mechanism" + f"{'s' if visible_count != 1 else ''}.[/dark_sea_green3]" + ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "message": f"Subnet {netuid} already has {visible_count} mechanisms.", + "extrinsic_identifier": None, + } + ) + ) + else: + console.print(message) + return True + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) + + logger.debug( + "args:\n" + f"network: {network}\n" + f"netuid: {netuid}\n" + f"mechanism_count: {mechanism_count}\n" + ) + + result, err_msg, ext_id = self._run_command( + subnet_mechanisms.set_mechanism_count( + wallet=wallet, + subtensor=subtensor, + netuid=netuid, + mechanism_count=mechanism_count, + previous_count=current_count or 0, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + json_output=json_output, + ) + ) + + if json_output: + json_console.print_json( + data={ + "success": result, + "message": err_msg, + "extrinsic_identifier": ext_id, + } + ) + + return result + + def mechanism_count_get( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Display how many mechanisms are registered under a subnet. + + Includes the base mechanism (index 0). Helpful for verifying the active + mechanism counts in a subnet. + + [green]$[/green] btcli subnet mech count --netuid 12 + """ + + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + return self._run_command( + subnet_mechanisms.count( + subtensor=subtensor, + netuid=netuid, + json_output=json_output, + ) + ) + + def mechanism_emission_set( + self, + network: Optional[list[str]] = Options.network, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + netuid: int = Options.netuid, + split: Optional[str] = typer.Option( + None, + "--split", + help="Comma-separated relative weights for each mechanism (normalised automatically).", + ), + wait_for_inclusion: bool = Options.wait_for_inclusion, + wait_for_finalization: bool = Options.wait_for_finalization, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Update the emission split across mechanisms for a subnet. + + Accepts comma-separated weights (U16 values or percentages). When `--split` + is omitted and prompts remain enabled, you will be guided interactively and + the CLI automatically normalises the weights. + + [bold]Common Examples:[/bold] + + 1. Configure the split interactively: + [green]$[/green] btcli subnet mech emissions-split --netuid 12 + + 2. Apply a 70/30 distribution in one command: + [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin + """ + + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + ) + + return self._run_command( + subnet_mechanisms.set_emission_split( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + new_emission_split=split, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + prompt=prompt, + json_output=json_output, + ) + ) + + def mechanism_emission_get( + self, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + ): + """ + Display the current emission split across mechanisms for a subnet. + + Shows raw U16 weights alongside percentage shares for each mechanism. Useful + for verifying the emission split in a subnet. + + [green]$[/green] btcli subnet mech emissions --netuid 12 + """ + + self.verbosity_handler(quiet, verbose, json_output) + subtensor = self.initialize_chain(network) + return self._run_command( + subnet_mechanisms.get_emission_split( + subtensor=subtensor, + netuid=netuid, + json_output=json_output, + ) + ) + def sudo_set( self, network: Optional[list[str]] = Options.network, @@ -5125,7 +5456,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), @@ -5137,7 +5468,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( @@ -5222,7 +5561,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", ), ): """ @@ -5299,11 +5638,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( @@ -5343,6 +5684,53 @@ def sudo_get_take( sudo.display_current_take(self.initialize_chain(network), wallet) ) + def sudo_trim( + self, + network: Optional[list[str]] = Options.network, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + netuid: int = Options.netuid, + max_uids: int = typer.Option( + None, + "--max", + "--max-uids", + help="The maximum number of allowed uids to which to trim", + prompt="Max UIDs", + ), + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, + prompt: bool = Options.prompt, + period: int = Options.period, + ): + """ + Allows subnet owners to trim UIDs on their subnet to a specified max number of netuids. + + EXAMPLE + [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64 + """ + self.verbosity_handler(quiet, verbose, json_output) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + self._run_command( + sudo.trim( + subtensor=self.initialize_chain(network), + wallet=wallet, + netuid=netuid, + max_n=max_uids, + period=period, + json_output=json_output, + prompt=prompt, + ) + ) + def subnets_list( self, network: Optional[list[str]] = Options.network, @@ -5512,6 +5900,7 @@ def subnets_show( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + mechanism_id: Optional[int] = Options.mechanism_id, sort: bool = typer.Option( False, "--sort", @@ -5523,18 +5912,43 @@ def subnets_show( json_output: bool = Options.json_output, ): """ - Displays detailed information about a subnet including participants and their state. + Inspect the metagraph for a subnet. - EXAMPLE + Shows miners, validators, stake, ranks, emissions, and other runtime stats. + When multiple mechanisms exist, the CLI prompts for one unless `--mechid` + is supplied. Netuid 0 always uses mechid 0. + + [bold]Common Examples:[/bold] + + 1. Inspect the mechanism with prompts for selection: + [green]$[/green] btcli subnets show --netuid 12 - [green]$[/green] btcli subnets show + 2. Pick mechanism 1 explicitly: + [green]$[/green] btcli subnets show --netuid 12 --mechid 1 """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) + if netuid == 0: + mechanism_count = 1 + selected_mechanism_id = 0 + if mechanism_id not in (None, 0): + console.print( + "[dim]Mechanism selection ignored for the root subnet (only mechanism 0 exists).[/dim]" + ) + else: + mechanism_count = self._run_command( + subtensor.get_subnet_mechanisms(netuid), exit_early=False + ) + selected_mechanism_id = self.ask_subnet_mechanism( + mechanism_id, mechanism_count, netuid + ) + return self._run_command( subnets.show( subtensor=subtensor, netuid=netuid, + mechanism_id=selected_mechanism_id, + mechanism_count=mechanism_count, sort=sort, max_rows=None, delegate_selection=False, @@ -5820,13 +6234,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, @@ -6154,6 +6570,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/__init__.py b/bittensor_cli/src/__init__.py index f93aed504..bc46bd485 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -666,6 +666,7 @@ class WalletValidationTypes(Enum): "user_liquidity_enabled": ("toggle_user_liquidity", True), "bonds_reset_enabled": ("sudo_set_bonds_reset_enabled", False), "transfers_enabled": ("sudo_set_toggle_transfer", False), + "min_allowed_uids": ("sudo_set_min_allowed_uids", True), } HYPERPARAMS_MODULE = { @@ -699,6 +700,10 @@ class WalletValidationTypes(Enum): "GOVERNANCE": "Governance", "TAKE": "Delegate take configuration", }, + "MECHANISMS": { + "CONFIG": "Mechanism Configuration", + "EMISSION": "Mechanism Emission", + }, "SUBNETS": { "INFO": "Subnet Information", "CREATION": "Subnet Creation & Management", diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index 07fd8c906..ffd4ba3ff 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -13,6 +13,7 @@ u16_normalized_float as u16tf, u64_normalized_float as u64tf, decode_account_id, + get_netuid_and_subuid_by_storage_index, ) @@ -1084,12 +1085,13 @@ class MetagraphInfo(InfoBase): alpha_dividends_per_hotkey: list[ tuple[str, Balance] ] # List of dividend payout in alpha via subnet. + subuid: int = 0 @classmethod def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": """Returns a MetagraphInfo object from decoded chain data.""" # Subnet index - _netuid = decoded["netuid"] + _netuid, _subuid = get_netuid_and_subuid_by_storage_index(decoded["netuid"]) # Name and symbol decoded.update({"name": bytes(decoded.get("name")).decode()}) @@ -1102,6 +1104,7 @@ def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo": return cls( # Subnet index netuid=_netuid, + subuid=_subuid, # Name and symbol name=decoded["name"], symbol=decoded["symbol"], 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..f71e747ff 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: @@ -176,30 +181,18 @@ async def do_transfer() -> tuple[bool, str, str]: f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : " f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]" f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]\n" - 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:" + f"[bright_yellow]Transferring is not the same as staking. To instead stake, use " + f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]" ): - 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..d37d5f3db 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]: """ @@ -1171,6 +1175,55 @@ async def get_subnet_hyperparameters( return SubnetHyperparameters.from_any(result) + async def get_subnet_mechanisms( + self, netuid: int, block_hash: Optional[str] = None + ) -> int: + """Return the number of mechanisms that belong to the provided subnet.""" + + result = await self.query( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[netuid], + block_hash=block_hash, + ) + + if result is None: + return 0 + return int(result) + + async def get_all_subnet_mechanisms( + self, block_hash: Optional[str] = None + ) -> dict[int, int]: + """Return mechanism counts for every subnet with a recorded value.""" + + results = await self.substrate.query_map( + module="SubtensorModule", + storage_function="MechanismCountCurrent", + params=[], + block_hash=block_hash, + ) + res = {} + async for netuid, count in results: + res[int(netuid)] = int(count.value) + return res + + async def get_mechanism_emission_split( + self, netuid: int, block_hash: Optional[str] = None + ) -> list[int]: + """Return the emission split configured for the provided subnet.""" + + result = await self.query( + module="SubtensorModule", + storage_function="MechanismEmissionSplit", + params=[netuid], + block_hash=block_hash, + ) + + if not result: + return [] + + return [int(value) for value in result] + async def burn_cost(self, block_hash: Optional[str] = None) -> Optional[Balance]: result = await self.query_runtime_api( runtime_api="SubnetRegistrationRuntimeApi", @@ -1296,37 +1349,51 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid( else: return Balance.from_rao(fixed_to_float(_result)).set_unit(int(netuid)) + async def get_mechagraph_info( + self, netuid: int, mech_id: int, block_hash: Optional[str] = None + ) -> Optional[MetagraphInfo]: + """ + Returns the metagraph info for a given subnet and mechanism id. + And yes, it is indeed 'mecha'graph + """ + query = await self.query_runtime_api( + runtime_api="SubnetInfoRuntimeApi", + method="get_mechagraph", + params=[netuid, mech_id], + block_hash=block_hash, + ) + + if query is None: + return None + + return MetagraphInfo.from_any(query) + async def get_metagraph_info( self, netuid: int, block_hash: Optional[str] = None ) -> Optional[MetagraphInfo]: - hex_bytes_result = await self.query_runtime_api( + query = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_metagraph", params=[netuid], block_hash=block_hash, ) - if hex_bytes_result is None: + if query is None: return None - try: - bytes_result = bytes.fromhex(hex_bytes_result[2:]) - except ValueError: - bytes_result = bytes.fromhex(hex_bytes_result) - - return MetagraphInfo.from_any(bytes_result) + return MetagraphInfo.from_any(query) async def get_all_metagraphs_info( self, block_hash: Optional[str] = None ) -> list[MetagraphInfo]: - hex_bytes_result = await self.query_runtime_api( + query = await self.query_runtime_api( runtime_api="SubnetInfoRuntimeApi", method="get_all_metagraphs", params=[], block_hash=block_hash, ) - return MetagraphInfo.list_from_any(hex_bytes_result) + return MetagraphInfo.list_from_any(query) async def multi_get_stake_for_coldkey_and_hotkey_on_netuid( self, diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 80aab6916..c8be33563 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 @@ -34,6 +35,7 @@ BT_DOCS_LINK = "https://docs.learnbittensor.org" +GLOBAL_MAX_SUBNET_COUNT = 4096 console = Console() json_console = Console() @@ -507,6 +509,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 +1465,50 @@ def get_hotkey_pub_ss58(wallet: Wallet) -> str: return wallet.hotkeypub.ss58_address except (KeyFileError, AttributeError): return wallet.hotkey.ss58_address + + +def get_netuid_and_subuid_by_storage_index(storage_index: int) -> tuple[int, int]: + """Returns the netuid and subuid from the storage index. + + Chain APIs (e.g., SubMetagraph response) returns netuid which is storage index that encodes both the netuid and + subuid. This function reverses the encoding to extract these components. + + Parameters: + storage_index: The storage index of the subnet. + + Returns: + tuple[int, int]: + - netuid subnet identifier. + - subuid identifier. + """ + return ( + storage_index % GLOBAL_MAX_SUBNET_COUNT, + storage_index // GLOBAL_MAX_SUBNET_COUNT, + ) + + +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/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py new file mode 100644 index 000000000..bf329513a --- /dev/null +++ b/bittensor_cli/src/commands/subnets/mechanisms.py @@ -0,0 +1,498 @@ +import asyncio +import math +from typing import TYPE_CHECKING, Optional + +from bittensor_wallet import Wallet +from rich.prompt import Confirm, Prompt +from rich.table import Column, Table +from rich import box + +from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.commands import sudo +from bittensor_cli.src.bittensor.utils import ( + console, + err_console, + json_console, + U16_MAX, + print_extrinsic_id, +) + +if TYPE_CHECKING: + from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface + + +async def count( + subtensor: "SubtensorInterface", + netuid: int, + json_output: bool = False, +) -> Optional[int]: + """Display how many mechanisms exist for the provided subnet.""" + + block_hash = await subtensor.substrate.get_chain_head() + if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): + err_console.print(f"[red]Subnet {netuid} does not exist[/red]") + if json_output: + json_console.print_json( + data={"success": False, "error": f"Subnet {netuid} does not exist"} + ) + return None + + with console.status( + f":satellite:Retrieving mechanism count from {subtensor.network}...", + spinner="aesthetic", + ): + mechanism_count = await subtensor.get_subnet_mechanisms( + netuid, block_hash=block_hash + ) + if not mechanism_count: + if json_output: + json_console.print_json( + data={ + "netuid": netuid, + "count": None, + "error": "Failed to get mechanism count", + } + ) + else: + err_console.print( + "Subnet mechanism count: [red]Failed to get mechanism count[/red]" + ) + return None + + if json_output: + json_console.print_json( + data={ + "netuid": netuid, + "count": mechanism_count, + "error": "", + } + ) + else: + console.print( + f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] mechanism" + f"{'s' if mechanism_count != 1 else ''}." + f"\n[dim](Tip: 1 mechanism means there are no mechanisms beyond the main subnet)[/dim]" + ) + + return mechanism_count + + +async def get_emission_split( + subtensor: "SubtensorInterface", + netuid: int, + json_output: bool = False, +) -> Optional[dict]: + """Display the emission split across mechanisms for a subnet.""" + + count_ = await subtensor.get_subnet_mechanisms(netuid) + if count_ == 1: + console.print( + f"Subnet {netuid} only has the primary mechanism (mechanism 0). No emission split to display." + ) + if json_output: + json_console.print_json( + data={ + "success": False, + "error": "Subnet only has the primary mechanism (mechanism 0). No emission split to display.", + } + ) + return None + + emission_split = await subtensor.get_mechanism_emission_split(netuid) or [] + + even_distribution = False + total_sum = sum(emission_split) + if total_sum == 0 and count_ > 0: + even_distribution = True + base, remainder = divmod(U16_MAX, count_) + emission_split = [base for _ in range(count_)] + if remainder: + emission_split[0] += remainder + total_sum = sum(emission_split) + + emission_percentages = ( + [round((value / total_sum) * 100, 6) for value in emission_split] + if total_sum > 0 + else [0.0 for _ in emission_split] + ) + + data = { + "netuid": netuid, + "raw_count": count_, + "visible_count": max(count_ - 1, 0), + "split": emission_split if count_ else [], + "percentages": emission_percentages if count_ else [], + "even_distribution": even_distribution, + } + + if json_output: + json_console.print_json(data=data) + else: + table = Table( + Column( + "[bold white]Mechanism Index[/]", + justify="center", + style=COLOR_PALETTE.G.NETUID, + ), + Column( + "[bold white]Weight (u16)[/]", + justify="right", + style=COLOR_PALETTE.STAKE.STAKE_ALPHA, + ), + Column( + "[bold white]Share (%)[/]", + justify="right", + style=COLOR_PALETTE.POOLS.EMISSION, + ), + title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet {netuid} • Emission split[/]\n" + f"[{COLOR_PALETTE.G.SUBHEAD}]Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]", + box=box.SIMPLE, + show_footer=True, + border_style="bright_black", + ) + + total_weight = sum(emission_split) + share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 + + for idx, value in enumerate(emission_split): + share = ( + emission_percentages[idx] if idx < len(emission_percentages) else 0.0 + ) + table.add_row(str(idx), str(value), f"{share:.6f}") + + table.add_row( + "[dim]Total[/dim]", + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", + f"[{COLOR_PALETTE.POOLS.EMISSION}]{share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", + ) + + console.print(table) + footer = "[dim]Totals are expressed as a fraction of 65535 (U16_MAX).[/dim]" + if even_distribution: + footer += ( + "\n[dim]No custom split found; displaying an even distribution.[/dim]" + ) + console.print(footer) + + return data + + +async def set_emission_split( + subtensor: "SubtensorInterface", + wallet: Wallet, + netuid: int, + new_emission_split: Optional[str], + wait_for_inclusion: bool, + wait_for_finalization: bool, + prompt: bool, + json_output: bool, +) -> bool: + """Set the emission split across mechanisms for a subnet.""" + + mech_count, existing_split = await asyncio.gather( + subtensor.get_subnet_mechanisms(netuid), + subtensor.get_mechanism_emission_split(netuid), + ) + + if mech_count == 0: + message = ( + f"Subnet {netuid} does not currently contain any mechanisms to configure." + ) + if json_output: + json_console.print_json(data={"success": False, "error": message}) + else: + err_console.print(message) + return False + + if not json_output: + await get_emission_split( + subtensor=subtensor, + netuid=netuid, + json_output=False, + ) + + existing_split = [int(value) for value in existing_split] + if len(existing_split) < mech_count: + existing_split.extend([0] * (mech_count - len(existing_split))) + + if new_emission_split is not None: + try: + weights = [ + float(item.strip()) + for item in new_emission_split.split(",") + if item.strip() != "" + ] + except ValueError: + message = ( + "Invalid `--split` values. Provide a comma-separated list of numbers." + ) + if json_output: + json_console.print_json(data={"success": False, "error": message}) + else: + err_console.print(message) + return False + else: + if not prompt: + err_console.print( + "Split values not supplied with `--no-prompt` flag. Cannot continue." + ) + return False + + weights: list[float] = [] + total_existing = sum(existing_split) or 1 + console.print("\n[dim]You either provide U16 values or percentages.[/dim]") + for idx in range(mech_count): + current_value = existing_split[idx] + current_percent = ( + (current_value / total_existing) * 100 if total_existing else 0 + ) + label = ( + "[blue]Main Mechanism (1)[/blue]" + if idx == 0 + else f"[blue]Mechanism {idx + 1}[/blue]" + ) + response = Prompt.ask( + ( + f"Relative weight for {label} " + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}](current: {current_value} ~ {current_percent:.2f}%)[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]" + ) + ) + try: + weights.append(float(response)) + except ValueError: + err_console.print("Invalid number provided. Aborting.") + return False + + if len(weights) != mech_count: + message = f"Expected {mech_count} weight values, received {len(weights)}." + if json_output: + json_console.print_json(data={"success": False, "error": message}) + else: + err_console.print(message) + return False + + if any(value < 0 for value in weights): + message = "Weights must be non-negative." + if json_output: + json_console.print_json(data={"success": False, "error": message}) + else: + err_console.print(message) + return False + + try: + normalized_weights, fractions = _normalize_emission_weights(weights) + except ValueError as exc: + message = str(exc) + if json_output: + json_console.print_json(data={"success": False, "error": message}) + else: + err_console.print(message) + return False + + if normalized_weights == existing_split: + message = ":white_heavy_check_mark: [dark_sea_green3]Emission split unchanged.[/dark_sea_green3]" + if json_output: + json_console.print_json( + data={ + "success": True, + "message": "Emission split unchanged.", + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + "extrinsic_identifier": None, + } + ) + else: + console.print(message) + return True + + if not json_output: + table = Table( + Column( + "[bold white]Mechanism Index[/]", + justify="center", + style=COLOR_PALETTE.G.NETUID, + ), + Column( + "[bold white]Weight (u16)[/]", + justify="right", + style=COLOR_PALETTE.STAKE.STAKE_ALPHA, + ), + Column( + "[bold white]Share (%)[/]", + justify="right", + style=COLOR_PALETTE.POOLS.EMISSION, + ), + title=( + f"\n[{COLOR_PALETTE.G.HEADER}]Proposed emission split[/{COLOR_PALETTE.G.HEADER}]\n" + f"[{COLOR_PALETTE.G.SUBHEAD}]Subnet {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]" + ), + box=box.SIMPLE, + show_footer=True, + border_style="bright_black", + ) + + total_weight = sum(normalized_weights) + total_share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0 + + for idx, weight in enumerate(normalized_weights): + share_percent = fractions[idx] * 100 if idx < len(fractions) else 0.0 + table.add_row(str(idx), str(weight), f"{share_percent:.6f}") + + table.add_row("", "", "", style="dim") + table.add_row( + "[dim]Total[/dim]", + f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]", + f"[{COLOR_PALETTE.POOLS.EMISSION}]{total_share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]", + ) + + console.print(table) + + if not Confirm.ask("Proceed with these emission weights?", default=True): + console.print(":cross_mark: Aborted!") + return False + + success, err_msg, ext_id = await set_mechanism_emission( + wallet=wallet, + subtensor=subtensor, + netuid=netuid, + split=normalized_weights, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + json_output=json_output, + ) + + if json_output: + json_console.print_json( + data={ + "success": success, + "err_msg": err_msg, + "split": normalized_weights, + "percentages": [round(value * 100, 6) for value in fractions], + "extrinsic_identifier": ext_id, + } + ) + + return success + + +def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[float]]: + total = sum(values) + if total <= 0: + raise ValueError("Sum of emission weights must be greater than zero.") + + fractions = [value / total for value in values] + scaled = [fraction * U16_MAX for fraction in fractions] + base = [math.floor(value) for value in scaled] + remainder = int(U16_MAX - sum(base)) + + if remainder > 0: + fractional_parts = [value - math.floor(value) for value in scaled] + order = sorted( + range(len(base)), key=lambda idx_: fractional_parts[idx_], reverse=True + ) + idx = 0 + length = len(order) + while remainder > 0 and length > 0: + base[order[idx % length]] += 1 + remainder -= 1 + idx += 1 + + return [int(value) for value in base], fractions + + +async def set_mechanism_count( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + mechanism_count: int, + previous_count: int, + wait_for_inclusion: bool, + wait_for_finalization: bool, + json_output: bool, +) -> tuple[bool, str, Optional[str]]: + """Set the number of mechanisms for a subnet.""" + + if mechanism_count < 1: + err_msg = "Mechanism count must be greater than or equal to one." + if not json_output: + err_console.print(err_msg) + return False, err_msg, None + + if not await subtensor.subnet_exists(netuid): + err_msg = f"Subnet with netuid {netuid} does not exist." + if not json_output: + err_console.print(err_msg) + return False, err_msg, None + + if not Confirm.ask( + f"Subnet [blue]{netuid}[/blue] currently has [blue]{previous_count}[/blue] mechanism" + f"{'s' if previous_count != 1 else ''}." + f" Set it to [blue]{mechanism_count}[/blue]?" + ): + return False, "User cancelled", None + + success, err_msg, ext_receipt = await sudo.set_mechanism_count_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + mech_count=mechanism_count, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + ext_id = await ext_receipt.get_extrinsic_identifier() if success else None + + if json_output: + return success, err_msg, ext_id + + if success: + await print_extrinsic_id(ext_receipt) + console.print( + ":white_heavy_check_mark: " + f"[dark_sea_green3]Mechanism count set to {mechanism_count} for subnet {netuid}[/dark_sea_green3]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg, ext_id + + +async def set_mechanism_emission( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + split: list[int], + wait_for_inclusion: bool, + wait_for_finalization: bool, + json_output: bool, +) -> tuple[bool, str, Optional[str]]: + """Set the emission split for mechanisms within a subnet.""" + + if not split: + err_msg = "Emission split must include at least one weight." + if not json_output: + err_console.print(err_msg) + return False, err_msg, None + + success, err_msg, ext_receipt = await sudo.set_mechanism_emission_extrinsic( + subtensor=subtensor, + wallet=wallet, + netuid=netuid, + split=split, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + ext_id = await ext_receipt.get_extrinsic_identifier() if success else None + + if json_output: + return success, err_msg, ext_id + + if success: + await print_extrinsic_id(ext_receipt) + console.print( + ":white_heavy_check_mark: " + f"[dark_sea_green3]Emission split updated for subnet {netuid}[/dark_sea_green3]" + ) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + + return success, err_msg, ext_id diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index d8571f3f6..664657986 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 @@ -216,8 +221,12 @@ async def subnets_list( """List all subnet netuids in the network.""" async def fetch_subnet_data(): - block_number_ = await subtensor.substrate.get_block_number(None) - subnets_ = await subtensor.all_subnets() + block_hash = await subtensor.substrate.get_chain_head() + subnets_, mechanisms, block_number_ = await asyncio.gather( + subtensor.all_subnets(block_hash=block_hash), + subtensor.get_all_subnet_mechanisms(block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), + ) # Sort subnets by market cap, keeping the root subnet in the first position root_subnet = next(s for s in subnets_ if s.netuid == 0) @@ -227,7 +236,7 @@ async def fetch_subnet_data(): reverse=True, ) sorted_subnets = [root_subnet] + other_subnets - return sorted_subnets, block_number_ + return sorted_subnets, block_number_, mechanisms def calculate_emission_stats( subnets_: list, block_number_: int @@ -315,10 +324,15 @@ def define_table( justify="left", overflow="fold", ) + defined_table.add_column( + "[bold white]Mechanisms", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_1"], + justify="center", + ) return defined_table # Non-live mode - def _create_table(subnets_, block_number_): + def _create_table(subnets_, block_number_, mechanisms): rows = [] _, percentage_string = calculate_emission_stats(subnets_, block_number_) @@ -398,6 +412,8 @@ def _create_table(subnets_, block_number_): else: tempo_cell = "-/-" + mechanisms_cell = str(mechanisms.get(netuid, 1)) + rows.append( ( netuid_cell, # Netuid @@ -409,6 +425,7 @@ def _create_table(subnets_, block_number_): alpha_out_cell, # Stake α_out supply_cell, # Supply tempo_cell, # Tempo k/n + mechanisms_cell, # Mechanism count ) ) @@ -430,7 +447,7 @@ def _create_table(subnets_, block_number_): defined_table.add_row(*row) return defined_table - def dict_table(subnets_, block_number_) -> dict: + def dict_table(subnets_, block_number_, mechanisms) -> dict: subnet_rows = {} total_tao_emitted, _ = calculate_emission_stats(subnets_, block_number_) total_emissions = 0.0 @@ -470,6 +487,7 @@ def dict_table(subnets_, block_number_) -> dict: "alpha_out": alpha_out, "supply": supply, "tempo": tempo, + "mechanisms": mechanisms.get(netuid, 1), } output = { "total_tao_emitted": total_tao_emitted, @@ -482,7 +500,7 @@ def dict_table(subnets_, block_number_) -> dict: return output # Live mode - def create_table_live(subnets_, previous_data_, block_number_): + def create_table_live(subnets_, previous_data_, block_number_, mechanisms): def format_cell( value, previous_value, unit="", unit_first=False, precision=4, millify=False ): @@ -718,6 +736,7 @@ def format_liquidity_cell( alpha_out_cell, # Stake α_out supply_cell, # Supply tempo_cell, # Tempo k/n + str(mechanisms.get(netuid, 1)), # Mechanisms ) ) @@ -764,7 +783,7 @@ def format_liquidity_cell( with Live(console=console, screen=True, auto_refresh=True) as live: try: while True: - subnets, block_number = await fetch_subnet_data() + subnets, block_number, mechanisms = await fetch_subnet_data() # Update block numbers previous_block = current_block @@ -776,7 +795,7 @@ def format_liquidity_cell( ) table, current_data = create_table_live( - subnets, previous_data, block_number + subnets, previous_data, block_number, mechanisms ) previous_data = current_data progress.reset(progress_task) @@ -802,11 +821,13 @@ def format_liquidity_cell( pass # Ctrl + C else: # Non-live mode - subnets, block_number = await fetch_subnet_data() + subnets, block_number, mechanisms = await fetch_subnet_data() if json_output: - json_console.print(json.dumps(dict_table(subnets, block_number))) + json_console.print( + json.dumps(dict_table(subnets, block_number, mechanisms)) + ) else: - table = _create_table(subnets, block_number) + table = _create_table(subnets, block_number, mechanisms) console.print(table) return @@ -872,6 +893,8 @@ def format_liquidity_cell( async def show( subtensor: "SubtensorInterface", netuid: int, + mechanism_id: Optional[int] = None, + mechanism_count: Optional[int] = None, sort: bool = False, max_rows: Optional[int] = None, delegate_selection: bool = False, @@ -1085,43 +1108,57 @@ async def show_root(): ) return selected_hotkey - async def show_subnet(netuid_: int): + async def show_subnet( + netuid_: int, + mechanism_id: Optional[int], + mechanism_count: Optional[int], + ): if not await subtensor.subnet_exists(netuid=netuid): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") return False + block_hash = await subtensor.substrate.get_chain_head() ( subnet_info, - subnet_state, identities, old_identities, current_burn_cost, ) = await asyncio.gather( subtensor.subnet(netuid=netuid_, block_hash=block_hash), - subtensor.get_subnet_state(netuid=netuid_, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), subtensor.get_delegate_identities(block_hash=block_hash), subtensor.get_hyperparameter( param_name="Burn", netuid=netuid_, block_hash=block_hash ), ) - if subnet_state is None: - print_error(f"Subnet {netuid_} does not exist") + + selected_mechanism_id = mechanism_id or 0 + + metagraph_info = await subtensor.get_mechagraph_info( + netuid_, selected_mechanism_id, block_hash=block_hash + ) + + if metagraph_info is None: + print_error( + f"Subnet {netuid_} with mechanism: {selected_mechanism_id} does not exist" + ) return False if subnet_info is None: print_error(f"Subnet {netuid_} does not exist") return False - if len(subnet_state.hotkeys) == 0: + if len(metagraph_info.hotkeys) == 0: print_error(f"Subnet {netuid_} is currently empty with 0 UIDs registered.") return False # Define table properties + mechanism_label = f"Mechanism {selected_mechanism_id}" + table = Table( title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}" f"{': ' + get_subnet_name(subnet_info)}" - f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + f"\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network} • {mechanism_label}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1133,33 +1170,11 @@ async def show_subnet(netuid_: int): ) # For table footers - alpha_sum = sum( - [ - subnet_state.alpha_stake[idx].tao - for idx in range(len(subnet_state.alpha_stake)) - ] - ) - stake_sum = sum( - [ - subnet_state.total_stake[idx].tao - for idx in range(len(subnet_state.total_stake)) - ] - ) - tao_sum = sum( - [ - subnet_state.tao_stake[idx].tao * TAO_WEIGHT - for idx in range(len(subnet_state.tao_stake)) - ] - ) - dividends_sum = sum( - subnet_state.dividends[idx] for idx in range(len(subnet_state.dividends)) - ) - emission_sum = sum( - [ - subnet_state.emission[idx].tao - for idx in range(len(subnet_state.emission)) - ] - ) + alpha_sum = sum(stake.tao for stake in metagraph_info.alpha_stake) + stake_sum = sum(stake.tao for stake in metagraph_info.total_stake) + tao_sum = sum((stake * TAO_WEIGHT).tao for stake in metagraph_info.tao_stake) + dividends_sum = sum(metagraph_info.dividends) + emission_sum = sum(emission.tao for emission in metagraph_info.emission) owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner_coldkey) if subnet_info.owner_hotkey not in owner_hotkeys: @@ -1174,7 +1189,7 @@ async def show_subnet(netuid_: int): break sorted_indices = sorted( - range(len(subnet_state.hotkeys)), + range(len(metagraph_info.hotkeys)), key=lambda i: ( # If sort is True, sort only by UIDs i @@ -1183,11 +1198,11 @@ async def show_subnet(netuid_: int): # Otherwise # Sort by owner status first not ( - subnet_state.coldkeys[i] == subnet_info.owner_coldkey - or subnet_state.hotkeys[i] in owner_hotkeys + metagraph_info.coldkeys[i] == subnet_info.owner_coldkey + or metagraph_info.hotkeys[i] in owner_hotkeys ), # Then sort by stake amount (higher stakes first) - -subnet_state.total_stake[i].tao, + -metagraph_info.total_stake[i].tao, ) ), ) @@ -1196,10 +1211,10 @@ async def show_subnet(netuid_: int): json_out_rows = [] for idx in sorted_indices: # Get identity for this uid - coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get( + coldkey_identity = identities.get(metagraph_info.coldkeys[idx], {}).get( "name", "" ) - hotkey_identity = old_identities.get(subnet_state.hotkeys[idx]) + hotkey_identity = old_identities.get(metagraph_info.hotkeys[idx]) uid_identity = ( coldkey_identity if coldkey_identity @@ -1207,8 +1222,8 @@ async def show_subnet(netuid_: int): ) if ( - subnet_state.coldkeys[idx] == subnet_info.owner_coldkey - or subnet_state.hotkeys[idx] in owner_hotkeys + metagraph_info.coldkeys[idx] == subnet_info.owner_coldkey + or metagraph_info.hotkeys[idx] in owner_hotkeys ): if uid_identity == "~": uid_identity = ( @@ -1220,44 +1235,44 @@ async def show_subnet(netuid_: int): ) # Modify tao stake with TAO_WEIGHT - tao_stake = subnet_state.tao_stake[idx] * TAO_WEIGHT + tao_stake = metagraph_info.tao_stake[idx] * TAO_WEIGHT rows.append( ( str(idx), # UID - f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}" + f"{metagraph_info.total_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose - else f"{millify_tao(subnet_state.total_stake[idx])} {subnet_info.symbol}", # Stake - f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" + else f"{millify_tao(metagraph_info.total_stake[idx])} {subnet_info.symbol}", # Stake + f"{metagraph_info.alpha_stake[idx].tao:.4f} {subnet_info.symbol}" if verbose - else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake + else f"{millify_tao(metagraph_info.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake f"τ {tao_stake.tao:.4f}" if verbose else f"τ {millify_tao(tao_stake)}", # Tao Stake - f"{subnet_state.dividends[idx]:.6f}", # Dividends - f"{subnet_state.incentives[idx]:.6f}", # Incentive - f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions - f"{subnet_state.hotkeys[idx][:6]}" + f"{metagraph_info.dividends[idx]:.6f}", # Dividends + f"{metagraph_info.incentives[idx]:.6f}", # Incentive + f"{Balance.from_tao(metagraph_info.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions + f"{metagraph_info.hotkeys[idx][:6]}" if not verbose - else f"{subnet_state.hotkeys[idx]}", # Hotkey - f"{subnet_state.coldkeys[idx][:6]}" + else f"{metagraph_info.hotkeys[idx]}", # Hotkey + f"{metagraph_info.coldkeys[idx][:6]}" if not verbose - else f"{subnet_state.coldkeys[idx]}", # Coldkey + else f"{metagraph_info.coldkeys[idx]}", # Coldkey uid_identity, # Identity ) ) json_out_rows.append( { "uid": idx, - "stake": subnet_state.total_stake[idx].tao, - "alpha_stake": subnet_state.alpha_stake[idx].tao, + "stake": metagraph_info.total_stake[idx].tao, + "alpha_stake": metagraph_info.alpha_stake[idx].tao, "tao_stake": tao_stake.tao, - "dividends": subnet_state.dividends[idx], - "incentive": subnet_state.incentives[idx], - "emissions": Balance.from_tao(subnet_state.emission[idx].tao) + "dividends": metagraph_info.dividends[idx], + "incentive": metagraph_info.incentives[idx], + "emissions": Balance.from_tao(metagraph_info.emission[idx].tao) .set_unit(netuid_) .tao, - "hotkey": subnet_state.hotkeys[idx], - "coldkey": subnet_state.coldkeys[idx], + "hotkey": metagraph_info.hotkeys[idx], + "coldkey": metagraph_info.coldkeys[idx], "identity": uid_identity, } ) @@ -1353,8 +1368,16 @@ async def show_subnet(netuid_: int): if current_burn_cost else Balance(0) ) + total_mechanisms = mechanism_count if mechanism_count is not None else 1 + output_dict = { "netuid": netuid_, + "mechanism_id": selected_mechanism_id, + **( + {"mechanism_count": mechanism_count} + if mechanism_count is not None + else {} + ), "name": subnet_name_display, "owner": subnet_info.owner_coldkey, "owner_identity": owner_identity, @@ -1372,8 +1395,21 @@ async def show_subnet(netuid_: int): if json_output: json_console.print(json.dumps(output_dict)) + mech_line = ( + f"\n Mechanism ID: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]#{selected_mechanism_id}" + f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]" + if total_mechanisms > 1 + else "" + ) + total_mech_line = ( + f"\n Total mechanisms: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]" + f"{total_mechanisms}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]" + ) + console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{mech_line}" + f"{total_mech_line}" f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" @@ -1419,7 +1455,7 @@ async def show_subnet(netuid_: int): # Check if the UID exists in the subnet if uid in [int(row[0]) for row in rows]: row_data = next(row for row in rows if int(row[0]) == uid) - hotkey = subnet_state.hotkeys[uid] + hotkey = metagraph_info.hotkeys[uid] identity = "" if row_data[9] == "~" else row_data[9] identity_str = f" ({identity})" if identity else "" console.print( @@ -1439,7 +1475,7 @@ async def show_subnet(netuid_: int): result = await show_root() return result else: - result = await show_subnet(netuid) + result = await show_subnet(netuid, mechanism_id, mechanism_count) return result @@ -1486,13 +1522,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 +1624,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 +1646,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 +1716,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 +1726,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 +2254,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 +2274,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 +2292,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 +2325,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 +2483,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 +2518,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 +2556,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 e6ac31185..955f52435 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -2,6 +2,7 @@ import json from typing import TYPE_CHECKING, Union, Optional +from async_substrate_interface import AsyncExtrinsicReceipt from bittensor_wallet import Wallet from rich import box from rich.table import Column, Table @@ -27,6 +28,7 @@ string_to_u16, string_to_u64, get_hotkey_pub_ss58, + print_extrinsic_id, ) if TYPE_CHECKING: @@ -169,6 +171,84 @@ def requires_bool(metadata, param_name, pallet: str = DEFAULT_PALLET) -> bool: raise ValueError(f"{param_name} not found in pallet.") +async def set_mechanism_count_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + mech_count: int, + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: + """Sets the number of mechanisms for a subnet via AdminUtils.""" + + unlock_result = unlock_key(wallet) + if not unlock_result.success: + return False, unlock_result.message, None + + substrate = subtensor.substrate + call_params = {"netuid": netuid, "mechanism_count": mech_count} + + with console.status( + f":satellite: Setting mechanism count to [white]{mech_count}[/white] on " + f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] ...", + spinner="earth", + ): + call = await substrate.compose_call( + call_module=DEFAULT_PALLET, + call_function="sudo_set_mechanism_count", + call_params=call_params, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + return False, err_msg, None + + return True, "", ext_receipt + + +async def set_mechanism_emission_extrinsic( + subtensor: "SubtensorInterface", + wallet: "Wallet", + netuid: int, + split: list[int], + wait_for_inclusion: bool = True, + wait_for_finalization: bool = True, +) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: + """Sets the emission split for a subnet's mechanisms via AdminUtils.""" + + unlock_result = unlock_key(wallet) + if not unlock_result.success: + return False, unlock_result.message, None + + substrate = subtensor.substrate + + with console.status( + f":satellite: Setting emission split for subnet {netuid}...", + spinner="earth", + ): + call = await substrate.compose_call( + call_module=DEFAULT_PALLET, + call_function="sudo_set_mechanism_emission_split", + call_params={"netuid": netuid, "maybe_split": split}, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=wait_for_inclusion, + wait_for_finalization=wait_for_finalization, + ) + + if not success: + return False, err_msg, None + + return True, "", ext_receipt + + async def set_hyperparameter_extrinsic( subtensor: "SubtensorInterface", wallet: "Wallet", @@ -178,7 +258,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 +271,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 +288,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 +310,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 +342,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 +370,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 +592,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 +601,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 +625,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 +650,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 +668,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 +688,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 +715,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 +724,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 +1001,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,32 +1020,97 @@ 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 + return False, None - result_ = await _do_set_take() + return await _do_set_take() - return result_ + +async def trim( + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + max_n: int, + period: int, + prompt: bool, + json_output: bool, +) -> bool: + """ + Trims a subnet's UIDs to a specified amount + """ + print_verbose("Confirming subnet owner") + subnet_owner = await subtensor.query( + module="SubtensorModule", + storage_function="SubnetOwner", + params=[netuid], + ) + if subnet_owner != wallet.coldkeypub.ss58_address: + err_msg = "This wallet doesn't own the specified subnet." + if json_output: + json_console.print_json(data={"success": False, "message": err_msg}) + else: + err_console.print(f":cross_mark: [red]{err_msg}[/red]") + return False + if prompt and not json_output: + if not Confirm.ask( + f"You are about to trim UIDs on SN{netuid} to a limit of {max_n}", + default=False, + ): + err_console.print(":cross_mark: [red]User aborted.[/red]") + call = await subtensor.substrate.compose_call( + call_module="AdminUtils", + call_function="sudo_trim_to_max_allowed_uids", + call_params={"netuid": netuid, "max_n": max_n}, + ) + 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, + "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, "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]" + ) + return True diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3b13a7fca..63edd7ef3 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 @@ -811,13 +813,22 @@ async def wallet_history(wallet: Wallet): console.print(table) -async def wallet_list(wallet_path: str, json_output: bool): +async def wallet_list( + wallet_path: str, json_output: bool, wallet_name: Optional[str] = None +): """Lists wallets.""" wallets = utils.get_coldkey_wallets_for_path(wallet_path) print_verbose(f"Using wallets path: {wallet_path}") if not wallets: err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]") + if wallet_name: + wallets = [wallet for wallet in wallets if wallet.name == wallet_name] + if not wallets: + err_console.print( + f"[red]Wallet '{wallet_name}' not found in dir: {wallet_path}[/red]" + ) + root = Tree("Wallets") main_data_dict = {"wallets": []} for wallet in wallets: @@ -874,7 +885,12 @@ async def wallet_list(wallet_path: str, json_output: bool): if not wallets: print_verbose(f"No wallets found in path: {wallet_path}") - root.add("[bold red]No wallets found.") + message = ( + "[bold red]No wallets found." + if not wallet_name + else f"[bold red]Wallet '{wallet_name}' not found." + ) + root.add(message) if json_output: json_console.print(json.dumps(main_data_dict)) else: @@ -1481,7 +1497,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 +1507,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 +1703,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 +1760,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 +1785,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 +1807,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 +2052,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 +2069,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/pyproject.toml b/pyproject.toml index b7c11b579..234acba94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.11.2" +version = "9.12.0" description = "Bittensor CLI" readme = "README.md" authors = [ diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py index 3af86c140..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,5 +119,57 @@ 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( + command="sudo", + sub_command="set", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--network", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--json-out", + "--no-prompt", + "--param", + "min_allowed_uids", + "--value", + "110", + ], + ) + 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( + command="sudo", + sub_command="trim", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--network", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--max", + "120", + "--json-out", + "--no-prompt", + ], + ) + 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(