diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 4520a17cd..2549dd032 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -1079,11 +1079,11 @@ def initialize_chain( 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" ) + self.subtensor = SubtensorInterface(self.config["network"]) else: self.subtensor = SubtensorInterface(defaults.subtensor.network) return self.subtensor @@ -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, @@ -5048,9 +5053,31 @@ 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) + + subtensor = self.initialize_chain(network) + non_archives = ["finney", "latent-lite", "subvortex"] + if not current_only and subtensor.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 + if netuids: netuids = parse_to_list( netuids, @@ -5080,10 +5107,11 @@ def subnets_price( return self._run_command( price.price( - self.initialize_chain(network), + subtensor, 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..38a20d00d 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,96 @@ 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))) + 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): + 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.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), + "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 +679,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']:.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']:.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) 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(