From 0100d8dd67e467e1bf5ad37a2059a1a59392721d Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Fri, 8 Aug 2025 14:56:40 +0200 Subject: [PATCH 01/57] WIP checkin --- bittensor_cli/cli.py | 18 ++++++++++++++---- bittensor_cli/src/__init__.py | 1 + .../src/bittensor/subtensor_interface.py | 16 +++++++--------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c26defd6..a85ae9d2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -618,6 +618,7 @@ def __init__(self): "wallet_hotkey": None, "network": None, "use_cache": True, + "disk_cache": False, "rate_tolerance": None, "safe_staking": True, "allow_partial_stake": False, @@ -1087,6 +1088,7 @@ def initialize_chain( "Verify this is intended.", ) if not self.subtensor: + use_disk_cache = self.config.get("disk_cache", False) if network: network_ = None for item in network: @@ -1103,15 +1105,15 @@ def initialize_chain( f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" ) - self.subtensor = SubtensorInterface(network_) + self.subtensor = SubtensorInterface(network_, use_disk_cache=use_disk_cache) elif self.config["network"]: console.print( f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" f"[/{COLORS.G.LINKS}] from config" ) - self.subtensor = SubtensorInterface(self.config["network"]) + self.subtensor = SubtensorInterface(self.config["network"], use_disk_cache=use_disk_cache) else: - self.subtensor = SubtensorInterface(defaults.subtensor.network) + self.subtensor = SubtensorInterface(defaults.subtensor.network, use_disk_cache=use_disk_cache) return self.subtensor def _run_command(self, cmd: Coroutine, exit_early: bool = True): @@ -1268,6 +1270,13 @@ def set_config( help="Disable caching of some commands. This will disable the `--reuse-last` and `--html` flags on " "commands such as `subnets metagraph`, `stake show` and `subnets list`.", ), + disk_cache: Optional[bool] = typer.Option( + None, + "--disk-cache/--no-disk-cache", + " /--no-disk-cache", + help="Enables or disables the caching on disk. Enabling this can significantly speed up commands run " + "sequentially" + ), rate_tolerance: Optional[float] = typer.Option( None, "--tolerance", @@ -1314,12 +1323,13 @@ def set_config( "wallet_hotkey": wallet_hotkey, "network": network, "use_cache": use_cache, + "disk_cache": disk_cache, "rate_tolerance": rate_tolerance, "safe_staking": safe_staking, "allow_partial_stake": allow_partial_stake, "dashboard_path": dashboard_path, } - bools = ["use_cache", "safe_staking", "allow_partial_stake"] + bools = ["use_cache", "disk_cache", "safe_staking", "allow_partial_stake"] if all(v is None for v in args.values()): # Print existing configs self.get_config() diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 7cd09ab8..afabc424 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -94,6 +94,7 @@ class config: "wallet_name": None, "wallet_hotkey": None, "use_cache": True, + "disk_cache": False, "metagraph_cols": { "UID": True, "GLOBAL_STAKE": True, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 0684b31e..ffe8d78e 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -42,12 +42,6 @@ get_hotkey_pub_ss58, ) -SubstrateClass = ( - DiskCachedAsyncSubstrateInterface - if os.getenv("DISK_CACHE", "0") == "1" - else AsyncSubstrateInterface -) - class ParamWithTypes(TypedDict): name: str # Name of the parameter. @@ -81,7 +75,7 @@ class SubtensorInterface: Thin layer for interacting with Substrate Interface. Mostly a collection of frequently-used calls. """ - def __init__(self, network): + def __init__(self, network, use_disk_cache: bool = False): if network in Constants.network_map: self.chain_endpoint = Constants.network_map[network] self.network = network @@ -111,8 +105,12 @@ def __init__(self, network): ) self.chain_endpoint = Constants.network_map[defaults.subtensor.network] self.network = defaults.subtensor.network - - self.substrate = SubstrateClass( + substrate_class = ( + DiskCachedAsyncSubstrateInterface + if (use_disk_cache or os.getenv("DISK_CACHE", "0") == "1") + else AsyncSubstrateInterface + ) + self.substrate = substrate_class( url=self.chain_endpoint, ss58_format=SS58_FORMAT, type_registry=TYPE_REGISTRY, From 5f5225819f489542bda6c5836935daeb32981e39 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 12 Aug 2025 22:38:20 +0200 Subject: [PATCH 02/57] Optimise btcli s list for disk caching --- bittensor_cli/src/commands/subnets/price.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 38a20d00..8e488a95 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -52,7 +52,17 @@ async def price( step = 300 start_block = max(0, current_block - total_blocks) - block_numbers = list(range(start_block, current_block + 1, step)) + + # snap start block down to nearest multiple of 10 + start_block -= start_block % 10 + + block_numbers = [] + for b in range(start_block, current_block + 1, step): + if b == current_block: + block_numbers.append(b) # exact current block + else: + block_numbers.append(b - (b % 5)) # snap down to multiple of 10 + block_numbers = sorted(set(block_numbers)) # Block hashes block_hash_cors = [ From 4f77804d675e6011757f700033e3a7c0f2886771 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 12 Aug 2025 22:38:28 +0200 Subject: [PATCH 03/57] Ruff --- bittensor_cli/cli.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index eeba4d61..6604c08b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1110,15 +1110,21 @@ def initialize_chain( f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" ) - self.subtensor = SubtensorInterface(network_, use_disk_cache=use_disk_cache) + self.subtensor = SubtensorInterface( + network_, use_disk_cache=use_disk_cache + ) elif self.config["network"]: console.print( f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" f"[/{COLORS.G.LINKS}] from config" ) - self.subtensor = SubtensorInterface(self.config["network"], use_disk_cache=use_disk_cache) + self.subtensor = SubtensorInterface( + self.config["network"], use_disk_cache=use_disk_cache + ) else: - self.subtensor = SubtensorInterface(defaults.subtensor.network, use_disk_cache=use_disk_cache) + self.subtensor = SubtensorInterface( + defaults.subtensor.network, use_disk_cache=use_disk_cache + ) return self.subtensor def _run_command(self, cmd: Coroutine, exit_early: bool = True): @@ -1280,7 +1286,7 @@ def set_config( "--disk-cache/--no-disk-cache", " /--no-disk-cache", help="Enables or disables the caching on disk. Enabling this can significantly speed up commands run " - "sequentially" + "sequentially", ), rate_tolerance: Optional[float] = typer.Option( None, From fc69e802e1b417ac468b52a01bd45811cb87f37b Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Wed, 13 Aug 2025 15:50:17 +0200 Subject: [PATCH 04/57] Better arg naming + type annotations --- bittensor_cli/src/commands/sudo.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py index 6d4ae13e..e6ac3118 100644 --- a/bittensor_cli/src/commands/sudo.py +++ b/bittensor_cli/src/commands/sudo.py @@ -34,6 +34,7 @@ SubtensorInterface, ProposalVoteData, ) + from scalecodec.types import GenericMetadataVersioned # helpers and extrinsics @@ -91,8 +92,8 @@ def search_metadata( param_name: str, value: Union[str, bool, float, list[float]], netuid: int, - metadata, - pallet: str = DEFAULT_PALLET, + metadata: "GenericMetadataVersioned", + pallet_name: str = DEFAULT_PALLET, ) -> tuple[bool, Optional[dict]]: """ Searches the substrate metadata AdminUtils pallet for a given parameter name. Crafts a response dict to be used @@ -103,7 +104,7 @@ def search_metadata( value: the value to set the hyperparameter netuid: the specified netuid metadata: the subtensor.substrate.metadata - pallet: the name of the module to use for the query. If not set, the default value is DEFAULT_PALLET + pallet_name: the name of the module to use for the query. If not set, the default value is DEFAULT_PALLET Returns: (success, dict of call params) @@ -125,7 +126,7 @@ def type_converter_with_retry(type_, val, arg_name): call_crafter = {"netuid": netuid} - pallet = metadata.get_metadata_pallet(pallet) + pallet = metadata.get_metadata_pallet(pallet_name) for call in pallet.calls: if call.name == param_name: if "netuid" not in [x.name for x in call.args]: From 74aa5985919a23191201d9e44f986b022dfba83a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 21:47:40 +0200 Subject: [PATCH 05/57] Ensure hotkeys are into lists before any processing --- bittensor_cli/cli.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6604c08b..766086f6 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3827,7 +3827,23 @@ def stake_remove( if amount and amount <= 0: print_error(f"You entered an incorrect unstake amount: {amount}") - raise typer.Exit() + return False + + if include_hotkeys: + include_hotkeys = parse_to_list( + include_hotkeys, + str, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--include-hotkeys hk1,hk2`.", + is_ss58=False, + ) + + if exclude_hotkeys: + exclude_hotkeys = parse_to_list( + exclude_hotkeys, + str, + "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", + is_ss58=False, + ) if ( not wallet_hotkey From 97b2b7591c71b2f5baf20752c893c331eeffdb9c Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 21:48:01 +0200 Subject: [PATCH 06/57] Correct type annotations --- bittensor_cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 766086f6..2d09335a 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -12,7 +12,7 @@ import warnings from dataclasses import fields from pathlib import Path -from typing import Coroutine, Optional, Union +from typing import Coroutine, Optional, Union, Literal import numpy as np import rich @@ -1671,7 +1671,7 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: Optional[list[str]] = None, + ask_for: Optional[list[Union[str, Literal]]] = None, validate: WV = WV.WALLET, return_wallet_and_hotkey: bool = False, ) -> Union[Wallet, tuple[Wallet, str]]: From bfe20482ae4c2ccc1be7b00d9fb75405d8ce5316 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 21:48:51 +0200 Subject: [PATCH 07/57] Raises to returns --- bittensor_cli/cli.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2d09335a..27762ed0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3798,11 +3798,11 @@ def stake_remove( "Interactive mode cannot be used with hotkey selection options like " "--include-hotkeys, --exclude-hotkeys, --all-hotkeys, or --hotkey." ) - raise typer.Exit() + return False if unstake_all and unstake_all_alpha: print_error("Cannot specify both unstake-all and unstake-all-alpha.") - raise typer.Exit() + return False if not interactive and not unstake_all and not unstake_all_alpha: netuid = get_optional_netuid(netuid, all_netuids) @@ -3811,19 +3811,19 @@ def stake_remove( "You have specified hotkeys to include and also the `--all-hotkeys` flag. The flag" " should only be used standalone (to use all hotkeys) or with `--exclude-hotkeys`." ) - raise typer.Exit() + return False if include_hotkeys and exclude_hotkeys: print_error( "You have specified both including and excluding hotkeys options. Select one or the other." ) - raise typer.Exit() + return False if unstake_all and amount: print_error( "Cannot specify both a specific amount and 'unstake-all'. Choose one or the other." ) - raise typer.Exit() + return False if amount and amount <= 0: print_error(f"You entered an incorrect unstake amount: {amount}") @@ -3891,12 +3891,12 @@ def stake_remove( if include_hotkeys: if len(include_hotkeys) > 1: print_error("Cannot unstake_all from multiple hotkeys at once.") - raise typer.Exit() + return False elif is_valid_ss58_address(include_hotkeys[0]): hotkey_ss58_address = include_hotkeys[0] else: print_error("Invalid hotkey ss58 address.") - raise typer.Exit() + return False elif all_hotkeys: wallet = self.wallet_ask( wallet_name, @@ -3981,6 +3981,7 @@ def stake_remove( "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--include-hotkeys hk1,hk2`.", is_ss58=False, ) + return False if exclude_hotkeys: exclude_hotkeys = parse_to_list( @@ -3989,6 +3990,7 @@ def stake_remove( "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", is_ss58=False, ) + return False return self._run_command( remove_stake.unstake( From a698c01db1d8adc85a6e5e4a65cba186ab6e2066 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 21:49:06 +0200 Subject: [PATCH 08/57] Formatting line length --- bittensor_cli/cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 27762ed0..c555161e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3860,7 +3860,8 @@ def stake_remove( default=self.config.get("wallet_name") or defaults.wallet.name, ) hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim](or Press Enter to view existing staked hotkeys)[/dim]", + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake from [dim]" + "(or Press Enter to view existing staked hotkeys)[/dim]", ) if hotkey_or_ss58 == "": wallet = self.wallet_ask( @@ -3907,7 +3908,8 @@ def stake_remove( else: if not hotkey_ss58_address and not wallet_hotkey: hotkey_or_ss58 = Prompt.ask( - "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from [dim](or enter 'all' to unstake from all hotkeys)[/dim]", + "Enter the [blue]hotkey[/blue] name or [blue]ss58 address[/blue] to unstake all from [dim]" + "(or enter 'all' to unstake from all hotkeys)[/dim]", default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey, ) From 3921e84d0d0146de2ee1703f8672d74a12700830 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 21:49:37 +0200 Subject: [PATCH 09/57] Error if using ambiguous args --- bittensor_cli/cli.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c555161e..f9faa2a0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3975,22 +3975,21 @@ def stake_remove( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - - if include_hotkeys: - include_hotkeys = parse_to_list( - include_hotkeys, - str, - "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--include-hotkeys hk1,hk2`.", - is_ss58=False, + if not amount and not prompt: + print_error( + f"Too ambiguous to use [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}]. You need to specify an amount," + f"or use " + f"[{COLORS.G.ARG}]--unstake-all[/{COLORS.G.ARG}]/[{COLORS.G.ARG}]--unstake-all-alpha[/{COLORS.G.ARG}]." ) return False - if exclude_hotkeys: - exclude_hotkeys = parse_to_list( - exclude_hotkeys, - str, - "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", - is_ss58=False, + if not amount and json_output: + json_console.print_json( + data={ + "success": False, + "err_msg": "Too amibuous to use '--no-prompt' without specifying and amount or " + "'--unstake-all'/'--unstake-all-alpha'", + } ) return False From 4eb75af91c27d746ab2ed38a0b530d882685baf4 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 21:54:59 +0200 Subject: [PATCH 10/57] Fix typing --- bittensor_cli/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index f9faa2a0..8f6a204c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1671,7 +1671,9 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: Optional[list[Union[str, Literal]]] = None, + ask_for: Optional[ + list[Union[str, Literal[WO.NAME, WO.PATH, WO.HOTKEY]]] + ] = None, validate: WV = WV.WALLET, return_wallet_and_hotkey: bool = False, ) -> Union[Wallet, tuple[Wallet, str]]: From ddd01c0f21ab94544e6be93d5d4cdba5e6e53b0d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 21:55:37 +0200 Subject: [PATCH 11/57] Fix typing --- bittensor_cli/cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8f6a204c..69784d02 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1671,9 +1671,7 @@ def wallet_ask( wallet_name: Optional[str], wallet_path: Optional[str], wallet_hotkey: Optional[str], - ask_for: Optional[ - list[Union[str, Literal[WO.NAME, WO.PATH, WO.HOTKEY]]] - ] = None, + ask_for: Optional[list[Literal[WO.NAME, WO.PATH, WO.HOTKEY]]] = None, validate: WV = WV.WALLET, return_wallet_and_hotkey: bool = False, ) -> Union[Wallet, tuple[Wallet, str]]: From 1a5f10f1a5bab519a2cd06fdf57beb3ab552238d Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Wed, 13 Aug 2025 16:27:44 -0400 Subject: [PATCH 12/57] fix typo and wording --- 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 69784d02..7665cd35 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3987,7 +3987,7 @@ def stake_remove( json_console.print_json( data={ "success": False, - "err_msg": "Too amibuous to use '--no-prompt' without specifying and amount or " + "err_msg": "Just using '--no-prompt' is too ambiguous without specifying an amount or " "'--unstake-all'/'--unstake-all-alpha'", } ) From 563a75037a83a6be41207ffbb5ebd2b4042a0774 Mon Sep 17 00:00:00 2001 From: BD Himes <37844818+thewhaleking@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:28:23 +0200 Subject: [PATCH 13/57] Update bittensor_cli/cli.py Co-authored-by: Cameron Fairchild --- 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 7665cd35..6072c378 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3977,7 +3977,7 @@ def stake_remove( ) if not amount and not prompt: print_error( - f"Too ambiguous to use [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}]. You need to specify an amount," + f"Just using [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}] is too ambiguous. You need to specify an amount," f"or use " f"[{COLORS.G.ARG}]--unstake-all[/{COLORS.G.ARG}]/[{COLORS.G.ARG}]--unstake-all-alpha[/{COLORS.G.ARG}]." ) From 7da9dd5fe1b5a2ec2f518d921965a50c79ce189a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 22:36:44 +0200 Subject: [PATCH 14/57] Update wording --- bittensor_cli/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6072c378..458cf1d4 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3977,8 +3977,8 @@ def stake_remove( ) if not amount and not prompt: print_error( - f"Just using [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}] is too ambiguous. You need to specify an amount," - f"or use " + f"Using [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}] without an amount/all is too ambiguous. " + f"You need to specify an [{COLORS.G.ARG}]--amount[/{COLORS.G.ARG}] or use " f"[{COLORS.G.ARG}]--unstake-all[/{COLORS.G.ARG}]/[{COLORS.G.ARG}]--unstake-all-alpha[/{COLORS.G.ARG}]." ) return False @@ -3987,8 +3987,8 @@ def stake_remove( json_console.print_json( data={ "success": False, - "err_msg": "Just using '--no-prompt' is too ambiguous without specifying an amount or " - "'--unstake-all'/'--unstake-all-alpha'", + "err_msg": "Using '--json-output' without an amount/all is too ambiguous. You need to specify an " + "'--amount' or use '--unstake-all'/'--unstake-all-alpha'", } ) return False From 2511eb24eef275d53380836f762be1df2db99cc3 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 22:37:35 +0200 Subject: [PATCH 15/57] Update wording --- bittensor_cli/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 458cf1d4..fbad86ba 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3977,7 +3977,7 @@ def stake_remove( ) if not amount and not prompt: print_error( - f"Using [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}] without an amount/all is too ambiguous. " + f"Using [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}] without an amount/'all' is too ambiguous. " f"You need to specify an [{COLORS.G.ARG}]--amount[/{COLORS.G.ARG}] or use " f"[{COLORS.G.ARG}]--unstake-all[/{COLORS.G.ARG}]/[{COLORS.G.ARG}]--unstake-all-alpha[/{COLORS.G.ARG}]." ) @@ -3987,7 +3987,7 @@ def stake_remove( json_console.print_json( data={ "success": False, - "err_msg": "Using '--json-output' without an amount/all is too ambiguous. You need to specify an " + "err_msg": "Using '--json-output' without an amount/'all' is too ambiguous. You need to specify an " "'--amount' or use '--unstake-all'/'--unstake-all-alpha'", } ) From 2d81d059fee165f333df54abf41329e263675859 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Wed, 13 Aug 2025 22:53:56 +0200 Subject: [PATCH 16/57] This can probably be further improved, but adds a helper function to arg format strings for rich output, so it's more readable. --- bittensor_cli/cli.py | 74 +++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fbad86ba..975e8a1e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -91,6 +91,13 @@ class GitError(Exception): np.set_printoptions(precision=8, suppress=True, floatmode="fixed") +def arg__(arg_name: str) -> str: + """ + Helper function to 'arg' format a string for rich console + """ + return f"[{COLORS.G.ARG}]{arg_name}[/{COLORS.G.ARG}]" + + class Options: """ Re-usable typer args @@ -675,8 +682,8 @@ def __init__(self): self.config_app = typer.Typer( epilog=_epilog, help=f"Allows for getting/setting the config. " - f"Default path for the config file is [{COLORS.G.ARG}]{defaults.config.path}[/{COLORS.G.ARG}]. " - f"You can set your own with the env var [{COLORS.G.ARG}]BTCLI_CONFIG_PATH[/{COLORS.G.ARG}]", + f"Default path for the config file is {arg__(defaults.config.path)}. " + f"You can set your own with the env var {arg__('BTCLI_CONFIG_PATH')}", ) self.wallet_app = typer.Typer(epilog=_epilog) self.stake_app = typer.Typer(epilog=_epilog) @@ -1107,7 +1114,7 @@ def initialize_chain( if not_selected_networks: console.print( f"Networks not selected: " - f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" + f"{arg__(', '.join(not_selected_networks))}" ) self.subtensor = SubtensorInterface( @@ -1389,8 +1396,7 @@ def set_config( if n := args.get("network"): if n in Constants.networks: if not Confirm.ask( - f"You provided a network [{COLORS.G.ARG}]{n}[/{COLORS.G.ARG}] which is mapped to " - f"[{COLORS.G.ARG}]{Constants.network_map[n]}[/{COLORS.G.ARG}]\n" + f"You provided a network {arg__(n)} which is mapped to {arg__(Constants.network_map[n])}\n" "Do you want to continue?" ): typer.Exit() @@ -1405,14 +1411,13 @@ def set_config( ) args["network"] = known_network if not Confirm.ask( - f"You provided an endpoint [{COLORS.G.ARG}]{n}[/{COLORS.G.ARG}] which is mapped to " - f"[{COLORS.G.ARG}]{known_network}[/{COLORS.G.ARG}]\n" + f"You provided an endpoint {arg__(n)} which is mapped to {arg__(known_network)}\n" "Do you want to continue?" ): raise typer.Exit() else: if not Confirm.ask( - f"You provided a chain endpoint URL [{COLORS.G.ARG}]{n}[/{COLORS.G.ARG}]\n" + f"You provided a chain endpoint URL {arg__(n)}\n" "Do you want to continue?" ): raise typer.Exit() @@ -1486,17 +1491,11 @@ def del_config( if not any(args.values()): for arg in args.keys(): if self.config.get(arg) is not None: - if Confirm.ask( - f"Do you want to clear the [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config?" - ): + if Confirm.ask(f"Do you want to clear the {arg__(arg)} config?"): self.config[arg] = None - console.print( - f"Cleared [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config and set to 'None'." - ) + console.print(f"Cleared {arg__(arg)} config and set to 'None'.") else: - console.print( - f"Skipped clearing [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config." - ) + console.print(f"Skipped clearing {arg__(arg)} config.") else: # Check each specified argument @@ -1504,21 +1503,18 @@ def del_config( if should_clear: if self.config.get(arg) is not None: if Confirm.ask( - f"Do you want to clear the [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}]" + f"Do you want to clear the {arg__(arg)}" f" [bold cyan]({self.config.get(arg)})[/bold cyan] config?" ): self.config[arg] = None console.print( - f"Cleared [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config and set to 'None'." + f"Cleared {arg__(arg)} config and set to 'None'." ) else: - console.print( - f"Skipped clearing [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config." - ) + console.print(f"Skipped clearing {arg__(arg)} config.") else: console.print( - f"No config set for [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}]." - f" Use [{COLORS.G.ARG}]`btcli config set`[/{COLORS.G.ARG}] to set it." + f"No config set for {arg__(arg)}. Use {arg__('btcli config set')} to set it." ) with open(self.config_path, "w") as f: safe_dump(self.config, f) @@ -1534,8 +1530,7 @@ def get_config(self): Column("[bold white]Value", style="gold1"), Column("", style="medium_purple"), box=box.SIMPLE_HEAD, - title=f"[{COLORS.G.HEADER}]BTCLI Config[/{COLORS.G.HEADER}]: " - f"[{COLORS.G.ARG}]{self.config_path}[/{COLORS.G.ARG}]", + title=f"[{COLORS.G.HEADER}]BTCLI Config[/{COLORS.G.HEADER}]: {arg__(self.config_path)}", ) for key, value in self.config.items(): @@ -3977,9 +3972,9 @@ def stake_remove( ) if not amount and not prompt: print_error( - f"Using [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}] without an amount/'all' is too ambiguous. " - f"You need to specify an [{COLORS.G.ARG}]--amount[/{COLORS.G.ARG}] or use " - f"[{COLORS.G.ARG}]--unstake-all[/{COLORS.G.ARG}]/[{COLORS.G.ARG}]--unstake-all-alpha[/{COLORS.G.ARG}]." + f"Using {arg__('--no-prompt')} without an amount/'all' is too ambiguous. " + f"You need to specify an {arg__('--amount')} or use " + f"{arg__('--unstake-all')}/{arg__('--unstake-all-alpha')}." ) return False @@ -4806,12 +4801,8 @@ def sudo_set( ) return False param_name = "alpha_values" - low_val = FloatPrompt.ask( - f"Enter the new value for [{COLORS.G.ARG}]alpha_low[/{COLORS.G.ARG}]" - ) - high_val = FloatPrompt.ask( - f"Enter the new value for [{COLORS.G.ARG}]alpha_high[/{COLORS.G.ARG}]" - ) + low_val = FloatPrompt.ask(f"Enter the new value for {arg__('alpha_low')}") + high_val = FloatPrompt.ask(f"Enter the new value for {arg__('alpha_high')}") param_value = f"{low_val},{high_val}" if param_name == "yuma_version": if not prompt: @@ -4835,7 +4826,7 @@ def sudo_set( if param_name == "subnet_is_active": err_console.print( f"[{COLORS.SU.HYPERPARAM}]subnet_is_active[/{COLORS.SU.HYPERPARAM}] " - f"is set by using [{COLORS.G.ARG}]`btcli subnets start`[/{COLORS.G.ARG}] command." + f"is set by using {arg__('btcli subnets start')} command." ) return False @@ -5177,14 +5168,12 @@ def subnets_price( """ if json_output and html_output: print_error( - f"Cannot specify both [{COLORS.G.ARG}]--json-output[/{COLORS.G.ARG}] " - f"and [{COLORS.G.ARG}]--html[/{COLORS.G.ARG}]" + f"Cannot specify both {arg__('--json-output')} and {arg__('--html')}" ) return if current_only and html_output: print_error( - f"Cannot specify both [{COLORS.G.ARG}]--current[/{COLORS.G.ARG}] " - f"and [{COLORS.G.ARG}]--html[/{COLORS.G.ARG}]" + f"Cannot specify both {arg__('--current')} and {arg__('--html')}" ) return self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) @@ -5195,9 +5184,8 @@ def subnets_price( Constants.network_map[x] for x in non_archives ]: err_console.print( - f"[red]Error[/red] Running this command without [{COLORS.G.ARG}]--current[/{COLORS.G.ARG}] requires " - "use of an archive node. " - f"Try running again with the [{COLORS.G.ARG}]--network archive[/{COLORS.G.ARG}] flag." + f"[red]Error[/red] Running this command without {arg__('--current')} requires use of an archive node. " + f"Try running again with the {arg__('--network archive')} flag." ) return False From e96a5c8f7951f7f496f1024208b86e2aac572bbe Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 16:15:07 +0200 Subject: [PATCH 17/57] Update text --- bittensor_cli/cli.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fbad86ba..67fe1ef5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3977,9 +3977,9 @@ def stake_remove( ) if not amount and not prompt: print_error( - f"Using [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}] without an amount/'all' is too ambiguous. " - f"You need to specify an [{COLORS.G.ARG}]--amount[/{COLORS.G.ARG}] or use " - f"[{COLORS.G.ARG}]--unstake-all[/{COLORS.G.ARG}]/[{COLORS.G.ARG}]--unstake-all-alpha[/{COLORS.G.ARG}]." + f"Ambiguous request! Specify [{COLORS.G.ARG}]--amount[/{COLORS.G.ARG}], " + f"[{COLORS.G.ARG}]--all[/{COLORS.G.ARG}], " + f"or [{COLORS.G.ARG}]--all-alpha[/{COLORS.G.ARG}] to use [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}]" ) return False @@ -3987,8 +3987,8 @@ def stake_remove( json_console.print_json( data={ "success": False, - "err_msg": "Using '--json-output' without an amount/'all' is too ambiguous. You need to specify an " - "'--amount' or use '--unstake-all'/'--unstake-all-alpha'", + "err_msg": "Ambiguous request! Specify '--amount', '--all', " + "or '--all-alpha' to use '--json-output'", } ) return False From aeedbf8fbff9508ec9656c1a4e899e3beb5444af Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 16:20:28 +0200 Subject: [PATCH 18/57] Merge conflict --- bittensor_cli/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index fb619568..2044bae7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3972,9 +3972,8 @@ def stake_remove( ) if not amount and not prompt: print_error( - f"Ambiguous request! Specify [{COLORS.G.ARG}]--amount[/{COLORS.G.ARG}], " - f"[{COLORS.G.ARG}]--all[/{COLORS.G.ARG}], " - f"or [{COLORS.G.ARG}]--all-alpha[/{COLORS.G.ARG}] to use [{COLORS.G.ARG}]--no-prompt[/{COLORS.G.ARG}]" + f"Ambiguous request! Specify {arg__('--amount')}, {arg__('--all')}, or {arg__('--all-alpha')} " + f"to use {arg__('--no-prompt')}" ) return False From 2497c06dbda6017eb565d35fc849b74259aa972d Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 17:49:05 +0200 Subject: [PATCH 19/57] Return --- bittensor_cli/src/commands/stake/remove.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 67f0109f..3a37b8cb 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -361,6 +361,7 @@ async def unstake( ) if json_output: json_console.print(json.dumps(successes)) + return True async def unstake_all( From fb10995dc1ff01be59f8f744477d5955fc4abeee Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 22:26:28 +0200 Subject: [PATCH 20/57] Initial add for just the async-substrate-interface logger --- bittensor_cli/cli.py | 14 ++++++++++++++ bittensor_cli/src/__init__.py | 1 + pyproject.toml | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6604c08b..694ed8ab 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4,6 +4,7 @@ import curses import importlib import json +import logging import os.path import re import ssl @@ -665,6 +666,9 @@ def __init__(self): self.config_path = os.getenv("BTCLI_CONFIG_PATH") or os.path.expanduser( defaults.config.path ) + self.debug_file_path = os.getenv("BTCLI_DEBUG_FILE") or os.path.expanduser( + defaults.config.debug_file_path + ) self.app = typer.Typer( rich_markup_mode="rich", @@ -1221,6 +1225,16 @@ def main_callback( for k, v in config.items(): if k in self.config.keys(): self.config[k] = v + if self.config.get("use_cache", False): + with open(self.debug_file_path, "w+") as f: + f.write(f"BTCLI {__version__}\nCommand: {' '.join(sys.argv)}\n\n") + asi_logger = logging.getLogger("async_substrate_interface") + asi_logger.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s') + handler = logging.FileHandler(self.debug_file_path) + handler.setFormatter(formatter) + asi_logger.addHandler(handler) + def verbosity_handler( self, quiet: bool, verbose: bool, json_output: bool = False diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index afabc424..ef9493ca 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -88,6 +88,7 @@ class Defaults: class config: base_path = "~/.bittensor" path = "~/.bittensor/config.yml" + debug_file_path = "~/.bittensor/debug.txt" dictionary = { "network": None, "wallet_path": None, diff --git a/pyproject.toml b/pyproject.toml index 767047b3..5a47f2b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ scripts = { btcli = "bittensor_cli.cli:main" } requires-python = ">=3.9,<3.14" dependencies = [ "wheel", - "async-substrate-interface>=1.4.2", + "async-substrate-interface>=1.5.2", "aiohttp~=3.10.2", "backoff~=2.2.1", "GitPython>=3.0.0", From bd4de55c34ef9a5358610a3584cf77213132c8eb Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 23:00:09 +0200 Subject: [PATCH 21/57] WIP check-in --- bittensor_cli/cli.py | 144 ++++++++++++++++++++++---- bittensor_cli/src/commands/wallets.py | 2 +- 2 files changed, 122 insertions(+), 24 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 694ed8ab..71ea58ea 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -87,6 +87,7 @@ class GitError(Exception): pass +logger = logging.getLogger("btcli") _epilog = "Made with [bold red]:heart:[/bold red] by The Openτensor Foundaτion" np.set_printoptions(precision=8, suppress=True, floatmode="fixed") @@ -1227,14 +1228,19 @@ def main_callback( self.config[k] = v if self.config.get("use_cache", False): with open(self.debug_file_path, "w+") as f: - f.write(f"BTCLI {__version__}\nCommand: {' '.join(sys.argv)}\n\n") + f.write( + f"BTCLI {__version__}\nCommand: {' '.join(sys.argv)}\n{self.config}\n\n" + ) asi_logger = logging.getLogger("async_substrate_interface") asi_logger.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s') + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(name)s - %(module)s:%(lineno)d - %(message)s" + ) handler = logging.FileHandler(self.debug_file_path) handler.setFormatter(formatter) asi_logger.addHandler(handler) - + logger.addHandler(handler) def verbosity_handler( self, quiet: bool, verbose: bool, json_output: bool = False @@ -1436,6 +1442,7 @@ def set_config( for arg, val in args.items(): if val is not None: + logger.debug(f"Config: setting {arg} to {val}") self.config[arg] = val with open(self.config_path, "w") as f: safe_dump(self.config, f) @@ -1503,6 +1510,7 @@ def del_config( if Confirm.ask( f"Do you want to clear the [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config?" ): + logger.debug(f"Config: clearing {arg}.") self.config[arg] = None console.print( f"Cleared [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config and set to 'None'." @@ -1522,6 +1530,7 @@ def del_config( f" [bold cyan]({self.config.get(arg)})[/bold cyan] config?" ): self.config[arg] = None + logger.debug(f"Config: clearing {arg}.") console.print( f"Cleared [{COLORS.G.ARG}]{arg}[/{COLORS.G.ARG}] config and set to 'None'." ) @@ -1623,26 +1632,31 @@ def ask_safe_staking( bool: Safe staking setting """ if safe_staking is not None: + enabled = "enabled" if safe_staking else "disabled" console.print( - f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan]." + f"[dim][blue]Safe staking[/blue]: [bold cyan]{enabled}[/bold cyan]." ) + logger.debug(f"Safe staking {enabled}") return safe_staking elif self.config.get("safe_staking") is not None: safe_staking = self.config["safe_staking"] + enabled = "enabled" if safe_staking else "disabled" console.print( - f"[dim][blue]Safe staking[/blue]: [bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] (from config)." + f"[dim][blue]Safe staking[/blue]: [bold cyan]{enabled}[/bold cyan] (from config)." ) + logger.debug(f"Safe staking {enabled}") return safe_staking else: safe_staking = True console.print( "[dim][blue]Safe staking[/blue]: " - + f"[bold cyan]{'enabled' if safe_staking else 'disabled'}[/bold cyan] " - + "by default. Set this using " - + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " - + "or " - + "[dark_sea_green3 italic]`--safe/--unsafe`[/dark_sea_green3 italic] flag[/dim]" + f"[bold cyan]enabled[/bold cyan] " + "by default. Set this using " + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + "or " + "[dark_sea_green3 italic]`--safe/--unsafe`[/dark_sea_green3 italic] flag[/dim]" ) + logger.debug(f"Safe staking enabled.") return safe_staking def ask_partial_stake( @@ -1659,25 +1673,31 @@ def ask_partial_stake( bool: Partial stake setting """ if allow_partial_stake is not None: + partial_staking = "enabled" if allow_partial_stake else "disabled" console.print( - f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan]." + f"[dim][blue]Partial staking[/blue]: [bold cyan]{partial_staking}[/bold cyan]." ) + logger.debug(f"Partial staking {partial_staking}") return allow_partial_stake elif self.config.get("allow_partial_stake") is not None: config_partial = self.config["allow_partial_stake"] + partial_staking = "enabled" if allow_partial_stake else "disabled" console.print( - f"[dim][blue]Partial staking[/blue]: [bold cyan]{'enabled' if config_partial else 'disabled'}[/bold cyan] (from config)." + f"[dim][blue]Partial staking[/blue]: [bold cyan]{partial_staking}[/bold cyan] (from config)." ) + logger.debug(f"Partial staking {partial_staking}") return config_partial else: + partial_staking = "enabled" if allow_partial_stake else "disabled" console.print( "[dim][blue]Partial staking[/blue]: " - + f"[bold cyan]{'enabled' if allow_partial_stake else 'disabled'}[/bold cyan] " - + "by default. Set this using " - + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " - + "or " - + "[dark_sea_green3 italic]`--partial/--no-partial`[/dark_sea_green3 italic] flag[/dim]" + f"[bold cyan]{partial_staking}[/bold cyan] " + "by default. Set this using " + "[dark_sea_green3 italic]`btcli config set`[/dark_sea_green3 italic] " + "or " + "[dark_sea_green3 italic]`--partial/--no-partial`[/dark_sea_green3 italic] flag[/dim]" ) + logger.debug(f"Partial staking {partial_staking}") return False def wallet_ask( @@ -1750,6 +1770,7 @@ def wallet_ask( if wallet_path: wallet_path = os.path.expanduser(wallet_path) wallet = Wallet(name=wallet_name, path=wallet_path, hotkey=wallet_hotkey) + logger.debug(f"Using wallet {wallet}") # Validate the wallet if required if validate == WV.WALLET or validate == WV.WALLET_AND_HOTKEY: @@ -1905,6 +1926,14 @@ def wallet_overview( str, "Hotkeys names must be a comma-separated list, e.g., `--exclude-hotkeys hk1,hk2`.", ) + logger.debug( + f"all_wallets: {all_wallets}\n" + f"sort_by: {sort_by}\n" + f"sort_order: {sort_order}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"netuids: {netuids}\n" + ) return self._run_command( wallets.overview( @@ -1994,6 +2023,14 @@ def wallet_transfer( amount = 0 elif not amount: amount = FloatPrompt.ask("Enter amount (in TAO) to transfer.") + logger.debug( + f"destination: {destination_ss58_address}\n" + f"amount: {amount}\n" + f"transfer_all: {transfer_all}\n" + f"allow_death: {allow_death}\n" + f"period: {period}\n" + f"prompt: {prompt}\n" + ) return self._run_command( wallets.transfer( wallet=wallet, @@ -2063,6 +2100,12 @@ def wallet_swap_hotkey( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + f"original_wallet: {original_wallet}\n" + f"new_wallet: {new_wallet}\n" + f"netuid: {netuid}\n" + f"prompt: {prompt}\n" + ) self.initialize_chain(network) return self._run_command( wallets.swap_hotkey( @@ -2226,6 +2269,16 @@ def wallet_faucet( ask_for=[WO.NAME, WO.PATH], validate=WV.WALLET, ) + logger.debug( + f"network {network}\n" + f"threads_per_block {threads_per_block}\n" + f"update_interval {update_interval}\n" + f"processors {processors}\n" + f"use_cuda {use_cuda}\n" + f"dev_id {dev_id}\n" + f"output_in_place {output_in_place}\n" + f"max_successes {max_successes}\n" + ) return self._run_command( wallets.faucet( wallet, @@ -2293,6 +2346,7 @@ def wallet_regen_coldkey( mnemonic, seed, json_path, json_password = get_creation_data( mnemonic, seed, json_path, json_password ) + # logger.debug should NOT be used here, it's simply too risky return self._run_command( wallets.regen_coldkey( wallet, @@ -2362,6 +2416,7 @@ def wallet_regen_coldkey_pub( ): rich.print("[red]Error: Invalid SS58 address or public key![/red]") return + # do not logger.debug any creation cmds return self._run_command( wallets.regen_coldkey_pub( wallet, ss58_address, public_key_hex, overwrite, json_output @@ -2414,6 +2469,7 @@ def wallet_regen_hotkey( mnemonic, seed, json_path, json_password = get_creation_data( mnemonic, seed, json_path, json_password ) + # do not logger.debug any creation cmds return self._run_command( wallets.regen_hotkey( wallet, @@ -2483,6 +2539,7 @@ def wallet_regen_hotkey_pub( ): rich.print("[red]Error: Invalid SS58 address or public key![/red]") return False + # do not logger.debug any creation cmds return self._run_command( wallets.regen_hotkey_pub( wallet, ss58_address, public_key_hex, overwrite, json_output @@ -2547,6 +2604,7 @@ def wallet_new_hotkey( ) if not uri: n_words = get_n_words(n_words) + # do not logger.debug any creation cmds return self._run_command( wallets.new_hotkey( wallet, n_words, use_password, uri, overwrite, json_output @@ -2613,7 +2671,12 @@ def wallet_associate_hotkey( f"hotkey [blue]{wallet_hotkey}[/blue] " f"[{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" ) - + logger.debug( + f"network {network}\n" + f"hotkey_ss58 {hotkey_ss58}\n" + f"hotkey_display {hotkey_display}\n" + f"prompt {prompt}\n" + ) return self._run_command( wallets.associate_hotkey( wallet, @@ -2761,7 +2824,8 @@ def wallet_check_ck_swap( 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]", + "[blue]Enter the block number[/blue] where the swap was scheduled " + "[dim](optional, press enter to skip)[/dim]", default="", ) if block_input: @@ -2770,7 +2834,11 @@ def wallet_check_ck_swap( except ValueError: print_error("Invalid block number") raise typer.Exit() - + logger.debug( + f"scheduled_block {scheduled_block}\n" + f"ss58_address {ss58_address}\n" + f"network {network}\n" + ) return self._run_command( wallets.check_swap_status(self.subtensor, ss58_address, scheduled_block) ) @@ -2827,6 +2895,7 @@ def wallet_create_wallet( ) if not uri: n_words = get_n_words(n_words) + # do not logger.debug any creation commands return self._run_command( wallets.wallet_create( wallet, n_words, use_password, uri, overwrite, json_output @@ -2935,6 +3004,11 @@ def wallet_balance( ask_for=ask_for, validate=validate, ) + logger.debug( + f"all_balances {all_balances}\n" + f"ss58_addresses {ss58_addresses}\n" + f"network {network}" + ) subtensor = self.initialize_chain(network) return self._run_command( wallets.wallet_balance( @@ -3085,6 +3159,7 @@ def wallet_set_id( additional, github_repo, ) + logger.debug(f"identity {identity}\nnetwork {network}\n") return self._run_command( wallets.set_id( @@ -3346,7 +3421,11 @@ def wallet_swap_coldkey( f"[dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" ) new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address - + logger.debug( + f"network {network}\n" + f"new_coldkey_ss58 {new_wallet_coldkey_ss58}\n" + f"force_swap {force_swap}" + ) return self._run_command( wallets.schedule_coldkey_swap( wallet=wallet, @@ -3418,7 +3497,12 @@ def stake_list( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) - + logger.debug( + f"coldkey_ss58 {coldkey_ss58}\n" + f"network {network}\n" + f"live: {live}\n" + f"no_prompt: {no_prompt}\n" + ) return self._run_command( list_stake.stake_list( wallet, @@ -3667,6 +3751,7 @@ def stake_add( ), exit_early=False, ) + logger.debug(f"Free balance: {free_balance}") if free_balance == Balance.from_tao(0): print_error("You dont have any balance to stake.") return @@ -3687,7 +3772,20 @@ def stake_add( f"You dont have enough balance to stake. Current free Balance: {free_balance}." ) raise typer.Exit() - + logger.debug( + f"network: {network}\n" + f"netuids: {netuids}\n" + f"stake_all: {stake_all}\n" + f"amount: {amount}\n" + f"prompt: {prompt}\n" + f"all_hotkeys: {all_hotkeys}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"safe_staking: {safe_staking}\n" + f"rate_tolerance: {rate_tolerance}\n" + f"allow_partial_stake: {allow_partial_stake}\n" + f"period: {period}\n" + ) return self._run_command( add_stake.stake_add( wallet, diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ff37a50e..b45a5d0e 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1836,7 +1836,7 @@ async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): async def sign( - wallet: Wallet, message: str, use_hotkey: str, json_output: bool = False + wallet: Wallet, message: str, use_hotkey: bool, json_output: bool = False ): """Sign a message using the provided wallet or hotkey.""" From ddd67668a7fd7e11aa31593afff9fa6aee68afc5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 23:31:08 +0200 Subject: [PATCH 22/57] Added most things --- bittensor_cli/cli.py | 122 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 8 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 71ea58ea..a4eefb62 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4035,6 +4035,16 @@ def stake_remove( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + f"network: {network}\n" + f"hotkey_ss58_address: {hotkey_ss58_address}\n" + f"unstake_all: {unstake_all}\n" + f"unstake_all_alpha: {unstake_all_alpha}\n" + f"all_hotkeys: {all_hotkeys}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"era: {period}" + ) return self._run_command( remove_stake.unstake_all( wallet=wallet, @@ -4085,7 +4095,21 @@ def stake_remove( "Hotkeys must be a comma-separated list of ss58s or names, e.g., `--exclude-hotkeys hk3,hk4`.", is_ss58=False, ) - + logger.debug( + f"network: {network}\n" + f"hotkey_ss58_address: {hotkey_ss58_address}\n" + f"all_hotkeys: {all_hotkeys}\n" + f"include_hotkeys: {include_hotkeys}\n" + f"exclude_hotkeys: {exclude_hotkeys}\n" + f"amount: {amount}\n" + f"prompt: {prompt}\n" + f"interactive: {interactive}\n" + f"netuid: {netuid}\n" + f"safe_staking: {safe_staking}\n" + f"rate_tolerance: {rate_tolerance}\n" + f"allow_partial_stake: {allow_partial_stake}\n" + f"era: {period}" + ) return self._run_command( remove_stake.unstake( wallet=wallet, @@ -4249,7 +4273,18 @@ def stake_move( destination_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" ) - + logger.debug( + f"network: {network}\n" + f"origin_netuid: {origin_netuid}\n" + f"origin_hotkey: {origin_hotkey}\n" + f"destination_hotkey: {destination_hotkey}\n" + f"destination_netuid: {destination_netuid}\n" + f"amount: {amount}\n" + f"stake_all: {stake_all}\n" + f"era: {period}\n" + f"interactive_selection: {interactive_selection}\n" + f"prompt: {prompt}\n" + ) result = self._run_command( move_stake.move_stake( subtensor=self.initialize_chain(network), @@ -4414,7 +4449,17 @@ def stake_transfer( dest_netuid = IntPrompt.ask( "Enter the [blue]destination subnet[/blue] (netuid)" ) - + logger.debug( + f"network: {network}\n" + f"origin_hotkey: {origin_hotkey}\n" + f"origin_netuid: {origin_netuid}\n" + f"dest_netuid: {dest_netuid}\n" + f"dest_hotkey: {origin_hotkey}\n" + f"dest_coldkey_ss58: {dest_ss58}\n" + f"amount: {amount}\n" + f"era: {period}\n" + f"stake_all: {stake_all}" + ) result = self._run_command( move_stake.transfer_stake( wallet=wallet, @@ -4522,7 +4567,18 @@ def stake_swap( ) if not amount and not swap_all: amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to swap") - + logger.debug( + f"network: {network}\n" + f"origin_netuid: {origin_netuid}\n" + f"dest_netuid: {dest_netuid}\n" + f"amount: {amount}\n" + f"swap_all: {swap_all}\n" + f"era: {period}\n" + f"interactive_selection: {interactive_selection}\n" + f"prompt: {prompt}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) result = self._run_command( move_stake.swap_stake( wallet=wallet, @@ -4671,6 +4727,14 @@ def stake_set_children( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug( + f"network: {network}\n" + f"netuid: {netuid}\n" + f"children: {children}\n" + f"proportions: {proportions}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) return self._run_command( children_hotkeys.set_children( wallet=wallet, @@ -4736,6 +4800,12 @@ def stake_revoke_children( netuid = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) + logger.debug( + f"network: {network}\n" + f"netuid: {netuid}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) return self._run_command( children_hotkeys.revoke_children( wallet, @@ -4814,6 +4884,13 @@ def stake_childkey_take( netuid = IntPrompt.ask( "Enter netuid (leave blank for all)", default=None, show_default=True ) + logger.debug( + f"network: {network}\n" + f"netuid: {netuid}\n" + f"take: {take}\n" + f"wait_for_inclusion: {wait_for_inclusion}\n" + f"wait_for_finalization: {wait_for_finalization}\n" + ) results: list[tuple[Optional[int], bool]] = self._run_command( children_hotkeys.childkey_take( wallet=wallet, @@ -4949,6 +5026,12 @@ def sudo_set( wallet = self.wallet_ask( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) + logger.debug( + f"network: {network}\n" + f"netuid: {netuid}\n" + f"param_name: {param_name}\n" + f"param_value: {param_value}" + ) result, err_msg = self._run_command( sudo.sudo_set_hyperparameter( wallet, @@ -5069,6 +5152,7 @@ def sudo_senate_vote( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug(f"network: {network}\nproposal: {proposal}\nvote: {vote}\n") return self._run_command( sudo.senate_vote( wallet, self.initialize_chain(network), proposal, vote, prompt @@ -5121,7 +5205,7 @@ def sudo_set_take( f"Take value must be between {min_value} and {max_value}. Provided value: {take}" ) raise typer.Exit() - + logger.debug(f"network: {network}\ntake: {take}") result = self._run_command( sudo.set_take(wallet, self.initialize_chain(network), take) ) @@ -5465,6 +5549,7 @@ def subnets_create( logo_url=logo_url, additional=additional_info, ) + logger.debug(f"network: {network}\nidentity: {identity}\n") self._run_command( subnets.create( wallet, self.initialize_chain(network), identity, json_output, prompt @@ -5526,6 +5611,7 @@ def subnets_start( ], validate=WV.WALLET, ) + logger.debug(f"network: {network}\nnetuid: {netuid}\n") return self._run_command( subnets.start_subnet( wallet, @@ -5641,7 +5727,7 @@ def subnets_set_identity( logo_url=logo_url, additional=additional_info, ) - + logger.debug(f"network: {network}\nnetuid: {netuid}\nidentity: {identity}") success = self._run_command( subnets.set_identity( wallet, self.initialize_chain(network), netuid, identity, prompt @@ -5779,6 +5865,7 @@ def subnets_register( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + logger.debug(f"network: {network}\nnetuid: {netuid}\nperiod: {period}\n") return self._run_command( subnets.register( wallet, @@ -5976,7 +6063,6 @@ def weights_reveal( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - return self._run_command( weights_cmds.reveal_weights( self.initialize_chain(network), @@ -6222,7 +6308,13 @@ def liquidity_add( if price_low >= price_high: err_console.print("The low price must be lower than the high price.") return False - + logger.debug( + f"hotkey: {hotkey}\n" + f"netuid: {netuid}\n" + f"liquidity: {liquidity_}\n" + f"price_low: {price_low}\n" + f"price_high: {price_high}\n" + ) return self._run_command( liquidity.add_liquidity( subtensor=self.initialize_chain(network), @@ -6323,6 +6415,13 @@ def liquidity_remove( validate=WV.WALLET, return_wallet_and_hotkey=True, ) + logger.debug( + f"network: {network}\n" + f"hotkey: {hotkey}\n" + f"netuid: {netuid}\n" + f"position_id: {position_id}\n" + f"all_liquidity_ids: {all_liquidity_ids}\n" + ) return self._run_command( liquidity.remove_liquidity( subtensor=self.initialize_chain(network), @@ -6387,6 +6486,13 @@ def liquidity_modify( f"[blue]{position_id}[/blue] (can be positive or negative)", negative_allowed=True, ) + logger.debug( + f"network: {network}\n" + f"hotkey: {hotkey}\n" + f"netuid: {netuid}\n" + f"position_id: {position_id}\n" + f"liquidity_delta: {liquidity_delta}" + ) return self._run_command( liquidity.modify_liquidity( From 27322279c7aaa453cdab3c8aa9805c840e44fd98 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 23:45:53 +0200 Subject: [PATCH 23/57] Added `--debug` --- bittensor_cli/cli.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a4eefb62..4cafc10d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -594,7 +594,29 @@ def commands_callback(value: bool): if value: cli = CLIManager() console.print(cli.generate_command_tree()) - raise typer.Exit() + + +def debug_callback(value: bool): + if value: + save_file_loc_ = Prompt.ask( + "Enter the file location to save the debug log for the previous command.", + default="~/.bittensor/debug-export", + ).strip() + save_file_loc = os.path.expanduser(save_file_loc_) + try: + with ( + open(save_file_loc, "w+") as save_file, + open( + os.getenv("BTCLI_DEBUG_FILE") + or os.path.expanduser(defaults.config.debug_file_path), + "r", + ) as current_file, + ): + save_file.write(current_file.read()) + console.print(f"Saved debug log to {save_file_loc}") + except FileNotFoundError: + print_error(f"The filepath '{save_file_loc}' does not exist.") + raise typer.Exit() class CLIManager: @@ -1192,6 +1214,14 @@ def main_callback( "--commands", callback=commands_callback, help="Show BTCLI commands" ), ] = None, + debug_log: Annotated[ + Optional[bool], + typer.Option( + "--debug", + callback=debug_callback, + help="Saves the debug log from the last used command", + ), + ] = None, ): """ Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be From 9dfa055cb1fcf675df96f11016e3822f8e54e2cd Mon Sep 17 00:00:00 2001 From: bdhimes Date: Thu, 14 Aug 2025 23:52:54 +0200 Subject: [PATCH 24/57] ruff --- bittensor_cli/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index ea8df234..c507f237 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4160,7 +4160,6 @@ def stake_remove( f"era: {period}" ) - return self._run_command( remove_stake.unstake( wallet=wallet, From d79248cebf4c8220939f45e86c78f20252018b92 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 00:11:36 +0200 Subject: [PATCH 25/57] Fixes the example config.yml in the README, and adds a section on environment variables --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5d899fba..f9ea6cec 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,13 @@ You can set the commonly used values, such as your hotkey and coldkey names, the The default location of the config file is: `~/.bittensor/config.yml`. An example of a `config.yml` is shown below: ```yaml -chain: ws://127.0.0.1:9945 network: local -no_cache: False -wallet_hotkey: hotkey-user1 +use_cache: true +dashboard_path: null +disk_cache: false +rate_tolerance: null +safe_staking: true +wallet_hotkey: default wallet_name: coldkey-user1 wallet_path: ~/.bittensor/wallets metagraph_cols: @@ -165,6 +168,13 @@ metagraph_cols: btcli config --help ``` +### ENV VARS +BTCLI accepts a few environment variables that can alter how it works: + - USE_TORCH (default 0): If set to 1, will use torch instead of numpy + - DISK_CACHE (default 0, also settable in config): If set to 1 (or set in config), will use disk caching for various safe-cachable substrate +calls (such as block number to block hash mapping), which can speed up subsequent calls. + - BTCLI_CONFIG_PATH (default `~/.bittensor/config.yml`): This will set the config file location, creating if it does not exist. + --- ## License From 2960cdeda8ea0a01c99c06a199948c3663116fa4 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 00:22:57 +0200 Subject: [PATCH 26/57] Expand README --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index f9ea6cec..6d8b6abf 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,21 @@ BTCLI accepts a few environment variables that can alter how it works: - DISK_CACHE (default 0, also settable in config): If set to 1 (or set in config), will use disk caching for various safe-cachable substrate calls (such as block number to block hash mapping), which can speed up subsequent calls. - BTCLI_CONFIG_PATH (default `~/.bittensor/config.yml`): This will set the config file location, creating if it does not exist. + - BTCLI_DEBUG_FILE (default `~/.bittensor/debug.txt`): The file stores the most recent's command's debug log. + +--- + +## Debugging +BTCLI will store a debug log for every command run. This file is overwritten for each new command run. The default location +of this file is `~/.bittensor/debug.txt` and can be set with the `BTCLI_DEBUG_FILE` env var (see above section). + +The debug log will **NOT** contain any sensitive data (private keys), and is intended to be sent to the developers +for debugging. This file contains basic information about the command being run, the config, and the back and forth of requests and responses +to and from the chain. + +If you encounter an issue, and would like to save the file somewhere it won't be overwritten, run `btcli --debug`, +and set the save file location. We recommend doing this first before anything, and then starting your debugging with +us on our [Discord](https://discord.gg/bittensor). --- From c82f079b2eaeb8865ccfec33434cc39165179d8e Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 13:53:42 +0200 Subject: [PATCH 27/57] Missed indent --- 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 c507f237..a9a8a61e 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -616,7 +616,7 @@ def debug_callback(value: bool): console.print(f"Saved debug log to {save_file_loc}") except FileNotFoundError: print_error(f"The filepath '{save_file_loc}' does not exist.") - raise typer.Exit() + raise typer.Exit() class CLIManager: From 750221db7373ef58856195a0148c2a8ed3cc3fb4 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 13:56:40 +0200 Subject: [PATCH 28/57] Add info about async substrate interface and bt-wallet --- bittensor_cli/cli.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index a9a8a61e..41b163a5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1259,7 +1259,11 @@ def main_callback( if self.config.get("use_cache", False): with open(self.debug_file_path, "w+") as f: f.write( - f"BTCLI {__version__}\nCommand: {' '.join(sys.argv)}\n{self.config}\n\n" + f"BTCLI {__version__}\n" + f"Async-Substrate-Interface: {importlib.metadata.version('async-substrate-interface')}\n" + f"Bittensor-Wallet: {importlib.metadata.version('bittensor-wallet')}\n" + f"Command: {' '.join(sys.argv)}\n" + f"Config: {self.config}\n\n" ) asi_logger = logging.getLogger("async_substrate_interface") asi_logger.setLevel(logging.DEBUG) From 5580ee275ca18a56a48b583c3db0ee73775badf3 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 13:59:08 +0200 Subject: [PATCH 29/57] Add system info --- bittensor_cli/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 41b163a5..c17fcc4f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1263,7 +1263,9 @@ def main_callback( f"Async-Substrate-Interface: {importlib.metadata.version('async-substrate-interface')}\n" f"Bittensor-Wallet: {importlib.metadata.version('bittensor-wallet')}\n" f"Command: {' '.join(sys.argv)}\n" - f"Config: {self.config}\n\n" + f"Config: {self.config}\n" + f"Python: {sys.version}\n" + f"System: {sys.platform}\n\n" ) asi_logger = logging.getLogger("async_substrate_interface") asi_logger.setLevel(logging.DEBUG) From fac14ccc4605d4caa5f524f6808a0aa77d52f207 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 14:04:38 +0200 Subject: [PATCH 30/57] Additional debug info --- bittensor_cli/cli.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index c17fcc4f..df38a4f0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1963,6 +1963,7 @@ def wallet_overview( "Hotkeys names must be a comma-separated list, e.g., `--exclude-hotkeys hk1,hk2`.", ) logger.debug( + "args:\n" f"all_wallets: {all_wallets}\n" f"sort_by: {sort_by}\n" f"sort_order: {sort_order}\n" @@ -2060,6 +2061,7 @@ def wallet_transfer( elif not amount: amount = FloatPrompt.ask("Enter amount (in TAO) to transfer.") logger.debug( + "args:\n" f"destination: {destination_ss58_address}\n" f"amount: {amount}\n" f"transfer_all: {transfer_all}\n" @@ -2137,6 +2139,7 @@ def wallet_swap_hotkey( validate=WV.WALLET_AND_HOTKEY, ) logger.debug( + "args:\n" f"original_wallet: {original_wallet}\n" f"new_wallet: {new_wallet}\n" f"netuid: {netuid}\n" @@ -2306,6 +2309,7 @@ def wallet_faucet( validate=WV.WALLET, ) logger.debug( + "args:\n" f"network {network}\n" f"threads_per_block {threads_per_block}\n" f"update_interval {update_interval}\n" @@ -2708,6 +2712,7 @@ def wallet_associate_hotkey( f"[{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]" ) logger.debug( + "args:\n" f"network {network}\n" f"hotkey_ss58 {hotkey_ss58}\n" f"hotkey_display {hotkey_display}\n" @@ -2871,6 +2876,7 @@ def wallet_check_ck_swap( print_error("Invalid block number") raise typer.Exit() logger.debug( + "args:\n" f"scheduled_block {scheduled_block}\n" f"ss58_address {ss58_address}\n" f"network {network}\n" @@ -3041,6 +3047,7 @@ def wallet_balance( validate=validate, ) logger.debug( + "args:\n" f"all_balances {all_balances}\n" f"ss58_addresses {ss58_addresses}\n" f"network {network}" @@ -3195,7 +3202,7 @@ def wallet_set_id( additional, github_repo, ) - logger.debug(f"identity {identity}\nnetwork {network}\n") + logger.debug(f"args:\nidentity {identity}\nnetwork {network}\n") return self._run_command( wallets.set_id( @@ -3458,6 +3465,7 @@ def wallet_swap_coldkey( ) new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address logger.debug( + "args:\n" f"network {network}\n" f"new_coldkey_ss58 {new_wallet_coldkey_ss58}\n" f"force_swap {force_swap}" @@ -3534,6 +3542,7 @@ def stake_list( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) logger.debug( + "args:\n" f"coldkey_ss58 {coldkey_ss58}\n" f"network {network}\n" f"live: {live}\n" @@ -3809,6 +3818,7 @@ def stake_add( ) raise typer.Exit() logger.debug( + "args:\n" f"network: {network}\n" f"netuids: {netuids}\n" f"stake_all: {stake_all}\n" @@ -4090,6 +4100,7 @@ def stake_remove( validate=WV.WALLET_AND_HOTKEY, ) logger.debug( + "args:\n" f"network: {network}\n" f"hotkey_ss58_address: {hotkey_ss58_address}\n" f"unstake_all: {unstake_all}\n" @@ -4151,6 +4162,7 @@ def stake_remove( ) return False logger.debug( + "args:\n" f"network: {network}\n" f"hotkey_ss58_address: {hotkey_ss58_address}\n" f"all_hotkeys: {all_hotkeys}\n" @@ -4330,6 +4342,7 @@ def stake_move( "Enter the [blue]destination subnet[/blue] (netuid) to move stake to" ) logger.debug( + "args:\n" f"network: {network}\n" f"origin_netuid: {origin_netuid}\n" f"origin_hotkey: {origin_hotkey}\n" @@ -4506,6 +4519,7 @@ def stake_transfer( "Enter the [blue]destination subnet[/blue] (netuid)" ) logger.debug( + "args:\n" f"network: {network}\n" f"origin_hotkey: {origin_hotkey}\n" f"origin_netuid: {origin_netuid}\n" @@ -4624,6 +4638,7 @@ def stake_swap( if not amount and not swap_all: amount = FloatPrompt.ask("Enter the [blue]amount[/blue] to swap") logger.debug( + "args:\n" f"network: {network}\n" f"origin_netuid: {origin_netuid}\n" f"dest_netuid: {dest_netuid}\n" @@ -4784,6 +4799,7 @@ def stake_set_children( validate=WV.WALLET_AND_HOTKEY, ) logger.debug( + "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" f"children: {children}\n" @@ -4857,6 +4873,7 @@ def stake_revoke_children( "Enter netuid (leave blank for all)", default=None, show_default=True ) logger.debug( + "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" f"wait_for_inclusion: {wait_for_inclusion}\n" @@ -4941,6 +4958,7 @@ def stake_childkey_take( "Enter netuid (leave blank for all)", default=None, show_default=True ) logger.debug( + "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" f"take: {take}\n" @@ -5083,6 +5101,7 @@ def sudo_set( wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH] ) logger.debug( + "args:\n" f"network: {network}\n" f"netuid: {netuid}\n" f"param_name: {param_name}\n" @@ -5208,7 +5227,7 @@ def sudo_senate_vote( ask_for=[WO.NAME, WO.PATH, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - logger.debug(f"network: {network}\nproposal: {proposal}\nvote: {vote}\n") + logger.debug(f"args:\nnetwork: {network}\nproposal: {proposal}\nvote: {vote}\n") return self._run_command( sudo.senate_vote( wallet, self.initialize_chain(network), proposal, vote, prompt @@ -5261,7 +5280,7 @@ def sudo_set_take( f"Take value must be between {min_value} and {max_value}. Provided value: {take}" ) raise typer.Exit() - logger.debug(f"network: {network}\ntake: {take}") + logger.debug(f"args:\nnetwork: {network}\ntake: {take}") result = self._run_command( sudo.set_take(wallet, self.initialize_chain(network), take) ) @@ -5605,7 +5624,7 @@ def subnets_create( logo_url=logo_url, additional=additional_info, ) - logger.debug(f"network: {network}\nidentity: {identity}\n") + logger.debug(f"args:\nnetwork: {network}\nidentity: {identity}\n") self._run_command( subnets.create( wallet, self.initialize_chain(network), identity, json_output, prompt @@ -5667,7 +5686,7 @@ def subnets_start( ], validate=WV.WALLET, ) - logger.debug(f"network: {network}\nnetuid: {netuid}\n") + logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\n") return self._run_command( subnets.start_subnet( wallet, @@ -5783,7 +5802,9 @@ def subnets_set_identity( logo_url=logo_url, additional=additional_info, ) - logger.debug(f"network: {network}\nnetuid: {netuid}\nidentity: {identity}") + logger.debug( + f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}" + ) success = self._run_command( subnets.set_identity( wallet, self.initialize_chain(network), netuid, identity, prompt @@ -5921,7 +5942,7 @@ def subnets_register( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) - logger.debug(f"network: {network}\nnetuid: {netuid}\nperiod: {period}\n") + logger.debug(f"args:\nnetwork: {network}\nnetuid: {netuid}\nperiod: {period}\n") return self._run_command( subnets.register( wallet, @@ -6365,6 +6386,7 @@ def liquidity_add( err_console.print("The low price must be lower than the high price.") return False logger.debug( + f"args:\n" f"hotkey: {hotkey}\n" f"netuid: {netuid}\n" f"liquidity: {liquidity_}\n" @@ -6472,6 +6494,7 @@ def liquidity_remove( return_wallet_and_hotkey=True, ) logger.debug( + f"args:\n" f"network: {network}\n" f"hotkey: {hotkey}\n" f"netuid: {netuid}\n" @@ -6543,6 +6566,7 @@ def liquidity_modify( negative_allowed=True, ) logger.debug( + f"args:\n" f"network: {network}\n" f"hotkey: {hotkey}\n" f"netuid: {netuid}\n" From 5ab9e8bdae2593aaf2e609f865c51ff9050214fd Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 14:07:08 +0200 Subject: [PATCH 31/57] Update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d8b6abf..e3d8d38a 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,8 @@ to and from the chain. If you encounter an issue, and would like to save the file somewhere it won't be overwritten, run `btcli --debug`, and set the save file location. We recommend doing this first before anything, and then starting your debugging with -us on our [Discord](https://discord.gg/bittensor). +us on our [Discord](https://discord.gg/bittensor), or by opening an issue on [GitHub](https://github.com/opentensor/btcli/issues/new) +(where you can also upload your debug file). --- From fb400e7055c5114a94179ad84a1584cf91ad17ff Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 16:18:21 +0200 Subject: [PATCH 32/57] Adds a command to compare latencies of given connections to help the user pick the fastest for their region. --- bittensor_cli/cli.py | 59 ++++++++++++++++++- bittensor_cli/src/__init__.py | 1 + .../src/bittensor/subtensor_interface.py | 34 +++++++++-- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 2044bae7..938fcbf0 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -42,7 +42,10 @@ from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import SubnetHyperparameters -from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface +from bittensor_cli.src.bittensor.subtensor_interface import ( + SubtensorInterface, + best_connection, +) from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -1048,6 +1051,9 @@ def __init__(self): "remove", rich_help_panel=HELP_PANELS["LIQUIDITY"]["LIQUIDITY_MGMT"] )(self.liquidity_remove) + # utils app + self.utils_app.command("latency")(self.best_connection) + def generate_command_tree(self) -> Tree: """ Generates a rich.Tree of the commands, subcommands, and groups of this app @@ -6326,6 +6332,57 @@ def convert( f"{Balance.from_tao(tao).rao}{Balance.rao_unit}", ) + def best_connection( + self, + additional_networks: Optional[list[str]] = typer.Option( + None, + "--network", + help="Network(s) to test for the best connection", + ), + ): + f""" + This command will give you the latency of all finney-like network in additional to any additional networks you specify via the {arg__("--network")} flag + + The results are two-fold. One column is the overall time to initialise a connection, send a request, and wait for the result. The second column measures single ping-pong speed once connected. + + EXAMPLE + + [green]$[/green] btcli utils latency --network ws://189.234.12.45 --network wss://mysubtensor.duckdns.org + + """ + additional_networks = additional_networks or [] + if any(not x.startswith("ws") for x in additional_networks): + err_console.print( + "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint.", + ) + return False + results: dict[str, list[float]] = self._run_command( + best_connection(Constants.finney_nodes + (additional_networks or [])) + ) + sorted_results = { + k: v for k, v in sorted(results.items(), key=lambda item: item[1][0]) + } + table = Table( + Column("Network"), + Column("End to End Latency", style="cyan"), + Column("Single Request Ping", style="cyan"), + title="Connection Latencies (seconds)", + caption="lower value is faster", + ) + for n_name, (overall_latency, single_request) in sorted_results.items(): + table.add_row(n_name, str(overall_latency), str(single_request)) + console.print(table) + fastest = next(iter(sorted_results.keys())) + if conf_net := self.config.get("network", ""): + if not conf_net.startswith("ws") and conf_net in Constants.networks: + conf_net = Constants.network_map[conf_net] + if conf_net != fastest: + console.print( + f"The fastest network is {fastest}. You currently have {conf_net} selected as your default network." + f"\nYou can update this with {arg__(f'btcli config set --network {fastest}')}" + ) + return True + def run(self): self.app() diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index afabc424..eb8f2acb 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -23,6 +23,7 @@ class Constants: dev_entrypoint = "wss://dev.chain.opentensor.ai:443" local_entrypoint = "ws://127.0.0.1:9944" latent_lite_entrypoint = "wss://lite.sub.latent.to:443" + finney_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index ffe8d78e..05a09dfb 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1,21 +1,22 @@ import asyncio import os +import time from typing import Optional, Any, Union, TypedDict, Iterable import aiohttp +from async_substrate_interface.async_substrate import ( + DiskCachedAsyncSubstrateInterface, + AsyncSubstrateInterface, +) +from async_substrate_interface.errors import SubstrateRequestException from async_substrate_interface.utils.storage import StorageKey from bittensor_wallet import Wallet from bittensor_wallet.bittensor_wallet import Keypair from bittensor_wallet.utils import SS58_FORMAT from scalecodec import GenericCall -from async_substrate_interface.errors import SubstrateRequestException import typer +import websockets - -from async_substrate_interface.async_substrate import ( - DiskCachedAsyncSubstrateInterface, - AsyncSubstrateInterface, -) from bittensor_cli.src.bittensor.chain_data import ( DelegateInfo, StakeInfo, @@ -1654,3 +1655,24 @@ async def get_subnet_prices( map_[netuid_] = Balance.from_rao(int(current_price * 1e9)) return map_ + + +async def best_connection(networks: list[str]): + """ + Basic function to compare the latency of a given list of websocket endpoints + Args: + networks: list of network URIs + + Returns: + {network_name: [end_to_end_latency, single_request_latency]} + + """ + results = {n: [0.0, 0.0] for n in networks} + for network in networks: + t1 = time.monotonic() + async with websockets.connect(network) as websocket: + pong = await websocket.ping() + latency = await pong + t2 = time.monotonic() + results[network] = [t2 - t1, latency] + return results From 96539e9f4b79402e5ce0cdf0bfef9e6f8fdc59c5 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 16:37:38 +0200 Subject: [PATCH 33/57] Also send a real request --- bittensor_cli/cli.py | 13 ++++++++++--- bittensor_cli/src/bittensor/subtensor_interface.py | 11 ++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 938fcbf0..e2140c1f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6343,7 +6343,7 @@ def best_connection( f""" This command will give you the latency of all finney-like network in additional to any additional networks you specify via the {arg__("--network")} flag - The results are two-fold. One column is the overall time to initialise a connection, send a request, and wait for the result. The second column measures single ping-pong speed once connected. + The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. EXAMPLE @@ -6366,11 +6366,18 @@ def best_connection( Column("Network"), Column("End to End Latency", style="cyan"), Column("Single Request Ping", style="cyan"), + Column("Chain Head Request Latency", style="cyan"), title="Connection Latencies (seconds)", caption="lower value is faster", ) - for n_name, (overall_latency, single_request) in sorted_results.items(): - table.add_row(n_name, str(overall_latency), str(single_request)) + for n_name, ( + overall_latency, + single_request, + chain_head, + ) in sorted_results.items(): + table.add_row( + n_name, str(overall_latency), str(single_request), str(chain_head) + ) console.print(table) fastest = next(iter(sorted_results.keys())) if conf_net := self.config.get("network", ""): diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 05a09dfb..f4b765e1 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1664,15 +1664,20 @@ async def best_connection(networks: list[str]): networks: list of network URIs Returns: - {network_name: [end_to_end_latency, single_request_latency]} + {network_name: [end_to_end_latency, single_request_latency, chain_head_request_latency]} """ - results = {n: [0.0, 0.0] for n in networks} + results = {} for network in networks: t1 = time.monotonic() async with websockets.connect(network) as websocket: pong = await websocket.ping() latency = await pong + pt1 = time.monotonic() + await websocket.send( + "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" + ) + await websocket.recv() t2 = time.monotonic() - results[network] = [t2 - t1, latency] + results[network] = [t2 - t1, latency, t2 - pt1] return results From 293e349d23cd584d4881149229578e8a9a96176b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 16:44:43 +0200 Subject: [PATCH 34/57] Fix help --- bittensor_cli/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e2140c1f..f0e83fbd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -622,7 +622,7 @@ class CLIManager: wallet_app: typer.Typer subnets_app: typer.Typer weights_app: typer.Typer - utils_app = typer.Typer(epilog=_epilog) + utils_app: typer.Typer view_app: typer.Typer asyncio_runner = asyncio @@ -695,6 +695,7 @@ def __init__(self): self.weights_app = typer.Typer(epilog=_epilog) self.view_app = typer.Typer(epilog=_epilog) self.liquidity_app = typer.Typer(epilog=_epilog) + self.utils_app = typer.Typer(epilog=_epilog) # config alias self.app.add_typer( @@ -1052,6 +1053,7 @@ def __init__(self): )(self.liquidity_remove) # utils app + self.utils_app.command("convert")(self.convert) self.utils_app.command("latency")(self.best_connection) def generate_command_tree(self) -> Tree: @@ -6302,7 +6304,6 @@ def liquidity_modify( ) @staticmethod - @utils_app.command("convert") def convert( from_rao: Optional[str] = typer.Option( None, "--rao", help="Convert amount from Rao" @@ -6340,8 +6341,8 @@ def best_connection( help="Network(s) to test for the best connection", ), ): - f""" - This command will give you the latency of all finney-like network in additional to any additional networks you specify via the {arg__("--network")} flag + """ + This command will give you the latency of all finney-like network in additional to any additional networks you specify via the '--network' flag The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. From 3398925eb332003e6b0f6f9158e808272cdfd516 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 17:01:49 +0200 Subject: [PATCH 35/57] No longer hide utils --- 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 f0e83fbd..6c3ab4b5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -770,7 +770,7 @@ def __init__(self): # utils app self.app.add_typer( - self.utils_app, name="utils", no_args_is_help=True, hidden=True + self.utils_app, name="utils", no_args_is_help=True, hidden=False ) # view app From ada539c741610de226ce1b6058d9739997b68daf Mon Sep 17 00:00:00 2001 From: bdhimes Date: Fri, 15 Aug 2025 17:01:56 +0200 Subject: [PATCH 36/57] Ruff --- bittensor_cli/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 6c3ab4b5..7ccff4cd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6343,11 +6343,11 @@ def best_connection( ): """ This command will give you the latency of all finney-like network in additional to any additional networks you specify via the '--network' flag - + The results are three-fold. One column is the overall time to initialise a connection, send the requests, and wait for the results. The second column measures single ping-pong speed once connected. The third makes a real world call to fetch the chain head. - + EXAMPLE - + [green]$[/green] btcli utils latency --network ws://189.234.12.45 --network wss://mysubtensor.duckdns.org """ From 418ec07df9183d974525b99fadaadcc280ab732a Mon Sep 17 00:00:00 2001 From: Hudson Graeme Date: Tue, 19 Aug 2025 20:41:44 +0000 Subject: [PATCH 37/57] Update childkey proportion CLI argument in docs --- 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 2044bae7..c60186d2 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4539,7 +4539,7 @@ def stake_set_children( EXAMPLE - [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 -p 0.3 -p 0.7 + [green]$[/green] btcli stake child set -c 5FCL3gmjtQV4xxxxuEPEFQVhyyyyqYgNwX7drFLw7MSdBnxP -c 5Hp5dxxxxtGg7pu8dN2btyyyyVA1vELmM9dy8KQv3LxV8PA7 --hotkey default --netuid 1 --prop 0.3 --prop 0.7 """ self.verbosity_handler(quiet, verbose, json_output) netuid = get_optional_netuid(netuid, all_netuids) From 65c3bafa80c3b338437d31e55920f80a90da4ee0 Mon Sep 17 00:00:00 2001 From: Hudson Graeme Date: Fri, 22 Aug 2025 21:02:02 +0000 Subject: [PATCH 38/57] Update example for show command --- 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 c60186d2..513207c7 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5247,7 +5247,7 @@ def subnets_show( EXAMPLE - [green]$[/green] btcli subnets list + [green]$[/green] btcli subnets show """ self.verbosity_handler(quiet, verbose, json_output) subtensor = self.initialize_chain(network) From 047fb48842c629b8d2420da1f32644e2bea7fc2e Mon Sep 17 00:00:00 2001 From: Caleb Gates Date: Thu, 28 Aug 2025 10:06:57 -0400 Subject: [PATCH 39/57] Update cli.py to remove double negative typo Ran into this error in the CLI stating the wallet did not not exist. Looked into if this is an intentional double negative. My research points to "no" it's a typo. Looking at the logic: 1. utils.is_valid_wallet(wallet) returns a tuple where the first element (valid[0]) indicates if the wallet exists 2. The condition if not valid[0]: triggers when the wallet does NOT exist 3. So the error message should indicate the wallet doesn't exist --- 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 6fddc03d..e5d37403 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1726,7 +1726,7 @@ def wallet_ask( valid = utils.is_valid_wallet(wallet) if not valid[0]: utils.err_console.print( - f"[red]Error: Wallet does not not exist. \n" + f"[red]Error: Wallet does not exist. \n" f"Please verify your wallet information: {wallet}[/red]" ) raise typer.Exit() From fcdc1c998a67454242fc0cf11abf59c5c151c113 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 1 Sep 2025 14:04:38 +0200 Subject: [PATCH 40/57] PR suggestions --- bittensor_cli/cli.py | 6 +++-- bittensor_cli/src/__init__.py | 2 +- .../src/bittensor/subtensor_interface.py | 25 +++++++++++-------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7ccff4cd..30b0c665 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -6354,11 +6354,13 @@ def best_connection( additional_networks = additional_networks or [] if any(not x.startswith("ws") for x in additional_networks): err_console.print( - "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint.", + "Invalid network endpoint. Ensure you are specifying a valid websocket endpoint" + f" (starting with [{COLORS.G.LINKS}]ws://[/{COLORS.G.LINKS}] or " + f"[{COLORS.G.LINKS}]wss://[/{COLORS.G.LINKS}]).", ) return False results: dict[str, list[float]] = self._run_command( - best_connection(Constants.finney_nodes + (additional_networks or [])) + best_connection(Constants.lite_nodes + additional_networks) ) sorted_results = { k: v for k, v in sorted(results.items(), key=lambda item: item[1][0]) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index eb8f2acb..67d3b0ec 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -23,7 +23,7 @@ class Constants: dev_entrypoint = "wss://dev.chain.opentensor.ai:443" local_entrypoint = "ws://127.0.0.1:9944" latent_lite_entrypoint = "wss://lite.sub.latent.to:443" - finney_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] + lite_nodes = [finney_entrypoint, subvortex_entrypoint, latent_lite_entrypoint] network_map = { "finney": finney_entrypoint, "test": finney_test_entrypoint, diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index f4b765e1..cb3b295f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1669,15 +1669,18 @@ async def best_connection(networks: list[str]): """ results = {} for network in networks: - t1 = time.monotonic() - async with websockets.connect(network) as websocket: - pong = await websocket.ping() - latency = await pong - pt1 = time.monotonic() - await websocket.send( - "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" - ) - await websocket.recv() - t2 = time.monotonic() - results[network] = [t2 - t1, latency, t2 - pt1] + try: + t1 = time.monotonic() + async with websockets.connect(network) as websocket: + pong = await websocket.ping() + latency = await pong + pt1 = time.monotonic() + await websocket.send( + "{'jsonrpc': '2.0', 'method': 'chain_getHead', 'params': [], 'id': '82'}" + ) + await websocket.recv() + t2 = time.monotonic() + results[network] = [t2 - t1, latency, t2 - pt1] + except Exception as e: + err_console.print(f"Error attempting network {network}: {e}") return results From 8391861d7d86835f9953658558ef92a6559f301b Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Sep 2025 01:18:19 -0700 Subject: [PATCH 41/57] fixes registration check on new hotkey --- bittensor_cli/src/bittensor/extrinsics/registration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py index b2461d89..2c237176 100644 --- a/bittensor_cli/src/bittensor/extrinsics/registration.py +++ b/bittensor_cli/src/bittensor/extrinsics/registration.py @@ -1757,11 +1757,13 @@ async def swap_hotkey_extrinsic( """ block_hash = await subtensor.substrate.get_chain_head() hk_ss58 = get_hotkey_pub_ss58(wallet) + new_hk_ss58 = get_hotkey_pub_ss58(new_wallet) + netuids_registered = await subtensor.get_netuids_for_hotkey( hk_ss58, block_hash=block_hash ) netuids_registered_new_hotkey = await subtensor.get_netuids_for_hotkey( - hk_ss58, block_hash=block_hash + new_hk_ss58, block_hash=block_hash ) if netuid is not None and netuid not in netuids_registered: @@ -1777,7 +1779,6 @@ async def swap_hotkey_extrinsic( ) return False - new_hk_ss58 = get_hotkey_pub_ss58(new_wallet) if netuid is not None: if netuid in netuids_registered_new_hotkey: err_console.print( From 3f5d558a3f6c937f85584184e3dbec310fbf07a5 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Fri, 5 Sep 2025 09:13:46 -0700 Subject: [PATCH 42/57] bumps version and changelog --- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aacd1c3..60783ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 9.11.0 /2025-09-05 + +## What's Changed +* Better arg naming + type annotations by @thewhaleking in https://github.com/opentensor/btcli/pull/590 +* disk cache in config by @thewhaleking in https://github.com/opentensor/btcli/pull/588 +* Unstake no prompts by @thewhaleking in https://github.com/opentensor/btcli/pull/591 +* expand readme by @thewhaleking in https://github.com/opentensor/btcli/pull/598 +* Better arg formatting for readability by @thewhaleking in https://github.com/opentensor/btcli/pull/592 +* Update childkey proportion CLI argument in docs by @HudsonGraeme in https://github.com/opentensor/btcli/pull/602 +* Update example for show command by @HudsonGraeme in https://github.com/opentensor/btcli/pull/604 +* New cmd: `btcli utils latency` by @thewhaleking in https://github.com/opentensor/btcli/pull/599 +* Fix: Swap hotkey - new hotkey reg check by @ibraheem-abe in https://github.com/opentensor/btcli/pull/608 + +**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.10.1...v9.11.0 + ## 9.10.1 /2025-08-12 * Removes double param for `--cache` in `config set` by @thewhaleking in https://github.com/opentensor/btcli/pull/579 * change root only sudo hyperparams by @thewhaleking in https://github.com/opentensor/btcli/pull/580 diff --git a/pyproject.toml b/pyproject.toml index 767047b3..66967592 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "bittensor-cli" -version = "9.10.1" +version = "9.11.0" description = "Bittensor CLI" readme = "README.md" authors = [ From 49f4af99a610e3cee65ca5cbf6a5d7f3be2c6c83 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 14:58:52 +0200 Subject: [PATCH 43/57] Better shows hotkeypubs in `w list` --- bittensor_cli/src/bittensor/utils.py | 20 ++++++++++++++++++-- bittensor_cli/src/commands/wallets.py | 13 +++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index 8611db3f..1497470a 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -269,10 +269,26 @@ def get_hotkey_wallets_for_wallet( except FileNotFoundError: hotkeys = [] for h_name in hotkeys: - hotkey_for_name = Wallet(path=str(wallet_path), name=wallet.name, hotkey=h_name) + if h_name.endswith("pub.txt"): + if h_name.split("pub.txt")[0] in hotkeys: + continue + else: + hotkey_for_name = Wallet( + path=str(wallet_path), + name=wallet.name, + hotkey=h_name.split("pub.txt")[0], + ) + else: + hotkey_for_name = Wallet( + path=str(wallet_path), name=wallet.name, hotkey=h_name + ) try: + exists = ( + hotkey_for_name.hotkey_file.exists_on_device() + or hotkey_for_name.hotkeypub_file.exists_on_device() + ) if ( - (exists := hotkey_for_name.hotkey_file.exists_on_device()) + exists and not hotkey_for_name.hotkey_file.is_encrypted() # and hotkey_for_name.coldkeypub.ss58_address and get_hotkey_pub_ss58(hotkey_for_name) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ff37a50e..ab002919 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -846,12 +846,21 @@ async def wallet_list(wallet_path: str, json_output: bool): data = f"[bold red]Hotkey[/bold red][green] {hkey}[/green] (?)" hk_data = {"name": hkey.name, "ss58_address": "?"} if hkey: - hkey_ss58 = get_hotkey_pub_ss58(hkey) + try: + hkey_ss58 = hkey.get_hotkey().ss58_address + pub_only = False + except KeyFileError: + hkey_ss58 = hkey.get_hotkeypub().ss58_address + pub_only = True try: data = ( f"[bold red]Hotkey[/bold red] [green]{hkey.hotkey_str}[/green] " - f"ss58_address [green]{hkey_ss58}[/green]\n" + f"ss58_address [green]{hkey_ss58}[/green]" ) + if pub_only: + data += " [blue](hotkeypub only)[/blue]\n" + else: + data += "\n" hk_data["name"] = hkey.hotkey_str hk_data["ss58_address"] = hkey_ss58 except UnicodeDecodeError: From da8215c7224ace888d399f689917fae97dd049a7 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 15:48:35 +0200 Subject: [PATCH 44/57] Adds cmd for setting subnet symbol. --- bittensor_cli/cli.py | 32 ++++++++++ bittensor_cli/src/commands/subnets/subnets.py | 63 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index d3d490d2..47205d66 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -960,6 +960,9 @@ def __init__(self): self.subnets_app.command( "check-start", rich_help_panel=HELP_PANELS["SUBNETS"]["INFO"] )(self.subnets_check_start) + self.subnets_app.command( + "set-symbol", rich_help_panel=HELP_PANELS["SUBNETS"]["IDENTITY"] + )(self.subnets_set_symbol) # weights commands self.weights_app.command( @@ -5793,6 +5796,35 @@ def subnets_metagraph( ) ) + def subnets_set_symbol( + self, + wallet_name: str = Options.wallet_name, + wallet_path: str = Options.wallet_path, + wallet_hotkey: str = Options.wallet_hotkey, + network: Optional[list[str]] = Options.network, + netuid: int = Options.netuid, + json_output: bool = Options.json_output, + prompt: bool = Options.prompt, + quiet: bool = Options.quiet, + verbose: bool = Options.verbose, + symbol: str = typer.Argument(help="The symbol to set for your subnet."), + ): + """ + Allows the user to update their subnet symbol. + + EXAMPLE + + [green]$[/green] btcli subnets set-symbol --netuid 1 ‡ + """ + self.verbosity_handler(quiet, verbose, json_output) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.HOTKEY], + validate=WV.WALLET_AND_HOTKEY, + ) + def weights_reveal( self, network: Optional[list[str]] = Options.network, diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index bb42132c..8daec6d2 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -2448,3 +2448,66 @@ async def start_subnet( await get_start_schedule(subtensor, netuid) print_error(f":cross_mark: Failed to start subnet: {error_msg}") return False + + +async def set_symbol( + wallet: "Wallet", + subtensor: "SubtensorInterface", + netuid: int, + symbol: str, + prompt: bool = False, + json_output: bool = False, +) -> bool: + """Set a subtensor's symbol""" + if not await subtensor.subnet_exists(netuid): + err = f"Subnet {netuid} does not exist." + if json_output: + json_console.print_json(data={"success": False, "message": err}) + else: + err_console.print(err) + return False + + if prompt and not json_output: + sn_info = await subtensor.subnet(netuid=netuid) + if not Confirm.ask( + f"Your current subnet symbol for SN{netuid} is {sn_info.symbol}. Do you want to update it to {symbol}?" + ): + return False + + if not (unlock_status := unlock_key(wallet, print_out=False)).success: + err = unlock_status.message + if json_output: + json_console.print_json(data={"success": False, "message": err}) + else: + console.print(err) + return False + + start_call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="update_symbol", + call_params={"netuid": netuid, "symbol": symbol}, + ) + + signed_ext = await subtensor.substrate.create_signed_extrinsic( + call=start_call, + keypair=wallet.coldkey, + ) + + response = await subtensor.substrate.submit_extrinsic( + extrinsic=signed_ext, + wait_for_inclusion=True, + ) + if await response.is_success: + message = f"Successfully updated SN{netuid}'s symbol to {symbol}." + if json_output: + json_console.print_json(data={"success": True, "message": message}) + else: + console.print(f":white_heavy_check_mark:[dark_sea_green3] {message}\n") + return True + else: + err = format_error_message(await response.error_message) + if json_output: + json_console.print_json(data={"success": False, "message": err}) + else: + err_console.print(f":cross_mark: [red]Failed[/red]: {err}") + return False From 1694d4c71d4e504ff2be38343261c48e3b220892 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 15:52:35 +0200 Subject: [PATCH 45/57] Add test --- tests/e2e_tests/test_staking_sudo.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index cd4bf09c..571f86ce 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -10,6 +10,7 @@ * btcli subnets create * btcli subnets set-identity * btcli subnets get-identity +* btcli subnets set-symbol * btcli subnets register * btcli subnets price * btcli stake add @@ -235,6 +236,33 @@ def test_staking(local_chain, wallet_setup): assert get_identity_output["logo_url"] == sn_logo_url assert get_identity_output["additional"] == sn_add_info + # set symbol + set_symbol = exec_command_alice( + "subnets", + "set-symbol", + 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, + "--json-output", + "--no-prompt", + "‡", + ], + ) + set_symbol_output = json.loads(set_symbol.stdout) + assert set_symbol_output["success"] is True + assert ( + set_symbol_output["message"] + == f"Successfully updated SN{netuid}'s symbol to ‡." + ) + get_s_price = exec_command_alice( "subnets", "price", From 438a6a578efd4af18f5baf1ab2b4e8b061c8a315 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 15:58:27 +0200 Subject: [PATCH 46/57] Forgot to include the actual run logic. --- bittensor_cli/cli.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 47205d66..b559c1f1 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5817,6 +5817,9 @@ def subnets_set_symbol( [green]$[/green] btcli subnets set-symbol --netuid 1 ‡ """ self.verbosity_handler(quiet, verbose, json_output) + if len(symbol) > 1: + err_console.print("Your symbol must be a single character.") + return False wallet = self.wallet_ask( wallet_name, wallet_path, @@ -5824,6 +5827,16 @@ def subnets_set_symbol( ask_for=[WO.NAME, WO.HOTKEY], validate=WV.WALLET_AND_HOTKEY, ) + return self._run_command( + subnets.set_symbol( + wallet=wallet, + subtensor=self.initialize_chain(network), + netuid=netuid, + symbol=symbol, + prompt=prompt, + json_output=json_output, + ) + ) def weights_reveal( self, From 1ee8160625dd3ed4df8a1c6b3f03afdd4c97a217 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 16:03:41 +0200 Subject: [PATCH 47/57] debug --- 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 571f86ce..bc3e9b6e 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -257,7 +257,7 @@ def test_staking(local_chain, wallet_setup): ], ) set_symbol_output = json.loads(set_symbol.stdout) - assert set_symbol_output["success"] is True + assert set_symbol_output["success"] is True, set_identity_output assert ( set_symbol_output["message"] == f"Successfully updated SN{netuid}'s symbol to ‡." From e13f1a00415d38cc463c5cf7812d6fae4680d39a Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 16:09:23 +0200 Subject: [PATCH 48/57] Debug --- bittensor_cli/src/commands/subnets/subnets.py | 2 +- tests/e2e_tests/test_staking_sudo.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 8daec6d2..14562a4e 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -2485,7 +2485,7 @@ async def set_symbol( start_call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="update_symbol", - call_params={"netuid": netuid, "symbol": symbol}, + call_params={"netuid": netuid, "symbol": symbol.encode("utf-8")}, ) signed_ext = await subtensor.substrate.create_signed_extrinsic( diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index bc3e9b6e..fbc981b2 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -257,7 +257,8 @@ def test_staking(local_chain, wallet_setup): ], ) set_symbol_output = json.loads(set_symbol.stdout) - assert set_symbol_output["success"] is True, set_identity_output + assert set_symbol_output["success"] is True + assert set_symbol_output["success"] is True, set_symbol_output assert ( set_symbol_output["message"] == f"Successfully updated SN{netuid}'s symbol to ‡." From 7e215b9a88fb76965e23a8a712e2aa225c924ab0 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 16:12:28 +0200 Subject: [PATCH 49/57] Debug --- 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 fbc981b2..79e7c96a 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -257,7 +257,7 @@ def test_staking(local_chain, wallet_setup): ], ) set_symbol_output = json.loads(set_symbol.stdout) - assert set_symbol_output["success"] is True + assert set_symbol_output["success"] is True, set_symbol_output assert set_symbol_output["success"] is True, set_symbol_output assert ( set_symbol_output["message"] From e64fd597bf21b2f7dcbfda50e9d92de4ce782cdf Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 16:18:05 +0200 Subject: [PATCH 50/57] Symbol didn't exist. --- bittensor_cli/cli.py | 2 +- tests/e2e_tests/test_staking_sudo.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b559c1f1..242be57f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5814,7 +5814,7 @@ def subnets_set_symbol( EXAMPLE - [green]$[/green] btcli subnets set-symbol --netuid 1 ‡ + [green]$[/green] btcli subnets set-symbol --netuid 1 シ """ self.verbosity_handler(quiet, verbose, json_output) if len(symbol) > 1: diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 79e7c96a..359fbb50 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -253,7 +253,7 @@ def test_staking(local_chain, wallet_setup): netuid, "--json-output", "--no-prompt", - "‡", + "シ", ], ) set_symbol_output = json.loads(set_symbol.stdout) @@ -261,7 +261,7 @@ def test_staking(local_chain, wallet_setup): assert set_symbol_output["success"] is True, set_symbol_output assert ( set_symbol_output["message"] - == f"Successfully updated SN{netuid}'s symbol to ‡." + == f"Successfully updated SN{netuid}'s symbol to シ." ) get_s_price = exec_command_alice( From 0c5b70af3d8ae9a1e37e684913a0868c8b66c974 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 18:41:56 +0200 Subject: [PATCH 51/57] Update docstrings --- bittensor_cli/cli.py | 11 +++++++++-- bittensor_cli/src/commands/subnets/subnets.py | 7 ++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 242be57f..39b40158 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5810,11 +5810,18 @@ def subnets_set_symbol( symbol: str = typer.Argument(help="The symbol to set for your subnet."), ): """ - Allows the user to update their subnet symbol. + Allows the user to update their subnet symbol to a different available symbol. The full list of available symbols can be found here: + [#8CB9E9]https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/symbols.rs#L8[/#8CB9E9] + EXAMPLE - [green]$[/green] btcli subnets set-symbol --netuid 1 シ + [green]$[/green] btcli subnets set-symbol [dark_orange]--netuid 1 シ[/dark_orange] + + + JSON OUTPUT: + If --json-output is used, the output will be in the following schema: + [#AFEFFF]{success: [dark_orange]bool[/dark_orange], message: [dark_orange]str[/dark_orange]}[/#AFEFFF] """ self.verbosity_handler(quiet, verbose, json_output) if len(symbol) > 1: diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 14562a4e..d8571f3f 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -2458,7 +2458,12 @@ async def set_symbol( prompt: bool = False, json_output: bool = False, ) -> bool: - """Set a subtensor's symbol""" + """ + Set a subtensor's symbol, given the netuid and symbol. + + The symbol must be a symbol that subtensor recognizes as available + (defined in https://github.com/opentensor/subtensor/blob/main/pallets/subtensor/src/subnets/symbols.rs#L8) + """ if not await subtensor.subnet_exists(netuid): err = f"Subnet {netuid} does not exist." if json_output: From 0a6692aecac78df4ea8c5c3bce98107eed4e6ded Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 20:54:48 +0200 Subject: [PATCH 52/57] Update changelog --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60783ca3..aa278f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,6 @@ # Changelog ## 9.11.0 /2025-09-05 - -## What's Changed * Better arg naming + type annotations by @thewhaleking in https://github.com/opentensor/btcli/pull/590 * disk cache in config by @thewhaleking in https://github.com/opentensor/btcli/pull/588 * Unstake no prompts by @thewhaleking in https://github.com/opentensor/btcli/pull/591 @@ -12,6 +10,13 @@ * Update example for show command by @HudsonGraeme in https://github.com/opentensor/btcli/pull/604 * New cmd: `btcli utils latency` by @thewhaleking in https://github.com/opentensor/btcli/pull/599 * Fix: Swap hotkey - new hotkey reg check by @ibraheem-abe in https://github.com/opentensor/btcli/pull/608 +* Update cli.py to remove double negative typo by @calebcgates in https://github.com/opentensor/btcli/pull/606 +* Better shows hotkeypubs in `w list` by @thewhaleking in https://github.com/opentensor/btcli/pull/611 +* subnet symbol set command by @thewhaleking in https://github.com/opentensor/btcli/pull/613 +* Debug log by @thewhaleking in https://github.com/opentensor/btcli/pull/597 + +### New Contributors +* @calebcgates made their first contribution in https://github.com/opentensor/btcli/pull/606 **Full Changelog**: https://github.com/opentensor/btcli/compare/v9.10.1...v9.11.0 From 286980880638b241a00916c98f16ba98b9ef7c0b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 21:05:29 +0200 Subject: [PATCH 53/57] Adds parent path creation for debug log. --- bittensor_cli/cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0f35f125..7ce8bbc8 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -612,7 +612,12 @@ def debug_callback(value: bool): "Enter the file location to save the debug log for the previous command.", default="~/.bittensor/debug-export", ).strip() - save_file_loc = os.path.expanduser(save_file_loc_) + save_file_loc = Path(os.path.expanduser(save_file_loc_)) + if not save_file_loc.parent.exists(): + if Confirm.ask( + f"The directory '{save_file_loc.parent}' does not exist. Would you like to create it?" + ): + save_file_loc.parent.mkdir(parents=True, exist_ok=True) try: with ( open(save_file_loc, "w+") as save_file, From b4560e1cff999a60384d9cd6bd220fa8cd74793f Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 21:17:14 +0200 Subject: [PATCH 54/57] Error handling --- bittensor_cli/cli.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 7ce8bbc8..1f89a77c 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -608,6 +608,18 @@ def commands_callback(value: bool): def debug_callback(value: bool): if value: + debug_file_loc = Path( + os.getenv("BTCLI_DEBUG_FILE") + or os.path.expanduser(defaults.config.debug_file_path) + ) + if not debug_file_loc.exists(): + print_error( + f"The debug file '{debug_file_loc}' does not exist. This indicates that you have not run a command " + f"which has logged debug output, or you deleted this file. If the debug file was created using the " + f"BTCLI_DEBUG_FILE environment variable, please set the value for the same location, and re-run " + f"this `btcli --debug` command." + ) + raise typer.Exit() save_file_loc_ = Prompt.ask( "Enter the file location to save the debug log for the previous command.", default="~/.bittensor/debug-export", @@ -621,16 +633,12 @@ def debug_callback(value: bool): try: with ( open(save_file_loc, "w+") as save_file, - open( - os.getenv("BTCLI_DEBUG_FILE") - or os.path.expanduser(defaults.config.debug_file_path), - "r", - ) as current_file, + open(debug_file_loc, "r") as current_file, ): save_file.write(current_file.read()) console.print(f"Saved debug log to {save_file_loc}") - except FileNotFoundError: - print_error(f"The filepath '{save_file_loc}' does not exist.") + except FileNotFoundError as e: + print_error(str(e)) raise typer.Exit() From 6ec1ad3ff966c8cea770aa3ce680611d316b3e16 Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 21:31:59 +0200 Subject: [PATCH 55/57] Update error message --- bittensor_cli/cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1f89a77c..4275cd15 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -613,11 +613,12 @@ def debug_callback(value: bool): or os.path.expanduser(defaults.config.debug_file_path) ) if not debug_file_loc.exists(): - print_error( - f"The debug file '{debug_file_loc}' does not exist. This indicates that you have not run a command " - f"which has logged debug output, or you deleted this file. If the debug file was created using the " - f"BTCLI_DEBUG_FILE environment variable, please set the value for the same location, and re-run " - f"this `btcli --debug` command." + err_console.print( + f"[red]Error: The debug file '{arg__(str(debug_file_loc))}' does not exist. This indicates that you have" + f" not run a command which has logged debug output, or you deleted this file. Debug logging only occurs" + f" if {arg__('use_cache')} is set to True in your config ({arg__('btcli config set')}). If the debug " + f"file was created using the {arg__('BTCLI_DEBUG_FILE')} environment variable, please set the value for" + f" the same location, and re-run this {arg__('btcli --debug')} command.[/red]" ) raise typer.Exit() save_file_loc_ = Prompt.ask( From 52f58fb08190feb73bb3aba074fc39d0b36d335b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 21:48:02 +0200 Subject: [PATCH 56/57] Ensure that bool config items are bool. --- bittensor_cli/cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4275cd15..88ae4058 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1257,7 +1257,7 @@ def main_callback( ): """ Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be - overriden by passing them explicitly in the command line. + overridden by passing them explicitly in the command line. """ # Load or create the config file if os.path.exists(self.config_path): @@ -1281,6 +1281,9 @@ def main_callback( if sub_key not in config[key]: config[key][sub_key] = sub_value updated = True + elif isinstance(value, bool) and config[key] is None: + config[key] = value + updated = True if updated: with open(self.config_path, "w") as f: safe_dump(config, f) From a6d2f66f0b300fd53ddac534a5d3d00cad28491b Mon Sep 17 00:00:00 2001 From: bdhimes Date: Mon, 8 Sep 2025 21:52:07 +0200 Subject: [PATCH 57/57] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa278f53..827a54af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * Better shows hotkeypubs in `w list` by @thewhaleking in https://github.com/opentensor/btcli/pull/611 * subnet symbol set command by @thewhaleking in https://github.com/opentensor/btcli/pull/613 * Debug log by @thewhaleking in https://github.com/opentensor/btcli/pull/597 +* Debug log additional by @thewhaleking in https://github.com/opentensor/btcli/pull/615 ### New Contributors * @calebcgates made their first contribution in https://github.com/opentensor/btcli/pull/606