From 912f8985dffdac6c1bc5bfe28ccafd7187457106 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 7 Mar 2025 15:03:06 +0200 Subject: [PATCH 01/13] Init commit --- bittensor_cli/cli.py | 32 +++++++++++++++++++-------- bittensor_cli/src/commands/wallets.py | 28 +++++++++++++++++++---- bittensor_cli/version.py | 1 + 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 79f4d2fe..b1d7e2d3 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -14,11 +14,16 @@ import rich import typer import numpy as np +from async_substrate_interface.errors import SubstrateRequestException from bittensor_wallet import Wallet from rich import box from rich.prompt import Confirm, FloatPrompt, Prompt, IntPrompt from rich.table import Column, Table from rich.tree import Tree +from typing_extensions import Annotated +from websockets import ConnectionClosed, InvalidHandshake +from yaml import safe_dump, safe_load + from bittensor_cli.src import ( defaults, HELP_PANELS, @@ -31,7 +36,6 @@ from bittensor_cli.version import __version__, __version_as_int__ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance -from async_substrate_interface.errors import SubstrateRequestException from bittensor_cli.src.commands import sudo, wallets, view from bittensor_cli.src.commands import weights as weights_cmds from bittensor_cli.src.commands.subnets import price, subnets @@ -61,9 +65,6 @@ is_linux, validate_rate_tolerance, ) -from typing_extensions import Annotated -from websockets import ConnectionClosed, InvalidHandshake -from yaml import safe_dump, safe_load try: from git import Repo, GitError @@ -267,6 +268,12 @@ class Options: "--dashboard.path", help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.", ) + json_output = typer.Option( + False, + "--json-output", + "--json-out", + help="Outputs the result of the command as JSON.", + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -933,6 +940,7 @@ def initialize_chain( """ if not self.subtensor: if network: + network_ = None for item in network: if item.startswith("ws"): network_ = item @@ -950,7 +958,8 @@ def initialize_chain( elif self.config["network"]: self.subtensor = SubtensorInterface(self.config["network"]) console.print( - f"Using the specified network [{COLOR_PALETTE['GENERAL']['LINKS']}]{self.config['network']}[/{COLOR_PALETTE['GENERAL']['LINKS']}] from config" + f"Using the specified network [{COLOR_PALETTE['GENERAL']['LINKS']}]{self.config['network']}" + f"[/{COLOR_PALETTE['GENERAL']['LINKS']}] from config" ) else: self.subtensor = SubtensorInterface(defaults.subtensor.network) @@ -1201,7 +1210,8 @@ def set_config( elif arg == "rate_tolerance": while True: val = FloatPrompt.ask( - f"What percentage would you like to set for [red]{arg}[/red]?\nValues are percentages (e.g. 0.05 for 5%)", + f"What percentage would you like to set for [red]{arg}[/red]?\n" + f"Values are percentages (e.g. 0.05 for 5%)", default=0.05, ) try: @@ -1501,7 +1511,7 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: list[str] = [], + ask_for: list[str] = None, validate: WV = WV.WALLET, ) -> Wallet: """ @@ -1510,9 +1520,10 @@ def wallet_ask( :param wallet_path: root path of the wallets :param wallet_hotkey: name of the wallet hotkey file :param validate: flag whether to check for the wallet's validity - :param ask_type: aspect of the wallet (name, path, hotkey) to prompt the user for + :param ask_for: aspect of the wallet (name, path, hotkey) to prompt the user for :return: created Wallet object """ + ask_for = ask_for or [] # Prompt for missing attributes specified in ask_for if WO.NAME in ask_for and not wallet_name: if self.config.get("wallet_name"): @@ -1585,6 +1596,7 @@ def wallet_list( wallet_path: str = Options.wallet_path, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Displays all the wallets and their corresponding hotkeys that are located in the wallet path specified in the config. @@ -1604,7 +1616,7 @@ 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)) + return self._run_command(wallets.wallet_list(wallet.path, json_output)) def wallet_overview( self, @@ -1644,6 +1656,7 @@ def wallet_overview( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Displays a detailed overview of the user's registered accounts on the Bittensor network. @@ -1705,6 +1718,7 @@ def wallet_overview( exclude_hotkeys, netuids_filter=netuids, verbose=verbose, + json_output=json_output, ) ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 3df948ba..108242d8 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1,5 +1,6 @@ import asyncio import itertools +import json import os from collections import defaultdict from typing import Generator, Optional @@ -525,7 +526,7 @@ async def wallet_history(wallet: Wallet): console.print(table) -async def wallet_list(wallet_path: str): +async def wallet_list(wallet_path: str, json_output: bool): """Lists wallets.""" wallets = utils.get_coldkey_wallets_for_path(wallet_path) print_verbose(f"Using wallets path: {wallet_path}") @@ -533,6 +534,7 @@ async def wallet_list(wallet_path: str): err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]") root = Tree("Wallets") + d_out = {"wallets": []} for wallet in wallets: if ( wallet.coldkeypub_file.exists_on_device() @@ -545,23 +547,40 @@ async def wallet_list(wallet_path: str): wallet_tree = root.add( f"[bold blue]Coldkey[/bold blue] [green]{wallet.name}[/green] ss58_address [green]{coldkeypub_str}[/green]" ) + wallet_hotkeys = [] + wallet_d = { + "name": wallet.name, + "ss58_address": coldkeypub_str, + "hotkeys": wallet_hotkeys, + } + d_out["wallets"].append(wallet_d) hotkeys = utils.get_hotkey_wallets_for_wallet( wallet, show_nulls=True, show_encrypted=True ) for hkey in hotkeys: data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)" + hk_data = {"name": hkey.name, "ss58_address": "?"} if hkey: try: - data = f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n" + data = ( + f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] " + f"ss58_address [green]{hkey.hotkey.ss58_address}[/green]\n" + ) + hk_data["name"] = hkey.hotkey_str + hk_data["ss58_address"] = hkey.hotkey.ss58_address except UnicodeDecodeError: pass wallet_tree.add(data) + wallet_hotkeys.append(hk_data) if not wallets: print_verbose(f"No wallets found in path: {wallet_path}") root.add("[bold red]No wallets found.") - - console.print(root) + if json_output: + console.print("[JSON_OUTPUT]") + console.print(json.dumps(d_out)) + else: + console.print(root) async def _get_total_balance( @@ -638,6 +657,7 @@ async def overview( exclude_hotkeys: Optional[list[str]] = None, netuids_filter: Optional[list[int]] = None, verbose: bool = False, + json_output: bool = False, ): """Prints an overview for the wallet's coldkey.""" diff --git a/bittensor_cli/version.py b/bittensor_cli/version.py index 4ad0d510..60a542da 100644 --- a/bittensor_cli/version.py +++ b/bittensor_cli/version.py @@ -15,5 +15,6 @@ def version_as_int(version): __new_signature_version__ = 360 return __version_as_int__ + __version__ = "9.1.1" __version_as_int__ = version_as_int(__version__) From 1573a80f7ed82bc059e554a078e68ea50277ae55 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 7 Mar 2025 15:45:51 +0200 Subject: [PATCH 02/13] General improvements to wallet overview --- bittensor_cli/src/commands/wallets.py | 29 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 108242d8..8d576278 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -663,18 +663,27 @@ async def overview( total_balance = Balance(0) - # We are printing for every coldkey. - block_hash = await subtensor.substrate.get_chain_head() - all_hotkeys, total_balance = await _get_total_balance( - total_balance, subtensor, wallet, all_wallets, block_hash=block_hash - ) - _dynamic_info = await subtensor.all_subnets() - dynamic_info = {info.netuid: info for info in _dynamic_info} - with console.status( f":satellite: Synchronizing with chain [white]{subtensor.network}[/white]", spinner="aesthetic", ) as status: + # We are printing for every coldkey. + block_hash = await subtensor.substrate.get_chain_head() + ( + (all_hotkeys, total_balance), + _dynamic_info, + block, + all_netuids, + ) = await asyncio.gather( + _get_total_balance( + total_balance, subtensor, wallet, all_wallets, block_hash=block_hash + ), + subtensor.all_subnets(block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_all_subnet_netuids(block_hash=block_hash), + ) + dynamic_info = {info.netuid: info for info in _dynamic_info} + # We are printing for a select number of hotkeys from all_hotkeys. if include_hotkeys or exclude_hotkeys: all_hotkeys = _get_hotkeys(include_hotkeys, exclude_hotkeys, all_hotkeys) @@ -686,10 +695,6 @@ async def overview( # Pull neuron info for all keys. neurons: dict[str, list[NeuronInfoLite]] = {} - block, all_netuids = await asyncio.gather( - subtensor.substrate.get_block_number(None), - subtensor.get_all_subnet_netuids(), - ) netuids = await subtensor.filter_netuids_by_registered_hotkeys( all_netuids, netuids_filter, all_hotkeys, reuse_block=True From ed7a43b738ac905fbb0390aa30ca8220495f54cb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 7 Mar 2025 16:21:33 +0200 Subject: [PATCH 03/13] Wallet overview --- bittensor_cli/src/commands/wallets.py | 50 +++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 8d576278..05a2c2c5 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -729,6 +729,12 @@ async def overview( neurons = _process_neuron_results(results, neurons, netuids) # Setup outer table. grid = Table.grid(pad_edge=True) + d_grid = { + "wallet": "", + "network": subtensor.network, + "subnets": [], + "total_balance": 0.0, + } # Add title if not all_wallets: @@ -736,9 +742,11 @@ async def overview( details = f"[bright_cyan]{wallet.name}[/bright_cyan] : [bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]" grid.add_row(Align(title, vertical="middle", align="center")) grid.add_row(Align(details, vertical="middle", align="center")) + d_grid["wallet"] = f"{wallet.name}|{wallet.coldkeypub.ss58_address}" else: title = "[underline dark_orange]All Wallets:[/underline dark_orange]" grid.add_row(Align(title, vertical="middle", align="center")) + d_grid["wallet"] = "All" grid.add_row( Align( @@ -756,6 +764,14 @@ async def overview( ) for netuid, subnet_tempo in zip(netuids, tempos): table_data = [] + d_data = { + "netuid": netuid, + "tempo": subnet_tempo, + "neurons": [], + "name": "", + "symbol": "", + } + d_grid["subnets"].append(d_data) total_rank = 0.0 total_trust = 0.0 total_consensus = 0.0 @@ -810,6 +826,26 @@ async def overview( ), nn.hotkey[:10], ] + d_row = { + "coldkey": hotwallet.name, + "hotkey": hotwallet.hotkey_str, + "uid": uid, + "active": active, + "stake": stake, + "rank": rank, + "trust": trust, + "consensus": consensus, + "incentive": incentive, + "dividends": dividends, + "emission": emission, + "validator_trust": validator_trust, + "validator_permit": validator_permit, + "last_update": last_update, + "axon": int_to_ip(nn.axon_info.ip) + ":" + str(nn.axon_info.port) + if nn.axon_info.port != 0 + else None, + "hotkey_ss58": nn.hotkey, + } total_rank += rank total_trust += trust @@ -822,11 +858,16 @@ async def overview( total_neurons += 1 table_data.append(row) + d_data["neurons"].append(d_row) # Add subnet header + sn_name = get_subnet_name(dynamic_info[netuid]) + sn_symbol = dynamic_info[netuid].symbol grid.add_row( - f"Subnet: [dark_orange]{netuid}: {get_subnet_name(dynamic_info[netuid])} {dynamic_info[netuid].symbol}[/dark_orange]" + f"Subnet: [dark_orange]{netuid}: {sn_name} {sn_symbol}[/dark_orange]" ) + d_data["name"] = sn_name + d_data["symbol"] = sn_symbol width = console.width table = Table( show_footer=False, @@ -961,6 +1002,7 @@ def overview_sort_function(row_): caption = "\n[italic][dim][bright_cyan]Wallet balance: [dark_orange]\u03c4" + str( total_balance.tao ) + d_grid["total_balance"] = total_balance.tao grid.add_row(Align(caption, vertical="middle", align="center")) if console.width < 150: @@ -968,7 +1010,11 @@ def overview_sort_function(row_): "[yellow]Warning: Your terminal width might be too small to view all information clearly" ) # Print the entire table/grid - console.print(grid, width=None) + if not json_output: + console.print(grid, width=None) + else: + console.print("[JSON_OUTPUT]") + console.print(json.dumps(d_grid)) def _get_hotkeys( From ddf26e443b34d27934bf070a310ab8d51441770b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 7 Mar 2025 16:28:53 +0200 Subject: [PATCH 04/13] Var naming --- bittensor_cli/src/commands/wallets.py | 35 +++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 05a2c2c5..40c3531d 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -534,7 +534,7 @@ async def wallet_list(wallet_path: str, json_output: bool): err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]") root = Tree("Wallets") - d_out = {"wallets": []} + main_data_dict = {"wallets": []} for wallet in wallets: if ( wallet.coldkeypub_file.exists_on_device() @@ -548,12 +548,12 @@ async def wallet_list(wallet_path: str, json_output: bool): f"[bold blue]Coldkey[/bold blue] [green]{wallet.name}[/green] ss58_address [green]{coldkeypub_str}[/green]" ) wallet_hotkeys = [] - wallet_d = { + wallet_dict = { "name": wallet.name, "ss58_address": coldkeypub_str, "hotkeys": wallet_hotkeys, } - d_out["wallets"].append(wallet_d) + main_data_dict["wallets"].append(wallet_dict) hotkeys = utils.get_hotkey_wallets_for_wallet( wallet, show_nulls=True, show_encrypted=True ) @@ -578,7 +578,7 @@ async def wallet_list(wallet_path: str, json_output: bool): root.add("[bold red]No wallets found.") if json_output: console.print("[JSON_OUTPUT]") - console.print(json.dumps(d_out)) + console.print(json.dumps(main_data_dict)) else: console.print(root) @@ -729,7 +729,7 @@ async def overview( neurons = _process_neuron_results(results, neurons, netuids) # Setup outer table. grid = Table.grid(pad_edge=True) - d_grid = { + data_dict = { "wallet": "", "network": subtensor.network, "subnets": [], @@ -739,14 +739,17 @@ async def overview( # Add title if not all_wallets: title = "[underline dark_orange]Wallet[/underline dark_orange]\n" - details = f"[bright_cyan]{wallet.name}[/bright_cyan] : [bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]" + details = ( + f"[bright_cyan]{wallet.name}[/bright_cyan] : " + f"[bright_magenta]{wallet.coldkeypub.ss58_address}[/bright_magenta]" + ) grid.add_row(Align(title, vertical="middle", align="center")) grid.add_row(Align(details, vertical="middle", align="center")) - d_grid["wallet"] = f"{wallet.name}|{wallet.coldkeypub.ss58_address}" + data_dict["wallet"] = f"{wallet.name}|{wallet.coldkeypub.ss58_address}" else: title = "[underline dark_orange]All Wallets:[/underline dark_orange]" grid.add_row(Align(title, vertical="middle", align="center")) - d_grid["wallet"] = "All" + data_dict["wallet"] = "All" grid.add_row( Align( @@ -764,14 +767,14 @@ async def overview( ) for netuid, subnet_tempo in zip(netuids, tempos): table_data = [] - d_data = { + subnet_dict = { "netuid": netuid, "tempo": subnet_tempo, "neurons": [], "name": "", "symbol": "", } - d_grid["subnets"].append(d_data) + data_dict["subnets"].append(subnet_dict) total_rank = 0.0 total_trust = 0.0 total_consensus = 0.0 @@ -826,7 +829,7 @@ async def overview( ), nn.hotkey[:10], ] - d_row = { + neuron_dict = { "coldkey": hotwallet.name, "hotkey": hotwallet.hotkey_str, "uid": uid, @@ -858,7 +861,7 @@ async def overview( total_neurons += 1 table_data.append(row) - d_data["neurons"].append(d_row) + subnet_dict["neurons"].append(neuron_dict) # Add subnet header sn_name = get_subnet_name(dynamic_info[netuid]) @@ -866,8 +869,8 @@ async def overview( grid.add_row( f"Subnet: [dark_orange]{netuid}: {sn_name} {sn_symbol}[/dark_orange]" ) - d_data["name"] = sn_name - d_data["symbol"] = sn_symbol + subnet_dict["name"] = sn_name + subnet_dict["symbol"] = sn_symbol width = console.width table = Table( show_footer=False, @@ -1002,7 +1005,7 @@ def overview_sort_function(row_): caption = "\n[italic][dim][bright_cyan]Wallet balance: [dark_orange]\u03c4" + str( total_balance.tao ) - d_grid["total_balance"] = total_balance.tao + data_dict["total_balance"] = total_balance.tao grid.add_row(Align(caption, vertical="middle", align="center")) if console.width < 150: @@ -1014,7 +1017,7 @@ def overview_sort_function(row_): console.print(grid, width=None) else: console.print("[JSON_OUTPUT]") - console.print(json.dumps(d_grid)) + console.print(json.dumps(data_dict)) def _get_hotkeys( From cdf7ee45081cf29885d3493b8711c8ee90cf62ab Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 7 Mar 2025 22:17:53 +0200 Subject: [PATCH 05/13] mypy --- bittensor_cli/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b1d7e2d3..6428ba62 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1511,7 +1511,7 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: list[str] = None, + ask_for: Optional[list[str]] = None, validate: WV = WV.WALLET, ) -> Wallet: """ From 44a7482eb3f1e0977b2dc2069260ab3d02865399 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 10 Mar 2025 15:11:55 +0200 Subject: [PATCH 06/13] Change json_output to use a separate console which allows us to quiet all other consoles. json_output automatically triggers quiet output. --- bittensor_cli/cli.py | 14 +++++++++----- bittensor_cli/src/bittensor/utils.py | 1 + bittensor_cli/src/commands/wallets.py | 7 +++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6428ba62..9816fb95 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1075,12 +1075,16 @@ def main_callback( except ModuleNotFoundError: self.asyncio_runner = asyncio.run - def verbosity_handler(self, quiet: bool, verbose: bool): + def verbosity_handler( + self, quiet: bool, verbose: bool, json_output: bool = False + ) -> None: if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") raise typer.Exit() - - if quiet: + elif json_output and verbose: + err_console.print("Cannot specify both `--verbose` and `--json-output`") + raise typer.Exit() + if json_output or quiet: verbosity_console_handler(0) elif verbose: verbosity_console_handler(2) @@ -1612,7 +1616,7 @@ def wallet_list( [bold]NOTE[/bold]: This command is read-only and does not modify the filesystem or the blockchain state. It is intended for use with the Bittensor CLI to provide a quick overview of the user's wallets. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE ) @@ -1673,7 +1677,7 @@ def wallet_overview( It provides a quick and comprehensive view of the user's network presence, making it useful for monitoring account status, stake distribution, and overall contribution to the Bittensor network. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if include_hotkeys and exclude_hotkeys: utils.err_console.print( "[red]You have specified both the inclusion and exclusion options. Only one of these options is allowed currently." diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 5fbf37bf..b9456114 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -34,6 +34,7 @@ from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters console = Console() +json_console = Console() err_console = Console(stderr=True) verbose_console = Console(quiet=True) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 40c3531d..d52e6a5b 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -35,6 +35,7 @@ console, convert_blocks_to_time, err_console, + json_console, print_error, print_verbose, get_all_wallets_for_path, @@ -577,8 +578,7 @@ async def wallet_list(wallet_path: str, json_output: bool): print_verbose(f"No wallets found in path: {wallet_path}") root.add("[bold red]No wallets found.") if json_output: - console.print("[JSON_OUTPUT]") - console.print(json.dumps(main_data_dict)) + json_console.print(json.dumps(main_data_dict)) else: console.print(root) @@ -1016,8 +1016,7 @@ def overview_sort_function(row_): if not json_output: console.print(grid, width=None) else: - console.print("[JSON_OUTPUT]") - console.print(json.dumps(data_dict)) + json_console.print(json.dumps(data_dict)) def _get_hotkeys( From 28e8b41c9d4980ed4151cee2f1131a3c19074ac6 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 10 Mar 2025 16:16:23 +0200 Subject: [PATCH 07/13] More wallets commands --- bittensor_cli/cli.py | 19 ++++++++++---- bittensor_cli/src/bittensor/utils.py | 1 - bittensor_cli/src/commands/wallets.py | 38 +++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f0a7d294..e46344e1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1752,6 +1752,7 @@ def wallet_transfer( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Send TAO tokens from one wallet to another wallet on the Bittensor network. @@ -1775,7 +1776,7 @@ def wallet_transfer( print_error("You have entered an incorrect ss58 address. Please try again.") raise typer.Exit() - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -1799,6 +1800,7 @@ def wallet_transfer( amount, transfer_all, prompt, + json_output, ) ) @@ -1814,6 +1816,7 @@ def wallet_swap_hotkey( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Swap hotkeys of a given wallet on the blockchain. For a registered key pair, for example, a (coldkeyA, hotkeyA) pair, this command swaps the hotkeyA with a new, unregistered, hotkeyB to move the original registration to the (coldkeyA, hotkeyB) pair. @@ -1832,7 +1835,7 @@ def wallet_swap_hotkey( [green]$[/green] btcli wallet swap_hotkey destination_hotkey_name --wallet-name your_wallet_name --wallet-hotkey original_hotkey """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) original_wallet = self.wallet_ask( wallet_name, wallet_path, @@ -1854,7 +1857,9 @@ def wallet_swap_hotkey( ) self.initialize_chain(network) return self._run_command( - wallets.swap_hotkey(original_wallet, new_wallet, self.subtensor, prompt) + wallets.swap_hotkey( + original_wallet, new_wallet, self.subtensor, prompt, json_output + ) ) def wallet_inspect( @@ -1873,6 +1878,7 @@ def wallet_inspect( netuids: str = Options.netuids, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Displays the details of the user's wallet pairs (coldkey, hotkey) on the Bittensor network. @@ -1907,7 +1913,7 @@ def wallet_inspect( """ print_error("This command is disabled on the 'rao' network.") raise typer.Exit() - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if netuids: netuids = parse_to_list( @@ -2004,6 +2010,7 @@ def wallet_faucet( [bold]Note[/bold]: This command is meant for used in local environments where users can experiment with the blockchain without using real TAO tokens. Users must have the necessary hardware setup, especially when opting for CUDA-based GPU calculations. It is currently disabled on testnet and mainnet (finney). You can only use this command on a local blockchain. """ + # TODO should we add json_output? wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2040,6 +2047,7 @@ def wallet_regen_coldkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Regenerate a coldkey for a wallet on the Bittensor blockchain network. @@ -2057,7 +2065,7 @@ def wallet_regen_coldkey( [bold]Note[/bold]: This command is critical for users who need to regenerate their coldkey either for recovery or for security reasons. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( @@ -2085,6 +2093,7 @@ def wallet_regen_coldkey( json_password, use_password, overwrite, + json_output, ) ) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 33505a1d..c02d3202 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -5,7 +5,6 @@ import sqlite3 import platform import webbrowser -import sys from pathlib import Path from typing import TYPE_CHECKING, Any, Collection, Optional, Union, Callable from urllib.parse import urlparse diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 9d2d6dc9..fb3d6cdf 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -57,6 +57,7 @@ async def regen_coldkey( json_password: Optional[str] = "", use_password: Optional[bool] = True, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new coldkey under this wallet""" json_str: Optional[str] = None @@ -79,10 +80,34 @@ async def regen_coldkey( "\n✅ [dark_sea_green]Regenerated coldkey successfully!\n", f"[dark_sea_green]Wallet name: ({new_wallet.name}), path: ({new_wallet.path}), coldkey ss58: ({new_wallet.coldkeypub.ss58_address})", ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": new_wallet.name, + "path": new_wallet.path, + "hotkey": new_wallet.hotkey_str, + "hotkey_ss58": new_wallet.hotkey.ss58_address, + "coldkey_ss58": new_wallet.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except ValueError: print_error("Mnemonic phrase is invalid") + if json_output: + json_console.print( + '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}' + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def regen_coldkey_pub( @@ -1182,9 +1207,10 @@ async def transfer( amount: float, transfer_all: bool, prompt: bool, + json_output: bool, ): """Transfer token of amount to destination.""" - await transfer_extrinsic( + result = await transfer_extrinsic( subtensor=subtensor, wallet=wallet, destination=destination, @@ -1192,6 +1218,9 @@ async def transfer( transfer_all=transfer_all, prompt=prompt, ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result async def inspect( @@ -1200,6 +1229,7 @@ async def inspect( netuids_filter: list[int], all_wallets: bool = False, ): + # TODO add json_output when this is re-enabled and updated for dTAO def delegate_row_maker( delegates_: list[tuple[DelegateInfo, Balance]], ) -> Generator[list[str], None, None]: @@ -1375,14 +1405,18 @@ async def swap_hotkey( new_wallet: Wallet, subtensor: SubtensorInterface, prompt: bool, + json_output: bool, ): """Swap your hotkey for all registered axons on the network.""" - return await swap_hotkey_extrinsic( + result = await swap_hotkey_extrinsic( subtensor, original_wallet, new_wallet, prompt=prompt, ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def create_identity_table(title: str = None): From 1fc534aea0c9627e820617f68e50cd0ac31cb879 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 10 Mar 2025 17:49:27 +0200 Subject: [PATCH 08/13] Wallets commands klaar --- bittensor_cli/cli.py | 67 ++++++--- bittensor_cli/src/commands/wallets.py | 202 ++++++++++++++++++++++++-- 2 files changed, 233 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e46344e1..9555a799 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2107,6 +2107,7 @@ def wallet_regen_coldkey_pub( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Regenerates the public part of a coldkey (coldkeypub.txt) for a wallet. @@ -2123,7 +2124,7 @@ def wallet_regen_coldkey_pub( [bold]Note[/bold]: This command is particularly useful for users who need to regenerate their coldkeypub, perhaps due to file corruption or loss. You will need either ss58 address or public hex key from your old coldkeypub.txt for the wallet. It is a recovery-focused utility that ensures continued access to your wallet functionalities. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( @@ -2152,7 +2153,9 @@ def wallet_regen_coldkey_pub( rich.print("[red]Error: Invalid SS58 address or public key![/red]") raise typer.Exit() return self._run_command( - wallets.regen_coldkey_pub(wallet, ss58_address, public_key_hex, overwrite) + wallets.regen_coldkey_pub( + wallet, ss58_address, public_key_hex, overwrite, json_output + ) ) def wallet_regen_hotkey( @@ -2171,6 +2174,7 @@ def wallet_regen_hotkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Regenerates a hotkey for a wallet. @@ -2189,7 +2193,7 @@ def wallet_regen_hotkey( [bold]Note[/bold]: This command is essential for users who need to regenerate their hotkey, possibly for security upgrades or key recovery. It should be used with caution to avoid accidental overwriting of existing keys. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2209,6 +2213,7 @@ def wallet_regen_hotkey( json_password, use_password, overwrite, + json_output, ) ) @@ -2231,6 +2236,7 @@ def wallet_new_hotkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Create a new hotkey for a wallet. @@ -2246,7 +2252,7 @@ def wallet_new_hotkey( [italic]Note[/italic]: This command is useful to create additional hotkeys for different purposes, such as running multiple subnet miners or subnet validators or separating operational roles within the Bittensor network. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_name: wallet_name = Prompt.ask( @@ -2270,7 +2276,9 @@ def wallet_new_hotkey( if not uri: n_words = get_n_words(n_words) return self._run_command( - wallets.new_hotkey(wallet, n_words, use_password, uri, overwrite) + wallets.new_hotkey( + wallet, n_words, use_password, uri, overwrite, json_output + ) ) def wallet_new_coldkey( @@ -2289,6 +2297,7 @@ def wallet_new_coldkey( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Create a new coldkey. A coldkey is required for holding TAO balances and performing high-value transactions. @@ -2303,7 +2312,7 @@ def wallet_new_coldkey( [bold]Note[/bold]: This command is crucial for users who need to create a new coldkey for enhanced security or as part of setting up a new wallet. It is a foundational step in establishing a secure presence on the Bittensor network. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( @@ -2326,7 +2335,9 @@ def wallet_new_coldkey( if not uri: n_words = get_n_words(n_words) return self._run_command( - wallets.new_coldkey(wallet, n_words, use_password, uri, overwrite) + wallets.new_coldkey( + wallet, n_words, use_password, uri, overwrite, json_output + ) ) def wallet_check_ck_swap( @@ -2349,6 +2360,7 @@ def wallet_check_ck_swap( [green]$[/green] btcli wallet check_coldkey_swap """ + # TODO add json_output if this ever gets used again (doubtful) self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) self.initialize_chain(network) @@ -2365,6 +2377,7 @@ def wallet_create_wallet( overwrite: bool = Options.overwrite, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Create a complete wallet by setting up both coldkey and hotkeys. @@ -2379,6 +2392,7 @@ def wallet_create_wallet( [bold]Note[/bold]: This command is for new users setting up their wallet for the first time, or for those who wish to completely renew their wallet keys. It ensures a fresh start with new keys for secure and effective participation in the Bittensor network. """ + self.verbosity_handler(quiet, verbose, json_output) if not wallet_path: wallet_path = Prompt.ask( "Enter the path of wallets directory", default=defaults.wallet.path @@ -2395,7 +2409,6 @@ def wallet_create_wallet( default=defaults.wallet.hotkey, ) - self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2406,7 +2419,9 @@ def wallet_create_wallet( if not uri: n_words = get_n_words(n_words) return self._run_command( - wallets.wallet_create(wallet, n_words, use_password, uri, overwrite) + wallets.wallet_create( + wallet, n_words, use_password, uri, overwrite, json_output + ) ) def wallet_balance( @@ -2424,6 +2439,7 @@ def wallet_balance( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Check the balance of the wallet. This command shows a detailed view of the wallet's coldkey balances, including free and staked balances. @@ -2449,7 +2465,7 @@ def wallet_balance( [green]$[/green] btcli w balance --ss58 --ss58 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = None if all_balances: ask_for = [WO.PATH] @@ -2512,7 +2528,9 @@ def wallet_balance( ) subtensor = self.initialize_chain(network) return self._run_command( - wallets.wallet_balance(wallet, subtensor, all_balances, ss58_addresses) + wallets.wallet_balance( + wallet, subtensor, all_balances, ss58_addresses, json_output + ) ) def wallet_history( @@ -2536,6 +2554,7 @@ def wallet_history( """ # TODO: Fetch effective network and redirect users accordingly - this only works on finney + # TODO: Add json_output if this gets re-enabled # no_use_config_str = "Using the network [dark_orange]finney[/dark_orange] and ignoring network/chain configs" # if self.config.get("network"): @@ -2602,6 +2621,7 @@ def wallet_set_id( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Create or update the on-chain identity of a coldkey or a hotkey on the Bittensor network. [bold]Incurs a 1 TAO transaction fee.[/bold] @@ -2620,7 +2640,7 @@ def wallet_set_id( [bold]Note[/bold]: This command should only be used if the user is willing to incur the a recycle fee associated with setting an identity on the blockchain. It is a high-level command that makes changes to the blockchain state and should not be used programmatically as part of other scripts or applications. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -2669,6 +2689,7 @@ def wallet_set_id( identity["additional"], identity["github_repo"], prompt, + json_output, ) ) @@ -2690,6 +2711,7 @@ def wallet_get_id( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the identity details of a user's coldkey or hotkey. @@ -2708,7 +2730,7 @@ def wallet_get_id( [bold]Note[/bold]: This command is primarily used for informational purposes and has no side effects on the blockchain network state. """ - wallet = None + self.verbosity_handler(quiet, verbose, json_output) if not wallet_name: if coldkey_ss58: if not is_valid_ss58_address(coldkey_ss58): @@ -2733,9 +2755,8 @@ def wallet_get_id( ) coldkey_ss58 = wallet.coldkeypub.ss58_address - self.verbosity_handler(quiet, verbose) return self._run_command( - wallets.get_id(self.initialize_chain(network), coldkey_ss58) + wallets.get_id(self.initialize_chain(network), coldkey_ss58, json_output) ) def wallet_sign( @@ -2751,6 +2772,7 @@ def wallet_sign( message: str = typer.Option("", help="The message to encode and sign"), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Allows users to sign a message with the provided wallet or wallet hotkey. Use this command to easily prove your ownership of a coldkey or a hotkey. @@ -2766,12 +2788,17 @@ def wallet_sign( [green]$[/green] btcli wallet sign --wallet-name default --wallet-hotkey hotkey --message '{"something": "here", "timestamp": 1719908486}' """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if use_hotkey is None: use_hotkey = Confirm.ask( - f"Would you like to sign the transaction using your [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]?" - f"\n[Type [{COLOR_PALETTE['GENERAL']['HOTKEY']}]y[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] for [{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" - f" and [{COLOR_PALETTE['GENERAL']['COLDKEY']}]n[/{COLOR_PALETTE['GENERAL']['COLDKEY']}] for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]] (default is [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}])", + f"Would you like to sign the transaction using your " + f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]?" + f"\n[Type [{COLOR_PALETTE['GENERAL']['HOTKEY']}]y" + f"[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] for " + f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]hotkey[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]" + f" and [{COLOR_PALETTE['GENERAL']['COLDKEY']}]n[/{COLOR_PALETTE['GENERAL']['COLDKEY']}] for " + f"[{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]] " + f"(default is [{COLOR_PALETTE['GENERAL']['COLDKEY']}]coldkey[/{COLOR_PALETTE['GENERAL']['COLDKEY']}])", default=False, ) @@ -2784,7 +2811,7 @@ def wallet_sign( if not message: message = Prompt.ask("Enter the [blue]message[/blue] to encode and sign") - return self._run_command(wallets.sign(wallet, message, use_hotkey)) + return self._run_command(wallets.sign(wallet, message, use_hotkey, json_output)) def stake_list( self, diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index fb3d6cdf..5835959a 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -115,6 +115,7 @@ async def regen_coldkey_pub( ss58_address: str, public_key_hex: str, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new coldkeypub under this wallet.""" try: @@ -126,10 +127,31 @@ async def regen_coldkey_pub( if isinstance(new_coldkeypub, Wallet): console.print( "\n✅ [dark_sea_green]Regenerated coldkeypub successfully!\n", - f"[dark_sea_green]Wallet name: ({new_coldkeypub.name}), path: ({new_coldkeypub.path}), coldkey ss58: ({new_coldkeypub.coldkeypub.ss58_address})", + f"[dark_sea_green]Wallet name: ({new_coldkeypub.name}), path: ({new_coldkeypub.path}), " + f"coldkey ss58: ({new_coldkeypub.coldkeypub.ss58_address})", ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": new_coldkeypub.name, + "path": new_coldkeypub.path, + "hotkey": new_coldkeypub.hotkey_str, + "hotkey_ss58": new_coldkeypub.hotkey.ss58_address, + "coldkey_ss58": new_coldkeypub.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def regen_hotkey( @@ -140,6 +162,7 @@ async def regen_hotkey( json_password: Optional[str] = "", use_password: Optional[bool] = False, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new hotkey under this wallet.""" json_str: Optional[str] = None @@ -151,22 +174,47 @@ async def regen_hotkey( json_str = f.read() try: - new_hotkey = wallet.regenerate_hotkey( + new_hotkey_ = wallet.regenerate_hotkey( mnemonic=mnemonic, seed=seed, json=(json_str, json_password) if all([json_str, json_password]) else None, use_password=use_password, overwrite=overwrite, ) - if isinstance(new_hotkey, Wallet): + if isinstance(new_hotkey_, Wallet): console.print( "\n✅ [dark_sea_green]Regenerated hotkey successfully!\n", - f"[dark_sea_green]Wallet name: ({new_hotkey.name}), path: ({new_hotkey.path}), hotkey ss58: ({new_hotkey.hotkey.ss58_address})", + f"[dark_sea_green]Wallet name: ({new_hotkey_.name}), path: ({new_hotkey_.path}), " + f"hotkey ss58: ({new_hotkey_.hotkey.ss58_address})", ) + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": new_hotkey_.name, + "path": new_hotkey_.path, + "hotkey": new_hotkey_.hotkey_str, + "hotkey_ss58": new_hotkey_.hotkey.ss58_address, + "coldkey_ss58": new_hotkey_.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except ValueError: print_error("Mnemonic phrase is invalid") + if json_output: + json_console.print( + '{"success": false, "error": "Mnemonic phrase is invalid", "data": null}' + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def new_hotkey( @@ -175,6 +223,7 @@ async def new_hotkey( use_password: bool, uri: Optional[str] = None, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new hotkey under this wallet.""" try: @@ -183,6 +232,7 @@ async def new_hotkey( keypair = Keypair.create_from_uri(uri) except Exception as e: print_error(f"Failed to create keypair from URI {uri}: {str(e)}") + return wallet.set_hotkey(keypair=keypair, encrypt=use_password) console.print( f"[dark_sea_green]Hotkey created from URI: {uri}[/dark_sea_green]" @@ -194,8 +244,28 @@ async def new_hotkey( overwrite=overwrite, ) console.print("[dark_sea_green]Hotkey created[/dark_sea_green]") + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def new_coldkey( @@ -204,6 +274,7 @@ async def new_coldkey( use_password: bool, uri: Optional[str] = None, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new coldkey under this wallet.""" try: @@ -224,8 +295,28 @@ async def new_coldkey( overwrite=overwrite, ) console.print("[dark_sea_green]Coldkey created[/dark_sea_green]") + if json_output: + json_console.print( + json.dumps( + { + "success": True, + "data": { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + }, + "error": "", + } + ) + ) except KeyFileError: print_error("KeyFileError: File is not writable") + if json_output: + json_console.print( + '{"success": false, "error": "Keyfile is not writable", "data": null}' + ) async def wallet_create( @@ -234,16 +325,28 @@ async def wallet_create( use_password: bool = True, uri: Optional[str] = None, overwrite: Optional[bool] = False, + json_output: bool = False, ): """Creates a new wallet.""" + output_dict = {"success": False, "error": "", "data": None} if uri: try: keypair = Keypair.create_from_uri(uri) wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=False) wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=False) wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=False) + output_dict["success"] = True + output_dict["data"] = { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + } except Exception as e: - print_error(f"Failed to create keypair from URI: {str(e)}") + err = f"Failed to create keypair from URI: {str(e)}" + print_error(err) + output_dict["error"] = err console.print( f"[dark_sea_green]Wallet created from URI: {uri}[/dark_sea_green]" ) @@ -255,9 +358,18 @@ async def wallet_create( overwrite=overwrite, ) console.print("[dark_sea_green]Coldkey created[/dark_sea_green]") + output_dict["success"] = True + output_dict["data"] = { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + } except KeyFileError: - print_error("KeyFileError: File is not writable") - + err = "KeyFileError: File is not writable" + print_error(err) + output_dict["error"] = err try: wallet.create_new_hotkey( n_words=n_words, @@ -265,8 +377,20 @@ async def wallet_create( overwrite=overwrite, ) console.print("[dark_sea_green]Hotkey created[/dark_sea_green]") + output_dict["success"] = True + output_dict["data"] = { + "name": wallet.name, + "path": wallet.path, + "hotkey": wallet.hotkey_str, + "hotkey_ss58": wallet.hotkey.ss58_address, + "coldkey_ss58": wallet.coldkeypub.ss58_address, + } except KeyFileError: - print_error("KeyFileError: File is not writable") + err = "KeyFileError: File is not writable" + print_error(err) + output_dict["error"] = err + if json_output: + json_console.print(json.dumps(output_dict)) def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: @@ -306,6 +430,7 @@ async def wallet_balance( subtensor: SubtensorInterface, all_balances: bool, ss58_addresses: Optional[str] = None, + json_output: bool = False, ): """Retrieves the current balance of the specified wallet""" if ss58_addresses: @@ -421,6 +546,31 @@ async def wallet_balance( ) console.print(Padding(table, (0, 0, 0, 4))) await subtensor.substrate.close() + if json_output: + output_balances = { + key: { + "coldkey": value[0], + "free": value[1].tao, + "staked": value[2].tao, + "staked_with_slippage": value[3].tao, + "total": (value[1] + value[2]).tao, + "total_with_slippage": (value[1] + value[3]).tao, + } + for (key, value) in balances.items() + } + output_dict = { + "balances": output_balances, + "totals": { + "free": total_free_balance.tao, + "staked": total_staked_balance.tao, + "staked_with_slippage": total_staked_with_slippage.tao, + "total": (total_free_balance + total_staked_balance).tao, + "total_with_slippage": ( + total_free_balance + total_staked_with_slippage + ).tao, + }, + } + json_console.print(json.dumps(output_dict)) return total_free_balance @@ -1455,9 +1605,10 @@ async def set_id( additional: str, github_repo: str, prompt: bool, + json_output: bool = False, ): """Create a new or update existing identity on-chain.""" - + output_dict = {"success": False, "identity": None, "error": ""} identity_data = { "name": name.encode(), "url": web_url.encode(), @@ -1484,20 +1635,31 @@ async def set_id( 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 - - console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") - identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) + else: + console.print(":white_heavy_check_mark: [dark_sea_green3]Success!") + output_dict["success"] = True + identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) table = create_identity_table(title="New on-chain Identity") table.add_row("Address", wallet.coldkeypub.ss58_address) for key, value in identity.items(): table.add_row(key, str(value) if value else "~") - - return console.print(table) + output_dict["identity"] = identity + console.print(table) + if json_output: + json_console.print(json.dumps(output_dict)) -async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = None): +async def get_id( + subtensor: SubtensorInterface, + ss58_address: str, + title: str = None, + json_output: bool = False, +): with console.status( ":satellite: [bold green]Querying chain identity...", spinner="earth" ): @@ -1509,6 +1671,8 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = f" for [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" f" on {subtensor}" ) + if json_output: + json_console.print("{}") return {} table = create_identity_table(title) @@ -1517,6 +1681,8 @@ async def get_id(subtensor: SubtensorInterface, ss58_address: str, title: str = table.add_row(key, str(value) if value else "~") console.print(table) + if json_output: + json_console.print(json.dumps(identity)) return identity @@ -1557,7 +1723,9 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): ) -async def sign(wallet: Wallet, message: str, use_hotkey: str): +async def sign( + wallet: Wallet, message: str, use_hotkey: str, json_output: bool = False +): """Sign a message using the provided wallet or hotkey.""" if not use_hotkey: @@ -1577,4 +1745,6 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str): signed_message = keypair.sign(message.encode("utf-8")).hex() console.print("[dark_sea_green3]Message signed successfully:") + if json_output: + json_console.print(json.dumps({"signed_message": signed_message})) console.print(signed_message) From 7dd4fe5d65336aa719e7f1aea36ca4c3396961df Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 10 Mar 2025 22:08:24 +0200 Subject: [PATCH 09/13] Stake List --- bittensor_cli/cli.py | 25 +++++--- bittensor_cli/src/commands/stake/list.py | 75 +++++++++++++++++------- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 9555a799..04a04376 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -52,6 +52,7 @@ console, err_console, verbose_console, + json_console, is_valid_ss58_address, print_error, validate_chain_endpoint, @@ -319,22 +320,31 @@ def verbosity_console_handler(verbosity_level: int = 1) -> None: :param verbosity_level: int corresponding to verbosity level of console output (0 is quiet, 1 is normal, 2 is verbose) """ - if verbosity_level not in range(3): + if verbosity_level not in range(4): raise ValueError( - f"Invalid verbosity level: {verbosity_level}. Must be one of: 0 (quiet), 1 (normal), 2 (verbose)" + f"Invalid verbosity level: {verbosity_level}. " + f"Must be one of: 0 (quiet + json output), 1 (normal), 2 (verbose), 3 (json output + verbose)" ) if verbosity_level == 0: console.quiet = True err_console.quiet = True verbose_console.quiet = True + json_console.quiet = False elif verbosity_level == 1: console.quiet = False err_console.quiet = False verbose_console.quiet = True + json_console.quiet = True elif verbosity_level == 2: console.quiet = False err_console.quiet = False verbose_console.quiet = False + json_console.quiet = True + elif verbosity_level == 3: + console.quiet = True + err_console.quiet = True + verbose_console.quiet = False + json_console.quiet = False def get_optional_netuid(netuid: Optional[int], all_netuids: bool) -> Optional[int]: @@ -1081,10 +1091,9 @@ def verbosity_handler( if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") raise typer.Exit() - elif json_output and verbose: - err_console.print("Cannot specify both `--verbose` and `--json-output`") - raise typer.Exit() - if json_output or quiet: + if json_console and verbose: + verbosity_console_handler(3) + elif json_output or quiet: verbosity_console_handler(0) elif verbose: verbosity_console_handler(2) @@ -2831,6 +2840,7 @@ def stake_list( quiet: bool = Options.quiet, verbose: bool = Options.verbose, no_prompt: bool = Options.prompt, + json_output: bool = Options.json_output, # TODO add: all-wallets, reuse_last, html_output ): """ @@ -2852,7 +2862,7 @@ def stake_list( 4. Verbose output with full values: [green]$[/green] btcli stake list --wallet.name my_wallet --verbose """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = None if coldkey_ss58: @@ -2883,6 +2893,7 @@ def stake_list( live, verbose, no_prompt, + json_output, ) ) diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index d5b5493b..015068af 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -1,5 +1,6 @@ import asyncio - +import json +from collections import defaultdict from typing import TYPE_CHECKING, Optional from bittensor_wallet import Wallet @@ -18,6 +19,7 @@ print_error, millify_tao, get_subnet_name, + json_console, ) if TYPE_CHECKING: @@ -31,7 +33,9 @@ async def stake_list( live: bool = False, verbose: bool = False, prompt: bool = False, + json_output: bool = False, ): + # TODO fix all the name shadowing in this function coldkey_address = coldkey_ss58 if coldkey_ss58 else wallet.coldkeypub.ss58_address async def get_stake_data(block_hash: str = None): @@ -153,6 +157,7 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): reverse=True, ) sorted_substakes = root_stakes + other_stakes + substakes_values = [] for substake_ in sorted_substakes: netuid = substake_.netuid pool = dynamic_info[netuid] @@ -196,7 +201,8 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): if not verbose else f"{substake_.stake.tao:,.4f}" ) - subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {get_subnet_name(dynamic_info[netuid])}" + subnet_name = get_subnet_name(dynamic_info[netuid]) + subnet_name_cell = f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}] {subnet_name}" rows.append( [ @@ -221,11 +227,26 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): str(Balance.from_tao(per_block_tao_emission)), ] ) + substakes_values.append( + { + "netuid": netuid, + "subnet_name": subnet_name, + "value": tao_value.tao, + "stake_value": stake_value, + "rate": pool.price.tao, + "swap_value": swap_value, + "registered": True if substake_.is_registered else False, + "emission": { + "alpha": per_block_emission, + "tao": per_block_tao_emission, + }, + } + ) table = define_table(name, rows, total_tao_value, total_swapped_tao_value) for row in rows: table.add_row(*row) console.print(table) - return total_tao_value, total_swapped_tao_value + return total_tao_value, total_swapped_tao_value, substakes_values def create_live_table( substakes: list, @@ -408,22 +429,23 @@ def format_cell( # Main execution block_hash = await subtensor.substrate.get_chain_head() ( - sub_stakes, - registered_delegate_info, - dynamic_info, - ) = await get_stake_data(block_hash) - balance = await subtensor.get_balance(coldkey_address) + ( + sub_stakes, + registered_delegate_info, + dynamic_info, + ), + balance, + ) = await asyncio.gather( + get_stake_data(block_hash), + subtensor.get_balance(coldkey_address, block_hash=block_hash), + ) # Iterate over substakes and aggregate them by hotkey. - hotkeys_to_substakes: dict[str, list[StakeInfo]] = {} + hotkeys_to_substakes: dict[str, list[StakeInfo]] = defaultdict(list) for substake in sub_stakes: - hotkey = substake.hotkey_ss58 - if substake.stake.rao == 0: - continue - if hotkey not in hotkeys_to_substakes: - hotkeys_to_substakes[hotkey] = [] - hotkeys_to_substakes[hotkey].append(substake) + if substake.stake.rao != 0: + hotkeys_to_substakes[substake.hotkey_ss58].append(substake) if not hotkeys_to_substakes: print_error(f"No stakes found for coldkey ss58: ({coldkey_address})") @@ -536,15 +558,24 @@ def format_cell( num_hotkeys = len(hotkeys_to_substakes) all_hks_swapped_tao_value = Balance(0) all_hks_tao_value = Balance(0) - for hotkey in hotkeys_to_substakes.keys(): + dict_output = { + "stake_info": {}, + "coldkey_address": coldkey_address, + "network": subtensor.network, + "free_balance": 0.0, + "total_tao_value": 0.0, + "total_swapped_tao_value": 0.0, + } + for hotkey, substakes in hotkeys_to_substakes.items(): counter += 1 - tao_value, swapped_tao_value = create_table( - hotkey, hotkeys_to_substakes[hotkey] + tao_value, swapped_tao_value, substake_values_ = create_table( + hotkey, substakes ) + dict_output["stake_info"][hotkey] = substake_values_ all_hks_tao_value += tao_value all_hks_swapped_tao_value += swapped_tao_value - if num_hotkeys > 1 and counter < num_hotkeys and prompt: + if num_hotkeys > 1 and counter < num_hotkeys and prompt and not json_output: console.print("\nPress Enter to continue to the next hotkey...") input() @@ -558,7 +589,6 @@ def format_cell( if not verbose else all_hks_swapped_tao_value ) - console.print("\n\n") console.print( f"Wallet:\n" @@ -567,6 +597,11 @@ def format_cell( f" Total TAO Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\n" f" Total TAO Swapped Value ({Balance.unit}): [{COLOR_PALETTE['GENERAL']['BALANCE']}]{total_swapped_tao_value}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]" ) + dict_output["free_balance"] = balance.tao + dict_output["total_tao_value"] = all_hks_tao_value.tao + dict_output["total_swapped_tao_value"] = all_hks_swapped_tao_value.tao + if json_output: + json_console.print(json.dumps(dict_output)) if not sub_stakes: console.print( f"\n[blue]No stakes found for coldkey ss58: ({coldkey_address})" From d64edf26a4bba9030836550627b8df682c183da3 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 11 Mar 2025 18:22:56 +0200 Subject: [PATCH 10/13] stake add --- bittensor_cli/cli.py | 4 +- bittensor_cli/src/commands/stake/add.py | 225 +++++++++++++----------- 2 files changed, 129 insertions(+), 100 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 04a04376..6c2aba56 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2940,6 +2940,7 @@ def stake_add( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Stake TAO to one or more hotkeys on specific netuids with your coldkey. @@ -2972,7 +2973,7 @@ def stake_add( • [blue]--partial[/blue]: Complete partial stake if rates exceed tolerance """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: rate_tolerance = self.ask_rate_tolerance(rate_tolerance) @@ -3130,6 +3131,7 @@ def stake_add( safe_staking, rate_tolerance, allow_partial_stake, + json_output, ) ) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 6932a229..dcfa6652 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -1,4 +1,6 @@ import asyncio +import json +from collections import defaultdict from functools import partial from typing import TYPE_CHECKING, Optional @@ -17,6 +19,7 @@ print_error, print_verbose, unlock_key, + json_console, ) from bittensor_wallet import Wallet @@ -38,6 +41,7 @@ async def stake_add( safe_staking: bool, rate_tolerance: float, allow_partial_stake: bool, + json_output: bool, ): """ Args: @@ -55,11 +59,13 @@ async def stake_add( safe_staking: whether to use safe staking rate_tolerance: rate tolerance percentage for stake operations allow_partial_stake: whether to allow partial stake + json_output: whether to output stake info in JSON format Returns: bool: True if stake operation is successful, False otherwise """ + # TODO name shadowing async def safe_stake_extrinsic( netuid: int, amount: Balance, @@ -69,25 +75,25 @@ async def safe_stake_extrinsic( wallet: Wallet, subtensor: "SubtensorInterface", status=None, - ) -> None: + ) -> bool: err_out = partial(print_error, status=status) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid}" ) - current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.coldkeypub.ss58_address - ) - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake_limit", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_staked": amount.rao, - "limit_price": price_limit, - "allow_partial": allow_partial_stake, - }, + current_balance, next_nonce, call = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake_limit", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_staked": amount.rao, + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + }, + ), ) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, nonce=next_nonce @@ -104,71 +110,78 @@ async def safe_stake_extrinsic( f"Either increase price tolerance or enable partial staking.", status=status, ) - return + return False else: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return + return False + if not await response.is_success: + err_out( + f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" + ) + return False else: - await response.process_events() - if not await response.is_success: - err_out( - f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" - ) - else: - block_hash = await subtensor.substrate.get_chain_head() - new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), - subtensor.get_stake( - hotkey_ss58=hotkey_ss58, - coldkey_ss58=wallet.coldkeypub.ss58_address, - netuid=netuid, - block_hash=block_hash, - ), - ) - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid}[/dark_sea_green3]" - ) - console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" - ) - - amount_staked = current_balance - new_balance - if allow_partial_stake and (amount_staked != amount): - console.print( - "Partial stake transaction. Staked:\n" - f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " - f"instead of " - f"[blue]{amount}[/blue]" - ) + if json_output: + # the rest of this checking is not necessary if using json_output + return True + block_hash = await subtensor.substrate.get_chain_head() + new_balance, new_stake = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), + subtensor.get_stake( + hotkey_ss58=hotkey_ss58, + coldkey_ss58=wallet.coldkeypub.ss58_address, + netuid=netuid, + block_hash=block_hash, + ), + ) + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Finalized. " + f"Stake added to netuid: {netuid}[/dark_sea_green3]" + ) + console.print( + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + ) + amount_staked = current_balance - new_balance + if allow_partial_stake and (amount_staked != amount): console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " - f"Stake:\n" - f" [blue]{current_stake}[/blue] " - f":arrow_right: " - f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + "Partial stake transaction. Staked:\n" + f" [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_staked}" + f"[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] " + f"instead of " + f"[blue]{amount}[/blue]" ) + console.print( + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Stake:\n" + f" [blue]{current_stake}[/blue] " + f":arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" + ) + return True + async def stake_extrinsic( netuid_i, amount_, current, staking_address_ss58, status=None - ): + ) -> bool: err_out = partial(print_error, status=status) - current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) + current_balance, next_nonce, call = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={ + "hotkey": staking_address_ss58, + "netuid": netuid_i, + "amount_staked": amount_.rao, + }, + ), + ) failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - next_nonce = await subtensor.substrate.get_account_next_index( - wallet.coldkeypub.ss58_address - ) - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="add_stake", - call_params={ - "hotkey": staking_address_ss58, - "netuid": netuid_i, - "amount_staked": amount_.rao, - }, - ) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey, nonce=next_nonce ) @@ -178,35 +191,46 @@ async def stake_extrinsic( ) except SubstrateRequestException as e: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return + return False else: - await response.process_events() if not await response.is_success: err_out( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) + return False else: + if json_output: + # the rest of this is not necessary if using json_output + return True + new_block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( - subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.get_balance( + wallet.coldkeypub.ss58_address, block_hash=new_block_hash + ), subtensor.get_stake( hotkey_ss58=staking_address_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, netuid=netuid_i, + block_hash=new_block_hash, ), ) console.print( - f":white_heavy_check_mark: [dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" + f":white_heavy_check_mark: " + f"[dark_sea_green3]Finalized. Stake added to netuid: {netuid_i}[/dark_sea_green3]" ) console.print( - f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" + f"Balance:\n [blue]{current_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}" ) console.print( - f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " + f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" + f"{netuid_i}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] " f"Stake:\n" f" [blue]{current}[/blue] " f":arrow_right: " f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n" ) + return True netuids = ( [int(netuid)] @@ -322,7 +346,9 @@ async def stake_extrinsic( base_row.extend( [ f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", - f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # safe staking + f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]" + # safe staking + f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", ] ) @@ -341,7 +367,7 @@ async def stake_extrinsic( return False if safe_staking: - stake_coroutines = [] + stake_coroutines = {} for i, (ni, am, curr, price_with_tolerance) in enumerate( zip( netuids, amounts_to_stake, current_stake_balances, prices_with_tolerance @@ -350,29 +376,25 @@ async def stake_extrinsic( for _, staking_address in hotkeys_to_stake_to: # Regular extrinsic for root subnet if ni == 0: - stake_coroutines.append( - stake_extrinsic( - netuid_i=ni, - amount_=am, - current=curr, - staking_address_ss58=staking_address, - ) + stake_coroutines[(ni, staking_address)] = stake_extrinsic( + netuid_i=ni, + amount_=am, + current=curr, + staking_address_ss58=staking_address, ) else: - stake_coroutines.append( - safe_stake_extrinsic( - netuid=ni, - amount=am, - current_stake=curr, - hotkey_ss58=staking_address, - price_limit=price_with_tolerance, - wallet=wallet, - subtensor=subtensor, - ) + stake_coroutines[(ni, staking_address)] = safe_stake_extrinsic( + netuid=ni, + amount=am, + current_stake=curr, + hotkey_ss58=staking_address, + price_limit=price_with_tolerance, + wallet=wallet, + subtensor=subtensor, ) else: - stake_coroutines = [ - stake_extrinsic( + stake_coroutines = { + (ni, staking_address): stake_extrinsic( netuid_i=ni, amount_=am, current=curr, @@ -382,12 +404,15 @@ async def stake_extrinsic( zip(netuids, amounts_to_stake, current_stake_balances) ) for _, staking_address in hotkeys_to_stake_to - ] - + } + successes = 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 coroutine in stake_coroutines: - await coroutine + for (ni, staking_address), coroutine in stake_coroutines.items(): + success = await coroutine + successes[ni][staking_address] = success + if json_output: + json_console.print(json.dumps({"staking_success": successes})) # Helper functions @@ -590,7 +615,9 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b console.print(base_description + (safe_staking_description if safe_staking else "")) -def _calculate_slippage(subnet_info, amount: Balance) -> tuple[Balance, str, float]: +def _calculate_slippage( + subnet_info, amount: Balance +) -> tuple[Balance, str, float, str]: """Calculate slippage when adding stake. Args: From 3349cf6026f31aa5930c4d4486d2dd331dbbeefe Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 11 Mar 2025 19:16:33 +0200 Subject: [PATCH 11/13] stake remove --- bittensor_cli/cli.py | 8 +- bittensor_cli/src/commands/stake/remove.py | 165 +++++++++++---------- 2 files changed, 96 insertions(+), 77 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6c2aba56..2c1aa5f4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3192,6 +3192,7 @@ def stake_remove( ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Unstake TAO from one or more hotkeys and transfer them back to the user's coldkey wallet. @@ -3223,7 +3224,7 @@ def stake_remove( • [blue]--tolerance[/blue]: Max allowed rate change (0.05 = 5%) • [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not unstake_all and not unstake_all_alpha: safe_staking = self.ask_safe_staking(safe_staking) if safe_staking: @@ -3235,7 +3236,8 @@ def stake_remove( [hotkey_ss58_address, include_hotkeys, exclude_hotkeys, all_hotkeys] ): print_error( - "Interactive mode cannot be used with hotkey selection options like --include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." + "Interactive mode cannot be used with hotkey selection options like " + "--include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." ) raise typer.Exit() @@ -3372,6 +3374,7 @@ def stake_remove( include_hotkeys=include_hotkeys, exclude_hotkeys=exclude_hotkeys, prompt=prompt, + json_output=json_output, ) ) elif ( @@ -3426,6 +3429,7 @@ def stake_remove( safe_staking=safe_staking, rate_tolerance=rate_tolerance, allow_partial_stake=allow_partial_stake, + json_output=json_output, ) ) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 1097faf7..d5ccfcaf 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -1,4 +1,5 @@ import asyncio +import json from functools import partial from typing import TYPE_CHECKING, Optional @@ -20,6 +21,7 @@ format_error_message, group_subnets, unlock_key, + json_console, ) if TYPE_CHECKING: @@ -41,6 +43,7 @@ async def unstake( safe_staking: bool, rate_tolerance: float, allow_partial_stake: bool, + json_output: bool, ): """Unstake from hotkey(s).""" unstake_all_from_hk = False @@ -241,8 +244,11 @@ async def unstake( base_unstake_op["price_with_tolerance"] = price_with_tolerance base_table_row.extend( [ - f"{rate_with_tolerance:.4f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", # Rate with tolerance - f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", # Partial unstake + # Rate with tolerance + f"{rate_with_tolerance:.4f} {Balance.get_unit(0)}/{Balance.get_unit(netuid)}", + # Partial unstake + f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]" + f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]", ] ) @@ -273,45 +279,45 @@ async def unstake( if not unlock_key(wallet).success: return False + successes = [] with console.status("\n:satellite: Performing unstaking operations...") as status: - if safe_staking: - for op in unstake_operations: - if op["netuid"] == 0: - await _unstake_extrinsic( - wallet=wallet, - subtensor=subtensor, - netuid=op["netuid"], - amount=op["amount_to_unstake"], - current_stake=op["current_stake_balance"], - hotkey_ss58=op["hotkey_ss58"], - status=status, - ) - else: - await _safe_unstake_extrinsic( - wallet=wallet, - subtensor=subtensor, - netuid=op["netuid"], - amount=op["amount_to_unstake"], - current_stake=op["current_stake_balance"], - hotkey_ss58=op["hotkey_ss58"], - price_limit=op["price_with_tolerance"], - allow_partial_stake=allow_partial_stake, - status=status, - ) - else: - for op in unstake_operations: - await _unstake_extrinsic( - wallet=wallet, - subtensor=subtensor, - netuid=op["netuid"], - amount=op["amount_to_unstake"], - current_stake=op["current_stake_balance"], - hotkey_ss58=op["hotkey_ss58"], - status=status, - ) + for op in unstake_operations: + common_args = { + "wallet": wallet, + "subtensor": subtensor, + "netuid": op["netuid"], + "amount": op["amount_to_unstake"], + "current_stake": op["current_stake_balance"], + "hotkey_ss58": op["hotkey_ss58"], + "status": status, + } + + if safe_staking and op["netuid"] != 0: + func = _safe_unstake_extrinsic + specific_args = { + "price_limit": op["price_with_tolerance"], + "allow_partial_stake": allow_partial_stake, + } + else: + func = _unstake_extrinsic + specific_args = {} + + suc = await func(**common_args, **specific_args) + + successes.append( + { + "netuid": op["netuid"], + "hotkey_ss58": op["hotkey_ss58"], + "unstake_amount": op["amount_to_unstake"].tao, + "success": suc, + } + ) + console.print( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." ) + if json_output: + json_console.print(json.dumps(successes)) async def unstake_all( @@ -323,6 +329,7 @@ async def unstake_all( include_hotkeys: list[str] = [], exclude_hotkeys: list[str] = [], prompt: bool = True, + json_output: bool = False, ) -> bool: """Unstakes all stakes from all hotkeys in all subnets.""" @@ -448,11 +455,16 @@ async def unstake_all( slippage_pct, ) console.print(table) - message = "" if max_slippage > 5: - message += f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n" - message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" - message += "-------------------------------------------------------------------------------------------------------------------\n" + message = ( + f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]--------------------------------------------------------------" + f"-----------------------------------------------------\n" + f"[bold]WARNING:[/bold] The slippage on one of your operations is high: " + f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage:.4f}%" + f"[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n" + "----------------------------------------------------------------------------------------------------------" + "---------\n" + ) console.print(message) console.print( @@ -466,10 +478,10 @@ async def unstake_all( if not unlock_key(wallet).success: return False - + successes = {} with console.status("Unstaking all stakes...") as status: for hotkey_ss58 in hotkey_ss58s: - await _unstake_all_extrinsic( + successes[hotkey_ss58] = await _unstake_all_extrinsic( wallet=wallet, subtensor=subtensor, hotkey_ss58=hotkey_ss58, @@ -477,6 +489,8 @@ async def unstake_all( unstake_all_alpha=unstake_all_alpha, status=status, ) + if json_output: + return json_console.print(json.dumps({"success": successes})) # Extrinsics @@ -488,7 +502,7 @@ async def _unstake_extrinsic( current_stake: Balance, hotkey_ss58: str, status=None, -) -> None: +) -> bool: """Execute a standard unstake extrinsic. Args: @@ -510,15 +524,17 @@ async def _unstake_extrinsic( f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..." ) - current_balance = await subtensor.get_balance(wallet.coldkeypub.ss58_address) - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_unstaked": amount.rao, - }, + current_balance, call = await asyncio.gather( + subtensor.get_balance(wallet.coldkeypub.ss58_address), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + }, + ), ) extrinsic = await subtensor.substrate.create_signed_extrinsic( call=call, keypair=wallet.coldkey @@ -528,15 +544,12 @@ async def _unstake_extrinsic( response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False ) - await response.process_events() - if not await response.is_success: err_out( f"{failure_prelude} with error: " f"{format_error_message(await response.error_message)}" ) - return - + return False # Fetch latest balance and stake block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -557,9 +570,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 except Exception as e: err_out(f"{failure_prelude} with error: {str(e)}") + return False async def _safe_unstake_extrinsic( @@ -572,7 +587,7 @@ async def _safe_unstake_extrinsic( price_limit: Balance, allow_partial_stake: bool, status=None, -) -> None: +) -> bool: """Execute a safe unstake extrinsic with price limit. Args: @@ -598,26 +613,27 @@ async def _safe_unstake_extrinsic( block_hash = await subtensor.substrate.get_chain_head() - current_balance, next_nonce, current_stake = await asyncio.gather( + current_balance, next_nonce, current_stake, call = await asyncio.gather( subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash), subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), subtensor.get_stake( hotkey_ss58=hotkey_ss58, coldkey_ss58=wallet.coldkeypub.ss58_address, netuid=netuid, + block_hash=block_hash, + ), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="remove_stake_limit", + call_params={ + "hotkey": hotkey_ss58, + "netuid": netuid, + "amount_unstaked": amount.rao, + "limit_price": price_limit, + "allow_partial": allow_partial_stake, + }, + block_hash=block_hash, ), - ) - - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="remove_stake_limit", - call_params={ - "hotkey": hotkey_ss58, - "netuid": netuid, - "amount_unstaked": amount.rao, - "limit_price": price_limit, - "allow_partial": allow_partial_stake, - }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( @@ -636,17 +652,15 @@ async def _safe_unstake_extrinsic( f"Either increase price tolerance or enable partial unstaking.", status=status, ) - return else: err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") - return + return False - await response.process_events() if not await response.is_success: err_out( f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}" ) - return + return False block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -677,6 +691,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 async def _unstake_all_extrinsic( From 9b1fbce56af2c0d2656a01aab655bc29d3c58e4f Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 11 Mar 2025 21:32:55 +0200 Subject: [PATCH 12/13] More stake commands. --- bittensor_cli/cli.py | 36 ++++++++++++++++------ bittensor_cli/src/commands/stake/move.py | 18 +++++------ bittensor_cli/src/commands/stake/remove.py | 1 - 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2c1aa5f4..304dd6ee 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2,6 +2,7 @@ import asyncio import curses import importlib +import json import os.path import re import ssl @@ -3460,6 +3461,7 @@ def stake_move( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Move staked TAO between hotkeys while keeping the same coldkey ownership. @@ -3481,13 +3483,14 @@ def stake_move( [green]$[/green] btcli stake move """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) console.print( "[dim]This command moves stake from one hotkey to another hotkey while keeping the same coldkey.[/dim]" ) if not destination_hotkey: dest_wallet_or_ss58 = Prompt.ask( - "Enter the [blue]destination wallet[/blue] where destination hotkey is located or [blue]ss58 address[/blue]" + "Enter the [blue]destination wallet[/blue] where destination hotkey is located or " + "[blue]ss58 address[/blue]" ) if is_valid_ss58_address(dest_wallet_or_ss58): destination_hotkey = dest_wallet_or_ss58 @@ -3574,7 +3577,7 @@ def stake_move( "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" ) - return self._run_command( + result = self._run_command( move_stake.move_stake( subtensor=self.initialize_chain(network), wallet=wallet, @@ -3588,6 +3591,9 @@ def stake_move( prompt=prompt, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def stake_transfer( self, @@ -3624,6 +3630,7 @@ def stake_transfer( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Transfer stake between coldkeys while keeping the same hotkey ownership. @@ -3657,10 +3664,10 @@ def stake_transfer( Transfer all available stake from origin hotkey: [green]$[/green] btcli stake transfer --all --origin-netuid 1 --dest-netuid 2 """ + self.verbosity_handler(quiet, verbose, json_output) console.print( "[dim]This command transfers stake from one coldkey to another while keeping the same hotkey.[/dim]" ) - self.verbosity_handler(quiet, verbose) if not dest_ss58: dest_ss58 = Prompt.ask( @@ -3732,7 +3739,7 @@ def stake_transfer( "Enter the [blue]destination subnet[/blue] (netuid)" ) - return self._run_command( + result = self._run_command( move_stake.transfer_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -3746,6 +3753,9 @@ def stake_transfer( prompt=prompt, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def stake_swap( self, @@ -3784,6 +3794,7 @@ def stake_swap( wait_for_finalization: bool = Options.wait_for_finalization, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Swap stake between different subnets while keeping the same coldkey-hotkey pair ownership. @@ -3805,10 +3816,10 @@ def stake_swap( Swap 100 TAO from subnet 1 to subnet 2: [green]$[/green] btcli stake swap --wallet-name default --wallet-hotkey default --origin-netuid 1 --dest-netuid 2 --amount 100 """ + self.verbosity_handler(quiet, verbose, json_output) console.print( "[dim]This command moves stake from one subnet to another subnet while keeping the same coldkey-hotkey pair.[/dim]" ) - self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, @@ -3833,7 +3844,7 @@ def stake_swap( if not amount and not swap_all: amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to swap") - return self._run_command( + result = self._run_command( move_stake.swap_stake( wallet=wallet, subtensor=self.initialize_chain(network), @@ -3847,6 +3858,9 @@ def stake_swap( wait_for_finalization=wait_for_finalization, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def stake_get_children( self, @@ -3868,6 +3882,7 @@ def stake_get_children( ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Get all the child hotkeys on a specified subnet. @@ -3879,7 +3894,7 @@ def stake_get_children( [green]$[/green] btcli stake child get --netuid 1 [green]$[/green] btcli stake child get --all-netuids """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -3900,11 +3915,14 @@ def stake_get_children( "Enter a netuid (leave blank for all)", default=None, show_default=True ) - return self._run_command( + result = self._run_command( children_hotkeys.get_children( wallet, self.initialize_chain(network), netuid ) ) + if json_output: + json_console.print(json.dumps(result)) + return result def stake_set_children( self, diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 0aa23278..debb872b 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -442,7 +442,7 @@ async def move_stake( stake_all: bool, interactive_selection: bool = False, prompt: bool = True, -): +) -> bool: if interactive_selection: try: selection = await stake_move_transfer_selection(subtensor, wallet) @@ -509,8 +509,10 @@ async def move_stake( if amount_to_move_as_balance > origin_stake_balance: err_console.print( f"[red]Not enough stake[/red]:\n" - f" Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" - f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"{origin_stake_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f"{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" ) return False @@ -559,13 +561,12 @@ async def move_stake( console.print(":white_heavy_check_mark: [green]Sent[/green]") return True else: - await response.process_events() 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 + return False else: console.print( ":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]" @@ -597,7 +598,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 + return True async def transfer_stake( @@ -734,7 +735,6 @@ async def transfer_stake( console.print(":white_heavy_check_mark: [green]Sent[/green]") return True - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " @@ -869,7 +869,8 @@ async def swap_stake( return False with console.status( - f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] to netuid [blue]{destination_netuid}[/blue]..." + f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " + f"to netuid [blue]{destination_netuid}[/blue]..." ): call = await subtensor.substrate.compose_call( call_module="SubtensorModule", @@ -896,7 +897,6 @@ async def swap_stake( console.print(":white_heavy_check_mark: [green]Sent[/green]") return True - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red] with error: " diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index d5ccfcaf..2a0d2352 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -46,7 +46,6 @@ async def unstake( json_output: bool, ): """Unstake from hotkey(s).""" - unstake_all_from_hk = False with console.status( f"Retrieving subnet data & identities from {subtensor.network}...", spinner="earth", From bdbf76bdf5c4d09b7b03e763ce757801100fe552 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 12 Mar 2025 15:15:36 +0200 Subject: [PATCH 13/13] Childkey commands (not all yet) --- bittensor_cli/cli.py | 11 +++++-- .../src/commands/stake/children_hotkeys.py | 30 +++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 304dd6ee..9d24963a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3947,6 +3947,7 @@ def stake_set_children( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Set child hotkeys on a specified subnet (or all). Overrides currently set children. @@ -3959,7 +3960,7 @@ def stake_set_children( [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 -p 0.3 -p 0.7 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) netuid = get_optional_netuid(netuid, all_netuids) children = list_prompt( @@ -3999,6 +4000,7 @@ def stake_set_children( wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, prompt=prompt, + json_output=json_output, ) ) @@ -4025,6 +4027,7 @@ def stake_revoke_children( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Remove all children hotkeys on a specified subnet (or all). @@ -4035,7 +4038,7 @@ def stake_revoke_children( [green]$[/green] btcli stake child revoke --hotkey --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -4060,6 +4063,7 @@ def stake_revoke_children( wait_for_inclusion, wait_for_finalization, prompt=prompt, + json_output=json_output, ) ) @@ -4094,6 +4098,7 @@ def stake_childkey_take( prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Get and set your child hotkey take on a specified subnet. @@ -4110,7 +4115,7 @@ def stake_childkey_take( [green]$[/green] btcli stake child take --hotkey --take 0.12 --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 89b5ade4..e19fff73 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import Optional from bittensor_wallet import Wallet @@ -19,6 +20,7 @@ is_valid_ss58_address, format_error_message, unlock_key, + json_console, ) @@ -500,6 +502,7 @@ async def set_children( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, + json_output: bool = False, ): """Set children hotkeys.""" # Validate children SS58 addresses @@ -520,6 +523,7 @@ async def set_children( f"Proposed sum of proportions is {total_proposed}." ) children_with_proportions = list(zip(proportions, children)) + successes = {} if netuid is not None: success, message = await set_children_extrinsic( subtensor=subtensor, @@ -531,12 +535,20 @@ async def set_children( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + successes[netuid] = { + "success": success, + "error": message, + "completion_block": None, + "set_block": None, + } # Result if success: if wait_for_inclusion and wait_for_finalization: current_block, completion_block = await get_childkey_completion_block( subtensor, netuid ) + successes[netuid]["completion_block"] = completion_block + successes[netuid]["set_block"] = current_block console.print( f"Your childkey request has been submitted. It will be completed around block {completion_block}. " f"The current block is {current_block}" @@ -555,7 +567,7 @@ async def set_children( if netuid_ == 0: # dont include root network continue console.print(f"Setting children on netuid {netuid_}.") - await set_children_extrinsic( + success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid_, @@ -568,6 +580,12 @@ async def set_children( current_block, completion_block = await get_childkey_completion_block( subtensor, netuid_ ) + successes[netuid_] = { + "success": success, + "error": message, + "completion_block": completion_block, + "set_block": current_block, + } console.print( f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around " f"block {completion_block}. The current block is {current_block}." @@ -575,6 +593,8 @@ async def set_children( console.print( ":white_heavy_check_mark: [green]Sent set children request for all subnets.[/green]" ) + if json_output: + json_console.print(json.dumps(successes)) async def revoke_children( @@ -584,10 +604,12 @@ async def revoke_children( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, + json_output: bool = False, ): """ Revokes the children hotkeys associated with a given network identifier (netuid). """ + dict_output = {} if netuid: success, message = await set_children_extrinsic( subtensor=subtensor, @@ -599,6 +621,7 @@ async def revoke_children( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) + dict_output[netuid] = {"success": success, "error": message} # Result if success: @@ -618,7 +641,7 @@ async def revoke_children( if netuid == 0: # dont include root network continue console.print(f"Revoking children from netuid {netuid}.") - await set_children_extrinsic( + success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, netuid=netuid, @@ -628,9 +651,12 @@ async def revoke_children( wait_for_inclusion=True, wait_for_finalization=False, ) + dict_output[netuid] = {"success": success, "error": message} console.print( ":white_heavy_check_mark: [green]Sent revoke children command. Finalization may take a few minutes.[/green]" ) + if json_output: + json_console.print(json.dumps(dict_output)) async def childkey_take(