From 912f8985dffdac6c1bc5bfe28ccafd7187457106 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 7 Mar 2025 15:03:06 +0200 Subject: [PATCH 01/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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/96] 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( From 8f0437c946fbf3148aa3eaed3eae110a0fee224a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 13 Mar 2025 22:02:34 +0200 Subject: [PATCH 14/96] Merge conflict --- bittensor_cli/src/commands/stake/remove.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 3c71349e..7749fc54 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -287,7 +287,6 @@ async def unstake( "subtensor": subtensor, "netuid": op["netuid"], "amount": op["amount_to_unstake"], - # TODO verify current_stake used in both "hotkey_ss58": op["hotkey_ss58"], "status": status, } @@ -300,7 +299,9 @@ async def unstake( } else: func = _unstake_extrinsic - specific_args = {} + specific_args = { + "current_stake": op["current_stake_balance"] + } suc = await func(**common_args, **specific_args) From 1faddeb6bcee3d4778cf806b1df1b7a5532912b0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 13 Mar 2025 22:50:38 +0200 Subject: [PATCH 15/96] Childkey take --- bittensor_cli/cli.py | 8 +- .../src/commands/stake/children_hotkeys.py | 99 +++++++++---------- bittensor_cli/src/commands/stake/remove.py | 4 +- 3 files changed, 57 insertions(+), 54 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 56182150..8b0208c2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4128,7 +4128,7 @@ def stake_childkey_take( netuid = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) - return self._run_command( + results: list[tuple[Optional[int], bool]] = self._run_command( children_hotkeys.childkey_take( wallet=wallet, subtensor=self.initialize_chain(network), @@ -4140,6 +4140,12 @@ def stake_childkey_take( prompt=prompt, ) ) + if json_output: + output = {} + for netuid_, success in results: + output[netuid_] = success + json_console.print(json.dumps(output)) + return results def sudo_set( self, diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 96ea8990..12f52658 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -3,7 +3,7 @@ from typing import Optional from bittensor_wallet import Wallet -from rich.prompt import Confirm, Prompt, IntPrompt +from rich.prompt import Confirm, IntPrompt, FloatPrompt from rich.table import Table from rich.text import Text from async_substrate_interface.errors import SubstrateRequestException @@ -671,8 +671,13 @@ async def childkey_take( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = True, -): - """Get or Set childkey take.""" +) -> list[tuple[Optional[int], bool]]: + """ + Get or Set childkey take. + + Returns: + List of (netuid, success) for specified netuid (or all) and their success in setting take + """ def validate_take_value(take_value: float) -> bool: if not (0 <= take_value <= 0.18): @@ -682,20 +687,7 @@ def validate_take_value(take_value: float) -> bool: return False return True - def print_all_takes(takes: list[tuple[int, float]], ss58: str): - """Print table with netuids and Takes""" - table = Table( - title=f"Current Child Takes for [bright_magenta]{ss58}[/bright_magenta]" - ) - table.add_column("Netuid", justify="center", style="cyan") - table.add_column("Take (%)", justify="right", style="magenta") - - for take_netuid, take_value in takes: - table.add_row(str(take_netuid), f"{take_value:.2f}%") - - console.print(table) - - async def display_chk_take(ss58, take_netuid): + async def display_chk_take(ss58, take_netuid) -> float: """Print single key take for hotkey and netuid""" chk_take = await get_childkey_take( subtensor=subtensor, netuid=take_netuid, hotkey=ss58 @@ -706,6 +698,7 @@ async def display_chk_take(ss58, take_netuid): console.print( f"Child take for {ss58} is: {chk_take * 100:.2f}% on netuid {take_netuid}." ) + return chk_take async def chk_all_subnets(ss58): """Aggregate data for childkey take from all subnets""" @@ -720,10 +713,18 @@ async def chk_all_subnets(ss58): if curr_take is not None: take_value = u16_to_float(curr_take) takes.append((subnet, take_value * 100)) + table = Table( + title=f"Current Child Takes for [bright_magenta]{ss58}[/bright_magenta]" + ) + table.add_column("Netuid", justify="center", style="cyan") + table.add_column("Take (%)", justify="right", style="magenta") + + for take_netuid, take_value in takes: + table.add_row(str(take_netuid), f"{take_value:.2f}%") - print_all_takes(takes, ss58) + console.print(table) - async def set_chk_take_subnet(subnet, chk_take): + async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool: """Set the childkey take for a single subnet""" success, message = await set_childkey_take_extrinsic( subtensor=subtensor, @@ -741,13 +742,17 @@ async def set_chk_take_subnet(subnet, chk_take): console.print( f"The childkey take for {wallet.hotkey.ss58_address} is now set to {take * 100:.2f}%." ) + return True else: console.print( f":cross_mark:[red] Unable to set childkey take.[/red] {message}" ) + return False # Print childkey take for other user and return (dont offer to change take rate) - if hotkey and hotkey != wallet.hotkey.ss58_address: + if not hotkey or hotkey == wallet.hotkey.ss58_address: + hotkey = wallet.hotkey.ss58_address + if hotkey != wallet.hotkey.ss58_address or not take: # display childkey take for other users if netuid: await display_chk_take(hotkey, netuid) @@ -755,70 +760,64 @@ async def set_chk_take_subnet(subnet, chk_take): console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return + return [(netuid, False)] else: - # show childhotkey take on all subnets + # show child hotkey take on all subnets await chk_all_subnets(hotkey) if take: console.print( f"Hotkey {hotkey} not associated with wallet {wallet.name}." ) - return + return [(netuid, False)] # Validate child SS58 addresses if not take: - # print current Take, ask if change - if netuid: - await display_chk_take(wallet.hotkey.ss58_address, netuid) - else: - # print take from all netuids - await chk_all_subnets(wallet.hotkey.ss58_address) - if not Confirm.ask("Would you like to change the child take?"): - return - new_take_str = Prompt.ask("Enter the new take value (between 0 and 0.18)") - try: - new_take_value = float(new_take_str) - if not validate_take_value(new_take_value): - return - except ValueError: - err_console.print( - ":cross_mark:[red] Invalid input. Please enter a number between 0 and 0.18.[/red]" + return [(netuid, False)] + new_take_value = -1.0 + while not validate_take_value(new_take_value): + new_take_value = FloatPrompt.ask( + "Enter the new take value (between 0 and 0.18)" ) - return take = new_take_value else: if not validate_take_value(take): - return + return [(netuid, False)] if netuid: - await set_chk_take_subnet(subnet=netuid, chk_take=take) - return + return [(netuid, await set_chk_take_subnet(subnet=netuid, chk_take=take))] else: new_take_netuids = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) if new_take_netuids: - await set_chk_take_subnet(subnet=new_take_netuids, chk_take=take) - return + return [ + ( + new_take_netuids, + await set_chk_take_subnet(subnet=new_take_netuids, chk_take=take), + ) + ] else: netuids = await subtensor.get_all_subnet_netuids() - for netuid in netuids: - if netuid == 0: + output_list = [] + for netuid_ in netuids: + if netuid_ == 0: continue - console.print(f"Sending to netuid {netuid} take of {take * 100:.2f}%") - await set_childkey_take_extrinsic( + console.print(f"Sending to netuid {netuid_} take of {take * 100:.2f}%") + result = await set_childkey_take_extrinsic( subtensor=subtensor, wallet=wallet, - netuid=netuid, + netuid=netuid_, hotkey=wallet.hotkey.ss58_address, take=take, prompt=prompt, wait_for_inclusion=True, wait_for_finalization=False, ) + output_list.append((netuid_, result)) console.print( f":white_heavy_check_mark: [green]Sent childkey take of {take * 100:.2f}% to all subnets.[/green]" ) + return output_list diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 7749fc54..52859ec8 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -299,9 +299,7 @@ async def unstake( } else: func = _unstake_extrinsic - specific_args = { - "current_stake": op["current_stake_balance"] - } + specific_args = {"current_stake": op["current_stake_balance"]} suc = await func(**common_args, **specific_args) From ad64a119d410c44e6b77629e13e0987b13861499 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 14 Mar 2025 12:27:09 +0200 Subject: [PATCH 16/96] Sudo commands. --- bittensor_cli/cli.py | 49 +++++++++++---- bittensor_cli/src/commands/sudo.py | 95 +++++++++++++++++++++++------- 2 files changed, 110 insertions(+), 34 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5f6cfd92..be6e002f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4163,6 +4163,7 @@ def sudo_set( ), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Used to set hyperparameters for a specific subnet. @@ -4173,7 +4174,7 @@ def sudo_set( [green]$[/green] btcli sudo set --netuid 1 --param tempo --value 400 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if not param_name or not param_value: hyperparams = self._run_command( @@ -4218,15 +4219,19 @@ def sudo_set( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) - return self._run_command( + result = self._run_command( sudo.sudo_set_hyperparameter( wallet, self.initialize_chain(network), netuid, param_name, param_value, + json_output, ) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def sudo_get( self, @@ -4234,6 +4239,7 @@ def sudo_get( netuid: int = Options.netuid, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows a list of the hyperparameters for the specified subnet. @@ -4244,7 +4250,9 @@ def sudo_get( """ self.verbosity_handler(quiet, verbose) return self._run_command( - sudo.get_hyperparameters(self.initialize_chain(network), netuid) + sudo.get_hyperparameters( + self.initialize_chain(network), netuid, json_output + ) ) def sudo_senate( @@ -4252,6 +4260,7 @@ def sudo_senate( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the Senate members of the Bittensor's governance protocol. @@ -4261,14 +4270,17 @@ def sudo_senate( EXAMPLE [green]$[/green] btcli sudo senate """ - self.verbosity_handler(quiet, verbose) - return self._run_command(sudo.get_senate(self.initialize_chain(network))) + self.verbosity_handler(quiet, verbose, json_output) + return self._run_command( + sudo.get_senate(self.initialize_chain(network), json_output) + ) def sudo_proposals( self, network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ View active proposals for the senate in the Bittensor's governance protocol. @@ -4278,7 +4290,7 @@ def sudo_proposals( EXAMPLE [green]$[/green] btcli sudo proposals """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) return self._run_command( sudo.proposals(self.initialize_chain(network), verbose) ) @@ -4317,6 +4329,7 @@ def sudo_senate_vote( EXAMPLE [green]$[/green] btcli sudo senate_vote --proposal """ + # TODO discuss whether this should receive json_output. I don't think it should. self.verbosity_handler(quiet, verbose) wallet = self.wallet_ask( wallet_name, @@ -4340,6 +4353,7 @@ def sudo_set_take( take: float = typer.Option(None, help="The new take value."), quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Allows users to change their delegate take percentage. @@ -4352,7 +4366,7 @@ def sudo_set_take( """ max_value = 0.18 min_value = 0.00 - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, @@ -4377,9 +4391,12 @@ def sudo_set_take( ) raise typer.Exit() - return self._run_command( + result = self._run_command( sudo.set_take(wallet, self.initialize_chain(network), take) ) + if json_output: + json_console.print(json.dumps({"success": result})) + return result def sudo_get_take( self, @@ -4389,6 +4406,7 @@ def sudo_get_take( wallet_hotkey: Optional[str] = Options.wallet_hotkey, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Allows users to check their delegate take percentage. @@ -4398,7 +4416,7 @@ def sudo_get_take( EXAMPLE [green]$[/green] btcli sudo get-take --wallet-name my_wallet --wallet-hotkey my_hotkey """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, @@ -4407,10 +4425,15 @@ def sudo_get_take( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - - self._run_command( - sudo.display_current_take(self.initialize_chain(network), wallet) - ) + if json_output: + result = self._run_command( + sudo.get_current_take(self.initialize_chain(network), wallet) + ) + json_console.print(json.dumps({"current_take": result})) + else: + self._run_command( + sudo.display_current_take(self.initialize_chain(network), wallet) + ) def subnets_list( self, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 8184bd79..6c9390cb 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -1,4 +1,5 @@ import asyncio +import json from typing import TYPE_CHECKING, Union, Optional from bittensor_wallet import Wallet @@ -19,6 +20,7 @@ blocks_to_duration, float_to_u64, float_to_u16, + json_console, ) if TYPE_CHECKING: @@ -350,6 +352,19 @@ def display_votes( return "\n".join(vote_list) +def serialize_vote_data( + vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails] +) -> list[dict[str, bool]]: + vote_list = [] + for address in vote_data.ayes: + f_add = delegate_info[address].display if address in delegate_info else address + vote_list.append({f_add: True}) + for address in vote_data.nays: + f_add = delegate_info[address].display if address in delegate_info else address + vote_list.append({f_add: False}) + return vote_list + + def format_call_data(call_data: dict) -> str: # Extract the module and call details module, call_details = next(iter(call_data.items())) @@ -559,6 +574,7 @@ async def sudo_set_hyperparameter( netuid: int, param_name: str, param_value: Optional[str], + json_output: bool, ): """Set subnet hyperparameters.""" @@ -584,17 +600,22 @@ async def sudo_set_hyperparameter( f"Hyperparameter [dark_orange]{param_name}[/dark_orange] value is not within bounds. " f"Value is {normalized_value} but must be {value}" ) - return + return False success = await set_hyperparameter_extrinsic( subtensor, wallet, netuid, param_name, value ) + if json_output: + return success if success: console.print("\n") print_verbose("Fetching hyperparameters") - return await get_hyperparameters(subtensor, netuid=netuid) + await get_hyperparameters(subtensor, netuid=netuid) + return success -async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): +async def get_hyperparameters( + subtensor: "SubtensorInterface", netuid: int, json_output: bool = False +) -> bool: """View hyperparameters of a subnetwork.""" print_verbose("Fetching hyperparameters") if not await subtensor.subnet_exists(netuid): @@ -607,32 +628,43 @@ async def get_hyperparameters(subtensor: "SubtensorInterface", netuid: int): return False table = Table( - Column("[white]HYPERPARAMETER", style=COLOR_PALETTE["SUDO"]["HYPERPARAMETER"]), - Column("[white]VALUE", style=COLOR_PALETTE["SUDO"]["VALUE"]), - Column("[white]NORMALIZED", style=COLOR_PALETTE["SUDO"]["NORMALIZED"]), - title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]\nSubnet Hyperparameters\n NETUID: " - f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}" + Column("[white]HYPERPARAMETER", style=COLOR_PALETTE.SU.HYPERPARAMETER), + Column("[white]VALUE", style=COLOR_PALETTE.SU.VALUE), + Column("[white]NORMALIZED", style=COLOR_PALETTE.SU.NORMAL), + title=f"[{COLOR_PALETTE.G.HEADER}]\nSubnet Hyperparameters\n NETUID: " + f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}" f"{f' ({subnet_info.subnet_name})' if subnet_info.subnet_name is not None else ''}" - f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" - f" - Network: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + f"[/{COLOR_PALETTE.G.SUBHEAD}]" + f" - Network: [{COLOR_PALETTE.G.SUBHEAD}]{subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", show_footer=True, width=None, pad_edge=False, box=box.SIMPLE, show_edge=True, ) + dict_out = [] normalized_values = normalize_hyperparameters(subnet) for param, value, norm_value in normalized_values: table.add_row(" " + param, value, norm_value) - + dict_out.append( + { + "hyperparameter": param, + "value": value, + "normalized_value": norm_value, + } + ) + if json_output: + json_console.print(json.dumps(dict_out)) console.print(table) return True -async def get_senate(subtensor: "SubtensorInterface"): - """View Bittensor's senate memebers""" +async def get_senate( + subtensor: "SubtensorInterface", json_output: bool = False +) -> None: + """View Bittensor's senate members""" with console.status( f":satellite: Syncing with chain: [white]{subtensor}[/white] ...", spinner="aesthetic", @@ -663,21 +695,27 @@ async def get_senate(subtensor: "SubtensorInterface"): border_style="bright_black", leading=True, ) + dict_output = [] for ss58_address in senate_members: + member_name = ( + delegate_info[ss58_address].display + if ss58_address in delegate_info + else "~" + ) table.add_row( - ( - delegate_info[ss58_address].display - if ss58_address in delegate_info - else "~" - ), + member_name, ss58_address, ) - + dict_output.append({"name": member_name, "ss58_address": ss58_address}) + if json_output: + json_console.print(json.dumps(dict_output)) return console.print(table) -async def proposals(subtensor: "SubtensorInterface", verbose: bool): +async def proposals( + subtensor: "SubtensorInterface", verbose: bool, json_output: bool = False +) -> None: console.print( ":satellite: Syncing with chain: [white]{}[/white] ...".format( subtensor.network @@ -723,6 +761,7 @@ async def proposals(subtensor: "SubtensorInterface", verbose: bool): width=None, border_style="bright_black", ) + dict_output = [] for hash_, (call_data, vote_data) in all_proposals.items(): blocks_remaining = vote_data.end - current_block if blocks_remaining > 0: @@ -741,6 +780,7 @@ async def proposals(subtensor: "SubtensorInterface", verbose: bool): if vote_data.threshold > 0 else 0 ) + f_call_data = format_call_data(call_data) table.add_row( hash_ if verbose else f"{hash_[:4]}...{hash_[-4:]}", str(vote_data.threshold), @@ -748,8 +788,21 @@ async def proposals(subtensor: "SubtensorInterface", verbose: bool): f"{len(vote_data.nays)} ({nays_threshold:.2f}%)", display_votes(vote_data, registered_delegate_info), vote_end_cell, - format_call_data(call_data), + f_call_data, + ) + dict_output.append( + { + "hash": hash_, + "threshold": vote_data.threshold, + "ayes": len(vote_data.ayes), + "nays": len(vote_data.nays), + "votes": serialize_vote_data(vote_data, registered_delegate_info), + "end": vote_data.end, + "call_data": f_call_data, + } ) + if json_output: + json_console.print(json.dumps(dict_output)) console.print(table) console.print( "\n[dim]* Both Ayes and Nays percentages are calculated relative to the proposal's threshold.[/dim]" From 70138f3921dbebc03311a3c62a37388b4ced8613 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 14 Mar 2025 13:03:33 +0200 Subject: [PATCH 17/96] Subnets list --- bittensor_cli/cli.py | 7 +- bittensor_cli/src/commands/subnets/subnets.py | 138 +++++++++++++----- 2 files changed, 104 insertions(+), 41 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index be6e002f..3c9f1f03 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4441,6 +4441,7 @@ def subnets_list( quiet: bool = Options.quiet, verbose: bool = Options.verbose, live_mode: bool = Options.live, + json_output: bool = Options.json_output, ): """ List all subnets and their detailed information. @@ -4468,7 +4469,10 @@ def subnets_list( [green]$[/green] btcli subnets list """ - self.verbosity_handler(quiet, verbose) + if json_output and live_mode: + print_error("Cannot use --json-output and --live at the same time.") + return + self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) return self._run_command( subnets.subnets_list( @@ -4478,6 +4482,7 @@ def subnets_list( not self.config.get("use_cache", True), verbose, live_mode, + json_output, ) ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 81043d8c..64efb1ae 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -34,6 +34,7 @@ prompt_for_identity, get_subnet_name, unlock_key, + json_console, ) if TYPE_CHECKING: @@ -206,40 +207,41 @@ async def subnets_list( no_cache: bool, verbose: bool, live: bool, + json_output: bool, ): """List all subnet netuids in the network.""" async def fetch_subnet_data(): - block_number = await subtensor.substrate.get_block_number(None) - subnets = await subtensor.all_subnets() + block_number_ = await subtensor.substrate.get_block_number(None) + subnets_ = await subtensor.all_subnets() # Sort subnets by market cap, keeping the root subnet in the first position - root_subnet = next(s for s in subnets if s.netuid == 0) + root_subnet = next(s for s in subnets_ if s.netuid == 0) other_subnets = sorted( - [s for s in subnets if s.netuid != 0], + [s for s in subnets_ if s.netuid != 0], key=lambda x: (x.alpha_in.tao + x.alpha_out.tao) * x.price.tao, reverse=True, ) sorted_subnets = [root_subnet] + other_subnets - return sorted_subnets, block_number + return sorted_subnets, block_number_ def calculate_emission_stats( - subnets: list, block_number: int + subnets_: list, block_number_: int ) -> tuple[Balance, str]: # We do not include the root subnet in the emission calculation total_tao_emitted = sum( - subnet.tao_in.tao for subnet in subnets if subnet.netuid != 0 + subnet.tao_in.tao for subnet in subnets_ if subnet.netuid != 0 ) - emission_percentage = (total_tao_emitted / block_number) * 100 + emission_percentage = (total_tao_emitted / block_number_) * 100 percentage_color = "dark_sea_green" if emission_percentage < 100 else "red" formatted_percentage = ( f"[{percentage_color}]{emission_percentage:.2f}%[/{percentage_color}]" ) if not verbose: - percentage_string = f"τ {millify_tao(total_tao_emitted)}/{millify_tao(block_number)} ({formatted_percentage})" + percentage_string = f"τ {millify_tao(total_tao_emitted)}/{millify_tao(block_number_)} ({formatted_percentage})" else: percentage_string = ( - f"τ {total_tao_emitted:.1f}/{block_number} ({formatted_percentage})" + f"τ {total_tao_emitted:.1f}/{block_number_} ({formatted_percentage})" ) return total_tao_emitted, percentage_string @@ -249,7 +251,7 @@ def define_table( total_netuids: int, tao_emission_percentage: str, ): - table = Table( + defined_table = Table( title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnets" f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}\n\n", show_footer=True, @@ -262,61 +264,61 @@ def define_table( pad_edge=True, ) - table.add_column( + defined_table.add_column( "[bold white]Netuid", style="grey89", justify="center", footer=str(total_netuids), ) - table.add_column("[bold white]Name", style="cyan", justify="left") - table.add_column( + defined_table.add_column("[bold white]Name", style="cyan", justify="left") + defined_table.add_column( f"[bold white]Price \n({Balance.get_unit(0)}_in/{Balance.get_unit(1)}_in)", style="dark_sea_green2", justify="left", footer=f"τ {total_rate}", ) - table.add_column( + defined_table.add_column( f"[bold white]Market Cap \n({Balance.get_unit(1)} * Price)", style="steel_blue3", justify="left", ) - table.add_column( + defined_table.add_column( f"[bold white]Emission ({Balance.get_unit(0)})", style=COLOR_PALETTE["POOLS"]["EMISSION"], justify="left", footer=f"τ {total_emissions}", ) - table.add_column( + defined_table.add_column( f"[bold white]P ({Balance.get_unit(0)}_in, {Balance.get_unit(1)}_in)", style=COLOR_PALETTE["STAKE"]["TAO"], justify="left", footer=f"{tao_emission_percentage}", ) - table.add_column( + defined_table.add_column( f"[bold white]Stake ({Balance.get_unit(1)}_out)", style=COLOR_PALETTE["STAKE"]["STAKE_ALPHA"], justify="left", ) - table.add_column( + defined_table.add_column( f"[bold white]Supply ({Balance.get_unit(1)})", style=COLOR_PALETTE["POOLS"]["ALPHA_IN"], justify="left", ) - table.add_column( + defined_table.add_column( "[bold white]Tempo (k/n)", style=COLOR_PALETTE["GENERAL"]["TEMPO"], justify="left", overflow="fold", ) - return table + return defined_table # Non-live mode - def create_table(subnets, block_number): + def _create_table(subnets_, block_number_): rows = [] - _, percentage_string = calculate_emission_stats(subnets, block_number) + _, percentage_string = calculate_emission_stats(subnets_, block_number_) - for subnet in subnets: + for subnet in subnets_: netuid = subnet.netuid symbol = f"{subnet.symbol}\u200e" @@ -363,7 +365,7 @@ def create_table(subnets, block_number): # Prepare cells netuid_cell = str(netuid) subnet_name_cell = ( - f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]" + f"[{COLOR_PALETTE.G.SYM}]{subnet.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE.G.SYM}]" f" {get_subnet_name(subnet)}" ) emission_cell = f"τ {emission_tao:,.4f}" @@ -396,23 +398,76 @@ def create_table(subnets, block_number): ) total_emissions = round( - sum(subnet.tao_in_emission.tao for subnet in subnets if subnet.netuid != 0), + sum( + subnet.tao_in_emission.tao for subnet in subnets_ if subnet.netuid != 0 + ), 4, ) total_rate = round( - sum(float(subnet.price.tao) for subnet in subnets if subnet.netuid != 0), 4 + sum(float(subnet.price.tao) for subnet in subnets_ if subnet.netuid != 0), 4 ) - total_netuids = len(subnets) - table = define_table( + total_netuids = len(subnets_) + defined_table = define_table( total_emissions, total_rate, total_netuids, percentage_string ) for row in rows: - table.add_row(*row) - return table + defined_table.add_row(*row) + return defined_table + + def dict_table(subnets_, block_number_) -> dict: + subnet_rows = {} + total_tao_emitted, _ = calculate_emission_stats(subnets_, block_number_) + total_emissions = 0.0 + total_rate = 0.0 + total_netuids = len(subnets_) + emission_percentage = (total_tao_emitted / block_number_) * 100 + for subnet in subnets_: + total_emissions += subnet.tao_in_emission.tao + total_rate += subnet.price.tao + netuid = subnet.netuid + if netuid == 0: + emission_tao = 0.0 + else: + emission_tao = subnet.tao_in_emission.tao + alpha_in_value = subnet.alpha_in.tao + alpha_out_value = subnet.alpha_out.tao + price_value = subnet.price.tao + market_cap = (subnet.alpha_in.tao + subnet.alpha_out.tao) * subnet.price.tao + tao_in = subnet.tao_in.tao if netuid != 0 else None + alpha_in = alpha_in_value if netuid != 0 else None + alpha_out = alpha_out_value if netuid != 0 else None + supply = subnet.alpha_in.tao + subnet.alpha_out.tao + subnet_name = get_subnet_name(subnet) + tempo = { + "blocks_since_last_step": ( + subnet.blocks_since_last_step if netuid != 0 else None + ), + "sn_tempo": (subnet.tempo if netuid != 0 else None), + } + subnet_rows[netuid] = { + "netuid": netuid, + "subnet_name": subnet_name, + "price": price_value, + "market_cap": market_cap, + "emission": emission_tao, + "liquidity": {"tao_in": tao_in, "alpha_in": alpha_in}, + "alpha_out": alpha_out, + "supply": supply, + "tempo": tempo, + } + output = { + "total_tao_emitted": total_tao_emitted, + "total_emissions": total_emissions, + "total_rate": total_rate, + "total_netuids": total_netuids, + "emission_percentage": emission_percentage, + "subnets": subnet_rows, + } + return output # Live mode - def create_table_live(subnets, previous_data, block_number): + def create_table_live(subnets_, previous_data_, block_number_): def format_cell( value, previous_value, unit="", unit_first=False, precision=4, millify=False ): @@ -516,9 +571,9 @@ def format_liquidity_cell( rows = [] current_data = {} # To store current values for comparison in the next update - _, percentage_string = calculate_emission_stats(subnets, block_number) + _, percentage_string = calculate_emission_stats(subnets_, block_number_) - for subnet in subnets: + for subnet in subnets_: netuid = subnet.netuid symbol = f"{subnet.symbol}\u200e" @@ -541,7 +596,7 @@ def format_liquidity_cell( "supply": supply, "blocks_since_last_step": subnet.blocks_since_last_step, } - prev = previous_data.get(netuid, {}) if previous_data else {} + prev = previous_data_.get(netuid, {}) if previous_data_ else {} # Prepare cells if netuid == 0: @@ -652,9 +707,9 @@ def format_liquidity_cell( ) # Calculate totals - total_netuids = len(subnets) + total_netuids = len(subnets_) _total_emissions = sum( - subnet.tao_in_emission.tao for subnet in subnets if subnet.netuid != 0 + subnet.tao_in_emission.tao for subnet in subnets_ if subnet.netuid != 0 ) total_emissions = ( f"{millify_tao(_total_emissions)}" @@ -662,7 +717,7 @@ def format_liquidity_cell( else f"{_total_emissions:,.2f}" ) - total_rate = sum(subnet.price.tao for subnet in subnets if subnet.netuid != 0) + total_rate = sum(subnet.price.tao for subnet in subnets_ if subnet.netuid != 0) total_rate = ( f"{millify_tao(total_rate)}" if not verbose else f"{total_rate:,.2f}" ) @@ -733,8 +788,11 @@ def format_liquidity_cell( else: # Non-live mode subnets, block_number = await fetch_subnet_data() - table = create_table(subnets, block_number) - console.print(table) + if json_output: + json_console.print(json.dumps(dict_table(subnets, block_number))) + else: + table = _create_table(subnets, block_number) + console.print(table) return # TODO: Temporarily returning till we update docs From c98d8ff5ce7cef0fef1babb1e194e0e4e2b1d29c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 14 Mar 2025 13:26:06 +0200 Subject: [PATCH 18/96] Subnets price --- bittensor_cli/cli.py | 18 ++++++++++++----- bittensor_cli/src/commands/subnets/price.py | 22 ++++++++++++--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 3c9f1f03..2597e205 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4470,7 +4470,7 @@ def subnets_list( [green]$[/green] btcli subnets list """ if json_output and live_mode: - print_error("Cannot use --json-output and --live at the same time.") + print_error("Cannot use `--json-output` and `--live` at the same time.") return self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) @@ -4515,6 +4515,9 @@ def subnets_price( help="Show the price in log scale.", ), html_output: bool = Options.html_output, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the historical price of a subnet for the past 24 hours. @@ -4532,6 +4535,10 @@ def subnets_price( [green]$[/green] btcli subnets price --all --html [green]$[/green] btcli subnets price --netuids 1,2,3,4 --html """ + if json_output and html_output: + print_error("Cannot specify both `--json-output` and `--html`") + return + self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) if netuids: netuids = parse_to_list( netuids, @@ -4540,15 +4547,15 @@ def subnets_price( ) if all_netuids and netuids: print_error("Cannot specify both --netuid and --all-netuids") - raise typer.Exit() + return if not netuids and not all_netuids: netuids = Prompt.ask( - "Enter the [blue]netuid(s)[/blue] to view the price of in comma-separated format [dim](or Press Enter to view all subnets)[/dim]", + "Enter the [blue]netuid(s)[/blue] to view the price of in comma-separated format [dim]" + "(or Press Enter to view all subnets)[/dim]", ) if not netuids: all_netuids = True - html_output = True else: netuids = parse_to_list( netuids, @@ -4556,7 +4563,7 @@ def subnets_price( "Netuids must be a comma-separated list of ints, e.g., `--netuids 1,2,3,4`.", ) - if all_netuids: + if all_netuids and not json_output: html_output = True if html_output and is_linux(): @@ -4570,6 +4577,7 @@ def subnets_price( interval_hours, html_output, log_scale, + json_output, ) ) diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index eb6b7b5c..ccc71997 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -13,6 +13,7 @@ err_console, get_subnet_name, print_error, + json_console, ) if TYPE_CHECKING: @@ -26,6 +27,7 @@ async def price( interval_hours: int = 24, html_output: bool = False, log_scale: bool = False, + json_output: bool = False, ): """ Fetch historical price data for subnets and display it in a chart. @@ -60,7 +62,7 @@ async def price( all_subnet_infos = await asyncio.gather(*subnet_info_cors) subnet_data = _process_subnet_data( - block_numbers, all_subnet_infos, netuids, all_netuids, interval_hours + block_numbers, all_subnet_infos, netuids, all_netuids ) if not subnet_data: @@ -71,17 +73,13 @@ async def price( await _generate_html_output( subnet_data, block_numbers, interval_hours, log_scale ) + elif json_output: + json_console.print(json.dumps(_generate_json_output(subnet_data))) else: _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) -def _process_subnet_data( - block_numbers, - all_subnet_infos, - netuids, - all_netuids, - interval_hours, -): +def _process_subnet_data(block_numbers, all_subnet_infos, netuids, all_netuids): """ Process subnet data into a structured format for price analysis. """ @@ -772,6 +770,10 @@ async def _generate_html_output( print_error(f"Error generating price chart: {e}") +def _generate_json_output(subnet_data): + return {netuid: data for netuid, data in subnet_data.items()} + + def _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale): """ Render the price data in a textual CLI style with plotille ASCII charts. @@ -802,7 +804,9 @@ def color_label(text): fig.plot( block_numbers, - data["prices"], + data[ + "prices" + ], # TODO should this use `prices` instead of `data["prices"]`? label=f"Subnet {netuid} Price", interp="linear", lc="bae98f", From fb22b427eac9e685e8e45602a9770f6dca5d1afd Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 14 Mar 2025 18:05:01 +0200 Subject: [PATCH 19/96] Subnets show check-in (incomplete) --- bittensor_cli/cli.py | 4 +- bittensor_cli/src/commands/subnets/subnets.py | 56 +++++++++++++++---- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2597e205..141d8f76 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4593,6 +4593,7 @@ def subnets_show( quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, + json_output: bool = Options.json_output, ): """ Displays detailed information about a subnet including participants and their state. @@ -4601,7 +4602,7 @@ def subnets_show( [green]$[/green] btcli subnets list """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) return self._run_command( subnets.show( @@ -4612,6 +4613,7 @@ def subnets_show( delegate_selection=False, verbose=verbose, prompt=prompt, + json_output=json_output, ) ) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 64efb1ae..59423121 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -862,20 +862,22 @@ async def show( delegate_selection: bool = False, verbose: bool = False, prompt: bool = True, + json_output: bool = False, ) -> Optional[str]: async def show_root(): + # TODO json_output for this, don't forget block_hash = await subtensor.substrate.get_chain_head() - all_subnets = await subtensor.all_subnets(block_hash=block_hash) - root_info = next((s for s in all_subnets if s.netuid == 0), None) - if root_info is None: - print_error("The root subnet does not exist") - return False - root_state, identities, old_identities = await asyncio.gather( + all_subnets, root_state, identities, old_identities = await asyncio.gather( + subtensor.all_subnets(block_hash=block_hash), subtensor.get_subnet_state(netuid=0, block_hash=block_hash), subtensor.query_all_identities(block_hash=block_hash), subtensor.get_delegate_identities(block_hash=block_hash), ) + root_info = next((s for s in all_subnets if s.netuid == 0), None) + if root_info is None: + print_error("The root subnet does not exist") + return False if root_state is None: err_console.print("The root subnet does not exist") @@ -887,12 +889,11 @@ async def show_root(): ) return - tao_sum = sum( - [root_state.tao_stake[idx].tao for idx in range(len(root_state.tao_stake))] - ) + tao_sum = sum(root_state.tao_stake).tao table = Table( - title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Root Network\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n", + title=f"[{COLOR_PALETTE.G.HEADER}]Root Network\n[{COLOR_PALETTE.G.SUBHEAD}]" + f"Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]\n", show_footer=True, show_edge=False, header_style="bold white", @@ -1177,6 +1178,7 @@ async def show_subnet(netuid_: int): ) rows = [] + json_out_rows = [] for idx in sorted_indices: # Get identity for this uid coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get( @@ -1228,6 +1230,22 @@ async def show_subnet(netuid_: int): uid_identity, # Identity ) ) + json_out_rows.append( + { + "uid": idx, + "stake": subnet_state.total_stake[idx].tao, + "alpha_stake": subnet_state.alpha_stake[idx].tao, + "tao_stake": tao_stake.tao, + "dividends": subnet_state.dividends[idx], + "incentive": subnet_state.incentives[idx], + "emissions": Balance.from_tao(subnet_state.emission[idx].tao) + .set_unit(netuid_) + .tao, + "hotkey": subnet_state.hotkeys[idx], + "coldkey": subnet_state.coldkeys[idx], + "identity": uid_identity, + } + ) # Add columns to the table table.add_column("UID", style="grey89", no_wrap=True, justify="center") @@ -1320,6 +1338,24 @@ async def show_subnet(netuid_: int): if current_burn_cost else Balance(0) ) + output_dict = { + "netuid": netuid_, + "name": subnet_name_display, + "owner": subnet_info.owner_coldkey, + "owner_identity": owner_identity, + "rate": subnet_info.price.tao, + "emission": subnet_info.emission.tao, + "tao_pool": subnet_info.tao_in.tao, + "alpha_pool": subnet_info.alpha_in.tao, + "tempo": { + "block_since_last_step": subnet_info.blocks_since_last_step, + "tempo": subnet_info.tempo, + }, + "registration_cost": current_registration_burn.tao, + "uids": json_out_rows, + } + if json_output: + json_console.print(json.dumps(output_dict)) console.print( f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]" From 0ae490efa61a9dfc960d5834837011244a2935a8 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 17 Mar 2025 23:46:13 -0700 Subject: [PATCH 20/96] fix --- tests/e2e_tests/conftest.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index e3625683..f0deb853 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -184,17 +184,9 @@ def try_start_docker(): ) if not result.stdout.strip(): raise RuntimeError("Docker container failed to start.") - - substrate = AsyncSubstrateInterface(url="ws://127.0.0.1:9944") - yield substrate + yield AsyncSubstrateInterface(url="ws://127.0.0.1:9944") finally: - try: - if substrate: - substrate.close() - except Exception: - pass - try: subprocess.run(["docker", "kill", container_name]) process.wait(timeout=10) From 47fd63557137decc2ad3f21db21d7d98baa00e47 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 18 Mar 2025 00:23:12 -0700 Subject: [PATCH 21/96] update docker image name --- tests/e2e_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index f0deb853..92a69f95 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -142,7 +142,7 @@ def try_start_docker(): return False container_name = f"test_local_chain_{str(time.time()).replace('.', '_')}" - image_name = "ghcr.io/opentensor/subtensor-localnet:latest" + image_name = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" # Command to start container cmds = [ From c01f874cb5cfd2e980e640f97b533bc9c86c1446 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 18 Mar 2025 00:34:32 -0700 Subject: [PATCH 22/96] do tests for all python-version --- .github/workflows/e2e-subtensor-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index b4dc3120..7e3d4a1d 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -73,7 +73,7 @@ jobs: os: - ubuntu-latest test-file: ${{ fromJson(needs.find-tests.outputs.test-files) }} - # python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - name: Check-out repository uses: actions/checkout@v4 From 1cfe029562bf0022b38c9cdf887ec58429d02fb2 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 18 Mar 2025 09:31:15 -0700 Subject: [PATCH 23/96] apply asyncio run to async substrate.close() call --- tests/e2e_tests/conftest.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index 92a69f95..bf993325 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -1,3 +1,4 @@ +import asyncio import logging import os import re @@ -184,9 +185,16 @@ def try_start_docker(): ) if not result.stdout.strip(): raise RuntimeError("Docker container failed to start.") - yield AsyncSubstrateInterface(url="ws://127.0.0.1:9944") + substrate = AsyncSubstrateInterface(url="ws://127.0.0.1:9944") + yield substrate finally: + try: + if substrate: + asyncio.run(substrate.close()) + except Exception: + logging.warning("Failed to close substrate connection.") + try: subprocess.run(["docker", "kill", container_name]) process.wait(timeout=10) From 330d12853014ea899f0217b05d508291cd8255bf Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 18 Mar 2025 12:30:41 -0700 Subject: [PATCH 24/96] added verbose logging --- tests/e2e_tests/test_unstaking.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 66dda33c..7b6b836f 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -213,6 +213,16 @@ def test_unstaking(local_chain, wallet_setup): float(inital_stake_netuid_2) ) + print( + "Bob wallet deets: ", + "hotkey: ", + wallet_bob.hotkey_str, + "name: ", + wallet_bob.name, + "path: ", + wallet_bob.path, + ) + # Remove all alpha stakes unstake_alpha = exec_command_bob( command="stake", @@ -222,7 +232,7 @@ def test_unstaking(local_chain, wallet_setup): wallet_path_bob, "--wallet-name", wallet_bob.name, - "--hotkey", + "--wallet-hotkey", wallet_bob.hotkey_str, "--chain", "ws://127.0.0.1:9945", From 04601d244f08b4d9a04231236dce5ce7c5cf3922 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 18 Mar 2025 15:57:03 -0700 Subject: [PATCH 25/96] add get_scheduled_coldkey_swap --- .../src/bittensor/subtensor_interface.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index dba78636..e47cfd56 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1501,3 +1501,28 @@ async def get_stake_fee( ) return Balance.from_rao(result) + + async def get_scheduled_coldkey_swap( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[list[str]]: + """ + Queries the chain to fetch the list of coldkeys that are scheduled for a swap. + + :param block_hash: Block hash at which to perform query. + :param reuse_block: Whether to reuse the last-used block hash. + + :return: A list of SS58 addresses of the coldkeys that are scheduled for a coldkey swap. + """ + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="ColdkeySwapScheduled", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + keys_pending_swap = [] + async for ss58, _ in result: + keys_pending_swap.append(decode_account_id(ss58)) + return keys_pending_swap From 30508d46a0d6bea566aeca3f15c043953482ffb2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 18 Mar 2025 15:58:02 -0700 Subject: [PATCH 26/96] Add schedule_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 82b1253a..6285402e 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -14,6 +14,7 @@ from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding +from rich.prompt import Confirm from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor import utils @@ -1472,3 +1473,74 @@ 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:") console.print(signed_message) + + +async def schedule_coldkey_swap( + wallet: Wallet, + subtensor: SubtensorInterface, + new_coldkey_ss58: str, + force_swap: bool = False, +) -> bool: + """Schedules a coldkey swap operation to be executed at a future block. + + Args: + wallet (Wallet): The wallet initiating the coldkey swap + subtensor (SubtensorInterface): Connection to the Bittensor network + new_coldkey_ss58 (str): SS58 address of the new coldkey + force_swap (bool, optional): Whether to force the swap even if the new coldkey is already scheduled for a swap. Defaults to False. + Returns: + bool: True if the swap was scheduled successfully, False otherwise + """ + if not is_valid_ss58_address(new_coldkey_ss58): + print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") + return False + + scheduled_coldkey_swap = await subtensor.get_scheduled_coldkey_swap() + if wallet.coldkeypub.ss58_address in scheduled_coldkey_swap: + print_error( + f"Coldkey {wallet.coldkeypub.ss58_address} is already scheduled for a swap." + ) + console.print("[dim]Use the force_swap (--force) flag to override this.[/dim]") + if not force_swap: + return False + else: + console.print( + "[yellow]Continuing with the swap due to force_swap flag.[/yellow]\n" + ) + + prompt = ( + "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n" + f"Current ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"New ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{new_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + "Are you sure you want to continue?" + ) + if not Confirm.ask(prompt): + return False + + if not unlock_key(wallet).success: + return False + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="schedule_swap_coldkey", + call_params={ + "new_coldkey": new_coldkey_ss58, + }, + ) + + with console.status(":satellite: Scheduling coldkey swap on-chain..."): + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + if not success: + print_error(f"Failed to schedule coldkey swap: {err_msg}") + return False + + console.print( + ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" + ) + return True From dc9e692421716bf3cacc487696445f84a8966f14 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 18 Mar 2025 15:59:34 -0700 Subject: [PATCH 27/96] Adds cli entry --- bittensor_cli/cli.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 68d95db4..8bb7691d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -684,6 +684,9 @@ def __init__(self): self.wallet_app.command( "swap-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_swap_hotkey) + self.wallet_app.command( + "swap-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + )(self.wallet_swap_coldkey) self.wallet_app.command( "regen-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_regen_coldkey) @@ -2762,6 +2765,91 @@ def wallet_sign( return self._run_command(wallets.sign(wallet, message, use_hotkey)) + def wallet_swap_coldkey( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + new_wallet_or_ss58: Optional[str] = typer.Option( + None, + "--new-coldkey", + "--new-coldkey-ss58", + "--new-wallet", + "--new", + help="SS58 address of the new coldkey that will replace the current one.", + ), + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + force_swap: bool = typer.Option( + False, + "--force", + "-f", + "--force-swap", + help="Force the swap even if the new coldkey is already scheduled for a swap.", + ), + ): + """ + Schedule a coldkey swap for a wallet. + + This command allows you to schedule a coldkey swap for a wallet. You can either provide a new wallet name, or SS58 address. + + EXAMPLES + + [green]$[/green] btcli wallet schedule-coldkey-swap --new-wallet my_new_wallet + + [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q + """ + self.verbosity_handler(quiet, verbose) + + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue] which you want to swap the coldkey for", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + validate=WV.WALLET, + ) + console.print( + f"\nWallet selected to swap the [blue]coldkey[/blue] from: \n" + f"[dark_sea_green3]{wallet}[/dark_sea_green3]\n" + ) + + if not new_wallet_or_ss58: + new_wallet_or_ss58 = Prompt.ask( + "Enter the [blue]new wallet name[/blue] or [blue]SS58 address[/blue] of the new coldkey", + ) + + if is_valid_ss58_address(new_wallet_or_ss58): + new_wallet_coldkey_ss58 = new_wallet_or_ss58 + else: + new_wallet_name = new_wallet_or_ss58 + new_wallet = self.wallet_ask( + new_wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + validate=WV.WALLET, + ) + console.print( + f"\nNew wallet to swap the [blue]coldkey[/blue] to: \n" + f"[dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" + ) + new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address + + return self._run_command( + wallets.schedule_coldkey_swap( + wallet=wallet, + subtensor=self.initialize_chain(network), + new_coldkey_ss58=new_wallet_coldkey_ss58, + force_swap=force_swap, + ) + ) + def stake_list( self, network: Optional[list[str]] = Options.network, From 89b83d21d027c317411c337558c2f8e2ff3864bc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 15:45:39 -0700 Subject: [PATCH 28/96] Adds block calculation --- bittensor_cli/src/commands/wallets.py | 77 +++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 6285402e..4883fb95 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -16,7 +16,7 @@ from rich.padding import Padding from rich.prompt import Confirm -from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src import COLOR_PALETTE, COLORS from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -1510,8 +1510,8 @@ async def schedule_coldkey_swap( prompt = ( "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n" - f"Current ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"New ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{new_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"Current ss58: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]\n" + f"New ss58: [{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]\n" "Are you sure you want to continue?" ) if not Confirm.ask(prompt): @@ -1520,12 +1520,15 @@ async def schedule_coldkey_swap( if not unlock_key(wallet).success: return False - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="schedule_swap_coldkey", - call_params={ - "new_coldkey": new_coldkey_ss58, - }, + block_pre_call, call = await asyncio.gather( + subtensor.substrate.get_block_number(), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="schedule_swap_coldkey", + call_params={ + "new_coldkey": new_coldkey_ss58, + }, + ), ) with console.status(":satellite: Scheduling coldkey swap on-chain..."): @@ -1535,6 +1538,7 @@ async def schedule_coldkey_swap( wait_for_inclusion=True, wait_for_finalization=True, ) + block_post_call = await subtensor.substrate.get_block_number() if not success: print_error(f"Failed to schedule coldkey swap: {err_msg}") @@ -1543,4 +1547,59 @@ async def schedule_coldkey_swap( console.print( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) + + block_num, dest_coldkey = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, + ) + + if block_num is not None: + console.print( + f"\n[green]Coldkey swap details:[/green]" + f"\nBlock number: {block_num}" + f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" + f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" + f"\n\nYou can provide this block number to `btcli wallet swap check`" + ) + else: + console.print( + "[yellow]Warning: Could not find the swap extrinsic in recent blocks" + ) return True + + +async def find_coldkey_swap_extrinsic( + subtensor: SubtensorInterface, + start_block: int, + end_block: int, + wallet_ss58: str, +) -> tuple[Optional[int], Optional[str]]: + """Search for a coldkey swap extrinsic in a range of blocks. + + Args: + subtensor: SubtensorInterface for chain queries + start_block: Starting block number to search + end_block: Ending block number to search (inclusive) + wallet_ss58: SS58 address of the signing wallet + + Returns: + tuple[Optional[int], Optional[str]]: + (block number, destination coldkey ss58) if found, + (None, None) if not found + """ + for block_num in range(start_block, end_block + 1): + block_data = await subtensor.substrate.get_block(block_number=block_num) + for extrinsic in block_data["extrinsics"]: + extrinsic_data = extrinsic.value + if ( + "call" in extrinsic_data + and extrinsic_data["call"].get("call_function") + == "schedule_swap_coldkey" + and extrinsic_data.get("address") == wallet_ss58 + ): + new_coldkey_ss58 = extrinsic_data["call"]["call_args"][0]["value"] + return block_num, new_coldkey_ss58 + + return None, None From f3826b113e0ad432304cf3385b6bdbb5a4bd54ec Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 15:59:53 -0700 Subject: [PATCH 29/96] Adds duration --- bittensor_cli/src/commands/wallets.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 4883fb95..314dc0b4 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -45,6 +45,7 @@ millify_tao, unlock_key, WalletLike, + blocks_to_duration, ) @@ -1548,11 +1549,14 @@ async def schedule_coldkey_swap( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) - block_num, dest_coldkey = await find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=block_pre_call, - end_block=block_post_call, - wallet_ss58=wallet.coldkeypub.ss58_address, + (block_num, dest_coldkey), schedule_duration = await asyncio.gather( + find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, + ), + subtensor.get_coldkey_swap_schedule_duration() ) if block_num is not None: @@ -1561,7 +1565,8 @@ async def schedule_coldkey_swap( f"\nBlock number: {block_num}" f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" - f"\n\nYou can provide this block number to `btcli wallet swap check`" + f"\nThe swap will be completed in [green]{blocks_to_duration(schedule_duration)} (Block: {block_num+schedule_duration})[/green] from now." + f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" ) else: console.print( From 7ab8b5ea99dac61846faa8626ef2d8788e5a7ae2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 16:00:33 -0700 Subject: [PATCH 30/96] Adds get_coldkey_swap_schedule_duration call --- .../src/bittensor/subtensor_interface.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e47cfd56..a19dc8b8 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1526,3 +1526,28 @@ async def get_scheduled_coldkey_swap( async for ss58, _ in result: keys_pending_swap.append(decode_account_id(ss58)) return keys_pending_swap + + async def get_coldkey_swap_schedule_duration( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> int: + """ + Retrieves the duration (in blocks) required for a coldkey swap to be executed. + + Args: + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + int: The number of blocks required for the coldkey swap schedule duration. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapScheduleDuration", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return result From 6b794ca9f74f7e05c38eaa9bf960cf7769479383 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 17:28:32 -0700 Subject: [PATCH 31/96] Updates wallet_check_ck_swap --- bittensor_cli/cli.py | 98 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8bb7691d..38aeb1ab 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -109,6 +109,17 @@ class Options: "--wallet.hotkey", help="Hotkey of the wallet", ) + wallet_ss58_address = typer.Option( + None, + "--wallet-name", + "--name", + "--wallet_name", + "--wallet.name", + "--address", + "--ss58", + "--ss58-address", + help="SS58 address or wallet name to check. Leave empty to be prompted.", + ) wallet_hotkey_ss58 = typer.Option( None, "--hotkey", @@ -687,6 +698,9 @@ def __init__(self): self.wallet_app.command( "swap-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_swap_coldkey) + self.wallet_app.command( + "swap-check", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + )(self.wallet_check_ck_swap) self.wallet_app.command( "regen-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_regen_coldkey) @@ -2309,28 +2323,98 @@ def wallet_new_coldkey( def wallet_check_ck_swap( self, - wallet_name: Optional[str] = Options.wallet_name, + wallet_ss58_address: Optional[str] = Options.wallet_ss58_address, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, + scheduled_block: Optional[int] = typer.Option( + None, + "--block", + help="Block number where the swap was scheduled", + ), + show_all: bool = typer.Option( + False, + "--all", + "-a", + help="Show all pending coldkey swaps", + ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Check the status of your scheduled coldkey swap. + Check the status of scheduled coldkey swaps. USAGE - Users should provide the old coldkey wallet to check the swap status. + This command can be used in three ways: + 1. Show all pending swaps (--all) + 2. Check status of a specific wallet's swap or SS58 address + 3. Check detailed swap status with block number (--block) - EXAMPLE + EXAMPLES + + Show all pending swaps: + [green]$[/green] btcli wallet swap-check --all - [green]$[/green] btcli wallet check_coldkey_swap + Check specific wallet's swap: + [green]$[/green] btcli wallet swap-check --wallet.name my_wallet + + Check swap using SS58 address: + [green]$[/green] btcli wallet swap-check --ss58-address 5DkQ4... + + Check swap details with block number: + [green]$[/green] btcli wallet swap-check --wallet.name my_wallet --block 12345 """ self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) self.initialize_chain(network) - return self._run_command(wallets.check_coldkey_swap(wallet, self.subtensor)) + + if not wallet_ss58_address: + wallet_or_ss58_address = Prompt.ask( + "Enter the [blue]SS58 address[/blue] or the [blue]wallet name[/blue]. [dim]Leave blank to check all pending swaps[/dim]" + ) + if not wallet_or_ss58_address: + show_all = True + else: + if is_valid_ss58_address(wallet_or_ss58_address): + return self._run_command( + wallets.check_swap_status( + self.subtensor, wallet_or_ss58_address, scheduled_block + ) + ) + else: + wallet_name = wallet_or_ss58_address + + if show_all: + return self._run_command( + wallets.check_swap_status(self.subtensor, None, None) + ) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + if not scheduled_block: + block_input = Prompt.ask( + "[blue]Enter the block number[/blue] where the swap was scheduled [dim](optional, press enter to skip)[/dim]", + default="", + ) + if block_input: + try: + scheduled_block = int(block_input) + except ValueError: + console.print( + "[red]Invalid block number. Skipping block check.[/red]" + ) + + return self._run_command( + wallets.check_swap_status( + self.subtensor, wallet.coldkeypub.ss58_address, scheduled_block + ) + ) def wallet_create_wallet( self, From 3b1a9c450a04699403931dc57174e327c80396bc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 17:28:56 -0700 Subject: [PATCH 32/96] Updates wallet check swap --- bittensor_cli/src/commands/wallets.py | 107 +++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 314dc0b4..ca8bb301 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1556,7 +1556,7 @@ async def schedule_coldkey_swap( end_block=block_post_call, wallet_ss58=wallet.coldkeypub.ss58_address, ), - subtensor.get_coldkey_swap_schedule_duration() + subtensor.get_coldkey_swap_schedule_duration(), ) if block_num is not None: @@ -1608,3 +1608,108 @@ async def find_coldkey_swap_extrinsic( return block_num, new_coldkey_ss58 return None, None + + +async def check_swap_status( + subtensor: SubtensorInterface, + origin_ss58: Optional[str] = None, + expected_block_number: Optional[int] = None, +) -> None: + """ + Check the status of a coldkey swap. + + Args: + subtensor: Connection to the network + origin_ss58: The SS58 address of the original coldkey + block_number: Optional block number where the swap was scheduled + """ + scheduled_swaps = await subtensor.get_scheduled_coldkey_swap() + + if not origin_ss58: + if not scheduled_swaps: + console.print("[yellow]No pending coldkey swaps found.[/yellow]") + return + + table = Table( + Column( + "Original Coldkey", + justify="Left", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + no_wrap=True, + ), + Column( + "Status", + style="dark_sea_green3" + ), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swaps\n", + show_header=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, + ) + + for coldkey in scheduled_swaps: + table.add_row( + coldkey, + "Pending" + ) + + console.print(table) + console.print( + "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address[/dim]" + ) + return + + is_pending = origin_ss58 in scheduled_swaps + + if not is_pending: + console.print( + f"[red]No pending swap found for coldkey:[/red] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + return + + console.print( + f"[green]Found pending swap for coldkey:[/green] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + + if expected_block_number is None: + return + + # Find the swap extrinsic details + block_num, dest_coldkey = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=expected_block_number, + end_block=expected_block_number, + wallet_ss58=origin_ss58, + ) + + if block_num is None: + console.print( + f"[yellow]Warning: Could not find swap extrinsic at block {expected_block_number}[/yellow]" + ) + return + + current_block, schedule_duration = await asyncio.gather( + subtensor.substrate.get_block_number(), + subtensor.get_coldkey_swap_schedule_duration(), + ) + + completion_block = block_num + schedule_duration + remaining_blocks = completion_block - current_block + + if remaining_blocks <= 0: + console.print("[green]Swap period has completed![/green]") + return + + console.print( + "\n[green]Coldkey swap details:[/green]" + f"\nScheduled at block: {block_num}" + f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" + f"\nCompletion block: {completion_block}" + f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" + ) From f03f2afcc5e8bf93529dc01a204e0f4e96c73ceb Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 13:36:27 -0700 Subject: [PATCH 33/96] Improves how we fetch events --- bittensor_cli/src/commands/wallets.py | 113 ++++++++++++++------------ 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ca8bb301..2206356a 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -46,6 +46,7 @@ unlock_key, WalletLike, blocks_to_duration, + decode_account_id, ) @@ -1549,39 +1550,36 @@ async def schedule_coldkey_swap( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) - (block_num, dest_coldkey), schedule_duration = await asyncio.gather( - find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=block_pre_call, - end_block=block_post_call, - wallet_ss58=wallet.coldkeypub.ss58_address, - ), - subtensor.get_coldkey_swap_schedule_duration(), + swap_info = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, ) - if block_num is not None: - console.print( - f"\n[green]Coldkey swap details:[/green]" - f"\nBlock number: {block_num}" - f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" - f"\nThe swap will be completed in [green]{blocks_to_duration(schedule_duration)} (Block: {block_num+schedule_duration})[/green] from now." - f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" - ) - else: + if not swap_info: console.print( "[yellow]Warning: Could not find the swap extrinsic in recent blocks" ) return True + console.print( + "\n[green]Coldkey swap details:[/green]" + f"\nBlock number: {swap_info['block_num']}" + f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" + f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" + f"\nThe swap will be completed at block: [green]{swap_info['execution_block']}[/green]" + f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" + ) + async def find_coldkey_swap_extrinsic( subtensor: SubtensorInterface, start_block: int, end_block: int, wallet_ss58: str, -) -> tuple[Optional[int], Optional[str]]: - """Search for a coldkey swap extrinsic in a range of blocks. +) -> dict: + """Search for a coldkey swap event in a range of blocks. Args: subtensor: SubtensorInterface for chain queries @@ -1590,24 +1588,42 @@ async def find_coldkey_swap_extrinsic( wallet_ss58: SS58 address of the signing wallet Returns: - tuple[Optional[int], Optional[str]]: - (block number, destination coldkey ss58) if found, - (None, None) if not found + dict: Contains the following keys if found: + - block_num: Block number where swap was scheduled + - dest_coldkey: SS58 address of destination coldkey + - execution_block: Block number when swap will execute + Empty dict if not found """ - for block_num in range(start_block, end_block + 1): - block_data = await subtensor.substrate.get_block(block_number=block_num) - for extrinsic in block_data["extrinsics"]: - extrinsic_data = extrinsic.value + block_hashes = await asyncio.gather( + *[ + subtensor.substrate.get_block_hash(block_num) + for block_num in range(start_block, end_block + 1) + ] + ) + block_events = await asyncio.gather( + *[ + subtensor.substrate.get_events(block_hash=block_hash) + for block_hash in block_hashes + ] + ) + + for block_num, events in zip(range(start_block, end_block + 1), block_events): + for event in events: if ( - "call" in extrinsic_data - and extrinsic_data["call"].get("call_function") - == "schedule_swap_coldkey" - and extrinsic_data.get("address") == wallet_ss58 + event.get("event", {}).get("module_id") == "SubtensorModule" + and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled" ): - new_coldkey_ss58 = extrinsic_data["call"]["call_args"][0]["value"] - return block_num, new_coldkey_ss58 + attributes = event["event"].get("attributes", {}) + old_coldkey = decode_account_id(attributes["old_coldkey"][0]) - return None, None + if old_coldkey == wallet_ss58: + return { + "block_num": block_num, + "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), + "execution_block": attributes["execution_block"], + } + + return {} async def check_swap_status( @@ -1637,10 +1653,7 @@ async def check_swap_status( style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], no_wrap=True, ), - Column( - "Status", - style="dark_sea_green3" - ), + Column("Status", style="dark_sea_green3"), title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swaps\n", show_header=True, show_edge=False, @@ -1653,10 +1666,7 @@ async def check_swap_status( ) for coldkey in scheduled_swaps: - table.add_row( - coldkey, - "Pending" - ) + table.add_row(coldkey, "Pending") console.print(table) console.print( @@ -1680,26 +1690,21 @@ async def check_swap_status( return # Find the swap extrinsic details - block_num, dest_coldkey = await find_coldkey_swap_extrinsic( + swap_info = await find_coldkey_swap_extrinsic( subtensor=subtensor, start_block=expected_block_number, end_block=expected_block_number, wallet_ss58=origin_ss58, ) - if block_num is None: + if not swap_info: console.print( f"[yellow]Warning: Could not find swap extrinsic at block {expected_block_number}[/yellow]" ) return - current_block, schedule_duration = await asyncio.gather( - subtensor.substrate.get_block_number(), - subtensor.get_coldkey_swap_schedule_duration(), - ) - - completion_block = block_num + schedule_duration - remaining_blocks = completion_block - current_block + current_block = await subtensor.substrate.get_block_number() + remaining_blocks = swap_info["execution_block"] - current_block if remaining_blocks <= 0: console.print("[green]Swap period has completed![/green]") @@ -1707,9 +1712,9 @@ async def check_swap_status( console.print( "\n[green]Coldkey swap details:[/green]" - f"\nScheduled at block: {block_num}" + f"\nScheduled at block: {swap_info['block_num']}" f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" - f"\nCompletion block: {completion_block}" + f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" + f"\nCompletion block: {swap_info['execution_block']}" f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" ) From 35c32da4c7dd40afe37f4fb6666a82b9b4311a10 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 17:13:12 -0700 Subject: [PATCH 34/96] Adds archive node logic --- bittensor_cli/src/__init__.py | 4 ++++ bittensor_cli/src/commands/wallets.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 1ecd29a3..7011dfd4 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -33,6 +33,10 @@ class Constants: "latent-lite": latent_lite_entrypoint, "subvortex": subvortex_entrypoint, } + genesis_block_hash_map = { + "finney": "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03", + "test": "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105", + } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 2206356a..2841a139 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -16,7 +16,7 @@ from rich.padding import Padding from rich.prompt import Confirm -from bittensor_cli.src import COLOR_PALETTE, COLORS +from bittensor_cli.src import COLOR_PALETTE, COLORS, Constants from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -1594,6 +1594,19 @@ async def find_coldkey_swap_extrinsic( - execution_block: Block number when swap will execute Empty dict if not found """ + + current_block, genesis_block = await asyncio.gather( + subtensor.substrate.get_block_number(), + subtensor.substrate.get_block_hash(0) + ) + if ( + current_block - start_block > 300 + and genesis_block == Constants.genesis_block_hash_map["finney"] + ): + console.print("Querying archive node for coldkey swap events...") + await subtensor.substrate.close() + subtensor = SubtensorInterface("archive") + block_hashes = await asyncio.gather( *[ subtensor.substrate.get_block_hash(block_num) From ba05ad22041f5d0dae729e7127fd2523e49fb653 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 17:26:39 -0700 Subject: [PATCH 35/96] Finalise feat --- bittensor_cli/cli.py | 58 ++++++++++++--------------- bittensor_cli/src/commands/wallets.py | 2 +- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 38aeb1ab..4b399834 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2357,45 +2357,42 @@ def wallet_check_ck_swap( [green]$[/green] btcli wallet swap-check --all Check specific wallet's swap: - [green]$[/green] btcli wallet swap-check --wallet.name my_wallet + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet Check swap using SS58 address: - [green]$[/green] btcli wallet swap-check --ss58-address 5DkQ4... + [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... Check swap details with block number: - [green]$[/green] btcli wallet swap-check --wallet.name my_wallet --block 12345 + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet --block 12345 """ self.verbosity_handler(quiet, verbose) self.initialize_chain(network) - if not wallet_ss58_address: - wallet_or_ss58_address = Prompt.ask( - "Enter the [blue]SS58 address[/blue] or the [blue]wallet name[/blue]. [dim]Leave blank to check all pending swaps[/dim]" - ) - if not wallet_or_ss58_address: - show_all = True - else: - if is_valid_ss58_address(wallet_or_ss58_address): - return self._run_command( - wallets.check_swap_status( - self.subtensor, wallet_or_ss58_address, scheduled_block - ) - ) - else: - wallet_name = wallet_or_ss58_address - if show_all: return self._run_command( wallets.check_swap_status(self.subtensor, None, None) ) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH], - validate=WV.WALLET, - ) + if not wallet_ss58_address: + wallet_ss58_address = Prompt.ask( + "Enter [blue]wallet name[/blue] or [blue]SS58 address[/blue] [dim](leave blank to show all pending swaps)[/dim]" + ) + if not wallet_ss58_address: + return self._run_command( + wallets.check_swap_status(self.subtensor, None, None) + ) + + if is_valid_ss58_address(wallet_ss58_address): + ss58_address = wallet_ss58_address + else: + wallet = self.wallet_ask( + wallet_ss58_address, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + ss58_address = wallet.coldkeypub.ss58_address if not scheduled_block: block_input = Prompt.ask( @@ -2406,14 +2403,11 @@ def wallet_check_ck_swap( try: scheduled_block = int(block_input) except ValueError: - console.print( - "[red]Invalid block number. Skipping block check.[/red]" - ) + print_error("Invalid block number") + raise typer.Exit() return self._run_command( - wallets.check_swap_status( - self.subtensor, wallet.coldkeypub.ss58_address, scheduled_block - ) + wallets.check_swap_status(self.subtensor, ss58_address, scheduled_block) ) def wallet_create_wallet( diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 2841a139..dbf9f5f1 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1683,7 +1683,7 @@ async def check_swap_status( console.print(table) console.print( - "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address[/dim]" + "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address and the block number.[/dim]" ) return From f295a9642c493c791f8644735969dbac63f4fac4 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 17:26:59 -0700 Subject: [PATCH 36/96] cleanup --- bittensor_cli/src/commands/wallets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index dbf9f5f1..61d0bcb6 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1702,7 +1702,6 @@ async def check_swap_status( if expected_block_number is None: return - # Find the swap extrinsic details swap_info = await find_coldkey_swap_extrinsic( subtensor=subtensor, start_block=expected_block_number, From 2d3a32cc3f83a6bba731eaeed8fe63888aac16e2 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 20:32:16 -0700 Subject: [PATCH 37/96] add pull command --- tests/e2e_tests/conftest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py index bf993325..ba2c6c01 100644 --- a/tests/e2e_tests/conftest.py +++ b/tests/e2e_tests/conftest.py @@ -14,6 +14,8 @@ from .utils import setup_wallet +LOCALNET_IMAGE_NAME = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" + def wait_for_node_start(process, pattern, timestamp: int = None): for line in process.stdout: @@ -113,6 +115,7 @@ def is_docker_running(): stderr=subprocess.DEVNULL, check=True, ) + subprocess.run(["docker", "pull", LOCALNET_IMAGE_NAME], check=True) return True except subprocess.CalledProcessError: return False @@ -143,7 +146,6 @@ def try_start_docker(): return False container_name = f"test_local_chain_{str(time.time()).replace('.', '_')}" - image_name = "ghcr.io/opentensor/subtensor-localnet:devnet-ready" # Command to start container cmds = [ @@ -156,7 +158,7 @@ def try_start_docker(): "9944:9944", "-p", "9945:9945", - image_name, + LOCALNET_IMAGE_NAME, params, ] From a36cfdb7ec4ae3e82297874a8c1e6a5a3ac6d0a7 Mon Sep 17 00:00:00 2001 From: Roman Date: Thu, 20 Mar 2025 20:58:57 -0700 Subject: [PATCH 38/96] ghcr.io/opentensor/subtensor-localnet:devnet-ready --- .github/workflows/e2e-subtensor-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml index 7e3d4a1d..241abdc7 100644 --- a/.github/workflows/e2e-subtensor-tests.yml +++ b/.github/workflows/e2e-subtensor-tests.yml @@ -48,10 +48,10 @@ jobs: run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin - name: Pull Docker Image - run: docker pull ghcr.io/opentensor/subtensor-localnet:latest + run: docker pull ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Save Docker Image to Cache - run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:latest + run: docker save -o subtensor-localnet.tar ghcr.io/opentensor/subtensor-localnet:devnet-ready - name: Upload Docker Image as Artifact uses: actions/upload-artifact@v4 From 5fbeabb4a79ee46a8e7ed5492e247ffcc4d390fa Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 22:07:11 -0700 Subject: [PATCH 39/96] adds cli cmd --- bittensor_cli/cli.py | 61 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4b399834..547c5bab 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -716,6 +716,9 @@ def __init__(self): self.wallet_app.command( "new-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"] )(self.wallet_new_coldkey) + self.wallet_app.command( + "associate-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"] + )(self.wallet_associate_hotkey) self.wallet_app.command( "create", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"] )(self.wallet_create_wallet) @@ -2265,6 +2268,64 @@ def wallet_new_hotkey( wallets.new_hotkey(wallet, n_words, use_password, uri, overwrite) ) + def wallet_associate_hotkey( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, + network: Optional[list[str]] = Options.network, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + ): + """ + Associate a hotkey with a wallet(coldkey). + + USAGE + + This command is used to associate a hotkey with a wallet(coldkey). + + EXAMPLE + + [green]$[/green] btcli wallet associate-hotkey --hotkey-name hotkey_name + [green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4... + """ + self.verbosity_handler(quiet, verbose) + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue] [dim](which you want to associate with the hotkey)[/dim]", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + if not wallet_hotkey: + hotkey_or_wallet = Prompt.ask( + "Enter the [blue]hotkey[/blue] name or " + "[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]" + ) + if is_valid_ss58_address(hotkey_or_wallet): + hotkey_ss58 = hotkey_or_wallet + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + hotkey_or_wallet, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + hotkey_ss58 = wallet.hotkey.ss58_address + return self._run_command( + wallets.associate_hotkey( + wallet, + self.initialize_chain(network), + hotkey_ss58, + ) + ) + def wallet_new_coldkey( self, wallet_name: Optional[str] = Options.wallet_name, From 0710029e9a93aba741331f93c9956c4707e7aec2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 22:07:45 -0700 Subject: [PATCH 40/96] adds associate_hotkey --- bittensor_cli/src/commands/wallets.py | 76 ++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 61d0bcb6..ceec822a 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -50,6 +50,70 @@ ) +async def associate_hotkey( + wallet: Wallet, + subtensor: SubtensorInterface, + hotkey_ss58: str, +): + """Associates a hotkey with a wallet""" + owner_ss58 = await subtensor.get_hotkey_owner(hotkey_ss58) + if owner_ss58: + if owner_ss58 == wallet.coldkeypub.ss58_address: + console.print( + f":white_heavy_check_mark: Hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] is already " + f"associated with \nwallet [blue]{wallet.name}[/blue], " + f"SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" + ) + return True + else: + owner_wallet = _get_wallet_by_ss58(wallet.path, owner_ss58) + wallet_name = owner_wallet.name if owner_wallet else "unknown wallet" + console.print( + f"[yellow]Warning[/yellow]: Hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] is already associated with \n" + f"wallet: [blue]{wallet_name}[/blue], SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" + ) + return False + else: + console.print( + f"Hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] is not associated with any wallet" + ) + + if not Confirm.ask("Do you want to continue with the association?"): + return False + + if not unlock_key(wallet).success: + return False + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="try_associate_hotkey", + call_params={ + "hotkey": hotkey_ss58, + }, + ) + + with console.status(":satellite: Associating hotkey on-chain..."): + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=False, + ) + + if not success: + console.print( + f"[red]:cross_mark: Failed to associate hotkey: {err_msg}[/red]" + ) + return False + + console.print( + f"[green]:white_heavy_check_mark: Successfully associated hotkey[/green] " + f"[{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] with \nwallet [blue]{wallet.name}[/blue], " + f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]" + ) + return True + + async def regen_coldkey( wallet: Wallet, mnemonic: Optional[str], @@ -257,6 +321,15 @@ def get_coldkey_wallets_for_path(path: str) -> list[Wallet]: return wallets +def _get_wallet_by_ss58(path: str, ss58_address: str) -> Optional[Wallet]: + """Find a wallet by its SS58 address in the given path.""" + ss58_addresses, wallet_names = _get_coldkey_ss58_addresses_for_path(path) + for wallet_name, addr in zip(wallet_names, ss58_addresses): + if addr == ss58_address: + return Wallet(path=path, name=wallet_name) + return None + + def _get_coldkey_ss58_addresses_for_path(path: str) -> tuple[list[str], list[str]]: """Get all coldkey ss58 addresses from path.""" @@ -1596,8 +1669,7 @@ async def find_coldkey_swap_extrinsic( """ current_block, genesis_block = await asyncio.gather( - subtensor.substrate.get_block_number(), - subtensor.substrate.get_block_hash(0) + subtensor.substrate.get_block_number(), subtensor.substrate.get_block_hash(0) ) if ( current_block - start_block > 300 From ba519d737ef9bc3d3cb25b70e9db872fe355c3f5 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 22:47:35 -0700 Subject: [PATCH 41/96] adds e2e test --- tests/e2e_tests/test_wallet_interactions.py | 108 ++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 076f03b3..bfabc776 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -510,3 +510,111 @@ def test_wallet_identities(local_chain, wallet_setup): assert "Message signed successfully" in sign_using_coldkey.stdout print("✅ Passed wallet set-id, get-id, sign command") + + +def test_wallet_associate_hotkey(local_chain, wallet_setup): + """ + Test the associating hotkeys and their different cases. + + Steps: + 1. Create wallets for Alice, Bob, and Charlie + 2. Associate a hotkey with Alice's wallet using hotkey name + 3. Verify the association is successful + 4. Try to associate Alice's hotkey with Bob's wallet (should fail) + 5. Try to associate Alice's hotkey again (should show already associated) + 6. Associate Charlie's hotkey with Bob's wallet using SS58 address + + Raises: + AssertionError: If any of the checks or verifications fail + """ + print("Testing wallet associate-hotkey command 🧪") + + _, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup("//Alice") + _, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup("//Bob") + _, wallet_charlie, _, _ = wallet_setup("//Charlie") + + # Associate Alice's default hotkey with her wallet + result = exec_command_alice( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--no-prompt", + ], + ) + + # Assert successful association + assert "Successfully associated hotkey" in result.stdout + assert wallet_alice.hotkey.ss58_address in result.stdout + assert wallet_alice.coldkeypub.ss58_address in result.stdout + + # Try to associate Alice's hotkey with Bob's wallet (should fail) + result = exec_command_bob( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_bob.name, + "--hotkey-ss58", + wallet_alice.hotkey.ss58_address, + "--no-prompt", + ], + ) + + assert "Warning" in result.stdout + assert "is already associated with" in result.stdout + + # Try to associate Alice's hotkey again with Alice's wallet + result = exec_command_alice( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--wallet-hotkey", + wallet_alice.hotkey_str, + "--no-prompt", + ], + ) + + assert "is already associated with" in result.stdout + assert "wallet" in result.stdout + assert wallet_alice.name in result.stdout + + # Associate Charlie's hotkey with Bob's wallet using SS58 address + result = exec_command_bob( + command="wallet", + sub_command="associate-hotkey", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_bob.name, + "--hotkey-ss58", + wallet_charlie.hotkey.ss58_address, + "--no-prompt", + ], + ) + + assert "Successfully associated hotkey" in result.stdout + assert wallet_charlie.hotkey.ss58_address in result.stdout + assert wallet_bob.coldkeypub.ss58_address in result.stdout + + print("✅ Passed wallet associate-hotkey command") From fbb721e725a27f68879c15d334d1faacb61859da Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 22:55:33 -0700 Subject: [PATCH 42/96] Updates --- bittensor_cli/cli.py | 45 ++++++++++++++------------- bittensor_cli/src/commands/wallets.py | 5 ++- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 547c5bab..e7564767 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2274,6 +2274,7 @@ def wallet_associate_hotkey( wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58, network: Optional[list[str]] = Options.network, + prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): @@ -2296,33 +2297,33 @@ def wallet_associate_hotkey( default=self.config.get("wallet_name") or defaults.wallet.name, ) if not wallet_hotkey: - hotkey_or_wallet = Prompt.ask( + wallet_hotkey = Prompt.ask( "Enter the [blue]hotkey[/blue] name or " "[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]" ) - if is_valid_ss58_address(hotkey_or_wallet): - hotkey_ss58 = hotkey_or_wallet - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH], - validate=WV.WALLET, - ) - else: - wallet = self.wallet_ask( - wallet_name, - wallet_path, - hotkey_or_wallet, - ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], - validate=WV.WALLET_AND_HOTKEY, - ) - hotkey_ss58 = wallet.hotkey.ss58_address + + if is_valid_ss58_address(wallet_hotkey): + hotkey_ss58 = wallet_hotkey + wallet = self.wallet_ask( + wallet_name, + wallet_path, + None, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + else: + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + hotkey_ss58 = wallet.hotkey.ss58_address + return self._run_command( wallets.associate_hotkey( - wallet, - self.initialize_chain(network), - hotkey_ss58, + wallet, self.initialize_chain(network), hotkey_ss58, prompt ) ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ceec822a..b39ddb6f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -54,6 +54,7 @@ async def associate_hotkey( wallet: Wallet, subtensor: SubtensorInterface, hotkey_ss58: str, + prompt: bool = False, ): """Associates a hotkey with a wallet""" owner_ss58 = await subtensor.get_hotkey_owner(hotkey_ss58) @@ -78,7 +79,9 @@ async def associate_hotkey( f"Hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] is not associated with any wallet" ) - if not Confirm.ask("Do you want to continue with the association?"): + if prompt and not Confirm.ask( + "Do you want to continue with the association?" + ): return False if not unlock_key(wallet).success: From cc801153aba8934fab38e528e1862f6e96488eb3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 21 Mar 2025 11:44:15 -0700 Subject: [PATCH 43/96] add hk display --- bittensor_cli/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e7564767..dbf13be3 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2302,6 +2302,7 @@ def wallet_associate_hotkey( "[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]" ) + hotkey_display = None if is_valid_ss58_address(wallet_hotkey): hotkey_ss58 = wallet_hotkey wallet = self.wallet_ask( @@ -2311,6 +2312,9 @@ def wallet_associate_hotkey( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) + hotkey_display = ( + f"hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}]" + ) else: wallet = self.wallet_ask( wallet_name, @@ -2320,10 +2324,15 @@ def wallet_associate_hotkey( validate=WV.WALLET_AND_HOTKEY, ) hotkey_ss58 = wallet.hotkey.ss58_address + hotkey_display = f"hotkey [blue]{wallet_hotkey}[/blue] [{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" return self._run_command( wallets.associate_hotkey( - wallet, self.initialize_chain(network), hotkey_ss58, prompt + wallet, + self.initialize_chain(network), + hotkey_ss58, + hotkey_display, + prompt, ) ) From 5a36b2149be36c70d1d31eba61c594067c22b6da Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 21 Mar 2025 11:44:29 -0700 Subject: [PATCH 44/96] update func --- bittensor_cli/src/commands/wallets.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b39ddb6f..cde31859 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -54,14 +54,16 @@ async def associate_hotkey( wallet: Wallet, subtensor: SubtensorInterface, hotkey_ss58: str, + hotkey_display: str, prompt: bool = False, ): """Associates a hotkey with a wallet""" + owner_ss58 = await subtensor.get_hotkey_owner(hotkey_ss58) if owner_ss58: if owner_ss58 == wallet.coldkeypub.ss58_address: console.print( - f":white_heavy_check_mark: Hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] is already " + f":white_heavy_check_mark: {hotkey_display} is already " f"associated with \nwallet [blue]{wallet.name}[/blue], " f"SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" ) @@ -70,18 +72,14 @@ async def associate_hotkey( owner_wallet = _get_wallet_by_ss58(wallet.path, owner_ss58) wallet_name = owner_wallet.name if owner_wallet else "unknown wallet" console.print( - f"[yellow]Warning[/yellow]: Hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] is already associated with \n" + f"[yellow]Warning[/yellow]: {hotkey_display} is already associated with \n" f"wallet: [blue]{wallet_name}[/blue], SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" ) return False else: - console.print( - f"Hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] is not associated with any wallet" - ) + console.print(f"{hotkey_display} is not associated with any wallet") - if prompt and not Confirm.ask( - "Do you want to continue with the association?" - ): + if prompt and not Confirm.ask("Do you want to continue with the association?"): return False if not unlock_key(wallet).success: @@ -110,8 +108,8 @@ async def associate_hotkey( return False console.print( - f"[green]:white_heavy_check_mark: Successfully associated hotkey[/green] " - f"[{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}] with \nwallet [blue]{wallet.name}[/blue], " + f"[green]:white_heavy_check_mark: Successfully associated {hotkey_display} with \n" + f"wallet [blue]{wallet.name}[/blue], " f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]" ) return True From b7699bf22b11f55f66a826219a858ef621d61908 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 21 Mar 2025 11:45:41 -0700 Subject: [PATCH 45/96] update test --- tests/e2e_tests/test_wallet_interactions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index bfabc776..19cdcc22 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -554,6 +554,7 @@ def test_wallet_associate_hotkey(local_chain, wallet_setup): assert "Successfully associated hotkey" in result.stdout assert wallet_alice.hotkey.ss58_address in result.stdout assert wallet_alice.coldkeypub.ss58_address in result.stdout + assert wallet_alice.hotkey_str in result.stdout # Try to associate Alice's hotkey with Bob's wallet (should fail) result = exec_command_bob( From 274de24c8ff7aac5aa13157ae2553fba1b3ec85d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Fri, 21 Mar 2025 11:49:22 -0700 Subject: [PATCH 46/96] update capatalize --- bittensor_cli/src/commands/wallets.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index cde31859..333e5e0f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -63,7 +63,7 @@ async def associate_hotkey( if owner_ss58: if owner_ss58 == wallet.coldkeypub.ss58_address: console.print( - f":white_heavy_check_mark: {hotkey_display} is already " + f":white_heavy_check_mark: {hotkey_display.capitalize()} is already " f"associated with \nwallet [blue]{wallet.name}[/blue], " f"SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" ) @@ -72,12 +72,14 @@ async def associate_hotkey( owner_wallet = _get_wallet_by_ss58(wallet.path, owner_ss58) wallet_name = owner_wallet.name if owner_wallet else "unknown wallet" console.print( - f"[yellow]Warning[/yellow]: {hotkey_display} is already associated with \n" + f"[yellow]Warning[/yellow]: {hotkey_display.capitalize()} is already associated with \n" f"wallet: [blue]{wallet_name}[/blue], SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]" ) return False else: - console.print(f"{hotkey_display} is not associated with any wallet") + console.print( + f"{hotkey_display.capitalize()} is not associated with any wallet" + ) if prompt and not Confirm.ask("Do you want to continue with the association?"): return False @@ -108,7 +110,7 @@ async def associate_hotkey( return False console.print( - f"[green]:white_heavy_check_mark: Successfully associated {hotkey_display} with \n" + f":white_heavy_check_mark: Successfully associated {hotkey_display} with \n" f"wallet [blue]{wallet.name}[/blue], " f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]" ) From 801496f606c1c2d370eb29d76db4fbe0b47accad Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 26 Mar 2025 17:38:29 -0700 Subject: [PATCH 47/96] fix staking --- tests/e2e_tests/test_staking_sudo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index f8bfc6d0..deaf1b69 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -152,7 +152,7 @@ def test_staking(local_chain, wallet_setup): "--chain", "ws://127.0.0.1:9945", "--amount", - "100", + str(float(stake_added) - 5), "--tolerance", "0.1", "--partial", From c03e82a78c6510abac00c7096a9cebdba07fa56c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 26 Mar 2025 17:43:39 -0700 Subject: [PATCH 48/96] fix unstaking --- tests/e2e_tests/test_unstaking.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 7b6b836f..91eb406e 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -208,21 +208,11 @@ def test_unstaking(local_chain, wallet_setup): cleaned_stake = [ re.sub(r"\s+", " ", line) for line in stake_list.stdout.splitlines() ] - stake_after_unstaking_netuid_2 = cleaned_stake[9].split("│")[3].strip().split()[0] + stake_after_unstaking_netuid_2 = cleaned_stake[10].split("│")[3].strip().split()[0] assert Balance.from_tao(float(stake_after_unstaking_netuid_2)) <= Balance.from_tao( float(inital_stake_netuid_2) ) - print( - "Bob wallet deets: ", - "hotkey: ", - wallet_bob.hotkey_str, - "name: ", - wallet_bob.name, - "path: ", - wallet_bob.path, - ) - # Remove all alpha stakes unstake_alpha = exec_command_bob( command="stake", From c82eb08e95a36b2daba6881596df5d20c009e64d Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 26 Mar 2025 17:50:21 -0700 Subject: [PATCH 49/96] Update --- tests/e2e_tests/test_staking_sudo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index deaf1b69..5ab21460 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -136,7 +136,7 @@ def test_staking(local_chain, wallet_setup): stake_added = cleaned_stake[8].split("│")[3].strip().split()[0] assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(90) - # Execute remove_stake command and remove all 100 TAO from Alice + # Execute remove_stake command and remove all alpha stakes from Alice remove_stake = exec_command_alice( command="stake", sub_command="remove", @@ -152,7 +152,7 @@ def test_staking(local_chain, wallet_setup): "--chain", "ws://127.0.0.1:9945", "--amount", - str(float(stake_added) - 5), + str(float(stake_added) - 1), "--tolerance", "0.1", "--partial", From ff0466bf70af7bcb4e4de98dd32f4163575b05d3 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 27 Mar 2025 17:55:30 -0700 Subject: [PATCH 50/96] Removes name conflict --- 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 dbf13be3..2df8721b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4747,7 +4747,7 @@ def subnets_create( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, subnet_name: Optional[str] = typer.Option( - None, "--subnet-name", "--name", help="Name of the subnet" + None, "--subnet-name", help="Name of the subnet" ), github_repo: Optional[str] = typer.Option( None, "--github-repo", "--repo", help="GitHub repository URL" From 87abec726911edaf5c5510ad890173c970dab85c Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 27 Mar 2025 17:58:55 -0700 Subject: [PATCH 51/96] update to use price var --- bittensor_cli/src/commands/subnets/price.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index ccc71997..fa05b946 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -804,9 +804,7 @@ def color_label(text): fig.plot( block_numbers, - data[ - "prices" - ], # TODO should this use `prices` instead of `data["prices"]`? + prices, label=f"Subnet {netuid} Price", interp="linear", lc="bae98f", From bb01ef722d78d18c218dcc31dfd6ba453d2b3081 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 27 Mar 2025 18:13:09 -0700 Subject: [PATCH 52/96] update tests --- tests/e2e_tests/test_staking_sudo.py | 2 +- tests/e2e_tests/test_unstaking.py | 4 ++-- tests/e2e_tests/test_wallet_interactions.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 5ab21460..9630ea19 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -52,7 +52,7 @@ def test_staking(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet", "--repo", "https://github.com/username/repo", diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 91eb406e..5e9d13c9 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -46,7 +46,7 @@ def test_unstaking(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet 2", "--repo", "https://github.com/username/repo", @@ -78,7 +78,7 @@ def test_unstaking(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet 3", "--repo", "https://github.com/username/repo", diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 19cdcc22..02af15c7 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -57,7 +57,7 @@ def test_wallet_overview_inspect(local_chain, wallet_setup): wallet.name, "--wallet-hotkey", wallet.hotkey_str, - "--name", + "--subnet-name", "Test Subnet", "--repo", "https://github.com/username/repo", @@ -372,7 +372,7 @@ def test_wallet_identities(local_chain, wallet_setup): wallet_alice.name, "--wallet-hotkey", wallet_alice.hotkey_str, - "--name", + "--subnet-name", "Test Subnet", "--repo", "https://github.com/username/repo", From 3abe6d04a35f7d57016c7a4909b2984210f6bf09 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 28 Mar 2025 15:08:07 +0200 Subject: [PATCH 53/96] Allows user to set era period in all stake transactions. Still need to add tests. --- bittensor_cli/cli.py | 14 ++++++++++++++ bittensor_cli/src/commands/stake/add.py | 7 ++++--- bittensor_cli/src/commands/stake/move.py | 9 ++++++--- bittensor_cli/src/commands/stake/remove.py | 17 +++++++++++++---- tests/e2e_tests/test_senate.py | 7 ++++--- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index dbf13be3..ff3dd6a4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -279,6 +279,9 @@ class Options: "--dashboard.path", help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.", ) + era = typer.Option( + 3, help="Length (in blocks) for which the transaction should be valid." + ) def list_prompt(init_var: list, list_type: type, help_text: str) -> list: @@ -3112,6 +3115,7 @@ def stake_add( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, + era: int = Options.era, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3305,6 +3309,7 @@ def stake_add( safe_staking, rate_tolerance, allow_partial_stake, + era, ) ) @@ -3356,6 +3361,7 @@ def stake_remove( rate_tolerance: Optional[float] = Options.rate_tolerance, safe_staking: Optional[bool] = Options.safe_staking, allow_partial_stake: Optional[bool] = Options.allow_partial_stake, + era: int = Options.era, prompt: bool = Options.prompt, interactive: bool = typer.Option( False, @@ -3545,6 +3551,7 @@ def stake_remove( include_hotkeys=include_hotkeys, exclude_hotkeys=exclude_hotkeys, prompt=prompt, + era=era, ) ) elif ( @@ -3599,6 +3606,7 @@ def stake_remove( safe_staking=safe_staking, rate_tolerance=rate_tolerance, allow_partial_stake=allow_partial_stake, + era=era, ) ) @@ -3626,6 +3634,7 @@ def stake_move( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + era: int = Options.era, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3753,6 +3762,7 @@ def stake_move( destination_hotkey=destination_hotkey, amount=amount, stake_all=stake_all, + era=era, interactive_selection=interactive_selection, prompt=prompt, ) @@ -3790,6 +3800,7 @@ def stake_transfer( stake_all: bool = typer.Option( False, "--stake-all", "--all", help="Stake all", prompt=False ), + era: int = Options.era, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -3910,6 +3921,7 @@ def stake_transfer( dest_netuid=dest_netuid, dest_coldkey_ss58=dest_ss58, amount=amount, + era=era, interactive_selection=interactive_selection, stake_all=stake_all, prompt=prompt, @@ -3948,6 +3960,7 @@ def stake_swap( "--all", help="Swap all available stake", ), + era: int = Options.era, prompt: bool = Options.prompt, wait_for_inclusion: bool = Options.wait_for_inclusion, wait_for_finalization: bool = Options.wait_for_finalization, @@ -4010,6 +4023,7 @@ def stake_swap( destination_netuid=dest_netuid, amount=amount, swap_all=swap_all, + era=era, interactive_selection=interactive_selection, prompt=prompt, wait_for_inclusion=wait_for_inclusion, diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 2451d51b..1949c77c 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -38,6 +38,7 @@ async def stake_add( safe_staking: bool, rate_tolerance: float, allow_partial_stake: bool, + era: int, ): """ Args: @@ -53,6 +54,7 @@ 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 + era: Blocks for which the transaction should be valid. Returns: bool: True if stake operation is successful, False otherwise @@ -86,7 +88,7 @@ async def safe_stake_extrinsic( }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) try: response = await subtensor.substrate.submit_extrinsic( @@ -105,7 +107,6 @@ async def safe_stake_extrinsic( err_out(f"\n{failure_prelude} with error: {format_error_message(e)}") return 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)}" @@ -166,7 +167,7 @@ async def stake_extrinsic( }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) try: response = await subtensor.substrate.submit_extrinsic( diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index 403dc966..f67ac305 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -444,6 +444,7 @@ async def move_stake( destination_hotkey: str, amount: float, stake_all: bool, + era: int, interactive_selection: bool = False, prompt: bool = True, ): @@ -563,7 +564,7 @@ async def move_stake( }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( extrinsic, wait_for_inclusion=True, wait_for_finalization=False @@ -622,6 +623,7 @@ async def transfer_stake( origin_netuid: int, dest_netuid: int, dest_coldkey_ss58: str, + era: int, interactive_selection: bool = False, stake_all: bool = False, prompt: bool = True, @@ -747,7 +749,7 @@ async def transfer_stake( ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( @@ -798,6 +800,7 @@ async def swap_stake( destination_netuid: int, amount: float, swap_all: bool = False, + era: int = 3, interactive_selection: bool = False, prompt: bool = True, wait_for_inclusion: bool = True, @@ -917,7 +920,7 @@ async def swap_stake( ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 898af9cf..3488546a 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -41,6 +41,7 @@ async def unstake( safe_staking: bool, rate_tolerance: float, allow_partial_stake: bool, + era: int, ): """Unstake from hotkey(s).""" with console.status( @@ -302,6 +303,7 @@ async def unstake( current_stake=op["current_stake_balance"], hotkey_ss58=op["hotkey_ss58"], status=status, + era=era, ) else: await _safe_unstake_extrinsic( @@ -313,6 +315,7 @@ async def unstake( price_limit=op["price_with_tolerance"], allow_partial_stake=allow_partial_stake, status=status, + era=era, ) else: for op in unstake_operations: @@ -324,6 +327,7 @@ async def unstake( current_stake=op["current_stake_balance"], hotkey_ss58=op["hotkey_ss58"], status=status, + era=era, ) console.print( f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed." @@ -338,6 +342,7 @@ async def unstake_all( all_hotkeys: bool = False, include_hotkeys: Optional[list[str]] = None, exclude_hotkeys: Optional[list[str]] = None, + era: int = 3, prompt: bool = True, ) -> bool: """Unstakes all stakes from all hotkeys in all subnets.""" @@ -512,6 +517,7 @@ async def unstake_all( hotkey_name=hotkey_names.get(hotkey_ss58, hotkey_ss58), unstake_all_alpha=unstake_all_alpha, status=status, + era=era, ) @@ -524,6 +530,7 @@ async def _unstake_extrinsic( current_stake: Balance, hotkey_ss58: str, status=None, + era: int = 3, ) -> None: """Execute a standard unstake extrinsic. @@ -535,6 +542,7 @@ async def _unstake_extrinsic( wallet: Wallet instance subtensor: Subtensor interface status: Optional status for console updates + era: blocks for which the transaction is valid """ err_out = partial(print_error, status=status) failure_prelude = ( @@ -557,7 +565,7 @@ async def _unstake_extrinsic( }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) try: @@ -607,6 +615,7 @@ async def _safe_unstake_extrinsic( price_limit: Balance, allow_partial_stake: bool, status=None, + era: int = 3, ) -> None: """Execute a safe unstake extrinsic with price limit. @@ -655,7 +664,7 @@ async def _safe_unstake_extrinsic( ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce + call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} ) try: @@ -720,6 +729,7 @@ async def _unstake_all_extrinsic( hotkey_name: str, unstake_all_alpha: bool, status=None, + era: int = 3, ) -> None: """Execute an unstake all extrinsic. @@ -770,8 +780,7 @@ async def _unstake_all_extrinsic( try: response = await subtensor.substrate.submit_extrinsic( extrinsic=await subtensor.substrate.create_signed_extrinsic( - call=call, - keypair=wallet.coldkey, + call=call, keypair=wallet.coldkey, era={"period": era} ), wait_for_inclusion=True, wait_for_finalization=False, diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index dfd2cfa5..d2badc69 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -222,9 +222,10 @@ def test_senate(local_chain, wallet_setup): assert proposals_after_nay_output[9].split()[4] == "1" # Assert Alice has voted Nay - assert proposals_after_nay_output[10].split()[0].strip( - ":" - ) == wallet_alice.hotkey.ss58_address + assert ( + proposals_after_nay_output[10].split()[0].strip(":") + == wallet_alice.hotkey.ss58_address + ) # Assert vote casted as Nay assert proposals_after_nay_output[10].split()[1] == "Nay" From 9dc1b99369dfcdb5cab214350e0d1722073a97f3 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 31 Mar 2025 16:30:56 +0200 Subject: [PATCH 54/96] Registration. --- bittensor_cli/cli.py | 24 +++++++++++----- .../src/bittensor/extrinsics/registration.py | 28 ++++++++++++++++--- .../src/bittensor/extrinsics/transfer.py | 3 +- .../src/bittensor/subtensor_interface.py | 6 +++- bittensor_cli/src/commands/subnets/subnets.py | 7 ++++- bittensor_cli/src/commands/wallets.py | 2 ++ 6 files changed, 56 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ff3dd6a4..0753c147 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -279,7 +279,7 @@ class Options: "--dashboard.path", help="Path to save the dashboard HTML file. For example: `~/.bittensor/dashboard`.", ) - era = typer.Option( + era: int = typer.Option( 3, help="Length (in blocks) for which the transaction should be valid." ) @@ -1752,6 +1752,7 @@ def wallet_transfer( transfer_all: bool = typer.Option( False, "--all", prompt=False, help="Transfer all available balance." ), + era: int = Options.era, wallet_name: str = Options.wallet_name, wallet_path: str = Options.wallet_path, wallet_hotkey: str = Options.wallet_hotkey, @@ -1800,12 +1801,13 @@ def wallet_transfer( amount = FloatPrompt.ask("Enter amount (in TAO) to transfer.") return self._run_command( wallets.transfer( - wallet, - subtensor, - destination_ss58_address, - amount, - transfer_all, - prompt, + wallet=wallet, + subtensor=subtensor, + destination=destination_ss58_address, + amount=amount, + transfer_all=transfer_all, + era=era, + prompt=prompt, ) ) @@ -5055,6 +5057,13 @@ def subnets_register( wallet_hotkey: str = Options.wallet_hotkey, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, + era: Optional[ + int + ] = typer.Option( # Should not be Options.era bc this needs to be an Optional[int] + None, + help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " + "use an era for this transaction that you may pay a different fee to register than the one stated.", + ), prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5083,6 +5092,7 @@ def subnets_register( wallet, self.initialize_chain(network), netuid, + era, prompt, ) ) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index 65d8f348..bcd809bd 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -676,6 +676,7 @@ async def burned_register_extrinsic( old_balance: Balance, wait_for_inclusion: bool = True, wait_for_finalization: bool = True, + era: Optional[int] = None, prompt: bool = False, ) -> bool: """Registers the wallet to chain by recycling TAO. @@ -704,13 +705,32 @@ async def burned_register_extrinsic( my_uid = await subtensor.query( "SubtensorModule", "Uids", [netuid, wallet.hotkey.ss58_address] ) + block_hash = await subtensor.substrate.get_chain_head() print_verbose("Checking if already registered", status) neuron = await subtensor.neuron_for_uid( - uid=my_uid, - netuid=netuid, - block_hash=subtensor.substrate.last_block_hash, + uid=my_uid, netuid=netuid, block_hash=block_hash ) + if not era: + current_block, tempo, blocks_since_last_step = await asyncio.gather( + subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_hyperparameter( + "Tempo", netuid=netuid, block_hash=block_hash + ), + subtensor.query( + "SubtensorModule", + "BlocksSinceLastStep", + [netuid], + block_hash=block_hash, + ), + ) + validity_period = tempo - blocks_since_last_step + era_ = { + "period": validity_period, + "current": current_block, + } + else: + era_ = {"period": era} if not neuron.is_null: console.print( @@ -734,7 +754,7 @@ async def burned_register_extrinsic( }, ) success, err_msg = await subtensor.sign_and_send_extrinsic( - call, wallet, wait_for_inclusion, wait_for_finalization + call, wallet, wait_for_inclusion, wait_for_finalization, era=era_ ) if not success: diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 5a167f0b..0deefc46 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -24,6 +24,7 @@ async def transfer_extrinsic( wallet: Wallet, destination: str, amount: Balance, + era: int = 3, transfer_all: bool = False, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, @@ -84,7 +85,7 @@ async def do_transfer() -> tuple[bool, str, str]: call_params={"dest": destination, "value": amount.rao}, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + call=call, keypair=wallet.coldkey, era={"period": era} ) response = await subtensor.substrate.submit_extrinsic( extrinsic, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index a19dc8b8..ecd58f7c 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1048,6 +1048,7 @@ async def sign_and_send_extrinsic( wallet: Wallet, wait_for_inclusion: bool = True, wait_for_finalization: bool = False, + era: Optional[dict[str, int]] = None, ) -> tuple[bool, str]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1059,8 +1060,11 @@ async def sign_and_send_extrinsic( :return: (success, error message) """ + call_args = {"call": call, "keypair": wallet.coldkey} + if era is not None: + call_args["era"] = era extrinsic = await self.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey + **call_args ) # sign with coldkey try: response = await self.substrate.submit_extrinsic( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 81043d8c..174622a3 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1436,7 +1436,11 @@ async def pow_register( async def register( - wallet: Wallet, subtensor: "SubtensorInterface", netuid: int, prompt: bool + wallet: Wallet, + subtensor: "SubtensorInterface", + netuid: int, + era: Optional[int], + prompt: bool, ): """Register neuron by recycling some TAO.""" @@ -1534,6 +1538,7 @@ async def register( netuid=netuid, prompt=False, old_balance=balance, + era=era, ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 333e5e0f..28cb7130 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1188,6 +1188,7 @@ async def transfer( destination: str, amount: float, transfer_all: bool, + era: int, prompt: bool, ): """Transfer token of amount to destination.""" @@ -1197,6 +1198,7 @@ async def transfer( destination=destination, amount=Balance.from_tao(amount), transfer_all=transfer_all, + era=era, prompt=prompt, ) From 46db5b2cd2be3b3469f79643afcc3694556c022d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 31 Mar 2025 20:25:51 +0200 Subject: [PATCH 55/96] Fix tests --- bittensor_cli/src/commands/stake/add.py | 5 ++++- tests/e2e_tests/test_staking_sudo.py | 6 +++++- tests/e2e_tests/test_unstaking.py | 10 ++++++++++ tests/e2e_tests/test_wallet_interactions.py | 2 ++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 1949c77c..79d6fd3c 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -88,7 +88,10 @@ async def safe_stake_extrinsic( }, ) extrinsic = await subtensor.substrate.create_signed_extrinsic( - call=call, keypair=wallet.coldkey, nonce=next_nonce, era={"period": era} + call=call, + keypair=wallet.coldkey, + nonce=next_nonce, + era={"period": era}, ) try: response = await subtensor.substrate.submit_extrinsic( diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 5ab21460..0f1d817a 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -112,9 +112,11 @@ def test_staking(local_chain, wallet_setup): "0.1", "--partial", "--no-prompt", + "--era", + "144", ], ) - assert "✅ Finalized" in add_stake.stdout + assert "✅ Finalized" in add_stake.stdout, add_stake.stderr # Execute stake show for Alice's wallet show_stake = exec_command_alice( @@ -157,6 +159,8 @@ def test_staking(local_chain, wallet_setup): "0.1", "--partial", "--no-prompt", + "--era", + "144", ], ) assert "✅ Finalized" in remove_stake.stdout diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 91eb406e..bf29d185 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -139,6 +139,8 @@ def test_unstaking(local_chain, wallet_setup): "--partial", "--tolerance", "0.5", + "--era", + "144", ], ) assert "✅ Finalized" in stake_result.stdout @@ -184,6 +186,8 @@ def test_unstaking(local_chain, wallet_setup): "--partial", "--tolerance", "0.5", + "--era", + "144", ], ) assert "✅ Finalized" in partial_unstake_netuid_2.stdout @@ -229,6 +233,8 @@ def test_unstaking(local_chain, wallet_setup): "--all-alpha", "--no-prompt", "--verbose", + "--era", + "144", ], ) @@ -258,6 +264,8 @@ def test_unstaking(local_chain, wallet_setup): "--partial", "--tolerance", "0.5", + "--era", + "144", ], ) assert "✅ Finalized" in stake_result.stdout @@ -277,6 +285,8 @@ def test_unstaking(local_chain, wallet_setup): "ws://127.0.0.1:9945", "--all", "--no-prompt", + "--era", + "144", ], ) assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 19cdcc22..3d08591c 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -245,6 +245,8 @@ def test_wallet_transfer(local_chain, wallet_setup): "--amount", "100", "--no-prompt", + "--era", + "144", ], ) From 9703584b52a84d66285d522fc0369919344a9692 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 1 Apr 2025 16:12:48 +0200 Subject: [PATCH 56/96] Ruff --- bittensor_cli/src/commands/stake/remove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index c807f10c..9130e6c5 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -307,7 +307,7 @@ async def unstake( "amount": op["amount_to_unstake"], "hotkey_ss58": op["hotkey_ss58"], "status": status, - "era": era + "era": era, } if safe_staking and op["netuid"] != 0: From 337c04ed927964ddac9758bf7e1f63171fd0e111 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 1 Apr 2025 17:09:29 +0200 Subject: [PATCH 57/96] temporary --- bittensor_cli/cli.py | 2 +- tests/e2e_tests/test_senate.py | 15 +++++++++++++++ tests/e2e_tests/test_staking_sudo.py | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 29c0d559..77ed377d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4545,7 +4545,7 @@ def sudo_proposals( """ self.verbosity_handler(quiet, verbose, json_output) return self._run_command( - sudo.proposals(self.initialize_chain(network), verbose) + sudo.proposals(self.initialize_chain(network), verbose, json_output) ) def sudo_senate_vote( diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index d2badc69..95ea695a 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -8,6 +8,7 @@ """ import asyncio +import json from .utils import call_add_proposal @@ -103,6 +104,7 @@ def test_senate(local_chain, wallet_setup): "--verbose", ], ) + print(">>>", proposals.stdout, proposals.stderr) proposals_output = proposals.stdout.splitlines()[9].split() # Assert the hash is of correct format @@ -118,6 +120,19 @@ def test_senate(local_chain, wallet_setup): # Assert initial threshold is 3 assert proposals_output[1] == "3" + json_proposals = exec_command_bob( + command="sudo", + sub_command="proposals", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--json-output" + ] + ) + json_proposals_outout = json.loads(json_proposals.stdout) + + assert False, json_proposals_outout + # Vote on the proposal by Bob (vote aye) vote_aye = exec_command_bob( command="sudo", diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index a3c5dae2..a7e691f8 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -138,6 +138,20 @@ def test_staking(local_chain, wallet_setup): stake_added = cleaned_stake[8].split("│")[3].strip().split()[0] assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(90) + show_stake_json = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--chain", + "ws://127.0.0.1:9945", + "--json-output" + ], + ) + # Execute remove_stake command and remove all alpha stakes from Alice remove_stake = exec_command_alice( command="stake", From 512a2951da9aa3ce1ee50c965421b4234b5d8b59 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 1 Apr 2025 18:44:12 +0200 Subject: [PATCH 58/96] Test senate --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/sudo.py | 6 +-- tests/e2e_tests/test_senate.py | 55 +++++++++++++++++++++++----- tests/e2e_tests/test_staking_sudo.py | 2 +- 4 files changed, 51 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 77ed377d..f558da3e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1116,7 +1116,7 @@ def verbosity_handler( if quiet and verbose: err_console.print("Cannot specify both `--quiet` and `--verbose`") raise typer.Exit() - if json_console and verbose: + if json_output and verbose: verbosity_console_handler(3) elif json_output or quiet: verbosity_console_handler(0) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 6c9390cb..95d3f3ac 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -355,13 +355,13 @@ def display_votes( def serialize_vote_data( vote_data: "ProposalVoteData", delegate_info: dict[str, DelegatesDetails] ) -> list[dict[str, bool]]: - vote_list = [] + vote_list = {} for address in vote_data.ayes: f_add = delegate_info[address].display if address in delegate_info else address - vote_list.append({f_add: True}) + vote_list[f_add] = True for address in vote_data.nays: f_add = delegate_info[address].display if address in delegate_info else address - vote_list.append({f_add: False}) + vote_list[f_add] = False return vote_list diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py index 95ea695a..9a295557 100644 --- a/tests/e2e_tests/test_senate.py +++ b/tests/e2e_tests/test_senate.py @@ -9,6 +9,7 @@ import asyncio import json + from .utils import call_add_proposal @@ -104,7 +105,6 @@ def test_senate(local_chain, wallet_setup): "--verbose", ], ) - print(">>>", proposals.stdout, proposals.stderr) proposals_output = proposals.stdout.splitlines()[9].split() # Assert the hash is of correct format @@ -114,7 +114,7 @@ def test_senate(local_chain, wallet_setup): # 0 Ayes for the proposal assert proposals_output[2] == "0" - # 0 Nayes for the proposal + # 0 Nays for the proposal assert proposals_output[4] == "0" # Assert initial threshold is 3 @@ -123,15 +123,16 @@ def test_senate(local_chain, wallet_setup): json_proposals = exec_command_bob( command="sudo", sub_command="proposals", - extra_args=[ - "--chain", - "ws://127.0.0.1:9945", - "--json-output" - ] + extra_args=["--chain", "ws://127.0.0.1:9945", "--json-output"], ) - json_proposals_outout = json.loads(json_proposals.stdout) + json_proposals_output = json.loads(json_proposals.stdout) - assert False, json_proposals_outout + assert len(json_proposals_output) == 1 + assert json_proposals_output[0]["threshold"] == 3 + assert json_proposals_output[0]["ayes"] == 0 + assert json_proposals_output[0]["nays"] == 0 + assert json_proposals_output[0]["votes"] == {} + assert json_proposals_output[0]["call_data"] == "System.remark(remark: (0,))" # Vote on the proposal by Bob (vote aye) vote_aye = exec_command_bob( @@ -176,6 +177,26 @@ def test_senate(local_chain, wallet_setup): # Nay votes remain 0 assert proposals_after_aye_output[4] == "0" + proposals_after_aye_json = exec_command_bob( + command="sudo", + sub_command="proposals", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + proposals_after_aye_json_output = json.loads(proposals_after_aye_json.stdout) + assert len(proposals_after_aye_json_output) == 1 + assert proposals_after_aye_json_output[0]["threshold"] == 3 + assert proposals_after_aye_json_output[0]["ayes"] == 1 + assert proposals_after_aye_json_output[0]["nays"] == 0 + assert len(proposals_after_aye_json_output[0]["votes"]) == 1 + assert proposals_after_aye_json_output[0]["votes"][keypair_bob.ss58_address] is True + assert ( + proposals_after_aye_json_output[0]["call_data"] == "System.remark(remark: (0,))" + ) + # Register Alice to the root network (0) # Registering to root automatically makes you a senator if eligible root_register = exec_command_alice( @@ -245,4 +266,20 @@ def test_senate(local_chain, wallet_setup): # Assert vote casted as Nay assert proposals_after_nay_output[10].split()[1] == "Nay" + proposals_after_nay_json = exec_command_bob( + command="sudo", + sub_command="proposals", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + proposals_after_nay_json_output = json.loads(proposals_after_nay_json.stdout) + assert len(proposals_after_nay_json_output) == 1 + assert proposals_after_nay_json_output[0]["nays"] == 1 + assert ( + proposals_after_nay_json_output[0]["votes"][keypair_alice.ss58_address] is False + ) + print("✅ Passed senate commands") diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index a7e691f8..4e5290c4 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -148,7 +148,7 @@ def test_staking(local_chain, wallet_setup): wallet_alice.name, "--chain", "ws://127.0.0.1:9945", - "--json-output" + "--json-output", ], ) From cb9d160d563c78d041c45a4c0e95da1d00fa54c8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 1 Apr 2025 19:59:35 +0200 Subject: [PATCH 59/96] Test Staking --- bittensor_cli/cli.py | 2 +- bittensor_cli/src/commands/stake/list.py | 4 +-- bittensor_cli/src/commands/sudo.py | 3 +- tests/e2e_tests/test_staking_sudo.py | 45 +++++++++++++++++++++++- 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f558da3e..143189d1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4501,7 +4501,7 @@ def sudo_get( [green]$[/green] btcli sudo get --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) return self._run_command( sudo.get_hyperparameters( self.initialize_chain(network), netuid, json_output diff --git a/bittensor_cli/src/commands/stake/list.py b/bittensor_cli/src/commands/stake/list.py index f8169b1d..2e6d39d7 100644 --- a/bittensor_cli/src/commands/stake/list.py +++ b/bittensor_cli/src/commands/stake/list.py @@ -229,8 +229,8 @@ def create_table(hotkey_: str, substakes: list[StakeInfo]): { "netuid": netuid, "subnet_name": subnet_name, - "value": tao_value.tao, - "stake_value": stake_value, + "value": tao_value_.tao, + "stake_value": substake_.stake.tao, "rate": pool.price.tao, "swap_value": swap_value, "registered": True if substake_.is_registered else False, diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 95d3f3ac..e5502714 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -657,7 +657,8 @@ async def get_hyperparameters( ) if json_output: json_console.print(json.dumps(dict_out)) - console.print(table) + else: + console.print(table) return True diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 4e5290c4..ac82817d 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -1,3 +1,4 @@ +import json import re from bittensor_cli.src.bittensor.balances import Balance @@ -129,8 +130,10 @@ def test_staking(local_chain, wallet_setup): wallet_alice.name, "--chain", "ws://127.0.0.1:9945", + "--verbose", ], ) + # Assert correct stake is added cleaned_stake = [ re.sub(r"\s+", " ", line) for line in show_stake.stdout.splitlines() @@ -151,6 +154,9 @@ def test_staking(local_chain, wallet_setup): "--json-output", ], ) + show_stake_json_output = json.loads(show_stake_json.stdout) + alice_stake = show_stake_json_output["stake_info"][keypair_alice.ss58_address][0] + assert Balance.from_tao(alice_stake["stake_value"]) > Balance.from_tao(90.0) # Execute remove_stake command and remove all alpha stakes from Alice remove_stake = exec_command_alice( @@ -196,7 +202,24 @@ def test_staking(local_chain, wallet_setup): max_burn_tao = all_hyperparams[22].split()[3] # Assert max_burn is 100 TAO from default - assert Balance.from_tao(float(max_burn_tao)) == Balance.from_tao(100) + assert Balance.from_tao(float(max_burn_tao)) == Balance.from_tao(100.0) + + hyperparams_json = exec_command_alice( + command="sudo", + sub_command="get", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--json-output", + ], + ) + hyperparams_json_output = json.loads(hyperparams_json.stdout) + max_burn_tao_from_json = next( + filter(lambda x: x["hyperparameter"] == "max_burn", hyperparams_json_output) + )["value"] + assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(100.0) # Change max_burn hyperparameter to 10 TAO change_hyperparams = exec_command_alice( @@ -241,4 +264,24 @@ def test_staking(local_chain, wallet_setup): # Assert max_burn is now 10 TAO assert Balance.from_tao(float(updated_max_burn_tao)) == Balance.from_tao(10) + + updated_hyperparams_json = exec_command_alice( + command="sudo", + sub_command="get", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--json-output", + ], + ) + updated_hyperparams_json_output = json.loads(updated_hyperparams_json.stdout) + max_burn_tao_from_json = next( + filter( + lambda x: x["hyperparameter"] == "max_burn", updated_hyperparams_json_output + ) + )["value"] + assert Balance.from_rao(max_burn_tao_from_json) == Balance.from_tao(10.0) + print("✅ Passed staking and sudo commands") From 20f8c4a83562e611817270100d4c8953f8dfea53 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 1 Apr 2025 20:31:03 +0200 Subject: [PATCH 60/96] Test Unstaking --- tests/e2e_tests/test_unstaking.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py index 96fd2558..c9f99796 100644 --- a/tests/e2e_tests/test_unstaking.py +++ b/tests/e2e_tests/test_unstaking.py @@ -1,3 +1,4 @@ +import json import re from bittensor_cli.src.bittensor.balances import Balance @@ -217,6 +218,25 @@ def test_unstaking(local_chain, wallet_setup): float(inital_stake_netuid_2) ) + show_stake_json = exec_command_alice( + command="stake", + sub_command="list", + extra_args=[ + "--wallet-path", + wallet_path_bob, + "--wallet-name", + wallet_bob.name, + "--chain", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + show_stake_json_output = json.loads(show_stake_json.stdout) + bob_stake = show_stake_json_output["stake_info"][keypair_bob.ss58_address] + assert Balance.from_tao( + next(filter(lambda x: x["netuid"] == 2, bob_stake))["stake_value"] + ) <= Balance.from_tao(float(inital_stake_netuid_2)) + # Remove all alpha stakes unstake_alpha = exec_command_bob( command="stake", From 6c82ee837fac4103eaa90cd5a82a368dbca6eed1 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 1 Apr 2025 23:47:46 +0200 Subject: [PATCH 61/96] version.py was not updated for 9.2.0 --- bittensor_cli/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/version.py b/bittensor_cli/version.py index 528f624b..ce4cdf13 100644 --- a/bittensor_cli/version.py +++ b/bittensor_cli/version.py @@ -16,5 +16,5 @@ def version_as_int(version): return __version_as_int__ -__version__ = "9.1.4" +__version__ = "9.2.0" __version_as_int__ = version_as_int(__version__) From f2af18bac5a6e96b2673afe9a4f6306fec2c686a Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 1 Apr 2025 23:53:40 +0200 Subject: [PATCH 62/96] Pull the version from the toml file. --- bittensor_cli/version.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/version.py b/bittensor_cli/version.py index ce4cdf13..64a18e97 100644 --- a/bittensor_cli/version.py +++ b/bittensor_cli/version.py @@ -1,4 +1,13 @@ import re +import toml +import pathlib + + +def get_toml_version(): + toml_file = pathlib.Path(__file__).parent.parent / "pyproject.toml" + with open(toml_file, "r") as f: + tf = toml.load(f) + return tf["project"]["version"] def version_as_int(version): @@ -16,5 +25,9 @@ def version_as_int(version): return __version_as_int__ -__version__ = "9.2.0" +__version__ = get_toml_version() __version_as_int__ = version_as_int(__version__) + + +if __name__ == "__main__": + print(get_toml_version()) From b3ea6e0dcab27bac01986e8ac5ef7db33de581be Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 12:51:39 +0200 Subject: [PATCH 63/96] Pull the version from the package metadata. --- bittensor_cli/version.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/version.py b/bittensor_cli/version.py index 64a18e97..b500f31e 100644 --- a/bittensor_cli/version.py +++ b/bittensor_cli/version.py @@ -1,6 +1,5 @@ +import importlib.metadata import re -import toml -import pathlib def get_toml_version(): @@ -25,7 +24,7 @@ def version_as_int(version): return __version_as_int__ -__version__ = get_toml_version() +__version__ = importlib.metadata.version("bittensor-cli") __version_as_int__ = version_as_int(__version__) From bd81cba85a2f661a6ed56790e0be4aadf2515de9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 12:53:13 +0200 Subject: [PATCH 64/96] test --- tests/e2e_tests/test_wallet_creations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index ea095998..6dcf0cdd 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -188,8 +188,8 @@ def test_wallet_creations(wallet_setup): ) # Assert default keys are present before proceeding - f"default ss58_address {wallet.coldkeypub.ss58_address}" in result.stdout - f"default ss58_address {wallet.hotkey.ss58_address}" in result.stdout + assert f"default ss58_address {wallet.coldkeypub.ss58_address}" in result.stdout + assert f"default ss58_address {wallet.hotkey.ss58_address}" in result.stdout wallet_status, message = verify_wallet_dir( wallet_path, "default", hotkey_name="default" ) From 7739f80353ab65ebb4a6e87fda74b0a343b11392 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 12:54:36 +0200 Subject: [PATCH 65/96] Importlib metadata --- bittensor_cli/version.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/bittensor_cli/version.py b/bittensor_cli/version.py index b500f31e..d66077f5 100644 --- a/bittensor_cli/version.py +++ b/bittensor_cli/version.py @@ -2,13 +2,6 @@ import re -def get_toml_version(): - toml_file = pathlib.Path(__file__).parent.parent / "pyproject.toml" - with open(toml_file, "r") as f: - tf = toml.load(f) - return tf["project"]["version"] - - def version_as_int(version): _core_version = re.match(r"^\d+\.\d+\.\d+", version).group(0) _version_split = _core_version.split(".") @@ -26,7 +19,3 @@ def version_as_int(version): __version__ = importlib.metadata.version("bittensor-cli") __version_as_int__ = version_as_int(__version__) - - -if __name__ == "__main__": - print(get_toml_version()) From b5cf8ae5da4f468509747034df81b0d8576af51b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 18:50:18 +0200 Subject: [PATCH 66/96] Test Wallet Creations --- bittensor_cli/src/commands/wallets.py | 17 ++++-- tests/e2e_tests/test_wallet_creations.py | 73 +++++++++++++++++++++++- 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 2f21ec3f..006fb511 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -144,11 +144,12 @@ async def regen_coldkey( use_password=use_password, overwrite=overwrite, ) - if isinstance(new_wallet, Wallet): console.print( "\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})", + f"[dark_sea_green]Wallet name: ({new_wallet.name}), " + f"path: ({new_wallet.path}), " + f"coldkey ss58: ({new_wallet.coldkeypub.ss58_address})", ) if json_output: json_console.print( @@ -373,19 +374,23 @@ async def new_coldkey( "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: + except KeyFileError as e: print_error("KeyFileError: File is not writable") if json_output: json_console.print( - '{"success": false, "error": "Keyfile is not writable", "data": null}' + json.dumps( + { + "success": False, + "error": f"Keyfile is not writable: {e}", + "data": None, + } + ) ) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 6dcf0cdd..25d46a5f 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -1,3 +1,4 @@ +import json import os import re import time @@ -195,6 +196,15 @@ def test_wallet_creations(wallet_setup): ) assert wallet_status, message + json_result = exec_command( + command="wallet", + sub_command="list", + extra_args=["--wallet-path", wallet_path, "--json-output"], + ) + json_wallet = json.loads(json_result.stdout)["wallets"][0] + assert json_wallet["ss58_address"] == wallet.coldkey.ss58_address + assert json_wallet["hotkeys"][0]["ss58_address"] == wallet.hotkey.ss58_address + # ----------------------------- # Command 1: # ----------------------------- @@ -267,6 +277,27 @@ def test_wallet_creations(wallet_setup): wallet_status, message = verify_wallet_dir(wallet_path, "new_coldkey") assert wallet_status, message + json_creation = exec_command( + "wallet", + "new-coldkey", + extra_args=[ + "--wallet-name", + "new_json_coldkey", + "--wallet-path", + wallet_path, + "--n-words", + "12", + "--no-use-password", + "--json-output", + ], + ) + json_creation_output = json.loads(json_creation.stdout) + assert json_creation_output["success"] is True + assert json_creation_output["data"]["name"] == "new_json_coldkey" + assert "coldkey_ss58" in json_creation_output["data"] + assert json_creation_output["error"] == "" + new_json_coldkey_ss58 = json_creation_output["data"]["coldkey_ss58"] + # ----------------------------- # Command 3: # ----------------------------- @@ -303,6 +334,29 @@ def test_wallet_creations(wallet_setup): ) assert wallet_status, message + new_hotkey_json = exec_command( + "wallet", + sub_command="new-hotkey", + extra_args=[ + "--wallet-name", + "new_json_coldkey", + "--hotkey", + "new_json_hotkey", + "--wallet-path", + wallet_path, + "--n-words", + "12", + "--no-use-password", + "--json-output", + ], + ) + new_hotkey_json_output = json.loads(new_hotkey_json.stdout) + assert new_hotkey_json_output["success"] is True + assert new_hotkey_json_output["data"]["name"] == "new_json_coldkey" + assert new_hotkey_json_output["data"]["hotkey"] == "new_json_hotkey" + assert new_hotkey_json_output["data"]["coldkey_ss58"] == new_json_coldkey_ss58 + assert new_hotkey_json_output["error"] == "" + def test_wallet_regen(wallet_setup, capfd): """ @@ -385,7 +439,24 @@ def test_wallet_regen(wallet_setup, capfd): assert ( initial_coldkey_mod_time != new_coldkey_mod_time ), "Coldkey file was not regenerated as expected" - print("Passed wallet regen_coldkey command ✅") + # Underlying Rust is broken. Will add this in when that is fixed: https://github.com/opentensor/btwallet/issues/118 + # json_result = exec_command( + # command="wallet", + # sub_command="regen-coldkey", + # extra_args=[ + # "--wallet-name", + # "new_wallet", + # "--hotkey", + # "new_hotkey", + # "--wallet-path", + # wallet_path, + # "--mnemonic", + # mnemonics["coldkey"], + # "--no-use-password", + # "--overwrite", + # "--json-output" + # ], + # ) # ----------------------------- # Command 2: From 8152b1e2487facd1f2abe9f0ffc334386b787c55 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 19:00:25 +0200 Subject: [PATCH 67/96] Wallet Balances --- tests/e2e_tests/test_wallet_creations.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index 25d46a5f..c9ee8623 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -588,4 +588,15 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd): wallet_name in output ), f"Wallet {wallet_name} not found in balance --all output" + json_results = exec_command( + "wallet", + "balance", + extra_args=["--wallet-path", wallet_path, "--all", "--json-output"], + ) + json_results_output = json.loads(json_results.stdout) + for wallet_name in wallet_names: + assert wallet_name in json_results_output["balances"].keys() + assert json_results_output["balances"][wallet_name]["total"] == 0.0 + assert "coldkey" in json_results_output["balances"][wallet_name] + print("Passed wallet balance --all command with 100 wallets ✅") From 2f1c60d7b554ebd16788c0e2a06d5fb3b0ed4641 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 20:43:52 +0200 Subject: [PATCH 68/96] Updates test_wallet_creations.py --- tests/e2e_tests/test_wallet_creations.py | 45 ++++++++++++++---------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py index c9ee8623..5dd3bd63 100644 --- a/tests/e2e_tests/test_wallet_creations.py +++ b/tests/e2e_tests/test_wallet_creations.py @@ -4,6 +4,8 @@ import time from typing import Dict, Optional, Tuple +from bittensor_wallet import Wallet + """ Verify commands: @@ -412,6 +414,9 @@ def test_wallet_regen(wallet_setup, capfd): # ----------------------------- print("Testing wallet regen_coldkey command 🧪") coldkey_path = os.path.join(wallet_path, "new_wallet", "coldkey") + initial_coldkey_ss58 = Wallet( + name="new_wallet", path=wallet_path + ).coldkey.ss58_address initial_coldkey_mod_time = os.path.getmtime(coldkey_path) result = exec_command( @@ -439,24 +444,28 @@ def test_wallet_regen(wallet_setup, capfd): assert ( initial_coldkey_mod_time != new_coldkey_mod_time ), "Coldkey file was not regenerated as expected" - # Underlying Rust is broken. Will add this in when that is fixed: https://github.com/opentensor/btwallet/issues/118 - # json_result = exec_command( - # command="wallet", - # sub_command="regen-coldkey", - # extra_args=[ - # "--wallet-name", - # "new_wallet", - # "--hotkey", - # "new_hotkey", - # "--wallet-path", - # wallet_path, - # "--mnemonic", - # mnemonics["coldkey"], - # "--no-use-password", - # "--overwrite", - # "--json-output" - # ], - # ) + json_result = exec_command( + command="wallet", + sub_command="regen-coldkey", + extra_args=[ + "--wallet-name", + "new_wallet", + "--hotkey", + "new_hotkey", + "--wallet-path", + wallet_path, + "--mnemonic", + mnemonics["coldkey"], + "--no-use-password", + "--overwrite", + "--json-output", + ], + ) + + json_result_out = json.loads(json_result.stdout) + assert json_result_out["success"] is True + assert json_result_out["data"]["name"] == "new_wallet" + assert json_result_out["data"]["coldkey_ss58"] == initial_coldkey_ss58 # ----------------------------- # Command 2: From 87cfda954b41cff68fab9dcca8211f1b2e211a11 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 20:45:16 +0200 Subject: [PATCH 69/96] Bumps btwallet requirement. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2652c869..7dd501e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "scalecodec==1.2.11", "typer~=0.12", "websockets>=14.1", - "bittensor-wallet>=3.0.4", + "bittensor-wallet>=3.0.5", "plotille>=5.0.0", "pywry>=0.6.2", "plotly>=6.0.0", From 63cc649058bba03691119bcec003fd26d81507c8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 21:53:06 +0200 Subject: [PATCH 70/96] Trigger no-op From d269ec1f5a77a346f6ff33e3913895b442404bc9 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 21:53:27 +0200 Subject: [PATCH 71/96] Bumps versions. --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2652c869..9354746a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,16 +22,16 @@ dependencies = [ "GitPython>=3.0.0", "fuzzywuzzy~=0.18.0", "netaddr~=1.3.0", - "numpy>=2.0.1", + "numpy>=2.0.1,<3.0.0", "Jinja2", "pycryptodome>=3.0.0,<4.0.0", "PyYAML~=6.0.1", "pytest", "python-Levenshtein", - "rich~=13.7", + "rich>=13.7,<15.0", "scalecodec==1.2.11", - "typer~=0.12", - "websockets>=14.1", + "typer>=0.12,<0.16", + "websockets>=14.1,<16.0", "bittensor-wallet>=3.0.4", "plotille>=5.0.0", "pywry>=0.6.2", From cc395cff5ee35ebd0d114b128d55bc0beb20aaf2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 2 Apr 2025 21:53:54 +0200 Subject: [PATCH 72/96] Trigger no-op From 17902e57a127fe3a0b29f029ae79eb5fcfd606c5 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 3 Apr 2025 16:27:08 +0200 Subject: [PATCH 73/96] General cleanup --- bittensor_cli/cli.py | 9 ++++-- bittensor_cli/src/bittensor/chain_data.py | 8 ++++-- .../src/bittensor/extrinsics/registration.py | 28 +++++++++++-------- .../src/bittensor/extrinsics/root.py | 1 - .../src/bittensor/extrinsics/transfer.py | 5 ++-- .../src/bittensor/subtensor_interface.py | 8 +++--- bittensor_cli/src/bittensor/utils.py | 4 ++- bittensor_cli/src/commands/stake/remove.py | 1 - bittensor_cli/src/commands/subnets/subnets.py | 1 - bittensor_cli/src/commands/weights.py | 3 -- tests/e2e_tests/utils.py | 1 - 11 files changed, 38 insertions(+), 31 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 143189d1..47f831f1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4067,7 +4067,8 @@ def stake_swap( """ 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]" + "[dim]This command moves stake from one subnet to another subnet while keeping " + "the same coldkey-hotkey pair.[/dim]" ) wallet = self.wallet_ask( @@ -4222,7 +4223,8 @@ def stake_set_children( proportions = list_prompt( proportions, float, - "Enter comma-separated proportions equal to the number of children (sum not exceeding a total of 1.0)", + "Enter comma-separated proportions equal to the number of children " + "(sum not exceeding a total of 1.0)", ) if len(proportions) != len(children): @@ -4340,7 +4342,8 @@ def stake_childkey_take( None, "--take", "-t", - help="Use to set the take value for your child hotkey. When not used, the command will fetch the current take value.", + help="Use to set the take value for your child hotkey. When not used, the command will fetch the current " + "take value.", prompt=False, ), wait_for_inclusion: bool = Options.wait_for_inclusion, diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index ddbbc724..1b9d8709 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -749,7 +749,9 @@ def tao_to_alpha_with_slippage( self, tao: Balance ) -> tuple[Balance, Balance, float]: """ - Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool state. + Returns an estimate of how much Alpha would a staker receive if they stake their tao using the current pool + state. + Args: tao: Amount of TAO to stake. Returns: @@ -792,7 +794,9 @@ def alpha_to_tao_with_slippage( self, alpha: Balance ) -> tuple[Balance, Balance, float]: """ - Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool state. + Returns an estimate of how much TAO would a staker receive if they unstake their alpha using the current pool + state. + Args: alpha: Amount of Alpha to stake. Returns: diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index bcd809bd..0eaa2760 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -524,9 +524,11 @@ async def get_neuron_for_pubkey_and_subnet(): if prompt: if not Confirm.ask( f"Continue Registration?\n" - f" hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]({wallet.hotkey_str})[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]:\t[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" - f" coldkey [{COLOR_PALETTE['GENERAL']['COLDKEY']}]({wallet.name})[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]:\t[{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f" network:\t\t[{COLOR_PALETTE['GENERAL']['LINKS']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['LINKS']}]\n" + f" hotkey [{COLOR_PALETTE.G.HK}]({wallet.hotkey_str})[/{COLOR_PALETTE.G.HK}]:" + f"\t[{COLOR_PALETTE.G.HK}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}]\n" + f" coldkey [{COLOR_PALETTE.G.CK}]({wallet.name})[/{COLOR_PALETTE.G.CK}]:" + f"\t[{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n" + f" network:\t\t[{COLOR_PALETTE.G.LINKS}]{subtensor.network}[/{COLOR_PALETTE.G.LINKS}]\n" ): return False @@ -611,7 +613,6 @@ async def get_neuron_for_pubkey_and_subnet(): if not wait_for_finalization and not wait_for_inclusion: success, err_msg = True, "" else: - await response.process_events() success = await response.is_success if not success: success, err_msg = ( @@ -689,6 +690,7 @@ async def burned_register_extrinsic( `False` if the extrinsic fails to enter the block within the timeout. :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`, or returns `False` if the extrinsic fails to be finalized within the timeout. + :param era: the period (in blocks) for which the transaction should remain valid. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for @@ -735,10 +737,10 @@ async def burned_register_extrinsic( if not neuron.is_null: console.print( ":white_heavy_check_mark: [dark_sea_green3]Already Registered[/dark_sea_green3]:\n" - f"uid: [{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]{neuron.uid}[/{COLOR_PALETTE['GENERAL']['NETUID_EXTRA']}]\n" - f"netuid: [{COLOR_PALETTE['GENERAL']['NETUID']}]{neuron.netuid}[/{COLOR_PALETTE['GENERAL']['NETUID']}]\n" - f"hotkey: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{neuron.hotkey}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" - f"coldkey: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{neuron.coldkey}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" + f"uid: [{COLOR_PALETTE.G.NETUID_EXTRA}]{neuron.uid}[/{COLOR_PALETTE.G.NETUID_EXTRA}]\n" + f"netuid: [{COLOR_PALETTE.G.NETUID}]{neuron.netuid}[/{COLOR_PALETTE.G.NETUID}]\n" + f"hotkey: [{COLOR_PALETTE.G.HK}]{neuron.hotkey}[/{COLOR_PALETTE.G.HK}]\n" + f"coldkey: [{COLOR_PALETTE.G.CK}]{neuron.coldkey}[/{COLOR_PALETTE.G.CK}]" ) return True @@ -781,7 +783,8 @@ async def burned_register_extrinsic( console.print( "Balance:\n" - f" [blue]{old_balance}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]" + f" [blue]{old_balance}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{new_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]" ) if len(netuids_for_hotkey) > 0: @@ -914,7 +917,6 @@ async def run_faucet_extrinsic( ) # process if registration successful, try again if pow is still valid - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red]: " @@ -1757,7 +1759,8 @@ async def swap_hotkey_extrinsic( ) if not len(netuids_registered) > 0: err_console.print( - f"Destination hotkey [dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] is not registered. Please register and try again" + f"Destination hotkey [dark_orange]{new_wallet.hotkey.ss58_address}[/dark_orange] is not registered. " + f"Please register and try again" ) return False @@ -1774,7 +1777,8 @@ async def swap_hotkey_extrinsic( ): return False print_verbose( - f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address}) with {new_wallet.name}s hotkey ({new_wallet.hotkey.ss58_address})" + f"Swapping {wallet.name}'s hotkey ({wallet.hotkey.ss58_address}) with " + f"{new_wallet.name}s hotkey ({new_wallet.hotkey.ss58_address})" ) with console.status(":satellite: Swapping hotkeys...", spinner="aesthetic"): call = await subtensor.substrate.compose_call( diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 29b9e510..6b91d469 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -410,7 +410,6 @@ async def _do_set_weights(): if not wait_for_finalization and not wait_for_inclusion: return True, "Not waiting for finalization or inclusion." - await response.process_events() if await response.is_success: return True, "Successfully set weights." else: diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index 0deefc46..3796353e 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -37,6 +37,7 @@ async def transfer_extrinsic( :param wallet: Bittensor wallet object to make transfer from. :param destination: Destination public key address (ss58_address or ed25519) of recipient. :param amount: Amount to stake as Bittensor balance. + :param era: Length (in blocks) for which the transaction should be valid. :param transfer_all: Whether to transfer all funds from this wallet to the destination address. :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`, or returns `False` if the extrinsic fails to enter the block within the timeout. @@ -97,7 +98,6 @@ async def do_transfer() -> tuple[bool, str, str]: return True, "", "" # Otherwise continue with finalization. - await response.process_events() if await response.is_success: block_hash_ = response.block_hash return True, block_hash_, "" @@ -156,7 +156,8 @@ async def do_transfer() -> tuple[bool, str, str]: if not Confirm.ask( "Do you want to transfer:[bold white]\n" f" amount: [bright_cyan]{amount}[/bright_cyan]\n" - f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : [bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]" + f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : " + f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]" f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]" ): return False diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index ecd58f7c..7f21f90d 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1057,6 +1057,7 @@ async def sign_and_send_extrinsic( :param wallet: the wallet whose coldkey will be used to sign the extrinsic :param wait_for_inclusion: whether to wait until the extrinsic call is included on the chain :param wait_for_finalization: whether to wait until the extrinsic call is finalized on the chain + :param era: The length (in blocks) for which a transaction should be valid. :return: (success, error message) """ @@ -1075,7 +1076,6 @@ async def sign_and_send_extrinsic( # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: return True, "" - await response.process_events() if await response.is_success: return True, "" else: @@ -1372,11 +1372,11 @@ async def get_stake_for_coldkeys( This function is useful for analyzing the stake distribution and delegation patterns of multiple accounts simultaneously, offering a broader perspective on network participation and investment strategies. """ - BATCH_SIZE = 60 + batch_size = 60 tasks = [] - for i in range(0, len(coldkey_ss58_list), BATCH_SIZE): - ss58_chunk = coldkey_ss58_list[i : i + BATCH_SIZE] + for i in range(0, len(coldkey_ss58_list), batch_size): + ss58_chunk = coldkey_ss58_list[i : i + batch_size] tasks.append( self.query_runtime_api( runtime_api="StakeInfoRuntimeApi", diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index aedfe2ef..b24cea56 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -1053,6 +1053,7 @@ def get_effective_network(config, network: Optional[list[str]]) -> str: the configuration, and the default. """ if network: + network_ = "" for item in network: if item.startswith("ws"): network_ = item @@ -1309,7 +1310,8 @@ def print_linux_dependency_message(): "\tAdd this into the file and save: [green]deb http://archive.ubuntu.com/ubuntu jammy main universe[/green]" ) console.print( - "\tUpdate the repository and install the webkit dependency: [green]sudo apt update && sudo apt install libwebkit2gtk-4.0-dev[/green]" + "\tUpdate the repository and install the webkit dependency: [green]sudo apt update && sudo apt install " + "libwebkit2gtk-4.0-dev[/green]" ) console.print("\nFedora / CentOS / AlmaLinux:") console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]\n\n") diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 9130e6c5..65772232 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -798,7 +798,6 @@ async def _unstake_all_extrinsic( wait_for_inclusion=True, wait_for_finalization=False, ) - await response.process_events() if not await response.is_success: err_out( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index a856304c..b9d32308 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -178,7 +178,6 @@ async def _find_event_attributes_in_extrinsic_receipt( if not wait_for_finalization and not wait_for_inclusion: return True - await response.process_events() if not await response.is_success: err_console.print( f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 29337be4..2c40a001 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -260,7 +260,6 @@ async def _do_set_weights(): if not self.wait_for_finalization and not self.wait_for_inclusion: return True, "Not waiting for finalization or inclusion." - await response.process_events() if await response.is_success: return True, "Successfully set weights." else: @@ -316,7 +315,6 @@ async def reveal_weights_extrinsic( success, error_message = True, "" else: - await response.process_events() if await response.is_success: success, error_message = True, "" else: @@ -354,7 +352,6 @@ async def do_commit_weights(self, commit_hash): if not self.wait_for_finalization and not self.wait_for_inclusion: return True, None - await response.process_events() if await response.is_success: return True, None else: diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 665a4fa2..9379b962 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -297,5 +297,4 @@ async def call_add_proposal( wait_for_finalization=True, ) - await response.process_events() return await response.is_success From 742e4794b03eb9655ed5fd7561958775700a7db0 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 3 Apr 2025 18:23:53 +0200 Subject: [PATCH 74/96] Additional CLI commands with JSON outputs --- bittensor_cli/cli.py | 61 +++++------ .../src/bittensor/extrinsics/registration.py | 14 +-- .../src/bittensor/extrinsics/root.py | 20 ++-- bittensor_cli/src/commands/subnets/subnets.py | 101 +++++++++++++----- 4 files changed, 116 insertions(+), 80 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 143189d1..452d71fd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4875,6 +4875,7 @@ def subnets_burn_cost( network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Shows the required amount of TAO to be recycled for creating a new subnet, i.e., cost of registering a new subnet. @@ -4885,8 +4886,10 @@ def subnets_burn_cost( [green]$[/green] btcli subnets burn_cost """ - self.verbosity_handler(quiet, verbose) - return self._run_command(subnets.burn_cost(self.initialize_chain(network))) + self.verbosity_handler(quiet, verbose, json_output) + return self._run_command( + subnets.burn_cost(self.initialize_chain(network), json_output) + ) def subnets_create( self, @@ -4919,6 +4922,7 @@ def subnets_create( additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), + json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4937,7 +4941,7 @@ def subnets_create( 2. Create with GitHub repo and contact email: [green]$[/green] btcli subnets create --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -4959,34 +4963,19 @@ def subnets_create( description=description, additional=additional_info, ) - success = self._run_command( - subnets.create(wallet, self.initialize_chain(network), identity, prompt), - exit_early=False, + self._run_command( + subnets.create( + wallet, self.initialize_chain(network), identity, json_output, prompt + ) ) - if success and prompt: - set_id = Confirm.ask( - "[dark_sea_green3]Do you want to set/update your identity?", - default=False, - show_default=True, - ) - if set_id: - self.wallet_set_id( - wallet_name=wallet.name, - wallet_hotkey=wallet.hotkey, - wallet_path=wallet.path, - network=network, - prompt=prompt, - quiet=quiet, - verbose=verbose, - ) - def subnets_get_identity( self, network: Optional[list[str]] = Options.network, netuid: int = Options.netuid, quiet: bool = Options.quiet, verbose: bool = Options.verbose, + json_output: bool = Options.json_output, ): """ Get the identity information for a subnet. @@ -4995,11 +4984,10 @@ def subnets_get_identity( [green]$[/green] btcli subnets get-identity --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) return self._run_command( subnets.get_identity( - self.initialize_chain(network), - netuid, + self.initialize_chain(network), netuid, json_output=json_output ) ) @@ -5035,6 +5023,7 @@ def subnets_set_identity( additional_info: Optional[str] = typer.Option( None, "--additional-info", help="Additional information" ), + json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5052,7 +5041,7 @@ def subnets_set_identity( 2. Set subnet identity with specific values: [green]$[/green] btcli subnets set-identity --netuid 1 --subnet-name MySubnet --github-repo https://github.com/myorg/mysubnet --subnet-contact team@mysubnet.net """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5070,7 +5059,9 @@ def subnets_set_identity( exit_early=False, ) if current_identity is None: - raise typer.Exit() + if json_output: + json_console.print('{"success": false}') + return identity = prompt_for_subnet_identity( current_identity=current_identity, @@ -5083,15 +5074,13 @@ def subnets_set_identity( additional=additional_info, ) - return self._run_command( + success = self._run_command( subnets.set_identity( - wallet, - self.initialize_chain(network), - netuid, - identity, - prompt, + wallet, self.initialize_chain(network), netuid, identity, prompt ) ) + if json_output: + json_console.print(json.dumps({"success": success})) def subnets_pow_register( self, @@ -5196,6 +5185,7 @@ def subnets_register( help="Length (in blocks) for which the transaction should be valid. Note that it is possible that if you " "use an era for this transaction that you may pay a different fee to register than the one stated.", ), + json_output: bool = Options.json_output, prompt: bool = Options.prompt, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5211,7 +5201,7 @@ def subnets_register( [green]$[/green] btcli subnets register --netuid 1 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5225,6 +5215,7 @@ def subnets_register( self.initialize_chain(network), netuid, era, + json_output, prompt, ) ) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index bcd809bd..b5e44019 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -678,7 +678,7 @@ async def burned_register_extrinsic( wait_for_finalization: bool = True, era: Optional[int] = None, prompt: bool = False, -) -> bool: +) -> tuple[bool, str]: """Registers the wallet to chain by recycling TAO. :param subtensor: The SubtensorInterface object to use for the call, initialized @@ -691,8 +691,8 @@ async def burned_register_extrinsic( or returns `False` if the extrinsic fails to be finalized within the timeout. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for - finalization/inclusion, the response is `True`. + :return: (success, msg), where success is `True` if extrinsic was finalized or included in the block. If we did not + wait for finalization/inclusion, the response is `True`. """ if not (unlock_status := unlock_key(wallet, print_out=False)).success: @@ -740,7 +740,7 @@ async def burned_register_extrinsic( f"hotkey: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{neuron.hotkey}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n" f"coldkey: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{neuron.coldkey}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]" ) - return True + return True, "Already registered" with console.status( ":satellite: Recycling TAO for Registration...", spinner="aesthetic" @@ -760,7 +760,7 @@ async def burned_register_extrinsic( if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") await asyncio.sleep(0.5) - return False + return False, err_msg # Successful registration, final check for neuron and pubkey else: with console.status(":satellite: Checking Balance...", spinner="aesthetic"): @@ -788,13 +788,13 @@ async def burned_register_extrinsic( console.print( f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]" ) - return True + return True, f"Registered on {netuid} with UID {my_uid}" else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False + return False, "Unknown error. Neuron not found." async def run_faucet_extrinsic( diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py index 29b9e510..44517ad3 100644 --- a/bittensor_cli/src/bittensor/extrinsics/root.py +++ b/bittensor_cli/src/bittensor/extrinsics/root.py @@ -291,7 +291,7 @@ async def root_register_extrinsic( wait_for_inclusion: bool = True, wait_for_finalization: bool = True, prompt: bool = False, -) -> bool: +) -> tuple[bool, str]: r"""Registers the wallet to root network. :param subtensor: The SubtensorInterface object @@ -302,12 +302,12 @@ async def root_register_extrinsic( or returns `False` if the extrinsic fails to be finalized within the timeout. :param prompt: If `True`, the call waits for confirmation from the user before proceeding. - :return: `True` if extrinsic was finalized or included in the block. If we did not wait for finalization/inclusion, - the response is `True`. + :return: (success, msg), with success being `True` if extrinsic was finalized or included in the block. If we did + not wait for finalization/inclusion, the response is `True`. """ - if not unlock_key(wallet).success: - return False + if not (unlock := unlock_key(wallet)).success: + return False, unlock.message print_verbose(f"Checking if hotkey ({wallet.hotkey_str}) is registered on root") is_registered = await is_hotkey_registered( @@ -317,7 +317,7 @@ async def root_register_extrinsic( console.print( ":white_heavy_check_mark: [green]Already registered on root network.[/green]" ) - return True + return True, "Already registered on root network" with console.status(":satellite: Registering to root network...", spinner="earth"): call = await subtensor.substrate.compose_call( @@ -334,8 +334,8 @@ async def root_register_extrinsic( if not success: err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - time.sleep(0.5) - return False + await asyncio.sleep(0.5) + return False, err_msg # Successful registration, final check for neuron and pubkey else: @@ -348,13 +348,13 @@ async def root_register_extrinsic( console.print( f":white_heavy_check_mark: [green]Registered with UID {uid}[/green]" ) - return True + return True, f"Registered with UID {uid}" else: # neuron not found, try again err_console.print( ":cross_mark: [red]Unknown error. Neuron not found.[/red]" ) - return False + return False, "Unknown error. Neuron not found." async def set_root_weights_extrinsic( diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index a856304c..e899f1f1 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -52,7 +52,7 @@ async def register_subnetwork_extrinsic( wait_for_inclusion: bool = False, wait_for_finalization: bool = True, prompt: bool = False, -) -> bool: +) -> tuple[bool, Optional[int]]: """Registers a new subnetwork. wallet (bittensor.wallet): @@ -101,7 +101,7 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}] " f"to register a subnet." ) - return False + return False, None if prompt: console.print( @@ -110,7 +110,7 @@ async def _find_event_attributes_in_extrinsic_receipt( if not Confirm.ask( f"Do you want to burn [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost} to register a subnet?" ): - return False + return False, None call_params = { "hotkey": wallet.hotkey.ss58_address, @@ -152,10 +152,10 @@ async def _find_event_attributes_in_extrinsic_receipt( f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n" f"Value '{value.decode()}' is {len(value)} bytes." ) - return False + return False, None if not unlock_key(wallet).success: - return False + return False, None with console.status(":satellite: Registering subnet...", spinner="earth"): substrate = subtensor.substrate @@ -176,7 +176,7 @@ async def _find_event_attributes_in_extrinsic_receipt( # We only wait here if we expect finalization. if not wait_for_finalization and not wait_for_inclusion: - return True + return True, None await response.process_events() if not await response.is_success: @@ -184,7 +184,7 @@ async def _find_event_attributes_in_extrinsic_receipt( f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}" ) await asyncio.sleep(0.5) - return False + return False, None # Successful registration, final check for membership else: @@ -194,7 +194,7 @@ async def _find_event_attributes_in_extrinsic_receipt( console.print( f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" ) - return True + return True, int(attributes[0]) # commands @@ -1428,7 +1428,9 @@ async def show_subnet(netuid_: int): return result -async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: +async def burn_cost( + subtensor: "SubtensorInterface", json_output: bool = False +) -> Optional[Balance]: """View locking cost of creating a new subnetwork""" with console.status( f":satellite:Retrieving lock cost from {subtensor.network}...", @@ -1436,26 +1438,47 @@ async def burn_cost(subtensor: "SubtensorInterface") -> Optional[Balance]: ): current_burn_cost = await subtensor.burn_cost() if current_burn_cost: - console.print( - f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_burn_cost}" - ) + if json_output: + json_console.print( + json.dumps({"burn_cost": current_burn_cost, "error": ""}) + ) + else: + console.print( + f"Subnet burn cost: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_burn_cost}" + ) return current_burn_cost else: - err_console.print( - "Subnet burn cost: [red]Failed to get subnet burn cost[/red]" - ) + if json_output: + json_console.print( + json.dumps( + {"burn_cost": None, "error": "Failed to get subnet burn cost"} + ) + ) + else: + err_console.print( + "Subnet burn cost: [red]Failed to get subnet burn cost[/red]" + ) return None async def create( - wallet: Wallet, subtensor: "SubtensorInterface", subnet_identity: dict, prompt: bool + wallet: Wallet, + subtensor: "SubtensorInterface", + subnet_identity: dict, + json_output: bool, + prompt: bool, ): """Register a subnetwork""" # Call register command. - success = await register_subnetwork_extrinsic( + success, netuid = await register_subnetwork_extrinsic( subtensor, wallet, subnet_identity, prompt=prompt ) + if json_output: + # technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present + # (2025/04/03), we always use the default `wait_for_finalization=True`, so it will always have a netuid. + json_console.print(json.dumps({"success": success, "netuid": netuid})) + return success if success and prompt: # Prompt for user to set identity. do_set_identity = Confirm.ask( @@ -1534,6 +1557,7 @@ async def register( subtensor: "SubtensorInterface", netuid: int, era: Optional[int], + json_output: bool, prompt: bool, ): """Register neuron by recycling some TAO.""" @@ -1543,6 +1567,12 @@ async def register( block_hash = await subtensor.substrate.get_chain_head() if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash): err_console.print(f"[red]Subnet {netuid} does not exist[/red]") + if json_output: + json_console.print( + json.dumps( + {"success": False, "error": f"Subnet {netuid} does not exist"} + ) + ) return # Check current recycle amount @@ -1564,7 +1594,7 @@ async def register( ) return - if prompt: + if prompt and not json_output: # TODO make this a reusable function, also used in subnets list # Show creation table. table = Table( @@ -1616,17 +1646,19 @@ async def register( console.print(table) if not ( Confirm.ask( - f"Your balance is: [{COLOR_PALETTE['GENERAL']['BALANCE']}]{balance}[/{COLOR_PALETTE['GENERAL']['BALANCE']}]\nThe cost to register by recycle is " - f"[{COLOR_PALETTE['GENERAL']['COST']}]{current_recycle}[/{COLOR_PALETTE['GENERAL']['COST']}]\nDo you want to continue?", + f"Your balance is: [{COLOR_PALETTE.G.BAL}]{balance}[/{COLOR_PALETTE.G.BAL}]\n" + f"The cost to register by recycle is " + f"[{COLOR_PALETTE.G.COST}]{current_recycle}[/{COLOR_PALETTE.G.COST}]\n" + f"Do you want to continue?", default=False, ) ): return if netuid == 0: - await root_register_extrinsic(subtensor, wallet=wallet) + success, msg = await root_register_extrinsic(subtensor, wallet=wallet) else: - await burned_register_extrinsic( + success, msg = await burned_register_extrinsic( subtensor, wallet=wallet, netuid=netuid, @@ -1634,6 +1666,8 @@ async def register( old_balance=balance, era=era, ) + if json_output: + json_console.print(json.dumps({"success": success, "msg": msg})) # TODO: Confirm emissions, incentive, Dividends are to be fetched from subnet_state or keep NeuronInfo @@ -2225,10 +2259,15 @@ async def set_identity( return True -async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str = None): +async def get_identity( + subtensor: "SubtensorInterface", + netuid: int, + title: str = None, + json_output: bool = False, +) -> Optional[dict]: """Fetch and display existing subnet identity information.""" if not title: - title = "Subnet Identity" + title = f"Current Subnet {netuid} Identity" if not await subtensor.subnet_exists(netuid): print_error(f"Subnet {netuid} does not exist.") @@ -2246,10 +2285,12 @@ async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str f" for subnet [blue]{netuid}[/blue]" f" on {subtensor}" ) + if json_output: + json_console.print("{}") return {} - - if identity: - table = create_identity_table(title=f"Current Subnet {netuid} Identity") + else: + table = create_identity_table(title=title) + dict_out = {} table.add_row("Netuid", str(netuid)) for key in [ "subnet_name", @@ -2262,5 +2303,9 @@ async def get_identity(subtensor: "SubtensorInterface", netuid: int, title: str ]: value = getattr(identity, key, None) table.add_row(key, str(value) if value else "~") - console.print(table) + dict_out[key] = value + if json_output: + json_console.print(json.dumps(dict_out)) + else: + console.print(table) return identity From 7fed1227ef243294c02f6b494a24c0dad3fb0e24 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 3 Apr 2025 18:34:13 +0200 Subject: [PATCH 75/96] Last commands finished. --- bittensor_cli/cli.py | 12 +++++++---- bittensor_cli/src/commands/weights.py | 29 +++++++++++++++++++-------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 452d71fd..ffce7d44 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5341,6 +5341,7 @@ def weights_reveal( "-s", help="Corresponding salt for the hash function, e.g. -s 163,241,217 ...", ), + json_output: bool = Options.json_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -5354,7 +5355,7 @@ def weights_reveal( [green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189 """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) uids = list_prompt(uids, int, "UIDs of interest for the specified netuid") weights = list_prompt( weights, float, "Corresponding weights for the specified UIDs" @@ -5387,7 +5388,7 @@ def weights_reveal( err_console.print( "The number of UIDs you specify must match up with the specified number of weights" ) - raise typer.Exit() + return if salt: salt = parse_to_list( @@ -5416,6 +5417,7 @@ def weights_reveal( salt, __version_as_int__, prompt=prompt, + json_output=json_output, ) ) @@ -5439,6 +5441,7 @@ def weights_commit( "-s", help="Corresponding salt for the hash function, e.g. -s 163 -s 241 -s 217 ...", ), + json_output: bool = Options.json_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, prompt: bool = Options.prompt, @@ -5456,7 +5459,7 @@ def weights_commit( [italic]Note[/italic]: This command is used to commit weights for a specific subnet and requires the user to have the necessary permissions. """ - self.verbosity_handler(quiet, verbose) + self.verbosity_handler(quiet, verbose, json_output) if uids: uids = parse_to_list( @@ -5485,7 +5488,7 @@ def weights_commit( err_console.print( "The number of UIDs you specify must match up with the specified number of weights" ) - raise typer.Exit() + return if salt: salt = parse_to_list( @@ -5512,6 +5515,7 @@ def weights_commit( weights, salt, __version_as_int__, + json_output=json_output, prompt=prompt, ) ) diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py index 29337be4..e89f1556 100644 --- a/bittensor_cli/src/commands/weights.py +++ b/bittensor_cli/src/commands/weights.py @@ -1,4 +1,5 @@ import asyncio +import json import os from datetime import datetime, timedelta from typing import TYPE_CHECKING @@ -9,7 +10,12 @@ from rich.prompt import Confirm from async_substrate_interface.errors import SubstrateRequestException -from bittensor_cli.src.bittensor.utils import err_console, console, format_error_message +from bittensor_cli.src.bittensor.utils import ( + err_console, + console, + format_error_message, + json_console, +) from bittensor_cli.src.bittensor.extrinsics.root import ( convert_weights_and_uids_for_emit, generate_weight_hash, @@ -372,6 +378,7 @@ async def reveal_weights( weights: list[float], salt: list[int], version: int, + json_output: bool = False, prompt: bool = True, ) -> None: """Reveal weights for a specific subnet.""" @@ -395,11 +402,13 @@ async def reveal_weights( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) success, message = await extrinsic.reveal(weight_uids, weight_vals) - - if success: - console.print("Weights revealed successfully") + if json_output: + json_console.print(json.dumps({"success": success, "message": message})) else: - err_console.print(f"Failed to reveal weights: {message}") + if success: + console.print("Weights revealed successfully") + else: + err_console.print(f"Failed to reveal weights: {message}") async def commit_weights( @@ -410,6 +419,7 @@ async def commit_weights( weights: list[float], salt: list[int], version: int, + json_output: bool = False, prompt: bool = True, ): """Commits weights and then reveals them for a specific subnet""" @@ -429,7 +439,10 @@ async def commit_weights( subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt ) success, message = await extrinsic.set_weights_extrinsic() - if success: - console.print("Weights set successfully") + if json_output: + json_console.print(json.dumps({"success": success, "message": message})) else: - err_console.print(f"Failed to commit weights: {message}") + if success: + console.print("Weights set successfully") + else: + err_console.print(f"Failed to commit weights: {message}") From c7b89a2deb9637fa65fc52c2ed6b589caf5d4caa Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Thu, 3 Apr 2025 21:49:10 +0200 Subject: [PATCH 76/96] Tests added --- bittensor_cli/src/commands/subnets/subnets.py | 2 +- tests/e2e_tests/test_staking_sudo.py | 101 +++++++++++++++++- 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 78485fc0..fac96979 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -1439,7 +1439,7 @@ async def burn_cost( if current_burn_cost: if json_output: json_console.print( - json.dumps({"burn_cost": current_burn_cost, "error": ""}) + json.dumps({"burn_cost": current_burn_cost.to_dict(), "error": ""}) ) else: console.print( diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index ac82817d..2dfd1102 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -6,7 +6,10 @@ """ Verify commands: +* btcli s burn-cost * btcli subnets create +* btcli subnets set-identity +* btcli subnets get-identity * btcli subnets register * btcli stake add * btcli stake remove @@ -40,6 +43,21 @@ def test_staking(local_chain, wallet_setup): wallet_path_alice ) + burn_cost = exec_command_alice( + "subnets", + "burn-cost", + extra_args=[ + "--network", + "ws://127.0.0.1:9945", + "--json-output", + ], + ) + burn_cost_output = json.loads(burn_cost.stdout) + expected_burn_cost = Balance.from_tao(1000.0) + assert burn_cost_output["error"] == "" + assert burn_cost_output["burn_cost"]["rao"] == expected_burn_cost.rao + assert burn_cost_output["burn_cost"]["tao"] == expected_burn_cost.tao + # Register a subnet with sudo as Alice result = exec_command_alice( command="subnets", @@ -68,9 +86,12 @@ def test_staking(local_chain, wallet_setup): "--additional-info", "Created by Alice", "--no-prompt", + "--json-output", ], ) - assert f"✅ Registered subnetwork with netuid: {netuid}" in result.stdout + result_output = json.loads(result.stdout) + assert result_output["success"] is True + assert result_output["netuid"] == 2 # Register Alice in netuid = 1 using her hotkey register_subnet = exec_command_alice( @@ -92,6 +113,84 @@ def test_staking(local_chain, wallet_setup): ) assert "✅ Already Registered" in register_subnet.stdout + register_subnet_json = exec_command_alice( + command="subnets", + sub_command="register", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + "--json-output", + ], + ) + register_subnet_json_output = json.loads(register_subnet_json.stdout) + assert register_subnet_json_output["success"] is True + assert register_subnet_json_output["msg"] == "Already registered" + + # set identity + set_identity = exec_command_alice( + "subnets", + "set-identity", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--subnet-name", + sn_name := "Test Subnet", + "--github-repo", + sn_github := "https://github.com/username/repo", + "--subnet-contact", + sn_contact := "alice@opentensor.dev", + "--subnet-url", + sn_url := "https://testsubnet.com", + "--discord", + sn_discord := "alice#1234", + "--description", + sn_description := "A test subnet for e2e testing", + "--additional-info", + sn_add_info := "Created by Alice", + "--json-output", + "--no-prompt", + ], + ) + set_identity_output = json.loads(set_identity.stdout) + assert set_identity_output["success"] is True + + get_identity = exec_command_alice( + "subnets", + "get-identity", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--json-output", + ], + ) + get_identity_output = json.loads(get_identity.stdout) + assert get_identity_output["subnet_name"] == sn_name + assert get_identity_output["github_repo"] == sn_github + assert get_identity_output["subnet_contact"] == sn_contact + assert get_identity_output["subnet_url"] == sn_url + assert get_identity_output["discord"] == sn_discord + assert get_identity_output["description"] == sn_description + assert get_identity_output["additional"] == sn_add_info + # Add stake to Alice's hotkey add_stake = exec_command_alice( command="stake", From 2596878f83e0ff9b6cf8da798a3fc8478e943ac2 Mon Sep 17 00:00:00 2001 From: BD Himes <37844818+thewhaleking@users.noreply.github.com> Date: Fri, 4 Apr 2025 15:52:12 +0200 Subject: [PATCH 77/96] Update pyproject.toml Co-authored-by: Roman <167799377+roman-opentensor@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9354746a..99f00bb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ "scalecodec==1.2.11", "typer>=0.12,<0.16", "websockets>=14.1,<16.0", - "bittensor-wallet>=3.0.4", + "bittensor-wallet>=3.0.7", "plotille>=5.0.0", "pywry>=0.6.2", "plotly>=6.0.0", From 3b94054810bb5c00ae7b718f664fb62ab7605e7e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Apr 2025 16:07:15 +0200 Subject: [PATCH 78/96] New color palette --- bittensor_cli/src/__init__.py | 69 ++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 7011dfd4..af646393 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -746,23 +746,26 @@ def __init__(self): self.SU = self.SUDO class General(Gettable): - HEADER = "#4196D6" # Light Blue - LINKS = "#8CB9E9" # Sky Blue - HINT = "#A2E5B8" # Mint Green - COLDKEY = "#9EF5E4" # Aqua - HOTKEY = "#ECC39D" # Light Orange/Peach - SUBHEADING_MAIN = "#7ECFEC" # Light Cyan - SUBHEADING = "#AFEFFF" # Pale Blue - SUBHEADING_EXTRA_1 = "#96A3C5" # Grayish Blue - SUBHEADING_EXTRA_2 = "#6D7BAF" # Slate Blue - CONFIRMATION_Y_N_Q = "#EE8DF8" # Light Purple/Pink - SYMBOL = "#E7CC51" # Gold - BALANCE = "#4F91C6" # Medium Blue - COST = "#53B5A0" # Teal - SUCCESS = "#53B5A0" # Teal - NETUID = "#CBA880" # Tan - NETUID_EXTRA = "#DDD5A9" # Light Khaki - TEMPO = "#67A3A5" # Grayish Teal + HEADER = "#57878B" # mid blue green + LINKS = "#8CAAAE" # Pale Blue + HINT = "#6C8871" # Muted Green + COLDKEY = "#676B72" # Dark grey blue + HOTKEY = "#747065" # Dark orange grey + SUBHEADING_MAIN = "#8AA499" # muted Blue green + SUBHEADING = "#9BAC9C" # pale green + SUBHEADING_EXTRA_1 = "#C596A3" # Dusty Rose + SUBHEADING_EXTRA_2 = "#9BAC9C" # pale green + CONFIRMATION_Y_N_Q = "#EFB7AB" # pale Pink + SYMBOL = "#FE917A" # Salmon orange + SUBNET_NAME = "#C596A3" # Dusty Rose + VALIDATOR_NAME = "#9BAC9C" # Mid Lime Green + MINER_NAME = "#A17E7E" # Dusty Red + BALANCE = "#757B7B" # Muted teal Blue + COST = "#8EB27A" # Green + SUCCESS = "#3D7F71" # dark Teal + NETUID = "#BDC1C6" # GREY_400 + NETUID_EXTRA = "#D4D0C1" # Light yellow grey + TEMPO = "#927A71" # dark tan brown # aliases CK = COLDKEY HK = HOTKEY @@ -777,25 +780,25 @@ class Stake(Gettable): STAKE_AMOUNT = "#53B5A0" # Teal STAKE_ALPHA = "#53B5A0" # Teal STAKE_SWAP = "#67A3A5" # Grayish Teal - TAO = "#4F91C6" # Medium Blue - SLIPPAGE_TEXT = "#C25E7C" # Rose - SLIPPAGE_PERCENT = "#E7B195" # Light Coral - NOT_REGISTERED = "#EB6A6C" # Salmon Red - EXTRA_1 = "#D781BB" # Pink + TAO = "#8AA499" # faded Blue green + SLIPPAGE_TEXT = "#BA938A" # Brown Salmon + SLIPPAGE_PERCENT = "#CD8B7B" # Muted Salmon + NOT_REGISTERED = "#A87D7D" # Medium Red brown + EXTRA_1 = "#A45E44" # Deep autumn orange # aliases AMOUNT = STAKE_AMOUNT ALPHA = STAKE_ALPHA SWAP = STAKE_SWAP class Pools(Gettable): - TAO = "#4F91C6" # Medium Blue - ALPHA_IN = "#D09FE9" # Light Purple - ALPHA_OUT = "#AB7CC8" # Medium Purple - RATE = "#F8D384" # Light Orange - TAO_EQUIV = "#8CB9E9" # Sky Blue - EMISSION = "#F8D384" # Light Orange - EXTRA_1 = "#CAA8FB" # Lavender - EXTRA_2 = "#806DAF" # Dark Purple + TAO = "#8AA499" # faded Blue green + ALPHA_IN = "#C1913C" # Mustard + ALPHA_OUT = "#B49766" # Khaki Mustard + RATE = "#A46844" # Deep orange + TAO_EQUIV = "#93BBAF" # Teal Blue + EMISSION = "#B58065" # Med Orange + EXTRA_1 = "#919170" # Autumn green + EXTRA_2 = "#667862" # Forest green class Grey(Gettable): GREY_100 = "#F8F9FA" # Almost White @@ -820,9 +823,9 @@ class Grey(Gettable): G_900 = GREY_900 class Sudo(Gettable): - HYPERPARAMETER = "#4F91C6" # Medium Blue - VALUE = "#D09FE9" # Light Purple - NORMALIZED = "#AB7CC8" # Medium Purple + HYPERPARAMETER = "#93AFA3" # Forest + VALUE = "#B58065" # Burnt orange + NORMALIZED = "#A87575" # Burnt Red # aliases HYPERPARAM = HYPERPARAMETER NORMAL = NORMALIZED From 1ef0076d1c304c859a5a4500788c854bb4eead76 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Apr 2025 16:27:28 +0200 Subject: [PATCH 79/96] Fixed capitalisations --- bittensor_cli/src/__init__.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index af646393..9cd29041 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -746,26 +746,26 @@ def __init__(self): self.SU = self.SUDO class General(Gettable): - HEADER = "#57878B" # mid blue green + HEADER = "#57878B" # Mid Blue Green LINKS = "#8CAAAE" # Pale Blue HINT = "#6C8871" # Muted Green - COLDKEY = "#676B72" # Dark grey blue - HOTKEY = "#747065" # Dark orange grey - SUBHEADING_MAIN = "#8AA499" # muted Blue green - SUBHEADING = "#9BAC9C" # pale green + COLDKEY = "#676B72" # Dark Grey Blue + HOTKEY = "#747065" # Dark Orange Grey + SUBHEADING_MAIN = "#8AA499" # Muted Blue Green + SUBHEADING = "#9BAC9C" # Pale Green SUBHEADING_EXTRA_1 = "#C596A3" # Dusty Rose - SUBHEADING_EXTRA_2 = "#9BAC9C" # pale green - CONFIRMATION_Y_N_Q = "#EFB7AB" # pale Pink - SYMBOL = "#FE917A" # Salmon orange + SUBHEADING_EXTRA_2 = "#9BAC9C" # Pale Green + CONFIRMATION_Y_N_Q = "#EFB7AB" # Pale Pink + SYMBOL = "#FE917A" # Salmon Orange SUBNET_NAME = "#C596A3" # Dusty Rose VALIDATOR_NAME = "#9BAC9C" # Mid Lime Green MINER_NAME = "#A17E7E" # Dusty Red BALANCE = "#757B7B" # Muted teal Blue COST = "#8EB27A" # Green - SUCCESS = "#3D7F71" # dark Teal + SUCCESS = "#3D7F71" # Dark Teal NETUID = "#BDC1C6" # GREY_400 - NETUID_EXTRA = "#D4D0C1" # Light yellow grey - TEMPO = "#927A71" # dark tan brown + NETUID_EXTRA = "#D4D0C1" # Light Yellow Grey + TEMPO = "#927A71" # Dark Tan Brown # aliases CK = COLDKEY HK = HOTKEY @@ -780,21 +780,21 @@ class Stake(Gettable): STAKE_AMOUNT = "#53B5A0" # Teal STAKE_ALPHA = "#53B5A0" # Teal STAKE_SWAP = "#67A3A5" # Grayish Teal - TAO = "#8AA499" # faded Blue green + TAO = "#8AA499" # Faded Blue Green SLIPPAGE_TEXT = "#BA938A" # Brown Salmon SLIPPAGE_PERCENT = "#CD8B7B" # Muted Salmon NOT_REGISTERED = "#A87D7D" # Medium Red brown - EXTRA_1 = "#A45E44" # Deep autumn orange + EXTRA_1 = "#A45E44" # Deep Autumn Orange # aliases AMOUNT = STAKE_AMOUNT ALPHA = STAKE_ALPHA SWAP = STAKE_SWAP class Pools(Gettable): - TAO = "#8AA499" # faded Blue green + TAO = "#8AA499" # Faded Blue Green ALPHA_IN = "#C1913C" # Mustard ALPHA_OUT = "#B49766" # Khaki Mustard - RATE = "#A46844" # Deep orange + RATE = "#A46844" # Deep Orange TAO_EQUIV = "#93BBAF" # Teal Blue EMISSION = "#B58065" # Med Orange EXTRA_1 = "#919170" # Autumn green @@ -824,7 +824,7 @@ class Grey(Gettable): class Sudo(Gettable): HYPERPARAMETER = "#93AFA3" # Forest - VALUE = "#B58065" # Burnt orange + VALUE = "#B58065" # Burnt Orange NORMALIZED = "#A87575" # Burnt Red # aliases HYPERPARAM = HYPERPARAMETER From 495bfb59732813656f6fd9c98d9b907651cdc291 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 4 Apr 2025 17:32:10 +0200 Subject: [PATCH 80/96] Reduce direct requirements, and import exceptions for catching from async-substrate-interface --- bittensor_cli/cli.py | 7 +++++-- pyproject.toml | 3 +-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 5b508184..84497ce6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -15,14 +15,17 @@ import rich import typer import numpy as np -from async_substrate_interface.errors import SubstrateRequestException +from async_substrate_interface.errors import ( + SubstrateRequestException, + ConnectionClosed, + InvalidHandshake, +) 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 ( diff --git a/pyproject.toml b/pyproject.toml index 99f00bb0..e5768d6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ requires-python = ">=3.9,<3.14" dependencies = [ "wheel", "async-property==0.2.2", - "async-substrate-interface>=1.0.8", + "async-substrate-interface>=1.0.9", "aiohttp~=3.10.2", "backoff~=2.2.1", "GitPython>=3.0.0", @@ -31,7 +31,6 @@ dependencies = [ "rich>=13.7,<15.0", "scalecodec==1.2.11", "typer>=0.12,<0.16", - "websockets>=14.1,<16.0", "bittensor-wallet>=3.0.7", "plotille>=5.0.0", "pywry>=0.6.2", From f58663ae5c7ab9c9c6f3bc99171d137c519f3206 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Apr 2025 20:47:03 +0200 Subject: [PATCH 81/96] Bump substrate --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e5768d6a..4a406900 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ requires-python = ">=3.9,<3.14" dependencies = [ "wheel", "async-property==0.2.2", - "async-substrate-interface>=1.0.9", + "async-substrate-interface>=1.1.0", "aiohttp~=3.10.2", "backoff~=2.2.1", "GitPython>=3.0.0", From 9198997485c431be45887b8ea3ceb0ba46695e83 Mon Sep 17 00:00:00 2001 From: Doug Sillars Date: Mon, 7 Apr 2025 16:03:09 -0400 Subject: [PATCH 82/96] spelling fix --- bittensor_cli/src/commands/stake/remove.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 898af9cf..d1169a28 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -434,7 +434,7 @@ async def unstake_all( style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"], ) table.add_column( - f"Recieved ({Balance.unit})", + f"Received ({Balance.unit})", justify="center", style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"], ) From 80d0d2f7537d584edad977a0478de2f564c9a59e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Mon, 7 Apr 2025 22:47:23 +0200 Subject: [PATCH 83/96] Trigger no-op From e12bd7e0680c6f9e919ecc52ba85787b0b499553 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 8 Apr 2025 13:06:55 -0700 Subject: [PATCH 84/96] Updates sn symbols --- bittensor_cli/src/__init__.py | 110 ++++++++++++++++------------------ 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 9cd29041..c451cecb 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -169,7 +169,7 @@ class WalletValidationTypes(Enum): UNITS = [ # Greek Alphabet (0-24) - "\u03c4", # τ (tau, 0) + "\u03A4", # Τ (Upper case Tau, 0) "\u03b1", # α (alpha, 1) "\u03b2", # β (beta, 2) "\u03b3", # γ (gamma, 3) @@ -252,47 +252,47 @@ class WalletValidationTypes(Enum): "\u0648", # و (waw, 78) "\u064a", # ي (ya, 79) "\u0649", # ى (alef maksura, 80) - "\u064a", # ي (ya, 81) - # Runic Alphabet (82-90) - "\u16a0", # ᚠ (fehu, 82) - "\u16a2", # ᚢ (uruz, 83) - "\u16a6", # ᚦ (thurisaz, 84) - "\u16a8", # ᚨ (ansuz, 85) - "\u16b1", # ᚱ (raidho, 86) - "\u16b3", # ᚲ (kaunan, 87) - "\u16c7", # ᛇ (eihwaz, 88) - "\u16c9", # ᛉ (algiz, 89) - "\u16d2", # ᛒ (berkanan, 90) + # Runic Alphabet (81-90) + "\u16a0", # ᚠ (Fehu, wealth, 81) + "\u16a2", # ᚢ (Uruz, strength, 82) + "\u16a6", # ᚦ (Thurisaz, giant, 83) + "\u16a8", # ᚨ (Ansuz, god, 84) + "\u16b1", # ᚱ (Raidho, ride, 85) + "\u16b3", # ᚲ (Kaunan, ulcer, 86) + "\u042b", # Ы (Cyrillic Yeru, 87) + "\u16c9", # ᛉ (Algiz, protection, 88) + "\u16d2", # ᛒ (Berkanan, birch, 89) # Ogham Alphabet (91-97) - "\u1680", #   (Space, 91) - "\u1681", # ᚁ (Beith, 92) - "\u1682", # ᚂ (Luis, 93) - "\u1683", # ᚃ (Fearn, 94) - "\u1684", # ᚄ (Sail, 95) - "\u1685", # ᚅ (Nion, 96) - "\u169b", # ᚛ (Forfeda, 97) + "\u1680", # (Space, 90) + "\u1681", # ᚁ (Beith, birch, 91) + "\u1682", # ᚂ (Luis, rowan, 92) + "\u1683", # ᚃ (Fearn, alder, 93) + "\u1684", # ᚄ (Sail, willow, 94) + "\u1685", # ᚅ (Nion, ash, 95) + "\u169b", # ᚛ (Forfeda, 96) # Georgian Alphabet (98-103) - "\u10d0", # ა (ani, 98) - "\u10d1", # ბ (bani, 99) - "\u10d2", # გ (gani, 100) - "\u10d3", # დ (doni, 101) - "\u10d4", # ე (eni, 102) - "\u10d5", # ვ (vini, 103) + "\u10d0", # ა (Ani, 97) + "\u10d1", # ბ (Bani, 98) + "\u10d2", # გ (Gani, 99) + "\u10d3", # დ (Doni, 100) + "\u10d4", # ე (Eni, 101) + "\u10d5", # ვ (Vini, 102) # Armenian Alphabet (104-110) - "\u0531", # Ա (Ayp, 104) - "\u0532", # Բ (Ben, 105) - "\u0533", # Գ (Gim, 106) - "\u0534", # Դ (Da, 107) - "\u0535", # Ե (Ech, 108) - "\u0536", # Զ (Za, 109) - "\u055e", # ՞ (Question mark, 110) + "\u0531", # Ա (Ayp, 103) + "\u0532", # Բ (Ben, 104) + "\u0533", # Գ (Gim, 105) + "\u0534", # Դ (Da, 106) + "\u0535", # Ե (Ech, 107) + "\u0536", # Զ (Za, 108) + "\u055e", # ՞ (Question mark, 109) # Cyrillic Alphabet (111-116) - "\u0400", # Ѐ (Ie with grave, 111) - "\u0401", # Ё (Io, 112) - "\u0402", # Ђ (Dje, 113) - "\u0403", # Ѓ (Gje, 114) - "\u0404", # Є (Ukrainian Ie, 115) - "\u0405", # Ѕ (Dze, 116) + "\u0400", # Ѐ (Ie with grave, 110) + "\u0401", # Ё (Io, 111) + "\u0402", # Ђ (Dje, 112) + "\u0403", # Ѓ (Gje, 113) + "\u0404", # Є (Ukrainian Ie, 114) + "\u0405", # Ѕ (Dze, 115) + "\u044a", # ъ (Hard sign, 116) # Coptic Alphabet (117-122) "\u2c80", # Ⲁ (Alfa, 117) "\u2c81", # ⲁ (Small Alfa, 118) @@ -306,13 +306,13 @@ class WalletValidationTypes(Enum): "\U00011002", # 𑀂 (I, 125) "\U00011003", # 𑀃 (Ii, 126) "\U00011005", # 𑀅 (U, 127) - # Tifinagh Alphabet (128-133) - "\u2d30", # ⴰ (Ya, 128) - "\u2d31", # ⴱ (Yab, 129) - "\u2d32", # ⴲ (Yabh, 130) - "\u2d33", # ⴳ (Yag, 131) - "\u2d34", # ⴴ (Yagh, 132) - "\u2d35", # ⴵ (Yaj, 133) + # Sinhala Alphabet (128-133) + "\u0db1", # න (La, 128) + "\u0db2", # ඲ (Va, 129) + "\u0db3", # ප (Sha, 130) + "\u0db4", # ඵ (Ssa, 131) + "\u0db5", # බ (Sa, 132) + "\u0db6", # භ (Ha, 133) # Glagolitic Alphabet (134-166) "\u2c00", # Ⰰ (Az, 134) "\u2c01", # Ⰱ (Buky, 135) @@ -452,13 +452,13 @@ class WalletValidationTypes(Enum): "\u12b4", # ኴ (Ke, 265) "\u12b5", # ኵ (Kwe, 266) "\u12b6", # ኶ (Ko, 267) - "\u12a0", # ጐ (Go, 268) - "\u12a1", # ጑ (Gu, 269) - "\u12a2", # ጒ (Gi, 270) - "\u12a3", # መ (Gua, 271) - "\u12a4", # ጔ (Ge, 272) - "\u12a5", # ጕ (Gwe, 273) - "\u12a6", # ጖ (Go, 274) + "\u1290", # ጐ (Go, 268) + "\u1291", # ጑ (Gu, 269) + "\u1292", # ጒ (Gi, 270) + "\u1293", # መ (Gua, 271) + "\u1294", # ጔ (Ge, 272) + "\u1295", # ጕ (Gwe, 273) + "\u1296", # ጖ (Go, 274) # Devanagari Alphabet (275-318) "\u0905", # अ (A, 275) "\u0906", # आ (Aa, 276) @@ -627,12 +627,6 @@ class WalletValidationTypes(Enum): "\u0dae", # ථ (Ma, 436) "\u0daf", # ද (Ya, 437) "\u0db0", # ධ (Ra, 438) - "\u0db1", # ඲ (La, 439) - "\u0db2", # ඳ (Va, 440) - "\u0db3", # ප (Sha, 441) - "\u0db4", # ඵ (Ssa, 442) - "\u0db5", # බ (Sa, 443) - "\u0db6", # භ (Ha, 444) ] NETWORK_EXPLORER_MAP = { From 23cbac0dff2a743e2442e6209502b2c5b26cb32b Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 8 Apr 2025 13:09:34 -0700 Subject: [PATCH 85/96] Update comments --- bittensor_cli/src/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index c451cecb..586c11ca 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -262,7 +262,7 @@ class WalletValidationTypes(Enum): "\u042b", # Ы (Cyrillic Yeru, 87) "\u16c9", # ᛉ (Algiz, protection, 88) "\u16d2", # ᛒ (Berkanan, birch, 89) - # Ogham Alphabet (91-97) + # Ogham Alphabet (90-96) "\u1680", # (Space, 90) "\u1681", # ᚁ (Beith, birch, 91) "\u1682", # ᚂ (Luis, rowan, 92) @@ -270,14 +270,14 @@ class WalletValidationTypes(Enum): "\u1684", # ᚄ (Sail, willow, 94) "\u1685", # ᚅ (Nion, ash, 95) "\u169b", # ᚛ (Forfeda, 96) - # Georgian Alphabet (98-103) + # Georgian Alphabet (97-102) "\u10d0", # ა (Ani, 97) "\u10d1", # ბ (Bani, 98) "\u10d2", # გ (Gani, 99) "\u10d3", # დ (Doni, 100) "\u10d4", # ე (Eni, 101) "\u10d5", # ვ (Vini, 102) - # Armenian Alphabet (104-110) + # Armenian Alphabet (103-109) "\u0531", # Ա (Ayp, 103) "\u0532", # Բ (Ben, 104) "\u0533", # Գ (Gim, 105) @@ -285,7 +285,7 @@ class WalletValidationTypes(Enum): "\u0535", # Ե (Ech, 107) "\u0536", # Զ (Za, 108) "\u055e", # ՞ (Question mark, 109) - # Cyrillic Alphabet (111-116) + # Cyrillic Alphabet (110-116) "\u0400", # Ѐ (Ie with grave, 110) "\u0401", # Ё (Io, 111) "\u0402", # Ђ (Dje, 112) From 37e4b3b606f7574275e1da0b1a7a3c79c6b5b2da Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 8 Apr 2025 14:05:51 -0700 Subject: [PATCH 86/96] updates sn symbols --- bittensor_cli/src/__init__.py | 899 +++++++++++++++++----------------- 1 file changed, 440 insertions(+), 459 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 586c11ca..6ffebe86 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -168,465 +168,446 @@ class WalletValidationTypes(Enum): } UNITS = [ - # Greek Alphabet (0-24) - "\u03A4", # Τ (Upper case Tau, 0) - "\u03b1", # α (alpha, 1) - "\u03b2", # β (beta, 2) - "\u03b3", # γ (gamma, 3) - "\u03b4", # δ (delta, 4) - "\u03b5", # ε (epsilon, 5) - "\u03b6", # ζ (zeta, 6) - "\u03b7", # η (eta, 7) - "\u03b8", # θ (theta, 8) - "\u03b9", # ι (iota, 9) - "\u03ba", # κ (kappa, 10) - "\u03bb", # λ (lambda, 11) - "\u03bc", # μ (mu, 12) - "\u03bd", # ν (nu, 13) - "\u03be", # ξ (xi, 14) - "\u03bf", # ο (omicron, 15) - "\u03c0", # π (pi, 16) - "\u03c1", # ρ (rho, 17) - "\u03c3", # σ (sigma, 18) - "t", # t (tau, 19) - "\u03c5", # υ (upsilon, 20) - "\u03c6", # φ (phi, 21) - "\u03c7", # χ (chi, 22) - "\u03c8", # ψ (psi, 23) - "\u03c9", # ω (omega, 24) - # Hebrew Alphabet (25-51) - "\u05d0", # א (aleph, 25) - "\u05d1", # ב (bet, 26) - "\u05d2", # ג (gimel, 27) - "\u05d3", # ד (dalet, 28) - "\u05d4", # ה (he, 29) - "\u05d5", # ו (vav, 30) - "\u05d6", # ז (zayin, 31) - "\u05d7", # ח (het, 32) - "\u05d8", # ט (tet, 33) - "\u05d9", # י (yod, 34) - "\u05da", # ך (final kaf, 35) - "\u05db", # כ (kaf, 36) - "\u05dc", # ל (lamed, 37) - "\u05dd", # ם (final mem, 38) - "\u05de", # מ (mem, 39) - "\u05df", # ן (final nun, 40) - "\u05e0", # נ (nun, 41) - "\u05e1", # ס (samekh, 42) - "\u05e2", # ע (ayin, 43) - "\u05e3", # ף (final pe, 44) - "\u05e4", # פ (pe, 45) - "\u05e5", # ץ (final tsadi, 46) - "\u05e6", # צ (tsadi, 47) - "\u05e7", # ק (qof, 48) - "\u05e8", # ר (resh, 49) - "\u05e9", # ש (shin, 50) - "\u05ea", # ת (tav, 51) - # Arabic Alphabet (52-81) - "\u0627", # ا (alif, 52) - "\u0628", # ب (ba, 53) - "\u062a", # ت (ta, 54) - "\u062b", # ث (tha, 55) - "\u062c", # ج (jeem, 56) - "\u062d", # ح (ha, 57) - "\u062e", # خ (kha, 58) - "\u062f", # د (dal, 59) - "\u0630", # ذ (dhal, 60) - "\u0631", # ر (ra, 61) - "\u0632", # ز (zay, 62) - "\u0633", # س (seen, 63) - "\u0634", # ش (sheen, 64) - "\u0635", # ص (sad, 65) - "\u0636", # ض (dad, 66) - "\u0637", # ط (ta, 67) - "\u0638", # ظ (dha, 68) - "\u0639", # ع (ain, 69) - "\u063a", # غ (ghain, 70) - "\u0641", # ف (fa, 71) - "\u0642", # ق (qaf, 72) - "\u0643", # ك (kaf, 73) - "\u0644", # ل (lam, 74) - "\u0645", # م (meem, 75) - "\u0646", # ن (noon, 76) - "\u0647", # ه (ha, 77) - "\u0648", # و (waw, 78) - "\u064a", # ي (ya, 79) - "\u0649", # ى (alef maksura, 80) - # Runic Alphabet (81-90) - "\u16a0", # ᚠ (Fehu, wealth, 81) - "\u16a2", # ᚢ (Uruz, strength, 82) - "\u16a6", # ᚦ (Thurisaz, giant, 83) - "\u16a8", # ᚨ (Ansuz, god, 84) - "\u16b1", # ᚱ (Raidho, ride, 85) - "\u16b3", # ᚲ (Kaunan, ulcer, 86) - "\u042b", # Ы (Cyrillic Yeru, 87) - "\u16c9", # ᛉ (Algiz, protection, 88) - "\u16d2", # ᛒ (Berkanan, birch, 89) - # Ogham Alphabet (90-96) - "\u1680", # (Space, 90) - "\u1681", # ᚁ (Beith, birch, 91) - "\u1682", # ᚂ (Luis, rowan, 92) - "\u1683", # ᚃ (Fearn, alder, 93) - "\u1684", # ᚄ (Sail, willow, 94) - "\u1685", # ᚅ (Nion, ash, 95) - "\u169b", # ᚛ (Forfeda, 96) - # Georgian Alphabet (97-102) - "\u10d0", # ა (Ani, 97) - "\u10d1", # ბ (Bani, 98) - "\u10d2", # გ (Gani, 99) - "\u10d3", # დ (Doni, 100) - "\u10d4", # ე (Eni, 101) - "\u10d5", # ვ (Vini, 102) - # Armenian Alphabet (103-109) - "\u0531", # Ա (Ayp, 103) - "\u0532", # Բ (Ben, 104) - "\u0533", # Գ (Gim, 105) - "\u0534", # Դ (Da, 106) - "\u0535", # Ե (Ech, 107) - "\u0536", # Զ (Za, 108) - "\u055e", # ՞ (Question mark, 109) - # Cyrillic Alphabet (110-116) - "\u0400", # Ѐ (Ie with grave, 110) - "\u0401", # Ё (Io, 111) - "\u0402", # Ђ (Dje, 112) - "\u0403", # Ѓ (Gje, 113) - "\u0404", # Є (Ukrainian Ie, 114) - "\u0405", # Ѕ (Dze, 115) - "\u044a", # ъ (Hard sign, 116) - # Coptic Alphabet (117-122) - "\u2c80", # Ⲁ (Alfa, 117) - "\u2c81", # ⲁ (Small Alfa, 118) - "\u2c82", # Ⲃ (Vida, 119) - "\u2c83", # ⲃ (Small Vida, 120) - "\u2c84", # Ⲅ (Gamma, 121) - "\u2c85", # ⲅ (Small Gamma, 122) - # Brahmi Script (123-127) - "\U00011000", # 𑀀 (A, 123) - "\U00011001", # 𑀁 (Aa, 124) - "\U00011002", # 𑀂 (I, 125) - "\U00011003", # 𑀃 (Ii, 126) - "\U00011005", # 𑀅 (U, 127) - # Sinhala Alphabet (128-133) - "\u0db1", # න (La, 128) - "\u0db2", # ඲ (Va, 129) - "\u0db3", # ප (Sha, 130) - "\u0db4", # ඵ (Ssa, 131) - "\u0db5", # බ (Sa, 132) - "\u0db6", # භ (Ha, 133) - # Glagolitic Alphabet (134-166) - "\u2c00", # Ⰰ (Az, 134) - "\u2c01", # Ⰱ (Buky, 135) - "\u2c02", # Ⰲ (Vede, 136) - "\u2c03", # Ⰳ (Glagoli, 137) - "\u2c04", # Ⰴ (Dobro, 138) - "\u2c05", # Ⰵ (Yest, 139) - "\u2c06", # Ⰶ (Zhivete, 140) - "\u2c07", # Ⰷ (Zemlja, 141) - "\u2c08", # Ⰸ (Izhe, 142) - "\u2c09", # Ⰹ (Initial Izhe, 143) - "\u2c0a", # Ⰺ (I, 144) - "\u2c0b", # Ⰻ (Djerv, 145) - "\u2c0c", # Ⰼ (Kako, 146) - "\u2c0d", # Ⰽ (Ljudije, 147) - "\u2c0e", # Ⰾ (Myse, 148) - "\u2c0f", # Ⰿ (Nash, 149) - "\u2c10", # Ⱀ (On, 150) - "\u2c11", # Ⱁ (Pokoj, 151) - "\u2c12", # Ⱂ (Rtsy, 152) - "\u2c13", # Ⱃ (Slovo, 153) - "\u2c14", # Ⱄ (Tvrido, 154) - "\u2c15", # Ⱅ (Uku, 155) - "\u2c16", # Ⱆ (Fert, 156) - "\u2c17", # Ⱇ (Xrivi, 157) - "\u2c18", # Ⱈ (Ot, 158) - "\u2c19", # Ⱉ (Cy, 159) - "\u2c1a", # Ⱊ (Shcha, 160) - "\u2c1b", # Ⱋ (Er, 161) - "\u2c1c", # Ⱌ (Yeru, 162) - "\u2c1d", # Ⱍ (Small Yer, 163) - "\u2c1e", # Ⱎ (Yo, 164) - "\u2c1f", # Ⱏ (Yu, 165) - "\u2c20", # Ⱐ (Ja, 166) - # Thai Alphabet (167-210) - "\u0e01", # ก (Ko Kai, 167) - "\u0e02", # ข (Kho Khai, 168) - "\u0e03", # ฃ (Kho Khuat, 169) - "\u0e04", # ค (Kho Khon, 170) - "\u0e05", # ฅ (Kho Rakhang, 171) - "\u0e06", # ฆ (Kho Khwai, 172) - "\u0e07", # ง (Ngo Ngu, 173) - "\u0e08", # จ (Cho Chan, 174) - "\u0e09", # ฉ (Cho Ching, 175) - "\u0e0a", # ช (Cho Chang, 176) - "\u0e0b", # ซ (So So, 177) - "\u0e0c", # ฌ (Cho Choe, 178) - "\u0e0d", # ญ (Yo Ying, 179) - "\u0e0e", # ฎ (Do Chada, 180) - "\u0e0f", # ฏ (To Patak, 181) - "\u0e10", # ฐ (Tho Than, 182) - "\u0e11", # ฑ (Tho Nangmontho, 183) - "\u0e12", # ฒ (Tho Phuthao, 184) - "\u0e13", # ณ (No Nen, 185) - "\u0e14", # ด (Do Dek, 186) - "\u0e15", # ต (To Tao, 187) - "\u0e16", # ถ (Tho Thung, 188) - "\u0e17", # ท (Tho Thahan, 189) - "\u0e18", # ธ (Tho Thong, 190) - "\u0e19", # น (No Nu, 191) - "\u0e1a", # บ (Bo Baimai, 192) - "\u0e1b", # ป (Po Pla, 193) - "\u0e1c", # ผ (Pho Phung, 194) - "\u0e1d", # ฝ (Fo Fa, 195) - "\u0e1e", # พ (Pho Phan, 196) - "\u0e1f", # ฟ (Fo Fan, 197) - "\u0e20", # ภ (Pho Samphao, 198) - "\u0e21", # ม (Mo Ma, 199) - "\u0e22", # ย (Yo Yak, 200) - "\u0e23", # ร (Ro Rua, 201) - "\u0e25", # ล (Lo Ling, 202) - "\u0e27", # ว (Wo Waen, 203) - "\u0e28", # ศ (So Sala, 204) - "\u0e29", # ษ (So Rusi, 205) - "\u0e2a", # ส (So Sua, 206) - "\u0e2b", # ห (Ho Hip, 207) - "\u0e2c", # ฬ (Lo Chula, 208) - "\u0e2d", # อ (O Ang, 209) - "\u0e2e", # ฮ (Ho Nokhuk, 210) - # Hangul Consonants (211-224) - "\u1100", # ㄱ (Giyeok, 211) - "\u1101", # ㄴ (Nieun, 212) - "\u1102", # ㄷ (Digeut, 213) - "\u1103", # ㄹ (Rieul, 214) - "\u1104", # ㅁ (Mieum, 215) - "\u1105", # ㅂ (Bieup, 216) - "\u1106", # ㅅ (Siot, 217) - "\u1107", # ㅇ (Ieung, 218) - "\u1108", # ㅈ (Jieut, 219) - "\u1109", # ㅊ (Chieut, 220) - "\u110a", # ㅋ (Kieuk, 221) - "\u110b", # ㅌ (Tieut, 222) - "\u110c", # ㅍ (Pieup, 223) - "\u110d", # ㅎ (Hieut, 224) - # Hangul Vowels (225-245) - "\u1161", # ㅏ (A, 225) - "\u1162", # ㅐ (Ae, 226) - "\u1163", # ㅑ (Ya, 227) - "\u1164", # ㅒ (Yae, 228) - "\u1165", # ㅓ (Eo, 229) - "\u1166", # ㅔ (E, 230) - "\u1167", # ㅕ (Yeo, 231) - "\u1168", # ㅖ (Ye, 232) - "\u1169", # ㅗ (O, 233) - "\u116a", # ㅘ (Wa, 234) - "\u116b", # ㅙ (Wae, 235) - "\u116c", # ㅚ (Oe, 236) - "\u116d", # ㅛ (Yo, 237) - "\u116e", # ㅜ (U, 238) - "\u116f", # ㅝ (Weo, 239) - "\u1170", # ㅞ (We, 240) - "\u1171", # ㅟ (Wi, 241) - "\u1172", # ㅠ (Yu, 242) - "\u1173", # ㅡ (Eu, 243) - "\u1174", # ㅢ (Ui, 244) - "\u1175", # ㅣ (I, 245) - # Ethiopic Alphabet (246-274) - "\u12a0", # አ (Glottal A, 246) - "\u12a1", # ኡ (Glottal U, 247) - "\u12a2", # ኢ (Glottal I, 248) - "\u12a3", # ኣ (Glottal Aa, 249) - "\u12a4", # ኤ (Glottal E, 250) - "\u12a5", # እ (Glottal Ie, 251) - "\u12a6", # ኦ (Glottal O, 252) - "\u12a7", # ኧ (Glottal Wa, 253) - "\u12c8", # ወ (Wa, 254) - "\u12c9", # ዉ (Wu, 255) - "\u12ca", # ዊ (Wi, 256) - "\u12cb", # ዋ (Waa, 257) - "\u12cc", # ዌ (We, 258) - "\u12cd", # ው (Wye, 259) - "\u12ce", # ዎ (Wo, 260) - "\u12b0", # ኰ (Ko, 261) - "\u12b1", # ኱ (Ku, 262) - "\u12b2", # ኲ (Ki, 263) - "\u12b3", # ኳ (Kua, 264) - "\u12b4", # ኴ (Ke, 265) - "\u12b5", # ኵ (Kwe, 266) - "\u12b6", # ኶ (Ko, 267) - "\u1290", # ጐ (Go, 268) - "\u1291", # ጑ (Gu, 269) - "\u1292", # ጒ (Gi, 270) - "\u1293", # መ (Gua, 271) - "\u1294", # ጔ (Ge, 272) - "\u1295", # ጕ (Gwe, 273) - "\u1296", # ጖ (Go, 274) - # Devanagari Alphabet (275-318) - "\u0905", # अ (A, 275) - "\u0906", # आ (Aa, 276) - "\u0907", # इ (I, 277) - "\u0908", # ई (Ii, 278) - "\u0909", # उ (U, 279) - "\u090a", # ऊ (Uu, 280) - "\u090b", # ऋ (R, 281) - "\u090f", # ए (E, 282) - "\u0910", # ऐ (Ai, 283) - "\u0913", # ओ (O, 284) - "\u0914", # औ (Au, 285) - "\u0915", # क (Ka, 286) - "\u0916", # ख (Kha, 287) - "\u0917", # ग (Ga, 288) - "\u0918", # घ (Gha, 289) - "\u0919", # ङ (Nga, 290) - "\u091a", # च (Cha, 291) - "\u091b", # छ (Chha, 292) - "\u091c", # ज (Ja, 293) - "\u091d", # झ (Jha, 294) - "\u091e", # ञ (Nya, 295) - "\u091f", # ट (Ta, 296) - "\u0920", # ठ (Tha, 297) - "\u0921", # ड (Da, 298) - "\u0922", # ढ (Dha, 299) - "\u0923", # ण (Na, 300) - "\u0924", # त (Ta, 301) - "\u0925", # थ (Tha, 302) - "\u0926", # द (Da, 303) - "\u0927", # ध (Dha, 304) - "\u0928", # न (Na, 305) - "\u092a", # प (Pa, 306) - "\u092b", # फ (Pha, 307) - "\u092c", # ब (Ba, 308) - "\u092d", # भ (Bha, 309) - "\u092e", # म (Ma, 310) - "\u092f", # य (Ya, 311) - "\u0930", # र (Ra, 312) - "\u0932", # ल (La, 313) - "\u0935", # व (Va, 314) - "\u0936", # श (Sha, 315) - "\u0937", # ष (Ssa, 316) - "\u0938", # स (Sa, 317) - "\u0939", # ह (Ha, 318) - # Katakana Alphabet (319-364) - "\u30a2", # ア (A, 319) - "\u30a4", # イ (I, 320) - "\u30a6", # ウ (U, 321) - "\u30a8", # エ (E, 322) - "\u30aa", # オ (O, 323) - "\u30ab", # カ (Ka, 324) - "\u30ad", # キ (Ki, 325) - "\u30af", # ク (Ku, 326) - "\u30b1", # ケ (Ke, 327) - "\u30b3", # コ (Ko, 328) - "\u30b5", # サ (Sa, 329) - "\u30b7", # シ (Shi, 330) - "\u30b9", # ス (Su, 331) - "\u30bb", # セ (Se, 332) - "\u30bd", # ソ (So, 333) - "\u30bf", # タ (Ta, 334) - "\u30c1", # チ (Chi, 335) - "\u30c4", # ツ (Tsu, 336) - "\u30c6", # テ (Te, 337) - "\u30c8", # ト (To, 338) - "\u30ca", # ナ (Na, 339) - "\u30cb", # ニ (Ni, 340) - "\u30cc", # ヌ (Nu, 341) - "\u30cd", # ネ (Ne, 342) - "\u30ce", # ノ (No, 343) - "\u30cf", # ハ (Ha, 344) - "\u30d2", # ヒ (Hi, 345) - "\u30d5", # フ (Fu, 346) - "\u30d8", # ヘ (He, 347) - "\u30db", # ホ (Ho, 348) - "\u30de", # マ (Ma, 349) - "\u30df", # ミ (Mi, 350) - "\u30e0", # ム (Mu, 351) - "\u30e1", # メ (Me, 352) - "\u30e2", # モ (Mo, 353) - "\u30e4", # ヤ (Ya, 354) - "\u30e6", # ユ (Yu, 355) - "\u30e8", # ヨ (Yo, 356) - "\u30e9", # ラ (Ra, 357) - "\u30ea", # リ (Ri, 358) - "\u30eb", # ル (Ru, 359) - "\u30ec", # レ (Re, 360) - "\u30ed", # ロ (Ro, 361) - "\u30ef", # ワ (Wa, 362) - "\u30f2", # ヲ (Wo, 363) - "\u30f3", # ン (N, 364) - # Tifinagh Alphabet (365-400) - "\u2d30", # ⴰ (Ya, 365) - "\u2d31", # ⴱ (Yab, 366) - "\u2d32", # ⴲ (Yabh, 367) - "\u2d33", # ⴳ (Yag, 368) - "\u2d34", # ⴴ (Yagh, 369) - "\u2d35", # ⴵ (Yaj, 370) - "\u2d36", # ⴶ (Yach, 371) - "\u2d37", # ⴷ (Yad, 372) - "\u2d38", # ⴸ (Yadh, 373) - "\u2d39", # ⴹ (Yadh, emphatic, 374) - "\u2d3a", # ⴺ (Yaz, 375) - "\u2d3b", # ⴻ (Yazh, 376) - "\u2d3c", # ⴼ (Yaf, 377) - "\u2d3d", # ⴽ (Yak, 378) - "\u2d3e", # ⴾ (Yak, variant, 379) - "\u2d3f", # ⴿ (Yaq, 380) - "\u2d40", # ⵀ (Yah, 381) - "\u2d41", # ⵁ (Yahh, 382) - "\u2d42", # ⵂ (Yahl, 383) - "\u2d43", # ⵃ (Yahm, 384) - "\u2d44", # ⵄ (Yayn, 385) - "\u2d45", # ⵅ (Yakh, 386) - "\u2d46", # ⵆ (Yakl, 387) - "\u2d47", # ⵇ (Yahq, 388) - "\u2d48", # ⵈ (Yash, 389) - "\u2d49", # ⵉ (Yi, 390) - "\u2d4a", # ⵊ (Yij, 391) - "\u2d4b", # ⵋ (Yizh, 392) - "\u2d4c", # ⵌ (Yink, 393) - "\u2d4d", # ⵍ (Yal, 394) - "\u2d4e", # ⵎ (Yam, 395) - "\u2d4f", # ⵏ (Yan, 396) - "\u2d50", # ⵐ (Yang, 397) - "\u2d51", # ⵑ (Yany, 398) - "\u2d52", # ⵒ (Yap, 399) - "\u2d53", # ⵓ (Yu, 400) - # Sinhala Alphabet (401-444) - "\u0d85", # අ (A, 401) - "\u0d86", # ආ (Aa, 402) - "\u0d87", # ඉ (I, 403) - "\u0d88", # ඊ (Ii, 404) - "\u0d89", # උ (U, 405) - "\u0d8a", # ඌ (Uu, 406) - "\u0d8b", # ඍ (R, 407) - "\u0d8c", # ඎ (Rr, 408) - "\u0d8f", # ඏ (L, 409) - "\u0d90", # ඐ (Ll, 410) - "\u0d91", # එ (E, 411) - "\u0d92", # ඒ (Ee, 412) - "\u0d93", # ඓ (Ai, 413) - "\u0d94", # ඔ (O, 414) - "\u0d95", # ඕ (Oo, 415) - "\u0d96", # ඖ (Au, 416) - "\u0d9a", # ක (Ka, 417) - "\u0d9b", # ඛ (Kha, 418) - "\u0d9c", # ග (Ga, 419) - "\u0d9d", # ඝ (Gha, 420) - "\u0d9e", # ඞ (Nga, 421) - "\u0d9f", # ච (Cha, 422) - "\u0da0", # ඡ (Chha, 423) - "\u0da1", # ජ (Ja, 424) - "\u0da2", # ඣ (Jha, 425) - "\u0da3", # ඤ (Nya, 426) - "\u0da4", # ට (Ta, 427) - "\u0da5", # ඥ (Tha, 428) - "\u0da6", # ඦ (Da, 429) - "\u0da7", # ට (Dha, 430) - "\u0da8", # ඨ (Na, 431) - "\u0daa", # ඪ (Pa, 432) - "\u0dab", # ණ (Pha, 433) - "\u0dac", # ඬ (Ba, 434) - "\u0dad", # ත (Bha, 435) - "\u0dae", # ථ (Ma, 436) - "\u0daf", # ද (Ya, 437) - "\u0db0", # ධ (Ra, 438) + b"\xCE\xA4".decode(), # Τ (Upper case Tau, 0) + b"\xCE\xB1".decode(), # α (Alpha, 1) + b"\xCE\xB2".decode(), # β (Beta, 2) + b"\xCE\xB3".decode(), # γ (Gamma, 3) + b"\xCE\xB4".decode(), # δ (Delta, 4) + b"\xCE\xB5".decode(), # ε (Epsilon, 5) + b"\xCE\xB6".decode(), # ζ (Zeta, 6) + b"\xCE\xB7".decode(), # η (Eta, 7) + b"\xCE\xB8".decode(), # θ (Theta, 8) + b"\xCE\xB9".decode(), # ι (Iota, 9) + b"\xCE\xBA".decode(), # κ (Kappa, 10) + b"\xCE\xBB".decode(), # λ (Lambda, 11) + b"\xCE\xBC".decode(), # μ (Mu, 12) + b"\xCE\xBD".decode(), # ν (Nu, 13) + b"\xCE\xBE".decode(), # ξ (Xi, 14) + b"\xCE\xBF".decode(), # ο (Omicron, 15) + b"\xCF\x80".decode(), # π (Pi, 16) + b"\xCF\x81".decode(), # ρ (Rho, 17) + b"\xCF\x83".decode(), # σ (Sigma, 18) + "t", # t (Tau, 19) + b"\xCF\x85".decode(), # υ (Upsilon, 20) + b"\xCF\x86".decode(), # φ (Phi, 21) + b"\xCF\x87".decode(), # χ (Chi, 22) + b"\xCF\x88".decode(), # ψ (Psi, 23) + b"\xCF\x89".decode(), # ω (Omega, 24) + b"\xD7\x90".decode(), # א (Aleph, 25) + b"\xD7\x91".decode(), # ב (Bet, 26) + b"\xD7\x92".decode(), # ג (Gimel, 27) + b"\xD7\x93".decode(), # ד (Dalet, 28) + b"\xD7\x94".decode(), # ה (He, 29) + b"\xD7\x95".decode(), # ו (Vav, 30) + b"\xD7\x96".decode(), # ז (Zayin, 31) + b"\xD7\x97".decode(), # ח (Het, 32) + b"\xD7\x98".decode(), # ט (Tet, 33) + b"\xD7\x99".decode(), # י (Yod, 34) + b"\xD7\x9A".decode(), # ך (Final Kaf, 35) + b"\xD7\x9B".decode(), # כ (Kaf, 36) + b"\xD7\x9C".decode(), # ל (Lamed, 37) + b"\xD7\x9D".decode(), # ם (Final Mem, 38) + b"\xD7\x9E".decode(), # מ (Mem, 39) + b"\xD7\x9F".decode(), # ן (Final Nun, 40) + b"\xD7\xA0".decode(), # נ (Nun, 41) + b"\xD7\xA1".decode(), # ס (Samekh, 42) + b"\xD7\xA2".decode(), # ע (Ayin, 43) + b"\xD7\xA3".decode(), # ף (Final Pe, 44) + b"\xD7\xA4".decode(), # פ (Pe, 45) + b"\xD7\xA5".decode(), # ץ (Final Tsadi, 46) + b"\xD7\xA6".decode(), # צ (Tsadi, 47) + b"\xD7\xA7".decode(), # ק (Qof, 48) + b"\xD7\xA8".decode(), # ר (Resh, 49) + b"\xD7\xA9".decode(), # ש (Shin, 50) + b"\xD7\xAA".decode(), # ת (Tav, 51) + b"\xD8\xA7".decode(), # ا (Alif, 52) + b"\xD8\xA8".decode(), # ب (Ba, 53) + b"\xD8\xAA".decode(), # ت (Ta, 54) + b"\xD8\xAB".decode(), # ث (Tha, 55) + b"\xD8\xAC".decode(), # ج (Jim, 56) + b"\xD8\xAD".decode(), # ح (Ha, 57) + b"\xD8\xAE".decode(), # خ (Kha, 58) + b"\xD8\xAF".decode(), # د (Dal, 59) + b"\xD8\xB0".decode(), # ذ (Dhal, 60) + b"\xD8\xB1".decode(), # ر (Ra, 61) + b"\xD8\xB2".decode(), # ز (Zay, 62) + b"\xD8\xB3".decode(), # س (Sin, 63) + b"\xD8\xB4".decode(), # ش (Shin, 64) + b"\xD8\xB5".decode(), # ص (Sad, 65) + b"\xD8\xB6".decode(), # ض (Dad, 66) + b"\xD8\xB7".decode(), # ط (Ta, 67) + b"\xD8\xB8".decode(), # ظ (Dha, 68) + b"\xD8\xB9".decode(), # ع (Ain, 69) + b"\xD8\xBA".decode(), # غ (Ghayn, 70) + b"\xD9\x81".decode(), # ف (Fa, 71) + b"\xD9\x82".decode(), # ق (Qaf, 72) + b"\xD9\x83".decode(), # ك (Kaf, 73) + b"\xD9\x84".decode(), # ل (Lam, 74) + b"\xD9\x85".decode(), # م (Mim, 75) + b"\xD9\x86".decode(), # ن (Nun, 76) + b"\xD9\x87".decode(), # ه (Ha, 77) + b"\xD9\x88".decode(), # و (Waw, 78) + b"\xD9\x8A".decode(), # ي (Ya, 79) + b"\xD9\x89".decode(), # ى (Alef Maksura, 80) + b"\xE1\x9A\xA0".decode(), # ᚠ (Fehu, wealth, 81) + b"\xE1\x9A\xA2".decode(), # ᚢ (Uruz, strength, 82) + b"\xE1\x9A\xA6".decode(), # ᚦ (Thurisaz, giant, 83) + b"\xE1\x9A\xA8".decode(), # ᚨ (Ansuz, god, 84) + b"\xE1\x9A\xB1".decode(), # ᚱ (Raidho, ride, 85) + b"\xE1\x9A\xB3".decode(), # ᚲ (Kaunan, ulcer, 86) + b"\xD0\xAB".decode(), # Ы (Cyrillic Yeru, 87) + b"\xE1\x9B\x89".decode(), # ᛉ (Algiz, protection, 88) + b"\xE1\x9B\x92".decode(), # ᛒ (Berkanan, birch, 89) + b"\xE1\x9A\x80".decode(), #   (Space, 90) + b"\xE1\x9A\x81".decode(), # ᚁ (Beith, birch, 91) + b"\xE1\x9A\x82".decode(), # ᚂ (Luis, rowan, 92) + b"\xE1\x9A\x83".decode(), # ᚃ (Fearn, alder, 93) + b"\xE1\x9A\x84".decode(), # ᚄ (Sail, willow, 94) + b"\xE1\x9A\x85".decode(), # ᚅ (Nion, ash, 95) + b"\xE1\x9A\x9B".decode(), # ᚛ (Forfeda, 96) + b"\xE1\x83\x90".decode(), # ა (Ani, 97) + b"\xE1\x83\x91".decode(), # ბ (Bani, 98) + b"\xE1\x83\x92".decode(), # გ (Gani, 99) + b"\xE1\x83\x93".decode(), # დ (Doni, 100) + b"\xE1\x83\x94".decode(), # ე (Eni, 101) + b"\xE1\x83\x95".decode(), # ვ (Vini, 102) + b"\xD4\xB1".decode(), # Ա (Ayp, 103) + b"\xD4\xB2".decode(), # Բ (Ben, 104) + b"\xD4\xB3".decode(), # Գ (Gim, 105) + b"\xD4\xB4".decode(), # Դ (Da, 106) + b"\xD4\xB5".decode(), # Ե (Ech, 107) + b"\xD4\xB6".decode(), # Զ (Za, 108) + b"\xD5\x9E".decode(), # ՞ (Question mark, 109) + b"\xD0\x80".decode(), # Ѐ (Ie with grave, 110) + b"\xD0\x81".decode(), # Ё (Io, 111) + b"\xD0\x82".decode(), # Ђ (Dje, 112) + b"\xD0\x83".decode(), # Ѓ (Gje, 113) + b"\xD0\x84".decode(), # Є (Ukrainian Ie, 114) + b"\xD0\x85".decode(), # Ѕ (Dze, 115) + b"\xD1\x8A".decode(), # Ъ (Hard sign, 116) + b"\xE2\xB2\x80".decode(), # Ⲁ (Alfa, 117) + b"\xE2\xB2\x81".decode(), # ⲁ (Small Alfa, 118) + b"\xE2\xB2\x82".decode(), # Ⲃ (Vida, 119) + b"\xE2\xB2\x83".decode(), # ⲃ (Small Vida, 120) + b"\xE2\xB2\x84".decode(), # Ⲅ (Gamma, 121) + b"\xE2\xB2\x85".decode(), # ⲅ (Small Gamma, 122) + b"\xF0\x91\x80\x80".decode(), # 𑀀 (A, 123) + b"\xF0\x91\x80\x81".decode(), # 𑀁 (Aa, 124) + b"\xF0\x91\x80\x82".decode(), # 𑀂 (I, 125) + b"\xF0\x91\x80\x83".decode(), # 𑀃 (Ii, 126) + b"\xF0\x91\x80\x85".decode(), # 𑀅 (U, 127) + b"\xE0\xB6\xB1".decode(), # ඲ (La, 128) + b"\xE0\xB6\xB2".decode(), # ඳ (Va, 129) + b"\xE0\xB6\xB3".decode(), # ප (Sha, 130) + b"\xE0\xB6\xB4".decode(), # ඵ (Ssa, 131) + b"\xE0\xB6\xB5".decode(), # බ (Sa, 132) + b"\xE0\xB6\xB6".decode(), # භ (Ha, 133) + b"\xE2\xB0\x80".decode(), # Ⰰ (Az, 134) + b"\xE2\xB0\x81".decode(), # Ⰱ (Buky, 135) + b"\xE2\xB0\x82".decode(), # Ⰲ (Vede, 136) + b"\xE2\xB0\x83".decode(), # Ⰳ (Glagoli, 137) + b"\xE2\xB0\x84".decode(), # Ⰴ (Dobro, 138) + b"\xE2\xB0\x85".decode(), # Ⰵ (Yest, 139) + b"\xE2\xB0\x86".decode(), # Ⰶ (Zhivete, 140) + b"\xE2\xB0\x87".decode(), # Ⰷ (Zemlja, 141) + b"\xE2\xB0\x88".decode(), # Ⰸ (Izhe, 142) + b"\xE2\xB0\x89".decode(), # Ⰹ (Initial Izhe, 143) + b"\xE2\xB0\x8A".decode(), # Ⰺ (I, 144) + b"\xE2\xB0\x8B".decode(), # Ⰻ (Djerv, 145) + b"\xE2\xB0\x8C".decode(), # Ⰼ (Kako, 146) + b"\xE2\xB0\x8D".decode(), # Ⰽ (Ljudije, 147) + b"\xE2\xB0\x8E".decode(), # Ⰾ (Myse, 148) + b"\xE2\xB0\x8F".decode(), # Ⰿ (Nash, 149) + b"\xE2\xB0\x90".decode(), # Ⱀ (On, 150) + b"\xE2\xB0\x91".decode(), # Ⱁ (Pokoj, 151) + b"\xE2\xB0\x92".decode(), # Ⱂ (Rtsy, 152) + b"\xE2\xB0\x93".decode(), # Ⱃ (Slovo, 153) + b"\xE2\xB0\x94".decode(), # Ⱄ (Tvrido, 154) + b"\xE2\xB0\x95".decode(), # Ⱅ (Uku, 155) + b"\xE2\xB0\x96".decode(), # Ⱆ (Fert, 156) + b"\xE2\xB0\x97".decode(), # Ⱇ (Xrivi, 157) + b"\xE2\xB0\x98".decode(), # Ⱈ (Ot, 158) + b"\xE2\xB0\x99".decode(), # Ⱉ (Cy, 159) + b"\xE2\xB0\x9A".decode(), # Ⱊ (Shcha, 160) + b"\xE2\xB0\x9B".decode(), # Ⱋ (Er, 161) + b"\xE2\xB0\x9C".decode(), # Ⱌ (Yeru, 162) + b"\xE2\xB0\x9D".decode(), # Ⱍ (Small Yer, 163) + b"\xE2\xB0\x9E".decode(), # Ⱎ (Yo, 164) + b"\xE2\xB0\x9F".decode(), # Ⱏ (Yu, 165) + b"\xE2\xB0\xA0".decode(), # Ⱐ (Ja, 166) + b"\xE0\xB8\x81".decode(), # ก (Ko Kai, 167) + b"\xE0\xB8\x82".decode(), # ข (Kho Khai, 168) + b"\xE0\xB8\x83".decode(), # ฃ (Kho Khuat, 169) + b"\xE0\xB8\x84".decode(), # ค (Kho Khon, 170) + b"\xE0\xB8\x85".decode(), # ฅ (Kho Rakhang, 171) + b"\xE0\xB8\x86".decode(), # ฆ (Kho Khwai, 172) + b"\xE0\xB8\x87".decode(), # ง (Ngo Ngu, 173) + b"\xE0\xB8\x88".decode(), # จ (Cho Chan, 174) + b"\xE0\xB8\x89".decode(), # ฉ (Cho Ching, 175) + b"\xE0\xB8\x8A".decode(), # ช (Cho Chang, 176) + b"\xE0\xB8\x8B".decode(), # ซ (So So, 177) + b"\xE0\xB8\x8C".decode(), # ฌ (Cho Choe, 178) + b"\xE0\xB8\x8D".decode(), # ญ (Yo Ying, 179) + b"\xE0\xB8\x8E".decode(), # ฎ (Do Chada, 180) + b"\xE0\xB8\x8F".decode(), # ฏ (To Patak, 181) + b"\xE0\xB8\x90".decode(), # ฐ (Tho Than, 182) + b"\xE0\xB8\x91".decode(), # ฑ (Tho Nangmontho, 183) + b"\xE0\xB8\x92".decode(), # ฒ (Tho Phuthao, 184) + b"\xE0\xB8\x93".decode(), # ณ (No Nen, 185) + b"\xE0\xB8\x94".decode(), # ด (Do Dek, 186) + b"\xE0\xB8\x95".decode(), # ต (To Tao, 187) + b"\xE0\xB8\x96".decode(), # ถ (Tho Thung, 188) + b"\xE0\xB8\x97".decode(), # ท (Tho Thahan, 189) + b"\xE0\xB8\x98".decode(), # ธ (Tho Thong, 190) + b"\xE0\xB8\x99".decode(), # น (No Nu, 191) + b"\xE0\xB8\x9A".decode(), # บ (Bo Baimai, 192) + b"\xE0\xB8\x9B".decode(), # ป (Po Pla, 193) + b"\xE0\xB8\x9C".decode(), # ผ (Pho Phung, 194) + b"\xE0\xB8\x9D".decode(), # ฝ (Fo Fa, 195) + b"\xE0\xB8\x9E".decode(), # พ (Pho Phan, 196) + b"\xE0\xB8\x9F".decode(), # ฟ (Fo Fan, 197) + b"\xE0\xB8\xA0".decode(), # ภ (Pho Samphao, 198) + b"\xE0\xB8\xA1".decode(), # ม (Mo Ma, 199) + b"\xE0\xB8\xA2".decode(), # ย (Yo Yak, 200) + b"\xE0\xB8\xA3".decode(), # ร (Ro Rua, 201) + b"\xE0\xB8\xA5".decode(), # ล (Lo Ling, 202) + b"\xE0\xB8\xA7".decode(), # ว (Wo Waen, 203) + b"\xE0\xB8\xA8".decode(), # ศ (So Sala, 204) + b"\xE0\xB8\xA9".decode(), # ษ (So Rusi, 205) + b"\xE0\xB8\xAA".decode(), # ส (So Sua, 206) + b"\xE0\xB8\xAB".decode(), # ห (Ho Hip, 207) + b"\xE0\xB8\xAC".decode(), # ฬ (Lo Chula, 208) + b"\xE0\xB8\xAD".decode(), # อ (O Ang, 209) + b"\xE0\xB8\xAE".decode(), # ฮ (Ho Nokhuk, 210) + b"\xE1\x84\x80".decode(), # ㄱ (Giyeok, 211) + b"\xE1\x84\x81".decode(), # ㄴ (Nieun, 212) + b"\xE1\x84\x82".decode(), # ㄷ (Digeut, 213) + b"\xE1\x84\x83".decode(), # ㄹ (Rieul, 214) + b"\xE1\x84\x84".decode(), # ㅁ (Mieum, 215) + b"\xE1\x84\x85".decode(), # ㅂ (Bieup, 216) + b"\xE1\x84\x86".decode(), # ㅅ (Siot, 217) + b"\xE1\x84\x87".decode(), # ㅇ (Ieung, 218) + b"\xE1\x84\x88".decode(), # ㅈ (Jieut, 219) + b"\xE1\x84\x89".decode(), # ㅊ (Chieut, 220) + b"\xE1\x84\x8A".decode(), # ㅋ (Kieuk, 221) + b"\xE1\x84\x8B".decode(), # ㅌ (Tieut, 222) + b"\xE1\x84\x8C".decode(), # ㅍ (Pieup, 223) + b"\xE1\x84\x8D".decode(), # ㅎ (Hieut, 224) + b"\xE1\x85\xA1".decode(), # ㅏ (A, 225) + b"\xE1\x85\xA2".decode(), # ㅐ (Ae, 226) + b"\xE1\x85\xA3".decode(), # ㅑ (Ya, 227) + b"\xE1\x85\xA4".decode(), # ㅒ (Yae, 228) + b"\xE1\x85\xA5".decode(), # ㅓ (Eo, 229) + b"\xE1\x85\xA6".decode(), # ㅔ (E, 230) + b"\xE1\x85\xA7".decode(), # ㅕ (Yeo, 231) + b"\xE1\x85\xA8".decode(), # ㅖ (Ye, 232) + b"\xE1\x85\xA9".decode(), # ㅗ (O, 233) + b"\xE1\x85\xAA".decode(), # ㅘ (Wa, 234) + b"\xE1\x85\xAB".decode(), # ㅙ (Wae, 235) + b"\xE1\x85\xAC".decode(), # ㅚ (Oe, 236) + b"\xE1\x85\xAD".decode(), # ㅛ (Yo, 237) + b"\xE1\x85\xAE".decode(), # ㅜ (U, 238) + b"\xE1\x85\xAF".decode(), # ㅝ (Weo, 239) + b"\xE1\x85\xB0".decode(), # ㅞ (We, 240) + b"\xE1\x85\xB1".decode(), # ㅟ (Wi, 241) + b"\xE1\x85\xB2".decode(), # ㅠ (Yu, 242) + b"\xE1\x85\xB3".decode(), # ㅡ (Eu, 243) + b"\xE1\x85\xB4".decode(), # ㅢ (Ui, 244) + b"\xE1\x85\xB5".decode(), # ㅣ (I, 245) + b"\xE1\x8A\xA0".decode(), # አ (Glottal A, 246) + b"\xE1\x8A\xA1".decode(), # ኡ (Glottal U, 247) + b"\xE1\x8A\xA2".decode(), # ኢ (Glottal I, 248) + b"\xE1\x8A\xA3".decode(), # ኣ (Glottal Aa, 249) + b"\xE1\x8A\xA4".decode(), # ኤ (Glottal E, 250) + b"\xE1\x8A\xA5".decode(), # እ (Glottal Ie, 251) + b"\xE1\x8A\xA6".decode(), # ኦ (Glottal O, 252) + b"\xE1\x8A\xA7".decode(), # ኧ (Glottal Wa, 253) + b"\xE1\x8B\x88".decode(), # ወ (Wa, 254) + b"\xE1\x8B\x89".decode(), # ዉ (Wu, 255) + b"\xE1\x8B\x8A".decode(), # ዊ (Wi, 256) + b"\xE1\x8B\x8B".decode(), # ዋ (Waa, 257) + b"\xE1\x8B\x8C".decode(), # ዌ (We, 258) + b"\xE1\x8B\x8D".decode(), # ው (Wye, 259) + b"\xE1\x8B\x8E".decode(), # ዎ (Wo, 260) + b"\xE1\x8A\xB0".decode(), # ኰ (Ko, 261) + b"\xE1\x8A\xB1".decode(), # ኱ (Ku, 262) + b"\xE1\x8A\xB2".decode(), # ኲ (Ki, 263) + b"\xE1\x8A\xB3".decode(), # ኳ (Kua, 264) + b"\xE1\x8A\xB4".decode(), # ኴ (Ke, 265) + b"\xE1\x8A\xB5".decode(), # ኵ (Kwe, 266) + b"\xE1\x8A\xB6".decode(), # ኶ (Ko, 267) + b"\xE1\x8A\x90".decode(), # ጐ (Go, 268) + b"\xE1\x8A\x91".decode(), # ጑ (Gu, 269) + b"\xE1\x8A\x92".decode(), # ጒ (Gi, 270) + b"\xE1\x8A\x93".decode(), # መ (Gua, 271) + b"\xE1\x8A\x94".decode(), # ጔ (Ge, 272) + b"\xE1\x8A\x95".decode(), # ጕ (Gwe, 273) + b"\xE1\x8A\x96".decode(), # ጖ (Go, 274) + b"\xE0\xA4\x85".decode(), # अ (A, 275) + b"\xE0\xA4\x86".decode(), # आ (Aa, 276) + b"\xE0\xA4\x87".decode(), # इ (I, 277) + b"\xE0\xA4\x88".decode(), # ई (Ii, 278) + b"\xE0\xA4\x89".decode(), # उ (U, 279) + b"\xE0\xA4\x8A".decode(), # ऊ (Uu, 280) + b"\xE0\xA4\x8B".decode(), # ऋ (R, 281) + b"\xE0\xA4\x8F".decode(), # ए (E, 282) + b"\xE0\xA4\x90".decode(), # ऐ (Ai, 283) + b"\xE0\xA4\x93".decode(), # ओ (O, 284) + b"\xE0\xA4\x94".decode(), # औ (Au, 285) + b"\xE0\xA4\x95".decode(), # क (Ka, 286) + b"\xE0\xA4\x96".decode(), # ख (Kha, 287) + b"\xE0\xA4\x97".decode(), # ग (Ga, 288) + b"\xE0\xA4\x98".decode(), # घ (Gha, 289) + b"\xE0\xA4\x99".decode(), # ङ (Nga, 290) + b"\xE0\xA4\x9A".decode(), # च (Cha, 291) + b"\xE0\xA4\x9B".decode(), # छ (Chha, 292) + b"\xE0\xA4\x9C".decode(), # ज (Ja, 293) + b"\xE0\xA4\x9D".decode(), # झ (Jha, 294) + b"\xE0\xA4\x9E".decode(), # ञ (Nya, 295) + b"\xE0\xA4\x9F".decode(), # ट (Ta, 296) + b"\xE0\xA4\xA0".decode(), # ठ (Tha, 297) + b"\xE0\xA4\xA1".decode(), # ड (Da, 298) + b"\xE0\xA4\xA2".decode(), # ढ (Dha, 299) + b"\xE0\xA4\xA3".decode(), # ण (Na, 300) + b"\xE0\xA4\xA4".decode(), # त (Ta, 301) + b"\xE0\xA4\xA5".decode(), # थ (Tha, 302) + b"\xE0\xA4\xA6".decode(), # द (Da, 303) + b"\xE0\xA4\xA7".decode(), # ध (Dha, 304) + b"\xE0\xA4\xA8".decode(), # न (Na, 305) + b"\xE0\xA4\xAA".decode(), # प (Pa, 306) + b"\xE0\xA4\xAB".decode(), # फ (Pha, 307) + b"\xE0\xA4\xAC".decode(), # ब (Ba, 308) + b"\xE0\xA4\xAD".decode(), # भ (Bha, 309) + b"\xE0\xA4\xAE".decode(), # म (Ma, 310) + b"\xE0\xA4\xAF".decode(), # य (Ya, 311) + b"\xE0\xA4\xB0".decode(), # र (Ra, 312) + b"\xE0\xA4\xB2".decode(), # ल (La, 313) + b"\xE0\xA4\xB5".decode(), # व (Va, 314) + b"\xE0\xA4\xB6".decode(), # श (Sha, 315) + b"\xE0\xA4\xB7".decode(), # ष (Ssa, 316) + b"\xE0\xA4\xB8".decode(), # स (Sa, 317) + b"\xE0\xA4\xB9".decode(), # ह (Ha, 318) + b"\xE3\x82\xA2".decode(), # ア (A, 319) + b"\xE3\x82\xA4".decode(), # イ (I, 320) + b"\xE3\x82\xA6".decode(), # ウ (U, 321) + b"\xE3\x82\xA8".decode(), # エ (E, 322) + b"\xE3\x82\xAA".decode(), # オ (O, 323) + b"\xE3\x82\xAB".decode(), # カ (Ka, 324) + b"\xE3\x82\xAD".decode(), # キ (Ki, 325) + b"\xE3\x82\xAF".decode(), # ク (Ku, 326) + b"\xE3\x82\xB1".decode(), # ケ (Ke, 327) + b"\xE3\x82\xB3".decode(), # コ (Ko, 328) + b"\xE3\x82\xB5".decode(), # サ (Sa, 329) + b"\xE3\x82\xB7".decode(), # シ (Shi, 330) + b"\xE3\x82\xB9".decode(), # ス (Su, 331) + b"\xE3\x82\xBB".decode(), # セ (Se, 332) + b"\xE3\x82\xBD".decode(), # ソ (So, 333) + b"\xE3\x82\xBF".decode(), # タ (Ta, 334) + b"\xE3\x83\x81".decode(), # チ (Chi, 335) + b"\xE3\x83\x84".decode(), # ツ (Tsu, 336) + b"\xE3\x83\x86".decode(), # テ (Te, 337) + b"\xE3\x83\x88".decode(), # ト (To, 338) + b"\xE3\x83\x8A".decode(), # ナ (Na, 339) + b"\xE3\x83\x8B".decode(), # ニ (Ni, 340) + b"\xE3\x83\x8C".decode(), # ヌ (Nu, 341) + b"\xE3\x83\x8D".decode(), # ネ (Ne, 342) + b"\xE3\x83\x8E".decode(), # ノ (No, 343) + b"\xE3\x83\x8F".decode(), # ハ (Ha, 344) + b"\xE3\x83\x92".decode(), # ヒ (Hi, 345) + b"\xE3\x83\x95".decode(), # フ (Fu, 346) + b"\xE3\x83\x98".decode(), # ヘ (He, 347) + b"\xE3\x83\x9B".decode(), # ホ (Ho, 348) + b"\xE3\x83\x9E".decode(), # マ (Ma, 349) + b"\xE3\x83\x9F".decode(), # ミ (Mi, 350) + b"\xE3\x83\xA0".decode(), # ム (Mu, 351) + b"\xE3\x83\xA1".decode(), # メ (Me, 352) + b"\xE3\x83\xA2".decode(), # モ (Mo, 353) + b"\xE3\x83\xA4".decode(), # ヤ (Ya, 354) + b"\xE3\x83\xA6".decode(), # ユ (Yu, 355) + b"\xE3\x83\xA8".decode(), # ヨ (Yo, 356) + b"\xE3\x83\xA9".decode(), # ラ (Ra, 357) + b"\xE3\x83\xAA".decode(), # リ (Ri, 358) + b"\xE3\x83\xAB".decode(), # ル (Ru, 359) + b"\xE3\x83\xAC".decode(), # レ (Re, 360) + b"\xE3\x83\xAD".decode(), # ロ (Ro, 361) + b"\xE3\x83\xAF".decode(), # ワ (Wa, 362) + b"\xE3\x83\xB2".decode(), # ヲ (Wo, 363) + b"\xE3\x83\xB3".decode(), # ン (N, 364) + b"\xE2\xB4\xB0".decode(), # ⴰ (Ya, 365) + b"\xE2\xB4\xB1".decode(), # ⴱ (Yab, 366) + b"\xE2\xB4\xB2".decode(), # ⴲ (Yabh, 367) + b"\xE2\xB4\xB3".decode(), # ⴳ (Yag, 368) + b"\xE2\xB4\xB4".decode(), # ⴴ (Yagh, 369) + b"\xE2\xB4\xB5".decode(), # ⴵ (Yaj, 370) + b"\xE2\xB4\xB6".decode(), # ⴶ (Yach, 371) + b"\xE2\xB4\xB7".decode(), # ⴷ (Yad, 372) + b"\xE2\xB4\xB8".decode(), # ⴸ (Yadh, 373) + b"\xE2\xB4\xB9".decode(), # ⴹ (Yadh, emphatic, 374) + b"\xE2\xB4\xBA".decode(), # ⴺ (Yaz, 375) + b"\xE2\xB4\xBB".decode(), # ⴻ (Yazh, 376) + b"\xE2\xB4\xBC".decode(), # ⴼ (Yaf, 377) + b"\xE2\xB4\xBD".decode(), # ⴽ (Yak, 378) + b"\xE2\xB4\xBE".decode(), # ⴾ (Yak, variant, 379) + b"\xE2\xB4\xBF".decode(), # ⴿ (Yaq, 380) + b"\xE2\xB5\x80".decode(), # ⵀ (Yah, 381) + b"\xE2\xB5\x81".decode(), # ⵁ (Yahh, 382) + b"\xE2\xB5\x82".decode(), # ⵂ (Yahl, 383) + b"\xE2\xB5\x83".decode(), # ⵃ (Yahm, 384) + b"\xE2\xB5\x84".decode(), # ⵄ (Yayn, 385) + b"\xE2\xB5\x85".decode(), # ⵅ (Yakh, 386) + b"\xE2\xB5\x86".decode(), # ⵆ (Yakl, 387) + b"\xE2\xB5\x87".decode(), # ⵇ (Yahq, 388) + b"\xE2\xB5\x88".decode(), # ⵈ (Yash, 389) + b"\xE2\xB5\x89".decode(), # ⵉ (Yi, 390) + b"\xE2\xB5\x8A".decode(), # ⵊ (Yij, 391) + b"\xE2\xB5\x8B".decode(), # ⵋ (Yizh, 392) + b"\xE2\xB5\x8C".decode(), # ⵌ (Yink, 393) + b"\xE2\xB5\x8D".decode(), # ⵍ (Yal, 394) + b"\xE2\xB5\x8E".decode(), # ⵎ (Yam, 395) + b"\xE2\xB5\x8F".decode(), # ⵏ (Yan, 396) + b"\xE2\xB5\x90".decode(), # ⵐ (Yang, 397) + b"\xE2\xB5\x91".decode(), # ⵑ (Yany, 398) + b"\xE2\xB5\x92".decode(), # ⵒ (Yap, 399) + b"\xE2\xB5\x93".decode(), # ⵓ (Yu, 400) + b"\xE0\xB6\x85".decode(), # අ (A, 401) + b"\xE0\xB6\x86".decode(), # ආ (Aa, 402) + b"\xE0\xB6\x87".decode(), # ඉ (I, 403) + b"\xE0\xB6\x88".decode(), # ඊ (Ii, 404) + b"\xE0\xB6\x89".decode(), # උ (U, 405) + b"\xE0\xB6\x8A".decode(), # ඌ (Uu, 406) + b"\xE0\xB6\x8B".decode(), # ඍ (R, 407) + b"\xE0\xB6\x8C".decode(), # ඎ (Rr, 408) + b"\xE0\xB6\x8F".decode(), # ඏ (L, 409) + b"\xE0\xB6\x90".decode(), # ඐ (Ll, 410) + b"\xE0\xB6\x91".decode(), # එ (E, 411) + b"\xE0\xB6\x92".decode(), # ඒ (Ee, 412) + b"\xE0\xB6\x93".decode(), # ඓ (Ai, 413) + b"\xE0\xB6\x94".decode(), # ඔ (O, 414) + b"\xE0\xB6\x95".decode(), # ඕ (Oo, 415) + b"\xE0\xB6\x96".decode(), # ඖ (Au, 416) + b"\xE0\xB6\x9A".decode(), # ක (Ka, 417) + b"\xE0\xB6\x9B".decode(), # ඛ (Kha, 418) + b"\xE0\xB6\x9C".decode(), # ග (Ga, 419) + b"\xE0\xB6\x9D".decode(), # ඝ (Gha, 420) + b"\xE0\xB6\x9E".decode(), # ඞ (Nga, 421) + b"\xE0\xB6\x9F".decode(), # ච (Cha, 422) + b"\xE0\xB6\xA0".decode(), # ඡ (Chha, 423) + b"\xE0\xB6\xA1".decode(), # ජ (Ja, 424) + b"\xE0\xB6\xA2".decode(), # ඣ (Jha, 425) + b"\xE0\xB6\xA3".decode(), # ඤ (Nya, 426) + b"\xE0\xB6\xA4".decode(), # ට (Ta, 427) + b"\xE0\xB6\xA5".decode(), # ඥ (Tha, 428) + b"\xE0\xB6\xA6".decode(), # ඦ (Da, 429) + b"\xE0\xB6\xA7".decode(), # ට (Dha, 430) + b"\xE0\xB6\xA8".decode(), # ඨ (Na, 431) + b"\xE0\xB6\xAA".decode(), # ඪ (Pa, 432) + b"\xE0\xB6\xAB".decode(), # ණ (Pha, 433) + b"\xE0\xB6\xAC".decode(), # ඬ (Ba, 434) + b"\xE0\xB6\xAD".decode(), # ත (Bha, 435) + b"\xE0\xB6\xAE".decode(), # ථ (Ma, 436) + b"\xE0\xB6\xAF".decode(), # ද (Ya, 437) + b"\xE0\xB6\xB0".decode(), # ධ (Ra, 438) + ] NETWORK_EXPLORER_MAP = { From 946b3ca0d25e019194d63964eaaa6a7d3addfe8f Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 8 Apr 2025 14:10:53 -0700 Subject: [PATCH 87/96] update setuptools --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4a406900..4dfa5a99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools~=70.0.0", "wheel"] +requires = ["setuptools>=70.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] From 5231b5e37df340d58c7f49ed93addb6553f53bc1 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 8 Apr 2025 14:45:52 -0700 Subject: [PATCH 88/96] Updates tests --- tests/e2e_tests/test_staking_sudo.py | 4 ++-- tests/e2e_tests/utils.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 2dfd1102..b34b05b1 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -298,7 +298,7 @@ def test_staking(local_chain, wallet_setup): # Parse all hyperparameters and single out max_burn in TAO all_hyperparams = hyperparams.stdout.splitlines() - max_burn_tao = all_hyperparams[22].split()[3] + max_burn_tao = all_hyperparams[22].split()[2].strip('\u200e') # Assert max_burn is 100 TAO from default assert Balance.from_tao(float(max_burn_tao)) == Balance.from_tao(100.0) @@ -359,7 +359,7 @@ def test_staking(local_chain, wallet_setup): # Parse updated hyperparameters all_updated_hyperparams = updated_hyperparams.stdout.splitlines() - updated_max_burn_tao = all_updated_hyperparams[22].split()[3] + updated_max_burn_tao = all_updated_hyperparams[22].split()[2].strip('\u200e') # Assert max_burn is now 10 TAO assert Balance.from_tao(float(updated_max_burn_tao)) == Balance.from_tao(10) diff --git a/tests/e2e_tests/utils.py b/tests/e2e_tests/utils.py index 9379b962..490ae594 100644 --- a/tests/e2e_tests/utils.py +++ b/tests/e2e_tests/utils.py @@ -57,7 +57,7 @@ def exec_command( return keypair, wallet, wallet_path, exec_command -def extract_coldkey_balance(text: str, wallet_name: str, coldkey_address: str) -> dict: +def extract_coldkey_balance(cleaned_text: str, wallet_name: str, coldkey_address: str) -> dict: """ Extracts the free, staked, and total balances for a given wallet name and coldkey address from the input string. @@ -72,11 +72,12 @@ def extract_coldkey_balance(text: str, wallet_name: str, coldkey_address: str) - each containing the corresponding balance as a Balance object. Returns a dictionary with all zeros if the wallet name or coldkey address is not found. """ + cleaned_text = cleaned_text.replace('\u200e', '') pattern = ( - rf"{wallet_name}\s+{coldkey_address}\s+" r"τ\s*([\d,]+\.\d+)" # Free Balance + rf"{wallet_name}\s+{coldkey_address}\s+([\d,]+\.\d+)\s*τ" # Free Balance ) - match = re.search(pattern, text) + match = re.search(pattern, cleaned_text) if not match: return { From b52c0024cdcb27ac47726471a343213ba44bf08e Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Apr 2025 15:38:52 +0200 Subject: [PATCH 89/96] Updates language for confirmation about revoking. --- bittensor_cli/src/commands/stake/children_hotkeys.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 12f52658..8cb38cce 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -86,7 +86,7 @@ async def set_children_extrinsic( if prompt: if all_revoked: if not Confirm.ask( - f"Do you want to revoke all children hotkeys for hotkey {hotkey}?" + f"Do you want to revoke all children hotkeys for hotkey {hotkey} on netuid {netuid}?" ): return False, "Operation Cancelled" else: @@ -136,17 +136,9 @@ async def set_children_extrinsic( console.print(":white_heavy_check_mark: [green]Included[/green]") if wait_for_finalization: console.print(":white_heavy_check_mark: [green]Finalized[/green]") - # bittensor.logging.success( - # prefix=operation, - # suffix="Finalized: " + str(success), - # ) return True, f"Successfully {operation.lower()} and Finalized." else: err_console.print(f":cross_mark: [red]Failed[/red]: {error_message}") - # bittensor.logging.warning( - # prefix=operation, - # suffix="Failed: " + str(error_message), - # ) return False, error_message @@ -613,7 +605,7 @@ async def revoke_children( Revokes the children hotkeys associated with a given network identifier (netuid). """ dict_output = {} - if netuid: + if netuid is not None: success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, From 3af47fc94c7126440a15c08d6111e491f397e493 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Apr 2025 16:20:16 +0200 Subject: [PATCH 90/96] Fix calculation for childkey set completion block --- bittensor_cli/src/commands/stake/children_hotkeys.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 12f52658..9218d19a 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -30,21 +30,19 @@ async def get_childkey_completion_block( """ Calculates the block at which the childkey set request will complete """ + bh = await subtensor.substrate.get_chain_head() blocks_since_last_step_query = subtensor.query( - "SubtensorModule", - "BlocksSinceLastStep", - params=[netuid], + "SubtensorModule", "BlocksSinceLastStep", params=[netuid], block_hash=bh ) tempo_query = subtensor.get_hyperparameter( - param_name="Tempo", - netuid=netuid, + param_name="Tempo", netuid=netuid, block_hash=bh ) block_number, blocks_since_last_step, tempo = await asyncio.gather( - subtensor.substrate.get_block_number(), + subtensor.substrate.get_block_number(block_hash=bh), blocks_since_last_step_query, tempo_query, ) - cooldown = block_number + 1 + cooldown = block_number + 7200 blocks_left_in_tempo = tempo - blocks_since_last_step next_tempo = block_number + blocks_left_in_tempo next_epoch_after_cooldown = (cooldown - next_tempo) % tempo + cooldown From 035bf47d4a85edf977a2be1fc5e13aec86967341 Mon Sep 17 00:00:00 2001 From: BD Himes <37844818+thewhaleking@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:26:59 +0200 Subject: [PATCH 91/96] Update bittensor_cli/src/commands/stake/children_hotkeys.py Co-authored-by: Cameron Fairchild --- bittensor_cli/src/commands/stake/children_hotkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 9218d19a..7ad94f13 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -45,7 +45,7 @@ async def get_childkey_completion_block( cooldown = block_number + 7200 blocks_left_in_tempo = tempo - blocks_since_last_step next_tempo = block_number + blocks_left_in_tempo - next_epoch_after_cooldown = (cooldown - next_tempo) % tempo + cooldown + next_epoch_after_cooldown = (cooldown - next_tempo) % (tempo + 1) + cooldown return block_number, next_epoch_after_cooldown From b2e208bb7b2ccf97d27a1754607f1901e82ba92d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Apr 2025 17:46:52 +0200 Subject: [PATCH 92/96] Adds test --- tests/e2e_tests/test_children_hotkeys.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/e2e_tests/test_children_hotkeys.py diff --git a/tests/e2e_tests/test_children_hotkeys.py b/tests/e2e_tests/test_children_hotkeys.py new file mode 100644 index 00000000..f286012f --- /dev/null +++ b/tests/e2e_tests/test_children_hotkeys.py @@ -0,0 +1,15 @@ +import pytest + +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.commands.stake.children_hotkeys import ( + get_childkey_completion_block, +) + + +@pytest.mark.asyncio +async def test_get_childkey_completion_block(local_chain): + async with SubtensorInterface("ws://127.0.0.1:9945") as subtensor: + current_block, completion_block = await get_childkey_completion_block( + subtensor, 1 + ) + assert (completion_block - current_block) >= 7200 From 34b71ec169a289eebbeb007ebb20e470d6171863 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Apr 2025 18:58:28 +0200 Subject: [PATCH 93/96] Improve language for childkey revoke --- .../src/commands/stake/children_hotkeys.py | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 7ad94f13..68be315c 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -622,14 +622,23 @@ async def revoke_children( wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, ) - dict_output[netuid] = {"success": success, "error": message} + dict_output[netuid] = { + "success": success, + "error": message, + "set_block": None, + "completion_block": None, + } # Result if success: - if wait_for_finalization and wait_for_inclusion: - await get_children(wallet, subtensor, netuid) + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid + ) + dict_output[netuid]["completion_block"] = completion_block + dict_output[netuid]["set_block"] = current_block console.print( - ":white_heavy_check_mark: [green]Revoked children hotkeys.[/green]" + f":white_heavy_check_mark: Your childkey revocation request for netuid {netuid} has been submitted. " + f"It will be completed around block {completion_block}. The current block is {current_block}" ) else: console.print( @@ -638,10 +647,10 @@ async def revoke_children( else: # revoke children from ALL netuids netuids = await subtensor.get_all_subnet_netuids() - for netuid in netuids: - if netuid == 0: # dont include root network + for netuid_ in netuids: + if netuid_ == 0: # dont include root network continue - console.print(f"Revoking children from netuid {netuid}.") + console.print(f"Revoking children from netuid {netuid_}.") success, message = await set_children_extrinsic( subtensor=subtensor, wallet=wallet, @@ -652,10 +661,23 @@ 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]" - ) + dict_output[netuid_] = { + "success": success, + "error": message, + "set_block": None, + "completion_block": None, + } + if success: + current_block, completion_block = await get_childkey_completion_block( + subtensor, netuid_ + ) + dict_output[netuid_]["completion_block"] = completion_block + dict_output[netuid_]["set_block"] = current_block + console.print( + f":white_heavy_check_mark: Your childkey revocation request for netuid {netuid_} has been " + f"submitted. It will be completed around block {completion_block}. The current block " + f"is {current_block}" + ) if json_output: json_console.print(json.dumps(dict_output)) From af48417aa08582615b44cb6eb448e725b305d0a4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 9 Apr 2025 18:59:33 +0200 Subject: [PATCH 94/96] Added sad path --- bittensor_cli/src/commands/stake/children_hotkeys.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py index 68be315c..37d1273e 100644 --- a/bittensor_cli/src/commands/stake/children_hotkeys.py +++ b/bittensor_cli/src/commands/stake/children_hotkeys.py @@ -678,6 +678,10 @@ async def revoke_children( f"submitted. It will be completed around block {completion_block}. The current block " f"is {current_block}" ) + else: + err_console.print( + f"Childkey revocation failed for netuid {netuid_}: {message}." + ) if json_output: json_console.print(json.dumps(dict_output)) From 12a952589f3ad9a8ca4b6a5efbde75d3c5fd18ed Mon Sep 17 00:00:00 2001 From: BD Himes <37844818+thewhaleking@users.noreply.github.com> Date: Wed, 9 Apr 2025 19:25:14 +0200 Subject: [PATCH 95/96] Revert "new color palette" --- bittensor_cli/src/__init__.py | 69 +++++++++++++++++------------------ 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 6ffebe86..e8dcdc53 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -721,26 +721,23 @@ def __init__(self): self.SU = self.SUDO class General(Gettable): - HEADER = "#57878B" # Mid Blue Green - LINKS = "#8CAAAE" # Pale Blue - HINT = "#6C8871" # Muted Green - COLDKEY = "#676B72" # Dark Grey Blue - HOTKEY = "#747065" # Dark Orange Grey - SUBHEADING_MAIN = "#8AA499" # Muted Blue Green - SUBHEADING = "#9BAC9C" # Pale Green - SUBHEADING_EXTRA_1 = "#C596A3" # Dusty Rose - SUBHEADING_EXTRA_2 = "#9BAC9C" # Pale Green - CONFIRMATION_Y_N_Q = "#EFB7AB" # Pale Pink - SYMBOL = "#FE917A" # Salmon Orange - SUBNET_NAME = "#C596A3" # Dusty Rose - VALIDATOR_NAME = "#9BAC9C" # Mid Lime Green - MINER_NAME = "#A17E7E" # Dusty Red - BALANCE = "#757B7B" # Muted teal Blue - COST = "#8EB27A" # Green - SUCCESS = "#3D7F71" # Dark Teal - NETUID = "#BDC1C6" # GREY_400 - NETUID_EXTRA = "#D4D0C1" # Light Yellow Grey - TEMPO = "#927A71" # Dark Tan Brown + HEADER = "#4196D6" # Light Blue + LINKS = "#8CB9E9" # Sky Blue + HINT = "#A2E5B8" # Mint Green + COLDKEY = "#9EF5E4" # Aqua + HOTKEY = "#ECC39D" # Light Orange/Peach + SUBHEADING_MAIN = "#7ECFEC" # Light Cyan + SUBHEADING = "#AFEFFF" # Pale Blue + SUBHEADING_EXTRA_1 = "#96A3C5" # Grayish Blue + SUBHEADING_EXTRA_2 = "#6D7BAF" # Slate Blue + CONFIRMATION_Y_N_Q = "#EE8DF8" # Light Purple/Pink + SYMBOL = "#E7CC51" # Gold + BALANCE = "#4F91C6" # Medium Blue + COST = "#53B5A0" # Teal + SUCCESS = "#53B5A0" # Teal + NETUID = "#CBA880" # Tan + NETUID_EXTRA = "#DDD5A9" # Light Khaki + TEMPO = "#67A3A5" # Grayish Teal # aliases CK = COLDKEY HK = HOTKEY @@ -755,25 +752,25 @@ class Stake(Gettable): STAKE_AMOUNT = "#53B5A0" # Teal STAKE_ALPHA = "#53B5A0" # Teal STAKE_SWAP = "#67A3A5" # Grayish Teal - TAO = "#8AA499" # Faded Blue Green - SLIPPAGE_TEXT = "#BA938A" # Brown Salmon - SLIPPAGE_PERCENT = "#CD8B7B" # Muted Salmon - NOT_REGISTERED = "#A87D7D" # Medium Red brown - EXTRA_1 = "#A45E44" # Deep Autumn Orange + TAO = "#4F91C6" # Medium Blue + SLIPPAGE_TEXT = "#C25E7C" # Rose + SLIPPAGE_PERCENT = "#E7B195" # Light Coral + NOT_REGISTERED = "#EB6A6C" # Salmon Red + EXTRA_1 = "#D781BB" # Pink # aliases AMOUNT = STAKE_AMOUNT ALPHA = STAKE_ALPHA SWAP = STAKE_SWAP class Pools(Gettable): - TAO = "#8AA499" # Faded Blue Green - ALPHA_IN = "#C1913C" # Mustard - ALPHA_OUT = "#B49766" # Khaki Mustard - RATE = "#A46844" # Deep Orange - TAO_EQUIV = "#93BBAF" # Teal Blue - EMISSION = "#B58065" # Med Orange - EXTRA_1 = "#919170" # Autumn green - EXTRA_2 = "#667862" # Forest green + TAO = "#4F91C6" # Medium Blue + ALPHA_IN = "#D09FE9" # Light Purple + ALPHA_OUT = "#AB7CC8" # Medium Purple + RATE = "#F8D384" # Light Orange + TAO_EQUIV = "#8CB9E9" # Sky Blue + EMISSION = "#F8D384" # Light Orange + EXTRA_1 = "#CAA8FB" # Lavender + EXTRA_2 = "#806DAF" # Dark Purple class Grey(Gettable): GREY_100 = "#F8F9FA" # Almost White @@ -798,9 +795,9 @@ class Grey(Gettable): G_900 = GREY_900 class Sudo(Gettable): - HYPERPARAMETER = "#93AFA3" # Forest - VALUE = "#B58065" # Burnt Orange - NORMALIZED = "#A87575" # Burnt Red + HYPERPARAMETER = "#4F91C6" # Medium Blue + VALUE = "#D09FE9" # Light Purple + NORMALIZED = "#AB7CC8" # Medium Purple # aliases HYPERPARAM = HYPERPARAMETER NORMAL = NORMALIZED From e7db3e47b595eee0eec1d4b330a4fba8fe7891b8 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 9 Apr 2025 11:11:16 -0700 Subject: [PATCH 96/96] Bumps version and changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75397415..366c0106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 9.2.1 /2025-04-09 + +## What's Changed +* Fix e2e test by @basfroman in https://github.com/opentensor/btcli/pull/396 +* Btwallet e2e test - verbose printing by @ibraheem-abe in https://github.com/opentensor/btcli/pull/397 +* Feat/swap coldkey by @ibraheem-abe in https://github.com/opentensor/btcli/pull/399 +* Add logic for keep docker image up to date by @basfroman in https://github.com/opentensor/btcli/pull/400 +* Feat/associate hotkey by @ibraheem-abe in https://github.com/opentensor/btcli/pull/401 +* Fixes staking/unstaking e2e tests by @ibraheem-abe in https://github.com/opentensor/btcli/pull/404 +* Adds `era` param for stake transactions by @thewhaleking in https://github.com/opentensor/btcli/pull/406 +* Fix: Removes name conflict in Sn create by @ibraheem-abe in https://github.com/opentensor/btcli/pull/405 +* Pull version.py version from package metadata by @thewhaleking in https://github.com/opentensor/btcli/pull/409 +* json output for commands by @thewhaleking in https://github.com/opentensor/btcli/pull/369 +* General code cleanup by @thewhaleking in https://github.com/opentensor/btcli/pull/411 +* More json outputs by @thewhaleking in https://github.com/opentensor/btcli/pull/412 +* new color palette by @thewhaleking in https://github.com/opentensor/btcli/pull/413 +* bump versions by @thewhaleking in https://github.com/opentensor/btcli/pull/410 +* spelling fix "Received" by @dougsillars in https://github.com/opentensor/btcli/pull/414 +* Updates Subnet symbols by @ibraheem-abe in https://github.com/opentensor/btcli/pull/416 +* Fix calculation for childkey set by @thewhaleking in https://github.com/opentensor/btcli/pull/418 +* Revoke children msg by @thewhaleking in https://github.com/opentensor/btcli/pull/419 +* Update revoke children language by @thewhaleking in https://github.com/opentensor/btcli/pull/417 +* Revert "new color palette" by @thewhaleking in https://github.com/opentensor/btcli/pull/420 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.2.0...v9.2.1 + ## 9.2.0 /2025-03-18 ## What's Changed diff --git a/pyproject.toml b/pyproject.toml index 4dfa5a99..8237bf87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.2.0" +version = "9.2.1" description = "Bittensor CLI" readme = "README.md" authors = [