From 77d7a41742b065355b2563778094d053be08c0c4 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:21:20 +0200 Subject: [PATCH 1/7] Added flag for current only --- bittensor_cli/cli.py | 17 +++ bittensor_cli/src/commands/subnets/price.py | 151 ++++++++++++++++---- 2 files changed, 139 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4520a17cd..78e79690f 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5026,6 +5026,11 @@ def subnets_price( "--log", help="Show the price in log scale.", ), + current_only: bool = typer.Option( + False, + "--current", + help="Show only the current data, and no historical data.", + ), html_output: bool = Options.html_output, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -5050,6 +5055,17 @@ def subnets_price( if json_output and html_output: print_error("Cannot specify both `--json-output` and `--html`") return + non_archives = ["finney", "latent-lite", "subvortex"] + if not current_only and self._determine_network(network) in non_archives + [ + 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." + ) + return False + self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) if netuids: netuids = parse_to_list( @@ -5084,6 +5100,7 @@ def subnets_price( netuids, all_netuids, interval_hours, + current_only, html_output, log_scale, json_output, diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index e5dae1b6f..6189a8e5c 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -10,6 +10,7 @@ import plotly.graph_objects as go from bittensor_cli.src import COLOR_PALETTE +from bittensor_cli.src.bittensor.chain_data import DynamicInfo from bittensor_cli.src.bittensor.utils import ( console, err_console, @@ -28,6 +29,7 @@ async def price( netuids: list[int], all_netuids: bool = False, interval_hours: int = 4, + current_only: bool = False, html_output: bool = False, log_scale: bool = False, json_output: bool = False, @@ -41,45 +43,93 @@ async def price( blocks_per_hour = int(3600 / 12) # ~300 blocks per hour total_blocks = blocks_per_hour * interval_hours - with console.status(":chart_increasing: Fetching historical price data..."): - current_block_hash = await subtensor.substrate.get_chain_head() - current_block = await subtensor.substrate.get_block_number(current_block_hash) + if not current_only: + with console.status(":chart_increasing: Fetching historical price data..."): + current_block_hash = await subtensor.substrate.get_chain_head() + current_block = await subtensor.substrate.get_block_number( + current_block_hash + ) - step = 300 - start_block = max(0, current_block - total_blocks) - block_numbers = list(range(start_block, current_block + 1, step)) + step = 300 + start_block = max(0, current_block - total_blocks) + block_numbers = list(range(start_block, current_block + 1, step)) - # Block hashes - block_hash_cors = [ - subtensor.substrate.get_block_hash(bn) for bn in block_numbers - ] - block_hashes = await asyncio.gather(*block_hash_cors) + # Block hashes + block_hash_cors = [ + subtensor.substrate.get_block_hash(bn) for bn in block_numbers + ] + block_hashes = await asyncio.gather(*block_hash_cors) - # We fetch all subnets when there is more than one netuid - if all_netuids or len(netuids) > 1: - subnet_info_cors = [subtensor.all_subnets(bh) for bh in block_hashes] - else: - # If there is only one netuid, we fetch the subnet info for that netuid - netuid = netuids[0] - subnet_info_cors = [subtensor.subnet(netuid, bh) for bh in block_hashes] - all_subnet_infos = await asyncio.gather(*subnet_info_cors) + # We fetch all subnets when there is more than one netuid + if all_netuids or len(netuids) > 1: + subnet_info_cors = [subtensor.all_subnets(bh) for bh in block_hashes] + else: + # If there is only one netuid, we fetch the subnet info for that netuid + netuid = netuids[0] + subnet_info_cors = [subtensor.subnet(netuid, bh) for bh in block_hashes] + all_subnet_infos = await asyncio.gather(*subnet_info_cors) subnet_data = _process_subnet_data( block_numbers, all_subnet_infos, netuids, all_netuids ) + if not subnet_data: + err_console.print("[red]No valid price data found for any subnet[/red]") + return - if not subnet_data: - err_console.print("[red]No valid price data found for any subnet[/red]") - return - - if html_output: - await _generate_html_output( - subnet_data, block_numbers, interval_hours, log_scale + if html_output: + await _generate_html_output( + subnet_data, block_numbers, interval_hours, log_scale + ) + elif json_output: + json_console.print(json.dumps(_generate_json_output(subnet_data))) + else: + _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) + else: + with console.status("Fetching current price data..."): + if all_netuids or len(netuids) > 1: + all_subnet_info = await subtensor.all_subnets() + else: + all_subnet_info = [await subtensor.subnet(netuid=netuids[0])] + subnet_data = _process_current_subnet_data( + all_subnet_info, netuids, all_netuids ) - elif json_output: - json_console.print(json.dumps(_generate_json_output(subnet_data))) + _generate_cli_output_current(subnet_data) + + +def _process_current_subnet_data(subnet_infos: list[DynamicInfo], netuids, all_netuids): + subnet_data = {} + if all_netuids or len(netuids) > 1: + # Most recent data for statistics + for subnet_info in subnet_infos: + stats = { + "current_price": subnet_info.price, + "supply": subnet_info.alpha_in.tao + subnet_info.alpha_out.tao, + "market_cap": subnet_info.price.tao + * (subnet_info.alpha_in.tao + subnet_info.alpha_out.tao), + "emission": subnet_info.emission.tao, + "stake": subnet_info.alpha_out.tao, + "symbol": subnet_info.symbol, + "name": get_subnet_name(subnet_info), + } + subnet_data[subnet_info.netuid] = { + "stats": stats, + } else: - _generate_cli_output(subnet_data, block_numbers, interval_hours, log_scale) + subnet_info = subnet_infos[0] + stats = { + "current_price": subnet_info.price, + "supply": subnet_info.alpha_in.tao + subnet_info.alpha_out.tao, + "market_cap": subnet_info.price.tao + * (subnet_info.alpha_in.tao + subnet_info.alpha_out.tao), + "emission": subnet_info.emission.tao, + "stake": subnet_info.alpha_out.tao, + "symbol": subnet_info.symbol, + "name": get_subnet_name(subnet_info), + } + subnet_data[subnet_info.netuid] = { + "stats": stats, + } + return subnet_data def _process_subnet_data(block_numbers, all_subnet_infos, netuids, all_netuids): @@ -626,3 +676,46 @@ def color_label(text): ) console.print(stats_text) + + +def _generate_cli_output_current(subnet_data): + for netuid, data in subnet_data.items(): + stats = data["stats"] + + if netuid != 0: + console.print( + f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} " + f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n" + f"Current: [blue]{stats['current_price'].tao:.6f}{stats['symbol']}[/blue]\n" + ) + else: + console.print( + f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} " + f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n" + f"Current: [blue]{stats['symbol']} {stats['current_price'].tao:.6f}[/blue]\n" + ) + + if netuid != 0: + stats_text = ( + "\nLatest stats:\n" + f"Supply: [{COLOR_PALETTE.P.ALPHA_IN}]" + f"{stats['supply']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.P.ALPHA_IN}]\n" + f"Market Cap: [steel_blue3]{stats['market_cap']:,.2f} {stats['symbol']} / 21M[/steel_blue3]\n" + f"Emission: [{COLOR_PALETTE.P.EMISSION}]" + f"{stats['emission']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.P.EMISSION}]\n" + f"Stake: [{COLOR_PALETTE.S.TAO}]" + f"{stats['stake']:,.2f} {stats['symbol']}[/{COLOR_PALETTE.S.TAO}]" + ) + else: + stats_text = ( + "\nLatest stats:\n" + f"Supply: [{COLOR_PALETTE.P.ALPHA_IN}]" + f"{stats['symbol']} {stats['supply']:,.2f}[/{COLOR_PALETTE.P.ALPHA_IN}]\n" + f"Market Cap: [steel_blue3]{stats['symbol']} {stats['market_cap']:,.2f} / 21M[/steel_blue3]\n" + f"Emission: [{COLOR_PALETTE.P.EMISSION}]" + f"{stats['symbol']} {stats['emission']:,.2f}[/{COLOR_PALETTE.P.EMISSION}]\n" + f"Stake: [{COLOR_PALETTE.S.TAO}]" + f"{stats['symbol']} {stats['stake']:,.2f}[/{COLOR_PALETTE.S.TAO}]" + ) + + console.print(stats_text) From a2ac201003bd08fa524f8c3e2d58ca58d846e60c Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:22:06 +0200 Subject: [PATCH 2/7] Changed `_determine_network` to its own method to use it without instantiating a SubtensorInterface obj --- bittensor_cli/cli.py | 51 +++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 78e79690f..1a898a169 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1061,32 +1061,35 @@ def initialize_chain( "Verify this is intended.", ) if not self.subtensor: - if network: - network_ = None - for item in network: - if item.startswith("ws"): - network_ = item - break - else: - network_ = item - - not_selected_networks = [net for net in network if net != network_] - if not_selected_networks: - console.print( - f"Networks not selected: " - f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" - ) + self.subtensor = SubtensorInterface(self._determine_network(network)) + return self.subtensor - self.subtensor = SubtensorInterface(network_) - elif self.config["network"]: - self.subtensor = SubtensorInterface(self.config["network"]) - console.print( - f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" - f"[/{COLORS.G.LINKS}] from config" - ) + def _determine_network(self, network: Optional[list[str]] = None): + if network: + network_ = None + for item in network: + if item.startswith("ws"): + network_ = item + break else: - self.subtensor = SubtensorInterface(defaults.subtensor.network) - return self.subtensor + network_ = item + + not_selected_networks = [net for net in network if net != network_] + if not_selected_networks: + console.print( + f"Networks not selected: " + f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" + ) + + return network_ + elif self.config["network"]: + console.print( + f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" + f"[/{COLORS.G.LINKS}] from config" + ) + return self.config["network"] + else: + return defaults.subtensor.network def _run_command(self, cmd: Coroutine, exit_early: bool = True): """ From dbca3877af6a344f273a5a98e55b56d5e5061c06 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:26:29 +0200 Subject: [PATCH 3/7] Reverted _determine_network change. --- bittensor_cli/cli.py | 56 +++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 1a898a169..34b282331 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1061,35 +1061,32 @@ def initialize_chain( "Verify this is intended.", ) if not self.subtensor: - self.subtensor = SubtensorInterface(self._determine_network(network)) - return self.subtensor - - def _determine_network(self, network: Optional[list[str]] = None): - if network: - network_ = None - for item in network: - if item.startswith("ws"): - network_ = item - break - else: - network_ = item + if network: + network_ = None + for item in network: + if item.startswith("ws"): + network_ = item + break + else: + network_ = item - not_selected_networks = [net for net in network if net != network_] - if not_selected_networks: - console.print( - f"Networks not selected: " - f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" - ) + not_selected_networks = [net for net in network if net != network_] + if not_selected_networks: + console.print( + f"Networks not selected: " + f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" + ) - return network_ - elif self.config["network"]: - console.print( - f"Using the specified network [{COLORS.G.LINKS}]{self.config['network']}" - f"[/{COLORS.G.LINKS}] from config" - ) - return self.config["network"] - else: - return defaults.subtensor.network + self.subtensor = network_ + 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 = self.config["network"] + else: + self.subtensor = defaults.subtensor.network + return self.subtensor def _run_command(self, cmd: Coroutine, exit_early: bool = True): """ @@ -5058,8 +5055,9 @@ def subnets_price( if json_output and html_output: print_error("Cannot specify both `--json-output` and `--html`") return + subtensor = self.initialize_chain(network) non_archives = ["finney", "latent-lite", "subvortex"] - if not current_only and self._determine_network(network) in non_archives + [ + if not current_only and subtensor.network in non_archives + [ Constants.network_map[x] for x in non_archives ]: err_console.print( @@ -5099,7 +5097,7 @@ def subnets_price( return self._run_command( price.price( - self.initialize_chain(network), + subtensor, netuids, all_netuids, interval_hours, From 92f54f3a7ef95fddd9baf8213c9eb0d508e3f1c2 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:30:53 +0200 Subject: [PATCH 4/7] Reverted _determine_network change. --- 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 34b282331..b13c3e93d 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1077,15 +1077,15 @@ def initialize_chain( f"[{COLORS.G.ARG}]{', '.join(not_selected_networks)}[/{COLORS.G.ARG}]" ) - self.subtensor = network_ + self.subtensor = SubtensorInterface(network_) 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 = self.config["network"] + self.subtensor = SubtensorInterface(self.config["network"]) else: - self.subtensor = defaults.subtensor.network + self.subtensor = SubtensorInterface(defaults.subtensor.network) return self.subtensor def _run_command(self, cmd: Coroutine, exit_early: bool = True): From 3b7025d186ae817cf11519a00c0823e528b8bebb Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:39:29 +0200 Subject: [PATCH 5/7] Added JSON output support --- bittensor_cli/cli.py | 3 ++- bittensor_cli/src/commands/subnets/price.py | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b13c3e93d..0abe3d40b 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5055,6 +5055,8 @@ def subnets_price( if json_output and html_output: print_error("Cannot specify both `--json-output` and `--html`") return + self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) + subtensor = self.initialize_chain(network) non_archives = ["finney", "latent-lite", "subvortex"] if not current_only and subtensor.network in non_archives + [ @@ -5067,7 +5069,6 @@ def subnets_price( ) return False - self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) if netuids: netuids = parse_to_list( netuids, diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py index 6189a8e5c..38a20d00d 100644 --- a/bittensor_cli/src/commands/subnets/price.py +++ b/bittensor_cli/src/commands/subnets/price.py @@ -93,7 +93,10 @@ async def price( subnet_data = _process_current_subnet_data( all_subnet_info, netuids, all_netuids ) - _generate_cli_output_current(subnet_data) + if json_output: + json_console.print(json.dumps(_generate_json_output(subnet_data))) + else: + _generate_cli_output_current(subnet_data) def _process_current_subnet_data(subnet_infos: list[DynamicInfo], netuids, all_netuids): @@ -117,7 +120,7 @@ def _process_current_subnet_data(subnet_infos: list[DynamicInfo], netuids, all_n else: subnet_info = subnet_infos[0] stats = { - "current_price": subnet_info.price, + "current_price": subnet_info.price.tao, "supply": subnet_info.alpha_in.tao + subnet_info.alpha_out.tao, "market_cap": subnet_info.price.tao * (subnet_info.alpha_in.tao + subnet_info.alpha_out.tao), @@ -686,13 +689,13 @@ def _generate_cli_output_current(subnet_data): console.print( f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} " f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n" - f"Current: [blue]{stats['current_price'].tao:.6f}{stats['symbol']}[/blue]\n" + f"Current: [blue]{stats['current_price']:.6f}{stats['symbol']}[/blue]\n" ) else: console.print( f"\n[{COLOR_PALETTE.G.SYM}]Subnet {netuid} - {stats['symbol']} " f"[cyan]{stats['name']}[/cyan][/{COLOR_PALETTE.G.SYM}]\n" - f"Current: [blue]{stats['symbol']} {stats['current_price'].tao:.6f}[/blue]\n" + f"Current: [blue]{stats['symbol']} {stats['current_price']:.6f}[/blue]\n" ) if netuid != 0: From d3d3950a3b67f23908c8bf5b25dc1f7c57ac16c7 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 22:47:12 +0200 Subject: [PATCH 6/7] Handle html --- bittensor_cli/cli.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 0abe3d40b..2549dd032 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -5053,7 +5053,16 @@ def subnets_price( [green]$[/green] btcli subnets price --netuids 1,2,3,4 --html """ if json_output and html_output: - print_error("Cannot specify both `--json-output` and `--html`") + print_error( + f"Cannot specify both [{COLORS.G.ARG}]--json-output[/{COLORS.G.ARG}] " + f"and [{COLORS.G.ARG}]--html[/{COLORS.G.ARG}]" + ) + 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}]" + ) return self.verbosity_handler(quiet=quiet, verbose=verbose, json_output=json_output) From 6ac423214d5a3326fd5c99b218e04c83f6207fa8 Mon Sep 17 00:00:00 2001 From: Benjamin Himes Date: Tue, 29 Jul 2025 23:00:15 +0200 Subject: [PATCH 7/7] E2E test --- tests/e2e_tests/test_staking_sudo.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py index 8cb5caca4..cd4bf09c3 100644 --- a/tests/e2e_tests/test_staking_sudo.py +++ b/tests/e2e_tests/test_staking_sudo.py @@ -11,6 +11,7 @@ * btcli subnets set-identity * btcli subnets get-identity * btcli subnets register +* btcli subnets price * btcli stake add * btcli stake remove * btcli stake show @@ -234,6 +235,25 @@ def test_staking(local_chain, wallet_setup): assert get_identity_output["logo_url"] == sn_logo_url assert get_identity_output["additional"] == sn_add_info + get_s_price = exec_command_alice( + "subnets", + "price", + extra_args=[ + "--chain", + "ws://127.0.0.1:9945", + "--netuid", + netuid, + "--current", + "--json-output", + ], + ) + get_s_price_output = json.loads(get_s_price.stdout) + assert str(netuid) in get_s_price_output.keys() + stats = get_s_price_output[str(netuid)]["stats"] + assert stats["name"] == sn_name + assert stats["current_price"] == 0.0 + assert stats["market_cap"] == 0.0 + # Start emissions on SNs for netuid_ in multiple_netuids: start_subnet_emissions = exec_command_alice(